上一篇文章写了SpringBoot和Redis的基本操作(SpringBoot整合Redis之入门篇)。这些都是小打小闹,本篇文章我们来一些进阶的操作。主要讲解一下SpringBoot中使用Redis的基本操作类,实现Redis的整合。Redis的基本操作类就是已经封装好的CacheFetchUtils和RedisOperations,在之前的文章:Redis中常用的两个操作工具类中已经贴出,现在我们来看一下他们的整合。
我们先来写这样一个Controller,用于查询数据库中的资讯列表List:
@RequestMapping("/")
public ModelAndView index(){
ModelAndView mv = new ModelAndView("/index/index");
List infoList = informationService.findList();
mv.addObject("infoList" ,infoList);
return mv;
}
而InformationService中查询操作则是最基本的MyBatis走MySql的查询:
@Autowired
private InformationMapper informationMapper;
public List findList(){
return informationMapper.findList();
}
当然,这是并没有使用缓存的,现在我们在Service中整合一下缓存:
@Autowired
private RedisOperations redisOperations;
public List findList(){
List list = CacheFetchUtils.fromRedisList(redisOperations,"infoList",InformationModel.class,()->getData());
return list;
}
public List getData(){
return informationMapper.findList();
}
我们看一下上面的代码,当controller层调用findList方法时,本来是直接去Mysql查询数据的。但是现在我们走了CacheFetchUtils的fromRedisList方法,我们来看一下这个方法:
public static List fromRedisList(RedisOperations redisOperations, String redisKey, Class clazz, Supplier dbFunc,Object... object) {
List 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.getVal(redisKey)方法,根据指定的key去Redis中查询结果,如果存在,那么将查询到的JSON以指定的泛型类转为List,然后return掉;如果不存在,那么执行dbFun.get()方法,如果此方法查询结果仍为null,那么不好意思,只能返回null了,如果存在,那么执行valSerialze()方法,将Mysql中查询到的结果序列化到Redis中。这个逻辑非常清楚,我们主要来看一下dbFunc.get()。
Supplier是Java8中的新特性,称之为惰性求值,也就是把具体的求值过程延迟到真正需要的时候再去进行,看一下它的源码:
@FunctionalInterface
public interface Supplier {
/**
* 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);
}
}
解释一下其中的各个参数:
我们来看一下其中的逻辑:如果不定长参数没有值(也就是没有指定过期时间),那么直接执行putVal方法;如果不定长参数有值,那么判断其中的第一个参数:如果是时间类型的值(也即指定缓存的具体过期时间点),那么调用putValExpireAt方法,如果是Long类型的值(也即指定了缓存存储时长),那么调用putVal方法。来看一下这几个方法:
public void putVal(String key, V value) {
this.boundOptions(key).set(JSON.toJSONString(value));
}
public 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 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 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对象,传入即可)。
比如我们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 fromRedis(RedisOperations redisOperations, String redisKey, Class clazz, Supplier 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;
}