项目开发一:实现固定大小的cache
cache的发展
当我们应用有一定流量之后或者查询数据库特别频繁,这个时候就可以祭出我们的java中自带的HashMap或者ConcurrentHashMap。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public clasas CustomerService{ private HashMap<String,String> hashMap = new HashMap<>(); private CustomerMapper customerMapper; public String getCustomer(String name){ String customer = hashMap.get(name); if(customer == null){ customer = customerMapper.get(name); hashMap.put(name,customer); } return customer; } }
|
- 实现的功能:通过HashMap实现了数据的添加和查找—作为缓存。
- 存在的问题:HashMap无法进行数据淘汰,内存会无限制的增长。
LRUHashMap
在HashMap的基础上,增加了数据淘汰,下面为几种常见的数据淘汰算法
1 2 3
| 先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰。这种可谓是最简单的了,但是会导致我们命中率很低。
试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。
|
1 2 3 4 5
| 最近最少使用算法。
在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。
但是这个依然有个问题,如果有个数据在1个小时的前59分钟访问了1万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。
|
1 2 3
| 最近最少频率使用。
在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了LRU不能处理时间段的问题。
|
上面列举了三种淘汰策略,对于这三种,实现成本是一个比一个高,同样的命中率也是一个比一个好。而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的LRU,如何实现一个LRUMap呢?
我们可以通过继承 LinkedHashMap,重写 removeEldestEntry 方法,即可完成一个简单的 LRUMap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| class LRUMap extends LinkedHashMap { private final int max; private Object lock; public LRUMap(int max, Object lock) { super((int) (max * 1.4f), 0.75f, true); this.max = max; this.lock = lock; }
@Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > max; } public Object getValue(Object key) { synchronized (lock) { return get(key); } } public void putValue(Object key, Object value) { synchronized (lock) { put(key, value); } } public boolean removeValue(Object key) { synchronized (lock) { return remove(key) != null; } } public boolean removeAll(){ clear(); return true; } } 。
|
在LinkedHashMap中维护了一个entry(用来放key和value的对象)链表。在每一次get或者put的时候都会把插入的新entry,或查询到的老entry放在我们链表末尾。
可以注意到我们在构造方法中,设置的大小特意设置到max*1.4,
在下面的removeEldestEntry方法中只需要size>max就淘汰,这样我们这个map永远也走不到扩容的逻辑了,
通过重写LinkedHashMap,几个简单的方法我们实现了我们的LruMap。
-
- 锁竞争严重,可以看见我的代码中,Lock是全局锁,在方法级别上面的,当调用量较大时,性能必然会比较低。
- 不支持过期时间
- 不支持自动刷新
Guava cache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public static void main(String[] args) throws ExecutionException { LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(100) .expireAfterWrite(30L, TimeUnit.MILLISECONDS) .expireAfterAccess(30L, TimeUnit.MILLISECONDS) .refreshAfterWrite(20L, TimeUnit.MILLISECONDS) .weakKeys() .build(createCacheLoader()); System.out.println(cache.get("hello")); cache.put("hello1", "我是hello1"); System.out.println(cache.get("hello1")); cache.put("hello1", "我是hello2"); System.out.println(cache.get("hello1")); }
public static com.google.common.cache.CacheLoader<String, String> createCacheLoader() { return new com.google.common.cache.CacheLoader<String, String>() { @Override public String load(String key) throws Exception { return key; } }; }
|
本项目实现
接口定义
为了兼容 Map,我们定义缓存接口继承自 Map 接口。
为了实现一个固定大小的cache,Cache中对Map接口方法进行重写
put时的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Override @CacheInterceptor(aof = true, evict = true) public V put(K key, V value) { CacheEvictContext<K,V> context = new CacheEvictContext<>(); context.key(key).size(sizeLimit).cache(this);
ICacheEntry<K,V> evictEntry = evict.evict(context);
if(ObjectUtil.isNotNull(evictEntry)) { ICacheRemoveListenerContext<K,V> removeListenerContext = CacheRemoveListenerContext.<K,V>newInstance().key(evictEntry.key()) .value(evictEntry.value()) .type(CacheRemoveType.EVICT.code()); for(ICacheRemoveListener<K,V> listener : context.cache().removeListeners()) { listener.listen(removeListenerContext); } }
if(isSizeLimit()) { throw new CacheRuntimeException("当前队列已满,数据添加失败!"); }
return map.put(key, value); }
|
淘汰策略
淘汰策略可以有多种,比如 LRU/LFU/FIFO 等等,我们此处实现一个最基本的 FIFO。所有实现以接口的方式实现,便于后期灵活替换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class CacheEvictFifo<K,V> extends AbstractCacheEvict<K,V> {
private final Queue<K> queue = new LinkedList<>();
@Override public CacheEntry<K,V> doEvict(ICacheEvictContext<K, V> context) { CacheEntry<K,V> result = null;
final ICache<K,V> cache = context.cache(); if(cache.size() >= context.size()) { K evictKey = queue.remove(); V evictValue = cache.remove(evictKey); result = new CacheEntry<>(evictKey, evictValue); }
final K key = context.key(); queue.add(key);
return result; }
}
|
除了FIFO策略的实现,还内置了其他几种实现策略
引导类
为了便于用户使用,我们实现类似于 guava 的引导类。所有参数都提供默认值,使用 fluent 流式写法,提升用户体验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
| public final class CacheBs<K,V> {
private CacheBs(){}
public static <K,V> CacheBs<K,V> newInstance() { return new CacheBs<>(); }
private Map<K,V> map = new HashMap<>();
private int size = Integer.MAX_VALUE;
private ICacheEvict<K,V> evict = CacheEvicts.fifo();
private final List<ICacheRemoveListener<K,V>> removeListeners = CacheRemoveListeners.defaults();
private final List<ICacheSlowListener> slowListeners = CacheSlowListeners.none();
private ICacheLoad<K,V> load = CacheLoads.none();
private ICachePersist<K,V> persist = CachePersists.none();
public CacheBs<K, V> map(Map<K, V> map) { ArgUtil.notNull(map, "map");
this.map = map; return this; }
public CacheBs<K, V> size(int size) { ArgUtil.notNegative(size, "size");
this.size = size; return this; }
public CacheBs<K, V> evict(ICacheEvict<K, V> evict) { ArgUtil.notNull(evict, "evict");
this.evict = evict; return this; }
public CacheBs<K, V> load(ICacheLoad<K, V> load) { ArgUtil.notNull(load, "load");
this.load = load; return this; }
public CacheBs<K, V> addRemoveListener(ICacheRemoveListener<K,V> removeListener) { ArgUtil.notNull(removeListener, "removeListener");
this.removeListeners.add(removeListener); return this; }
public CacheBs<K, V> addSlowListener(ICacheSlowListener slowListener) { ArgUtil.notNull(slowListener, "slowListener");
this.slowListeners.add(slowListener); return this; }
public CacheBs<K, V> persist(ICachePersist<K, V> persist) { this.persist = persist; return this; }
public ICache<K,V> build() { Cache<K,V> cache = new Cache<>(); cache.map(map); cache.evict(evict); cache.sizeLimit(size); cache.removeListeners(removeListeners); cache.load(load); cache.persist(persist); cache.slowListeners(slowListeners);
cache.init(); return CacheProxy.getProxy(cache); }
}
|