任何架构的演进都是由场景驱动的,离开场景谈架构就是耍流氓。因此,做架构设计之前,我们要先了解当下的场景。场景就是需求,一般可以将需求分为三类:商业需求、功能需求和质量需求。
功能需求描述的就是系统应该提供的服务,包括为用户提供的服务,也包括为其他系统提供的服务,比如开放API。
那么,对于一个数字资产交易平台来说,其MVP最核心的功能就是可以让用户完成资产交易,要能让用户完成交易,最简化的流程就是:
交易其实还有内部流程,用户可以进行下单和撤单。如果下单后没撮合成交,那用户就可以撤单。如果撮合成交了,交易也完成了。
质量需求方面,也不需要满足太多特性,只要保证服务基本可用,用户的资产相对安全即可。
至此,总结一下,我们的数字资产交易平台,第一个版本的需求分析结果,需要完成的功能需求包括:
需求分析完成之后,就可以进入架构设计了。对于MVP版本来说,最重要的就是简单快速实现需求并上线,那就没必要考虑SOA、微服务等分布式架构,就直接用单体架构最合适。但并不是说用单体架构就可以不用做架构设计了,单体也只是服务端用单体而已,但整个交易系统并不只有服务端,还包括客户端和数据库,而且单体内部又如何组织,这也是需要设计的。而且,架构师还应该具备一定的前瞻性,要考虑到后续的业务发展,要有适度超前的设计思维,从而设计出能满足当前场景需求的系统,并具备扩展性,能快速实现满足下一阶段的业务需求,以及能方便快捷地进行架构演进。
接着,就来说说我对当前版本的设计思路。先从整体来考虑,首先,是否要前后端分离?分离的话,API要怎么设计?其次,数据库的选型,是用传统的关系型数据库(也称为OldSQL),还是NoSQl,抑或NewSQL?数据库的表又应该如何设计?最后,服务端的代码应该如何组织,单体内部采用什么架构模式?等等。
先说第一个问题,是否要前后端分离?答案是肯定要分离的,虽然第一版只需要支持Web端,但后续肯定还要支持移动端App,甚至支持桌面客户端,不做前后端分离的话就很难做到多端支持。既然前后端是分离的,那就需要对客户端与服务端之间交互的API进行设计,包括使用什么通讯协议、数据传输协议、安全机制等。
通讯协议主要就是HTTP和WebSocket了,HTTP只能由客户端发起通信,但它是无状态的,HTTP服务就容易通过横向扩展实现负载均衡;WebSocket则支持全双工通信,但它是有状态的,扩展就比较麻烦,安全性也较差。对于交易平台这样的系统,大部分情况下用HTTP是比较合适的,安全性较高,且无状态的,高并发情况下也容易扩展HTTP服务器。不过,行情数据比较特殊,因为更新频率比较高,数据量也较大,对安全性的要求也不高,用WebSocket建立连接,由服务端不断向客户端推送数据,这种方式是比较合适的。但从另一方面来说,如果两种协议都要支持,无疑会增加开发成本。这时候应该怎么选型呢?
其实,做架构设计,很多时候就是需要在这样相互矛盾的场景下做选择、做平衡。架构师要做的并不是设计最好的完美架构,而是满足当下的合适架构。对于我们当前的场景来说,减低开发成本更重要,因此,我们只用HTTP一种协议就行了,至于行情数据,就先用轮询请求的方式去获取。
数据传输协议则比较多,有JSON、ProtoBuf、FlatBuffers、MessagePack、Thrift等等,各有各的优缺点,这又该如何选型呢?如果论性能,MessagePack是最快的,但扩展性较差,维护成本也较高。ProtoBuf性能次于MessagePack,大部分成熟项目选用它,缺点就是可读性差、库比较大、维护成本较高。JSON是最简单易用的,开发成本也最低,可读性也好,缺点就是体积大、性能低。对于我们当前的场景,其实也不需要考虑太多,开发成本最重要,所以选择JSON是最合适的。
不同领域的接口,在URL上就可以区分开,比如,用户域的接口可以统一用domain.com/user/为前缀,账户域可以设置为domain.com/account/,交易域为domain.com/trade/,行情域为domain.com/market/,这样区分之后,后续拆分了微服务之后,网关层就容易对接口进行服务路由了。
另外,查询类的读请求可以统一用GET方法,非查询类请求可以统一用POST方法,以方便后续可以对请求进行读写分离。
其实,这种业务领域的划分,可以用DDD(领域驱动设计)的思想去分析和设计,不只是用于API设计,也用于数据库设计,以及整个应用的领域建模设计。在一开始就做好这种领域建模,后续将单体应用拆分为多个微服务的时候就会顺畅很多。
至此,API设计部分就先讲这么多了,更细节的设计就不再去深入了。
API接口确定之后,很多人下一步做的是直接进行数据库设计,其实,在这之前,应该先梳理分析关键的实现流程,梳理之后,才会清楚,哪些数据需要持久化?是否需要用到MQ中间件?是否需要用到缓存?需要接入哪些第三方平台?等等。
充提币的流程,就需要对接区块链系统了。这也有两种方案,一是自己搭建区块链节点,二是接入第三方提供的API。第一种方案的实现成本比较高,所以尽量是选择第二种方案。前面我们说过,第一版只先接以太坊链,而以太坊链目前最常用的第三方API就是infura,其免费版本就已经足够使用。
下单和撤单,内部就涉及到撮合流程了,这个流程就稍微复杂一些,但从整体实现来说,有数据库撮合和内存撮合两种方案。数据库撮合实现起来相对简单,但性能较差。内存撮合性能高,但实现就比较复杂了,比较考验架构师的设计能力。我们第一版其实对性能要求不高,所以可以先用简单的数据库撮合方案。
撮合成功后就会产生成交价和成交记录,这也是需要持久化保存的,程序还需要将每一次成交记录累加起来计算出K线数据,以及更新Ticker信息。这些数据都应该持久化到数据库保存起来。接口获取行情数据时,就从数据库读取即可,这是最简单的方案。
关键流程就这些了,接下来,就进入数据库设计阶段了。
数据库选型方面其实也很简单,虽然现在NewSQL很火,但其主要应用于分布式的大数据场景,我们当前版本根本用不上,所以暂时不用考虑。OldSQL应用最广泛的当属MySQL,这是无可厚非的,这也是我们的优先选择。NoSQL方面,应用最广泛的就是内存数据库Redis,主要被用来替代Memcached作缓存系统;其次是MongoDB,在很多方面并不比MySQL差,不足的就是无法支持复杂事务和复杂查询。刚好,交易系统对复杂事务和复杂查询的场景还是比较常见的,所以MongoDB并不太合适,因此我们只能选择MySQL。
不过,如果不考虑开发成本,其实,行情数据是比较适合用MongoDB存储的,因为数据量大、查询频率高,也没有复杂事务和复杂查询。我们可以在后续版本考虑此方案。
设计关系数据库时,可以尽量遵循一些设计规范,这些规范也称为范式,从而设计出结构合理、冗余较小的数据库。总共有六大范式,因为篇幅原因,我就不展开细讲了。不过,在实际设计的时候,其实也不是必须完全遵循这些范式,只是尽量遵循,但对于一些特殊情况,是可以有一些妥协的,比如,在一张表中增加冗余字段可能不符合范式,但可以提高查询效率。不过,有一条原则需要遵循:选择作为冗余的字段应不需要额外的工作来保持数据一致性。比如用户昵称,这是用户可以随时修改的,就不适合作为冗余字段。
另外,在这些范式之外,还有其他一些设计原则也很重要,我挑几点重要但却容易被忽略的点说说:
数据表的设计,前面也有提到,可以用DDD的思想作为设计的方法论,而根据我们前面确定的业务需求,最终我分为了以下几张表:
限于篇幅,数据库设计方面我也不再继续深入了,后面就聊聊最后一块,服务端的设计。
服务端是个单体应用,对其的设计,主要是对内部模块的划分,以及代码结构的组织。
首先,最简单的划分就是采用三层架构,分为:API层、Service层、DAO层。API层,也有的称为Controller层或Web层,主要负责用户请求的处理;Service层也是业务逻辑层,实现核心的业务逻辑,包括事务控制;DAO层则是数据访问层,负责访问数据库,实现数据的增删改查操作。三层之间的调用关系是从上到下的,API层依赖于Service层,Service层依赖于DAO层,不能倒过来,也不能跨层依赖,即API层不能直接调用DAO层。而且,层与层之间的依赖应该依赖于接口,而不是依赖于具体实现,遵循依赖倒置原则,从而减低不同层级间的耦合性。
另外,期间涉及到一些不同的对象,缺乏经验的人很容易搞混,有必要明确区分一下,尤其是PO、DAO、DTO。
PO=PersistantObject,持久对象。对应于数据库的数据模型,一个简单的PO对应于数据库中某个表中的一条记录,多条记录则用PO集合。在概念上,PO不包含对数据库的任何操作。PO还是Service层和DAO层之间传输数据的对象。
DAO=DataAccessObject,数据访问对象。也是数据访问层最核心的对象,其封装了对数据库进行CRUD操作的各种方法,为Service层提供调用接口,通常和PO结合使用。
DTO=DataTransferObject,数据传输对象。和PO很类似,不过是在API层和Service层之间传递数据的对象,一般也是返回给到前端的对象。
三层架构只是在水平方向功能维度的一个划分,其实,还可以在垂直方向业务维度再进行划分。我们前面就将接口划分为了用户、账户、交易、行情4个模块,那在API层和Service层其实也是一样的,也可以再划分为这4个业务模块。不过,目前的DAO层可能不太适合这么去划分,就直接按不同数据表的维度去组织代码即可。
然后,关于编程语言方面的选型,主要也是看团队所熟悉的技术栈了,如果是从0到1搭建团队来做,我推荐尽量用Golang,因为交易系统的特性就是高并发,而Golang的语言特性就非常适合开发高并发的应用。
更深入的代码实现层面,我就不再深入探讨了。根据以上所描述的架构设计结果来看,在代码实现层面,其实也没什么难点。
1.0版本的交易系统架构设计,我就聊这么多了,因为篇幅原因,也无法面面俱到,很多细节也没有讲,但大体的设计思路应该是讲到位的了。另外,虽然我是在聊交易平台的架构设计,但背后的本质,更多其实是想传达更普适的架构思想。比如:
1.0版本的交易系统还是很简陋,性能也很低,很多设计其实也还不怎么优雅,这些在后续的版本中我会慢慢讲怎么优化、怎么不断演进。