一個慢日誌問題引出的 MySQL 半一致性讀的應用場景

作者通過一個慢日誌問題,引出 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 條件,若不滿足則不會加鎖(提前釋放鎖)。

那麼對於區分度低的字段就可以使用半一致性讀特性來優化,這樣更新不同的值就不會互相等待,導致業務卡頓。

結論

  1. 行鎖機制是基於索引列實現的,若沒有使用到索引,則會進行全表掃描。
  2. 半一致性讀是基於 RC 隔離級別的優化,可以減少鎖衝突以及鎖等待,提升併發。

關於 SQLE

愛可生開源社區的 SQLE 是一款面向數據庫使用者和管理者,支持多場景審覈,支持標準化上線流程,原生支持 MySQL 審覈且數據庫類型可擴展的 SQL 審覈工具。

SQLE 獲取

類型 地址
版本庫 https://github.com/actiontech/sqle
文檔 https://actiontech.github.io/sqle-docs/
發佈信息 https://github.com/actiontech/sqle/releases
數據審覈插件開發文檔 https://actiontech.github.io/sqle-docs-cn/3.modules/3.7_auditplugin/auditplugin_development.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章