前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis面试(九):分布式问题

Redis面试(九):分布式问题

原创
作者头像
传说之下的花儿
发布2023-09-27 08:30:26
2530
发布2023-09-27 08:30:26
举报

9. 分布式问题

9.1 分布式锁,为什么用?四大特性[❤️]

锁在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问。

Go语言中的锁(如sync.Mutexsync.RWMutex等)只能用于在单个进程或单个机器上实现并发控制和数据同步。

在分布式环境下,常见的并发控制和数据同步方法包括 分布式锁分布式事务一致性协议等。这些方法在保证数据一致性的同时,也能在分布式系统中实现并发控制。

分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源。

思路是:在整个系统提供一个全局、唯一的获取锁的 “东西”,然后每个系统在需要加锁时,都去问这个“东西” 拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个“东西”,可以是 ZooKeeper、etcd、Redis等,也可以是数据库。

这些服务提供了分布式锁机制,可以协调多个节点之间的并发访问,确保在某个节点获取锁时其他节点无法同时获取锁,从而实现分布式环境下的并发控制。

一般来说,分布式锁需要满足的特性有这么几点:

  1. 互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
  2. 高可用性/可靠性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
  3. 安全性:避免死锁情况发⽣。当⼀个竞争者在持有锁期间内,由于意外崩溃⽽导致未能主动解锁,其持有的锁也能够被正常释放,并保证后续其它竞争者也能加锁;
  4. 独占性:同⼀个锁,加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。

9.2 常见的分布式锁方案

实现分布式锁目前有三种流行方案,即基于关系型数据库、缓存(Redis)、ZooKeeper的方案:

  1. 基于关系型数据库的实现: 用MySQL举例,可以使用数据库的事务和唯一约束来实现分布式锁。例如,在数据库中创建一个特定的表,使用某一行作为锁,通过事务来获取和释放锁。其他节点在尝试获取锁时会遇到唯一约束,从而实现了互斥访问。
    • 缺点:
      1. 依赖于数据库的性能和可用性,数据库的故障可能导致锁失效。
      2. 依赖于数据库的性能和可用性,数据库的故障可能导致锁失效。
      3. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
      4. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
  2. 基于缓存的实现: 使用分布式缓存系统如Redis或Memcached来实现分布式锁。 通过在缓存中设置一个特定的键值对作为锁,利用缓存的原子操作(如setnx)来实现锁的获取和释放。其他节点在尝试获取锁时,如果发现锁已经存在,则表示锁被其他节点占用,需要等待或重试。
    • 优点:
      1. Redis 锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。
      2. 缓存系统通常具有高性能和低延迟,适合用于实现高并发的分布式锁。
    • 缺点:
      1. Redis 容易单点故障,集群部署,并不是强一致性的,锁的不够健壮;
      2. 需要保证缓存系统的高可用性和数据持久性。
      3. 需要自己不断去尝试获取锁,比较消耗性能。
  3. 基于ZooKeeper实现: 使用ZooKeeper作为分布式协调服务,利用其顺序节点和临时节点的特性来实现分布式锁。每个节点在获取锁时在ZooKeeper中创建一个有序临时节点,根据节点的顺序来确定锁的归属。其他节点通过监视前一个节点的删除来判断是否获取到了锁。
    • 优点 zookeeper 天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一个监听器就可以了(通过观察前一个节点的删除来实现锁的竞争),不用一直轮询,性能消耗较小。
    • 缺点
      1. 在高请求高并发下,系统疯狂的加锁释放锁,最后 zk 承受不住这么大的压力可能会存在宕机的风险。
      2. ZooKeeper的部署和维护相对复杂。

9.3 Redis实现分布式锁[❤️]

分布式锁的三个核心要素:

  1. 加锁: 使用 setnx 来加锁,key 是锁的唯一标识,按业务来决定命名,value 这里设置为 test。 $ setnx key test setnx:在缓存中设置一个键值对,只有在该键不存在时才设置成功,返回值表示设置是否成功。 当一个线程执行 setnx 返回 1,说明 key 原本不存在,该线程成功得到了锁; 当一个线程执行 setnx 返回 0,说明 key 已经存在,该线程抢锁失败;
  2. 解锁: 有加锁就得有解锁。当得到的锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式就是执行 del 指令。 $ del key 释放锁之后,其他线程就可以继续执行 setnx 命令来获得锁。
  3. 锁超时: 锁超时导致的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程别想进来。 所以,setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一段时间后自动释放。setnx 不支持超时参数,所以需要额外指令, $ expire key 30

