`
suhuanzheng7784877
  • 浏览: 691780 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
Ff8d036b-05a9-33b5-828a-2633bb68b7e6
读金庸故事,品程序人生
浏览量:47239
社区版块
存档分类
最新评论

用xmemcache作为JPA(Hibernate实现)二级缓存

 
阅读更多

1.  持久层的缓存

Hibernate中提供了两级Cache,第一级别的缓存是Session级别的缓存,它是属于事务范围的缓存。这一级别的缓存由hibernate自身进行管理的,一般情况下无需进行干预,默认一级缓存也是打开的;第二级别的缓存是SessionFactory级别的缓存,它是属于进程范围或群集范围的缓存。这一级别的缓存比较重量级,可以进行配置和更改,并且可以动态加载和卸载。 Hibernate还为查询结果提供了一个查询缓存,它依赖于第二级缓存。

每个事务都有单独的第一级缓存进程范围或集群范围,缓存被同一个进程或集群范围内的所有事务共享并发访问策略。由于每个事务都拥有单独的第一级缓存,因此一级缓存不会出现并发问题,无需提供并发访问策略,由于多个事务会同时访问第二级缓存中相同数据,因此二级缓存必须提供适当的并发访问策略,来保证特定的事务隔离级别数据过期策略没有提供数据过期策略。处于一级缓存中的对象永远不会过期,除非应用程序显式清空缓存或者清除特定的对象必须提供数据过期策略,如基于内存的缓存中的对象的最大数目,允许对象处于缓存中的最长时间,以及允许对象处于缓存中的最长空闲时间物理存储介质内存内存和硬盘。

对象的散装数据首先存放在基于内存的缓存中,当内存中对象的数目达到数据过期策略中指定上限时,就会把其余的对象写入基于硬盘的缓存中。缓存的软件实现是在HibernateSession的实现中包含了缓存的实现由第三方提供,Hibernate仅提供了缓存适配器(CacheProvider),用于把特定的缓存插件集成到Hibernate中。启用缓存的方式只要应用程序通过Session接口来执行保存、更新、删除、加载和查询数据库数据的操作,Hibernate就会启用第一级缓存,把数据库中的数据以对象的形式拷贝到缓存中,对于批量更新和批量删除操作,如果不希望启用第一级缓存,可以绕过Hibernate API,直接通过JDBC API来执行操作。用户可以在单个类或类集合的粒度上配置第二级缓存。如果类的实例被经常读但很少被修改,例如常量数据集合,就可以考虑使用第二级缓存。

只有为某个类或集合配置了第二级缓存,Hibernate在运行时才会把它的实例加入到第二级缓存中。用户管理缓存的方式第一级缓存的物理介质为内存,内存虽然存取速度比较快,奈何由于内存容量有限,必须通过恰当的检索策略和检索方式来限制加载对象的数目。Session evit()方法可以显式清空缓存中特定对象,但这种方法不值得推荐。第二级缓存的物理介质可以是内存和硬盘,因此第二级缓存可以存放大量的数据,数据过期策略的maxElementsInMemory属性值可以控制内存中的对象数目。管理第二级缓存主要包括两个方面:选择需要使用第二级缓存的持久类,设置合适的并发访问策略:选择缓存适配器,设置合适的数据过期策略。

对应于一级缓存,当应用程序调用Sessionsave()update()savaeOrUpdate()get()load(),以及调用查询接口的list()iterate()filter()方法时,如果在Session缓存中还不存在相应的对象,Hibernate就会把该对象加入到第一级缓存中。当清理缓存时,Hibernate会根据缓存中对象的状态变化来同步更新数据库。Session为应用程序提供了两个管理缓存的方法:evict(Object obj):从缓存中清除参数指定的持久化对象。 clear():清空缓存中所有持久化对象。

Hibernate二级缓存策略的一般过程如下:
1)
条件查询的时候,总是发出一条select语句(选择所有字段)这样的SQL语句查询数据库,一次获得所有的数据对象。

2) 把获得的所有数据对象根据ID放入到第二级缓存中。

3) Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;查不到,如果配置了二级缓存,那么从二级缓存中查;查不到,再查询数据库,把结果按照ID放入到缓存。

4) 删除、更新、增加数据的时候,同时更新缓存。

Hibernate二级缓存策略,是针对于ID(主键)查询的缓存策略,对于条件查询则毫无作用。为此,Hibernate提供了针对条件查询的Query Cache,就是之前说的查询缓存。更新缓存的代价比从缓存中查询的代价大得多,因为缓存底层是个大Map或者是个大Item(org.hibernate.cache.ReadWriteCache.Item)进行存储的。而且存储的一般都是对象,空间移动,存取,修改的代价可想而知。那么什么样的数据适合存放到第二级缓存中?

1) 很少被修改的数据

2) 不是很重要的数据,允许出现偶尔并发的数据

3) 不会被高并发访问的数据

4) 参考数据,指的是供应用参考的常量数据,它的实例数目有限,它的实例会被许多其他类的实例引用,实例极少或者从来不会被修改。

那么什么样的数据不适合存放到第二级缓存中?

1) 经常被修改的数据,代价太大了,得不偿失。

2) 金钱敏感数据,绝对不允许出现并发

3) 与其他应用共享的数据。

Hibernate的二级缓存往往要借助第三方的工具,下面是几种常用的缓存工具:

1):EhCache:可作为进程范围的缓存,存放数据的物理介质可以是内存或硬盘,对Hibernate的查询缓存提供了支持。

2):OSCache:可作为进程范围的缓存,存放数据的物理介质可以是内存或硬盘,提供了丰富的缓存数据过期策略,对Hibernate的查询缓存提供了支持。

3):SwarmCache:可作为群集范围内的缓存,但不支持Hibernate的查询缓存。

4):JBossCache:可作为群集范围内的缓存,支持事务型并发访问策略,对Hibernate的查询缓存提供了支持。

5):当然了还有我们这次要介绍的Memcacahe

Memcacahe这个开源项目,是一个高性能的分布式的内存对象缓存系统,通过在内存里维护一个统一的巨大的Hash表,能够用来存储各种格式的数据。其实Memcache最开始是作为高可用集群的缓存解决方案的。一些互联网的非敏感信息就放到Memcache中,用户第一次访问还是从数据库里面进行查询,之后放到缓存中,之后用户的访问都从缓存中去取。至于缓存更新,根据业务不同,有不同的方案,比如一旦数据发生变更,就是先更新数据库后,立即更新缓存数据,还有一种做法就是对于十分不敏感的数据仅仅更新缓存,定时向数据库进行同步和更新。它还可以作为高分布式系统的Session的解决方案。Memcache还可以作为Hibernate的二级缓存插件。hibernate-memcached这个java类库用于在Hibernate中使用Memcached作为一个二级分布式缓存。支持实体和查询缓存。

2.  搭建环境

hibernate-memcachedGoogle的托管项目,开发源代码。

http://code.google.com/p/hibernate-memcached/可以下载

 

Memcache自身需要一个服务端,windows版本,笔者找了好久,终于在http://code.jellycan.com/memcached/找到,读者可自己下载。下载后是一个exe文件。

要想在我们自己的项目中使用好Memcache,还得使用一个Memcacahe客户端程序。笔者极力推崇dennis兄的Xmemcached作为客户端。可以从

http://code.google.com/p/xmemcached/下载。当然memcache还需要依赖slf4j-api包。

之后就是JPA规范与Hibernate相关包的导入了,当然可以借助IDE了,笔者前面也有相关笔记写到,在此就不再赘述了。搭建好环境后的工作空间如下

 3.  编写业务代码

首先先配置JPA的属性

	<persistence-unit name="HibernateSecondCachePU"
		transaction-type="RESOURCE_LOCAL">
		<provider>org.hibernate.ejb.HibernatePersistence</provider>
		<class>pojo.Productsmessageinfo</class>
		<class>pojo.Products</class>
		<properties>
			<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
			<property name="hibernate.connection.url" value="jdbc:mysql://127.0.0.1:3306/tgweb" />
			<property name="hibernate.connection.username" value="liuyan" />
			<property name="hibernate.connection.password" value="111111" />
			<property name="hibernate.hbm2ddl.auto" value="update" />
			<property name="hibernate.cache.use_second_level_cache"
				value="true" />
			<property name="hibernate.show_sql" value="true" />
			<!-- 结构化方式存储 -->
			<property name="hibernate.cache.use_structured_entries"
				value="true" />
			<!-- 查询缓存 -->
			<property name="hibernate.cache.use_query_cache" value="true" />
			<!-- 二级缓存服务类 -->
			<property name="hibernate.cache.provider_class"
				value="com.googlecode.hibernate.memcached.MemcachedCacheProvider" />
			<!-- 二级缓存服务地址和端口 -->
			<property name="hibernate.memcached.servers" value="127.0.0.1:11211" />
			<!-- memcache的调用客户端 -->
			<property name="hibernate.memcached.memcacheClientFactory"
				value="net.rubyeye.xmemcached.utils.hibernate.XmemcachedClientFactory" />
		</properties>
	</persistence-unit>

当着明人不说暗话,其实这段配置中,其他配置都好说,就是hibernate.cache.use_structured_entries这个配置,笔者查了半天,不是很明白。文档说是结构化存储,没说怎么个结构化存储~网上都说是人性化存储,笔者的疑问是,什么是人性化存储?怎么样进行人性化存储?不是特别明白,这个大家可以讨论。

下面咱们来看实体POJO

package pojo;

import static javax.persistence.GenerationType.IDENTITY;

import java.sql.Timestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Table(name = "products", catalog = "tgweb")
@org.hibernate.annotations.Cache(usage =CacheConcurrencyStrategy.READ_WRITE)
public class Products implements java.io.Serializable {

	// Fields

	private Long id;
	private Productsmessageinfo productsmessageinfo;
	private Double nowPrice;
	private String picture;
	private String productMess;
	private String productName;
	private Double sourcePrice;
	private Timestamp updateTime;
	private String productsMessageInfo;

	// Constructors

	/** default constructor */
	public Products() {
	}

	@Id
	@GeneratedValue(strategy = IDENTITY)
	@Column(name = "id", unique = true, nullable = false)
	public Long getId() {
		return this.id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "productsMessageInfo_ID")
	public Productsmessageinfo getProductsmessageinfo() {
		return this.productsmessageinfo;
	}
    …………………………省去setter/getter

}

 其中类上的标注,详细解释请看http://www.iteye.com/problems/49111它是二级缓存的读取策略。针对不同的实体需求,配置不同的访问策略。请注意注解的全路径。

DAO代码就不全给出了,比较简单,仅仅给出片段即可

	public static EntityManager getEntityManager() {
		EntityManager manager = threadLocal.get();		
		if (manager == null || !manager.isOpen()) {
			manager = emf.createEntityManager();
			threadLocal.set(manager);
		}
		return manager;
	}

	public Products findById(Long id) {
		try {
			Products instance = getEntityManager().find(Products.class, id);
			return instance;
		} catch (RuntimeException re) {
			throw re;
		}
	}

 4.  测试

public void test02() {

		Products products1 = productsDAO.findById(1l);
		System.out
				.println(products1.getId() + ":" + products1.getProductName());
		Products products2 = productsDAO.findById(1l);
		System.out
				.println(products2.getId() + ":" + products2.getProductName());

	}

 我们先将Hibernate的二级缓存在配置文件中关掉。就是设为false运行测试代码试试。

运行后控制台效果如下

log4j:WARN No appenders could be found for logger (org.hibernate.cfg.annotations.Version).
log4j:WARN Please initialize the log4j system properly.
2011-6-29 16:19:37 dao.EntityManagerHelper log
信息: finding Products instance with id: 1
Hibernate: select products0_.id as id1_0_, products0_.nowPrice as nowPrice1_0_, products0_.picture as picture1_0_, products0_.productMess as productM4_1_0_, products0_.productName as productN5_1_0_, products0_.productsMessageInfo as products6_1_0_, products0_.productsMessageInfo_ID as products9_1_0_, products0_.sourcePrice as sourcePr7_1_0_, products0_.updateTime as updateTime1_0_ from tgweb.products products0_ where products0_.id=?
2011-6-29 16:19:37 dao.EntityManagerHelper log
信息: finding Products instance with id: 1
1:商品
1:商品

 从输出的sql语句可以看到执行后,先从数据库查询,之后去取记录从Hibernate的一级缓存中获得,就不在查库了。好的,我们再次运行测试代码,输出的效果和第一次运行一样,还是要先查一道数据库,控制台会输出sql语句。

下面我们开启memcached服务,将Hibernate的二级缓存配置打开。运行测试代码,第一次运行和没开启二级缓存前效果一样,但是第二次运行测试代码,效果如下

log4j:WARN No appenders could be found for logger (org.hibernate.cfg.annotations.Version).
log4j:WARN Please initialize the log4j system properly.
2011-6-29 16:31:08 dao.EntityManagerHelper log
信息: finding Products instance with id: 1
1:商品
2011-6-29 16:31:08 dao.EntityManagerHelper log
信息: finding Products instance with id: 1
1:商品

 此时并没有再次访问数据库,而是直接从memcached缓存中获取。之后数次运行测试代码,效果都是从缓存获取,那么我们关闭memcached服务。再运行测试代码,效果如下

log4j:WARN No appenders could be found for logger (org.hibernate.cfg.annotations.Version).
log4j:WARN Please initialize the log4j system properly.
2011-6-29 16:33:25 dao.EntityManagerHelper log
信息: finding Products instance with id: 1
Hibernate: select products0_.id as id1_0_, products0_.nowPrice as nowPrice1_0_, products0_.picture as picture1_0_, products0_.productMess as productM4_1_0_, products0_.productName as productN5_1_0_, products0_.productsMessageInfo as products6_1_0_, products0_.productsMessageInfo_ID as products9_1_0_, products0_.sourcePrice as sourcePr7_1_0_, products0_.updateTime as updateTime1_0_ from tgweb.products products0_ where products0_.id=?
1:商品
1:商品
2011-6-29 16:33:26 dao.EntityManagerHelper log
信息: finding Products instance with id: 1

 发现依然和没开启缓存的效果是一样的,从数据库去取。证明缓存生效。

而且我们还可以通过XMemcached客户端代码获取缓存对象,代码如下

package dao;

import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.TimeoutException;

import net.rubyeye.xmemcached.KeyIterator;
import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.MemcachedClientBuilder;
import net.rubyeye.xmemcached.XMemcachedClientBuilder;
import net.rubyeye.xmemcached.exception.MemcachedException;
import net.rubyeye.xmemcached.utils.AddrUtil;

import org.hibernate.cache.ReadWriteCache.Item;

public class MemcacheClientTest {
	/**
	 * @param args
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	public static void main(String[] args) throws IOException {
		MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil
				.getAddresses("localhost:11211"));
		MemcachedClient memcachedClient = null;
		try {
			memcachedClient = builder.build();
			// 取所有key
			KeyIterator it = memcachedClient.getKeyIterator(AddrUtil
					.getOneAddress("localhost:11211"));
			while (it.hasNext()) {
				String key = it.next();
				System.out.println("key------:" + key);
			}

			Item item = memcachedClient.get("pojo.Products:0:1");
			HashMap productsHashMap = (HashMap) item.getValue();
			System.out.println("products:" + productsHashMap.get("id") + ":"+ productsHashMap.get("productName"));
		} catch (MemcachedException e) {
			System.err.println("MemcachedClient operation fail");
			e.printStackTrace();
		} catch (TimeoutException e) {
			System.err.println("MemcachedClient operation timeout");
			e.printStackTrace();
		} catch (InterruptedException e) { 
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		try {
			memcachedClient.shutdown();
		} catch (IOException e) {
			System.err.println("Shutdown MemcachedClient fail");
			e.printStackTrace();
		}
	}
}

 5.  总结

当然,就像刚开始说的,Hibernate还有很多其他缓存组件可用,不过笔者认为memcache最大的优点就是达到了分布式的二级缓存。尤其是面向集群系统,并发量又比较大,多个服务器之间公用一个缓存,减轻各个Node的负载,当然放到二级缓存的数据并发量不能太大。读取操作倒是可以,如果遇到更新操作会有极小的数据有效性的延迟。就是数据更新了,缓存还没来得及更新呢,就被另一个线程取走了。这个时候就是无效的,当然客户端XMemcachedCAS进行乐观锁锁定,显示报出异常等等措施,但是对于系统的运行效率上就得牺牲一些。总得来说现在使用memcache作为ORM二级缓存渐渐成为了趋势。

 

PS:参考资料

http://developer.51cto.com/art/200909/153715.htm

  • 大小: 44.7 KB
8
4
分享到:
评论
2 楼 zarkk 2011-10-24  
非常好的一个技术,对我非常有用
1 楼 uuid198909 2011-07-12  

顶起
···

相关推荐

Global site tag (gtag.js) - Google Analytics