在电商系统中,商品详情页是一个典型的高频访问场景。当用户请求某个商品的详情时,系统会优先从缓存中获取数据。如果缓存中没有该商品的详情,系统会去数据库查询并更新缓存。然而,如果某个热门商品的缓存失效,大量请求会同时查询数据库,导致数据库压力骤增,这就是缓存击穿问题。
以下是一个结合布隆过滤器防止缓存击穿的Java伪代码实现案例:
场景描述
商品详情查询:用户通过商品ID查询商品详情。
缓存层:使用Redis作为缓存,存储商品详情。
布隆过滤器:使用RedisBloom模块实现布隆过滤器,存储所有可能被查询的商品ID。
数据库层:存储商品详情的数据库。
实现思路
初始化布隆过滤器:
在系统启动时,将数据库中所有商品的ID插入到布隆过滤器中。
查询流程:
当用户请求商品详情时,先通过布隆过滤器判断该商品ID是否存在。
如果布隆过滤器判断不存在,则直接返回“商品不存在”。
如果布隆过滤器判断可能存在,则去缓存中查询。
如果缓存中有数据,则直接返回缓存结果。
如果缓存中没有数据,则去数据库查询,并将结果放入缓存。
Java伪代码实现
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.bloom.BloomOptions;
import io.lettuce.core.bloom.RedisBloomCommands;import java.util.concurrent.locks.ReentrantLock;public class ProductService {// Redis客户端private RedisClient redisClient;private RedisCommands<String, String> syncCommands;private RedisBloomCommands<String, String> bloomCommands;// 数据库客户端private DatabaseClient databaseClient;// 布隆过滤器的Keyprivate static final String BLOOM_FILTER_KEY = "product:bloomfilter";// 缓存Key前缀private static final String CACHE_KEY_PREFIX = "product:cache:";// 锁,用于防止缓存击穿时的并发问题private ReentrantLock lock = new ReentrantLock();public ProductService(RedisClient redisClient, DatabaseClient databaseClient) {this.redisClient = redisClient;this.syncCommands = redisClient.connect().sync();this.bloomCommands = redisClient.connect().sync();this.databaseClient = databaseClient;}// 初始化布隆过滤器public void initBloomFilter() {// 获取所有商品IDList<String> productIds = databaseClient.getAllProductIds();// 初始化布隆过滤器bloomCommands.bfCreate(BLOOM_FILTER_KEY, BloomOptions.defaults(), productIds.size());// 将所有商品ID插入布隆过滤器for (String productId : productIds) {bloomCommands.bfAdd(BLOOM_FILTER_KEY, productId);}}// 查询商品详情public Product getProductDetails(String productId) {// 1. 使用布隆过滤器判断商品ID是否存在boolean exists = bloomCommands.bfExists(BLOOM_FILTER_KEY, productId);if (!exists) {// 如果布隆过滤器判断不存在,直接返回商品不存在return null;}// 2. 从缓存中查询商品详情String cacheKey = CACHE_KEY_PREFIX + productId;String productDetails = syncCommands.get(cacheKey);if (productDetails != null) {// 如果缓存中有数据,直接返回return new Product(productDetails);}// 3. 缓存中没有数据,加锁防止缓存击穿lock.lock();try {// 再次检查缓存,防止并发问题productDetails = syncCommands.get(cacheKey);if (productDetails != null) {return new Product(productDetails);}// 4. 查询数据库Product product = databaseClient.getProductById(productId);if (product != null) {// 将查询结果放入缓存syncCommands.set(cacheKey, product.toJson());}return product;} finally {lock.unlock();}}
}// 数据库客户端
class DatabaseClient {// 获取所有商品IDpublic List<String> getAllProductIds() {// 查询数据库,返回所有商品IDreturn database.query("SELECT id FROM products");}// 根据商品ID查询商品详情public Product getProductById(String productId) {// 查询数据库,返回商品详情return database.query("SELECT * FROM products WHERE id = ?", productId);}
}// 商品类
class Product {private String id;private String name;private double price;public Product(String details) {// 从JSON字符串解析商品详情this.id = parseId(details);this.name = parseName(details);this.price = parsePrice(details);}public String toJson() {// 将商品详情转换为JSON字符串return "{\"id\":\"" + id + "\",\"name\":\"" + name + "\",\"price\":" + price + "}";}
}
代码说明
布隆过滤器初始化:
在系统启动时,调用initBloomFilter方法,将所有商品ID插入到布隆过滤器中。
查询流程:
使用布隆过滤器判断商品ID是否存在。
如果布隆过滤器判断不存在,则直接返回null。
如果布隆过滤器判断可能存在,则去缓存中查询。
如果缓存中没有数据,则加锁并查询数据库,将结果放入缓存。
锁机制:
使用ReentrantLock防止缓存击穿时的并发问题。
在加锁后再次检查缓存,确保只有一个线程去查询数据库。
优点
减少无效查询:布隆过滤器可以快速判断商品ID是否存在,减少对不存在商品的查询。
减轻数据库压力:即使缓存失效,也能通过布隆过滤器减少对数据库的直接查询。
缺点
布隆过滤器误判:虽然误判率可以通过调整参数降低,但无法完全避免。
锁机制的开销:在高并发场景下,锁可能会成为性能瓶颈。
通过以上实现,电商系统可以在商品详情查询场景中有效缓解缓存击穿问题,同时结合布隆过滤器减少对数据库的无效查询。