7.AOP实现事务管理

本章目标

  1. 事务的应用场景(理解)
  2. Spring AOP事务管理相关概念(理解)
  3. 声明式事务处理(掌握)

本章内容

一、事务的应用场景

Mybatis默认是手动提交事务的

JDBC默认是自动提交事务的

当Spring整合Mybatis时,事务默认是自动提交的

MySQL事务官网

1、事务简介

事务管理在应用程序中起着至关重要的作用:它是一系列任务的组成工作单元,在这个工作单元中,所有的任务必须同时执行。它们只有二种可能执行结果,要么所有任务全部执行成功,要么所有任务全部执行失败。

事务使用ACID特性来衡量事务的质量。

  1. 原子性 :事务必须是原子的,在事务结束的时候,事务中的所有任务必须全部成功完成,否则全部失败,事务回滚到事务开始之间的状态。
  2. 一致性 :事务必须保证和数据库的一致性,即数据库中的所有数据和现实保持一致。如果事务失败数据必须返回到事务执行之前的状态,反之修改数据和现实的同步。
  3. 隔离性:隔离性是事务与事务之间的屏障,每个事务必须与其他事务的执行结果隔离开,直到该事务执行完毕,它保证了事务的访问的任何数据不会受其他事务执行结果的影响。
  4. 持久性:如果事务成功执行,无论系统发生任何情况,事务的持久性都必须保证事务的执行结果是永久的。

2、为什么要添加事务

  • Dao每个方法针对是一次数据库操作

    @Repository
     public void update(){
     }
    
  • service层每一个方法针对是一个业务功能

    @service
     public  void transAcccount(){
          update();-
          update();+
     }
    

3、银行转账的例子

为了演示方便,我们先来看一个转账的功能,首先先建立两张表,账户表,交易记录表

  • 数据库脚本

    #账户表
     create table account(
     id int primary key auto_increment,
     account_no varchar(50),
     account_name varchar(50),
     account_balance float
     );
     
     #测试数据
     insert into account(account_no,account_name,account_balance)VALUES(‘6212264100011335371’,‘张三’,1000);
     insert into account(account_no,account_name,account_balance)VALUES(‘6212264100011335372’,‘李四’,1000);
     COMMIT;
    
  • 编写实体对象

    @Data
     public class Account {
         private Integer id;
         private String accountNo;
         private String accountName;
         private Float accountBalance;
     }
    
  • 编写数据交互层代码

    public interface AccountMapper {
         //通过账号查找账户对象
         Account findAccoutByAccountNo(String accountNo);
         //修改账户余额
         void updateBalanceByAccountNo(Account account);
     }
    
  • SQL映射文件

AccountMapper.xml

 <?xml version="1.0" encoding="utf-8" ?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
 <mapper namespace="com.woniuxy.ssm.mapper.AccountMapper">
     <update id="updateBalanceByAccountNo" parameterType="Account">
         UPDATE account SET account_balance=#{accountBalance} WHERE account_no=#{accountNo}
     </update>
     <select id="findAccoutByAccountNo" parameterType="string" resultType="Account">
         SELECT * FROM account WHERE account_no=#{accountNo}
     </select>
 </mapper>
  • 业务层接口

     public interface AccountService {
         void transferAccounts(String fromAccount, String toAccount,float money);
     }
    
  • 业务层代码

    @Service
     public class AccountServiceImpl implements AccountService {
         @Autowired
         private AccountMapper accountMapper;
     
         @Override
         public void transferAccounts(String fromAccount, String toAccount, float money) {
             //查询余额
             Account fromAccountEntity = accountMapper.findAccoutByAccountno(fromAccount);
             fromAccountEntity.setAccountbalance(fromAccountEntity.getAccountbalance() - money);
             accountMapper.updateBalanceByAccountno(fromAccountEntity);
             //修改收款者余额
             Account toAccountEntity = accountMapper.findAccoutByAccountno(toAccount);
             toAccountEntity.setAccountbalance(toAccountEntity.getAccountbalance() + money);
             accountMapper.updateBalanceByAccountno(toAccountEntity);
        }
     }
    
  • 测试代码

    BeanFactory context=new ClassPathXmlApplicationContext(“spring-config.xml”);
     AccountService accountService=(AccountService) context.getBean(“accountServiceImpl”);
     String fromAccount=“6212264100011335371”;
     String toAccount=“6212264100011335372”;
     float money=100;
     accountService.transferAccounts(fromAccount,toAccount,money);
    
  • 执行后,正常情况下,已经完成了张三向李四的转账功能

  • 将toAccount改为一个不存在的账户,并测试,

