Ehcache的简单学习3-实际项目使用

本文深入探讨Ehcache的特点及应用场景,对比Guava Cache、Redis及Memcached,解析Ehcache在高并发下的表现,并提供实用代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说明:所有的基础配置和Ehcache的简单学习2【https://my.oschina.net/hanchao/blog/1596654】是一样的,只是多了一些具体代码,下面贴出来。

一、实际使用

1.简单回顾了解

Ehcache是一个用Java实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的cache管理方案。同时ehcache作为开放源代码项目,采用限制比较宽松的Apache License V2.0作为授权方式,被广泛地用于Hibernate, Spring,Cocoon等其他开源系统。Ehcache 从 Hibernate 发展而来,逐渐涵盖了 Cahce 界的全部功能,是目前发展势头最好的一个项目。具有快速,简单,低消耗,依赖性小,扩展性强,支持对象或序列化缓存,支持缓存或元素的失效,提供 LRU、LFU 和 FIFO 缓存策略,支持内存缓存和磁盘缓存,分布式缓存机制等等特点。

特性:

快速;
简单;
多种缓存策略;
缓存数据有两级:内存和磁盘,因此无需担心容量问题;
缓存数据会在虚拟机重启的过程中写入磁盘;
可以通过 RMI、可插入 API 等方式进行分布式缓存;
具有缓存和缓存管理器的侦听接口;
支持多缓存管理器实例,以及一个实例的多个缓存区域;
提供 Hibernate 的缓存实现;

2.实际工作使用

在实际工作中,我更多是将Ehcache作为与Redis配合的二级缓存。

1.第一种方式

09165942_JTgt.png

注意:这种方式通过应用服务器的Ehcache定时轮询Redis缓存服务器更同步更新本地缓存,缺点是因为每台服务器定时Ehcache的时间不一样,那么不同服务器刷新最新缓存的时间也不一样,会产生数据不一致问题,对一致性要求不高可以使用。【我们现在的例子就是这种方式】

2.第二种方式

09165942_a7Ea.png

注意:通过引入了MQ队列,使每台应用服务器的Ehcache同步侦听MQ消息,这样在一定程度上可以达到准同步更新数据,通过MQ推送或者拉取的方式,但是因为不同服务器之间的网络速度的原因,所以也不能完全达到强一致性。基于此原理使用Zookeeper等分布式协调通知组件也是如此

3.总结
《1.》使用二级缓存的好处是减少缓存数据的网络传输开销,当集中式缓存出现故障的时候,Ehcache等本地缓存依然能够支撑应用程序正常使用,增加了程序的健壮性。另外使用二级缓存策略可以在一定程度上阻止缓存穿透问题。

《2.》根据CAP原理我们可以知道,如果要使用强一致性缓存(根据自身业务决定),集中式缓存是最佳选择,如(Redis,Memcached等)。

二、spring管理bean

183127_iCjY_220449.png

三、本地缓存代码

package com.book.web.controller;

import net.sf.ehcache.Cache;

/**
 * 本地缓存
 * @author liweihan
 *
 */
public interface LocalEhCache {
	
	/**
	 * 获取Cache对象
	 * @return
	 */
    public Cache getEhCache();
    /**
     * 根据key获取缓存对象
     * @param key	缓存的key
     * @param tClass	类似的class
     * @return
     */
    public <T> T get(Object key, Class<T> tClass);

    /**
     * 缓存对象
     * @param key
     * @param value
     */
    public void put(Object key, Object value);
    
    /**
     * 可以控制时间的缓存对象
     * @param key
     * @param obj
     * @param idleTime	无访问缓存时间(单位:秒)
     * @param liveTime	生存时间(单位:秒)
     * 		
     */
    public void put(Object key, Object obj, Integer idleTime, Integer liveTime);
    
    /**
     * 根据key清除缓存对象
     * @param key
     */
    public boolean remove(Object key);
    
    /**
     * 判断缓存中是否包括key的缓存
     * @param key
     * @return
     */
    public boolean containsKey(Object key);
    
    /**
     * 缓存的数量
     * @return
     */
    public int size();
    
    /**
     * 清除过期的缓存
     * 注意的是触发ehcache去检查这个元素是否过期expiry,
     * 是由用户访问了元素,即调用cache.get(key)按需触发,
     * 这时ehcache才会去检查这个元素是否过期,
     * 如果过期就把该元素清除,并返回null。
     * 
     * 所以如果存在这样的场景:
     * 有些元素我们一直都不去访问,
     * 且内存中的元素数量又没超出maxElementsInMemory的值,
     * 那么这些过期元素将一直驻留在内存中。
     * 
     * 为了解决这个问题,我们应该创建一个后台线程,
     * 这个线程可以过一段时间去触发一下cache.evictExpiredElements(),
     * 这样即可把内存中驻留的过期元素清除。
     */
    public void evictExpiredElements();
    
    /**
     * 清除所有ehcache缓存
     */
    public void removeAll();
    
}
package com.book.web.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.ehcache.EhCacheCacheManager;

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

public class LocalEhCacheImpl implements LocalEhCache{
	
