菜鸟学Spring之——事务的传播特性,及在Spring中开启事务的过程

    技术2022-07-10  138

    事务的传播特性,以及在Spring中开启事务的过程

    Java中对数据库的事务管理可以依托Spring管理。那么要在那一层来开启事务呢?

    在service层,为啥不在dao层开启呢?是因为dao层是最终要执行数据库操作的地方,但是你能保证本次链接只执行一次数据库操作吗?比如你既要入库用户,又要入库用户信息,这时就要调用两个mapper里的两个方法,这样就会开启两次事务。而如果在service开启事务只需要开启一次事务就可以了。在Service中将事务打开,这个方法执行多少次数据库操作的动作都会用的这一次事务。

    我们可以这样理解,开启一次事务就等于打开了一次命令窗口。如果在dao层中开启事务,那么service中一个方法调用两个dao中的方法就会打开两个命令窗口。如果执行第一个操作成功后,执行第二个操作失败了,(比如插入老师用户名,然后再插入老师信息时出错了,这时按理说老师用户名也不能插入到数据库中的)这时需要回滚操作。但他只能回滚到第一次提交事务后的地方,而不能回滚到最初始的位置。(即老师用户名插入了,但是老师信息没有插入,不合理)而在Service层中就相当于打开了一次命令窗口,然后执行增删改查操作,这时如果有一个动作出错了,那么还可以回滚,且回滚到service最初的地方。(即老师用户名也没有被插入,回滚了。合理),所以事务应该开启在service层。

    所有的SqlSessionFactory、TransactionManager都关联到了DataSource。所以SqlSessionFactory和TransactionManager也关联着。即在Mybatis和DataSource之间加了一个TransactionManager,只要拿了一个链接就一定符合我们配置的事务管理。 (即sqlSessionFactory在创建的时候就一定符合TransactionManager的管理规则,因为他们两个拿个是同一个数据源,SQLSessionFactory是用数据库的,而TransactionManager是用来管理数据库的)

    原来,Spring控制了Mybatis直接链接到了DataSource(通过sqlSessionFactoryBean来链的)现在就通过一个Spring创建一个TransactionManager然后植入到Mybatis链接到数据库的中间。现在Mybatis只要从数据库中拿链接,就会先给他加入事务管理,然后再给Mybatis。

    接下来要考虑写代码时怎么在代码中开启关闭事务。

    怎么通过Spring来开启事务???

    事务开启两种方式:(配置文件型和注解型,先看看配置文件型)

    配置文件开启事务:

    <?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:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="com.lanou"></context:component-scan> <!--这个数据源出来TransactionManager用还有SqlSessionFactory用--> <bean id="dataSource" class="org.apache.ibatis.datasource.pooled.PooledDataSource"> <property name="driver" value="com.mysql.cj.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:3306/db_zbmanager?serverTimezone"></property> <property name="username" value="root"></property> <property name="password" value="zaq991221"></property> </bean> <!--这个sqlSessionFactory在创建的时候就一定符合transactionManager的管理规则--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> <property name="mapperLocations" value="classpath:mapper/*.xml"></property> </bean> <bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.lanou.mapper"></property> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> </bean> <!--配置数据元的事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!--对事物管理的时候需要有一个事务管理对象(transactionManager),且这个对象必须要放在Spring中,Spring对事务管理就是通过AOP增强来管理的--> <tx:advice id="interceptor" transaction-manager="transactionManager"> <tx:attributes> <!--针对哪些方法配置哪些事务特征--> <tx:method name="save*" rollback-for="java.lang.Exception" propagation="REQUIRED"/><!--这里配置的方法是为了让我们实现二次过滤和针对不同的方法开启不同的事务隔离级别和事务的传播特性--> <tx:method name="modify*" rollback-for="java.lang.Exception" propagation="REQUIRED"></tx:method> <tx:method name="remove*" rollback-for="java.lang.Exception" propagation="REQUIRED"></tx:method> <tx:method name="query*" rollback-for="java.lang.Exception" propagation="NOT_SUPPORTED"></tx:method> <!--一般isolation就选择默认的,因为数据库都是根据大部分用户的需求定制的--> <!--timeout:超时时间--> <!--rollback-for:指当遇到这个异常就回滚--> <!--no-rollback-for:如果出现这个异常那就不回滚--> <!--propagation:传播特性(七种)--> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="adv" expression="execution(* com.lanou.service..*.*(..))"/> <aop:advisor advice-ref="interceptor" pointcut-ref="adv"></aop:advisor> <!--将事务植入到对象的方法前面--> </aop:config> </beans>

    增删改都要在是事务下执行,因为这三个动作会对数据库表的内容进行修改。而查可以不需要

    在aop中只能针对所有的方法添加增强,但是不是所有的方法都属于同一套传播特性和隔离级别,所以要对aop的execution筛选出来的这些方法进行二次筛选(什么样的方法,符合什么条件的方法执行什么样的传播特性和隔离级别)。

    这时我们需要将写在service层的方法名按照一定的规则来命名,关于查询的都命名为queryXXX,关于插入的都命名为saveXXX,关于修改的都命名为modifyXXX,关于删除的都命名为removeXXX。

    tx:method标签的属性:

    rollback-for:指当遇到这个异常就回滚

    no-rollback-for:指当遇到这个异常就不回滚

    propagation:传播特性(七种)

    REQUIRED:必要的,指当前方法必须要在事务下执行,如果当前没有事务,就新建一个事务。这是最常见的选择。

    REQUIRES_NEW:指当前方法必须要在事务下执行,如果这时开启了一个事务,则将原来的事务挂起来。然后在新建一个事务

    上面两个的区别是:

    如果当前都没有事务,则都开启一个事务(两个的作用一模一样)。

    如果当前有事务,REQUIRES_NEW是把当前事务挂起,然后新开一个事务执行,而REQUIRED直接在原来的事务中执行。

    SUPPORTS:如果当前有事务,就以事务方式执行,如果当前没有事务,就以非事务方式执行。 相当于随便~

    NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

    MANDATORY:如果当前有事务,就以事务方式执行,如果当前没有事务,就抛出异常。

    NESTED:如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。

    NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

    演示事务的过程

    RegisterController

    @RestController @RequestMapping("/user") public class RegisterController { @Autowired IUserService service; @RequestMapping("/register.do") public void Register(User user) { service.saveUser(user); } }

    UserMapper

    @Repository public interface UserMapper { public User queryUser(@Param("user") User user, @Param("column") String column); public int saveUser(@Param("user") User user); }

    UserService

    @Service public class UserService implements IUserService { @Autowired UserMapper mapper; @Autowired SqlSessionFactory factory; @Autowired IUserInfoService service; @Override public User login(User user) { SqlSession sqlSession = factory.openSession(false); return mapper.queryUser(user,"username"); } @Override public boolean saveUser(User user) { int i = mapper.saveUser(user); int a = i/0;//故意设置抛出异常,开启事务后,如果抛出异常则会回滚 // service.queryByUserId(i); return i>=1; } }

    IUserService

    public interface IUserService { public User login(User user); public boolean saveUser(User user); }

    usermapper.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.lanou.mapper.UserMapper"> <!--User select(@Param("user") User user);--> <select id="queryUser" resultType="com.lanou.bean.User"> select * from tb_user where ${column}=#{user.username} and pwd=#{user.pwd} </select> <insert id="saveUser"> insert into tb_user(username, pwd) values (#{user.username}, #{user.pwd}) </insert> </mapper>

    这时在浏览器访问,注册一个用户。因为在事务中会抛出异常,所以插入的数据将会被回滚。即插不进去数据。

    zaq没有插进去,因为事务打开着。(如果没打开事务,则就算抛错也会插入进去)

    关掉事务,试着插入一条数据,看到数据插入进去了。

    同时,注意一下,插入进入的id值为8,为啥不是7呢,就是以为上一次我们测试的时候,打开了事务,插入一条数据,但是抛错了,发生了回滚,让本来插入进去的id为7的数据又被删除了。


    接下来做一个小测试

    在UserService的save方法中调用UserInfoService中的query方法(故意在这个方法中抛出异常),query方法的传播特性是SUPPORTS的,即外面事务调用他,他就开启事务

    UserService

    @Service public class UserService implements IUserService { @Autowired UserMapper mapper; @Autowired SqlSessionFactory factory; @Autowired IUserInfoService service; @Override public User login(User user) { SqlSession sqlSession = factory.openSession(false); return mapper.queryUser(user,"username"); } @Override public boolean saveUser(User user) { int i = mapper.saveUser(user); service.queryByUserId(i); return i>=1; } }

    UserInfoService

    @Service public class UserInfoService implements IUserInfoService { public boolean saveUserInfo() { return false; } @Override public UserInfo queryByUserId(int id) { int i = 1/0; return null; } }

    继续注册一个用户信息,发现,这个信息没有进入数据库。这是因为在Save方法中调用的另一个事务抛出了异常,save会发现这个异常,那么save方法的这个事务也会回滚。


    现在将query方法的传播特性设置为REQUIRES_NEW(挂起当前事务,执行一个新的事务),还是执行上面的流程,结果会是怎么样的。还是不能入库。

    在AService中(开启事务)调用BService(开启事务)里的方法,BService里的方法抛出异常,AService会不会回滚?会。

    如果将query的传播特性改为NOT_SUPPORTED(以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 ),还是上面的过程,用户还是不能入库,即AService还是会回滚。

    为什么呢,这是因为B确实是将A事务挂起了,在执行自己的这个方法时没有事务,但是不代表我这个方法的异常不能被A捕获到。归根结底,A事务会产生回滚的原因是B事务抛出的方法被A捕获到了,所以他会回滚。

    如果希望内部方法回滚,外部方法不会滚,怎么做?

    自定义一个异常,然后让B事务rollback-for自定义的异常,而A事务no-rollback-for自定义的异常。这时就可以了。

    事务的传播特性就是为了控制在切面切出的那层的所有切面点之间的交互过程(你抛出了异常我要不要回滚,我要不要在你的事务里执行,我要不要创建新事务,在新事务中执行还会这涉及到隔离级别) 。

    通过传播特性不仅仅控制的是回滚的过程,还控制的是数据的可见性的过程,如果我们在同一事务中,那么我们的数据是可见的。如果我们不在同一事务,那么你的事务中的数据对我来说是不可见的。

    非事务的时候读的数据(表)是当前的库的表,如果是事务的,读的就是快照(类似于备份),所以传播特性特指的就是数据的传播特性 ,他和数据库没有关系,和数据库有关系的是隔离级别,隔离级别规定的是两个事物的数据可见性,传播特性规定的就是在Java代码中两个或多个方法之间的事务的关系,是两个方法共用一个事务,还是一人一个事务。

    如果两个事务是嵌套关系(父子),那么内层事务可以读到外层事务的数据,就算事务的隔离级别是RR的,外层事务修改了数据,没有提交,内层事务也能读到修改的事务,因为内层事务读到数据不是库中的数据,而是外层事务的快照。如果内层事务不提交,那么外层事务就不能提交

    Spring对事务的管理是通过AOP来管理的,Spring配置事务配置的包叫tx,他在配置事务时所用到的是tx包中的DataSourceTransactionManger事务管理器

    下面来将注解式编程

    配置文件式变成的唯一缺点就是方法的命名要按照一定规范

    <?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:context="http://www.springframework.org/schema/context" 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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="com.lanou"></context:component-scan> <!--这个数据源出来TransactionManager用还有SqlSessionFactory用--> <bean id="dataSource" class="org.apache.ibatis.datasource.pooled.PooledDataSource"> <property name="driver" value="com.mysql.cj.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://localhost:3306/db_zbmanager?serverTimezone"></property> <property name="username" value="root"></property> <property name="password" value="zaq991221"></property> </bean> <!--这个sqlSessionFactory在创建的时候就一定符合transactionManager的管理规则--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> <property name="mapperLocations" value="classpath:mapper/*.xml"></property> </bean> <bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.lanou.mapper"></property> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> </bean> <!--配置数据元的事务管理器--> <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> <!--开启了注解扫描--> </beans> @Service @Transactional public class UserService implements IUserService { @Autowired UserMapper mapper; @Autowired SqlSessionFactory factory; @Autowired IUserInfoService service; @Override public User login(User user) { SqlSession sqlSession = factory.openSession(false); return mapper.queryUser(user,"username"); } @Override @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class) //写一个方法就要写一个Transactional,其实没有配置文件方式的方便 public boolean saveUser(User user) { int i = mapper.saveUser(user); int x = i / 0 ; return i>=1; } }
    Processed: 0.011, SQL: 9