查看控制台,已经报了异常错误信息

发现账户减了,但由于另一个账户不存在,程序出错了

这是因为程序在执行转账功能的时候,由于收款者的账户没有,所以出现NullPointerException,在空指向异常之前的代码成功执行,但是在空指向异常之后的代码不再执行,导致转账出现了问题,显然这样肯定不可以。

那么我们应该怎么办?实际上解决这个问题需要配置事务可以解决。

二、Spring AOP事务管理相关概念

官网事务说明

Spring提供了两种事务管理方式:

  • 声明式事务管理:它基于AOP实现,无须编写任何事务管理代码,通过配置和注解即可完成事务管理(推荐)
  • 编程式事务处理:必须在事务上下文中运行并显式使用的应用程序代码 TransactionTemplate等类,耦合度高

1、事务属性

事务属性包含了5个方面:

  • 传播行为
  • 隔离规则
  • 回滚规则
  • 事务超时
  • 是否只读

事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。

2、传播行为

事务的第一个方面是传播行为(propagation behavior)。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:

传播行为 意 义
PROPAGATION_MANDATORY 表示该方法必须运行在一个事务中。如果当前事务不存在,抛出异常
PROPAGATION_NESTED 如果存在事务,那么该方法运行在一个嵌套的事务中;不存在,和REQUIRED一样;需要厂商支持
PROPAGATION_NEVER 表示该方法不应该运行在一个事务上下文中。如果存在一个事务,则会抛出一个异常
PROPAGATION_NOT_SUPPORTED 表示该方法不应该在事务中运行。如果一个现有的事务正在运行,该方法在运行期被挂起
PROPAGATION_REQUIRED 表示该方法必须运行在一个事务中。如果一个现有的事务正在运行中,该方法将运行在这个事务中。否则,开始一个新事务
PROPAGATION_REQUIRES_NEW 表示当前方法必须运行在它自己的事务里。它将启动一个新的事务,如果已经有事务运行,挂起
PROPAGATION_SUPPORTS 当前方法不需要事务处理环境,但如果有一个事务在运行,这个方法也可以在这个事务里运行

3、事务并发引发的问题

如果没有接触过并发引发的问题,可以看一下E:章中关于并发引发的问题相关内容

并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多用户。

3.1 事务并发引发的问题

1、脏读(Dirty Reads) 一个事务正在对一条记录进行修改,这个事务完成并提交前,这条记录的数据就处于不一致的状态。此时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,

事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。

2、不可重复读(Non-Repeatable Reads) 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”,重复读到的是不同的数据。

事务A读取到了事务B已经提交的修改数据,不符合隔离性。

3、幻读(Phantom Reads) 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

比如事务A第一次查询到表Student中有a、b、c三条数据,然后事务B向里面添加一条d数据,事务A再按照原来的查询条件查询发现查询出四条数据,这让事务A产生幻想,之前 明明就只有三条数据,为什么现在却有四条数据了呢? 事务A读取到了事务B提交的新增数据,不符合隔离性。

3.2 解决方案:

解决更新丢失主要有以下两个方式:

  1. 使用事务+锁定读,也就是for update
  2. 不使用事务,用CAS自旋来操作

脏读、不可重复读和幻读其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。那有哪些隔离级别呢?主要有以下四种隔离级别:

隔离级别 脏读 不可重复读 幻读
读未提交(Read uncommitted) 不能解决 不能解决 不能解决
读已提交(Read committed) 可以解决 不能解决 不能解决
可重复读(Repeatable read) 可以解决 可以解决 不能解决
可串行化(Serializable) 可以解决 可以解决 可以解决

假设现在有两个事务,事务A和事务B,那么上面的四种隔离级别是什么意思呢? 1、读未提交(Read uncommitted) 字面意思,就是可以读到别的事务未提交的数据,也就是事务A可以读取事务B未提交的数据,这种情况肯定可能会导致脏读、不可重复度和幻读。

