1. 缓存更新策略
- 读写锁策略:
- 使用读写锁(如Redisson的读写锁),读操作加读锁,写操作加写锁。在更新热门商品信息时,先获取写锁,更新Redis缓存和数据库,完成后释放写锁。读操作获取读锁后从Redis读取数据,确保在写操作时不会有读操作进行,避免脏读。
- 示例代码(以Java和Redisson为例):
RedissonClient redisson = Redisson.create();
RLock writeLock = redisson.getLock("product:write:lock");
try {
writeLock.lock();
// 更新数据库
productDao.update(product);
// 更新Redis缓存
redisTemplate.opsForValue().set("product:" + product.getId(), product);
} finally {
writeLock.unlock();
}
- 异步更新策略:
- 当商品信息变动时,先更新数据库,然后通过消息队列(如Kafka、RabbitMQ)发送更新消息。有一个消费者监听队列,接收到消息后更新Redis缓存。这样可以减少更新操作对业务流程的阻塞。
- 示例:
kafkaTemplate.send("product-update-topic", productUpdateMessage);
- 消费者更新缓存:
@KafkaListener(topics = "product-update-topic", groupId = "product-update-group")
public void handleProductUpdate(ProductUpdateMessage message) {
// 根据消息中的商品ID更新Redis缓存
redisTemplate.opsForValue().set("product:" + message.getProductId(), message.getUpdatedProduct());
}
- 缓存版本控制:
- 为每个商品设置一个版本号。每次商品信息更新时,版本号加1。读取数据时,先从Redis获取版本号和数据,若版本号不一致,重新从数据库读取数据并更新Redis缓存和版本号。
- 示例代码:
// 获取商品数据
String versionKey = "product:version:" + productId;
Long version = redisTemplate.opsForValue().get(versionKey);
Product product = redisTemplate.opsForValue().get("product:" + productId);
if (version == null || product == null) {
// 从数据库读取
product = productDao.findById(productId);
version = 1L;
redisTemplate.opsForValue().set(versionKey, version);
redisTemplate.opsForValue().set("product:" + productId, product);
} else {
// 假设从数据库获取最新版本号
Long newVersion = productDao.findVersionById(productId);
if (!version.equals(newVersion)) {
product = productDao.findById(productId);
redisTemplate.opsForValue().set(versionKey, newVersion);
redisTemplate.opsForValue().set("product:" + productId, product);
}
}
2. 防止缓存穿透
- 布隆过滤器:
- 在系统启动时,将所有商品ID加载到布隆过滤器中。当查询商品时,先通过布隆过滤器判断商品ID是否存在。如果不存在,直接返回,不再查询数据库,避免大量无效请求穿透到数据库。
- 示例代码(以Guava的布隆过滤器为例):
// 初始化布隆过滤器
BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), productIds.size(), 0.01);
for (Long productId : productIds) {
bloomFilter.put(productId);
}
// 查询商品时使用布隆过滤器
if (!bloomFilter.mightContain(productId)) {
return null;
}
// 继续查询Redis或数据库
- 缓存空值:
- 当查询数据库发现商品不存在时,将空值缓存到Redis中,并设置一个较短的过期时间。这样后续相同的无效请求直接从Redis获取空值,避免穿透到数据库。
- 示例代码:
Product product = redisTemplate.opsForValue().get("product:" + productId);
if (product == null) {
product = productDao.findById(productId);
if (product == null) {
// 缓存空值
redisTemplate.opsForValue().set("product:" + productId, null, 10, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set("product:" + productId, product);
}
}
return product;
3. 防止缓存雪崩
- 设置随机过期时间:
- 为热门商品缓存设置一个随机的过期时间,避免大量缓存同时过期。例如,原本设置过期时间为1小时,可以在50 - 70分钟之间随机设置过期时间。
- 示例代码:
int randomExpireTime = ThreadLocalRandom.current().nextInt(50, 70) * 60;
redisTemplate.opsForValue().set("product:" + productId, product, randomExpireTime, TimeUnit.SECONDS);
- 加锁排队:
- 当缓存过期时,使用分布式锁(如Redis的SETNX命令实现)保证只有一个请求去查询数据库并更新缓存,其他请求等待。这样可以避免大量请求同时查询数据库,减轻数据库压力。
- 示例代码(以Jedis为例):
Jedis jedis = new Jedis("localhost");
String lockKey = "product:lock:" + productId;
String lockValue = UUID.randomUUID().toString();
if ("OK".equals(jedis.set(lockKey, lockValue, "NX", "EX", 10))) {
try {
Product product = productDao.findById(productId);
redisTemplate.opsForValue().set("product:" + productId, product);
} finally {
jedis.del(lockKey);
}
} else {
// 等待一段时间后重试
Thread.sleep(100);
return getProductFromCache(productId);
}