目录
1、Redis简介
1.1 补充数据类型Stream
1.2 Redis底层数据结构
1.3 Redis为什么快
1.4 持久化机制*
1.4.1 RDB持久化
bgsave执行流程
如何保证数据一致性
快照操作期间服务崩溃
RDB优缺点
1.4.2 AOF持久化
为什么采用写后日志
如何实现AOF
什么是AOF重写
AOF重写会阻塞吗
重写日志时新数据写入怎么办
主线程fork出子进程的是如何复制内存数据的
AOF持久化的优缺点
1.4.3 RDB和AOF混合方式
1.4.4 从持久化中恢复数据
1.4 发布订阅模式
1.4.1 基于频道(Channel)的发布/订阅
1.4.2 基于模式(pattern)的发布/订阅
1.5 事件机制
1.6 Redis事务详解*
1.6.1 什么时Redis事务
1.6.2 事务执行流程
1.6.3 CAS操作实现乐观锁
1.6.4 其他相关问题
2、集群相关
2.1 主从复制-高可用
2.1.1 全量复制流程
2.1.2 增量复制流程
2.1.3 其他相关问题
2.2 哨兵机制-高可用
2.2.1 哨兵集群实现原理
2.2.2 哨兵集群的选举
2.2.3 进行故障转移
2.3 分片技术-高扩展
2.3.1 Redis哈希槽
2.3.2 请求重定向
2.3.3 扩容/缩容
2.4 其他问题
3、缓存问题*
3.1 缓存击穿、穿透、雪崩
3.1.1 缓存击穿
3.1.2 缓存穿透
3.1.3 缓存雪崩
3.2 缓存污染(缓存写满)
3.2.1 缓存大小设置
3.2.2 缓存淘汰策略
3.3 数据库和缓存一致性
3.3.1 问题场景
3.3.2 保证一致性方案
(1)使用队列+重试机制:
(2)异步更新缓存(基于订阅binlog的同步机制)
4、应用场景
4.1 Redis优化
4.1.1 BigKey
4.1.2 集中过期
4.1.3 内存上限
4.2 redis实现分布式锁
1、Redis简介
Redis是一款内存高速缓存数据库。使用C语言编写,Redis支持key-value等多种数据结构的存储系统,可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
PS:除上述五个基础数据类型以外,redis还有支持三个特殊的类型
- HyperLogLogs(基数统计):举个例子,A = {1, 2, 3, 4, 5}, B = {3, 5, 6, 7, 9};那么基数(不重复的元素)= 1, 2, 4, 6, 7, 9;这个结构可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数等。
- Bitmap (位存储):即位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态。统计用户信息,活跃,不活跃! 登录,未登录! 打卡,不打卡! 两个状态的,都可以使用 Bitmaps!如果存储一年的打卡状态需要多少内存呢? 365 天 = 365 bit 1字节 = 8bit 46 个字节左右!
- geospatial (地理位置): Redis 3.2 版本就推出了! 这个功能可以推算地理位置的信息: 两地之间的距离, 方圆几里的人。
1.1 补充数据类型Stream
Redis5.0 中还增加了一个数据结构Stream,从字面上看是流类型,但其实从功能上看,应该是Redis对消息队列(MQ,Message Queue)的完善实现(大致了解吧,我没遇到过)。每个 Stream 都有唯一的名称,它就是Redis的key,在我们首次使用xadd 指令追加消息时自动创建。
上图解析:
- Consumer Group :消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer), 这些消费者之间是竞争关系。
- last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
- pending_ids :消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。
此外我们还需要理解两点:
- 消息ID: 消息ID的形式是timestampInMillis-sequence,例如1527846880572-5,它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第5条消息。消息ID可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的ID要大于前面的消息ID。
- 消息内容: 消息内容就是键值对,形如hash结构的键值对,这没什么特别之处
消费组消费图:
1.2 Redis底层数据结构
了Redis的每种对象其实都由对象结构(redisObject) 与 对应编码的数据结构组合而成,而每种对象类型对应若干编码方式,不同的编码方式所对应的底层数据结构是不同:
具体每种数据结构的详细讲解,参考大佬的:Redis进阶 - 数据结构:底层数据结构详解 | Java 全栈知识体系
1.3 Redis为什么快
Redis之所以能够实现高性能,主要有以下几个方面的原因:
- 内存数据结构设计:Redis 将数据存储在内存中,摆脱了磁盘 I/O 的瓶颈,大大提高了读写速度。 Redis 提供了多种数据结构,如字符串、哈希表、列表、集合等,针对不同场景进行优化设计,能够高效地支持各种数据操作。
- 单线程模型: Redis 使用单线程模型处理客户端请求,避免了线程切换和竞争带来的性能开销。 通过高效的事件循环,Redis 能够快速地响应大量并发请求。
- 高效的网络 I/O 模型:Redis 使用非阻塞的 I/O 模型,配合 epoll/kqueue 等高性能的事件处理机制,能够高效地处理网络 I/O。 Redis 采用了 Redis 协议(RESP)作为通信协议,相比 HTTP 等通用协议,RESP 更加简单高效。
- 内存管理优化:Redis 采用自定义的内存分配器,避免了标准 malloc 分配器的性能损耗。 通过对内存进行预分配和复用,Redis 能够减少内存申请和释放的开销。
- 异步操作:Redis 将一些耗时的操作如 BGSAVE、BGREWRITEAOF 等异步化处理,避免阻塞主线程,提高了响应速度。
- 持久化优化:Redis 的 RDB 和 AOF 两种持久化机制都进行了优化,尽量减少持久化过程对性能的影响。
PS:什么是I/O多路复用?
多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
- I/O :网络 I/O
- 多路 :多个网络连接
- 复用:复用同一个线程。
- IO多路复用其实就是一种同步IO模型,它实现了一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;而没有文件句柄就绪时,就会阻塞应用程序,交出cpu。
1.4 持久化机制*
Redis是个基于内存的数据库。那服务一旦宕机,内存中的数据将全部丢失。通常的解决方案是从后端数据库恢复这些数据,但后端数据库有性能瓶颈,如果是大数据量的恢复,1、会对数据库带来巨大的压力,2、数据库的性能不如Redis。导致程序响应慢。所以对Redis来说,实现数据的持久化,避免从后端数据库中恢复数据,是至关重要的。
1.4.1 RDB持久化
RDB中文是快照/内存快照,RDB持久化就是把当前进程数据生成快照持久化到磁盘上的过程(save和bgsave命令)。触发持久化有两种方式,分别是手动触发和自动触发:
手动触发
- save命令:阻塞当前Redis服务器,直到RDB过程完成,对于内存比较大的实例会造成较长时间的阻塞。
- bgsave命令:redis进程执行fork操作创建子进程,RDB过程由子进程完成,晚会层厚自动结束,阻塞只发生在fork阶段,消耗时间很短。
自动触发
- redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;
- 主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;
- 执行debug reload命令重新加载redis时也会触发bgsave操作;
- 默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作;
bgsave执行流程
具体流程如下:
- redis客户端执行bgsave命令或者自动触发bgsave命令;
- 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
- 如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作;
- 子进程先将数据写入到临时的rdb文件中,待快照数据写入完成后再原子替换旧的rdb文件;
- 同时发送信号给主进程,通知主进程rdb持久化完成,主进程更新相关的统计信息(info Persitence下的rdb_*相关选项)
如何保证数据一致性
由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。那么如何保证数据一致性呢?
(1)Copy-on-write(写时复制)机制
- 在进行RDB持久化时,Redis会使用Copy-on-write机制来处理写入请求。这意味着如果一个键正在被写入,Redis会复制该键的值,以便在生成快照时不受新写入操作的影响。
- 当有新的写入请求进来时,Redis会将新写入请求写入到内存中的副本中,而不是直接修改原始数据,从而保证生成的RDB快照是在一个一致的状态下进行的。
(2)RDB持久化期间的写入缓冲
- Redis在进行RDB持久化时,会将新的写入请求暂时缓存在内存中,而不是直接写入到持久化文件中。
- 这样做可以确保生成的RDB快照是在一个一致的时间点上进行的,而不会包含持久化过程中新写入的数据。
快照操作期间服务崩溃
在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。所以期间是有小量数据丢失的可能,有几个尽量降低数据丢失风险的措施:
- 持久化频率调整:调整RDB持久化的频率,可以增加数据持久化的频率,减少数据丢失的可能性。可以通过修改Redis配置文件中的save指令来设置持久化的频率,比如减少保存快照的时间间隔或者增加保存快照的条件。
- 数据备份: 定期对Redis数据进行备份是一种常见的做法,可以将快照文件拷贝到其他地方,比如另一台机器或者云存储服务中,以便在发生数据丢失时进行恢复。
- 使用AOF持久化: 可以同时启用AOF(Append Only File)持久化来记录每次写操作,以便在服务重启时通过重放AOF文件来恢复数据。AOF持久化可以提供更加实时的数据保护,但会增加对硬盘的写入负担。
- 主从复制:在Redis中可以设置主从复制机制,将数据复制到多个节点上,这样即使主节点发生故障,从节点仍然可以提供服务并保证数据的可用性。
- 监控和报警: 部署监控系统来实时监控Redis的运行状态和持久化过程,设置报警规则以便在发生异常时及时通知管理员进行处理。
PS:虽然持久化频率越高,数据丢失的可能性越低,但是在实际应用中也要注意,频繁持久化可能会造成磁盘IO的负担,降低Redis性能,并且增加磁盘的写入次数可能会对磁盘的使用寿命也有影响哦。
RDB优缺点
优点:
- RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;
- Redis加载RDB文件恢复数据要远远快于AOF方式;
缺点:
- RDB方式实时性不够,无法做到秒级的持久化;
- 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;
- RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;
- 版本兼容RDB文件问题;
针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。
1.4.2 AOF持久化
Redis是“写后”日志(即先写内存,后写日志),Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。PS: 大多数的数据库采用的是写前日志(WAL),例如MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性。
为什么采用写后日志
Redis要求高性能,采用写后日志有两方面好处:
- 避免额外的检查开销:Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查,所以如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
- 不会阻塞当前的写操作
但这种方式存在潜在风险:
- 如果命令执行完成,写日志之前宕机了,会丢失数据。
- 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。
如何实现AOF
AOF日志记录Redis的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。
- 命令追加:当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
- 文件写入和同步:关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:
什么是AOF重写
AOF会记录每个写命令到AOF文件,随着时间越来越长,AOF文件会变得越来越大。如果不加以控制,会对Redis服务器,甚至对操作系统造成影响,而且AOF文件越大,数据恢复也越慢。为了解决AOF文件体积膨胀的问题,Redis提供AOF文件重写机制来对AOF文件进行“瘦身”。Redis通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新AOF文件没有了冗余命令:
- AOF重写的触发条件: AOF重写可以通过BGREWRITEAOF命令手动触发,也可以通过配置文件中的auto-aof-rewrite-percentage和auto-aof-rewrite-min-size参数来自动触发AOF重写。
- AOF重写的过程: 在AOF重写过程中,Redis会遍历当前内存中的数据结构,同时记录这些数据结构的生成命令,将这些生成命令写入到新的AOF文件中。
- AOF重写的完成: 当AOF重写完成后,Redis会用新的AOF文件替换旧的AOF文件,这样可以减小AOF文件的体积,提高AOF文件的读取效率。
AOF重写会阻塞吗
AOF重写过程是由后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。所以aof在重写时,在fork进程时是会阻塞住主线程的。
重写日志时新数据写入怎么办
重写过程总结为:“一个拷贝,两处日志”。在fork出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个aof日志内存缓冲区中。
- 主线程fork出子进程重写aof日志,重写过程中Redis 会将新的写入操作先暂存在内存中的 AOF 缓冲区
- 当 AOF 重写完成后,Redis 会将 AOF 缓冲区中积累的新写入操作,追加到新生成的 AOF 文件的末尾。
- Redis 会用新生成的 AOF 文件替换旧的 AOF 文件,这个替换过程是原子性的,可以确保数据的一致性(旧的日志文件:主线程使用的日志文件,新的日志文件:bgrewriteaof进程使用的日志文件)
主线程fork出子进程的是如何复制内存数据的
(1)Copy-on-Write (写时复制) 机制
- Redis 利用操作系统提供的 Copy-on-Write 机制,在 fork 子进程时,子进程会共享主进程的内存页。
- 只有当主进程或子进程有写操作时,操作系统才会为该内存页创建副本,以保证数据的隔离。
- 这种机制可以大幅减少内存的复制开销,提高 fork 子进程的效率。
(2)增量复制
- 在 AOF 重写过程中,子进程只需要复制主进程当前时刻的内存数据,而不需要复制所有历史数据。
- 子进程可以incrementally地复制主进程的增量数据变化,进一步优化内存复制的开销。
(3)异步处理
- Redis 将 AOF 重写的工作放在子进程中执行,主进程继续处理客户端的请求,避免阻塞服务。这种异步的设计,也有利于减小主进程在 fork 子进程时的开销。
PS:为什么AOF重写不复用原AOF日志? 两方面原因:
- 父子进程写同一个文件会产生竞争问题,影响父进程的性能。
- 如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用。
在重写日志整个过程时,主线程有哪些地方会被阻塞?
- fork子进程时,需要拷贝虚拟页表,会对主线程阻塞。
- 主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。
- 子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞。
AOF持久化的优缺点
优点:
- 数据安全性:AOF 持久化可以实时记录每一个写操作,即使在断电或宕机的情况下也能根据 AOF 文件完整地恢复数据,提供更强的数据安全性。
- 数据完整性:AOF 文件记录了所有的写操作,即使 Redis 发生故障重启,也能确保数据完整性,丢失的可能性很小。
- 灾难恢复:AOF 文件可以用于灾难恢复,当 Redis 运行所在的机器发生故障时,通过 AOF 文件可以将数据快速恢复到最新状态。
- 支持数据备份:可以定期备份 AOF 文件,作为数据备份使用。
缺点:
- 写入性能:每次写操作都需要追加写入 AOF 文件,频繁的磁盘 I/O 会对写入性能产生一定影响。
- 文件体积增长:随着写操作的不断增加,AOF 文件会不断增大,占用更多磁盘空间。
- 数据恢复时间:AOF 文件越大,在Redis重启时加载 AOF 文件并重放命令的时间也会越长,数据恢复速度会更慢。
- 兼容性问题:AOF 文件包含了执行写操作的具体命令,如果 Redis 版本发生变化,可能会导致 AOF 文件无法兼容需要进行转换。
为了平衡 AOF 的优缺点,Redis 还提供了 RDB(Redis DataBase)持久化方式,通常情况下,可以结合使用 AOF 和 RDB 两种持久化方式,来获得更好的性能和数据安全性。
1.4.3 RDB和AOF混合方式
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势, 实际环境中用的很多。
1.4.4 从持久化中恢复数据
数据的备份、持久化做完了,我们如何从这些持久化文件中恢复数据呢?如果一台服务器上有既有RDB文件,又有AOF文件,该加载谁呢?
- redis重启时判断是否开启aof,如果开启了aof,那么就优先加载aof文件;
- 如果aof存在,那么就去加载aof文件,加载成功的话redis重启成功,如果aof文件加载失败,那么会打印日志表示启动失败,此时可以去修复aof文件后重新启动;
- 若aof文件不存在,那么redis就会转而去加载rdb文件,如果rdb文件不存在,redis直接启动成功;
- 如果rdb文件存在就会去加载rdb文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么redis重启成功,且使用rdb文件恢复数据;
1.4 发布订阅模式
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。edis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。Redis有两种发布/订阅模式:
- 基于频道(Channel)的发布/订阅
- 基于模式(pattern)的发布/订阅
1.4.1 基于频道(Channel)的发布/订阅
发布/订阅模式包含两种角色,分别是发布者和订阅者。发布者可以向指定的频道(channel)发送消息,订阅者可以订阅一个或者多个频道(channel),所有订阅此频道的订阅者都会收到此消息。
基于频道的发布订阅底层数据结构是一个字典树:
1.4.2 基于模式(pattern)的发布/订阅
如果有某个/某些模式和这个频道匹配的话,那么所有订阅这个/这些频道的客户端也同样会收到信息:
基于模式的发布订阅底层数据结构是一个链表:
1.5 事件机制
Redis 采用了事件驱动(event-driven)的架构设计,其核心就是基于事件循环(event loop)的处理机制。Redis 的事件机制主要包括以下几个部分:
(1)文件事件:
- Redis 使用 I/O 多路复用机制(如 epoll/kqueue)监听客户端连接的读写事件。
- 当有新的客户端连接或者已连接客户端有读写事件时,会产生相应的文件事件。
- Redis 的主事件循环会处理这些文件事件,读取客户端请求,并向客户端发送响应。
(2)时间事件:
- Redis 内部维护了一个时间事件链表,用于处理定期执行的任务,如删除过期 key、主从复制的同步、AOF 重写等。
- 时间事件会被加入到事件循环中,在指定时间触发执行。
(3)事件循环:
- Redis 的主事件循环会不断地检查是否有文件事件或时间事件发生。
- 当有事件发生时,事件循环会调用相应的事件处理器进行处理。
- 事件循环采用了边缘触发(edge-triggered)的I/O多路复用机制,能够高效地处理大量并发的客户端连接。
(4)事件处理器:
- Redis 为不同类型的事件定义了相应的事件处理器,如连接处理器、命令请求处理器、复制处理器等。
- 事件处理器负责对事件进行具体的处理逻辑,如解析客户端命令、更新数据、发送响应等。
总的来说,Redis 的事件机制设计得非常出色,充分利用了 I/O 多路复用和边缘触发的技术,再加上高效的事件循环和事件处理器,使得 Redis 能够以出色的性能处理大量的并发客户端请求,这也是 Redis 能够实现高性能的一个关键所在。
1.6 Redis事务详解*
1.6.1 什么时Redis事务
Redis事务本质上来说是一组命令的集合,具有一致性、顺序性、排他性的执行一个队列中的命令。相关命令的使用如下:
- MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
- EXEC:执行事务中的所有操作命令。
- DISCARD:取消事务,放弃执行事务块中的所有命令。
- WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
- UNWATCH:取消WATCH对所有key的监视
1.6.2 事务执行流程
Redis事务执行是三个阶段:
- 开启:以MULTI开始一个事务
- 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
- 执行:由EXEC命令触发事务
当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:
- 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。
- 与此相反, 如果是这四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。
1.6.3 CAS操作实现乐观锁
WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。被 WATCH 的键会被监视,并会发觉这些键是否被改动过了,如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。watch是如何监视实现的呢?
1.6.4 其他相关问题
为什么Rdeis不像关系型数据库一样支持回滚?
- 设计简单性:支持完整的回滚功能会增加 Redis 内部事务处理逻辑的复杂度,这与 Redis 追求简单高效的设计理念不符。
- 性能考虑:实现完整的回滚需要保存事务执行前的数据状态,会增加内存开销和计算开销,降低 Redis 的整体性能。
- 实际场景考虑:redis命令错误一般是语法错误或者领命用在了错误的键上面,这属于编程错误,应该在开发环境中发现。
Redis 选择在事务中忽略个别命令的失败,而是保证整个事务的原子性执行,是一种权衡设计。这样虽然牺牲了部分灵活性,但大大简化了Redis 的内部实现,提高了整体的性能和可靠性,这对绝大部分应用场景来说是可以接受的。实在需要回滚的场景,程序员可自行定义命令执行脚本。
如何理解Redis与事务的ACID?
事务有四个性质称为ACID,分别是原子性,一致性,隔离性和持久性。在Redis中,是否也完全遵循呢?
- 原子性(Atomicity):Redis 的事务是原子性的,即事务中的所有命令要么全部执行成功要么全部不执行。 即使事务中有个别命令执行失败,Redis 也会忽略这些失败的命令,继续执行其他命令。
- 一致性(Consistency):Redis 的事务可以保证数据库从一种一致状态转移到另一种一致状态。 即使事务中有命令失败,Redis 也会确保数据库最终处于一致的状态。
- 隔离性(Isolation):Redis 的事务具有隔离性,即事务内部的操作不会被其他客户端的操作所干扰。 事务内部的操作对外部是不可见的,直到事务全部执行完毕。
- 持久性(Durability):Redis 的事务具有持久性,即事务一旦提交,其结果就会永久地保存在数据库中。 即使 Redis 服务器发生故障,用户也能通过持久化机制(RDB/AOF)恢复事务的结果。
需要注意的是,Redis 的事务并不完全满足传统数据库的 ACID 特性:
- 部分事务失败问题:如前所述,Redis 的事务中如果有个别命令执行失败,Redis 会继续执行其他命令而不是进行完全的回滚,这与传统数据库的"全有或全无"的事务语义有所不同。
- 隔离性的局限性:Redis 的事务隔离级别较低,只能保证事务内部的操作互不干扰,但无法防止"脏读"、"不可重复读"等问题发生。
因此,我们可以认为 Redis 的事务实现了 Atomicity、Consistency 和 Durability,但在 Isolation 方面与传统数据库还有一定差距,对于需要更严格事务语义的应用,可以考虑结合 Lua 脚本等方式来实现更完善的事务处理。
2、集群相关
Redis集群是Redis提供的一套分布式数据存储方案,它的主要目的是实现Redis的水平扩展和高可用。Redis集群的主要特点:
- 数据分片:Redis集群将数据分散存储在多个节点上,每个节点只存储一部分数据。集群使用哈希槽将数据映射到不同节点上,来实现数据的自动分片。
- 高可用性:Redis集群支持主从复制和故障转移,当主节点故障时,副节点可以自动接管,保证服务的可用性。
- 自动故障恢复:Redis集群能够自动检测节点故障,并进行主从切换,剔除集群中的故障节点,集群能在线动态的增加和剔除节点,实现弹性扩展。
- 一致性保证:Redis集群提供了一致性保证,可以保证数据的强一致性,满足大多数应用场景。
- 客户端路由:Redis集群要求客户端直接与集群交互,所以客户端需要实现集群感知和路由逻辑,Redis 客户端库通常会内置这种集群感知和路由的功能。
- 局限性:Redis 集群不支持事务跨多个 key 操作,只支持单 key 的事务。 Redis 集群无法原生支持数据的全局查询和聚合操作。
2.1 主从复制-高可用
主从复制就字面理解,和关系型数据库的持久化类似,有主从库备份数据防止丢失。主从复制的作用有:
- 数据冗余:主从复制实现了数据的热备份,是持久化外的一种数据冗余方式
- 故障恢复:当主节点故障时,可以由从节点提供服务,实现故障恢复,实际也是一种服务冗余
- 负载均衡:主从复制的基础上配合读写分离(主节点读写操作均可,从节点只读),可以分担服务器负载
- 高可用的基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
在Redis 2.8之前只支持全量复制(比如第一次同步时),在2.8之后新增支持增量复制(只会把主从库网络断连期间主库收到的命令,同步给从库)。
2.1.1 全量复制流程
主要分为三个阶段:
- 第一阶段是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。
- 第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。
- 第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。
2.1.2 增量复制流程
首先要理解两个关键概念:
- repl_backlog_buffer:它是为了从库断开之后,如何找到主从差异数据而设计的环形缓冲区,从而避免全量复制带来的性能开销。如果从库断开时间太久,repl_backlog_buffer环形缓冲区被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量复制,所以repl_backlog_buffer配置尽量大一些,可以降低主从断开后全量复制的概率。
- replication buffer:每个client连上Redis后会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传播用户的写命令到从库,保证主从数据一致,我们通常把它叫做replication buffer。
PS:如果在网络断开期间,repl_backlog_size环形缓冲区写满之后,从库是会丢失掉那部分被覆盖掉的数据,还是直接进行全量复制呢?
对于这个问题来说,有两个关键点:
- 一个从库如果和主库断连时间过长,造成它在主库repl_backlog_buffer的slave_repl_offset位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。
- 每个从库会记录自己的slave_repl_offset,每个从库的复制进度也不一定相同。在和主库重连进行恢复时,从库会通过psync命令把自己记录的slave_repl_offset发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制
2.1.3 其他相关问题
为什么主从全量复制使用RDB而不使用AOF?
- 性能和效率:RDB 是一种更加高效的持久化方式,在生成快照和传输数据方面,相比 AOF 具有更好的性能。 而且RDB 可以生成紧凑的二进制数据,文件体积更小,传输更快。
- 完整性和一致性:RDB 快照记录了数据库在某个时间点的完整状态,能够确保从主服务器向从服务器复制的数据是完整一致的。 而 AOF 日志可能存在部分命令丢失或顺序错乱的问题,不利于保证数据的一致性。
- 启动效率:从服务器在启动时,加载 RDB 快照比加载 AOF 日志更快,可以更快地完成数据初始化。 这对于集群规模较大的场景非常重要,可以缩短服务恢复的时间。
- 可靠性:RDB 快照文件相对来说更加可靠和稳定,不易受到意外情况的影响,如程序 bug、硬盘故障等。 而 AOF 日志可能会遭遇文件损坏或写入失败等问题,影响数据的可靠性。
主从复制基础上实现的读写分离存在什么问题?
(1)延迟与不一致问题
由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。
(2)数据过期问题
在单机版Redis中,存在两种删除策略:
- 惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
- 定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。
在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。
(3)故障切换问题
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。
2.2 哨兵机制-高可用
Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是主节点的自动故障转移。下图是一个典型的哨兵集群监控的逻辑图:
哨兵机制实现了什么功能:
- 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
- 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
- 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
- 通知(Notification):哨兵可以将故障转移的结果发送给客户端。
2.2.1 哨兵集群实现原理
哨兵集群间的通信通过Redis的发布/订阅机制,在主从集群中,主库上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的。
而哨兵监控什么呢?怎么监控呢?由哨兵向主库发送 INFO 命令来完成的:
2.2.2 哨兵集群的选举
哨兵的选举机制其实很简单,就是一个Raft选举算法(后续其他篇目再出关于前面这些基础知识用到的算法解释): 选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举。任何一个想成为 Leader 的哨兵,要满足两个条件:
- 拿到半数以上的赞成票;
- 拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
新主库的选出过程:
- 过滤掉不健康的(下线或断线)没有回复过哨兵ping响应的从节点
- 选择salve-priority从节点优先级最高(redis.conf)的
- 选择复制偏移量最大,只复制最完整的从节点
2.2.3 进行故障转移
新的主库选择出来后,就可以开始进行故障的转移了。故障转移流程如下:
将slave-1脱离原从节点升级主节点,将从节点slave-2指向新的主节点,通知客户端主节点已更换将原主节点(oldMaster)变成从节点,指向新的主节点。转移之后:
2.3 分片技术-高扩展
Redis-cluster是一种服务器Sharding技术,Redis3.0以后版本正式提供支持。个人理解就类似于是MySQL的水平分表。
2.3.1 Redis哈希槽
Redis-cluster没有使用一致性hash,而是引入了哈希槽的概念。Redis-cluster中有16384(即2的14次方)个哈希槽,每个key通过CRC16校验后对16383取模来决定放置哪个槽,Cluster中的每个节点负责一部分哈希槽。
Ps:其他一些有助理解但又不想深入理解的关键词辅助理解:
- Keys hash tags:Hash tags提供了一种途径将多个(相关的)key分配到相同的hash slot中,这是Redis Cluster中实现multi-key操作的基础)。
- Cluster nodes属性:每个节点在cluster中有一个唯一的名字。这个名字由160bit随机十六进制数字表示,并在节点启动时第一次获得。
- Cluster总线:每个Redis Cluster节点有一个额外的TCP端口用来接受其他节点的连接。这个端口与用来接收client命令的普通TCP端口有一个固定的offset。该端口等于普通命令端口加上10000。
- 集群拓扑 :Redis Cluster是一张全网拓扑,节点与其他每个节点之间都保持着TCP连接。
- 节点握手 :节点总是接受集群总线端口的链接,并且总是会回复ping请求,即使ping来自一个不可信节点。然而,如果发送节点被认为不是当前集群的一部分,所有其他包将被抛弃。
2.3.2 请求重定向
Moved重定向:
ASK重定向:Ask重定向发生于集群伸缩时,集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用Ask重定向来解决此种情况。
2.3.3 扩容/缩容
当集群出现容量限制或者其他一些原因需要扩容时,redis cluster提供了比较优雅的集群扩容方案。-整个扩容/缩容过程也是在线进行的,不会影响集群的正常运行。
扩容过程:
- 准备新的 Redis 节点,确保新节点与集群中现有节点能够互联。
- 使用 redis-trib.rb 工具或 Redis 客户端库提供的集群管理命令,将新节点加入集群。
- 集群会自动将部分哈希槽从现有节点迁移到新加入的节点,实现数据的负载均衡。
缩容过程:
- 确定需要从集群中移除的节点,并确保这些节点没有包含任何哈希槽。
- 使用 redis-trib.rb 工具或 Redis 客户端库提供的集群管理命令,将待移除的节点从集群中删除。
- 集群会自动将待移除节点上的哈希槽迁移到其他节点,以保证数据完整性。
2.4 其他问题
Redis集群方案是官方提供的,看起来在分布式系统中使用已经足够了,那为什么很多大型互联网公司还是会重新封装公司内部的分布式环境下Redis插件供内部使用呢?
- 定制化需求:大型互联网公司通常有非常复杂和特殊的应用场景,官方的 Redis 集群方案可能无法完全满足他们的定制化需求。自行封装插件可以根据具体需求进行深度定制和优化。
- 安全和监控:大公司对数据安全和系统监控有更高的要求,官方的 Redis 集群可能无法完全满足这些需求,自研插件可以集成公司内部的安全和监控体系,提供更全面的解决方案。
- 性能和可靠性:大型互联网公司的业务流量巨大,对 Redis 集群的性能和可靠性要求更高,自研插件可以针对公司的硬件环境和业务特点进行针对性优化,提升性能和可用性。
- 管理和运维:大公司通常有专门的 Redis 运维团队,自研插件可以更好地与公司的运维体系集成,简化管理和部署,公司内部的 Redis 插件可以提供更友好的监控和告警功能。
- 学习和积累:通过自研 Redis 插件,公司可以培养内部的分布式系统研发能力,积累相关的技术经验。这种技术积累对于公司的长远发展也很重要。
3、缓存问题*
在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问Mysql等数据库。这样可以大大缓解数据库的压力。当缓存库出现时,必须要考虑如下问题:缓存穿透、缓存穿击、缓存雪崩、缓存污染(或者满了)、缓存和数据库一致性。
3.1 缓存击穿、穿透、雪崩
3.1.1 缓存击穿
场景:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
- 热点key用不过期
- 接口限流和熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制。
- 加互斥锁,关键代码段一次只能被一个线程执行,就避免了大量并发。但加锁就要注意,小心死锁。
3.1.2 缓存穿透
场景:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询。
解决方案:
- 接口请求加上鉴权,避免无效数据请求
- 也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用),这样可以防止攻击用户反复用同一个id暴力攻击
- 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小。
3.1.3 缓存雪崩
场景:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
- 设置热点数据永远不过期
3.2 缓存污染(缓存写满)
缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。
3.2.1 缓存大小设置
系统的设计选择是一个权衡的过程:大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。但是随着数据增加缓存被写满是无可避免的,所以需要由缓存淘汰策略。
3.2.2 缓存淘汰策略
Redis共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。
主要看分三类看:
- 不淘汰
- noeviction (v4.0后默认的)
- 对设置了过期时间的数据中进行淘汰
- 随机:volatile-random
- ttl:volatile-ttl(Redis在筛选需删除的数据时,越早过期的数据越优先被选择)
- lru:volatile-lru(按照最近最少使用的原则来筛选数据)
- lfu:volatile-lfu(访问次数最低的数据淘汰出缓存,次数相同淘汰更久的)
- 全部数据进行淘汰
- 随机:allkeys-random
- lru:allkeys-lru(最近最少使用的原则)
- lfu:allkeys-lfu(访问次数最低的原则)
3.3 数据库和缓存一致性
3.3.1 问题场景
在日常工作中通常使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库,流程通常是下面这样:
But,当数据发生变更的时候,不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。
3.3.2 保证一致性方案
日常开发中一般采取这种方案:
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存
虽然不能保证强一致性,在写数据库到删缓存的期间,请求查询的数据还是缓存中的旧数据,这个短暂的过程无法避免,但是起码不会出现缓存中一直存在过期脏数据未更新的情况。
But,更新数据库成功后再删缓存的过程也有可能发生失败啊,所以可以进一步优化:
(1)使用队列+重试机制:
这个方案的缺点是:对业务代码有大量入侵。
(2)异步更新缓存(基于订阅binlog的同步机制)
这个方案缺点是:借助中间件流程变长,流程越长可能出现问题的环节就越多,而且会增加同步时间。
4、应用场景
4.1 Redis优化
4.1.1 BigKey
Redis中的大键值可能会导致以下方面的问题:
- 内存占用过大:big key会占用大量的内存空间,可能会导致 Redis 实例的内存耗尽,从而无法提供服务。 这种情况下,Redis 可能会被迫关闭或出现严重的性能问题。
- 读写性能下降:读取和修改 big key 会消耗更多的 CPU 和网络带宽资源。 这会导致读写性能下降,影响整个 Redis 集群的吞吐量。
- 网络传输延迟:由于 big key 数据量大,在网络传输时会产生较大的延迟。 这可能会影响客户端的响应时间,甚至引发客户端超时等问题。
- 持久化和复制问题:big key会严重拖慢 RDB 和 AOF 的持久化过程以及主从复制的速度,在某些场景下,可能会导致主从复制中断或者持久化失败。
- Key 删除问题:删除 big key 会消耗大量的时间和资源,可能会引发 Redis 实例短暂不可用。
优化方向:
- 尽量将数据拆分成多个small key
- 为big key 设置合理的过期时间
- 监控 Redis 实例的内存使用情况及时发现和处理big key
- 采用批量删除等方式来删除big key
4.1.2 集中过期
Redis 的过期数据采用被动过期 + 主动过期两种策略。
- 被动过期:只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
- 主动过期:Redis 内部维护了一个定时任务,默认每隔 100 毫秒(1秒10次)就会从全局的过期哈希表中随机取出 20 个 key,然后删除其中过期的 key。
注意,这个主动过期 key 的定时任务,是在 Redis 主线程中执行的。也就是说如果在执行主动过期的过程中,出现了需要大量删除过期 key 的情况,那么此时应用程序在访问 Redis 时,必须要等待这个过期任务执行结束,Redis 才可以服务这个客户端请求。慢日志中没有操作耗时的命令,但我们的应用程序却感知到了延迟变大,其实时间都花费在了删除过期 key 上。
规避方案:
- 集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期 key 的压力
- 如果你使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制,当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程
4.1.3 内存上限
如果你的 Redis 实例设置了内存上限 maxmemory,那么也有可能导致 Redis 变慢。当我们把 Redis 当做纯缓存使用时,通常会给这个实例设置一个内存上限 maxmemory,然后设置一个数据淘汰策略。而当实例的内存达到了 maxmemory 后,你可能会发现,在此之后每次写入新数据,操作延迟变大了。
原因:每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。这个踢出旧数据的逻辑也是需要消耗时间的,而具体耗时的长短,要取决于你配置的淘汰策略
4.2 redis实现分布式锁
在分布式系统中,多个节点/进程需要访问共享资源时需要资源互斥访问,分布式事务协调,控制并发访问等场景,可能需要分布式锁。而Redis就可以简单实现:
- 初始化 Redisson 客户端,配置 Redis 服务器地址。
- 获取一个名为 my_lock 的分布式锁对象。
- 尝试获取锁,最长等待时间 10 秒,锁租约时间 30 秒。
- 成功获取锁后执行业务逻辑,最后释放锁。
- 如果无法获取锁,则打印提示信息。
- 最后关闭 Redisson 客户端。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedisDistributedLock {
private static final String LOCK_KEY = "my_lock";
private static final long LOCK_TIMEOUT = 30000; // 30 seconds
public static void main(String[] args) {
// 初始化 Redisson 客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
// 获取分布式锁
RLock lock = redissonClient.getLock(LOCK_KEY);
try {
// 尝试获取锁,最长等待时间 10 秒,锁租约时间 30 秒
if (lock.tryLock(10, LOCK_TIMEOUT, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
System.out.println("Acquired lock and performing operation...");
} finally {
// 释放锁
lock.unlock();
}
} else {
System.out.println("Unable to acquire lock.");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关闭 Redisson 客户端
redissonClient.shutdown();
}
}
}