領導讓我
SQL
優化,我直接把服務幹掛了...html
MySQL
大表加字段或者加索引,是有必定風險的。mysql
大公司通常有DBA
,會幫助開發解決這個痛點,但是DBA
是怎麼作的呢?算法
小公司沒有DBA
,做爲開發咱們的責任就更大了。那麼咱們怎麼才能安全的加個索引呢?sql
今天,咱們經過模擬案例以及原理分析,去弄清楚MySQL
中DDL
的風險,以及如何避免事故發生。shell
# 若是存在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();
# 查看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的配置文件中配置,啓動時生效。安全
# 展現哪些線程正在運行 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();
後面咱們會主要用前兩條。微信
user
表除了主鍵是沒有其餘索引的。user
表數據量爲一百萬。MySQL
版本爲5.7.28
。運行測試項目session
這裏咱們能夠看到,項目已經正常啓動了。併發
postman
調用一下接口
這裏咱們隨便測試一個接口,請求時間2秒左右。
執行JMeter的Test Plan,觀察項目日誌
這裏咱們建立了四個線程組,每一個線程組調用一個咱們的接口。模擬10我的循環1000次的訪問。
這裏咱們看到該請求頻率下,日誌無異常。
慢SQL日誌
這裏咱們看到,百萬級的SQL,若是沒加索引SQL執行時間仍是比較長的,有的已經達到了2s。
加個索引,再觀察項目日誌
這裏咱們看到,項目已經開始報錯了,大量的Connection is not available, request timed out after 30001ms
。
SHOW PROCESSLIST
一下
這裏咱們看到,有大量的Waiting for table metadata lock
。
postman
再次調用一下接口
這個時候,調用接口已經報錯了,響應時間也比較久。此時,服務對用戶來講,已經基本不可用了。
我就想加個索引,怎麼就這麼難?
看吧,就由於我加了個索引,服務就掛了,我沒加以前仍是好好的。遇到問題,咱們要冷靜,不是咱們的鍋堅定不能背,真的是咱們的問題,下次必定要記得改正。那麼,此刻的服務爲何就不可用了呢?
首先咱們要知道,在InnoDB
事務中,鎖是在須要的時候才加上的,但並非不須要了就馬上釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。
而後,在MySQL5.5
版本中引入了MDL(Metadata Lock)
,當對一個表作增刪改查操做的時候,加MDL
讀鎖;當要對錶作結構變動操做的時候,加MDL
寫鎖。
咱們能夠簡單的嘗試一下下面的狀況。
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操做就會一直被堵塞,還會間接的影響後面其餘的查詢,致使全部的查詢都被堵塞。
這也就是爲何咱們把服務幹掛的緣由了。
針對上面出現的狀況,咱們怎麼解決呢?
Online DDL
MySQL
從5.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
,結果兩種寫法有的時候是同樣的...
這裏順便提一句,學習的途徑有不少,可是官網,的確能夠多看看。
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
建立觸發器的時候卡在那了。實際上這裏也是在等待鎖。
最終成功了,可是整個過程時間比較久。過程當中咱們也發現了一些死鎖的日誌。
其實,這個跟個人代碼有必定的關係,個人測試代碼隨機數生成的範圍是[0, 20000]
,而後我根據生成的隨機數,去查詢數據庫,鎖的衝突會比較多。把範圍修改成[0, 1000000]
會好不少。
Online DDL
由於剛纔咱們發現了,本身代碼寫的有一些問題,因此咱們剛纔的結論也有一些影響。咱們把隨機數的範圍改到100萬,從新測試一遍。
此次Online DDL
也成功了。可是也是有一些鏈接超時的日誌。以前的測試若是一直執行下去,也會成功,只不過堵塞時間太長,對用戶影響太大,我就中止算執行失敗了。
實際效果跟機器性能也是有一些關係的,這裏的關鍵點在於拿
MDL
寫鎖的等待時間,這個時間稍微久一些就會對用戶形成很大的影響。
pt-osc
執行過程_tablename_new
),執行alter
修改臨時表表結構。insert
delete
update
對應的觸發器,用於copy
數據的過程當中,在原表的更新操做,更新到新表。rename
原數據表爲old
表,把新表rename
爲原表名,並將old
表刪除。這裏面建立、刪除觸發器和rename
表的時候都會嘗試獲取DML
寫鎖,若是獲取不到會等待。就是咱們看到的Waiting for table metadata lock
。
因此,這些時間段若是長時間獲取不到鎖,就會一直堵塞,仍是會出現問題的。
Online DDL
執行過程MDL
寫鎖MDL
讀鎖DDL
MDL
寫鎖MDL
鎖一、4
若是沒有鎖衝突,執行時間很是短。第3步佔用了DDL
絕大部分時間,這期間這個表能夠正常讀寫數據,所以稱爲online。
可是,若是拿鎖的時候沒拿到,或者升級MDL
寫鎖不能成功,就會等待,咱們又會看到Waiting for table metadata lock
,而後就接着的一系列問題了。
加個索引,說難也難,說不難也不難。若是數據量大,又存在長事務,加索引的過程又有用戶訪問,Online DDL
和pt-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
歡迎關注我的微信公衆號【如逆水行舟】,用心輸出基礎、算法、源碼系列文章。