单线程游戏服务器+异步数据库操作

前段时间我们游戏服务器的开发环境升级到了Java8,这两天我又把服务器的线程模型重新设计了一下,用上了Lambda表达式。Lambda表达式确实能够大幅简化Java代码,特别是丑陋不堪的匿名内部类,这篇文章主要就是想和大家分享这一点。

线程模型

首先简单介绍一下我们游戏服务器的线程模型,大致如下图所示:

Netty线程池只处理消息的收发,当Netty收到消息之后,会交给游戏逻辑线程处理。由于是单线程在处理游戏逻辑,所以每一个消息必须很快处理完,也就是说,不能有数据库等耗时操作,不然逻辑线程很可能会被卡住。为了不卡住逻辑线程,数据库操作由单独的线程池来处理。逻辑线程发起数据库操作后便立即返回继续处理其他消息,数据库线程池处理完毕后,再通知逻辑线程,从而达到了异步数据库操作的效果。

GameAction

Netty部分的网络代码,在收到消息后,会根据消息找到对应的Action,然后执行。具体代码省略,下面是简化后的GameAction的代码:

public abstract class GameAction {/*** 在逻辑线程里处理消息.* @param gs* @param req 请求消息*/public void execute(GameSession gs, Object req) {GameLogicExecutor.execute(() -> {doExecute(gs, req);});}// 子类实现public abstract void doExecute(GameSession gs, Object req);}execute()方法里,使用了Lambda表达式来实现Runnable接口。GameLogicExecutor

GameLogicExecutor是游戏逻辑执行线程,代码如下所示:

import java.util.concurrent.Executor;import java.util.concurrent.Executors;/** * 游戏逻辑线程. */public class GameLogicExecutor {// todo 限制队列长度private static final Executor executor = Executors.newSingleThreadExecutor();/*** 把游戏逻辑放到队列.* @param gameLogic*/public static void execute(Runnable gameLogic) {executor.execute(gameLogic);}}GetPlayerListAction

下面看一个具体的GameAction实现,这个Action根据用户ID返回用户创建的玩家列表,代码如下:

import java.util.List;public class GetPlayerListAction extends GameAction {@Overridepublic void doExecute(GameSession gs, Object req) {int userId = (Integer) req;PlayerDao.getPlayerList(userId, (List<Player> players) -> {gs.write(players);});}}假设请求参数是玩家ID,doExecute()方法并没有等待数据库操作,而是马上就返回了。传递给DAO的回调对象是个Lambda表达式,在回调方法里,玩家列表通过GameSession被写到客户端(这只是演示,实际的响应消息可能是protobuf或JSON)。PlayerDaoimport java.util.ArrayList;import java.util.List;public class PlayerDao {public static void getPlayerList(int userId, DbOpCallback<List<Player>> cb) {DbOpExecutor.execute(() -> {try {List<Player> players = getPlayerList(userId);cb.ok(players);} catch (Exception e) {cb.fail(e);}});}// 耗时的数据库操作private static List<Player> getPlayerList(int userId) {return new ArrayList<>();}}

getPlayerList()方法接收两个参数,第一个参数是用户ID,第二个参数是个callback,当数据库操作完毕(成功或失败)时,会通知这个callback。DAO内部使用的可能是JDBC或MyBatis等,总之是耗时的操作,由数据库线程池执行。

DbOpExecutor

DbOpExecutor的代码比较简单,和GameLogicExecutor相似,如下所示:

import java.util.concurrent.Executor;import java.util.concurrent.Executors;/** * 数据库操作线程池. */public class DbOpExecutor {// todo 根据cpu数等确定线程数private static final Executor executor = Executors.newFixedThreadPool(4);/*** 把数据库操作放进队列.* @param dbOp*/public static void execute(Runnable dbOp) {executor.execute(dbOp);}}

DbOpCallback

最后看一下DbOpCallback代码:

/** * 数据库操作回调. * @param <T> */@FunctionalInterfacepublic interface DbOpCallback<T> {/*** 处理数据库返回结果.* @param result*/void handleResult(T result);/*** 数据库操作正常结束.* @param result*/default void ok(T result) {// 在游戏逻辑线程里处理结果GameLogicExecutor.execute(() -> {try {handleResult(result);} catch (Exception e) {// todo 处理异常}});}/*** 数据库操作出现异常.* @param e*/default void fail(Exception e) {// todo 处理异常}}@FunctionalInterface说明这是一个函数式接口,简单的说,就是只有一个抽象方法的接口。接口的default方法是Java8的新语法,具体请参考Java8相关方面的资料。ok()方法确保数据库操作的结果是在逻辑线程里处理。结论

上面的代码,在Java8之前用匿名内部类也是可以写的,只是相比Lambda表达式,更加冗长丑陋而已。另外要注意,上面的代码只是简化后的示例代码,并非真实代码。如果想将上面的代码应用到自己的项目中,,还请注意异常的处理。

而做人的能力则会给你一百种机会。

单线程游戏服务器+异步数据库操作

相关文章:

你感兴趣的文章:

标签云: