说起来有些尴尬。最近写了一个抽奖发券的功能,因为是100%中奖,倒是抽奖没怎么折腾。但是问题在于返券。
大致规则是这样子的:在活动期间每人每支付一个订单,就可以获取一次抽奖机会,而最多可以抽十次奖,抽奖之后得把奖品发放。
分析需求之后就变得很简单,无非是拉取符合条件的订单数,然后计算抽奖机会,满足则可抽奖,抽完奖同步中奖纪录,然后发放奖品。的确在流程上也没什么问题,主要注意同步中奖信息时的事物就好。这样一来只需要在方法上加上@Transactional注解就好了。
然后三下五除二搞定了,开始自己本地测试,也没啥问题。于是匆匆上了测试库。
过了一会前端告诉我他抽奖抽了十二次。我告诉他不可能了。十次的时候就会有check,你连正常访问都做不到。然后打开DB一查还真是。。于是我开始重新审视我的逻辑。但毕竟一个流程清晰的抽奖,在业务上不会犯这种奇怪的逻辑错误。
然后看了下DB插入时间,我似乎明白了啥。。于是我写了小的测试:
public static void main(String args[]){ ExecutorService pool = Executors.newFixedThreadPool(5); IntStream.range(0,4).forEach( i -> pool.execute(() -> { HttpUtil.get("http://localhost:8080/xxxx?xx=xx"); })); pool.shutdown(); }
然后一个人拿着一次的抽奖机会,抽中了七个奖品(我的次数check逻辑是通过奖品记录表来反减的)
然后我就想给这方法加锁。于是给方法加上了synchronized。这一幕被同事大哥给看见了。他问我我们的项目上锁别用这个,服务器有很多台,分布式的,你这个太鬼畜了。
我一想??那分布式锁如何实现?同事给我一条明路:
@Component public class RedisLockUtil { private static StringRedisTemplate redisTemplate; @Autowired public void setRedisTemplate(StringRedisTemplate redisTemplate) { RedisLockUtil.redisTemplate = redisTemplate; } /** * 获取锁 */ public static void lock(String key) { int i = 1; while (!StringUtil.isEmptyOrNull(redisTemplate.opsForValue().getAndSet(key, "1"))) { i++; try { Thread.sleep(500); } catch (InterruptedException e) { } if (i >= 10) { break; } } redisTemplate.expire(key, 60, TimeUnit.SECONDS); } /** * 释放锁 */ public static void unlock(String key) { redisTemplate.delete(key); } /** * 通用锁控制 * * @param key * 加锁主键 * @param redisLockProcess * 被锁定流程 */ public <R> R process(String key, RedisLockProcess<? extends R> redisLockProcess) { try { lock(key); return redisLockProcess.process(); } catch(BaseException e){ throw e; } catch (Exception e) { throw new BaseException("请求处理异常", e); } finally { unlock(key); } } }
利用Redis快速起了一个分布式锁,完美解决了上面的问题,这样只需在方法执行前加上加锁的过程:
public Map<String, Object> lockProcess(String xx){ String key = "NAME_SPACE_"+xx; return redisLockUtil.process(key, () -> this.fangfa(xx)); }
这样除了事务的保护之外还有针对并发的处理,问题就被解决了(对了,在此之前记得配置Namespce以及Spring的Bean注入。差点。。感谢前端的光速请求)
期间顺便参观了下别人的用法:
https://blog.csdn.net/he90227/article/details/69568702#t0
现在回看这种方案的使用,利用Redis做锁原则上可行,但是在效率和资源上我觉得还是自己真的土豪,居然拿这个去锁一个方法,其实这里不应该在使用这样的设计思路去设计。虽然思路简单实现简单,但是生产环境不推荐这样去做。
@Nostring 此类型的活动,在一般情况下若访问量和并发量不高,可以直接 select for update,悲观锁搞起来。
@Nostring 这XD