最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

解決Redis緩存擊穿問題(互斥鎖、邏輯過期)

 更新時(shí)間:2026年02月06日 15:38:32   作者:小璐亂撞xllz  
本文介紹了背景緩存擊穿問題及兩種常見解決方案:互斥鎖與邏輯過期,互斥鎖通過獲取鎖來確保數(shù)據(jù)最新,但響應(yīng)速度慢,可能有死鎖風(fēng)險(xiǎn),邏輯過期利用過期字段判斷緩存是否過期,提高性能但不保證一致性,需要額外內(nèi)存,通過JMeter測試,兩種方案都能有效緩解緩存擊穿問題

背景

緩存擊穿也叫熱點(diǎn) Key 問題,就是一個(gè)被高并發(fā)訪問并且緩存重建業(yè)務(wù)較復(fù)雜的 key 突然失效了,無數(shù)的請(qǐng)求訪問會(huì)在瞬間給數(shù)據(jù)庫帶來巨大的沖擊

常見的解決方案有兩種:

  • 1.互斥鎖
  • 2.邏輯過期

互斥鎖:

本質(zhì)就是讓所有線程在緩存未命中時(shí),需要先獲取互斥鎖才能從數(shù)據(jù)庫查詢并重建緩存,而未獲取到互斥鎖的,需要不斷循環(huán)查詢緩存、未命中就嘗試獲取互斥鎖的過程。因此這種方式可以讓所有線程返回的數(shù)據(jù)都一定是最新的,但響應(yīng)速度不高

邏輯過期:

本質(zhì)就是讓熱點(diǎn) key 在 redis 中永不過期,而通過過期字段來自行判斷該 key 是否過期,如果未過期,則直接返回;如果過期,則需要獲取互斥鎖,并開啟新線程來重建緩存,而原線程可以直接返回舊數(shù)據(jù);如果獲取互斥鎖失敗,就代表已有其他線程正在執(zhí)行緩存重建工作,此時(shí)直接返回舊數(shù)據(jù)即可

兩者的對(duì)比:

解決方案優(yōu)點(diǎn)缺點(diǎn)
互斥鎖沒有額外的內(nèi)存消耗保證一致性實(shí)現(xiàn)簡單線程需要等待,性能受影響可能有死鎖風(fēng)險(xiǎn)
邏輯過期線程無需等待,性能較好不保證一致性有額外內(nèi)存消耗實(shí)現(xiàn)復(fù)雜

代碼實(shí)現(xiàn)

前置

這里以根據(jù) id 查詢商品店鋪為案例

實(shí)體類

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

    /**
     * 商鋪圖片,多個(gè)圖片以','隔開
     */
    private String images;

    /**
     * 商圈,例如陸家嘴
     */
    private String area;

    /**
     * 地址
     */
    private String address;

    /**
     * 經(jīng)度
     */
    private Double x;

    /**
     * 維度
     */
    private Double y;

    /**
     * 均價(jià),取整數(shù)
     */
    private Long avgPrice;

    /**
     * 銷量
     */
    private Integer sold;

    /**
     * 評(píng)論數(shù)量
     */
    private Integer comments;

    /**
     * 評(píng)分,1~5分,乘10保存,避免小數(shù)
     */
    private Integer score;

    /**
     * 營業(yè)時(shí)間,例如 10:00-22:00
     */
    private String openHours;

    /**
     * 創(chuàng)建時(shí)間
     */
    private LocalDateTime createTime;

    /**
     * 更新時(shí)間
     */
    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 {

    // 將對(duì)象轉(zhuǎn)為 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); // 設(shè)置為可訪問私有字段
            Object fieldValue = field.get(obj);
            if (fieldValue != null) {
                result.put(field.getName(), field.get(obj).toString());
            }
        }
        return result;
    }

    // 將 Map 轉(zhuǎn)為對(duì)象
    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); // 設(shè)置為可訪問私有字段
            String fieldValueStr = fieldValue.toString();
            // 根據(jù)字段類型進(jìn)行轉(zhuǎn)換
            fillField(obj, field, fieldValueStr);

        }
        return obj;
    }

    // 將 Map 轉(zhuǎn)為對(duì)象(含排除字段)
    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); // 設(shè)置為可訪問私有字段
            String fieldValueStr = fieldValue.toString();
            // 根據(jù)字段類型進(jìn)行轉(zhuǎn)換
            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));
        }
    }

}

結(jié)果返回類

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

    /**
     * 根據(jù)id查詢商鋪信息
     * @param id 商鋪id
     * @return 商鋪詳情數(shù)據(jù)
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return shopService.queryShopById(id);
    }
    
}

互斥鎖方案

流程圖為:

服務(wù)層代碼:

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) { // 獲取成功,進(jìn)行下一步
                break;
            }
            // 獲取失敗,睡眠后重試
            Thread.sleep(50);
        } while(true); //未獲取到鎖,休眠后重試
        // 查詢數(shù)據(jù)庫
        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 進(jìn)行測試

運(yùn)行結(jié)果如下:

可以看到控制臺(tái)只有一個(gè)查詢數(shù)據(jù)庫的請(qǐng)求,說明互斥鎖生效了

邏輯過期方案

流程圖如下:

采用邏輯過期的方式時(shí),key 是不會(huì)過期的,而這里由于是熱點(diǎn) key,我們默認(rèn)其是一定存在于 redis 中的(可以做緩存預(yù)熱事先加入 redis),因此如果 redis 沒命中,就直接返回空

服務(wù)層代碼:

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)) {
            // 獲取成功
            // 雙重檢驗(yàn)
            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;
            }
            // 通過線程池完成重建緩存任務(wù)
            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);
    // 添加邏輯過期時(shí)間
    map.put(RedisConstants.EXPIRE_KEY, LocalDateTime.now().plusMinutes(expireTime).toString());
    redisTemplate.opsForHash().putAll(RedisConstants.CACHE_SHOP_KEY + id, map);
}

測試:

這里先預(yù)熱,將 id 為 1 的數(shù)據(jù)加入,并且讓過期字段為過去的時(shí)間,即表示此數(shù)據(jù)已過期

然后將數(shù)據(jù)庫中對(duì)應(yīng)的 name 由 “101茶餐廳” 改為 “103茶餐廳”

然后使用 JMeter 測試

測試結(jié)果:

可以看到部分結(jié)果返回的舊數(shù)據(jù),而部分結(jié)果返回的是新數(shù)據(jù)

且 redis 中的數(shù)據(jù)也已經(jīng)更新

并且,系統(tǒng)中只有一條查詢數(shù)據(jù)庫的請(qǐng)求

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。 

相關(guān)文章

  • RedisTemplate中boundHashOps的使用小結(jié)

    RedisTemplate中boundHashOps的使用小結(jié)

    redisTemplate.boundHashOps(key)?是 RedisTemplate 類的一個(gè)方法,本文主要介紹了RedisTemplate中boundHashOps的使用小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-04-04
  • Redis?如何清空所有數(shù)據(jù)

    Redis?如何清空所有數(shù)據(jù)

    這篇文章主要介紹了Redis?如何清空所有數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • Redis之ZipList壓縮列表的使用

    Redis之ZipList壓縮列表的使用

    這篇文章主要介紹了Redis之ZipList壓縮列表的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-06-06
  • Redis和數(shù)據(jù)庫 數(shù)據(jù)同步問題的解決

    Redis和數(shù)據(jù)庫 數(shù)據(jù)同步問題的解決

    這篇文章主要介紹了Redis和數(shù)據(jù)庫 數(shù)據(jù)同步問題的解決操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2021-01-01
  • Redis中Hash從使用過程到原理說明

    Redis中Hash從使用過程到原理說明

    Redis Hash結(jié)構(gòu)用于存儲(chǔ)字段-值對(duì),適合對(duì)象數(shù)據(jù),支持HSET、HGET等命令,采用ziplist或hashtable編碼,通過漸進(jìn)式rehash優(yōu)化性能,適用于購物車、計(jì)數(shù)器等場景
    2025-09-09
  • Redis異地多活實(shí)現(xiàn)跨地域高可用的實(shí)踐

    Redis異地多活實(shí)現(xiàn)跨地域高可用的實(shí)踐

    本文主要介紹了Redis異地多活實(shí)現(xiàn)跨地域高可用的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2026-04-04
  • 詳解redis在服務(wù)器linux下啟動(dòng)的相關(guān)命令(安裝和配置)

    詳解redis在服務(wù)器linux下啟動(dòng)的相關(guān)命令(安裝和配置)

    這篇文章主要介紹了redis在服務(wù)器linux下的啟動(dòng)的相關(guān)命令(安裝和配置),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-08-08
  • redis限流的實(shí)際應(yīng)用

    redis限流的實(shí)際應(yīng)用

    這篇文章主要介紹了redis限流的實(shí)際應(yīng)用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • 聊聊使用RedisTemplat實(shí)現(xiàn)簡單的分布式鎖的問題

    聊聊使用RedisTemplat實(shí)現(xiàn)簡單的分布式鎖的問題

    這篇文章主要介紹了使用RedisTemplat實(shí)現(xiàn)簡單的分布式鎖問題,文中給大家介紹在SpringBootTest中編寫測試模塊的詳細(xì)代碼,需要的朋友可以參考下
    2021-11-11
  • macOS上Redis的安裝與測試操作

    macOS上Redis的安裝與測試操作

    這篇文章主要介紹了macOS上Redis的安裝與測試操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2020-07-07

最新評(píng)論

舒城县| 莱芜市| 原平市| 丰都县| 昌江| 沅陵县| 高雄县| 青阳县| 湖南省| 开江县| 南溪县| 垫江县| 江门市| 北流市| 镇沅| 乾安县| 呼和浩特市| 吴桥县| 兴和县| 新巴尔虎右旗| 张掖市| 志丹县| 大方县| 内乡县| 报价| 镇康县| 泸定县| 盐城市| 恭城| 井研县| 塘沽区| 汉寿县| 黑龙江省| 镇江市| 岳池县| 中西区| 海淀区| 铜山县| 安阳市| 重庆市| 霍林郭勒市|