1. 引子
在多线程环境下,如果想对共享内存中的数据进行互斥访问且想保证线程安全,Java可以使用很多种锁来实现;但是在分布式环境下,由于线程不在一个进程中,使用Java中提供的锁就没有用了,一般会使用redis的setnx来实现分布式锁。
2. 加锁的正确姿势
使用setnx加锁需要满足下面三个正确姿势:
- 必须给锁设置一个失效时间:防止特殊情况下锁等不到释放从而使得其他线程死锁。
- 其中加锁时,保存的value是一个随机字符串:这是因为可能存在这种情况:线程A先执行setnx加锁,并给key设置的超时时间为2秒,然后由于线程繁忙,3秒后才去解锁,而这个key在2秒的时候超时删除了,线程B这个时候就获取到了锁。这就可能导致3秒的时候,线程A将线程B上的锁给解了。为避免这种情况,应该在解锁的时候通过value来判断是不是自己上的锁,并要求只有自己上的锁才能解锁,不能解锁别人上的锁。
- 写入随机值和设置超时时间必须为原子性操作:不能先设置了key value,后设置超时时间,应该一步就将这两个操作同时做了。
3. 解锁的正确姿势
同样的,解锁分为三步:
- 从redis获取数据。
- 判断value是否和本地保存的value一致(是不是自己上的锁)
- 删除该键值对
这三步也要求保证原子性,不然在高并发环境下会有问题。一般要保证原子性,可以在redis使用lua脚步来实现:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
4. 注意
Redis的setnx命令本身不带expire参数以设置过期时间,因此只能先setnx获取锁后,再给这个key设置过期时间。但是由于这是两步操作,可能会导致刚执行完setnx后,redis就挂了,这种请求就会导致这个锁永远不能释放。为解决该问题,redis 2.6的版本后,支持set命令可以同时支持nx和过期时间两个操作:
屏幕快照 2019-06-23 下午7.19.36.png所以使用上述set命令才能万无一失的实现分布式锁。
网友评论