1. 缓存与队列的本质矛盾
在分布式系统和高并发场景中,缓存和队列这对"黄金搭档"常常需要协同工作,但它们却有着截然不同的性格特征。缓存就像个急性子,追求的是"快"——快速读取、快速响应,用空间换时间;而队列则是个慢性子,讲究的是"稳"——顺序处理、持久存储,用时间换可靠性。当这两个性格迥异的组件需要配合时,就会出现一个经典难题:快的如何等慢的?
我曾在电商秒杀系统中深刻体会过这种矛盾。当十万级QPS的请求瞬间涌入时,Redis缓存可以轻松应对读取压力,但下游的订单队列却因为MySQL的写入瓶颈开始堆积。这时缓存层已经返回了"库存充足"的响应,而队列可能还在处理几分钟前的请求,最终导致超卖事故。这种"缓存说可以,队列说不行"的尴尬局面,正是我们需要解决的核心问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 缓存等队列的四种典型场景
2.1 秒杀系统中的库存扣减
在秒杀场景下,常规做法是:
- 读取Redis缓存校验库存
- 扣减Redis库存
- 发送MQ消息异步创建订单
但这里存在一个危险的时间差:当缓存库存扣减成功后,如果消息队列堆积,实际数据库库存可能并未减少。此时若缓存过期或崩溃重新加载数据库数据,就会出现库存超卖。我们曾因此一夜之间多卖了2000台iPhone,不得不全额退款并补偿优惠券。
解决方案是引入分布式事务的预扣机制:
java复制// 伪代码示例
boolean tryLock = redis.opsForValue().setIfAbsent("lock:"+skuId, requestId, 10, TimeUnit.SECONDS);
if (tryLock) {
try {
// 1. 预扣缓存库存(实际库存-预扣库存)
Long remain = redis.opsForValue().decrement("stock:"+skuId);
// 2. 发送预扣消息到延迟队列
mqTemplate.sendDelayMessage("stock_prehold",
new StockPrehold(skuId, userId, 1),
30, TimeUnit.SECONDS);
// 3. 返回秒杀成功
return Result.success();
} finally {
redis.delete("lock:"+skuId);
}
}
2.2 支付系统中的状态同步
支付系统往往采用"缓存状态+队列通知"的架构。当第三方支付回调时,会先更新Redis中的支付状态,再通过消息队列通知业务系统。但某些业务系统要求严格顺序处理,当队列消费延迟时,用户可能在缓存中看到支付成功,但实际业务还未处理完成。
某次大促期间,我们监控到这样的异常流:
code复制08:00:00 缓存更新支付成功
08:00:05 用户查询缓存显示已支付
08:00:30 队列才开始处理该支付消息
08:00:35 业务系统完成订单发货
这30秒的时间差导致大量用户投诉"支付成功但订单消失"。
改进方案是双校验机制:
- 前端查询支付状态时,若缓存返回成功但订单未更新,则触发补偿查询
- 消息队列采用优先级队列,支付消息设置为最高优先级
- 关键业务路径增加同步校验接口
2.3 社交媒体的点赞计数
微博类系统的点赞设计通常采用"缓存累加+队列持久化"模式。在流量高峰时,Redis的计数器可能已经显示1000+点赞,但数据库实际只处理了前200条消息。如果此时缓存崩溃,重新加载数据库数据会导致点赞数"时光倒流"。
我们通过以下结构解决这个问题:
code复制┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 客户端请求 │───▶│ Redis累加 │───▶│ Kafka队列 │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ MySQL持久化 │
└─────────────┘
关键点在于:
- Redis使用Lua脚本保证原子性递增
- Kafka消息携带当前Redis的准确计数值
- 后台服务消费时对比DB现有值与消息值,取较大者更新
2.4 配置中心的动态更新
在微服务架构中,配置更新通常走"缓存生效+队列广播"的路径。当管理员修改配置后,配置中心会立即更新本地缓存,同时通过消息队列通知所有客户端。但网络分区或队列延迟可能导致部分客户端长期使用旧配置。
我们曾遇到过一个经典案例:某个功能开关在控制台显示已开启,但30%的节点仍在用旧配置。最终发现是因为RabbitMQ的镜像队列同步延迟。解决方案是:
- 每次配置变更生成唯一版本号
- 客户端定时轮询校验版本号
- 结合长连接推送实现双保险
3. 技术选型与架构设计
3.1 缓存等队列的中间态设计
当快的需要等慢的时,关键在于设计合理的中间状态。以电商下单为例:
| 状态阶段 | 缓存表现 | 队列表现 | 用户感知 |
|---|---|---|---|
| 初始态 | 库存充足 | 无订单消息 | 可下单 |
| 中间态 | 预扣减库存 | 消息堆积中 | 下单中 |
| 完成态 | 最终库存 | 消息已消费 | 下单成功 |
实现这种状态机需要:
python复制class OrderStateMachine:
def __init__(self):
self.cache = Redis()
self.mq = Kafka()
def create_order(self, user_id, item_id):
# 第一阶段:尝试预占资源
prehold_key = f"prehold:{user_id}:{item_id}"
if not self.cache.setnx(prehold_key, "processing", ttl=300):
raise ConcurrentConflict()
try:
# 第二阶段:缓存快速响应
self.cache.decr(f"stock:{item_id}")
self.cache.set(f"order_status:{user_id}", "processing")
# 第三阶段:异步持久化
self.mq.publish("order_created",
{"user_id": user_id, "item_id": item_id})
return {"status": "processing"}
except Exception as e:
