SpringBoot整合Redis之进阶篇

上一篇文章写了SpringBoot和Redis的基本操作(SpringBoot整合Redis之入门篇)。这些都是小打小闹,本篇文章我们来一些进阶的操作。主要讲解一下SpringBoot中使用Redis的基本操作类,实现Redis的整合。Redis的基本操作类就是已经封装好的CacheFetchUtils和RedisOperations,在之前的文章:Redis中常用的两个操作工具类中已经贴出,现在我们来看一下他们的整合。

1.List操作

我们先来写这样一个Controller,用于查询数据库中的资讯列表List:

@RequestMapping("/")
public ModelAndView index(){
    ModelAndView mv = new ModelAndView("/index/index");
    List<InformationModel> infoList = informationService.findList();
    mv.addObject("infoList" ,infoList);
    return mv;
}

而InformationService中查询操作则是最基本的MyBatis走MySql的查询:

    @Autowired
    private InformationMapper informationMapper;

    public List<InformationModel> findList(){
        return informationMapper.findList();
    }

当然,这是并没有使用缓存的,现在我们在Service中整合一下缓存:

@Autowired
private RedisOperations redisOperations;

public List<InformationModel> findList(){
    List<InformationModel> list = CacheFetchUtils.fromRedisList(redisOperations,"infoList",InformationModel.class,()->getData());

    return list;
}

public List<InformationModel> getData(){
    return informationMapper.findList();
}

我们看一下上面的代码,当controller层调用findList方法时,本来是直接去Mysql查询数据的。但是现在我们走了CacheFetchUtils的fromRedisList方法,我们来看一下这个方法:

    public static <T> List<T> fromRedisList(RedisOperations redisOperations, String redisKey, Class<T> clazz, Supplier<List> dbFunc,Object... object) {
        List<T> result = JSON.parseArray(redisOperations.getVal(redisKey),clazz);
        if(CollectionUtils.isEmpty(result)) {
            result = dbFunc.get();
            if(result == null) {
                logger.error("fetch " + clazz + " error, redisKey: " + redisKey);
                return null;
            }
            valSerialize(redisOperations,redisKey,result,object);
        }
        return result;
    }

分别解释一下其中的各个参数:

  • RedisOperations redisOperations:就是另外一个Redis操作封装类RedisOperations(并不是org.springframework.data.redis.core.RedisOperations这个源码类)
  • String redisKey:Redis中存储的key值
  • Class<T> clazz:所查询的List集合的泛型
  • Supplier<List> dbFunc:当Redis中查询不到结果时所执行的方法(一般就是去MySql中拿数据)
  • Object...object:不定参数,此处主要用于Redis过期时间

我们来看一下上面方法的逻辑:首先,调用了redisOperations.getVal(redisKey)方法,根据指定的key去Redis中查询结果,如果存在,那么将查询到的JSON以指定的泛型类转为List,然后return掉;如果不存在,那么执行dbFun.get()方法,如果此方法查询结果仍为null,那么不好意思,只能返回null了,如果存在,那么执行valSerialze()方法,将Mysql中查询到的结果序列化到Redis中。这个逻辑非常清楚,我们主要来看一下dbFunc.get()。

Supplier是Java8中的新特性,称之为惰性求值,也就是把具体的求值过程延迟到真正需要的时候再去进行,看一下它的源码:

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

非常简单的一个定义,仅仅是得到一个对象。但它有什么用呢?我们可以把耗资源运算放到get方法里,在程序里,我们传递的是Supplier对象,直到调用get方法时,运算才会执行。这就是所谓的惰性求值。他的原理在这里不过多赘述。

书归正传,继续看上面的方法。我们有必要了解一下redisOperations.getVal(redisKey)方法是怎样在Redis中查询的:

public String getVal(String key) {
        return this.redisTemplate.execute((connection) ->
        {
            byte[] bytes = connection.get(key.getBytes());
            try {
                return null != bytes ? new String(bytes, CharsetNames.UTF_8) : null;
            }
            catch (UnsupportedEncodingException e) {
                logger.error("UnsupportedEncodingException:{}" + e);
            }
            return null;
        }, true);
    }

在RedisOperations的操作类中,注入了StringRedisTemplate类,这个类是Redis基本的操作类。

再来看一下valSerialze()方法:

