一篇学会资源库Repository的性能优化

在DDD中,聚合根需通过资源库(Repository)持久化,资源库将聚合根的存储与存储中间件(Mysql、ElasticSearch、MonogoDB等)解耦,我们可以根据聚合的业务特性决定选择关系型数据库还是非关系型数据库存储聚合根。

让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:申请域名网站空间、营销软件、网站建设、榆中网站维护、网站推广。

很多读者可能还存在疑问,为什么资源库只提供一个save方法持久化聚合根。原因是在DDD中,资源库是聚合根的容器,但并不限制容器是什么做的,也就是前面说的与底层解耦。如果容器是Key-value数据库做的,是不支持update某个字段的,并且inset和update是不区分的。资源库与DAO不同,资源库只是向领域模型提供聚合根以及持久化聚合根。

如果我们选择关系型数据库作为聚合根的容器,那么在存储聚合根时可能就需要将聚合根以及聚合根下的实体拆分到多个表存储,这就可能导致每次save聚合根都需要执行多条update语句,即便聚合根下的实体并没有发生任何的改变,即便只是聚合根修改了一个字段(值对象),因此会严重影响到应用的性能。

为解决选择关系数据库作为聚合根容器导致的性能问题,我们需要付出额外的努力,如用内存快照去判断每次save聚合根只需要更新哪些表。

基于每个业务用例都需要通过资源库获取聚合根最后也通过资源库持久化聚合根的特性,我们可以在获取聚合根时创建快照,并且在持久化聚合根时对比(diff)快照,获取差异信息,只执行需要更新的差异信息。

本篇分享的是笔者实现的一种方案,虽然每个团队定义的DDD代码规范不同,但资源库的实现上差异也并不大,因此也具有参考价值。

首先,抽象聚合根快照存储器AggregateRootSnapshot,提供缓存聚合根快照、根据聚合根ID获取快照、移除快照的方法。

提示:我们约定聚合根必须继承一个抽象类BaseAggregate,该抽象类定义获取聚合根ID的方法,在缓存快照时可以用聚合根id作为key缓存,这样在拿的时候才能根据聚合根id拿到。

我们可以使用redis实现聚合根的缓存,但不建议使用性能低的存储中间件存储,因为那样不仅资源库的性能没能得到优化,反正还更影响性能。当然,最好的方式是存储在内存中,虽然牺牲点内存,这便是以空间换时间。

我们使用ThreadLocal存储聚合根快照,因此编写的AggregateRootSnapshot实现类如下。

如果聚合根id不是依赖数据库生成的(我们也不推荐聚合根id依赖数据库生成,原因在之前的文章已经介绍过了)。为了避免在聚合根为新创建的情况下获取到错误的快照,如线程在执行上一次业务用例(一次接口请求)时,只调用获取聚合根的方法,之后没有调用聚合根的存储方法移除快照(如获取聚合根详情),而这次是创建新的聚合根,当然也没有调用一次资源库获取聚合根的方法更新快照,那么这次获取的就将是前一次的快照,因此我们还需要对比聚合根id是否相同。

只对比聚合根id当然不能确保获取的就是新的聚合根,能确保聚合根唯一还有这个条件:“基于每个业务用例都需要先通过资源库获取到聚合根,最后也需要通过资源库持久化聚合根的特性”,这句话才是最重要的。

提示:ThreadLocal类型字段非静态,不会导致内存泄露吗?答案是不会,后面会讲到。

接着,我们为使用关系型数据库存储聚合根的资源库写一个抽象类,需要使用快照优化性能的资源库可继承此抽象类。

RepositorySnapshotSupper实现Repositor接口的findById、save、deleteById方法,另外提供抽象方法由子类实现。因为我们需要在findById获取到聚合根时创建一份聚合根快照并缓存,在真正save聚合根之前获取快照完成diff判断,然后将diff结果交给子类,这样子类在实现save时就可以根据diff结果减少不必要的sql。

