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来实现。
如下图:

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应用提供缓存。

标签: java, GuavaCache, Cache, Ehcache, redis

添加新评论