Redis預防緩存穿透的6種策略
在高并發(fā)系統(tǒng)中,Redis作為緩存中間件已成為標配,它能有效減輕數(shù)據(jù)庫壓力、提升系統(tǒng)響應速度。然而,緩存并非萬能,在實際應用中我們常常面臨一個嚴峻問題——緩存穿透。
這種現(xiàn)象可能導致Redis失效,使大量請求直接沖擊數(shù)據(jù)庫,造成系統(tǒng)性能急劇下降甚至宕機。
緩存穿透原理分析
什么是緩存穿透
緩存穿透是指查詢一個根本不存在的數(shù)據(jù),由于緩存不命中,請求會穿透緩存層直接訪問數(shù)據(jù)庫。這種情況下,數(shù)據(jù)庫也無法查詢到對應數(shù)據(jù),因此無法將結(jié)果寫入緩存,導致每次同類請求都會重復訪問數(shù)據(jù)庫。
典型場景與危害
Client ---> Redis(未命中) ---> Database(查詢無果) ---> 不更新緩存 ---> 循環(huán)重復
緩存穿透的主要危害:
- 數(shù)據(jù)庫壓力激增:大量無效查詢直接落到數(shù)據(jù)庫
- 系統(tǒng)響應變慢:數(shù)據(jù)庫負載過高導致整體性能下降
- 資源浪費:無謂的查詢消耗CPU和IO資源
- 安全風險:可能被惡意利用作為拒絕服務攻擊的手段
緩存穿透通常有兩種情況:
- 正常業(yè)務查詢:查詢的數(shù)據(jù)確實不存在
- 惡意攻擊:故意構(gòu)造不存在的key進行大量請求
下面介紹六種有效的防范策略。
策略一:空值緩存
原理
空值緩存是最簡單直接的防穿透策略。當數(shù)據(jù)庫查詢不到某個key對應的值時,我們?nèi)匀粚⑦@個"空結(jié)果"緩存起來(通常以null值或特定標記表示),并設(shè)置一個相對較短的過期時間。這樣,下次請求同一個不存在的key時,可以直接從緩存返回"空結(jié)果",避免再次查詢數(shù)據(jù)庫。
實現(xiàn)示例
@Service public class UserServiceImpl implements UserService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private UserMapper userMapper; private static final String KEY_PREFIX = "user:"; private static final String EMPTY_VALUE = "{}"; // 空值標記 private static final long EMPTY_VALUE_EXPIRE_SECONDS = 300; // 空值過期時間 private static final long NORMAL_EXPIRE_SECONDS = 3600; // 正常值過期時間 @Override public User getUserById(Long userId) { String redisKey = KEY_PREFIX + userId; // 1. 查詢緩存 String userJson = redisTemplate.opsForValue().get(redisKey); // 2. 緩存命中 if (userJson != null) { // 判斷是否為空值 if (EMPTY_VALUE.equals(userJson)) { return null; // 返回空結(jié)果 } // 正常緩存,反序列化并返回 return JSON.parseObject(userJson, User.class); } // 3. 緩存未命中,查詢數(shù)據(jù)庫 User user = userMapper.selectById(userId); // 4. 寫入緩存 if (user != null) { // 數(shù)據(jù)庫查到數(shù)據(jù),寫入正常緩存 redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(user), NORMAL_EXPIRE_SECONDS, TimeUnit.SECONDS); } else { // 數(shù)據(jù)庫未查到數(shù)據(jù),寫入空值緩存 redisTemplate.opsForValue().set(redisKey, EMPTY_VALUE, EMPTY_VALUE_EXPIRE_SECONDS, TimeUnit.SECONDS); } return user; } }
優(yōu)缺點分析
優(yōu)點
- 實現(xiàn)簡單,無需額外組件
- 對系統(tǒng)侵入性低
- 立竿見影的效果
缺點
- 可能會占用較多的緩存空間
- 如果空值較多,可能導致緩存效率下降
- 無法應對大規(guī)模的惡意攻擊
- 短期內(nèi)可能造成數(shù)據(jù)不一致(新增數(shù)據(jù)后緩存依然返回空值)
策略二:布隆過濾器
原理
布隆過濾器(Bloom Filter)是一種空間效率很高的概率型數(shù)據(jù)結(jié)構(gòu),用于檢測一個元素是否屬于一個集合。它的特點是存在誤判,即可能會將不存在的元素誤判為存在(false positive),但不會將存在的元素誤判為不存在(false negative)。
布隆過濾器包含一個很長的二進制向量和一系列哈希函數(shù)。當插入一個元素時,使用各個哈希函數(shù)計算該元素的哈希值,并將二進制向量中相應位置置為1。查詢時,同樣計算哈希值并檢查向量中對應位置,如果有任一位為0,則元素必定不存在;如果全部位都為1,則元素可能存在。
實現(xiàn)示例
使用Redis的布隆過濾器模塊(Redis 4.0+支持模塊擴展,需安裝RedisBloom):
@Service public class ProductServiceWithBloomFilter implements ProductService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private ProductMapper productMapper; private static final String BLOOM_FILTER_NAME = "product_filter"; private static final String CACHE_KEY_PREFIX = "product:"; private static final long CACHE_EXPIRE_SECONDS = 3600; // 初始化布隆過濾器,可在應用啟動時執(zhí)行 @PostConstruct public void initBloomFilter() { // 判斷布隆過濾器是否存在 Boolean exists = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.exists(BLOOM_FILTER_NAME.getBytes())); if (Boolean.FALSE.equals(exists)) { // 創(chuàng)建布隆過濾器,預計元素量為100萬,錯誤率為0.01 redisTemplate.execute((RedisCallback<Object>) connection -> connection.execute("BF.RESERVE", BLOOM_FILTER_NAME.getBytes(), "0.01".getBytes(), "1000000".getBytes())); // 加載所有商品ID到布隆過濾器 List<Long> allProductIds = productMapper.getAllProductIds(); for (Long id : allProductIds) { redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.execute("BF.ADD", BLOOM_FILTER_NAME.getBytes(), id.toString().getBytes()) != 0); } } } @Override public Product getProductById(Long productId) { String cacheKey = CACHE_KEY_PREFIX + productId; // 1. 使用布隆過濾器檢查ID是否存在 Boolean mayExist = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.execute("BF.EXISTS", BLOOM_FILTER_NAME.getBytes(), productId.toString().getBytes()) != 0); // 如果布隆過濾器判斷不存在,則直接返回 if (Boolean.FALSE.equals(mayExist)) { return null; } // 2. 查詢緩存 String productJson = redisTemplate.opsForValue().get(cacheKey); if (productJson != null) { return JSON.parseObject(productJson, Product.class); } // 3. 查詢數(shù)據(jù)庫 Product product = productMapper.selectById(productId); // 4. 更新緩存 if (product != null) { redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS); } else { // 布隆過濾器誤判,數(shù)據(jù)庫中不存在該商品 // 可以考慮記錄這類誤判情況,優(yōu)化布隆過濾器參數(shù) log.warn("Bloom filter false positive for productId: {}", productId); } return product; } // 當新增商品時,需要將ID添加到布隆過濾器 public void addProductToBloomFilter(Long productId) { redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.execute("BF.ADD", BLOOM_FILTER_NAME.getBytes(), productId.toString().getBytes()) != 0); } }
優(yōu)缺點分析
優(yōu)點
- 空間效率高,內(nèi)存占用小
- 查詢速度快,時間復雜度O(k),k為哈希函數(shù)個數(shù)
- 可以有效過濾大部分不存在的ID查詢
- 可以與其他策略組合使用
缺點
- 存在誤判可能(false positive)
- 無法從布隆過濾器中刪除元素(標準實現(xiàn))
- 需要預先加載所有數(shù)據(jù)ID,不適合動態(tài)變化頻繁的場景
- 實現(xiàn)相對復雜,需要額外維護布隆過濾器
- 可能需要定期重建以適應數(shù)據(jù)變化
策略三:請求參數(shù)校驗
原理
請求參數(shù)校驗是一種在業(yè)務層面防止緩存穿透的手段。通過對請求參數(shù)進行合法性校驗,過濾掉明顯不合理的請求,避免這些請求到達緩存和數(shù)據(jù)庫層。這種方法特別適合防范惡意攻擊。
實現(xiàn)示例
@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @GetMapping("/{userId}") public ResponseEntity<?> getUserById(@PathVariable String userId) { // 1. 基本格式校驗 if (!userId.matches("\d+")) { return ResponseEntity.badRequest().body("UserId must be numeric"); } // 2. 基本邏輯校驗 long id = Long.parseLong(userId); if (id <= 0 || id > 100000000) { // 假設(shè)ID范圍限制 return ResponseEntity.badRequest().body("UserId out of valid range"); } // 3. 調(diào)用業(yè)務服務 User user = userService.getUserById(id); if (user == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(user); } }
在服務層也可以增加參數(shù)檢驗:
@Service public class UserServiceImpl implements UserService { // 白名單,只允許查詢這些ID前綴(舉例) private static final Set<String> ID_PREFIXES = Set.of("100", "200", "300"); @Override public User getUserById(Long userId) { // 更復雜的業(yè)務規(guī)則校驗 String idStr = userId.toString(); boolean valid = false; for (String prefix : ID_PREFIXES) { if (idStr.startsWith(prefix)) { valid = true; break; } } if (!valid) { log.warn("Attempt to access invalid user ID pattern: {}", userId); return null; } // 正常業(yè)務邏輯... return getUserFromCacheOrDb(userId); } }
優(yōu)缺點分析
優(yōu)點
- 實現(xiàn)簡單,無需額外組件
- 能在請求早期攔截明顯不合理的訪問
- 可以結(jié)合業(yè)務規(guī)則進行精細化控制
- 減輕系統(tǒng)整體負擔
缺點
- 無法覆蓋所有非法請求場景
- 需要對業(yè)務非常了解,才能設(shè)計合理的校驗規(guī)則
- 可能引入復雜的業(yè)務邏輯
- 校驗過于嚴格可能影響正常用戶體驗
策略四:接口限流與熔斷
原理
限流是控制系統(tǒng)訪問頻率的有效手段,可以防止突發(fā)流量對系統(tǒng)造成沖擊。熔斷則是在系統(tǒng)負載過高時,暫時拒絕部分請求以保護系統(tǒng)。這兩種機制結(jié)合使用,可以有效防范緩存穿透帶來的系統(tǒng)風險。
實現(xiàn)示例
使用SpringBoot+Resilience4j實現(xiàn)限流和熔斷:
@Configuration public class ResilienceConfig { @Bean public RateLimiterRegistry rateLimiterRegistry() { RateLimiterConfig config = RateLimiterConfig.custom() .limitRefreshPeriod(Duration.ofSeconds(1)) .limitForPeriod(100) // 每秒允許100個請求 .timeoutDuration(Duration.ofMillis(25)) .build(); return RateLimiterRegistry.of(config); } @Bean public CircuitBreakerRegistry circuitBreakerRegistry() { CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(50) // 50%失敗率觸發(fā)熔斷 .slidingWindowSize(100) // 基于最近100次調(diào)用 .minimumNumberOfCalls(10) // 至少10次調(diào)用才會觸發(fā)熔斷 .waitDurationInOpenState(Duration.ofSeconds(10)) // 熔斷后等待時間 .build(); return CircuitBreakerRegistry.of(config); } } @Service public class ProductServiceWithResilience { private final ProductMapper productMapper; private final StringRedisTemplate redisTemplate; private final RateLimiter rateLimiter; private final CircuitBreaker circuitBreaker; public ProductServiceWithResilience( ProductMapper productMapper, StringRedisTemplate redisTemplate, RateLimiterRegistry rateLimiterRegistry, CircuitBreakerRegistry circuitBreakerRegistry) { this.productMapper = productMapper; this.redisTemplate = redisTemplate; this.rateLimiter = rateLimiterRegistry.rateLimiter("productService"); this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("productService"); } public Product getProductById(Long productId) { // 1. 應用限流器 return rateLimiter.executeSupplier(() -> { // 2. 應用熔斷器 return circuitBreaker.executeSupplier(() -> { return doGetProduct(productId); }); }); } private Product doGetProduct(Long productId) { String cacheKey = "product:" + productId; // 查詢緩存 String productJson = redisTemplate.opsForValue().get(cacheKey); if (productJson != null) { return JSON.parseObject(productJson, Product.class); } // 查詢數(shù)據(jù)庫 Product product = productMapper.selectById(productId); // 更新緩存 if (product != null) { redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product), 1, TimeUnit.HOURS); } else { // 空值緩存,短期有效 redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES); } return product; } // 熔斷后的降級方法 private Product fallbackMethod(Long productId, Throwable t) { log.warn("Circuit breaker triggered for productId: {}", productId, t); // 返回默認商品或者從本地緩存獲取 return new Product(productId, "Temporary Unavailable", 0.0); } }
優(yōu)缺點分析
優(yōu)點
- 提供系統(tǒng)級別的保護
- 能有效應對突發(fā)流量和惡意攻擊
- 保障系統(tǒng)穩(wěn)定性和可用性
- 可以結(jié)合監(jiān)控系統(tǒng)進行動態(tài)調(diào)整
缺點
- 可能影響正常用戶體驗
- 配置調(diào)優(yōu)有一定難度
- 需要完善的降級策略
- 無法徹底解決緩存穿透問題,只是減輕其影響
策略五:緩存預熱
原理
緩存預熱是指在系統(tǒng)啟動或特定時間點,提前將可能被查詢的數(shù)據(jù)加載到緩存中,避免用戶請求時因緩存不命中而導致的數(shù)據(jù)庫訪問。對于緩存穿透問題,預熱可以提前將有效數(shù)據(jù)的空間占滿,減少直接查詢數(shù)據(jù)庫的可能性。
實現(xiàn)示例
@Component public class CacheWarmUpTask { @Autowired private ProductMapper productMapper; @Autowired private StringRedisTemplate redisTemplate; @Autowired private RedisBloomFilter bloomFilter; // 系統(tǒng)啟動時執(zhí)行緩存預熱 @PostConstruct public void warmUpCacheOnStartup() { // 異步執(zhí)行預熱任務,避免阻塞應用啟動 CompletableFuture.runAsync(this::warmUpHotProducts); } // 每天凌晨2點刷新熱門商品緩存 @Scheduled(cron = "0 0 2 * * ?") public void scheduledWarmUp() { warmUpHotProducts(); } private void warmUpHotProducts() { log.info("開始預熱商品緩存..."); long startTime = System.currentTimeMillis(); try { // 1. 獲取熱門商品列表(例如銷量TOP5000) List<Product> hotProducts = productMapper.findHotProducts(5000); // 2. 更新緩存和布隆過濾器 for (Product product : hotProducts) { String cacheKey = "product:" + product.getId(); redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(product), 6, TimeUnit.HOURS ); // 更新布隆過濾器 bloomFilter.add("product_filter", product.getId().toString()); } // 3. 同時預熱一些必要的聚合信息 List<Category> categories = productMapper.findAllCategories(); for (Category category : categories) { String cacheKey = "category:" + category.getId(); List<Long> productIds = productMapper.findProductIdsByCategory(category.getId()); redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(productIds), 12, TimeUnit.HOURS ); } long duration = System.currentTimeMillis() - startTime; log.info("緩存預熱完成,耗時:{}ms,預熱商品數(shù)量:{}", duration, hotProducts.size()); } catch (Exception e) { log.error("緩存預熱失敗", e); } } }
優(yōu)缺點分析
優(yōu)點
- 提高系統(tǒng)啟動后的訪問性能
- 減少緩存冷啟動問題
- 可以定時刷新,保持數(shù)據(jù)鮮度
- 避免用戶等待
缺點
- 無法覆蓋所有可能的數(shù)據(jù)訪問
- 占用額外的系統(tǒng)資源
- 對冷門數(shù)據(jù)無效
- 需要合理選擇預熱數(shù)據(jù)范圍,避免資源浪費
策略六:分級過濾策略
原理
分級過濾策略是將多種防穿透措施組合使用,形成多層防護網(wǎng)。通過在不同層次設(shè)置過濾條件,既能保證系統(tǒng)性能,又能最大限度地防止緩存穿透。一個典型的分級過濾策略包括:前端過濾 -> API網(wǎng)關(guān)過濾 -> 應用層過濾 -> 緩存層過濾 -> 數(shù)據(jù)庫保護。
實現(xiàn)示例
以下是一個多層防護的綜合示例:
// 1. 網(wǎng)關(guān)層過濾(使用Spring Cloud Gateway) @Configuration public class GatewayFilterConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("product_route", r -> r.path("/api/product/**") // 路徑格式驗證 .and().predicate(exchange -> { String path = exchange.getRequest().getURI().getPath(); // 檢查product/{id}路徑,確保id為數(shù)字 if (path.matches("/api/product/\d+")) { String id = path.substring(path.lastIndexOf('/') + 1); long productId = Long.parseLong(id); return productId > 0 && productId < 10000000; // 合理范圍檢查 } return true; }) // 限流過濾 .filters(f -> f.requestRateLimiter() .rateLimiter(RedisRateLimiter.class, c -> c.setReplenishRate(10).setBurstCapacity(20)) .and() .circuitBreaker(c -> c.setName("productCB").setFallbackUri("forward:/fallback")) ) .uri("lb://product-service") ) .build(); } } // 2. 應用層過濾(Resilience4j + Bloom Filter) @Service public class ProductServiceImpl implements ProductService { private final StringRedisTemplate redisTemplate; private final ProductMapper productMapper; private final BloomFilter<String> localBloomFilter; private final RateLimiter rateLimiter; private final CircuitBreaker circuitBreaker; @Value("${cache.product.expire-seconds:3600}") private int cacheExpireSeconds; // 構(gòu)造函數(shù)注入... @PostConstruct public void initLocalFilter() { // 創(chuàng)建本地布隆過濾器作為二級保護 localBloomFilter = BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), 1000000, // 預期元素數(shù)量 0.001 // 誤判率 ); // 初始化本地布隆過濾器數(shù)據(jù) List<String> allProductIds = productMapper.getAllProductIdsAsString(); for (String id : allProductIds) { localBloomFilter.put(id); } } @Override public Product getProductById(Long productId) { String productIdStr = productId.toString(); // 1. 本地布隆過濾器預檢 if (!localBloomFilter.mightContain(productIdStr)) { log.info("Product filtered by local bloom filter: {}", productId); return null; } // 2. Redis布隆過濾器二次檢查 Boolean mayExist = redisTemplate.execute( (RedisCallback<Boolean>) connection -> connection.execute( "BF.EXISTS", "product_filter".getBytes(), productIdStr.getBytes() ) != 0 ); if (Boolean.FALSE.equals(mayExist)) { log.info("Product filtered by Redis bloom filter: {}", productId); return null; } // 3. 應用限流和熔斷保護 try { return rateLimiter.executeSupplier(() -> circuitBreaker.executeSupplier(() -> { return getProductFromCacheOrDb(productId); }) ); } catch (RequestNotPermitted e) { log.warn("Request rate limited for product: {}", productId); throw new ServiceException("Service is busy, please try again later"); } catch (CallNotPermittedException e) { log.warn("Circuit breaker open for product queries"); throw new ServiceException("Service is temporarily unavailable"); } } private Product getProductFromCacheOrDb(Long productId) { String cacheKey = "product:" + productId; // 4. 查詢緩存 String cachedValue = redisTemplate.opsForValue().get(cacheKey); if (cachedValue != null) { // 處理空值緩存情況 if (cachedValue.isEmpty()) { return null; } return JSON.parseObject(cachedValue, Product.class); } // 5. 查詢數(shù)據(jù)庫(加入DB保護) Product product = null; try { product = productMapper.selectById(productId); } catch (Exception e) { log.error("Database error when querying product: {}", productId, e); throw new ServiceException("System error, please try again later"); } // 6. 更新緩存(空值也緩存) if (product != null) { redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(product), cacheExpireSeconds, TimeUnit.SECONDS ); // 確保布隆過濾器包含此ID redisTemplate.execute( (RedisCallback<Boolean>) connection -> connection.execute( "BF.ADD", "product_filter".getBytes(), productId.toString().getBytes() ) != 0 ); localBloomFilter.put(productId.toString()); } else { // 緩存空值,短時間過期 redisTemplate.opsForValue().set( cacheKey, "", 60, // 空值短期緩存 TimeUnit.SECONDS ); } return product; } }
優(yōu)缺點分析
優(yōu)點
- 提供全方位的系統(tǒng)保護
- 各層防護互為補充,形成完整防線
- 可以靈活配置各層策略
- 最大限度減少資源浪費和性能損耗
缺點
- 實現(xiàn)復雜度高
- 各層配置需要協(xié)調(diào)一致
- 可能增加系統(tǒng)響應時間
- 維護成本相對較高
總結(jié)
防范緩存穿透不僅是技術(shù)問題,更是系統(tǒng)設(shè)計和運維的重要環(huán)節(jié)。
在實際應用中,應根據(jù)具體業(yè)務場景和系統(tǒng)規(guī)模選擇合適的策略組合。通常,單一策略難以完全解決問題,而組合策略能夠提供更全面的防護。無論采用何種策略,定期監(jiān)控和性能評估都是保障緩存系統(tǒng)高效運行的必要手段。
到此這篇關(guān)于Redis預防緩存穿透的6種策略的文章就介紹到這了,更多相關(guān)Redis緩存穿透內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis lettuce連接池經(jīng)常出現(xiàn)連接拒絕(Connection refused)問題解決
本文主要介紹了在Windows 10/11系統(tǒng)中使用Spring Boot和Lettuce連接池訪問Redis時,遇到的連接拒絕問題,下面就來介紹一下解決方法,感興趣的可以了解一下2025-03-03redis.clients.jedis.exceptions.JedisDataException異常的錯誤解決
本文主要介紹了redis.clients.jedis.exceptions.JedisDataException異常的錯誤解決,這個異常通常發(fā)生在嘗試連接到一個?Redis?服務器時,客戶端發(fā)送了一個?AUTH?命令來驗證密碼,但是沒有配置密碼驗證,下來就來解決一下2024-05-05Redis數(shù)據(jù)庫分布式設(shè)計方案介紹
大家好,本篇文章主要講的是Redis數(shù)據(jù)庫分布式設(shè)計方案介紹,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下2022-01-01