完美解決:應用版本更新,增加字段導致 Redis 舊數據反序列化報錯

完美解決:應用版本更新,增加字段導致 Redis 舊數據反序列化報錯

前言

在敏捷開發和快速迭代的今天,我們經常需要為現有的業務模型增加新的字段。但一個看似簡單的操作,卻可能給正在穩定運行的系統埋下“地雷”。

一個典型的場景是:我們的 Java 應用使用 Spring Data Redis 緩存對象,序列化方式為 JSON。當 V2 版本發布時,我們給 User 對象增加了一個 email 字段。部署新版本后,系統開始頻繁報錯,日志顯示在從 Redis 讀取舊的 User 數據時發生了反序列化異常。

這篇文章將深入剖析這個問題背后的原因,并提供在實際項目中行之有效的解決方案,無論你使用的是 Jackson 還是 Fastjson。

問題復現

假設我們的系統 V1 版本有這樣一個用戶類:

// V1 版本
public class User {private String name;private int age;// ... getters and setters
}

線上 Redis 緩存中存儲了大量序列化后的 User 對象,其 JSON 格式如下:

{"name": "Alice","age": 30
}

在 V2 版本中,我們為 User 類增加了一個 address 字段:

// V2 版本
public class User {private String name;private int age;private String address; // 新增字段// ... getters and setters
}

問題來了:當 V2 版本的應用啟動后,嘗試從 Redis 讀取 V1 版本存入的舊數據時,一切正常。但是,如果 V2 版本存入了一條新數據,而 V1 版本的(未下線的)服務嘗試讀取這條新數據時,就會立刻觸發致命錯誤!

V2 版本存入的數據:

{"name": "Bob","age": 25,"address": "123 Main St" // 新增字段
}

V1 版本的服務在讀取它時,會拋出類似這樣的異常:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "address" ...

這個錯誤會中斷業務邏輯,如果發生在核心流程上,甚至可能導致服務不可用。

為什么會報錯?深入 Jackson 的默認機制

在 Spring Boot 生態中,spring-boot-starter-data-redis 默認推薦使用 GenericJackson2JsonRedisSerializer 作為值的序列化器。它底層依賴于強大的 Jackson 庫。

問題的根源在于 Jackson 的一項默認安全特性

DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES

這個特性的默認值是 true。它意味著,當 Jackson 在反序列化一個 JSON 字符串時,如果在 JSON 中發現了目標 Java 類里不存在的屬性,它會認為這是一種潛在的錯誤或數據污染,并選擇立即拋出異常來提醒開發者。

這是一個“嚴格模式”的設計,旨在確保數據的精確匹配,防止意外的數據注入。但在版本迭代、字段只增不減的場景下,這個特性就成了我們需要解決的“麻煩”。

解決方案:配置你的 RedisTemplate

要解決這個問題,我們不能改變 Redis 中已存在的數據,只能讓我們的應用程序變得更加“寬容”和“健壯”,能夠向后兼容。

核心思路是:創建一個自定義配置的 ObjectMapper,關閉 FAIL_ON_UNKNOWN_PROPERTIES 特性,并將其應用到 RedisTemplate 中。

Spring Boot 配置實例

在你的配置類(如 RedisConfig.java)中,添加如下 Bean:

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);// --- 核心配置:創建自定義的 Jackson 序列化器 ---// 1. 創建 ObjectMapperObjectMapper objectMapper = new ObjectMapper();// 2. 配置 ObjectMapper:忽略在 JSON 中存在但 Java 對象中沒有的屬性objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);// 3. 注冊 Java 8 日期時間模塊,處理 LocalDateTime, LocalDate 等類型objectMapper.registerModule(new JavaTimeModule());// 4. 創建 GenericJackson2JsonRedisSerializerGenericJackson2JsonRedisSerializer jacksonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);// --- 設置 RedisTemplate 的序列化器 ---// Key 使用 String 序列化器template.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());// Value 使用我們自定義的 Jackson 序列化器template.setValueSerializer(jacksonSerializer);template.setHashValueSerializer(jacksonSerializer);template.afterPropertiesSet();return template;}
}

配置完成后,重啟你的應用。現在,即使應用讀取到包含未知字段的 JSON 數據,也不會再拋出異常,而是會優雅地忽略掉這些新字段,只解析它認識的字段。

如果我用的是 Fastjson 呢?

