资讯动态

Spring Boot集成Redis实战:从缓存热点到分布式锁与性能调优

发布时间:2026/10/16 3:48:37 来源:尧图企业网站定制
1. 从一次接口雪崩说起Redis解决的不是快而是缓存热点去年我在做一个会员积分查询的功能数据库里一张流水表累积了几亿条记录每次用户点开积分明细后端就要做一次大范围聚合查询单次响应稳定在800毫秒到1.5秒之间。一开始想着加索引、分页、SQL优化能压的都压了一遍效果还是有限。真正的问题在于大量用户同时访问同一批热门账户的积分数据数据库被重复计算打得满负荷运转。后来就是在这个项目里我正式把Spring Boot和Redis深度集成把热点账户的积分摘要缓存起来响应时间直接降到20毫秒以内数据库负载也回到正常水位。那一次重构让我对springboot使用redis有了完全不一样的认知Redis不是单纯用来提速的它真正解决的是热点数据无法被快速承载的问题。很多人只听说过Redis能做缓存但真到项目里它还能干并发去重、分布式锁、排行榜、限流计数、消息队列这些事。这篇博文我会从实际项目出发把Spring Boot集成Redis的完整过程、配置细节、序列化选型、注解缓存、分布式锁和最后的生产调优全部串起来讲适合正在用Spring Boot做后端、但还没系统搞过Redis的开发者也适合那些已经能在项目里跑通Redis、但遇到问题不知道从哪里排查的读者。我是怎么用Redis一步一步解决线上问题的包括那些文档里不怎么写、但非常影响稳定性的坑都会在下面展开。每个步骤和配置都能直接抄到自己的项目里不是那种念文档式的堆砌。2. Spring Boot接入Redis的完整动作依赖、配置与连接工厂2.1 引依赖别漏了连接池的包Spring Boot集成Redis起步只需要一个依赖就是spring-boot-starter-data-redis。如果你用的是Maven直接加这一段dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency这段依赖会自动引入Spring Data Redis和Lettuce连接器。注意Spring Boot从2.x开始默认的连接客户端已经是Lettuce而不是Jedis了。很多老项目迁移过来的时候还在项目里手动塞一个Jedis依赖其实没必要Lettuce是一个基于Netty的异步驱动性能更好连接管理也更省心。但有一个非常容易漏掉的坑如果你要配置连接池必须额外引入commons-pool2否则连接池参数根本不生效项目还会在启动时报找不到连接池工厂类。dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency为什么需要连接池Lettuce是异步模型默认情况下每次操作会复用同一个连接但如果你设置了某个超时时间或者打开了事务支持连接的使用方式会变复杂。规范做法是显式配置一个池子避免高频请求下连接被频繁创建销毁。这块后面讲连接数估算的时候再展开。2.2 配置文件参数每个字段都有它的用途在application.yml里最基础的配置长这样spring: data: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 3000ms lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 max-wait: 3000ms很多人搞不清这些参数的含义我逐个说一下host和port不用解释Redis服务端的地址和端口。password如果没有设置密码就留空不要写null。database表示选哪个逻辑库。Redis默认有16个库从0到15按业务模块隔离数据但生产环境建议一个应用固定用一个库不要到处换。timeout是连接超时时间单位毫秒。这里我建议至少给到1000ms以上太短容易在网络抖动时误报连接失败。lettuce.pool.max-active指连接池最大连接数。这个值不是随便写的要结合线上QPS和单次操作耗时来估算。max-idle是最大空闲连接数min-idle是最小空闲连接数max-wait是拿到连接的最大等待时间。有一点必须提醒Spring Boot 2.x里Redis配置前缀是spring.redis到了3.x改成了spring.data.redis。如果你把2.x的配置直接拷到3.x项目里不会报错但所有配置都会静默失效用默认的localhost:6379去连这在切换版本升级时是个高频翻车点。判断自己项目是哪个大版本看启动日志里Spring Boot的版本号就行。关于连接数算法给你一个直接的参考公式。假设你线上峰值QPS是1000一次Redis操作的耗时平均0.5ms那么同时维持在途的活跃连接数理论上只需要1000乘以0.0005也就是0.5个连接。但实际要考虑到网络往返、GC暂停、批量命令等因素通常把最大连接数设置在50到200之间并且要和Redis服务端的maxclients配置对齐。连接数不是越大越好过大反而会给Redis带来无意义的空连接占用。2.3 自动配置原理Spring Boot替你做了哪些事你可能会好奇为什么只加了个依赖、写了几行配置RedisTemplate拿过来就能用这背后是Spring Boot的自动配置机制在起作用。以2.x为例RedisAutoConfiguration这个自动配置类会做三件事第一读取你配置的连接参数生成一个LettuceConnectionFactory第二用这个连接工厂创建一个默认的RedisTemplatekey和value的序列化方式是JdkSerializationRedisSerializer第三创建一个StringRedisTemplate专门处理字符串场景。所以如果你不自定义任何Bean项目里可以直接注入RedisTemplate和StringRedisTemplate。但问题也出在这里默认的RedisTemplate用的序列化方案在真实项目里几乎不可用这一点我会在第三章专门展开。我在项目里的习惯是不依赖默认配置而是自己定义一个RedisConfig类显式声明一个配置好的RedisTemplateBean这样序列化、连接处理都在可控范围内。这个方法不只是为了规避默认序列化的问题更是为了方便后续动态切换主从、埋点监控这些扩展逻辑。3. 序列化是新手重灾区RedisTemplate的编码细节与通用配置3.1 默认序列化方案为什么不能直接用于生产刚接触Redis的新手最容易遇到的怪事是用RedisTemplate存进去一个对象再从Redis命令行客户端一查看到的是\xAC\xED\x00\x05t...这种完全看不懂的乱码。这不是数据损坏而是JDK序列化后的字节流。Spring Boot默认的JdkSerializationRedisSerializer会把Java对象以ObjectOutputStream的格式写进Redis这种方案有四个致命问题第一可读性为零你在Redis客户端里根本看不出存的是什么排查问题只能靠写代码 第二跨语言困难这套序列化格式只有Java能读懂如果业务将来要对接Go、Python的消费端数据直接没法用 第三存储空间大JDK序列化会附带大量类信息同一个对象比JSON格式往往大好几倍 第四一旦对象的类结构发生变化字段改名、类型调整反序列化极容易抛异常因为它在字节流里写死了类的指纹。所以凡是正经项目第一步都应该把RedisTemplate的序列化方案换成String加JSON这种组合。3.2 我最终采用的序列化配置方案我的通用配置是key用StringRedisSerializervalue用GenericJackson2JsonRedisSerializer。这样做的好处是key在Redis里是明文方便命令行排查value是JSON格式任何语言都能解析。Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); StringRedisSerializer stringSerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }这段配置里afterPropertiesSet()方法要注意不调用的话序列化器可能没有被正确初始化到内部属性上运行时会走回默认逻辑。GenericJackson2JsonRedisSerializer默认会在JSON里写入class字段记录对象的类型信息目的是一次性把对象的类型带进去反序列化的时候才知道还原成什么类型。但它用的时间序列化器比较弱如果你存入的对象里有LocalDateTime、LocalDate这类Java 8时间类型反序列化很可能会报错cannot construct instance of java.time.LocalDateTime。解决办法有两个方向一是手动给ObjectMapper注册JavaTimeModule然后基于它自定义一个序列化器二是尽量不直接缓存复杂对象的时间字段存字符串时间戳反而更省事。我建议在项目早期就约定好缓存对象里的时间一律用String格式的ISO标准时间或者用long型时间戳这样能绕过大多数序列化坑。类似的坑还包括枚举类、匿名内部类和某些代理对象缓存前一定要想清楚这个对象会不会被别的地方反序列化。3.3 按操作类型选择RedisTemplateValueOperations、HashOperations、ZSetOperations很多人在使用RedisTemplate时只知道opsForValue()结果遇到复杂结构时全用字符串拼接既难维护又容易出错。实际上Spring Data Redis把Redis的数据结构都封装成了Operations接口用法非常直观数据结构获取方式典型使用场景StringopsForValue()缓存单值、分布式锁、计数HashopsForHash()对象字段级读写、购物车、配置表ListopsForList()消息队列、简单先进先出队列SetopsForSet()去重、抽奖、共同好友ZSetopsForZSet()排行榜、延时队列、热榜排序举个例子我要实现一个排行榜最优雅的方式是ZSet直接redisTemplate.opsForZSet().add(ranking:2024, userId, score)然后reverseRangeWithScores()取前N名。如果你用字符串自己去维护排序那基本上是绕了一大圈。这里有个细节当使用Hash结构缓存一个对象时value的序列化器同样会被使用但hash key的序列化器是单独的。很多人只设置了setKeySerializer和setValueSerializer忘了setHashKeySerializer和setHashValueSerializer结果用opsForHash()操作的时候出现奇怪的类型转换异常。这两对序列化器是独立的不能只设置一对完事。4. 注解缓存一套带走Cacheable背后的穿透、击穿、雪崩4.1 从EnableCaching到三个核心注解Spring Boot里用Redis做缓存有两种模式第一种是直接用RedisTemplate手动控制读写第二种是使用缓存注解让Spring AOP拦截方法调用自动完成缓存读写。第二种写起来最简洁也是我主力推荐的方式。前提是在启动类或者配置类上加一个EnableCachingSpringBootApplication EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }然后就可以用Cacheable标记一个方法的结果需要缓存Cacheable(value user:detail, key #id, unless #result null) public User getUserById(Long id) { // 查询数据库 }Cacheable的含义是方法调用前先查缓存缓存命中就直接返回不再执行方法体缓存没有命中才执行方法并把结果写入缓存。value是缓存分区名key是缓存key的一部分用的是SpEL表达式这里#id就表示方法参数id的值。最终Redis里的key格式是user:detail::123这样中间会有两个冒号的分隔符这是缓存名称和key的默认拼接方式。另外两个常用的注解是CachePut和CacheEvict。CachePut表示不管缓存有没有命中都执行方法并把结果更新到缓存适合更新数据后同步刷新缓存的场景。CacheEvict表示执行方法后删除缓存适合删除数据后清掉对应缓存。4.2 key生成器和缓存过期策略从默认规则到自定义方案默认情况下Cacheable的key生成规则是如果没写key属性就使用SimpleKeyGenerator它会根据方法参数生成一个key。但问题是如果方法没有参数key是一个固定的SimpleKey.EMPTY这会导致不同用户查询同一个无参方法时命中同一个缓存逻辑上直接出错。所以我的经验是所有Cacheable都要显式写明key不要偷懒。key建议统一采用业务模块:主键:业务维度的格式比如order:detail: #orderId。这样在Redis命令行里看前缀清晰定位问题非常方便。过期时间这块如果使用Spring Cache注解一个缓存分区只能配一个固定的过期时间需要在RedisCacheManager里定制。如果你希望同一个分区内不同数据有过期时间的差异那就只能落到RedisTemplate手动操作的层面了。对于大部分场景分区级别定过期时间已经够用我通常的做法是Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .withInitialCacheConfigurations(initialCacheConfigs()) .build(); }不同业务缓存分区可以单独设置TTL比如热点商品缓存10分钟用户会话缓存1小时。如果都用一个默认值要么有些数据白白占空间要么有些缓存过期太早起不到作用。4.3 缓存穿透、击穿、雪崩注解救不了这三个问题说实话这三个雪崩三兄弟是所有缓存系统的难点Spring Cache注解本身一个都解决不了。项目里如果压力不大可以忽视但只要上线的是高并发场景这三个问题一定要提前设计。缓存穿透查询一个不存在的key缓存和数据库都没有请求直接打到数据库。这个场景最典型的是恶意请求用一个不存在的ID循环查询。应对手段有两个一是把空值也缓存起来TTL设短一点这样下一次同样的请求会被缓存拦截二是在缓存之前加布隆过滤器先判断这个key是否可能存在不存在就直接拒绝不再打到数据库。缓存击穿某一个热点key的过期瞬间大量并发请求同时发现缓存没有命中一起冲进数据库。应对手段是加互斥锁让只有一个请求去数据库查询并回填缓存其他请求等待缓存重建。用RedisTemplate实现互斥锁的通用写法是这样的String lockKey lock:user: id; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 3, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { Object result queryFromDatabase(); redisTemplate.opsForValue().set(key, result, 30, TimeUnit.MINUTES); return result; } finally { redisTemplate.delete(lockKey); } } // 未拿到锁先睡一会再查缓存 Thread.sleep(50); return redisTemplate.opsForValue().get(key);缓存雪崩大量key在同一时间段集体过期导致请求全部涌向数据库。这个应对手段最简单也最有效给key的过期时间加一个随机值把过期时间打散。比如基准TTL是30分钟实际TTL是30分钟加0到300秒的随机值。这样一来同一批key不会同时失效。另一个额外助攻是多级缓存。在Spring Boot项目里可以在Redis前面再放一层本地缓存比如Caffeine热点key的查询先查本地本地没有再到Redis。这样即使Redis出现抖动本地缓存依然能顶住一部分流量。但是要注意本地缓存是每个节点一份的更新的时候要做主动失效否则会出现各节点数据不一致的问题。4.4 自调用导致缓存注解失效的坑Cacheable的原理是Spring AOP代理只有外部调用方法时代理对象才会生效。如果你在一个类内部调用另一个带缓存注解的方法比如public Order getOrderAndCache(Order order) { return this.getOrderFromDb(order.getId()); // 自调用缓存注解失效 } Cacheable(value order:detail, key #orderId) public Order getOrderFromDb(Long orderId) { // ... }因为this.getOrderFromDb()走的是原始对象而不是代理对象注解完全不会起作用缓存形同虚设。这个问题排查起来很容易被忽视代码看着没问题但缓存就是不生效。解决办法有三种一是把缓存方法拆到另一个独立的Service类里让类之间互相调用二是注入自身的代理对象来调用三是在方法内部先通过ApplicationContext.getBean拿到代理对象再调用。我推荐第一种类职责拆分本身就是好事还能避免其他AOP问题。5. 分布式锁的三层演进setnx、Lua脚本与Redisson5.1 为什么需要分布式锁从订单防重复提交说起我有一次做一个优惠券秒杀功能用户点击领取按钮后前端做了防抖但压测时发现同一用户连续点击后端能收到多次领取请求。数据库层面虽然有唯一索引兜底但很多前置判断逻辑比如用户积分足够才能扣减在并发下会判断失误。这个场景就是分布式锁的典型适用场景。单体应用时代Java的synchronized或者ReentrantLock就能搞定因为所有线程都在同一个JVM里。一旦应用部署了多节点比如两个实例负载均衡每个实例里的synchronized只能锁住自己节点的线程另一个节点的请求照样穿过去这就是分布式锁必须存在的理由。5.2 第一阶段SETNX加过期时间的原子操作在Redis 2.6.12之前实现分布式锁要两步走先setnx再expire中间两步不是原子的服务重启就会造成锁没有过期时间。现在直接用一条命令带过期时间就行了。Spring Data Redis里对应的API是setIfAbsentString lockKey lock:coupon:receive: userId; String requestId UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);设置requestId是一个随机值这一步非常重要。为什么因为释放锁的时候不能直接delete而是要校验这个锁是不是自己的。如果A线程拿到锁执行时间超过了30秒锁自动过期B线程拿到锁这时候A线程终于执行完直接delete会把B线程的锁删掉导致B线程的临界区完全暴露。正确写法是先比较再删除if (Boolean.TRUE.equals(locked)) { try { // 业务逻辑 } finally { String value redisTemplate.opsForValue().get(lockKey); if (requestId.equals(value)) { redisTemplate.delete(lockKey); } } }然而这个先比较再删除又产生了新的原子性问题compare和delete两步之间可能有时间差。想要完全正确得用Lua脚本把这两个操作做成原子的if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 endSpring Data Redis里可以通过DefaultRedisScript执行这段Lua脚本。这一步虽然啰嗦但严谨起见必须做否则高并发下就会出现误删锁的场景。5.3 第二阶段锁自动过期时间怎么算自己用setnx实现锁最头疼的其实是锁的过期时间。时间设短了业务还没执行完锁就没了导致锁形同虚设时间设长了如果业务执行过程中抛异常就算finally里释放了锁还是会在异常期间空占锁很久。比较常见的优化思路是锁续期起一个后台定时任务每隔锁过期时间的三分之一去检查锁是否还在如果在就把过期时间续上直到业务执行完释放。这个思路本身不难但自己实现要小心线程泄漏、任务清理这些边界。另一个简单粗暴的替代方案不设锁的自动过期时间让锁永远有效然后依赖finally释放。但是一旦业务方法因为OOM、进程被杀等原因没走到finally这个锁就永远解不开了下周一运维跑过来问为什么所有领券请求都报系统繁忙那画面太美我不敢看。所以纯手动方案的可靠性终究有限这也是我最终转向Redisson的原因。5.4 第三阶段Redisson看门狗机制与公平锁Redisson是一个Redis客户端增强库它在Redis之上封装了大量分布式数据结构分布式锁是其中最成熟的部分。用法非常简洁dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId /dependencyRLock lock redissonClient.getLock(lock:coupon:receive: userId); boolean locked lock.tryLock(0, 30, TimeUnit.SECONDS); if (locked) { try { // 业务逻辑 } finally { lock.unlock(); } }三行代码把锁的获取、自动续期、释放全解决了。Redisson底层默认的锁过期时间是30秒但它有一个看门狗机制如果持有锁的线程还活着后台会每10秒自动续期一次把过期时间重新拉回30秒直到业务线程执行完释放锁。这个机制把业务执行时间超过锁过期时间的问题自动消解了不用自己算时间。还有一个值得说的点是tryLock的waitTime参数。第一个参数传0表示拿不到锁立刻返回false不无限等待。如果业务上允许等待可以给100到500毫秒的时间让部分请求稍微排队提高锁获取的成功率。Redisson的坑反而是集群模式下的问题如果你部署的是Redis主从集群锁在master节点写入后master还没来得及同步到slave就宕机了另一台节点上的请求就还能获取到同样的锁。Redisson为此提供了RedLock多节点锁方案但RedLock本身存在争议我自己在实践中很少真正用到多节点加锁业务量级达到那个程度时首先要考虑的是不是应该把锁粒度再拆细一点而不是盲目上RedLock。5.5 锁粒度最容易被忽视的性能杀手写分布式锁最容易犯的错误是锁粒度过大。比如你做库存扣减锁key直接写成lock:stock那么所有商品的扣减请求都串行执行Redis虽然快也扛不住全站尖峰流量。正确做法是把锁key缩小到业务维度比如lock:stock: skuId不同商品互不干扰。同样锁的跨度也要小。锁里面只放临界区代码也就是真正需要互斥的那几行操作查询、组装、渲染之类的操作尽量挪到锁外面。很多新人喜欢把整个方法体塞进锁里导致锁的持有时间拉得很长高并发下单量一上来很多请求都会卡在等锁上效果适得其反。6. 生产环境里的Redis体检慢查询、Big Key与连接池6.1 用Redis日志和监控命令定位慢命令Redis本身是单线程模型所有命令在主线程里排队执行。这意味着某一条命令如果执行时间特别长它后面排队的命令全部都会变慢表现就是接口的整体延迟上升。所以Redis性能问题的排查思路最先看的应该是哪条命令拖慢了主线程。Redis的SLOWLOG命令可以直接看慢命令记录SLOWLOG GET 10它会返回最近10条执行时间超过阈值的命令。慢命令阈值的默认值是10000微秒10毫秒生产环境我一般把它调小到5000微秒CONFIG SET slowlog-log-slower-than 5000日志里主要关注两个信息命令本身和key名称。如果看到大量KEYS命令基本能确定是代码里用了KEYS *这种全库扫描的写法这在生产环境是灾难一定要改成SCAN游标方式或者直接把key设计成可查询的前缀结构。类似要注意的危险命令还有HGETALL拉一个超大的hash、SMEMBERS拉超大set、LRANGE一个超长的list。Spring Boot应用里排查Redis问题还有一个好用的入口Actuator暴露的Redis健康指标。引入spring-boot-starter-actuator后/actuator/health会显示Redis连接状态。进阶的话可以配置Redis的INFO命令输出到监控系统重点关注connected_clients、used_memory、evicted_keys这几个指标。6.2 Big Key是延迟的隐形杀手Big Key指的是某个key对应的value特别庞大。比如一个Hash里塞了几十万个字段或者一个List有上百万条记录。这种key在读写时会占用大量内存和网络带宽而且因为Redis单线程执行一个Big Key的操作会阻塞整个实例。判断Big Key的常见姿势是命令行redis-cli --bigkeys它会扫一遍全库统计每个key的元素数量和占用空间输出Top N。注意这个命令在数据量大的实例上执行也会对主线程有压力建议放在低峰期跑。发现Big Key之后的处理思路有三个方向第一拆分。比如用户购物车原来是一个Hashfield是用户IDvalue是购物车列表一个key存了所有用户的数据。拆成每个用户一个keykey是cart: userId这样单个key的数据量上限天然受控。第二压缩。如果value是JSON等文本格式可以改成压缩后存储比如Gzip压缩到原来的1/10代价是读写时多一步压缩和解压的CPU开销。对于大对象缓存非常值得。第三换结构。某些场景用String存大JSON可以用Hash存储字段级数据只读需要的字段而不是每次都把整个JSON拉回后端再解析。6.3 Lettuce连接池与超时参数的工程化配置Lettuce是Spring Boot默认的Redis客户端底层用Netty管理连接但即使它是异步的在Spring Data Redis的封装下你调用的API还是同步的。如果不配置连接池默认行为是复用共享连接在高并发下会有连接等待还可能出现超时。我项目里的标准配置长这样spring: data: redis: timeout: 3000ms lettuce: pool: max-active: 100 max-idle: 50 min-idle: 10 max-wait: 2000ms shutdown-timeout: 200ms需要注意的是Lettuce共享连接和阻塞操作有个坑。比如BLPOP、BRPOP这类阻塞命令如果走共享连接一个命令阻塞了其他命令全部卡住。Spring Data Redis检测到阻塞命令时会开启一个专用连接来执行这是默认行为但代价是专用连接的创建和销毁有开销。所以在代码里尽量避免使用阻塞命令能用轮询或消息队列替代就替代。还有一个常见误区把timeout设成负数或0表示无限等待。有些场景觉得多等等总能等到但生产环境一旦Redis崩了所有请求都卡在等连接上应用线程池被占满引起连锁雪崩。宁可快速失败也不要无限等待。max-wait同理如果拿不到连接等待超过2000ms直接抛异常让上层去降级而不是傻等着。6.4 热Key问题本地缓存和读改写策略比Big Key更隐蔽的是热Key。单个key的访问频率指数级高于其他key比如大促时的爆款商品、某个明星的空降直播间每秒可能被读取上万次。就算Redis单机能扛几十万QPS这个key对应的Redis节点依然会成为瓶颈而且因为是热点网络带宽也可能被打满。处理热Key的常规方案是给Redis前面再加一层本地缓存。在Spring Boot里可以用Caffeine配置一个容量小的热点缓存比如最多存1000个key过期时间5秒。查询流程变成先查本地缓存再查Redis最后查数据库。Redis的热Key访问量被本地缓存拦掉一大半Redis压力骤降。但本地缓存有一个强一致性的问题如果数据在数据库被更新了但本地缓存还没过期读到的还是旧数据。解决办法是更新数据后主动调本地缓存清除接口这个在单体应用里很好实现用ApplicationContext发布一个事件所有节点的本地缓存监听事件后删除对应key。多机房部署时会有延迟但5秒的过期时间也能兜底整体可控。7. 我最后想分享的三个Redis使用习惯项目跑顺之后我回头看自己从刚开始用Redis到现在最有价值的不是某一个配置而是几个使用习惯。第一个习惯是写代码之前先想清楚Redis里存的到底是什么数据可不可以接受丢失或延迟。Redis更适合缓存那些丢了能从数据库恢复、延迟几秒能接受的数据。如果是承载核心交易的实时状态比如订单支付状态直接放Redis当唯一数据源那不叫性能优化而是给自己埋雷。我见过因为Redis主从切换导致缓存丢失订单状态回滚的例子复盘时的教训就是缓存永远只是加速层不是持久层。第二个习惯是所有key的前缀必须加业务模块名例如order:detail:123、coupon:user:456。没有前缀的key在项目早期看着没什么影响等到Redis里的数据量大了你连这个key是哪个业务写入的都查不出来排查慢查询和内存泄漏时会非常痛苦。命名规范这件事在Redis场景里比在数据库表设计里还重要因为Redis里没有表结构帮你做约束。第三个习惯是一张完整的Redis配置检查清单每次新项目接入时我都会对着过一遍连接池有没有配、序列化器换没换、key前缀规不规范、慢命令阈值有没有调低、Big Key有没有预案、本地缓存要不要加。这套清单看起来都是小事但每个对应的问题我都付出过线上代价。把这些写下来希望你能把这些前置性工作做在前面而不是等线上出问题之后再去翻日志。Spring Boot接Redis本身是个很成熟的事情但成熟不等于简单每个环节都有它的为什么。希望这篇从踩坑记录和工程实践出发的分享能让你接入Redis时少走几段弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