当前位置:   article > 正文

Springboot——事物管理_springboot事务管理

springboot事务管理

事务管理

一、 Spring事务管理

1.1 事务回顾

事务: 是一组操作的集合,是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败

事务的操作

  • 开启事务(一组操作开始前,开启事务):start transaction / begin
  • 提交事务:(这组操作全部成功后,提交事务),commit
  • 回滚事务:(中间任何一个操作出现异常,回滚事务),rollback

1.2 案例: 解散部门(未开启事务)

需求:解散部门(删除部门),同时删除该部门下的员工

我们之前业务逻辑仅仅删除了部门,并没有删除该部门下的员工,此时造成数据的不完整、不一致。

@Delete("delete from dept where id= #{id}")
void deleteById(Integer id);
  • 1
  • 2

下面进行完善

SQL

DeptMapper

@Delete("delete from dept where id= #{id}")
void deleteById(Integer id);
  • 1
  • 2

EmpMapper

/**
 * 根据部门ID删除该部门下的员工数据
 * @param id 部门id
 */
@Delete("delete from emp where dept_id  =#{id}")
void deleteByDeptId(Integer id);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

业务代码

@Override
public void deleteById(Integer id) {
    //根据id删除部门数据
    deptMapper.deleteById(id);

    //根据部门id删除员工数据
    empMapper.deleteByDeptId(id);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

假如在业务代码执行过程中出现异常了,会发生什么情况?

​ 可能会发生部门删除了但是部门里面员工没有被删除。此时造成数据的不一致。

​ 为了保证数据一致性,我们要保证删除部门和删除员工同时成功或者同时失败,也就是说这两步操作都在同一个事务当中

1.3 事务管理注解@Transactional

位置: 业务(Service)层方法上、类上、接口上

作用: 将当前方法交给Spring进行事务管理,方法执行前开启事务;成功执行完毕,提交事务;出现异常,回滚事务

我们一般添加在业务层执行多次数据访问操作的方法上

@Transactional
@Override
public void deleteById(Integer id) {
    //根据id删除部门数据
    deptMapper.deleteById(id);

    //根据部门id删除员工数据
    empMapper.deleteByDeptId(id);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

1.4 事务管理日志开关

logging:
  level:
    org.springframework.jdbc.support JdbcTransactionManager: debug
  • 1
  • 2
  • 3

1.5 rollbackFor 异常回滚属性

  • 默认情况下,只有出现RuntimeException才会回滚异常。

比如说我们手动throw了一个Exception,并不会出现回滚的情况,而是直接将事务提交了

@Transactional
@Override
public void deleteById(Integer id) throws Exception {
    //根据id删除部门数据
    deptMapper.deleteById(id);
    if (true) {
        throw new Exception("出错了");
    }
    //根据部门id删除员工数据
    empMapper.deleteByDeptId(id);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • rollbackFor 属性用于控制出现哪一种异常类型的时候,进行回滚事务

    这样配置后,所有的异常都会进行事务的回滚

@Transactional(rollbackFor = Exception.class)
@Override
public void deleteById(Integer id) throws Exception {
    //根据id删除部门数据
    deptMapper.deleteById(id);
    if (true) {
        throw new Exception("出错了");
    }
    //根据部门id删除员工数据
    empMapper.deleteByDeptId(id);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

1.6 propagation 事务传播行为

事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

属性值含义
REQUIRED[默认值] 需要事务,有则加入(b事务加入到a事务),无则创建新事务
**QEQUIRES_NEW **需要创建新事务,无论有无,总是创建新事务(b创立一个新事务),如果创建新事务,当前事务进行挂起,等新事务完成后再进行当前事务
SUPPORTS支持事务,有则加入,无则在无事务状态
NOT_SUPPORTED不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务(a事务先挂起先执行b事务,b事务完成后再执行a事务)
MANDATORY必须有事务,否则抛异常
NEVER必须没事务,否则抛异常

image-20230517142250530

1.7 解散部门并记录操作日志

需求:解散部门时,无论成功还是失败,都要记录操作日志

步骤

① 解散部门: 删除部门、删除部门下的员工

② 记录日志到数据库表中

1.7.1 创建数据库表

create table dept_log(
   	id int auto_increment comment '主键ID' primary key,
    create_time datetime null comment '操作时间',
    description varchar(300) null comment '操作描述'
)comment '部门操作日志表';
  • 1
  • 2
  • 3
  • 4
  • 5

1.7.2 代码实现

日志信息实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
    private Integer id;
    private LocalDateTime createTime;
    private String description;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

日志插入SQL

@Mapper
public interface DeptLogMapper {

    @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
    void insert(DeptLog log);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

日志插入业务代码

@Service
public class DeptLogServiceImpl implements DeptLogService {

    @Autowired
    private DeptLogMapper deptLogMapper;

    @Transactional //事务传播行为:有事务就加入、没有事务就新建事务
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

删除部门业务代码

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deleteById(Integer id) throws Exception {
        //根据id删除部门数据
        deptMapper.deleteById(id);

        //根据部门id删除员工数据
        empMapper.deleteByDeptId(id);

//      TODO 记录操作日志
        DeptLog deptLog = new DeptLog();
        deptLog.setCreateTime(LocalDateTime.now());
        deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
        //调用其他业务类中的方法
        deptLogService.insert(deptLog);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

此时方法调用有两个 @Transactional注解

  • 一个在deleteById方法,删除部门与对应用户
  • 一个在insert方法,并且这个方法在deleteById方法中被调用

此时涉及事务传播行为

进行测试,发现数据库中并不存在日志信息,是什么原因?

两个方法都有 @Transactional注解,采用的是默认事务传播行为,需要事务,有则加入(b事务加入到a事务),无则创建新事务。

很显然insert事务会加入到deleteById事务,但是在deleteById业务执行时发生异常,进行回滚,那同属于一个事务的insert也会回滚,导致数据库中没有记录。

所以现在我们需要修改一下事务传播行为

propagation = Propagation.REQUIRES_NEW,表示无论deleteById中是否有事务,insert方法中都会新开启一个新的事物

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
    deptLogMapper.insert(deptLog);
}
  • 1
  • 2
  • 3
  • 4
  • 5

当在deleteById事务中开启了insert事务,此时deleteById事务会被挂起,进行insert事务,当insert事务进行完成后继续运行deleteById事务

propagation = Propagation.REQUIRES_NEW,表示无论deleteById中是否有事务,insert方法中都会新开启一个新的事物

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
    deptLogMapper.insert(deptLog);
}
  • 1
  • 2
  • 3
  • 4
  • 5

当在deleteById事务中开启了insert事务,此时deleteById事务会被挂起,进行insert事务,当insert事务进行完成后继续运行deleteById事务

二、Spring基于AOP的声明式事务控制

2.1 Spring事务概述

Spring的事务分为:编程式事务控制 和 声明式事务控制

  • 编程式事务控制

Spring提供了事务控制的类和方法,使用编码的方式对业务代码进行事务控制,事务控制代码和业务操作代码耦合到了一起,开发中不使用

  • 声明式事务控制

Spring将事务控制的代码封装,对外提供了Xml和注解配置方式,通过配置的方式完成事务的控制,可以达到事务控制与业务操作代码解耦合,开发中推荐使用

Spring事务编程相关的类主要有如下三个

  • 平台事务管理器 PlatformTransactionManager

是一个接口标准,实现类都具备事务提交、回滚和获得事务对象的功能,不同持久层框架可能会有不同实现方案

  • **事务定义 TransactionDefinition **

封装事务的隔离级别、传播行为、过期时间等属性信息

  • 事务状态 TransactionStatus

存储当前事务的状态信息,如果事务是否提交、是否回滚、是否有回滚点等

2.2 搭建测试环境

​ 搭建一个转账的环境,dao层一个转出钱的方法,一个转入钱的方法,service层一个转账业务方法,内部分别调用dao层转出钱和转入钱的方法,准备工作如下:

Maven坐标

        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.6</version>
        </dependency>
        <!--Mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency><dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.2</version>
        </dependency>
        <!-- druid数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.23</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.7</version>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

jdbc连接信息

jdbc.driver = com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/itcast
jdbc.username=root
jdbc.password=root
  • 1
  • 2
  • 3
  • 4
  • 数据库准备一个账户表account;

    image-20230609175807358

  • dao层准备一个AccountMapper,包括incrMoney和decrMoney两个方法

public interface AccountMapper {
//   加钱
    @Update("update account set money=money+#{money} where id=#{id}")
    public void incrMoney(@Param("id") String account,@Param("money") Integer money);
//   减钱
    @Update("update account set money=money-#{money} where id=#{id}")
    public void decrMoney(@Param("id") String account,@Param("money") Integer money);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • service层准备一个transferMoney方法,分别调用incrMoney和decrMoney方法
public interface AccountService {
    void transferMoney(String outAccount,String inAccount , Integer money);
}
  • 1
  • 2
  • 3

实现类

@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void transferMoney(String outAccount, String inAccount, Integer money) {
        accountMapper.decrMoney(outAccount,money);
        accountMapper.incrMoney(inAccount,money);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 在applicationContext文件中进行Bean的管理配置

xml配置文件

<!--组件扫描-->
<context:component-scan base-package="com.zhangjingqi"></context:component-scan>

<!--加载JDBC properties-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>

<!--配置数据源信息-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <!--配置必要属性-->
    <property name="driverClassName" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<!--配置SqlSessionFactoryBean ,将SqlSessionFactory存储到Spring容器-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"></property>
</bean>

<!--MapperScannerConfigurer,作用赛秒指定的包,产生Mapper对象存储到Spring容器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.zhangjingqi.mapper"></property>
</bean>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 测试正常转账与异常转账
public class AccountTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService accountService = applicationContext.getBean(AccountService.class);
        accountService.transferMoney("1","2",500);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.3xml方式声明事务

2.3.1 快速入门

我们要把AccountServiceImpl类中的转账方法设置在一个事务之中

下面的代码是两个事务

@Override
public void transferMoney(String outAccount, String inAccount, Integer money) {
    accountMapper.decrMoney(outAccount,money);
    accountMapper.incrMoney(inAccount,money);
}
  • 1
  • 2
  • 3
  • 4
  • 5

以使用AOP对Service的方法进行事务的增强

  • 目标类: 自定义的AccountServiceImpl,内部的方法是切点
  • 通知类:Spring提供的,通知方法已经定义好,只需要配置即可

分析

  • 通知类是Spring提供的,需要导入Spring事务的相关的坐标
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.13.RELEASE</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

其实这个坐标我们之前已经导入过了,这个坐标下面有事务相关的坐标

image-20230610124509971

  • 配置目标类AccountServiceImpl
  • 使用advisor标签配置切面

如果有所忘记的话,可以看一下这篇文章 SpringBoot——IOC与AOP

xml配置文件新配置信息

<!--配置一个平台实物管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--内部需要注入一个DataSource-->
    <property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置Spring提供好的Advice,需要配置一个平台事务管理器transaction-manager-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--代表任意的方法都使用默认情况下的那些事务的属性-->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

<!--事务增强的AOP-->
<aop:config>
    <!--配置切点表达式-->
    <aop:pointcut id="txPointcut" expression="execution(* com.zhangjingqi.service.impl.*.*(..))"/>
    <!-- 配置织入     advice-ref说明:通知/增强引用Spring提供好的-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"></aop:advisor>
</aop:config>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

故意在impl层出现异常

@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void transferMoney(String outAccount, String inAccount, Integer money) {
        accountMapper.decrMoney(outAccount,money);
        int c = 3/0;
        accountMapper.incrMoney(inAccount,money);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

进行测试

出现了异常,但是两个人的钱并没有扣,很完美 就是我们想要的结果

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
accountService.transferMoney("1","2",500);
  • 1
  • 2
  • 3

2.3.2 详解

  • 平台事务管理器

我们再看一下xml中配置的平台事务管理器

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--内部需要注入一个DataSource-->
    <property name="dataSource" ref="dataSource"></property>
</bean>
  • 1
  • 2
  • 3
  • 4

其中DataSourceTransactionManager是一个类,实现了很多接口,其中一个接口的父接口是PlatformTransactionManager接口(一个平台事务管理器),接口中有提交和回滚的方法

PlatformTransactionManager接口的实现类是选哪一个,取决于当前DAO层使用的框架是什么样的

比如如果使用的是最简单的JDBC、Mybatis,那实现类就是DataSourceTransactionManager

如果DAO层使用的是Hibernate,那对应的平台管理器就是HibernateTransactionManager

image-20230610131659253

  • Spring提供好的Advice 通知/增强

刚刚我们在配置类中是这么配置的

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--代表任意的方法都使用默认情况下的那些事务的属性-->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

底层用到的事务的操作都在transactionManager中封装这

主要看一下事务的属性

忘记的话可以看一下下面这个文章MySQL基础 — 多表查询以及事务管理

<tx:attributes>
<tx:method name="方法名称"
           isolation="隔离级别,解决事务并发问题"
           propagation="传播行为,事务嵌套问题"
           read-only="只读状态"
           timeout="超时时间,单位是秒,访问数据库有一个时间,不能一直在查询,一般设置为-1,表示没有超时时间,因为数据库自己有自己的超时时间"/>
</tx:attributes>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

下面这个操作是配置不同方法的不同属性,name代表方法名称,*代表通配符,下面这个配置的意思就是所有的方法都用当前这一套事务的配置,如果没配置事务的属性,就是默认值

<tx:method name="*"/>
  • 1

也就是说可以配置好几套。比如下面,没有配置的参数就按照默认

<tx:method name="transferMoney"></tx:method>
<tx:method name="registAccount"></tx:method>
  • 1
  • 2

但是方法很多的时候一个一个添加又不太现实,所以我们的命名一定要规范,这样就能使用通配符进行匹配

比如下面是匹配add开头的方法

<tx:method name="add*"></tx:method>
  • 1

2.3.3 原理剖析(浅浅看一下 )

主要看一下下面这个配置的原理

    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--代表任意的方法都使用默认情况下的那些事务的属性-->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

<aop:config>
    <!--配置切点表达式-->
    <aop:pointcut id="txPointcut" expression="execution(* com.zhangjingqi.service.impl.*.*(..))"/>
    <!-- 配置织入     advice-ref说明:通知/增强引用Spring提供好的-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"></aop:advisor>
</aop:config>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

首先找到对应的处理器

image-20230610141624327

找到对应的标签点进去

image-20230610141752059

发现TxAdviceBeanDefinitionParser类中没有parse方法,但是有一个doparse方法,代表着调用他爹的

image-20230610142237853

然后看一下AbstractSingleBeanDefinitionParser类,也没有parse方法,

那就再看一下AbstractSingleBeanDefinitionParser类的爹AbstractBeanDefinitionParser类中有没有,发现是有的

并且调用了一个parseInternal方法

image-20230610142720804

点过去发现parseInternal方法是抽象的,说明具体的实现在他儿子那里

image-20230610142807004

AbstractBeanDefinitionParser类的儿子是AbstractSingleBeanDefinitionParser类,里面确实有parseInternal方法的具体实现

image-20230610142955925

这个地方感觉太难了,看不下去了,就到这里吧

2.4 注解方式声明事务

标题一就是注解方式声明事务

就是用注解代替通知的配置、织入的配置,也就是代替下面两部分

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--代表任意的方法都使用默认情况下的那些事务的属性-->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

<!--事务增强的AOP-->
<aop:config>
    <!--配置切点表达式-->
    <aop:pointcut id="txPointcut" expression="execution(* com.zhangjingqi.service.impl.*.*(..))"/>
    <!-- 配置织入     advice-ref说明:通知/增强引用Spring提供好的-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"></aop:advisor>
</aop:config>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

注解方式,依然可以使用xml中对应的参数

@Transactional可以使用在类上,也可以使用在方法上

@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = 
Propagation.REQUIRED,readOnly = false,timeout = 5)
  • 1
  • 2

仍然需要xml配置,开启事务开关

<bean id="transactionManager"  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSource"/>
</bean>

<!--配置事务的注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.5 切点表达式配置方法与实物属性配置方法区别

下图中我们已经选出合适的切点了

image-20230610151246654

为什么还要在事务管理这个地方再配置一下对应哪个方法?

image-20230610151319336

因为我们在切点中筛选出来的方法很多,所实现的业务也不一样

那如果不在事务管理再分或者说筛选的话,那所有的方法事务配置的属性都一个样子了,这可能不会符合现实

切点表达式,是过滤哪些方法可以进行事务增强

事务属性信息的name,是指定哪个方法要进行哪些事务属性的配置

image-20230610151233531

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/煮酒与君饮/article/detail/995300
推荐阅读
相关标签
  

闽ICP备14008679号