數據庫系列:高並發下的數據字段變更


1 背景

經常會遇到這種情況,我們的業務已經穩定地運行一段時間了,並且流量漸漸已經上去了。這時候,卻因為某些原因(比如功能調整或者業務擴展),你需要對數據表進行調整,加字段 or 修改表結構。
可能很多人說 alter table add column ... / alter table modify ...,輕輕松松就解決了。 這樣其實是有風險的
對於復雜度比較高、數據量比較大的表。調整表結構、創建或刪除索引、觸發器,都可能引起鎖表,而鎖表的時長依你的數據表實際情況而定。 本人有過慘痛的教訓,在一次業務上線過程中沒有評估好數據規模,導致長時間業務數據寫入不進來。
那么有什么辦法對數據庫的業務表進行無縫升級,讓該表對用戶透明無感呢?下面我們一個個來討論。

2 新增關聯表

最簡單的一種辦法,把新增的字段存儲在另外一張輔表上,用外鍵關聯到主表的主鍵。達到動態擴展的目標。后續功能上線之后,新增的數據會存儲到輔表中,主表無需調整,透明、無損。
image
存在的問題:

  • 讀取數據時,聯表查詢效率低下,數據量越大,數據越復雜,劣勢越明顯。
  • 並沒有徹底的解決問題,之后有新增字段,照樣面臨是新增表還是修改原表的問題。即使后續新增的字段都加在輔表上,同樣面臨鎖表的問題。
  • 輔表的作用僅僅是解決字段新增的問題,並未解決字段更新的問題(如修改字段名、數據類型等)。

3 新增通用列

假設我們原有表結構如下,為了保障業務的持續發展,后續不間斷的會有字段擴展。這時候就需要考慮增加一個可自動擴縮的通用字段。
image
以MySQL為例子,5.7版本版本之后提供了Json字段類型,方便我們存儲復雜的Json對象數據。

use test;
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE "t_user" (
  "id" bigint(20) NOT NULL AUTO_INCREMENT,
  "name" varchar(20) NOT NULL,
  "age" int(11) DEFAULT NULL,
  "address" varchar(255) DEFAULT NULL,
  "sex" int(11) DEFAULT '1',
  "ext_data" json DEFAULT NULL COMMENT 'json字符串',
  PRIMARY KEY ("id")
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES ('1', 'brand', '21', 'fuzhou', '1', '{"tel": "13212345678", "name": "brand", "address": "fuzhou"}');

代碼中 ext_data 采用Json數據類型,是一種可擴展的對象載體,存放被查詢數據的信息補充。
同樣的,MySQL提供的這種數據類型,也提供了很強大的Json函數進行操作。

SELECT id,`name`,age,address FROM `t_user` WHERE json_extract(ext_data,'$.tel') = '13212345678';

結果如下:
image

之前寫MySQL系列的時候,博客園的一位讀者留言要我歸納一下MySQL Json 的用法,一直沒時間,大家可以看一下官網的文檔,還是比較清晰的。

Json結構一般來說是向下兼容的,所以你在設計字段擴展的時候,一般建議往前增,不建議刪除舊屬性。但是這也有個問題,就是業務越復雜,Json復雜度也越高,冗余屬性也越多。
比如上文中我們的json包含三個屬性,tel、name、address,之后的業務調整中,發現tel沒用了,加了個age屬性,那tel要不要刪除?
有一種比較好的辦法,是給表加上version屬性,每個時期的業務對應一個version,每個version對應的Json數據結構也不一樣。
image

優點:

  • 可以隨時動態擴展屬性
  • 新舊兩種數據可以同時存在
  • 遷移數據方便,寫個程序將舊版本ext的改為新版本的ext,並修改version

不足:

  • ext_data里的字段無法建立索引
  • ext_data里的key會有大量空間占用,建議key簡短一些
  • 從json中去統計某個字段數據之類的很麻煩,而且效率低
  • 查詢相對效率較低,操作復雜。
  • 更新Json中的某個字段效率較低,不適合存儲業務邏輯復雜的數據。
  • 統計數據復雜,建議需要做報表的數據不要存json。

改進:

  • 如果ext里的屬性有索引之類的需求,可能NoSql(如MongoDB)會更適合

4 新表+數據遷移

4.1 利用觸發器進行數據遷移

image
整個步驟如下:

  • 新建一個表t_user_v1 (id, name, age, address, sex, ext_column),包含了擴展字段 ext_column
  • 原有表上添加觸發器,原表的DML操作(主要INSERT、UPDATE、DELETE),都會觸發操作,把數據轉存到新表t_user_v1中
  • 對於舊表中原有的數據,逐步的遷移直至完成
  • 刪掉觸發器,把原表移走(默認是drop掉)
  • 把新表t_user_v1重命名(rename)成原表t_user
    通過上述步驟,逐漸的將數據遷移到新表,並替換舊表,整個操作無需停服維護,對用業務無損

4.2 利用Binlog 進行數據遷移

如果是MySQL數據庫,可以通過復制binlog的操作進行數據遷移的,效果一樣,比起觸發器,更穩定一點。
image

4.3 存在的問題

  • 操作繁瑣,效率低下
  • 數據遷移和數據表切換之間存在操作間隙,對於高並發、高頻操作的數據表,還是有風險的,會引起短暫連接失效 和 數據不一致。
  • 對於大數據表,同步時間長

5 字段預留

預留字段 和 字段與表格名稱映射的辦法。
image

5.1 存在的問題

  • 同樣的,查詢效率低
  • 預設存在未知數,可能存在預設的字段不夠,也可能存在空間冗余
  • 冗余過多的空子字段,對存儲空間的占用和性能的提升存在阻礙
  • 該方法還是比較笨的,不適合程序員思維

6 多主模式和分級更新

如果業務流量比較小,可以直接在表上進行字段新增或者修改,短暫的寫鎖是可以承受的。但如果是高並發、集群化、分布式的系統,則從數據層面上就應該進行主從或者分庫分表治理。
以下是典型的的多主要模式下,進行數據庫表結構升級的過程。
image

  1. 正常兩主模式下,主主同步,可以使用DBproxy、Fabric 等數據中間件做負載均衡,也可以自己定義一些負載策略,比如 Range、Hash。
  2. 修改配置,讓流量都切到其中一台上,然后對另外一台進行數據表升級(比如切DB1,只使用DB2)。切記在業務低峰期進行,避免流量過大導致另外一個數據庫實例負載過大而掛起。
  3. 輪流這個操作,但是這時候不需要再升級DB2了,因為是主主同步。DB instance 1 已經是新的表結構了,這時候會連同架構包括數據一起更新到 DB2 上。
  4. 等兩個數據庫實例都一致了,修改配置,重設兩個數據庫實例的負載,恢復到之前的狀態。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM