9、Spring_事务管理

六、Spring 事务管理

1.Spring 事务简介

  • 事务概述:保证数据库操作同时成功或者同时失败

  • Spring 事务的概述:在数据层保证数据库操作同时成功或者同时失败

2.转账案例分析

  • 转账肯定有一个业务方法:给转出用户减钱,给转入用户加钱
  • 要求:
    • 要么同时成功要么同时失败

2.1Spring 平台事务管理器

  • 提供规范接口

    public interface PlatformTransactionManager {
        TransactionStatus getTransaction(TransactionDefinition var1) throws TransactionException;
    
        void commit(TransactionStatus var1) throws TransactionException;
    
        void rollback(TransactionStatus var1) throws TransactionException;
    }
    
  • 方法

    • void commit(TransactionStatus var1):用于提交事务
    • void rollback(TransactionStatus var1):用户事务回滚
  • 具体实现:DataSourceTransactionManager 来实现的,通过DataSource dataSource 以 JDBC 事务的方式来控制事务9、Spring_事务管理_第1张图片

2.2转账案例分析

  • 业务分析

    • 在业务层需要保证事务的同时成功或者同时失败
    • 结果
      • 出现异常:张三转账给李四,比如中途出现问题,张三的钱和李四的钱应该不出现误差
      • 没有出现异常:张三转账给李四,没有出现问题,张三的钱正常减少,李四的钱正常增多
  • 提供service 方法

    public interface IAccountService {
        /**
         * 实现转账操作
         * @param srcId 转账人
         * @param deskId 接收人
         * @param money 转账金额
         */
        public void transfer(Long srcId,Long deskId,int money);
    }
    
  • 下载插件的官方地址 https://plugins.jetbrains.com/

  • 提供service 实现方法

    @Service
    public class AccountServiceImpl implements IAccountService {
    
        //会使用到 mapper
        @Autowired
        private AccountMapper mapper;
    
        public void transfer(Long srcId, Long deskId, int money) {
            mapper.outAccount(srcId,money);//转账扣钱
    
            mapper.inAccount(deskId,money);//接收转账钱
        }
    }
    
  • 提供 mapper 接口

    public interface AccountMapper {
        void outAccount(@Param("id") Long srcId, @Param("money") int money);
    
        void inAccount(@Param("id")Long deskId,@Param("money") int money);
    }
    
  • 提供 mapper.xml

    update>
        <update id="outAccount">
        update account
        set money = money-#{money,jdbcType=INTEGER}
        where id = #{id,jdbcType=BIGINT}
        update>
      <update id="inAccount">
        update account
        set money = money+#{money,jdbcType=INTEGER}
        where id = #{id,jdbcType=BIGINT}
      update>
    
  • 测试

     @Test
        public void testMybatis(){
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
            IAccountService bean = context.getBean(IAccountService.class);
            bean.transfer(1L,2L,500);
    
        }
    

3.基于注解的方式实现

3.1@EnableTransactionManagement

  • @EnableTransactionManagement:用于开启事务支持的,直接添加到spring 配置类

  • 说明

    名称 @EnableTransactionManagement
    位置 配置类上方
    作用 设置当前spring环境支持事务
  • 修改配置类

    @Configuration
    @ComponentScan("cn.sycoder")
    @PropertySource("db.properties")
    @Import({JdbcConfig.class,MyBatisConfig.class})
    //开启事务支持
    @EnableTransactionManagement
    public class SpringConfig {
    }
    

3.2 配置事务管理,配置数据源

  • 如果不配置的话会报:找不到数据源错误
    9、Spring_事务管理_第2张图片

  • PlatformTransactionManager

  • 代码

    public class JdbcConfig {
        @Value("${jdbc.username}")
        private String username;
        @Value("${jdbc.password}")
        private String password;
        @Value("${jdbc.driverClassName}")
        private String driverClassName;
        @Value("${jdbc.url}")
        private String url;
        //配置连接池
        @Bean
        public DataSource dataSource(){
            DruidDataSource source = new DruidDataSource();
            source.setUsername(username);
            source.setPassword(password);
            source.setDriverClassName(driverClassName);
            source.setUrl(url);
            return source;
        }
    
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource){
            DataSourceTransactionManager manager = new DataSourceTransactionManager();
            manager.setDataSource(dataSource);
            return manager;
        }
    }
    

