MySQL的這個bug,坑了多少人?

Java圈子發表於2020-04-06

​1、問題描述

近期,線上有個重要Mysql客戶的表在從5.6升級到5.7後,master上插入過程中出現"Duplicate key"的錯誤,而且是在主備及RO例項上都出現。

以其中一個表為例,遷移前通過“show create table” 命令檢視的auto increment id為1758609, 遷移後變成了1758598,實際對遷移生成的新表的自增列用max求最大值為1758609。

使用者採用的是Innodb引擎,而且據運維同學介紹,之前碰到過類似問題,重啟即可恢復正常。

2、核心問題排查

由於使用者反饋在5.6上訪問正常,切換到5.7後就報錯。因此,首先得懷疑是5.7核心出了問題,因此第一反應是從官方bug list中搜尋一下是否有類似問題存在,避免重複造車。經過搜尋,發現官方有1個類似的bug,這裡簡單介紹一下該bug。

背景知識1

Innodb引擎中的auto increment 相關引數及資料結構

主要引數包括:innodb_autoinc_lock_mode用於控制獲取自增值的加鎖方式,auto_increment_increment, auto_increment_offset用於控制自增列的遞增的間隔和起始偏移。

主要涉及的結構體包括:資料字典結構體,儲存整個表的當前auto increment值以及保護鎖;事務結構體,儲存事務內部處理的行數;handler結構體,儲存事務內部多行的迴圈迭代資訊。

背景知識2

mysql及Innodb引擎中對autoincrement訪問及修改的流程

(1) 資料字典結構體(dict_table_t)換入換出時對autoincrement值的儲存和恢復。換出時將autoincrement儲存在全域性的的對映表中,然後淘汰記憶體中的dict_table_t。換入時通過查詢全域性對映表恢復到dict_table_t結構體中。相關的函式為dict_table_add_to_cache及dict_table_remove_from_cache_low。

(2) row_import, table truncate過程更新autoincrement。

(3) handler首次open的時候,會查詢當前表中最大自增列的值,並用最大列的值加1來初始化表的data_dict_t結構體中的autoinc的值。

(4) insert流程。相關對autoinc修改的堆疊如下:

ha_innobase::write_row:write_row的第三步中呼叫handler控制程式碼中的update_auto_increment函式更新auto increment的值
    handler::update_auto_increment: 呼叫Innodb介面獲取一個自增值,並根據當前的auto_increment相關變數的值調整獲取的自增值;同時設定當前handler要處理的下一個自增列的值。
        ha_innobase::get_auto_increment:獲取dict_tabel中的當前auto increment值,並根據全域性引數更新下一個auto increment的值到資料字典中
            ha_innobase::dict_table_autoinc_initialize:更新auto increment的值,如果指定的值比當前的值大,則更新。
        handler::set_next_insert_id:設定當前事務中下一個要處理的行的自增列的值。複製程式碼

(5) update_row。對於”INSERT INTO t (c1,c2) VALUES(x,y) ON DUPLICATE KEY UPDATE”語句,無論唯一索引列所指向的行是否存在,都需要推進auto increment的值。

相關程式碼如下:

    if (error == DB_SUCCESS
        && table->next_number_field
        && new_row == table->record[0]
        && thd_sql_command(m_user_thd) == SQLCOM_INSERT
        && trx->duplicates)  {
        ulonglong    auto_inc;
                ……
        auto_inc = table->next_number_field->val_int();
        auto_inc = innobase_next_autoinc(auto_inc, 1, increment, offset, col_max_value);
            error = innobase_set_max_autoinc(auto_inc);
                ……
    }複製程式碼

從我們的實際業務流程來看,我們的錯誤只可能涉及insert及update流程。

BUG 76872 / 88321: "InnoDB AUTO_INCREMENT produces same value twice"

(1) bug概述:當autoinc_lock_mode大於0,且auto_increment_increment大於1時,系統剛重啟後多執行緒同時對錶進行insert操作會產生“duplicate key”的錯誤。

(2) 原因分析:重啟後innodb會把autoincrement的值設定為max(id) + 1。此時,首次插入時,write_row流程會呼叫handler::update_auto_increment來設定autoinc相關的資訊。

首先通過ha_innobase::get_auto_increment獲取當前的autoincrement的值(即max(id) + 1),並根據autoincrement相關引數修改下一個autoincrement的值為next_id。當auto_increment_increment大於1時,max(id) + 1 會不大於next_id。

handler::update_auto_increment獲取到引擎層返回的值後為了防止有可能某些引擎計算自增值時沒有考慮到當前auto increment引數,會重新根據引數計算一遍當前行的自增值,由於Innodb內部是考慮了全域性引數的,因此handle層對Innodb返回的自增id算出的自增值也為next_id,即將會插入一條自增id為next_id的行。

