注意Spring事务这一点,避免出现大事务

背景

本篇文章主要分享压测的(高并发)时候发现的一些问题。以前的两篇文章已经讲述了在高并发的状况下,消息队列和数据库链接池的一些总结和优化,有兴趣的能够在个人公众号中去翻阅。废话很少说,进入正题。sql

事务,想必各位CRUD之王对其并不陌生,基本上有多个写请求的都须要使用事务,而Spring对于事务的使用又特别的简单,只须要一个@Transactional注解便可,以下面的例子:数据库

@Transactional
    public int createOrder(Order order){
        orderDbStorage.save(order);
        orderItemDbStorage.save(order.getItems());
        return order.getId();
    }

在咱们建立订单的时候, 一般须要将订单和订单项放在同一个事务里面保证其知足ACID,这里咱们只须要在咱们建立订单的方法上面写上事务注解便可。缓存

事务的合理使用

对于上面的建立订单的代码,若是如今须要新增一个需求,在建立订单以后发送一个消息到消息队列或者调用一个RPC,你会怎么作呢?不少同窗首先会想到,直接在事务方法里面进行调用:网络

@Transactional
    public int createOrder(Order order){
        orderDbStorage.save(order);
        orderItemDbStorage.save(order.getItems());
        sendRpc();
        sendMessage();
        return order.getId();
    }

这种代码在不少人写的业务中都会出现,事务中嵌套rpc,嵌套一些非DB的操做,通常状况下这么写的确也没什么问题,一旦非DB写操做出现比较慢,或者流量比较大,就会出现大事务的问题。因为事务的一直不提交,就会致使数据库链接被占用。这个时候你可能会问,我扩大点数据库链接不就好了吗,100个不行就上1000个,在上篇文章已经讲过数据库链接池大小依然会影响咱们数据库的性能,因此,数据库链接并非想扩多少扩多少。多线程

那咱们应该怎么对其进行优化呢?在这里能够仔细想一想,咱们的非db操做,实际上是不知足咱们事务的ACID的,那么干吗要写在事务里面,因此这里咱们能够将其提取出来。并发

public int createOrder(Order order){
        createOrderService.createOrder(order);
        sendRpc();
        sendMessage();
    }

在这个方法里面先去调用事务的建立订单,而后在去调用其余非DB操做。若是咱们如今想要更复杂一点的逻辑,好比建立订单成功就发送成功的RPC请求,失败就发送失败的RPC请求,由上面的代码咱们能够作以下转化:异步

public int createOrder(Order order){
        try {
            createOrderService.createOrder(order);
            sendSuccessedRpc();
        }catch (Exception e){
            sendFailedRpc();
            throw e;
        }
    }

一般咱们会捕获异常,或者根据返回值来进行一些特殊处理,这里的实现须要显示的捕获异常,而且在次抛出,这种方式不是很优雅,那么怎么才能更好的写这种话逻辑呢?分布式

TransactionSynchronizationManager

在Spring的事务中恰好提供了一些工具方法,来帮助咱们完成这种需求。在TransactionSynchronizationManager中提供了让咱们对事务注册callBack的方法:ide

public static void registerSynchronization(TransactionSynchronization synchronization)
			throws IllegalStateException {

		Assert.notNull(synchronization, "TransactionSynchronization must not be null");
		if (!isSynchronizationActive()) {
			throw new IllegalStateException("Transaction synchronization is not active");
		}
		synchronizations.get().add(synchronization);
	}

TransactionSynchronization也就是咱们事务的callBack,提供了一些扩展点给咱们:高并发

public interface TransactionSynchronization extends Flushable {

	int STATUS_COMMITTED = 0;
	int STATUS_ROLLED_BACK = 1;
	int STATUS_UNKNOWN = 2;
	
	/**
	 * 挂起时触发
	 */
	void suspend();

	/**
	 * 挂起事务抛出异常的时候 会触发
	 */
	void resume();


	@Override
	void flush();

	/**
	 * 在事务提交以前触发
	 */
	void beforeCommit(boolean readOnly);

	/**
	 * 在事务完成以前触发
	 */
	void beforeCompletion();

	/**
	 * 在事务提交以后触发
	 */
	void afterCommit();

	/**
	 * 在事务完成以后触发
	 */
	void afterCompletion(int status);
}

咱们能够利用afterComplettion方法实现咱们上面的业务逻辑:

@Transactional
    public int createOrder(Order order){
        orderDbStorage.save(order);
        orderItemDbStorage.save(order.getItems());
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCompletion(int status) {
                if (status == STATUS_COMMITTED){
                    sendSuccessedRpc();
                }else {
                    sendFailedRpc();
                }
            }
        });
        return order.getId();
    }