對于使用 Fastjson 的開發者來說,情況恰好相反。Fastjson 默認行為就非常“寬容”。

  • 當 JSON 字段比 Java 對象多時:Fastjson 默認會忽略未知字段,不會報錯。這正是我們期望的行為。
  • 當 Java 對象字段比 JSON 多時:和 Jackson 一樣,Fastjson 也不會報錯,缺失的字段會被賦予 null 或 Java 默認值。

下表總結了二者的核心區別:

不匹配情況Fastjson 默認行為Jackson 默認行為
JSON 字段 > Java 字段<br>(JSON 中有未知字段)忽略未知字段,不報錯拋出異常報錯
Java 字段 > JSON 字段<br>(JSON 中缺少字段)缺失字段賦予默認值不報錯缺失字段賦予默認值不報錯

如果你因為某些原因,希望 Fastjson 像 Jackson 一樣實行嚴格模式,可以在解析時傳入 Feature.FailOnUnmatchedProperties

?? 安全提醒:雖然 Fastjson 在此場景下行為友好,但其歷史上因 autoType 功能(@type)存在多個嚴重的安全漏洞。請務必使用最新版本,并絕對不要開啟 autoType,除非你完全了解其風險。

簡單的驗證過程

<dependencies><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>2.7.15</version> </dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.15.2</version> </dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>2.15.2</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId><version>2.15.2</version></dependency></dependencies>
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;import java.io.Serializable;
import java.util.Arrays;public class JacksonSerializerTest {// V1 版本的學生類static class StudentV1 implements Serializable {private String name;private int age;// 必須有無參構造函數public StudentV1() {}public StudentV1(String name, int age) {this.name = name;this.age = age;}// getters and setters...public String getName() { return name; }public void setName(String name) { this.name = name; }public int getAge() { return age; }public void setAge(int age) { this.age = age; }@Overridepublic String toString() {return "StudentV1{" + "name='" + name + '\'' + ", age=" + age + '}';}}// V2 版本的學生類(增加了 address 字段)static class StudentV2 implements Serializable {private String name;private int age;private String address; // 新增字段public StudentV2() {}// getters and setters...public String getName() { return name; }public void setName(String name) { this.name = name; }public int getAge() { return age; }public void setAge(int age) { this.age = age; }public String getAddress() { return address; }public void setAddress(String address) { this.address = address; }@Overridepublic String toString() {return "StudentV2{" + "name='" + name + '\'' + ", age=" + age + ", address='" + address + '\'' + '}';}}public static void main(String[] args) {// 創建默認的序列化器(FAIL_ON_UNKNOWN_PROPERTIES = true)GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();// 1. 模擬場景:新版代碼(V2)序列化,舊版代碼(V1)反序列化System.out.println("--- 場景1:JSON字段比Java對象多 (默認會報錯) ---");StudentV2 newStudent = new StudentV2();newStudent.setName("Charlie");newStudent.setAge(22);newStudent.setAddress("456 Park Ave");// 序列化 V2 對象byte[] serializedData = serializer.serialize(newStudent);System.out.println("V2對象序列化后的JSON: " + new String(serializedData));// 嘗試用 V1 的類去反序列化try {StudentV1 oldStudent = (StudentV1) serializer.deserialize(serializedData, StudentV1.class);System.out.println("反序列化成功: " + oldStudent);} catch (SerializationException e) {System.err.println("反序列化失敗,符合預期!錯誤: " + e.getCause().getMessage());}System.out.println("\n--- 場景2:JSON字段比Java對象少 (默認不報錯) ---");StudentV1 oldStudent = new StudentV1("David", 35);// 序列化 V1 對象byte[] oldSerializedData = serializer.serialize(oldStudent);System.out.println("V1對象序列化后的JSON: " + new String(oldSerializedData));// 嘗試用 V2 的類去反序列化try {StudentV2 studentWithNewField = (StudentV2) serializer.deserialize(oldSerializedData, StudentV2.class);System.out.println("反序列化成功,符合預期!結果: " + studentWithNewField);System.out.println("新增的 address 字段值為: " + studentWithNewField.getAddress());} catch (SerializationException e) {System.err.println("反序列化失敗: " + e.getMessage());}}
}

結論

在分布式和微服務架構中,保證不同版本服務之間的兼容性至關重要。由于增加字段而導致的反序列化失敗是一個常見但容易被忽視的問題。

最佳實踐是:

  1. 預見性地配置:在項目初期就為你的 RedisTemplate 配置一個“寬容模式”的 JSON 序列化器。
  2. 明確序列化策略:團隊內應統一 JSON 庫的選型和核心配置,避免因默認行為不一致導致問題。
  3. 擁抱兼容性設計:在設計數據模型時,應始終考慮未來的擴展性,盡量做到只增不減,并確保你的應用能夠優雅地處理新舊數據格式。

