前言
最近有个网友问了我一个问题:零碎中大事务问题要如何解决?
正好前段时间我在公司解决过这个问题,咱们过后因为我的项目初期工夫比拟缓和,为了疾速实现业务性能,疏忽了零碎局部性能问题。我的项目顺利上线后,专门抽了一个迭代的工夫去解决大事务问题,目前曾经优化实现,并且顺利上线。现给大家总结了一下,咱们过后应用的一些解决办法,以便大家被雷同问题困扰时,能够参考一下。
大事务引发的问题
在分享解决办法之前,先看看零碎中如果呈现大事务可能会引发哪些问题
从上图能够看出如果零碎中呈现大事务时,问题还不小,所以咱们在理论我的项目开发中应该尽量避免大事务的状况。如果咱们已有零碎中存在大事务问题,该如何解决呢?
解决办法
少用 @Transactional 注解
大家在理论我的项目开发中,咱们在业务办法加上 @Transactional 注解开启事务性能,这是十分广泛的做法,它被称为申明式事务。
局部代码如下:
@Transactional(rollbackFor=Exception.class)
public void save(User user) {doSameThing...}
然而,我要说的第一条是:少用 @Transactional 注解。
为什么?
咱们晓得 @Transactional 注解是通过 spring 的 aop 起作用的,然而如果使用不当,事务性能可能会生效。如果凑巧你经验不足,这种问题不太好排查。至于事务哪些状况下会生效,能够参考我之前写的《spring 事务的这 10 种坑,你稍不留神可能就会踩中!!!》这篇文章。
@Transactional 注解个别加在某个业务办法上,会导致整个业务办法都在同一个事务中,粒度太粗,不好管制事务范畴,是呈现大事务问题的最常见的起因。
那咱们该怎么办呢?
能够应用编程式事务,在 spring 我的项目中应用 TransactionTemplate 类的对象,手动执行事务。
局部代码如下:
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {transactionTemplate.execute((status) => {
doSameThing...
return Boolean.TRUE;
})
}
从下面的代码中能够看出,应用 TransactionTemplate 的编程式事务性能本人灵便管制事务的范畴,是防止大事务问题的首选方法。
当然,我说少应用 @Transactional 注解开启事务,并不是说肯定不能用它,如果我的项目中有些业务逻辑比较简单,而且不常常变动,应用 @Transactional 注解开启事务开启事务也不妨,因为它更简略,开发效率更高,然而千万要小心事务生效的问题。
将查问 (select) 办法放到事务外
如果呈现大事务,能够将查问 (select) 办法放到事务外,也是比拟罕用的做法,因为个别状况下这类办法是不须要事务的。
比方呈现如下代码:
@Transactional(rollbackFor=Exception.class)
public void save(User user) {queryData1();
queryData2();
addData1();
updateData2();}
能够将 queryData1 和 queryData2 两个查询方法放在事务外执行,将真正须要事务执行的代码才放到事务中,比方:addData1 和 updateData2 办法,这样就能无效的缩小事务的粒度。
如果应用 TransactionTemplate 的编程式事务这里就十分好批改。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {queryData1();
queryData2();
transactionTemplate.execute((status) => {addData1();
updateData2();
return Boolean.TRUE;
})
}
然而如果你切实还是想用 @Transactional 注解,该怎么拆分呢?
public void save(User user) {queryData1();
queryData2();
doSave();}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {addData1();
updateData2();}
这个例子是十分经典的谬误,这种间接办法调用的做法事务不会失效,给正在坑中的敌人提个醒。因为 @Transactional 注解的申明式事务是通过 spring aop 起作用的,而 spring aop 须要生成代理对象,间接办法调用应用的还是原始对象,所以事务不会失效。
有没有方法解决这个问题呢?
1. 新加一个 Service 办法
这个办法非常简单,只须要新加一个 Service 办法,把 @Transactional 注解加到新 Service 办法上,把须要事务执行的代码移到新办法中。具体代码如下:
@Servcie
publicclass ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
publicclass ServiceB {@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {addData1();
updateData2();}
}
2. 在该 Service 类中注入本人
如果不想再新加一个 Service 类,在该 Service 类中注入本人也是一种抉择。具体代码如下:
@Servcie
publicclass ServiceA {
@Autowired
prvate ServiceA serviceA;
public void save(User user) {queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {addData1();
updateData2();}
}
可能有些人可能会有这样的疑难:这种做法会不会呈现循环依赖问题?
其实 spring ioc 外部的三级缓存保障了它,不会呈现循环依赖问题。如果你想进一步理解循环依赖问题,能够看看我之前文章《spring 解决循环依赖为什么要用三级缓存?》。
3. 在该 Service 类中应用 AopContext.currentProxy()获取代理对象
下面的办法 2 的确能够解决问题,然而代码看起来并不直观,还能够通过在该 Service 类中应用 AOPProxy 获取代理对象,实现雷同的性能。具体代码如下:
@Servcie
publicclass ServiceA {public void save(User user) {queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {addData1();
updateData2();}
}
事务中防止近程调用
咱们在接口中调用其余零碎的接口是不能防止的,因为网络不稳固,这种近程调的响应工夫可能比拟长,如果近程调用的代码放在某个事物中,这个事物就可能是大事务。当然,近程调用不仅仅是指调用接口,还有包含:发 MQ 音讯,或者连贯 redis、mongodb 保留数据等。
@Transactional(rollbackFor=Exception.class)
public void save(User user) {callRemoteApi();
addData1();}
近程调用的代码可能耗时较长,切记肯定要放在事务之外。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {callRemoteApi();
transactionTemplate.execute((status) => {addData1();
return Boolean.TRUE;
})
}
有些敌人可能会问,近程调用的代码不放在事务中如何保证数据一致性呢?这就须要建设:重试 + 弥补机制,达到数据最终一致性了。
事务中防止一次性解决太多数据
如果一个事务中须要解决的数据太多,也会造成大事务问题。比方为了操作不便,你可能会一次批量更新 1000 条数据,这样会导致大量数据锁期待,特地在高并发的零碎中问题尤为显著。
解决办法是分页解决,1000 条数据,分 50 页,一次只解决 20 条数据,这样能够大大减少大事务的呈现。
非事务执行
在应用事务之前,咱们都应该思考一下,是不是所有的数据库操作都须要在事务中执行?
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {transactionTemplate.execute((status) => {addData();
addLog();
updateCount();
return Boolean.TRUE;
})
}
下面的例子中,其实 addLog 减少操作日志办法 和 updateCount 更新统计数量办法,是能够不在事务中执行的,因为操作日志和统计数量这种业务容许大量数据不统一的状况。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {transactionTemplate.execute((status) => {addData();
return Boolean.TRUE;
})
addLog();
updateCount();}
当然大事务中要甄别出哪些办法能够非事务执行,其实没那么容易,须要对整个业务梳理一遍,能力找出最正当的答案。
异步解决
还有一点也十分重要,是不是事务中的所有办法都须要同步执行?咱们都晓得,办法同步执行须要期待办法返回,如果一个事务中同步执行的办法太多了,势必会造成等待时间过长,呈现大事务问题。
看看上面这个列子:
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {transactionTemplate.execute((status) => {order();
delivery();
return Boolean.TRUE;
})
}
order 办法用于下单,delivery 办法用于发货,是不是下单后就肯定要马上发货呢?
答案是否定的。
这里发货性能其实能够走 mq 异步解决逻辑。
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {transactionTemplate.execute((status) => {order();
return Boolean.TRUE;
})
sendMq();}
总结
自己从网友的一个问题登程,联合本人理论的工作教训分享了解决大事务的 6 种方法:
- 少用 @Transactional 注解
- 将查问 (select) 办法放到事务外
- 事务中防止近程调用
- 事务中防止一次性解决太多数据
- 非事务执行
- 异步解决
最初说一句(求关注,不要白嫖我)
如果这篇文章对您有所帮忙,或者有所启发的话,帮忙扫描下发二维码关注一下,或者点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、工夫治理有超赞的粉丝福利,另外回复:加群,能够跟很多大厂的前辈交换和学习。
本文由博客群发一文多发等经营工具平台 OpenWrite 公布