读书人

有限失灵的本地缓存策略

发布时间: 2012-12-25 16:18:28 作者: rapoo

有限失效的本地缓存策略

参考

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

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

?

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

?

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

?

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

?

也就是说我们必须做到缓存可以很快的更新,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;}@Overridepublic Object get(String key) {CacheObject cacheObject = map.get(key);if (cacheObject == null) {return null;}// 维护最后访问时间cacheObject.setLastVisitTime(System.currentTimeMillis());return cacheObject.getValue();}@Overridepublic 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();}// 如果元素个数大于阀值,触发缓存GCif (count.get() > threshold) {gc();}return true;}@Overridepublic int size() {// 也可以直接返回ConcurrentHashMap.size(),视调用量而定return count.get();}@Overridepublic 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>>() {@Overridepublic 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点整数据依然是准时更新的。

?

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

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

读书人网 >互联网

热点推荐