Redis 的那点事,我全都记下来了(上)

101 阅读51分钟

 📘 Redis 总结

这里是对Redis的总结,更多相关内容可移步至我的博客

north000.top/

1. Redis 是啥?

Redis 是一种基于内存的非关系型数据库(NoSQL),对数据的读写操作都是在内存中完成,这里的“内存” 指 主内存(RAM):这是直接CPU 访问的高速存储因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。可以实现高频、高并发、存储和操作大量数据的业务

转存失败,建议直接上传图片文件编辑

这里补充知识点1 :

我一直认为Redis中数据存储在缓存Cache中,这当然是不对的,不只是Redis的存储位置,更重要的是对Cache的认识。

Cache的定义:临时存储热点数据的高速介质,*是任何比 原始存储 更快的介质

Cache不是一个物理性的存在,而是概念性的存在,是一个更先前更快速的排头兵。如 : CPU 缓存是主内存的 Cache;主内存可以是磁盘的 Cache ,所以可以说在项目中Redis是 MySQL 的 Cache。

2. 为什么用 Redis 作为 MySQL 的缓存

因为 Redis 具备**「高性能」(响应速度快)和「高并发」**(处理请求量大)两种特性。

  1. 存储位置:Redis 的核心数据(键值对)全部存储在内存中,这是其 “高性能” 的最根本原因

  2. 线程结构: “单主线程” 处理所有命令请求(仅网络 IO 和部分辅助操作由其他线程处理),避免了多线程中的同步和锁的问题。

    • 事件驱动架构:Redis 的操作基于 I/O 多路复用(epoll/kqueue/select 等技术),一个线程通过事件循环来同时处理多个请求,无需阻塞等待,这样的单线程处理无需切换,减少了无效开销。
    • 避免线程开销:多线程操作共享数据时需加,锁的获取 / 释放会导致阻塞;单线程操作内存数据时无需加锁,所有命令串行执行。多线程的 切换(保存 / 恢复线程状态)会消耗 CPU 资源,
  3. 数据结构:Redis底层采用 经过优化的专用数据结构, 这些数据结构不仅设计简单,且操作复杂度很低,执行速度快。

谈到redis的线程模型,Redis 的核心命令处理采用单线程模型,但并不是完全单线程部分功能(如 I/O、删除、加载)使用了多线程或后台线程来增强性能。

3.Redis与MySQL 数据结构

MySQL : 表结构(行+列), 通过主外键关联, 本质上是“磁盘专用、索引优先”,核心依赖 B + 树(适配磁盘 IO),通过聚簇索引存储数据、二级索引加速查询。

转存失败,建议直接上传图片文件​编辑

Redis: 键(Key)- 值(Value)”对形式存储,Key 是唯一标识(字符串类型),Value 支持多种数据结构(String、Hash、List、Set、ZSet 等)。

转存失败,建议直接上传图片文件​编辑

来自小林同学的图:

Redis 键值对数据库的全景图,简述Redis 对象和数据结构的关系了

转存失败,建议直接上传图片文件​编辑

存储结构

  • MySQL:靠Table(表)区分不同业务数据,比如有user表存用户数据、product表存商品数据,表结构清晰界定了数据归属,不同表的同值 ID(如用户 ID 1 和商品 ID 1)不会混淆 。

转存失败,建议直接上传图片文件编辑

  • Redis:无Table概念,所有数据都以Key-Value形式平铺存储。若直接用1当 Key 存用户和商品,就会冲突(Redis 中 Key 唯一),需要得想办法区分不同业务的 Key 。

转存失败,建议直接上传图片文件编辑

解决办法:通过自定义 约定 Key 的命名格式,把业务类型、标识等信息编码进 Key 里,常见做法是用前缀 + 分隔符 + 唯一标识转存失败,建议直接上传图片文件​编辑

Redis有多种数据类型来支持不同的业务场景,比如 String、Hash、 List 、Set、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。

转存失败,建议直接上传图片文件​编辑

转存失败,建议直接上传图片文件编辑

Redis 数据类型的应用场景:

  • String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
  • List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
  • Hash 类型:缓存对象、购物车等。
  • Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  • Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
  • BitMap:二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
  • HyperLogLog:海量数据基数统计的场景,比如百万级网页 UV 计数等;
  • GEO:存储地理位置信息的场景,比如滴滴叫车;
  • Stream :消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

String 类型

常见命令:set、get、decr、incr、mget等。

基本特点:string数据结构是简单的key-value类型,value其实不仅可以是String,也可以是int、float,还可以是

应用场景:常规计数:微博数,粉丝数等。

Redis 的 String 类型底层是自定义了一种名为 SDS(Simple Dynamic String,简单动态字符串) 的结构,不是 C 语言的 char*,具有动态扩容、安全、二进制安全等优势,支持存储字符串、整数、浮点数等内容。

结构设计:

SDS分为三部分(大致)

  • len:字符串的实际长度(无需像 C 字符串那样遍历计算);
  • free:剩余可用空间(预分配的空闲字节);
  • buf:存储字符串的字符数组。
实现细节:

编码转换
当 String 类型存储的是 整数值(且范围在 long 类型内)时,Redis 会采用更节省内存int 编码(直接存储长整型,而非 SDS)。只有当值无法用 int 表示时,才使用 SDS 存储。

最大长度限制
String 类型的最大长度为 512MB(由 buf 数组的容量决定),足以满足绝大多数场景(如存储图片二进制、大文本等)。

List 类型

List 类型的底层数据结构是由双向链表压缩列表实现的:

当满足以下任意条件时,Redis 自动从压缩列表转换为 双向链表, 其结构与标准双向链表类似,但增加了 表头、表尾指针 和 长度计数器。

  • 如果列表的元素个数超过 512 个(默认值,可配置)
  • 列表每个元素的值都超过64 字节(默,可)

但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

QuickList(快速链表) 实现,是多个压缩列表(ZipList)的双向链表,既节省内存又支持高效插入删除。

Hash 类型

Redis 的 Hash 类型底层是 ZipList + Hashtable 的双结构实现,自动根据数据量选择,兼顾节省内存与高性能,小对象节省空间( ZipList ),大对象保障性能( Hashtable ,特别适合存储结构化数据对象,是实际项目中替代 JSON/String 的常用模式。

  • Redis 的 Hash 就像一个 键值对集合(Map) ,适合存储结构化的数据对象。
  • 每个 Hash 有一个 key,内部包含多个 field-value 对。

当满足以下任意条件时,Redis 自动从 ZipList 转换为 Hashtable:

  • field 数量超过 hash-max-ziplist-entries
  • field/value 字符串长度超过 hash-max-ziplist-value转存失败,建议直接上传图片文件​编辑

Set 类型

Redis 的 Set 类型是一个无序且元素唯一的集合,底层由 IntSetHashtable(字典)实现,自动根据数据类型和大小切换。可以说Redis 的 Set 是一个 去重的集合结构,可以存储多个不重复的元素。结构。

当集合满足以下条件时,使用 整数集合(intset) ,若不满足会自动转换为 哈希表(dict) 来实现

  • 集合中的所有元素都是 整数
  • 集合中的元素数量比较 (Redis 没有明确固定的数量阈值,但一般在元素数量不多时采用)

ZSet类型

Redis 的 ZSet(有序集合)是一个元素唯一、score 可重复、自动按 score 排序的数据结构,底层由 跳表(SkipList)+ 哈希表(Dict) 组成,兼顾快速排序与快速访问。

简单来说:ZSet 是一种支持按照“score 分数”排序的集合。

在 Redis 的 ZSet 类型中,HashTable 和 SkipList ( 跳表 )是同时存在的始终一起使用

什么是跳表?

跳表是一种支持快速查找、有序插入的数据结构,效率接近平衡树。

  • 平均时间复杂度:O(log n)
  • 支持排序、范围查询、按 rank 查找等功能。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack(列表包) 数据结构来实现了。

ZSet 这个“数据类型”在不同条件下,有两种编码方式

编码方式适用场景底层结构
ziplist(Redis 6) / listpack(Redis 7+)元素较少,数据较小时压缩结构,节省内存
skiplist + dict(标准实现)元素较多时高性能查找 + 排序组合结构

这里补充知识点2 :

Redis 的类型(比如 ZSet、Hash)≠ 它的底层实现结构。
同一个类型,在不同条件下可能会使用不同的底层编码方式,比如压缩列表(ZipList)、Listpack、哈希表、跳表等。这里先不深究它的底层编码方式。

转存失败,建议直接上传图片文件编辑

Redis 的两个层面(重点理解!

层级含义示例
数据类型(Data Type)用户看到的类型String, List, Set, ZSet, Hash
编码方式(Encoding)Redis 底层用来存储该类型的真实结构int, raw, ziplist, listpack, hashtable, skiplist, quicklist

这里补充知识点3 :

到这里有个疑问,发现Redis在数据量不同的时候底层存储结构是会变化的,所以MySQL也是这样子吗?

✅ 一句话回答:

不是只有 Redis 才会“自动更换底层数据结构”,
MySQL 等数据库系统在一些场景下也会根据数据规模或数据特征选择/切换底层实现。

但两者的设计思路不同、粒度不同、自动化程度也不同

📌 总结对比图

项目RedisMySQL
是否自动切换结构✅ 是(如 ziplist → skiplist)多数手动选择,优化器决定路径
切换的粒度单 key / value表级、索引级
设计目标极限性能、极低延迟通用复杂查询场景
调整触发点数据量阈值、字段长度查询语义、数据分布、索引状态

4.什么是 Redis 持久化?

Redis 是内存数据库,数据默认保存在内存中。为了防止服务重启后数据丢失,Redis 提供了两种持久化机制,将数据保存在磁盘。Redis 提供 RDB(快照)AOF(日志) 两种持久化机制,各有特点且可组合使用。

转存失败,建议直接上传图片文件编辑

AOF 持久化(追加式)

AOF 是将 Redis 执行的写命令(如 SET key value、ZADD 等),以追加的方式记录到 AOF 文件中,类似于流水账,记录了所有对数据进行修改的操作。转存失败,建议直接上传图片文件​编辑

AOF 重写机制:AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大。 如果当 AOF 日志文件过大就会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。

为了减少文件占用的磁盘空间,Redis 提供了 BGREWRITEAOF 命令,它会 fork 一个子进程,子进程对当前的 AOF 文件进行重写,去除掉其中冗余的命令(例如对同一个键多次修改,只保留最后一次修改的命令)。生成一个压缩后的新 AOF 文件,然后将新文件替换旧文件。

此外,还可以通过配置 auto - aof - rewrite - percentage 和 auto - aof - rewrite - min - size 来自动触发 AOF 文件重写。

AOF 重写过程是耗时的后台操作(需要扫描内存、生成新命令序列),那如果客户端在这期间又有新写入怎么办?

为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。

在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」(追加到旧 AOF 文件中)和 「AOF 重写缓冲区」(为即将完成的新 AOF 文件保留)

重写子进程完成后,Redis 会将重写缓冲区的命令追加到新 AOF 文件末尾, 然后替换原来的 AOF 文件

这样新 AOF 文件就包含了:

  • 之前所有数据的最小命令集(子进程生成)
  • 重写期间新发生的写操作(来自重写缓冲区)

数据一致,且无命令丢失

转存失败,建议直接上传图片文件​编辑

RDB 持久化(快照式)

因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢。

这里补充知识点4 :

Redis在重启时内存是的,所以需要AOF重新执行,redis中每条命令的执行结果都依赖上一条命令产生的内存状态

为了解决这个问题,Redis 增加了 RDB 快照。所谓的快照,就是记录某一个瞬间东西,

RDB工作原理:

  • Redis 会在某些时机(如满足配置条件、手动触发)fork 出一个子进程
  • 将当前内存中的数据生成快照,写入 .rdb 文件,注意这里,RDB 快照记录的是实际数据,不是AOF 的操作命令

因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样记录的执行操作命令的步骤才能恢复数据。

此处注意:,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以比较费时,费空间。尽量不要执行这个操作频率太高,影响Redis性能。可若频率太低,服务器故障时,丢失的数据会更多,要注意平衡。

触发方式有多种:

方式命令
定时触发配置文件:save 900 1(900秒内有1次写操作)
手动触发SAVE(阻塞) 或 BGSAVE(后台)
异常退出生成重启或关闭异常时可能触发自动 RDB
主从同步主机会生成一次 RDB 发给从节点

实现RDB的两种方法,区别就在于是否在「主线程」里执行:

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞

那么又到了Redis中必然要考虑的问题,RDB 在执行快照的时候,数据能修改吗?

当然能修改! Redis这么强当然允许修改内容。但不会进入修改开始之前的快照

Redis 使用 写时复制机制(Copy-On-Write, COW) 来确保 RDB 快照文件保存的是生成快照那一刻的内存数据,不会受到之后写入操作的影响,也就是说你可以写新的,但它会保存之前旧的。

COW:

Redis 在执行 RDB 快照时(通过 BGSAVE),会 fork 出一个子进程来完成快照保存。
Redis 利用了操作系统的 写时复制机制(COW) ,确保主线程可以继续处理写操作,而子进程依旧可以看到 fork 时刻的内存快照。
所以写操作不会影响快照的一致性和完整性,性能和可用性也能兼顾。

这里补充知识点5 :

COW 的机制是: 在 Linux 上,fork 操作不会立刻复制父进程内存,而是父子进程共享物理内存页(只读) ,直到某个进程修改该内存页时,才将旧的这页复制一份副本,进入子进程。转存失败,建议直接上传图片文件​编辑

混合持久化

混合持久化是为了解决「RDB 启动快但不完整、AOF 数据完整但启动慢」的问题,Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,结合两者优势,在保证数据恢复速度的同时,也最大程度保证了数据完整性。

实现原理:

在 AOF 重写生成新 AOF 文件的过程中,不是从旧 AOF 文件里提取命令重写
而是直接把当前的 RDB 快照内容作为 AOF 文件的开头( 这里把内存直接导出成 RDB 二进制快照,速度很快
然后再追加「最近的增量写命令」(这里是AOF的AOF 纯命令格式)来补全数据。

转存失败,建议直接上传图片文件编辑

混合持久化优点:

  • 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险

混合持久化缺点:

  • AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差
  • 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。
名称全称持久化方式说明
RDBRedis DataBase快照保存内存数据.rdb 文件定期保存
AOFAppend Only File将所有写命令追加到日志文件实时记录操作日志
混合持久化方式RDB+AOF重写时,Redis 会先使用 RDB 方式将内存中的数据写入 AOF 文件集成了 AOF 和 RBD 的优点

5.缓存策略与缓存问题解决方案

项目中的基本缓存模型:客户端先发送请求先进入Redis如果有则命中返回,如果没有再进入数据库查询返回,然后将本次数据库中用到的数据写入Redis中以备下次使用,无需访问数据库直接命中返回。

转存失败,建议直接上传图片文件编辑

所以这里就涉及到了缓存的更新策略,缓存与数据库的数据是如何产生的?

这里选择的是Cache-Aside(旁路缓存) 更多的更新策略如下表:

转存失败,建议直接上传图片文件编辑

在高并发系统中,Redis 作为缓存中间件,可以大大提升访问效率、减轻数据库压力。但同时也会面临缓存穿透、缓存击穿、缓存雪崩等问题。以下结合理论与黑马点评项目,进行系统总结。

缓存穿透

请求的数据在数据库与缓存中都不存在,每次查询都会穿过缓存,打到数据库,如果同时出现大量的请求会全部打在数据库上(可能有坏人专门同时大量发送恶意请求(如查询 ID=-1 的用户)来攻击数据库),导致数据库压力激增。

转存失败,建议直接上传图片文件编辑

应对缓存穿透的方案,常见的方案有三种:

  • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
  • 设置空值或者默认值:查询数据源无结果时,缓存空值(如null)并设置短期 TTL(如 1 分钟),避免重复穿透;这种方法实现容易、方便维护,但是可能会造成一定的内存消耗

转存失败,建议直接上传图片文件编辑

  //2.判断是否命中

        if (StrUtil.isNotBlank(shopJson)) {

            //3.命中,返回商铺信息

 return JSONUtil.toBean(shopJson, Shop.class);}

        //如果未命中,判断命中的值是否为空

        if (shopJson != null) { return null;}

        //4.如果未命中,通过id查询数据库

        Shop shop = getById(id);

        //5.若不存在,返回404

        if (shop == null) {

            //防止存储穿透,使用存储空对象的方法,将空值写入reids

            stringRedisTemplate.opsForValue().set(key,"", CACHE_NULL_TTL, TimeUnit.MINUTES);

            //返回错误信息

            return null;}

转存失败,建议直接上传图片文件

  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:将数据源中所有有效 Key(如用户 ID、商品 ID)提前存入布隆过滤器,请求先过过滤器,无效 Key 直接拦截(注意:布隆过滤器有小概率误判,需配合空值缓存兜底)。

转存失败,建议直接上传图片文件编辑

缓存击穿

某个热点数据刚好过期,此时大量请求同时访问该 key,导致大量请求穿透缓存,打爆数据库。

转存失败,建议直接上传图片文件编辑

解决办法:

  • 互斥锁(分布式锁) :当缓存未命中,先尝试加锁,只有一个线程能去数据库加载,其它线程等待。锁释放后再从缓存中读取。(如用 Redis 的SET NX实现锁);
//6.1.获取互斥锁

        String lockKey = LOCK_SHOP_KEY + id;

        boolean isLock = tyrlock(lockKey);

        //再一次检测Redis缓存是否过期,做doublecheck ,若存在无需重建缓存

        if(expireTime.isAfter(LocalDateTime.now())){

            //.未过期,直接返回店铺信息

            return r;}

        //6.2.判断是否取锁成功

        if(isLock){

            // 6.3.成功,开启独立线程,实现缓存重建

            CACHE_REBUILD_EXECUTOR.submit(() -> {

                    //6.3.1查询数据库

                    R r1 = dbFallack.apply(id);

                    //6.3.2.写入redis

                    this.setWithLogicalExpire(key,r1, time, unit);

                } catch (Exception e) {

                    throw new RuntimeException(e);

                }finally {

                    //释放锁

                    unlock(lockKey);

                }

            });

转存失败,建议直接上传图片文件

  • 逻辑过期 + 异步更新(黑马点评封装 RedisData 结构):
  1. 先是提交请求到Redis里查,

  2. 然后判断逻辑过去时间,

    • 如果查到了并且也没有过期说明没问题直接返回。

    • 如果查到了,但过期了,就需要获取锁,

      • 如果能够获取到锁(说明这是过期后的第一个请求,所以还没有上锁),就开独立线程到数据库里查。
      • 如果获取不到锁,说明已经上锁,这是后续的请求,便返回旧的数据并且开始等待,就是由一个后台线程异步去刷新数据,前台仍使用旧缓存。

此处注意:Redis 默认不会给你设置 TTL(过期时间) ,你必须自己显式设置,否则 key 会永久存在,直到你手动删除或被覆盖。只要这个 key 设置了过期时间(TTL),即使你一直不访问它,它也会自动过期、被删除。

所以总结来说:Redis 默认支持自动过期机制(Redis 内部判断(TTL)),但在高并发场景下,也可以使用逻辑过期(由代码判断)来更灵活控制缓存刷新时机。

转存失败,建议直接上传图片文件编辑

// 封装逻辑过期时间并写入 Redis
public void save2Redis(Long id, long expireSeconds) throws InterruptedException {
    // 1. 查询店铺数据
    Shop shop = getById(id);
    Thread.sleep(200); // 模拟数据库查询耗时

    // 2. 封装逻辑过期时间(核心:设置过期时间)
    RedisData redisData = new RedisData();
    redisData.setData(shop); // 存储实际业务数据(店铺信息)
    // 设置逻辑过期时间 = 当前时间 + 过期秒数(如20秒)
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));

    // 3. 将封装后的数据写入 Redis(不设置 Redis 自身的 TTL)
    stringRedisTemplate.opsForValue().set(
        CACHE_SHOP_KEY + id,  // Key:店铺缓存的唯一标识
        JSONUtil.toJsonStr(redisData)  // Value:包含逻辑过期时间的 JSON 字符串
        // 注意:这里没有设置 Redis 自带的过期时间(如 TimeUnit.MINUTES)
    );
}




// 重建缓存时,传入过期秒数为 20L(20秒)
    this.save2Redis(id,20L);               

转存失败,建议直接上传图片文件

缓存雪崩

在同一时间大量缓存key同时失效或者Redis服务宕机,导致所有请求都打到数据库,引起数据库压力剧增,甚至崩溃,系统整体不可用。转存失败,建议直接上传图片文件​编辑

解决方案:

  • 将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)或者让过期时间错开分布,这样会在不同时间点逐步过期,不集中失效,也就降低了缓存集体失效的概率。
  • 使用逻辑过期 + 异步缓存重建(黑马点评用法) 手动控制过期时间, 不让热点 key 自行过期,如果判断逻辑时间已过期,先返回旧数据,再异步重建缓存(防止请求打爆数据库),从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。👉 这是黑马点评中解决热点缓存一致性的关键方案。

