redis--007
发布时间:2026/8/25 22:29:40 作者:尧图编辑部 阅读量:1,286

一、逻辑过期解决缓存击穿缓存击穿含义热点高频访问的缓存 key 过期大量并发请求同时涌向数据库。逻辑过期核心思想Redis 不设置真实的过期删除时间把过期时间保存在缓存 value 内部就算数据逻辑上过期依旧返回旧缓存数据后台异步去更新缓存用户请求不会等待数据库查询。业务执行流程根据商铺 id 查询 Redis 缓存如果缓存未命中直接返回结果结束。逻辑过期方案需要提前预热热点数据存入 Redis不会自动访问数据库加载。缓存命中读取 value 内部存储的逻辑过期时间未过期直接返回缓存中的业务数据已逻辑过期尝试获取互斥锁获取锁成功开启独立后台线程由后台线程访问数据库更新 Redis 缓存并写入新的逻辑过期时间更新完成释放锁主线程直接返回旧缓存不等待更新完成获取锁失败直接返回旧缓存数据优缺点✅优点并发性能高用户请求不会阻塞。 ⚠️缺点会短暂返回过期旧数据数据短暂不一致必须提前预热热点 key。private static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10); public Shop queryWithLogicalExpire(Long id) { //查ID String shopJson stringRedisTemplate.opsForValue().get(cache:shop id); //存在返回不存在查数据库 if(StrUtil.isBlank(shopJson)) { Shop shop JSONUtil.toBean(shopJson, Shop.class); return shop; } //Json反序列话为对象 RedisData redisData JSONUtil.toBean(shopJson, RedisData.class); JSONObject data (JSONObject) redisData.getData(); Shop shop JSONUtil.toBean(data, Shop.class); LocalDateTime expireTime redisData.getExpireTime(); //判断过期 if (expireTime.isAfter(LocalDateTime.now())) { return shop; } String lockKey LOCK_SHOP_KEY id; boolean b tryLock(lockKey); if (b) { CACHE_REBUILD_EXECUTOR.submit(()-{ try { this.saveShop2Redis(id,20L); } catch (Exception e) { throw new RuntimeException(e); } finally { unlock(lockKey); } }); } return shop; }对比互斥锁方案互斥锁让请求阻塞等待新数据数据一致性更好但并发能力差。二、缓存三大问题回顾表格问题现象主流解决思路缓存穿透查询数据库不存在的数据缓存不生效请求直达 DB缓存空值、布隆过滤器缓存击穿热点 key 失效大量并发打数据库互斥锁、逻辑过期缓存雪崩大量缓存 key 集中同时过期压力全部落到数据库过期时间打散随机、Redis 集群、多级缓存三、缓存工具封装需求梳理基于StringRedisTemplate封装通用缓存工具4 个方法分工普通存入缓存Java 对象转为 JSON 存入 Redis设置真实 TTL 物理过期时间。用于普通业务缓存。逻辑过期存入对象包装内部记录逻辑过期时间Redis 本身不设置过期时间。专门用于处理缓存击穿。普通查询缓存读取缓存反序列化缓存查询不到时往 Redis 存入空值并设置短 TTL解决缓存穿透。逻辑过期查询缓存完整执行逻辑过期整套流程判断内部过期时间过期就异步更新缓存返回旧数据解决缓存击穿。Component public class CacheClient { private StringRedisTemplate stringRedisTemplate; public CacheClient(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate stringRedisTemplate; ; } public void set(String key, Object value, Long time, TimeUnit unit) { stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit); } public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) { //设置逻辑过期 RedisData redisData new RedisData(); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time))); stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), time); } public R, ID R queryWithPassThrough( String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit ) { String key keyPrefix id; //查ID String json stringRedisTemplate.opsForValue().get(key); //存在返回不存在查数据库 if (StrUtil.isNotBlank(json)) { return JSONUtil.toBean(json, type); } if (json ! null) { return null; } R byId dbFallback.apply(id); //数据库结果 if (byId null) { //将空值写入redis stringRedisTemplate.opsForValue().set(key, , 10, TimeUnit.MINUTES); return null; } this.set(key, byId, time, unit); return byId; } private static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10); public R,ID R queryWithLogicalExpire(String keyPrefix, ID id, ClassR type, FunctionID, R dbFallback, Long time, TimeUnit unit) { String key keyPrefix id; //查ID String json stringRedisTemplate.opsForValue().get(key); //存在返回不存在查数据库 if (StrUtil.isBlank(json)) { return JSONUtil.toBean(json, type); } //Json反序列话为对象 RedisData redisData JSONUtil.toBean(json, RedisData.class); JSONObject data (JSONObject) redisData.getData(); R r JSONUtil.toBean(data, type); LocalDateTime expireTime redisData.getExpireTime(); //判断过期 if (expireTime.isAfter(LocalDateTime.now())) { return r; } String lockKey LOCK_SHOP_KEY id; boolean b tryLock(lockKey); if (b) { CACHE_REBUILD_EXECUTOR.submit(() - { try { R r1 dbFallback.apply(id); this.setWithLogicalExpire(key,r1,time,unit); } catch (Exception e) { throw new RuntimeException(e); } finally { unlock(lockKey); } }); } return r; } private boolean tryLock(String key) { Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.MINUTES); return BooleanUtil.isTrue(flag); } private void unlock(String key) { stringRedisTemplate.delete(key); } }