diva-notes
  • README
  • Ads
    • 定价策略
    • 广告层级
    • 归因模型
    • 买量
    • Chat GPT
    • Google
  • AI
    • 参考资料
    • Chat GPT
    • stable-diffusion-webui安装
  • Algorithm
    • 倍增
    • 并查集
    • 参考
    • 环的判断
    • 凸包
    • 蓄水池抽样
    • 最短路径
    • 最小生成树
    • KMP算法
    • Rabin-Karp算法
    • Tarjan桥算法
  • Architecture
    • Serverless
  • Career
  • CICD
    • 代码质量
    • CICD实践
  • Data Structure
    • 布谷鸟过滤器
    • 布隆过滤器
    • 浮点
    • 红黑树
    • 锁
    • LSM树
  • DB
    • My SQL
      • 隔离级别
      • 架构
      • 索引
      • 锁
      • 页结构
      • 主从同步
      • ACID
      • Log
      • MVCC
      • Questions
    • Postgres
      • 持久化
      • 对比MySQL
      • 隔离级别
      • 索引
      • Greenpulm
      • MVCC
    • 倒排索引
    • 列式存储
    • H Base
    • HDFS
    • MPP数据库选型
    • Questions
  • Distributed System
    • 分布式事务
    • 服务网格
    • BASE理论
    • CAP
    • Etcd
    • Raft协议
    • ZAB协议
  • Go
    • 1.语言基础
      • 1.CPU寄存器
      • 2-1.函数调用
      • 2-2.函数调用栈
      • 2.接口
      • 3.汇编
      • 4.调试
    • 2.编译
      • 1.编译
      • 2.词法与语法分析
      • 3.类型检查
      • 4.中间代码生成
      • 5.机器码生成
    • 3.数据结构
      • 1.数组array
      • 2.切片slice
      • 3.哈希表map
      • 4.字符串
    • 4.常用关键字
      • 1.循环
      • 2.defer
      • 3.panic和recover
      • 4.make和new
    • 5.并发编程
      • 1.上下文Context的实现
      • 2-1.runtime.sema信号量
      • 2-2.sync.Mutex的实现
      • 2-3.sync.WaitGroup
      • 2-4.sync.Once的实现
      • 2-5.sync.Map的实现
      • 2-6.sync.Cond
      • 2-7.sync.Pool的实现
      • 2-8.sync.Semaphore的实现
      • 2-9.sync.ErrGroup
      • 3.定时器Timer的实现
      • 4.Channel的实现
      • 5-1.调度-线程
      • 5-2.调度-MPG
      • 5-3.调度-程序及调度启动
      • 5-4.调度-调度策略
      • 5-5.调度-抢占
      • 6.netpoll实现
      • 7.atomic
    • 6.内存管理
      • 1-1.内存分配基础-TCmalloc
      • 1-2.内存分配
      • 2.垃圾回收
      • 3.栈内存管理
    • 参考
    • 各版本特性
    • 坑
    • Go程序性能优化
    • http.Client
    • net.http路由
    • profile采样的实现
    • Questions
    • time的设计
  • Kafka
    • 高可用
    • 架构
    • 消息队列选型
    • ISR
    • Questions
  • Network
    • ARP
    • DNS
    • DPVS
    • GET和POST
    • HTTP 2
    • HTTP 3
    • HTTPS
    • LVS的转发模式
    • NAT
    • Nginx
    • OSI七层模型
    • Protobuf
    • Questions
    • REST Ful
    • RPC
    • socket缓冲区
    • socket详解
    • TCP滑动窗口
    • TCP连接建立源码
    • TCP连接四元组
    • TCP三次握手
    • TCP数据结构
    • TCP四次挥手
    • TCP拥塞控制
    • TCP重传机制
    • UDP
  • OS
    • 磁盘IO
    • 调度
    • 进程VS线程
    • 零拷贝
    • 内存-虚拟内存
    • 内存分配
    • 用户态VS内核态
    • 中断
    • COW写时复制
    • IO多路复用
    • Questions
  • Redis
    • 安装
    • 参考
    • 高可用-持久化
    • 高可用-主从同步
    • 高可用-Cluster
    • 高可用-Sentinel
    • 缓存一致性
    • 事务
    • 数据结构-SDS
    • 数据结构-Skiplist
    • 数据结构-Ziplist
    • 数据结构
    • 数据类型-Hashtable
    • 数据类型-List
    • 数据类型-Set
    • 数据类型-Zset
    • 数据淘汰机制
    • 通信协议-RESP
    • Questions
    • Redis6.0多线程
    • Redis分布式锁
    • Redis分片
  • System Design
    • 本地缓存
    • 错误处理
    • 大文件处理
    • 点赞收藏关注
    • 短链接生成系统
    • 负载均衡
    • 高并发高可用
    • 规则引擎
    • 集卡活动
    • 秒杀系统
    • 评论系统
    • 熔断
    • 限流
    • 延迟队列
    • Docker
    • ES
    • K 8 S
    • Node.js
    • Questions
  • Work
    • Bash
    • Charles
    • Code Review
    • Ffmpeg
    • Git
    • intellij插件
    • I Term 2
    • Mac
    • mysql命令
    • Nginx
    • postgresql命令
    • Protoc
    • Ssh
    • Systemd
    • Tcp相关命令
    • Vim
