Springboot——事物管理

文章目录

  • 事务管理
  • 一、 Spring事务管理
    • 1.1 事务回顾
    • 1.2 案例: 解散部门(未开启事务)
    • 1.3 事务管理注解@Transactional
    • 1.4 事务管理日志开关
    • 1.5 rollbackFor 异常回滚属性
    • 1.6 propagation 事务传播行为
    • 1.7 解散部门并记录操作日志
      • 1.7.1 创建数据库表
      • 1.7.2 代码实现
  • 二、Spring基于AOP的声明式事务控制
    • 2.1 Spring事务概述
    • 2.2 搭建测试环境
    • 2.3xml方式声明事务
      • 2.3.1 快速入门
      • 2.3.2 详解
      • 2.3.3 原理剖析(浅浅看一下 )
    • 2.4 注解方式声明事务
    • 2.5 切点表达式配置方法与实物属性配置方法区别

事务管理

一、 Spring事务管理

1.1 事务回顾

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

事务的操作

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

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

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

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

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

下面进行完善

SQL

DeptMapper

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

EmpMapper

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

业务代码

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

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

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

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

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

1.3 事务管理注解@Transactional

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

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

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

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

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

1.4 事务管理日志开关

logging:
  level:
    org.springframework.jdbc.support JdbcTransactionManager: debug

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);
}
  • 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.6 propagation 事务传播行为

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

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

Springboot——事物管理_第1张图片

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.7.2 代码实现

日志信息实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
    private Integer id;
    private LocalDateTime createTime;
    private String description;
}

日志插入SQL

@Mapper
public interface DeptLogMapper {

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

}

日志插入业务代码

@Service
public class DeptLogServiceImpl implements DeptLogService {

    @Autowired
    private DeptLogMapper deptLogMapper;

    @Transactional //事务传播行为:有事务就加入、没有事务就新建事务
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}

删除部门业务代码

    @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);
    }

此时方法调用有两个 @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);
}

当在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);
}

当在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坐标

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.19version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-jdbcartifactId>
            <version>6.0.6version>
        dependency>
        
        <dependency>
            <groupId>org.mybatisgroupId>
            <artifactId>mybatisartifactId>
            <version>3.4.6version>
        dependency><dependency>
            <groupId>org.mybatisgroupId>
            <artifactId>mybatis-springartifactId>
            <version>3.0.2version>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.1.23version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>5.3.7version>
        dependency>

jdbc连接信息

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

    Springboot——事物管理_第2张图片

  • 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);
}

  • service层准备一个transferMoney方法,分别调用incrMoney和decrMoney方法
public interface AccountService {
    void transferMoney(String outAccount,String inAccount , Integer money);
}

实现类

@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);
    }
}
  • 在applicationContext文件中进行Bean的管理配置

xml配置文件


<context:component-scan base-package="com.zhangjingqi">context:component-scan>


<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>


<bean class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource">property>
bean>


<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.zhangjingqi.mapper">property>
bean>
  • 测试正常转账与异常转账
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);
    }
}

2.3xml方式声明事务

2.3.1 快速入门

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

下面的代码是两个事务

@Override
public void transferMoney(String outAccount, String inAccount, Integer money) {
    accountMapper.decrMoney(outAccount,money);
    accountMapper.incrMoney(inAccount,money);
}

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

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

分析

  • 通知类是Spring提供的,需要导入Spring事务的相关的坐标
<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-jdbcartifactId>
    <version>5.2.13.RELEASEversion>
dependency>

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

Springboot——事物管理_第3张图片

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

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

xml配置文件新配置信息


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

<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.*.*(..))"/>
    
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut">aop:advisor>
aop:config>

故意在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);
    }
}

进行测试

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

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

2.3.2 详解

  • 平台事务管理器

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

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

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

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

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

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

Springboot——事物管理_第4张图片

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

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

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        
        <tx:method name="*"/>
    tx:attributes>
tx:advice>

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

主要看一下事务的属性

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

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

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

<tx:method name="*"/>

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

<tx:method name="transferMoney">tx:method>
<tx:method name="registAccount">tx:method>

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

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

<tx:method name="add*">tx:method>

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.*.*(..))"/>
    
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut">aop:advisor>
aop:config>

首先找到对应的处理器

Springboot——事物管理_第5张图片

找到对应的标签点进去

Springboot——事物管理_第6张图片

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

Springboot——事物管理_第7张图片

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

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

并且调用了一个parseInternal方法

Springboot——事物管理_第8张图片

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

Springboot——事物管理_第9张图片

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

Springboot——事物管理_第10张图片

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

2.4 注解方式声明事务

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

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

<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.*.*(..))"/>
    
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut">aop:advisor>
aop:config>

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

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

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

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

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


<tx:annotation-driven transaction-manager="transactionManager"/>

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

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

Springboot——事物管理_第11张图片

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

Springboot——事物管理_第12张图片

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

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

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

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

Springboot——事物管理_第13张图片

你可能感兴趣的:(#,SpringbootWeb,spring,boot,java,spring)