真实面经题目 · 原创解析
热点数据怎么保证redis和db中的一致?
热点数据的 Redis 与数据库一致性,核心不是追求所有场景下绝对强一致,而是先明确业务可接受的一致性等级,再选择缓存模式、写入顺序、失效策略和补偿机制。高频面试答案应围绕 Cache Aside、先更新数据库再删除缓存、删除失败补偿、TTL 兜底、版本号防乱序、binlog/CDC 异步修复、热点保护与强一致场景降级到串行化读写展开。
真实面经题目 · 原创解析
热点数据的 Redis 与数据库一致性,核心不是追求所有场景下绝对强一致,而是先明确业务可接受的一致性等级,再选择缓存模式、写入顺序、失效策略和补偿机制。高频面试答案应围绕 Cache Aside、先更新数据库再删除缓存、删除失败补偿、TTL 兜底、版本号防乱序、binlog/CDC 异步修复、热点保护与强一致场景降级到串行化读写展开。
我会先区分业务要求:如果是库存扣减、余额、订单状态这类强一致字段,不能只依赖普通缓存读写,要通过事务、锁、版本号或直接读主库保证关键链路正确;如果是商品详情、配置、榜单等热点读多写少数据,一般采用 Cache Aside,也就是读缓存,未命中查数据库并回填;写入时先更新数据库,再删除 Redis 缓存,而不是直接更新缓存。先更新数据库是因为数据库是权威数据源,删除缓存是为了让后续读重新加载最新值。删除失败要有补偿,比如重试、消息队列、binlog/CDC 监听后异步删除或刷新缓存,并用 TTL 做最终兜底。对于并发场景,还要处理缓存重建期间的旧值回写、乱序消息和热点击穿,可用版本号、更新时间戳、互斥重建、逻辑过期、短 TTL 加抖动等方式控制风险。延迟双删可以降低某些并发读写导致的旧值回填概率,但不是银弹,延迟时间难以精确,消息乱序和主从延迟仍然可能造成短暂不一致。比较完整的方案是:数据库为准、缓存可丢、写后删缓存、失败可重试、TTL 兜底、binlog/CDC 校准、版本号防旧覆盖,并根据业务在强一致和最终一致之间做取舍。
不能一上来就说加锁或双删,应该先问数据类型和一致性要求。热点数据通常读多写少,缓存的目标是承担读压力,数据库才是最终可信的数据源。对于商品详情、文章内容、活动配置这类允许短暂旧读的数据,可以接受最终一致;对于余额、库存扣减、支付状态、权限变更这类不可错读的数据,关键链路不能依赖普通缓存一致性,需要以数据库事务、条件更新、版本校验或直接读主库为准。
常见方案是 Cache Aside:读的时候先查 Redis,未命中再查数据库,然后写入 Redis;写的时候先更新数据库,再删除 Redis 中的缓存。这样做的前提是数据库作为权威源,缓存只是派生副本。相比写数据库后更新缓存,删除缓存更稳,因为缓存内容可能来自多个表、聚合逻辑或动态字段,直接更新容易遗漏计算逻辑,也容易被并发旧请求覆盖。
如果先删缓存再更新数据库,删除之后可能有读请求发现缓存为空,于是读到数据库旧值并回填缓存,随后写请求才完成数据库更新,缓存里就长期保留旧值。如果先更新数据库再删除缓存,理论上即使删除前有读请求读到了旧缓存,也只是短暂旧读;删除后新请求会从数据库加载新值。这个顺序不能消除所有不一致,但在读多写少场景下风险更小,工程上也更容易补偿。
只写先更新数据库再删缓存还不够,因为删除 Redis 可能失败,应用进程可能宕机,网络可能超时。可靠方案要把失败删除变成可重试事件,例如写数据库成功后发送消息队列重试删除,或者由独立组件监听数据库 binlog/CDC,发现数据变更后删除或刷新对应缓存。TTL 也是必要兜底,避免缓存因为一次失败永久脏读。
延迟双删的做法一般是先删除缓存,更新数据库,再等待一小段时间后再次删除缓存,用来清理并发读请求在写入过程中回填的旧值。它能降低某些竞态概率,但不能保证强一致,因为延迟时间很难覆盖所有数据库主从延迟、慢查询、网络抖动和线程调度差异。它适合做轻量补强,不应作为唯一一致性保障。
热点数据在高并发下容易出现缓存击穿和旧值覆盖。常见做法是回填缓存时带版本号、更新时间戳或数据版本,写缓存前比较版本,避免旧请求把新值覆盖掉。对于缓存重建,可以用互斥锁、singleflight、本地短暂合并请求、逻辑过期后台刷新等方式,让同一热点 key 在过期瞬间只有少量请求访问数据库。分布式锁只应保护重建或关键写路径,不应把所有读请求都锁住。
热点数据一致性还要考虑可用性。短 TTL 会提升新鲜度,但会增加数据库压力;长 TTL 能抗压,但旧读时间更长。因此常用做法是热点 key 设置合理 TTL 并增加随机抖动,避免同一时间大面积过期;对极热点数据使用逻辑过期和后台刷新,前台优先返回可接受范围内的旧值;对绝不能旧读的数据,则绕过缓存或采用强校验策略。
因为先删缓存后,可能有并发读请求立刻发现缓存未命中,读取数据库旧值并回填 Redis。之后写请求才把新值写入数据库,导致缓存长期保存旧值。先更新数据库再删除缓存虽然也可能出现短暂旧读,但删除成功后后续请求会重新加载新值,风险更可控。
不能只记录日志就结束。常见处理是本地重试加异步重试,或者把删除事件写入消息队列,由消费者重试删除;更稳的方式是监听 binlog/CDC,根据数据库变更异步删除或刷新缓存。同时设置 TTL 作为兜底,防止失败缓存永久存在。
不能保证,只能降低并发读写导致旧值回填的概率。延迟时间无法准确覆盖所有慢查询、主从延迟和网络抖动,而且第二次删除也可能失败。因此延迟双删最多是补强手段,仍然需要重试、TTL 和异步校准机制。
可以用互斥重建、singleflight、逻辑过期、后台刷新和本地短暂缓存来控制并发回源。对于极热点数据,前台可以在可接受范围内返回旧值,由后台异步刷新;对必须新鲜的数据,则要限制并发、读主库或走更强一致的路径。
缓存值中带版本号、更新时间戳或数据版本,回填缓存前进行版本比较,只允许新版本覆盖旧版本。对于异步消息刷新缓存,也要处理消息乱序,不能让较早的变更事件覆盖较新的数据。
当服务很多、更新入口分散、缓存删除失败需要统一补偿,或者希望把缓存一致性逻辑从业务代码中解耦时,可以使用 binlog 或 CDC。它监听数据库真实变更,再异步删除或刷新缓存,适合最终一致场景,但仍要考虑消息延迟、重复消费和乱序。