游戏服务器的并发挑战
概述
游戏服务器是典型的高并发有状态服务:海量玩家同时在线,每个玩家的状态不断被读写,且大量操作需要原子性。并发问题的核心不是"怎么加锁",而是先评估并发量、再设计无锁路径、最后才用锁。本文从并发量评估出发,讲线程安全设计原则、锁粒度控制与 CAS 无锁编程在游戏服务器中的应用。
一、请求并发量评估
1.1 评估模型
并发量估算:
在线人数 × 人均操作频率 = 总请求 QPS
示例:
1 万人在线 × 每 10 秒 1 次操作 = 1000 QPS
十万人在线 × 每 5 秒 1 次操作 = 2 万 QPS
大型活动(冲榜)峰值 × 3-5 倍
关注峰值而非均值:
晚上 8-10 点为在线高峰
开服/活动开启瞬间有流量尖峰
用峰值规划容量1.2 并发热点
并发压力往往集中在少数热点:
热点类型:
单玩家状态(背包/货币)→ 该玩家串行即可
全局资源(全服排行榜/全服活动进度)→ 高并发写热点
共享服务(匹配池/房间分配)→ 跨玩家共享状态
对策:
玩家维度数据 → 按玩家分片,串行处理
全局热点 → 异步化 + 合并写 + 缓存
共享状态 → 分区/分片,避免单点二、线程安全设计原则
2.1 分区串行(核心原则)
游戏服务器最实用的并发模型是按玩家分区串行:同一玩家的所有操作都在同一线程执行,玩家之间互不干扰,从根本上消除锁竞争。
分区串行:
玩家 ID → Hash → 固定线程(EventExecutorGroup)
该玩家所有消息在同一线程处理
玩家状态无需加锁(单线程访问)
适用:
Netty 消息分发(见消息分发章节)
Akka Actor(每 Actor 单线程邮箱)// 玩家绑定线程示例(Netty)
DefaultEventExecutorGroup bizGroup = new DefaultEventExecutorGroup(16);
pipeline.addLast(bizGroup, new PlayerMsgHandler());
// 同一玩家消息由相同 executor 处理(hash 到同一线程)2.2 不可变与局部变量
线程安全设计:
优先不可变对象(final + 不暴露 setter)
无状态服务(只依赖参数)
局部变量优先(栈私有)
共享可变状态最少化设计倾向:
请求处理不修改全局 → 无需锁
数据快照传递 → 避免共享引用
写操作收敛到单一入口 → 好加锁/好审计2.3 可见性
共享变量的修改必须保证可见性:
可见性工具:
volatile:单一字段可见性
synchronized:原子性 + 可见性
ConcurrentHashMap 等并发容器
Atomic* 原子类
常见错误:
无 volatile 的共享布尔标志 → 可能永不生效
HashMap 被多线程读(无写)→ 尚可,但不推荐三、锁粒度控制
3.1 锁粒度原则
锁粒度选择:
粗锁(同步方法)→ 实现简单,但竞争激烈
细锁(分段/对象锁)→ 竞争小,但容易出问题
原则:
能锁数据不锁代码
能锁单对象不锁类
能分区不锁全局
锁内只做最小操作(IO 移出锁外)3.2 分级锁示例
全局锁 → 分片锁:
排行榜更新:整榜锁 → 按分数段锁/合并写
房间操作:全局房间锁 → 每房间一个锁
对象锁替代类锁:
// 反例:整个类锁,所有玩家互相阻塞
synchronized (RoomManager.class) { ... }
// 正例:按房间 ID 锁,互不干扰
Room room = rooms.get(roomId);
synchronized (room) { ... }锁内最小化:
锁内只做内存状态修改
数据库/网络操作移到锁外(或异步化)
减少持锁时间 = 减少等待3.3 分布式锁
单机锁只保护单进程,跨服共享资源需要分布式锁(Redis/Redisson):
适用场景:
定时任务防重复执行(见每日刷新章节)
跨服排行榜结算
全局唯一资源发放
要点:
锁必须带超时(防死锁)
看门狗续期(Redisson)
业务幂等兜底(锁不是万能的)四、无锁编程 CAS
4.1 CAS 原理
CAS(Compare And Swap):
比较期望值 → 相同则交换新值(原子)
不同则失败(自旋重试)
Java 实现:
AtomicInteger/AtomicLong/LongAdder
Unsafe.compareAndSwap*
适用:
高频计数器(在线人数、生成 ID)
单字段状态更新(房间状态 CAS 切换)
乐观并发控制// 计数器 CAS 示例
private final AtomicLong onlineCount = new AtomicLong();
public void onLogin() {
onlineCount.incrementAndGet(); // 无锁自增
}4.2 CAS 状态机
游戏状态流转常用 CAS 保证一次成功切换:
// 房间状态机 CAS(见房间系统章节)
if (room.compareAndSetStatus(WAITING, READY)) {
// 只有 WAITING → READY 成功才继续
}CAS 注意事项:
ABA 问题(值被改回原值)→ 版本号/引用
自旋竞争激烈时性能下降 → 降级为锁或分片
只能保护单字段 → 多字段用锁或版本对象4.3 LongAdder 与并发容器
并发计数场景:
AtomicLong 高并发下自旋 → 用 LongAdder(分段计数)
读多写少 → 并发容器优于手动锁
常用并发容器:
ConcurrentHashMap:分段锁/无锁读
CopyOnWriteArrayList:读多写少
ConcurrentLinkedQueue:无锁队列游戏内典型用法:
在线人数 → LongAdder
玩家会话表 → ConcurrentHashMap
消息队列 → 有界阻塞队列/Disruptor(无锁 RingBuffer)五、实现要点
设计清单:
先评估 QPS 与热点,再谈并发方案
玩家维度 → 分区串行(首选)
全局热点 → 异步合并 + 缓存
需要锁 → 粒度尽量细、锁内最小化
高频计数/状态切换 → CAS 无锁
跨服共享 → 分布式锁 + 幂等常见坑:
分区不均(Hash 倾斜)→ 一致哈希/虚拟节点
锁内做 IO → 锁竞争爆炸
读共享可变数据不加同步 → 可见性问题
分布式锁无超时 → 死锁与其他系统衔接:
分区串行 → 消息分发章节
CAS 状态机 → 房间系统
分布式锁 → Redis 缓存章节
并发容器 → 网络层 Session 管理