注意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:

相關文章
相關標籤/搜索