通過上述簡單的配置,你就可以讓你的應用在版本迭代中更加健壯,從容應對數據結構的變化。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/922069.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/922069.shtml
英文地址,請注明出處:http://en.pswp.cn/news/922069.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

66-python中的文件操作

1. 文件的編碼 UTF-8 GBK GB2312 Big5 GB18030 2. 文件讀取 文件操作步驟: 打開文件 讀\寫文件 關閉文件 open(name,mode,encoding) name:文件名字符串 “D:/haha.txt” mode: 只讀、寫入、追加 r:以只讀方式打開 w: 只用于寫 a :用于追加 encoding:編碼方式 # -*- coding: utf…

FPGA實例源代碼集錦:27個實戰項目

本文還有配套的精品資源&#xff0c;點擊獲取 簡介&#xff1a;FPGA是一種可編程邏輯器件&#xff0c;允許用戶根據需求配置硬件功能。本壓縮包提供27個不同的FPGA應用實例源代碼&#xff0c;旨在幫助初學者深入學習FPGA設計&#xff0c;并為專業工程師提供靈感。內容涵蓋了…

基于 Vue+Mapbox 的智慧礦山可視化功能的技術拆解

01、項目背景 在全球礦業加速向 “高端化、智能化、綠色化” 轉型的浪潮下&#xff0c;傳統礦業面臨的深地開采難題、效率瓶頸與安全隱患日益凸顯。 在礦業轉型的迫切需求與政策、技術支撐的背景下依托 GIS 技術&#xff0c;開展了 “中國智礦” GIS 開發項目&#xff0c;旨在…

進程狀態(Linux)

進程狀態Linux進程狀態Linux進程狀態進程描述R運行狀態S睡眠狀態D磁盤休眠狀態T停止狀態t被追蹤狀態(調試狀態)X死亡狀態Z僵死狀態其實大致也就可以分為三種運行&#xff0c;阻塞&#xff0c;掛起。運行狀態每個cpu里都有一個運行隊列&#xff0c;進程在運行隊列里&#xff0c;…

物聯網領域中PHP框架的最佳選擇有哪些?

物聯網&#xff08;IoT&#xff09;作為近年來快速發展的技術領域&#xff0c;已經滲透到智能家居、工業自動化、智慧城市等方方面面。作為Web開發中廣泛使用的語言&#xff0c;PHP憑借其易學易用、開發效率高和生態豐富的特點&#xff0c;也在物聯網領域找到了用武之地。 本文…

java反射(詳細教程)

我們平常創建類的實例并調用類中成員需要建立在一個前提下&#xff0c;就是已經知道類名和類中成員的信息&#xff0c;靈活性大大降低。甚至在一些項目中還需要修改源碼來滿足使用條件&#xff0c;大大降低了操作的靈活性。Java 反射&#xff08;Reflection&#xff09;是 Java…

消息隊列-初識kafka

優缺點 消息隊列的優點&#xff1a; 實現系統解耦&#xff1a; :::color5 系統解耦解釋 有 MQ 時是 “服務 A 發消息到隊列&#xff0c;其他服務從隊列拿消息&#xff0c;新增服務接隊列就行”&#xff1b;無 MQ 時是 “服務 A 直接調其他服務的接口 / 依賴&#xff0c;新增 / …

實踐《數字圖像處理》之Canny邊緣檢測、霍夫變換與主動二值化處理在短線段清除應用中的實踐

在最近的圖像處理項目中&#xff0c;其中一個環節&#xff1a;圖片中大量短線&#xff08;不是噪聲&#xff09;&#xff0c;需要在下一步處理前進行清除。在確定具體實現時&#xff0c;碰到了Canny邊緣檢測、霍夫變換與主動二值化處理的辯證使用&#xff0c;相關邏輯從圖片灰度…

vue3與ue5通信-工具類

工具 ue5-simple.js /*** UE5 通信工具* 兩個核心方法&#xff1a;發送消息和接收消息*/// 確保全局對象存在 if (typeof window ! undefined) {window.ue window.ue || {};window.ue.interface window.ue.interface || {}; }/*** 生成 UUID*/ function generateUUID() {retu…

在kotlin中如何使用像java中的static

