在现代系统中,特别是互联网软件,通常会涉及到大量用户的并发访问,我们的系统一定要在架构上支持高性能、大并发的访问。一个高性能的系统通常由很多的方面组成,包括数据库高性能、Web服务器高性能、负载均衡、缓存、软件架构等。我们这篇文章先从软件开发架构的角度作为切入点来介绍如何构建高性能的系统。
传统架构性能的问题
我们先来看看DDD经典架构中,在多用户、大并发访问的情况下,对性能产生不利影响的因素。先来看看简单架构图:
1.通常会在当前界限上下文中只有一个领域模型,这个领域模型既会用于领域逻辑,同时也会用于持久化。
2.领域模型既会用于用例、也会用于查询。
3.当前界限上下文通常只会有一个WebApi项目,这个项目中不同的Action Api用于不同的功能,有查询的,有用例的。
从上面几点大家可以看出,有以下几个原因带来性能的瓶颈:
1.上下文的查询和用例通过一个模型来做,对应到数据库来讲,就是增、删、改、查针对同一个数据库的相关表,通过阻塞会造成性能问题;虽然通过建立好的索引可以进行缓解,但解决问题不彻底。
2.领域模型通常是根据业务需要进行设计的,也就是用于用例。如果用于查询需求的话,可能会连接多个业务表才能完成查询,查询性能出现问题。
3.通常经典DDD是完成领域逻辑后,通过应用服务协调领域逻辑与仓储来将领域对象持久化到数据存储中,然后通过WebApi返回用户结果。如果领域复杂,用户并发量大的话,这个过程反馈到前端有一定的时间,用户体验不好。
4.当前界限上下文是一个WebApi项目,无论是用例还是查询;这样也无法对性能进行扩展,比如用例的在一些主机上,查询的在另一些主机上。
为了有效的解决上述出现的性能问题,业界总结了一种架构风格,也就是CQRS(命令查询职责分离)。通过CQRS的理念,可以有效的提高系统对大并发的支持。
命令指的是要更改对象状态的行为,对系统有副作用;查询指的是不更改对象状态的行为,对系统无副作用。其实CQRS不仅仅用于大并发的处理,在日常开发中,其实也是可以利用这种理念的。
命令与查询混在一起的情况:
private int Add(int a,int b)
{
int result = a + b;
return result;
}
从上面代码可以看出,更改状态与查询是混在一起的;我们可以改造成命令与查询分离的方式:
private int result;
private void Add(int a,int b)
{
int result = a + b;
}
private int QueryResult()
{
return result;
}
基于上述代码的思路与经典DDD架构的问题,我们就引入CQRS架构风格来解决性能问题。
CQRS架构
我们先来看看CQRS整体的架构图,然后再说它是怎么解决性能问题的。
我们来看看整体架构图的流程以及它是如何解决性能问题的:
1.首先可以看到命令与查询走不同的WebApi服务,这样可以将更改系统状态的行为与查询的行为做很好的隔离,并可以部署微服务到不同的服务器上,当然还可以通过NLB做进一步的扩展。
2.命令端的WebApi并不直接处理调用用例完成,而是接收到用户命令时,将命令消息发布到消息总线,然后立刻返回一个操作信息给用户,这样用户体验很好,不需要等待业务逻辑完成与持久化完成。
3.命令处理器WebApi从消息队列侦听到消息,然后进行处理,处理的主要内容是完成领域逻辑调用,直接添加事件数据到事件存储中。这里需要注意的是,并不是持久化到业务数据库中。首先完成领域逻辑调用,可以得到用例最终正确的领域对象,然后存储事件时,存储这次领域对象的状态,并且是直接添加。这样做的好处有:一是加快持久化,二是能够保存领域对象每次变化的信息,未来可以用于历史追踪、事件溯源与最终一致性。
4.命令处理器将领域对象发送到消息总线中,事件处理器会侦听队列,并最终将消息信息涉及到的领域对象持久化到业务数据库中。
5.在查询WebApi中,可以直接查询业务库,如果业务库并不适合多表连接查询时,可以再单独做个拉平的为查询提供服务的查询库。查询库的内容可以通过业务库更新成功后,发布消息到另一个队列中,然后通过处理器来处理这些数据到查询库中。