如何不改表結(jié)構(gòu)動態(tài)擴展字段?
痛點
軟件行業(yè)唯一不變的就是變化,比如功能上線之后,客戶或 PM 需要對已有的功能增加一些合理的需求,完成這些工作必須通過添加字段解決,或者某些功能的實現(xiàn)需要通過增加字段來降低實現(xiàn)的復雜性等等。
這些問題都會改動線上的數(shù)據(jù)庫表結(jié)構(gòu),一旦改動就會導致鎖表,會使所有的寫入操作一直等待,直到表鎖關(guān)閉,特別是對于數(shù)據(jù)量大的熱點表,添加一個字段可能會因為鎖表時間過長而導致部分請求超時,這可能會對企業(yè)間接造成經(jīng)濟上的損失。
解決方案
增加 json 格式的擴展字段。
下面配合一些代碼來描述這個解決方案,讀者便于去理解。另外,MySQL 系列面試題和答案全部整理好了,微信搜索Java技術(shù)棧,在后臺發(fā)送:面試,可以在線閱讀。
mysql 數(shù)據(jù)庫腳本:
- DROP TABLE IF EXISTS `cs_dustbin`;
- CREATE TABLE IF NOT EXISTS `cs_dustbin` (
- `id` VARCHAR(45) NOT NULL COMMENT '主鍵自增id',
- `rfid_no` VARCHAR(20) NOT NULL COMMENT 'rfid 卡號',
- `state` INT(1) NOT NULL COMMENT '垃圾桶狀態(tài):0:已注銷;1:未使用;2:待使用;3:已使用(綁定收集點);',
- `user_id` INT NOT NULL COMMENT '登記人,負責錄入垃圾桶的人',
- `type` INT(1) NOT NULL DEFAULT 1 COMMENT '垃圾桶類型:1:餐廚垃圾桶',
- `street_code` INT(11) DEFAULT NULL COMMENT '所在鎮(zhèn)街 code,根據(jù)狀態(tài),這里的含義可能是領用鎮(zhèn)街、退還鎮(zhèn)街。',
- `create_time` DATETIME NOT NULL DEFAULT now() COMMENT '創(chuàng)建時間',
- `update_time` DATETIME NOT NULL DEFAULT now() COMMENT '更新時間',
- `ext` VARCHAR(1000) NOT NULL DEFAULT '{}' COMMENT '擴展字段',
- ...
- PRIMARY KEY (`id`))
- ENGINE = InnoDB
- COMMENT = '垃圾桶表';
Java 代碼:
- import com.alibaba.fastjson.JSON;
- import lombok.Data;
- import javax.validation.constraints.NotNull;
- import java.util.Date;
- import java.util.List;
- /**
- * 垃圾桶實體
- * Created by Blink on 6/28/2018 AD.
- *
- * @author Blink
- */
- @Data
- public class Dustbin {
- private String id;
- /**
- * rfid 卡號
- */
- @NotNull
- private String rfidNo;
- /**
- * 垃圾桶狀態(tài):0:已注銷;1:未使用;2:待使用;3:已使用(綁定收集點);
- * 對應 Dustbin.StateEnum 類
- */
- @NotNull
- private Integer state;
- /**
- * 錄入垃圾桶的人員id
- */
- @NotNull
- private Long userId;
- /**
- * 垃圾桶類型:1:餐廚垃圾桶
- * DefaultValue: 1
- */
- @NotNull
- private Integer type;
- /**
- * 所在鎮(zhèn)街 code
- * 根據(jù)狀態(tài),這里的含義可能是領用鎮(zhèn)街、退還鎮(zhèn)街
- */
- private Integer streetCode;
- /**
- * 創(chuàng)建時間
- * defaultValue : now()
- */
- @NotNull
- private Date createTime;
- /**
- * 更新時間
- */
- @NotNull
- private Date updateTime;
- /**
- * 擴展字段,詳細數(shù)據(jù)查看 DustbinExt.java
- * DefaultValue: {}
- */
- private String ext;
- ...
- public DustbinExt getExtObject() {
- return JSON.parseObject(this.getExt(), DustbinExt.class);
- }
- public void setExtObject(DustbinExt ext) {
- this.ext = JSON.toJSONString(ext);
- }
- /**
- * 垃圾桶擴展屬性
- * Created by Blink on 6/28/2018 AD.
- *
- * @author Blink
- */
- @Data
- public static class DustbinExt {
- /**
- * 所在鎮(zhèn)街
- * 根據(jù)狀態(tài),這里的含義可能是領用鎮(zhèn)街、退還鎮(zhèn)街、綁定的鎮(zhèn)街
- */
- private String street;
- /**
- * 客戶(收集點)id,綁定收集點的時候需要填入
- * 根據(jù)目前的需求(2018-06-29),當收集點解綁的時候
- * 需要保存垃圾桶最新綁定收集點名稱,所以在解綁垃圾桶的時候不會把這個信息刪掉
- * 只有當綁定收集點的時候才把他覆蓋
- */
- private Long customerId;
- /**
- * 客戶(收集點)名稱,綁定收集點的時候需要填入
- * 根據(jù)目前的需求(2018-06-29),當收集點解綁的時候
- * 需要保存垃圾桶最新綁定收集點名稱,所以在解綁垃圾桶的時候不會把這個信息刪掉
- * 只有當綁定收集點的時候才把他覆蓋
- */
- private String customer;
- /**
- * 損壞部位
- * 1:桶蓋;2:桶口;3:桶身;4:桶軸;5:桶底;6:桶輪;
- * 對應 DustbinDamagePartEnum 類
- */
- private List<Integer> parts;
- }
- ...
- }
mysql 腳本可以看到擴展字段的信息:
- ext VARCHAR(1000) NOT NULL DEFAULT '{}' COMMENT '擴展字段'
可以看到這么一段 Java 代碼:
- ...
- /**
- * 擴展字段,詳細字段查看 DustbinExt 類
- * DefaultValue: {}
- */
- private String ext;
- public DustbinExt getExtObject() {
- return JSON.parseObject(this.getExt(), DustbinExt.class);
- }
- public void setExtObject(DustbinExt ext) {
- this.ext = JSON.toJSONString(ext);
- }
- ...
可以看到 ext 字段就是用來存儲 json 格式的數(shù)據(jù),它可以動態(tài)地增加任何字段,甚至是對象,不需要通過 DDL(Data Definition Language) 去創(chuàng)建字段,非常適合用來解決上面提到的問題。
Java 代碼在這里起到輔助性作用,通過定義一個內(nèi)部類來管理擴展字段的屬性,方便我們了解和管理擴展字段,提高代碼的可讀性和可維護性,java 這種方式也是筆者總結(jié)出來的較為優(yōu)雅的做法(個人觀點)。
局限性
有經(jīng)驗的讀者可能會提出,ext 字段在 Mysql 5.7.8 以下版本無法對擴展字段中的某一個或一部分字段建立索引,因為 Mysql 5.7.8 版本以下不支持(Mysql 5.7.8 支持為 Json Data Type 建立索引)。MySQL數(shù)據(jù)庫開發(fā)的 36 條軍規(guī)!
沒錯,這是這個解決方案的一個局限性,在 Mysql 5.7.8 以下版本,我的建議是, ext 擴展字段不要存儲熱點數(shù)據(jù),只存儲非熱點數(shù)據(jù),這樣就可以避免查詢操作,降低維護 ext 字段帶來的成本和風險,那如何識別新增字段是不是熱點數(shù)據(jù)呢?
這個需要結(jié)合實際業(yè)務需求來判斷,也可以詢問對業(yè)務和技術(shù)更有經(jīng)驗的同事,便于讀者更快得出結(jié)論。
終極版解決方案
在一些極端的情況下,變化可能來得太快,而我們要的是減少變化帶來的成本和風險,所以在表設計之初可以根據(jù)自身經(jīng)驗,或者找更有經(jīng)驗的人尋求幫助,預估一下需要預留多少個備用字段,再配合擴展字段,基本上可以把改變(添加字段)表結(jié)構(gòu)的次數(shù)降至一個非常少的次數(shù)。
總結(jié)
在特殊情況下,通過擴展字段 + 預留字段基本上可以做到動態(tài)擴展字段,又不會影響為熱點數(shù)據(jù)建立索引的情況,這樣我們得到了一個非常靈活的表結(jié)構(gòu),便于我們應對未來的變化,但是請注意,要維護好我們的實體,包括里面的每一個字段,敬畏每一行代碼。
最后,關(guān)注公眾號Java技術(shù)棧,在后臺回復:面試,可以獲取我整理的 MySQL 系列面試題和答案,非常齊全。