极端事务处理模式:Write-behind缓存(转)
原文:http://www.infoq.com/cn/articles/write-behind-caching 介绍
应用程序通常使用数据缓存来提高性能,尤其针对那些大量应用只读事务的应用程序更是如此。当数据发生变化时,这些应用程序会直接更新数据库。问题在于随着负载的增加,响应时间将随着更新的增长而延长。数据库并不擅于执行大量处理少量记录的并发事务。相对而言,处理批量事务才是数据库的强项。
对于WebSphere eXtrem Scale而言,在objectgrid.xml配置中,通过将writeBehind属性添加到backingMap元素,就启用了write- behind功能,如下所示。参数的值使用语法“"[T(time)][;][C(count)]",用以指定数据库更新发生的时间。当到达设置的时间,或者队列集中的变化次数达到设定的count值,更新就会被写入到持久存储中。
列表1:write-behind配置的示例
<objectGrid name="UserGrid"><backingMap name="Map" pluginCollectionRef="User" lockStrategy="PESSIMISTIC" writeBehind="T180;C1000" />JPA加载器
WebSphere eXtreme Scale使用了加载器读取内存中缓存的数据,以及将数据写入到数据库中。从WebSphere eXtreme Scale的6.1.0.3版本开始,包含了两个内建的加载器,它们与JPA提供器相交互,负责将关系数据映射到ObjectGrid集中,这两个内建的加载器分别为JPALoader和JPAEntityLoader。JPALoader用于缓存中存储POJO,JPAEntityLoader则用于缓存中存储ObjectGrid实体。
若要配置JPA加载器,就必须修改添加到META-INF目录下的objectgrid.xml和persistence.xml文件。加载器 bean可以与必要的实体类名一起被加入到objectgrid.xml中。在objectgrid.xml中必须定义事务的回调函数,这样就可以接收事务提交或回滚的事件,并将其发送到JPA层。persistence.xml文件则表示一个特殊的JPA提供器,它针对具有提供器的特定属性的持久化单元。
考察一个商业案例考察一个在线的银行网站,当用户数增长时,响应时间变慢,且存在可伸缩性问题。他们需要在现有硬件环境下支持客户需求。接下来,我们就以这一案例来展示write-behind特性是如何帮助他们解决这一问题的。
用例:门户个性化与直接从数据库中拉取用户个人信息不同,我们首先会将数据库中取出的个人信息,预先加载到缓存中。这意味着缓存是为读请求服务,而不是数据库。在旧系统中,个人信息的更新同样是直接写入到数据库。在可接受的响应时间内,随着数据库服务器的超负荷,每秒钟发生的并发更新数将受到限制。新的系统则是将个人信息的变更写入到网格中,然后使用write-behind技术将这些变更推到数据库中。这就保证了服务的质量和性能,并完全解除了单实例数据库与读写个人信息操作之间的耦合。现在,客户只需要将更多的JVMs/server添加到网格中,就可以扩大个人信息服务的规模。数据库不再成为瓶颈,因为发送到后端的事务数得到了锐减。更快的响应时间带来更快的页面加载,大大改善了用户体验,有效地利用了个人信息服务器的规模,提高了可用性,因为数据库不再成为单一失败点。
对于本用例,我们对DB2O数据库采用了WebSphere eXtreme Scale以及OpenJPA提供器。这一场景的数据模型为User与UserAccount以及UserTransaction之间的一对多关系。下面为User类的代码片段,展现了这样一种关系:
列表2:用例的实体关系
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = { Cas-cadeType.ALL })private Set<UserAccount> accounts = new HashSet<UserAccount>(); @OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = { Cas-cadeType.ALL })private Set<UserTransaction> transactions = new HashSet<UserTransaction>();1.生成数据库
示例代码包含了PopulateDB类,它能够加载某些用户数据到数据库中。DB2数据库的连接信息被定义在之前展示的 persistance.xml文件中。在persistence.xml中的持久单元名(persistence unit name)将用于创建JPA EntityManagerFactory。用户对象会被创建,然后被批量地持久化到数据库中。
2. 准备缓存一旦数据库被加载,数据网格代理(data grid agents)就会预先加载缓存。记录被批量写入到缓存中,因此在客户端与服务器之间几乎没有产生通信。还可以使用多客户端(multiple clients)来加快准备时间。在准备缓存时,一些“热门”数据可以作为所有记录的子集,其余数据则可以采用延迟加载。预先加载缓存提高了缓存命中的几率,减少了从后端层中获取数据的需要。在这个例子中,匹配数据库记录的数据会被插入到缓存中,而不是从数据库中加载,从而加快了执行时间。
3.在网格上生成加载示例代码包含一个客户端驱动器,模仿在网格上的操作,用以演示write-behind缓存功能如何提高性能。客户端包含多个选项调整加载行为。下面的命令使用了10个线程,以每个线程200个请求将500k的记录加载到“UseGrid”网格中。
$JAVA_HOME/bin/java -Xms1024M -Xmx1024M -verbose:gc -classpath$TEST_CLASSPATH ogdriver.ClientDriver -m Map -n 500000 -g UserGrid -nt 10 -r200 -c $CATALOG_14.结果
使用write-behind特性能够显著提高性能。我们分别使用了write-through和write-behind技术运行示例代码,比较了各自的响应时间以及数据库的CPU占用率。插入到缓存中的数据与数据库中的记录相匹配,这就避免了缓存的准备时间,保证读取的响应时间一致,这样才能比较写的响应时间。列表3展示了write-through场景下的响应时间,读取数据花费了不到10ms,而写花费的时间达到了450——550ms。在执行过程中,网络跳转与磁盘I/O操作耗费了大多数时间。列表4展示了write-behind场景下的响应时间,在这种情况下所有的事务都被提交到网格中。从图表中可以看出,它所消耗的读和写的响应时间几乎相同,都消耗了2.5-5ms。这两个图表极为明显地说明,对于更新而言,write- through响应时间更长,而write-behind耗费的更新时间几乎与读取操作相同。如果添加更多的JVM,则能够增加缓存的容量,无需改变响应时间,数据库也不再成为瓶颈。
列表3:采用write-through缓存的响应时间图表
列表4:采用write-behind缓存的响应时间图表
数据库的CPU占用率图表展现了使用write-behind对后端加载的改善。列表5是一幅采用write-through产生的数据库CPU占用图。可以看见,在运行及时写入数据库的所有更新时,CPU的使用是连续的。使用时,CPU空闲状态的波动在4%到10-12%之间。采用write- through技术,后端加载保持了恒定的值。如果采用write-behind,在满足缓存的间隔时间内,后端的加载耗费了更低的CPU占用率,如列表 6所示。该图显示,当数据库较为空闲时,占用率为4%,在三分钟间隔时间结束,或者更新记录数达到峰值时,会批量地将更新写入到磁盘中,此时CPU占用率会在短时间内达到12-16%。write-behind的配置可以调整,以满足自己的运行环境,这其中包括写事务的比例,相同记录更新的频率,以及数据库的更新延迟。?
列表5:采用write-through缓存的数据库CPU占用率图表?
列表6:采用write-behind缓存的数据库CPU占用率图表
结论本文重新审视了write-behind缓存技术,并展示了如何在WebSphere eXtreme Scale下实现这一特性。write-behind缓存功能减少了后端加载,降低了事务的响应时间,并将应用程序与后端失败隔离开。这些优势以及它配置的简单性,都使得write-behind缓存异常的强大。
参考感谢Billy Newport,Jian Tang,Thuc Nguyen,Tom Alcott和Art Jolin对撰写本文给予的帮助。
查看英文原文:Extreme Transaction Processing Patterns: Write-behind Caching