前言
传播行为是指方法之间调用事务采取的策略问题。在大多数情况下,我们会人为数据库事务要么全部成功,要么全部失败。现实中也许会有特殊情况。例如,执行一个批量程序,他会处理很多交易,绝大部分交易都是可以正常进行的,但是也有极少数的交易因为特殊原因不能完成而发生异常,这时,我们不应该因为极少数的交易不能完成而回滚批量任务调用的其他交易,使得那些本能完成的交易也变的不能完成了。此时,我们真实的需求是,在一个批量执行的过程中,调用多个交易时,如果一些交易出现异常,只是回滚那些出现异常的交易,而不是整个批量任务,这样就能够使得那些没有问题的即哦阿姨可以顺利进行,而有问题的交易则不做任何事情。如图
在Spring中一个方法调用另一个方法,可以让事务采用不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。这么讲述还是有点抽象,我们呢回到上图,图中,批量事务我们称之为当前事务,当他调用单个交易时,称呼单个交易为子方法,当方法调用子方法时,让每个子方法不再当前事务中执行,而是创建一个新的事务去执行子方法,我们就说当前方法调用子方法的传播行为为新建事务。此外,还可能让子方法在无事务,独立事务中执行,这些完全取决于你的业务需求
1. 传播行为的定义
在Spring事务机制中对数据库存在7种传播行为,他是通过枚举类Propagation定义的。下面先来研究他的代码,代码如下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.transaction.annotation;
public enum Propagation {
/**
* 需要事务,他是默认传播行为,如果当前存在事务,就沿用当前事务。否则新建一个事务运行子方法
*/
REQUIRED(0),
/**
*支持事务,如果当前存在事务,就沿用当前事务,如果不存在则继续使用无事务的形式运行子方法
*
*/
SUPPORTS(1),
/**
*必须使用事务,如果当前没有事务,则会抛出异常,如果存在当前事务,就沿用当前事务
*/
MANDATORY(2),
/**
*无论当前事务是否存在,都会创建新事务运行方法,这样新事务就可以拥有新的锁和隔离级别的特性, 与当前事务相互独立
*/
REQUIRES_NEW(3),
/**
*不支持事务,当前存在事务时,将挂起事务,运行方法
*/
NOT_SUPPORTED(4),
/**
*不支持事务,如果当前方法存在事务,则会抛出异常,否则继续以无事务机制运行
*/
NEVER(5),
/**
*在当前方法调用子方法时,如果子方法发生异常,只是回滚子方法执行过得SQL而不是回滚当前方法的事务
*/
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
当前代码中加入的中文注释解释了每一种传播行为的定义。传播行为一共分为七种,但是常用的只有代码清单加粗的三种,REQUIRED,REQUIRES_NEWS,NESTED其他的使用率比较低。基于实用的原则,本书只是讨论这三种传播行为。
2. 测试传播行为
继续沿用以前章节的代码来测试REQUIRED,REQUIRES_NEW,NESTED这三种常用的传播行为用来批量测试更新.给出接口
package cn.hctech2006.boot.bootmybatis.service;
import cn.hctech2006.boot.bootmybatis.bean.SysRole;
import java.util.List;
public interface SysRoleBatchService {
int saveRoles(List<SysRole> records);
}
然后给出他的实现类
package cn.hctech2006.boot.bootmybatis.service.impl;
@Service
public class SysRoleBatchServiceImpl implements SysRoleBatchService {
@Autowired
private SysRoleMapper sysRoleMapper;
@Autowired
private SysRoleService sysRoleService;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED
)
public int saveRoles(List<SysRole> records) {
int count = 0;
for(SysRole sysRole: records){
//调用子方法,将使用@Tranactional定义的传播行为
count += sysRoleService.save(sysRole);
}
return count;
}
}
这里将调用insert方法,只是insert方法没有设置传播行为。按照我们之前的论述,他会采用REQUIRED,也就是沿用当前的事务,所以他将与saveRoles使用同一个事务,下面给出控制器(RoleController)的基础上新增一个方法测试他
@RequestMapping("/insertRoles")
public Map<String, Object> insertRoles(){
SysRole sysRole1 = new SysRole();
sysRole1.setCreateBy("lidengyin2");
sysRole1.setCreateTime(new Date());
sysRole1.setDelFlag((byte)0);
sysRole1.setName("asa33");
sysRole1.setRemark("ssss");
sysRole1.setLastUpdateBy("lidengyin");
sysRole1.setLastUpdateTime(new Date());
SysRole sysRole2 = new SysRole();
sysRole2.setCreateBy("lidengyin3");
sysRole2.setCreateTime(new Date());
sysRole2.setDelFlag((byte)0);
sysRole2.setName("asa33");
sysRole2.setRemark("ssss");
sysRole2.setLastUpdateBy("lidengyin");
sysRole2.setLastUpdateTime(new Date());
List<SysRole> sysRoles = new ArrayList<>();
sysRoles.add(sysRole1);
sysRoles.add(sysRole2);
//结果会回填主键返回插入条数
int inserts = sysRoleService.saveRoles(sysRoles);
Map<String, Object> result = new HashMap<>();
result.put("success", inserts > 0);
result.put("Role", sysRoles);
return result;
}
这样我们就可以实现用户的批量插入了,在浏览器地址中输入:http://localhost:8242/mybatis/insertRoles,就可以观察后台日志
我们可以看到都是在沿用已经存在的当前事务
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@69749d0a] from current transaction
接着我们把saveRoles的注解改为下面部分
@Override
@Transactional(isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRES_NEW
)
public int saveRoles(List<SysRole> records) {
int count = 0;
for(SysRole sysRole: records){
//调用子方法,将使用@Tranactional定义的传播行为
count += sysRoleMapper.insert(sysRole);
}
return count;
}
再次进行测试,得到如下日志
Mapped to cn.hctech2006.boot.bootmybatis.controller.RoleController#insertRoles()
#创建当前方法事务(SysRoleBatchServiceImpl的saveRoles方法)
Creating new transaction with name [cn.hctech2006.boot.bootmybatis.service.impl.SysRoleBatchServiceImpl.saveRoles]: PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED
Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1c120499] for JDBC transaction
#设置当前事务的隔离级别为读写提交
Changing isolation level of JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1c120499] to 2
Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1c120499] to manual commit
#创建子方法事务(SysRoleServiceImpl的save)同时挂起当前事务
Suspending current transaction, creating new transaction with name [cn.hctech2006.boot.bootmybatis.service.impl.SysRoleServiceImpl.save]
TLSHandshake: 172.17.0.1:3306, TLSv1.2, TLS_RSA_WITH_AES_256_CBC_SHA256, 2126950672
Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] for JDBC transaction
#设置子事务隔离级别为读写提交
Changing isolation level of JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] to 2
Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] to manual commit
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@953a72d]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] will be managed by Spring
==> Preparing: insert into sys_role (id, name, remark, create_by, create_time, last_update_time, last_update_by, del_flag) values (?, ?, ?, ?, ?, ?, ?, ?)
==> Parameters: null, uiy6fft2122ytts4(String), ssss(String), lidengyin12(String), 2020-04-15 10:46:13.864(Timestamp), 2020-04-15 10:46:13.864(Timestamp), lidengyin23(String), 0(Byte)
<== Updates: 1
{conn-10002, pstmt-20000} enter cache
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@953a72d]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@953a72d]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@953a72d]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@953a72d]
Initiating transaction commit
#独立提交
Committing JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca]
Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] after transaction
#内部事务完成后,回复被挂起的事务
Resuming suspended transaction after completion of inner transaction
#创建子方法事务(SysRoleServiceImpl的save)同时挂起当前事务
Suspending current transaction, creating new transaction with name [cn.hctech2006.boot.bootmybatis.service.impl.SysRoleServiceImpl.save]
Acquired Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] for JDBC transaction
#设置子事务隔离级别为读写提交
Changing isolation level of JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] to 2
Switching JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] to manual commit
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70224101]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] will be managed by Spring
==> Preparing: insert into sys_role (id, name, remark, create_by, create_time, last_update_time, last_update_by, del_flag) values (?, ?, ?, ?, ?, ?, ?, ?)
==> Parameters: null, a1232dsdw23643(String), ssss(String), lidengyin213(String), 2020-04-15 10:46:13.864(Timestamp), 2020-04-15 10:46:13.864(Timestamp), lidengyin(String), 0(Byte)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70224101]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70224101]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70224101]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70224101]
Initiating transaction commit
#独立提交
Committing JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca]
Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1a9bfcca] after transaction
Resuming suspended transaction after completion of inner transaction
Initiating transaction commit
Committing JDBC transaction on Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1c120499]
Releasing JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1c120499] after transaction
Using 'application/json;q=0.8', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [application/json, application/*+json, application/json, application/*+json]
Writing [{Role=[SysRole [Hash = 1251462742, id=36, name=uiy6fft2122ytts4, remark=ssss, createBy=lidengyin12, (truncated)...]
Completed 200 OK
在日志中,为了让读者更好的理解,加入了注解的文字。从日志中可以看出,他启用了新的数据库事务去运行每一个save方法,并且独立提交,这样就完全脱离了原有事务的管控,没一个事务都可以拥有自己独立的隔离级别和锁
最后我们再来测试一下NESTED隔离级别。他是一个如果子方法回滚而当前事务不回滚的方法,于是我们把代码清单修改为如下,进行测试
大部分数据库,一段SQL语句中都可以设置一个标志位,然后代码执行时如果有异常,只是回滚到这个标志位的数据状态,而不会让这个标志位之前的代码也回滚。这个标志位,在数据库的概念中称之为保存点(savepoint)。从日志中可以看出,Spring为我们生成了nested事务,而从其日志信息中可以看到保存点的释放,可见Spring也是使用保存点技术来完成让子事务回滚而不至于使得当前事务回滚的工作。注意,并不是所有的数据库都支持保存点技术,因此Spring内部有这样的规定:当数据库事务支持保存点技术时,就开启保存点技术,如果不能支持,就新建一个事务,去运行你的代码,即等价于REQUIRES_NEW传播行为
NESTED传播行为和REQUIRES_NEW还是有区别的。NESTED传播行为为会沿用当前事务的隔离级别和锁等特性。而REQUIRES_NEW则可以拥有自己独立的隔离级别和锁登特性,这是在应用中需要注意的地方