- 需求
- 案例
- 单元测试
- Service实现类
- 源码解析
- NamedThreadLocal
- TransactionSynchronizationManager
- TransactionSynchronization
- 踩坑
- afterCompletion的坑
- 优化
- 总结
在业务开发的过程中,经常会遇到在事务做一些非操作DB的逻辑,如:下单完成之后给发送MQ消息给会员增加积分,更新数据之后即时更新缓存。这些操作放到事务内部就会导致耗时,事务时间长,持有数据库连接的时间长,影响数据库的性能。因为这些非DB操作可以不在ACID内,我们可以通过TransactionSynchronizationManager把它解耦出来。
案例 单元测试@Test
public void updateAdminById() {
umsAdminService.updateAdminById(7L);
}
Service实现类
使用姿势很简单,只需在事务的方法中,通过TransactionSynchronizationManager.registerSynchronization()来注册同步器,并覆写其中的方法来实现我们的钩子逻辑。
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAdminById(Long id) {
UmsAdmin umsAdminUpdate = new UmsAdmin();
umsAdminUpdate.setUsername("Lfc");
umsAdminUpdate.setId(id);
int updateResult = commonService.getUmsAdminMapper().updateById(umsAdminUpdate);
System.out.println("更新结果: " + updateResult);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
System.out.println("事务提交完成,正在做其他耗时操作,当前时间戳: " + Calendar.getInstance().getTimeInMillis());
}
@Override
public void afterCompletion(int status) {
if (status == STATUS_COMMITTED) {
System.out.println("已提交事务,准备执行Feign调用");
} else if(status == STATUS_ROLLED_BACK) {
System.out.println("已回滚");
} else {
System.out.println("未知状态");
}
}
});
}
源码解析
NamedThreadLocal
必须有名字的ThreadLocal
public class NamedThreadLocalTransactionSynchronizationManagerextends ThreadLocal { private final String name; public NamedThreadLocal(String name) { Assert.hasText(name, "Name must not be empty"); this.name = name; } public String toString() { return this.name; } }
在此列出包含的属性和几个主要的方法
public abstract class TransactionSynchronizationManager {
// 日志操作句柄
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
// 事务资源,key类型为:DruidDataSourceWrapper,value: ConnectionHolder
private static final ThreadLocal
TransactionSynchronization
public interface TransactionSynchronization extends Flushable {
int STATUS_COMMITTED = 0;
int STATUS_ROLLED_BACK = 1;
int STATUS_UNKNOWN = 2;
default void suspend() {
}
default void resume() {
}
default void flush() {
}
default void beforeCommit(boolean readOnly) {
}
default void beforeCompletion() {
}
default void afterCommit() {
}
default void afterCompletion(int status) {
}
}
踩坑
afterCompletion的坑
在Spring的AbstractPlatformTransactionManager中,对commit处理的代码如下:
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;
try {
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked = true;
boolean globalRollbackOnly = false;
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
globalRollbackOnly = status.isGlobalRollbackOnly();
}
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Releasing transaction savepoint");
}
status.releaseHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
doCommit(status);
}
// Throw UnexpectedRollbackException if we have a global rollback-only
// marker but still didn't get a corresponding exception from commit.
if (globalRollbackOnly) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
}
// Trigger afterCommit callbacks, with an exception thrown there
// propagated to callers but the transaction still considered as committed.
try {
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}
}
finally {
cleanupAfterCompletion(status);
}
}
倒数第二个执行逻辑,当执行完所有的代码之后就会执行我们的cleanupAfterCompletion,而我们的归还数据库连接也在这段代码之中,这样就导致了我们获取数据库连接变慢。
优化- 非DB操作不放事务之内:对于一些简单的逻辑可以提取,但是对于一些复杂的逻辑,比如事务的嵌套,嵌套里面调用了afterCompletion,这样做会增大很多工作量,并且很容易出现问题。
- @Async异步执行:提升数据库连接池归还速度,这种适合于注册afterCompletion时写在事务最后的时候,直接将需要做的放在其它线程去做。但是如果注册afterCompletion的时候出现在我们事务之间,比如嵌套事务,就会导致我们要做的后续业务逻辑和事务并行。
- 模仿Spring事务回调注册,实现新的注解:上面两种方法都有各自的弊端,所以最后我们采用了这种方法,实现了一个自定义注解@MethodCallBack,再使用事务的上面都打上这个注解,然后通过类似的注册代码进行。
大事务产生的原因无非一下几种:
- 数据操作得很多:比如在一个事务里面插入了很多数据,那么这个事务执行时间自然就会变得很长。
- 锁的竞争大:当所有的连接都同时对同一个数据进行操作,那么就会出现排队等待,事务时间自然就会变长。
- 事务中包含其他非DB操作:比如一些RPC请求,有些人说我的RPC很快的,不会增加事务的运行时间,但是RPC请求本身就是一个不稳定的因素,受很多因素影响,网络波动,下游服务响应缓慢,如果这些因素一旦出现,就会有大量的事务时间很长,有可能导致Mysql挂掉,从而引起雪崩。
为了避免大事务,我们在日常编码的过程中,应该注意就避免以上几种情况即可。
参考文档:
注意Spring事务这一点,避免出现大事务



