分布式锁 Redis vs etcd

为什么要实现分布式锁?
  • 了解分布式锁的底层工作原理,以及如果在工程中应用,我们应该如何选择.
为什么需要分布式锁,分布式锁的作用是什么,哪些场景会使用到分布式锁?

为什么需要分布式锁

  • 解决分布式业务需要互斥的问题(同时只能有一次客户端的一个线程可以执行的场景),保护共享数据
  • 具体场景: 支付业务(避免多端同时支付),消息队列的消费(避免多端同时消费产生重复消费的问题),服务器的偏执业务(有些业务(数据同步)只需要一个服务器执行,不需要全部都开启,这时候可以使用分布式锁,拿到锁的执行,其他就不执行)

分布式锁的作用

  • 分布式实现线程互斥,确保只有一个线程能够执行

分布式锁的使用场景

  • 需要分布式线程互斥逻辑的场景都可以使用分布式锁
分布式锁的实现方式有哪些
  • redis 分布式锁
  • etcd 分布式锁
  • ...
分布式锁的核心原理是什么
  • 互斥性:确保只有一个程序的一个线程可以获取到锁
  • 死锁预防: 如果程序意外终止会释放锁而不是持续占用锁资源,导致死锁
    • 实例自动回收(过期),客户端定时续期来实现
  • 锁的公平竞争: 避免锁被饿死

如何实现分布式锁

redis(自旋锁版本)
  • 原子性保证

    • redis 单线程本身的读写都是原子的,没有并发问题
    • 对相同的 key 使用 setnx(set not exit 只会在不存在的时候设置成功,如果存在表示锁被占用的情况则会返回 0(获取锁失败))
  • 死锁预防:

    • 使用 redis 过期机制实现

    • 加锁: 使用合成命令或者 lua 脚本保证加锁的原子性

      • 合成命令: setnx k v ex ex_time 一次性在获取锁的时候就设置完成过期时间
      • 将获取锁与设置锁关过期时间的两条 redis 命令写到一个 lua 脚本中保证锁设置的原子性
        • 可不可以将其拆分成为两步:先抢锁(setnx),在设置过期时间(setex)
          • 不可以:因为设置过期时间可能有失败的风险(比如网络问题导致失败,那么这个可以有死锁的风险
      • 续期:
        • 如果加锁成功则立即开启定时任务(ticker),定时更新过期时间,直到锁的释放(接收到 delete 信号)
        • 避免信号延迟或者丢失造成锁的自动过期导致的原子性被破坏(多个线程同时获取到锁):一个过期时间内将会发送三次续期的心跳,只要任何一个续期请求到达 redis 都可以续期成功
    • 锁的公平性:
      • 所有的锁都以自旋的方式获取锁,公平的竞争锁
        • 自旋时间间隔随自旋的次数增加而减少(每次自旋减少 1ms,最低 10ms 自旋一次),所以阻塞时间越久的线程获取到锁的概率越高,防止锁被饿死.
      • 问题,再大量并发的情况下,依旧有被饿死的风险
    • 锁的误删避免: 确保每个线程只能删除自己的锁不能删除其他线程的锁
      • 如果锁的删除命令因为网络等原因导致延迟到达(其他线程获取了锁,这时删除命令到达,会误删去他的锁,破坏锁的原子性)
      • 使用唯一 value + lua 脚本进行实现
        • 使用 lua 脚本在保证原子性的情况下删除自己的 key
          • 加锁时 value 在客户端生成唯一标识(可以使用 uuid)
          • 释放锁的时候:lua(检查 redis 中value 是否与客户端的 value 相同,如果相同就删除,如果不同证明自己已经不持有该锁,就不做任何操作)
etcd 的分布式锁(互斥锁(信号控制)版本)

etcd 没有像 redis 那样 setnx 的命令,但是他的 Revision(版本号)与 watch 机制可以实现锁

  • 原子性: 保证只有一个线程获取到锁
    • 预写: 所有获取锁的线程都会先将 k-v 写到 etcd 中,etcd 会给每个写操作都迭代一个递增的版本号(获取锁的基本原理就是所有 kv 中最小的为当前持有锁)(有问题)
    • 试加锁: 再检查自己的版本号是否是列表中最小的(如果是就获取到锁,直接返回)
    • 等待锁 :等待自己前面的锁全部释放,直到自己是 kv 对中版本最小的一个
      • 使用 watch 监听自己前面的删除,直到自己前面已经没有任何 kv 对(有点问题在里面)
    • 再检查: 检查自己的kv 对是否在队列中,如果不在就不能加锁,因为不知道被谁删除了,或者过期了,就自己返回错误)(因为你的 k-v 不在 etcd 中的话,你的后面一个就会发现自己是最小的,如果你也运行程序,那么就会有两个线程同时获取到锁,破坏锁的原子性,所以要先检查再获取锁)
  • 防止死锁:
    • 租约 : etcd 有自己的租约,每一个 k-v 设置的时候就会绑定租约(与 redis 的过期时间+心跳续期的原理类似,只是这个是隐式的,在会话中实现的)
    • 当锁释放的时候就会停止续租并删除 k-v对(在后面的线程就会监听到他的删除)
  • 锁的公平性:(版本号 )
    • etcd 锁是天然公平的,是通过每个 kv 的版本号控制的
    • 因为版本号是递增的,那么整个锁就形成了一个先进先出的队列模式,只有到达队头(版本号最小)的线程会获取到锁执行业务逻辑,其他线程都是阻塞等待的状态
  • 锁的误删:
    • etcd 中每个锁都只持有自己的 key,所以只能对自己的 k-v 对进行操作,不会出现误删的情况.

