# 高并发与分布式一致性

用生活化的比喻,让你从"只会用锁"到"能设计秒杀系统和分布式一致方案"

前置知识:第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; } `

注意事项:

  1. value 必须唯一(UUID),防止误解锁别人的锁
  2. 必须设置过期时间,防止死锁
  3. 解锁必须用 Lua 脚本,保证"判断+删除"原子性
  4. Redis 主从切换可能丢锁 → RedLock 算法

ZooKeeper 分布式锁

` ZK 锁原理:临时顺序节点

  1. 创建临时顺序节点 /lock/lock-0000000001
  2. 获取 /lock 下所有子节点,排序
  3. 如果自己是最小节点 → 获取锁
  4. 如果不是 → 监听前一个节点的删除事件
  5. 前一个节点删除(释放锁)→ 重新检查
  6. 会话断开 → 临时节点自动删除 → 不会死锁 `

Redis vs ZooKeeper:

Redis ZooKeeper
性能 高(内存操作) 中(网络通信)
可靠性 主从切换可能丢锁 强一致,不会丢锁
过期处理 自动过期 临时节点 + 会话
推荐 性能优先 可靠性优先

三、Raft 一致性算法

生活类比:Raft 就像"公司董事会决策"——选出董事长(Leader),所有决策由董事长提出,多数人同意就生效。

Raft 三大子问题

`

  1. 领导者选举(Leader Election)

    • 所有节点初始为 Follower
    • 选举超时 → 变为 Candidate → 请求投票
    • 获得多数投票 → 成为 Leader
    • Leader 定期发心跳维持权威
  2. 日志复制(Log Replication)

    • 客户端请求 → Leader 写入本地日志
    • Leader 将日志复制到 Follower
    • 多数确认 → 提交(commit)
    • 通知客户端成功
  3. 安全性(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
  • 消费者幂等处理(防止重复消费)

游戏场景的分布式事务设计

` 游戏充值流程(最关键的分布式事务):

  1. 创建充值订单(订单服务)
  2. 第三方支付(支付网关)
  3. 支付回调通知(支付服务)
  4. 增加游戏币(游戏服务)
  5. 发送奖励邮件(邮件服务)

推荐方案:

  • 订单服务和支付服务之间:本地消息表
  • 支付回调和游戏服务之间:事务消息(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 编译、连接池初始化)。


实践任务

  1. 实现秒杀接口:Redis 原子扣减 + MQ 异步下单 + 接口限流
  2. 实现分布式锁:Redis SET NX EX + Lua 解锁,测试并发安全性
  3. Raft 模拟:实现简化版 Raft 选举过程(3 个节点)
  4. 本地消息表:实现充值流程的分布式事务,保证幂等
  5. 压测:用 Locust 压测秒杀接口,目标 1 万 QPS

与其他章节的关联

本节内容 关联章节 关联点
并发控制 第02章 Java 并发编程深度 分布式锁是 JVM 锁的延伸
数据库事务 第06章 数据库深度进阶 MVCC 和分布式事务的关系
消息队列 第08章 RPC 框架与微服务通信 MQ 是分布式事务的基础
服务器架构 第11章 游戏服务器架构 架构设计需要考虑一致性
容器化 第15章 云原生与容器化 分布式锁在 K8s 中的实践

上一章:09-JVM调优实战 | 下一章:11-游戏服务器架构