2、读已提交(Read committed) 字面意思,就是只可以读到别的事务已提交的数据,也就是事务A只可以读取到事务B已经提交了的数据,那么在B未提交之前的数据是读取不到的,也就不可能产生脏读,但是因为事务B已提交的 数据是可以读取到的,所以可能会导致不可重复读和幻读。

3、可重复读(Repeatable read) 字面意思,就是事务可以重复读取数据,在事务期间,每次读取的数据都是一样的,也就是事务A开启事务后,读取了某一个表的5条数据,不管你事务B怎么对这5条数据修改操作,我事务A每次 查询都是5条一摸一样的数据,所以是可重复读的,因此不可能导致脏读,不可重复读,但是还是可能导致幻读的。

4、可串行化(Serializable) mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。

3.3 对应Spring AOP中事务隔离级别

隔离级别 含义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITTED 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
ISOLATION_REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
ISOLATION_SERIALIZABLE 最高的隔离级别,完全服从ACID的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的

4、事务声明为只读

在对数据库的操作中,查询是使用最频繁的操作,每次执行查询时都要从数据库中重新读取数据,有时多次读取的数据都是相同的,这样的数据操作不仅浪费了系统资源,还影响了系统速度。对访问量大的程序来说,节省这部分资源可以大大提升系统速度。

如果将事务声明为只读的,那么数据库可以根据事务的特性优化事务的读取操作。

 <prop key="query*">PROPAGATION_REQUIRED,readOnly</prop>

事务的只读属性需要配合事务的传播行为共同设置。

5、事务的超时属性

为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。

6、回滚规则

默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与EJB的回滚行为是一致的) 但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。

Spring提供了对编程式事务和声明式事务的支持,编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于AOP)有助于用户将操作与事务规则进行解耦。

三、声明式事务处理

在Spring中配置声明式事务处理有两种方式,一种是基于XML的声明式事务处理,还有另外一种是基于Annotaion的事务处理

Spring提供了声明式事务处理机制,它基于AOP实现,事务的管理将由Spring框架自动进行处理,无须编写任何事务管理代码,所有的工作全在配置文件中完成。这意味着业务代码完全分离,配置即可用,降低了开发和维护的难度。

1、基于XML的声明式事务处理(了解)

这种方式的使用非常简单,开发者只需做三件事情

  • 在pom.xml文件中添加spring-tx的组件依赖(如果已经导入可忽略)

    <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-tx</artifactId>
         <version>${spring.version}</version>
     </dependency>
    
  • 在spring容器配置事务管理器

配置文件在官网位置

 <!--声明事务管理器-->
   <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
         <property name="dataSource" ref="dataSource"/>
     </bean>
 
     <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
     <tx:advice id="txAdvice" transaction-manager="txManager">
         <!-- the transactional semantics... -->
         <tx:attributes>
             <!-- all methods starting with 'get' are read-only -->
             <tx:method name="get*" read-only="true"/>

              <!--配置哪些方法是事务性的方法
         propagation:执行每一个方法的时候都要开启事务
         rollback-for:设置事务回滚的异常类型
         -->
             <!-- other methods use the default transaction settings (see below) -->
             <tx:method name="*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
         </tx:attributes>
     </tx:advice>
 
     <!-- ensure that the above transactional advice runs for any execution
         of an operation defined by the FooService interface -->
     <aop:config>
         <aop:pointcut id="fooServiceOperation" expression="execution(* com.woniuxy.ssm.service.*.*(..))"/>
         <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
     </aop:config>
  • 再次运行上述代码,会发现出错之后事务会回滚

2、基于注解的事务处理(掌握)

@Transactional可以作用于接口、接口方法、类以及类方法上。

  • 修改spring-config.xml