handler層會在write_row結束的時候根據當前行的值next_id設定下一個autoincrement值。如果在write_row尚未設定表的下一個autoincrement期間,有另外一個執行緒也在進行插入流程,那麼它獲取到的自增值將也是next_id。這樣就產生了重複。

(3) 解決辦法:引擎內部獲取自增列時考慮全域性autoincrement引數,這樣重啟後第一個插入執行緒獲取的自增值就不是max(id) + 1,而是next_id,然後根據next_id設定下一個autoincrement的值。由於這個過程是加鎖保護的,其他執行緒再獲取autoincrement的時候就不會獲取到重複的值。

通過上述分析,這個bug僅在autoinc_lock_mode > 0 並且auto_increment_increment > 1的情況下會發生。實際線上業務對這兩個引數都設定為1,因此,可以排除這個bug造成線上問題的可能性。

3、現場分析及復現驗證

既然官方bug未能解決我們的問題,那就得自食其力,從錯誤現象開始分析了。

(1) 分析max id及autoincrement的規律 由於使用者的表設定了ON UPDATE CURRENT_TIMESTAMP列,因此可以把所有的出錯的表的max id、autoincrement及最近更新的幾條記錄抓取出來,看看是否有什麼規律。抓取的資訊如下:

file

乍看起來,這個錯誤還是很有規律的,update time這一列是最後插入或者修改的時間,結合auto increment及max id的值,現象很像是最後一批事務只更新了行的自增id,沒有更新auto increment的值。

聯想到官方文件中對auto increment用法的介紹,update操作是可以只更新自增id但不觸發auto increment推進的。按照這個思路,我嘗試復現了使用者的現場。復現方法如下:

file

同時在binlog中,我們也看到有update自增列的操作。如圖:

file

不過,由於binlog是ROW格式,我們也無法判斷這是核心出問題導致了自增列的變化還是使用者自己更新所致。因此我們聯絡了客戶進行確認,結果使用者很確定沒有進行更新自增列的操作。那麼這些自增列到底是怎麼來的呢?

(2) 分析使用者的表及sql語句 繼續分析,發現使用者總共有三種型別的表(hz_notice_stat_sharding, hz_notice_group_stat_sharding,hz_freeze_balance_sharding),這三種表都有自增主鍵。

但是前面兩種都出現了autoinc錯誤,唯獨hz_freeze_balance_sharding表沒有出錯。難道是使用者對這兩種表的訪問方式不一樣?

抓取使用者的sql語句,果然,前兩種表用的都是replace into操作,最後一種表用的是update操作。難道是replace into語句導致的問題?

搜尋官方bug, 又發現了一個疑似bug。

bug #87861: “Replace into causes master/slave have different auto_increment offset values”

原因:

(1) Mysql對於replace into實際是通過delete + insert語句實現,但是在ROW binlog格式下,會向binlog記錄update型別日誌。Insert語句會同步更新autoincrement,update則不會。

(2) replace into在Master上按照delete+insert方式操作, autoincrement就是正常的。基於ROW格式複製到slave後,slave機上按照update操作回放,只更新行中自增鍵的值,不會更新autoincrement。因此在slave機上就會出現max(id)大於autoincrement的情況。此時在ROW模式下對於insert操作binlog記錄了所有的列的值,在slave上回放時並不會重新分配自增id,因此不會報錯。但是如果slave切master,遇到Insert操作就會出現”Duplicate key”的錯誤。

(3) 由於使用者是從5.6遷移到5.7,然後直接在5.7上進行插入操作,相當於是slave切主,因此會報錯。

4、解決方案

業務側的可能解決方案:

(1) binlog改為mixed或者statement格式

(2) 用Insert on duplicate key update代替replace into

核心側可能解決方案:

(1) 在ROW格式下如果遇到replace into語句,則記錄statement格式的logevent,將原始語句記錄到binlog。

(2) 在ROW格式下將replace into語句的logevent記錄為一個delete event和一個insert event。

5、心得

(1) autoincrement的autoinc_lock_mode及auto_increment_increment這兩個引數變化容易導致出現重複的key,使用過程中要儘量避免動態的去修改。

(2) 在碰到線上的問題時,首先應該做好現場分析,明確故障發生的場景、使用者的SQL語句、故障發生的範圍等資訊,同時要對涉及例項的配置資訊、binlog甚至例項資料等做好備份以防過期丟失。只有這樣才能在找官方bug時精準的匹配場景,如果官方沒有相關bug,也能通過已有線索獨立分析。

原文連結: cloud.tencent.com/developer/a…

文源網路,僅供學習之用,如有侵權,聯絡刪除。

我將優質的技術文章和經驗總結都彙集在了我的公眾號【Java圈子】裡。

為方便大家學習,我整理了一套學習資料,涵蓋Java虛擬機器、spring框架、Java執行緒、資料結構、設計模式等等,免費提供給熱愛Java的同學! 更有學習交流群,多交流問題才能更快進步~

file

相關文章