想必很多人遇到过这样子的场景,但愿在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) { } }