游戏服务器使用actor模型
1.Actor 模型简介与并发优势
Actor 模型是一种基于 “并发实体(Actor)” 的分布式并发计算模型,由计算机科学家 Carl Hewitt 于 1973 年提出,旨在解决传统多线程并发编程中的锁竞争、死锁等复杂问题。其核心思想是:将系统中的所有并发单元抽象为 “Actor”,每个 Actor 是独立的计算实体,仅通过 “消息传递” 交互,不共享状态。
1.1.Actor 模型的核心概念
要理解 Actor 模型,需先掌握其 3 个核心组成部分,这也是它与传统多线程模型的本质区别:
1.1.1. 核心实体:Actor
Actor 是模型的最小并发单元,可理解为 “具有状态、行为和消息邮箱的独立个体”,每个 Actor 具备 3 个关键能力:
- 维护私有状态:Actor 的状态完全私有,仅能通过自身行为修改(外部无法直接访问或修改),从根源杜绝 “共享状态竞争”(传统多线程的核心痛点)。
- 处理消息:Actor 通过 “接收消息” 触发行为(如计算、修改自身状态、发送新消息),消息处理是串行的(同一时间仅处理一条消息),无需加锁
1.1.2. 交互方式:异步消息传递
Actor 之间不直接调用方法,而是通过异步、无阻塞的消息传递交互,消息具有以下特性:
- 异步性:发送方发送消息后无需等待接收方处理,可立即继续执行自身逻辑(类似现实中的 “发邮件”,无需等对方回复)。
- 可靠性:模型通常保证 “消息按发送顺序投递”(FIFO),且不丢失(具体需依赖实现框架的保障)。
- 无共享:消息是 Actor 间唯一的交互媒介,不存在 “共享内存”,因此无需考虑锁、信号量等同步机制。
1.1.3. 消息邮箱:Actor 的 “消息队列”
每个 Actor 都有一个消息邮箱(Mailbox),用于缓存接收的消息:
- 外部发送的消息会先进入邮箱,Actor 按 “先进先出” 顺序依次处理(避免并发修改状态)。
- 若 Actor 处理消息耗时较长,新消息会在邮箱中排队,不会阻塞发送方或其他 Actor。
2、传统线程性能瓶颈
2.1.线程执行“冷热不均”
@Overridepublic void accept(BaseGameTask task) {if (task == null) {throw new NullPointerException("task is null");}if (!running.get()) {return;}int distributeKey = (int) (task.getDispatchKey() % workerPool.length);workerPool[distributeKey].receive(task);}
服务器线程模型会把客户端的IO事件封装成一个任务,这个任务根据某种hash算法进行映射,例如根据session自增长id,或者根据玩家id作hash,或者根据地图场景id作hash。无论何种策略,都无法做到每条线程一样的任务量,比如,战斗场景的任务远远高于普通场景的任务。如此,便出现了每条线程“冷热不均”。而actor作为线程的基本执行单元,不同的actor依次被线程组的线程单独执行,如此,可保证线程“近乎公平”。如图所示(绿色方块代表采样点是否处于执行状态,绿色方块均匀分布):
5000个机器人模拟玩家在线,疯狂发数据,总体而言,线程冷热均衡
2.2.业务线程分类过多
有些游戏服务器会根据功能对线程池进行分组,例如分为业务线程池,战斗线程池,聊天线程池。每一组线程池都有大量的线程,这样虽然可以隔离任务,但导致的严重后果就是线程切换额外消耗。而actor线程池不区分模块也可保证任务均匀执行,可减少线程的总数量。
3.代码实现
3.1.Actor
Actor, 是线程执行的基本单元。一个Actor代表一个对象,拥有一个邮箱,用于接收消息。
public interface Actor extends Runnable {/*** 绑定的邮箱*/Mailbox getMailBox();/*** 发送消息到当前Actor*/default void tell(Mail message) {tell(message, null);}/*** 发送消息到当前Actor** @param message 消息* @param sender 发送者*/default void tell(Mail message, Actor sender) {Objects.requireNonNull(message);message.setSender(sender);Mailbox mailBox = getMailBox();if (mailBox != null) {mailBox.receive(message);}}/*** 获取Actor模型名称,例如player, monster, guild等*/default String getModel() {return getClass().getSimpleName();}}
3.2.基础Actor
为Actor接口提供基础实现
public class AbsActor implements Actor {protected final Logger logger = LoggerFactory.getLogger(getClass());/*** 任务堆积警戒值,超过这个值会预警*/static int THRESHOLD = 100;/*** 单次处理最大任务数,防止其他任务饥饿*/static int MAX_TASKS_PER_RUN = 50;/*** 绑定的邮箱*/private Mailbox mailBox;/*** actor名称/路径*/private final String actorPath;/*** 当前任务是否在parent队列里*/private AtomicBoolean queued = new AtomicBoolean(false);
}
需要注意的是,由于java不支持多继承,继承该类后,便无法继承其他类,若需要继承其他类,建议采用组合模式,把该类作为一个属性。例如:
@Entity(name = "playerent")
@Getter
@Setter
public class PlayerEnt extends BaseEntity<String> {/*** actor邮箱*/@Setterprivate Actor actor;}
消息接收
Actor实现Runnable接口,但一开始它是不会被任意线程执行的,只有当系统往actor的邮箱里投放邮件,这个actor才开始丢到actor线程池这个一级队列,代码如下:
@Overridepublic void tell(Mail message, Actor sender) {Objects.requireNonNull(message);if (!actorSystem.running.get()) {return;}message.setSender(sender);message.setReceiver(this);Mailbox mailBox = getMailBox();mailBox.receive(message);// 丢到线程池运行if (queued.compareAndSet(false, true)) {actorSystem.accept(this);}}
当线程池里的任务(Actor)执行时,如果actor的任务全部执行完毕,那么这个actor的任务就暂时告一段落里,线程池也不会有这个Actor(除非后续继续调用Actor#tell方法)。当Actor的任务非常多,一次性执行的话,可能导致其他任务无法按时完成任务(饥饿),会暂时终止此次Actor的执行(有点类似Thread#yield()方法)
/*** 负责遍历和调度Mailbox中的Mail, 原子性执行,不会出现并发问题*/@Overridepublic void run() {// 防止任务一直占线int size = mailBox.getTaskSize();if (size > THRESHOLD) {logger.warn("[{}]任务堆积严重,任务数量[{}]", actorPath, size);}try {// 限制单次处理任务数量,防止饥饿int processedCount = 0;Runnable mail;while ((mail = mailBox.poll()) != null && processedCount < MAX_TASKS_PER_RUN) {mail.run();processedCount++;}} catch (Exception e) {logger.error("[{}]任务执行异常", actorPath, e);} finally {queue.set(false);// 如果还有任务,重新加入队列if (!mailBox.isEmpty()) {if (queued.compareAndSet(false, true)) {actorSystem.accept(this);}}}}
3.3.邮箱
每个 Actor 都有自己的邮箱(Mailbox),Actor 通过接收消息(Mail)来执行任务。
邮箱本质上是一个“二级队列”。邮箱根据种类可以分成多种,例如:
无界邮箱:可以存储无限的邮件
有界邮箱:可以存储有限的邮件,溢出的邮件根据策略,可能是丢弃,也可能是阻塞
优先邮箱:投递到邮箱的邮件可以设置一个优先级,高优先级的邮件会被优先执行。
* 邮箱* 一级任务队列为线程池 {@link ActorThreadModel#threadPool}* actor模型里的邮箱, 邮箱相当于一个二级队列, 当{@link ActorThreadModel#threadPool}的每一个任务被执行时,该邮箱的任务会按顺序串行执行* 绝对不存在同一个actor的邮箱被多个线程同时执行,保证了线程安全* 需要注意的是,同一个actor的邮箱在同一时刻只会被一个线程执行,但在不同时刻,有可能在不同的线程执行*/
public class Mailbox {protected BlockingQueue<Mail> mails;public Mailbox() {this.mails = new ArrayBlockingQueue<>(512);}public Mailbox(BlockingQueue<Mail> mails) {this.mails = mails;}public void receive(Mail mail) {if (!this.mails.offer(mail)) {throw new IllegalStateException("mail box queue is full");}}/*** 获取当前邮件数量*/public int getTaskSize() {return mails.size();}public boolean isEmpty() {return mails.isEmpty();}public Mail poll() {return mails.poll();}}
3.4.邮件Mail
邮件是任务执行的基本单元,每次Actor执行的时候,会有若干个Mail被执行。
/*** 邮件抽象基类* 所有投递到Actor邮箱的消息都应该继承此类*/
public abstract class Mail extends BaseTask {/*** 邮件创建时间*/protected final long createdTime;/*** 发送者(可选,用于追踪)*/protected Actor sender;/*** 接收者(可选,用于追踪)*/protected Actor receiver;public Mail() {this.createdTime = System.currentTimeMillis();}/*** 邮件处理逻辑,子类必须实现* 负责执行具体的业务逻辑*/@Overridepublic abstract void action();}
3.5.Actor 线程池
Actor 线程池,是一个“一级任务队列”(rootQueue),可直接使用JDK提供的线程池即可。
NamedThreadFactory threadFactory = new NamedThreadFactory("message-business");threadPool = new ThreadPoolExecutor(coreSize, coreSize, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), threadFactory);
Actor任务执行的机制如下:
rootQueue (一级队列) → Mailbox (二级队列) → Mail.run() (具体任务)
- rootQueue: 全局任务调度,平衡负载
- Mailbox: Actor级别的消息缓冲,避免阻塞
- Mail.run(): 直接执行,减少上下文切换
3.6.ActorSystem
负责actor调度,创建与管理。
public class ActorThreadModel implements ThreadModel { public Actor createActor(String actorPath) {if (actors.containsKey(actorPath)) {throw new IllegalArgumentException("Actor already exists: " + actorPath);}AbsActor actor = new AbsActor(this, actorPath, systemConfig);actors.put(actorPath, actor);logger.debug("Created actor: {}", actorPath);return actor;}public Actor getActor(String actorPath) {return actors.get(actorPath);}/*** 移除指定的actor** @param actorPath actor路径*/public void removeActor(String actorPath) {Actor removed = actors.remove(actorPath);if (removed != null) {logger.debug("Removed actor: {}", actorPath);}}/*** 获取或创建actor** @param actorPath actor路径* @return actor*/public Actor getOrCreateActor(String actorPath) {return actors.computeIfAbsent(actorPath, path -> {AbsActor actor = new AbsActor(this, path, systemConfig);logger.debug("Auto-created actor: {}", path);return actor;});}
}
4.常见问题
4.1.为什么 Mail 和 Actor 都有 run() 方法?
A: 这是两级队列架构的设计。 Actor#run() 负责遍历和调度消息,Mail.run() 负责执行具体的业务逻辑。
Actor使用Mailbox才保存任务,mailbox只代表一个二级队列,如果Actor没有run()方法, Actor线程池的任务总线,很难发挥作用。
ActorThreadPool调度分发 →不同Actor均衡投递到每条业务线程→Actor.run() → 遍历内部Mail → 调用Mail.run()
4.2.共享Actor的作用
工具库里有一个,称为SharedActor的类,如下:
public class SharedActor {/*** 由若干个Actor组成一个集体*/private final Actor[] group;public SharedActor(Actor[] group) {this.group = group;}/*** 根据key获取共享Actor** @param key* @return*/public Actor getSharedActor(long key) {int index = Math.abs((int) (key % group.length));return group[index];}/*** 获取工作线程数量*/public int getWorkerSize() {return group.length;}/*** 获取邮箱组*/public Actor[] getGroup() {return group;}}
这个对象,可以视作一种逻辑主体,例如,玩家在正式登录成功后,会将请求绑定到具体的Actor对象,但在绑定之前,这些任务,需要绑定到一个逻辑主体,并且能够保证线程安全。这就是共享Actor的作用了。
手游服务端开源框架系列完整的代码请移步github ->> jforgame