巧用Spring事务TransactionSynchronizationManager避免长事务

需求

在业务开发的过程中,经常会遇到在事务做一些非操作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事务这一点,避免出现大事务

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值