Java8 Lambda表達應用 -- 單線程遊戲server+異步數據庫操做

前段時間咱們遊戲server升級到開發環境Java8,這些天,我再次server的線程模型再次設計了一下,耗費Lambda表情java

LambdaJava代碼。特別是醜陋不堪的匿名內部類,這篇文章主要就是想和你們分享這一點。數據庫

線程模型

首先簡介一下咱們遊戲server的線程模型。大體例如如下圖所看到的:網絡

Netty線程池僅僅處理消息的收發,當Netty收到消息以後。會交給遊戲邏輯線程處理。由於是單線程在處理遊戲邏輯,因此每一個消息必須很是快處理完。也就是說,不能有數據庫等耗時操做。否則邏輯線程很是可能會被卡住。爲了避免卡住邏輯線程,數據庫操做由單獨的線程池來處理。異步

邏輯線程發起數據庫操做後便立刻返回繼續處理其它消息。數據庫線程池處理完成後。再通知邏輯線程,從而達到了異步數據庫操做的效果。ide

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返回用戶建立的玩家列表。代碼例如如下:code

import java.util.List;

public class GetPlayerListAction extends GameAction {

    @Override
    public void doExecute(GameSession gs, Object req) {
        int userId = (Integer) req;
        PlayerDao.getPlayerList(userId, (List<Player> players) -> {
            gs.write(players);
        });
    }
    
}
若是請求參數是玩家ID。doExecute()方法並無等待數據庫操做。而是當即就返回了。傳遞給DAO的回調對象是個Lambda表達式,在回調方法裏,玩家列表經過GameSession被寫到client( 這僅僅是演示,實際的響應消息多是protobuf或JSON)。

PlayerDao

import 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等,總之是耗時的操做,由數據庫線程池運行。server

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>
 */
@FunctionalInterface
public 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表達式,更加冗長醜陋而已。另外要注意,上面的代碼僅僅是簡化後的演示樣例代碼,並非真實代碼。

假上面的代碼設想到本身的項目,還須要注意的是異常處理。

相關文章
相關標籤/搜索