项目开发一:实现固定大小的cache

项目开发一:实现固定大小的cache

cache的发展

  • HashMap

当我们应用有一定流量之后或者查询数据库特别频繁,这个时候就可以祭出我们的java中自带的HashMap或者ConcurrentHashMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
//在java代码中的表现 
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的基础上,增加了数据淘汰,下面为几种常见的数据淘汰算法

    • FIFO
1
2
3
先进先出,在这种淘汰算法中,先进入缓存的会先被淘汰。这种可谓是最简单的了,但是会导致我们命中率很低。

试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。
    • LRU
1
2
3
4
5
最近最少使用算法。

在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。

但是这个依然有个问题,如果有个数据在1个小时的前59分钟访问了1万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。
    • LFU
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;
}

/**
* 重写LinkedHashMap的removeEldestEntry方法即可
* 在Put的时候判断,如果为true,就会删除最老的
* @param eldest
* @return
*/
@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)
//写之后30ms过期
.expireAfterWrite(30L, TimeUnit.MILLISECONDS)
//访问之后30ms过期
.expireAfterAccess(30L, TimeUnit.MILLISECONDS)
//20ms之后刷新
.refreshAfterWrite(20L, TimeUnit.MILLISECONDS)
//开启weakKey key 当启动垃圾回收时,该缓存也被回收
.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) {
//1.1 尝试驱除
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);
}
}

//2. 判断驱除后的信息
if(isSizeLimit()) {
throw new CacheRuntimeException("当前队列已满,数据添加失败!");
}

//3. 执行添加
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> {

/**
* queue 信息
* @since 0.0.2
*/
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(){}

/**
* 创建对象实例
* @param <K> key
* @param <V> value
* @return this
* @since 0.0.2
*/
public static <K,V> CacheBs<K,V> newInstance() {
return new CacheBs<>();
}

/**
* map 实现
* @since 0.0.2
*/
private Map<K,V> map = new HashMap<>();

/**
* 大小限制
* @since 0.0.2
*/
private int size = Integer.MAX_VALUE;

/**
* 驱除策略
* @since 0.0.2
*/
private ICacheEvict<K,V> evict = CacheEvicts.fifo();

/**
* 删除监听类
* @since 0.0.6
*/
private final List<ICacheRemoveListener<K,V>> removeListeners = CacheRemoveListeners.defaults();

/**
* 慢操作监听类
* @since 0.0.9
*/
private final List<ICacheSlowListener> slowListeners = CacheSlowListeners.none();

/**
* 加载策略
* @since 0.0.7
*/
private ICacheLoad<K,V> load = CacheLoads.none();

/**
* 持久化实现策略
* @since 0.0.8
*/
private ICachePersist<K,V> persist = CachePersists.none();

/**
* map 实现
* @param map map
* @return this
* @since 0.0.2
*/
public CacheBs<K, V> map(Map<K, V> map) {
ArgUtil.notNull(map, "map");

this.map = map;
return this;
}

/**
* 设置 size 信息
* @param size size
* @return this
* @since 0.0.2
*/
public CacheBs<K, V> size(int size) {
ArgUtil.notNegative(size, "size");

this.size = size;
return this;
}

/**
* 设置驱除策略
* @param evict 驱除策略
* @return this
* @since 0.0.2
*/
public CacheBs<K, V> evict(ICacheEvict<K, V> evict) {
ArgUtil.notNull(evict, "evict");

this.evict = evict;
return this;
}

/**
* 设置加载
* @param load 加载
* @return this
* @since 0.0.7
*/
public CacheBs<K, V> load(ICacheLoad<K, V> load) {
ArgUtil.notNull(load, "load");

this.load = load;
return this;
}

/**
* 添加删除监听器
* @param removeListener 监听器
* @return this
* @since 0.0.6
*/
public CacheBs<K, V> addRemoveListener(ICacheRemoveListener<K,V> removeListener) {
ArgUtil.notNull(removeListener, "removeListener");

this.removeListeners.add(removeListener);
return this;
}

/**
* 添加慢日志监听器
* @param slowListener 监听器
* @return this
* @since 0.0.9
*/
public CacheBs<K, V> addSlowListener(ICacheSlowListener slowListener) {
ArgUtil.notNull(slowListener, "slowListener");

this.slowListeners.add(slowListener);
return this;
}

/**
* 设置持久化策略
* @param persist 持久化
* @return this
* @since 0.0.8
*/
public CacheBs<K, V> persist(ICachePersist<K, V> persist) {
this.persist = persist;
return this;
}

/**
* 构建缓存信息
* @return 缓存信息
* @since 0.0.2
*/
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);
}

}
谢谢你的支持哦,继续加油.