一、問題說明
公司開發人員在更新資料時使用了 replace into 語句,由於使用不當導致了資料的大量丟失,到底是如何導致的資料丟失?現分析如下。
二、問題分析
a. REPLACE 原理
REPLACE INTO 原理的官方解釋為:
REPLACE works exactly like INSERT, except that if an old row in the table has the same value as a new row for a PRIMARY KEY or a UNIQUE index, the old row is deleted before the new row is inserted.
如果新插入行的主鍵或唯一鍵在表中已經存在,則會刪除原有記錄並插入新行;如果在表中不存在,則直接插入
地址:https://dev.mysql.com/doc/refman/5.6/en/replace.html
b. 問題現象
丟失資料的表結構如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
CREATE TABLE `active_items` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `ad_id` char(32) NOT NULL DEFAULT '' COMMENT 'XXX', `ap_id` tinyint(4) NOT NULL DEFAULT '1' COMMENT 'XXX', `price` bigint(20) NOT NULL DEFAULT '0' COMMENT 'xxx', `rate` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `cc_price` bigint(20) NOT NULL DEFAULT '0' COMMENT 'XXX', `sale_count` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `prom_count` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `c1` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'XXX', `order_num` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `score` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `audit_time` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `prom_modify_time` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `sales_modify_time` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `create_time` int(11) NOT NULL DEFAULT '0' COMMENT '建立時間', `modify_time` int(11) NOT NULL DEFAULT '0' COMMENT '最後修改時間', `score_modify_time` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `brand_id` int(11) NOT NULL DEFAULT '0' COMMENT 'XXX', `freight_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'XXX', `flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'XXX', PRIMARY KEY (`id`), UNIQUE KEY `idx_ad_id` (`ad_id`) USING BTREE, ... ) ENGINE=InnoDB AUTO_INCREMENT=2699 DEFAULT CHARSET=utf8 COMMENT='XXXXX' |
執行的replace語句如下(多條):
1 2 |
REPLACE INTO active_items(ad_id,score) VALUES('XXXXXXX', 1800); |
通過查詢binlog找到執行記錄,部分如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
### UPDATE `items`.`active_items` ### WHERE ### @1=21926 /* LONGINT meta=0 nullable=0 is_null=0 */ ### @2='XXXXXXX' /* STRING(96) meta=65120 nullable=0 is_null=0 */ ### @3=1 /* TINYINT meta=0 nullable=0 is_null=0 */ ### @4=3900 /* LONGINT meta=0 nullable=0 is_null=0 */ ### @5=315 /* INT meta=0 nullable=0 is_null=0 */ ### @6=1228 /* LONGINT meta=0 nullable=0 is_null=0 */ ### @7=19 /* INT meta=0 nullable=0 is_null=0 */ ### @8=0 /* INT meta=0 nullable=0 is_null=0 */ ### @9=-6 (4294967290) /* INT meta=0 nullable=0 is_null=0 */ ### @10=0 /* TINYINT meta=0 nullable=0 is_null=0 */ ### @11=0 /* INT meta=0 nullable=0 is_null=0 */ ### @12=8 /* INT meta=0 nullable=0 is_null=0 */ ### @13=1489997272 /* INT meta=0 nullable=0 is_null=0 */ ### @14=1495765800 /* INT meta=0 nullable=0 is_null=0 */ ### @15=1495728000 /* INT meta=0 nullable=0 is_null=0 */ ### @16=1489996426 /* INT meta=0 nullable=0 is_null=0 */ ### @17=1489997272 /* INT meta=0 nullable=0 is_null=0 */ ### @18=1495728725 /* INT meta=0 nullable=0 is_null=0 */ ### @19=0 /* INT meta=0 nullable=0 is_null=0 */ ### @20=1 /* TINYINT meta=0 nullable=0 is_null=0 */ ### @21=0 /* TINYINT meta=0 nullable=0 is_null=0 */ ### SET ### @1=35313 /* LONGINT meta=0 nullable=0 is_null=0 */ ### @2='XXXXXXX' /* STRING(96) meta=65120 nullable=0 is_null=0 */ ### @3=1 /* TINYINT meta=0 nullable=0 is_null=0 */ ### @4=0 /* LONGINT meta=0 nullable=0 is_null=0 */ ### @5=0 /* INT meta=0 nullable=0 is_null=0 */ ### @6=0 /* LONGINT meta=0 nullable=0 is_null=0 */ ### @7=0 /* INT meta=0 nullable=0 is_null=0 */ ### @8=0 /* INT meta=0 nullable=0 is_null=0 */ ### @9=0 /* INT meta=0 nullable=0 is_null=0 */ ### @10=0 /* TINYINT meta=0 nullable=0 is_null=0 */ ### @11=0 /* INT meta=0 nullable=0 is_null=0 */ ### @12=3150 /* INT meta=0 nullable=0 is_null=0 */ ### @13=0 /* INT meta=0 nullable=0 is_null=0 */ ### @14=0 /* INT meta=0 nullable=0 is_null=0 */ ### @15=0 /* INT meta=0 nullable=0 is_null=0 */ ### @16=0 /* INT meta=0 nullable=0 is_null=0 */ ### @17=0 /* INT meta=0 nullable=0 is_null=0 */ ### @18=0 /* INT meta=0 nullable=0 is_null=0 */ ### @19=0 /* INT meta=0 nullable=0 is_null=0 */ ### @20=0 /* TINYINT meta=0 nullable=0 is_null=0 */ ### @21=0 /* TINYINT meta=0 nullable=0 is_null=0 */ |
- 操作的ad_id已經存在,因此先刪除後插入,可以看到除了指定的 ad_id,score,其他欄位都變為預設值,導致原有資料丟失(雖然在日誌中轉為了update)
c. 對比測試
接下來我進行了如下測試:
- 左側使用
REPLACE
語句,右側使用DELETE
+INSERT
語句,最後結果完全相同 - 原主鍵id為1的行被刪除,新插入行主鍵id更新為4,沒有指定內容的欄位c則插入了預設值
- 使用
REPLACE
更新了一行資料,MySQL提示受影響行數為2行 - 綜上所述,說明確實是刪除一行,插入一行
三、資料恢復
資料丟失或資料錯誤後,可以有如下幾種方式恢復:
- 業務方自己寫指令碼恢復
- 通過MySQL的binlog查出誤操作sql,生成反向sql進行資料恢復(適合sql資料量較小的情況)
- 通過歷史備份檔案+增量binlog將資料狀態恢復到誤操作的前一刻
四、問題擴充套件
通過上述分析可以發現,REPLACE
會刪除舊行並插入新行,但是binlog中是以update形式記錄,這樣就帶來另一個問題:
從庫自增長值小於主庫
1. 測試
a. 主從一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
主庫: mysql> show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT '0', `b` int(11) DEFAULT '0', `c` int(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `a` (`a`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) 從庫: mysql> show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT '0', `b` int(11) DEFAULT '0', `c` int(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `a` (`a`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) |
b. 主庫REPLACE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
主庫: mysql> replace into t (a,b)values(1,7); Query OK, 2 rows affected (0.01 sec) mysql> show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT '0', `b` int(11) DEFAULT '0', `c` int(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `a` (`a`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) 從庫: mysql> show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT '0', `b` int(11) DEFAULT '0', `c` int(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `a` (`a`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) |
- 注意此時主從兩個表的AUTO_INCREMENT值已經不同了
c. 模擬從升主,在從庫進行INSERT:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
mysql> insert into t (a,b,c)values(4,4,4); ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY' mysql> show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT '0', `b` int(11) DEFAULT '0', `c` int(11) DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `a` (`a`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) mysql> insert into t (a,b,c)values(4,4,4); Query OK, 1 row affected (0.00 sec) |
- 從庫插入時會報錯,主鍵重複,報錯後AUTO_INCREMENT會 +1,因此再次執行就可以成功插入
2. 結論
這個問題在平時不會有絲毫影響,但是:
如果主庫平時大量使用
REPLACE
語句,造成從庫AUTO_INCREMENT
值落後主庫太大,當主從發生切換後,再次插入資料時新的主庫就會出現大量主鍵重複報錯,導致資料無法插入。
3. 參考文章
http://www.cnblogs.com/monian/archive/2014/10/09/4013784.html