如何利用redis实现缓存

redis是典型的非关系型数据库,支持key-value,hash,list,set等各种数据结构。那么如何利用redis实现缓存呢?

接口定义

首先,我们需要定义一个数据包装类,用来包装缓存的值,为什么需要包装类呢?举个例子,假设我们缓存了一个null值,那么缓存返回的也是null值,使用者怎么知道这个null是说缓存中没数据还是缓存的就是null?

CacheWrapper类:

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
public class CacheWrapper<E> implements Serializable {
private E value;

private boolean exist = false;

public CacheWrapper() {
}

public CacheWrapper(E e) {
this.value = e;
}

public CacheWrapper(E value, boolean exist) {
this.value = value;
this.exist = exist;
}

public E getValue() {
return value;
}

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

public boolean isExist() {
return exist;
}

public void setExist(boolean exist) {
this.exist = exist;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CacheWrapper)) return false;

CacheWrapper<?> that = (CacheWrapper<?>) o;

if (isExist() != that.isExist()) return false;
return getValue() != null ? getValue().equals(that.getValue()) : that.getValue() == null;
}

@Override
public int hashCode() {
int result = getValue() != null ? getValue().hashCode() : 0;
result = 31 * result + (isExist() ? 1 : 0);
return result;
}
}

接下来,我们需要定一个接口,方便未来扩展。

Cache接口:

1
2
3
4
5
6
7
8
public interface Cache<K, V> {
// 设置
boolean put(K k, V v);
// 获取
CacheWrapper<V> get(K k);
// 清除指定key
boolean expulse(K k);
}

key-value序列化

我们知道,redis只能存string/byte数组/int/long等基础类型的数据,一般我们用的比较多的也是string类型。那么针对java中众多对象,我们需要定义一个序列化方法和反序列化方法,方便存取数据。

简单来说,使用json序列化和反序列化可以满足需求。但是json序列化得到的数据长度较长,占内存多。所以我们选择了hessian。

以下是HessianSerialize静态类:

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
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class HessianSerialize {
private static byte[] _serialize(Object obj) throws IOException {
if (obj == null) throw new NullPointerException();
ByteArrayOutputStream os = new ByteArrayOutputStream();
HessianOutput ho = new HessianOutput(os);
ho.writeObject(obj);
return os.toByteArray();
}

private static Object _deserialize(byte[] by) throws IOException {
if (by == null) throw new NullPointerException();
ByteArrayInputStream is = new ByteArrayInputStream(by);
HessianInput hi = new HessianInput(is);
return hi.readObject();
}

public static byte[] serialize(Object obj) throws Exception {
if (obj == null) throw new NullPointerException();
return _serialize(obj);
}

public static Object deserialize(byte[] by) throws Exception {
if (by == null) throw new NullPointerException();
return _deserialize(by);
}
}

redisCache实现类

接下来就是实现代码了,在实现中需要配置一下expireTime,防止数据无限缓存,还有在出现异常时,是否需要抛出异常。

RedisCache类:

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
import redis.clients.jedis.Jedis;

public class RedisCache<K, V> implements Cache<K, V> {
// 默认的缓存时间,如果不设置,数据一直保存在redis中,对redis来说压力太大
private static final int DEFAULT_CACHE_TIME_LIMIT = 24 * 60 * 60;
// 出现异常是否静默
private boolean throwSilent = true;
// 缓存时间
private int defaultCacheTimeLimit = DEFAULT_CACHE_TIME_LIMIT;
// redis
private Jedis jedis;

/**
* 先set已经序列化的数据,value值被CacheWrapper包装
* @param k
* @param v
* @return
*/
public boolean put(K k, V v) {
try {
jedis.set(HessianSerialize.serialize(k), HessianSerialize.serialize(new CacheWrapper<V>(v, true)));
jedis.expire(SerializeUtil.hessian2Serialize(k), defaultCacheTimeLimit);
return true;
} catch (Exception e) {
if (throwSilent) {
e.printStackTrace();
return false;
} else {
throw new RuntimeException(e);
}
}
}

/**
* 获取数据
* @param k
* @return
*/
public CacheWrapper<V> get(K k) {
try {
byte[] result = jedis.get(HessianSerialize.serialize(k));
if (result == null) {
return new CacheWrapper<V>(null, false);
}
return (CacheWrapper<V>) HessianSerialize.deserialize(result);
} catch (Exception e) {
if (throwSilent) {
e.printStackTrace();
return new CacheWrapper<V>(null, false);
} else {
throw new RuntimeException(e);
}

}
}

/**
* 清除缓存
* @param k
* @return
*/
public boolean expulse(K k) {
try {
jedis.del(HessianSerialize.serialize(k));
return true;
} catch (Exception e) {
if (throwSilent) {
e.printStackTrace();
return false;
} else {
throw new RuntimeException(e);
}

}
}

public boolean isThrowSilent() {
return throwSilent;
}

public void setThrowSilent(boolean throwSilent) {
this.throwSilent = throwSilent;
}

public int getDefaultCacheTimeLimit() {
return defaultCacheTimeLimit;
}

public void setDefaultCacheTimeLimit(int defaultCacheTimeLimit) {
this.defaultCacheTimeLimit = defaultCacheTimeLimit;
}

public Jedis getJedis() {
return jedis;
}

public void setJedis(Jedis jedis) {
this.jedis = jedis;
}
}

缓存过期策略

缓存过期策略是指由于缓存大小有限,当新的缓存数据加入进来的时候,需要清理掉旧的缓存数据,腾出有限空间。

缓存过期策略分为以下三种:

  • FIFO:First In First Out,先进先出
  • LRU:Least Recently Used,最近最少使用
  • LFU:Least Frequently Used,最不经常使用

原理和实现如下:

FIFO

FIFO按照“先进先出(First In,First Out)”的原理淘汰数据,正好符合队列的特性,数据结构上使用队列Queue来实现。
如下图:

upload successful

  1. 新访问的数据插入FIFO队列尾部,数据在FIFO队列中顺序移动;

  2. 淘汰FIFO队列头部的数据;

LRU

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

upload successful

  1. 新数据插入到链表头部;

  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

  3. 当链表满的时候,将链表尾部的数据丢弃。

LFU

LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。

具体实现如下:

upload successful

  1. 新加入数据插入到队列尾部(因为引用计数为1);

  2. 队列中的数据被访问后,引用计数增加,队列重新排序;

  3. 当需要淘汰数据时,将已经排序的列表最后的数据块删除。

redis中的实现

操作redis数据不像操作java集合一样方便,如果redis内存足够大,我们可以模拟以上三种过期策略。

在本文代码中,我们统一设置了缓存失效时间,也就是说先缓存的数据会先被清理掉,这和FIFO策略很类似。

如何实现LRU呢?我们可以在get数据时,如果在redis中得到了key和对应的value,就刷新key的过期时间expireTime,这就相当于将最近使用的key放到了链表的表头。

如何实现LFU?LFU比LRU高级一点,需要对每个key的get次数计数,这种redis操作也比较难,那如何实现呢?我们可以在get到数据后,在这个key的过期时间上再加一个countTime计数时间。相当于每get一次key,这个key的过期时间就会被增加一点,变相实现了LFU。

总结

为什么会想用redis做缓存?之前调研过Ehcache和Guava中的Cache,参见Ehcache与Guava Cache之间的区别Guava学习:Cache缓存入门。Ehcache虽然使用RMI实现了分布式缓存,但使用起来配置较多,较复杂,Guava Cache虽简单易用,但是仅限于单jvm使用。

redis经过简单的封装就能给跨jvm应用提供缓存。