Loading... # **Spring声明式事务** ## **目标** 1. **回顾事务,了解事务基本概念** 2. **了解Spring中的事务管理** 3. **熟练掌握事务在Spring的实现** ## **一、事务** **事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用。** ### ****1. 事务四个属性ACID**** 1. **原子性(atomicity)** **事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用** 2. **一致性(consistency)** **一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中** 3. **隔离性(isolation)** **可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏** 4. **持久性(durability)** **事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中** ### **2. 并发下事务会产生的问题** **举个例子,事务A和事务B操纵的是同一个资源,事务A有若干个子事务,事务B也有若干个子事务,事务A和事务B在高并发的情况下,会出现各种各样的问题。"各种各样的问题",总结一下主要就是五种:第一类丢失更新、第二类丢失更新、脏读、不可重复读、幻读。五种之中,第一类丢失更新、第二类丢失更新不重要,不讲了,讲一下脏读、不可重复读和幻读。** ****1、脏读**** **所谓脏读,就是指******事务A读到了事务B还没有提交的数据******,比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务-->取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。** ****2、不可重复读**** **所谓不可重复读,就是指******在一个事务里面读取了两次某个数据,读出来的数据不一致******。还是以银行取钱为例,事务A开启事务-->查出银行卡余额为1000元,此时切换到事务B事务B开启事务-->事务B取走100元-->提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。** ****3、幻读**** **所谓幻读,就是指******在一个事务里面的操作中发现了未被操作的数据******。比如学生信息,事务A开启事务-->修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务-->事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。** ### **3. 事务隔离级别** **事务隔离级别,就是为了解决上面几种问题而诞生的。为什么要有事务隔离级别,因为******事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大********,因此很多时候必须在并发性和性能之间做一个权衡**。所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,对于在事务隔离级别之外会产生的并发问题,在代码中做补偿。** **事务隔离级别有4种,但是像Spring会提供给用户5种,来看一下:** ****1、DEFAULT**** **默认隔离级别,每种数据库支持的事务隔离级别不一样,如果Spring配置事务时将isolation设置为这个值的话,那么将使用底层数据库的默认事务隔离级别。顺便说一句,如果使用的MySQL,可以使用"******select @@tx_isolation******"来查看默认的事务隔离级别** ****2、READ_UNCOMMITTED**** **读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用** ****3、READ_COMMITED**** **读已提交,即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读** ****4、REPEATABLE_READ**** **重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决** ****5、SERLALIZABLE**** **串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了** **网上专门有图用表格的形式列出了事务隔离级别解决的并发问题:**  **再必须强调一遍,不是事务隔离级别设置得越高越好,事务隔离级别设置得越高,意味着势必要花手段去加锁用以保证事务的正确性,那么效率就要降低,因此实际开发中往往要在效率和并发正确性之间做一个取舍,一般情况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,之后再通过一些别的手段去解决不可重复读和幻读的问题就好了。** ### **4. 案例测试** **在Spring-mybatis整合项目中,我们给UserMapper接口新增两个方法,删除和增加用户;** ``` /** *功能描述: 添加一个用户 * @param user 待添加的用户信息 * @return int * @author jiaoqianjin * @date 2021/3/16 */ int addUser(User user); /** *功能描述: 根据id删除用户 * @param id 待删除的用户id * @return int * @author jiaoqianjin * @date 2021/3/16 */ int deleteUser(int id); ``` **UserMapper.xml文件,我们故意把 delete 写错,测试事务!** ``` <insert id="addUser" parameterType="com.shida.entity.User"> insert into user (name,phone) values (#{name},#{phone}) </insert> <delete id="deleteUser" parameterType="int"> deletes from user where id = #{id} </delete> ``` **编写接口的实现类,在实现类中,我们去操作一下** ``` package com.shida.dao; import com.shida.entity.User; import org.mybatis.spring.SqlSessionTemplate; import java.util.List; /** * Description: * * @author jiaoqianjin * Date: 2021/3/15 22:27 **/ public class UserMapperImpl implements UserMapper { //sqlSession不用我们自己创建了,Spring来管理 private SqlSessionTemplate sqlSession; public void setSqlSession(SqlSessionTemplate sqlSession) { this.sqlSession = sqlSession; } /** *功能描述: 改造:先增加再删除 * @param * @return java.util.List<com.shida.entity.User> * @author jiaoqianjin * @date 2021/3/16 */ public List<User> selectUser() { User user = new User(4,"小明","123456"); UserMapper mapper = sqlSession.getMapper(UserMapper.class); mapper.addUser(user); mapper.deleteUser(4); return mapper.selectUser(); } /** *功能描述: 添加用户 * @param user 待添加的用户信息 * @return int * @author jiaoqianjin * @date 2021/3/16 */ public int addUser(User user) { UserMapper mapper = sqlSession.getMapper(UserMapper.class); return mapper.addUser(user); } /** *功能描述: 根据用户id删除用户 * @param id 用户id * @return int * @author jiaoqianjin * @date 2021/3/16 */ public int deleteUser(int id) { UserMapper mapper = sqlSession.getMapper(UserMapper.class); return mapper.deleteUser(id); } } ``` **测试一波** ``` @Test public void test(){ ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); UserMapper mapper = (UserMapper) context.getBean("userMapper"); List<User> user = mapper.selectUser(); System.out.println(user); } ``` **结果**   **报错:sql异常,delete写错了** **结果 :插入成功!** **没有进行事务的管理** **如果我们想让他们都成功才成功,有一个失败,就都失败,我们就应该需要******手动添加事务!**** **但是Spring给我们提供了事务管理,我们只需要配置即可;** ## **二、Spring中的事务管理** **Spring在不同的事务管理API之上定义了一个抽象层,使得开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。Spring支持编程式事务管理和声明式的事务管理。** ****编程式事务管理**** * **将事务管理代码嵌到业务方法中来控制事务的提交和回滚** * **缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码** ****声明式事务管理**** * **一般情况下比编程式事务好用。** * **将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。** * **将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理。** ****使用Spring管理事务,注意头文件的约束导入 : tx**** ``` xmlns:tx="http://www.springframework.org/schema/tx" http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> ``` ****事务管理器**** * **无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。** * **就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。** ****配置声明式事务**** ``` <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> ``` ****配置好事务管理器后我们需要去配置事务的通知**** ``` <!--配置事务通知--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--配置哪些方法使用什么样的事务,配置事务的传播特性--> <tx:method name="add" propagation="REQUIRED"/> <tx:method name="delete" propagation="REQUIRED"/> <tx:method name="update" propagation="REQUIRED"/> <tx:method name="search*" propagation="REQUIRED"/> <tx:method name="get" read-only="true"/> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> ``` ****spring事务传播特性:**** **事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为:** * **propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。** * **propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。** * **propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。** * **propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。** * **propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。** * **propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。** * **propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作** **Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。** ### ****1. 配置AOP实现**** **导入aop的头文件!** ``` xmlns:aop="http://www.springframework.org/schema/aop" http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd ``` ``` <!--配置aop织入事务--> <aop:config> <aop:pointcut id="txPointcut" expression="execution(* com.shida.dao.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/> </aop:config> ``` **完整的beans.xml** ``` <?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:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" 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"> <!--配置数据源:数据源有非常多,可以使用第三方的,也可使使用Spring的--> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis_01?useSSL=false&useUnicode=true&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!--配置事务通知--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--配置哪些方法使用什么样的事务,配置事务的传播特性--> <tx:method name="add" propagation="REQUIRED"/> <tx:method name="delete" propagation="REQUIRED"/> <tx:method name="update" propagation="REQUIRED"/> <tx:method name="search*" propagation="REQUIRED"/> <tx:method name="get" read-only="true"/> <tx:method name="*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <!--配置SqlSessionFactory--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!--关联Mybatis--> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:com/shida/dao/*.xml"/> </bean> <!--注册sqlSessionTemplate , 关联sqlSessionFactory--> <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"> <!--利用构造器注入--> <constructor-arg index="0" ref="sqlSessionFactory"/> </bean> <bean id="userMapper" class="com.shida.dao.UserMapperImpl"> <property name="sqlSession" ref="sqlSession"/> </bean> <!--配置aop织入事务--> <aop:config> <aop:pointcut id="txPointcut" expression="execution(* com.shida.dao..*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/> </aop:config> </beans> ``` ### **2. 注解实现** ``` <!--注解实现事务--> <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven> ``` ****@Transactional注解**** ****事务的隔离级别******:是指若干个并发的事务之间的隔离程度** ``` 1. @Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据(会出现脏读, 不可重复读) 基本不使用 2. @Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据(会出现不可重复读和幻读) 3. @Transactional(isolation = Isolation.REPEATABLE_READ):可重复读(会出现幻读) 4. @Transactional(isolation = Isolation.SERIALIZABLE):串行化 ``` ****事务传播行为******:如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为** ``` 1. TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。 2. TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 3. TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 5. TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 6. TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 7. TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行; 如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED ``` ***@Transactional属性*** | **属性** | **类型** | **描述** | | ---------------------------- | ---------------------------------------- | -------------------------------------------- | | **value** | **String** | **可选的限定描述符,指定使用的事务管理器** | | **propagation** | **enum: Propagation** | **可选的事务传播行为设置** | | **isolation** | **enum: Isolation** | **可选的事务隔离级别设置** | | **readOnly** | **boolean** | **读写或只读事务,默认读写** | | **timeout** | **int (in seconds granularity)** | **事务超时时间设置** | | **rollbackFor** | **Class对象数组,必须继承自Throwable** | **导致事务回滚的异常类数组** | | **rollbackForClassName** | **类名数组,必须继承自Throwable** | **导致事务回滚的异常类名字数组** | | **noRollbackFor** | **Class对象数组,必须继承自Throwable** | **不会导致事务回滚的异常类数组** | | **noRollbackForClassName** | **类名数组,必须继承自Throwable** | **不会导致事务回滚的异常类名字数组** | ***用法*** **@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。** **虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。** Last modification:June 3rd, 2021 at 10:54 am © 允许规范转载