Spring Data Redis對(duì)象緩存序列化問(wèn)題

相信在項(xiàng)目中凯楔,你一定是經(jīng)常使用 Redis ,那么锦募,你是怎么使用的呢摆屯?在使用時(shí),有沒(méi)有遇到同我一樣糠亩,對(duì)象緩存序列化問(wèn)題的呢虐骑?那么,你又是如何解決的呢赎线?

Redis 使用示例

添加依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在應(yīng)用啟動(dòng)如何添加啟用緩存注解(@EnableCaching)廷没。

假如我們有一個(gè)用戶對(duì)象(UserVo):

@Data
public class UserVo implements Serializable {

    @Serial
    private static final long serialVersionUID = 2215423070276994378L;

    private Long id;

    private String name;

    private LocalDateTime createDateTime;

}

這里,我們實(shí)現(xiàn)了 Serializable 接口垂寥。

在我們需要緩存的方法上颠黎,使用 @Cacheable 注解,就表示如果返回的對(duì)象不是 null 時(shí)滞项,就會(huì)對(duì)其進(jìn)行緩存狭归,下次查詢,首先會(huì)去緩存中查詢文判,查到了过椎,就直接返回,不會(huì)再去數(shù)據(jù)庫(kù)查詢戏仓,查不到疚宇,再去數(shù)據(jù)庫(kù)查詢。

@Service
@Slf4j
public class UserServiceImpl implements IUserService {

    @Override
    @Cacheable(
            value = "sample-redis",
            key = "'user-'+#id",
            unless = "#result == null"
    )
    public UserVo getUserById(Long id) {

        log.info("userVo from db query");

        UserVo userVo = new UserVo();
        userVo.setId(1L);
        userVo.setName("Zhang San");
        userVo.setCreateDateTime(LocalDateTime.now());

        return userVo;
    }

}

核心代碼:

@Cacheable(
        value = "sample-redis",
        key = "'user-'+#id",
        unless = "#result == null"
)

模擬測(cè)試赏殃,再寫(xiě)一個(gè)測(cè)試接口:

@RestController
@RequestMapping("/sample")
@RequiredArgsConstructor
@Slf4j
public class SampleController {

    private final IUserService userService;

    @GetMapping("/user/{id}")
    public UserVo getUserById(@PathVariable Long id) {

        UserVo vo = userService.getUserById(id);

        log.info("vo: {}", JacksonUtils.json(vo));

        return vo;
    }

}

我們?cè)偌由线B接 redis 的配置:

spring:
  data:
    redis:
      host: localhost
      port: 6379

測(cè)試:

### getUserById
GET http://localhost:8080/sample/user/1


image-20231229232659949.png

輸出結(jié)果跟我們想的一樣敷待,第一次從數(shù)據(jù)庫(kù)查,后面都從緩存直接返回嗓奢。

總結(jié)一下:

  1. 添加 spring-boot-starter-data-redis 依賴。

  2. 使用啟用緩存注解(@EnableCaching)浑厚。

  3. 需要緩存的對(duì)象實(shí)現(xiàn) Serializable 接口股耽。

  4. 使用 @Cacheable 注解緩存查詢的結(jié)果。

遇到問(wèn)題

在上面我們通過(guò) spring boot 提供的 redis 實(shí)現(xiàn)了查詢對(duì)象緩存這樣一個(gè)功能钳幅,有下面幾個(gè)問(wèn)題:

  1. 緩存的對(duì)象物蝙,必須序列化,不然會(huì)報(bào)錯(cuò)敢艰。
  2. redis 存儲(chǔ)的數(shù)據(jù)诬乞,看不懂,可以轉(zhuǎn)成 json 格式嗎?
  3. 使用 Jackson 時(shí)震嫉,遇到特殊類型的字段會(huì)報(bào)錯(cuò)森瘪,比如 LocalDateTime。

第1個(gè)問(wèn)題票堵,如果對(duì)象沒(méi)有實(shí)現(xiàn) Serializable接口扼睬,會(huì)報(bào)錯(cuò):

image-20231230230844373.png

關(guān)鍵信息:

java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [xxx.xxx.UserVo]

我詳細(xì)描述一下第3個(gè)問(wèn)題,默認(rèn)是使用 Jdk序列化 JdkSerializationRedisSerializer悴势,redis 里面存的數(shù)據(jù)如下:

image-20231229232824285.png

問(wèn)題很明顯窗宇,對(duì)象必須要實(shí)現(xiàn)序列化接口,存的數(shù)據(jù)不易查看特纤,所以军俊,改用 GenericJackson2JsonRedisSerializer ,這就有了第3個(gè)問(wèn)題捧存。

我們加上下面的配置粪躬,就能解決第2個(gè)問(wèn)題。

