如何利用redis实现缓存
redis是典型的非关系型数据库,支持key-value,hash,list,set等各种数据结构。那么如何利用redis实现缓存呢?
接口定义
首先,我们需要定义一个数据包装类,用来包装缓存的值,为什么需要包装类呢?举个例子,假设我们缓存了一个null值,那么缓存返回的也是null值,使用者怎么知道这个null是说缓存中没数据还是缓存的就是null?
CacheWrapper类:
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接口:
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静态类:
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类:
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来实现。
如下图:
- 新访问的数据插入FIFO队列尾部,数据在FIFO队列中顺序移动;
- 淘汰FIFO队列头部的数据;
LRU
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
LFU
LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
LFU的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。
具体实现如下:
- 新加入数据插入到队列尾部(因为引用计数为1);
- 队列中的数据被访问后,引用计数增加,队列重新排序;
- 当需要淘汰数据时,将已经排序的列表最后的数据块删除。
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应用提供缓存。