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

如何解決Redis緩存穿透(緩存空對(duì)象、布隆過(guò)濾器)

 更新時(shí)間:2024年11月04日 11:43:19   作者:小璐亂撞xllz  
緩存穿透是一個(gè)常見(jiàn)的問(wèn)題,它發(fā)生當(dāng)請(qǐng)求的數(shù)據(jù)既不在緩存中也不在數(shù)據(jù)庫(kù)中,文章通過(guò)一個(gè)查詢商品店鋪的案例,展示了如何結(jié)合這兩種方法來(lái)避免緩存穿透,首先利用布隆過(guò)濾器過(guò)濾掉不存在的id,對(duì)于誤判的情況,則采用緩存空對(duì)象的策略進(jìn)行補(bǔ)救

背景

緩存穿透是指客戶端請(qǐng)求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫(kù)中都不存在,這樣緩存永遠(yuǎn)不會(huì)生效,這些請(qǐng)求都會(huì)打到數(shù)據(jù)庫(kù)

常見(jiàn)的解決方案有兩種,分別是緩存空對(duì)象布隆過(guò)濾器

1.緩存空對(duì)象

優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,維護(hù)方便

缺點(diǎn):額外的內(nèi)存消耗、可能造成短期的不一致

2.布隆過(guò)濾器

優(yōu)點(diǎn):內(nèi)存占用較少,沒(méi)有多余key

缺點(diǎn):實(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è)圖片以','隔開(kāi)
     */
    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íng)業(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 Long CACHE_NULL_TTL = 2L;
    public static final Long CACHE_SHOP_TTL = 30L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";
}

工具類

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 則跳過(guò)
            if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
                continue;
            }
            field.setAccessible(true); // 設(shè)置為可訪問(wèn)私有字段
            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è)置為可訪問(wèn)私有字段
            String fieldValueStr = fieldValue.toString();
            // 根據(jù)字段類型進(jìn)行轉(zhuǎn)換
            if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
                field.set(obj, Integer.parseInt(fieldValueStr));
            } else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
                field.set(obj, Boolean.parseBoolean(fieldValueStr));
            } else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
                field.set(obj, Double.parseDouble(fieldValueStr));
            } else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
                field.set(obj, Long.parseLong(fieldValueStr));
            } else if (field.getType().equals(String.class)) {
                field.set(obj, fieldValueStr);
            } else if(field.getType().equals(LocalDateTime.class)) {
                field.set(obj, LocalDateTime.parse(fieldValueStr));
            }
        }
        return obj;
    }
}

結(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);
    }
    /**
     * 新增商鋪信息
     * @param shop 商鋪數(shù)據(jù)
     * @return 商鋪id
     */
    @PostMapping
    public Result saveShop(@RequestBody Shop shop) {
        return shopService.saveShop(shop);
    }
    /**
     * 更新商鋪信息
     * @param shop 商鋪數(shù)據(jù)
     * @return 無(wú)
     */
    @PutMapping
    public Result updateShop(@RequestBody Shop shop) {
        return shopService.updateShop(shop);
    }
}

緩存空對(duì)象

流程圖為:

服務(wù)層代碼:

public Result queryShopById(Long id) {
    // 從 redis 查詢
    String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
    Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);
    // 緩存命中
    if(!entries.isEmpty()) {
        try {
            // 如果是空對(duì)象,表示一定不存在數(shù)據(jù)庫(kù)中,直接返回(解決緩存穿透)
            if(entries.containsKey("")) {
                return Result.fail("店鋪不存在");
            }
            // 刷新有效期
            redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);
            return Result.ok(shop);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // 查詢數(shù)據(jù)庫(kù)
    Shop shop = this.getById(id);
    if(shop == null) {
        // 存入空值
        redisTemplate.opsForHash().put(shopKey, "", "");
        redisTemplate.expire(shopKey, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
        // 不存在,直接返回
        return Result.fail("店鋪不存在");
    }
    // 存在,寫(xiě)入 redis
    try {
        redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));
        redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
    return Result.ok(shop);
}

布隆過(guò)濾器

這里選擇使用布隆過(guò)濾器存儲(chǔ)存在于數(shù)據(jù)庫(kù)中的 id,原因在于,如果存儲(chǔ)了不存在于數(shù)據(jù)庫(kù)中的 id,首先由于 id 的取值范圍很大,那么不存在的 id 有很多,因此更占用空間;其次,由于布隆過(guò)濾器有一定的誤判率,那么可能導(dǎo)致少數(shù)原本存在于數(shù)據(jù)庫(kù)中的 id 被判為了不存在,然后直接返回了,此時(shí)就會(huì)出現(xiàn)根本性的正確性錯(cuò)誤。相反,如果存儲(chǔ)的是數(shù)據(jù)庫(kù)中存在的 id,那么即使少數(shù)不存在的 id 被判為了存在,由于數(shù)據(jù)庫(kù)中確實(shí)沒(méi)有對(duì)應(yīng)的 id,那么也會(huì)返回空,最終結(jié)果還是正確的

