高并发抢单逻辑
高并发流量拦截,数据库落库,性能抗压;
10个请求同事争抢数据库的同一行数据锁,大量请求阻塞排队,不仅瞬时打满数据库连接池,还会造成频的线程的上下文切换,极大消耗了服务器的性能,这时候Redis就派上用场,它不仅仅速度快,它解决抢单这种冲突是王牌,因为redis是单线程,串行执行模型,10个司机在抢单请求,在外网层面,是并行涌入的,但是全部打入到Redis后,所有请求就会强制排队,串行执行,不存在并发抢占的冲突,我们提前为当前的订单在Redis中设置一个唯一抢单令牌,利用redis的原始命令进行抢占判定最先到达的请求成功抢占到令牌,直接判定抢单成功,其余9名后续排队请求,查询令牌已经被占用了,直接快速返回抢单失败,全程没有锁竞争内耗,没有线程切换开销,1秒就能完成争抢判定,轻松顶住海量并发请求。
消息队列,异步解耦的思想,只要Redis层面判定司机抢单成功,系统立刻响应用户弹窗接单成功,提升了用户体验。而修改订单状态,绑定司机信息,还有像更新订单数据,这些耗时的数据库操作,全部封装成消息投递到MQ中,由后台消费程序,异步平缓执行入库操作,让前端响应极致流畅,让数据库从容处理数据落地,前后端各司其职。系统稳定拉满。
AI总结:
作为架构师,我会把这道题答成:高并发抢单不是“Redis 快所以用 Redis”,而是用 Redis 做原子预占、把冲突拦截在 DB 之前,再用可靠 MQ 异步落库,最后用 DB 唯一约束、幂等、对账、补偿兜底一致性。
核心一句话:
入口限流 → Redis 原子抢占 → 快速失败 → 可靠 MQ 削峰 → 异步落库 → DB 唯一约束/乐观锁 → 幂等消费 → 对账补偿 → 监控降级。
Redis 解决的是“高并发冲突判定”和“数据库行锁阻塞”,不是替代数据库;MQ 解决的是“异步解耦和削峰”,不是绕过一致性。
一、先纠偏原答案
-
“Redis 单线程”要准确说
Redis 6.0 之前命令执行单线程;6.0 之后网络 IO 多线程,但命令执行仍是单线程。Lua 脚本原子执行。所以多个抢单命令到 Redis 后是串行处理,但不是说客户端没有网络等待。 -
“没有锁竞争”不准确
Redis 内部也是串行处理,只是没有 DB 行锁等待和大量线程上下文切换。准确说:把数据库行锁竞争前移为 Redis 原子命令竞争,失败快速返回。 -
“1 秒完成”不准确
Redis 单命令亚毫秒级,加上网络 RTT,通常几毫秒到几十毫秒。说“毫秒级”更专业。 -
“MQ 异步落库就完事”不准确
Redis 抢单成功、MQ 发送成功、DB 落库失败,仍可能不一致。必须做:可靠消息、消费幂等、重试、死信、对账、补偿。核心订单不能只靠“发个 MQ”就返回成功。 -
“修改订单状态、绑定司机全部异步”要分场景
如果业务允许“抢单成功,确认中”,可以异步。如果必须强一致,至少要同步写可靠消息表或事务消息,不能裸发 MQ。
二、2 分钟面试逐字稿
这个场景本质是同一订单或同一库存行被大量请求争抢,如果直接打 DB,会行锁排队、连接池打满、线程上下文切换,最后雪崩。
我的方案分五层。
第一,入口拦截。网关限流、防重、黑名单、验证码、Sentinel 限流,先把无效和重复流量挡掉。
第二,Redis 原子抢占。为订单生成 key,比如
order:grab:{orderId},用SET key driverId NX PX ttl,或者 Lua 脚本原子判断“是否已被抢、司机是否重复、司机接单上限”,成功写入占位,失败立即返回。这样第一个请求成功,其余快速失败,不进入 DB。第三,快速响应。Redis 判定成功后,立即返回用户“抢单成功”或“确认中”。同时发送可靠 MQ 消息,消息体带 orderId、driverId、requestId、traceId。
第四,异步落库。消费者更新订单状态、绑定司机、写流水。DB 层用唯一索引和乐观锁兜底,比如
update orders set driver_id=?, status='ACCEPTED' where id=? and status='WAITING',影响行数为 1 才算成功。消费端必须幂等,用消息 ID 去重、Redis SETNX 或 DB 唯一索引。第五,可靠性和补偿。MQ 用 RocketMQ 事务消息或本地消息表/Outbox,保证 Redis 成功后消息不丢。消费失败重试、死信、对账、补偿。如果最终落库失败,要回滚 Redis 占位、通知用户或转人工。监控 Redis 延迟、MQ 积压、DB 连接池、锁等待、重复派单率。
总结:Redis 负责高并发原子抢占,MQ 负责削峰异步落库,DB 负责最终一致和唯一约束,监控补偿兜底。这样既抗并发,又不丢数据。
三、完整链路
用户请求
↓
网关限流 / 防重 / 鉴权
↓
Redis 原子抢占:SET NX PX 或 Lua
├─ 失败:快速返回“已被抢”
└─ 成功:写占位,发可靠 MQ
↓
立即响应“抢单成功/确认中”
↓
MQ 削峰
↓
消费者异步落库
↓
DB 唯一索引 + 乐观锁 + 短事务
↓
成功:更新订单、绑定司机、写流水
失败:重试、死信、对账、补偿、回滚 Redis
四、Java 落地要点
1. Redis 原子抢占
简单版:
Boolean ok = redisTemplate.opsForValue()
.setIfAbsent("order:grab:" + orderId, driverId, Duration.ofSeconds(30));
if (Boolean.TRUE.equals(ok)) {
// 抢到,发可靠 MQ
} else {
// 快速失败
}
复杂版用 Lua:
-- KEYS[1] = order:grab:{orderId}
-- ARGV[1] = driverId
-- ARGV[2] = ttl ms
if redis.call('exists', KEYS[1]) == 1 then
return 0
end
redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])
return 1
注意:
- Redis 集群下多 key 要用 hash tag
{orderId}保证同 slot; - TTL 防止死锁;
- 误删要用 Lua 校验 value;
- 司机维度去重:
driver:grabbing:{driverId}。
2. 可靠 MQ
- RocketMQ 事务消息;
- 本地消息表 / Outbox;
- 先写消息表,再发 MQ,定时补偿;
- 消息带唯一
requestId; - 消费端幂等:Redis SETNX、DB 唯一索引、去重表。
3. DB 落库
update orders
set driver_id = ?, status = 'ACCEPTED', update_time = now()
where id = ? and status = 'WAITING';
- 影响行数 1 成功,0 失败;
- 唯一索引:
uk_order_driver(order_id)防一单多司机; - 司机接单上限用 Redis 计数或 DB 约束;
- 短事务,禁止事务内远程调用。
4. Java 并发与连接池
- HikariCP 控制连接池,不要盲目加大;
- 线程池隔离,抢单接口独立线程池;
- Sentinel 限流降级;
- Micrometer + Prometheus + Grafana 监控。
5. 降级预案
- Redis 挂了:降级 DB 乐观锁 + 强限流 + 排队;
- MQ 积压:扩容消费者、限流生产、降级非核心;
- DB 压力大:读写分离、分库分表、异步落库。
五、面试官可能追问
Q1:为什么不用 DB 行锁直接抢?
行锁会让大量请求阻塞,占连接、占线程,上下文切换高,容易打满连接池。Redis 原子抢占把冲突前移,失败快速返回。
Q2:Redis 成功,DB 失败怎么办?
可靠消息 + 重试 + 死信 + 对账 + 补偿。最终失败要回滚 Redis 占位、通知用户。核心是消息不能丢,消费必须幂等。
Q3:消息重复消费怎么办?
消费端幂等:消息 ID 去重、Redis SETNX、DB 唯一索引、乐观锁。重复消息直接返回成功。
Q4:怎么防一单多司机?
Redis Lua 原子占位 + DB 唯一索引
uk_order_driver(order_id)+ 乐观锁更新。
Q5:Redis 热 key 怎么办?
抢单单订单 key 量有限;秒杀库存热 key 可分片
stock:{skuId}:{shard},本地缓存,读写分离。
Q6:Redis 挂了怎么办?
降级 DB 乐观锁 + 强限流 + 排队;或返回稍后重试。不能把 DB 直接暴露给全量流量。
Q7:双写能不能保证一致?
业务层双写不能保证原子性。用事务消息、本地消息表、CDC、对账补偿,接受最终一致。
六、收尾话术
所以高并发抢单的核心是:入口限流,Redis 原子抢占,失败快速返回,成功发可靠 MQ,异步落库,DB 唯一约束和乐观锁兜底,消费幂等,对账补偿,监控降级。Redis 负责抗并发,MQ 负责削峰,DB 负责最终一致。这样既能顶住海量流量,又能保证订单不丢、不重、不超卖。

浙公网安备 33010602011771号