如果用注解实现则按照如下方式配置(注解就删除上方事务管理器之下的配置)

 <!--声明事务管理器-->
     <bean id="transactionManager"
           class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
         <property name="dataSource" ref="dataSource"></property>
     </bean>
    <!--注册事务注解驱动-->
     <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
  • 在需要使用事务的spring Bean类或者Bean类的方法上添加注解@Transactional,

    如果将注解添加在Bean类上,则表示事务的设置对整个Bean类的所有方法都起作用;如果将注解添加在Bean类中的某个方法上,则表示事务的设置只对该方法有效。

    @Service
     @Transactional
     public class AccountServiceImpl implements AccountService {
     
         @Autowired
         private AccountMapper accountMapper;
     
         @Override
         public void transferAccounts(String fromAccount, String toAccount,float money) {
             //查询余额
             Account fromAccountEntity=accountMapper.findAccoutByAccountno(fromAccount);
             fromAccountEntity.setAccountbalance(fromAccountEntity.getAccountbalance()-money);
             accountMapper.updateBalanceByAccountno(fromAccountEntity);
             //修改收款者余额
             Account toAccountEntity=accountMapper.findAccoutByAccountno(toAccount);
             toAccountEntity.setAccountbalance(toAccountEntity.getAccountbalance()+money);
             accountMapper.updateBalanceByAccountno(toAccountEntity);
     
        }
     }
    

添加上事务后,如果程序中没有异常正常转账,如果程序中有异常数据库中会及时回滚,不会出现上面的现象。

3、也可以放在方法上

 @Service
 public class AccountServiceImpl implements AccountService {
     @Autowired
     private AccountMapper accountMapper;
     @Override
     @Transactional(propagation = Propagation.REQUIRED)
     public void transferAccounts(String from, String to, float money) {
         //修改转账者
         Account fromAccount = accountMapper.selectByAccountNo(from);
         fromAccount.setAccountBalance(fromAccount.getAccountBalance()-money);
         accountMapper.updateByPrimaryKeySelective(fromAccount);
         //修改收款者
         Account toAccount = accountMapper.selectByAccountNo(to);
         fromAccount.setAccountBalance(toAccount.getAccountBalance()+money);
         accountMapper.updateByPrimaryKeySelective(toAccount);
     }
 }

4、 @Transactional不起作用的解决办法

  1. 检查方法是不是public

  2. 异常类型是不是unchecked异常(注解默认处理runtimeException,但是配置文件会根据配置的来)

  3. 数据库引擎要支持事务,如果是mysql,注意表要使用支持事务的引擎innodb

  4. 是否开启了对注解的解析

  5. spring是否扫描到你这个包,如下是扫描到com.woniuxy下面的包

  6. 业务方法有没有加了try…catch代码?请记住由系统来处理,不要自己使用try….catch来处理

    @Override
         public void transferAccounts(String fromAccount, String toAccount, float money) {
             try{
                 //查询余额
                 Account fromAccountEntity = accountMapper.findAccoutByAccountno(fromAccount);
                 fromAccountEntity.setAccountbalance(fromAccountEntity.getAccountbalance() - money);
                 accountMapper.updateBalanceByAccountno(fromAccountEntity);
                 //修改收款者余额
                 Account toAccountEntity = accountMapper.findAccoutByAccountno(toAccount);
                 toAccountEntity.setAccountbalance(toAccountEntity.getAccountbalance() + money);
                 accountMapper.updateBalanceByAccountno(toAccountEntity);
            }catch (Exception e){
                 System.out.println(e.getMessage());
            }
     
        }
    

idea中try/catch快捷键:选中需要的代码,按下键盘上的的“ctrl+alt+t”即可。

Spring通过AOP框架在容器启动时,自动发现需要事务代理的类或方法(即标注了@Transactional的类或方法),为这些方法嵌入事务切面(即BeanFactoryTransactionAttributeSourceAdvisor)生成代理类,之后我们从容器获取到的对应的bean就是进行事务增强后的代理类。大致的步骤包括:

1.InfrastructureAdvisorAutoProxyCreator作为BeanPostProcessor,在容器启动期间其postProcessAfterInitialization方法被调用,作为创建事务增强代理对象的入口

2.InfrastructureAdvisorAutoProxyCreator之后从beanfactory中获取BeanFactoryTransactionAttributeSourceAdvisor的实例,使用BeanFactoryTransactionAttributeSourceAdvisor中的Pointcut对当前正在创建的bean进行匹配,如果匹配成功,则证明需要生成事务增强代理。将目标对象和切面设置到ProxyFactory中,用于生成代理 ,通过ProxyFactory来生成事务增强代理

思维导图

image

posted @ 2025-04-21 17:57  icui4cu  阅读(67)  评论(0)    收藏  举报