Redis分布式锁
在高并发情况下,经常会出现数据问题,以下展示了redis分布式锁的演进过程。
1.使用synchronize关键字
使用synchronize进行并发控制,在单体架构(单机环境)中可以正常运行,但是分布式应用中,就会出现多个请求同时分发到不同的应用实例(tomcat),各实例并发执行减库存操作,导致数据不一致问题。假设服务实例1和2查询到当前库存为100,两个实例同时执行减一操作,并将库存设置为99,这时库存数量就不正确了。
@RestController("/redis") class RedisTest { @RequestMapping("/reduceStock") public String reduceStock { synchronized(this) { return reduce(); } } private String reduce() { int stock = Integer.parseInt(jedis.get("stock")); if(stock > 0) { stock--; jedis.set("stock", stock); return "剩余库存:" + stock; } else { return "库存不足!"; } } }
2.使用setnx命令加锁
setnx key value 只有在key不存在的时候,才会将key的值设置为value
使用setnx获取锁成功后减库存,之后再释放锁。这种虽然可以使用try捕获程序中出现的异常,但是如果在执行finally之前整个服务器挂了,还是会造成锁没有释放,最终导致后面的请求都无法再减库存。
public String reduceStock() { final String LOCK_KEY = "lockKey"; Boolean locked = jedis.setIfAbsent(LOCK_KEY, true); if(!locked) { return "获取分布式锁失败!"; } try { reduce(); } finally { jedis.delete(LOCK_KEY); } }
3. setnx加锁时设置过期时间
在setnx获取锁时,给LOCK_KEY加上过期时间
Boolean locked = jedis.setIfAbsent(LOCK_KEY, true, 10, TimeUnit.SECOND);
注意上面这句不能替换成下面两句,因为这两句不是原子操作,有可能在执行setnx之后失败了,导致后面无法设置过期时间,造成永远无法获取该锁的问题。
Boolean locked = jedis.setIfAbsent(LOCK_KEY, true);
jedis.expire(LOCK_KEY, 10, TimeUnit.SECOND);
4. 解决锁提前过期问题
但是上面简单地设置过期时间还是会有问题,可能还没执行完业务代码,锁就过期了,这时有新的请求过来就能拿到锁了。比如锁10s过期,这时虽然该线程还没执行完,但锁已经失效了,这时其他线程又可以获取该锁了。
a. 第0s:假设线程1持有锁,并设置锁过期时间为10s
b. 第10s:锁失效了,这时候有新的请求过来,线程2能拿到了锁
c. 第15s:线程1处理完业务逻辑,准备释放锁,这时删除的锁是线程2加的锁,就出现了本线程加的锁被其他线程释放掉。
下面的方式可以确保本线程不会删除其他线程的锁,但是还是会存在线程1还没执行完,线程2又拿到锁也并发执行的问题。所以这种方式没有彻底解决高并发问题。
public String reduceStock() { final String LOCK_KEY = "lockKey"; String threadId = UUID.randomUUID().toString(); Boolean locked = jedis.setIfAbsent(LOCK_KEY, threadId, 10, TimeUnit.SECOND); if(!locked) { return "获取分布式锁失败!"; } try { reduce(); } finally { if(threadId.equals(jedis.get(LOCK_KEY))) { jedis.delete(LOCK_KEY); } } }
5. 使用timer定时器更新过期时间
假设设置LOCK_KEY的过期时间为30s,可以每隔10s重新更新LOCK_KEY的过期时间为30s,延长锁的持有时间
注意:过期时间时间不能设置太长,否则其他线程阻塞的时间就越长;刷新的时间间隔一般为过期时间的1/3。
6. 使用redisson
redisson已经有一套完善的加锁机制,可以直接用它的api进行加锁。它相当于帮我们实现了第5种方案,获取锁时设置锁过期时间为30s,然后启动一个后台线程,每隔10s重新设置LOCK_KEY的过期时间为30s直至该线程结束。
public String reduceStock() { RLock redissonLock = redisson.getLock(LOCK_KEY); try { redissonLock.lock(); reduce(); } finally { redissonLock.unlock(); } }
存在问题:
1. 如果redis是集群架构,比如主从架构,假设线程A在master上加了锁,结果master挂了,而且锁没有及时同步到slave上。当slave升级为master后,其他线程检测到新的master上没有加锁,便会获取锁,从而造成并发执行的问题。
2. 分布式锁从底层原理来说就是将并发请求处理成串行队列,虽然redis处理速度快,但想要追求更高并发的性能,还要另寻他法。