news 2026/10/17 5:47:34

Redis分布式缓存实战:从穿透、击穿到一致性设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis分布式缓存实战:从穿透、击穿到一致性设计

做后端这几年,Redis几乎贯穿了我负责过的所有核心系统。商品详情、用户会话、排行榜、库存扣减,背后都躲不过一个Redis实例。有一年某线上项目大促前例行压测,数据库连接数频频告警,排查下来发现绝大多数请求都在查同一批热点数据,数据库每次都要白花几十毫秒去走磁盘索引。后来引入分布式缓存之后,接口平均响应时间从180ms降到了18ms,数据库峰值QPS从将近两万直接落到了三千出头。这种体感,应该很多后端同行都有过。

标题里的“分布式缓存”,说白了就是把缓存从应用进程内抽离出去,独立成一个共享的缓存服务,让所有应用节点访问同一份缓存数据。它解决的不只是单机缓存的内存空间受限、节点间数据不一致、重启即丢失这些问题,更多是解决高并发场景下数据库不堪重负的问题。这篇内容基于我对Redis缓存的落地实践,重点聊聊分布式缓存的核心设计、三大经典问题、缓存与数据库的一致性,以及高可用部署和日常运维。适合刚开始接触Redis的开发者,也适合正在做架构优化、准备把缓存体系补齐的后端同学参考。

1. 先想清楚:分布式缓存到底解决了什么问题

1.1 数据库瓶颈是怎么出现的:一次真实压测记录

很多团队一开始是没想着上缓存的。系统刚上线,用户量不大,数据库随便查都能扛住。但随着业务增长,总有几个接口会被频繁调用,比如商品详情、用户信息、配置数据。这类接口的特征很明显:读多写少、请求量大、数据变化不频繁。

我经历过一个具体场景。某系统里有一张两千多万行的订单扩展表,单行查询在MySQL里平均耗时8ms左右。听起来不慢吧?但问题是这个接口被多个上游服务循环调用,峰值时每秒有两万多次查询。8ms乘以两万,数据库的连接池瞬间被打满,大量请求排队等待,最后接口P99延迟直接飙到两秒以上。这就是典型的数据库读压力瓶颈。

这时候加一台数据库从库能缓解一部分压力,但治标不治本。业务量再翻一倍,又得加机器。而且从库数据同步有延迟,某些场景下读到旧数据反而更麻烦。分布式缓存的核心思路是:把热点数据提前放到内存里,请求先查缓存,缓存不命中才查数据库。内存的访问延迟是纳秒级到微秒级,就算加上网络往返,也比走一次磁盘索引快一个数量级。实测下来,同一个查询在缓存命中时平均耗时0.5ms左右,数据库只要处理真正没被缓存覆盖的请求。

压测数据很直观:引入Redis缓存后,数据库每秒实际查询量从两万降到了三千,接口平均响应时间从180ms降到了18ms。数据库连接池的占用率也从接近100%回到了40%以内。这就是分布式缓存最直接的价值:把数据库从高频查询中解放出来,让它专心处理写入和一致性要求高的事务。

1.2 本地缓存与分布式缓存的取舍

聊分布式缓存之前,得先说说本地缓存。本地缓存就是把数据放在应用进程内,比如Java里的Caffeine、Guava Cache,Go里自己写个sync.Map配合定时清理。本地缓存的优势是极致的快——数据就在进程内,没有网络开销,命中后纳秒级返回。对于单体应用、节点数不多、数据量不大的场景,本地缓存完全够用。

但它有三个绕不开的问题。

第一是数据不一致。应用部署多个节点时,每个节点各存一份缓存。某个节点更新了数据,其他节点还保留着旧值。除非给缓存加全局版本号,否则用户在一次请求里可能命中节点A的新数据,下一次请求命中节点B的旧数据。

第二是内存受限。进程内的堆内存是给业务逻辑用的,不能都拿去堆缓存。JVM默认堆内存几个GB,能放下几百万个对象就算不错了,一旦缓存数据量上来,GC压力和OOM风险都很难受。

第三是无法集中管理。每个节点独立缓存,没有统一的监控面板,缓存命中率、内存占用、热点Key分布都得一台台机器去看,运维成本很高。