這里使用 guava 依賴的布隆過(guò)濾器

依賴為:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>

封裝了布隆過(guò)濾器的類(注意初始化時(shí)要把數(shù)據(jù)庫(kù)中已有的 id 加入布隆過(guò)濾器):

public class ShopBloomFilter {
    private BloomFilter<Long> bloomFilter;
    public ShopBloomFilter(ShopMapper shopMapper) {
        // 初始化布隆過(guò)濾器,設(shè)計(jì)預(yù)計(jì)元素?cái)?shù)量為100_0000L,誤差率為1%
        bloomFilter = BloomFilter.create(Funnels.longFunnel(), 100_0000, 0.01);
        // 將數(shù)據(jù)庫(kù)中已有的店鋪 id 加入布隆過(guò)濾器
        List<Shop> shops = shopMapper.selectList(null);
        for (Shop shop : shops) {
            bloomFilter.put(shop.getId());
        }
    }
    public void add(long id) {
        bloomFilter.put(id);
    }
    public boolean mightContain(long id){
        return bloomFilter.mightContain(id);
    }
}

對(duì)應(yīng)的配置類(將其設(shè)置為 bean)

@Configuration
public class BloomConfig {
    @Bean
    public ShopBloomFilter shopBloomFilter(ShopMapper shopMapper) {
        return new ShopBloomFilter(shopMapper);
    }
}

首先要修改查詢方法,在根據(jù) id 查詢時(shí),如果對(duì)應(yīng) id 不在布隆過(guò)濾器中,則直接返回。然后還要修改保存方法,在保存的時(shí)候還需要將對(duì)應(yīng)的 id 加入布隆過(guò)濾器中

@Override
public Result queryShopById(Long id) {
    // 如果不在布隆過(guò)濾器中,直接返回
    if(!shopBloomFilter.mightContain(id)) {
        return Result.fail("店鋪不存在");
    }
    // 從 redis 查詢
    String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
    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 Result.ok(shop);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // 查詢數(shù)據(jù)庫(kù)
    Shop shop = this.getById(id);
    if(shop == null) {
        // 不存在,直接返回
        return Result.fail("店鋪不存在");
    }
    // 存在,寫(xiě)入 redis
    try {
        redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));
        redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
    return Result.ok(shop);
}
@Override
public Result saveShop(Shop shop) {
    // 寫(xiě)入數(shù)據(jù)庫(kù)
    this.save(shop);
    // 將 id 寫(xiě)入布隆過(guò)濾器
    shopBloomFilter.add(shop.getId());
    // 返回店鋪 id
    return Result.ok(shop.getId());
}

結(jié)合兩種方法

由于布隆過(guò)濾器有一定的誤判率,所以這里可以進(jìn)一步優(yōu)化,如果出現(xiàn)誤判情況,即原本不存在于數(shù)據(jù)庫(kù)中的 id 被判為了存在,就用緩存空對(duì)象的方式將其緩存到 redis 中

