文章

《Redis 知识个人笔记》

《Redis 知识个人笔记》

一、Redis 单线程模型

Redis 内部使用文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。

它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。


文件事件处理器的结构

文件事件处理器包含 4 个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────┐
│     多个 Socket                  │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│     IO 多路复用程序               │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│     文件事件分派器                │
└─────────────────────────────────┘
              ↓
┌─────────────────────────────────┐
│     事件处理器                    │
│  • 连接应答处理器                 │
│  • 命令请求处理器                 │
│  • 命令回复处理器                 │
└─────────────────────────────────┘

工作原理

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件:

  1. IO 多路复用程序会监听多个 socket
  2. 将 socket 产生的事件放入队列中排队
  3. 事件分派器每次从队列中取出一个事件
  4. 把该事件交给对应的事件处理器进行处理

二、Redis 中的数据结构类型

1. String(字符串)

实现原理:

  • 没有用 C 语言原生的 string
  • 而是自己构建了一个简单动态字符串(SDS)

特点:

  • ✅ 既可以保存文本数据也可以保存二进制数据
  • ✅ 可以 O(1) 级别获取字符串长度
  • ✅ SDS API 安全,不会造成缓冲区溢出

应用场景:

  • 一般常用在需要计数的场景
  • 比如用户的访问次数、热点文章的点赞转发数量等等

2. Hash(哈希)

实现原理:

  • 类似于 JDK1.8 前的 HashMap
  • 内部实现也差不多(数组 + 链表)

特点:

  • 特别适合用于存储对象
  • 每一个 hash 容器有一个 key,value 则是多个键值对

示例:

HSET student name "guide" age "24"

相当于存了一个 student 的对象,value 是他的属性键值对。

应用场景:

  • 系统中对象数据的存储

3. List(列表)

实现原理:

  • 底层是一个双向链表
  • 支持反向查找与遍历

应用场景:

  • 发布与订阅或者说消息队列
  • 慢查询

4. Set(集合)

特点:

  • 无序集合
  • 提供查看某数据是否在 set 里面的重要接口

应用场景:

  • 需要存放的数据不能重复
  • 需要获取多个数据源交集和并集等场景

5. ZSet(有序集合)

特点:

  • 相较于 set,增加了权值参数 score
  • 使得集合里面的元素可以按照 score 来排序
  • 还可以根据权重范围来获取元素列表

实现原理:

  • 底层通过跳表来实现
  • 当数据比较少的时候也可以通过压缩链表来实现

应用场景:

  • 需要对数据根据某个权重进行排序的场景
  • 例如:实时排行榜、弹幕消息(可以理解为按消息维度的消息排行榜)等信息

三、过期数据删除策略

1. 定期删除

机制:

  • Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key
  • 检查其是否过期,如果过期就删除

注意:这里是随机抽取的。

为什么要随机?

如果 redis 存了几十万个 key,每隔 100ms 就遍历所有的设置过期时间的 key,就会给 CPU 带来很大的负载!


2. 惰性删除

机制:

  • 只会在取出 key 的时候才对数据进行过期检查

特点:

  • ✅ 对 CPU 最友好
  • ❌ 可能会造成太多过期 key 没有被删除

问题分析

问题:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?

答案:如果大量过期 key 堆积在内存里,导致 redis 内存快耗尽了。

解决方案:Redis 内存淘汰机制


四、Redis 内存淘汰机制

💡 面试题:MySQL 里有 2000w 数据,Redis 只存 20w 数据,如何保证 Redis 中的数据都是热点数据?

内存淘汰策略

针对设置了过期时间的数据

策略说明
volatile-lru从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random从已设置过期时间的数据集中随机选择数据淘汰
volatile-lfu从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
(4.0 版本后新加)

针对所有数据

策略说明
allkeys-lru当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key
⭐️ 这个是最常用的
allkeys-random从数据集中随机选择数据淘汰
allkeys-lfu当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
(4.0 版本后新加)

不淘汰

策略说明
no-eviction禁止驱逐数据
当内存不足以容纳新写入数据时,新写入操作会报错
❌ 这个应该没人使用吧!

五、持久化机制

1. RDB(快照持久化)

定义:Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。

