场景应用:自己设计一个本地缓存(代码实现)

Posted 流楚丶格念

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了场景应用:自己设计一个本地缓存(代码实现)相关的知识,希望对你有一定的参考价值。

文章目录

想要设计一个本地缓存,考虑点主要在数据用何种方式存储,能存储多少数据,多余的数据如何处理等几个点,下面我们来详细的介绍每个考虑点:

设计思想

1. 数据结构

首要考虑的就是数据该如何存储,用什么数据结构存储。最简单的就直接用Map来存储数据,或者复杂的如redis一样提供了多种数据类型哈希,列表,集合,有序集合等,底层使用了双端链表,压缩列表,集合,跳跃表等数据结构。

2. 对象上限

因为是本地缓存,内存有上限,所以一般都会指定缓存对象的数量比如1024,当达到某个上限后需要有某种策略去删除多余的数据。

3. 清除策略

上面说到当达到对象上限之后需要有清除策略,常见的比如有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用)等策略。

4. 过期时间

除了使用清除策略,一般本地缓存也会有一个过期时间设置,比如redis可以给每个key设置一个过期时间,这样当达到过期时间之后直接删除,采用清除策略+过期时间双重保证。

5. 线程安全

像redis是直接使用单线程处理,所以就不存在线程安全问题。而我们现在提供的本地缓存往往是可以多个线程同时访问的,所以线程安全是不容忽视的问题,并且线程安全问题是不应该抛给使用者去保证。

6. 简明的接口

提供一个傻瓜式的对外接口是很有必要的,对使用者来说使用此缓存不是一种负担而是一种享受,提供常用的get,put,remove,clear,getSize等方法即可。

7. 是否持久化

这个其实不是必须的,是否需要将缓存数据持久化看需求。本地缓存如ehcache是支持持久化的,而guava是没有持久化功能的。分布式缓存如redis是有持久化功能的,memcached是没有持久化功能的。

8. 阻塞机制

我们使用缓存的目的就是因为被缓存的数据生成比较费时,比如调用对外的接口,查询数据库,计算量很大的结果等等。这时候如果多个线程同时调用get方法获取的结果都为null,每个线程都去执行一遍费时的计算,其实也是对资源的浪费。最好的办法是只有一个线程去执行,其他线程等待,计算一次就够了。但是此功能基本上都交给使用者来处理,很少有本地缓存有这种功能。

实现方案

以上大致介绍了实现一个本地缓存我们都有哪些需要考虑的地方,当然可能还有其他没有考虑到的点;下面继续看看关于每个点都应该如何去实现,重点介绍一下思路;

1.数据结构

本地缓存最常见的是直接使用Map来存储,比如guava使用ConcurrentHashMap,ehcache也是用了ConcurrentHashMap,Mybatis二级缓存使用HashMap来存储:

Map<Object, Object> cache = new ConcurrentHashMap<Object, Object>()

Mybatis使用HashMap本身是非线程安全的,所以可以看到起内部使用了一SynchronizedCache用来包装,保证线程的安全性;当然除了使用Map来存储,可能还使用其他数据结构来存储,比如redis使用了双端链表,压缩列表,整数集合,跳跃表和字典;当然这主要是因为redis对外提供的接口很丰富除了哈希还有列表,集合,有序集合等功能;

下面我们的案例是使用的LinkedHashMap

private LRULinkedHashMap<Object, CacheElement> cacheMap;

2.对象上限

本地缓存常见的一个属性,一般缓存都会有一个默认值比如1024,在用户没有指定的情况下默认指定;当缓存的数据达到指定最大值时,需要有相关策略从缓存中清除多余的数据这就涉及到下面要介绍的清除策略;

// 最大对象上限
protected final int DEFAULT_MAX_CAPACITY = 100000;

3.清除策略

配合对象上限之后使用,场景的清除策略如:
LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用);

清除策略说明
LRULeast Recently Used的缩写最近最少使用,移除最长时间不被使用的对象;常见的使用LinkedHashMap来实现,也是很多本地缓存默认使用的策略;
FIFO先进先出,按对象进入缓存的顺序来移除它们;常见使用队列Queue来实现;
LFULeast Frequently Used的缩写大概也是最近最少使用的意思,和LRU有点像;区别点在LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的;可以通过HashMap并且记录访问次数来实现;
SOFT软引用基于垃圾回收器状态和软引用规则移除对象;常见使用SoftReference来实现;
WEAK弱引用更积极地基于垃圾收集器状态和弱引用规则移除对象;常见使用WeakReference来实现;