在 Kotlin 中&#xff0c;沒有直接的 static 關鍵字&#xff0c;但有幾種等效的方式來實現 Java 中靜態成員的功能&#xff1a; 1. 伴生對象 (Companion Object) - 最常用 class MyClass {companion object {// 靜態常量const val STATIC_CONSTANT "constant value"…

如何在 Spring Boot 中指定不同的配置文件?

介紹 Spring Boot 提供了多種方式來管理和加載配置文件&#xff0c;特別是在多環境配置下&#xff0c;比如開發、測試和生產環境。通過指定不同的配置文件&#xff0c;可以靈活地調整應用程序的行為&#xff0c;以適應不同的需求。本文將介紹在 Spring Boot 中如何指定使用不同…

在centOS源碼編譯方式安裝MySQL5.7

一、前言 在生產環境中部署數據庫時&#xff0c;很多人會選擇直接使用 yum/apt 包管理器 安裝 MySQL&#xff0c;這樣簡單快速&#xff0c;但缺點是版本受限&#xff0c;靈活性不足。對于需要指定版本、啟用特定編譯參數或優化的場景&#xff0c;源碼編譯安裝 MySQL 就顯得非常…

探討Hyperband 等主要機器學習調優方法的機制和權衡

本篇文章Master Hyperband — An Efficient Hyperparameter Tuning Method in Machine Learning深入探討了Hyperband這一高效的超參數調優方法。文章的技術亮點在于其結合了多臂老虎機策略和逐次減半算法&#xff0c;能夠在大搜索空間中快速剔除表現不佳的配置&#xff0c;從而…

Mysql:InnoDB 關鍵特性

目錄 一、插入緩沖&#xff08;Change Buffer&#xff09;→ 快遞驛站的 “臨時存放區” 二、兩次寫&#xff08;Double Write&#xff09;→ 重要文件的 “備份存檔” 三、自適應哈希索引&#xff08;AHI&#xff09;→ 圖書館的 “熱門書快捷查找區” 四、異步 IO&#x…

STM32-----SPI

SPI簡介SCK:和I2C中SCL的時鐘線一個作用&#xff0c;都是在高電平拿出數據&#xff0c;在低電平寫數據MOSI:主機輸出從機輸入MISO:主機輸入從機輸出&#xff0c;只有當對應從機的SS為低電平&#xff0c;從機的MISO引腳才能設置推挽輸出&#xff0c;當從機SS為高電平時&#xff…

華為考試:HCIE數通考試難度分析

隨著信息技術的飛速發展&#xff0c;網絡技術已成為支撐各行各業運轉的重要基礎&#xff0c;市場對高水平網絡技術人才的需求持續增長。HCIE作為華為認證體系中的最高級別認證&#xff0c;代表了網絡技術領域的專業頂尖水平。本文將對HCIE數通認證的考試內容、難度及備考策略進…

一些常用的激活函數及繪圖

深度網絡的一些常用激活函數&#xff0c;并通過matplot繪制出來&#xff1a; import matplotlib.pyplot as plt import numpy as npdef relu(x):return np.maximum(0, x)def leaky_relu(x, alpha0.01):return np.where(x > 0, x, alpha * x)def gelu(x):return 0.5 * x * (1…

AE蘋果手機iPhone 17展示動畫片頭模板 App Promo Phone 17 Pro

專為 App 發布會、電商促銷、新品宣傳 打造的 iPhone 17 Pro 動畫展示 AE 模板。 4K 超清分辨率 26 張可替換照片位&#xff0c;無需第三方插件&#xff0c;拖拽即可輸出專業級手機宣傳片。 核心亮點 4K 超清&#xff1a;38402160 分辨率&#xff0c;大屏投放與社媒高清壓縮無…

基于Python的云原生TodoList Demo 項目,驗證云原生核心特性

以下是一個基于 Python 的云原生 TodoList Demo 項目&#xff0c;涵蓋 容器化、Kubernetes 編排、CI/CD、可觀測性、彈性擴縮容 等核心云原生特性&#xff0c;代碼簡潔且附詳細操作指南&#xff0c;適合入門學習。項目概覽 目標&#xff1a;實現一個支持增刪改查&#xff08;CR…

go 日志的分裝和使用 Zap + lumberjack

自帶的log無法滿足 按大小輪轉 &#xff0c;按天數清理舊日志 &#xff0c;自動壓縮 &#xff0c;限制備份數量 &#xff0c;防止磁盤寫滿 &#xff0c;生產環境推薦 等 使用 Zap lumberjack package mainimport ("go.uber.org/zap""go.uber.org/zap/zapcore&q…