想必不少人遇到过这样子的场景,希望在spring的事务完成后do something...
前言:
---------------------------------------------------------------------------------------------
我遇到的场景是,希望在抢购方法上使用redis setnx简单的做一下锁,来防止重复提交
步骤全部于@Transactional do something()内
1、使用userId+抢购专属id 为key 尝试setnx 如果setnx成功,执行2
2、给key设置10秒的expire
3、crud
4、finally块中执行删除key
以上是我的一个防止重提提交的简单办法,为了怕setnx死锁,所以给key设置了expire,由于步骤3中有查询用户是否已参与抢购的判断,类似与简单的乐观锁,所以以为本方案可行
但上线后发现,还是会有部分用户会存在重复抢购的问题,因此判断本方案存在问题。想了一下,由于@Transactional加在do something()上,所以可能存在问题如下:
用户的操作一,拿到了setnx,又重复操作二,三,而操作一又刚好很快的执行完,这个之后finally删除了key,所以操作二,三都有可能成功操作,而由于setnx在@Transactional do something()内部,而@Transactional采用默认事务(mysql rr),因此造成了重复抢购的问题
-----------------------------------------------------------------------------------------------
应急解决方案
1、将finally中步骤4,删除key的操作去掉
这个方案虽然是解决了线上的问题,但是可能存在以下问题(我能想到的就这个)
1、如果用户操作一这个事务处理为11秒,这个时候操作二进来了,那么就又会存在重复抢购的问题。
2、线上存在大量redis 10秒后消失的key
--------------------------------------------------------------------------------------------------------
其他解决方案:
也就是我们标题讲到的spring声明式事务@Transactional后置
后置方案一
1、在spring声明式事务@Transactional 方法do something()添加如下代码,在spring事务提交后再delete 对应的key
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { redisTemplate.delete(key); super.afterCommit(); } });
2、如果出现了异常,在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) { } }