基于Redis实现案例:从缓存穿透到分布式锁的架构实战
目录导读
- Redis在现代架构中的核心地位
- 缓存穿透解决方案(布隆过滤器+空值缓存)
- 缓存雪崩与热点Key重建(互斥锁+逻辑过期)
- 基于Redis的分布式锁(Redisson实现)
- Redis实现延时队列(ZSET+事件轮询)
- 高频面试问答与踩坑总结
- 生产环境监控与性能调优建议
Redis在现代架构中的核心地位
在互联网高并发场景下,Redis凭借其单线程模型、IO多路复用、内存级读写(10万+ QPS)成为缓存、计数、分布式锁的首选,但实际生产中,开发者往往只停留在“set/get”层面,导致遇到缓存穿透、雪崩、热点Key时手足无措,本文通过4个真实业务案例,手把手带你落地Redis高阶用法。

案例一:缓存穿透解决方案
问题背景:某电商平台的商品详情接口,遇到大量请求查询不存在的商品ID(如恶意攻击),导致每次请求都直接打到MySQL,数据库压力骤增。
传统方案弊端:如果直接查缓存为空就返回,攻击者可以伪造随机ID无限打穿缓存。
Redis实战方案(双重策略) :
// 策略1:布隆过滤器预判
// 初始化时将所有有效商品ID存入布隆过滤器
BloomFilter<String> bloom = BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.01);
// 请求进入时先判断
if (!bloom.mightContain(productId)) {
return "商品不存在"; // 直接拦截,不打DB
}
// 策略2:空值缓存(防并发穿透)
String cacheValue = redis.get("product:" + productId);
if (cacheValue == null) {
// 加锁查DB,防止击穿
String lockKey = "lock:product:" + productId;
boolean locked = redis.setIfAbsent(lockKey, "1", Duration.ofSeconds(5));
if (locked) {
Product product = db.query(productId);
if (product == null) {
// 缓存空值,设置较短过期时间(例如60s)
redis.set("product:" + productId, "NULL", Duration.ofSeconds(60));
} else {
redis.set("product:" + productId, JSON.toJSONString(product), Duration.ofMinutes(30));
}
}
}
核心要点:布隆过滤器挡住99%非法请求,空值缓存挡住合法但不存在的数据,两者结合将DB压力降低90%以上。
案例二:缓存雪崩与热点Key重建
问题背景:双11大促时,某爆款商品的缓存突然过期,同时有10万请求涌入,全部穿透到MySQL,导致数据库宕机。
Redis实战方案(逻辑过期+互斥锁) :
// 设计:缓存中存储过期时间戳,而非真正让Redis过期
// 写入时:value = {data, expireTime = now + 300s}
// 读取时:
public Product getProduct(String id) {
String json = redis.get("hot:" + id);
if (json == null) {
return queryDBWithLock(id); // 兜底
}
ProductCache cache = JSON.parseObject(json, ProductCache.class);
if (cache.getExpireTime() > System.currentTimeMillis()) {
return cache.getData(); // 未过期,直接返回
}
// 逻辑过期了,尝试获取互斥锁
String lockKey = "rebuild:" + id;
boolean locked = redis.setIfAbsent(lockKey, "1", Duration.ofSeconds(3));
if (locked) {
// 异步线程重建缓存
executor.submit(() -> {
Product fresh = db.query(id);
redis.set("hot:" + id, fresh, Duration.ofMinutes(30));
redis.del(lockKey);
});
}
// 返回旧数据,保证可用性
return cache.getData();
}
关键设计:牺牲了强一致性,但换取了高可用,即使缓存重建失败,用户仍能拿到旧数据,不会打垮DB。
案例三:基于Redis的分布式锁(Redisson)
问题背景:微服务架构下,多个节点同时处理同一订单的库存扣减,导致超卖。
传统setnx弊端:无法处理持有锁的线程崩溃导致死锁,也无法实现可重入。
Redisson实战方案:
RLock lock = redisson.getLock("inventory:1001");
try {
// 尝试加锁,等待最多5秒,自动释放30秒
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
int stock = Integer.parseInt(redis.get("stock:1001"));
if (stock > 0) {
int newStock = stock - 1;
redis.set("stock:1001", String.valueOf(newStock));
// 更新DB
orderMapper.deduct(1001, newStock);
return "扣减成功";
}
}
} finally {
lock.unlock(); // 确保释放
}
为何选Redisson:
- 看门狗机制:自动续期,避免业务未执行完锁就过期
- 可重入:同一线程可多次加锁
- 公平锁/读写锁:支持复杂场景
案例四:Redis实现延时队列(ZSET+事件轮询)
问题背景:订单支付超时自动关闭(30分钟未支付),需要精准延时触发。
Redis实战方案:
// 生产者:将订单ID和过期时间戳放入ZSET
long expireTime = System.currentTimeMillis() + 30 * 60 * 1000;
redis.zadd("delay:order", expireTime, orderId);
// 消费者(独立线程/定时任务)
while (true) {
Set<String> orders = redis.zrangeByScore("delay:order", 0, System.currentTimeMillis(), 0, 1);
if (orders.isEmpty()) {
Thread.sleep(500); // 避免空轮询
continue;
}
String orderId = orders.iterator().next();
// 移除元素(原子操作)
Long removed = redis.zrem("delay:order", orderId);
if (removed > 0) {
// 检查订单状态,若未支付则关闭
orderService.closeOverdue(orderId);
}
}
优化点:使用zrem确保多消费者不会重复处理,也可用Redis Stream替代,但ZSET实现简单且易理解。
高频面试问答与踩坑总结
Q1:Redis为什么是单线程却这么快? A:基于内存、IO多路复用(epoll)、避免上下文切换和锁竞争,但注意Redis 6.0后引入了多线程IO,但核心命令执行仍是单线程。
Q2:缓存击穿和穿透的区别? A:穿透是查不存在的Key,击穿是热点Key过期瞬间大量请求打到DB。
Q3:如何避免Big Key(大Value)阻塞?
A:拆分Value(如Hash分片)、异步删除(unlink)、监控redis-cli --bigkeys。
必踩坑提示:
- 分布式锁+事务必须搭配,锁释放前提交事务,否则导致脏数据
- 使用
set ex nx原子命令,避免先set后expire产生死锁 - 延时队列消费端必须做好幂等(防止重复关闭订单)
生产环境监控与性能调优建议
- 内存碎片率:
info memory,若mem_fragmentation_ratio > 1.5,启用activedefrag yes - 慢查询日志:
slowlog get 10,设置slowlog-log-slower-than 10000(微秒) - 键空间淘汰策略:推荐
allkeys-lru,避免内存写满导致服务不可用 - 连接数监控:
info clients,连接数过高时使用连接池,并设置maxclients
架构原则:Redis永远不是最终数据源,只能作为加速层,所有写操作必须最终落库。
本文基于真实生产案例整理,结合了Redis官方文档与社区最佳实践,建议读者在测试环境充分验证后上线。