分布式锁和数据一致性的讨论——redis集群做分布式锁的风险

文章目录

写在前面

分布式锁的应用场景就是高并发,高并发下如果锁出了任何问题,就可能会导致脏数据的产生,那么,用redis做分布式锁真的安全吗?

分布式锁的三个属性

  • 互斥(Mutual Exclusion):同⼀时刻只有⼀个客户端持有锁
  • 避免死锁(Dead lock free):设置锁的存活时间(Time To Live,TTL)
  • 容错(Fault tolerance):避免单点故障,锁服务要有⼀定容错性

分布式锁就⼀定要实现这三个属性吗?

未必!

如果你的业务⽆关紧要,如果你的业务是可以挂掉的内部系统,如果你的业务可以接受出错的时候,直接返回错误给⽤户,那⼀个单节点 Redis 或关系型数据库的分布式锁就能满⾜你的需求。

如果你的业务不允许随意宕机,那我们就要来好好讨论容错性了。

实现容错性

讨论"分布式",意味着可能会发⽣各种各样的错误,Google 公布的数据显示:

参考引⽤:research.google.com/people/jeff...

方法一:基于多个 Redis 节点实现分布式锁

加锁:

1.依次对多个 Redis 实例进⾏加锁,使⽤单实例 Redis 的加锁命令;

2.每次获取锁的超时时间远⼩于 TTL,超时则认为失败,继续向下⼀个节点获取锁;

3.计算总消耗时间,只有在超过半数节点都成功获取锁,并且总消耗时间⼩于 TTL,才认为成功持有锁;

4.成功获取锁后,要重新计算 TTL = TTL - 总消耗时间;

5.如果获取锁失败,要向所有 Redis 实例发送解锁命令。

解锁:

1.删除所有实例中的 key

也就是我们说的-红锁(RedLock),实现起来其实挺麻烦的,但是,实现了RedLock又引发了其他的问题。

问题一:进程可能会被挂起,直到锁的 TTL 过期

当客户端1获取到第一个锁之后,程序恰好进行GC了stop-the-world(或者其他原因导致的挂起),而这个时间恰好又超过了过期时间,就会造成客户端2获取不到锁。

该问题可以延长TTL,但是治标不治本,仍有系统挂起超时的隐患。

问题二:墙上时钟在分布式系统中不可靠

计算机科学家类Leslie Lamport的逻辑时钟论文中,提出了物理时钟在分布式系统中并不可靠,不能用物理时钟来判断事件的先后顺序。

  • 分布式集群靠 NTP 时钟同步,但仍未能保证每台机器⾛时相同
  • 时间戳不可靠:aphyr.com/posts/299-t...
  • Google Spanner 设计了⼀套复杂的时间机制(TrueTime)来实现强⼀致性

时钟漂移问题是真实存在的

也就是说,RedLock虽然避免了单点故障,但是有着一定程度上的时间不一致问题,仍会导致高并发下锁的不一致问题。

⽅法⼆:复制(Replication)

  • 不依赖多个 Redis 节点,数据存储服务⾃身保证容错性
  • 复制有很多种⽅法,但要保证数据强⼀致性,即 CAP 定理中的 CP
  • 提供⼀个强⼀致、能够容错⼀定数量节点的分布式锁服务

主从异步复制

1.主节点收到写请求,执⾏完毕

2.主节点响应客户端

3.主节点复制到从节点

如果复制之前主节点宕机、损坏------会造成数据丢失。

主从同步复制

1.主节点收到写请求,执⾏完毕

2.主节点复制到从节点

3.主节点收到所有从节点的确认信息,响应客户端

如果任意⼀个节点宕机、损坏、I/O 阻塞------会造成系统可⽤性降低(但数据没丢)

主从半同步复制

1.主节点收到写请求,执⾏完毕

2.主节点复制到从节点

3.主节点收到⼀个从节点的确认信息,响应客户端

4.其余从节点继续复制数据

如果从节点 2 因为某种原因写失败------会造成从节点数据不⼀致

基于 Quorum 的数据冗余复制

●W + R > N 且 W > N/2

●需要解决冲突

●需要数据修复

●案例:Dynamo、Cassandra

只能实现最终⼀致性

分布式共识算法 Paxos 或 Raft

  • 实现强⼀致性(线性⼀致性)
  • 容忍不超过半数节点故障
  • 案例:Spanner、etcd、CockroachDB......

缺点也不是没有,就是⼯程上⽐较难实现

Chubby 设计与实现

系统架构

  • 如 Chubby 由⼀个 Master 和多个副本组成,只有 Master 能够处理请求和读写⽂件,副本通过 Paxos 算法复制 Master 来实现容错
  • 提供类 UNIX ⽂件名称:/is/foo/wombat/pouch,可以降低培训难度
  • 每个节点包含⼀些 metadata,存储单调递增的编号、访问控制 ACL 策略等

其它系统特性,可以解决乱序到达等问题

  • sequencer:引⼊序列号,可以通过检查序列号是否合法来避免乱序到达问题
  • 事件:⽀持事件监听,例如:⽂件内容改变、⼦节点添加、锁的获取
  • 缓存:Chubby 通过客户端内存缓存来减少读流量
  • session 和 keepalive
  • fail-overs:能够处理 Master 宕机或重新选举

ZooKeeper 实现分布式锁

1.调⽤ create(),并设置 sequence 和 ephemeral 标志;

2.调⽤ getChildren() 获取⼦节点列表,不要设置 watch 标志(可以避免惊群效应);

3.检查⼦节点列表,如果步骤 1 创建的节点的序列号最⼩,则客户端持有该分布式锁,结束;

4.如果创建的序列号不是最⼩的,则客户端调⽤ exist(p, watch=true) 监听⽐当前序列号⼩⼀位的节点(记为 p),当 p 被删除时收到事件通知;

5.如果 exist() 返回 null,即前⼀个分布式锁被释放了,转到步骤 2;否则需要⼀直等待步骤 4 中 watch 的通知。

(惊群效应------持有锁的线程释放锁,剩下的线程争先抢后的竞争锁,导致cpu异常偏高)

参考引⽤:zookeeper.apache.org/doc/r3.7.0/...

etcd 实现分布式锁

共识算法 + TTL + 序列号 + watch 机制

参考引⽤:github.com/etcd-io/etc...

分布式互斥问题(Distributed mutual exclusion)

其他分布式算法简单介绍

● Lamport

● Ring-based

● Ricart-Agrawala

● Maekawa

总结

复盘

1.系统设计本质上是在做取舍(trade-off),没有完美的架构,更多的情况是要在⼏个互相竞争的问题之间进⾏权衡。有时候,基于单节点 Redis 实现的分布式锁就能满⾜需求;

2.微信实现了⾃⼰的 Chubby,Facebook 实现了 Delos,但实现⼀个共识协议(⽆论是 Paxos 还是 Raft)是颇具难度的,⽆⾮必要,不要轻易造轮⼦。

相关推荐
杜杜的man35 分钟前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*35 分钟前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu37 分钟前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s38 分钟前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子40 分钟前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
想进大厂的小王1 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
2402_857589362 小时前
SpringBoot框架:作业管理技术新解
java·spring boot·后端
一只爱打拳的程序猿2 小时前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring