目录
1、为什么要使用分布式锁
使用分布式锁的目的,无外乎就是保证同一时间只有一个客户端可以对共享资源进行操作。
根据锁的用途还可以细分为以下两类:
- 允许多个客户端操作共享资源
这种情况下,对共享资源的操作一定是幂等性操作,无论你操作多少次都不会出现不同结果。在这里使用锁,无外乎就是为了避免重复操作共享资源从而提高效率。 - 只允许一个客户端操作共享资源
这种情况下,对共享资源的操作一般是非幂等性操作。在这种情况下,如果出现多个客户端操作共享资源,就可能意味着数据不一致,数据丢失。比如在购物系统中,对于用户下单来说,对一件商品的购买同一时间只能一个人操作,比如说秒杀商品 下单减库存,否则多个用户同时下单同时减库存容易出现问题,比如说超卖。
2、分布式锁的技术有哪些
常见的有redis来实现分布式锁和zookpeer来实现分布式锁两种方式。
3、Redis分布式锁的实现方案
3.1、setnx实现方式
这是最普通的实现方式,就是在 redis 里使用 setnx 命令创建一个 key,执行下面这个命令就算加锁。
SET key random_value NX PX 30000
- random_value :value设置为随机值。
- NX:表示只有 key 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 nil)
- PX 30000:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。
释放锁就是删除key,为了保证删除key的原子性,一般使用lua脚本进行删除。删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。
if redis.call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])
else
return 0
end
将value设置随机值的原因就是假如A获取到了锁,由于执行逻辑时间长,超过锁过期时间,此时B获取到了锁,A执行完后会释放锁,如果不设置随机值,就会释放B获取到的锁,这是不对的。
缺点:单机下使用,容易会有单点故障,没有可用性,假如有主从复制,那么主挂掉,还没将锁复制到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。就会有两个线程持有锁。
3.2、Redisson框架实现方式
Redis推荐使用redlock算法实现分布式锁,实现框架有Redisson,在集群下可以使用。人家还支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构。
实现原理:
(1)加锁机制
咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。紧接着,就会发送一段lua脚本到redis上,为啥要用lua脚本呢?
因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。
hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
myLock :{
”8743c9c0-0795-4907-87fd-6c719a6b4586:1 ”:1
}
上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。
接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
好了,到此为止,ok,加锁完成了。
(2)锁互斥机制
那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?
首先会判断发现myLock这个锁key已经存在了。
接着再判断,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。
所以,客户端2会进入一个while循环,不停的尝试加锁。
(3)watch dog自动延期机制
客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?
这就用到了watch dog机制,只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。
(4)可重入加锁机制
那如果客户端1都已经持有了这把锁了,客户端1再次加锁,就会执行可重入加锁的逻辑,他会用:
incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
通过这个命令,对客户端1的加锁次数,累加1。
(5)释放锁机制
如果执行lock.unlock(),就可以释放分布式锁,每次释放锁都对myLock数据结构中的那个加锁次数减1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del myLock”命令,从redis里删除这个key。
然后客户端2就可以尝试完成加锁了。
这就是分布式锁Redisson框架的实现机制。
一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。
缺点:
其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的redis slave实例。
但是这个过程中一旦发生redis master宕机,还没来得进行异步复制,就进行了主备切换,redis slave变为了redis master。
接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
此时就会导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题&#x