@Rays
2017-07-18T09:56:13.000000Z
字数 9168
阅读 1833
ORM
语言开发
摘要: Goldman Sachs在投资银行业务领域广为人知,它同样也是一家领先的技术公司。Reladomo是Goldman Sachs主要采用的Java ORM,现在开源发布了。在本文中,Goldman Sachs的技术专家Mohammad Rezaei将为我们介绍Reladomo的一些高级特性,包括分片、缓存、双时态方法、性能以及测试。
作者: Mohammad Rezaei
审校: Victor Grazi
正文:
本文要点
- Reladomo是Goldman Sachs开发的一个企业级Java ORM,在2016年开源发布。
- 性能是Reladomo的关键交付。通过提供对IO最小化、组合并扩展的功能,赋予了开发人员优化自身应用性能的工具。
- Reladomo的定制内存有效地利用了内存,实现了IO的降低,并提升了性能。
- Reladomo的企业特性集合使得它有别于传统的ORM。分片和支持时态对象是企业特性中的亮点。
- 可测试性并非是后添加到Reladomo中的。Reladomo所提供的测试资源非常适合于编写高质量的测试,将会改进应用代码库的长期生存力。
在本文第一部分中,我们介绍了Reladomo的可用性和可编程性等特性,并给出了一些指导开发的核心理念。在第二部分和最后一部分中,我们将介绍Reladomo的性能、可测试性及部分聚焦于企业应用的特性。
高性能解决方案是大规模可扩展企业应用的基石。从框架的角度看,性能包括两个方面:
具体到数据库的ACID交互,最关键的关注点可归结为正确的IO操作。我们在Reladomo中还发现,相对于带宽而言,延迟的问题更大,因此我们围绕延迟开展优化。简而言之,“正确的IO操作”意味着编写可最小化、可组合(即批处理)和IO可扩展(即多线程)的代码。
在先前给出的本文第一部分中,我们已经介绍了deepFetch功能和Reladomo中的高级关系,这些功能显著地降低了对象图上的IO。在读路径上,Reladomo的完全定制缓存对降低IO会有显著的效果,我们随后将做详细介绍。在写路径上,对同一对象的多重写将会自动地组合到同一工作单元中,实现对数据库调用的最小化。
下面两个特性使得应用在进行查询时适当地组合IO:
回到我们分类账目例子中的Balance对象。如果我们想要从一对Account/Product的列表中检索Balance,可以如下编写代码:
MithraArrayTupleTupleSet tupleSet = new MithraArrayTupleTupleSet();
tupleSet.add(1234, 777); //在查询中添加Account和Product信息。
tupleSet.add(5678, 200);
tupleSet.add(1111, 250);
TupleAttribute acctAndProd = BalanceFinder.acctId().tupleWith(BalanceFinder.productId());
Operation op = BalanceFinder.businessDate().eq(today);
op = op.and(BalanceFinder.desk().eq("A"));
op = op.and(acctAndProd.in(tupleSet));
BalanceList balances = BalanceFinder.findMany(op);
对于小规模集合,Reladomo会将上面的操作转译为一个“or-and”语句。而对于大规模集合,则需要在后台使用到临时表,并对Balance表做连接运算。
在写路径上,包括自动批处理在内的批量操作工具将有助于增强写操作。Reladomo对事务中的写操作重新排序,在无需对正确性做出妥协的情况下最大化批处理。Reladomo也会选取一个适合于工作情况和数据库规模的批处理策略。例如,Reladomo可以在四种不同的插入策略(即bulk、union、multi-value和jdbc-batch)中做出选取。
Reladomo主要采用两种有助于扩展IO的模型:
Reladomo的多线程加载器就是这样一种集成了所有概念的通用工具。它覆盖了一个简单但是高度循环并可重用的用例,即对于给定的输入的大型数据集(例如一个文件),什么是最有效地在数据库中插入、更新和删除相应数据行的方法?当以批处理方式将数据从一个系统拷贝到另一个系统时,该用例是非常典型的。使用多线程加载器的情况下,源和目标将被异步读取、比较并有效地回写。其中所涉及的组件包括:完成比较功能的MatcherThread、读取源和Sink的InputThread和DbLoadThread,以及完成写操作智能批处理的SingleQueueExecutor。大量地对读、比较和写操作使用多线程,这就是我们扩展IO的方法。DbLoadThread使用Reladomo提供的forEachWithCursor方法构建数据库的数据流,使得读操作最小化。SingleQueueExecutor对IO做智能组合,降低了死锁。只需要几行代码,就能让多线程加载器的实例跑起来。如果你具有这个用例,那么尝试一下!
要使用上面所提供的功能,关键在于应用设计。在我们给出的分类账目例子中,如下设计将会显著地提升吞吐量:
Reladomo提供了一个定制缓存,该缓存比任何通用缓存都更好地匹配了ORM的需求。Reladomo缓存并非Map结构,而是一系列的无键索引,其中每个索引都是一个可搜索的集合或多重集。构成特定索引标识的属性是任意的。缓存总是具有一个主键索引,其它索引是根据应用的定义或是对象间的关系而添加。
为允许缓存覆盖整个JVM,Reladomo保证具有特定主键的对象在JVM中只存在一次。这使得该缓存比基于会话的缓存更为有用。此外,也使得该缓存比二级序列化缓存更加高效,因为无需对同一对象做二次存储,也无需做序列化和反序列化。不同于其它的ORM缓存,Reladomo缓存中的对象对应用是可见的,并可被应用所使用。
缓存也完全可感知Reladomo的事务上下文。事务上的更改操作(即insert/update/delete操作)在事务内部是可见的,但只有在更改提交后才对外部可见。
缓存可配置为按需填充(基于被触发的查询),或是在启动时完全填充。完全填充的缓存适用于小型静态对象(例如,“国家”或“货币”)。在一些适当的场景下,完全填充的缓存也适用于大型的数据集。
缓存结构也可完全定制,用于临时对象的存储(参见下文)。临时对象需要一种完全不同的内存中存储,因为业务主键并不能唯一地标识一行。Reladomo的SemiUniqueDatedIndex是一个单一数据结构,对同一数据以两种不同的方式做哈希。
此处对缓存做了更专业的讨论。
在企业场景中,一个特定的数据集不太可能只被一个JVM访问或更改,这对ORM缓存提出了严重的问题。为解决该问题,Reladomo的缓存支持缓存间的通知机制。通知由一系列轻量级的缓存过期消息组成,通过广播网络发送。Reladomo创造性地给出了一种广播网络的TCP实现,适合于数百个规模的JVM。该TCP实现为一个基本的“轴辐式”(Hub-Spoke)模型,并出于容错的考虑而加入了双重Hub。在该实现中也非常易于插入其它的广播实现,只需要实现一系列的小型接口。对缓存通知机制的更多介绍,参见此处。
根据应用访问模式的不同,一些问题需使用内存中架构才能很好地解决。如果数据是存活于数据库中的(并且可能是分片的和双时态的),内存中缓存的扩张并保持最新是非常困难的,其中的原因包括:
Reladomo的堆外缓存解决了如下的问题:
在企业场景中,相比于传统ORM可以提供的,需要对代码和数据做更宽泛的考虑。企业级ORM需要适合对象的全生命周期,从生成到停用。
回到有价证券分类账目这一教科书例子,它具有如下的需求:
要在统一的代码库中实现如上需求,Reladomo提供了一系列很广泛的能力。
ACID,即原子性(Atomic)、一致性(Consistent)、隔离性(Isolated)和持久性(Durable),是Reladomo存储的基本假设之一。扩展ACID需要进行审慎的设计。为此,Reladomo提供了内建的分片特性。对分片的识别也保存在对象中,一并维护在内存中,而不是持久性存储上,并成为对象完整身份的组成部分。这简化了传统主键的构建,不必担心全局唯一性。例如,可以赋予分片A中的一个交易以一个简单数字标识“17”,这样以此为标识的分片就可以被其它交易所用的分片重用。
分片是作为Reladomo API的头等部分处理的。这意味着,与分片对象交互不需要进行配置转换或是代码隔离。一个查询可以跨越多个分片,这是Finder API天然提供的功能,将分片属性与其他属性等同对待。例如:
Operation op = BalanceFinder.businessDate().eq(today);
op = op.and(BalanceFinder.desk().in(newSetWith("A", "DR", "T", "ZZ")));
op = op.and(moreOperations());
BalanceList balances = BalanceFinder.findMany(op);
balances.setNumberOfParallelThreads(3);
上面代码会使用多个线程访问所有列出的分片(如果是在事务之外)。
对于分类账目的例子,我们可以通过采用一种分片设计使用该功能。Reladomo将这种分片策略的决策权交给了应用。通常会使用下面两个策略之一:
分类账目交易处理流水线如下图所示:
交易来自于各种上游的源。它们被路由到适当的分片,并在分片中做处理(通常是以FIFO的方式)。可以将不同分片上的操作安排为完全独立的。该架构可以很好地扩展到上百个分片,很容易实现对上百万交易的处理。
以正确的时态顺序重新生成前期报告及属性活动,这是包括在分类账目的核心需求之中的。对于“可重新生成”,我们指的是获取与过去已完成的查询完全相同结果的能力。对于“正确的时态次序”,我们指的是可以使用新的信息修复对过去时刻的视图,而不会影响到过去已完成查询的结果。简单以交易为例,如果我们推迟了一天才访问分类账目。那么首要的是,由此新信息而采取的任何行动不能更改过去查询的结果。如果用户提问:“我们昨天下午五点发送了一个昨日交易活动的报告。它看上去如何?”分类账目系统必须能正确地回答该问题。其次,分类账目必须可以将该交易置于到昨日的事件流中。当一个用户提问:“根据我们现在所知的所有事情,昨日的交易活动情况如何?”分类账目系统必须可以在这些数据中展示新的交易。这一问题具有两个二维度。一个时间维度是完全可重生性,这在文献中被称为“交易时间”,我们在Reladomo中称其为“processingDate”。另一个时间维度用于表示业务视角的事件,而考虑实时发生的情况。这在文献中被称为“有效期”,我们在Reladomo中成为“businessDate”。处理日期完全由Reladomo处理,它总是对应于挂钟时间(Wall-clock Time)。业务数据是完全灵活的,应用必须决定如何处理它。
从API角度看,时态问题通过包括我们将在下面介绍的四个域,存在于Reladomo实现的每个方面上。时间维度是查询API的一部分:
Timestamp today = toTimestamp("2017-05-03");
Timestamp lastEvening = toTimestamp("2017-05-03 18:30");
Operation op = BalanceFinder.businessDate().eq(today);
op = op.and(BalanceFinder.processingDate().eq(lastEvening));
op = op.and(BalanceFinder.desk().eq("A"));
op = op.and(moreOperations());
BalanceList balances = BalanceFinder.findMany(op);
这将对特定业务日期和处理日期检索账户信息。对象域API包括getBusinessDate()和getProcessingDate()。换句话说,每个在应用中使用的对象被定位成二维时态空间中的某一点。值在被检索时或构建时,就标记在对象上。
Reladomo框架的一个亮点是定义对象间时态关系的功能。如果我们要检索一个账户对象,并询问其账号或产品,这些对象对应于时态空间中的同一个点。整个可浏览的对象图表示了一个一致的数据时态视图。这一概念同样也在查询上执行,即一旦一个关系在查询中被浏览,时态信息会被应用到查询上。例如:
Timestamp thirdQuarter = toTimestamp("2016-09-30");
Timestamp lastYear = toTimestamp("2016-12-01 20:00");
Operation op = BalanceFinder.businessDate().eq(thirdQuarter);
op = op.and(BalanceFinder.processingDate().eq(lastYear));
op = op.and(BalanceFinder.desk().eq("A"));
op = op.and(BalanceFinder.product().productName().startsWith("S"));
BalanceList balances = BalanceFinder.findMany(op);
In the above query, productName corresponds to what it was last year.
写入双时态对象需要一个更宽泛的API集合,隐藏了大量的更为复杂的实现。API覆盖了如下方面:
API的开发需求来自于已使用双时态存储的真实世界应用。对API的更全面介绍,参见“Reladomo Kata”中的双时态章节内容。下面让我们看一个例子。如果我们基于交易活动存储一个账户的余额量,在两次交易后,余额在数据库中如下图所示:
如果我们反过来添加一个交易,使用下面的代码就能更新余额:
Timestamp oldTradeDate = toTimestamp("2005-01-12");
Operation op = BalanceFinder.businessDate().eq(oldTradeDate);
op = op.and(BalanceFinder.desk().eq("A"));
op = op.and(BalanceFinder.balanceId().eq(1234));
Balance balance = BalanceFinder.findOne(op);
balance.incrementValue(40);
下图显示了上面代码对数据库中余额数据的作用:
Reladomo从一行代码incrementValue执行了两次Insert和两次Update操作。控制对双时态数据修改的规则是相当地复杂的。如果寄希望于每位开发人员能在传统的ORM上实现它们,那么就会导致软件缺陷和潜在的数据丢失。这种后台机制的实现就是Reladomo双时态API的价值定位。
Reladomo还提供了一系列较细微的特性,使得在企业场景中的工作更平滑。其中一些为:
一名好的开发人员应该知道,每个要运行于生产环境的应用应该具备经良好测试的代码。对于那些具有高度监管审查、信誉风险或安全问题的企业(例如金融和卫生保健),其真实性存疑。
要测试与持久数据存储交互的代码,存在着如下的问题:
Reladomo内建的测试框架通过提供一个对完全采样实例化一个内存中数据库的测试资源,解决了上述问题,参见“Reladomo Test Resource”一文。其中,数据库使用了一系列易于管理的文本文件填充;无需编码难以读取和修改的insert语句;所有交互不用物理资源就可以被测试,即测试可以运行在没有网络连接的机器上;开发人员可以轻易地检查所生成的SQL(不存在标识变量的问号!)并对IO做推理,例如,为定位缺失的deepFetch或对写操作优化;完全支持混合了所有操作(即写操作和读操作)的完整生命周期测试。
在首次发布以来,Reladomo的测试框架就作为一部分存在于代码中。Reladomo本身也大量使用这些测试。
对遗留代码的提升是内建测试框架的一个最好用例。对未经良好测试的已有代码引入测试,并使用Reladomo重新实现,实现了对遗留代码的高置信度替换。
Reladomo的可测试性、性能和企业级特性使得编写用于ACID数据库交互的面向对象代码成为现实。Reladomo填充了传统ORM特性在企业空间中的空白。Reladomo通过提供API以编写更好更紧致的代码,并无缝地测试这些代码,使得开发人员可以将他们的时间用于他们应用和业务逻辑的高层设计特性。
如果你有兴趣更深入地了解Reladomo,可以尝试Kata练习,它是我们给出的一套Reladomo教程,其中附带地使用了Reladomo测试框架,因此你不需要安装一个数据库才能开始尝试。欢迎访问我们的Github项目,并查看文档
Mohammad Rezaei是Reladomo的主架构师,任Goldman Sachs公司平台业务部门的技术专家(Technology Fellow)。他具有在多种环境中编写高性能Java代码的丰富经验,从分区、并发交易系统,乃至采用无锁算法实现最大吞吐量的大内存系统。Mohammad具有宾夕法尼亚大学的计算机科学本科学位,并在康奈尔大学获得物理学博士学位。
查看英文原文: Introducing Reladomo - Enterprise Open Source Java ORM, Batteries Included! (Part 2)