在电商系统中,商品详情页是一个典型的高频访问场景。当用户请求某个商品的详情时,系统会优先从缓存中获取数据。如果缓存中没有该商品的详情,系统会去数据库查询并更新缓存。然而,如果某个热门商品的缓存失效,大量请求会同时查询数据库,导致数据库压力骤增,这就是缓存击穿问题。
以下是一个结合布隆过滤器防止缓存击穿的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;
// 布隆过滤器的Key
private 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() {
// 获取所有商品ID
List<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 {
// 获取所有商品ID
public List<String> getAllProductIds() {
// 查询数据库,返回所有商品ID
return 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是否存在,减少对不存在商品的查询。
减轻数据库压力:即使缓存失效,也能通过布隆过滤器减少对数据库的直接查询。
缺点
布隆过滤器误判:虽然误判率可以通过调整参数降低,但无法完全避免。
锁机制的开销:在高并发场景下,锁可能会成为性能瓶颈。
通过以上实现,电商系统可以在商品详情查询场景中有效缓解缓存击穿问题,同时结合布隆过滤器减少对数据库的无效查询。