当前位置: 代码网 > it编程>数据库>Redis > 解决Redis缓存击穿问题(互斥锁、逻辑过期)

解决Redis缓存击穿问题(互斥锁、逻辑过期)

2026年02月07日 Redis 我要评论
背景缓存击穿也叫热点 key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击常见的解决方案有两种:1.互斥锁2.逻辑过期互斥锁:本

背景

缓存击穿也叫热点 key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击

常见的解决方案有两种:

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

互斥锁:

本质就是让所有线程在缓存未命中时,需要先获取互斥锁才能从数据库查询并重建缓存,而未获取到互斥锁的,需要不断循环查询缓存、未命中就尝试获取互斥锁的过程。因此这种方式可以让所有线程返回的数据都一定是最新的,但响应速度不高

逻辑过期:

本质就是让热点 key 在 redis 中永不过期,而通过过期字段来自行判断该 key 是否过期,如果未过期,则直接返回;如果过期,则需要获取互斥锁,并开启新线程来重建缓存,而原线程可以直接返回旧数据;如果获取互斥锁失败,就代表已有其他线程正在执行缓存重建工作,此时直接返回旧数据即可

两者的对比:

解决方案优点缺点
互斥锁没有额外的内存消耗保证一致性实现简单线程需要等待,性能受影响可能有死锁风险
逻辑过期线程无需等待,性能较好不保证一致性有额外内存消耗实现复杂

代码实现

前置

这里以根据 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 avgprice;

    /**
     * 销量
     */
    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<string, string> obj2map(object obj) throws illegalaccessexception {
        map<string, string> 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<object, object> map, class<?> clazz) throws exception {
        object obj = clazz.getdeclaredconstructor().newinstance();
        for (map.entry<object, object> 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<object, object> map, class<?> clazz, string... excludefields) throws exception {
        object obj = clazz.getdeclaredconstructor().newinstance();
        for (map.entry<object, object> 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
@allargsconstructor
public 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<object, object> 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),因此如果 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<object, object> 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<string, string> 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 的数据加入,并且让过期字段为过去的时间,即表示此数据已过期

然后将数据库中对应的 name 由 “101茶餐厅” 改为 “103茶餐厅”

然后使用 jmeter 测试

测试结果:

可以看到部分结果返回的旧数据,而部分结果返回的是新数据

且 redis 中的数据也已经更新

并且,系统中只有一条查询数据库的请求

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。 

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com