提示:RepositorySnapshotSupper的快照存储器并非静态的,而快照存储器的ThreadLocal类型字段也非静态,因此我们需要确保一个资源库只存在一个实例(单例),才不会导致ThreadLocal内存泄露,只是每个聚合根强引用一个ThreadLocal。

以上几步都不是难点,难点在于如何实现快照的创建,以及diff实现。

快照工具类(SnnapshotUtils)实现思路:

提前条件:要求实体与聚合根提供一个私有的无参构造函数,用于通过反射创建实例。

1.通过反射实现字段值拷贝,当聚合根的字段类型为非实体类型,那么就是值对象类型,对于值对象类型我们只需要拷贝引用即可;

2.如果是实体类型集合,则创建一个新的集合,并将原集合中每个实体元素都拷贝一份添加到新集合,将新集合赋给快照,实体的拷贝规则同聚合根,可使用递归实现。

Diff工具类实现思路:

先定义diff结果类型:未修改、新增、更新、删除。 图片

1.对于聚合根,如果不存在快照即认为Insert类型,聚合根下的实体也全部为Insert类型;

2.对于聚合根,如果存在快照,那么除实体类型或实体类型集合字段外,只要其它的任意一个值对象不同,就认为聚合根diff结果为Update类型,否则为Non类型;

3.只要聚合根不是新增,不管聚合根有没有更新,都不会影响聚合根下的实体的diff;

4.如果实体与聚合根一对一,即不是集合类型字段,那么:如果对应实体快照不存在,则认diff结果为Insert,否则如果实体快照存在但新的为null则认为是Delete,否则对比实体的各个值对象,未修改则为Non,修改则为Update;

5.如果实体与聚合根是多对一,即实体集合,如订单有多个订单item,那么需要一个个对比:新的item在快照中找不到,则为Insert,快照中的item已经不存在新的实体集合,则为Delete,否则对比item,未修改则为Non,修改则为Update。

定义存储diff结果的类:

由于BaseAggregate聚合根实现了实体接口(聚合根也是实体),因此我们在EntityDiff中使用Entity引用聚合根/实体,方便后续直接从diff中获取entity执行插入、更新,或是获取entitySnapshot执行删除。(对于实体集合,也可存实体在集合中的索引。)

如果聚合根下的实体字段是集合类型,那么diff结果也使用集合存储:

diff工具类的实现:

由于项目代码不便贴出来,在此我简单写了一个测试用例,分享下成果。

订单聚合根:

提示:使用lombok有个坑,如果使用@Builder注解,需要提供一个无参构建方法(建议是私有的构建方法),然后在构建方法上添加@Tolerate注解。

订单item实体:

订单资源库实现:

  • 当聚合根的diff结果类型为Insert时,全量存储聚合根、聚合根下的实体;
  • 当聚合根的diff结果类型为Non时,不需要更新聚合根,但聚合根下的实体是否需要更新还需要根据聚合根实体的diff结果确定;
  • 当聚合根的diff结果类型为Update时,需要更新聚合根;
  • 获取实体的diff结果,根据diff结果决定是插入、更新、删除、还是什么也不做。

单元测试:

单元测试结果如下:

总结

本篇介绍如何通过快照+diff的方式优化资源库的性能,之所有能这样做是因为每个业务用例都需要先通过资源库获取到聚合根,最后也需要通过资源库持久化聚合根。出于性能考虑,我们决定以空间换时间,使用ThrealLocal+反射实现创建和缓存聚合根快照,最后也使用反射完成diff逻辑。当然diff类还存在优化空间。

本篇介绍的快照是基于聚合根(DO)的,当然我们还可以基于(PO)去实现,也会更简单。

注意:本篇图片中的代码可能有bug,未更新到优化后的代码,懒得重新截图,仅供参考!

参考文献:

阿里技术专家详解DDD系列 第三讲 - Repository模式

本文转载自微信公众号「Java艺术」,可以通过以下二维码关注。转载本文请联系Java艺术公众号。

分享文章:一篇学会资源库Repository的性能优化
网页路径:http://www.shufengxianlan.com/qtweb/news8/113108.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联