基于Redis实现案例

wen java案例 2

基于Redis实现案例:从缓存穿透到分布式锁的架构实战

目录导读

  1. Redis在现代架构中的核心地位
  2. 缓存穿透解决方案(布隆过滤器+空值缓存)
  3. 缓存雪崩与热点Key重建(互斥锁+逻辑过期)
  4. 基于Redis的分布式锁(Redisson实现)
  5. Redis实现延时队列(ZSET+事件轮询)
  6. 高频面试问答与踩坑总结
  7. 生产环境监控与性能调优建议

Redis在现代架构中的核心地位

在互联网高并发场景下,Redis凭借其单线程模型、IO多路复用、内存级读写(10万+ QPS)成为缓存、计数、分布式锁的首选,但实际生产中,开发者往往只停留在“set/get”层面,导致遇到缓存穿透、雪崩、热点Key时手足无措,本文通过4个真实业务案例,手把手带你落地Redis高阶用法。

基于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产生死锁
  • 延时队列消费端必须做好幂等(防止重复关闭订单)

生产环境监控与性能调优建议

  1. 内存碎片率info memory,若mem_fragmentation_ratio > 1.5,启用activedefrag yes
  2. 慢查询日志slowlog get 10,设置slowlog-log-slower-than 10000(微秒)
  3. 键空间淘汰策略:推荐allkeys-lru,避免内存写满导致服务不可用
  4. 连接数监控info clients,连接数过高时使用连接池,并设置maxclients

架构原则:Redis永远不是最终数据源,只能作为加速层,所有写操作必须最终落库。


本文基于真实生产案例整理,结合了Redis官方文档与社区最佳实践,建议读者在测试环境充分验证后上线。

抱歉,评论功能暂时关闭!