ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Redis高并发缓存项目拆解:数据结构、持久化与三大缓存问题治理

Redis高并发缓存项目拆解:数据结构、持久化与三大缓存问题治理 简介面向毕业设计场景的Redis高并发缓存实战项目以Java语言和Maven工程结构组织适合需要掌握缓存技术、优化系统响应速度的开发者和学生。压缩包共79个文件核心为72个Java源码文件另有XML、Lua、YAML及SQL等配置和脚本整体仅93KB轻量易读。项目内容覆盖Redis常用数据类型的操作与适用场景解析RDB快照和AOF日志两种持久化方案并深入介绍发布订阅、事务、管道等支撑高并发处理的关键机制同时涉及集群搭建与数据分片帮助实现读写分离和负载均衡提升整体吞吐能力。借助完整的工程骨架、测试代码和配置文件学习者可以跟随实战环节从零搭建缓存系统理解缓存穿透、击穿、雪崩等常见问题的应对思路掌握缓存策略的设计与优化方法。目前已有48人学习。1. Redis高并发缓存项目拆解把“快”背后的坑一次补齐把 Redis 用“快”这个结论背得最熟的人往往也是缓存项目里翻车最多的人。把基于 Redis 实战的高并发缓存项目从里到外拆一遍会发现真正值钱的不是那几条高性能命令而是数据结构选型、持久化策略、缓存穿透与雪崩治理这些在异常流量下才暴露的问题。这个资源不是单机 demo从五种核心数据类型、RDB 与 AOF 持久化到发布订阅、事务、管道再到集群搭建和一套完整缓存系统的落地它把“会写 Redis 命令”和“能扛住线上高并发缓存”之间的路补全了。适合两类人一类被毕业设计压着需要一个能演示、能讲清楚原理的缓存系统另一类已经用着 Redis却在缓存失效、超时、连接耗尽的边缘反复踩坑想系统补一遍缓存治理。2. Redis数据类型与场景选型字符串、哈希、列表、集合、有序集合的取舍解压这套项目资源后能看到标准的 Maven 目录pom.xml 在根目录src/main 和 src/test 分开放业务代码与单元测试结构可以直接当毕业设计底子改。Redis 官方给自己的定位是“数据结构服务器”这句话决定了使用方式先想清楚业务对象长什么样再决定用哪个结构而不是一律 SET/GET。这一章把五种核心类型拆成三组每组对应一类典型业务场景。2.1 字符串与哈希热点对象缓存怎么选字符串和哈希是缓存业务对象时最容易纠结的两个结构。字符串适合整体写入、整体读出的数据一个 key 对应一个序列化后的 JSON哈希适合需要频繁更新个别字段的对象比如商品库存、用户资料里的某个属性。两者的本质区别在于“更新的粒度”字符串改一个字段也要 get 出来、反序列化、改完再 set两步之间有并发覆盖的窗口而哈希用一条命令就能只更新目标字段。# 登录会话字符串整体读写EX 指定过期时间单位是秒 SET login:1001 {userId:1001,level:6} EX 7200 # 商品缓存哈希结构字段级更新不用把整个对象读出来再写回去 HSET product:2001 name mechanical-keyboard price 399 stock 200 HINCRBY product:2001 stock -1 HGET product:2001 price这里的 HINCRBY 是原子操作多个客户端并发扣减库存不会丢更新如果换成字符串存 JSON就得 read-modify-write 三步走高并发下必然出现超卖或覆盖。所以选型的第一条判断标准是你的业务是整体读写还是局部字段更新。秒杀、购物车、用户资料这类对象密集更新的场景哈希通常比字符串更稳。2.2 列表、集合、有序集合队列、去重与榜单一站搞定列表最经典的两个用途是消息队列和时间线。LPUSH 投递、BRPOP 阻塞消费配合 timeout 参数可以模拟一个最基本的异步队列。集合天然幂等SADD 同一个成员多次不会重复适合做去重、共同关注这类集合运算。有序集合在排行榜场景里几乎是标准答案score 放分数member 放业务 IDZREVRANGE 直接取 Top N。# 简易任务队列LPUSH 投递BRPOP 阻塞消费超时5秒 LPUSH task:queue order:1001 BRPOP task:queue 5 # 直播间在线用户去重SADD 天然幂等重复加入不会增加成员 SADD stream:3001 user:1001 user:1002 user:1001 # 热销榜score 存销量ZREVRANGE 取 Top10 ZADD sale:rank 500 p:2001 320 p:2002 ZREVRANGE sale:rank 0 9 WITHSCORESBRPOP 的 timeout 传 0 表示永久阻塞传 5 表示最多等 5 秒返回空就继续轮询生产上一般给一个合理的超时值避免连接长期挂起。列表做队列有一个边界要记住BRPOP 返回后消费端宕机这条消息就丢了它没有 Ack 机制。要用 Redis 做可靠队列得在消费侧设计备份集合或等业务处理完再删原数据否则老老实实上专门的消息中间件。ZSET 的 score 设计决定了更新频率把频繁变化的销量当 score每次下单都更新写入压力很大有时会退化成把 score 改成定期批量的热度值。2.3 内存编码与容量观察类型选对之后压缩编码才是黑匣子类型选完只是第一步同一个类型在不同规模下的内部编码完全不同这是新手最容易忽略的黑匣子。Redis 对小列表、小哈希、小集合会用紧凑编码存储以节省内存超过阈值后自动膨胀成标准结构。线上内存涨得看不懂时先别急着调参用命令把真实编码和占用字节数捞出来看。# 查看键当前使用的内部编码 OBJECT ENCODING product:2001 # 估算某个键占用的内存字节数 MEMORY USAGE product:2001如果哈希的字段很少时是 listpack字段涨到几百个就变成了 hashtable列表短时是 quicklist 里的紧凑节点元素多了才拆成多节点链表。编码转换是自动的但转换阈值受配置控制比如 hash-max-listpack-entries 这类参数。学会这两个命令能避免很多“Redis 内存怎么突然多了一倍”的玄学排查。选型结论用一个表收住| 结构 | 典型场景 | 核心命令 | 最容易踩的坑 | | 字符串 | 会话、计数器、验证码 | SET / GET / INCR / SETNX | 大 Value 拖慢网络与持久化 | | 哈希 | 对象字段级操作 | HSET / HGET / HINCRBY | 字段过多退化成大 Key | | 列表 | 简单队列、时间线 | LPUSH / BRPOP | 没有 Ack消费端宕机会丢消息 | | 集合 | 去重、共同关注 | SADD / SINTER | 大数据量求交集耗 CPU | | 有序集合 | 排行榜、延迟队列 | ZADD / ZREVRANGE | score 频繁更新导致写放大 |3. Redis持久化机制RDB快照与AOF日志怎么选参数对照与恢复实操缓存系统最怕什么Redis 重启之后数据全没了数据库被回源流量瞬间打崩。持久化就是给缓存系统上最后一道保险也是面试里 redis 持久化机制详解最常被深挖的一节。这套项目里同时给了 RDB 和 AOF 的配置与分析这一章把两者的差异、参数含义和恢复验证完整走一遍。3.1 RDB与AOF的原理差异先搞清楚能丢多少数据RDB 是 fork 一个子进程把内存全量快照写入临时文件最终替换成 dump.rdbAOF 则是把每一条写命令追加到日志文件重启时重放命令恢复数据。两者不是竞争关系而是备份与恢复的互补关系。RDB 恢复速度极快文件紧凑适合做冷备和全量恢复AOF 能精确到秒级甚至命令级的数据安全但文件大、重放慢。| 对比维度 | RDB | AOF | | 数据形式 | 二进制快照 | 追加写命令 | | 触发方式 | save / bgsave / 自动阈值 | 每次写操作追加可配置刷盘策略 | | 恢复速度 | 快直接加载快照 | 慢需逐条重放命令 | | 数据安全 | 可能丢两次快照之间的数据 | everysec 最多丢 1 秒 | | 文件大小 | 紧凑 | 大靠重写收敛 | | 适合场景 | 备份、全量恢复 | 崩溃恢复、高可靠性要求 |常见做法是两者同时开AOF 保证崩溃后数据尽量不丢RDB 保证重启速度和大版本备份。如果只能选一个优先 AOF但一定要把 appendfsync 配到 everysec 级别。3.2 redis.conf里的持久化参数save、appendfsync与混合持久化这套资源里的配置示例基本覆盖了生产环境的核心参数。save 决定 RDB 自动触发的频率appendfsync 决定 AOF 刷盘的粒度aof-use-rdb-preamble 决定是否启用混合持久化。# RDB 自动触发条件时间段内达到写次数才生成快照 save 900 1 save 300 10 save 60 10000 # AOF 开关与刷盘策略 appendonly yes appendfsync everysec # AOF 重写触发文件比上次重写时涨了100%且超过64MB auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 混合持久化RDB头 AOF增量兼顾加载速度与数据安全 aof-use-rdb-preamble yessave 900 1 的含义是 900 秒内发生了至少 1 次写就触发一次 bgsave三条规则是“或”的关系任一满足就执行。appendfsync 有三个取值always 每条命令刷盘最安全但吞吐明显下降everysec 每秒刷一次最多丢 1 秒数据是性能和安全的折中点no 交给操作系统决定崩溃时丢的数据不可控。混合持久化开启后AOF 重写时先写一个 RDB 前缀再追加后续增量命令重启加载速度比纯 AOF 快不少。这里最常被问的坑是有人说“我开了 AOF 就不管 RDB 了”忽略了两者触发机制完全不同RDB 还承担着快速恢复和灾备的职责。3.3 恢复验证与常见误操作别等崩了才想起文件是坏的配置文件写对了不代表文件一定健康磁盘写满、意外断电都可能让快照或日志损坏。恢复前先做一次完整性检查比启动失败后再翻日志高效得多。# 检查 RDB 文件完整性 redis-check-rdb /var/lib/redis/dump.rdb # 检查 AOF 文件并自动修复截断的尾部 redis-check-aof /var/lib/redis/appendonly.aofredis-check-aof 发现文件尾部被截断时默认会丢弃不完整的部分并提示执行后就能正常启动。这个工具应该写进每次故障恢复的固定流程而不是等 redis 进程起不来才到处找原因。两个典型的误操作我也一起列出来一是只开 AOF 关掉 RDB看似省了写盘实际失去了快速恢复能力和灾备基线二是主从架构里把主从节点的持久化全部关掉只靠复制保命一旦同时宕机整条链路没有任何可恢复的数据。主从复制不等于持久化这个认知比任何参数都重要。4. 高并发缓存三件套发布订阅、事务与管道的边界与实战这套资源把发布订阅、事务、管道列为高并发操作的三大关键技术这个归纳很准。但它们的边界面经常被忽略发布订阅不持久化、事务不负责回滚、管道不保证原子性。这一章把每个机制的适用边界和正确用法讲透。4.1 发布订阅实时推送与缓存失效通知的正确姿势发布订阅适合“消息发出去就算数”的实时场景配置变更、缓存失效广播、实时通知。订阅端收到消息后立刻刷新本地数据这就是摘要里说的“利用发布订阅实现消息的实时更新和推送”。它不适合做可靠消息队列因为没有任何积压能力订阅端离线期间的消息直接丢弃。# 终端 A 订阅频道 SUBSCRIBE config:change # 终端 B 发布变更事件 PUBLISH config:change {product_list_version:3}在 Spring Boot 工程里常见做法是注册一个 RedisMessageListenerContainer 监听指定频道收到事件后清空或刷新本地内存缓存RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(redisConnectionFactory); container.addMessageListener((message, pattern) - { String channel new String(message.getChannel(), StandardCharsets.UTF_8); String body new String(message.getBody(), StandardCharsets.UTF_8); // 收到配置变更事件后清空本地缓存等下次读取时回源 localCache.clear(); }, new ChannelTopic(config:change));监听器里全量清空本地缓存的写法只适合配置类的小数据量如果缓存规模大建议按业务 key 精准失效。另外注意频道名称要统一规范发布端和订阅端写错一个字符消息就静默丢失这类问题排查起来非常头疼。4.2 事务、Lua脚本与分布式锁原子性到底靠谁保证MULTI/EXEC 在 Redis 里做的事情是“排队连续执行”它隔离了其他客户端插入命令但不提供回滚如果事务中间某条命令运行时出错之前的命令已经生效。这是与关系型数据库事务最本质的差别。真正需要“先判断再操作”这种原子逻辑时正解是 Lua 脚本。以分布式锁为例加锁一条命令搞定释放锁时必须用 Lua 比对持有者再删除否则会出现 A 删掉 B 的锁的问题。# MULTI/EXEC 只是批量排队执行遇到运行时错误不会回滚 MULTI SET stock:2001 199 INCR counter:2001 EXEC # 分布式锁一条命令完成加锁NX 表示不存在才设置EX 30 表示30秒自动过期 SET lock:order:1001 owner:instance-1 EX 30 NX # 释放锁Lua 校验持有者避免误删别人的锁 EVAL if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end 1 lock:order:1001 owner:instance-1EVAL 脚本里的 KEYS[1] 是锁的键名ARGV[1] 是持有者标识先 get 比对再 del这个判断和删除是原子执行的。SET NX EX 是官方推荐的加锁写法注意持锁逻辑如果跑超过 30 秒锁会自己过期需要额外的看门狗续期机制。分布式锁是面试题里的高频点但把这段 Lua 脚本吃透比背十道八股都管用。4.3 管道批量操作的RTT优化与边界管道解决的是网络往返问题。逐条发送 1000 条 Set 命令要 1000 次 RTT用管道一次性发出可以减少到 1 次。这个项目里的缓存回填、预热场景用管道非常合适。# 生成 1000 条 SET 命令并批量发送大幅减少网络往返 for i in $(seq 1 1000); do echo SET batch:key:$i value$i; done | redis-cli --pipe # Java 侧用 executePipelined 批量提交 ListObject results redisTemplate.executePipelined((RedisCallbackObject) connection - { for (int i 0; i 1000; i) { connection.stringCommands().set( (batch:key: i).getBytes(StandardCharsets.UTF_8), (value i).getBytes(StandardCharsets.UTF_8)); } return null; });前面的 for 循环生成 1000 条 SET 命令通过管道发送比逐条执行快一个数量级。Java 侧 executePipelined 的回调里连续执行命令返回的 List 与命令顺序一一对应。管道的边界在于它不提供原子性中途某条命令失败前后的命令照常执行。所以批量灌数据用管道要求“要么全成要么全不成”的场景不能用管道应该走 Lua 脚本这两个机制的分工要分清楚。5. 避坑指南缓存穿透、击穿、雪崩与Lettuce超时的排查记录缓存治理的核心不在加速而在接住所有“缓存没扛住”的流量。穿透、击穿、雪崩这三个词在面试八股和故障单里出现频率一样高下面五条是我在真实环境和毕业设计答辩里都见过的高频问题每条都按现象、原因、解决来拆。5.1 缓存穿透空值也缓存别让恶意流量直穿数据库现象Redis 命中率几乎为 0数据库被打到告警而且查询的 key 都是库里根本不存在的 ID。原因缓存策略是“查到才写缓存”查不到就不写同一个不存在的 key 每次都穿透到数据库。解决空值缓存加短 TTL或者前置布隆过滤器。String val redisTemplate.opsForValue().get(user: id); if (val ! null) { return null.equals(val) ? null : val; } User user userMapper.selectById(id); if (user null) { // 空值也缓存TTL 给短一点避免缓存堆积 redisTemplate.opsForValue().set(user: id, null, 60, TimeUnit.SECONDS); return null; } redisTemplate.opsForValue().set(user: id, JSON.toJSONString(user), 1800, TimeUnit.SECONDS); return user;代码里把“null”字符串也写进缓存后续请求在缓存层就直接返回了不再打到数据库。空值缓存的 TTL 控制在几十秒到几分钟太长会积累大量无意义 key。布隆过滤器适合 key 集合固定的业务能拦截大部分不存在请求但需要维护数据同步成本不低小项目直接用空值缓存更省事。5.2 缓存击穿热点key过期瞬间别让数据库扛全部流量现象某个热点 key秒杀商品、首页头条过期后瞬时大量请求同时回源数据库连接数瞬间打满。原因并发高但 key 只有一个过期瞬间没有缓存可读所有请求一起穿透。解决互斥锁重建缓存抢到锁的线程查库回填其他线程短暂等待后重查缓存。Boolean lock redisTemplate.opsForValue() .setIfAbsent(lock:hot:item:2001, 1, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(lock)) { try { // 查数据库并回填缓存 String json loadFromDb(2001); redisTemplate.opsForValue().set(hot:item:2001, json, 1800, TimeUnit.SECONDS); } finally { redisTemplate.delete(lock:hot:item:2001); } } else { Thread.sleep(50); // 忽略中断异常简化写法 return redisTemplate.opsForValue().get(hot:item:2001); }setIfAbsent 对应 SET NX拿到锁的线程负责回源拿不到的线程睡 50 毫秒重查缓存。锁必须带过期时间防止持锁线程异常退出造成死锁删除锁时要核对自己的持有者标识否则可能出现 A 线程删掉 B 线程刚拿到的锁配合第 4 章的 Lua 脚本才是完整闭环。热点 key 也可以不设过期时间由后台任务主动更新但要注意更新失败的兜底策略。5.3 缓存雪崩过期时间加随机抖动把回源流量摊平现象大量 key 在同一时刻过期数据库压力瞬间拉满甚至拖垮 Redis 本身。原因批量写入缓存时用了相同的过期时间比如统一设置 30 分钟所有 key 在同一分钟集体失效。解决过期时间加随机偏移让失效时间分散开。// 基础过期 30 分钟加上 0-300 秒的随机偏移 int ttl 1800 new Random().nextInt(300); redisTemplate.opsForValue().set(product: id, json, ttl, TimeUnit.SECONDS);这一段 Random 偏移是关键代码写起来就一行但能让原本同时过期的 key 分散到 5 分钟窗口内数据库回源流量被大幅摊平。生产上还会做缓存分层本地 Caffeine 缓存挂一层Redis 雪崩时本地缓存还能兜底。雪崩有时不是过期时间设计问题而是 Redis 实例本身宕机导致缓存层整体失效这种情况要回到集群和高可用方案去解决不能只靠代码。5.4 RedisCommandTimeoutException先看慢日志再看连接池现象Java 服务报 io.lettuce.core.RedisCommandTimeoutException: Redis command timed out接口大面积超时Redis 客户端控制台刷满错误。原因大多数情况下不是网络问题而是 Redis 内部有耗时命令卡住了事件循环或者 Lettuce 连接池被占满大量请求在池外排队等连接。先查服务端再查客户端这条顺序我踩过坑反着查很容易浪费时间调一堆没用的参数。# 查看最近的慢查询命令 SLOWLOG GET 10 # 连续 ping 10 次每次间隔 1 秒观察响应是否稳定 redis-cli -r 10 -i 1 ping慢日志里如果看到 KEYS、SMEMBERS 这类大范围扫描命令基本就是元凶KEYS 在线上是禁用级别的操作会阻塞整个实例。排除服务端问题后再去看 Lettuce 连接池配置血泪经验是连接池别调太大默认几十足够关键是操作要快而不是连接要多spring.redis.lettuce.pool.max-active50 spring.redis.lettuce.pool.max-idle20 spring.redis.lettuce.pool.min-idle5max-active 控制最大连接数max-idle 控制空闲保留数min-idle 是常驻最低数。调参的前提是服务端没有慢命令否则加再多连接也堵在同一个 redis-cli 上。5.5 可视化客户端连不上bind与protected-mode要一起看现象用 Redis Desktop Manager 或 Another Redis Desktop Manager 连远程 Redis提示 Connection refused 或 DENIED。原因redis.conf 默认 bind 127.0.0.1 并且 protected-mode yes只允许本机访问。解决先设置密码再放开监听地址。# 先设密码再放开监听避免裸奔 requirepass your-strong-password bind 0.0.0.0Windows 上装 Redis 也一样修改 redis.conf 里这两项后重启服务。注意 bind 0.0.0.0 只适合内网测试和开发环境生产环境应该保持默认本机监听用安全组或防火墙白名单把端口限定到业务服务器。很多人只改 bind 不改 protected-mode或者只改 protected-mode 不改 bind结果还是连不上这两个参数是配合生效的。6. 从单机到集群主从复制的最小方案与验证习惯如果这套缓存系统要往“高并发”再走一步第一步不是直接上 Cluster而是主从复制。主从解决读扩展和高可用Redis Cluster 解决数据分片这是两件事。这里给一个最小主从方案用 docker compose 拉起两个 Redis 容器镜像 tag 可以换成你本地可用或项目里指定的 Redis 版本。# docker-compose.yml 最小主从master 开 AOFslave 连 master services: master: image: redis:7 command: redis-server --appendonly yes --requirepass redis123 ports: [6379:6379] slave: image: redis:7 command: redis-server --slaveof master 6379 --masterauth redis123 --requirepass redis123 depends_on: [master] ports: [6380:6379]# 主节点写入 redis-cli -p 6379 -a redis123 SET cluster:test ok # 从节点读取从节点默认只读 redis-cli -p 6380 -a redis123 GET cluster:test # 确认复制状态master_link_status 必须是 up redis-cli -p 6380 -a redis123 INFO replicationslaveof 后面的参数是从节点连接主节点的地址和端口masterauth 填主节点的密码requirepass 是从节点自己的访问密码。INFO replication 输出里最关键的一行是 master_link_status:up看到 syncing 说明全量同步还没完成此时读从节点得到的是不完整数据。注意主从复制不是持久化的替代品从节点的数据来自主节点的同步推送同步断了数据就不新鲜了两边最好都保留持久化。我见过最典型的翻车是主从都配好了业务读写全打在主节点从节点只有复制流量主节点一抖动整个链路跟着抖从节点既没接住读也没接住故障。从那以后我每次搭主从或集群都会强制在从节点上执行一遍 INFO replication确认 master_link_status 是 up 才继续调业务同时把持久化和主从状态写进上线检查清单。这个习惯救过我至少两次希望帮到你。本文还有配套的精品资源点击获取
返回列表