并发场景下,由于修改和保存数据的过程不是原子性的,部分操作可能会丢失,在单服务中我们常用本地锁来避免并发带来的问题。但是本地锁无法在多服务器之间生效。
最直白的做法:SETNX
SETNX
is short for “SET if Not eXists”,即设置KEY如果不存在的话,value我们可以暂定设置1。
SETNX lockName 1
返回1说明key不存在设置成功,即获取到了锁,返回0则加锁失败。
删除命令:DEL
DEL lockName
删除了该key,此时其他线程就可以通过SETNX获取锁了。
设置key的过期时间:EXPIRE
EXPIRE lockName 20
为key设置一个超时时间,以保证即使锁没有被显示的释放时,在到达过期时间后也能自动释放锁,防止死锁的产生。
if(setnx(key,1) == 1){
expire(key,30)
try {
work....
} finally {
del(key)
}
}
在极端情况下,当线程执行完SETNX
还未执行EXPIRE
时服务挂掉。
此时该锁既不会被显示的解锁,也不会自动过期,其他线程再也无法获取到该锁了,game over。
SET命令加锁
SET lockName 1 EX 30
SETNX
命令是不支持传入超时时间的,不过幸好Redis2.6.12以后为SET指令增加了可选参数EX、PX属性,这样加锁和设置超时时间就是原子操作了。
回忆一下我们实现的锁机制,如果锁到期了任务未完成将产生两个严重问题。
解决这个问题,我们只需要在删除之前验证key对应的value是不是自己的线程。
我们可以把线程ID作为key对应的value,在删除之前验证一下锁是不是自己的锁。
伪代码:
加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,EX)
解锁:
if(threadId .equals(redisClient.get(key))){
del(key)
}
这里,判断锁和删除锁是两个独立操作,不是原子操作。
我们可以使用lua脚本来实现:
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
这样,判断和删除过程就是原子操作了。
上面我们解决了释放非自己锁的问题,但是AB两个线程同时执行任务也是不完美的。
我们可以让获得锁的线程开启一个守护线程,用来给快到期的锁续期。
Redis分布式锁在生产中使用自然不需要我们自己去实现每一个细节,Redis分布式锁在java中的解决方案官方推荐就会Redisson