	private Cache cache;
	private static Logger logger = LoggerFactory.getLogger(LocalEhCacheImpl.class);
	
	public LocalEhCacheImpl(EhCacheCacheManager ehCacheCacheManager,String name) {
    	logger.info(" ====== init ehcache!!");
		cache = ehCacheCacheManager.getCacheManager().getCache(name);
	}

	@Override
	public Cache getEhCache() {
		return this.cache;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T get(Object key, Class<T> tClass) {
		try {
			if (this.containsKey(key)) {
				return (T)cache.get(key).getObjectValue();
			}
		} catch (IllegalStateException e) {
			logger.error(" ====== Get from Ehcache Error ,key:{}",key,e);
		} 
		return null;
	}

	@Override
	public void put(Object key, Object value) {
		try {
			Element element = new Element(key, value);
			cache.put(element);
		} catch (Exception e) {
			logger.error(" ====== put into Ehcache Error ,key:{},value:{}",key,value,e);
		}
	}

	@Override
	public void put(Object key, Object value, Integer idleTime, Integer liveTime) {
		try {
			Element element = new Element(key, value, false, idleTime, liveTime);
			cache.put(element);
		} catch (Exception e) {
			logger.error(" ====== put into Ehcache Error ,key:{},value:{},idle:{},liveTime:{}",
					key,value,idleTime,liveTime,e);
		}
	}

	@Override
	public boolean remove(Object key) {
		return cache.remove(key);
	}

	@Override
	public boolean containsKey(Object key) {
		boolean f = false;
		try {
			Element element = cache.getQuiet(key);//静态的获取Element,不会产生update
			if (cache.isKeyInCache(key) && element != null && !cache.isExpired(element)) {
				f = true;
			} else {
				f = false;
			}
		} catch (Exception e) {
			logger.error(" ====== ehcache check Key ERROR ! key:{}",key,e);
			f = false;
		} 
		return f;
	}

	@Override
	public int size() {
		return cache.getSize();
	}

	@Override
	public void evictExpiredElements() {
		cache.evictExpiredElements();
	}

	@Override
	public void removeAll() {
		cache.removeAll();
	}

}

四、web调用时

package com.book.web.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.alibaba.fastjson.JSONObject;
import com.book.core.model.User;
import com.book.core.serializable.SerializationUtil;

/**
 * Ehcache实际使用
 * @author liweihan
 *
 */
@Controller
@RequestMapping("/c")
public class TestEhcache2Controller {
	
	private static Logger logger = LoggerFactory.getLogger(TestEhcache2Controller.class);
	@Autowired
	private LocalEhCache localEhcache;
	
	private static final String TEST_SUFF = "T_%s";
	
