ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot+Redis实战:从客户端选型到分布式锁与缓存治理

Spring Boot+Redis实战:从客户端选型到分布式锁与缓存治理 在Java后端项目里Redis几乎已经成了标配。不管是给接口做热点缓存、存登录会话、做排行榜和计数器还是分布式场景下抢库存、拿锁Redis都能稳稳接住而且Redis的响应速度比走MySQL快几个数量级。Spring Boot出现之后Java操作Redis的接入成本又被压到了极低只要加一个starter依赖写两行配置就能在Service里直接注入RedisTemplate。但真正深入用起来会发现里面藏着不少细节客户端到底选Jedis还是Lettuce、序列化为什么会出现乱码、缓存穿透击穿雪崩怎么防、分布式锁为什么还分手写和Redisson两种写法。这篇我结合自己多年来在业务和中间件维护上的实际经验把Java和Spring环境下操作Redis的完整路径讲透每个踩过的坑都会指出来适合刚接触Redis的Java开发也适合已经用了一两年但还停留在“默认配置一把梭”阶段、想彻底看明白原理的同学。1. Redis在Java技术栈里的定位与选型思路1.1 Redis到底在Java项目里解决什么问题很多Java开发者最早接触Redis都源于“缓存”两个字但Redis在业务系统里的作用远不止缓存这么简单。以我参与过的电商交易系统为例热点商品详情页的访问量远高于普通商品如果每个请求都去MySQL里查一遍数据库压力会迅速被打满而把商品详情、库存摘要、活动标签这些数据提前放进Redis热点接口的RT能从几十毫秒降到几毫秒带宽和DB连接数也都能省下来。除了做缓存Redis原生支持的几种数据结构在不同业务场景里各有用途String类型可以做计数器和库存扣减比如秒杀系统里的“扣减库存”操作只需要一条INCRBY或者DECR命令避免了多次查询和updateHash适合存对象属性比如用户信息、购物车商品数量只修改某一个字段不会整个对象反序列化List天然适合做最新消息列表和轻量级队列LPUSH加LTRIM就能维护一个固定长度的滚动列表Set可以做去重、抽奖、共同关注ZSet则是排行榜的利器分数就是排序依据还可以用score存时间戳实现延迟队列。Spring Boot项目中Redis出现的第二个重要场景是分布式系统下的“跨进程协作”。单机部署时用synchronized或者本地锁就够了一旦服务以多实例方式部署在负载均衡后面本地锁就拦不住同时到达的多个请求。这个时候用Redis的SETNX和过期时间做一个分布式锁是很多团队从单机走向微服务时最先落地的方案。所以这篇文章里我会重点讲分布式锁的正确写法因为这属于“平时不炸一炸就是生产事故”的典型环节。1.2 Java客户端三巨头Jedis、Lettuce、Redisson怎么选Java生态里能操作Redis的客户端不少但实际项目里高频使用的就是这三个Jedis、Lettuce、Redisson。很多Spring Boot新人会疑惑明明我已经引入了spring-boot-starter-data-redis为什么还需要单独了解它们。答案是Spring Data Redis本身不是一个客户端而是一个统一抽象层它底层默认用Lettuce作为连接实现同时也兼容Jedis。Jedis走的是BIO模型API非常接近Redis原生命令简单直观但连接实例不是线程安全的多线程并发使用就需要用连接池每次操作都要借还连接在高并发场景下连接池容易成为瓶颈。Lettuce基于Netty使用单连接多路复用一个连接可以被多个线程共享线程安全性好连接数占用低响应式编程里还有专门的Reactive API这正是Spring Boot 2.0之后官方把默认客户端从Jedis换成Lettuce的主要原因。Redisson和前面两个不是一类东西它提供了大量开箱即用的分布式数据结构比如RLock分布式锁、RAtomicLong原子计数器、RMap本地缓存加速、RQueue分布式队列、RBloomFilter布隆过滤器等。如果你要处理的不是简单命令而是“分布式锁”“分布式限流”“延迟队列”这类高层语义Redisson能省掉大量手写逻辑而且它在底层做了很多优化比如锁的看门狗自动续期就比手工设置过期时间稳妥得多。选型时我一般会按这个逻辑判断一是团队对Redis命令的熟悉程度如果用惯了命令行直接操作数据Jedis风格最顺手二是性能要求Lettuce的高并发表现最好默认集成省事三是场景复杂程度涉及分布式协同和高阶数据结构直接引入Redisson。下面的表格可以直观看出差异客户端连接模型线程安全主要定位适用场景JedisBIO连接需池化管理实例不安全原生命令简洁中小项目、熟悉原生命令LettuceNetty多路复用线程安全Spring默认客户端高并发、响应式、长连接RedissonNetty多路复用线程安全分布式服务框架分布式锁、分布式对象、高级场景2. Spring Boot整合Redis从依赖配置到RedisTemplate核心操作2.1 引入依赖与基础配置注意Spring Boot版本差异在Spring Boot项目里操作Redis第一步是在pom.xml中加入官方starter依赖。需要说明的是Spring Boot 2.x时代配置项前缀是spring.redis到了Spring Boot 3.x时代配合新版的Spring Framework 6配置前缀已经改成spring.data.redis。如果直接照抄网上的老配置轻则配置不生效重则应用启动报错找不到属性。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency第二个依赖commons-pool2并非必须但强烈建议加上。虽然Lettuce底层是多路复用但生产环境仍然需要连接池来控制资源上限防止突发流量把连接数打爆。如果不加这个依赖即使你在配置文件里写了连接池参数也会被忽略更糟糕的是Lettuce在空闲连接清理上可能产生奇怪的问题。我之前就遇到过应用启动正常、运行半小时后突然报连接不可用的现象排查到最后才发现是漏了commons-pool2依赖结果连接断开后没有新建连接的保护机制触发了一系列连锁故障。yaml基础配置我通常这样写spring: data: redis: host: 127.0.0.1 port: 6379 password: database: 0 timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 0 max-wait: 3000ms这里的几个参数值得展开说一下。max-active是连接池里最多能同时存在的连接数配置太高并不会让Redis变快反而会在Redis端耗尽文件描述符通常按接口QPS和单个连接能复用的并发数折算16到32是比较常见的区间。max-wait是请求从连接池获取连接的最大等待时间这里设成250ms到3秒都合理如果设成-1表示无限等待在高并发时会导致线程全部阻塞在连接获取上明显是不合理的。加连接池的核心目的不是“让连接更多”而是“让连接可控”必须想清楚这一点。本地开发时连接不上的问题也很常见。如果你在macOS上安装Redis并自行编译验证小Demo用brew install redis是最省事的方式装好后直接redis-server启动端口默认6379如果你更习惯容器环境用Docker跑一个Redis实例也就一条命令的时间。需要特别注意的是Redis桌面管理工具连接不上时最先怀疑的应该是Redis配置文件里的bind限制和protected-mode这两个开关默认会把外部连接挡在门外。2.2 RedisTemplate的五大类型操作String、Hash、List、Set、ZSetSpring Data Redis提供的核心类是RedisTemplate它在底层帮你完成了连接获取、命令执行、结果序列化让开发者感觉像是在操作一个本地Map。使用时首先要注入RedisTemplate泛型通常定义为RedisTemplateString, Object这样key用String序列化便于查看value可以用JSON序列化承载任意对象。我先说String类型。这是使用频率最高的类型场景包括缓存简单值、计数器、分布式锁。对应的操作对象是ValueOperationsValueOperationsString, Object stringOps redisTemplate.opsForValue(); stringOps.set(user:1001:name, 张三, 30, TimeUnit.MINUTES); Object name stringOps.get(user:1001:name); Long stock stringOps.increment(stock:sku_1001, -1);第二行设置了过期时间这是缓存场景最基础的约束。操作INCR/DECR时返回值是Long在做库存扣减时要判断返回值是否小于0避免把库存扣成负数。Hash类型适合存储对象因为它是field级别的操作不像String那样必须整个对象序列化或反序列化HashOperationsString, Object, Object hashOps redisTemplate.opsForHash(); hashOps.put(user:1001:profile, age, 26); hashOps.put(user:1001:profile, city, 杭州); Object age hashOps.get(user:1001:profile, age);这种设计有一个很大优势在线程并发修改对象不同字段时Hash能有效减少覆盖写入的概率。比如购物车场景中多个商品操作同时修改同一个用户的购物车Hash如果换成String存整个购物车对象并发场景非常容易丢更新。List类型常用于消息队列和最新列表。我用LPUSH往头部插入配合LTRIM只保留最近N条就能实现一个“最新N条通知”的轻量数据结构ListOperationsString, Object listOps redisTemplate.opsForList(); listOps.leftPush(news:latest, article-1001); listOps.leftPush(news:latest, article-1002); ListObject latest listOps.range(news:latest, 0, 9);Set类型处理的是“去重”和“集合关系”比如每日签到用户集合、抽奖参与者集合。isMember可以直接判断某个用户是否已经签到不用先去查库SetOperationsString, Object setOps redisTemplate.opsForSet(); setOps.add(activity:20250101:sign, user_1, user_2, user_3); Boolean signed setOps.isMember(activity:20250101:sign, user_2);ZSet类型则用在排行榜场景它通过score排序还支持按区间查询排名。给每个用户写入分数后一行代码就能取出TOP10ZSetOperationsString, Object zsetOps redisTemplate.opsForZSet(); zsetOps.add(rank:score, user_1, 95.5); zsetOps.add(rank:score, user_2, 88.0); SetObject top10 zsetOps.reverseRange(rank:score, 0, 9);这五类操作几乎覆盖了业务中90%的需求。对应到面试题里面试官问“Redis有哪些数据类型”标准答案就是基础的五种加后续版本引入的Bitmap、HyperLogLog、Geo、Stream前五种必须烂熟于心。2.3 连接池参数与超时排查再谈Lettuce的连接抖动这一节我想重点聊一个让很多Java开发者头疼的问题运行过程中突然报Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException。这个异常在网上搜索量很大很多帖子的回复都是“调整timeout参数”但实际上造成超时的原因往往是多方面的。首先排查是否因为连接等待时间过长。Lettuce虽然是多路复用但配置了连接池后高并发下可能出现连接池可用连接不足线程在max-wait时间内一直拿不到连接。这个时候异常堆栈已经暴露了本质它确实是超时但“超时在等连接”不是“Redis响应慢”。解决思路是调大max-active、缩短单个业务的Redis命令耗时。其次要考虑Redis本身是否存在慢命令。比如生产环境有人用KEYS命令去遍历数据或者某个大Value执行了全量读写Redis是单线程模型一个慢命令会阻塞后面所有命令的执行。排查时用redis-cli --latency看网络延迟用SLOWLOG GET看是否有慢查询用redis-cli --bigkeys找大Key。我在实际处理过一次线上超时事故最终定位到的是一个业务团队把几兆字节的对象存进Redis每次读取和序列化都要耗时几十毫秒紧接着其他命令全部排队表现就是客户端大面积超时。最后还要注意网络层面的因素。Redis部署在物理机时应用服务器和Redis之间如果经过负载均衡或者云安全组偶尔的TCP重传会导致请求超过默认的3秒超时。此时把timeout从1000ms调到3000ms同时开启TCP keepalive往往是立竿见影的。但不要为了“安全”把timeout调到十几秒那样只会让故障感知变慢正确的做法是先定位是连接池问题、慢命令问题还是网络问题再动手改参数。3. Redis序列化方案与缓存治理实战3.1 默认JDK序列化的坑为什么Redis里全是二进制乱码很多初学者用RedisTemplate第一次get数据时都会遇到一个现象在Redis客户端工具里看到一堆“\xAC\xED\x00\x05t\x00...”这样的二进制内容完全不知道是什么。这是因为Spring Data Redis默认使用JdkSerializationRedisSerializer来做Value的序列化它会把对象先转成Java序列化字节流再写入Redis。JDK默认序列化有几个明显的坏处。第一可读性极差运维人员用Redis Desktop Manager查缓存时根本看不懂内容排查问题相当于睁眼瞎。第二序列化体积大Java原生序列化会写入大量类描述信息同样是存储一个User对象JSON的体积可能只有它的三分之一这对Redis内存是实打实的消耗。第三和跨语言团队协作不友好如果下游是Go或Python服务它们消费不了这个数据。最典型的干扰场景是同一个项目中一部分缓存是代码写入的另一部分是人工通过命令行预设的。如果代码写入时用了JDK序列化人工用字符串格式写入的缓存在反序列化时直接报类型转换错误。所以项目一开始就应该把序列化方案定好这个决定关系到后续排障和跨系统对接。3.2 配置通用序列化方案String Key加Jackson JSON Value我在配置RedisTemplate序列化时的通用思路是Key统一用StringRedisSerializerValue统一用GenericJackson2JsonRedisSerializer。Key用String是因为Redis里的键能直观看到业务含义而且便于通过redis-cli按前缀扫描Value用JSON序列化是因为JSON可读、体积适中、支持嵌套对象。具体配置代码如下Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer keySerializer new StringRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); GenericJackson2JsonRedisSerializer valueSerializer new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }需要说清楚的是GenericJackson2JsonRedisSerializer在序列化时会把类型信息写进JSON里这样反序列化时能还原出原来的类。但它默认构造器在遇到某些Java时间类型如LocalDateTime时会有兼容问题配置里需要额外注册JavaTimeModule。我在项目里用到过自定义ObjectMapper的写法ObjectMapper objectMapper new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(objectMapper);第二个注意点是如果Value是复杂的泛型结构比如List 反序列化时容易出现类型丢失的报错。此时更稳妥的方案是封装一个类型安全的操作工具类专门处理List的写入与读取而不是一股脑依赖默认反序列化。此外Hash的HashValue也要单独设置因为如果不设置Hash类型的内部值依然会走默认序列化很多人在配置时只设置了ValueSerializer而漏了HashValueSerializer结果还是看到乱码。3.3 缓存三大问题的治理穿透、击穿、雪崩缓存治理是Redis使用中绕不开的话题网上高频搜索词里“redis缓存治理”背后对应的就是这三座大山。先说缓存穿透它指的是查询一个根本不存在的数据缓存和数据库都查不到于是每次请求都会打到数据库。恶意攻击时用不存在的ID循环请求就能把数据库拖垮。治理手段有三个层次第一层是接口层参数校验把明显不合理的ID直接拒绝第二层是即使查不到数据也把空值写入缓存并设置较短的TTL后续相同查询直接命中空缓存第三层是引入布隆过滤器把所有可能存在的ID提前加载到布隆过滤器里查询前先走一次过滤不存在的ID直接拦截。缓存击穿和穿透听起来像但本质不同。击穿指某个热点Key在缓存过期的瞬间大量并发请求同时发现没缓存全部去数据库查询引起数据库瞬时压力激增。解决思路是“互斥重建”和“逻辑过期”。互斥重建的做法很朴素当发现缓存不存在时先获取一个分布式锁只有拿到锁的线程才去查数据库回写缓存其他线程短暂等待后重新读取缓存。这能避免大量线程同时访问数据库代价是牺牲了一点点吞吐。逻辑过期则是存入缓存时不设置物理TTL而是在Value里额外记录一个逻辑过期时间字段读到时判断逻辑是否过期如果过期则返回旧数据同时异步去更新缓存。这种方式对性能更友好但实现复杂度更高。缓存雪崩是指大量Key在同一时间段集中过期或者Redis实例宕机导致所有请求直接打到数据库。对应策略包括给Key的TTL加上随机偏移量例如固定过期时间上增加几秒到几分钟的随机值缓存预热在流量进来之前先把热点数据加载好对数据库做熔断降级防止数据库被打挂Redis本身采用高可用部署避免单点故障。这些措施按需组合一般都能把雪崩的概率降到很低的水平。实际项目里我见过一种比较单一的做法只设置固定TTL比如都设成10分钟结果整点时刻缓存同时失效数据库压力瞬间翻倍。给TTL加随机偏移是成本最低、收益最高的一个小改动强烈建议你在所有批量写入缓存的逻辑里都加上。3.4 用Spring缓存注解做声明式缓存和容器三级缓存别搞混Spring从3.1开始就提供了CacheManager抽象用Cacheable、CachePut、CacheEvict几个注解就能在方法上声明缓存行为不需要手动写RedisTemplate。这个机制很香因为调用方不需要感知缓存的存在只需要在方法上加上注解即可。启用注解缓存的第一步是在配置类加EnableCaching然后注入一个RedisCacheManager来替换默认实现EnableCaching Configuration public class CacheConfig { Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .prefixCacheNameWith(cache:) .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); } }这样配置后用Cacheable时Key默认是“方法参数组合哈希参数值”如果想让缓存Key更直观可以使用SpEL表达式指定keyCacheable(value user, key #userId) public User getUserById(Long userId) { return userMapper.selectById(userId); } CacheEvict(value user, key #userId) public void deleteUser(Long userId) { userMapper.deleteById(userId); } CachePut(value user, key #user.getId()) public User createUser(User user) { userMapper.insert(user); return user; }使用注解有一个容易忽略的点Cacheable内部的逻辑默认有“短路”效果方法体在缓存命中时不会执行因此不适合做写入型操作。有同学把更新的逻辑放在Cacheable注解的方法里结果第二次调用走缓存后根本不执行更新数据就出错了。该用CachePut就用CachePut它保证方法执行并把返回值写入缓存。另外Spring容器自身有个“三级缓存”的概念是用来解决循环依赖的那是Bean生命周期层面的东西和这里的缓存注解没有任何关系面试时别回答串了。4. 分布式锁、事务管道与日常监控排查4.1 手写分布式锁的正确姿势以及Redisson看门狗机制分布式锁在Java项目里非常高频但也是最容易写错的地方。我先说简洁的手写版本基于RedisTemplate实现核心是利用String类型的SETNX命令配合过期时间做原子加锁。之所以强调原子是因为“加锁”和“设置过期时间”必须是同一条命令很多老教程中是先SETNX再EXPIRE中间一旦进程崩溃锁就没有过期时间会永久卡死。正确的写法是用setIfAbsent(key, value, timeout, TimeUnit)Redis内部会把它翻译为SET key value NX PX一条命令完成。请求ID的价值在于“释放锁时判断归属”。如果不判断归属可能会出现这样的问题线程A拿到锁后业务执行时间过长锁已经自动过期线程B拿到新锁开始执行此时线程A执行完毕执行DEL操作把线程B的锁给删了线程C又拿到锁三个线程同时执行临界区分布式锁形同虚设。为了解决这个问题释放锁时必须先比较锁里的值是否还是自己的请求ID再删除。这个“比较再删除”也不是两条独立命令而是要走Lua脚本保证原子性// 加锁 String lockKey lock:order: orderId; String requestId UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(30)); if (Boolean.TRUE.equals(locked)) { try { // 执行业务逻辑例如扣减库存 } finally { // 释放锁先比对再删除用Lua脚本保证原子性 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), requestId); } }这段代码里一定要写finally来释放锁否则业务抛异常时锁永远不会释放后续请求全部卡在获取锁上直接拖垮整个接口。过期时间也不能拍脑袋设得太短否则长任务没执行完锁就过期了也不能太长Redis宕机或持锁线程长时间不释放时会阻塞其他线程。这种矛盾是手写锁的天然短板所以更推荐你在复杂场景下直接用Redisson。Redisson的RLock配合看门狗机制能自动续期在锁持有期间持续把过期时间延长直到业务执行完成后再释放。其核心用法是RLock lock redissonClient.getLock(lock:order: orderId); try { if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 执行业务 } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }这里要区分两个时间参数的区别第一个参数是等待锁的时间第二个参数是锁的自动释放时间。如果不传第二个参数或者不指定leaseTimeRedisson默认会启用看门狗每10秒自动续期30秒。但如果你像上面这样显式传了leaseTimeRedisson就不会自动续期到期直接释放。很多网上文章说Redisson锁“永远不会自动过期”是不准确的准确说法是“默认看门狗模式不会”显式指定leaseTime时依然会到期释放。知道这个区别线上问题排查时会少走很多弯路。4.2 事务与管道批量写入时如何减少网络往返Redis的单条命令很快但如果业务逻辑里需要循环执行成百上千条命令每条命令都走一次网络往返总耗时就会非常夸张。我在项目里有一段“批量回刷”逻辑要给一组用户写入签到记录用普通方式循环SET1000条数据耗时接近10秒换成管道之后只需要几百毫秒。这里用到的就是Redis Pipeline机制它把多条命令打包后一次性发给Redis服务器减少交互次数微服务间RPC调用也常常用这个思想做性能优化。在Spring Data Redis中执行管道操作很简单调用executePipelined方法在回调里直接写底层命令即可ListObject results redisTemplate.executePipelined((RedisCallbackObject) connection - { for (int i 0; i 1000; i) { connection.stringCommands().set( (pipe:key: i).getBytes(StandardCharsets.UTF_8), String.valueOf(i).getBytes(StandardCharsets.UTF_8)); } return null; });需要说明的是管道并不是严格意义上的事务。管道里的命令依然会被逐条执行如果在执行中途某一环出错并不会像Redis事务的MULTI/EXEC那样支持整体回滚。两者虽然都是“批量执行”但语义不同。如果确实需要原子性应该用redisTemplate.execute配合MULTI/EXEC这在Spring Data Redis中有SessionCallback的写法。我建议组合使用需要原子性的时候走事务单纯追求批量速度时走管道两者适用边界不能混淆。另外管道内的命令如果存在互相依赖比如某条命令的结果要作为下一条命令的参数那就不适合用管道因为管道是异步一次性发出的拿不到单条命令的即时返回值。这种情况要么改用普通逐条命令要么用Lua脚本把流程搬到Redis服务端执行后者能保证原子和减少网络往返是更优方案。4.3 监控与排查常用手段Redis日志、慢查询、常用工具生产环境里Redis出问题最怕的是手里没有能快速定位问题的工具。我平时做Redis排障第一件事就是看几个关键指标内存使用率、命中率、连接数、慢命令数量。Redis内置的INFO命令足够满足大部分场景INFO memory能看到used_memory、maxmemoryINFO stats里可以直接查到keyspace_hits和keyspace_misses命中率低于90%就值得关注缓存效果是否正常。命令行的几个排查手段相当有用。redis-cli --latency可以快速检测到Redis实例的网络延迟连续输出几条延迟数据就能判断网络是否稳定redis-cli --bigkeys会扫描整个实例并输出占用空间最大的Key这个命令在生产实例上执行时要谨慎一旦碰上大Key扫描Redis单线程执行的理念会让它在扫描过程中阻塞其他请求所以建议在低峰期执行或者等着集群中某个副本去扫SLOWLOG GET 20能查看最近的20条慢查询结合日志时间还原现场。可视化工具方面Redis Desktop Manager是很多人熟悉的桌面客户端可以直观查看Key的分布、数据类型、过期情况。不过用这类工具时要注意它本质是发起Redis命令的客户端直接在界面里扫全库或者线上执行危险命令一样会造成性能影响别把线上库当成本地测试区来玩。Spring Boot应用层面也别忘了加Actuator监控并接入健康检查和指标收集management: endpoints: web: exposure: include: health,redis,metrics endpoint: health: show-details: always这样可以通过/actuator/health看到Redis的健康状态通过Micrometer指标追踪缓存操作耗时。Redis日志通常输出在Redis进程的日志文件里配置了logfile参数后可以在其中看到主从同步、慢查询以及OOM相关的记录。把这些信息汇总到一个归档平台出问题时有据可查比上线时“拍脑袋猜原因”强太多。回到我自己的体会手写分布式锁这件事项目初期用纯粹是为了少引一个依赖但线上出过几次锁超时导致的并发问题后我很快就切换到Redisson的RLock了续期和释放交给权威组件处理心智负担小很多。如果你也在Java和Spring环境下用Redis最值得在立项初期就确定下来的其实是序列化方案它影响范围比想象中大从二进制Key造成的排障困境到跨服务数据消费的兼容性都和它有关。这些小的技术取舍积累起来就是系统稳定性的差异。以后有机会我再单独聊聊大Key治理和集群迁移的实战过程到时候继续分享。
返回列表