Powered by GitBook
On this page
  • Redis 单节点方式实现
  • Redlock实现方案
  1. Redis

Redis分布式锁

实现分布式锁有很多方案,例如基于数据库实现,基于 zookeeper 实现,如果吞吐量还是不能满足,比较广泛的做法是用分布式缓存来实现。

Redis 单节点方式实现

核心就是围绕 SETNX(SETIF NOT EXISTS) 实现

当 key 不存在时返回 1,当 key 存在时返回 0。因为我们都知道 redis 是单线程的,所以在 redis 服务侧不会有线程安全问题。当返回 1 的时候认为获得锁成功,可以进行相应的业务处理,处理完成后,删除 key,释放锁,当返回 0 时表示失败

1. 超时问题

如果线程 A 拿到了锁,去处理业务的过程中,发生阻塞,例如数据库执行比较慢,或者服务发生故障了,这时候如果没有超时时间,系统将永久的死锁。所以 setnx 可以接受第三个参数,也就是超时时间。

这里面存在问题,因为你并不知道超时的情况下,业务到底有没有处理成功,还有没有在继续进行。所以,这里的锁并不是绝对的。

  • 第一种解决方法就是靠程序员自己去把握,预估一下业务代码需要执行的时间,然后设置超时时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。但是这并不是万全之策,比如网络抖动这种情况是无法预测的,也有可能业务代码执行的时间变长,所以并不安全。

  • 有一种方法比较靠谱一点,就是给锁 续期。在 Redisson 框架实现分布式锁的思路,就使用 watchDog 机制实现锁的续期。当加锁成功后,同时开启守护线程,默认有效期是 30 秒,每隔 10 秒就会给锁续期到 30 秒,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。

2. 如何释放锁?

直接 del 可以吗?答案是否定的。

service1 删除自己的锁的时候,实际删掉的可能是锁超时后,service2 刚加的新锁,造成误删。

解决方案就是 setnx 的时候在客户端生成一个随机值作为 value。删除时判断 value 是否等于当前任务持有的随机值。当然,这个地方必须是原子的,否则,判断到删除之前还是有可能发生变化。可以通过 lua 脚本来实现。

除了轮询,还可以通过 redis 的 pub-sub 模式订阅锁的释放信息。

3.单点问题

还有另外一个问题,redis 是单点的,如果 redis 一旦挂掉,整个全玩完。

有人说,可以用 master-slave 啊,但是一样,如果 master 刚加锁、数据还未同步到从节点上就挂掉,则此时从节点升为主节点,但从节点上是没有锁的。

Redlock实现方案

Redlock 是 redis 作者 antirez 大神在 redis 官网中给出的一种基于 redis 的分布式锁方案。直白点说,就是采用 N(通常是 5)个独立的 redis 节点,在全部节点上同时setnx,如果超过半数节点成功,就拿到了锁,这样就可以允许少数节点挂掉了。整个取锁、释放锁的操作和单节点类似。

不过,是不是这样就完美了呢?当然不是。

1. 重启问题

假设一共有5个节点(A/B/C/D/E),service1 成功获取了锁,注意这里,service1 在 A/B/C 上 setnx 成功,但是并没有在 D/E 上成功,如果 C 节点挂掉重启了,且 C 节点并没有持久化,这时候 service2 也可以成功锁住 C/D/E,导致锁失效。

有人说,那 C 持久化不就行了吗,实际上设置为同步的持久化方式对性能影响比较大。也就是常说的 appendfsync always,如果是机械硬盘,吞吐量可能会从 10万级降到几百。有人说用固态硬盘不就解决了吗?土豪是可以用的,吞吐量确实能到万级,但是大量的、频繁的写入容易导致写入放大。

这个问题的解决方案也非常简单。就是延迟重启,就是等到挂掉节点上的所有锁都过期了再重启,重启后,以前的锁都已经失效。(有些扯淡

2. 加锁失败

如果一个节点获取锁成功了,且获取详情为:四个节点都 setnx 成功,一个失败了,失败的情况是:发起请求成功,在返回 ack 的时候失败。这时候客户端会认为是失败了,而删除锁的时候没有删除这个节点,这就会导致过期之前,这个节点获取锁一直失败。

正确的做法是,解锁的时候,无论该节点之前 setnx 成功还是失败,都应该执行一次删除操作。

参考

PreviousRedis6.0多线程NextRedis分片

Last updated 2 years ago

Magic Kaito - 深度剖析:Redis分布式锁到底安全吗