6.Redis 分布式锁

这里先插个前导知识:

6.1分布式系统知识

分布式系统是什么?

分布式系统Distributed System)是指:由多个独立计算节点(服务器或进程)组成,通过网络协同完成一个共同任务的系统,但实际上是分布在不同的物理位置。

举个生活中的例子:
我们常用的外卖平台(如美团)就是典型的分布式系统 —— 用户下单、商家接单、骑手配送、支付结算等功能,可能分别由不同城市的服务器节点处理,节点之间通过网络同步订单状态,但用户使用时感知不到背后的 “分布式”,只觉得是一个统一的平台。

为什么要用分布式系统?

传统的 “单体系统”(所有功能部署在一台服务器上)在业务发展到一定阶段后,会遇到性能、可靠性、扩展性等瓶颈。分布式系统的核心价值就是解决这些问题,具体原因如下:

原因说明
❌ 单机性能瓶颈CPU、内存、磁盘、网络等资源有限,无法无限扩展
❌ 可用性差一台服务器宕机,整个系统瘫痪
❌ 难以扩展应用和数据库在一台机器上,无法水平拓展
❌ 维护困难大型单体应用代码庞大,部署慢,测试复杂

使用分布式可以解决上述问题。

🔧 分布式系统的核心组成(先做简单了解):
  1. 分布式计算:多个节点协同处理任务(如:MapReduce、Spark
  2. 分布式存储:数据分散存放在多个节点(如:HDFS、MongoDB、Redis Cluster)
  3. 分布式通信:节点间通过 RPC、HTTP、消息队列进行通信(如:gRPC、Kafka)
  4. 分布式协调:保证数据一致性、任务调度等(如:Zookeeper、Etcd)
  5. 分布式一致性算法:如 Paxos、Raft,保证状态同步

6.2什么是分布式锁?

在单机中,虽然Java 的 synchronized 或 ReentrantLock 就可以控制并发,但多个线程在同一 个JVM。在分布式系统中(多个 JVM 实例) 无法使用JDK中的锁保证数据的安全性,需要有一个集中式的锁服务,来保证多个节点之间的互斥操作。

转存失败,建议直接上传图片文件​编辑

单机系统:

多线程并发时,会有多个锁监视器,会有多个线程获取到锁,无法互斥。 转存失败,建议直接上传图片文件​编辑

这样会导致:

多个线程同时操作共享资源,多个线程同时修改同一变量,且操作未通过锁互斥,会导致数据一致性被破坏。具体表现为超卖、数据错乱、逻辑异常等

转存失败,建议直接上传图片文件编辑

分布式系统:

多个线程共用一个锁监视器,保证互斥性

转存失败,建议直接上传图片文件​编辑

分布式锁如何实现转存失败,建议直接上传图片文件​编辑

这里是基于Redis的实现:

最常见的是SETNX 命令实现

SETNX(SET if Not eXists)命令是 Redis 提供的一个原子操作命令,当且仅当键(NX参数)不存在时,将键设置为指定的值,返回 1 表示设置成功(获取锁成功);如果键已经存在,返回 0 表示设置失败(获取锁失败)。

SET lock_key unique_value NX PX 10000 

转存失败,建议直接上传图片文件

实现逻辑:
通过获取当前线程的唯一标识符(JVM 内唯一)threadId,作为锁的 value,用于后续释放锁时校验持有者身份(防止误释放其他线程的锁)。
然后利用setIfAbsent方法(对应 Redis 的 SETNX 命令),进行原子性操作。利用 Redis 的单线程特性,通过 SETNX 保证同一时刻只有一个客户端能创建锁键。然后锁的 value 存储线程 ID,用于校验持有者身份。

// 1. 获取当前线程的唯一标识(用于标记锁的持有者)
long threadId = Thread.currentThread().getId();

// 2. 执行 SETNX 命令:尝试创建锁(原子操作)
Boolean success = stringRedisTemplate.opsForValue()
        .setIfAbsent(
            KEY_PREFIX + name,  // 锁的 Key(如 "lock:order:100",保证唯一)
            threadId + "",      // 锁的 Value(存储线程 ID,用于校验持有者)
            timeoutSec,         // 锁的过期时间(防止死锁)
            TimeUnit.SECONDS    // 时间单位(秒)
        );

转存失败,建议直接上传图片文件

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件

  • 互斥性:通过 SETNX 原子操作,加锁时要带NX参数(0或1),保证同一时间只有一个线程能获取锁;
  • 防止死锁:设置过期时间(EX) 避免客户拿到锁后发生异常,发生异常锁一直无法释放。有了EX, 即使线程崩溃也能自动释放锁;
  • 防止误释放:通过线程 唯一值(ID 校验) 持有者身份,避免释放其他线程的锁。
Redis分布式锁的改进

这样实现看起来万无一失,但其实有一些问题:

1.删除锁的原子性问题:误删他人锁,破坏互斥性, 导致线程和业务十分混乱。

在多线程或分布式环境中,可能出现以下情况:

  1. 线程 A 判断后,即将释放锁时,可能发生
  • 产生阻塞(可能因为JVM垃圾回收)redis锁达到的过期时间阻塞仍未结束
  • 或者此时锁的过期时间到达,Redis 自动删除锁
  1. 线程 B 重新获取锁并修改 id
  2. 线程 A 执行 delete,误删线程 B 的锁

转存失败,建议直接上传图片文件​编辑

解决方案:给锁加唯一标识 + Lua 脚本删除

 public void unlock() {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁中的标识
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //判断标识是否一致
        if (threadId.equals(id)) {
            //一致释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
        //调用Lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId()
                );

    }

转存失败,建议直接上传图片文件


if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    return redis.call('DEL', KEYS[1])
end
return 0

转存失败,建议直接上传图片文件

1.通过加锁时附带唯一标识(例如UUID等),释放锁时再次校验标识,只有持有这个唯一标识的客户端才有权删除锁。

2.使用Lua脚本删除:因为Redis 执行 Lua 脚本时会以原子方式执行脚本里的一系列操作。Lua 脚本可以让多个命令“捆绑”成一个命令执行,保持原子性。

SETNX存在的问题

通过上面的配置,实现了锁的互斥性也保证了删除锁的原子性这样就完美了吗?

No,No,No,其实还有以下几个问题会影响正常业务实现。转存失败,建议直接上传图片文件​编辑

❌不可重入!

Redis 原生的 SET NX EX 是不可重入的,因为它是基于 key 是否存在来判断能否加锁的。

可重入锁是指:同一个线程或客户端在未释放锁的情况下,可以再次获取该锁,不会被阻塞。

🌟 场景:A 方法内部调用了 B 方法,而 A 和 B 都尝试获取同一把 Redis 锁,但现在锁是不可重入的,B获取不到锁,A也因为完成不了B方法也无法释放锁,形成循环等待,导致死锁

不可重试!

Redis 原生加锁方式是非阻塞的:如果加锁失败(key 已存在),直接返回失败返回 false不会等待。会导致业务直接失败。

🌟 场景举例

  • 高并发场景下,锁被其他线程占用,当前线程获取锁失败后,直接放弃执行;
  • 但实际业务中,可能只是 “暂时抢不到锁”,如果重试几次(比如等待 100ms 再重试),就有机会获取锁。把短暂的锁竞争直接转化为业务失败,影响吞吐量。
❌超时释放

锁设置了过期时间,但业务执行时间超过过期时间,会导致锁提前释放,引发 “锁失效”。

🌟 场景举例:A线程逻辑执行了 6 秒,锁在 4 秒时就自动失效,此时线程 B获取到锁,而线程 A 的业务还在执行,最终两个线程同时操作共享资源,数据冲突。

本来超时释放是为了避免死锁,但业务执行时间不可控时,会让锁失去 “互斥性”,引发更严重的线程安全问题。

❌主从一致性(主从集群场景)

✅ 什么是主从一致性问题?

Redis 的主从架构中,写操作先在 主节点 执行,然后异步复制从节点。

Redis 主从架构指的是:(先做简单了解后面会仔细说)

  • 主节点(Master) :处理写操作(也可以处理读操作),是数据的唯一写入入口

  • 从节点(Slave) :只处理读操作,通过主节点复制数据实现同步,不能写入

💡 默认情况下,所有写操作都必须在主节点上执行,从节点不能写。

🌟 场景举例

  1. 客户端 A 成功加锁(主节点执行 SET lock_key
  2. 主节点挂了,还没来得及同步到从节点
  3. Redis 发生故障转移,从节点变成新的主节点
  4. 客户端 B 去新主节点查询 lock_key,发现锁不存在,以为没人持锁 → 执行加锁→ 加锁成功

→ 实际上客户端 A 还在持有锁,锁被“误抢”了,出现并发冲突

面对这些问题虽然我们可以用一些原有的工具去解决部分问题,但还是些许麻烦,能不能找到一个强大有力的新的工具能够解决这些的问题。

千呼万唤始出来,终于要介绍这个能够解决这些问题的一个框架——Redisson

6.3.Redisson

简单来说:

Redisson = Redis 的 Java 工具集 + 分布式基础设施(锁、队列、缓存等)封装。由此可以看出Redissonn功能较多,这里以解决实际遇到的问题来讲解

首先是解决不可重入的问题:

在Redis中用于判断的的分布式锁的底层数据类型是String类型,有一个key,一个value,比较难以实现重入。转存失败,建议直接上传图片文件​编辑

仔细想想哈希结构其实比较适合,他有一个key,两个value。一个用来存线程名,一个用来记录次数

转存失败,建议直接上传图片文件编辑

那么底层实现原理就可以通过判断次数来解决重入问题

🔁 加锁流程:

  1. 客户端生成唯一标识(UUID + 线程ID);
  2. 使用 Lua 脚本判断锁是否存在;
  3. 如果是同一个线程,则只 自增重入次数,并刷新过期时间;
  4. 否则尝试抢锁。

🧹 解锁流程:

  1. 判断锁是否是当前线程加的;
  2. 如果是,减少重入次数;
  3. 如果次数为 0,删除 key,释放锁
  4. 否则只是更新剩余次数。

所以Redisson 提供的RLock基于 Redis 的 Hash 结构 实现的可重入锁

这段代码本身确实没有直接体现 Redisson 可重入锁 “通过计数解决重入问题” 的底层逻辑,因为 Redisson 已经将这些细节封装在了框架内部。直接调用RLock 接口,自带可重入、自动续期等特性,比原生 Redis 锁更完善。

private void handleVoucherOrder(VoucherOrder voucherOrder) {
    //获取用户
    Long userId = voucherOrder.getUserId();
    //1.创建锁对象
    RLock lock = redissonClient.getLock("order:" + userId);
    //2.获取锁
    boolean isLock = lock.tryLock();
    if (!isLock) {
        //获取锁失败,返回失败
        log.error("不允许重复下单");
        return;
    }
    try {
        proxy.createVoucherOrder(voucherOrder);
    }finally {
        //释放锁
        lock.unlock();
    }
}

转存失败,建议直接上传图片文件

Redisson 解决重试问题

自动重试 + 可配置等待时间

Redisson 提供 tryLock 接口,支持等待重试直到超时:


RLock lock = redisson.getLock("myLock");
boolean success = lock.tryLock(10, 5, TimeUnit.SECONDS); 
//最多 等待 10 秒 去抢锁;

//如果抢到锁,持有 5 秒后释放(除非业务提前 unlock);

//在等待期间会 自动重试获取锁。

转存失败,建议直接上传图片文件

接下来该解决超时释放的问题

这里主要使用了著名的看门狗机制

🧭 什么是 Redisson 的“看门狗”机制?

看门狗机制(WatchDog) 是 Redisson 为了解决 Redis 分布式锁“自动过期导致锁提前释放”的问题而设计的一种自动续期机制

触发看门狗机制非常简单,当你调用 Redisson 的 lock() 方法并不传入过期时间时,会自动触发看门狗机制

触发该机制Redisson 会:

  1. 默认加锁 30 秒
  2. 创建一个后台守护线程(看门狗线程)
  3. 每隔 10 秒 自动将锁的过期时间 重置为 30 秒
  4. 持续续期,直到你 unlock() 释放锁。

在大多数场景使用无参 lock(),交由 WatchDog 自动管理锁生命周期。

RLock lock = redisson.getLock("myLock");

lock.lock();  // 不指定时间,会触发看门狗机制

转存失败,建议直接上传图片文件

看门狗本质上是借助 Java 的 定时任务调度线程池 + Redis 的 PEXPIRE 命令来 周期性地刷新锁的过期时间,以确保在锁未释放的情况下不会自动过期。简单来说就是为了后台开启一个线程定时不断地向Redis发送延长锁过期时间的命令,直到释放锁。

看门狗”通用概念

看门狗(Watchdog) 本质上是一种定时监控机制,用于确保某个任务、服务或资源处于预期状态。一旦发现“超时未响应”或“不活跃”,就采取补救措施(重启、释放资源等)。

转存失败,建议直接上传图片文件​编辑

最后解决主从一致的问题:

黑马点评中提到的办法是使用Redisson 的 multiLock (多锁)机制, 它通过在多个独立 Redis 节点上加锁,绕开主从同步问题,避免因主从延迟造成“锁丢失”或“误释放”。

将这样的问题:

转存失败,建议直接上传图片文件​编辑

变成:转存失败,建议直接上传图片文件​编辑

部署多个节点必须在所有 Redis 节点上都成功加锁,才算获取锁成功,同步存储,同步操作,即使一个节点宕机了,还有其他。但这样做实现复杂、运维成本高、性能下降。

这里再介绍一个RedLock(红锁)算法:

RedLock 使用多个 完全独立的 Redis 实例(推荐 5 个) 来保证分布式锁的一致性。它并非依赖 Redis 的主从架构,而是多个主节点构成的“集群”。

RedLock(红锁)算法是为了解决:当同步锁数据到从节点之前,主节点宕机了导致锁失效,那么此时其 他线程就可以再次获取到锁,这个问题怎么解决?

前提条件: 想使用RedLock,你至少要部署5个Redis实例,而且都是主库,它们之间没有任何关系,都是一个个孤立 的实例。

✳️ 加锁流程如下:

  1. 客户端使用唯一值(k,v)尝试依次向多个 Redis 实例发起加锁操作(SET NX PX);同时客户端获取【当前时间戳T1】
  2. 客户端依次向这个5个Redis实例发起加锁请求,且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间), 如果某一个实例加锁失败(包括网络超时,锁被其他的人持有等各种异常情况),就立即向下一个Redis实例申请加锁
  3. 如果客户端从大多数(如 5 个节点中至少 3 个) 以上Redis实例加锁成功,则再次获取【当前时间戳T2】, 如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则加锁失败
  4. 加锁成功,去操作共享资源
  5. 加锁失败,向【全部节点】发起释放锁请求

到这里你肯定感觉疑惑:这能解决主从一致性问题吗?我怎么感觉没有解决锁被“误抢”了的问题。

是的, RedLock 设计初衷是解决部分实例不可用时仍能正确加锁的问题,而不是专门为“主从一致性”问题设计的机制。所以 RedLock 仍不能彻底避免主从复制延迟导致的“误抢锁”问题RedLock 要求的是多个主节点,不是主从架构。

项目Redisson MultiLockRedisson RedLock(红锁)
🔧 原理必须在所有 Redis 节点上都成功加锁,才算获取锁成功半数以上 Redis 节点成功加锁即可视为成功
❌ 容错性容错差,一个节点失败即整体失败容错强,少数节点失败不影响加锁成功
🧠 用途适用于多个业务系统共享多个锁实例的场景适用于跨节点容灾主从一致性保证等场景
⚙️ 实现RedissonMultiLockRedissonRedLock

7.队列 + 异步优化——Redisson实战

核心问题:高并发下的同步处理缺陷

在项目中,高并发场景(如优惠券秒杀、热门商品抢购)是核心挑战之一。这类场景的特点是瞬时请求量极大(可能每秒数万次),若采用同步处理模式,极易导致数据库压力骤增、接口超时甚至系统崩溃。而结合 Redisson 的分布式队列异步处理,可以实现 “削峰填谷 + 高效处理” 的优化,显著提升系统在高并发下的稳定性。

在未优化的秒杀流程中,用户请求会直接穿透到数据库:

  1. 用户发起抢购请求 → 接口校验库存 → 扣减库存 → 创建订单(全同步)。

  2. 问题:瞬时高并发会导致:

    • 数据库连接池耗尽,后续请求阻塞;
    • 库存扣减的锁竞争激烈,大量请求等待锁释放,响应超时;
    • 前端因等待过久出现 “白屏” 或 “超时提示”,用户体验差。
对秒杀业务优化思路:

关键优化主要通过 异步处理 + 分布式锁 + Redis 预校验 组合拳提升系统吞吐量,同时保障数据一致性。

  1. 使用redis完成库存余量、一人一单判断、完成抢单业务。

其中使用lua脚本,确保在redis中保持原子性,避免多线程并发导致的超卖和重复下单。80% 以上无效请求在 Redis 层被拦截,无需访问数据库。

转存失败,建议直接上传图片文件​编辑

  1. 将下单业务放入阻塞队列,利用独立线程异步下单,通过空间(队列缓冲)换时间(快速响应),解决了高并发下的性能瓶颈问题。

解耦前后端流程,主线程(接收请求)只需将订单信息放入队列后立即返回,无需等待数据库操作完成,实现 “快速响应”。

阻塞队列ArrayBlockingQueue底层基于数组实现,元素存储连续,存取效率高于链表结构的队列,适合高并发场景。

// 定义阻塞队列(容量为 1024*1024,基于数组实现的有界阻塞队列) 

private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);

