今年這種狀況,有時候不找好下家還真不敢跳,這不,前段時間剛跳到新東家,剛辦入職那天,就趕上事了,真的是嚇出一身冷汗(老大一直盯着我,說要快速解決這個問題),差點被(背)開(鍋)了....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
Innodb引擎中的auto increment 相關參數及數據結構。運維
主要參數包括:innodb_autoinc_lock_mode用於控制獲取自增值的加鎖方式,auto_increment_increment, auto_increment_offset用於控制自增列的遞增的間隔和起始偏移。函數
主要涉及的結構體包括:數據字典結構體,保存整個表的當前auto increment值以及保護鎖;事務結構體,保存事務內部處理的行數;handler結構體,保存事務內部多行的循環迭代信息。spa
mysql及Innodb引擎中對autoincrement訪問及修改的流程線程
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:設置當前事務中下一個要處理的行的自增列的值。
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"
此時,首次插入時,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。這樣就產生了重複。
經過上述分析,這個bug僅在autoinc_lock_mode > 0 而且auto_increment_increment > 1的狀況下會發生。實際線上業務對這兩個參數都設置爲1,所以,能夠排除這個bug形成線上問題的可能性。
既然官方bug未能解決咱們的問題,那就得自食其力,從錯誤現象開始分析了。
(1) 分析max id及autoincrement的規律 因爲用戶的表設置了ON UPDATE CURRENT_TIMESTAMP列,所以能夠把全部的出錯的表的max id、autoincrement及最近更新的幾條記錄抓取出來,看看是否有什麼規律。抓取的信息以下:
乍看起來,這個錯誤仍是頗有規律的,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」
緣由:
所以在slave機上就會出現max(id)大於autoincrement的狀況。此時在ROW模式下對於insert操做binlog記錄了全部的列的值,在slave上回放時並不會從新分配自增id,所以不會報錯。可是若是slave切master,遇到Insert操做就會出現」Duplicate key」的錯誤。
業務側的可能解決方案:
內核側可能解決方案:
只有這樣才能在找官方bug時精準的匹配場景,若是官方沒有相關bug,也能經過已有線索獨立分析。
做者:騰訊數據庫技術
來源: http://r6e.cn/df8b