想必很多人遇到過這樣子的場景,但願在spring的事務完成後do something...java
前言:mysql
---------------------------------------------------------------------------------------------redis
我遇到的場景是,但願在搶購方法上使用redis setnx簡單的作一下鎖,來防止重複提交spring
步驟所有於@Transactional do something()內sql
一、使用userId+搶購專屬id 爲key 嘗試setnx 若是setnx成功,執行2ide
二、給key設置10秒的expire.net
三、crudcode
四、finally塊中執行刪除key事務
以上是個人一個防止重提提交的簡單辦法,爲了怕setnx死鎖,因此給key設置了expire,因爲步驟3中有查詢用戶是否已參與搶購的判斷,相似與簡單的樂觀鎖,因此覺得本方案可行get
但上線後發現,仍是會有部分用戶會存在重複搶購的問題,所以判斷本方案存在問題。想了一下,因爲@Transactional加在do something()上,因此可能存在問題以下:
用戶的操做一,拿到了setnx,又重複操做二,三,而操做一又恰好很快的執行完,這個以後finally刪除了key,因此操做二,三都有可能成功操做,而因爲setnx在@Transactional do something()內部,而@Transactional採用默認事務(mysql rr),所以形成了重複搶購的問題
-----------------------------------------------------------------------------------------------
應急解決方案
一、將finally中步驟4,刪除key的操做去掉
這個方案雖然是解決了線上的問題,可是可能存在如下問題(我能想到的就這個)
一、若是用戶操做一這個事務處理爲11秒,這個時候操做二進來了,那麼就又會存在重複搶購的問題。
二、線上存在大量redis 10秒後消失的key
--------------------------------------------------------------------------------------------------------
其餘解決方案:
也就是咱們標題講到的spring聲明式事務@Transactional後置
後置方案一
一、在spring聲明式事務@Transactional 方法do something()添加以下代碼,在spring事務提交後再delete 對應的key
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { redisTemplate.delete(key); super.afterCommit(); } });
二、若是出現了異常,在catch中同樣執行delete key的操做
(若是方案有任何問題,還請拍磚,本人仍是小菜,但願有大神指點)
----------------------------------------------------------------------------------------------------------------
後置方法二:
使用spring自帶的TransactionTemplate ,手動提交事務
----------------------------------------------------------------------------------------------------------------
後置方法三:
與方法一相似
extends TransactionSynchronizationAdapter implements AfterCommitExecutor
相似可能用到spring事務後置處理的場景頗有不少,固然這裏也能夠處理前置,支持的操做以下代碼
public abstract class TransactionSynchronizationAdapter implements TransactionSynchronization, Ordered { public TransactionSynchronizationAdapter() { } public int getOrder() { return 2147483647; } public void suspend() { } public void resume() { } public void flush() { } public void beforeCommit(boolean readOnly) { } public void beforeCompletion() { } public void afterCommit() { } public void afterCompletion(int status) { } }