聊聊最常用也是最簡單的 BEGIN 語句,開始一個事務的過程中都幹了什麼。
作者:操盛春,愛可生技術專家,公眾號『一樹一溪』作者,專注於研究 MySQL 和 OceanBase 源碼。
愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。
BEGIN 語句會馬上啓動事務嗎?
本文基於 MySQL 8.0.32 源碼,存儲引擎為 InnoDB。
目錄
[TOC]
正文
1. BEGIN 語句的七十二變
我們查看官方文檔中開始一個事務的語法,會發現還挺複雜:
START TRANSACTION
[transaction_characteristic [, transaction_characteristic] ...]
transaction_characteristic: {
WITH CONSISTENT SNAPSHOT
| READ WRITE
| READ ONLY
}
BEGIN [WORK]
上面眼花繚亂的語法,按照各種組合展開之後,可以得到這些 SQL 語句:
/* 1 */ BEGIN
/* 2 */ BEGIN WORK
/* 3 */ START TRANSACTION
/* 4 */ START TRANSACTION READ WRITE
/* 5 */ START TRANSACTION READ ONLY
/* 6 */ START TRANSACTION WITH CONSISTENT SNAPSHOT
/* 7 */ START TRANSACTION WITH CONSISTENT SNAPSHOT, READ WRITE
/* 8 */ START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY
/* 9 */ START TRANSACTION WITH CONSISTENT SNAPSHOT, READ WRITE, READ ONLY
/* 10 */ START TRANSACTION READ WRITE, READ ONLY
其中,語句 1 ~ 8 都能正常執行,語句 9、10 會報語法錯誤:
(1064,
"You have an error in your SQL syntax;
check the manual that corresponds
to your MySQL server version
for the right syntax to use
near '' at line 1")
語句 9、10 報語法錯誤,並不是因為 MySQL 不能識別這兩種語法,而是識別語法之後進行判斷給出的錯誤提示:
start:
START_SYM TRANSACTION_SYM opt_start_transaction_option_list
{
LEX *lex= Lex;
lex->sql_command= SQLCOM_BEGIN;
/* READ ONLY and READ WRITE are mutually exclusive. */
if (($3 & MYSQL_START_TRANS_OPT_READ_WRITE) &&
($3 & MYSQL_START_TRANS_OPT_READ_ONLY))
{
YYTHD->syntax_error();
MYSQL_YYABORT;
}
lex->start_transaction_opt= $3;
}
;
上面是解析 START TRANSACTION 的部分邏輯,通過以上邏輯可以看到,當 START TRANSACTION 同時包含以下兩項時:
- MYSQL_START_TRANS_OPT_READ_WRITE
- MYSQL_START_TRANS_OPT_READ_ONLY
MySQL 會通過 YYTHD->syntax_error() 主動拋出一個語法錯誤,告訴我們不支持這樣的語法。
在可以正常執行的語句 1 ~ 8 中:
- 語句 1 ~ 4:用於開始一個新的讀寫事務。
語句 5:用於開始一個新的只讀事務。
這兩類語句都不需立即創建一致性讀視圖,事務的啓動將延遲至實際需要時。 - 語句 6 ~ 7:用於開始一個新的讀寫事務。
語句 8:用於開始一個新的只讀事務。
這兩類語句都會先啓動事務,隨後立即創建一致性讀視圖。
如果要投票選出我們最常用於開始一個事務的語句,大概非 BEGIN 莫屬了。
接下來,我們就用 BEGIN 作為語句 1 ~ 5 的代表,來聊聊開始一個新事務的過程中,MySQL 做的那些事。
2. BEGIN 語句都幹什麼了?
如果用一個詞語描述 BEGIN 語句要做的事,那就是辭舊迎新,展開來説,BEGIN 語句主要做兩件事:
- 辭舊:提交老事務。
- 迎新:準備新事務。
2.1 提交老事務
我們先來看一個場景:
在 MySQL 客户端命令行(mysql)中,我們通過 BEGIN 語句開始了一個事務(事務 1),並且已經執行了一條 INSERT 語句。
事務 1 還沒有提交(即處於活躍狀態),我們在同一個連接中又執行了 BEGIN 語句,事務 1 會發生什麼?
答案是:事務 1 會被提交。
原因是: MySQL 不支持嵌套事務。事務 1 沒有提交的情況下,又要開始一個新事務,事務 1 將無處安放,只能被動提交了。
回到本小節主題,我們來看看 BEGIN 語句提交老事務的流程。
首先,BEGIN 語句會判斷當前連接中是否有可能存在未提交事務,判斷邏輯為:當前連接的線程是否被打上了 OPTION_NOT_AUTOCOMMIT 或 OPTION_BEGIN 標誌位(如下代碼所示)。
if (thd->in_multi_stmt_transaction_mode() || ...) {
...
}
inline bool in_multi_stmt_transaction_mode() const {
return variables.option_bits &
(OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN);
}
只要 variables.option_bits 包含其中一個標誌位,就説明當前連接中可能存在未提交事務。
BEGIN 語句想要開始一個新事務,就必須先執行一次提交操作,把可能未提交的事務給提交了(如下代碼所示)。
if (thd->in_multi_stmt_transaction_mode() || ...) {
...
res = ha_commit_trans(thd, true);
}
如果 variables.option_bits 沒有包含兩個標誌位中的任何一個,説明當前連接中沒有未提交事務,可以直接開始一個新事務。
2.2 準備新事務
辭舊完事,就該迎新了。
由於 MySQL 一向秉持不鋪張浪費的原則,對於資源,能少分配就少分配、能晚分配就晚分配。
啓動事務也需要分配資源,遵循不鋪張浪費的原則,BEGIN 語句執行過程中,並不會馬上啓動一個新事務,只會為新事務做一點點準備工作。
這個一點點真的是一點點,你看:
thd->variables.option_bits |= OPTION_BEGIN;
上面的準備工作就是給當前連接的線程打上 OPTION_BEGIN 標誌。
有了 OPTION_BEGIN 標誌,MySQL 就不會每次執行完一條 SQL 語句就提交事務,而是需要用户發起 commit 語句才提交事務,這樣的事務就可以執行多條 SQL 了。
3. 總結
一句話總結:BEGIN 語句執行過程中,要做的事情就是辭舊(提交老事務)迎新(準備新事務),並不會馬上啓動一個新事務。
本期問題:對於 START TRANSACTION 同時指定 READ WRITE、READ ONLY,除了報錯,你還有別的思路解決這個問題嗎?歡迎大家留言交流。
下期預告:我是一個事務,請給我一個對象。