背景
号源数量有一定限制,而在进行取号时有可能出现并发取号导致实际取号数量超出号源数量的情况,故需要对号源数量这个资源进行加锁,避免并发问题。
技术选型
由于项目是采用分布式部署的方式,
- Java自带的并发处理方式如:synchronized、lock只适用于单机;
- 而基于数据库实现的乐观锁又可能会导致数据库压力过大;
- zookeeper也可作为分布式锁,但由于自身对Redis了解更多,且公司内部对Redis的封装更完善
- 故选择使用Redis作为分布式锁的实现
解决思路
思考并发点
- 管理端对号源数量进行修改
- 号源缓存丢失,进行号源初始化
号源设置处理逻辑
采用加锁避免并发。由于号源设置触发的场景较少,为保证号源数量修改后能及时同步到取号端,故采用先落库再删内存的方案。
取号号源扣减及缓存失效重建逻辑
- 在取号端首先会通过 lua脚本进行号源的判断,保证号源判断和号源扣减操作的原子性。(为什么原子:在redis 的官方文档中有描述lua脚本在执行的时候具有排他性,不允许其他命令或者脚本执行,类似于事务。但是存在的另一个问题是,它在执行的过程中如果一个命令报错不会回滚已执行的命令,所以要保证lua脚本的正确性)
- 缓存失效重建
2.1.号源缓存失效重建。当号源缓存失效时,执行lua脚本会出现attempt to compare nil with number的异常,此时就需要对号源缓存进行加锁,查MySQL重建缓存。
2.2.取号号码缓存失效重建。同样进行加锁,查MySQL重建缓存。
注意点
要注意单粒度加锁,再保证共享资源正确被操作的同时,提供又更好的并发能力。
附上lua脚本
这段判断数量再扣减的脚本比较通用,再很多场景都能用上。
if(tonumber(redis.call('get', KEYS[1])) <= 0 then
return -1
end
redis.call('incrby', KEYS[1], -1)
return