分布式缓存把这些痛点全部解决掉了。数据集中存储,所有应用节点共享同一份缓存,一致性只需要在缓存服一端保证。容量可以横向扩展,单实例不够就上集群。监控也方便了,一套独立的缓存服务可以直接采集指标。代价是牺牲了一点性能——每次读写都要走一次网络,本地缓存是微秒级,Redis分布式缓存一般在亚毫秒到几毫秒之间。但相比于一次磁盘IO和数据库查询,这个代价完全可以接受。

选型结论也比较明确:如果你只有一个应用实例、数据总量很小、对一致性要求又不高,那就用本地缓存;一旦应用开始多节点部署、共享数据变多、需要统一治理缓存,就果断切到Redis这样的分布式缓存中间件。

2. Redis缓存的数据模型与生命周期设计

2.1 数据结构选型:String、Hash、ZSet怎么选

Redis能成为分布式缓存的主流选择,靠的不只是快,还有丰富的数据结构。很多缓存中间件就是简单的key-value存取,Redis则提供了String、Hash、List、Set、ZSet五种基础结构,每一类都能对应到具体的业务场景。

String是最常用的,适合存序列化后的对象或者简单值。比如一个用户的基本信息,JSON序列化后直接SET user:1001 {json字符串}。优点是使用简单、兼容性好,缺点也很明显:如果对象里只有一个字段变了,也要整个反序列化、修改、再序列化写回,对大对象来说比较浪费。而且String内部编码是SDS,频繁修改会产生内存碎片。

如果你有一个对象需要频繁更新部分字段,Hash更合适。把对象的每个字段映射为Hash的一个field,比如HSET user:1001 name "张三" age 25。这样更新age字段只需要一条HSET命令,不用动整个对象。Hash在field数量少时使用ziplist编码,内存非常紧凑,field多了自动切换为hashtable,性能依然在线。我实际项目中存用户资料、订单状态这类半结构化数据,基本都用Hash。

ZSet适合做排行榜和范围查询。它每个成员带一个score,Redis内部用跳表实现,可以用ZREVRANGE取TopN,也可以用ZSCORE查某个成员的分数。直播打赏榜、积分排名、商品热度榜这类需求,ZSet是标准答案。

Set适合做去重和集合运算,比如用户标签、好友关系、抽奖池。List适合做简单的消息队列或者时间线,但消息队列场景更推荐Stream。选型就一句话:不要什么结构都往String里塞,先用业务访问模式来确定结构。频繁更新字段选Hash,排序取范围选ZSet,集合运算选Set,简单键值用String。

2.2 TTL与过期策略:让缓存自动“保鲜”

缓存不能永久有效。业务数据会更新,缓存如果一直不过期,就会长期展示旧值。而且Redis内存是有限的,永久key最终会把内存耗干。所以每个缓存key都要设置合理的过期时间(TTL)。

TTL怎么定?我一般按数据的变更频率来分档。基础配置类数据,比如系统字典、渠道参数,一天甚至一周不变,TTL可以设24小时以上。业务核心数据,比如商品详情、用户基础信息,变更不算频繁但也不固定,设半小时到两小时比较稳妥。中间态数据,比如验证码、限流计数、临时token,几分钟就够了。核心原则是:宁可多查一次数据库,也不要让用户长期看到旧数据。

TTL还有一个容易被忽略的细节:过期时间在Redis里会自然累积一个“僵尸区间”。某些场景下大量key都在同一时间窗口过期,可能引发缓存雪崩。所以设置TTL时我会故意加一个随机抖动,比如基础TTL是1小时,实际设置时在3600秒基础上随机加0到300秒。这个细节后面讲雪崩时还会展开。

Redis的过期清理是两种策略配合。惰性删除:每次读取key时,发现已过期就删除并返回空。定期删除:后台每隔100ms随机抽取一批设置了TTL的key,检查并清除已经过期的。惰性删除保证了读写性能,但可能让过期key残留在内存里;定期删除把残留清一部分,但不会扫全库。两者配合下,内存中短时间存在少量过期key是正常现象,只要不是大量堆积就没问题。

2.3 内存淘汰机制:Redis内存满了怎么办

