背景 缓存击穿是热点Key引发的高并发问题——一个被频繁访问的Key,恰好在缓存重建业务复杂时失效,瞬间大量请求直接冲击数据库,后果严重。 业界主流应对方案包括互斥锁与逻辑过期两种,以下逐一分析。 1. 互斥锁 2. 逻辑过期 互斥锁 原理:缓存未命中时,所有线程必须先获取互斥锁,才有资格查询数据库
缓存击穿是热点Key引发的高并发问题——一个被频繁访问的Key,恰好在缓存重建业务复杂时失效,瞬间大量请求直接冲击数据库,后果严重。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
业界主流应对方案包括互斥锁与逻辑过期两种,以下逐一分析。
原理:缓存未命中时,所有线程必须先获取互斥锁,才有资格查询数据库并重建缓存。未抢到锁的线程循环等待,直到缓存被重建。优点:数据绝对一致;缺点:响应速度因排队而下降,存在死锁风险。

思路:让热点Key在Redis中永不过期,但额外附加“过期时间”字段判断数据是否需刷新。若未过期则直接返回;若过期则尝试获取锁,抢到锁的线程启动新线程后台重建缓存,原线程直接返回旧数据;未抢到锁则说明已有线程在重建,直接返回旧数据,用户无感知。

两种方案对比:
| 解决方案 | 优点 | 缺点 |
|---|---|---|
| 互斥锁 | 无额外内存消耗;保证一致性;实现简单 | 线程等待,性能受影响;可能存在死锁风险 |
| 逻辑过期 | 线程无需等待,性能较好 | 不保证一致性;有额外内存消耗;实现复杂 |
以根据id查询商品店铺为例,先搭建基础框架。
@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)@TableName("tb_shop")public class Shop implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 商铺名称 */ private String name; /** * 商铺类型的id */ private Long typeId; /** * 商铺图片,多个图片以','隔开 */ private String images; /** * 商圈,例如陆家嘴 */ private String area; /** * 地址 */ private String address; /** * 经度 */ private Double x; /** * 维度 */ private Double y; /** * 均价,取整数 */ private Long a vgPrice; /** * 销量 */ private Integer sold; /** * 评论数量 */ private Integer comments; /** * 评分,1~5分,乘10保存,避免小数 */ private Integer score; /** * 营业时间,例如 10:00-22:00 */ private String openHours; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新时间 */ private LocalDateTime updateTime; @TableField(exist = false) private Double distance;}
public class RedisConstants { public static final String CACHE_SHOP_KEY = "cache:shop:"; public static final String LOCK_SHOP_KEY = "lock:shop:"; public static final Long LOCK_SHOP_TTL = 10L; public static final String EXPIRE_KEY = "expire";}
public class ObjectMapUtils { // 将对象转为 Map public static Map obj2Map(Object obj) throws IllegalAccessException { Map result = new HashMap<>(); Class> clazz = obj.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { // 如果为 static 且 final 则跳过 if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) { continue; } field.setAccessible(true); // 设置为可访问私有字段 Object fieldValue = field.get(obj); if (fieldValue != null) { result.put(field.getName(), field.get(obj).toString()); } } return result; } // 将 Map 转为对象 public static Object map2Obj(Map
@Data@NoArgsConstructor@AllArgsConstructorpublic class Result { private Boolean success; private String errorMsg; private Object data; private Long total; public static Result ok(){ return new Result(true, null, null, null); } public static Result ok(Object data){ return new Result(true, null, data, null); } public static Result ok(List> data, Long total){ return new Result(true, null, data, total); } public static Result fail(String errorMsg){ return new Result(false, errorMsg, null, null); }}
@RestController@RequestMapping("/shop")public class ShopController { @Resource public IShopService shopService; /** * 根据id查询商铺信息 * @param id 商铺id * @return 商铺详情数据 */ @GetMapping("/{id}") public Result queryShopById(@PathVariable("id") Long id) { return shopService.queryShopById(id); } }
流程图如下:

服务层代码实现:
public Result queryShopById(Long id) { Shop shop = queryWithMutex(id); if(shop == null) { return Result.fail("店铺不存在"); } return Result.ok(shop);}// 互斥锁解决缓存击穿public Shop queryWithMutex(Long id) { String shopKey = RedisConstants.CACHE_SHOP_KEY + id; boolean flag = false; try { do { // 从 redis 查询 Map entries = redisTemplate.opsForHash().entries(shopKey); // 缓存命中 if(!entries.isEmpty()) { try { // 刷新有效期 redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class); return shop; } catch (Exception e) { throw new RuntimeException(e); } } // 缓存未命中,尝试获取互斥锁 flag = tryLock(id); if(flag) { // 获取成功,进行下一步 break; } // 获取失败,睡眠后重试 Thread.sleep(50); } while(true); //未获取到锁,休眠后重试 // 查询数据库 Shop shop = this.getById(id); if(shop == null) { // 不存在,直接返回 return null; } // 存在,写入 redis try { // 测试,延迟缓存重建过程 /*try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); }*/ redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop)); redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (IllegalAccessException e) { throw new RuntimeException(e); } return shop; } catch (InterruptedException e) { throw new RuntimeException(e); } finally { if(flag) { // 获取了锁需要释放 unlock(id); } }}
测试环节:使用JMeter模拟高并发。


运行结果:


控制台日志显示,仅有一个查询数据库的请求进入,说明互斥锁成功阻止了其他线程的并发访问。
流程图如下:

逻辑过期方案中,Key永不过期。由于是热点Key,默认存在于Redis中(通过缓存预热提前写入),若Redis查不到则直接返回空。
服务层代码:
public Result queryShopById(Long id) { // 逻辑过期解决缓存击穿 Shop shop = queryWithLogicalExpire(id); if (shop == null) { return Result.fail("店铺不存在"); } return Result.ok(shop);}// 逻辑过期解决缓存击穿private Shop queryWithLogicalExpire(Long id) { String shopKey = RedisConstants.CACHE_SHOP_KEY + id; // 从 redis 查询 Map entries = redisTemplate.opsForHash().entries(shopKey); // 缓存未命中,返回空 if(entries.isEmpty()) { return null; } try { Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class, RedisConstants.EXPIRE_KEY); LocalDateTime expire = LocalDateTime.parse(entries.get(RedisConstants.EXPIRE_KEY).toString()); // 判断缓存是否过期 if(expire.isAfter(LocalDateTime.now())) { // 未过期则直接返回 return shop; } // 过期需要先尝试获取互斥锁 if(tryLock(id)) { // 获取成功 // 双重检验 entries = redisTemplate.opsForHash().entries(shopKey); shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class, RedisConstants.EXPIRE_KEY); expire = LocalDateTime.parse(entries.get(RedisConstants.EXPIRE_KEY).toString()); if(expire.isAfter(LocalDateTime.now())) { // 未过期则直接返回 unlock(id); return shop; } // 通过线程池完成重建缓存任务 CACHE_REBUILD_EXECUTOR.submit(() -> { try { rebuildCache(id, 20L); } catch (Exception e) { throw new RuntimeException(e); } finally { unlock(id); } }); } return shop; } catch (Exception e) { throw new RuntimeException(e); }}// 尝试加锁private boolean tryLock(Long id) { Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(RedisConstants.LOCK_SHOP_KEY + id, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS); return Boolean.TRUE.equals(isLocked);}// 解锁private void unlock(Long id) { redisTemplate.delete(RedisConstants.LOCK_SHOP_KEY + id);}// 重建缓存private void rebuildCache(Long id, Long expireTime) throws IllegalAccessException { Shop shop = this.getById(id); Map map = ObjectMapUtils.obj2Map(shop); // 添加逻辑过期时间 map.put(RedisConstants.EXPIRE_KEY, LocalDateTime.now().plusMinutes(expireTime).toString()); redisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id, map);}
测试时先进行缓存预热:将id=1的数据写入Redis,并将过期字段设为过去的时间,模拟数据已过期。

然后将数据库中对应的name从“101茶餐厅”改为“103茶餐厅”。使用JMeter并发请求:


测试结果:


结果:部分请求返回旧数据(直接返回过期缓存),部分请求返回新数据(后台重建完成)。Redis中数据已更新:

系统日志显示,整个过程中仅有一条SQL查询:

互斥锁与逻辑过期各具适用场景。若对数据一致性要求极高,互斥锁是首选,但需接受短暂等待延迟;若追求高吞吐、允许短暂脏读,逻辑过期方案能带来更好的响应体验。实际选型需根据业务场景和容忍度权衡。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述