用途:

  • ✅ 对快照进行备份
  • ✅ 将快照复制到其他服务器从而创建具有相同数据的服务器副本
  • ✅ 将快照留在原地以便重启服务器的时候使用

配置:

  • 快照持久化是 Redis 默认采用的持久化方式
  • 在 redis.conf 配置文件中默认有此配置

2. AOF(追加文件持久化)

定义:只追加文件(Append Only File)

对比:与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。

开启方式:

1
appendonly yes

📌 默认情况下 Redis 没有开启 AOF 持久化


AOF 持久化方式

Redis 的配置文件中存在三种不同的 AOF 持久化方式:

方式说明
appendfsync always每次有数据修改发生时都会写入 AOF 文件
严重降低 Redis 的速度
appendfsync everysec每秒钟同步一次
显式地将多个写命令同步到硬盘
appendfsync no让操作系统决定何时进行同步

补充:AOF 重写

什么是 AOF 重写?

AOF 重写可以产生一个新的 AOF 文件:

  • 这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样
  • 但体积更小

重写原理

注意:AOF 重写是一个有歧义的名字。

该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。


BGREWRITEAOF 命令

在执行 BGREWRITEAOF 命令时:

  1. Redis 服务器会维护一个 AOF 重写缓冲区
  2. 该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令
  3. 当子进程完成创建新 AOF 文件的工作之后
  4. 服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾
  5. 使得新旧两个 AOF 文件所保存的数据库状态一致
  6. 最后,服务器用新的 AOF 文件替换旧的 AOF 文件
  7. 以此来完成 AOF 文件重写操作

六、Redis 事务

(1)概念

Redis 事务的本质是一组命令的集合。

事务支持一次执行多个命令,一个事务中所有命令都会被序列化。

在事务执行过程中,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。


(2)Redis 事务没有隔离级别的概念

批量操作在发送 EXEC 命令前被放入队列,并不会被实际执行。


(3)Redis 不保证原子性

Redis 中:

  • ✅ 单条命令是原子性执行的
  • ❌ 事务不保证原子性,且没有回滚
  • ⚠️ 事务中任意命令执行失败,其余的命令仍会被执行

(4)三个阶段

  1. 开始事务
  2. 命令入队
  3. 执行事务

(5)Redis 事务相关命令

命令说明
WATCH key1 key2 …监视一或多个 key
如果在事务执行之前,被监视的 key 被其他命令改动,则事务被打断
(类似乐观锁)
MULTI标记一个事务块的开始(queued)
EXEC执行所有事务块的命令
一旦执行 exec 后,之前加的监控锁都会被取消掉
DISCARD取消事务,放弃事务块中的所有命令
UNWATCH取消 WATCH 对所有 key 的监控

(6)案例

案例 1:正常执行

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> GET k1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) "v1"

案例 2:取消事务

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> DISCARD
OK

案例 3:命令性错误(编译性错误)

若在事务队列中存在命令性错误(类似于 Java 编译性错误):

  • 则执行 EXEC 命令时,所有命令都不会执行

案例 4:语法性错误(运行时异常)

若在事务队列中存在语法性错误(类似于 Java 的 1/0 的运行时异常):

  • 则执行 EXEC 命令时,其他正确命令会被执行,错误命令抛出异常

案例 5:WATCH 监控 - 成功

场景:使用 WATCH 检测 balance,事务期间 balance 数据未变动,事务执行成功

127.0.0.1:6379> SET balance 100
OK
127.0.0.1:6379> WATCH balance
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY balance 20
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 80

案例 6:WATCH 监控 - 失败

场景:

  1. 使用 WATCH 检测 balance
  2. 在开启事务后,在新窗口执行操作,更改 balance 的值
  3. 模拟其他客户端在事务执行期间更改 WATCH 监控的数据
  4. 然后再执行事务,执行 EXEC 后,事务未成功执行

重要提示:

  • 一旦执行 EXEC 开启事务的执行后,无论事务是否执行成功,WATCH 对变量的监控都将被取消
  • 当事务执行失败后,需重新执行 WATCH 命令对变量进行监控,并开启新的事务进行操作

原理:

  • WATCH 指令类似于乐观锁
  • 在事务提交时,如果 WATCH 监控的多个 KEY 中任何 KEY 的值已经被其他客户端更改
  • 则使用 EXEC 执行事务时,事务队列将不会被执行
  • 同时返回 Nullmulti-bulk 应答以通知调用者事务执行失败

七、缓存击穿、缓存雪崩、缓存穿透

(1)缓存击穿

定义

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期):

  • 这时由于并发用户特别多
  • 同时读缓存没读到数据,又同时去数据库去取数据
  • 引起数据库压力瞬间增大,造成过大压力

解决方案

✅ 设置热点数据永远不过期


(2)缓存雪崩

定义

缓存雪崩是指缓存中数据大批量过期:

  • 而查询数据量巨大直接请求到数据库
  • 引起数据库压力过大甚至 down 机

区别:

  • 缓存击穿:并发查同一条数据
  • 缓存雪崩:不同数据都过期了,很多数据都查不到从而查数据库

解决方案

  1. ✅ 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
  2. ✅ 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中
  3. ✅ 设置热点数据永远不过期
  4. ✅ 限流 & 降级:用一个队列让请求量没有那么大

(3)缓存穿透(两层都透了)

定义

缓存穿透是指缓存和数据库中都没有的数据:

  • 而用户不断发起请求
  • 如发起 id 为 “-1” 的数据或 id 为特别大不存在的数据
  • 这时的用户很可能是攻击者
  • 攻击会导致数据库压力过大

解决方案

  1. ✅ 接口层增加校验
    • 如用户鉴权校验
    • id 做基础校验,id <= 0 的直接拦截
  2. ✅ 缓存空值
    • 从缓存取不到的数据,在数据库中也没有取到
    • 这时也可以将 key-value 对写为 key-null
    • 缓存有效时间可以设置短点,如 30 秒
    • 设置太长会导致正常情况也没法使用
    • 这样可以防止攻击用户反复用同一个 id 暴力攻击
  3. ✅ 采用布隆过滤器
    • 使用一个足够大的 bitmap
    • 用于存储可能访问的 key
    • 不存在的 key 直接被过滤

八、缓存预热、缓存更新

(1)缓存预热

定义

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。

这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!

用户直接查询事先被预热的缓存数据!


解决思路

  1. ✅ 直接写个缓存刷新页面,上线时手工操作
  2. ✅ 数据量不大,可以在项目启动的时候自动进行加载
  3. ✅ 定时刷新缓存

(2)缓存更新

除了了解缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 种策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰。

常见的策略有两种:

策略 1:定时清理

定时去清理过期的缓存


策略 2:请求时判断

当有用户请求过来时,再判断这个请求所用到的缓存是否过期:

  • 过期的话就去底层系统得到新数据并更新缓存

对比

策略优点缺点
定时清理逻辑简单维护大量缓存的 key 比较麻烦
请求时判断按需更新每次用户请求过来都要判断缓存失效
逻辑相对比较复杂

💡 建议:具体用哪种方案,可以根据自己的应用场景来权衡。


九、布隆过滤器

作用

帮助我们快速判定一个数据是否存在于海量数据当中。


常见场景

  • 网页黑名单系统
  • 垃圾邮件过滤系统
  • 爬虫的网址判重系统
  • 解决缓存穿透:加在缓存前面

特点

  • ✅ 如果判定在,小概率可能不在
  • ✅ 如果判定不在,那么一定不在

组成

一个超大的 bit 位数组 + 一定数量的 hash 函数


工作原理

添加元素

当一个元素加入布隆过滤器中的时候:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到几个哈希值
    • 有几个哈希函数得到几个哈希值
  2. 根据得到的哈希值,在位数组中把对应下标的值置为 1

判断元素是否存在

需要判断一个元素是否存在于布隆过滤器的时候:

  1. 对给定元素再次进行相同的哈希计算
  2. 得到值之后判断位数组中的每个元素是否都为 1
    • 如果值都为 1,那么说明这个值在布隆过滤器中
    • 如果存在一个值不为 1,说明该元素不在布隆过滤器中

误差原因

可能存在不同的字符串哈希出来的位置相同。

