Redis
认识 Redis
Redis 是什么
- 基于内存的数据库,读写速度快,高性能
- 用于缓存,消息队列、分布式锁
- 操作有原子性,执行命令由单线程负责,不存在并发竞争
- 支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制
Redis 数据结构

- String:缓存对象、常规计数、分布式锁、共享 Session 信息
- List:消息队列(生产者需实现全局唯一 ID;无法实现消费组)
- Hash:缓存对象、购物车
- Set:聚合计算(并交差):点赞、共同关注、抽奖
- Zset:排序场景:排行榜、姓名排序
- BitMap:二值状态统计:签到、用户登录状态
- HyperLogLog:海量数据技术统计场景:百万级网页 UV 计数
- GEO:地理位置信息场景:打车、地图
- Stream:消息队列(实现全局唯一 ID,支持消费组)
内部实现

String
String 底层由简单动态字符串(Simple Dynamic String, SDS) 实现
SDS 可以保存文本数据和二进制数据(如图片、视频等)
- 使用
len 来判断字符串是否结束
- 使用
SDS 以 O(1) 时间获取长度(
len)SDS API 是安全的,拼接字符串不会导致缓冲区溢出
List
双向链表或压缩列表
- 元素个数小于 512,每个元素值小于 64 字节,使用压缩列表
- 否则使用双向链表
- 3.2 后使用 quicklist
Hash
压缩列表或哈希表
- 元素个数小于 512,每个元素值小于 64 字节,使用压缩列表
- 否则使用哈希表
- 7.0 后使用 listpack
Set
哈希表或整数集合
- 元素均为整数且个数小于 512,使用整数集合
- 否则使用哈希表
ZSet
压缩列表或跳表
- 元素个数小于 128,每个元素值小于 64 字节,使用压缩列表
- 否则使用跳表
- 7.0 后使用 listpack
Redis 线程模型
单线程:“接收客户端请求 → 解析请求 → 进行数据读写等操作 → 发送数据给客户端”,指这个过程是由一个主线程完成的
但是 Redis 程序不是单线程的,启动时开启后台线程(BIO)
- 2.6,两个后台线程:关闭文件、AOF 刷盘
- 4.0,三个后台线程:关闭文件、AOF 刷盘、lazyfree 异步释放(
del 主线程处理,unlink 异步删除)

单线程模式

Redis 初始化
-
epoll_create() 创建 epoll 对象,socket() 创建服务端 socket -
bind() 绑定端口,listen() 监听 socket -
epoll_ctl() 将listen socket 加入 epoll,注册连接事件处理函数
事件循环函数
- 调用处理发送队列函数,查看发送队列中是否有任务
- 存在发送任务,
write() 将客户端发送缓存区里的数据发送。若这一轮数据未发送完,注册写事件处理函数,等待epoll_wait() 发现可写后再处理
- 存在发送任务,
- 调用
epoll_wait() 等待事件到来,以到来事件类型调用对应处理函数- 连接事件
- 调用
accpet 获取已连接的 socket - 调用
epoll_ctl 将已连接的 socket 加入到 epoll - 注册读事件处理函数
- 调用
- 读事件
- 调用
read() 获取客户端发送数据 - 解析、处理命令
- 客户端对象添加到发送队列
- 执行结果写到发送缓存区等待发送
- 调用
- 写事件
-
write() 发送客户端发送缓存区的数据 - 若这一轮数据未发送完,继续注册写事件处理函数,等待
epoll_wait() 发现可写后再处理
-
- 连接事件
单线程为什么这么快
- 大部分操作在内存中完成,CPU 并不是制约 Redis 性能表现的瓶颈所在
- 单线程模型避免多线程竞争,减少上下文切换,避免死锁
- I/O 多路复用机制处理大量客户端 Socket 请求
- select/epoll 机制
- 同时监听多个 socket,一旦有请求到达就会交给 Redis 处理
6.0 引入多线程
- 使用多个 I/O 线程处理网络请求
- 但是对于命令执行仍是单线程
一般会创建以下线程:
- Redis-server:Redis 的主线程,主要负责执行命令;
- bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF 刷盘任务、释放内存任务
- io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力
Redis 持久化
3 种持久化方式:
- AOF 日志:每执行一条写操作,追加该命令到一个文件
- RDB 快照:将某一时刻的数据写入磁盘
- 混合持久化方式:4.0 新增,结合两者
AOF 日志
Append Only File
- 每执行一条写操作,追加该命令到一个文件。
- Redis 重启时读取该文件,逐一执行命令恢复数据

日志内容

-
*3 表示当前命令有三个部分,每部分都是以+数字 开头,后面跟具体的命令、键或值。 - 数字表示这部分中的命令、键或值一共有多少字节
执行顺序
- Redis 先执行命令,再把命令追加到
server.aof_buf 缓冲区 -
write() 系统调用,缓冲区数据写入 AOF 文件(拷贝到内核缓冲区) - 内核将数据写入硬盘
优点:
- 避免额外检查开销
- 不会阻塞写操作命令
缺点:
- 数据可能丢失(在两个操作间隙 Redis 宕机)
- 阻塞其他操作(AOF 日志在主线程中执行)
AOF 写回策略
即控制写入硬盘的策略
- Always:每次执行完写操作后写入硬盘
- Everysec:每隔一秒写入硬盘
- No:由操作系统决定写入硬盘时机

AOF 重写
扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志
重写机制
AOF 重写机制:当文件大小超过阈值,压缩 AOF 文件
方式:在重写时,读取数据库所有键值对,将每一个键值对用一条命令记录到新的 AOF 文件(即只保留,对同一键值对的多个写操作中,最新的一条)
重写过程
由后台子进程 bgrewriteaof 执行
- 并行处理,避免阻塞主进程
- 子进程带有主进程的数据副本(不是线程)。父子进程共享内存数据(只读,写时复制)
触发重写机制后,主进程创建重写 AOF 子进程,重写子进程只读数据,将内存数据键值对转为成命令,再写入日志
重写缓冲区

在重写 AOF 期间,当 Redis 执行完一个写命令后(修改数据,发生写时复制,父子进程不再共享相同数据),会同时将该命令追加入 AOF 缓冲区和 AOF 重写缓冲区
- 当子进程完成 AOF 重写工作,会向主进程发送异步信号
- 主进程收到信号,调用信号处理函数
- 将 AOF 重写缓冲区的所有内容追加到新文件
- 新文件改名,覆盖旧文件
RDB 快照
Redis Database
记录某一时刻的内存数据
做快照过程
-
save:在主线程生成 RDB 文件 -
bgsave:创建子进程来生成
Redis 的快照为全量快照,会保存内存中的所有数据
写时复制
在做快照过程中,主进程可同时处理命令(数据可修改),可见 xv6 进程部分
- 只读:共享数据
- 发生写操作:进行复制,对副本数据做快照
混合持久化
- RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
- AOF 优点是丢失数据少,但是数据恢复不快。
混合持久化:
- 在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件
- 然后主线程处理的操作命令会被记录在重写缓冲区里
- 重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件
- 写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件
即 AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
优点:
- 开头为 RDB 的格式,使得 Redis 可以更快的启动
- 结合 AOF 的优点,有减低了大量数据丢失的风险
缺点:
- AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差
- 兼容性差(4.0 版本后)
Redis 集群
服务高可用
主从复制

读写分离
- 主服务器可进行读写操作
- 当发生写操作时自动将写操作发送给从服务器
- 从服务器只读,接收主服务器的写操作
主服务器向从服务器发送写命令,不会等待从服务器响应(异步),无法实现强一致性
哨兵模式
主从服务出现宕机需要手动恢复
哨兵模式:监控主从服务器,提供主从节点故障转移
切片集群
当缓存数据量过大,需要使用切片集群,将数据分布在多个服务器上
- 采用哈希槽(Hash Slot)处理数据和节点的映射关系
- 根据 key,以 CRC16 算法计算 16bit 的值
- 该值对 16384 取模
映射方案
- 平均分配:使用
cluster create 创建集群时,自动平均分配哈希槽 - 手动分配:
cluster meet 手动创建节点连接,组成集群。cluster addslots 指定哈希槽数量(且必须将 16384 个哈希槽全部进行分配)

