在谈分布式锁之前,需要明确分布式锁解决了什么问题。众所周知,锁是为了解决对于单机多线程的并发竞争问题,但是对于微服务而言,需要控制集群的并发操作,而单机锁的维度太小,显然无法实现功能,这时候就需要一个维度横跨整个集群的锁来控制节点间的并发竞争。
对于单机并发而言,普通的并发锁可以覆盖整个应用程序。同样的,对于集群而言,分布式锁需要能够覆盖整个集群,也就是说分布式锁最少需要满足以下条件:
- 能够被集群所有节点访问
- 具备数据存储和查询功能
- 具备一定的可靠性
显然,数据库类产品最适合做分布式锁,目前常见的分布式锁也均是数据库类产品,可以分为以下三类:
- 基于数据库的分布式锁
- 基于zookeeper的分布式锁
- 基于缓存的分布式锁
前两种在这里不做过多介绍,主要重点介绍基于缓存的分布式锁
分布式锁的设计思路
在谈论分布式锁的设计思路之前,先来谈谈单机锁的设计思路。对于常见的单机锁而言,无论是什么设计实现,其根源可以归纳为:
在执行操作前先获取对应信号量,获取成功后再执行操作,执行完毕释放信号量
整个流程可以用以下模板代码表示,其中不同的分布式锁只需要实现tryLock
和unlock
方法即可:
while (true) {
if (tryLock(key)) {
try {
doSomething();
} finally {
unlock(key);
}
}
}
对于分布式锁而言,理念是通用的,设计核心就在于信号量机制。信号量数据需要具有唯一性,可以进行增删改查,在一些特殊场景中还需要可以过期。对基于缓存的分布式锁而言,可以使用键值来作为这样的信号量
分布式锁的实现
基于key唯一性的实现
通过缓存key可以很容易地实现锁机制,利用key的唯一性,在执行操作前,先检查缓存中是否存在对应的key,如果不存在,则创建对应的key,当操作执行完毕再删除该key。具体的实现代码如下*(这里的cacheService只是抽象的缓存服务,如果使用请换成公司内部或市场同类产品)*:
boolean tryLock(String key) {
// 这里的2指的是版本号
Result result = cacheService.put(key, "", 2, EXPIRE_TIME);
return result.isSuccess();
}
void unlock(String key) {
cacheService.invalid(key);
}
这里的put操作涉及到一个version的问题,使用了version来避免并发更新冲突。以put操作为例,在put时,会检查传入的version与缓存中key对应的version是否一致,如果不一致则返回错误码。这里传入的version值需要为大于1的数,即不能为0或1。当version为0时会强制覆盖,无法做到检查缓存key是否存在的作用;当version为1时,在第一次put请求后的第二次put请求总是会成功,也不符合期望。
基于自增/自减限值的实现
针对自增/自减操作,如果对应的cache服务具备手动设置上限值的能力,则可以采用这种方式方便的实现分布式锁。通过手动设置对应的上限/下限值,当超过了界限值之后,再调用自增/自减操作就会报错
boolean tryLock(String key) {
//方法签名:Result incr(key, value, defaultValue, expireTime, lowBound, upperBound)
Result result = cacheService.incr(key, 1, 0, EXPIRE_TIME, 0, 1);
return result.isSuccess();
}
void unlock(String key) {
cacheService.decr(key, 1, EXPIRE_TIME);
}
上面即为自增操作的实现方式,自增一次之后就达到了限值,再次自增之后就会报错,从而通过自减操作来释放锁,也是一种常见的实现。