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

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

前言

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

大公司通常有DBA,會幫助開發解決這個痛點,但是DBA是怎麼作的呢?算法

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

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

準備

軟件以及項目

  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毫秒。

步驟

運行測試項目session

項目啓動圖

這裏咱們能夠看到,項目已經正常啓動了。併發

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

一、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

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

相關文章
相關標籤/搜索