3.3 @Transactional

  • @Transactional:为业务添加事务的

  • 说明

    名称 @Transactional
    位置 业务层接口上方,或者实现类上方,或者具体业务方法上方
    作用 为当前的业务方法添加事务支持
  • 修改业务层

    • 业务方法上添加

      @Transactional
      public void transfer(Long srcId, Long deskId, int money) {
          mapper.outAccount(srcId,money);//转账扣钱
          System.out.println(1/0);
          mapper.inAccount(deskId,money);//接收转账钱
      }
      
    • 业务类上添加

      @Service
      @Transactional
      public class AccountServiceImpl implements IAccountService {
      
          //会使用到 mapper
          @Autowired
          private AccountMapper mapper;
      
      
          public void transfer(Long srcId, Long deskId, int money) {
              mapper.outAccount(srcId,money);//转账扣钱
              System.out.println(1/0);
              mapper.inAccount(deskId,money);//接收转账钱
          }
      }
      
    • 接口层添加

      @Transactional
      public interface IAccountService {
          /**
           * 实现转账操作
           * @param srcId 转账人
           * @param deskId 接收人
           * @param money 转账金额
           */
          public void transfer(Long srcId,Long deskId,int money);
      }
      

    4.事务角色

  • 在没有开启Spring事务之前:两条语句分别开启两个事务 T1 和 T2

    • 如果同时成功,T1和T2都会正常提交
    • 如果T1正常,T2之前抛出异常,就会出现T1能够正常转账,但是T2收不到钱,因为不是同一个事务导致金钱异常
    public void transfer(Long srcId, Long deskId, int money) {
        mapper.outAccount(srcId,money);//转账扣钱
        System.out.println(1/0);
        mapper.inAccount(deskId,money);//接收转账钱
    }
    

    9、Spring_事务管理_第3张图片

  • 开启Spring 事务管理之后

    9、Spring_事务管理_第4张图片

    • 在转账 transfer 方法上加入 @Transactional 注解之后,该方法会新建一个事务T
    • 把 mapper 中 outAccount 事务 T1 加入到 事务T中,把 mapper 中 inAccount 事务 T2 也加入到事务T中
    • 通过 @Transactional 注解统一了 transfer 方法的事务保证转账和入账方法变成同一事务操作

4.1事务管理员&事务协调员

  • 事务管理员:发起新事务,使用 @Transactional 注解开启事务
  • 事务协调员:加入新事务,保证多个事务变成同一事务下的操作

5.@Transactional 属性

5.1 readOnly

  • 概述:表示只读,没有写操作。可以通过这个属性告诉数据库我们没有写操作,从而数据库可以针对只读sql做优化操作

  • 使用

    @Transactional(readOnly = true)
    public Account selectById(Long id){
        return mapper.selectByPrimaryKey(id);
    }
    
  • 如果对于有写操作的使用这个属性,会报如下错误

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhqysmIG-1693295276402)(picture/image-20221105212707549.png)]

5.2 timeout

  • 超时概述:事务再执行的时候,由于某些原因卡住,长时间占用数据库资源。此时很可能程序sql有问题,希望撤销事务,能够让事务结束,释放资源,即超时回滚。

  • 默认值是-1.-1表示用不回滚,单位是秒

  • int timeout() default -1;
    
  • 使用

    @Transactional(readOnly = true,timeout = 1)
        public Account selectById(Long id){
            try {
                Thread.sleep(10000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return mapper.selectByPrimaryKey(id);
        }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hamXB1vD-1693295276402)(picture/image-20221105213517098.png)]

5.3 rollbackFor&rollbackForClassName

  • 回滚概述:回滚策略,希望对于什么样的异常回顾

  • 注意:并不是所有的异常 Spring 都会回滚,Spring 只对 Error 异常和 RuntimeException 异常回滚

  • 使用

    @Transactional(rollbackFor = IOException.class)
        public void transfer(Long srcId, Long deskId, int money) throws IOException {
            mapper.outAccount(srcId,money);//转账扣钱
            if(true){
                throw new IOException("");
            }
            mapper.inAccount(deskId,money);//接收转账钱
        }
    
    @Transactional(rollbackForClassName = "IOException")
        public void transfer(Long srcId, Long deskId, int money) throws IOException {
            mapper.outAccount(srcId,money);//转账扣钱
            if(true){
                throw new IOException("");
            }
            mapper.inAccount(deskId,money);//接收转账钱
        }
    

5.4 noRollbackFor&noRollbackForClassName

  • 不会滚概述:出现这个异常不回滚

  • 使用

    @Transactional(noRollbackFor = ArithmeticException.class)
        public void transfer(Long srcId, Long deskId, int money) throws IOException {
            mapper.outAccount(srcId,money);//转账扣钱
            System.out.println(1/0);
            mapper.inAccount(deskId,money);//接收转账钱
        }
    
    @Transactional(noRollbackForClassName = "ArithmeticException")
        public void transfer(Long srcId, Long deskId, int money) throws IOException {
            mapper.outAccount(srcId,money);//转账扣钱
            System.out.println(1/0);
            mapper.inAccount(deskId,money);//接收转账钱
        }
    

5.5 isolation

  • 概述:设置事务隔离级别;

  • 如果不记得事务隔离级别,回去复习一下我讲的MySql

    • DEFAULT :默认隔离级别, 会采用数据库的隔离级别
    • READ_UNCOMMITTED : 读未提交
    • READ_COMMITTED : 读已提交
    • REPEATABLE_READ : 重复读取
    • SERIALIZABLE: 串行化
  • 使用

    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public Account selectById(Long id) throws IOException {
        return mapper.selectByPrimaryKey(id);
    }
    

