为什么使用分布式锁
为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行
分布式锁应该具备哪些条件?
- 1.在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
- 2.高可用的获取锁和释放锁
- 3.高性能的获取锁和释放锁
- 4.具备可重入特性
- 5.具备锁实效机制,防止死锁
- 6.具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
分布式锁的三种实现方式:
分布式的CAP理论告诉我们任何一个分布式系统都无法同时满足「一致性」(Consistency)、「可用性」(Availability)和「分区容错性」(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
1.基于数据库实现排他锁
- 悲观锁
利用select … where … for update 排他锁。阻塞的,容易锁表。 - 乐观锁
update version通过增加递增的版本号字段实现乐观锁。
基于redis实现
- 1.简化版setnx命令
if (jedis.setnx(lockKey, val) == 1) {
jedis.expire(lockKey, timeout);
}
很显然,加锁操作和后面的设置超时时间是分开的,并非原子操作。假如加锁成功,但是设置超时时间失败了,该lockKey就变成永不失效。
- 2.set(lockKey, requestId, "NX", "PX", expireTime)命令,该命令可以指定多个参数。
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
其中:
lockKey:锁的标识
requestId:请求id
NX:只在键不存在时,才对键进行设置操作。
PX:设置键的过期时间为 millisecond 毫秒。
expireTime:过期时间
满足了原子性,但是每次都要达到了超时时间才释放锁,显然也不是很合理,那么如何手工释放锁?
- 3.finally中释放锁 + 全局唯一标识
try{ String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
} finally {
if (jedis.get(lockKey).equals(requestId)) {
jedis.del(lockKey);
return true;
}
return false;
}
无论代码执行成功或失败了,都需要释放锁。如果有异常了,到达超时时间,锁还是会被redis自动释放。
而requestId是全局唯一的,保证了自己只能释放自己加的锁,不存在加锁和释放别人锁的情况。
-
4.redis+Lua
Redis+Lua,可以说是专门为解决原子问题而生。Lua专门整合原子操作。有了 Lua 的特性,Redis 才真正在分布式锁、秒杀等场景,有了用武之地。
为什么要用Lua脚本呢?因为一段复杂的业务逻辑,可以通过封装在Lua脚本中发送给Redis,保证这段复杂业务逻辑执行的原子性。 -
5.Redision的看门狗
如果锁达到了超时时间,但业务代码还没执行完怎么办?
看门狗可以通过定时任务不断刷新锁的获取事件,从而在用户获取锁到释放锁期间保持一直持有锁
eg:我们可以使用TimerTask类,来实现自动续期的功能:获取锁之后,自动开启一个定时任务,每隔10秒钟,自动刷新一次过期时间。这种机制就是redisson框架中的
watch dogTimer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run(Timeout timeout)throws Exception {//自动续期逻辑 } },
10000, TimeUnit.MILLISECONDS);
基于zookeeper实现分布式锁
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
- (1)创建一个目录mylock;
- (2)线程A想获取锁就在mylock目录下创建临时顺序节点;
- (3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
- (4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
- (5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
通常使用ZooKeeper的一个客户端Curator,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
总结:
数据库锁
- db操作性能较差,并且有锁表的风险
- 非阻塞操作失败后,需要轮询,占用cpu资源;
redis分布式锁:
- 主从切换的情况下可能出现多客户端获取锁的情况;
- Lua脚本在单机上具有原子性,主从同步时不具有原子性
基于Zookeeper的分布式锁:
- 需要引入Zookeeper集群,比较重量级;
- 具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
- 因为需要频繁的创建和删除节点,性能上不如Redis方式。
小结:
如果你的实际业务场景,更需要的是保证数据一致性。那么请使用CP类型的分布式锁,比如:zookeeper
如果你的实际业务场景,更需要的是保证数据高可用性。那么请使用AP类型的分布式锁,比如:redis;
通常redis足够用了,通过最终一致性大部分即可满足需求了,强一致性实时数据还是要依赖数据库的事务。