转存失败,建议直接上传图片文件

                   //1.获取队列中的订单信息

                    VoucherOrder voucherOrder = orderTasks.take();

                    //2.创建订单

                    handleVoucherOrder(voucherOrder);

转存失败,建议直接上传图片文件

什么是阻塞队列

阻塞队列:是一种特殊的队列数据结构,它支持:

  • 在队列阻塞获取元素的线程
  • 在队列阻塞插入元素的线程

这种特性使其在多线程环境中非常实用,尤其适合用于线程间的通信、任务调度、生产者 - 消费者模型等场景。
线程安全:内部通过锁机制(如ReentrantLock)保证多线程操作的安全性,无需额外同步。

什么是异步处理

是指程序中的任务不是一步一步、等待完成后才继续,而是先触发任务执行,然后立即返回,等任务执行完后再通知或回调处理结果。

一句话解释:

异步处理 = “我把任务交给你,我先走了,忙完记得告诉我”

转存失败,建议直接上传图片文件​编辑

代码是通过 线程池后台消费者线程 实现了任务的异步处理,核心代码如下:

线程池设计

单线程池:Executors.newSingleThreadExecutor() 确保订单处理串行化,避免多线程并发导致的数据库冲突(如同一用户重复下单)。

// 创建单线程线程池(也可根据需求调整为多线程,如newFixedThreadPool(10)) 

private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

转存失败,建议直接上传图片文件

生命周期管理:通过 @PostConstruct 注解,在服务启动时自动启动消费者线程,确保队列中的订单能被及时处理。

// 服务启动后立即执行(@PostConstruct 注解) 

    @PostConstruct

    private void init(){                 // 提交异步任务(VoucherOrderHandler 实现 Runnable 接口

        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());

    }

转存失败,建议直接上传图片文件

异步处理的核心流程:
  • 主线程(请求处理线程) :用户发起秒杀请求时,seckillVoucher 方法将任务(VoucherOrder)放入阻塞队列后,立即返回订单 ID(Result.ok(orderId)),不等待任务处理完成,实现快速响应;
  • 后台线程(消费者线程)VoucherOrderHandler 线程在后台循环从队列中取任务,异步执行 handleVoucherOrder 处理下单逻辑(扣减库存、创建订单等),与主线程完全解耦。
private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        while (true) { // 无限循环,持续消费任务
            try {
                // 1. 从阻塞队列获取任务(阻塞等待)
                VoucherOrder voucherOrder = orderTasks.take();
                // 2. 异步处理任务(创建订单)
                handleVoucherOrder(voucherOrder);
            } catch (Exception e) {
                log.error("处理订单异常", e);
            }
        }
    }
}