分布式锁对比

redis vs etcd

性能: 吞吐量

redis: 性能本身就更好,redis 有 20w 并发

redis 的性能瓶颈在网络 io,Reds 使用的 tcp 的协议,io 的性能就是 20 万

etcd 要略低只有 10 多万作用

etcd 使用的 grpc 的架构,底层是 http2 的传输协议; grpc 的性能是 15 万的 qps,所以 etcd 的性能只有 15 万左右

强一致性

Redis 分片的意外崩溃可能会导致锁的原子性被破坏

解决方法:redlock 模式

etcd 集群本身具有强一致性,不需要担心单个实例的崩溃破坏锁原子性的问题



服务器的影响:

redis 使用的自旋的形式获取锁,会消费一定的服务器 cpu

etcd 是等待信号通知的形式(watch),不需要自旋与循环,对服务器性能影响小


实例储存的数据量

Redis 一个 lock 只需要维护一个 k-v 对,储存数据少,但是 Redis 可能会处理大量自旋请求.

etcd 每一个线程的获取锁都是一个 k- v 对,储存的数据多,并且要维护所有线程的连接,开销大,不能同时接受太多获取锁的请求,否则会资源耗尽(连接)(如果并发数量高,并且有线程长期持有锁可能会导致连接的大量堆积)

总结

  1. 当我们不需要很强的一致性,但是需要比较高的性能的时候,大量并发获取锁的情景下我们可以选择 Redis 的分布式锁
  2. 如果我们需要强一致,高性能,高并发的场景的话我们可以使用 Redis 的 redlock 模式
  3. 如果我们需要强一致,但是性能不需要太高,并且并发数量并不高的情况,可以选择使用 etcd 的分布式锁

参考:

https://blog.csdn.net/qq_16399991/article/details/130732780

https://time.geekbang.org/column/article/350285

https://blog.csdn.net/boonya/article/details/117307663

相关推荐
WnHj3 小时前
kafka的数据消费通过flinksql 入数到Doris的报错(Connection timed out)
分布式·kafka
我真的是大笨蛋12 小时前
Redis的String详解
java·数据库·spring boot·redis·spring·缓存
Java战神14 小时前
Hadoop
大数据·hadoop·分布式
zhengzizhe14 小时前
Redssion出现attempt to unlock lock, not locked by current thread by node id
redis
不会写代码的加加16 小时前
告别重构噩梦:基于 Oinone 实现单体到微服务的平滑演进
spring boot·分布式
szxinmai主板定制专家17 小时前
RK3588+AI算力卡替代英伟达jetson方案,大算力,支持FPGA自定义扩展
arm开发·人工智能·分布式·fpga开发
兜兜风d'18 小时前
redis字符串命令
数据库·redis·缓存
西瓜er19 小时前
Docker 一键部署指南:GitLab、Nacos、Redis、MySQL 与 MinIO 全解析
redis·docker·gitlab
Pota-to成长日记19 小时前
Redisson 看门狗机制深度解析:分布式锁的守护者
分布式·wpf
道可到20 小时前
别再瞎拼技术栈!Postgres 已经能干 Redis 的活了
redis·后端·postgresql