首页 > 数据库 >解决Redis缓存击穿问题:互斥锁与逻辑过期

解决Redis缓存击穿问题:互斥锁与逻辑过期

来源:互联网 2026-07-07 09:07:07

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

背景

缓存击穿是热点Key引发的高并发问题——一个被频繁访问的Key,恰好在缓存重建业务复杂时失效,瞬间大量请求直接冲击数据库,后果严重。

解决Redis缓存击穿问题:互斥锁与逻辑过期

长期稳定更新的攒劲资源: >>>点此立即查看<<<

业界主流应对方案包括互斥锁与逻辑过期两种,以下逐一分析。

  • 1. 互斥锁
  • 2. 逻辑过期

互斥锁

原理:缓存未命中时,所有线程必须先获取互斥锁,才有资格查询数据库并重建缓存。未抢到锁的线程循环等待,直到缓存被重建。优点:数据绝对一致;缺点:响应速度因排队而下降,存在死锁风险。

解决Redis缓存击穿问题:互斥锁与逻辑过期

逻辑过期

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

解决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 map, Class clazz) throws Exception {        Object obj = clazz.getDeclaredConstructor().newInstance();        for (Map.Entry entry : map.entrySet()) {            Object fieldName = entry.getKey();            Object fieldValue = entry.getValue();            Field field = clazz.getDeclaredField(fieldName.toString());            field.setAccessible(true); // 设置为可访问私有字段            String fieldValueStr = fieldValue.toString();            // 根据字段类型进行转换            fillField(obj, field, fieldValueStr);        }        return obj;    }    // 将 Map 转为对象(含排除字段)    public static Object map2Obj(Map map, Class clazz, String... excludeFields) throws Exception {        Object obj = clazz.getDeclaredConstructor().newInstance();        for (Map.Entry entry : map.entrySet()) {            Object fieldName = entry.getKey();            if(Arrays.asList(excludeFields).contains(fieldName)) {                continue;            }            Object fieldValue = entry.getValue();            Field field = clazz.getDeclaredField(fieldName.toString());            field.setAccessible(true); // 设置为可访问私有字段            String fieldValueStr = fieldValue.toString();            // 根据字段类型进行转换            fillField(obj, field, fieldValueStr);        }        return obj;    }    // 填充字段    private static void fillField(Object obj, Field field, String value) throws IllegalAccessException {        if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {            field.set(obj, Integer.parseInt(value));        } else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {            field.set(obj, Boolean.parseBoolean(value));        } else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {            field.set(obj, Double.parseDouble(value));        } else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {            field.set(obj, Long.parseLong(value));        } else if (field.getType().equals(String.class)) {            field.set(obj, value);        } else if(field.getType().equals(LocalDateTime.class)) {            field.set(obj, LocalDateTime.parse(value));        }    }}

结果返回类

@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);    }    }

互斥锁方案

流程图如下:

解决Redis缓存击穿问题:互斥锁与逻辑过期

服务层代码实现:

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模拟高并发。

解决Redis缓存击穿问题:互斥锁与逻辑过期

解决Redis缓存击穿问题:互斥锁与逻辑过期

运行结果:

解决Redis缓存击穿问题:互斥锁与逻辑过期

解决Redis缓存击穿问题:互斥锁与逻辑过期

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

逻辑过期方案

流程图如下:

解决Redis缓存击穿问题:互斥锁与逻辑过期

逻辑过期方案中,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,并将过期字段设为过去的时间,模拟数据已过期。

解决Redis缓存击穿问题:互斥锁与逻辑过期

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

解决Redis缓存击穿问题:互斥锁与逻辑过期

解决Redis缓存击穿问题:互斥锁与逻辑过期

测试结果:

解决Redis缓存击穿问题:互斥锁与逻辑过期

解决Redis缓存击穿问题:互斥锁与逻辑过期

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

解决Redis缓存击穿问题:互斥锁与逻辑过期

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

解决Redis缓存击穿问题:互斥锁与逻辑过期

总结

互斥锁与逻辑过期各具适用场景。若对数据一致性要求极高,互斥锁是首选,但需接受短暂等待延迟;若追求高吞吐、允许短暂脏读,逻辑过期方案能带来更好的响应体验。实际选型需根据业务场景和容忍度权衡。

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备14008430号-1 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。