解决方案:

  • 可以适当增加位数组大小
  • 或者调整哈希函数来降低概率

十、Redis 集群

(1)主从复制

复制模式

模式说明
全量复制Master 全部同步到 Slave
部分复制Slave 数据丢失进行备份

会出现的问题

  1. ⚠️ 同步故障
  2. ⚠️ 复制数据延迟(不一致)
  3. ⚠️ 读取过期数据(Slave 不能删除数据)
  4. ⚠️ 主/从节点故障
  5. ⚠️ 配置不一致
    • maxmemory 不一致:丢失数据
    • 优化参数不一致:内存不一致

避免全量复制

  1. ✅ 选择小主节点(分片)、低峰期间操作
  2. ✅ 如果节点运行 id 不匹配(如主节点重启、运行 id 发生变化)
    • 此时要执行全量复制
    • 应该配合哨兵和集群解决
  3. ✅ 主从复制挤压缓冲区不足产生的问题(网络中断,部分复制无法满足)
    • 可增大复制缓冲区(repl_backlog_size 参数)

(2)哨兵机制

1)哨兵系统的三个任务

Redis 的哨兵(Sentinel)系统用于管理多个 Redis 服务器,该系统执行以下三个任务:

任务说明
监控(Monitoring)哨兵会不断地检查你的 Master 和 Slave 是否运作正常
提醒(Notification)当被监控的某个 Redis 出现问题时
哨兵可以通过 API 向管理员或者其他应用程序发送通知
自动故障迁移(Automatic failover)当一个 Master 不能正常工作时
哨兵会开始一次自动故障迁移操作

自动故障迁移操作

当 Master 失效时:

  1. 将失效 Master 的其中一个 Slave 升级为新的 Master
  2. 让失效 Master 的其他 Slave 改为复制新的 Master
  3. 当客户端试图连接失效的 Master 时
  4. 集群也会向客户端返回新 Master 的地址
  5. 使得集群可以使用 Master 代替失效 Master

2)哨兵模式运行流程

哨兵(Sentinel)是一个分布式系统:

  • 你可以在一个架构中运行多个哨兵进程
  • 这些进程使用流言协议(gossip protocols)来接收关于 Master 是否下线的信息
  • 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移
  • 以及选择哪个 Slave 作为新的 Master

工作机制

每个哨兵会向其它哨兵、master、slave 定时发送消息:

  • 以确认对方是否”活”着
  • 如果发现对方在指定时间(可配置)内未回应
  • 则暂时认为对方已挂(所谓的”主观认为宕机” Subjective Down,简称 sdown)

若”哨兵群”中的多数 Sentinel都报告某一 Master 没响应:

  • 系统才认为该 Master “彻底死亡”(即:客观上的真正 down 机,Objective Down,简称 odown)
  • 通过一定的 vote 算法,从剩下的 Slave 节点中,选一台提升为 Master
  • 然后自动修改相关配置

启动方式

虽然哨兵释出为一个单独的可执行文件 redis-sentinel:

  • 但实际上它只是一个运行在特殊模式下的 Redis 服务器
  • 你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵

3)故障转移(选择新 Master)

步骤:

  1. Sentinel 选出一个合适的 Slave 作为新的 Master(slaveof no one 命令)
  2. 向其余 Slave 发出通知,让它们成为新 Master 的 Slave(parallel-syncs 参数)
  3. 等待旧 Master 复活,并使之成为新 Master 的 Slave
  4. 向客户端通知 Master 变化

从 Slave 中选择新 Master 节点的规则

Slave 升级成 Master 之后的选择规则:

  1. 选择 slave-priority 最高的节点
  2. 选择复制偏移量最大的节点(同步数据最多)
  3. 选择 runId 最小的节点

4)Leader 选举规则(Sentinel 选举)

  1. 每个主观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者
  2. 收到命令的 Sentinel 节点如果没有同意通过其他 Sentinel 节点发送的命令,则同意该请求,否则拒绝
  3. 如果该 Sentinel 节点发现自己的票数已经超过 Sentinel 集合半数且超过 quorum,则它成为领导者
  4. 如果此过程有多个 Sentinel 节点成为领导者,则等待一段时间再重新进行选举

5)节点下线(包括 master、sentinel、slave 节点)

主观下线(Subjective Down)

定义:Sentinel 节点对 Redis 节点失败的偏见,超出超时时间认为 Master 已经宕机。

机制:

  • Sentinel 集群的每一个 Sentinel 节点会定时对 Redis 集群的所有节点发心跳包检测节点是否正常
  • 如果一个节点在 down-after-milliseconds 时间内没有回复 Sentinel 节点的心跳包
  • 则该 Redis 节点被该 Sentinel 节点主观下线

客观下线(Objective Down)

定义:所有 Sentinel 节点对 Redis 节点失败要达成共识,即超过 quorum 个统一。

机制:

  • 当节点被一个 Sentinel 节点记为主观下线时,并不意味着该节点肯定故障了
  • 还需要 Sentinel 集群的其他 Sentinel 节点共同判断为主观下线才行
  • 该 Sentinel 节点会询问其它 Sentinel 节点
  • 如果 Sentinel 集群中超过 quorum 数量的 Sentinel 节点认为该 Redis 节点主观下线
  • 则该 Redis 客观下线

6)定时任务

间隔任务
每 1s每个 Sentinel 对其他 Sentinel 和 Redis 执行 ping
进行心跳检测
每 2s每个 Sentinel 通过 Master 的 Channel 交换信息
(pub - sub)
每 10s每个 Sentinel 对 Master 和 Slave 执行 info
目的是发现 Slave 节点、确定主从关系

十一、主从同步

1)初次全量同步

当一个 Redis 服务器初次向主服务器发送 SLAVEOF 命令时,Redis 从服务器会进行一次全量同步。

同步步骤

  1. Slave 发送 PSYNC 命令
    • Slave 服务器向 Master 发送 psync ? -1
    • 告诉 Master:我需要同步数据了
  2. Master 生成 RDB 文件
    • Master 接收到 psync 命令后会进行 BGSAVE 命令生成 RDB 文件快照
  3. Master 发送 RDB 文件
    • 生成完后,会将 RDB 文件发送给 Slave
  4. Slave 载入 RDB 快照
    • Slave 接收到文件会载入 RDB 快照
    • 并且将数据库状态变更为 Master 在执行 BGSAVE 时的状态一致
  5. Master 发送缓冲区命令
    • Master 会发送保存在缓冲区里的所有写命令
    • 告诉 Slave 可以进行同步了
  6. Slave 执行写命令
    • Slave 执行这些写命令

2)命令传播

Slave 已经同步过 Master 了,那么如果后续 Master 进行了写操作:

例如:一个简单的 SET name redis

流程:

  • Master 执行过当前命令后
  • 会将当前命令发送给 Slave 也执行一遍
  • 达成数据一致性

3)重新复制

当 Slave 断开重连之后会进行重新同步,重新同步有:

  • 完全同步
  • 部分同步

部分同步流程

  1. Slave 发送 PSYNC 命令
    • 当 Slave 断开重连后,会发送 psync 命令给 Master
  2. Master 返回 +CONTINUE
    • Master 收到 psync 后会返回 +CONTINUE 回复
    • 表示 Slave 可以执行部分同步了
  3. Master 发送写命令
    • Master 发送断线后的写命令给 Slave
  4. Slave 执行写命令
    • Slave 执行写命令

Master 判断是否进行部分同步的三个要素

实际上当 Slave 发送 psync 命令给 Master 之后,Master 还需要根据以下三点判断是否进行部分同步。


(1)服务器运行 ID

机制:

  • 每个 Redis 服务器开启后会生成运行 ID
  • 当进行初次同步时,Master 会将自己的 ID 告诉 Slave
  • Slave 会记录下来

判断:

  • 当 Slave 断线重连后,发现 ID 是这个 Master 的,就会尝试进行部分重同步
  • 当 ID 与现在连接的 Master 不一样时,会进行完整重同步

(2)复制偏移量

包括:

  • Master 复制偏移量
  • Slave 复制偏移量

机制:

  • 当初次同步过后,两个数据库的复制偏移量相同
  • 之后 Master 执行一次写命令,那么 Master 的偏移量 +1
  • Master 将写命令给 Slave,Slave 执行一次,Slave 偏移量 +1
  • 这样版本就能一致

(3)复制积压缓冲区

定义:复制积压缓冲区是由 Master 维护的固定长度的先进先出的队列。

判断机制:

  • 当 Slave 发送 psync,会将自己的偏移量也发送给 Master
  • 当 Slave 的偏移量之后的数据在缓冲区还存在
    • 就会返回 +CONTINUE 通知 Slave 进行部分重同步
  • 当 Slave 的偏移量之后的数据不在缓冲区了
    • 就会进行完整重同步

结合以上三点,完整的重新复制流程

  1. 当 Slave 断开重连后,会发送 psync 命令给 Master

  2. Master 判断服务器运行 ID
    • 如果与自己相同就进行判断偏移量
  3. Master 判断偏移量
    • 判断自己的偏移量与 Slave 的偏移量是否一致
  4. 如果不一致,Master 判断缓冲区
    • 去缓冲区中判断 Slave 的偏移量之后的数据是否存在
  5. 如果存在
    • 返回 +CONTINUE 回复,表示 Slave 可以执行部分同步了
  6. Master 发送断线后的写命令给 Slave

  7. Slave 执行写命令

十二、分布式锁

(1)单节点

实现方式

  1. 获取锁
    • 利用 SET key value EX 30 NX 在 Redis 里面设置一个锁
    • 表示获取到了
    • 其他机器如果发现里面有这个 key,那么就等待
    • 说明现在其他机器获得到了锁
  2. 释放锁
    • 利用 Lua 脚本写一个释放锁的代码
    • 添加和删除都要保证原子性
  3. 守护线程
    • 需要一个线程去守护当前机器
    • 如果发现 Redis 要到期,但是代码还没执行完
    • 那么就延长 Redis 里面 key 的时间,达到延期的效果

(2)RedLock

最低要求

最低保证分布式锁的有效性及安全性的要求如下:

  1. ✅ 互斥:任何时刻只能有一个 client 获取锁
  2. ✅ 释放死锁:即使锁定资源的服务崩溃或者分区,仍然能释放锁
  3. ✅ 容错性:只要多数 Redis 节点(一半以上)在使用,client 就可以获取和释放锁

多节点 Redis 实现的分布式锁算法(RedLock)

有效防止单点故障

假设有 5 个完全独立的 Redis 主服务器:


获取锁的步骤

1. 获取当前时间戳


2. Client 尝试按照顺序获取所有 Redis 服务的锁

  • 使用相同的 key, value 获取所有 Redis 服务的锁
  • 在获取锁的过程中的获取时间比锁过期时间短很多
  • 这是为了不要过长时间等待已经关闭的 Redis 服务
  • 并且试着获取下一个 Redis 实例

例如:

  • TTL 为 5s,设置获取锁最多用 1s
  • 所以如果一秒内无法获取锁,就放弃获取这个锁
  • 从而尝试获取下个锁

3. 计算获取锁的时间

  • Client 通过获取所有能获取的锁后的时间减去第一步的时间
  • 这个时间差要小于 TTL 时间
  • 并且至少有 3 个 Redis 实例成功获取锁
  • 才算真正的获取锁成功

4. 计算锁的真正有效时间

  • 如果成功获取锁,则锁的真正有效时间是:TTL 减去第三步的时间差的时间

例如:

  • TTL 是 5s,获取所有锁用了 2s
  • 则真正锁有效时间为 3s(其实应该再减去时钟漂移)

5. 获取锁失败的处理

  • 如果客户端由于某些原因获取锁失败
  • 便会开始解锁所有 Redis 实例
  • 因为可能已经获取了小于 3 个锁,必须释放
  • 否则影响其他 client 获取锁

RedLock 失败重试

当 client 不能获取锁时:

  • 应该在随机时间后重试获取锁
  • 并且最好在同一时刻并发的把 SET 命令发送给所有 Redis 实例

对于已经获取锁的 client:

  • 在完成任务后要及时释放锁
  • 这是为了节省时间

RedLock 释放锁

机制:

  • 由于释放锁时会判断这个锁的 value 是不是自己设置的
  • 如果是才删除

操作:

  • 所以在释放锁时非常简单
  • 只要向所有实例都发出释放锁的命令
  • 不用考虑能否成功释放锁

RedLock 性能及崩溃恢复的相关解决方法

问题 1:没有持久化功能

场景:

  • 如果 Redis 没有持久化功能
  • 在 clientA 获取锁成功后,所有 Redis 重启
  • clientB 能够再次获取到锁
  • 这样违反了锁的排他互斥性

问题 2:AOF 持久化的风险

场景:

  • 如果启动 AOF 永久化存储,事情会好些
  • 当我们重启 Redis 后,由于 Redis 过期机制是按照 unix 时间戳走的
  • 所以在重启后,然后会按照规定的时间过期,不影响业务

风险:

  • 但是由于 AOF 同步到磁盘的方式默认是每秒一次
  • 如果在一秒内断电,会导致数据丢失
  • 立即重启会造成锁互斥性失效

Always 模式的问题:

  • 如果同步磁盘方式使用 Always(每一个写命令都同步到硬盘)
  • 会造成性能急剧下降

权衡:

  • 所以在锁完全有效性和性能方面要有所取舍

有效解决方案:延迟重启

方法:

  • 既保证锁完全有效性
  • 又保证性能高效
  • 即使断电也能处理

具体做法:

  • Redis 同步到磁盘方式保持默认的每秒
  • 在 Redis 无论因为什么原因停掉后
  • 要等待 TTL 时间后再重启(学名:延迟重启)

缺点:

  • 在 TTL 时间内服务相当于暂停状态

十三、怎么保证缓存和数据的双写一致

策略

更新数据

如果更新数据,那么就:

  1. 先删除 Redis 中的数据
  2. 再更新数据库数据

访问数据

如果访问数据,那么就:

  1. 先看 Redis 中是否含有
  2. 如果没有就读取数据库
  3. 然后添加到 Redis

高并发情况

如果高并发情况下:

  • 将请求操作放入到一个队列中
  • 保证串行执行

十四、Redis 为啥这么快

三大原因

  1. ⚡️ 纯内存操作
  2. ⚡️ 单线程模型,避免了上下文切换
  3. ⚡️ 采用了非阻塞的 I/O 多路复用技术

十五、对于大量的请求怎么样处理

Redis 是一个单线程程序:

  • 也就说同一时刻它只能处理一个客户端请求

Redis 是通过 IO 多路复用来处理多个客户端请求的:

  • select
  • epoll
  • kqueue
  • 依据不同的平台,采取不同的实现

十六、Redis 常见性能问题和解决方案

1. Master 持久化

✅ Master 最好不要做任何持久化工作

  • 如 RDB 内存快照和 AOF 日志文件

2. Slave 备份

✅ 如果数据比较重要,某个 Slave 开启 AOF 备份数据

  • 策略设置为每秒同步一次

3. 网络环境

✅ 为了主从复制的速度和连接的稳定性

  • Master 和 Slave 最好在同一个局域网内

4. 避免在压力大的主库上增加从库

✅ 尽量避免在压力很大的主库上增加从库


5. 使用单向链表结构

✅ 主从复制不要用图状结构,用单向链表结构更为稳定

即:Master <- Slave1 <- Slave2 <- Slave3...


十七、为什么 Redis 的操作是原子性的,怎么保证原子性的?

原子性定义

对于 Redis 而言,命令的原子性指的是:

  • 一个操作的不可以再分
  • 操作要么执行,要么不执行

Redis 操作原子性的原因

Redis 的操作之所以是原子性的,是因为 Redis 是单线程的。


Redis API 的原子性

Redis 本身提供的所有 API 都是原子操作。

Redis 中的事务其实是要保证批量操作的原子性。


多个命令在并发中也是原子性的吗?

不一定


如何保证批量操作的原子性?

方法 1:使用单命令操作

将 get 和 set 改成单命令操作,如 incr


方法 2:使用 Redis 的事务

使用 Redis 的事务机制


方法 3:使用 Redis + Lua

使用 Redis + Lua 的方式实现


📚 参考资料:本文内容基于个人学习笔记整理

本文由作者按照 CC BY 4.0 进行授权