转存失败,建议直接上传图片文件

异步 + 阻塞队列的协同作用

在秒杀场景中,两者的配合解决了高并发下的核心问题:

  1. 削峰填谷:阻塞队列缓冲瞬时大量请求,避免数据库被直接压垮;
  2. 异步解耦:主线程快速响应前端(告知 “已排队”),后台线程慢慢处理任务,提升用户体验;
  3. 流量控制:阻塞队列的有界性(1024*1024)可防止内存溢出(当队列满时,新请求会阻塞或失败,避免无限制堆积任务)。

8.Redis 消息队列

什么是Redis 消息队列:Redis 消息队列 (Message Queue) 是一种利用 Redis 提供的数据结构(主要是 ListStream)来实现生产者-消费者模式的机制,常用于异步处理、削峰填谷、任务调度、系统解耦等场景。

🧠 一句话定义:

Redis 消息队列是指基于 Redis 实现的一种轻量级任务队列系统,让“消息”先临时存放在 Redis 中,由消费者异步取出处理,从而实现系统间的异步通信和流量削峰

转存失败,建议直接上传图片文件​编辑

使用 List + 阻塞命令(经典消息队列) 工作原理:

  • LPUSH:生产者将消息插入队列左侧
  • BRPOP:消费者阻塞等待并从右侧取出消息