就算设计了TTL,Redis的内存还是有可能被写满。大量业务key涌入、TTL设置过长、某次批量写入没控制好,都有可能突破maxmemory阈值。这时Redis会触发内存淘汰策略,也就是在maxmemory配置下决定怎么踢掉一些key来腾出空间。

Redis提供的淘汰策略有这几种:

策略含义适用场景
noeviction不淘汰,写请求直接报错需要强一致的缓存服务
allkeys-lru从所有key中按LRU淘汰通用缓存场景,推荐
volatile-lru只从设置了TTL的key中按LRU淘汰有部分key必须常驻内存
allkeys-random从所有key中随机淘汰数据访问无明显热点
volatile-random只从设置了TTL的key中随机淘汰与上面类似,限定范围
volatile-ttl按剩余TTL最短优先淘汰希望快过期的先被清掉

我实测下来的经验是,缓存场景首选allkeys-lru。因为缓存里的数据本来就是可丢弃的,LRU能保住热数据,随机淘汰则可能把正在用的key清掉。如果某些key必须常驻,比如分布式锁、全局配置,那就不应该给它们设置TTL,再配合volatile-lru让其他带TTL的key被优先清掉。

LRU在Redis里的实现其实是一种近似LRU。它不会真的去记录每个key的最后访问时间做全量排序,而是抽取一小部分样本,从中选最久未访问的key淘汰。默认采样数量是5,可以通过maxmemory-samples参数调节。采样越大,淘汰结果越接近真实LRU,但CPU消耗也越高。生产环境我一般保持默认或者调到10,效果足够。

还要提一个生产常见坑:Redis分配内存超过物理内存,触发系统swap,性能会急剧恶化。maxmemory设置的时候要预留30%左右的余量给Redis自身的数据结构开销、复制缓冲区和AOF重写空间,不能把物理内存全分给maxmemory。

3. 实战:缓存穿透、击穿、雪崩的三种防御实录

3.1 缓存穿透:恶意请求直接打到数据库

缓存穿透是指请求查询的数据在缓存里不存在,数据库里也不存在。比如用一个不存在的用户ID去查用户详情,Redis查不到,数据库也查不到,于是每次请求都穿透到数据库。如果是恶意攻击者用随机ID循环请求,数据库会被无效查询打垮。

第一次遇到穿透是在某系统上线后没几天,监控发现数据库的慢查询突然激增,全是查某个场景下的不存在记录。当时第一反应是数据问题,排查了半天发现是有人用工具扫接口,每次传一个不存在的ID。这种攻击的成本极低,一个for循环就能把数据库连接池打满。

解决方案有两种。第一种是缓存空值。查数据库发现结果为空时,也把空结果写入缓存,TTL设短一些,比如2到5分钟。这样后续相同请求直接命中空缓存,不会打到数据库。空值缓存的代价是Redis里会存一些空key,但短TTL保证了它们很快被清理,不会长期占用内存。

第二种是布隆过滤器。在查询之前先用布隆过滤器判断这个key在不在数据集合中。布隆过滤器用多个哈希函数把key映射到一个bitmap上。判断时只要有一个位为0,就能确定key不存在;如果都位为1,则大概率存在(可能误判)。把数据库里所有合法ID提前写入布隆过滤器,查询时先判断,不存在的直接拒绝,误判的仍会走一次数据库,但概率很低。误判率可以通过bitmap长度和哈希函数数量来控制,一般控制在1%以内。

这两种方案各有适用场景。缓存空值实现简单,适合数据集合不固定、非法key比例不高的场景。布隆过滤器需要提前构建key集合,适合ID类数据相对固定的场景。生产上我倾向于先用缓存空值快速止血,后面如果有恶意扫描特征再考虑布隆过滤器。注意布隆过滤器本身不支持删除,如果业务有删除ID的需求,要么定期重建过滤器,要么选用布谷鸟过滤器这类支持删除的变体。

3.2 缓存击穿:热点Key集体失效

缓存击穿和穿透容易混淆。击穿说的是:某个热点key在缓存中失效的瞬间,大量并发请求同时打到数据库。场景上更像“同时刻抢一个热点”。典型例子是某个爆款商品的详情页,平时这个key一直命中缓存,某天它刚好过期了,瞬间几千上万个请求同时发现缓存没数据,于是一齐涌入数据库。