集群脑裂问题
集群脑裂会产生数据丢失
- 在 Redis 主从架构中,一般使用一主多从的部署方式。
- 如果发生主节点网络丢失,和从节点失联,但是主节点和客户端仍然正常通信,客户端向失联的主节点写入数据。主节点将新写入的数据缓存到缓冲区内,但是由于网络问题无法将数据同步给从节点
- 之后哨兵发现主节点失联,会在从节点中重新选择一个节点,此时就会存在两个主节点
- 这样当网络恢复正常时,旧的主节点重新接入,但是由于哨兵已经选取出了新的主节点,那么旧主节点就需要降级为从节点(记为 A 节点)
- 当 A 节点被降级后,会向新的主节点请求数据同步。而第一次数据同步为全量同步,此时 A 结点会先清空本地数据,再做全量同步,会导致网络丢失阶段写入 A 节点的数据全部丢失,即集群产生脑裂数据丢失
解决方案
当主节点发现从节点下线或者通信超时的总数量小于阈值时,禁用主节点进行写数据,并返回错误回客户端
-
min-slaves-to-write x:主节点必须有至少 x 个从节点连接,否则会禁止写数据 -
min-slaves-max-lag x:主从数据复制和同步的延迟不能超过 x 秒,否则会禁止写数据
即设置:主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了
这样无论是主节点发生故障还是网络故障,原主节点都会被限制接收客户端写请求,随后选举产生新主节点,由新主节点进行处理,就不会导致脑裂数据丢失问题了
Redis 过期删除与内存淘汰 Evict(驱逐)
过期删除实现
每当对一个 key 设置了过期时间,Redis 会将该 key 以及过期时间存储到过期字典(expires dict) 中
- 查询 key 时,首先检查该 key 是否存在于该过期字典中
- 如果不在,正常读取键值
- 如果存在,获取过期时间,与当前系统时间比对,大于系统时间则没有过期,否则过期
过期删除策略
惰性删除 + 定期删除
惰性删除策略
不主动删除过期键,只当访问 key 时,判断是否过期,如果过期则删除
优点:
- 每次访问时才会检查过期情况,CPU 资源占用小
缺点:
- 已过期 key 仍然保留在内存中,内存空间浪费
定期删除策略
每个一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中过期的 key
流程:
- 从过期字典中随机抽取 20 个 key
- 检查这 20 个 key 是否过期,并删除已过期的 key
- 如果本轮检查的已过期 key 数量占全部抽取数量 25% 以上,则重复步骤一
优点:
- 限制删除操作执行的时长和频率,减少删除操作对 CPU 的影响,同时也能删除一部分过期数据,减少过期键对空间的无效占用
缺点:
- 难以确定删除操作执行的时长和频率
- 执行太频繁:CPU 不友好
- 执行太少:退化成惰性删除,占用内存
持久化时处理过期键
AOF
- AOF 文件写入阶段:如果数据库某个过期键没被删除,AOF 文件会保留此键,当该过期键被删除后,Redis 向 AOF 文件追加
DEL 命令显式删除该键 - AOF 重写阶段:过期键不会被保存在重写后的 AOF 文件中,即不会对 AOF 重写造成任何影响
RDB
- RDB 文件生成阶段:过期键不会被保存在 RDB 文件中
- RDB 加载阶段:
- 主节点:载入 RDB 文件时,检查键,不会载入过期键
- 从节点:载入所有键,但是主从节点进行数据同步时,从服务器的数据会被清空,所以过期键不会造成影响
主从模式处理过期键
- 主节点:key 过期,在 AOF 文件中增加
DEL 指令,并同步到所有从节点 - 从节点:不进行过期扫描,等待主节点
DEL 命令同步时才删除
核心就是从节点只读,所有写操作都由主节点同步而来
Redis 内存淘汰
maxmemory
内存淘汰策略
八大策略
不进行数据淘汰策略
- noeviction:当前运行内存超过最大设置内存时,不淘汰任何数据,但是不再提供服务,直接返回错误(Redis3.0 后的默认策略)
进行数据淘汰策略
设置过期时间的数据中淘汰
- volatile-random:随机淘汰设置了过期时间的任意键值
- volatile-ttl:优先淘汰更早过期的键值
- volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值
- volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值
所有数据范围内淘汰
- allkeys-random:随机淘汰任意键值
- allkeys-lru:淘汰整个键值中最久未使用的键值
- allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值
LRU
最近最少使用,Redis 实际实现是近似 LRU 的算法
- 在对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间
- 进行内存淘汰时,使用随机采样的方式淘汰数据,随机取多个值,淘汰最久未使用的数据
优点:
- 不用为所有数据维护大链表,节省空间
- 不用在每次数据访问时都移动链表项,提升缓存性能
但是无法解决缓存污染问题:一次读取大量数据,难以批量删除,长期存储在缓存中。因此引入 LFU
LFU
最近最不常用的:根据访问次数来淘汰数据
LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,增加该数据访问次数
Redis 多记录了数据访问频次信息
typedef struct redisObject {
...
// 24 bits,用于记录对象的访问信息
unsigned lru:24;
...
} robj;
Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同
- LRU:记录 key 的访问时间戳
- LFU:被分成两段来存储
- 高 16bit 存储 ldt(Last Decrement Time),记录 key 的访问时间戳
- 低 8bit 存储 logc(Logistic Counter),记录 key 的访问频次
Redis 缓存设计
缓存雪崩

