使用Redis正确实现锁机制的姿势你get了吗?
场景代入
(相关资料图)
先看一个场景,某个接口请求量比较大,为了提升接口响应速度,引入了缓存机制。缓存策略是这样的:处理请求时,先查询缓存,缓存中有数据则直接使用缓存数据,缓存中没有数据则查询数据库,查询到数据后将数据写入缓存并给缓存数据设置一个有效期。
使用锁机制
这种缓存策略有一个重要的问题需要处理好,当缓存过期的瞬间,大量并发请求会同时查询数据库进而可能导致服务雪崩。解决这个问题的方法也很容易想到:引入锁机制,当缓存失效时,只让一个请求去查询数据库并更新缓存。
下面是很多人下意识想到的加锁方法:
借助Redis的setnx方法,setnx是set if not exists的缩写,当key不存在的时候才会设置key的值,设置成返回1,设置失败返回0。看下主要的逻辑,以伪代码方式说明:
rs = redis.SetNX(key, value)if rs {//处理更新缓存逻辑//......//释放锁 redis.Del(key)}
通过setnx加锁,如果成功则更新缓存然后释放锁。这么做有一个严重的问题:如果程序更新缓存的时候因为意外原因退出了,那么这个锁就不会被释放而一直存在,以至于缓存再也得不到更新。为了解决这个问题会想到给锁设置一个过期时间,如下
redis.Multi()redis.SetNX(key, value)redis.Expire(key, ttl)redis.Exec()
因为setnx不能设置过期时间,所以要借助expire方法来实现,同时需要使用Multi/Exec来确保操作的原子性,以免setnx成功了而expire却失败了。这样做依然有问题:当多个请求到达时,虽然只有一个请求的setnx操作可以成功,但是任何一个请求的expire操作却都可以成功,这就意味着即便获取不到锁也可以刷新过期时间,导致锁一直有效,还是解决不了上面的问题。
显然借助setnx是解决不了问题的,幸好Redis从2.6.12版本起,set涵盖了setnx的功能,使用set就可以解决上述问题。
rs = redis.Set(key, value,"nx", ttl)if rs {//处理更新缓存逻辑//......//释放锁 redis.Del(key)}
到这一步依然有问题,如果一个请求更新缓存的时间比锁的有效期还要长,导致缓存更新过程中锁就被释放了,此时另一个请求就会获取到锁,但前一个请求在缓存更新完毕的时候释放锁的话就会出现误释放其它请求创建的锁的情况。要解决这个问题,可以在创建锁的时候引入一个随机数并在释放锁的时候判断一下:
rs = redis.Set(key, value,"nx", ttl)if rs {//处理更新缓存逻辑//......//先判断随机数,是同一个则释放锁 if redis.Get(key)== randomStr { redis.Del(key)}}
标签: