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关键字,但是这样一是线程争夺锁导致效率降低,二是业务逻辑与线程安全逻辑混合,增加了编码的复杂度
线程池的内部是一个阻塞队列,因此这个问题的总体抽象如下图所示
可能有的人会有这样的疑问:将所有消息处理逻辑放在单线程,会不会导致并发量太低,速度慢?其实不然,因为游戏服务器速度的瓶颈都出现在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 传入//登陆完毕的后续操作代码
}
)
这样就算完美了吗?其实不然,因为我们的回调函数实际上是在我们的异步线程中被调用的,也就是说,业务逻辑也是在异步线程中执行的,而这又会引发数据的脏读写问题,而我们理想的情况应该是,异步线程将执行完毕的结果传给我们的消息处理线程,我们用那个线程来执行业务逻辑。
那么,针对这个问题的解决方案是什么呢?
首先,我们定义一个接口 :
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中执行。
注:AsyncGetUserEntity为实现IAsyncOperation接口的类,其中的doAsync执行的是登陆逻辑
问题三:这样一来虽然将登陆放到别的线程中执行,那么阻塞的问题不是依然没有解决吗?因为IO线程只有一个
解决方案:在线程池中多开辟几个线程
问题四:如果用户连按两次登陆按钮,两个线程会同时开始登陆过程,有可能造成数据库的数据发生异常,一个用户被注册了两遍
解决方案:对用户名进行hash,使得每个用户的任务只能提交给某个固定的线程
Java游戏服务器架构的并发问题及解决方案相关推荐
- 【阅读笔记】Java游戏服务器架构实战
[阅读笔记]Java游戏服务器架构实战 书籍链接:Java游戏服务器架构实战 作者提供的源码链接:kebukeYi / book-code 这里对书籍中比较重要的知识点(精华部分)进行摘录(总结) 文 ...
- 【游戏开发】《Java游戏服务器架构实战》项目在windows上部署
[游戏开发]<Java游戏服务器架构实战>项目在windows上部署 文章目录 [游戏开发]<Java游戏服务器架构实战>项目在windows上部署 一.配置项目基础环境 二. ...
- ioGame 网络游戏服务器框架 (java)、java游戏服务器、netty 集群分步式的网络游戏服务器
ioGame 国内首个基于蚂蚁金服 SOFABolt 的 java 网络游戏服务器框架:无锁异步化.事件驱动的架构设计 通过 ioGame 可以很容易的搭建出一个集群无中心节点.有状态多进程的分步式游 ...
- 游戏服务器架构设计的一些整理
一.前言 没有最好的架构,只有最适合自身业务的架构. 首先我们应该确定的是大的架构方向:分布式 / 单应用+负载均衡,这两种架构设计直接影响后续的网络层.缓存层.数据层.业务层的设计.笔者这两种架构的 ...
- Java游戏服务器成长之路——感悟篇
又是一个美好的周末啊,现在一到周末,早上就起得晚,下午困了又会睡一两个小时,上班的时候,早上起来喝一杯咖啡,然后就能高效的工作一整天,然而到了周末人就懒散了,哈哈. 最近刚跳槽,到新公司已经干了有两周 ...
- 学习JAVA游戏服务器开发需要了解的情况
一,游戏服务器开发的工作介绍 近来遇到有很多人想从其它开发领域转到游戏服务器开发行业上来,他们或许觉得游戏服务器开发工资高,或许觉得做游戏服务器需要掌握的技术更高级,可以锻炼自己,或许觉得想换个环境等 ...
- 百万用户同时在线游戏服务器架构实现.doc 基于epoll 通信模型
http://wenku.baidu.com/view/02033d0af78a6529647d53fc.html 百万用户在线网络游戏服务器架构实现 一. 前言 事实上100万 ...
- java游戏服务器必备
推荐阅读: 我的CSDN 我的博客园 QQ群:704621321 对于一个新手,想接触游戏服务器,一定会有个疑问--使用Java开发服务器需要学习什么? Java语言,由于学习成本低,开发速度快,稳定 ...
- 通用游戏服务器架构设计
经过研究多个框架和自己经历的项目,我总结出来一套较为通用的,以8c16g机器来说,如何设计出来一套单进程多线程.分区分服的java游戏服务器框架呢? 1.逻辑层(带养成数据的模块,io密集) 线程池: ...
最新文章
- 又快又简单的sql2005分页存储过程
- perl中Net::FTP帮助文档
- mybatis整体架构
- python学习笔记之装饰器、递归、算法(第四天)
- FJ的字符串(字符串)
- 8.1 段子中“酷毙”了的IT行业——《逆袭大学》连载
- Win7 64位系统下Auto CAD 2010注册激活,出现警告:Make sure you can write to current directory...
- 配音软件哪个好?这三款很火的配音软件,简直是短视频后期配音必备
- nginxconsul
- windows10未激活更换壁纸
- 【读书笔记】数学之美
- Dicom学习之一:大尾和小尾LittleEndian/BigEndian
- win10照片查看器_Win10小技巧,帮你事半功倍
- ***我是如何通过华为面试的?群面+技术面+综合面+英语面(Android岗)***
- mac 上安装 ettercap-gtk,driftnet
- php-调用阿里云第三方短信接口
- 数据库提示错误代码ora 01688
- 运行命令打开电脑程序 快捷键
- isqlplus 连接标识符 (奇怪的问题)--(解决)
- ENVI5.1新增波谱库及波谱曲线工具