private static void valSerialize(RedisOperations redisOperations,String redisKey, Object result,Object... object){
        Object[] objects= object;
        if(objects!=null && objects.length>0){
            Object obj= objects[0];
            if(obj instanceof Date){
                redisOperations.putValExpireAt(redisKey, result, (Date) obj);
            }else {
                Long expireTime= Long.valueOf(obj.toString());
                if(expireTime==0){
                    return;
                }
                redisOperations.putVal(redisKey, result,expireTime);
            }
        }else{
            redisOperations.putVal(redisKey, result);
        }
    }

解释一下其中的各个参数:

  • RedisOperations redisOperations:Redis操作封装类RedisOperations
  • String redisKey:Redis中存储的key值
  • Object result:需要序列化到Redis中的数据
  • Object...object:不定参数,此处主要用于Redis过期时间

我们来看一下其中的逻辑:如果不定长参数没有值(也就是没有指定过期时间),那么直接执行putVal方法;如果不定长参数有值,那么判断其中的第一个参数:如果是时间类型的值(也即指定缓存的具体过期时间点),那么调用putValExpireAt方法,如果是Long类型的值(也即指定了缓存存储时长),那么调用putVal方法。来看一下这几个方法:

  • 没有指定过期时间
    public <V> void putVal(String key, V value) {
        this.boundOptions(key).set(JSON.toJSONString(value));
    }
  • 指定过期时间点:
    public <V> void putValExpireAt(String key, V value, Date date) {
        BoundValueOperations operation = this.boundOptions(key);
        if (operation != null) {
            operation.set(JSON.toJSONString(value, new SerializerFeature[0]));
            operation.expireAt(date);
        }
    }
  • 指定过期时长
    public <V> void putVal(String key, V value, long expireSeconds) {
        BoundValueOperations operation = this.boundOptions(key);
        if (operation != null) {
            operation.set(JSON.toJSONString(value, new SerializerFeature[0]));
            operation.expire(expireSeconds, TimeUnit.SECONDS);
        }
    }

在这三个方法中,都调用了boundOptions(key)方法:

    private BoundValueOperations<String, String> boundOptions(String key) {
        return this.redisTemplate.boundValueOps(key);
    }

这个方法生成了BoundValueOperations对象,我们主要是使用这个对象来操作Redis的。

这样,我们就完成了Service层中对Redis的封装,当我们第一次访问这个接口时,首先会访问Redis数据库,当然这时是没有数据的,于是去MySql中查询,当查询到数据后,就会向Redis中序列化一份,当我们以后再访问该接口时,就无需再访问MySql了,而是直接在Redis中拿缓存的数据。

如上图中所示,我的本地Redis Desktop Manager中已经缓存上了需要的数据。可以看到,由于我之前没有设置缓存过期时间,所以这里的TTL标记为-1。

然而,一般情况下缓存都是有过期时间的。比如新闻资讯等,可能会有1小时的过期时间,甚至指定其每天凌晨1点自动过期。我们在Redis Desktop Manager中将该缓存手动清除掉,并尝试将Service层中该缓存过期时间改为1小时,只需在CacheFtechUtils.fromRedisList方法的最后加上Long型的3600(单位为秒)即可。再次调用接口,会发现缓存过期时间还有将近1小时。

如果需要指定其在每天指定时间点过期,只需传入一个Date型的参数即可(比如每天凌晨1点过期,然后每天第一次序列化时,需要计算一个凌晨1点的Date对象,传入即可)。

 

2.其他非List操作

比如我们Controller需要查询一个指定的InformationModel对象,在Service层中类似的,只需要调用CacheFetchUtils.fromRedis方法:

    public InformationModel findKeyWord(){
        InformationModel result = CacheFetchUtils.fromRedis(redisOperations,"keyWord",InformationModel.class,()->getKeyWord());
        return result;
    }

    private InformationModel getKeyWord(){
        return informationMapper.findKeyWord(60);
    }

其原理和CacheFetchUtils.fromRedisList基本一致,并且其中的泛型可以是Object的,同样可以设置过期时间:

    public static <T> T fromRedis(RedisOperations redisOperations, String redisKey, Class<T> clazz, Supplier<T> dbFunc, Object... object) {
        T result = redisOperations.getVal(redisKey, clazz);
        if(result == null) {
            result = dbFunc.get();
            if(result == null) {
                logger.error("fetch " + clazz + " error, redisKey: " + redisKey);
                return null;
            }
            valSerialize(redisOperations,redisKey,result,object);
        }
        return result;
    }

猜你喜欢

转载自blog.csdn.net/hz_940611/article/details/80848050