一入職,就遇到 MySQL 這麼大 Bug!差點背鍋走人...

今年這種狀況,有時候不找好下家還真不敢跳,這不,前段時間剛跳到新東家,剛辦入職那天,就趕上事了,真的是嚇出一身冷汗(老大一直盯着我,說要快速解決這個問題),差點被(背)開(鍋)了....mysql

狀況如何?且聽我下面慢慢道來!!!但願對你們有所幫助與借鑑。sql

問題描述

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

以其中一個表爲例,遷移前經過「show create table」 命令查看的auto increment id爲1758609, 遷移後變成了1758598,實際對遷移生成的新表的自增列用max求最大值爲1758609。數據結構

用戶採用的是Innodb引擎,並且據運維同窗介紹,以前碰到過相似問題,重啓便可恢復正常。多線程

內核問題排查

因爲用戶反饋在5.6上訪問正常,切換到5.7後就報錯。所以,首先得懷疑是5.7內核出了問題,所以第一反應是從官方bug list中搜索一下是否有相似問題存在,避免重複造車。通過搜索,發現官方有1個相似的bug,這裏簡單介紹一下該bug。app

背景知識1

Innodb引擎中的auto increment 相關參數及數據結構。運維

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

主要涉及的結構體包括:數據字典結構體,保存整個表的當前auto increment值以及保護鎖;事務結構體,保存事務內部處理的行數;handler結構體,保存事務內部多行的循環迭代信息。spa

背景知識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形成線上問題的可能性。

現場分析及復現驗證

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

(1) 分析max id及autoincrement的規律 因爲用戶的表設置了ON UPDATE CURRENT_TIMESTAMP列,所以能夠把全部的出錯的表的max id、autoincrement及最近更新的幾條記錄抓取出來,看看是否有什麼規律。抓取的信息以下:

image.png

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

聯想到【官方文檔】中對auto increment用法的介紹,update操做是能夠只更新自增id但不觸發auto increment推動的。按照這個思路,我嘗試復現了用戶的現場。復現方法以下:

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

不過,因爲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切主,所以會報錯。

解決方案

業務側的可能解決方案:

  • (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。

心得

  • (1) autoincrement的autoinc_lock_mode及auto_increment_increment這兩個參數變化容易致使出現重複的key,使用過程當中要儘可能避免動態的去修改。
  • (2) 在碰到線上的問題時,首先應該作好現場分析,明確故障發生的場景、用戶的SQL語句、故障發生的範圍等信息,同時要對涉及實例的配置信息、binlog甚至實例數據等作好備份以防過時丟失。

只有這樣才能在找官方bug時精準的匹配場景,若是官方沒有相關bug,也能經過已有線索獨立分析。

做者:騰訊數據庫技術
來源: http://r6e.cn/df8b

相關文章
相關標籤/搜索