但是,上面这种通过 setnx+del+expire 实现的分布式锁存在一定问题。

  1. 问题一:setnx + expire 是非原子性的。 假设一个场景中,某一个线程刚执行 setnx,成功得到了锁。此时 setnx 刚执行成功,还未来得及执行 expire 命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。 解决措施: 由于 setnx 指令本身是不支持传入超时时间的,而在 Redis2.6.12 版本上为 set 指令增加了可选参数, 用法如下: $ SET lock_key unique_value [EX seconds][PX milliseconds] [NX|XX] # 例如: $ SET lock_key unique_value EX 10 NX
    • unique_value 是客户端⽣成的唯⼀的标识,区分来⾃不同客户端的锁操作
    • EX second: 设置键的过期时间为 second 秒;
    • PX millisecond:设置键的过期时间为 millisecond 毫秒;
    • NX:只在键不存在时,才对键进行设置操作;
    • XX:只在键已经存在时,才对键进行设置操作;

    SET 操作完成时,返回 OK,否则返回 nil。

  2. 问题二:锁误解除 如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。 解决方法: 在 del 释放锁之前加一个判断,验证当前的锁是不是自己加的锁。 具体在加锁的时候把当前线程的 id 当做 value,可生成一个 UUID 标识当前线程,在删除之前验证 key 对应的 value 是不是自己线程的 id。 还可以使用 lua 脚本做验证标识和解锁操作。
  3. 问题三:超时解锁导致并发 如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。 解决方案: A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题:
    1. 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
    2. 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。
  4. 问题四:不可重入 当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。 Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。
  5. 问题五:无法等待锁释放 上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。 解决方案
    • 可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。
    • 另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅 锁释放消息,获取锁成功后释放时,发送锁释放消息。

9.4 了解RedLock吗?

RedLock 是一种分布式锁算法,用于在多个 Redis 实例之间实现分布式锁。它是由 Redis 官方提供的一种分布式锁解决方案,旨在解决 Redis 单实例的单点故障和数据丢失的风险。

RedLock 的实现基于多个 Redis 实例,并且要求这些实例分布在不同的物理节点上。

RedLock 的目标是通过 多数原则 来确保锁的可用性和一致性。即使在少数 Redis 实例发生故障或网络分区的情况下,只要大多数实例可用,锁仍然可以被获取和释放。

需要注意的是,RedLock 仍然是一个简化的分布式锁方案,不是一个完美的解决方案,也有一些潜在的问题和限制。

此种方式具有以下特性:

  • 互斥访问:即永远只有一个 client 能拿到锁
  • 容错性:只要大部分 Redis 节点存活(一半以上),就可以正常提供服务

9.4 RedLock 的基本原理

  1. 获取当前时间戳
  2. 获取锁:
    • 客户端在多个 Redis 实例上尝试获取锁。这些 Redis 实例应该分布在不同的物理节点上,以增加系统的可靠性。
    • 客户端可以选择 获取锁时的当前时间 作为 锁的值,并设置锁的过期时间(TTL)。
    • 客户端使用 SET 命令尝试在多个 Redis 实例上设置锁,设置成功的实例数需要满足多数原则(Majority),即大多数实例成功设置了锁,才认为获取到了锁。
    • 如果成功获取到锁,即满足多数原则,并且客户端在获取锁的时间内没有超过锁的有效期,则表示获取锁成功。客户端可以继续执行临界区内的操作。
  3. 释放锁:
    • 释放锁时,客户端会在所有获取锁的 Redis 实例上执行 DEL 命令,删除对应的锁。
    • 需要确保在释放锁的时间内 没有 超过锁的有效期,以避免误释放其他客户端的锁。

可能有些迷惑,我们来举个例子:

9.4.1 正常情况

首先,我们需要至少5台(大于等于5的奇数个)Redis服务器,这5台Redis之间相互独立,没有任何主从、集群关系。

接着,我们按照从左到右的顺序,在Redis服务器上获取锁,我们假设

  • 锁的过期时间为10s
  • 加锁的开始时间是00:00:00
  • 在第一台服务器上获取到锁的时间为00:00:01
  • 在第二台服务器上获取到锁的时间为00:00:02
  • 在第三台服务器上获取到锁的时间为00:00:03

现在,已经有超过半数(3/5)的Redis服务器获取到了锁。

  • 获取锁所用的时间 = 最后一台获取到锁的Redis服务器获取到锁的时间 - 加锁的开始时间
  • 锁的有效剩余时间(TTL) = 锁的过期时间 - 获取锁所用的时间

获取锁所用的时间 = 00:00:03 - 00:00:00 = 3s,TTL = 10s - (00:00:03 - 00:00:00) = 7s。

所以,获取锁的时间并没有超过锁的有效期,我们认为获取锁成功。

认为锁获取成功的条件有两个:

  1. 超过半数的Redis服务器获取到了锁
  2. 获取锁的时间没有超过锁的有效期
9.4.2 重试

以上列举的示例是非常顺利获取到锁的情况,然而很多时候,分布式锁的获取没那么顺利,很可能出现以下情况:

  • A已经获取到了两台Redis服务器的锁
  • B已经获取到了两台Redis服务器的锁
  • C已经获取到了一台Redis服务器的锁

如果三台客户端的请求一直处于阻塞状态(直到达到锁的有效期),会严重影响锁的获取效率,这时就需要重试机制

重试机制:在一开始,同时向所有的(这里是5台)Redis服务器,发送SET key value EX senconds NX命令,当所有服务器都返回结果后,判断是否以达成“锁获取成功的两个条件”,如果达成了,则锁获取成功。如果没有,则立即将已获取的锁释放掉,并等待一小段时间,重复以上步骤(一般会尝试3次)。如果这期间仍未达成“锁获取成功的两个条件”,则认为锁获取失败。

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 9. 分布式问题
    • 9.1 分布式锁,为什么用?四大特性[❤️]
      • 9.2 常见的分布式锁方案
        • 9.3 Redis实现分布式锁[❤️]
          • 9.4 了解RedLock吗?
            • 9.4 RedLock 的基本原理
              • 9.4.1 正常情况
              • 9.4.2 重试
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档