转存失败,建议直接上传图片文件​编辑

使用 Stream

(Redis 5.0新增 的强大消息系统)

Redis Stream 更像是 Kafka 的轻量版,支持多消费者组消费确认消息追踪等。

消费者组(Consumer Group):Redis Stream 的核心特性,允许多个消费者分担处理消息,提高并发能力。

优化选择 Redis Stream 的原因:

在秒杀系统中,订单处理需要高可靠性(避免消息丢失)、可扩展性(支持多节点并行处理)和历史查询能力(如对账需求),而 Redis Stream 恰好满足这些需求。相比之下,阻塞队列在分布式场景下的局限性明显,因此被替换。

Redis Stream 优势:

  • 持久化: Redis 中消息存储为结构化数据(类似日志),支持 RDB/AOF 持久化机制,服务重启不会丢失未处理的消息。
  • 多消费者:支持多个订单处理服务实例组成消费者组,并行处理订单,可横向扩展处理能力。
  • 消息确认:通过 ACK (消息确认)机制确保消息处理可靠,消费者必须显式 ACK 消息,未确认的消息会进入 Pending List。Stream 会保留未确认消息。
  • 消息回溯:可从历史消息重新消费(如读取 Pending List),按时间顺序存储消息,每个消息有唯一 ID

转存失败,建议直接上传图片文件​编辑