击穿的解决办法围绕“只让一个请求去重建缓存”展开。

第一种是互斥锁。当缓存未命中时,先尝试获取分布式锁,只有拿到锁的请求才去查数据库并回写缓存,其他请求等待锁释放后再读缓存。Redis实现互斥锁很简单,用SET key value NX EX 10,拿到锁的线程执行重建逻辑,其他线程拿不到锁就循环等待几毫秒再重试读缓存。我这里给一段常见的伪代码:

public Object getProduct(String key) { Object value = redis.get(key); if (value != null) { return value; } String lockKey = "lock:" + key; boolean locked = redis.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if (locked) { try { value = db.query(key); redis.set(key, value, 3600, TimeUnit.SECONDS); return value; } finally { redis.delete(lockKey); } } else { Thread.sleep(50); return getProduct(key); } }

注意锁的过期时间,如果重建逻辑超过锁的过期时间,锁会提前释放,导致多个线程同时重建。设置锁过期时间时要把重建耗时冗余考虑进去,或者用更稳妥的续期机制。

第二种是逻辑过期。缓存里存的不只是业务数据,还额外存一个过期时间戳。读取时发现逻辑过期,当前线程直接返回旧值,同时发起一个异步线程去重建缓存。这样用户请求永远不会直接打到数据库,最多就是拿到略微过期的数据。适合能容忍短暂旧数据的场景,比如商品名称、非关键的配置信息。实现上就是在value里放一个自定义的ExpireTime:

public Object getByLogicExpire(String key) { CacheValue cache = redis.get(key); if (cache.getExpireTime() > System.currentTimeMillis()) { return cache.getValue(); } // 异步重建缓存 executor.submit(() -> { Object newValue = db.query(key); redis.set(key, new CacheValue(newValue, now + 3600 * 1000)); }); return cache.getValue(); }

两种方案我更常用互斥锁,因为实现简单、语义清晰。逻辑过期适合并发量极高的热点Key,因为连等待锁的阻塞都省了,缺点是实现复杂一点,还要自己管理时间戳。

3.3 缓存雪崩:时间错峰与多级兜底

缓存雪崩是大规模缓存失效导致整体流量打到数据库。和击穿的区别在于,击穿是单点,雪崩是面。触发原因主要有两种:一是大量key在同一时间段过期,二是Redis服务整体不可用。

大量key在同一个时间点过期,往往是TTL设置太整齐导致的。比如每天晚上零点批量刷新缓存,所有key的TTL都设在零点后几分钟。零点一过,一大波key同时失效,数据库承受所有请求,直接被打崩。前面提过的TTL加随机抖动就是针对这个问题的:

import random ttl = base_ttl + random.randint(0, 300) redis.set(key, value, ex=ttl)

这样key的过期时间被平摊到一个时间窗口里,不会集中爆发。实际经验是抖动区间至少取基础TTL的5%到10%,时间仓促时取一个固定随机范围也行。

另一种雪崩是Redis宕机造成的。Redis一挂,所有缓存请求全部落到数据库,数据库根本扛不住。防御手段分几层:

  • 多级缓存:在Redis前面再加一层本地缓存,Redis不可用时本地缓存还能扛住一部分读请求。本地缓存规模小,但能给Redis的恢复争取时间。
  • 熔断降级:Redis不可用时,对非核心接口直接返回默认数据或降级提示,而不是让请求穿透到数据库。
  • 快速恢复:Redis高可用架构要完善,主节点挂了直接故障转移到从节点,恢复时间控制在秒级。

雪崩这块我踩过大跟头。某次做缓存刷新时写了一段批量更新任务,把所有key的TTL重置成同一个过期时间,上线后第二天准点把数据库打挂了。后来教训很深刻:任何批量刷新任务都要随机化TTL,而且上线前先小流量试跑半小时,观察key的过期分布再全量生效。

4. 缓存与数据库的一致性方案落地

4.1 Cache Aside模式:为什么缓存删除优先于更新

缓存和数据的一致性是分布式缓存绕不开的难题。数据库有事务保证,Redis也有自己的原子性,但两者之间没有跨系统的强一致协议。业界最常用的模式是Cache Aside,也叫旁路缓存。

读操作:先读缓存,缓存未命中就读数据库,然后把数据写回缓存。

写操作:先更新数据库,然后删除缓存。

为什么更新数据库后是删除缓存而不是直接更新缓存?因为更新缓存的操作不是原子的。如果同时有多个并发请求在更新同一个key,后写的请求可能覆盖先写的数据,导致缓存里存了一个旧值。而删除缓存就没有这个问题,下一次读请求会miss,然后从数据库读取最新值并回填缓存。

举个例子,商品价格从100改成80。更新数据库成功,然后删除缓存。这个过程中如果有用户读到旧缓存100,最多多看到几次旧价格,等缓存被删掉后,后续请求就会看到新价格。如果更新数据库后直接把缓存改成80,而另一个并发线程正在写缓存100,就变成缓存永久存了100,彻底和数据库不一致了。

所以Cache Aside的核心思想是:用删除代替更新,让缓存最坏情况下短暂不一致,但很快自愈。自愈的速度取决于读请求的频率,热点数据几毫秒内就会重新填充,冷数据就算缓存缺失也只是多查一次数据库。

这里有个小坑:删除缓存的时机。我见过有人先删缓存再更新数据库,这是错误示范。考虑一个并发场景,线程A删除缓存,线程B读缓存发现miss,从数据库读到旧值并写回缓存,然后线程A才更新数据库为新值。结果旧值已经回到缓存里,而且因为TTL很长,这个旧值会存活很久。先更新数据库再删缓存,至少能保证读请求在更新完成后拿到的都是新值。

4.2 延迟双删的细节与边界

先更新数据库再删缓存看起来没问题,但在极端并发下还是有一丝缝隙。考虑这个时序:线程A更新数据库成功,开始删除缓存;在线程A删除缓存之前,线程B读缓存命中旧值,直接返回。等等,这里线程B读到了旧缓存,这种情况是不可避免的,因为A还没来得及删。但更麻烦的是这个时序:

线程A读数据库旧值,还没写回缓存; 线程B更新数据库为新值,删除缓存; 线程A这时候把旧值写回缓存。

这个时序里,A在更新之前就开始了读操作,把旧值写回了缓存。因为A写的时刻晚于B删缓存的时刻,缓存里残留了旧值。这种窗口非常小,但只要并发足够高就有可能发生。

网上常见的解决思路是延迟双删:先删缓存、再更新数据库、延迟一小段时间后再删一次缓存。第二次删除的目的是清掉“旧值写回缓存”这个窗口期产生的脏数据。延迟时间的经验值是500ms到1秒,业务侧可以结合读多写少的特征自己调整。

public void updateData(Data data) { redis.delete(key); // 第一次删除 db.update(data); // 更新数据库 Thread.sleep(500); // 延迟 redis.delete(key); // 第二次删除 }

注意延迟双删不是强一致方案,它只是把一个极小概率的窗口进一步缩小,并不能100%消灭不一致。而且如果第二次删除失败,缓存还是会遗留旧值。所以延迟双删后面通常还要挂一个异步重试兜底,比如把删除操作扔到消息队列,消费端不断重试直到删除成功。

我在实际项目中很少单纯依赖延迟双删。因为sleep会阻塞线程,高并发下对性能有影响。更常见的是数据库更新成功后发一条消息到消息队列,消费者拿到消息后执行删除缓存。这样不用在请求线程里sleep,还能保证删除一定会执行。

4.3 异步消息最终一致方案

延迟双删是一种同步补偿思路,更适合对数据一致性要求不那么敏感的场景。如果业务对缓存中的数据一致性要求更高,可以上异步消息的最终一致方案。

逻辑是这样的:业务系统更新数据库的事务提交成功后,顺手发送一条消息,消息内容是“某个key的缓存需要删除”。独立的消费服务收到消息后执行Redis删除操作。删除失败的话,消息队列会进行重试,直到成功。

这套方案的优点很明显:

  • 请求线程不用自己删缓存,删缓存的操作完全异步,对接口响应时间没有影响。
  • 消息队列天然有重试机制,删除失败可以反复重试,不会像直接删除那样失败了就丢了。
  • 缓存删除和数据更新解耦,业务代码不需要关心缓存细节。

实际落地时要注意几个细节。消息的发送时机必须在数据库事务提交之后,如果在事务内发送,事务回滚后消息却发出去了,会误删缓存,反而让缓存多miss一次。可以使用事务同步器,在事务真正提交后才发消息。另外消费端要保证幂等,重复消费同一消息不会产生副作用,Redis删除本来就是幂等的,删一次和删多次效果一样,不用额外处理。

还有一个进阶玩法:监听数据库的binlog或者CDC变更事件,由单独的同步组件把变更信息转成消息,推送删除缓存的操作。这样可以做到代码里完全不埋点,业务层只关心数据库事务。代价是需要额外引入一个数据同步组件,运维复杂度增加。

最终一致方案的核心原则是:允许系统在某个时间窗内是不一致的,但必须保证不管发生什么,数据最终会收敛到正确状态。这个时间窗越短,用户体验越好;时间窗越长,出错排查越难。我一般会把删除失败的告警打进监控系统,连续失败N次的key直接触发人工介入。

5. 分布式缓存的高可用部署与日常运维

5.1 从单机到Cluster:架构演进怎么走

缓存可以有单点吗?可以,但如果业务量上来了,单点早晚是事故源头。Redis的部署形态演进是有套路的:单机、主从、哨兵、Cluster。

单机最简单,一个Redis实例扛所有读写。适合开发环境和小流量系统,故障直接全挂,不用考虑什么高可用。

主从复制解决的是“读多写少”场景下的扩展和备份问题。一个主实例负责写,挂多个从实例分担读请求。主实例把写操作同步给从实例。主挂了,从还在,但因为没有自动切换机制,需要人工干预。而且主从同步是异步的,主节点崩溃瞬间可能有少量数据没同步过来。

哨兵模式在主从基础上加了一个监控组件,哨兵集群负责监听主从健康状态。主节点挂了,哨兵自动选出一个从节点提升为主节点,应用只需要重新感知新的主节点地址。Sentinel解决了“Redis宕机怎么办”的问题,但整体容量仍然受限,因为所有写操作都落在主节点上。

当数据总量超过单机内存容量,或者写并发超过单个主节点能扛住的量级,就得用Cluster模式。Redis Cluster把数据分成16384个slot,每个节点负责一部分slot。key通过CRC16算法算出slot,再映射到具体节点。这样数据分布到多个节点,容量和并发都能水平扩展。

集群选型建议:

场景推荐形态
开发环境、内部系统单机或主从
生产小规模、需要自动故障转移哨兵模式
数据量超过单机内存或写并发高Cluster模式

Cluster配置的时候要注意客户端需要支持集群协议,使用普通的单机客户端连接会报错。另外Cluster模式下多key操作需要key的hash tag一致,比如{user:1001}:name和{user:1001}:age能保证落到同一个slot,否则MGET这类跨key命令会报错。

5.2 日常监控与性能调优清单

缓存上线后,监控比调优更重要。Redis的nginx就是INFO命令提供的一堆指标,但真正该关注的没有想象中多。

  • 缓存命中率:这是最重要的指标。Redis提供了INFO stats里的keyspace_hits和keyspace_misses,命中率 = hits / (hits + misses)。健康状态下命中率应该在90%以上,低于80%就要检查是不是缓存key设计不合理、TTL太短、或者被迫频繁淘汰。
  • 内存使用率:used_memory / maxmemory。超过70%就要关注,超过80%建议扩容或者优化数据结构。内存打满触发淘汰后,热点key可能被清掉,命中率会骤降。
  • 慢查询:通过CONFIG SET slowlog-log-slower-than设定慢查询阈值,默认10000微秒太长,生产我一般设1000微秒。慢日志查一下,看看是不是有O(N)命令打到大Key上。
  • 连接数:connected_clients。接近maxclients时,新请求会排队,应该检查应用侧连接池是不是没设置上限,或者有没有连接泄漏。
  • 拒绝连接数:如果出现命令执行超时甚至服务不可用,优先看是不是连接被打满或者内存爆了。

性能调优方面的经验,我总结为几个经常被忽略的细节。

第一个是连接池大小不是越大越好。很多团队把Jedis或Lettuce连接池设成几百上千,结果Redis单实例本身能抗的连接数是有限的,连接数过多反而导致CPU空转。连接池的合理上限一般是几十到一两百,具体结合业务QPS和单个连接的复用率来定。

第二个是善用批量操作。大量连续取key时,用MGET替代逐个GET,能省掉大量网络往返。写入时用pipeline把多条命令一次发给Redis,性能提升非常明显。注意pipeline没有事务语义,中途出错不会回滚。

第三个是警惕大Key。一个Hash里有几百万field,或者一个String有几十MB JSON,用户访问一次就要传输一大块数据,删除这个大key时也会阻塞Redis。大key的排查可以用redis-cli --bigkeys定期扫一遍。遇到大Key,能拆就拆,比如把一个大Hash按业务维度拆成多个小Hash,或者把大JSON改存Hash结构,每个字段一个field。

第四个是避免O(N)命令。KEYS、SMEMBERS、HGETALL这类命令在数据量大时会让Redis单线程卡住,阻塞所有读写。不要用KEYS模糊匹配,用SCAN命令分批迭代查找。这条规则基本是生产事故教出来的,我见过一次线上有人用KEYS匹配用户前缀,几千万个key直接把Redis阻塞了十几秒,上游所有请求超时。

缓存的使用其实就是性能和一致性的博弈,Redis只是工具,真正决定系统健壮性的是使用它的人。我个人感受最深的一点是:不要想着缓存解决所有问题,它只负责拦截高频读请求,数据正确性的根基还是数据库。所以每次改动缓存逻辑,我都会问三个问题:缓存miss了会怎样?缓存和数据库不一致了能恢复吗?Redis挂了业务还能降级吗?这三个问题都有明确答案,缓存体系才算真正成熟。

最后分享一个小技巧:缓存上线后不要急着批量刷数据,先小流量预热热点key,观察命中率和数据库压力变化,稳定一天后再逐步放开流量。我看过太多团队一把梭全量预热,结果Redis内存被打满、数据库连接被瞬时拉满的事故。缓存预热讲究的是循序渐进,跟系统扩容一样,永远不要一次推到底。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 5:44:12

GCD求和难题解析:容斥原理与欧拉函数反演

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:43:31

C#多数据库实战:Oracle、SQL Server、SQLite统一接入与避坑指南

简介:这份 dataDemo.rar 面向具备一定 C# 基础、希望系统掌握多数据库访问的开发者,聚焦 Oracle、SQL Server、SQLite 等数据库在 .NET 环境下的连接与操作实践。压缩包共 38 个文件,约 623KB,以 cs 源码为主,辅以 sln…

作者头像 李华
网站建设 2026/10/17 5:43:06

AI代理批量创建PostgreSQL数据库的风险与最佳实践

最近有个趋势值得拿出来聊一聊:AI 代理开始从“帮忙写代码”进化到“直接创建数据库”,而且不是建一两个,是批量建。3月19日这天,我在一个内部讨论群里看到某同事贴出来一条日志,团队里的 AI 代理接到需求后一口气创建…

作者头像 李华
网站建设 2026/10/17 5:41:18

使用Python管理PPT页面与文档属性

前言 用 Python 批量处理 PPT,最常见的两类需求是「调整页面」和「改文档属性」。前者包括新建、遍历、按条件筛选页面;后者是标题、作者、主题、备注这类不显示在幻灯片上、但会出现在文件详细信息里的元数据。很多单位的文档规范会要求统一这些属性&am…

作者头像 李华
网站建设 2026/10/17 5:41:12

Claude CLI工具链:基于MCP协议的AST感知编码工作流

1. 项目概述:这不是一个“模板库”,而是一套让Claude真正落地编码的CLI工作流“claude-code-templates”这个标题,第一眼容易被误解成一个GitHub上放着几十个.js或.py文件的静态代码仓库。但如果你真这么想,就完全错过了它在当前A…

作者头像 李华
网站建设 2026/10/17 5:40:59

战地6启动失败闪退进不去?一套完整的系统排查指南

战地6启动失败、闪退、进不去这组关键词,我这一年里看到太多次了。每天都有玩家来问:点启动没反应、加载地图闪退、刚进主菜单黑屏、玩几分钟直接被踢回桌面。多数人第一反应是去重装游戏,结果折腾一整晚问题还在。作为一个常年和各种游戏兼容…

作者头像 李华