1. Redis缓存实战:从基础到进阶
Redis作为内存数据库的标杆,在现代Web架构中扮演着关键角色。我首次在生产环境使用Redis是在2015年一个电商秒杀项目中,当时用简单的key-value存储就解决了MySQL扛不住瞬时高并发的问题。但随着业务复杂度提升,单纯set/get已经不能满足需求,需要更系统的缓存策略。
1.1 缓存数据类型选型实战
Redis支持5种核心数据结构,实际项目中我这样选择:
- 商品详情页缓存:使用String类型,配合
SETEX设置自动过期 - 购物车关系:Hash类型存储用户ID与商品ID的映射
- 最近浏览记录:ZSET按时间戳排序,轻松实现LRU逻辑
- 秒杀库存:用LIST实现预扣减队列,避免超卖
关键经验:String类型并非万能,Hash类型在字段较多时可节省30%内存
1.2 缓存失效策略设计
缓存失效是实际项目中最容易出问题的环节。我们曾因缓存雪崩导致全站不可用,最终采用分层失效策略:
- 基础数据(如城市列表)设置永久缓存,通过后台进程主动更新
- 热点数据(如商品详情)采用
基础过期时间+随机抖动,例如:bash复制EXPIRE product:123 ${3600 + $RANDOM % 300} # 1小时±5分钟 - 实时性要求高的数据(如库存)设置短过期时间(30秒)+ 延迟双删
1.3 缓存穿透防御方案
当遇到恶意查询不存在的数据时,我们的防御组合拳:
- 布隆过滤器:在Redis前增加BloomFilter层
- 空值缓存:对查询结果为NULL的key也缓存5分钟
- 互斥锁:当缓存未命中时,用SETNX锁住查询过程
实测下来,这套方案将缓存穿透导致的数据库压力降低了98%。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分布式锁深度实践
在微服务架构下,分布式锁是保证数据一致性的关键。我经历过多次因锁问题导致的资损事故,最终沉淀出这套方案。
2.1 基于Redis的锁实现演进
第一代(问题版本):
java复制// 错误示范!
Boolean result = redis.setnx("lock_key", "1");
if(result) {
// 业务逻辑
redis.del("lock_key");
}
这个方案有三个致命缺陷:
- 没有过期时间,进程崩溃会导致死锁
- 非原子性操作,设置值和过期时间可能分离
- 可能误删其他进程的锁
第二代(基础版):
bash复制SET lock_key $unique_id NX PX 30000
通过NX和PX参数实现原子操作,配合唯一ID避免误删。但依然存在锁续期问题。
第三代(生产级):
采用Redisson的看门狗机制,主要特性:
- 自动续期:默认30秒过期,每10秒检查续期
- 可重入设计:支持同一线程多次加锁
- 故障转移:支持Redis集群模式
2.2 锁的边界条件处理
在支付系统灰度期间,我们遇到过这些典型问题:
场景1:锁等待超时
当持有锁的进程执行时间超过锁有效期时:
- 方案:设置合理的超时时间(业务最大耗时*2)
- 监控:通过Redis的慢查询日志监控锁持有时间
场景2:集群脑裂
当Redis主从切换时可能出现双锁:
- 解决方案:使用RedLock算法(需至少3个独立Master节点)
- 妥协方案:允许短暂不一致,通过对账系统修复
场景3:锁重试风暴
当大量请求争抢同一个锁时:
java复制// 使用随机退避算法
int maxWait = 1000;
int baseWait = 10;
int retries = 0;
while(!tryLock()){
Thread.sleep(baseWait * (1 + random.nextFloat()) * Math.min(retries, 10));
if(retries++ > 10) throw new BusyException();
}
3. 高并发场景下的缓存与锁组合应用
在去年双11大促中,我们通过以下设计支撑了10万QPS的秒杀场景。
3.1 秒杀系统三级缓存架构
- 本地缓存(Caffeine):存储静态配置如秒杀开始时间
java复制Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .maximumSize(10_000) .build(); - Redis集群:处理动态库存数据
- 采用分片集群模式(16节点)
- 使用Lua脚本保证原子性:
lua复制local stock = tonumber(redis.call('GET', KEYS[1])) if stock <= 0 then return 0 end redis.call('DECR', KEYS[1]) return 1
- 数据库层:最终数据持久化
- 采用批量异步写入
- 使用影子表进行压力隔离
3.2 热点Key处理方案
当某个商品(如iPhone新品)成为超级热点时:
- 本地缓存:在应用层缓存商品详情
- Key分片:将库存拆分为10个子Key(stock:product:123:1~10)
- 随机路由:客户端随机选择分片进行扣减
实测将单Key的10万QPS压力分散到10个节点,Redis CPU负载从90%降到35%。
4. 生产环境中的疑难问题排查
4.1 Redis内存突然增长分析
现象:Redis内存使用率在凌晨突增40%
排查过程:
- 使用
redis-cli --bigkeys发现大量临时Key - 检查日志发现定时任务没有设置过期时间
- 最终定位到新上线的推荐服务生成的特征向量缓存
解决方案:
- 为所有临时Key设置TTL
- 增加内存使用率监控告警
- 使用
CONFIG SET maxmemory-policy allkeys-lru作为兜底
4.2 分布式锁失效案例
现象:订单重复创建
排查链路:
- 检查Redisson锁日志发现大量
lock acquired记录 - 发现Pod频繁重启导致锁过期
- 根本原因是K8s健康检查配置不当
优化措施:
- 调整健康检查超时为业务耗时的2倍
- 在业务代码中添加中间状态标记
- 实现锁的优雅释放钩子:
java复制Runtime.getRuntime().addShutdownHook(new Thread(() -> { if(lock.isHeldByCurrentThread()){ lock.unlock(); } }));
4.3 缓存一致性保障
我们采用"标记失效+异步刷新"策略:
- 任何数据变更操作都先发MQ消息
- 消费者处理:
python复制def handle_message(msg): redis.delete(msg.key) # 立即失效 db_data = query_db(msg.query) # 异步加载 redis.setex(msg.key, 3600, db_data) - 前端请求时如果缓存不存在:
- 先返回旧版本数据(如有)
- 同时触发异步加载任务
这套方案在保证性能的同时,将数据不一致时间窗口控制在200ms内。
