概念定义

到底什么是并发

  • 并发 = 多个操作在同一时间窗口内同时进行,无论来自同一人还是不同人。

因为数据只有一份,多人同时操纵就会有风险:谁说了算呢?所以会产生竟态问题。cpu的并发就是最好的理解方式了:cpu核只有一个的情况下,不可能同一时刻给多人使用,所以只能大家轮着时间切片用。

一条数据会不会在同一时刻被两个操作同时修改?是的话就是存在并发问题。 image.png|325

什么是分布式

  • 分布式:系统本身被拆分成了多个服务/节点。系统架构是分散的

一个人讲话给自己听,很难搞错; 三个人ABC一起传话,中间的B听错了,那传到C耳朵里的就变了。大家拿着的信息不一样了,所以分布式场景就是要解决怎么尽量去统一大家信息的问题。

什么是幂等

幂等就是:同一个操作,执行一次和多次,结果不变。

为什么我要单独把他拎出来讲?因为不幂等的设计,既是并发问题,也是分布式问题。

并发如何体现: 多个人同时下单; 一个人同一时刻点赞十次;

分布式如何体现: 项目有三个角色,分别是生产者、MQ、消费者; 还可能是更复杂的微服务项目;

通用分析方法

首先我想声明一句:

在分布式并发问题中,任何不根据具体情景分析,只讲技巧的,都是耍流氓!并发场景信息杂糅、需求各不一致。技术本身是为了解决问题的,脱离了真实情况的技术毫无用处,反而成了心智负担。

四个问题,四个数

遇到一个百万级QPS以内的分布式、并发场景,依次问四个问题,就能精准锁定架构与技术方案方向:

1. 人数

这个操作是 “单人” 还是 “多人”?

  • 单人操作(比如用户修改自己的头像):不会有多人写冲突,只可能有重复提交(浏览器重试),用幂等键就够了(如 Redis SETNX)。
  • 多人操作(比如点赞、关注、下单扣库存):有并发争抢、写冲突风险

2. 数据库行数 & 表数

这个操作是 “单行单表数据” 还是 “多行 / 跨表数据”?

  • 单行单表(点赞计数、关注关系):数据库层的唯一索引 + 状态字段可以直接解决。
  • 多行或跨表(下单:扣库存→生成订单→扣钱):数据库行锁或乐观锁(version) 是核心,唯一索引只能防重复记录,但防不了库存超卖。

3. 重试数

错误可以 “直接返回” 还是必须 “排队等待”?也就是业务对失败的容忍度如何?

  • 可以直接返回 “请稍后重试”(如点赞冲突、重复领券):用乐观锁唯一键冲突,返回失败由客户端自行重试。
  • 必须排队串行执行、不能失败丢弃(如秒杀扣库存、限量抢购):用行锁消息队列串行化,牺牲部分吞吐,强保数据不出错。

4. 一致数

业务需要 “强一致性” 还是 “最终一致性”?

  • 强一致性(转账、钱包扣款、资金交易):必须即时数据一致,不能异步兜底,要用分布式事务、行锁、悲观锁,不能靠异步补偿。
  • 最终一致性(普通下单、发积分、返优惠券、日志统计):允许短暂数据不一致,可异步慢慢补齐,优先用MQ 异步、本地消息表、事务消息解耦,提升吞吐与可用性。

情景演练

情景一:关注 / 取关

问题 答案 人数 多人操作(多人同时对同一博主关注/取关,粉丝数共享) 表数 单行单表(核心socials一行,accounts计数的更新属于事务外 MQ) 重试 直接返回成功(重复操作不报错,与幂等一致) 一致性 核心强一致(关系状态原子翻转),计数最终一致(MQ 异步)

分层设计

· DB:UNIQUE(follower_id, vlogger_id) + status 字段;关注用 INSERT … ON DUPLICATE KEY UPDATE status=1,取关用 UPDATE … WHERE status=1,通过 rowsAffected 判断是否状态翻转。计数 SQL 加 GREATEST(follower_count + ?, 0) 防负数。 · Service:rows > 0 才 MQ 发事件(覆盖新关注 rows=1 和重新关注 rows=2),rows=0 直接返回。 · Worker/MQ:消费 MQ social.follow/unfollow 事件更新 accounts.follower_count,用 Redis SETNX 基于事件唯一 ID 防 MQ 短时重投。 · 缓存:无。


