高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。
分布式锁特点:
- 安全性。在任意时刻,只有一个客户端可以获得锁(排他性)
- 避免死锁。client最终一定可以获得锁,即使锁住资源的客户端在release锁之前崩溃或不可达
- 容错性。只要锁服务集群中的大部分节点存活,Client 就可以进行加锁解锁操作。
分布式锁服务,你需要考虑下面几个设计
- 需要给一个锁被释放的方式,以避免请求者不把锁还回来,导致死锁的问题。Redis 使用超时时间,ZooKeeper 可以依靠自身的 sessionTimeout 来删除节点。
- 分布式锁服务应该是高可用的,而且需要持久化。可以参考 Redis 的文档 RedLock
- 非阻塞方式的锁服务。
- 支持锁的可重入性。
这个方案相对于memcached()的add()方案,redis占优势的是,其支持的数据类型更多,而memcached只支持String一种数据类型。除此之外,无论是从性能上来说,还是操作方便性来说,其实都没有太多的差异,完全看你的选择,比如公司中用哪个比较多,你就可以用哪个。
首先说明一下setnx()命令,setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。但是要注意的是setnx命令不能设置key的超时时间,只能通过expire()来对key设置。
具体的使用步骤如下:
-
setnx(lockkey, 1) 如果返回0,则说明占位失败;如果返回1,则说明占位成功
-
expire()命令对lockkey设置超时时间,为的是避免死锁问题。
-
执行完业务代码后,可以通过delete命令删除key。
这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。比如,如果在第一步setnx执行成功后,在expire()命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用redis的setnx()、get()和getset()方法来实现分布式锁。
这个方案的背景主要是在setnx()和expire()的方案上针对可能存在的死锁问题,做了一版优化。
那么先说明一下这三个命令,对于setnx()和get()这两个命令,相信不用再多说什么。那么getset()命令?这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。假设key原来是不存在的,那么多次执行这个命令,会出现下边的效果:
-
getset(key, “value1″) 返回nil 此时key的值会被设置为value1
-
getset(key, “value2″) 返回value1 此时key的值会被设置为value2
-
依次类推!
介绍完要使用的命令后,具体的使用步骤如下:
-
setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
-
get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
-
计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
-
判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
-
在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
SET resource_name my_random_value NX PX 30000
-
my_random_value是由客户端生成的一个随机字符串,相当于是客户端持有锁的标志
-
NX表示只有当resource_name对应的key值不存在的时候才能SET成功,相当于只有第一个请求的客户端才能获得锁
-
PX 30000表示这个锁有一个30秒的自动过期时间。
至于解锁,为了防止客户端1获得的锁,被客户端2给释放,采用下面的Lua脚本来释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
在执行这段LUA脚本的时候,KEYS[1]的值为resource_name,ARGV[1]的值为my_random_value。原理就是先获取锁对应的value值,保证和客户端传进去的my_random_value值相等,这样就能避免自己的锁被其他人释放。另外,采取Lua脚本操作保证了原子性。
例如,下面的例子演示了不区分 Client 会导致错误
- Client A 获得了一个锁。
- 当尝试释放锁的请求发送给 Redis 时被阻塞,没有及时到达Redis
- 锁定时间超时,Redis 认为锁的租约到期,释放了这个锁。
- Client B 重新申请到了这个锁。
- Client A 的解锁请求到达,将 Client B 锁定的key解锁
- Client C 也获得了锁。
- Client B 和 Client C 同时持有锁。
通过执行上面脚本的方式释放锁,Client 的解锁操作只会解锁自己曾经加锁的资源,所以是安全的。
-
基于数据库表做乐观锁,用于分布式锁。
-
使用memcached的add()方法,用于分布式锁。
-
使用redis的setnx()、expire()方法,用于分布式锁。
-
使用redis的setnx()、get()、getset()方法,用于分布式锁。
http://mp.weixin.qq.com/s/JTsJCDuasgIJ0j95K8Ay8w?utm_source=tuicool&utm_medium=referral