# 高并发与分布式一致性
用生活化的比喻,让你从"只会用锁"到"能设计秒杀系统和分布式一致方案"
前置知识:第02章 Java 并发编程深度(AQS、锁、内存屏障)、第06章 数据库深度进阶(MVCC、锁)
阅读指南(初学者必看)
为什么你需要学习高并发与分布式一致性?
游戏运营最怕什么?活动上线瞬间,10 万玩家同时涌入:
- 限时皮肤开售 → 10 万人同时抢购 → 库存超卖?
- 赛季结算奖励 → 10 万人同时领取 → 数据库扛不住?
- 服务器集群 → 数据在不同节点 → 怎么保证一致?
学完本章,你能回答:
- 怎么设计一个支持 10 万 QPS 的秒杀系统?
- 分布式锁怎么实现?Redis 和 ZooKeeper 方案各有什么优缺点?
- Raft 算法是怎么保证分布式一致性的?
- 分布式事务有哪些方案?游戏场景怎么选?
本文结构
第一部分:秒杀系统设计(高并发架构实战) 第二部分:分布式锁(跨进程的互斥) 第三部分:分布式一致性算法(Raft) 第四部分:分布式事务(跨服务的数据一致性)
一、秒杀系统设计
生活类比:秒杀就像"春运抢火车票"——海量人同时抢有限资源。
秒杀架构
用户请求 → CDN(静态资源)→ 网关(限流) → 秒杀服务(预扣减)→ 库存服务(Redis 原子扣减) → 订单服务 → MQ(异步)→ 数据库
核心技术点
`java // 1. Redis 原子扣减库存 public boolean deductStock(String itemId) { Long remain = redisTemplate.opsForValue().increment("stock:" + itemId, -1); if (remain < 0) { redisTemplate.opsForValue().increment("stock:" + itemId, 1); return false; } return true; }
// 2. 限流:令牌桶算法 public class RateLimiter { private final long capacity; private final long refillRate; private long tokens; private long lastRefillTime;
public synchronized boolean tryAcquire() {
refill();
if (tokens > 0) {
tokens--;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long newTokens = (now - lastRefillTime) / 1000 * refillRate;
tokens = Math.min(capacity, tokens + newTokens);
lastRefillTime = now;
}
}
// 3. MQ 异步下单(削峰) public class SeckillService { @Autowired private RocketMQTemplate rocketMQTemplate;
public Result seckill(Long userId, Long itemId) {
if (isAlreadyBought(userId, itemId)) return Result.fail("已购买");
if (!deductStock(itemId)) return Result.fail("已售罄");
SeckillMessage msg = new SeckillMessage(userId, itemId);
rocketMQTemplate.convertAndSend("seckill-topic", msg);
return Result.success("排队中");
}
} `
秒杀防作弊
| 作弊手段 | 防御方法 |
|---|---|
| 机器人刷接口 | 验证码 + 接口加密 + 设备指纹 |
| 同一用户多次抢 | Redis Set 去重(SADD) |
| 超卖 | Redis 原子扣减 + 数据库乐观锁 |
| 接口重放 | 请求签名 + 时间戳 + nonce |
二、分布式锁
生活类比:分布式锁就像"会议室预约系统"——多个办公室(进程)的人要抢同一个会议室(资源),需要统一预约。
Redis 分布式锁
`java // 加锁(使用 SET NX EX 原子命令) public boolean tryLock(String key, String value, long expireSeconds) { return redisTemplate.opsForValue() .setIfAbsent(key, value, expireSeconds, TimeUnit.SECONDS); }
// 解锁(Lua 脚本保证原子性) public boolean unlock(String key, String value) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList(key), value); return result != null && result > 0; } `
注意事项:
- value 必须唯一(UUID),防止误解锁别人的锁
- 必须设置过期时间,防止死锁
- 解锁必须用 Lua 脚本,保证"判断+删除"原子性
- Redis 主从切换可能丢锁 → RedLock 算法
ZooKeeper 分布式锁
` ZK 锁原理:临时顺序节点
- 创建临时顺序节点 /lock/lock-0000000001
- 获取 /lock 下所有子节点,排序
- 如果自己是最小节点 → 获取锁
- 如果不是 → 监听前一个节点的删除事件
- 前一个节点删除(释放锁)→ 重新检查
- 会话断开 → 临时节点自动删除 → 不会死锁 `
Redis vs ZooKeeper:
| Redis | ZooKeeper | |
|---|---|---|
| 性能 | 高(内存操作) | 中(网络通信) |
| 可靠性 | 主从切换可能丢锁 | 强一致,不会丢锁 |
| 过期处理 | 自动过期 | 临时节点 + 会话 |
| 推荐 | 性能优先 | 可靠性优先 |
三、Raft 一致性算法
生活类比:Raft 就像"公司董事会决策"——选出董事长(Leader),所有决策由董事长提出,多数人同意就生效。
Raft 三大子问题
`
领导者选举(Leader Election)
- 所有节点初始为 Follower
- 选举超时 → 变为 Candidate → 请求投票
- 获得多数投票 → 成为 Leader
- Leader 定期发心跳维持权威
日志复制(Log Replication)
- 客户端请求 → Leader 写入本地日志
- Leader 将日志复制到 Follower
- 多数确认 → 提交(commit)
- 通知客户端成功
安全性(Safety)
- 选举限制:只有日志最新的节点才能当选
- Leader 完整性:已提交的日志不会丢失
- 状态机安全:所有节点按相同顺序执行相同日志 `
Raft 选举过程
` 时间线: Follower --超时--> Candidate --获多数票--> Leader │ │ │ │ │ ├── 定期发心跳 │ │ └── 接收客户端请求 │ │ │ ├── 增加当前任期 │ ├── 投自己一票 │ └── 请求其他节点投票 │ └── 收到更高任期的消息 → 回退为 Follower
游戏应用:
- 游戏服务器集群用 Raft 保证数据一致
- 如:TiKV(Rust 实现的 KV 存储)、etcd
- 但游戏实时同步(帧同步/状态同步)不用 Raft(延迟太高) `
四、分布式事务
生活类比:分布式事务就像"跨行转账"——你的银行扣钱和对方银行加钱必须同时成功或同时失败。
方案对比
| 方案 | 一致性 | 性能 | 复杂度 | 游戏场景 |
|---|---|---|---|---|
| 2PC | 强一致 | 低 | 中 | 太慢 |
| TCC | 最终一致 | 中 | 高 | 支付场景 |
| Saga | 最终一致 | 高 | 中 | 业务流程 |
| 本地消息表 | 最终一致 | 高 | 低 | 推荐首选 |
| 事务消息 | 最终一致 | 高 | 低 | 有 MQ 支持 |
本地消息表方案(游戏推荐)
本地消息表的核心思想:业务操作和消息写入在同一个本地事务中,然后通过后台线程扫描并发送未发送的消息。
业务操作和消息写入在同一个事务:
- UPDATE player SET gold = gold - 100 WHERE id = 123;
- INSERT INTO outbox_message (...) VALUES (...);
后台线程扫描并发送未发送的消息:
- 发送成功后标记为 SENT
- 消费者幂等处理(防止重复消费)
游戏场景的分布式事务设计
` 游戏充值流程(最关键的分布式事务):
- 创建充值订单(订单服务)
- 第三方支付(支付网关)
- 支付回调通知(支付服务)
- 增加游戏币(游戏服务)
- 发送奖励邮件(邮件服务)
推荐方案:
- 订单服务和支付服务之间:本地消息表
- 支付回调和游戏服务之间:事务消息(RocketMQ)
- 游戏服务和邮件服务之间:异步消息
关键:每个步骤都要幂等!
- 同一笔订单不会重复扣款
- 同一个回调不会重复加币
- 同一封邮件不会重复发送 `
自问自答
Q1:游戏为什么不用 2PC(两阶段提交)?
2PC 需要锁住资源等所有参与者准备,延迟太高(100ms+)。游戏要求毫秒级响应,用最终一致性方案更适合。
Q2:Redis 分布式锁可靠吗?
单机 Redis 锁在主从切换时可能丢失。对可靠性要求高的场景用 RedLock(多节点)或 ZooKeeper。游戏场景用 Redis 足够(锁丢失概率极低)。
Q3:Raft 和 Paxos 有什么区别?
Raft 是 Paxos 的简化版,更容易理解和实现。功能等价,游戏场景用 Raft 就够了。
Q4:怎么保证接口幂等?
每个请求带唯一 ID(如订单号),服务端用 Redis/数据库记录已处理的 ID。收到重复 ID 直接返回之前的结果。
Q5:秒杀系统怎么压测?
用 Locust/JMeter 模拟 10 万并发。先测 Redis 单独能力,再测完整链路。注意预热(JIT 编译、连接池初始化)。
实践任务
- 实现秒杀接口:Redis 原子扣减 + MQ 异步下单 + 接口限流
- 实现分布式锁:Redis SET NX EX + Lua 解锁,测试并发安全性
- Raft 模拟:实现简化版 Raft 选举过程(3 个节点)
- 本地消息表:实现充值流程的分布式事务,保证幂等
- 压测:用 Locust 压测秒杀接口,目标 1 万 QPS
与其他章节的关联
| 本节内容 | 关联章节 | 关联点 |
|---|---|---|
| 并发控制 | 第02章 Java 并发编程深度 | 分布式锁是 JVM 锁的延伸 |
| 数据库事务 | 第06章 数据库深度进阶 | MVCC 和分布式事务的关系 |
| 消息队列 | 第08章 RPC 框架与微服务通信 | MQ 是分布式事务的基础 |
| 服务器架构 | 第11章 游戏服务器架构 | 架构设计需要考虑一致性 |
| 容器化 | 第15章 云原生与容器化 | 分布式锁在 K8s 中的实践 |
上一章:09-JVM调优实战 | 下一章:11-游戏服务器架构