文章目录
需求
在业务开发的过程中,经常会遇到在事务做一些非操作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 NamedThreadLocal<T> extends ThreadLocal<T> {
private final String name;
/**
* 必须给ThreadLocal设置名字
*/
public NamedThreadLocal(String name) {
Assert.hasText(name, "Name must not be empty");
this.name = name;
}
public String toString() {
return this.name;
}
}
TransactionSynchronizationManager
在此列出包含的属性和几个主要的方法
public abstract class TransactionSynchronizationManager {
// 日志操作句柄
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
// 事务资源,key类型为:DruidDataSourceWrapper,value: ConnectionHolder
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
// 事务同步器(就是待执行的钩子部分的逻辑)
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
// 当前的事务名称
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
// 当前事务只读状态
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
// 当前的事务的隔离级别
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
// 真正活跃的事务
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
/**
* 绑定数据库连接资源
*/
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = (Map)resources.get();
if (map == null) {
map = new HashMap();
resources.set(map);
}
Object oldValue = ((Map)map).put(actualKey, value);
if (oldValue instanceof ResourceHolder && ((ResourceHolder)oldValue).isVoid()) {
oldValue = null;
}
if (oldValue != null) {
throw new IllegalStateException("Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
} else {
if (logger.isTraceEnabled()) {
logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + Thread.currentThread().getName() + "]");
}
}
}
/**
* 注册同步器(钩子逻辑)
*/
public static void registerSynchronization(TransactionSynchronization synchronization) throws IllegalStateException {
Assert.notNull(synchronization, "TransactionSynchronization must not be null");
Set<TransactionSynchronization> synchs = (Set)synchronizations.get();
if (synchs == null) {
throw new IllegalStateException("Transaction synchronization is not active");
} else {
synchs.add(synchronization);
}
}
}
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事务这一点,避免出现大事务