redis在我们日常开发中,除了用来做缓存提高应用程序的性能,降低数据库压力之外。可能用途最广泛地当属用redis来做分布式锁了。
在单机中,我们要解决并发时线程安全的问题会使用JDK的synchronized
或者Lock
类,或者直接使用线程安全的类,例如JUC(java.util.concurrent并发包)。而在大型的应用程序中,单机部署显然不能满足我们的需求,这个时候要在分布式集群环境中对互斥资源进行控制访问,就需要使用到分布式锁。
在本章中,我们着重介绍基于redis的分布式锁,同时将简单介绍其他分布式锁的解决方案。
开始之前先总结无论什么方式的分布式锁,其核心都是如有不存在某个key则写入,存在则返回写入失败。
redis中主要通过setnx
命令实现,全称是“SET if Not eXists”,意为如果存在则写入。如果不存在key则返回1,已经存在了这个key,则会返回0。释放锁时直接调用del
命令删除即可。
127.0.0.1:6379> setnx redis_lock a
(integer) 1
127.0.0.1:6379> setnx redis_lock a
(integer) 0
但是请注意,使用setnx
有一定的风险,我们知道加锁就有存在“死锁”的可能性,而打破死锁的方法之一就是主动释放资源(设置锁过期时间),然而setnx
并没有提供过期时间的设置,redis提供了另外一个命令——expire
来设置key值得过期时间,所以改造上面的例子为以下所示:
127.0.0.1:6379> setnx redis_lock a #设置一个分布式锁的key为redis_lock
(integer) 1
127.0.0.1:6379> expire redis_lock 5 #设置redis_lock的过期时间为5秒,到期自动删除
(integer) 1
127.0.0.1:6379> setnx redis_lock a #此时再设置分布式锁的key为redis_lock,返回0失败
(integer) 0
127.0.0.1:6379> setnx redis_lock a #过5秒再设置分布式锁的key为redis_lock,返回1成功
(integer) 1
可以看到通过组合setnx
和expire
命令,能达到我们想要的结果。但是请注意,它仍然存在一个问题,那就是这两个命令并不是原子性的,如果在执行expire redis_lock 5
时,redis服务恰好宕机,此时这个key将会一直存在。
好在redis为我们提供了set
命令的分布式用法并且可以设置为过期时间,关键是原子性的。官方的命令参数为set key value [expiration EX seconds|PX milliseconds] [NX|XX]
。
[expiration EX seconds|PX milliseconds]
参数EX表示过期时间单位为“秒”,PX表示过期时间单位为“毫秒”。
[NX|XX]
参数NX表示“SET if Not eXists”不存在则写入,XX表示“SET if eXists”存在则写入,分布式锁的场景中使用“NX”参数。
所以我们设置一个key值名为“lock”的锁,5秒后自动删除:
127.0.0.1:6379> set lock a ex 5 nx #设置一个key值名为“lock”的锁,5秒后自动删除
OK
127.0.0.1:6379> set lock a ex 5 nx #5秒内设置一个key值名为“lock”的锁,5秒后自动删除。返回nil失败
(nil)
127.0.0.1:6379> set lock a ex 5 nx #5秒后设置一个key值名为“lock”的锁,5秒后自动删除。返OK成功
OK
使用redis作为分布式锁,最好要设置过期时间,也就是最好使用set命令。
ZooKeeper是一个分布式协调服务中间件,它可以用作注册中心、动态配置中心等等。
我们利用ZooKeeper的临时有序节点也可以实现分布式锁。
ZooKeeper的数据结构类似Linux中的文件结构,总体来讲它时“一棵树”,节点中记录相关信息。节点分为“永久节点”和“临时节点”。当我们要获取一个锁时,需要在ZooKeeper的结构中创建一个临时有序节点,释放锁同样时删除节点。获取分布式锁,即获取一个ZooKeeper的临时有序节点,如果获取到的有序节点存在比序号比自己更小的兄弟节点,即获取锁失败。
基于ZooKeeper实现分布式锁可以利用ZooKeeper监听的特性,一旦有节点发生变化可以进行通知。这点是Redis不具备的。但由于它的实现方式是创建和删除节点,所以在性能上不如redis。
通过MySQL实现分布式锁是我以前遇到的一个面试问题,思考以下实现方式:
在MySQL创建一个有关锁的表“tb_lock”,一共有两列,一列叫“key”并设置为唯一索引,另一列设置为“value”。
获取锁时,通过
insert
插入一条记录,如果插入成功则获取锁成功;插入失败则获取锁失败。
一听,是不是觉得有点意思,好像确实能通过MySQL来实现分布式锁,这样我们就不必引入redis或ZooKeeper。那为什么我们日常开发中几乎没有人这样用过呢?实际上,MySQL实现分布式锁,它仅仅满足了控制互斥资源这一点,尽管它是最核心的,但分布式锁不仅是控制互斥资源,它还需要具备以下特性:
- 可设置过期时间,防止死锁
- 需要具备阻塞获取锁的特性
- 较高的性能和可靠性
- 锁还需要可重入
- ……
所以如果要使用MySQL来实现分布式锁,你需要去解决以上的问题,对于成熟的redis和ZooKeeper分布式锁方案,我们大可不必再造一个不可靠的轮子。