分布式锁
在多线程情况下访问一些共享资源需要加锁,不然就hi出现数据被写乱的问题。一般可以用数据库DB,Redis和Zookeeper实现
分布式锁特点:
- 安全性(Safety):在任意时刻,只有一个客户端可以获得锁(排他性)
- 避免死锁:客户端最终一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达
- 容错性:只要锁服务集群中的大部分节点存活,Client就可以进行加锁解锁操作
Redis的分布式锁服务
在redis中,我们可以通过以下命令
SET resource_name my_random_value NX PX 30000
-
SET NX
只会在key
不存在的时候给key
赋值,PX
通知redis保存这个key
30000ms -
my_random_value
必须是全局唯一的值。这个随机数在释放锁时保证释放锁操作的安全性 -
PX
操作后面的参数代表的是这个key
的存活时间,称为锁过期时间 - 当资源被锁定超过这个时间时,锁将自动释放
- 获得锁的客户端如果没有在这个时间窗口内完成操作,就可能会有其他客户端获得锁,引起争用问题
这里的原理是,只有在某个key不存在的情况下才能设置(set)成功该key这就导致多个进程并发设置同一个key
申请成功的锁解锁:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
如果key对应的value一致,则删除这个key。
通过这个方式释放锁时为了避免Client释放了其他Client申请的锁
例如,这个例子演示了不区分Client出现的问题
- Client A获得了一把锁
- 当尝试释放锁的请求发送给Redis时被阻塞,没有即使到达Redis
- 锁定时间超时,Redis认为锁的租约到期,释放了这个锁
- Client B重新申请到了这个锁
- Client A的解锁请求到达,释放了Client B的锁
- Client C也获得了锁
- Client B 和 Client C 同时持有锁
通过上面这种方式,Client的解锁操作只会解锁自己曾经加锁的资源,所以是安全的
关于 value 的生成,官方推荐从 /dev/urandom 中取 20 个 byte 作为随机数。或者采用更加简单的方式,例如使用 RC4 加密算法在 /dev/urandom 中得到一个种子(Seed),然后生成一个伪随机流
也可以采用更简单的方法,使用时间戳 + 客户端编号的方式生成随机数。Redis 的官方文档说:“这种方式的安全性较差一些,但对于绝大多数的场景来说已经足够安全了”。
分布式锁服务的一个问题
为了避免Client端把锁占住不放,然后,Redis在超时后把其释放掉。这样就很不靠谱了
举个例子
- 如果 Client A 先取得了锁。
- 其它 Client(比如说 Client B)在等待 Client A 的工作完成。
- 这个时候,如果 Client A 被挂在了某些事上,比如一个外部的阻塞调用,或是 CPU 被别的进程吃满,或是不巧碰上了 Full GC,导致 Client A 花了超过平时几倍的时间。
- 然后,我们的锁服务因为怕死锁,就在一定时间后,把锁给释放掉了。
- 此时,Client B 获得了锁并更新了资源。
- 这个时候,Client A 服务缓过来了,然后也去更新了资源。于是乎,把 Client B 的更新给冲掉了。
- 这就造成了数据出错。
要解决这个问题,我们可以引入fence(栅栏)技术。一般来说,这是乐观锁机制,需要一个版本号排它
我们从图中可以看到
- 锁服务需要有一个单调递增的版本号
- 写数据的时候,也需要带上自己的版本号
- 数据库服务需要保存数据的版本号,然后对请求做检查
如果使用ZooKeeper做锁服务的话,那么可以使用zxid
或znode
的版本号来做这个fence版本号
从乐观锁到CAS
在数据库中可以使用数据版本(Version)记录机制,即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的"version"字段来实现。当读取数据时,将 version 字段的值一同读出,数据每更新一次,对此 version 值加一。
这是乐观锁最常用的一种实现方式。是的,如果我们使用版本号,或是 fence token 这种方式,就不需要使用分布式锁服务了。
分布式锁设计的重点
一般情况下,我们可以使用数据库,Redis或者ZooKeeper来做分布式锁服务
分布式锁的特点是,保证在一个集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。这就是所谓的分布式互斥。
我们需要明确一下分布式锁服务的初衷和几个概念性问题
- 如果锁的进程挂掉了怎么办?一般的处理方法是在锁服务加上一个过期时间,如果这个时间内锁没有被还回来,那么锁服务要自动解锁,以避免全部锁住
- 如果锁服务自动解锁了,新的进程拿到锁了,但之前的进程以为自己还有锁,那么就出现了两个进程拿到同一个锁的问题,它们在更新数据的时候就会产生问题:像Redis那样也可以使用Check and Set的方式来保证数据的一致性,这就有点像CAS。那还需要分布式锁服务吗?的确是不需要,但现实生活中也有不需要更新某个数据的场景,只是为了同步或是互斥一下不同机器上的线程,这时候分布式锁就有意义了
如果确定要分布式锁服务,你需要考虑下面几个设计:
- 需要给一个锁被释放的方式,以避免请求者不把锁还回来导致死锁的问题。Redis使用超时时间,ZooKeeper可以依靠自身的sessionTimeout来删除节点
- 分布式锁服务应该是高可用的,而且需要持久化
- 要提供非阻塞方式的锁服务
- 还要考虑锁的可重入性