@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
    return RedisCacheConfiguration
            .defaultCacheConfig()
            .serializeValuesWith(
                    RedisSerializationContext
                            .SerializationPair
                            .fromSerializer(RedisSerializer.json())
            );
}

下面看第三個(gè)問(wèn)題的錯(cuò)誤:

image-20231229233716885.png

如何解決矗蕊?

既然有了明確的錯(cuò)誤提示短蜕,那也是好解決的,我們可以這樣:

@JsonDeserialize(using = LocalDateTimeDeserializer.class)       // 反序列化
@JsonSerialize(using = LocalDateTimeSerializer.class)           // 序列化
private LocalDateTime createDateTime;

這樣就可以了傻咖,我們看下redis里面存的數(shù)據(jù):

{"@class":"com.fengwenyi.erwin.component.sample.redis.vo.UserVo","id":1,"name":"Zhang San","createDateTime":[2023,12,29,23,44,3,479011000]}

其實(shí)到這里朋魔,已經(jīng)解決了問(wèn)題,那有沒(méi)有更省心的辦法呢卿操?

解決辦法

其實(shí)我們知道警检,使用的就是 Jackson 進(jìn)行 json 轉(zhuǎn)換,而 json 轉(zhuǎn)換害淤,遇到 LocalDateTime 問(wèn)題時(shí)扇雕,我們配置一下 module 就可以了,因?yàn)槟J(rèn)用的 SimpleModule窥摄,我們改用 JavaTimeModule 就可以了镶奉。

這時(shí)候問(wèn)題又來(lái)啦,錯(cuò)誤如下:

image-20231229233248619.png

這時(shí)候存的數(shù)據(jù)如下:

{"id":1,"name":"Zhang San","createDateTime":"2023-12-29T23:31:52.548517"}

這就涉及到 Jackson 序列化漏洞的問(wèn)題了崭放,采用了白名單機(jī)制哨苛,我們就粗暴一點(diǎn):

jsonMapper.activateDefaultTyping(
  LaissezFaireSubTypeValidator.instance, 
  ObjectMapper.DefaultTyping.NON_FINAL
);

redis 存的數(shù)據(jù)如下:

["com.fengwenyi.erwin.component.sample.redis.vo.UserVo",{"id":1,"name":"Zhang San","createDateTime":"2023-12-29T23:56:18.197203"}]

最后,來(lái)一段完整的 RedisCacheConfiguration 配置代碼:

@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
    return RedisCacheConfiguration
            .defaultCacheConfig()
            .serializeValuesWith(
                    RedisSerializationContext
                            .SerializationPair
//                            .fromSerializer(RedisSerializer.json())
//                            .fromSerializer(
//                                    new GenericJackson2JsonRedisSerializer()
//                            )
                            .fromSerializer(redisSerializer())
            );
}

private RedisSerializer<Object> redisSerializer() {
    JsonMapper jsonMapper = new JsonMapper();
    JacksonUtils.configure(jsonMapper);
    jsonMapper.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance, 
            ObjectMapper.DefaultTyping.NON_FINAL
    );
    return new GenericJackson2JsonRedisSerializer(jsonMapper);
}

希望今天的分享對(duì)你有一定的幫助币砂。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末建峭,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子决摧,更是在濱河造成了極大的恐慌亿蒸,老刑警劉巖凑兰,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異边锁,居然都是意外死亡砚蓬,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)物延,“玉大人仅父,你說(shuō)我怎么就攤上這事笙纤∈∪荩” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)洒放。 經(jīng)常有香客問(wèn)我滨砍,道長(zhǎng),這世上最難降的妖魔是什么领追? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任蔓腐,我火速辦了婚禮龄句,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘分歇。我一直安慰自己,他們只是感情好葬燎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著壕探,像睡著了一般李请。 火紅的嫁衣襯著肌膚如雪导盅。 梳的紋絲不亂的頭發(fā)上白翻,一...
    開(kāi)封第一講書(shū)人閱讀 51,754評(píng)論 1 307
  • 那天嘁字,我揣著相機(jī)與錄音杉畜,去河邊找鬼。 笑死此叠,一個(gè)胖子當(dāng)著我的面吹牛纯续,可吹牛的內(nèi)容都是我干的灭袁。 我是一名探鬼主播猬错,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼茸歧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了软瞎?” 一聲冷哼從身側(cè)響起逢唤,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拉讯,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后魔慷,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年伍茄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幻林。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沪饺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出整葡,到底是詐尸還是另有隱情件余,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布遭居,位于F島的核電站啼器,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏俱萍。R本人自食惡果不足惜端壳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枪蘑。 院中可真熱鬧损谦,春花似錦、人聲如沸岳颇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)话侧。三九已至栗精,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瞻鹏,已是汗流浹背悲立。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工赢赊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人级历。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像叭披,于是被迫代替她去往敵國(guó)和親寥殖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容