1. 问题描述
TopLink缓存是一块位于内存中的交换区,用于存储最近读写的实体对象。该缓存主要用于以下几方面:
1) 通过读写缓存来减少数据库的连接次数,从而提高系统性能
2) 管理锁和隔离机制
3) 管理数据完整性
TopLink采用了两种类型的缓存,分别为SessionCache(L2)和Unit of Work Cache(L1),其中前者用于存储数据库读写过程中产生的实体对象,而后者用于存储一次事务中的数据。图1描述了Session Cache、Unitof Work Cache及数据库之间如何协同工作:
图1.TopLink缓存工作机制(引自官方文档“Developer's Guide for Oracle TopLink”)
在单机环境下,我们通过TopLink提供的实体缓存机制能够有效地减少数据库访问次数从而提高系统性能,但是对于大量运行在集群服务器环境下的J2EE应用,缓存机制反而会带来一系列意想不到的问题,其中最显著的一个问题就是由于集群中各节点的缓存不一致导致的脏数据,如图2所示:
图2.集群环境下的实体缓存不一致问题
集群环境中实体缓存不一致问题产生的原因简述如下:假设客户端执行了几次查询操作,并且查询的是同一个数据Q(假设其值为1),则在集群负载均衡的作用下,这些查询操作会分摊到节点A及节点B。由于TopLink缓存机制的存在,节点A和节点B均会在自己的内存区域中保存一份Q实体对象。随后,客户端对数据Q发出了修改操作命令,将其值修改为2,并且在集群负载均衡算法的作用下,该命令被分配给节点A执行,那么在此次操作执行完毕后,数据库中相应记录的值被改为2,且缓存A中的实体对象内容也被更新为2,需要注意的是,此时缓存B中的内容并未更新,仍然为1。接下去,客户端又发出了一条查询数据Q的命令,这时就会有两种情况:假如该命令被分配到节点A执行,那么由于缓存A中存在Q实体对象,所以节点A直接将其值返回而不再去访问数据库,这时我们将获得正确的结果2;如果该命令被分配到节点B执行,由于缓存B中也存在Q实体对象,所以节点B也不会访问数据库,而是直接返回Q对象的值1,显然,这个结果是错误的。
2. 解决方案
针对上一节提出的问题,Oracle官方总结了下列几种解决方案:
(1)关闭缓存,或仅对只读对象进行缓存
(2)利用乐观锁防止对脏数据的更新,同时触发缓存刷新操作
(3)采用分布式缓存框架(如Oracle Coherence)
(4)利用缓存同步机制保证集群中各节点缓存的一致性
其中,方案(1)直接关闭了L2缓存,即每次需要读写数据时均重新建立数据库连接,完全放弃了缓存带来的性能优势。方案(2)中的乐观锁机制并不能直接解决集群缓存一致性问题,它的作用仅仅是禁止对脏数据的更新操作,通常是配合其它方法一起使用,关于这一点在下文中还会提及。方案(3)是基于Oracle的Coherence集群技术的分布式缓存框架,是通过使用一个具体的产品解决问题,不具备通用性。下面重点介绍方案(4)。
方案(4)的原理是当集群节点中的某一个节点的数据发生更新时,通过广播通知集群中的其它所有节点进行同步更新。虽然该方法可以保证集群缓存的一致性,但其产生的网络通讯及同步开销也是不可忽略的。但和直接关闭缓存相比,在绝大多数情况下其性能仍然具有优势(除非是在极端情况下,例如应用中几乎没有单纯查询操作,需要频繁地更新数据,集群规模极其庞大),毕竟网络IO的效率通常高于数据库IO。目前主流的JPA实现(包括TopLink)均提供了三种集群缓存同步方式:JMS、RMI及CORBA。Oracle在其官方文档中明确建议使用JMS技术进行缓存同步,下面也将重点介绍该方法。
3.基于JMS的JPA缓存同步配置
JMS(Java Message Service)是JAVA专门为企业级消息服务提供的面向消息中间件的API,相比同步调用的RMI和CORBA技术,JMS提供了客户端和服务器间的异步通信,并实现了客户端和服务器端生命周期的解耦。本章阐述如何利用JMS技术进行集群缓存同步,主要分为以下几个步骤:
(1) JMS服务器创建
进入Weblogic控制台,依次选择服务->消息传送->JMS服务器,打开JMS服务器概要页面。点击“新建”按钮开始创建JMS服务器,在接下去的页面中,需要设置JMS服务器的名称,选择无持久性存储,并选择JMS服务器配置于哪个Weblogic服务器实例上(一般为集群中的管理服务器)。
(2) JMS资源配置
依次选择服务->消息传送->JMS模块,进入JMS模块管理页面。选择“新建”开始创建JMS模块。在接下去的页面中需要设置JMS模块的名称,以及该JMS模块的部署目标(选择管理服务器及集群中的所有服务器)。
在JMS模块管理页面中选择刚才创建的JMS模块,点击“子部署”标签,随后点击“新建”按钮开始创建子部署(子部署是一种机制, 通过此机制可将 JMS 模块资源,如队列, 主题和连接工厂等分组并将其定位到服务器资源,如 JMS 服务器, 服务器实例或集群)。在接下去的页面中需要设置子部署名称及部署目标(选择刚创建的JMS服务器)。
退回到JMS模块管理页面,选择刚才创建的JMS模块并点击“配置”标签,随后点击“新建”按钮开始创建JMS资源。在这里我们需要配置两种JMS资源,分别是JMS连接工厂及JMS主题。首先创建“主题”,在接下去的页面中需要设置JMS主题的名称、JNDI名称及子部署(选择刚创建的子部署)。随后再创建“连接工厂”,在接下去的页面中需要设置名称、JNDI名称、订阅共享策略(选择“可共享的”)、客户机ID策略(选择“无限制”)及子部署(选择刚创建的子部署)。
至此,我们就完成了JMS服务器端的配置,可能需要重启服务器已完成部分更新。
(3) persistence.xml中的JMS配置
在需要进行集群缓存同步的JPA应用的persistence.xml文件中添加下列配置信息:
(设置缓存同步方式为JMS)
<propertyname="eclipselink.cache.coordination.protocol" value="jms"/>
(设置JMS Topic为刚创建的JMS主题)
<propertyname="eclipselink.cache.coordination.jms.topic" value="CoordinateTopic"/>
(设置JMS ConnectionFactory为刚创建的连接工厂)
<propertyname="eclipselink.cache.coordination.jms.factory"
value="CoordinateTopicConnectionFactory"/>
(设置JMS URL HOST为JMS服务器地址)
<property name="eclipselink.cache.coordination.jms.host"
value="t3://localhost:7001/"/>
(设置JNDI用户名为JMS服务器所在的Weblogic服务器实例的用户名)
<propertyname="eclipselink.cache.coordination.jndi.user"
value="weblogic"/>
(设置JNDI密码为JMS服务器所在的Weblogic服务器实例的登录密码)
<property name="eclipselink.cache.coordination.jndi.password"
value="weblogic1"/>
完成配置后重新部署应用到集群服务器,即可实现集群环境下的TopLink缓存同步。
4. 乐观锁配置
乐观锁是一种防止对脏数据进行更新操作的机制,有多种实现方式,其中最常用的是基于版本号的乐观锁。CVS、SVN等版本控制软件就是乐观锁技术应用的典型案例。回到第一节讨论的问题,假设节点A和节点B均已持有实体对象Q的缓存(此时Q的版本号为1),若节点A对Q进行了更新操作并提交事务,此时Q的版本号变为2;之后若节点B试图对Q进行更新操作,在事务提交时由于检测到Q的实际版本高于节点B所持有的Q的版本,将产生OptimisticLockException。因此单纯的乐观锁只是防止了对脏数据的更新操作,而并非解决缓存同步的问题。Oracle官方文档中指出,当使用了任何一种缓存同步策略后,建议配合某种乐观锁机制,以最大程度地保证数据一致性。
5. 结语
都是ORM框架惹的祸~