分布式深层剖析

CAP原理

CAP原理指的是:在一个分布式系统(互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。也就是说,在分布式系统的设计中,没有一种设计可以同时满足这3个特性,要么CA,要么CP,要么AP!

  • 强一致性(C):在分布式系统中的所有数据备份,在同一时刻是否有同样的值,即写操作之后的读操作,必须返回该值。也就是说,数据更新完,立刻访问任何节点看到的数据都完全一致(与最终一致性区分开)。
  • 高可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
  • 分区容错性(P):当部分节点由于网络阻塞等不可预估的问题出现消息丢失或故障的时候,分布式系统仍能正常工作。以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

Base理论

基本可用(Basically Available),软状态(Soft State)和最终一致性(Eventually Consistent)。既然无法做到强一致性,那么不同的应用可用根据自己的业务特点,采用适当的方式来达到最终一致性。Base理论是对CAP理论的实际应用

  • 基本可用性:不追求强可用性,强调系统基本能够一直运行对外提供服务。当分布式系统遇到不可预估的故障时,允许一定程度上的不可用,比如:对请求进行限流排队,使得部分用户响应时间变长,或对非核心服务进行降级。
  • 软状态:比如对于数据库中事务的原子性,要么全部成功,要不全部不成功,而软状态允许系统中的数据存在中间状态。
  • 最终一致性:数据不可能一直都是软状态,必须在一个时间期限之后达到各个节点的一致性。在此之后,所有的节点的数据都是一致的,系统达到最终一致性。

分布式锁

分布式锁是一种常用的技术,在高并发场景下,为了避免多个进程或线程同时操作同一资源造成冲突,引入分布式锁机制。

分布式锁的原理是在多个机器上设置同一把锁,这个锁通常通过某些中间件实现。当一个线程想要获取锁的时候,首先会去尝试获取锁,如果获取成功,那么就可以执行任务;如果获取失败,那么就只能等待,直到锁被释放。

分布式锁的核心目的是保证共享资源的独占

特性:

  • 互斥:不同线程之间互斥,只有一个线程能持有锁。
  • 超时机制:因代码耗时过长、网络原因等,导致锁一直被占用而造成死锁,所以引入超时机制,超过指定时间自动释放锁。
  • 完备的锁接口:阻塞的和非阻塞的接口都要有,lock 和 tryLock。
  • 可重入性:当前请求的节点 + 线程唯一标识,可以再次获取同一把锁。
  • 公平性:锁唤醒的时候按照顺序唤醒,如果不公平就有可能出现饥饿现象。

使用场景:

  • 多个应用实例需要同时修改同一份数据,需要保证数据的一致性。例如:秒杀抢购、优惠券领取等。
  • 系统需要进行任务调度,任务之间需要互斥执行。例如:定时任务等。
  • 避免重复处理数据。例如:调度任务在多台机器重复执行,缓存过期所有请求都去加载数据库。
  • 保证数据的正确性。例如:秒杀的时候防止商品超卖,表单重复提交,接口幂等性。

MySQL

基于索引

通过在数据库的某个字段上加上了唯一索引,这样在写入同一个数据时,只能有一个线程写入,其他的线程由于索引的唯一性而无法写入,通过这样的方法就实现了一个分布式锁。

这种锁的实现比较简单,但会面临锁无法过期,锁的可靠性依赖于 MySQL 数据库的可用性等问题。

基于乐观锁

基于乐观锁的实现原理是多个线程可以同时对资源进行修改,但最终只能有一个修改成功,其他的回退。乐观锁的实现一般是基于版本号的机制,比如在更新数据时,先获取当前版本号,然后更新数据,再更新版本号。如果更新失败,说明数据已经被其他线程更新过了,那么就需要重试。

每个线程的执行逻辑如下:

  • 获取资源:SELECT resource, version FROM optimistic_lock WHERE id = 1
  • 执行业务逻辑
  • 更新资源:UPDATE optimistic_lock SET resource = resource -1, version = version + 1 WHERE id = 1 AND version = oldVersion

通过比对修改后的 version 和修改之前的 oldVersion,如果一致,说明数据没有被其他线程更新过,那么就更新成功,否则就需要重试。

这种锁的实现比较复杂,但也能保证数据的一致性。在检测数据冲突时并不依赖数据库本身的锁机制,不会影响请求的性能。但是需要对表的设计增加额外的字段,增加了数据库的冗余。并且高并发的情况下增加了重试的次数,会影响性能。

基于悲观锁

基于悲观锁的实现原理是多个线程只能一个一个地获取锁,直到获取锁的线程释放锁,其他线程才能获取锁。我们在基于 MySQL 的悲观锁的实现中,一般采用 MySQL 自带的锁机制,比如 SELECT … FOR UPDATE。数据库会在查询的过程中加上排他锁,那么这样别的事务就无法对该资源进行修改。

基于悲观锁的实现过程如下:

  • 获取资源:SELECT * FROM optimistic_lock WHERE id = 1 FOR UPDATE
  • 执行业务逻辑
  • 释放资源:COMMIT

相当于我们基于 SELECT ... FOR UPDATE 获取了这行数据的锁,并且在同一事务下执行修改的业务逻辑,最终在 COMMIT 提交事务时释放锁。

这种锁的的实现也比较简单,主要是基于数据库的事务和行锁。但要注意行锁失效的情况。并且每次请求都会额外产生加锁的开销且未获取到锁的请求将会阻塞等待锁的获取,在高并发环境下,容易造成大量请求阻塞,影响系统可用性。

Zookeeper

基于 Zookeeper 的分布式锁,主要来自于 Zookeeper 的两个机制:

  • 临时顺序节点机制:Zookeeper 的节点是一个类似于文件系统的目录结构,每个节点都可以设置临时顺序节点。也就是说,在创建节点时,可以指定一个顺序,然后 Zookeeper 会根据这个顺序来分配节点的唯一标识符。除此以外节点也可以被标记为持久节点,持久节点会一直存在直到主动删除。
  • watch 机制:Zookeeper 的 watch 机制允许用户在指定的节点上注册一个监听器,当节点发生变化时,Zookeeper 会通知监听器,并触发监听器的回调函数。

基于这两个机制,可以实现一个基于 Zookeeper 的分布式锁:

  • 首先建立一个父节点,这个父节点是一个持久节点,用来表示共享资源。
  • 在父节点下创建临时顺序节点,这个临时顺序节点用来标识当前获得锁的线程,这样就在父节点之下建立了一个类似于队列的结构。
  • 然后判断当前节点是不是最小的节点,如果是最小的节点,那么就获取锁,否则就监听前一个节点的删除事件,直到获得锁。
  • 每次节点使用完共享资源,就会删除该节点,进而释放锁,后面的节点通过 watch 监听前一个节点的删除事件,获得锁。

Zookeeper 实现分布式锁的优点是可以实现顺序的公平锁。并且可以实现强一致性,所有的操作都可以被保证是原子性的。假如某个节点宕机了,那么会自动释放锁,防止了死锁,提高了系统的可用性。

缺点是节点的创建和销毁开销比较大,在高并发的环境下可能有较大的性能问题。另外,watch 机制也会增加系统的复杂度,需要考虑节点的删除和创建的时机,以及节点的连接状态等。

Redis

用 Redis 实现分布式锁,利用的是 SETNX + EXPIRE 命令。

SETNX 命令的作用是设置一个 key,当 key 不存在时,返回 1,如果 key 已经存在,返回 0。EXPIRE 命令的作用是设置一个 key 的过期时间,当 key 过期时,Redis 会自动删除该 key。

一般这两条命令写在一行来确保指令的原子性,如:

SETNX lock_key some_unique_value EXPIRE lock_key 10  # 设置过期时间为10秒

当两个线程同时执行这个命令时,只有一个线程会成功对 lock_key 的值进行修改,其他线程会失败,这样就达到了分布式锁的目的。

基于 Redis 实现的分布式锁是对值的修改,因此性能较好。但如果是在 Redis 集群环境下,由于 Redis 集群同步是异步的,就可能会出现如下的问题。如果在 Master 节点上设置锁,Slave 节点可能没有同步到最新的数据。此时 Master 节点崩溃了但是理论上锁不应当被释放,但由于 Master 的宕机导致了锁物理上被释放,所以其他客户端可能会加新的锁来对共享资源进行修改,这样就出现了问题。

解决这个问题的方法就是 RedLock 算法——也就是 Redisson 的实现原理。

Redisson

Redisson 的主要特性:

  • 看门狗机制
  • 集群支持
  • 公平锁

Redisson 的公平锁的实现原理类似于 ReentrankLock 的公平锁机制,主要维护一个等待队列,通过控制锁的获取顺序来实现。

Redisson看门狗机制目的是检查锁的状态,自动管理分布式锁过期时间。主要通过一个后台线程(俗称看门狗),每隔锁的 1/3 时间检查锁的状态,只要持有锁的线程仍在执行且没有主动释放锁,看门狗就会持续进行续期操作;如果没有线程持有锁,看门狗就会自动释放锁。

RedLock 算法的主要目的是为了解决 Master 节点宕机导致锁的释放问题。RedLock 算法的基本思路是,在多个 Redis 节点上同时加锁,只要大多数 Redis 节点都加锁成功,那么加锁成功;如果加锁失败,则释放所有锁并重试。

RedLock 算法的流程如下:

  1. 客户端获取当前时间戳。
  2. 客户端在每个 Redis 节点上尝试用相同的锁名和 UUID 获取锁,并设置一个较短的过期时间。获取成功则记录加锁节点,否则记录失败节点。并记录加锁的总用时。
  3. 如果成功加锁的节点大于等于 N/2+1(N 为 Redis 节点数),并且获取锁的总时间小于锁的过期时间,则认为加锁成功并执行业务逻辑;否则认为获取锁失败,释放所有锁

Redisson 通过 RedLock 算法,保证了集群环境中锁的可靠性。

参考资料:


不准投币喔 👆

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