MST

星途 面试题库

面试题:Redis在电商热门商品缓存中的缓存更新策略

在电商系统中,热门商品的信息(如价格、库存)可能会频繁变动,如何设计一个合理的Redis缓存更新策略,确保数据一致性的同时尽量减少缓存穿透和缓存雪崩的风险?
17.9万 热度难度
数据库Redis

知识考点

AI 面试

面试题答案

一键面试

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);
}