首页 / 资讯中心 / 文章详情

Redis核心机制与Java工程实践:缓存穿透、分布式锁及性能优化全解析

Redis核心机制与Java工程实践:缓存穿透、分布式锁及性能优化全解析 ★ FEATURED ARTICLE
开头我就直说了Redis 这个东西凡是做后端开发的几乎没人敢说自己没用过。但用过和用明白之间隔着一条很宽的技术鸿沟。我见过不少项目Redis 挂了缓存穿透把数据库打崩了程序员一脸懵——Redis 不是号称高性能吗怎么还能出这种事问题不在于 Redis 本身而在于使用的人只把 Redis 当成了一个速度更快的 Map。这篇文章我想换一个讲法不按官方文档的目录一项项罗列命令而是从真实项目会遇到的痛点和问题出发把 Redis 的核心机制拆开看再给一套完整的 Java 项目落地实践。无论你是在校学生准备面试还是工作两三年的后端开发想系统补齐 Redis 短板这篇文章应该都能让你少走不少弯路。1. 先把底层的快和稳搞清楚Redis 的运行模型1.1 单线程模型到底意味着什么很多人第一次接触 Redis听到的第一句话就是Redis 是单线程的所以快。这句话既对也不对。对的一面在于Redis 确实把所有命令的执行放在了一个主线程里避免了多线程锁竞争和上下文切换的开销不对的一面在于如果你因此以为 Redis 在多核 CPU 上跑不满、浪费资源那就误会了。Redis 的单线程是指处理命令是串行的但它同时用了多路复用的 I/O 模型select / epoll / kqueue 这一层能够同时监听成千上万个客户端连接。你可以把它这样类比一个能力超强的服务员一个人同时接待很多桌客人每桌点了什么菜他记得清清楚楚上菜速度极快但如果某桌客人非要拉着服务员聊十分钟电话那其他桌全都得等着。这也是为什么 Redis 官方和社区反复强调千万不要在 Redis 里存大 key 或执行慢命令比如 KEYS *、对一个百万元素的 Hash 做 HGETALL因为一旦主线程被阻塞全站所有请求都会排队Redis 的整体吞吐瞬间崩塌。1.2 命令执行的串行特性给业务带来的启发单线程串行执行这本身是一个强大的业务设计工具。很多分布式场景下需要保证原子性的操作如果依赖数据库锁或者分布式锁会有一定的网络开销和实现复杂度但在 Redis 里因为命令天然是串行执行的只要把检查-写入两步合并到一个 Lua 脚本或一个原子命令里就能达到类似的效果。举个例子限制某个用户每分钟最多发 10 条消息。用数据库实现要开事务、加锁用 Redis 则可以用计数器 过期时间的组合或者直接跑一段 Lua 脚本在脚本里完成计数校验和自增。由于 Redis 执行脚本期间不允许其他命令插入整个逻辑是原子且互不干扰的。这也引出一个非常关键的设计原则能用一条命令完成的绝不分成多条命令必须分成多条的优先考虑 Lua 脚本而不是在 Java 代码里串行调用。2. 五种数据结构的选型逻辑与应用实战2.1 String 类型的真实利用率比你想的高String 是所有 Redis 类型的基础本身可以存字符串、数字也可以存二进制数据比如小尺寸的图片字节流。在实际项目中String 最常见的用途远不止缓存 JSON第一是计数器INCR、DECR、INCRBY这类命令天然支持自增自减原子性由 Redis 保证可以用来做访问次数统计、库存扣减、消息未读数等场景。注意如果你的计数需要做持久化要考虑 RDB 和 AOF 的落盘策略如果是纯实时指标放在内存里定期做快照即可。第二是分布式锁的最简实现SET key value NX EX 5NX 表示不存在才写入EX 表示过期时间。这里建议带上一个唯一标识作为 value这样删除锁时先比较再删除避免误删别人的锁。后面第 5 章会给完整的 Java 代码。第三是限流滑动窗口限流可以用 ZSet 实现但如果是固定窗口限流用 String 的SETNX加过期时间就非常方便。这里我想多说一句使用 String 时最容易犯的毛病是把 value 存得过大。单个 String 超过 10KB 就要警惕了尤其是大 JSON。如果确实需要一个对象的多个字段优先考虑 Hash。2.2 Hash对象缓存的最佳载体很多新手喜欢把整个对象序列化成 JSON 塞进 String其实绝大多数场景下Hash 是更合适的选择。Hash 的每一个 field 可以理解为对象的一个属性。以用户信息为例你可以把 User 对象的 name、age、avatar 分别存入同一个 key 的各个 field。这样带来的好处是更新某一个字段时不需要把整个大 JSON 读出来再反序列化再写回去只需要HSET user:1001 age 26一条命令。尤其是在读多写少的场景下Hash 的内存效率和更新效率都明显优于 String 存的整块 JSON。底层结构上也值得一提当 Hash 的字段数量少且字段值不大时Redis 会使用紧凑的内存编码6.x 之后逐步演进方向上是从 ziplist 过渡到 listpack随着元素增多才转换成 hashtable。这意味着小 Hash 非常节省内存很多 Redis 内存优化方案都建议用 Hash 来压缩存储批量数据。我个人的建议是缓存单一实体对象优先选 Hash缓存一个整体且很少改动的数据比如配置项、页面静态片段的字符串才选 String。2.3 List、Set、ZSet 各自的不可替代性List 按照插入顺序存储左边推入右边弹出非常适合做消息队列、简单的时间线列表。它的LPUSHBRPOP模式可以作为一个可靠度够用的异步队列。对比 RabbitMQ 这类专业 MQRedis List 队列少了一些复杂路由和可靠投递语义但胜在轻量、不需要额外部署中间件。小规模项目里用这个方案是一件非常务实的做法。Set 的核心价值是去重和集合运算。抽奖、签到、关注列表交集、两个人之间的共同好友这类场景直接用SADD、SINTER交集、SUNION并集、SDIFF差集就能高效完成。如果不用 Set用数据库做这些运算不仅 SQL 麻烦数据量大时性能也很难看。ZSet 是在 Set 的基础上给每个元素加了一个 score分数按分数排序。这个数据结构实在太适合排行榜了比如积分榜只需要ZADD leaderboard 100 userA然后ZREVRANGE leaderboard 0 99 WITHSCORES就能拿到前 100 名。它还能用来做延迟队列把任务执行时间戳作为 score定时用ZRANGEBYSCORE取到期的任务。还有一种用法经常被忽略ZSet 实现分布式限流的滑动窗口。把每个请求的时间戳塞进 ZSetscore 就是时间戳滑动窗口检查窗口内元素数量超出阈值就拒绝。这个方案在实现复杂度和精度之间平衡得很好。2.4 为什么 Redis 7.0 之后的 ZSet 更值得关注ZSet 的底层传统上是跳跃表 字典的组合跳跃表保证排序和范围查询的高效字典保证按成员查分数的 O(1) 速度。在 Redis 7.0 中官方引入了一个叫 listpack 的新底层结构替换掉了部分场景中的 ziplist目的是解决 ziplist 在连锁更新场景下的性能抖动问题。简单说小数据量时用紧凑内存结构大数据量时自动升级为跳跃表。这套动态切换机制是 Redis 能在大数据量下依然保持稳定延迟的重要原因。对业务开发来说你不需要直接操作这些底层结构但理解它有助于你判断一个 ZSet 在元素不多但 score 频繁更新的场景下是否适合。通常来说ZSet 的成员数量控制在几十万以内性能表现都很理想超过这个量级建议考虑分片存储或换用专业组件。3. 缓存穿透、击穿、雪崩防御体系的完整搭建这是面试高频题也是线上故障的高发区。我把三个问题放在一起讲因为它们经常被混淆但本质上发生的原因和应对手段完全不同。3.1 缓存穿透恶意请求直达数据库的坑穿透指的是请求的数据在缓存和数据库中都不存在。因为缓存里没有每次请求都会打到数据库数据库压力骤增。最典型的是用不存在的用户 ID 连续刷接口此时缓存永远不可能命中。解决穿透的思路有三个方向一是接口层的参数校验。对明显不合法的请求比如 ID 为负数或超大数值直接拒绝这是成本最低的第一道防线。二是缓存空值。查数据库发现没有数据也往 Redis 里写一个空值过期时间设置短一些比如 60 秒。这样后续同样的请求就能命中缓存不会打到数据库。缺点是会缓存大量空 key要在 key 的设计上做好区分避免污染正常数据。三是布隆过滤器这也是我推荐在生产环境使用的方式。布隆过滤器是一个很巧妙的数据结构它用多个哈希函数把元素映射到一个位数组上判断某个 key 是否存在如果它说不存在那一定不存在如果它说存在可能误判但概率很低可以通过参数控制。在查询缓存之前先过一遍布隆过滤器不存在的 key 直接拦截在入口层数据库几乎不会收到无效查询。Java 工程里可以用 Redisson 自带的布隆过滤器也可以用 Google 的 BloomFilter 自行实现。初始化时把已知的合法 ID 集合预先放入后续新增元素同步加入即可。3.2 缓存击穿热点 key 过期的瞬间击穿和穿透一字之差意思却完全不同击穿指的是某个热点 key 在缓存过期的那一瞬间大量请求同时打到数据库。数据库单点扛不住可能直接宕机。最常见的修复方案是互斥锁。当缓存没有命中时不直接查数据库而是先尝试获取 Redis 分布式锁拿到锁的线程去查数据库并回写缓存其他线程排队等待并重新查缓存。这个方案实现简单但有个缺点如果热点 key 重建时间较长会有一批线程阻塞等待接口延迟变大。另一个方案是逻辑过期。也就是缓存里的 value 并不设置 Redis 物理过期时间而是存业务数据和过期时间戳每次读取时检查逻辑是否过期如果过期则返回旧数据同时异步启动一个后台线程更新缓存并替换为新时间戳。这个方案能保证用户请求基本不会被阻塞但实现复杂度更高要处理并发下多次触发更新任务的问题通常配合分布式锁保证只有一个人去更新缓存。我的建议是如果项目并发量中等Redis QPS 几千到几万互斥锁足够如果某个 key 会被几十万甚至上百万的请求同时打必须考虑逻辑过期方案。3.3 缓存雪崩成片 key 同时失效雪崩指的是大量 key 在同一时间段集中过期导致请求分散命中到各个 key 时缓存整体都失效了数据库流量瞬间增加数倍甚至数十倍。这和击穿很像但范围不是一个 key而是一大片 key。最简单的防御手段是给过期时间加随机扰动。比如原本统一过期时间是 10 分钟改成 10 分钟 ± 1~3 分钟的随机数这样各个 key 的过期时间就错开了数据库不会在某一瞬间收到全部回源请求。我见过不少项目缓存 key 的过期时间写成了一个固定的常量比如set(key, value, 3600)这种写法在数据量不大时问题不明显但当你一次性预热了大量 key或者大量 key 都在同一时间段被写入时雪崩几乎是必然的。所以请把过期时间增加随机值写进自己的编码习惯里。还有一层是多级缓存热数据放在本地缓存如 Caffeine里Redis 做二级缓存数据库在最底层。即使 Redis 失效了本地缓存还能顶住一部分流量给 Redis 重建赢得时间。3.4 快速区分三类问题的排查口诀穿透是数据压根不存在特征是数据库收到的请求全是查不到数据的查询Redis 中始终没有对应 key击穿是单个热点 key 失效特征是某一瞬间某个 key 的流量冲垮数据库排查时能看到异常请求集中在同一个 key雪崩是大量 key 同时失效特征是时间上对齐缓存命中率大幅下降数据库压力呈阶梯式上升。4. 内存管理与持久化机制数据不会凭空消失4.1 内存淘汰策略Redis 内存满了之后会发生什么Redis 的数据存在内存里这既是它快的原因也是它的软肋。你必须提前想清楚当内存写满时Redis 会怎么做默认情况下写入会直接报错OOM command not allowed when used memory maxmemory整个服务虽然活着但已经无法写入新数据了。在配置文件 redis.conf 中有一项maxmemory-policy常见的有以下八种策略策略含义适用场景noeviction内存满时拒绝写入某些不允许丢数据的场景但会导致写入失败allkeys-lru所有 key 中淘汰最近最少使用的通用缓存场景最常用volatile-lru仅对设置了过期时间的 key 淘汰 LRU希望没有过期时间的 key 永不被淘汰allkeys-lfu所有 key 中淘汰最不经常使用的热点分布不均、存在极热门 key 的场景volatile-lfu仅对设置了过期时间的 key 淘汰 LFU同上但保护未过期 keyallkeys-random随机淘汰所有 key数据分布均匀、无需优先级的场景volatile-random随机淘汰设置了过期时间的 key极少使用volatile-ttl淘汰剩余存活时间最短的 key希望尽快腾出空间的场景生产环境我建议优先考虑allkeys-lru或allkeys-lfu。如果你的业务有明确的冷热数据区分LFU 通常比 LRU 更能抵抗偶发扫库类操作对热数据的覆盖。具体怎么选要结合业务特征但别选 noeviction除非你确定内存永远不会被打满。4.2 持久化RDB 和 AOF 到底怎么取舍Redis 虽然叫缓存但很多人把它当数据库用存储的数据具有一定的资产属性这时候你不能只依赖内存必须开启持久化。持久化就涉及两个机制RDB快照按照配置的时间间隔比如 900 秒内有 1 次修改或 300 秒内有 10 次修改生成整个数据集的二进制快照文件。优点是文件紧凑、恢复速度快缺点是两次快照之间的数据可能丢失。AOF追加日志记录每一条写命令追加到文件中。优点是数据安全性高可以把丢失范围控制在几秒甚至一条命令内缺点是文件体积大恢复速度比 RDB 慢。AOF 有三种刷盘策略always每条命令都刷盘最安全但最慢、everysec每秒刷一次推荐、no交给操作系统决定安全性最低。组合使用时官方现在的默认做法是打开appendonly yes并开启aof-use-rdb-preamble yes——AOF 文件头部可以是一个 RDB 格式的二进制快照后面再追加增量命令。这样既保证了数据恢复速度又保证了增量数据的安全性。企业在重启或迁移时通常还会配合redis-check-aof --fix做完整性检查。4.3 一个容易踩的坑持久化也要考虑 fork 阻塞不管选择哪种持久化方案在生成快照或重写 AOF 时Redis 会 fork 出一个子进程而 fork 这个动作本身会短暂阻塞主线程。如果内存占用特别大比如几十 GBfork 消耗的时间可能达到秒级这对低延迟业务来说是无法接受的。解决办法是控制单个 Redis 实例的数据量或者用集群模式把大实例拆成若干小实例。这也是为什么在超大规模缓存场景里一般不会让单个 Redis 节点堆到几十 GB而是把内存控制在 4~8GB 以内宁可多开几个分片也不要在一个节点上压榨到极致。5. Java 项目落地实践从依赖到业务代码前面讲完底层原理这一章直接进入实战。我们用 Spring Boot 生态来做示例这是目前 Java 后端最主流的接入方式。5.1 项目依赖与基础配置dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependencyspring-boot-starter-data-redis 默认使用的是 Lettuce 连接池我们需要在 application.yml 里做基础配置spring: data: redis: host: 192.168.1.100 port: 6379 timeout: 3s lettuce: pool: max-active: 30 max-idle: 10 min-idle: 5 max-wait: 500ms这里有几个参数值得解释max-active表示连接池最大活跃连接数过高会导致 Redis 端文件描述符压力骤增过低则可能在高峰期出现请求等待max-wait是获取连接的超时时间超过这个时间直接抛异常避免线程无限挂起。对于大多数中小项目30~50 的 max-active 是合理的起步值。注意如果你在 Spring Boot 2.x 使用 RedisTemplate默认的序列化器是 JdkSerializationRedisSerializer它会把 key 序列化成带有\xAC\xED之类的二进制前缀这不仅肉眼不可读还容易导致无法按预期 pattern 删除 key。生产项目强烈建议自定义序列化为 StringRedisSerializer GenericJackson2JsonRedisSerializer 的组合。5.2 缓存工具类封装与业务代码我用一个简化版的工具类示例来演示如何写一个带防穿透、防击穿逻辑的缓存读取入口Component public class OrderCacheService { Resource private RedisTemplateString, Object redisTemplate; /** * 查询订单详情带缓存重建策略 */ public Order getOrderById(Long id) { String key order:detail: id; // 1. 从缓存获取 Object cache redisTemplate.opsForValue().get(key); if (cache ! null) { // 假设缓存中存了空占位对象用于防穿透 if (cache instanceof EmptyPlaceholder) { return null; } return (Order) cache; } // 2. 获取分布式锁只让一个线程查库并回填 String lockKey lock:order:detail: id; String lockValue UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10)); if (Boolean.TRUE.equals(locked)) { try { // 二次检查可能另一个线程已经重建了缓存 cache redisTemplate.opsForValue().get(key); if (cache ! null) { return (Order) cache; } // 模拟查库 Order order queryFromDatabase(id); if (order null) { // 空值缓存防止穿透 redisTemplate.opsForValue().set(key, new EmptyPlaceholder(), Duration.ofSeconds(60)); } else { // 加随机过期时间防止雪崩 long expireSeconds 600 ThreadLocalRandom.current().nextInt(120); redisTemplate.opsForValue().set(key, order, Duration.ofSeconds(expireSeconds)); } return order; } finally { // 释放锁先比较后删除防止误删别人的锁 String currentValue (String) redisTemplate.opsForValue().get(lockKey); if (lockValue.equals(currentValue)) { redisTemplate.delete(lockKey); } } } // 3. 没抢到锁短暂等待后重新查缓存 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } cache redisTemplate.opsForValue().get(key); if (cache ! null) { return (Order) cache; } // 兜底直接查库虽然概率很低 return queryFromDatabase(id); } }这个工具类的思路是防穿透用空值占位防击穿用分布式锁 二次检查防雪崩用随机过期时间。它是一个通用模板完全可以复用到 User、Product 等其他领域的查询场景。5.3 分布式锁的完整闭环Redis 和 Redisson 的取舍上面代码里的SET key value NX EX是最基础的分布式锁但实际生产环境我更推荐直接用 Redisson它内置了看门狗自动续期机制避免业务执行时间超过锁过期时间导致的并发安全问题。Redisson 的经典用法Resource private RedissonClient redissonClient; public void deductStock(Long skuId, Integer count) { RLock lock redissonClient.getLock(stock:lock: skuId); try { // 尝试加锁最多等 3 秒10 秒后自动释放 if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 执行库存扣减逻辑 // 注意这里依然要保证 Redis 与数据库的操作具备幂等性 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }Redisson 的锁底层也是 Lua 脚本只不过它在客户端封装了续期和重入的逻辑可靠性比手工实现的 SETNX 方案高不少。但要注意在使用分布式锁时一定要考虑锁超时或业务异常导致锁未被正确释放的兜底方案以及 Redis 主从切换时的锁丢失问题极端场景可以引入 RedLock但绝大多数业务不需要这么复杂。5.4 序列化方案选择与连接池参数的经验值在这一步我强烈建议你自定义一个 RedisConfig 类不要直接使用 Spring 默认的 RedisTemplateConfiguration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer keySerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }注意 GenericJackson2JsonRedisSerializer 在反序列化时需要读取类型信息所以如果 value 里保存的是带多态的对象必须在 RedisTemplate 中传入 ObjectMapper 并激活 default typing。这是一个很隐蔽的坑不少同学上线后才发现反序列化报 ClassCastException。连接池参数我一般按这个起步值来配max-active 40max-idle 20min-idle 5max-wait 500ms。如果是读多写少的业务可以考虑把 max-idle 调高一些减少频繁创建连接的损耗如果是写多读少的场景反而要控制 max-active防止连接池占满导致 Redis 吞吐下降。6. 性能优化与线上事故复盘那些血泪教训6.1 Big Key一个超大 key 拖垮整个集群Big Key 指的是单个 key 存储的数据量过大比如一个 List 有几百万条记录或者一个 Hash 有几万个 field。它的危害体现在读取耗时巨大大 key 的网络传输会占用大量带宽删除或迁移大 key 时可能阻塞主线程几秒甚至更久写放大每次更新大 key 涉及的 AOF 重写和主从同步数据量都比较大。排查方法很简单使用redis-cli --bigkeys命令扫描全部 key它会按类型汇总最大元素数量。一旦发现大 key解决的思路是拆分或换结构。比如一个存储用户在线状态的 Hash如果 key 设计成online_users导致 field 数量巨大不如拆成online_user:{userId}的小 key 或分布式存储。我在实际项目里遇到过一次特别典型的故障某运营活动模块把单日所有用户的行为明细都塞进了一个 ZSet高峰期时这个 ZSet 元素超过 300 万每次运营同学手动查询排行榜都造成 Redis 长时间阻塞最终表现是接口超时、缓存雪崩连环爆炸。最后我们把日维度数据拆成小时维度每小时的 ZSet 控制在几十万以内问题才真正解决。6.2 Hot Key 与流量的碰撞拆 key 是门手艺Hot Key 指某一瞬间大量请求集中访问同一个 key。即使 Redis 本身性能再高单 key 的访问也会触及网卡或 CPU 瓶颈同时还会把底层存储打爆。解决方案的思路一般是复制 key为热点 key 增加随机后缀比如seckill:stock:1扩展为seckill:stock:1#0到seckill:stock:1#9十个副本。读取时随机选一个副本写入时则同步写全部副本或者用一致性哈希映射到某个副本。对 Java 项目来说还有一个常见做法是在应用层加 Caffeine 本地缓存把热点 key 的数据在 JVM 内存里再存一份Redis 压力瞬间减少一个量级。本地缓存的问题是一致的所以只适合读取频繁、时效性要求不太高的数据比如商品基本信息、配置表等。6.3 慢查询与监控用什么命令定位问题Redis 提供了慢查询日志默认阈值 10000 微秒10ms可以用CONFIG SET slowlog-log-slower-than 5000把它调低一些比如 5ms便于找出慢日志命令CONFIG SET slowlog-log-slower-than 5000 SLOWLOG GET 20 SLOWLOG LEN慢查询日志只记录执行时间不会记录网络耗时。如果你想更精确地分析某段时间的命令流量可以打开monitor命令观察实时命令流。但monitor本身也有副作用它会降低 Redis 的吞吐只建议在问题现场短暂开启。有一个非常实用的技巧在线上尽量全程开启latency monitor功能设置latency-monitor-threshold 100毫秒然后定期用LATENCY GRAPH查看事件耗时。它可以帮助定位 fork、AOF write、内存淘汰等内部操作的异常耗时。7. 从业务角度重新审视 Redis它不只是中间件7.1 用 Redis 做设计要提前想清楚哪些数据该进缓存很多人把缓存当作一个加速层却忽略了缓存引入后带来的数据一致性问题。缓存更新通常有两种策略先更新数据库再删除缓存Cache Aside还是先双删再更新延迟双删没有绝对正确的方案只有适合业务的方案。我推荐的做法是把数据库作为唯一数据源写操作先写 DB然后删除对应缓存读操作先读缓存不命中则读库并写缓存。这样最简单的实现是先把缓存淘汰掉不存在更新缓存的中间态即使出现并发最多也只是多查一次库不会把脏数据持久化在缓存里。对一致性要求更高的业务比如库存扣减可以用 Canal 订阅数据库 binlog 异步更新缓存配合业务幂等才能真正做到缓存与数据库最终一致。这类方案实现成本高但收益也直接。7.2 集群模式选型主从、哨兵还是 Cluster单机 Redis 在性能和可用性上都有上限生产环境一定不能跑单机。我的建议是数据量小于 10GB业务允许秒级故障用主从 哨兵Sentinel故障自动切换成本低数据量大、需要水平扩展用 Redis Cluster把 key 哈希到 16384 个 slot 上自动分片如果预算充足且对运维能力不自信可以直接选择云厂商的托管 Redis。选了 Sentinel 不等于万事大吉。哨兵模式下主从切换有短暂的不可用窗口同时要考虑 client 侧的连接重连机制选了 Cluster 则要注意多 key 操作比如 MGET 跨 slot在某些配置下会报CROSSSLOT错误业务代码需要提前规避。7.3 一次真实压测数据让我重新理解了持久化的性价比有一个项目上线前我用 4 核 8G 的机器做了简单压测纯读场景下 Redis QPS 大约在 8 万左右开启 AOF everysec 后降到 6.5 万左右开启 RDB 快照后对读性能影响不大但快照期间偶尔会出现毫秒级延迟尖刺。这个数据说明Redis 的性能高是有条件的。你加了持久化、开了集群、选了复杂的 value 结构每一样都在消耗性能预算。作为架构师你要做的不是追求极限 QPS而是找到一个能让业务稳定、数据可恢复、成本可控的组合。性能不是最优解够用且稳定才是。8. 几条贯穿始终的个人经验最后我分享一下自己长期做 Redis 项目沉淀下来的几条原则。第一永远不要相信客户端的超时配置能兜住所有问题。Redis 的阻塞往往是从主线程开始的你的客户端再多重试也叫不醒一个被大 key 阻塞的 Redis 实例。所以一定要做资源隔离把不重要的业务和核心业务拆到不同 Redis 实例至少分成缓存 Redis 和业务数据 Redis。第二上线前必须模拟故障演练。找一个周末低峰期手动执行 FLUSHALL 或者杀掉主节点看看你的应用层能不能自愈缓存重建路径是否顺畅。我见过不少项目故障发生时才发现缓存重建逻辑写错了导致雪崩时所有请求都落到数据库事故被二次放大。第三关注 Redis 版本升级的收益。Redis 7.x 在内存效率、命令丰富度和多线程 I/O 方面都有实质提升如果你的项目还在 5.x 或 6.x可以评估升级的必要性。升级前除了看 release notes一定要用你真实的业务流量做回归压测版本跳跃过大时部分命令行为可能有细微变化。第四在代码评审时把 Redis 命令当成 SQL 来审。凡是循环里执行 Redis 命令的都优先改成 pipeline 或 Lua凡是读出来的数据没有业务意义的都考虑是不是根本没存必要凡是缓存 key 没有业务前缀和清晰命名规范的赶紧定一套规范推下去。这些看起来是小事但线上事故往往就是从一行顺手写的遍历开始的。这篇内容从原理到实践覆盖了我这几年在项目里反复用到的 Redis 核心知识。如果你照着上面的思路搭建一套缓存服务再在压测环境里验证一遍我相信你对 Redis 的理解会上一个台阶也希望下一次 Redis 告警时你能从容地打开命令窗口而不是先慌乱地重启服务。
阅读完成 · 觉得有帮助?
咨询建站