目录
前言
什么是逻辑底层架构
逻辑底层架构的职责
1)Thread-线程
线程管理
线程通讯
线程安全锁机制
2)Network-网络
网络模型
网络消息协议
断线重连
网络安全
防范重复消息
防范篡改消息内容
防范篡改内存数据
网络承载
3)数据存储和访问
4)Event事件
5)Timer定时器
6)(tool工具类)通用类
7)引用通用基础框架
逻辑底层的设计
1)普通模式:
2)微服务services模式
特点与优势:
挑战:
3)component模式
什么是component模式
为什么需要用到组件模式
组件模式如何设计
总结:
前言
如果说部署架构犹如建房之中的工程设计图纸,那么逻辑底层架构就是地基和框架结构、承重墙设置。功能图纸的好坏决定了这个工程的模样和基本面貌以及这个功能的体量,逻辑底层架构就是决定着这房子能够抗震多少级,而业务架构那就是我们的装饰到底是怎么样的。
什么是逻辑底层架构
逻辑底层架构是在根据逻辑业务场景为而实现的代码技术建设。逻辑底层为具体功能提供服务,它是业务的基础建设,是一个服务者,而不是具体逻辑的实施者。
逻辑底层架构的职责
在逻辑架构考虑性能问题、易用性问题、逻辑代码的维护性,从业务基础功能角度考虑各个模块之间的顶层设计。比如services的服务设计方式,component的组件方式,线程模型,模块间通讯和响应模式,定时器功能、RPC功能、与客户端的通讯方式、存储方式等等一些列。
这好比一个庞大的工程,首先确定需要哪些部门,这些部门需要做什么事情,以及它们的职责、跨部门之间需要怎么沟通和协调,异常情况需要怎么解决等等。以一个服务者的心态来对逻辑底层做全局的考虑问题,而不是一开始就深耕业务写代码,尤其是做架构初期,很多人并不知晓这些道理。
对于逻辑底层架构我们考虑的业务所需要的技术的基础建设。它包含:
1)Thread-线程
线程模式的创建和管理及分配,线程之间的通讯以及同步问题的解决。
线程管理
线程管理是指对线程的分配、调度、创建、销毁等进行的控制。在多线程环境中,线程管理技术尤为重要,它涉及到线程间的同步、互斥、通信等问题,以确保各个线程能够正确地运行,并避免出现死锁等问题。
我们应该为操作线程提供一个统一的入口类,参考实际需要考虑为业务使用线程及相关操作提供便利。比如需要一个ThreadManager对线程操作进行管理,使用线程的其他操作都应该由该类提供对外的接口
线程通讯
学过多线程的程序小伙伴一定知道,线程往往出现读写问题和资源竞争问题。程序设计尽可能避免两个线程同步访问变化的数据,或对数据有读写权限问题。这样很容易出现多线程所引起的不可预知Bug。线程的通讯方式通常采用的是安全队列的方式,当发起某个操作时,实际操作的是其他线程的数据时,我们不应该在确认不安全的情况下访问其他线程的数据。
线程安全锁机制
Java 中提供了多种锁机制,用于控制并发访问共享资源。以下是 Java 中常见的几种锁:
- 内置锁(Synchronized):这是 Java 中最基础的锁机制,通过在方法或代码块上添加 synchronized 关键字实现。当一个线程进入一个 synchronized 代码块或方法时,会尝试获取锁,如果锁已经被其他线程持有,则该线程会被阻塞,直到获取到锁为止。
- ReentrantLock:ReentrantLock 是 Java 5 引入的一个可重入锁,它提供了比内置锁更灵活的锁控制能力。与 Synchronized 相比,ReentrantLock 支持更灵活的获取和释放控制,以及中断、定时等待和条件等待等操作。
- ReentrantReadWriteLock:这是一个读写锁,允许多个线程同时读取共享资源,但在写入时则需要独占式的访问。它提供了读锁和写锁两种类型的锁,允许多个读线程并发访问共享资源,但在写入时则会阻塞其他读线程和写线程。
- StampedLock:StampedLock 是 Java 8 引入的一个新的锁机制,它支持乐观读、悲观读、乐观写和悲观写四种模式,可以更加精细地控制对共享资源的访问。
- ConcurrentHashMap:ConcurrentHashMap 是 Java 中一个线程安全的哈希表实现,它使用了分段锁技术,将整个哈希表分成多个段(Segment),每个段上都有一把锁,通过这种方式实现了高并发访问。
- Phaser:Phaser 是 Java 中一个用于多阶段协调的同步器,它常用于替代 CountDownLatch。Phaser 可以让一组线程等待其他线程完成一些阶段性的工作。
以上是 Java 中常见的几种锁机制,每种锁都有其适用的场景和优缺点,根据实际需求选择合适的锁机制可以有效地提高程序的并发性能和稳定性。
2)Network-网络
在这一步我们要做的事情是网络连接的监听、管理,协议格式的定义以及解析,RPC消息的异步回调等。网络是框架最为核心内容,没有网络服务器谈何为架构,都是空谈,作为底层架构中最为核心部分,为它多加关注都不过。
网络部分应该包括:
网络模型
关于网络部分首先我们要考虑网络库与网络编程模型的选择。平台不同
网络模型就有不同;同时,不同的网络模型效率而有所不同
平台 | 通用网络模型 |
Window | Select模型 重叠IO模型之完成例程 完成端口(iocp) |
Linux | select模型 poll网络模型 epoll网络模型 |
这里就详细解释三种网络模型:
网络消息协议
网络协议是在网络的使用中的最基本的通信协议,对互联网中各部分进行通信的标准和方法进行了规定。并且,网络协议是保证网络数据信息及时、完整传输的重要协议
客户端与服务器通讯,我们首先要定义的是网络协议内容,在实际运用中,尤其是TCP通讯,我们采用的是protobuf消息协议进行相应的通讯。当然我早期的在没有用到protobuf时,我是自己写了一个类似protobuf消息协议样的字节流传输方式。
除了protobuf消息协议,我们可能需要用到json或者字节流等方式
不管用什么方式,但是都是客户端与服务器端约定俗成的。
断线重连
断线重连机制是指在网络波动或发生故障导致用户与服务器断开连接时,待网络恢复后,服务器尝试将用户重新连接到断开时的状态和数据1。
在网络情况下,断断续续,导致客户端无法和服务端交互,再比如,机房断电也会导致服务宕机,所以在netty中对服务进行断线重连是非常有必要的。
实际的业务场景很复杂,有可能我们正在发送的消息的时候网络断开,或者这时候进入电梯、又或者失去的WIFI连接。如何在业务逻辑想恢复现场,如果没有断线重连机制非常困难,因为你不知道在情况下网络不可用。
关于可以阅读我的断线重连那边文章。
网络安全
在有网络的情况,任何客户端都是不可信的,有可能对消息 篡改,有可能发送大量无用的消息内容。
防范重复消息
大量重复无用的消息发送给服务器如果不做预处理和防范措施,那么可能带来灾难性。消息内容一般包含两部分:message = header+body。我们如何在网络解析底层防范发送重复的消息呢,对于通用消息我们在消息头(header)中包含一个发送者序号,接受者将该序号保存,每次发送消息过来判断该序号是否是大于当前序号,如果小于或者等于保存的序号,那么一定是重复消息或者无效消息。
防范篡改消息内容
在我的开发项目中,好巧不巧出现过一次篡改网络消息的可以无限的刷道具,并将道具卖出,这个问题就已经相当严重了。当时我们排查了所有逻辑以及可疑的接口,没有发现。当我们的QA小伙伴偶然测试的时候修改了网路消息,将购买数量count设置为0时,竟然道具到手却不扣金币。
问题终于找到。我小伙伴计算逻辑是这样的,玩家购买道具判断玩家手中的金币足够时,就先将道具放入背包,并扣金币,但是扣金币时却是price*count =0金币。
防范篡改内存数据
在这一层对于服务器,我们应该遵循一个原则那就是客户端是不可信的,
如果玩家通过内存修改器修改内存数据,消息内容的验证是无法检测到的。
因为它是对原始数据的修改而不是对消息内容的直接篡改。
解决该问题的唯一办法就是在服务器端进行严格的条件验证和完善的设计逻辑实现。对于客户端发过来的协议要进行验证它的合理性。
网络承载
一个进程最多可以承载用户连接,一台物理服务器可以承载多少用户。网络承载的上线作为开发者必须需要考虑的,我们需要知道承载的上线,才能知道实际我们离上线还有多远。 网络承载与硬件设备、宽带、网络并发、网络模型等都有关联。
3)数据存储和访问
数据存储和访问我们首先要考虑的是选择什么样的数据库,mysql、mongodb等其他数据库。
中间存储插件我们考虑使用redis或者memcache。对于服务器配置的更新或节点管理我们通常用zookeeper来辅助实现。
在数据库访问的时候,技术设计上要考虑数据库访问的实时性以及瓶颈,数据库瓶颈操作相关瓶颈出现在以下几点:
1、逻辑对象的序列化与反序列化
大量对象的序列化和反序列化会造成卡顿、操作的超时等糟糕体验。如果同时有需要大
量对象需要序列化,那么可以通过毫秒级别的延时来做平滑处理。
2、访问和修改的吞吐量
在设计时应该考虑是否可以通过多线程或者线程池的方式访问数据库以避免单线程访问的
瓶颈问题。能否通过修改频率减少访问的吞吐量。同时应该有个平滑机制来做为处理,
当访问量达到一定的阈值时确定容错机制。在修改数据库数据时,是否可以批量修改而
不是一次次进行修改,利用数据库批量修改的特性进行一次性提交,以便减少数据内部
的IO次数
3、网络延时问题
访问数据库的进程尽可能安排在进程同一物理服务器上或者建设内部VPN的方式实现,减少网络延迟带来的不确定性。
4、表格独占
考虑分库分表的方式,在底层逻辑设计上给以支持,通过哈希分片算法给以支持。
5、访问线程耗尽问题
在压力测试时我们应该根据参数设置进行测试,确定资源的最大损耗来购置我们的硬件
设备以及线程启动数量,得出相应的阈值,从而从访问量的预估来判断使用和调配我们
的资源。
4)Event事件
底层逻辑交织在一起可读性以及维护成本很好,往往需要事件帮我降低耦合度,那么事件到底有什么好处呢?
- 解耦合:Event源和Event监听器之间可以实现解耦合。Event源只需要负责触发Event,而不需要关心具体Event的处理逻辑。Event监听器则只需要关注自己感兴趣的Event,并提供相应的处理方法。
- 灵活性:Event驱动的编程模型可以使程序更加灵活。通过注册不同Event的监听器,可以在不修改源代码的情况下改变程序的行为。这种扩展性使得程序更易于维护和扩展。
- 响应性:Event驱动的编程模型可以使程序更加响应用户的操作。例如,当用户点击按钮时,按钮会触发相应的点击Event,程序可以立即响应并执行相应的操作
Event的设计为代码的解耦和灵活性带来很大的好处。以Task任务为例,当某个任务需要一定数量的某个道具时这个任务就可以完成,通过时间我们怎么做呢?
- 第一步根据任务条件订阅一个任务事件
- 第二步:如果道具改变时抛出一个事件,挂载的事件监听到道具改变,判断是否符合任务条件
- 第三步:条件满足完成该任务,执行任务完成条件
就是这么简单,事件减少了不必要的代码交叉调用,整个代码设计相对清晰、简单。
5)Timer定时器
在游戏开发我们无法不需要定时器功能,定时器往往需要考虑几点:
- 一次性定时器:执行一次自动结束定时器
- 延时实现:定时器可以设定延迟触发时间点
- 循环定时器:只有到结束时间才结束的定时器
- 循环一定次数定时器:可以循环n次的定时器
在使用定时器,要让用户使用是安全的,我们应该提供对象本身的定时器,同时要提供全局的定时器。不能让用户过多关心设计细节或者说使用时要小心翼翼。
6)(tool工具类)通用类
通用工具类可以为我们实际开发节省很多开发时间,提高生产效率。每一个基础架构应该有自己独特的通用工具类,这里就简单列举我自己设计时几个常用的基础类
时间相关、反射、数学、System的扩展、String的扩展、同步ID的生成
7)引用通用基础框架
Spring框架对于Java后端开发能节省很多时间,这几年发现sprintboot通用框架能很好的运用游戏后端项目中,可配置化的特性以及丰富的开发库。
Springboot基础框架的好处:
- 快速构建独立的Spring应用。在构建Spring应用时,只需添加相应的场景依赖,Spring Boot就会根据添加的场景依赖自动进行配置,快速构建出一个独立的Spring应用。
- 简化构建配置。在Spring Boot项目构建过程中,只需在构建项目时根据开发场景需求选择对应的依赖启动器“starter”,在引入的依赖启动器“starter”内部已经包含了对应开发场景所需的依赖,并会自动下载和拉取相关JAR包。
- 自动化配置。使用Spring Boot开发项目时,一旦引入了某个场景的依赖启动器,Spring Boot内部提供的默认自动化配置类就会生效,开发者无须手动在配置文件中进行相关配置(除非开发者需要更改默认配置),从而极大减少了开发人员的工作量,提高了程序的开发效率。
- 提供生产就绪功能。Spring Boot提供了一些用于生产环境运行时的特性,例如指标、监控检查和外部化配置。其中,指标和监控检查可以帮助运维人员在运维期间监控项目运行情况;外部化配置可以使运维人员快速、方便地进行外部化配置和部署工作。
所有对于Java后端人员,强烈建议使用springboot作为我们的通用框架进行开发。
逻辑底层的设计
那么逻辑底层架构应该考虑哪些?对于我而言,应该考虑维护性,耦合度低,并能快速设计和实现。基于此那么模式设计就很关键了
在考虑模块的时候,我们往往会想着用什么方式对我们的业务进行组装和管理,以及模块之间怎么通讯。我对于模块的理解为三种方式: 普通模式、微服务services模式、以及我的component模式。
1)普通模式:
普通模式就是早期就有,运用设计模式进行各个功能的代码设计组装。各个模块之间往往通过单例或组合的方式进行代码管理。
我们以游戏中玩家为例,玩家的结构应该是这样的:
玩家管理器及相关接口:
玩家player管理相关功能
当我们通过单例的玩家管理器进行管家管理,玩家对象通过组合的方式有bag和task等系统
模块之间通过玩家对象进行交叉调用,单个玩家逻辑往往糅合在一起,无法单独分开。前期逻辑简单时候就比较直接,但是随着业务逻辑不断更新,维护成本就很高,这很考验开发者对业务的熟悉能力
当然,普通模式并不是没有好处,它在初期能够快速开发。因为开发者自己的业务可以顺序写下去,但是到后期它的弊端就暴露出来。
比如:当玩家获道具时,我们要将道具添加背包中,同时要查找那个任务需要这个道具,需要更新任务的完成条件,以及角色属性改变的可能。因为有些道具在获得的时候需要为人物添加技能或buff。
道具的获得的来源会多,那么需要在很多地方需要注意或维护获得道具的接口,如果接口在设计上的不统一,很容易出错,维护成本很高。
2)微服务services模式
微服务是一种架构风格,它将应用程序划分为一组小的服务,每个服务都运行在独立的进程中,并使用轻量级通信机制(如HTTP、RPC等)进行通信。这些服务围绕业务功能进行构建,并且可以通过自动化的部署机制进行独立部署和升级。每个服务都应该是松耦合的,这意味着它们之间的依赖关系应该尽可能少,以便于独立开发和部署。
特点与优势:
- 开发简单性:每个微服务都专注于一个特定的业务功能,这意味着代码库更小、更易于理解和维护。开发者可以更加专注于他们正在处理的功能,而不是整个应用的复杂性。
- 易于局部修改:由于每个微服务都是独立的,你可以独立地测试、部署、升级和发布它们。这意味着对某个微服务的修改只需要重新部署这个服务,而不会影响其他服务。
- 高容错性:每个微服务都可以独立部署和扩展,这意味着一个服务的失败不会影响到整个系统。这种分布式架构提高了系统的可靠性和容错性。
- 技术多样性:微服务架构允许你使用不同的技术栈来开发不同的服务。这为你提供了更大的灵活性,可以根据每个服务的需要选择最合适的技术。
- 高可伸缩性:你可以根据需求灵活地扩展或缩减每个微服务,从而实现高可伸缩性。这有助于应对流量波动和负载变化。
挑战:
- 复杂性:虽然每个微服务都很简单,但整个微服务架构可能变得非常复杂,特别是当你有大量的微服务时。这可能导致管理和维护的挑战。
- 故障诊断:在分布式系统中,当一个用户请求的业务涉及多个微服务时,故障诊断可能会变得困难。需要跟踪和监控每个微服务的交互和性能。
- 成本:微服务架构需要更多的运维投入,因为每个服务都需要独立管理。此外,随着服务数量的增加,管理的复杂性也会增加。
- 数据一致性:在微服务架构中,保持数据一致性是一个挑战。不同的微服务可能需要访问和修改相同的数据,因此需要一种机制来确保数据的一致性。
- 团队组织与协作:微服务架构要求团队更加细分和专业,不同团队负责不同的服务。这可能导致沟通成本增加,需要更好的团队协作和沟通机制。
总的来说,微服务架构为应用程序开发提供了许多优势,但也带来了一些挑战。在选择是否使用微服务架构时,需要权衡这些优劣势,并根据项目的具体需求做出决策。
微服务架构的主要优势包括提高了系统的可扩展性、可维护性和灵活性,但同时也带来了一些挑战,如服务之间的通信和协调、数据一致性、安全性等问题需要解决。
3)component模式
什么是component模式
组件式开发(CBD:Component-based Development)是以可重用的组件为基础 "装配出"解决需求软件的过程。组件是具有相对独立功能、接口由契约指定、和语境有明显依赖关系、可独立部署、可组装的软件实体2。组件式开发可以提高软件开发效率,减少重复开发,提高软件的可维护性和可扩展性。
通俗来讲,组件式开发的核心工作之一是创建可以当"积木"组合的软件组件。
为什么需要用到组件模式
当我们了解普通设计模式和微服务设计模式的特点时,我们不能对于某种设计模式过于迷信或者一股脑的摒弃。普通模式运用面向对象的设计方式与设计模式深度结合,实现我们需要的完整功能,在小系统设计的时候它是很管用的,但是当系统达到一定层级的时候复杂度就上来了。所以在小系统或者不复杂的功能时用它是合适的。
当我们要考虑功能之间是独立的并且交际很少,但是需要尽可能扩展,那么普通模式就不合适了,选择微服务的模式就比较合适。
由于我们讨论的是游戏架构设计模式。玩过游戏和做过游戏开发的人就知道,游戏设计是一个的大设计世界,功能与功能之间交互性非常强,一个人物属性的改变可能牵扯到一大堆的设计逻辑和功能。所以我们不能死板地使用普通模式还是微服务设计模式,在我看来,它们都无法满足游戏开发设计模式。
对于我而言,由于做过多个不同类型的引擎的项目,我发现unity中component组件设计方式在集成了普通设计模式的优点又结合了微服务设计理念。它完美的解决了我在模块和功能设计目的。即:有不希望耦合度过高又不希望过于独立而照成的维护成本过高
组件模式如何设计
在component设计中,我们可以将某一个实体对象分为Actor,而针对Actor操作的功能模块视为component,componnet就挂挂载在actor上,你也可以卸载它。component依附于actor,没有actor,那么component就没有意义了,component必须依赖于Actor而存活。
Component的通讯一般通过事件触发,这样当某个功能被卸载时同样不影响其功能。
例如:在游戏设计中,我们把玩家看成一个actor,那么背包、任务、邮件等功能都可以看成一个component。
玩家Player与相关功能的 actor与component对应图:
当然任何对象我们都可以看成是actor,任何一个相对于Actor的功能都可以是Component
对于component模式我会专门写篇文章做讲解。
总结:
逻辑底层架构设计代码工程实时的基础设计,从业务层反向考虑基础建设需要做什么,以什么样的结构承载我们的逻辑实现,为快速开发和逻辑实现做好扎实的基础 ,至于业务的逻辑框架,都应该遵循底层框架所提供的内容做扩展而已。在实际运用中当业务需求改变时,我们应该修改底层架构架构。