5.6propagation

  • 事务传播行为:事务协调员对事务管理员所携带的事务的处理态度

  • 说明

    传播属性 说明
    REQUIRED 外围方法会开启新事务,内部方法会加入到外部方法的事务中
    SUPPORTS 外围方法没有事务,则内部方法不执行事务
    MANDATORY 使用当前事务,如果当前没有事务就抛异常
    REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起
    NOT_SUPPORTED 不支持事务
    NEVER 不支持事务,如果存在事务还会抛异常
    NESTED 如果当前存在事务,则在嵌套事务内执行,如果不存在,执行REQUIRED类似操作
  • 实操

    • REQUIRED:T1和T2会加入T中

      @Transactional(propagation = Propagation.REQUIRED)//事务T
      public void transfer(Long srcId, Long deskId, int money) throws IOException {
          mapper.outAccount(srcId,money);//转账扣钱 //事务T1
          System.out.println(1/0);
          mapper.inAccount(deskId,money);//接收转账钱 //事务T2
      }
      
    • SUPPORTS:外围没事务,所以内部只执行自己的事务,T1 和 T2 单独执行

      public void transfer(Long srcId, Long deskId, int money) throws IOException {
              mapper.outAccount(srcId,money);//转账扣钱//事务T1
              System.out.println(1/0);
              mapper.inAccount(deskId,money);//接收转账钱//事务T2
          }
      
    • REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起

       @Transactional(propagation=Propagation.REQUIRES_NEW)
          public void outAccount(Long id,  int money){
              mapper.outAccount(id,money);//转账扣钱
          }
      
          @Transactional(propagation=Propagation.REQUIRES_NEW)
          public void inAccount(Long id,  int money){
              mapper.inAccount(id,money);//转账扣钱
          }
      
      public void transfer(Long srcId, Long deskId, int money) throws IOException {
          outAccount(srcId,money);
          inAccount(deskId,money);
          throw new RuntimeException();
      
      }
      
      • 这种情况上面一条语句能够正常执行
      @Transactional(propagation = Propagation.REQUIRES_NEW)
          public void outAccount(Long id, int money) {
              mapper.outAccount(id, money);//转账扣钱
          }
      
          @Transactional(propagation = Propagation.REQUIRES_NEW)
          public void inAccount(Long id, int money) {
              if (true)
                  throw new RuntimeException();
              mapper.inAccount(id, money);//转账扣钱
          }
      

6.基于XML事务

  • 导入依赖

    <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.2.17.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
                <version>5.2.17.RELEASE</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <!--            <scope>test</scope>-->
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.16</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.29</version>
            </dependency>
            <!--        spring 整合 mybatis 的包-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis-spring</artifactId>
                <version>1.3.0</version>
            </dependency>
            <!--        mybatis 包-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.6</version>
            </dependency>
            <!--        spring 操作 jdbc 包-->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>4.3.29.RELEASE</version>
            </dependency>
        </dependencies>
    
  • 配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/tx
     http://www.springframework.org/schema/tx/spring-tx.xsd
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
        <bean id="accountService" class="cn.sycoder.service.impl.AccountServiceImpl">
    <!--        <property name="mapper" ref="mapper"/>-->
        </bean>
        <!--    <bean id="mapper" class="cn.sycoder.mapper.AccountMapper"></bean>-->
    
        <aop:config>
            <aop:advisor advice-ref="tx" pointcut="execution(* cn.sycoder.service.impl.*.*(..))"></aop:advisor>
        </aop:config>
    
        <tx:advice id="tx" transaction-manager="txManager">
            <tx:attributes>
                <tx:method name="get*" read-only="true"/>
            </tx:attributes>
        </tx:advice>
    
        <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <!-- (this dependency is defined somewhere else) -->
            <property name="dataSource" ref="dataSource"/>
        </bean>
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="${jdbc.driverClassName}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </bean>
        <context:property-placeholder location="db.properties"/>
    
    
    </beans>
    
  • 配置详解

    • 引入db.properties

      <context:property-placeholder location="db.properties"/>
      
    • 配置连接池

      <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
          <property name="driverClassName" value="${jdbc.driverClassName}"/>
          <property name="url" value="${jdbc.url}"/>
          <property name="username" value="${jdbc.username}"/>
          <property name="password" value="${jdbc.password}"/>
      </bean>
      
    • 配置事务管理器

      <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSource"/>
      </bean>
      
    • 配置 aop 事务增强

      <aop:config>
              <aop:advisor advice-ref="tx" pointcut="execution(* cn.sycoder.service.impl.*.*(..))"/>
      </aop:config>
              
      <tx:advice id="tx" transaction-manager="txManager">
          <tx:attributes>
              <tx:method name="get*" read-only="true"/>
          </tx:attributes>
      </tx:advice>    
      
  • 注意:如果你还想通过 xml 配置 mybatis ,那么你还需要把 mybatis 配置文件搞一份过来,通过xml 配置好 mybatis 之后,然后再获取 sqlSessionFactory 去获取 mapper 文件

  • 注意:spring 是面试重头戏,所以,你需要花时间认真巩固和复习,ioc 和 di 特别是对于常用注解,以及事务机制,aop 等都很爱问。

你可能感兴趣的:(spring,数据库,java)