缓存雪崩:大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,导致数据库压力骤增,可能导致数据库宕机
- 缓存失效时间随机打散:增加随机值
- 设置缓存不过期:通过后台服务来更新缓存数据
缓存击穿

缓存击穿:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮(可以认为是缓存雪崩的子集)
- 互斥锁:
setnx 保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么返回空值或默认值 - 不设置过期时间,由后台异步更新;或者在即将过期前通知后台线程更新缓存,并重新设置过期时间
缓存穿透

缓存穿透:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增
发生情况:
- 业务误操作:误删除缓存和数据库中的数据
- 恶意攻击
应对方案:
- 限制非法请求:在 API 入口处判断请求参数是否合理:请求参数是否含有非法值、请求字段是否存在
- 返回空值或默认值:当出现缓存穿透时,在缓存中加载空值或者默认值,而不会继续查询数据库
- 布隆过滤器:在写入数据库数据时,使用布隆过滤器进行标记,查询时可以快速判断数据是否存在
Redis 缓存策略
热点数据动态缓存
核心思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据
- 先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前
- 同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中
- 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回
可以使用 zadd 和 zrange 来完成排序队列和获取商品操作
缓存更新策略
- 旁路缓存 Cache Aside:Redis 实际应用
- 读穿/写穿策略 Read/Write Through
- 写回策略 Write Back
Cache Aside
应用程序直接与数据库、缓存交互,并负责对缓存的维护。可细分为读策略和写策略

- 写策略
- 先更新数据库中的数据,再删除缓存中的数据(不能颠倒顺序,否则会导致缓存和数据库数据不一致)

- 读策略
- 读取的数据命中缓存,直接返回数据
- 没有命中缓存,从数据库中读取数据,写入缓存,再返回
如果颠倒写策略,会出现缓存和数据库数据不一致情况

但实际上也会出现以下情况,只不过缓存写入速度远快于写入数据库速度,所以出现概率极低

Cache Aside 策略适合读多写少的场景,不适合写多的场景。
- 当写入比较频繁时,缓存中的数据会被频繁地清理,会对缓存的命中率有一些影响
- 如果对于缓存命中率要求高
- 更新数据时也更新缓存,在更新缓存前加分布式锁,只允许同一时间只有一个线程更新缓存(影响写入性能)
- 更新数据时也更新缓存,但是给缓存较短的过期时间,即使出现缓存不一致的情况也会很快过期,对业务影响较小(看业务需求)
Read/Write Through
应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理
Read Through
先查询缓存中数据是否存在:
- 如果存在直接返回;
- 如果不存在,由缓存查询数据库,并写入缓存,再返回
Write Through
数据更新时,查询写入的数据在缓存中是否存在:
- 如果存在,则更新缓存,由缓存更新到数据库,并告知更新完成情况
- 如果不存在,则直接更新数据库,返回

在使用本地缓存时可以考虑使用
Redis、Memcached 不提供写入数据库和自动加载数据库数据的功能,无法使用
Write Back 写回策略
- 在更新数据时,只更新缓存,将缓存数据设为脏数据,不更新数据库,立即返回
- 数据库会异步进行批量更新
- Redis 无异步更新数据库功能,一般是用于 CPU 缓存、操作系统中的文件系统缓存
适合写多场景,只需要更新缓存,再批量更新到数据库(硬盘)
但是数据不是强一致性、持久化的,存在数据丢失问题

Redis 实战
实现延迟队列
将当前要做的任务推迟处理
ZSet,使用 score 记录延迟执行的时间
zadd score1 value1添加任务zrangebyscore查询符合条件的(将 score 与当前时间对比)、待处理的任务,取出处理
处理大 Key
大 key 指对应的 value 很大
- String 大于 10KB
- Hash、List、Set、ZSet 元素超过 5000 个
影响
- 客户端超时阻塞:Redis 执行命令为单线程,操作大 key 耗时长
- 网络阻塞:获取大 key 产生的网络流量大
- 工作线程阻塞:使用
del删除大 key 时,会阻塞工作线程 - 内存分布不均:集群模型在 slot 分片均匀的情况下,会出现数据和查询倾斜情况,有大 key 的节点占用内存多,QPS 会更大
查询大 key
redis-cli --bigkeysSCAN- RdbTools 工具
删除大 key
分批次删除
- 大 Hash:
hscan,每次获取 100 个字段;再用hdel每次删除 1 个字段 - 大 List:
ltrim,每次删除少量元素 - 大 Set:
sscan,每次获取 100 个元素;再用srem每次删除 1 个键 - 大 ZSet:
zremrangebyrank,每次删除 top100 个元素
异步删除
Redis4.0 后,可以用 unlink 替代 del 来删除,会将这个 key 放入异步线程中进行删除,不会阻塞主线程
也可以配置参数,当达到某些条件时自动进行异步删除
lazyfree-lazy-eviction
lazyfree-lazy-expire
lazyfree-lazy-server-del
noslave-lazy-flush
默认都是关闭的
- lazyfree-lazy-eviction:当 Redis 运行内存超过
maxmemory时,进行 lazy free 机制删除 - lazyfree-lazy-expire:当设置了过期时间的键值过期后,进行 lazy free 删除
- lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除
- noslave-lazy-flush:针对从节点(slave)进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除
管道
Pipeline,用于一次处理多个 Redis 命令。由客户端提供,不是 Redis 服务端功能
普通交互模式:

管道模式:

使用管道技术可以解决多个命令执行时的网络等待
事务
Redis 中并没有提供回滚机制,discard 能放弃事务执行,清空命令队列,但不会回滚。所以 Redis 不能保证原子性
分布式锁
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用

Redis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁,而且 Redis 的读写性能高,可以应对高并发的锁操作场景
Redis 的 SET 命令有个 NX 参数可以实现 key 不存在才插入,所以可以用它来实现分布式锁
- 如果 key 不存在,则显示插入成功,可以用来表示加锁成功
- 如果 key 存在,则显示插入失败,可以用来表示加锁失败
加锁
实现分布式锁时,对于加锁操作,需要满足以下条件:
- 加锁包括了读取、检查、设置锁变量三个操作,需要保证原子性,使用
SET NX保证 - 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,使用
SET EX或SET PX来设置过期时间 - 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时出现误释放操作,每个客户端使用一个唯一的值用来标识
SET lock_key unique_value NX PX 10000
解锁
删锁就是将 lock_key 删除,需要保证执行删除的客户端也是执行加锁的客户端,即先读 unique_value 的值,再删除 lock_key,需要两步操作,所以要使用 Lua 脚本来保证原子性
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
优缺点
优点:
- 性能高效(选择缓存实现分布式锁最核心的出发点)
- 实现方便,直接使用
SET NX即可 - 避免单点故障:Redis 扩集群部署
缺点:
- 超时时间不好设置:如果锁的超时时间设置过长,会影响性能;如果过短,则不能保护共享资源(比如业务代码执行时间比较长,超过了锁的超时时间,另一个线程就可能拿到锁)
- 基于续约的方式设置超时时间:先设置超时时间,再启动一个守护线程,守护线程在锁快要失效时,进行续约加锁,在主线程执行完毕后销毁续约锁
- Redis 主从复制模式中的数据是异步复制的,分布式锁存在不可靠性:主节点获取锁之后宕机,且没有同步到其他节点,新的主节点仍然可以获取锁,多个服务就可以同时获取锁了
集群情况下分布式锁的可靠性
使用 Redlock(红锁)
- 基于多个 Redis 节点,即使有节点发生故障,锁变量仍然存在
- 而且每个节点都是主节点,相互无关系,都是独立的节点
Redlock:让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,就认为客户端成功获得了分布式锁,否则加锁失败
加锁过程
- 客户端获取当前时间($t1$)
- 客户端按顺序依次向 N 个 Redis 节点执行加锁操作
SET NX EX或SET NX PX并带上客户端的唯一标识- 为了保证算法在某个 Redis 节点故障的情况下仍然能够运行,为加锁操作设置超时时间(而不是锁),加锁操作的超时时间远小于锁的过期时间(一般设置为几十毫秒)
- 一旦客户端从超过半数(大于等于 $N/2 + 1$)的 Redis 节点上成功获取了锁,就再次获取当前时间($t2$),然后计算整个加锁过程的总耗时($t2-t1$)
- 如果 $t2-t1$ 小于锁的过期时间,则认为加锁成功,否则认为加锁失败
即加锁成功需要满足 2 个条件
- 客户端从超过半数的 Redis 节点上成功获取到锁
- 客户端从大多数节点获取锁的总耗时小于锁的过期时间
加锁成功
加锁成功后,客户端重新计算锁的有效时间,结果为 $\text{最初的过期时间}-(t2-t1)$。根据计算结果来判断是否来得及完成共享数据的操作,如果来不及就直接释放锁,避免出现还没进行操作锁就过期的情况。
加锁失败
加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,即对各个节点执行上面的 Lua 脚本