分布式锁🔒是个啥❓ 其实就这么点事

用户头像
山中兰花草
关注
发布于: 2020 年 05 月 30 日
分布式锁🔒是个啥❓ 其实就这么点事

聊聊分布式锁,本文将以redis 为实现方式

Q:为什么会存在分布式锁

不同的进程需要用互斥的方式对共享的资源进行访问。

分布式锁示意图



Q:加锁有什么好处

  • 效率:加锁可以避免我们不必要的重复执行一个操作。比如说同一个计数器触发两次。

  • 正确性:加锁可以防止数据被并发的修改,避免了数据的损坏、丢失、不一致之类的情况。

Q:实现分布锁可以使用什么

  1. mysql

  2. zookeeper

  3. redis

Q:实现锁的时候需要注意什么

  1. 互斥:同一时间只能有一个进程持有锁。

  2. 容错:只要大多数redis节点启动,客户端就能获取和释放锁。

  3. 无死锁:即使加锁的客户端崩溃,其他客户端最终也是可以获取锁的。

Q:redis实现加锁🔐

  • redis加锁主要是通过 setnxhttps://redis.io/commands/setnx 进行操作,setnx 是 SET if Not eXists 的缩写

  • SETNX : 如果 key 不存在,就通过key 来设置value保存字符串,操作返回结果1, 在这种情况下操作等同于set。但是如果 key 存在时再进行 SETNX 操作,value 不会改变,操作返回结果0

setnx

当操作需要加锁的时,通过 setnx 进行赋值,如果返回1,说明加锁成功,执行完操作之后再进行del操作,以便其他进程使用。

@Test
Boolean distributedLocks() {
if (redisCli.setnx("hello","word") != 1) {
log.info("==加锁失败==");
return false;
}
redisCli.expire("hello",2); // 设置过期时间,防止死锁
// do something
try {
Thread.sleep(2000);
}catch (InterruptedException e) {
e.printStackTrace();
}
redisCli.del("hello"); // 执行完毕,删除锁
return true;
}

但是这样写是存在问题的,如果在 redisCli.setnx("hello","word") 设置时,客户端崩溃代码还没来得及设置过期时间,就会产生死锁

所以需要改进加锁的代码,可以通过 set https://redis.io/commands/set命令的第三个参数进行过期时间和不可更改的设置。

SetParams params = SetParams.setParams();
params.nx(); // 当key不存在时才能设置
params.ex(2); // 过期时间。second
if (!redisCli.set("hello","word",params).equals("OK")) {
log.info("==加锁失败==");
return false;
}



但是这样还是存在问题,如果进程a加锁成功,但是执行具体业务逻辑的代码超过了设置的过期时间,这时候锁过期失效,进程b就可以加锁成功。如果这时进程a执行完成也是可以删锁的,尽管现在锁属于进程b。所以需要对应每个进程加锁时进行区分,防止这种误删的操作出现。

可以通过设置一个唯一的uuid为value,保证删除时的准确性。

@Test
Boolean distributedLocks() {
// 一个唯一标示
String uuid = UUID.randomUUID().toString();
SetParams params = SetParams.setParams();
params.nx(); // 当key不存在时才能设置
params.ex(2); // 过期时间。second
if (!redisCli.set("hello",uuid,params).equals("OK")) {
log.info("==加锁失败==");
return false;
}
// do something
try {
Thread.sleep(2000);
}catch (InterruptedException e) {
e.printStackTrace();
}
if (redisCli.get("hello").equals(uuid)) {
redisCli.del("hello"); // 执行完毕,删除锁
return true;
}
return false;
}

如果这么写的话,还是不完美的,在删除锁的时候是两步操作,不符合原子性。像这种操作可以根据实际需要决定是不是可以容忍的,毕竟就算是删除没有执行成功,锁还是会自动过期的。如果不能容忍的话,可以通过一段Lua代码根据key和value决定要不要删除,保证客户端调用的原子性。

if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

关于加锁的失败的逻辑,你可以加锁失败后直接返回,也可以通过一个for 循环设置重试次数,当然也可以用一个while 循环直至加锁成功,具体实现可以根据业务逻辑自行判断。

Q:这样实现就是完美的了吗🤔️

以上的节点在单机的redis上是适用的,但是如果存在redis主从节点就会有问题。如果在集群中master节点由于某种原因发生了切换,可能会出现锁丢失的情况。试想这种情况:

  1. master 节点已经加锁成功

  2. 基于redis的复制是异步的,这个锁有可能还没来得及同步到slave节点上

  3. master发生故障挂了,故障转移,slave节点升级成为master

  4. 锁丢了,可能导致多个客户端可以加锁成功

面对这种情况,可以参考redis作者提出的更高级的实现方式redLockhttps://redis.io/topics/distlock

Q:什么是 redLock

  • redLock 是一种算法,redis官方推荐的Java实现 Redissonhttps://github.com/redisson/redisson中就实现了这种算法

  • 实现redLock 的前提是我们拥有N个master 节点,这些节点是相互独立的,互不干扰、独立运行

  • 以同时拥有5个节点为例,为了获取锁,客户端需要进行:

  1. 获取当前的时间(毫秒为单位

  2. 客户端将依次对5个节点使用相同的key、唯一的value以及附加参数 通过set 尝试获取锁。当客户端向节点尝试获取锁时,应该设置一个网络响应和超时的时间,这个时间应该小于锁的失效时间。假如说你的锁的实效时间是10秒,那么网络响应和超时的设置应该在5-50毫秒之间。这样可以防止客户端长时间和处于故障的节点通信。如果某个节点不可用,应该尽快与下一个节点进行通信

  3. 客户端获取当前时间,减去第1步的时间,就可以计算出获取锁时花费的时间,当且仅当大多数节点(N / 2 + 1,这里是3)获取到了锁,并且花费的时间小于锁的过期时间,锁才算获得成功。

  4. 如果客户端获取锁成功,那么锁的有效时间就是第1步的时间 减去 第2步流程的时间,就是第3步说的

  5. 如果客户端由于某种原因没有获取到锁(成功的节点少于大多数,或者获取有效时间为负),那客户端需要尝试解锁所有节点(即使有的节点根本没加锁成功)

redLock
  • 对接redLock 的理解可以是:既然单节点不可靠,那我就多放几个节点,各个节点相互独立,没有从属关系,即使偶尔几个节点挂掉了,只要保证大多数节点能请求成功,那么加锁流程就没有问题。这样的5个节点,即使4和5 都挂了,只要123能请求成功,满足大多数条件,没有超时,加锁还是可以成功的。

Q:关于redLock 的爱恨纠葛⚔️

Martin Kleppmann(《设计数据密集型应用》的作者)曾经写文章https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html 分析了redLock的缺点,主要集中在这几点:

  1. redLock 严重依赖服务器时间

  2. redLock 没有保证锁的正确性

redis的作者 antirez 也写文章回击http://antirez.com/news/101,就几个问题提出了解决方案,想了解的同学可以自行阅读。

Q:简单聊聊Redisson

Q:说在最后

  • redis 的分布式锁只看谁能加锁成功,如果不成功,要么进行重试,要么直接返回。如果希望所有的请求都以排队的形式进行等待,按照顺序一个一个处理,zookeeper无疑是最适合的。



发布于: 2020 年 05 月 30 日 阅读数: 61
用户头像

山中兰花草

关注

人需要的归属感 永远在他的归属地 2018.06.30 加入

还未添加个人简介

评论

发布
暂无评论
分布式锁🔒是个啥❓ 其实就这么点事