4.过期时间

设置过期时间,让缓存数据在指定时间过后自动删除;常见的过期数据删除策略有两种方式:被动删除和主动删除;被动删除:每次进行get/put操作的时候都会检查一下当前key是否已经过期,如果过期则删除,类似如下代码:

if (System.currentTimeMillis() - lastClear > clearInterval) 
      clear();

主动删除:专门有一个job在后台定期去检查数据是否过期,如果过期则删除,这其实可以有效的处理冷数据;

5.线程安全

尽量用线程安全的类去存储数据,比如使用ConcurrentHashMap代替HashMap;或者提供相应的同步处理类,比如Mybatis提供了SynchronizedCache:

public synchronized void putObject(Object key, Object object) 
  ...省略...


@Override
public synchronized Object getObject(Object key) 
  ...省略...

6.简明的接口

提供常用的get,put,remove,clear,getSize方法即可,比如Mybatis的Cache接口:

public interface Cache 
  String getId();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  Object removeObject(Object key);
  void clear();
  int getSize();
  ReadWriteLock getReadWriteLock();

再来看看guava提供的Cache接口,相对来说也是比较简洁的:

public interface Cache<K, V> 
  V getIfPresent(@CompatibleWith("K") Object key);
  V get(K key, Callable<? extends V> loader) throws ExecutionException;
  ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
  void put(K key, V value);
  void putAll(Map<? extends K, ? extends V> m);
  void invalidate(@CompatibleWith("K") Object key);
  void invalidateAll(Iterable<?> keys);
  void invalidateAll();
  long size();
  CacheStats stats();
  ConcurrentMap<K, V> asMap();
  void cleanUp();

7.是否持久化

持久化的好处是重启之后可以再次加载文件中的数据,这样就起到类似热加载的功效;比如ehcache提供了是否持久化磁盘缓存的功能,将缓存数据存放在一个.data文件中;

diskPersistent="false" //是否持久化磁盘缓存

redis更是将持久化功能发挥到极致,慢慢的有点像数据库了;提供了AOF和RDB两种持久化方式;当然很多情况下可以配合使用两种方式;

8.阻塞机制

除了在Mybatis中看到了BlockingCache来实现此功能,之前在看<<java并发编程实战>>的时候其中有实现一个很完美的缓存,大致代码如下:

public class Memoizerl<A, V> implements Computable<A, V> 
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;
 
    public Memoizerl(Computable<A, V> c) 
        this.c = c;
    
 
    @Override
    public V compute(A arg) throws InterruptedException, ExecutionException 
        while (true) 
            Future<V> f = cache.get(arg);
            if (f == null) 
                Callable<V> eval = new Callable<V>() 
                    @Override
                    public V call() throws Exception 
                        return c.compute(arg);
                    
                ;
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) 
                    f = ft;
                    ft.run();
                
                try 
                    return f.get();
                 catch (CancellationException e) 
                    cache.remove(arg, f);
                
            
        
    

compute是一个计算很费时的方法,所以这里把计算的结果缓存起来,但是有个问题就是如果两个线程同时进入此方法中怎么保证只计算一次,这里最核心的地方在于使用了ConcurrentHashMap的putIfAbsent方法,同时只会写入一个FutureTask;

代码实战

缓存元素CacheElement实现

package com.yyl.mycache;

/**
 * 缓存元素
 *
 */
