Redis - 分布式锁

目录

1. 分布式锁

2. 分布式锁的基础实现

3. 引入过期时间

4. 引入校验 id

5. 引入 lua

6. 引入 watch dog(看门狗)

7. 引入 Redlock 算法


1. 分布式锁

在一个分布式系统中,也会涉及多个节点访问同一个公共资源的情况,此时就需要通过加锁来做互斥控制,避免出现类似于"线程安全"的问题

在 Java 中的 synchronized 只能在当前进程中生效,在分布式这样多个进程多个主机的场景下就无能为力了,因此就需要分布式锁来解决这种问题

2. 分布式锁的基础实现

本质上就是通过一个键值对来标识锁的状态

例如在买票的场景中,车站提供了若干个车次,且每个车次的票数都是固定的,现在存在多个服务器节点,都可能需要处理买票的逻辑:先查询指定车次的车票,如果票 > 0,则设置票数 -=1

当客户端 1 先执行查询操作,发现票剩余 1 张,在执行票数 -=1 之前,客户端 2 也执行查询操作,发现票数剩余 1 张,也开始执行 -=1 操作,这时候就会出现"超卖"的情况,此时就会有线程安全的问题,因此需要用到锁来控制

加锁的方式就是在上述架构中引入一个 Redis 作为分布式锁的管理器

此时如果服务器 1 尝试买票,就需要先访问 redis,在 redis 上设置了一个键值对,其中 key 就是车次,而 value 就设置成 1,如果这个操作设置成功,就是为当前没有节点对 001 车次加锁,就可以进行数据库的读写操作,操作完成之后,再把 redis 上的这个键值对删除

当服务器 1 买票,也就是操作数据库的过程中,服务器 2 也想买 001 这个车次的票,服务器 2 久长时给 redis 上写一个键值对,用来加锁,但是 key 时同样的车次,此时设置的时候发现 key 已经存在了,则认为其他服务器正在持有锁,此时服务器 2 就需要等待或者暂时放弃

而 redis 中的 setnx操作就是和这个从场景,即 key 不存在才设置,存在直接失效

3. 引入过期时间

在上述中,当服务器 1 加锁之后,开始处理买票的过程,如果服务器 1 出现宕机,就会导致解锁操作(删除 key)不能够执行,就可能会引起其他服务器无法获取到锁的情况,要想解决这个问题,可以在设置 key 的时候同时引入过期时间,即这个锁最多持有多久就应该被释放

在 redis 中 set ex nx 和 setnx expire 可以解决这个问题,它们都是在设置的同时把过期时间也设置进去,但是此处只能使用 set ex nx 命令

因为在 redis 中,多个命令之间是无法保证原子性的,万一出现第一个命令成功,第二个命令失效的情况

4. 引入校验 id

对于 redis 中写入的加锁键值对,其他节点也是可以删除的

例如服务器 1 写入一个 "001":1 这样的键值对,服务器 2 是可以把 "001" 这个 key 给删除的,为了解决上述问题,可以引入一个校验 id

比如可以把设置的键值对的值,不设为 1,而是射程服务器的编号,例如 "001":"服务器 1",这样就可以在删除 key 的时候,先校验当前删除 key 的服务器是不是最开始加锁的服务器,如果是再删除

伪代码如下:

String key = [要加锁的资源 id];
String serverId = [服务器的编号];

// 加锁, 设置过期时间为 10s
redis.set(key, serverId, "NX", "EX", "10s");

// 执⾏各种业务逻辑, 比如修改数据库数据
doSomeThing();

// 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配


if (redis.get(key) == serverId) {
redis.del(key);
}

这样就可以解决上述问题,但是解锁逻辑中的 "get" 和 "del" 操作并非是原子的

5. 引入 lua

为了使解锁操作是原子的,可以使用 redis 的 lua 脚本功能来完成解锁

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

        return 0
end;

上述代码可以编写成一个 .lua 后缀文件,把这个脚本上传到 redis 服务器上,然后可以让客户端来控制 redis 执行上述脚本,redis 执行脚本的过程是原子的,相当于执行一条命令一样

6. 引入 watch dog(看门狗)

上述方案中,当我们设置了 key 过期时间之后(比如 1s),仍然存在当任务还没执行完,key 就先过期了,这就导致锁提前失效

所谓的 watch dog 本质上是加锁的服务器上的一个单独的线程,通过这个线程来对锁过期时间进行
"续约"

例如,初始情况下的设置的过期时间是 1s,同时设定看门狗每隔 300ms检测一次,当 300ms 时间到了时,看门狗就会判定当前任务是否完成

如果任务已经完成,直接通过 lua 脚本的方式,释放锁(删除 key)

如果任务未完成,则把过期时间重新设置 1s(即 "续约")

这样就不必再担心锁提前失效的情况,除此之外,如果服务器挂了,看门狗线程也就挂了,此时无人续约,这个 key 自然就可以迅速释放,让其他线程能够获取到锁了

7. 引入 Redlock 算法

在实践中的 redis 一般是以集群的方式部署的,那么就可能出现以下极端的情况

服务器 1 向 master 节点进行加锁操作,这个写入 key 的过程中,master 挂了,slave 节点升级成了新的 master 节点,但是由于刚才写入的这个 key 还没来得及同步给 slave 节点,此时就相当于服务器 1 的加锁操作没有执行,服务器 2 仍然可以进行加锁

为了解决这个问题,redis 作者提出了 Redlock 算法

引入一组 redis 节点,其中每一组 redis 节点都包含一个主节点和若干个从节点,并且组和组之间存储的数据都是一致的,相互之间是 "备份" 的关系,加锁的时候,按照一定的顺序,写多个 master 节点,在写锁的时候需要设定操作的 "超时实践",比如 50ms,即 setnx 操作超过了 50ms还没有成功就视为加锁失败

如果给某个节点加锁失败,就立刻尝试给下一个节点加锁,当加锁的节点数超过总结点数的一般,才视为加锁成功,上图中出现三个节点加锁成功,才视为成功,这样即使某些节点挂了,也不影响加锁的正确性 

简而言之,Redlock 算法的核心就是加锁操作不能只给一个 redis 节点,而是要写多个,在分布式系统中任何一个节点都是不可靠的,最终的加锁成功结论就是 "少数服从多数",由于分布式系统不太可能会出现大部分节点都同时出现故障,因此这样的可靠性要比单个节点来说可靠的多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值