当前位置: 首页 > 科技观察

一篇学习Repository性能优化的文章

时间:2023-03-12 19:44:08 科技观察

在DDD中,聚合根需要通过存储库(Repository)进行持久化,而存储库将聚合根的存储与存储中间件(Mysql、ElasticSearch、MonogoDB、等),我们可以根据聚合的业务特点来决定选择关系型数据库还是非关系型数据库来存储聚合根。很多读者可能还有疑问,为什么资源库只提供了一个save方法来持久化聚合根。原因是在DDD中,资源库是聚合根的容器,但并不限制容器做什么,也就是上面说的与底层的解耦。如果容器是Key-value数据库做的,不支持更新某个字段,不区分inset和update。资源库不同于DAO,资源库只提供聚合根和持久化聚合根给领域模型。如果我们选择关系型数据库作为聚合根的容器,我们在存储聚合根时可能需要将聚合根和聚合根下的实体拆分成多表存储,这样可能会导致每次保存聚合都需要执行root多次更新语句,即使聚合根下的实体没有发生任何变化,即使聚合根只修改了一个字段(值对象),也会严重影响应用程序的性能。为了解决选择关系型数据库作为聚合根容器带来的性能问题,我们需要做一些额外的努力,比如使用内存快照来确定每次保存聚合根只需要更新哪些表。基于每个业务用例需要通过资源库获取聚合根并最终通过资源库持久化聚合根,我们可以在获取聚合根时创建快照,在持久化时比较(diff)快照聚合根获取差异信息,只执行需要更新的差异信息。本文分享作者实现的一个解决方案。虽然每个团队定义的DDD代码规范不同,但是资源库的实现差别不大,所以也有参考价值。首先,抽象聚合根快照存储AggregateRootSnapshot提供了缓存聚合根快照、根据聚合根ID获取快照、移除快照的方法。提示:我们约定聚合根必须继承一个抽象类BaseAggregate,它定义了获取聚合根ID的方法。在缓存快照时,聚合根ID可以作为key缓存,这样可以根据聚合根ID获取。我们可以使用redis来实现聚合根的缓存,但是不建议使用低性能的存储中间件存储,因为不仅资源库的性能无法优化,而且无论如何都会影响性能。当然,最好的办法是存储在内存中,虽然牺牲了一些内存,但这是用空间换取时间。我们使用ThreadLocal来存储聚合根快照,所以编写的AggregateRootSnapshot实现类如下。如果聚合根id不是数据库生成的(我们不建议聚合根id由数据库生成,原因在上一篇文章中已经介绍过)。为了避免在新创建聚合根时获取到错误的快照,例如线程在执行最后一个业务用例(接口请求)时,只调用了获取聚合根的方法,而没有调用聚合根的存储方法移除Snapshot(比如获取聚合根的详细信息),这次是创建一个新的聚合根,当然资源库中获取聚合根的方法没有调用到更新快照,所以这次获取的快照会是之前的快照,所以我们还需要比较聚合根id是否相同。仅比较聚合根id并不能确保获得新的聚合根。要保证聚合根唯一,有这样一个条件:“基于每个业务用例,需要先通过资源库获取聚合根,最后需要通过资源库持久化”的特性。聚合根”,这句话是最重要的。提示:ThreadLocal类型字段是非静态的,不会造成内存泄漏吗?答案是否定的,我们以后再说。接下来,我们为使用关系数据库存储聚合根的资源库编写一个抽象类。需要使用快照来优化性能的资源库可以继承这个抽象类。RepositorySnapshotSupper实现了Repositor接口的findById、save和deleteById方法,并提供了抽象方法供子类实现。因为我们需要在findById获取聚合根的时候创建聚合根的快照并缓存,在真正保存聚合根之前获取快照完成diff判断,然后将diff结果交给子类,这样子类可以实现根据Diff结果保存,减少不必要的SQL。提示:RepositorySnapshotSupper的快照内存不是静态的,快照内存的ThreadLocal类型字段也是非静态的,所以我们需要保证一个repository中只存在一个实例(单例),以免造成ThreadLocal内存泄漏,但每个聚合根对ThreadLocal的强引用。以上步骤并不难,难点在于如何实现快照的创建和diff的实现。SnapshotUtils实现思路:前提条件:需要实体和聚合根提供一个私有的无参构造函数,用于通过反射创建实例。1、通过反射实现字段值复制。当聚合根的字段类型为非实体类型时,则为值对象类型。对于值对象类型,我们只需要复制引用即可;2、如果是实体类型的集合,则新建一个集合,并将原集合中的每个实体元素的副本添加到新集合中,并将新集合赋值给快照。实体的复制规则与聚合根相同,可以递归实现。Diff工具类的实现思路:首先定义diff结果类型:unmodified、new、updated、deleted。图1.对于聚合根,如果没有快照,则认为是Insert类型,聚合根下的所有实体也都是Insert类型;2.对于聚合根,如果存在快照,那么除了实体类型或者实体类型集合字段外,只要聚合根的其他任何一个值对象不同,都认为是聚合根diff结果为Update类型,否则为Non类型;3、只要不是新增聚合根,无论聚合根是否更新,都不会影响聚合根下实体的diff;4.如果实体与聚合根是一一对应的,即不是集合类型字段,那么:如果对应的实体快照不存在,diff结果认为是Insert,否则,如果实体snapshot存在但新的为null,则认为是Delete,否则比较entity的每个值对象,如果没有修改,则为Non,如果修改,则为Update;5.如果实体和聚合根是多对一的,即实体集合,如果订单有多个订单商品,那么需要一一比较:newitem如果找不到快照,它将被插入。如果快照中的项目没有新的实体集,它将被删除。否则比较item,没有修改就是Non,修改了就是Update。定义存储diff结果的类:由于BaseAggregate聚合根实现了实体接口(聚合根也是实体),我们在EntityDiff中使用Entity来引用聚合根/实体,这样方便获取entity直接从diff中执行插入、更新,或者获取entitySnapshot执行删除。(对于实体集合,也可以存储实体在集合中的索引。)如果聚合根下的实体字段是集合类型,则diff结果也存储在集合中:diff工具类的实现:由于项目代码不便贴出,这里简单写了一个测试用例,分享一下结果。Orderaggregateroot:提示:使用lombok有个坑。如果使用@Builder注解,需要提供一个无参构造方法(建议是私有构造方法),然后在构造方法上加上@Tolerate注解。订单项实体:订单资源库实现:当聚合根的diff结果类型为Insert时,全量存储聚合根和聚合根下的实体;当聚合根的diff结果类型为Non时,聚合根不需要更新,但是聚合根下的实体是否需要更新也需要根据聚合根实体的diff结果判断;当聚合根的diff结果类型为Update时,需要更新聚合根;获取实体的diff结果,根据diff结果决定插入、更新、删除,或者什么都不做。单元测试:单元测试结果如下:总结本文介绍了如何通过snapshot+diff来优化资源库的性能。之所以能做到这一点,是因为每个业务用例需要先通过资源库获取聚合根,最后需要通过存储库持久化聚合根。出于性能考虑,我们决定以空间换取时间,使用ThrealLocal+反射创建并缓存聚合根快照,最后使用反射完成diff逻辑。当然,diff类还是有优化空间的。本文介绍的快照是基于聚合根(DO)的。当然,我们也可以基于(PO)来实现,这样会更简单。注意:此图片中的代码可能存在错误。它尚未更新为优化代码。懒得再截图了。仅供参考!参考:阿里技术专家详解DDD系列第3讲-Repository模式本文转载自微信公众号《Java艺术》可通过以下二维码关注。转载本文请联系爪哇艺术公众号。