具体代码实现:

1. 读取 Pending List 消息(Stream 核心 API)

  • 消费者组( g1 )与消费者( c1 :Stream 通过消费者组实现消息的负载均衡和重复消费控制。同一消费者组内的多个消费者会分摊消息处理,避免重复消费;不同消费者组可独立消费同一Stream
  • ReadOffset.from("0-0")0-0 是 Stream 的起始偏移量(第一条消息的 ID 格式为 “时间戳 - 序列号”,如 1620000000000-0)。这里使用起始偏移量,是为了 强制读取 Pending List 中所有未确认的消息(正常消费时会使用 ReadOffset.lastConsumed(),即从上次消费的位置继续)。
List<MapRecord<String,Object,Object>> list = stringRedisTemplate.opsForStream().read(
        Consumer.from("g1", "c1"),  // 消费者组 g1 中的消费者 c1

        StreamReadOptions.empty().count(1),  // 每次读取 1 条消息

        StreamOffset.create(queueName, ReadOffset.from("0-0"))  // 从 Stream 起始位置读取
);

转存失败,建议直接上传图片文件

2. 消息处理与 ACK 确认

  • 消息解析与业务处理:Stream 中的消息以 Map 格式存储,需转换为业务对象(如 VoucherOrder)后执行核心逻辑(如订单处理)。
  • ACK 机制acknowledge 方法会将消息从 Pending List 中移除,确保消息不会被重复处理。若未 ACK(如处理过程中服务宕机),消息会留在 Pending List,等待后续重新处理(即这段代码的作用)。
// 解析消息为订单对象
Map<Object, Object> value = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);

// 处理订单(如扣减库存、创建订单)
handleVoucherOrder(voucherOrder);

// ACK 确认:告知 Redis 消息已处理完成,从 Pending List 中移除
stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());

转存失败,建议直接上传图片文件

3. 异常处理与重试策略

  • 当消息处理失败(如数据库临时不可用),代码通过 “短暂休眠后重试” 避免无限循环占用 CPU,是实战中常见的容错策略。
catch (Exception e) {
    log.error("处理PendingList订单异常", e);
    try {

        Thread.sleep(100);  // 休眠 100ms 再重试

    } catch (InterruptedException interruptedException) {

        interruptedException.printStackTrace();

    }
}

转存失败,建议直接上传图片文件

这段代码主要解决 “消息处理失败后的重试问题”

  • 例如:秒杀订单消息被消费者读取后,因数据库宕机未完成处理,消息进入 Pending List;
  • 服务重启后,这段代码会循环读取 Pending List 中的消息,重新处理订单,避免订单丢失导致的库存不一致或用户投诉。

最后,但没有结束

到这里博客已经覆盖了非常全面的Redis内容,自完成黑马点评项目,我一步步剖析了 Redis 的核心价值:它不仅仅是一个高性能的缓存工具,更是支撑现代高并发系统的关键组件。

在本阶段的学习中,我聚焦于:

  • Redis 的数据结构与底层实现逻辑
  • 持久化机制(RDB、AOF、混合)
  • 分布式锁方案及 Redisson 的改进模型
  • 异步队列、延迟任务与消息流的实际应用
  • 缓存策略及常见问题的解决方案

这不是终点,而是下一段进阶的起点,下一段进阶我们将聚焦与缓存设计策略、Redis的数据删除策略、Redis 集群与部署架构设计等Redis相关知识。