作者通過一個慢日誌問題,引出 MySQL 半一致性讀的概念及實際應用場景。
作者:龔唐傑
愛可生 DBA 團隊成員,主要負責 MySQL 技術支持,擅長 MySQL、PG、國產數據庫。
本文來源:原創投稿
- 愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。
背景
某系統執行更新操作發現很慢,發現有大量慢日誌,其中 Lock time 時間佔比很高,MySQL 版本爲 5.7.25,隔離級別爲 RR。
分析
查看錶結構以及 UPDATE
語句的執行計劃:
mysql> show create table test;
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| test | CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2621401 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+-------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> explain update test set name ='test' where name='a';
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| 1 | UPDATE | test | NULL | index | NULL | PRIMARY | 4 | NULL | 2355988 | 100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
1 row in set (0.00 sec)
通過執行計劃發現,該 SQL 是走的主鍵全索引掃描,並且對於 name
列未加索引,當多個事務同時執行時,就會觀察到有阻塞出現。
若 name
列的重複值不多,那麼可以對 name
列添加索引即可解決該問題。因爲 InnoDB 的行鎖機制是基於索引列來實現的,如果 UPDATE
語句能使用到 name
列的索引,那麼就不會產生阻塞,導致業務卡頓。
但若是 name
列的值的區分度很低,就會導致 SQL 不會走 name
列的索引,示例如下:
先添加索引
mysql> alter table test add index tt(name);
Query OK, 0 rows affected (2.74 sec)
Records: 0 Duplicates: 0 Warnings: 0
然後查看執行計劃,發現可能用到的索引有 tt
,但是實際情況依然走的主鍵全索引掃描。
mysql> explain update test set name ='test' where name='a';
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| 1 | UPDATE | test | NULL | index | tt | PRIMARY | 4 | NULL | 2355988 | 100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
1 row in set (0.00 sec)
因爲 MySQL 的優化器是基於代價來評估的,我們可以通過 optimizer trace
來觀察。
mysql> show variables like 'optimizer_trace';
+-----------------+--------------------------+
| Variable_name | Value |
+-----------------+--------------------------+
| optimizer_trace | enabled=off,one_line=off |
+-----------------+--------------------------+
1 row in set (0.01 sec)
可以看到值爲 enabled=off
,表明這個功能默認是關閉的。
如果想打開這個功能,必須⾸先把 enabled
的值改爲 on
。
mysql> set optimizer_trace="enabled=on";
Query OK, 0 rows affected (0.00 sec)
然後執行該 SQL,查看詳細的信息,這裏我們主要關注的是 PREPARE 階段的成本計算。
mysql> update test set name ='test' where name='a';
Query OK, 262144 rows affected (5.97 sec)
Rows matched: 262144 Changed: 262144 Warnings: 0
mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE\G
詳細結果如下。
mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE\G
*************************** 1. row ***************************
QUERY: update test set name ='test' where name='a'
TRACE: {
"steps": [
{
"substitute_generated_columns": {
}
},
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "(`test`.`name` = 'a')",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "multiple equal('a', `test`.`name`)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "multiple equal('a', `test`.`name`)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "multiple equal('a', `test`.`name`)"
}
]
}
},
{
"table": "`test`",
"range_analysis": {
"table_scan": {
"rows": 2355988,
"cost": 475206
},
"potential_range_indexes": [
{
"index": "PRIMARY",
"usable": true,
"key_parts": [
"id"
]
},
{
"index": "tt",
"usable": true,
"key_parts": [
"name",
"id"
]
}
],
"setup_range_conditions": [
],
"group_index_range": {
"chosen": false,
"cause": "no_join"
},
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "tt",
"ranges": [
"0x0100610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 <= name <= 0x0100610000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": true,
"using_mrr": false,
"index_only": false,
"rows": 553720,
"cost": 664465,
"chosen": false,
"cause": "cost"
}
],
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
}
}
}
}
]
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.00 sec)
可以發現執行全表掃描的成本爲 475206,走索引 tt
的成本爲 664465,所以 MySQL 選擇了全表掃描。
那麼如果是這種情況改怎麼處理呢?
如果 InnoDB 隔離級別是 RR,數據庫層面沒有太好的方式,推薦應用端進行改造。
如果數據庫隔離級別可以更改,那麼可以改爲 RC 來解決阻塞的問題。因爲 RC 模式下支持半一致性讀。
什麼是半一致性讀呢?
簡單來說就是當要對行進行加鎖時,會多一步判斷該行是不是真的需要上鎖。比如全表掃描更新的時候,我們只需要更新 WHERE
匹配到的行,如果是沒有半一致性讀就會把所有數據進行加鎖,但是有了半一致性讀,那麼會判斷是否滿足 WHERE
條件,若不滿足則不會加鎖(提前釋放鎖)。
那麼對於區分度低的字段就可以使用半一致性讀特性來優化,這樣更新不同的值就不會互相等待,導致業務卡頓。
結論
- 行鎖機制是基於索引列實現的,若沒有使用到索引,則會進行全表掃描。
- 半一致性讀是基於 RC 隔離級別的優化,可以減少鎖衝突以及鎖等待,提升併發。
關於 SQLE
愛可生開源社區的 SQLE 是一款面向數據庫使用者和管理者,支持多場景審覈,支持標準化上線流程,原生支持 MySQL 審覈且數據庫類型可擴展的 SQL 審覈工具。