最近在负责一个礼品卡模块,就是用返利获得的余额,在APP端兑换不同面值的礼品卡,用于购物使用,类似于现金券。业务很简单
,流程很简单,但是查询更新操作有好几个,可以肯定的是这些操作必须要在一个事务执行,失败必须回滚的。刚开始做的时候傻傻的以为同一个事务并发问题也不会有。功能做完之后,写了个测试用例,启20个线程同事去兑换礼品卡,结果发现有同一张礼品卡被兑换多次的问题。很明显的并发问题,多个线程获取到同一张未兑换的礼品卡。这个问题的原因是先查数据库获取未兑换的卡,这种做法肯定是不太好,即使同步去做,也需要进行一次查库,非常不必要。设想在礼品卡导入的时候,可以把礼品卡放入一个池子中,每次兑换的时候取走一个,这个方式还是比较好实现的,前提池子肯定也得保证线程安全。
废话了挺多,还是接着说下刚刚的问题,首先想到了jvm的同步方法,然后在方法或者需要同步的代码块上加上synchronized,然后还是开启20个线程继续测试,想象挺好,但是突然发现刚刚的问题怎么还是存在。一开始有点百思不得其姐。。咳咳,是解,不要想远了。都已经同步了,为什么还是会出现这种情况呢。想了想突然意识到,不管是同步代码块,还是同步方法,都是在@Transactional注解的方法下。线程释放锁的时候,也许AOP的事务处理,并未提交成功,然后另一个线程就拿到了钥匙。获取到了上个事务同一个未兑换的礼品卡,导致了这个现象出现。随即又写了个方法,在方法中写上同步代码块,调用事务方法,结果没有出现上面的情况。感觉猜想应该是正确的吧。。
问题虽然解决了一些,但是另一个问题接着就来了。。预生产,线上并非部署一个实例,如果使用jvm的同步方法,还是有一定几率出现并发问题,这就想到了分布式锁。之前一直听说,也没有实现过,马上网上搜罗了一下。
一般的解决方案如下:
- zookeeper分布式锁,基于自增节点
- memcache分布式锁,基于add函数
- Redis分布式锁,基于setnx命令【本次使用】
说下分布式锁的基本功能:
- 同一时刻只能存在一个锁
- 需要解决意外死锁问题,也就是锁能超时自动释放
- 支持主动释放锁
下面说下我的实现思路,通过注解的方式,定义方法注解,然后通过Spring 的AOP方式,在方法执行前加锁,方法执行后释放锁。
使用框架:
springboot + redis 集群
具体代码如下:
1.编写方法注解
在方法上使用此注解,就会被AOP拦截,并进行加锁处理。
2.编写方法参数上的注解
该注解的作用,主要是影响redis中锁的key。
3.在需要调用的方法使用注解
我这里使用的模块礼品卡,然后redis的锁的轮训时间为5s。用礼品卡的库存ID,当做唯一锁key。这样做,不同库存ID,或者说不同类型的礼品卡就是不同的锁,也会提高一些并发。
4.spring AOP 拦截是实现
这里主要做的就是获取获取注解的值,和注解的参数的值,然后创建锁对象获取锁。然后执行方法,最后在finally中释放锁。还是很简单的
5.redis获取锁的主要实现
这里通过不断的轮询,从redis获取锁。如果未获取到,怎返回false,业务处获取false直接抛异常。主要是通过redis的setNx方法实现分布式锁。删除锁的代码很简单,就是del,这里不展示了。
这里只是简单的实现了下,后续再继续完善。