Java游戏服务器架构的并发问题及解决方案

问题一:当A,B同时攻击C时,需要对C进行减血逻辑。如果A,B是在不同线程执行这个逻辑的,那么会引发C的血量异常问题。

解决方案:
将减血逻辑放在一个单独的线程执行。具体操作为,首先,创建一个MainMsgProcessor的单例工具类

public class MainMsgProcessor {

    static final MainMsgProcessor instance = new MainMsgProcessor();

    private MainMsgProcessor(){

    }

    public static MainMsgProcessor getInstance(){
        return instance;
    }

    public void process(){
        //消息处理逻辑
    }

}

我们用这个类统一处理消息,由他接管Netty的多线程handler处理消息。
怎么使用单线程处理呢?这里可以使用Java util concurrent包下面的

private final ExecutorService es = Executors.newSingleThreadExecutor();

创建一个单线程的线程池,将消息逻辑交由这个线程处理

因此process方法修改如下

public void process(){
        es.submit(()->{
            //处理消息逻辑
        });
    }

这样对业务逻辑的处理都会被交给这个单线程的线程池来处理。这样既避免了线程不安全的问题,又避免了业务逻辑与线程逻辑交杂的混乱。因为对于这个问题,我们可以在扣血的方法加上synchronized关键字,但是这样一是线程争夺锁导致效率降低,二是业务逻辑与线程安全逻辑混合,增加了编码的复杂度

线程池的内部是一个阻塞队列,因此这个问题的总体抽象如下图所示

Java游戏服务器架构的并发问题及解决方案_第1张图片
可能有的人会有这样的疑问:将所有消息处理逻辑放在单线程,会不会导致并发量太低,速度慢?其实不然,因为游戏服务器速度的瓶颈都出现在IO和网络请求,或者是内存占用率太高,在业务逻辑的计算这一块单线程不见得会比多线程慢

问题二:在解决了上述问题之后,如果我们的登陆操作的消息处理也被放入同一个线程,那么每次用户的登陆请求都涉及到对数据库的访问,会导致其他请求被阻塞

解决方案:对IO实行多线程管理。IO操作被放入其他线程当作一个异步操作

我们首先定义一个静态的单例异步操作处理器,同样,我们也给他一个单线程的线程池

public final class AsyncOperationProcessor {

    static final AsyncOperationProcessor instance = new AsyncOperationProcessor();


    private AsyncOperationProcessor(){}

    private final ExecutorService es = Executors.newSingleThreadExecutor();

    public static AsyncOperationProcessor getInstance(){
        return instance;
    }

    /**
     * 执行异步操作
     *
     * 
     * @param r
     */
    public void process(Runnable r){
        if(r==null){
            return;
        }
        es.submit(r);
    }
}

这样,我们可以把登陆部分的代码逻辑封装入这个类中,让登陆逻辑异步执行

AsyncOperationProcessor.getInstance().process(()->{
            //登陆逻辑
        });

当我们执行完登陆逻辑的时候,我们还需要拿到登陆逻辑的返回结果,这也就是所谓的异步:执行完了之后会将结果交到你的手中。那么,我们如何拿到返回结果呢?在这个例子中,假如我们登陆操作返回了一个user对象,我们怎样拿到这个对象呢?这也是异步调用的通用问题

首先,第一种很容易想到的思路是,在外部定义一个变量,然后执行完之后将结果赋予这个外部的变量。

public User userLogin(String username,String password){
	User user = null;
	AsyncOperationProcessor.getInstance().process(()->{
	            //登陆逻辑
				user = myUser
	        });
	return user;
}

这个思路是行不通的,因为process是在另一个线程中执行的,也就是说,当前线程并不会等另一个线程全部执行完才往下运行,所以我们返回的user很有可能是登陆逻辑没有执行完全的user,也就是一个null

解决这个问题的正确做法之一是,我们不能指望从userLogin这个方法立刻拿到返回值,而是传入一个回调函数,当我们计算出结果的时候,将结果作为参数传给这个回调函数并调用它。

public void userLogin(String username,String password,
Function<User,void> callback){
	User user = null;
	AsyncOperationProcessor.getInstance().process(()->{
	            //登陆逻辑
	            if(callback!=null){
	            	callback.apply(myUser)
	            }
	        });
}

那么我的userLogin调用的过程中,传入的参数应该是这样的

LoginService.getInstance().userLogin(username,password,(user)->{
	//当异步过程执行完后,会将计算结果作为user 传入
	//登陆完毕的后续操作代码
}
)

Java游戏服务器架构的并发问题及解决方案_第2张图片

这样就算完美了吗?其实不然,因为我们的回调函数实际上是在我们的异步线程中被调用的,也就是说,业务逻辑也是在异步线程中执行的,而这又会引发数据的脏读写问题,而我们理想的情况应该是,异步线程将执行完毕的结果传给我们的消息处理线程,我们用那个线程来执行业务逻辑。

那么,针对这个问题的解决方案是什么呢?
首先,我们定义一个接口 :

public interface IAsyncOperation {
    /**
     * 执行异步操作
     */
    void doAsync();


    /**
     * 执行异步操作完成之后的逻辑
     * 可以不实现
     */
    default void doFinish(){}
}

我们对我们上面的异步处理器类的process方法进行修改

public final class AsyncOperationProcessor {

    static final AsyncOperationProcessor instance = new AsyncOperationProcessor();


    private AsyncOperationProcessor(){}

    private final ExecutorService es = Executors.newSingleThreadExecutor();

    public static AsyncOperationProcessor getInstance(){
        return instance;
    }

    /**
     * 执行异步操作
     *
     *
     * @param op
     */
    public void process(IAsyncOperation op){
        if(op==null){
            return;
        }
        es.submit(()->{
            //执行异步逻辑
            op.doAsync();
            //执行完成逻辑
            op.doFinish();
        });
    }
}

同时,我们在MainMsgProcessor类中加入一个重载的process方法

public class MainMsgProcessor {

    static final MainMsgProcessor instance = new MainMsgProcessor();

    private final ExecutorService es = Executors.newSingleThreadExecutor();

    private MainMsgProcessor(){

    }

    public static MainMsgProcessor getInstance(){
        return instance;
    }

    public void process(){
        es.submit(()->{
            //处理消息逻辑
        });
    }

    public void process(Runnable r){
        if(r==null){
            return;
        }

        es.submit(r);
    }
}

随后,我们更改AsyncOperationProcessor的process方法,将doFinish提交给主线程去写

/**
     * 执行异步操作
     *
     *
     * @param op
     */
    public void process(IAsyncOperation op){
        if(op==null){
            return;
        }
        es.submit(()->{
            //执行异步逻辑
            op.doAsync();
            //执行完成逻辑
            MainMsgProcessor.getInstance().process(()->{
                op.doFinish();
            });
            
        });
    }

这样一来,我们只需要实现这个接口,将登陆逻辑放入doAsync,而将完成登陆后回调放在doFinish中执行。
Java游戏服务器架构的并发问题及解决方案_第3张图片
注:AsyncGetUserEntity为实现IAsyncOperation接口的类,其中的doAsync执行的是登陆逻辑

问题三:这样一来虽然将登陆放到别的线程中执行,那么阻塞的问题不是依然没有解决吗?因为IO线程只有一个

解决方案:在线程池中多开辟几个线程

问题四:如果用户连按两次登陆按钮,两个线程会同时开始登陆过程,有可能造成数据库的数据发生异常,一个用户被注册了两遍

解决方案:对用户名进行hash,使得每个用户的任务只能提交给某个固定的线程

你可能感兴趣的:(Netty,javaIO,并发,多线程)