接着上文:
当然了这个无法只是相对的,肯定是有办法的,只不过要付出一点代价:
我们将要缓存的字符串封装成一个map,map的key随便取自己记得就行,然后执行:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DaSWebApplicationTests {
@Autowired
private SysService sysService;
@Test
public void contextLoads() {
Map<String,String> o1=(Map<String,String>)this.sysService.redisTest2("testKey");
System.out.println(o1.get("key"));
Object o2=this.sysService.redisTest2("testKey");
System.out.println("********************************************");
Map<String,String> result=(Map<String,String>)JSONObject.toBean((JSONObject)o2,Map.class);
System.out.println(result.get("key"));
}
}
输出结果:
Redis数据库存储结果:
在上面得例子中,我们第一次执行redisTest2()这个方法的时候,因为没有对应的key-value,所以会执行redisTest2()这个方法,然后将方法的返回值缓存到数据库中,因为返回的是方法正常执行的结果,所以返回的是“Map“,所以我们在执行完这个方法后,可以直接通过强制类型转换,然后输出结果。
第二次执行redisTest2()这个方法的时候,对应的key在redis数据库中已经有了,所以方法不会正常执行,程序会从redis数据库中取出key对应的value然后通过我们自定义的序列化类中的反序列化方法,将字节数组转换成字符串,然后再转换成jsonObject,再强制转换成Object,最后作为方法的返回值返还给方法的调用者,也正是因此,我们第二次执行完redisTest2()这个方法后,拿到的实际上是一个jsonObject对象,我们必须通过json的相关方法转化成我们实际想要的数据类型。
所以可以看到虽然我们可以缓存普通的字符串了,但是实际上是非常不合理的,对原本的方法代码进行了与方法本身无关的修改。所以这种通过json技术序列化的方式,看似很美好,其实问题还是不少的,而且这还只是单纯的只使用注解来进行缓存操作的,如果与redisTemplate结合来使用,还会有其他的问题。
因此在我看来,如果只是为了不让序列化对象实现序列化接口,且为了通过redis客户端查看的时候能够友好显示,我建议不要去修改默认的value的序列化方式。毕竟jdk的序列化方式对于我们在程序中使用redis是没有影响的。
二.只使用redisTemplate或stringRedisTemplate来进行缓存操作
首先我们申明一点,这个只使用的意思是,数据的缓存或读取都是通过redisTemplate或stringRedisTemplate来操作的,存储到缓存数据库中后的值,也不需要通过注解来获取。
在springboot的开发环境下,redisTempalte或stringReidsTemplate由springboot为我们自动配置,我们只需要在使用的地方通过依赖注入,即可直接使用。
我们来看这个例子:
@Override
public int redisTest4(String key1) {
this.stringRedisTemplate.opsForValue().append(“templateDemo1”, “templateDemo1”);
this.redisTemplate.opsForValue().set(“templateDemo1”, “templateDemo2”);
return 0;
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class DaSWebApplicationTests {
@Autowired
private SysService sysService;
@Test
public void contextLoads() {
this.sysService.redisTest4("tempKey");
}
}
在这个例子中我们用stringRedisTemplate存储了一个key为redisTemplateDemo1的数据,然后用redisTemplate同样存储了一个key为redisTemplateDemo1的数据,然后看一下执行的结果:
在redis Studio里:
通过redis-cli查看:
我们可以看到用stringRedisTemplate存储的数据被保存在了redis数据库中,而且key和value显示的都是正常字符串的形式,但是用redisTemplate存储的数据却看不到,而且不知道大家有没有这样的疑问,redis里可以存储两个key一样的数据吗?答案是当然不可以的。看下面:
如果我们定义了一个key,然后再次设置同一个key,那么如果第二次设置的key的值符合第一次设置的key的值得数据结构的要求,那么第二次设置的值会覆盖第一设置的值,如果第二次设置的值不符合第一次设置的key的值得数据结构,redis是会报错误类型的错误的。
那么回到我们的例子中,我们两次设置的key都一样,都是templateDemo1,那么为什么只看到第一次的存储结果呢?
这就又涉及到序列化的问题。
@Override
public int redisTest4(String key1) {
System.out.println(this.stringRedisTemplate.getKeySerializer());
System.out.println(this.stringRedisTemplate.getValueSerializer());
System.out.println(“********************************”);
System.out.println(this.redisTemplate.getKeySerializer());
System.out.println(this.redisTemplate.getValueSerializer());
return 0;
}
通过上面的代码我们可以看到,stringRedisTemplate默认的key的序列化方式是“stringXXXX”,姑且简称为string方式,而它的value也是string方式,然后redisTemplate默认的key和value的序列化方式都是jdkXXXX,姑且简称jdk的方式。
Stirng方式的序列化可以让我们通过redis客户端直观的看到我们存储的key和value的值,它就是我们输入的key和value的字符串形式,而jdk方式的序列化在redis数据库里存储的key和value的值通过redis客户端查看的话就不那么友好了。看下面:
这样的key和value并不影响我们在程序中去使用redis,但是如果你想要通过客户端去管理或查看redis数据库,明显这样的数据形式就不方便了。
我们再回到上面的例子中,第一次我们用stringRedisTemplate存储templateDemo1的时候,因为默认的key和value都是string形式的序列化方式,所以存储到redis数据库中我们可以直接通过get templateDemo1的方式查看:
但是第二次用redisTemplate在存储同样的key之后,因为redisTemplate的value和key的序列化方式都是jdk的方式,所以实际上经过序列化处理之后,templateDemo1已经变成了类似于下面这种格式,所以这就是为什么在程序里好像是设置了两个一样的key,但是既不会发生覆盖,也不会报错的原因,因为两次存储到redis中的key在先经过序列化之后就变得不一样了。
如果这个时候我们想要查看jdk方式序列化的数据,必须通过get 序列化之后的key 的方式来获取:
这样虽然能获取到,但是是非常不方便的,更重要的是如果我们哪天修改了程序代码,将下面这段代码删除或修改了:
this.redisTemplate.opsForValue().set(“templateDemo1”, “templateDemo2”);
时间一长之后,你可能就不知道从redis中获取当初存的数据了,因为jdk序列化之后的值实在是不好记。
但是为什么说上面的一堆问题并不影响我们程序开发呢,和注解的序列化与发序列化问题一样,虽然jdk的序列化方式会导致产生的key和value的值不好记忆或不直观,但是在程序中使用的时候,因为我们用的是同样的序列化方式进行序列化和发序列化操作,所以对于我们程序员来说什么样的序列化方式是不重要的:
@Override
public int redisTest4(String key1) {
this.redisTemplate.opsForValue().set(“templateDemo2”, “templateDemo2”);
System.out.println(this.redisTemplate.opsForValue().get(“templateDemo2”));
return 0;
}
我们通过redisTemplate去取这个值得时候,它也是先将key通过jdk的序列化方式进行序列化操作,然后根据序列化之后的key去redis数据库查找,所以当然能找到对应的值。
但是我们一般建议最好还是将redisTemplate的key的序列化方式改为string方式,除了主观上查看的时候能更直观方便,还有一个很重要的原因是,如果我们在程序中使用注解加模板类这种混合开发方式的话,不管是通过模板类去取注解缓存的值,还是通过注解去取模板类缓存的值,它们的key的序列化方式必须一致,而因为注解的默认的key的序列化方式为stirng方式的,所以模板类的序列化方式最好也改成一样的。要不然比如通过模板类存储了一个数据key为xxx,但是经过jdk的序列化之后实际上在redis数据库中就变成了ooo,然后用注解去获取这个数据的时候,注解用的key虽然同样为xxx,但是经过string方式的序列化之后就不可是ooo,这样一来就找不到我们想要找到的数据。
那么设置模板类的key的序列化方式也很简单:
就是覆盖redisTemplate的默认配置,将模板类的setKeySerializer()设置为stirng方式的。
好了key的问题解决了,那么value需不需处理呢?
比如我们现在将value的序列化方式改成通过json来进行序列化,也就是使用我们在讲注解的时候,自定义的序列化方式:
首先它会报第一个错,这个错跟我们之前遇到的错误一样,都是因为存储的value的值不符合json的格式,无法转换成json对象,解决的办法和之前的处理方式一样(或者使用stringRedisTemplate来处理string类型的数据)。
接下来我们修改一下程序,再试试:
@Override
public int redisTest4(String key1) {
User user=new User();
user.setUsername(“hello”);
user.setPassword(“redis”);
this.redisTemplate.opsForValue().set(“templateDemo5”, user);
return 0;
}
我们可以看到,能够正确的缓存数据了,而且显示的结果是非常友好直观的json格式。再来通过客户端查看数据库:
效果和注解的时候更换序列化方式是一样的,但是问题是不是也一样存在呢:
@Override
public int redisTest4(String key1) {
User user=(User)this.redisTemplate.opsForValue().get(“templateDemo5”);
return 0;
}
因为我们上面存储的templateDemo5中的数据是一个user类型的对象,所以我们当然希望取这个值得时候直接通过强制类型转换,转换成User对象,但是实际情况事与愿违:
我们获得的实际上是一个jsonObject对象,我们必须通过json的方法转换成我们的实际数据类型。
所以综上所述,修改redisTemplate的value的序列化方式所遇到的问题和使用注解的时候改变序列化方式遇到的问题是一样的,所以要不要将value的序列化方式改成非jdk方式的,恐怕需要根据实际业务需求来权衡定夺。
三.混合使用注解和模板类
混合使用指的是通过注解来获取模板类缓存的数据,或者通过模板类来获取注解缓存的数据。
首先结合上面探讨的问题,我们要注意的第一点是,如果希望有业务需求需要混合使用,那么两种方式的key和value的序列化方式必须一致。
如果注解的序列化方式为key是string,value为jdk,那么模板类的key和value的序列化方式也必须是string和jdk。
我们来看一个例子:
@Override
public void redisTest4() {
User u=new User();
u.setUsername(“newUser”);
u.setPassword(“newUser”);
this.redisTemplate.opsForValue().set(“userCache::templateDemo6”,u);
}
@Override
@Cacheable(value=”userCache”,key=”#key1”)
public User redisTest5(String key1) {
User u=new User();
u.setUsername("lala");
u.setPassword("yeye");
return u;
}
在上面的这个例子中,redisTest4方法缓存了一个key为userCache::templateDemo6的数据(为什么要这样设置key,主要是为了迎合注解的key的生成策略,后面会专门聊),
然后在redisTest5这个方法上标注了@Cacheable这个注解,这个时候如果调用redisTest5这个方法,并且传入的参数为templateDemo6,那么注解生成的key则为userCache::templateDemo6,因为reidsTest4方法缓存过这个key的数据,所以它会根据这个key去找到对应的value,然后反序列化之后转换成User类型,返还给方法的调用者:
@Test
public void contextLoads() {
this.sysService.redisTest4();
User u=(User)this.sysService.redisTest5(“templateDemo6”);
System.out.println(u.toString());
}
也就是说上面这个方法应该能正确打印输出。我们来看一下实际运行结果:
很明显报错了,这个错误的意识无法进行反序列化处理,我们先看一下数据库中:
数据已经被正常存储了,也就是说redisTest4这个方法是正确被执行的,那为什么执行redisTest5的时候报错了呢?
我们看下配置类:
在配置类中我们覆盖了模板类的key和value的序列化方式,key的序列化方式与注解的key的序列化方式一致,所以能找到对应的key,但是value的序列化方式可不一样,注解的value的序列化方式是jdk,而模板类的value的序列化方式已经被改成了我们自定义的序列化方式,所以当使用@Cacheable注解通过key找到对应的key的value之后,注解尝试通过jdk的方式去反序列化value,而这个value在存储的时候是通过我们自定义的序列化方式进行序列化的,所以当然不能反序列化成功,要想解决这个问题有两种方式。
第一将注解的value的序列化方式也改成我们自定义的序列化方式:
@Bean
public RedisCacheConfiguration redisCacheConfiguration(){
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new MyRedisSerializable()));
return configuration;
}
然后再执行程序:
这个时候依然报错,这个错很熟悉了,因为我们采用了json的方式进行序列化,所以这个时候我们标注的方法的返回值得改成Object,这样才能将从缓存中取出来反序列化之后的json对象转换成方法的返回类型,然后返还给方法调用者:
@Override
@Cacheable(value=”userCache”,key=”#key1”)
public Object redisTest5(String key1) {
User u=new User();
u.setUsername(“lala”);
u.setPassword(“yeye”);
return u;
}
@Test
public void contextLoads() {
Object u=(Object)this.sysService.redisTest5(“templateDemo6”);
User uu=(User)JSONObject.toBean((JSONObject)u,User.class);
System.out.println(uu.toString());
}
这个时候就能正确的取出通过模板类缓存的数据了。
所以如果要混合使用第一个要注意的就是保证注解和模板类的key和value的序列化方式一致。
第二个要注意的问题:
在保持上面的配置环境不变的前提下:
@Override
public void redisTest6() {
this.redisTemplate.opsForHash().put(“userCache::redisDemo6”, “mykey”, “myvalue”);
}
@Override
@Cacheable(value=”userCache”,key=”#key1”)
public Object redisTest7(String key1) {
User u=new User();
u.setUsername(“mykey”);
u.setPassword(“myvalue”);
return u;
}
我们定义了两个方法,redisTest6这个方法中使用redisTemplate存储了一个hash结构的数据,然后在redisTest7中使用了@Cacheable注解,然后我们来运行一下程序:
@Test
public void contextLoads() {
this.sysService.redisTest6();
Object u=(Object)this.sysService.redisTest7("redisTemplateDemo6");
System.out.println(u.toString());
}