	@ResponseBody
	@RequestMapping("/get/{id}")
	public String get1(@PathVariable(value = "id") int id) {
		logger.info(" ----- 用户Id:{},{}",
				id,System.getProperty("java.io.tmpdir"));
		
		//字符串
		String key =  String.format(TEST_SUFF,1);
		localEhcache.put(key, "Test");
		logger.info(" ----- 获取缓存数据:key:{},value:{}",
				key,localEhcache.get(key, String.class));
		
		//整型
		String key2 = String.format(TEST_SUFF, 2);
		localEhcache.put(key2, 2);
		logger.info(" ----- 获取缓存数据:key:{},value:{}",
				key2,localEhcache.get(key2, Integer.class));
		
		//对象
		User user = new User();
		user.setId(1);
		user.setName("韩超");
		user.setEmail("hanchaohan@126.com");
		String key3 = String.format(TEST_SUFF, 3);
		long begin3 = System.currentTimeMillis();
		localEhcache.put(key3, user);
		logger.info(" ----- 获取缓存数据:key:{},value:{},time:{}",
				key3,localEhcache.get(key3, User.class).getName()
				,(System.currentTimeMillis()-begin3));
		
		//JSON对象
		JSONObject jsonObject = new JSONObject();
		jsonObject.put("user", "hanchao");
		jsonObject.put("blog", "https://my.oschina.net/hanchao/blog");
		String key4 = String.format(TEST_SUFF, 4);
		localEhcache.put(key4,jsonObject);
		logger.info(" ----- 获取缓存数据:key:{},value:{}",
				key4,localEhcache.get(key4,JSONObject.class).get("blog"));
		
		//序列化对象-注意:不是所有的数据都需要序列化的!比如JSON对象,字符串,等没有必要序列化
		//关于序列化参考一下:http://blog.51cto.com/hanchaohan/1962798
		//因为Ehcache一般作为第一层的本地化缓存,究竟要不要序列化,具体项目具体考虑效率,内存等!
		String key5 = String.format(TEST_SUFF, 5);
		try {
			long begin5 = System.currentTimeMillis();
			localEhcache.put(key5, SerializationUtil.object2Bytes(user));
			byte[] obj = localEhcache.get(key5, byte[].class);
			User result = (User)SerializationUtil.bytes2Object(obj);
			logger.info(" ----- 获取缓存数据:key:{},value:{},time:{}",
					key5,result.getName(),(System.currentTimeMillis() - begin5));
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "OK";
	}
	
	/**
	 * 根据获取
	 * @param key
	 * @return
	 */
	@ResponseBody
	@RequestMapping("/get2/{key}")
	public String get2(@PathVariable(value = "key") String key) {
		String key1 =  String.format(TEST_SUFF,1);
		logger.info(" ----- 获取缓存数据:key:{},value:{}",
				key,localEhcache.get(key1, String.class));
		
		String key4 = String.format(TEST_SUFF, 4);
		logger.info(" ----- 获取缓存数据:key:{},value:{}",
				key4,localEhcache.get(key4,JSONObject.class));
		
		return "OK";
	}
	
	/**
	 * 删除某个元素
	 * @param key
	 * @return
	 */
	@ResponseBody
	@RequestMapping("/r/{key}")
	public String remove(@PathVariable(value = "key") String key) {
		boolean result = localEhcache.remove(key);
		localEhcache.evictExpiredElements();
		logger.info(" ====== result : {} ,key :{}",result,key);
		return "OK";
	}

	
	@ResponseBody
	@RequestMapping("/i")
	public String info() {
		logger.info(" ====== size:{}",localEhcache.size());
		
		return "OK";
	}
	
	/**
	 * 删除所有元素
	 * @param key
	 * @return
	 */
	@ResponseBody
	@RequestMapping("/removeall")
	public String removeAll() {
		localEhcache.removeAll();
		return "OK";
	}
}

五、Ehcache与Guava的比较

《0.》两者都是很成熟的JVM级别缓存,所以在绝大多数情况都是可以满足要求的。

《1.》Ehcache支持持久化到本地磁盘,Guava不可以;

《2.》Ehcache有现成的集群解决方案,Guava没有。不过个人感觉比较鸡肋,对JVM级别的缓存来讲太重了。

《3.》Ehcache jar包庞大,Guava Cache只是Guava jar包中的工具之一,而且后者远远小于Ehcache;

《4.》两种缓存当缓存过期或者没有命中的时候都可以通过load接口重载数据,调用方式略有不同。两者的主要区别是Ehcache的缓存load的时候,允许用户返回null,而Guava Cache则不允许返回为null,因为Guava Cache是根据value的值是否为null来判断是否需要load,所以不允许返回为null,但是使用的时候可以使用空对象替换。不允许返回null是一个很好的考虑;

《5.》Ehcache有内存占用大小统计,Guava Cache没有,需要自己开发。(但是,打开Ehcache的统计也是需要耗费性能的!)

《6.》

适用Ehcache的情况:

《1.》需要持久化持久化。使用持久化功能需要,缓存稳定,以免持久化的数据不准确影响结果。

《2.》有集群解决方案。

适用Guava cache的情况:

Guava cache说简单点就是一个支持LRU的ConCurrentHashMap,它没有Ehcache那么多的各种特性,只是提供了增、删、改、查、刷新规则和时效规则设定等最基本的元素。做一个jar包中的一个功能之一,Guava cache极度简洁并能满足觉大部分人的要求。

总结:

Ehcache有着全面的缓存特性,但是略重。Guava cache有最基本的缓存特性,很轻。大家根据具体情况选择使用。

六、Ehcache和Redis、Memcache的比较

Ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。

redis是通过socket访问到缓存服务,效率比ecache低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。

如果是单个应用或者对缓存访问要求很高的应用,用ehcache。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用redis。

另:ehcache也有缓存共享方案,不过是通过RMI或者Jgroup多播方式进行广播缓存通知更新,缓存共享复杂,维护不方便;简单的共享可以,但是涉及到缓存恢复,大数据缓存,则不合适。

参考文档:

http://blog.youkuaiyun.com/jationxiaozi/article/details/8509732

七、Ehcache对并发的支持

在高并发的情况下,使用Ehcache缓存时,由于并发的读与写,我们读的数据有可能是错误的,我们写的数据也有可能意外的被覆盖。所幸的是Ehcache为我们提供了针对于缓存元素Key的Read(读)、Write(写)锁。

当一个线程获取了某一Key的Read锁之后,其它线程获取针对于同一个Key的Read锁不会受到限制,但其它线程(包括获取了该Key的Read锁的线程)如果想获取针对同一个Key的Write锁就不行,它需要等到针对于该Key的Read锁释放后才能获取其Write锁;

当一个线程获取了某一Key的Write锁之后,其它线程获取同一个Key的Read锁或者Write锁的请求将等待针对于该Key的Write锁释放后才能继续进行,但是同一个线程获取该Key对应的Read锁或者Write锁将不需要等待。获取了对应的锁之后,记得在不再需要该锁后释放该锁。并且需要注意不要引起死锁。

 

Ehcache对并发的支持,可以参考这篇文章,实际中,我使用也许是读居多!

http://blog.youkuaiyun.com/elim168/article/details/71723348

JSR107简单了解:http://tangmingjie2009.iteye.com/blog/2042485

转载于:https://my.oschina.net/hanchao/blog/1602404

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值