适用于高并发的本地缓存方案

使用本地缓存需要注意两个问题:

  1. 内存管理,及时解除无用对象的引用。防止大量无用对象进入old区,引发 full gc。
  2. 数据同步,如果应用是一个集群,需要保持各台机器的数据一致性。

问题1的解决可以采用LRU算法,预先定好缓存大小。达到最大值后,清除最近最少使用的对象。

问题2比较复杂,需要有一个集中的地方控制缓存一致,比如可以采用消息中间件,写时进行异步复制。这种方式成本较大。

其实对于业务系统,用户通过浏览器访问的数据,不需要很强的一致性。只要在3秒内,各台机器能够保持最终一致,用户是感觉不到不同机器数据的不同步。

因此通过控制缓存时间为3秒或其它很短的时间,就可以保证一定程度的数据一致,避免数据同步的开销和复杂度。

缓存时间短又会引发另外一个问题,就是缓存的命中率。在高并发访问下,缓存时间短可能会导致,大量的访问刚好都没有命中,缓存穿透给系统带来瞬间的高峰压力。

综合考虑以上几个矛盾点,可以对缓存数据进行封装,使用有效失效次数的缓存对象,保证在高并发情况下,大量的访问都能命中缓存,同时又能保证缓存及时失效和更新。代码如下

首先是LRUHashMap。需要注意的是LRUHashMap不是线程安全的,非线程安全访问是否会带来问题,取决于业务场景,对共享变量进行相互覆盖的影响(参考http://www.iteye.com/topic/656670)。

下面是一个控制缓存总体大小的自动适应的本地缓存代码:

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUHashMap extends LinkedHashMap<String, Object> {

    private int MAX_ENTRIES;

    public LRUHashMap(int size) {
        MAX_ENTRIES = size;
    }

    protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
        return size() > MAX_ENTRIES;
    }

}
import java.io.Serializable;
import java.util.concurrent.atomic.AtomicInteger;

public class CacheObject implements Serializable {

    long createTime;
    long lifeTime;
    Object value;
    AtomicInteger invalidTimes;

    public CacheObject(long lifeTime, Object value) {
        this.lifeTime = lifeTime;
        this.value = value;
        invalidTimes = new AtomicInteger(0);
        this.createTime = System.currentTimeMillis();
    }

    public long getCreateTime() {
        return createTime;
    }

    public void setCreateTime(long createTime) {
        this.createTime = createTime;
    }

    public long getLifeTime() {
        return lifeTime;
    }

    public void setLifeTime(long lifeTime) {
        this.lifeTime = lifeTime;
    }

    public Object getValue() {
        // 如果已经失效很久,说明很久没有被访问,那么直接返回null,不对失效次数进行判断。
        if ((System.currentTimeMillis() - createTime) > 10 * lifeTime) {
            return null;
        }
        // 保证在高并发下,缓存失效也可以保证较高的命中率
        if (System.currentTimeMillis() - createTime > lifeTime) {
            if (invalidTimes.incrementAndGet() > 3) {
                return value;
            } else {
                return null;
            }
        }
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

    public AtomicInteger getInvalidTimes() {
        return invalidTimes;
    }

    public void setInvalidTimes(AtomicInteger invalidTimes) {
        this.invalidTimes = invalidTimes;
    }

}
public class LocalCache {

    LRUHashMap cacheArea = new LRUHashMap(20);

    public Object get(String key) {
        CacheObject cacheObject = (CacheObject) cacheArea.get(key);
        return cacheObject == null ? null : cacheObject.getValue();
    }

    public void put(String key, Object value, long lifeTime) {
        CacheObject cacheObject = new CacheObject(lifeTime, value);
        cacheArea.put(key, cacheObject);
    }
}

LocalCache与集中式Cache

localcache与memcache性能比较

先来个本地缓存与memcache缓存的性能比较, 有个直观上的概念

Cache 请求方式 次数 时间 平均
Localcache hashmap中get请求 1亿 1344ms 0.00001344ms
Memcache 简单的get请求,不做序列化 1万 4437ms 0.4437ms
Db 单表查询(有索引) 1-2ms

以上测试在开发机器.生产环境采集的数据显示memcache的一次请求大约在0.2ms左右,如果存储的是java object,那算上发序列化的时间在0.5ms以上.与测试数据在同一个数量级上.

通过以上数据对比,可以得知localcache的效率比memcache高1万倍以上.这个数字让我对使用本地缓存充满了极大的兴趣.

使用localcache会带来哪些问题

localcache有着极大的性能优势,单机情况下,适当使用localcache会使程序的效率得到很大的提升.但在集群环境下localcache就存在很多问题了,主要体现在多个jvm之间cache的同步问题.

有很多框架在这上面做了很多工作,比如ehcache ,主要是通过cache复制(copy或invalidate)来解决,大概的思路是使用消息多播机制,当一个jvm中的数据做了更新操作后,首先更新本jvm内的localcache,然后广播消息,其他jvm接收到消息后更新自己的localcache. 但这种机制可能带来并发操作时出现脏数据的问题,具体见Potential Issues with Replicated Caching.

其他cache产品也遇到类似的问题,不再一一介绍.

那有没有很好的方法来解决localcache的同步问题,从而可以放心的品尝localcache这块"甜饼"呢?

这个问题我也很纠结,通过多种方案的组合及补偿机制似乎可以实现一个完美的方案.但也注定成为了一个复杂的方案.类似的方案可以有如下几种:

  1. localcache作为一级缓存,通过广播的方式同步缓存,同时设置缓存过期时间,以达到数据同步和出现脏数据后自动修复的功能.

  2. localcache作为一级缓存,数据更新后发送异步消息(MQ等),其余localcache订阅异步消息,并根据消息来同步缓存.

  3. localcache作为一级缓存,memcache中存放缓存变更的信息,定时任务定时获取memcache的信息,并决定是否更新localcache.

  4. localcache作为一级缓存,每次从memcache中获取数据更改的标记位,如果标记发生变化,更新localcache

以上的这些实现方案,都在一定程度上加大了架构的复杂性,当localcache中数据出现脏数据时,排查问题及清理数据都会变得复杂.

他人经验之谈

  1. sohu早期使用广播的方式(jgroup)同步localcache,结果经常会出现脏数据的问题,在后来的架构设计上干脆摒弃了localcache(即使使用,也不再作数据同步),全部使用memcache.

  2. taobao在生产环境也很少使用localcache同步,对于非敏感性数据,只是通过简单的过期策略,来保证数据的一致性.

总结

集群环境下对于敏感性要求不高的数据可以使用localcache,只配置简单的失效机制来保证数据的一致性.

对敏感性高的数据直接使用集中式缓存, 减低复杂度.

复杂方案看似完美的解决了问题, 实际上性能和稳定性却很可能大打折扣.

原文链接:https://blog.csdn.net/hill007299/java/article/details/84188351

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。