MySQL加索引鎖表怎麼解決?

領導讓我SQL優化,我直接把服務幹掛了…

前言

MySQL大表加字段或者加索引,是有一定風險的。

大公司一般有DBA,會幫助開發解決這個痛點,可是DBA是怎麼做的呢?

小公司沒有DBA,作爲開發我們的責任就更大了。那麼我們怎麼才能安全的加個索引呢?

今天,我們通過模擬案例以及原理分析,去弄清楚MySQLDDL的風險,以及如何避免事故發生。

準備

軟件以及項目

  1. 安裝本地版本MySQL。
  2. 一個簡單的增刪改查項目。
  3. 使用JMeter進行併發請求測試。

創建表

# 如果存在user表則刪除
DROP TABLE  IF EXISTS user;

# 創建user表
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `name` varchar(10) DEFAULT NULL COMMENT '姓名',
  `age` int(2) DEFAULT NULL COMMENT '年齡',
  `address` varchar(30) DEFAULT NULL COMMENT '地址',
  `description` varchar(100) DEFAULT NULL COMMENT '描述',
  `test_id` bigint DEFAULT NULL COMMENT '測試 id',
  `create_time` timestamp NULL DEFAULT NULL COMMENT '創建時間',
  `modify_time` timestamp NULL DEFAULT NULL COMMENT '修改時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='mysql ddl測試表';

創建存儲過程

# 如果存在test存儲過程則刪除
DROP PROCEDURE IF EXISTS `test`;

# 創建無參存儲過程,名稱爲test
CREATE PROCEDURE test()

BEGIN
    # 聲明變量
    DECLARE i INT;
    # 變量賦值
    SET i = 0;
    # 結束循環的條件: 當i等於100萬時跳出while循環
    WHILE i < 1000000 DO
    # 往t_test表添加數據
    INSERT INTO `test`.user (`name`, `age`, `address`, 
                             `description`, `test_id`, `create_time`, `modify_time`)
    VALUES ('iisheng', 26, '北京', '如逆水行舟', LAST_INSERT_ID() + 1, 
            '2020-05-17 16:01:44', '2020-05-17 16:01:51');

    # 循環一次, i加1
    SET i = i + 1;
    # 結束while循環
    END WHILE;

END

下面的創建存儲過程語句,是在IDE內選擇代碼塊執行的,如果在Terminal中執行,需要使用DELIMITER關鍵字,更改語句結束標誌。

調用存儲過程,生成百萬數據

CALL test();

開啓慢SQL日誌

# 查看MySQL是否開啓慢日誌記錄
SHOW VARIABLES LIKE 'slow_query_log';

# 開啓慢SQL日誌記錄
SET GLOBAL slow_query_log = 'ON';

# 查看慢SQL日誌位置
SHOW VARIABLES LIKE 'slow_query_log_file';

# 查看執行多久的SQL纔算慢SQL
SHOW VARIABLES LIKE 'long_query_time';

# 設置慢SQL執行時間 只有新session才生效
SET GLOBAL long_query_time = 1;

通常情況下這些會在MySQL的配置文件中配置,啓動時生效。

幾個有用的SQL語句

# 展示哪些線程正在運行
SHOW PROCESSLIST;

# 查看正在執行的事務
SELECT * FROM information_schema.INNODB_TRX;

# 查看正在鎖的事務
SELECT * FROM information_schema.INNODB_LOCKS;

# 查看正在等待鎖的事務
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

# 顯示innodb存儲引擎狀態的大量信息,包含死鎖日誌
SHOW ENGINE INNODB STATUS ;

# 展示數據庫最大連接數的配置
SHOW VARIABLES LIKE 'max_connections';

# 查看存在哪些觸發器
SELECT * FROM information_schema.TRIGGERS;

# 查看MySQL版本
SELECT VERSION();

後面我們會主要用前兩條。

事故現場

說明

  1. 我創建的user表除了主鍵是沒有其他索引的。
  2. 測試的user表數據量爲一百萬。
  3. 測試MySQL版本爲5.7.28
  4. 測試項目的邏輯:隨機get()、list()、update()、create(),每個操作都開啓事務,並且休眠500毫秒。

步驟

運行測試項目