这里咱们直接实现了afterCompletion,经过事务的status进行判断,咱们应该具体发送哪一个RPC。固然咱们能够进一步封装TransactionSynchronizationManager.registerSynchronization将其封装成一个事务的Util,可使咱们的代码更加简洁。

经过这种方式咱们没必要把全部非DB操做都写在方法以外,这样代码更具备逻辑连贯性,更加易读,而且优雅。

afterCompletion的坑

这个注册事务的回调代码在咱们在咱们的业务逻辑中常常会出现,好比某个事务作完以后的刷新缓存,发送消息队列,发送通知消息等等,在平常的使用中,你们用这个基本也没出什么问题,可是在打压的过程当中,发现了这一块出现了瓶颈,耗时特别久,经过一系列的监测,发现是从数据库链接池获取链接等待的时间较长,最终咱们定位到了afterCompeltion这个动做,竟然没有归还数据库链接。

在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);
		}
	}

这里咱们只须要关注 倒数几行代码便可,能够发现咱们的triggerAfterCompletion,是倒数第二个执行逻辑,当执行完全部的代码以后就会执行咱们的cleanupAfterCompletion,而咱们的归还数据库链接也在这段代码之中,这样就致使了咱们获取数据库链接变慢。

如何优化

对于上面的问题如何优化呢?这里有三种方案能够进行优化:

  • 将非DB操做提到事务以外,这种方法也就是咱们上面最原始的方法,对于一些简单的逻辑能够提取,可是对于一些复杂的逻辑,好比事务的嵌套,嵌套里面调用了afterCompletion,这样作会增大不少工做量,而且很容易出现问题。
  • 经过多线程异步去作,提高数据库链接池归还速度,这种适合于注册afterCompletion时写在事务最后的时候,直接将须要作的放在其它线程去作。可是若是注册afterCompletion的时候出如今咱们事务之间,好比嵌套事务,就会致使咱们要作的后续业务逻辑和事务并行。
  • 模仿Spring事务回调注册,实现新的注解。上面两种方法都有各自的弊端,因此最后咱们采用了这种方法,实现了一个自定义注解@MethodCallBack,在使用事务的上面都打上这个注解,而后经过相似的注册代码进行。
@Transactional
    @MethodCallBack
    public int createOrder(Order order){
        orderDbStorage.save(order);
        orderItemDbStorage.save(order.getItems());
        MethodCallbackHelper.registerOnSuccess(() -> sendSuccessedRpc());
         MethodCallbackHelper.registerOnThrowable(throwable -> sendFailedRpc());
        return order.getId();
    }

经过第三种方法基本只须要把咱们注册事务回调的地方都进行替换就能够正常使用了。

再谈大事务

说了这么久大事务,到底什么才是大事务呢?简单点就是事务时间运行得长,那么就是大事务。通常来讲致使事务时间运行时间长的因素不外乎下面几种:

  • 数据操做得不少,好比在一个事务里面插入了不少数据,那么这个事务执行时间天然就会变得很长。
  • 锁的竞争大,当全部的链接都同时对同一个数据进行操做,那么就会出现排队等待,事务时间天然就会变长。
  • 事务中有其余非DB操做,好比一些RPC请求,有些人说个人RPC很快的,不会增长事务的运行时间,可是RPC请求自己就是一个不稳定的因素,受不少因素影响,网络波动,下游服务响应缓慢,若是这些因素一旦出现,就会有大量的事务时间很长,有可能致使Mysql挂掉,从而引发雪崩。

上面的三种状况,前面两种可能来讲不是特别常见,可是第三种事务中有不少非DB操做,这个是咱们很是常见,一般出现这个状况的缘由不少时候是咱们本身习惯规范,初学者或者一些经验不丰富的人写代码,每每会先写一个大方法,直接在这个方法加上事务注解,而后再往里面补充,哪管他是什么逻辑,一把梭,就像下面这张图同样:

固然还有些人是想搞什么分布式事务,惋惜用错了方法,对于分布式事务能够关注Seata,一样能够用一个注解就能帮助你作到分布式事务。

最后

其实最后想一想,为何会出现这种问题呢?通常你们的理解都是会认为都是在完成以后作的了,数据库链接确定早都释放了,可是事实并不是如此。因此,咱们使用不少API的时候不能望文生义,若是其没有详细的doc,那么你应该更加深刻了解其实现细节。

固然最后但愿你们写代码以前尽可能仍是不要一把梭,认真对待每一句代码。

若是你们以为这篇文章对你有帮助,你的关注和转发是对我最大的支持,O(∩_∩)O:

相关文章
相关标签/搜索