如何解決Redis緩存穿透(緩存空對(duì)象、布隆過濾器)
背景
緩存穿透是指客戶端請(qǐng)求的數(shù)據(jù)在緩存中和數(shù)據(jù)庫中都不存在,這樣緩存永遠(yuǎn)不會(huì)生效,這些請(qǐng)求都會(huì)打到數(shù)據(jù)庫
常見的解決方案有兩種,分別是緩存空對(duì)象和布隆過濾器
1.緩存空對(duì)象
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,維護(hù)方便
缺點(diǎn):額外的內(nèi)存消耗、可能造成短期的不一致
2.布隆過濾器
優(yōu)點(diǎn):內(nè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è)圖片以','隔開 */ 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 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 則跳過 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)換 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 無 */ @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ù)庫中,直接返回(解決緩存穿透) 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ù)庫 Shop shop = this.getById(id); if(shop == null) { // 存入空值 redisTemplate.opsForHash().put(shopKey, "", ""); redisTemplate.expire(shopKey, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); // 不存在,直接返回 return Result.fail("店鋪不存在"); } // 存在,寫入 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); }
布隆過濾器
這里選擇使用布隆過濾器存儲(chǔ)存在于數(shù)據(jù)庫中的 id,原因在于,如果存儲(chǔ)了不存在于數(shù)據(jù)庫中的 id,首先由于 id 的取值范圍很大,那么不存在的 id 有很多,因此更占用空間;其次,由于布隆過濾器有一定的誤判率,那么可能導(dǎo)致少數(shù)原本存在于數(shù)據(jù)庫中的 id 被判為了不存在,然后直接返回了,此時(shí)就會(huì)出現(xiàn)根本性的正確性錯(cuò)誤。相反,如果存儲(chǔ)的是數(shù)據(jù)庫中存在的 id,那么即使少數(shù)不存在的 id 被判為了存在,由于數(shù)據(jù)庫中確實(shí)沒有對(duì)應(yīng)的 id,那么也會(huì)返回空,最終結(jié)果還是正確的
這里使用 guava 依賴的布隆過濾器
依賴為:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1.1-jre</version> </dependency>
封裝了布隆過濾器的類(注意初始化時(shí)要把數(shù)據(jù)庫中已有的 id 加入布隆過濾器):
public class ShopBloomFilter { private BloomFilter<Long> bloomFilter; public ShopBloomFilter(ShopMapper shopMapper) { // 初始化布隆過濾器,設(shè)計(jì)預(yù)計(jì)元素?cái)?shù)量為100_0000L,誤差率為1% bloomFilter = BloomFilter.create(Funnels.longFunnel(), 100_0000, 0.01); // 將數(shù)據(jù)庫中已有的店鋪 id 加入布隆過濾器 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 不在布隆過濾器中,則直接返回。然后還要修改保存方法,在保存的時(shí)候還需要將對(duì)應(yīng)的 id 加入布隆過濾器中
@Override public Result queryShopById(Long id) { // 如果不在布隆過濾器中,直接返回 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ù)庫 Shop shop = this.getById(id); if(shop == null) { // 不存在,直接返回 return Result.fail("店鋪不存在"); } // 存在,寫入 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) { // 寫入數(shù)據(jù)庫 this.save(shop); // 將 id 寫入布隆過濾器 shopBloomFilter.add(shop.getId()); // 返回店鋪 id return Result.ok(shop.getId()); }
結(jié)合兩種方法
由于布隆過濾器有一定的誤判率,所以這里可以進(jìn)一步優(yōu)化,如果出現(xiàn)誤判情況,即原本不存在于數(shù)據(jù)庫中的 id 被判為了存在,就用緩存空對(duì)象的方式將其緩存到 redis 中
@Override public Result queryShopById(Long id) { // 如果不在布隆過濾器中,直接返回 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ù)庫中,直接返回(解決緩存穿透) 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ù)庫 Shop shop = this.getById(id); if(shop == null) { // 存入空值 redisTemplate.opsForHash().put(shopKey, "", ""); redisTemplate.expire(shopKey, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); // 不存在,直接返回 return Result.fail("店鋪不存在"); } // 存在,寫入 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ān)Redis緩存穿透內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis.clients.jedis.exceptions.JedisBusyException無法處理異常的解決方法
redis.clients.jedis.exceptions.JedisBusyException異常通常不是 Jedis客戶端直接拋出的標(biāo)準(zhǔn)異常,本文就來介紹一下異常的解決方法,感興趣的可以了解一下2024-05-05Redis中Bloom filter布隆過濾器的學(xué)習(xí)
布隆過濾器是一個(gè)非常長(zhǎng)的二進(jìn)制向量和一系列隨機(jī)哈希函數(shù)的組合,可用于檢索一個(gè)元素是否存在,本文就詳細(xì)的介紹一下Bloom filter布隆過濾器,具有一定的參考價(jià)值,感興趣的可以了解一下2022-12-12Redis 的過期策略與鍵的過期時(shí)間設(shè)置方法
Redis通過惰性刪除和定期刪除策略管理內(nèi)存,提供多種命令設(shè)置鍵的過期時(shí)間,并通過過期字典高效處理過期鍵,合理設(shè)置過期時(shí)間、監(jiān)控過期鍵數(shù)量和避免大量鍵同時(shí)過期是最佳實(shí)踐,本文介紹Redis 的過期策略與鍵的過期時(shí)間設(shè)置,感興趣的朋友一起看看吧2025-03-03redis搭建哨兵模式實(shí)現(xiàn)一主兩從三哨兵
本文主要介紹了redis搭建哨兵模式實(shí)現(xiàn)一主兩從三哨兵,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08NestJS+Redis實(shí)現(xiàn)緩存步驟詳解
這篇文章主要介紹了NestJS+Redis實(shí)現(xiàn)緩存,本文分步驟給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08在CentOS 7環(huán)境下安裝Redis數(shù)據(jù)庫詳解
Redis是一個(gè)開源的、基于BSD許可證的,基于內(nèi)存的、鍵值存儲(chǔ)NoSQL數(shù)據(jù)本篇文章主要介紹了在CentOS 7環(huán)境下安裝Redis數(shù)據(jù)庫詳解,有興趣的可以了解一下。2016-11-11從MySQL到Redis的簡(jiǎn)單數(shù)據(jù)庫遷移方法
這篇文章主要介紹了從MySQL到Redis的簡(jiǎn)單數(shù)據(jù)庫遷移方法,注意Redis數(shù)據(jù)庫基于內(nèi)存,并不能代替?zhèn)鹘y(tǒng)數(shù)據(jù)庫,需要的朋友可以參考下2015-06-06