前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何利用redis实现缓存

如何利用redis实现缓存

作者头像
zhangheng
发布2020-04-28 18:16:24
3.2K0
发布2020-04-28 18:16:24
举报
文章被收录于专栏:张恒的网络日志

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

接口定义

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

CacheWrapper类:

代码语言:javascript
复制
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接口:

代码语言:javascript
复制
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静态类:

代码语言:javascript
复制
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类:

代码语言:javascript
复制
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来实现。

如下图:

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

LRU

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

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

  1. 新数据插入到链表头部;
  2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
  3. 当链表满的时候,将链表尾部的数据丢弃。

LFU

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

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

具体实现如下:

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-08-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 接口定义
  • key-value序列化
  • redisCache实现类
  • 缓存过期策略
    • FIFO
      • LRU
        • LFU
          • redis中的实现
          • 总结
          相关产品与服务
          云数据库 Redis®
          腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档