Redis 常见问题

张开发
2026/4/10 3:29:56 15 分钟阅读

分享文章

Redis 常见问题
1. 缓存雪崩问题描述缓存雪崩是指在同一时间大量的key同时失效导致所有请求直接涌向数据库造成数据库瞬时压力过大甚至崩溃的系统性故障现象。根本原因时间设置问题缓存键设置相同TTL批量失效。服务不可用Redis集群故障缓存完全失效。热点数据集中关键业务数据集中在少数缓存键。系统设计缺陷无降级、无限流、无熔断机制。解决方案设置不同的过期时间为缓存数据的过期时间添加随机值避免大量缓存同时失效。例如过期时间 基础时间 随机时间。缓存永不过期缓存数据不设置过期时间而是通过后台任务定期更新缓存。多级缓存使用多级缓存如本地缓存 Redis 缓存即使 Redis 缓存失效本地缓存仍能缓解数据库压力。限流和降级在缓存失效时通过限流如令牌桶算法和降级策略保护数据库不被压垮。2. 缓存穿透问题描述查询一个不存在的数据由于缓存中不命中每次请求都直接查询数据库导致数据库承受巨大压力的现象。根本原因恶意攻击或业务逻辑缺陷。解决方案缓存空值对于查询结果为空的请求将空结果缓存起来并设置较短的过期时间如 1-5 分钟。例如SET key-null “NULL” EX 60。布隆过滤器Bloom Filter使用布隆过滤器在缓存层拦截无效请求。布隆过滤器可以快速判断一个键是否存在于缓存中如果不存在则直接返回避免查询数据库。接口校验在业务层对请求参数进行校验过滤掉明显无效的请求如负数的 ID、非法的字符等。3. 缓存击穿问题描述某个热点key在缓存中过期的一瞬间大量并发请求同时涌入数据库导致数据库瞬时压力过大的现象。根本原因热点数据集中缓存过期策略不当。解决方案逻辑过期永不过期后台刷新不设置真实的过期时间而是将过期时间存储在value中。当发现数据逻辑过期时使用单独的线程去更新缓存。实现步骤缓存的值包含过期时间戳逻辑过期时间。当请求访问缓存时检查当前时间是否超过逻辑过期时间。如果未过期直接返回数据。如果已过期尝试获取锁然后开启一个异步线程去更新缓存当前请求返回旧数据。使用互斥锁分布式锁使用分布式锁保证同一时间只有一个请求去加载数据其他请求等待或重试。实现步骤请求访问缓存若缓存命中则返回数据。若缓存未命中尝试获取分布式锁如使用Redis的setnx命令。获取锁成功的请求从数据库加载数据写入缓存然后释放锁。其他请求等待如休眠一段时间然后重试或者直接返回默认值根据业务场景。永不过期结合后台更新对热点key不设置过期时间然后通过后台定时任务或异步线程定期更新缓存。实现步骤缓存设置为永不过期或很长过期时间。后台启动一个定时任务定期比如每分钟更新缓存。请求访问时直接返回缓存数据不考虑过期问题。缓存预热Cache Preheating在缓存过期前提前更新缓存避免缓存同时过期。实现步骤通过定时任务在缓存过期前比如提前5分钟重新加载数据并更新缓存。或者在系统启动时加载热点数据到缓存。4. 内存管理问题问题描述redis内存使用过多导致性能下降或服务崩溃。解决方案设置内存上限通过maxmemory参数设置最大内存使用量。选择合适的内存淘汰策略如volatile-lru、allkeys-lru等。使用数据结构优化例如使用hash代替多个string使用整数集合等。定期清理过期数据设置activeExpireCycle参数定期清理过期key。5. 持久化问题问题描述redis持久化策略选择不当导致数据丢失或性能问题。解决方案RDB快照适用于备份和灾难恢复但可能会丢失最后一次快照之后的数据。AOF追加写文件提供更好的持久性但文件较大恢复速度较慢。混合持久化redis4.0以上结合RDB和AOF先使用RDB进行全量备份然后使用AOF记录增量操作。6. 大 Key 问题问题描述指的是在Redis中一个key对应的value非常大。常见的大Key类型有字符串类型的value非常大比如超过10KB列表、集合、有序集合、哈希等元素数量过多比如超过10000个大Key会带来以下问题内存使用不均匀数据倾斜可能导致集群中某个节点内存使用过高。操作大Key耗时较长可能导致阻塞影响其他命令的执行。在集群模式下大Key无法迁移因为迁移是通过slot为单位但一个key不能拆分。删除大Key时可能会导致长时间阻塞甚至引发缓存雪崩。解决方案拆分大Key将一个大Key拆分成多个小Key例如将一个包含百万元素的哈希拆分成1000个哈希每个哈希包含1000个元素。在客户端通过映射关系如哈希取模来访问不同的key。使用合适的数据结构例如如果使用字符串存储一个大的JSON对象可以考虑使用哈希来存储因为哈希可以更高效地更新和获取部分数据。压缩数据对于字符串类型可以考虑使用压缩算法如gzip、snappy进行压缩但要注意这样会增加CPU开销。监控和预警通过监控工具发现大Key及时处理。7. 热点 Key 问题问题描述指在特定时间段内访问频率远高于其他 Key 的 Key。热点 Key 的危害性能瓶颈单线程模型下热点 Key 可能导致大量请求排队连接数堆积。数据倾斜集群模式下某个分片负载过高。缓存击穿风险热点 Key 过期瞬间大量请求直接打穿到数据库。解决方案本地缓存客户端缓存适用场景读多写少且可以容忍一定程度的数据不一致。实现方式在业务应用内部使用本地缓存如Guava Cache、Caffeine等缓存热点Key。设置较短的过期时间如几秒钟以避免与Redis中的数据长时间不一致。当本地缓存中没有数据时从Redis中获取并更新本地缓存。读写分离适用场景读热点且读请求量非常大单节点无法承受。实现方式使用Redis主从复制将读请求分散到多个从节点写请求仍然发往主节点。使用逻辑过期适用场景缓存击穿场景即热点Key过期瞬间大量请求直接打到数据库。实现方式在缓存Value中封装过期时间逻辑过期而不是使用Redis的物理过期。当发现缓存数据逻辑过期时使用分布式锁如Redis的setnx保证只有一个线程去更新缓存其他线程仍然返回旧数据。限流与降级适用场景突发流量且无法通过扩容快速解决。实现方式在应用层或网关层对热点Key的访问进行限流例如使用令牌桶、漏桶等算法。当达到限流阈值时返回降级内容如默认值、错误提示等。使用Redis集群的Hash Tag适用场景Redis Cluster模式下希望将相关Key分布到同一个节点但热点Key导致单个节点压力过大。实现方式使用Hash Tag将热点Key拆分成多个Key并让这些Key分布在不同的节点上。例如原Key为hot:key可以改为{hot:key}:1、{hot:key}:2通过改变Hash Tag来改变Key所在的Slot。缓存永不过期适用场景访问非常频繁且更新不频繁的热点Key。实现方式缓存不设置过期时间而是通过后台任务定期更新或使用消息队列通知更新。当数据需要更新时先更新数据库然后删除缓存后台任务再异步加载到缓存。8. Redis为什么这么快Redis之所以快主要得益于其内存存储、高效的数据结构、单线程模型、非阻塞I/O多路复用、以及精心的设计优化。内存存储数据存储在内存中读写速度远快于磁盘。高效的数据结构Redis提供了多种高效的数据结构如跳表、哈希表、压缩列表等这些数据结构在时间和空间上都有很好的优化。单线程模型避免了多线程的上下文切换和竞争条件同时通过非阻塞I/O多路复用来处理并发连接。I/O多路复用使用epoll、kqueue等系统调用可以同时监听多个连接提高网络I/O效率。优化的协议使用简单的RESP协议减少解析开销。其他优化如管道技术、零拷贝技术、事务支持、Lua脚本等。9. Redis 集群的脑裂问题问题描述脑裂是指分布式系统中由于网络分区导致集群分裂成多个独立工作的子集群每个子集群都认为自己是唯一正常的部分导致数据不一致和服务混乱。Redis 集群脑裂发生场景主从切换期间网络分区时间线 1. 主节点A从节点B 2. 主节点A网络延迟哨兵认为A宕机 3. 哨兵选举B为新的主节点 4. 但A实际上还在运行只是网络不通 5. 结果两个主节点同时对外服务Redis Cluster 网络分区原始集群6个节点[Master A]--[Master B]--[Master C]|||[Slave A1][Slave B1][Slave C1]网络分区后 分区14个节点 分区22个节点[Master A][Master B]|[Slave B1][Slave A1][Master C]|[Slave C1]两个分区都认为自己是主集群继续提供服务数据开始分叉。脑裂发生的原因网络问题# 网络分区类型1. 交换机故障2. 网络链路中断3. 防火墙配置错误4. 云网络VPC配置问题5. 网卡故障# 示例Redis集群节点间通信中断节点A、B、C之间能通信但与D、E无法通信 形成两个孤立的子集群配置问题# 错误配置导致1. cluster-node-timeout 设置过大# 默认为15000ms15秒# 如果设置过大故障检测延迟容易发生脑裂2. 主从复制超时时间设置不当 replica-timeout300# 默认60秒太长会增加脑裂风险3. 最少主节点数配置错误# 集群需要大多数节点同意才能进行故障转移资源问题# 系统资源不足1. CPU使用率100%无法及时响应心跳2. 内存不足进程被OOM Killer杀死3. 磁盘IO满载持久化操作阻塞4. 网络带宽耗尽心跳包延迟脑裂的危害数据不一致不同的子集群可能接受不同的写操作导致同一键在不同子集群中有不同的值。# 示例两个分区同时写入相同Key分区1写入set user:1001Alice分区2写入set user:1001Bob网络恢复后数据冲突# Redis使用最后写入胜利Last Write Wins# 但可能丢失重要数据数据丢失当网络恢复后两个子集群合并时会以某个子集群的数据为主另一个子集群的写操作可能会丢失。# 脑裂恢复后的数据丢失场景1. 分区1有3个节点多数派2. 分区2有2个节点少数派3. 分区1选举出新的Master4. 分区2的Master继续接收写操作5. 网络恢复后分区2的Master变成Slave6. 分区2期间的数据会被丢弃Redis 集群防脑裂机制主从复制机制# Redis Sentinel 防脑裂配置min-slaves-to-write1# 至少要有1个从节点连接才允许写min-slaves-max-lag10# 从节点延迟不超过10秒# Redis配置示例# redis.confmin-replicas-to-write1# Redis 5.0 新参数名min-replicas-max-lag10Cluster 防脑裂机制Redis Cluster 要求1. 主节点需要大多数其他主节点可达才能提供服务2. 每个主节点至少需要 N/2 1个主节点连接正常3. 否则进入 FAIL 状态停止接受写请求10. Redis String 类型的底层实现是什么Redis String 类型并不是简单的 “字符串”而是根据数据内容和长度智能选择最合适的底层数据结构以节省内存和提高性能。具体来说String类型的底层实现有三种编码方式int、embstr和raw。Redis 字符串类型的最大存储容量是 512 MB。int编码当存储的值是整数并且长度不超过20个字符在64位系统中long类型占8个字节所以整数范围是-9223372036854775808到9223372036854775807时Redis会使用int编码来存储。这样可以直接使用整数避免转换为字符串。embstr编码当存储的字符串长度小于等于44字节Redis 5.0版本之前是39字节不同版本可能有差异时Redis会使用embstr编码。embstr是一种紧凑的存储格式它将RedisObject对象和SDS简单动态字符串连续存储在一起只需要一次内存分配因此效率更高。raw编码当存储的字符串长度大于44字节时Redis会使用raw编码。raw编码会为RedisObject和SDS分别分配内存SDS可以存储更大的字符串并且支持动态扩容。因此当我们执行一个set命令时Redis会根据我们存储的值决定使用哪种编码。例如set age 30因为30是整数所以使用int编码。set name Tom因为Tom长度短使用embstr编码。set long_string 这是一个非常长的字符串长度超过了44字节...使用raw编码。三种编码的详细分析INT 编码整数存储// 存储整数10086redisObject:typeREDIS_STRING encodingREDIS_ENCODING_INT ptr(void*)10086// 直接将整数存储在指针位置 // 验证命令127.0.0.1:6379SET age30OK127.0.0.1:6379OBJECT ENCODING ageint// 整数范围对于64位系统 // -2^63 到2^63-1(-9223372036854775808 到9223372036854775807)INT 编码的存储原理// Redis 巧妙利用指针存储小整数 // 指针本身是8字节可以存储范围在 LONG_MIN 到 LONG_MAX 的整数if(value fitsinlong){robj-encodingOBJ_ENCODING_INT;robj-ptr(void*)((long)value);// 整数直接存到指针}else{// 使用 SDS 存储}EMBSTR 编码嵌入式字符串// 存储短字符串hello总内存redisObject(16字节) sdshdr8(3字节) 字符串(6字节)25字节 内存布局连续内存块 ┌─────────────────────────────────────────────────────────┐ │ redisObject(16B)│ sdshdr8(3B)│hello\0(6B)│ ← 一次性分配 └─────────────────────────────────────────────────────────┘ // 为什么是44字节分界线 // 内存分配器 jemalloc/tcmalloc 通常以64字节为单位分配 // redisObject(16) sdshdr8(3) 字符串(N1) 结束符(1)≤64//163 N 1≤64→ N ≤44EMBSTR 优势public class EmbstrAdvantage{//1. 内存分配一次分配 vs 两次分配 // RAW: malloc(redisObject) malloc(SDS)// EMBSTR: malloc(redisObject SDS)//2. 内存释放一次释放 vs 两次释放 // EMBSTR 减少内存碎片 //3. 缓存友好连续内存提高缓存命中率}RAW 编码原始字符串// 存储长字符串这是一个非常长的字符串...redisObject: ptr → sdshdrX → 字符串数据 内存布局不连续 ┌──────────────────┐ ┌─────────────────┐ │ redisObject │ │ SDS头 字符串 │ │ ptr: 0x7f... │───▶│ len:100│ │ │ │ alloc:128│ └──────────────────┘ │ flags:... │ │ buf:长字符串...│ └─────────────────┘11. Redis Zset 的实现原理是什么Redis 的 Zset有序集合 是通过两种底层数据结构实现的压缩列表ziplist 和 跳跃表skiplist 哈希表dict根据元素数量和元素大小自动切换。小数据用 ziplist节省内存大数据用 skiplistdict保证性能两者结合既支持高效范围查询又支持 O(1) 的单点查询ziplist压缩列表使用条件可通过参数调整元素数量 ≤ zset-max-ziplist-entries默认 128每个元素的成员字符串长度 ≤ zset-max-ziplist-value默认 64 字节存储结构[元素1成员, 元素1分值, 元素2成员, 元素2分值,...]按分值升序排列插入时需要重新分配内存适合小集合skiplist dict跳跃表哈希表当不满足上述条件时Zset 会转换为 组合结构跳跃表skiplist按分值排序支持范围操作哈希表dict存储 成员 - 分值 的映射支持 O(1) 查分值和判断成员存在12. Redis Zset 为什么不用红黑树或 B树Redis 选择跳跃表而不是红黑树或 B树其主要有以下几点实现复杂度与维护成本跳跃表实现简单直观代码量少Redis 中约 200 行调试和维护成本低。红黑树需要处理复杂的旋转和变色逻辑实现和调试难度较高。B 树更适合磁盘存储的层次化结构但在内存中实现冗余度高且需要处理节点分裂/合并。范围查询的高效性Zset 核心需求需要高效支持 ZRANGE、ZREVRANGE 等范围查询。跳跃表底层是有序链表范围查询只需定位起点后线性遍历时间复杂度 O(logN M)M 为返回元素数量。红黑树范围查询需要中序遍历实现相对复杂且可能涉及多次指针跳转。B 树虽然范围查询高效但在内存中层级过多可能增加访问开销。并发友好性跳跃表可以通过无锁CAS或细粒度锁实现并发操作扩展性更好如 LevelDB 的并发跳跃表。红黑树并发修改通常需要全局锁或复杂锁机制影响性能。内存与性能平衡跳跃表平均每个节点包含 1.33 个指针Redis 中随机层数概率为 0.25内存占用可控。B 树节点通常需预留空间内存利用率较低更适合磁盘分页。红黑树每个节点需存储颜色标记和多个指针与跳跃表内存消耗相近但实现复杂度更高。灵活性跳跃表可以通过调整节点的层数来平衡性能和内存使用。红黑树和B树的结构相对固定。13. Redis 性能瓶颈时如何处理Redis性能瓶颈处理需要从多个维度分析包括内存、CPU、网络、持久化等。以下是一些常见的性能瓶颈及处理策略内存瓶颈现象内存使用率高可能导致交换swap或OOM。处理使用INFO memory命令监控内存。优化数据结构使用适当的数据类型例如用Hash代替多个String使用压缩列表ziplist等。设置过期时间定期清理无用数据。考虑使用内存淘汰策略如LRU、LFU。分片Sharding将数据分布到多个实例。CPU瓶颈现象CPU使用率高命令延迟增加。处理使用INFO commandstats查看命令耗时。避免复杂度过高的命令如KEYS *、长时间阻塞的命令。使用管道pipeline或批量操作减少网络往返。考虑使用Lua脚本减少多次命令的通信。升级到更高性能的CPU或增加实例数分片。网络瓶颈现象网络带宽不足或延迟高。处理使用INFO stats查看网络指标。将客户端和Redis服务器放在同一局域网或使用更高速的网络。使用连接池避免频繁创建连接。减少单个请求/响应的大小例如使用压缩但会增加CPU开销。持久化瓶颈现象RDB或AOF导致Redis阻塞。处理RDB调整保存间隔使用bgsave避免主进程阻塞。AOF调整同步策略如每秒同步使用AOF重写优化。考虑在从节点进行持久化。配置优化调整maxmemory和淘汰策略。调整maxclients以支持更多连接。调整tcp-keepalive和timeout以减少空闲连接。14. Redis 事务Redis的事务是通过MULTI、EXEC、DISCARD和WATCH命令来实现的。它允许将多个命令打包然后一次性、按顺序地执行。但是Redis的事务与关系型数据库的事务不同它不支持回滚rollback操作。Redis事务的基本使用开始事务MULTI用于标记事务的开始。执行MULTI后Redis会将后续的命令都放入一个队列中直到执行EXEC或DISCARD。执行事务EXEC执行事务中的所有命令。执行后事务结束。取消事务DISCARD取消事务清空事务队列。执行后事务结束。监视键WATCH在事务开始前监视一个或多个键如果在事务执行前这些键被其他命令修改则事务会被打断执行EXEC时会返回nil表示事务失败。取消监视UNWATCH取消所有WATCH命令对键的监视。事务三阶段# 阶段1开始事务MULTI# 阶段2命令入队SET key1value1INCR counter SADD mysetmember# 阶段3执行事务EXEC取消事务MULTI SET key1value1DISCARD# 取消事务清空队列事务的特性原子性Redis的事务是原子性的这意味着事务中的命令要么全部执行要么全部不执行。但是Redis的事务不支持回滚即使某个命令执行失败后面的命令仍然会继续执行直到事务中的命令全部执行完毕。所以Redis的事务原子性是指执行过程的原子性而不是指每个命令都成功。隔离性Redis是单线程执行命令的所以事务中的命令在执行过程中不会被其他客户端发送的命令打断因此事务具有隔离性。一致性Redis的事务能够保证命令执行前后数据的一致性即使在执行过程中某个命令失败也不会回滚所以需要开发者保证每个命令的正确性。持久性持久性取决于Redis的持久化配置事务本身不提供额外的持久性保证。原子性Atomicity的误解# Redis 事务是部分原子的# 1. 所有命令按顺序执行# 2. 不会被其他客户端的命令打断# 3. BUT没有回滚rollback机制MULTI SET a1INCR b# 如果 b 不是数字会报错SET c3EXEC# 输出# 1) OK# 2) (error) ERR value is not an integer or out of range# 3) OK # 注意c 仍然被设置了错误处理机制# 1. 入队错误命令语法错误MULTI SET key value INCR key# 语法正确入队成功WRONGCMD# 语法错误立即报错EXEC# 结果事务被拒绝执行所有命令都不执行# 2. 执行错误运行时错误MULTI SET keyhelloINCR key# key 是字符串无法 INCREXEC# 结果错误的命令失败其他命令正常执行WATCH 命令乐观锁WATCH命令用于实现乐观锁。它会在事务开始前监视一个或多个键如果这些键在事务执行前被修改那么事务就会被打断。# 场景余额扣款保证并发安全WATCH balance# 监视 balance 键# 获取当前余额balanceGET balance MULTI SET balance$((balance-10))# 扣款 10EXEC# 如果在此期间 balance 被其他客户端修改EXEC 返回 nil事务失败15. Redis 与 MySQL 缓存一致性解决方案问题描述在互联网系统中Redis 常被用作 MySQL 前的缓存层以提升读性能。但当数据在 MySQL 中更新时如何保证 Redis 中的缓存与 MySQL 保持一致是一个经典难题。由于 Redis 不具备 MySQL 的事务回滚能力且两者操作无法原子性完成因此不存在绝对 100% 一致的方案只能根据业务场景选择“最终一致性”或“强一致性”的近似实现。典型流程读先查 Redis → 不存在查 MySQL → 写回 Redis写更新 MySQL → 同时要更新 / 删除 Redis不一致根源并发读写更新 DB 与操作缓存不是原子操作网络延迟 / 失败删缓存失败、消息丢失多线程 / 分布式请求乱序执行最终表现脏数据Redis 是旧值MySQL 是新值缓存与数据库短暂不一致最终一致极少情况长时间不一致解决方案方案一先更新数据库再删除缓存Cache Aside Pattern流程更新 MySQL删除 Redis 缓存下次读请求缓存未命中 → 查 MySQL → 写回 Redis优点简单、高效、无并发覆盖问题最终一致性几乎满足所有业务缺点极端并发下会出现极短暂旧数据可接受为什么是删除而不是更新更新缓存可能涉及复杂计算浪费资源并发更新时后更新的可能覆盖先更新的导致数据错乱并发问题分析时刻|线程A写|线程B读|问题 T1|更新MySQL(旧→新)||T2||读缓存(命中旧值)|T3|删除缓存||T4||返回旧值 ❌|存在短时不一致 但这个窗口极短且后续读取会纠正。示例ServicepublicclassUserService{AutowiredprivateUserMapperuserMapper;AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringCACHE_KEYuser:;/** * 更新用户信息保证缓存一致性 */TransactionalpublicvoidupdateUser(Useruser){// 1. 先更新数据库userMapper.updateById(user);// 2. 再删除缓存核心redisTemplate.delete(CACHE_KEYuser.getId());}/** * 查询用户缓存套路先查缓存不存在查DB再写回缓存 */publicUsergetUserById(Longid){StringkeyCACHE_KEYid;// 1. 查缓存StringuserStrredisTemplate.opsForValue().get(key);if(userStr!null){returnJSON.parseObject(userStr,User.class);}// 2. 缓存不存在查数据库UseruseruserMapper.selectById(id);if(user!null){// 3. 写入缓存并设置过期时间兜底redisTemplate.opsForValue().set(key,JSON.toJSONString(user),1,TimeUnit.HOURS);}returnuser;}}方案二先删除缓存再更新数据库不推荐单独使用流程删除 Redis 缓存更新 MySQL并发问题线程 A 删除缓存后还未更新 DB线程 B 读取缓存miss从 DB 读取旧数据因为 A 还未更新 DB然后写回缓存之后 A 更新 DB导致缓存中是旧数据DB 是新数据长期不一致。时刻|线程A写|线程B读 T1|删除缓存|T2||读缓存(未命中)T3||查MySQL(旧值)→ 写回缓存(旧值)T4|更新MySQL(新值)|T5||缓存中是旧值 ❌ 直到过期方案三先更新数据库再更新缓存极少使用流程更新 DB 后直接更新缓存并发问题并发写导致数据错乱线程 A 和 B 同时更新同一数据可能 A 后更新 DB 但先更新缓存导致缓存与 DB 不一致。如果更新缓存失败没有补偿长期不一致。写操作频繁时缓存会被频繁更新浪费资源。方案四延迟双删流程先删缓存更新数据库延迟几百毫秒再删一次缓存优点确保并发读请求写入的旧缓存被删除。缺点需要预估延迟时间sleep 影响性能。示例ServicepublicclassUserService{AutowiredprivateUserMapperuserMapper;AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringCACHE_KEYuser:;TransactionalpublicvoidupdateUser(Useruser){Longiduser.getId();// 1. 先删缓存redisTemplate.delete(CACHE_KEYid);// 2. 更新数据库userMapper.updateById(user);// 3. 延迟500ms再次删除用线程池异步不阻塞主线程taskExecutor.execute(()-{try{Thread.sleep(500);// 经验值大于读DB写缓存时间redisTemplate.delete(CACHE_KEYid);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}});}}方案五订阅 MySQL Binlog最终一致性使用 Canal / Debezium 监听 MySQL binlog异步更新/删除 Redis。架构MySQL → Binlog → Canal → MQ → 消费服务 → 删除/更新 Redis示例// Canal 客户端监听数据库变化自动删缓存ComponentpublicclassCanalClient{AutowiredprivateStringRedisTemplateredisTemplate;publicvoidlisten(){CanalConnectorconnectorCanalConnectors.newSingleConnector(newInetSocketAddress(127.0.0.1,11111),example,,);while(true){Messagemessageconnector.getWithoutAck(100);for(Entryentry:message.getEntries()){// 数据库更新事件if(entry.getHeader().getTableName().equals(user)entry.getEntryType()EntryType.ROWDATA){// 解析IDLonguserIdparseUserId(entry);// 删除缓存redisTemplate.delete(user:userId);}}connector.ack(message.getId());}}}

更多文章