Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑
前言
上一篇已經(jīng)介紹了優(yōu)雅的操作Redis:
【小家Spring】Spring Boot中使用RedisTemplate優(yōu)雅的操作Redis,并且解決RedisTemplate泛型注入的問題。本篇著重介紹一
下幾種常用的序列化方式
最近在做一個(gè)項(xiàng)目,由于并發(fā)量大,大量使用到了RedisTemplate來操作Redis。但使用過程中,遇到了不少的坑,各種翻看源碼
來跟蹤,也總結(jié)出了不少的經(jīng)驗(yàn)。
因此今天專門做一篇專文來記錄這些坑,也具體說說RedisTemplate的各種序列化方式的差異性。希望對(duì)大家也能有所幫助,幫
助大家解決一些疑惑
序列化問題
RedisTemplate在遇到復(fù)雜類型的返序列化時(shí),即使加了泛型,獲取到的實(shí)際類型為L(zhǎng)inedHashMap,需要得到結(jié)果后再次反序列
化,不然會(huì)報(bào)類型轉(zhuǎn)換異常。
如下:這樣處理才是安全的:
在執(zhí)行序列化的時(shí)候,操作的如果是Bean,必須有默認(rèn)構(gòu)造器,否則報(bào)錯(cuò)
redis集群?jiǎn)栴}(關(guān)于集群的這幾個(gè)問題,后續(xù)在專門演示和解釋)
如果連接的為Redis集群,則不能用管道的方法,除非改寫管道的類
模糊查詢的時(shí)候需要獲取到所有的node信息,依次查詢
Spring提供的序列化方式
從源碼里看:
我們可以很清晰的看到,Spring為我們提供了6種不同的序列化方式。
特別說明一下:如果你是在Spring Boot1.5.x環(huán)境下使用,你可能看到是9種實(shí)現(xiàn)或者是7種實(shí)現(xiàn),如下圖所示
解釋:
關(guān)于前面兩個(gè),并非Spring官方提供,而是由alibaba的FastJson自己實(shí)現(xiàn)的。我們看看FastJson的包結(jié)構(gòu),發(fā)現(xiàn)它很友好的提供了一些常用的轉(zhuǎn)化器:
因此此處暫時(shí)不做過多描述,后面再說。
另外還有一個(gè)JacksonJsonRedisSerializer類,被標(biāo)記為過期。而這個(gè)類在SpringBoot2.0就直接被移除掉了,因此以后的版本不用理會(huì)了。
下面主要介紹一下,Spring官方現(xiàn)在還存在的6大序列化器:
Generic單詞意思:一般的; 通用的;類的,屬性的;
- OxmSerializer
以xml格式存儲(chǔ)(但還是String類型~),解析起來也比較復(fù)雜,效率也比較低。因此幾乎沒有人再使用此方式了
- JdkSerializationRedisSerializer
從源碼里可以看出,這是RestTemplate類默認(rèn)的序列化方式。若你沒有自定義,那就是它了。
?? ?@Overridepublic void afterPropertiesSet() {super.afterPropertiesSet();boolean defaultUsed = false;if (defaultSerializer == null) {defaultSerializer = new JdkSerializationRedisSerializer(classLoader != null ? classLoader : this.getClass().getClassLoader());}...使用JDK自帶的序列化方式,有明顯的缺點(diǎn):
首先它要求存儲(chǔ)的對(duì)象都必須實(shí)現(xiàn)java.io.Serializable接口,比較笨重
其次,他存儲(chǔ)的為二進(jìn)制數(shù)據(jù),這對(duì)開發(fā)者是不友好的
再次,因?yàn)樗鎯?chǔ)的為二進(jìn)制。但是有時(shí)候,我們的Redis會(huì)在一個(gè)項(xiàng)目的多個(gè)project中共用,這樣如果同一個(gè)可以緩存的對(duì)象
在不同的project中要使用兩個(gè)不同的key來分別緩存,既麻煩,又浪費(fèi)。
使用JDK提供的序列化功能。 優(yōu)點(diǎn)是反序列化時(shí)不需要提供(傳入)類型信息(class),但缺點(diǎn)是需要實(shí)現(xiàn)Serializable接口,還有
序列化后的結(jié)果非常龐大,是JSON格式的5倍左右,這樣就會(huì)消耗redis服務(wù)器的大量?jī)?nèi)存。
? ? @Autowiredprivate RedisTemplate redisTemplate;@Testpublic void contextLoads() {ValueOperations<String, Person> valueOperations = redisTemplate.opsForValue();valueOperations.set("aaa", new Person("fsx", 24));Person p = valueOperations.get("aaa"); //Person(name=fsx, age=24)System.out.println(p);}存儲(chǔ)的為二進(jìn)制,根本開不出來是什么,對(duì)開發(fā)者調(diào)試也很不友好
- StringRedisSerializer
也是StringRedisTemplate默認(rèn)的序列化方式,key和value都會(huì)采用此方式進(jìn)行序列化,是被推薦使用的,對(duì)開發(fā)者友好,輕量
級(jí),效率也比較高。
(例子略)
- GenericToStringSerializer
他需要調(diào)用者給傳一個(gè)對(duì)象到字符串互轉(zhuǎn)的Converter(相當(dāng)于轉(zhuǎn)換為字符串的操作交給轉(zhuǎn)換器去做),個(gè)人覺得使用起來其比較麻煩,還不如直接用字符串呢。所以不太推薦使用
后面兩種序列化方式是重點(diǎn)
- Jackson2JsonRedisSerializer
從名字可以看出來,這是把一個(gè)對(duì)象以Json的形式存儲(chǔ),效率高且對(duì)調(diào)用者友好
優(yōu)點(diǎn)是速度快,序列化后的字符串短小精悍,不需要實(shí)現(xiàn)Serializable接口。
但缺點(diǎn)也非常致命:那就是此類的構(gòu)造函數(shù)中有一個(gè)類型參數(shù),必須提供要序列化對(duì)象的類型信息(.class對(duì)象)。 通過查看源代
碼,發(fā)現(xiàn)其在反序列化過程中用到了類型信息(必須根據(jù)此類型信息完成反序列化)。
- GenericJackson2JsonRedisSerializer
基本和上面的Jackson2JsonRedisSerializer功能差不多,使用方式也差不多,**但是是推薦使用的**
需要注意:(使用區(qū)別)
? ? @Testpublic void contextLoads() {redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new StringRedisSerializer());//ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();//此處泛型 因?yàn)榫幾g器無法校驗(yàn) ?所以如果value序列化方式是字符串 下面就會(huì)報(bào)錯(cuò)了ValueOperations<String, Person> valueOperations = redisTemplate.opsForValue();valueOperations.set("key", new Person("fsx", 24)); //java.lang.ClassCastException: com.fsx.run2.bean.Person cannot be cast to java.lang.StringPerson value = valueOperations.get("key");System.out.println(value);}如上,假如我value的序列化方式設(shè)置為String序列化器。但是set值的時(shí)候放對(duì)象了。這個(gè)時(shí)候就直接報(bào)錯(cuò)了,并不會(huì)自動(dòng)調(diào)用
toString()方法,此處一定要注意。還需要特別是初始化RestTemplate的時(shí)候,value的序列化方式禁止使用有類型偏向的
StringRedisSerializer。若有需要,你直接使用StringRedisTemplate操作即可
Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer的異同
Jackson2JsonRedisSerializer:為我們提供了兩個(gè)構(gòu)造方法,一個(gè)需要傳入序列化對(duì)象Class,一個(gè)需要傳入對(duì)象的JavaType:
?? ?public Jackson2JsonRedisSerializer(Class<T> type) {this.javaType = getJavaType(type);}public Jackson2JsonRedisSerializer(JavaType javaType) {this.javaType = javaType;}這種的壞處,很顯然,我們就不能全局使用統(tǒng)一的序列化方式了,而是每次調(diào)用RedisTemplate前,都需要類似這么處理:
redisTemplate.setKeySerializer(RedisSerializerType.StringSerializer.getRedisSerializer());redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Person.class));但因?yàn)閞edisTemplate我們都是單例的,所以這樣設(shè)置顯然是非常不可取的行為。雖然它有好處~~~~~~~~~~
這種序列化方式的好處:他能實(shí)現(xiàn)不同的Project之間數(shù)據(jù)互通(因?yàn)闆]有@class信息,所以只要字段名相同即可),因?yàn)槠鋵?shí)就
是Json的返序列化,只要你指定了類型,就能反序列化成功(因?yàn)樗桶麩o關(guān))
使用這種Json序列化方式果然是可以成功的在不同project中進(jìn)行序列化和反序列化的。但是,但是,但是:在實(shí)際的使用中,我們希望職責(zé)單一和高內(nèi)聚的,所以并不希望我們存在的對(duì)象,其它服務(wù)可以直接訪問,那樣就非常不好控制了,因此此種方式也不建議使用~
GenericJackson2JsonRedisSerializer:這種序列化方式不用自己手動(dòng)指定對(duì)象的Class。所以其實(shí)我們就可以使用一個(gè)全局通用
的序列化方式了。使用起來和JdkSerializationRedisSerializer基本一樣。
同樣的JdkSerializationRedisSerializer不能序列化和反序列化不同包路徑對(duì)象的毛病它也有。因?yàn)樗蛄谢蟮膬?nèi)容,是存儲(chǔ)
了對(duì)象的class信息的:
========> Jackson2JsonRedisSerializer的坑:
存儲(chǔ)普通對(duì)象的時(shí)候沒有問題,但是當(dāng)我們存儲(chǔ)帶泛型的List的時(shí)候,反序化就會(huì)報(bào)錯(cuò)了:
? ? @Testpublic void contextLoads() {redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(List.class));ValueOperations<String, List<Person>> valueOperations = redisTemplate.opsForValue();valueOperations.set("aaa", Arrays.asList(new Person("fsx", 24), new Person("fff", 30)));List<Person> p = valueOperations.get("aaa");System.out.println(p); //[{name=fsx, age=24}, {name=fff, age=30}]List<Person> aaa = (List<Person>) redisTemplate.opsForValue().get("aaa");System.out.println(aaa); //[{name=fsx, age=24}, {name=fff, age=30}]}結(jié)論:網(wǎng)上很多帖子都說這樣會(huì)出問題,但我實(shí)驗(yàn)過后發(fā)現(xiàn)不會(huì)有問題。時(shí)間有限,我這個(gè)是基于Spring Boot2.1進(jìn)行測(cè)試的,
若你們測(cè)試的版本有問題,歡迎告知我,我再做進(jìn)一步的驗(yàn)證,多謝。
========> GenericJackson2JsonRedisSerializer的坑:
? ? @Testpublic void contextLoads() {redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());ValueOperations<String, Long> valueOperations = redisTemplate.opsForValue();valueOperations.set("aaa", 1L);//Long p = valueOperations.get("aaa"); //轉(zhuǎn)換異常 java.lang.Integer cannot be cast to java.lang.LongObject p = valueOperations.get("aaa");System.out.println(p);}**坑1:**泛型里明明返回的就是Long類型,但你用Long接,就直接拋出轉(zhuǎn)換異常了
從上圖中我們可以清晰的看見,get出來返回的真實(shí)類型竟然是Integer類型,所以強(qiáng)轉(zhuǎn)肯定報(bào)錯(cuò)啊
再看一例:set類型
? ? @Testpublic void contextLoads() {redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());SetOperations<String, Long> setOperations = redisTemplate.opsForSet();setOperations.add("bbb", 1L);setOperations.add("bbb", 2L);Set<Long> p = setOperations.members("bbb");System.out.println(p);}我們發(fā)現(xiàn),里面裝的竟然,竟然是Integer類型。這種Java泛型的bug我們?cè)谥暗牟┪睦镉兄v述過,特別坑。這個(gè)時(shí)候這個(gè)變量
就是個(gè)地雷,只要一碰,就報(bào)錯(cuò)
另外,就算你獲取的并不是List類型,而是一個(gè)值,也必須要轉(zhuǎn)換一下,否則類型轉(zhuǎn)換異常。像下面這么操作才是安全的:
? ? ? ? ? ? Object teaIdObj = setOperLong.pop(teaCategoryKey);if (teaIdObj != null) {log.info("從redis老師倉(cāng)庫(kù){}里拿到了一個(gè)老師{}", teaCategoryKey, teaIdObj);teacherIds.add(Long.parseLong(teaIdObj.toString()));}類型轉(zhuǎn)換異常原因分析
因?yàn)镚enericJackson2JsonRedisSerializer這種序列化方式實(shí)在是太通用了,所以我還是希望找出原因,解決這個(gè)問題的。因此
我就跟蹤源碼,看看到底是哪里出了問題:
執(zhí)行setOperations.members("bbb")這句最終都到了RedisTemplate的execute方法:
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {...}方法體的這一行,解析了返回的value值:
T result = action.doInRedis(connToExpose);tips:Spring Boot1.x此處connToExpose使用的是jedis的,而Boot2.x使用的是Lettuce的了。但是對(duì)調(diào)用者是透明的,可謂非常友好
繼續(xù)跟蹤發(fā)現(xiàn),最終會(huì)調(diào)用我們配置好的序列化器進(jìn)行序列化:
?? ?V deserializeValue(byte[] value) {if (valueSerializer() == null) {return (V) value;}return (V) valueSerializer().deserialize(value);}因此啥都不說了,到GenericJackson2JsonRedisSerializer去看看它的deserialize方法吧,就在這一句話:
// 調(diào)用了jackson的ObjectMapper方法進(jìn)行返序列化 ?但是type為Object.class return mapper.readValue(source, type);為何我的泛型類型丟失了呢?向上追溯一步,我們發(fā)現(xiàn):
?? ?static <T extends Collection<?>> T deserializeValues(@Nullable Collection<byte[]> rawValues, Class<T> type,@Nullable RedisSerializer<?> redisSerializer) {// connection in pipeline/multi modeif (rawValues == null) {return (T) CollectionFactory.createCollection(type, 0);}Collection<Object> values = (List.class.isAssignableFrom(type) ? new ArrayList<>(rawValues.size()): new LinkedHashSet<>(rawValues.size()));for (byte[] bs : rawValues) {values.add(redisSerializer.deserialize(bs));}return (T) values;}我們的類型全部變成了Collection里面的Object類型,我們的泛型就這樣丟失了。所以在序列化的時(shí)候,只要遇到數(shù)字(或者泛
型),自然就是當(dāng)作Integer來處理了,因此就出現(xiàn)了我們看到的詭異現(xiàn)象。
因?yàn)镚enericJackson2JsonRedisSerializer本來處理序列化的都是與類型無關(guān)的,所以都轉(zhuǎn)換為Object進(jìn)行處理。因此出現(xiàn)此種
現(xiàn)象也是在情理之中的。
解決方案
既然你需要GenericJackson2JsonRedisSerializer它的通用性,那么你就得接受他只能處理Object類型。
因此在使用的時(shí)候遇上這種情況,需要稍加注意了。我們可以先用Object接收,然后轉(zhuǎn)成字符串再調(diào)用Long.valueOf()方法去間
接實(shí)現(xiàn)。。。或者你在使用前手動(dòng)指定序列化類型,但十分、十分不建議這么去做
它處理List、Set、Long類型等都會(huì)有類似的問題。使用的時(shí)候稍加注意即可(因?yàn)镴ava中默認(rèn)數(shù)字類型是Integer、Double等)
當(dāng)然還有一種方案是自定義序列化器:如自定義String序列化器,接受一切類型(官方的泛型限制了只接受String類型。這么一
來,@Cacheable等注解的key支持不僅僅是String類型了):
/*** 必須重寫序列化器,否則@Cacheable注解的key會(huì)報(bào)類型轉(zhuǎn)換錯(cuò)誤*/ public class StringRedisSerializer implements RedisSerializer<Object> {private final Charset charset;private final String target = "\"";private final String replacement = "";public StringRedisSerializer() {this(Charset.forName("UTF8"));}public StringRedisSerializer(Charset charset) {Assert.notNull(charset, "Charset must not be null!");this.charset = charset;}@Overridepublic String deserialize(byte[] bytes) {return (bytes == null ? null : new String(bytes, charset));}@Overridepublic byte[] serialize(Object object) {//底層還是調(diào)用的fastjson的工具來操作的String string = JSON.toJSONString(object);if (string == null) {return null;}string = string.replace(target, replacement);return string.getBytes(charset);} }順便提一句:單元測(cè)試的時(shí)候可能碰上這個(gè)異常:WRONGTYPE Operation against a key holding the wrong kind of value,不
要慌。這個(gè)是因?yàn)閗ey的類型不一致導(dǎo)致,一般只有在測(cè)試情況下才會(huì)發(fā)生**。比如之前這個(gè)key用用作k-v的形式,現(xiàn)在把這個(gè)
key當(dāng)作set數(shù)據(jù)類型來用,就會(huì)報(bào)這個(gè)錯(cuò),換給key就行。**
說明:Jackson2JsonRedisSerializer的效率稍微優(yōu)于GenericJackson2JsonRedisSerializer,但是使用起來遠(yuǎn)沒有Generic方便。
各位看官可以根據(jù)自己業(yè)務(wù)的實(shí)際情況,酌情選擇吧~~~~
第三方序列化器:FastJsonRedisSerializer、KryoRedisSerializer
由于Redis的流行,很多第三方組件都提供了對(duì)應(yīng)的序列化器。比較著名的有阿里巴巴的FastJsonRedisSerializer
還好ali默認(rèn)已經(jīng)幫我們實(shí)現(xiàn)了基于fastjson的序列化方式,我們都不用自己動(dòng)手了。
FastJsonRedisSerializer和GenericFastJsonRedisSerializer
和上面一樣講述的一樣,FastJsonRedisSerializer需要指定反序列化類型,而GenericFastJsonRedisSerializer則比較通用。但同
樣的Generic系列存在上面我說的同樣的問題,大家使用時(shí)需要多加注意。
KryoRedisSerializer:它就沒有這么這么友好了,但自己實(shí)現(xiàn)一個(gè)也是輕而易舉的事:
public class KryoRedisSerializer<T> implements RedisSerializer<T> {private Kryo kryo = new Kryo();@Overridepublic byte[] serialize(T t) throws SerializationException {System.out.println("[serialize]" + t);byte[] buffer = new byte[2048];Output output = new Output(buffer);kryo.writeClassAndObject(output, t);return output.toBytes();}@Overridepublic T deserialize(byte[] bytes) throws SerializationException {System.out.println("[deserialize]" + Arrays.toString(bytes));Input input = new Input(bytes);@SuppressWarnings("unchecked")T t = (T) kryo.readClassAndObject(input);return t;}}指定RedisTemplate的序列化方式
這個(gè)就比較簡(jiǎn)單了,可以在注冊(cè)Bean的時(shí)候就set(推薦),也可以使用的時(shí)候再做(非常非常不推薦,會(huì)有并發(fā)安全問題)
? ? @Beanpublic RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);// 使用Jackson2JsonRedisSerialize 替換默認(rèn)序列化(備注,此處我用Object為例,各位看官請(qǐng)換成自己的類型哦~)Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);// 設(shè)置value的序列化規(guī)則和 key的序列化規(guī)則redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setKeySerializer(new StringRedisSerializer());// 最好是調(diào)用一下這個(gè)方法redisTemplate.afterPropertiesSet();return redisTemplate;}java源生序列化的效率已經(jīng)非常高了,但是kryo是java原生序列化性能十幾倍(kryo只針對(duì)java語言,不跨語言。跨語言的序列化
方式有:Protostuff、Thrift等。 所以如果你想自定義序列化器的話,個(gè)人建議可以導(dǎo)入kryo包,然后自己書寫一個(gè)序列化器注冊(cè)
進(jìn)去~~~)
---------------------?
作者:_YourBatman?
來源:CSDN?
原文:https://blog.csdn.net/f641385712/article/details/84679456?
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請(qǐng)附上博文鏈接!
總結(jié)
以上是生活随笔為你收集整理的Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微服务架构与领域驱动设计应用实践
- 下一篇: MySQL全面优化,速度飞起来!