public class CacheElement 
    // 元素的键
    private Object key;
    // 元素值
    private Object value;
    // 创建时间
    private long createTime;
    // 存活时间
    private long lifeTime;
    // 调用次数
    private int hitCount;

    public CacheElement() 
    

    public CacheElement(Object key ,Object value) 
        this.key = key;
        this.value = value;
        this.createTime = System.currentTimeMillis();
    

    /**
     * 获取元素键
     * @return
     */
    public Object getKey() 
        return key;
    

    /**
     * 设置元素值
     * @param key 值
     */
    public void setKey(Object key) 
        this.key = key;
    

    public Object getValue() 
        hitCount++;
        return value;
    

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

    public long getCreateTime() 
        return createTime;
    

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

    public int getHitCount() 
        return hitCount;
    

    public void setHitCount(int hitCount) 
        this.hitCount = hitCount;
    

    public long getLifeTime() 
        return lifeTime;
    

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

    public boolean isExpired() 
        boolean isExpired = System.currentTimeMillis() - getCreateTime() > getLifeTime();
        return isExpired;
    

    /*
     * (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    public String toString() 
        StringBuffer sb = new StringBuffer();
        sb.append("[ key=").append(key).append(", isExpired=").append(isExpired())
                .append(", lifeTime=").append(lifeTime).append(", createTime=").append(createTime)
                .append(", hitCount=").append(hitCount)
                .append(", value=").append(value).append(" ]");
        return sb.toString();
    

    /*
     * (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    public final int hashCode()
        if(null == key)
            return "".hashCode();
        
        return this.key.hashCode();
    

    /*
     * (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    public final boolean equals(Object object) 
        if ((object == null) || (!(object instanceof CacheElement))) 
            return false;
        

        CacheElement element = (CacheElement) object;
        if ((this.key == null) || (element.getKey() == null)) 
            return false;
        

        return this.key.equals(element.getKey());
    

清除策略LRULinkedHashMap实现

package com.yyl.mycache;

import java.util.LinkedHashMap;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 实现 LRU策略的 LinkedHashMap
 *
 * @param <K>
 * @param <V>
 */
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V>

    protected static final long serialVersionUID = 2828675280716975892L;

    protected static final int DEFAULT_MAX_ENTRIES = 100;

    protected final int initialCapacity;
    protected final int maxCapacity;
    protected boolean enableRemoveEldestEntry = true;//是否允许自动移除比较旧的元素(添加元素时)

    protected static final float DEFAULT_LOAD_FACTOR = 0.8f;
    protected final Lock lock = new ReentrantLock();

    public LRULinkedHashMap(int initialCapacity)
    
        this(initialCapacity, DEFAULT_MAX_ENTRIES);
    

    public LRULinkedHashMap(int initialCapacity ,int maxCapacity)
    
        //set accessOrder=true, LRU
        super(initialCapacity, DEFAULT_LOAD_FACTOR, true);

        this.initialCapacity = initialCapacity;
        this.maxCapacity = maxCapacity;
    

    /*
     *  (non-Javadoc)
     * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
     */
    protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest)
    
        return enableRemoveEldestEntry && ( size() > maxCapacity );
    

    /*
     *  (non-Javadoc)
     * @see java.util.LinkedHashMap#get(java.lang.Object)
     */
    public V get(Object key)
    
        try 
            lock.lock();
            return super.get(key);
        
        finally 
            lock.unlock();
        
    

    /*
     *  (non-Javadoc)
     * @see java.util.HashMap#put(java.lang.Object, java.lang.Object)
     */
    public V put(K key, V value)
    
        try 
            lock.lock();
            return super.put(key, value);
        
        finally 
            lock.unlock();
        
    

    /*
     * (non-Javadoc)
     * @see java.util.HashMap#remove(java.lang.Object)
     */
    public V remove(Object key) 
        try 
            lock.lock();
            return super.remove(key);
        
        finally 
            lock.unlock();
        
    

    /*
     * (non-Javadoc)
     * @see java.util.LinkedHashMap#clear()
     */
    public void clear() 
        try 
            lock.lock();
            super.clear();
        
        finally 
            lock.unlock();
        
    

    /*
     * (non-Javadoc)
     * @see java.util.HashMap#keySet()
     */
    public Set<K> keySet() 
        try 
            lock.lock();
            return super.keySet();
        
        finally 
            lock.unlock();
        
    

    public boolean isEnableRemoveEldestEntry() 
        return enableRemoveEldestEntry;
    
    public void setEnableRemoveEldestEntry(boolean enableRemoveEldestEntry) 
        this.enableRemoveEldestEntry = enableRemoveEldestEntry;
    
    public int getInitialCapacity() 
        return initialCapacity;
    
    public int getMaxCapacity() 
        return maxCapacity;
    


缓存接口MyCache实现

package com.yyl.mycache;

/**
 * 缓存接口
 *
 */
public interface MyCache 

    /**
     * 获取缓存
     * @param key
     * @return
     */
    public <T> T getCache如何设计一个本地缓存,涨姿势了!

本地缓存的使用

动手实现一个 localcache - 设计篇

Java实现本地缓存分布式缓存及多级缓存

设计一个移动应用的本地缓存机制

设计一个移动应用的本地缓存机制