有限失效的本地缓存策略

参考

http://www.infoq.com/cn/articles/memcached-java

http://www.cnblogs.com/redcreen/archive/2011/02/15/1955248.html

 http://raychase.iteye.com/blog/1545906

http://rdc.taobao.com/team/jm/archives/689

谷歌也有本地缓存的实现

业务场景:一个页面,包含多个item列表,每个列表的抽取规则都不一样,每个item上面都有一个实时的字段(可能每秒都会变),并且要求在每天10点准时刷新(大量的用户会在10点不停F5直到看到新列表)。要求单机qps是1200.

 

完全无缓存的方案:不同的列表拼装不同的查询对象,通过搜索引擎查询。拿到结果列表,再根据列表中item的主键走一次远程调用实时读取每个item的库存。系统开销:假设有2个列表,走两次搜索引擎估计30ms,再从列表中拼接item主键批量去查询库存30ms,页面渲染20ms,整个过程得80ms左右。qps基本上没有可能达到要求。

 

使用分布式缓存:由于要在10点准时刷新,并且库存基本上每秒都会变化,所以缓存时间不能太长,必须在3秒左右。而用户访问特点是会不停刷新页面,直到缓存失效了,数据更新了才罢手。这样一来,在高峰访问瞬间,缓存的命中率是很低的。无法保证在高峰访问瞬间能有很高的qps。

扫描二维码关注公众号,回复: 647347 查看本文章

 

也就是说我们必须做到缓存可以很快的更新,3秒更新一次,并且能够保证瞬间高访问情况下有很高的缓存命中率。

 

于是我们采用了有限失效的缓存策略,为了减少远程调用的网络io开销,对象序列化的cpu开销,我们基于本地缓存来实现有限失效的缓存方案。

 

有限失效策略的核心是对缓存对象的包装,由CacheObject实现。 

 

 

/**
 * 有限失效次数的缓存对象
 */
public class CacheObject implements Serializable {

	private static final long serialVersionUID = -5101398256697770240L;
	// 以下单位均是ms
	// 创建时间
	long createTime;
	// 缓存有效时间
	long lifeTime;
	// 最后一次访问时间,用于LRU策略
	long lastVisitTime;
	// 真正的缓存值
	Object value;
	// 失效次数,cacheObject的核心概念
	AtomicInteger invalidTimes = new AtomicInteger();
	// 最大失效次数,这边设为一次。一般来说一次已经够了
	int maxInvalidTimes = 1;

	public CacheObject(long lifeT, Object v, int maxInvalid) {
		this.createTime = System.currentTimeMillis();
		this.lifeTime = lifeT;
		this.lastVisitTime = this.createTime;
		this.value = v;
		this.maxInvalidTimes = maxInvalid;
	}

	public CacheObject(long lifeT, Object v) {
		this.createTime = System.currentTimeMillis();
		this.lifeTime = lifeT;
		this.lastVisitTime = this.createTime;
		this.value = v;
	}

	/**
	 * 防止瞬间大量访问失效,对系统造成过载,对数据过期进行特殊处理,有限失效次数策略
	 * 
	 * @return
	 */
	public Object getValue() {
		// 判断是否过期
		if (isExpired()) {
			// 如果已经过期,并且已经有一个访问触发失效
			if (invalidTimes.incrementAndGet() > maxInvalidTimes) {
				// 如果已经有一个访问触发过失效,5s后对象没有更新(一般一个请求缓存没命中,会去读取数据源,并在很短的时间内更新该缓存,正常不会超过5s)
				// 那么可能触发失效的那个请求出现问题,为了防止缓存永不更新,再度触发失效
				if (isSoExpired()) {
					return null;
				}
				// 正常情况,第(maxInvalidTimes+1)次及以后的过期,不触发失效,直接返回值
				return value;
			} else {
				return null;
			}
		}
		// 如果不过期,直接返回值
		return value;
	}

	public boolean isExpired() {
		return (System.currentTimeMillis() - createTime) > lifeTime;
	}

	/**
	 * 判断失效时间是否超过5s
	 * 
	 * @return
	 */
	private boolean isSoExpired() {
		return (System.currentTimeMillis() - createTime - lifeTime) > 5000;
	}

	public long getLastVisitTime() {
		return lastVisitTime;
	}

	public void setLastVisitTime(long lastVisitTime) {
		this.lastVisitTime = lastVisitTime;
	}

	public long getCreateTime() {
		return createTime;
	}

	public long getLifeTime() {
		return lifeTime;
	}

}

 

 

缓存接口

 

/**
 * 缓存接口,按k-v存取,和一般缓存无异
 * 
 * @author Administrator
 * 
 */
public interface Cache {
	/**
	 * 按key读
	 * 
	 * @param key
	 * @return
	 */
	Object get(String key);

	/**
	 * 按key存
	 * 
	 * @param key
	 * @param value
	 * @param lifeTime
	 *            有效时间,单位ms
	 * @return
	 */
	boolean put(String key, Object value, long lifeTime);

	/**
	 * 返回缓存的元素个数
	 * 
	 * @return
	 */
	int size();

	/**
	 * 触发缓存的垃圾回收
	 * 
	 * @return
	 */
	boolean gc();
}

 

 

本地缓存的实现:

为了保证线程安全和高并发能力,我们用了ConcurrentHashMap和异步触发清理的机制。

异步触发清理,可以让本地缓存的并发性能接近ConcurrentHashMap本身的并发读写能力,同时由能够按照LRU有效的清理缓存数据。

 

 

public class LocalCache implements Cache {

	/**
	 * 缓存的存储实现,线程安全
	 */
	private Map<String, CacheObject> map = new ConcurrentHashMap<String, CacheObject>();

	/**
	 * 缓存的元素个数计数器,为了避免多次调用ConcurrentHashMap.size()产生的系统开销。
	 * ConcurrentHashMap.size()的近似值
	 */
	private AtomicInteger count = new AtomicInteger();

	/**
	 * 缓存元素的个数阀值。本地内存有限,需要做限制。(具体多少由业务场景定)
	 */
	private int threshold = 100;

	/**
	 * gc线程数量,用于控制并发的gc线程数
	 */
	private AtomicInteger gcThreadCount = new AtomicInteger();

	public LocalCache(int threshold) {
		this.threshold = threshold;
	}

	@Override
	public Object get(String key) {
		CacheObject cacheObject = map.get(key);
		if (cacheObject == null) {
			return null;
		}
		// 维护最后访问时间
		cacheObject.setLastVisitTime(System.currentTimeMillis());
		return cacheObject.getValue();
	}

	@Override
	public boolean put(String key, Object value, long lifeTime) {
		CacheObject cacheObject = new CacheObject(lifeTime, value);
		// 如果count大于2倍阀值,那么说明瞬间插入过多新值,gc未及时处理
		// 直接返回,不插入,防止内存爆掉
		if (count.get() > 2 * threshold) {
			gc();
			return false;
		}
		// 如果先前map不包含该k-v,那么表示新增,维护计数器
		if (map.put(key, cacheObject) == null) {
			count.incrementAndGet();
		}
		// 如果元素个数大于阀值,触发缓存GC
		if (count.get() > threshold) {
			gc();
		}
		return true;

	}

	@Override
	public int size() {
		// 也可以直接返回ConcurrentHashMap.size(),视调用量而定
		return count.get();
	}

	@Override
	public boolean gc() {
		// 防止阻塞,另起一个线程,并保证同时只会有一个gc线程
		if (gcThreadCount.incrementAndGet() == 1) {
			Thread gc = new GCThread();
			gc.start();
			return true;
		} else {
			return false;
		}

	}

	/**
	 * 按照LRU进行清理
	 * 
	 * @author Administrator
	 * 
	 */
	private class GCThread extends Thread {
		public void run() {
			System.out.println("GC start");
			long start = System.currentTimeMillis();
			List<Entry<String, CacheObject>> list = new ArrayList<Map.Entry<String, CacheObject>>(threshold * 2);
			Set<Entry<String, CacheObject>> entries = map.entrySet();
			// 为了方便排序处理,先存进列表
			for (Entry<String, CacheObject> entry : entries) {
				list.add(entry);
			}
	
			// 按照最近访问时间排序(这个也可以进一步抽象,让用户自定义清理策略)
			Collections.sort(list, new Comparator<Entry<String, CacheObject>>() {

				@Override
				public int compare(Entry<String, CacheObject> o1, Entry<String, CacheObject> o2) {
					if (o1.getValue().getLastVisitTime() > o2.getValue().getLastVisitTime()) {
						return 1;
					} else if (o1.getValue().getLastVisitTime() < o2.getValue().getLastVisitTime()) {
						return -1;
					} else {
						return 0;
					}
				}
			});
			System.out.println("---------------");
			System.out.println("map size before" + map.size());
			// 移除最近最少访问的元素,移除多少个可以视业务场景(这边为了防止频繁触发gc,所以移除较多元素)
			for (int i = 0; i < list.size() - threshold / 2; i++) {
				map.remove(list.get(i).getKey());
			}
			System.out.println("map size after" + map.size());
			// 对计数器进行修正
			count.set(map.size());
			// 线程基本退出,可以重置运行gc线程数
			gcThreadCount.set(0);
			System.out.println("gc cost" + (System.currentTimeMillis() - start));
		}
	}

}

 

当系统是一个集群的时候,使用本地缓存涉及到一个缓存同步的问题,鉴于缓存同步产生的系统开销,和缓存同步机制引入的系统复杂度带来的维护弊端。本方案不进行缓存同步,该机制适用于缓存时间很短的,或者对数据一致性不敏感的数据场景。并且是瞬间高并发的场景,这样才能充分发挥有限失效机制的优势。

 

结合上述本地缓存机制,再回到刚才的业务场景,问题就可以迎刃而解了。

我们对整个页面的渲染结果进行缓存,这样只要本地缓存命中了,那么页面的性能就和业务的处理无关了。

并且我们对只对页面缓存3s,如果同一个用户访问了同一台服务器,连续刷新两次页面基本上3秒已过,用户察觉不到有缓存。对于不同的用户访问到不同的服务器,最大不一致时间窗口也就是6秒,再加上页面也不一定是每秒都有大幅度的更新,也是察觉不到。

 

 

做了以上的优化后,基本上一个请求过来的处理过程就是到ConcurrentHashMap读一下,key为访问url,value可以是html字符串,也可以是html byte流(这个更佳)。

尽管缓存的是3s,但是在瞬间高并发访问下,qps1000的时候,缓存的命中率可以是99%以上,平均响应时间可以控制在10ms内(具体取决于页面大小)。

由于缓存的时间很短,所以对用户来说,10点整数据依然是准时更新的。

 

使用了这种缓存机制,既能保证页面数据基本上实时更新,又能保证高并发情况下,缓存的高命中率。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自hill007299.iteye.com/blog/1526811