@Override
public Result queryShopById(Long id) {
    // 如果不在布隆過(guò)濾器中,直接返回
    if(!shopBloomFilter.mightContain(id)) {
        return Result.fail("店鋪不存在");
    }
    // 從 redis 查詢
    String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
    Map<Object, Object> entries = redisTemplate.opsForHash().entries(shopKey);
    // 緩存命中
    if(!entries.isEmpty()) {
        try {
            // 如果是空對(duì)象,表示一定不存在數(shù)據(jù)庫(kù)中,直接返回(解決緩存穿透)
            if(entries.containsKey("")) {
                return Result.fail("店鋪不存在");
            }
            // 刷新有效期
            redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            Shop shop = (Shop) ObjectMapUtils.map2Obj(entries, Shop.class);
            return Result.ok(shop);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // 查詢數(shù)據(jù)庫(kù)
    Shop shop = this.getById(id);
    if(shop == null) {
        // 存入空值
        redisTemplate.opsForHash().put(shopKey, "", "");
        redisTemplate.expire(shopKey, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
        // 不存在,直接返回
        return Result.fail("店鋪不存在");
    }
    // 存在,寫(xiě)入 redis
    try {
        redisTemplate.opsForHash().putAll(shopKey, ObjectMapUtils.obj2Map(shop));
        redisTemplate.expire(shopKey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
    return Result.ok(shop);
}

到此這篇關(guān)于解決Redis緩存穿透(緩存空對(duì)象、布隆過(guò)濾器)的文章就介紹到這了,更多相關(guān)Redis緩存穿透內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Redis常見(jiàn)數(shù)據(jù)類型List列表使用詳解

    Redis常見(jiàn)數(shù)據(jù)類型List列表使用詳解

    Redis的List是一種有序的字符串集合,支持兩端高效插入和刪除,適用于隊(duì)列和棧,這篇文章主要介紹了Redis常見(jiàn)數(shù)據(jù)類型List列表使用的相關(guān)資料,需要的朋友可以參考下
    2024-12-12
  • 分布式使用Redis實(shí)現(xiàn)數(shù)據(jù)庫(kù)對(duì)象自增主鍵ID

    分布式使用Redis實(shí)現(xiàn)數(shù)據(jù)庫(kù)對(duì)象自增主鍵ID

    本文介紹在分布式項(xiàng)目中使用Redis生成對(duì)象的自增主鍵ID,通過(guò)Redis的INCR等命令實(shí)現(xiàn)計(jì)數(shù)器功能,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-12-12
  • Redis模糊查詢的幾種實(shí)現(xiàn)方法

    Redis模糊查詢的幾種實(shí)現(xiàn)方法

    本文主要介紹了Redis模糊查詢的幾種實(shí)現(xiàn)方法,包括兩種方法KEYS , SCAN,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-02-02
  • redis+lua實(shí)現(xiàn)限流的項(xiàng)目實(shí)踐

    redis+lua實(shí)現(xiàn)限流的項(xiàng)目實(shí)踐

    redis有很多限流的算法(比如:令牌桶,計(jì)數(shù)器,時(shí)間窗口)等,在分布式里面進(jìn)行限流的話,我們則可以使用redis+lua腳本進(jìn)行限流,下面就來(lái)介紹一下redis+lua實(shí)現(xiàn)限流
    2023-10-10
  • redis由于目標(biāo)計(jì)算機(jī)積極拒絕,無(wú)法連接的解決

    redis由于目標(biāo)計(jì)算機(jī)積極拒絕,無(wú)法連接的解決

    這篇文章主要介紹了redis由于目標(biāo)計(jì)算機(jī)積極拒絕,無(wú)法連接的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • Redis實(shí)現(xiàn)自動(dòng)清理過(guò)期鍵值對(duì)的代碼示例

    Redis實(shí)現(xiàn)自動(dòng)清理過(guò)期鍵值對(duì)的代碼示例

    在這個(gè)數(shù)據(jù)爆炸的時(shí)代,內(nèi)存就像珍貴的土地資源,而Redis則是這片土地上的智能管家,它不僅能高效存儲(chǔ)數(shù)據(jù),還能像秋葉定時(shí)凋零般,讓鍵值對(duì)在指定時(shí)間自動(dòng)消失,本文小編給大家介紹了Redis實(shí)現(xiàn)自動(dòng)清理過(guò)期鍵值對(duì)實(shí)戰(zhàn),需要的朋友可以參考下
    2025-06-06
  • 深入理解Redis7哨兵模式(保姆級(jí)教學(xué))

    深入理解Redis7哨兵模式(保姆級(jí)教學(xué))

    Redis的主從復(fù)制存在一定的缺陷,為了解決這一問(wèn)題,Redis官方推薦一種高可用方案哨兵模式,本文主要介紹了深入理解Redis7哨兵模式,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-01-01
  • redis過(guò)期回調(diào)功能實(shí)現(xiàn)示例

    redis過(guò)期回調(diào)功能實(shí)現(xiàn)示例

    Redis提供了一種過(guò)期回調(diào)的機(jī)制,可以在某個(gè)鍵過(guò)期時(shí)觸發(fā)一個(gè)回調(diào)函數(shù),本文就來(lái)介紹一下redis過(guò)期回調(diào)功能實(shí)現(xiàn)示例,感興趣的可以了解一下
    2023-09-09
  • Redis分布式鎖存在的問(wèn)題(推薦)

    Redis分布式鎖存在的問(wèn)題(推薦)

    有很多基于Redis實(shí)現(xiàn)的分布式鎖方案或者庫(kù),但是有些庫(kù)并沒(méi)有解決分布式環(huán)境下的一些問(wèn)題陷阱,這篇文章主要介紹了Redis分布式鎖存在的問(wèn)題,需要的朋友可以參考下
    2022-12-12
  • Redis中的zset類型詳解

    Redis中的zset類型詳解

    有序集合zset保留了set集合不能有重復(fù)成員的特點(diǎn),但與set集合不同的是,zset的每個(gè)member都有一個(gè)唯一的浮點(diǎn)數(shù)類型的分?jǐn)?shù)score與之關(guān)聯(lián),這篇文章主要介紹了Redis的zset類型,需要的朋友可以參考下
    2023-08-08

最新評(píng)論

正镶白旗| 蒙阴县| 类乌齐县| 噶尔县| 高雄县| 民和| 松溪县| 恩平市| 兴安县| 甘孜县| 永兴县| 湄潭县| 赞皇县| 周至县| 漾濞| 襄汾县| 六盘水市| 广昌县| 福海县| 辽宁省| 乌拉特后旗| 保靖县| 衡水市| 东阳市| 岐山县| 赤壁市| 汝阳县| 柘城县| 武平县| 武清区| 固原市| 赫章县| 通化市| 巴楚县| 阿拉善左旗| 浪卡子县| 连江县| 家居| 夏河县| 焦作市| 鸡泽县|