項目啓動圖

這裏我們可以看到,項目已經正常啓動了。

postman調用一下接口

接口請求圖

這裏我們隨便測試一個接口,請求時間2秒左右。

執行JMeter的Test Plan,觀察項目日誌

JMeter配置圖

這裏我們創建了四個線程組,每個線程組調用一個我們的接口。模擬10個人循環1000次的訪問。

正常項目日誌圖

這裏我們看到該請求頻率下,日誌無異常。

慢SQL日誌

慢SQL日誌圖

這裏我們看到,百萬級的SQL,如果沒加索引SQL執行時間還是比較長的,有的已經達到了2s。

加個索引,再觀察項目日誌

加索引過程日誌圖

這裏我們看到,項目已經開始報錯了,大量的Connection is not available, request timed out after 30001ms

SHOW PROCESSLIST一下

PROCESSLIST圖

這裏我們看到,有大量的Waiting for table metadata lock

postman再次調用一下接口

請求接口報錯圖

這個時候,調用接口已經報錯了,響應時間也比較久。此時,服務對用戶來說,已經基本不可用了。

爲什麼會這樣?

我就想加個索引,怎麼就這麼難?

看吧,就因爲我加了個索引,服務就掛了,我沒加之前還是好好的。遇到問題,我們要冷靜,不是我們的鍋堅決不能背,真的是我們的問題,下次一定要記得改正。那麼,此刻的服務爲什麼就不可用了呢?

首先我們要知道,在InnoDB事務中,鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議

然後,在MySQL5.5版本中引入了MDL(Metadata Lock),當對一個表做增刪改查操作的時候,加MDL讀鎖;當要對錶做結構變更操作的時候,加MDL寫鎖。

我們可以簡單的嘗試一下下面的情況。

DDL鎖等待圖

Session A開啓一個事務,執行了一個簡單的查詢語句。此時,Session B,執行另一個查詢語句,可以成功。接着,Session C執行了一個DDL操作,加了個字段,因爲Session A的事務沒有提交,而且Session A持有MDL讀鎖,Session C獲取不到MDL寫鎖,所以Session C堵塞等待MDL寫鎖。又由於MDL寫鎖獲取優先級高於MDL讀鎖,因此Session D這個時候也獲取不到MDL讀鎖,等待Session C獲取到MDL寫鎖之後它才能獲取到MDL讀鎖。

我們發現,DDL操作之前如果存在長事務,一直不提交,DDL操作就會一直被堵塞,還會間接的影響後面其他的查詢,導致所有的查詢都被堵塞。

這也就是爲什麼我們把服務幹掛的原因了。

目前主流解決方案

針對上面出現的情況,我們怎麼解決呢?

MySQL5.6的Online DDL

MySQL5.6開始,支持Online DDL。類似於這種的語句ALTER TABLE user ADD INDEX idx_test_id (test_id), ALGORITHM=INPLACE, LOCK=NONE在普通的ALTER TABLE或者CREATE INDEX語句後面添加ALGORITHM參數和LOCK參數。

實際上,ALTERT TABLE語句如果不加ALGORITHM參數,默認就會選擇ALGORITHM=INPLACE的形式,如果執行的語句支持INPLACE,否則,會使用ALGORITHM=COPY

以前寫SQL只會ALTER TABLE不知道後面還可以加ALGORITHM參數,後來知道了Online DDL,知道了可以加ALGORITHM=INPLACE,結果兩種寫法有的時候是一樣的…

MySQL官網截圖

這裏順便提一句,學習的途徑有很多,但是官網,的確可以多看看。

使用pt-online-schema-change

簡單說一下怎麼安裝這個東西

首先官網下載,然後校驗以及安裝,執行下面命令

perl Makefile.PL
make
make install

然後使用CPAN安裝相關依賴(適用Unix),CentOS下直接yum更簡單

perl -MCPAN -e shell
cpan> install DBI
cpan> install DBD::mysql

我自己Mac安裝沒啥問題,公司Mac安裝失敗了,然後升級了一下Perl版本就可以了。

語法

pt-online-schema-change --charset=utf8 --no-check-replication-filters --no-version-check --user=user --password=pass --host=host_addr  P=3306,D=database,t=table --alter "ADD INDEX idx_name(field_name)" --execute

