资讯详情

资讯详情

RedisUtils封装实战:从序列化配置到分布式锁与缓存防护

每个Java后端项目里几乎都会有一个RedisUtils。有的写了三次有的抄了五遍有的已经膨胀成了一千行的“万能类”。我自己在电商项目里封装过两版RedisUtils线上踩过序列化乱码、连接池被打满、缓存击穿导致数据库瞬时压力翻倍这些坑之后才慢慢明白这个工具类到底该怎么设计。这篇博文把我最终沉淀下来的一套方案完整拆给你看从配置到核心方法从分布式锁到缓存穿透、缓存击穿、缓存雪崩的防护每一行都尽量说清楚为什么这么写以及在什么场景下这套封装会失效。1. 先想清楚RedisUtils 到底要封装什么1.1 为什么不用原生 RedisTemplate 直接干活很多初学者为了方便直接在Service层里注入RedisTemplate然后到处写redisTemplate.opsForValue().set(user: id, JSON.toJSONString(user))。短期看没什么问题项目一复杂就露馅了。第一个问题是重复代码太多。同一个业务里set、get、设置过期时间、判空、转对象这几行代码能复制十几个地方。后面要加一个全局的缓存前缀、要统一处理空值、要记录缓存命中率就得满世界找代码改。第二个问题是序列化风格不统一。有人用StringRedisTemplate有人用RedisTemplate还有人在自定义配置里改序列化器。一旦两个服务之间共用同一份Redis数据Key的编码规则不一致直接读出乱码甚至类型转换异常。第三个问题是职责不清晰。Service层本来应该关心业务规则结果被Redis API细节塞满了。缓存穿透要做空值缓存缓存击穿要加互斥锁这些逻辑如果散落在各个业务代码里几乎没法维护。所以封装RedisUtils并不是为了炫技而是把“Redis访问”这个技术细节收敛到一个类里让业务代码写起来像调用本地方法一样简单。这也是我后来在团队里定的规矩除简单的内嵌缓存外所有Redis访问一律通过RedisUtils不允许直接注入RedisTemplate。1.2 封装方案选型StringRedisTemplate 还是 RedisTemplate这是封装前必须做的决定。很多人直接抄网上代码结果用了一阵发现两个问题Key变成\xAC\xED\x00\x05t\x00\x05name这种乱码或者存进去的对象反序列化时报类型错误。原因很简单Spring Boot自动配置的RedisTemplateObject,Object默认用的是JdkSerializationRedisSerializer序列化出来的二进制带一串类型描述存到Redis里就成了乱码而StringRedisTemplate的Key和Value都是StringRedisSerializer只能存字符串。我在项目里是两套并用但职责分得很清楚StringRedisTemplate主用。Value全部通过JSON序列化成字符串再存。简单、可控、排查方便在Redis客户端里看到的就是可读的JSON。RedisTemplateString,Object仅在某些需要直接序列化对象、且团队确认好类型规则的场景使用。工具类的主心骨我建议用StringRedisTemplate因为JSON字符串的方案足够通用而且通过GenericJackson2JsonRedisSerializer存对象时Redis里会多出一个class字段这个字段在跨语言、跨版本升级时会带来额外风险。字符串方案配合统一的类型转换反而是最稳的组合。1.3 工具类该有的模块划分与服务边界我见过最糟糕的工具类是几百个方法堆在一个类里连getBit、append、getRange这些都封装了看着很全实际根本用不上还让类变得很臃肿。我的做法是按功能域拆成几个职责清晰的类再通过一个门面类对外暴露。我实际用的模块划分大概是RedisStringOps字符串类型的set、get、incr、expire、delete等基础操作以及带超时时间的组合方法。RedisHashOpsHash结构的put、get、scan、批量获取等封装。RedisListOps/RedisSetOps/RedisZSetOps集合类结构的常用操作按业务场景按需封装。RedisLock分布式锁的获取、释放、续期内部实现全在这里不让锁逻辑混进业务缓存代码。RedisCacheGuard缓存穿透、缓存击穿、缓存雪崩的防御方法比如互斥锁重建、空值缓存、随机过期时间。RedisUtils门面类把上面几类通过组合方式暴露出去业务方只需要注入这一个类。这样做的好处是类不会失控每类的职责一眼就能看清测试时也能按模块单独覆盖以后想换Redisson或者加一个Lettuce的定制项不会大面积改业务代码。追求一个类搞定一切只会让后续维护越来越痛苦。2. 基础配置序列化器和连接池决定工具类的上限2.1 配置文件别把所有参数都交给默认值很多项目的Redis配置是这样的spring: redis: host: 127.0.0.1 port: 6379这套配置能跑但根本扛不住流量。Redis客户端的连接池、超时时间、空闲检测这些参数直接决定高并发下的表现。我一般至少会显式配置下面这些项以Spring Boot 2.x为例Spring Boot 3.x前缀是spring.data.redisspring: redis: host: 127.0.0.1 port: 6379 password: your_password database: 0 timeout: 3000ms lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4 max-wait: 3000ms shutdown-timeout: 200ms这里有几个容易被忽略的点timeout是连接超时还是读写超时在Lettuce里timeout是Redis命令执行超时时间默认60秒放在高并发场景下等于没有超时保护。我通常压到3秒以内宁可快速失败也不让线程卡在等待上。max-active是连接池最大连接数默认8。如果业务并发一上来8个连接很容易被打满后面全排队。但也不是越大越好连到128甚至更多的时候Redis服务端和网络带宽先扛不住。32到64是比较常见的区间具体看压测数据。max-wait是拿连接的最大等待时间默认-1表示无限等。这个必须设一个合理上限否则连接池满了之后调用线程全部阻塞表现就是接口RT飙高、线程池被打满。2.2 序列化器选型与配置代码序列化器选型我直接给结论性的对比序列化器是否适合做Key是否适合做Value说明JdkSerializationRedisSerializer不推荐不推荐默认方案二进制体积大Redis里不可读反序列化易受版本影响StringRedisSerializer推荐仅适合纯字符串就是UTF-8字符串简单安全GenericJackson2JsonRedisSerializer不推荐有条件推荐JSON可读但会写入class类型信息存在一定安全隐患仅在能控制类型白名单时使用Jackson2JsonRedisSerializer不推荐推荐需要手动配置ObjectMapper可精确指定反序列化类型适合做Value的通用序列化器我的方案是Key用StringRedisSerializerValue统一走StringRedisSerializer配合Fastjson2/Jackson手动序列化。如果硬要用RedisTemplateString,Object配置长这样Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer keySerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }注意afterPropertiesSet()这一行不调用的话部分序列化器不会在初始化阶段生效运行时暴露出来的问题会非常难查。这是我自己踩过一次的坑当时配置类的Bean能注入但数据写进Redis后Key还是带前缀的乱码一度怀疑是版本问题最后发现就是少调了这一步。2.3 工具类的初始化与依赖注入工具类用Component注册成Spring Bean通过构造器注入StringRedisTemplate这样业务代码里Autowired或Resource就能直接使用。构造器注入是首选比字段注入更容易写单元测试也更能让依赖关系一目了然。我习惯把一段初始化逻辑放在PostConstruct里做自检Component public class RedisUtils { private final StringRedisTemplate redisTemplate; public RedisUtils(StringRedisTemplate redisTemplate) { this.redisTemplate redisTemplate; } PostConstruct public void init() { // 启动时做一次连接自检尽早暴露配置问题 try { String pong redisTemplate.execute((RedisCallbackString) connection - { return connection.ping(); }); if (!PONG.equalsIgnoreCase(pong)) { log.error(Redis connection check failed: {}, pong); } } catch (Exception e) { log.error(Redis connection check error, e); } } }有同学会问启动时如果Redis没起来Application会启动失败吗默认不会因为PostConstruct里捕获了异常。但如果项目对缓存强依赖我建议把那个log.error改成抛异常让部署时快速失败别等服务起来之后请求全部超时才意识到Redis挂了。这个取舍取决于你的业务是不是能把Redis当可降级组件。3. 核心方法实现从单个命令到组合操作3.1 String 类型最常用的 set/get/expire 包装String类型的封装看起来最简单但细节都在组合方法里。我最终保留的方法集大致是public void set(String key, String value) { redisTemplate.opsForValue().set(key, value); } public void set(String key, String value, long timeout, TimeUnit unit) { redisTemplate.opsForValue().set(key, value, timeout, unit); } public String get(String key) { return redisTemplate.opsForValue().get(key); } public Boolean delete(String key) { return redisTemplate.delete(key); } public Boolean expire(String key, long timeout, TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } public Long incr(String key) { return redisTemplate.opsForValue().increment(key); } public Long incrBy(String key, long delta) { return redisTemplate.opsForValue().increment(key, delta); }很多人会忽略的一点是set(key, value, timeout, unit)底层走的是SET key value PX/EX这是原子的而如果先set(key, value)再expire(key, timeout)中间挂掉就可能导致缓存永不过期。所以我明确规定工具类里凡是要设置过期时间的一律使用带超时的set方法并且把不带过期的set方法标记为“谨慎使用”防止调用的人图省事引入隐患。还有个细节redisTemplate.delete(key)返回值是Boolean但批量删除传Collection进来时返回值是Long。这两个方法名一样很容易在工具类设计时混淆。建议起名时区分开单个删除叫delete批量删除叫deleteKeys。3.2 复杂类型操作Hash、List、Set、ZSet 的经验封装复杂类型的封装我不建议把底层API全部暴露一遍而是按业务场景封装“有用的组合”。比如Hash结构我最常用的两个场景是商品维度的小字段缓存product:123下面存name、price、stock需要更新某个字段而不动整个缓存。对象的部分实时属性用户在线状态、操作次数等。我封装的Hash方法会包含hashPut、hashGet、hashGetAll、hashIncrement、hashDelete特别注意hashGetAll返回的MapObject,Object需要进行一次LinkedHashMap转换否则调用方拿到的Map类型不好处理。List结构我更推荐的用法是做简单的队列或操作流水不推荐用rightPushAll一次性塞超大列表容易造成Big Key。Set结构常用在去重、标签、好友关系这种场景注意慎用members拿全量有些集合可能几十万成员直接拿全量内存就崩了应该用scan或者randomMembers。ZSet结构的封装要额外关注add方法的分数参数类型Spring Data Redis里分数是double一些金额敏感场景要提前约定精度避免浮点误差。我的习惯是在工具类内部对分数做一遍格式化统一保留两位小数。3.3 分布式锁setnx 的正确用法和 Lua 释放锁分布式锁是RedisUtils里最容易被写错的部分。先用一句话点出核心加锁要原子释放锁要校验归属锁必须有过期时间。加锁的正确写法public boolean tryLock(String lockKey, String requestId, long expire, TimeUnit unit) { return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expire, unit); }不要写“先setnx再expire”的两步操作中间那一下宕机会让锁永远不解锁。setIfAbsent支持直接带超时参数这是原子命令SET key value NX EX的标准封装。释放锁更要小心。很多人直接写redisTemplate.delete(lockKey)这就埋了一个大坑如果当前线程的锁已经因为超时被释放另一个线程又拿到了同一把锁此时第一个线程的delete会把别人的锁删了。所以在释放前必须校验value是不是自己的requestId而且校验和删除必须在一个Lua脚本里完成public void unlock(String lockKey, String requestId) { String script if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end; DefaultRedisScriptLong redisScript new DefaultRedisScript(script, Long.class); redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId); }拿锁时每次生成一个UUID.randomUUID().toString()作为requestId释放锁时传同一个requestId进来。Lua脚本保证“判断当前值是不是我的”和“删除这个key”这两步是原子的不会中间插入别的命令。这套锁能应付大多数场景但有几个要注意的边界锁超时时间不要设置5秒这种过短的值业务没执行完锁就到期了也不要设置30分钟这种过长值Redis宕机会把锁拖死。一般按业务耗时峰值的1.5到2倍设。可重入上面这个锁本身不可重入同一个线程拿锁后再次tryLock会失败。如果需要可重入要么引入Redisson要么用ThreadLocal维护重入计数。续期/看门狗Redisson的看门狗机制会在锁快过期时自动续期但自己用setIfAbsent实现的锁没有这个能力。所以长任务一定要评估锁超时别让锁先于业务释放。面试里被问到时按这个思路答基本稳先聊加锁的原子性再聊释放锁要校验归属最后聊超时续期和Redisson的对比。这三个层次都覆盖了就说明你真的在线上考虑过分布式锁问题。3.4 批量操作的性能优化Pipeline 而不是 for 循环写工具类时最容易出的性能问题是批量场景用for循环逐个set。比如要初始化1000条商品缓存for循环发1000个网络请求哪怕每条1ms总耗时也要1秒多。换成Pipeline一个请求把所有命令发过去总耗时能降到几十毫秒量级。我用Pipeline封装的一个典型方法public void batchSet(MapString, String kvMap) { redisTemplate.executePipelined((RedisCallbackObject) connection - { kvMap.forEach((key, value) - { byte[] rawKey redisTemplate.getStringSerializer().serialize(key); byte[] rawValue redisTemplate.getStringSerializer().serialize(value); connection.stringCommands().set(rawKey, rawValue); }); return null; }); }这里有几个注意点Pipeline是“批量发送命令”的管道不是事务。Pipeline执行过程中Redis会逐条处理命令中间如果出错不会像事务一样回滚。Pipeline的返回结果是所有命令执行结果的List如果你只关心“命令都发出去了”返回值可以忽略。一次性Pipeline几千条甚至上万条命令会造成响应数据积压内存开销大。我一般会拆批每批500到1000条。还有一类批量操作是multiGet一次性传入多个Key拿值public ListString multiGet(CollectionString keys) { return redisTemplate.opsForValue().multiGet(keys); }multiGet在底层其实也是一个批量请求但它在逻辑上要保证这批Key一起读取不像Pipeline能处理异构命令。如果业务要的是“一次拿到一批字符串值”直接用multiGet就对了没必要上Pipeline。4. 企业级场景用工具方法抵御缓存穿透、击穿与雪崩4.1 缓存穿透的兜底空值缓存与布隆过滤器缓存穿透指的是查询一个根本不存在的数据请求绕过缓存直接打到数据库。如果是正常业务这种请求量不大怕的是恶意请求不断构造不存在的ID比如订单号从1000试到10000每一个都查不到每次都穿透到数据库。我的第一个兜底手段是空值缓存当数据库查询结果为空时也往Redis写一个值只是过期时间设得短。这样后续同样的查询能在Redis里拿到“空”然后直接返回不再穿透到数据库。工具类方法设计public String getWithNullCache(String key, long nullTimeout, TimeUnit unit, SupplierString loader) { String cacheValue get(key); if (cacheValue ! null) { return cacheValue; } String loadedValue loader.get(); if (loadedValue null) { // 缓存空值短时间内直接命中保护数据库 set(key, , nullTimeout, unit); return null; } set(key, loadedValue, nullTimeout * 2, unit); return loadedValue; }空值缓存有个要注意的问题空值的过期时间不能和正常值一样长否则某个确定存在的Key只是因为暂时没写入就会被空值缓存挡很长时间。我习惯空值过期时间设30到60秒正常值按业务需求。更严格一点的方案是布隆过滤器。在缓存之前加一层布隆过滤器过滤器判断Key不存在就直接拒绝查询。但布隆过滤器有误判率只能确定“一定不存在”不能确定“一定存在”。所以它适合用来挡住大部分非法Key的访问再把小部分误判放回Redis和数据库。但这个方案要维护过滤器数据数据量小时不是必须数据量大时再上。4.2 缓存击穿互斥锁重建与逻辑过期缓存击穿是指一个非常热的Key在缓存过期的瞬间同时来了一大批请求这些请求全部穿到数据库数据库压力瞬间翻倍。热点新闻、秒杀商品这种场景最容易出现。解决方案很经典热点Key缓存过期时只有一个线程负责重建缓存其他线程等缓存重建完再读取。用互斥锁实现的方法public String getWithMutex(String key, long timeout, TimeUnit unit, SupplierString loader, long lockExpire, TimeUnit lockUnit) { String value get(key); if (value ! null) { return value; } String lockKey lock: key; String requestId UUID.randomUUID().toString(); boolean locked tryLock(lockKey, requestId, lockExpire, lockUnit); if (!locked) { // 拿不到锁说明有线程在重建缓存短暂阻塞后重试 sleep(50); return getWithMutex(key, timeout, unit, loader, lockExpire, lockUnit); } try { // 双重检查拿到锁后可能缓存已经被其他线程重建 value get(key); if (value ! null) { return value; } value loader.get(); if (value null) { set(key, , 30, TimeUnit.SECONDS); return null; } set(key, value, timeout, unit); return value; } finally { unlock(lockKey, requestId); } }这段代码有几个细节要说明拿不到锁之后的重试不能无限递归实际项目中要加次数限制或者超时退出否则极端情况下某线程会一直重试到栈溢出或者把请求拖垮。loader.get()是查数据库并回写缓存的回调一定要放在锁内执行否则多线程同时查库的问题还是没解决。锁的过期时间要大于数据库查询和重建缓存的耗时否则锁先过期后面进来的请求又会重建一次互斥就失效了。另一种思路是逻辑过期Value里存一个带过期时间的字段而不是真正给Redis设过期时间。当发现逻辑过期时拿到锁的线程重建缓存其他线程直接返回旧值这样能保证读请求不会阻塞但存在短暂的数据不一致。这个方案适合那些读多写少、能容忍短暂旧数据的场景。4.3 缓存雪崩过期时间加随机抖动缓存雪崩是指大量Key集中在同一时间过期导致这些Key的查询全部穿透到数据库。如果把雪崩和击穿对比一下击穿是单个热点Key过期雪崩是一大批Key同时过期。缓解办法最核心的就是打散过期时间。不要所有Key都设10分钟而是在10分钟基础上加一个随机值比如8到12分钟之间随机。这样同一批Key的过期时间不再集中在同一个时刻数据库的压力就被摊开了。工具类里的实现public void setWithRandomExpire(String key, String value, long baseTimeout, long randomBound, TimeUnit unit) { long randomExtra ThreadLocalRandom.current().nextLong(randomBound); long finalTimeout baseTimeout randomExtra; redisTemplate.opsForValue().set(key, value, finalTimeout, unit); }调用方式类似redisUtils.setWithRandomExpire(user: userId, userJson, 3600, 600, TimeUnit.SECONDS)表示基础1小时加0到10分钟的随机值。除了打散过期时间雪崩的防御还要配合“缓存永不失效后台定时刷新”的策略即过期时间设很长由一个定时任务按业务节奏主动更新缓存。这样即使缓存没有在预期时间过期也不会集体失效。这个方案适合数据实时性要求不高的场景比如首页推荐列表。4.4 缓存一致性延迟双删什么时候才能真正保证一致缓存和数据库的一致性是老生常谈的问题了。先更新数据库、再删除缓存Cache Aside Pattern是业界用得最多也最容易被理解的方案。它的问题是更新数据库后在删除缓存之前有一个短暂的窗口期请求可能读到脏缓存。更稳妥一点的延迟双删写法public void invalidateCacheWithDelay(String key) { // 第一次删除先让旧缓存失效 redisTemplate.delete(key); // 延迟再删一次把并发期间被写回的脏数据清掉 delayedCacheEvictExecutor.schedule(() - redisTemplate.delete(key), 500, TimeUnit.MILLISECONDS); }为什么延迟要放在删除之后而不是先延迟再删除因为延迟的目的是“等一个读请求从数据库读出旧值并写回缓存”的过程完成然后把这条脏数据清掉。如果延迟时间设得太短比如100毫秒读请求还没完成写回第二次删除等于白删设得太长比如5秒那在这5秒内读请求还是会拿到一次脏缓存。实际项目中我一般把延迟设为500到1000毫秒具体要看读取写缓存链路耗时。这个方法并不能100%保证强一致它只是在并发不高的情况下把不一致窗口缩短到极小。如果业务对一致性要求极高比如库存扣减这类强一致场景那就别依赖缓存兜底直接走数据库事务或者用带版本号的更新机制来校验。工具类里我会把延迟双删封装成独立的方法并且不要把它做成默认的缓存更新策略。因为频繁延迟删除会增加Redis的写压力。我的建议是只有那些并发读写都高、且容忍短暂不一致的业务才用延迟双删写入频率低的业务直接删一次缓存就够了。5. 线上故障与排查经验实录5.1 常见问题速查表先整理一张速查表这些是我和团队在线上踩过的真实问题每一行背后都有一次报警或者一次事故复盘。现象常见原因排查方向Redis里Key带\xAC\xED前缀序列化器配置错误默认Jdk序列化检查RedisTemplate的KeySerializerValue存进Redis后是乱码Value序列化器没配置检查ValueSerializer统一为JSON字符串接口偶发超时Redis连接池报错max-active太小或max-wait太短压测连接池参数观察活跃连接数缓存一直不失效set和expire分步操作后者失败改用带超时的set命令扫描代码里的两步操作某热点Key过期瞬间数据库压力飙升缓存击穿加互斥锁重建或逻辑过期同一批Key集体过期后数据库被打慢缓存雪崩过期时间加随机抖动或定时刷新缓存程序里delete缓存但Redis里还在删除后并发线程重新写回旧值延迟双删或从源头避免写回脏数据大Key删除后Redis阻塞删除大对象耗时过高用unlink异步删除同时拆分大Key5.2 案例一缓存全丢后数据库被击穿有次线上做缓存预热不小心用flushdb把整个Redis库清了。结果下一秒所有请求同时变成缓存未命中全部打到数据库。数据库连接数瞬间打满主库CPU飙升到90%以上接口大面积超时。复盘时发现如果当时多个核心业务方法都走的是getWithMutex或者至少是空值缓存方法数据库不至于被直接打穿。因为互斥锁会让大量请求等一个线程重建缓存而不是同时涌进数据库。这个教训之后我把“无兜底的get后直接查库”的代码全部加了保护缓存未命中时必须走防击穿方法或者至少确认这个Key未命中的量级在可控范围内。这里也提醒一点RedisUtils再完善也只是工具。真正的防护在于业务代码是否强制使用这些工具方法。所以我在团队里要求新的Redis缓存调用必须走工具类不允许直接get后查库再set。5.3 案例二连接池耗尽与 timeout 排查有一段线上服务RT从50ms涨到了800ms查日志发现大量这样的异常io.lettuce.core.RedisCommandTimeoutException: Command timed out after 3 seconds一开始以为是Redis服务器出问题了检查Redis自身CPU和内存都很正常。后来看监控才发现这个服务从单实例变成了多实例部署但每个实例的连接池配置还是默认的8。多实例加起来连接数倒是多了但单个实例在高峰期会有超过8个线程同时访问Redis后面的线程全在排队等待连接等待时间长了就会超时。解决方式就是把lettuce.pool.max-active从默认8调大到32同时把max-wait从无限等待改为2秒让拿不到连接的请求快速失败而不是无限阻塞把整个服务拖死。这个参数调整之后再压测RT就恢复到了正常水平。排查的时候要注意区分是连接池等待超时还是Redis命令执行超时这两种问题日志虽然都带timeout但根因完全不同。5.4 工具类维护拒绝“大杂烩”与滥用风险工具类写多了之后很容易变成“大杂烩”。今天有人加一个getRange明天有人加一个bitCount后天又有人把别的项目的工具方法粘进来。到后面类越来越大方法之间的命名和风格越来越乱调用方也不知道该用哪个。我的维护原则是一个方法必须在至少两个真实业务场景里被用到才有资格进入工具类一个方法如果三个版本没人使用直接从工具类里删掉。不要为了“全”而全工具类不是Redis命令大全。另外不要在工具类里写容易误用的方法比如无限重试逻辑、危险的keys *、monitor这类线上不能用或必须慎用的操作。需要遍历Key时工具类应提供基于scan的实现而不是暴露keys。public SetString scanKeys(String pattern) { SetString result new HashSet(); redisTemplate.execute((RedisCallbackSetString) connection - { try (Cursorbyte[] cursor connection.scan(ScanOptions.scanOptions() .match(pattern) .count(100) .build())) { while (cursor.hasNext()) { result.add(new String(cursor.next(), StandardCharsets.UTF_8)); } } catch (IOException e) { log.error(scan keys error, pattern: {}, pattern, e); } return result; }); return result; }scan比keys *安全但也不能在生产库上频繁执行大范围的scan它仍然会占用Redis服务端CPU。我用scan基本都加了精确前缀比如user:*并且限制count值降低服务端压力。6. 踩过几次坑之后的工具类使用心得做RedisUtils这个工具类最重要的不是把API写得多么齐全而是让业务代码的调用变得安全、一致、可维护。我踩过序列化配置少一行afterPropertiesSet的坑踩过锁释放误删别人锁的坑也踩过连接池满导致整个服务雪崩的坑。这些坑单独看都很基础但组合在一起就是线上稳定性的一道分水岭。如果你所在的项目还没有一套像样的Redis工具类可以从这篇文章里的基础方法抄起先把String操作、Hash操作、分布式锁和防击穿方法加到项目里。不要一开始就追求面面俱到先把最核心、最容易出问题的地方兜住。如果你已经在维护一套RedisUtils建议回头检查几个点所有带过期时间的set是否原子分布式锁释放时是否校验了归属热点Key过期时是否有互斥锁保护批量操作是否避开了for循环逐条调用这四点都稳了工具类至少不会在关键时候掉链子。另外团队里用的时候一定要把“什么时候用哪个方法”写进注释。比如getWithMutex适合热点Key防击穿getWithNullCache适合防穿透setWithRandomExpire适合防雪崩。这三个方法看起来有点像业务同学如果不看注释乱用效果会大打折扣。工具类好不好用很多时候不取决于代码写得有多炫而取决于下一次维护它的人能不能一眼看懂设计意图。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →