事务模板 VS声明式事务
一、 案例
我们首先看几个简单的案例。
1. 声明式事务示例一
@Transactional
public voidaddData(long orderId,Object orderDetailDTO){
//查询操作
OrderDO orderDO = orderManager.getById(orderId);
// 组装数据操作
OrderDetailDO orderDetailDO= OrderDetailHelper.create(orderDO, orderDetailDTO);
//db写操作
orderManager.update(orderDO);
orderDetailManager.insert(orderDetailDO);
}
这是一个数据组装和db写操作都放在同一个事务中的示例。
2. 声明式事务示例二
public voidaddDataV2(long orderId,Object orderDetailDTO){
//查询操作
OrderDO orderDO = orderManager.getById(orderId);
// 组装数据操作
OrderDetailDO orderDetailDO= OrderDetailHelper.create(orderDO, orderDetailDTO);
this.addDataV2DoDB(orderDO, orderDetailDO);
}
@Transactional
public voidaddDataV2DoDB(OrderDO orderDO, OrderDetailDOorderDetailDO){
//db写操作
orderManager.update(orderDO);
orderDetailManager.insert(orderDetailDO);
}
这是一个数据封装和db写操作分在两个方法中的示例,从代码上可以看到事务时间更短。
3. 声明式事务示例三
@Transactional
public voidaddDataV3(long orderId,Object orderDetailDTO){
//查询操作
OrderDO orderDO = orderManager.getById(orderId);
// 组装数据操作
OrderDetailDO orderDetailDO= OrderDetailHelper.create(orderDO, orderDetailDTO);
//db写操作
orderManager.update(orderDO);
orderDetailManager.insert(orderDetailDO);
orderManager.callback(orderDO);
}
和示例一唯一的差别就是事务执行完毕以后加了一个回调操作。
4. 事务模板示例一
public boolean addData(finalOrderDO orderDO, finalOrderDetailDO orderDetailDO){
Boolean executeResult = transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public BooleandoInTransaction(TransactionStatus status) {
try {
//插入order
long orderId = orderDao.insert(orderDO);
if (orderId <= 0 ) {
status.setRollbackOnly();
returnfalse;
}
//插入order detail
long orderDetailId = orderDetailDao.insert(orderDetailDO);
if (orderDetailId <= 0 ) {
status.setRollbackOnly();
returnfalse;
}
} catch (Exception e) {
status.setRollbackOnly();
return false;
}
return true;
}
});
return executeResult == null ? false : executeResult ;
}
这是一个使用TransactionTemplate实现事务的示例。可以看到,需要我们手动控制事务的回滚,代码逻辑比较复杂。
二、 案例分析
看了上面的四个示例,估计很多小伙伴很容易得出事务模板是最不应该采用的事务方式,代码繁琐。但我这边给出的结论是,应该放弃声明式事务,使用事务模板。接下来分析一下原因。
1. 声明式事务
1) 示例代码分析
a) “声明式事务示例一”的问题:事务范围过大,事务时间越长,占用的资源越多,性能越差,事务执行失败回滚的概率越高。
b) “声明式事务示例二”的问题:事务不会生效。声明式事务依赖于Transactional注解和TransactionInterceptor拦截器。拦截器生效的前提是调用者是代理,而不是当前类本身。
c) “声明式事务示例三”的问题:“声明式事务示例一”的所有问题,在此示例中都有;另外,业务层可能会进行RPC、HTTP请求,如果多次rpc中部分请求失败,事务会滚也不能保证数据一致性。
2) 声明式事务普遍性问题
声明式事务最大的优势是spring做了过多的封装,所以使用起来很简单,代码很简化;但这在使用中也经常会让我们的代码埋下一堆的坑。下面列举平常编码时会遇到的几个例子。
a) 绝大多数我们通过异常来回滚事务,但是如果底层模块捕获异常导致上层无法捕获异常,或者底层接口执行失败时不抛出异常(如update、delete时返回影响的记录数而不抛出异常),此时事务中的代码执行结果将会和我们的预期不一致。之前和不少人沟通过这个问题,但是不少人告诉我通过统一代码规范,来保证上层可以获取到异常,达到事务可以正常回滚的目的,但是从代码上看到的结果基本上都没有做到这一点。
b) 就我这么多年接触的平时喜欢使用@Transactional来做事务的开发同学而言,绝大多数都不清楚事务的5种隔离级别与7种传播属性他们是如何相互影响的。
c) 使用声明式事务的同学一般会将业务逻辑依平铺的方式写下来,举个例子来说,如果某个业务会在3个表中各生成一条记录,那写出来的程序很容易就出现生成一条记录插入一条记录的情况,然后在最外层包一个大事务。这容易造成逻辑不清晰,耦合严重,性能低下的问题,而且很容易在这个事务中写rpc、http调用,即出现“声明式事务示例三”的问题。
注意:上面列举的这些问题,在测试期间都不易发现问题,但是出问题后定位起来也比较麻烦。
2. 事务模板
1) 示例代码分析
从示例中可以看到使用事务模板的方式代码不够简化,但这种不够简化的代码正是我们验证每一步执行结果,判断和我们的预期是否一致引起的,这正是解决声明式事务不可靠(通过异常回滚事务)的关键,为此我们多写几行代码,我觉得是非常值得的事情。
保证数据正确性可以将我们从频繁定位线上问题、修复线上数据的泥潭中解救出来至关重要的一点。
事务模板不能解决事务中调用RPC、HTTP的问题,但是使用事务模板这种方式要求我们提前查询、计算好所有数据,然后放在一个事务模板中,一次更新掉,所以使用事务模板的性能会更好,因为提前已经将数据计算好了,所以事务会尽可能的短,性能会更好,会极大的避免事务中出现RPC、HTTP调用
我一向的观点是:数据是业务的生命,如果数据准确性不能保证,我们提供再多的功能也是没有什么意义的。如果实现成本差别不是非常大,应该尽一切可能保证数据可靠性。
另外,在传统的单系统的业务中,因为所有的表都在同一个库中,而且基本没有RPC、HTTP调用,所以使用声明式事务很多时候不会有问题,但是在SOA架构中,则绝大多数场景都会有问题。
三、 传播属性与隔离级别
1. 传播属性
REQUIRED |
支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。 |
SUPPORTS |
支持当前事务,如果当前没有事务,就以非事务方式执行。 |
MANDATORY |
支持当前事务,如果当前没有事务,就抛出异常。 |
REQUIRES_NEW |
新建事务,如果当前存在事务,把当前事务挂起。 |
NOT_SUPPORTED |
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
NEVER |
以非事务方式执行,如果当前存在事务,则抛出异常。 |
NESTED |
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED类似的操作 |
REQUIRED为默认的传播属性。
2. 隔离级别
1) 名词解释
a) 脏读:
针对未提交数据。
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
b) 不可重复读:
针对其他提交前后,读取数据本身的对比。
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容)
c) 幻读
针对其他提交前后,读取数据条数的对比。
幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。在Read Uncommitted隔离级别下, 不管事务2的插入操作是否提交,事务1在插入操作之前和之后执行相同的查询,取得的结果集是不同的,所以,ReadUncommitted同样无法避免幻读的问题。
2) 隔离级别
DEFAULT |
使用数据库默认的事务隔离级别 |
READ_UNCOMMITTED |
这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。 |
READ_COMMITTED |
保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。 |
REPEATABLE_READ |
这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。 |
SERIALIZABLE |
这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。 |
3) 影响
隔离级别 | 脏读 | 不可重复读 | 幻读 |
READ_UNCOMMITTED | √ | √ | √ |
READ_COMMITTED | × | √ | √ |
REPEATABLE_READ | × | × | √ |
SERIALIZABLE | × | × | × |
√: 可能出现 ×:不会出现