情景二:秒杀(1 元抢手机)

问题 答案 人数 多人操作 表数 多行跨表(seckill_stock + seckill_order) 重试 直接返回失败(没抢到就是没抢到,用户自行离开) 一致性 核心强一致(扣库存+生成订单事务),通知等最终一致(MQ)

分层设计

· DB:乐观锁库存 UPDATE … WHERE left_stock > 0 AND version = ?,订单表 UNIQUE(user_id, product_id) 防重。整个事务包裹扣库存和订单插入。 · Service:事务成功则发 MQ 通知,失败直接返回“已抢光”/“已参与”。 · MQ:异步生成支付链接、发积分、推送通知。 · 前端/缓存:无前置缓存,直接读库。


情景三:优惠券领取

问题 答案 人数 多人操作 表数 多行跨表(coupon_batch + user_coupon) 重试 直接返回失败(已领完/已领取) 一致性 核心强一致,附加最终一致

分层设计

· DB:乐观锁 UPDATE … WHERE left_count > 0 AND version = ?,领券记录 UNIQUE(user_id, batch_id) 防重。事务内串联扣库存和插入记录。 · Service:事务后发 MQ 激活优惠券、发通知。 · MQ:异步更新券状态、推送。 · 无 Redis 缓存:无需。


情景四:每日签到

问题 答案 人数 单人操作(每人只改自己的记录) 表数 多行跨表(check_in + user_score) 重试 直接返回“今日已签到” 一致性 核心强一致(事务内写签到记录+积分)

分层设计

· DB:UNIQUE(user_id, date) 防同一天重复签到;积分用 INSERT … ON DUPLICATE KEY UPDATE 原子累加。一步事务完成。 · Service:没有 MQ,同步返回结果。 · 完全无缓存/锁:极简设计。


情景五:实时热门视频榜单

问题 答案 人数 多人操作(多个 Worker 同时可能重建) 表数 跨存储系统(MySQL → Redis),写冲突目标是 Redis 单键 重试 直接跳过(冲突时放弃本次重建,等下一周期) 一致性 写入原子强一致(ZADD 整榜替换),数据同步最终一致(允1分钟延迟)

分层设计

· 读路径:前端直读 Redis Sorted Set ranking:hot,2 万 QPS <1ms。 · 写路径(Worker): · Redis 分布式锁 lock:ranking:rebuild(带过期 UUID 防死锁)保证只有一个 Worker 重建。 · 从 MySQL 查热度分 Top100,计算后 DEL + ZADD 原子替换榜单。 · 更新 version 号让前端判断刷新。 · 无 DB 事务:跨存储无事务,靠分布式锁确保不冲突。


情景六:支付回调幂等处理

问题 答案 人数 单人操作(同一订单无多人竞争) 表数 多行跨表(orders + user_account) 重试 直接返回 success 一致性 强一致(事务),处理允许异步重试(补偿)

分层设计

· DB:新建 pay_notify_log 表,以通知 ID 为主键,作为幂等凭证。事务内 INSERT INTO log …,若冲突直接回滚返回 success。悲观锁 SELECT … FOR UPDATE 锁定订单行,检查状态,若未支付则更新订单、加余额和累计充值。 · Service:纯数据库方案,无 Redis 无 MQ。 · 对账补偿:定时任务拉取支付平台未支付订单补充调用,实现最终一致。 · 无缓存无锁:单人操作,行锁足够。


全景结论

· 无并发或单人:数据库唯一索引/主键 + 事务即可(签到、回调)。 · 多人单表:唯一索引 + 状态字段 + 乐观锁(关注/取关)。 · 多人多表:乐观锁 + 事务 + MQ 异步副作用(秒杀、优惠券)。 · 超高读 + 多写冲突:Redis 缓存 + 分布式锁(热榜)。 · 需要区分失败原因:悲观锁代替乐观锁(支付回调)。

选择乐观锁还是悲观锁呢? 可以看用乐观锁,抢锁是否会出现errmaybe的情况,若出现,大概率用悲观锁,否则首推乐观锁

1. 锁的选型依据:业务并发冲突激烈程度 2. 冲突少、争抢低 → 用乐观锁,几乎无锁争抢报错 3. 冲突剧烈、热点争抢大 → 用悲观锁,极易出现锁抢占异常errmaybe