我的腳本添加索引

pt-online-schema-change --charset=utf8 --no-check-replication-filters --no-version-check --user=root --password=mGy6GAzdawFPTJ7R --host=127.0.0.1  P=3306,D=test,t=user --alter "add INDEX idx_test_id(test_id)" --execute

使用pt-osc測試

pt-osc執行圖

這裏我們看到,pt-osc創建觸發器的時候卡在那了。實際上這裏也是在等待鎖。

最終成功了,但是整個過程時間比較久。過程中我們也發現了一些死鎖的日誌。

pt-osc死鎖日誌

其實,這個跟我的代碼有一定的關係,我的測試代碼隨機數生成的範圍是[0, 20000],然後我根據生成的隨機數,去查詢數據庫,鎖的衝突會比較多。把範圍修改爲[0, 1000000]會好很多。

再看Online DDL

因爲剛纔我們發現了,自己代碼寫的有一些問題,所以我們剛纔的結論也有一些影響。我們把隨機數的範圍改到100萬,重新測試一遍。

Online DDL 成功

這次Online DDL也成功了。但是也是有一些連接超時的日誌。之前的測試如果一直執行下去,也會成功,只不過堵塞時間太長,對用戶影響太大,我就停止算執行失敗了。

實際效果跟機器性能也是有一些關係的,這裏的關鍵點在於拿MDL寫鎖的等待時間,這個時間稍微久一些就會對用戶造成很大的影響。

pt-osc執行過程

  1. 創建一個和原表表結構一樣的臨時表(_tablename_new),執行alter修改臨時表表結構。
  2. 在原表上創建3個與insert delete update對應的觸發器,用於copy數據的過程中,在原表的更新操作,更新到新表。
  3. 從原表拷貝數據到臨時表,拷貝過程中在原表進行的寫操作都會更新到新建的臨時表。
  4. rename原數據表爲old表,把新表rename爲原表名,並將old表刪除。
  5. 刪除觸發器。

這裏面創建、刪除觸發器和rename表的時候都會嘗試獲取DML寫鎖,如果獲取不到會等待。就是我們看到的Waiting for table metadata lock

所以,這些時間段如果長時間獲取不到鎖,就會一直堵塞,還是會出現問題的。

Online DDL執行過程

  1. MDL寫鎖
  2. 降級成MDL讀鎖
  3. 真正做DDL
  4. 升級成MDL寫鎖
  5. 釋放MDL

1、4如果沒有鎖衝突,執行時間非常短。第3步佔用了DDL絕大部分時間,這期間這個表可以正常讀寫數據,因此稱爲online

但是,如果拿鎖的時候沒拿到,或者升級MDL寫鎖不能成功,就會等待,我們又會看到Waiting for table metadata lock,然後就接着的一系列問題了。

總結

加個索引,說難也難,說不難也不難。如果數據量大,又存在長事務,加索引的過程又有用戶訪問,Online DDLpt-osc都不能保證對業務沒有影響。但是如果我們SQL的執行時間比較短,或者我們加索引的時候,對應的業務沒有多少請求。那麼我們就可以很快的加完索引。

加字段也是類似的過程,但是如果我們能保證沒有慢SQL,那麼就不會存在長事務,那麼執行時間就會很快,對用戶就可以做到幾乎沒有影響。至於選擇Online DDL還是pt-osc就要看他們的一些限制以及自己的場景需求了。感興趣的同學,自己嘗試一下。

最後想說

當萬丈高樓崩塌的時候,超人也不能將它復原。我們應該做的,是有一個好的規範,好的認知,好的監控,在問題沒有出現的時候,就將問題扼殺在搖籃中。而不是讓問題,日漸壯大,大到覆水難收…

參考文獻:
[1]:《MySQL實戰45講》
[2]: https://dev.mysql.com/doc/refman/5.7/en/
[3]: https://www.percona.com/doc/percona-toolkit/3.0/pt-online-schema-change.html

公衆號內回覆“抽獎”,抽200塊現金紅包,目前中間概率100%

歡迎關注個人微信公衆號【如逆水行舟】,用心輸出基礎、算法、源碼系列文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章