ACID:Atomic、Consistent、Isolated、Durable
存儲程序提供了一個絕佳的機制來定義、封裝和管理事務。
1,MySQL的事務支持
MySQL的事務支持不是綁定在MySQL服務器自己,而是與存儲引擎相關:java
- MyISAM:不支持事務,用於只讀程序提升性能
- InnoDB:支持ACID事務、行級鎖、併發
- Berkeley DB:支持事務
- MyISAM:不支持事務,用於只讀程序提升性能
- InnoDB:支持ACID事務、行級鎖、併發
- Berkeley DB:支持事務
隔離級別:
隔離級別決定了一個session中的事務可能對另外一個session的影響、併發session對數據庫的操做、一個session中所見數據的一致性
ANSI標準定義了4個隔離級別,MySQL的InnoDB都支持:mysql
- READ UNCOMMITTED:最低級別的隔離,一般又稱爲dirty read,它容許一個事務讀取還沒commit的數據,這樣可能會提升性能,可是dirty read可能不是咱們想要的
- READ COMMITTED:在一個事務中只容許已經commit的記錄可見,若是session中select還在查詢中,另外一session此時insert一條記錄,則新添加的數據不可見
- REPEATABLE READ:在一個事務開始後,其餘session對數據庫的修改在本事務中不可見,直到本事務commit或rollback。在一個事務中重複select的結果同樣,除非本事務中update數據庫。
- SERIALIZABLE:最高級別的隔離,只容許事務串行執行。爲了達到此目的,數據庫會鎖住每行已經讀取的記錄,其餘session不能修改數據直到前一事務結束,事務commit或取消時才釋放鎖。
- READ UNCOMMITTED:最低級別的隔離,一般又稱爲dirty read,它容許一個事務讀取還沒commit的數據,這樣可能會提升性能,可是dirty read可能不是咱們想要的
- READ COMMITTED:在一個事務中只容許已經commit的記錄可見,若是session中select還在查詢中,另外一session此時insert一條記錄,則新添加的數據不可見
- REPEATABLE READ:在一個事務開始後,其餘session對數據庫的修改在本事務中不可見,直到本事務commit或rollback。在一個事務中重複select的結果同樣,除非本事務中update數據庫。
- SERIALIZABLE:最高級別的隔離,只容許事務串行執行。爲了達到此目的,數據庫會鎖住每行已經讀取的記錄,其餘session不能修改數據直到前一事務結束,事務commit或取消時才釋放鎖。
可使用以下語句設置MySQL的session隔離級別:sql
- SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
- SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
MySQL默認的隔離級別是REPEATABLE READ,在設置隔離級別爲READ UNCOMMITTED或SERIALIZABLE時要當心,READ UNCOMMITTED會致使數據完整性的嚴重問題,而SERIALIZABLE會致使性能問題並增長死鎖的機率
事務管理語句:數據庫
- START TRANSACTION:開始事務,autocommit設爲0,若是已經有一個事務在運行,則會觸發一個隱藏的COMMIT
- COMMIT:提交事務,保存更改,釋放鎖
- ROLLBACK:回滾本事務對數據庫的全部更改,而後結束事務,釋放鎖
- SAVEPOINT savepoint_name:建立一個savepoint識別符來ROLLBACK TO SAVEPOINT
- ROLLBACK TO SAVEPOINT savepoint_name:回滾到從savepoint_name開始對數據庫的全部更改,這樣就容許回滾事務中的一部分,保證更改的一個子集被提交
- SET TRANSACTION:容許設置事務的隔離級別
- LOCK TABLES:容許顯式的鎖住一個或多個table,會隱式的關閉當前打開的事務,建議在執行LOCK TABLES語句以前顯式的commit或rollback。咱們通常因此通常在事務代碼裏不會使用LOCK TABLES
- START TRANSACTION:開始事務,autocommit設爲0,若是已經有一個事務在運行,則會觸發一個隱藏的COMMIT
- COMMIT:提交事務,保存更改,釋放鎖
- ROLLBACK:回滾本事務對數據庫的全部更改,而後結束事務,釋放鎖
- SAVEPOINT savepoint_name:建立一個savepoint識別符來ROLLBACK TO SAVEPOINT
- ROLLBACK TO SAVEPOINT savepoint_name:回滾到從savepoint_name開始對數據庫的全部更改,這樣就容許回滾事務中的一部分,保證更改的一個子集被提交
- SET TRANSACTION:容許設置事務的隔離級別
- LOCK TABLES:容許顯式的鎖住一個或多個table,會隱式的關閉當前打開的事務,建議在執行LOCK TABLES語句以前顯式的commit或rollback。咱們通常因此通常在事務代碼裏不會使用LOCK TABLES
2,定義事務
MySQL默認的行爲是在每條SQL語句執行後執行一個COMMIT語句,從而有效的將每條語句獨立爲一個事務。
在複雜的應用場景下這種方式就不能知足需求了。
爲了打開事務,容許在COMMIT和ROLLBACK以前多條語句被執行,咱們須要作如下兩步:
1, 設置MySQL的autocommit屬性爲0,默認爲1
2,使用START TRANSACTION語句顯式的打開一個事務
若是已經打開一個事務,則SET autocommit=0不會起做用,由於START TRANSACTION會隱式的提交session中全部當前的更改,結束已有的事務,並打開一個新的事務。
使用SET AUTOCOMMIT語句的存儲過程例子:服務器
- CREATE PROCEDURE tfer_funds
- (from_account int, to_account int, tfer_amount numeric(10,2))
- BEGIN
- SET autocommit=0;
-
- UPDATE account_balance SET balance=balance-tfer_amount WHERE account_id=from_account;
-
- UPDATE account_balance SET balance=balance+tfer_amount WHERE account_id=to_account;
-
- COMMIT;
- END;
- CREATE PROCEDURE tfer_funds
- (from_account int, to_account int, tfer_amount numeric(10,2))
- BEGIN
- SET autocommit=0;
-
- UPDATE account_balance SET balance=balance-tfer_amount WHERE account_id=from_account;
-
- UPDATE account_balance SET balance=balance+tfer_amount WHERE account_id=to_account;
-
- COMMIT;
- END;
使用START TRANSACITON打開事務的例子:session
- CREATE PROCEDURE tfer_funds
- (from_account int, to_account int, tfer_amount numeric(10,2))
- BEGIN
- START TRANSACTION;
-
- UPDATE account_balance SET balance=balance-tfer_amount WHERE account_id=from_account;
-
- UPDATE account_balance SET balance=balance+tfer_amount WHERE account_id=to_account;
-
- COMMIT;
- END;
- CREATE PROCEDURE tfer_funds
- (from_account int, to_account int, tfer_amount numeric(10,2))
- BEGIN
- START TRANSACTION;
-
- UPDATE account_balance SET balance=balance-tfer_amount WHERE account_id=from_account;
-
- UPDATE account_balance SET balance=balance+tfer_amount WHERE account_id=to_account;
-
- COMMIT;
- END;
一般COMMIT或ROLLBACK語句執行時才完成一個事務,可是有些DDL語句等會隱式觸發COMMIT,因此應該在事務中儘量少用或注意一下:併發
- ALTER FUNCTION
- ALTER PROCEDURE
- ALTER TABLE
- BEGIN
- CREATE DATABASE
- CREATE FUNCTION
- CREATE INDEX
- CREATE PROCEDURE
- CREATE TABLE
- DROP DATABASE
- DROP FUNCTION
- DROP INDEX
- DROP PROCEDURE
- DROP TABLE
- UNLOCK TABLES
- LOAD MASTER DATA
- LOCK TABLES
- RENAME TABLE
- TRUNCATE TABLE
- SET AUTOCOMMIT=1
- START TRANSACTION
- ALTER FUNCTION
- ALTER PROCEDURE
- ALTER TABLE
- BEGIN
- CREATE DATABASE
- CREATE FUNCTION
- CREATE INDEX
- CREATE PROCEDURE
- CREATE TABLE
- DROP DATABASE
- DROP FUNCTION
- DROP INDEX
- DROP PROCEDURE
- DROP TABLE
- UNLOCK TABLES
- LOAD MASTER DATA
- LOCK TABLES
- RENAME TABLE
- TRUNCATE TABLE
- SET AUTOCOMMIT=1
- START TRANSACTION
3,使用Savepoint
使用savepoint回滾不免有些性能消耗,通常能夠用IF改寫
savepoint的良好使用的場景之一是「嵌套事務」,你可能但願程序執行一個小的事務,可是不但願回滾外面更大的事務:app
- CREATE PROCEDURE nested_tfer_funds
- (in_from_acct INTEGER,
- in_to_acct INTEGER,
- in_tfer_amount DECIMAL(8,2))
- BEGIN
- DECLARE txn_error INTEGER DEFAULT 0;
-
- DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN
- SET txn_error=1;
- END
-
- SAVEPINT savepint_tfer;
-
- UPDATE account_balance
- SET balance=balance-in_tfer_amount
- WHERE account_id=in_from_acct;
-
- IF txn_error THEN
- ROLLBACK TO savepoint_tfer;
- SELECT 'Transfer aborted';
- ELSE
- UPDATE account_balance
- SET balance=balance+in_tfer_amount
- WHERE account_id=in_to_acct;
-
- IF txn_error THEN
- ROLLBACK TO savepoint_tfer;
- SELECT 'Transfer aborted';
-
- END IF:
- END IF;
- END;
- CREATE PROCEDURE nested_tfer_funds
- (in_from_acct INTEGER,
- in_to_acct INTEGER,
- in_tfer_amount DECIMAL(8,2))
- BEGIN
- DECLARE txn_error INTEGER DEFAULT 0;
-
- DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN
- SET txn_error=1;
- END
-
- SAVEPINT savepint_tfer;
-
- UPDATE account_balance
- SET balance=balance-in_tfer_amount
- WHERE account_id=in_from_acct;
-
- IF txn_error THEN
- ROLLBACK TO savepoint_tfer;
- SELECT 'Transfer aborted';
- ELSE
- UPDATE account_balance
- SET balance=balance+in_tfer_amount
- WHERE account_id=in_to_acct;
-
- IF txn_error THEN
- ROLLBACK TO savepoint_tfer;
- SELECT 'Transfer aborted';
-
- END IF:
- END IF;
- END;
4,事務和鎖
事務的ACID屬性只能經過限制數據庫的同步更改來實現,從而經過對修改數據加鎖來實現。
直到事務觸發COMMIT或ROLLBACK語句時鎖才釋放。
缺點是後面的事務必須等前面的事務完成才能開始執行,吞吐量隨着等待鎖釋放的時間增加而遞減。
MySQL/InnoDB經過行級鎖來最小化鎖競爭。這樣修改同一table裏其餘行的數據沒有限制,並且讀數據能夠始終沒有等待。
能夠在SELECT語句裏使用FOR UPDATE或LOCK IN SHARE MODE語句來加上行級鎖ide
- SELECT select_statement options [FOR UPDATE|LOCK IN SHARE MODE]
- SELECT select_statement options [FOR UPDATE|LOCK IN SHARE MODE]
FOR UPDATE會鎖住該SELECT語句返回的行,其餘SELECT和DML語句必須等待該SELECT語句所在的事務完成
LOCK IN SHARE MODE同FOR UPDATE,可是容許其餘session的SELECT語句執行並容許獲取SHARE MODE鎖
死鎖:
死鎖發生於兩個事務相互等待彼此釋放鎖的情景
當MySQL/InnoDB檢查到死鎖時,它會強制一個事務rollback並觸發一條錯誤消息
對InnoDB而言,所選擇的rollback的事務是完成工做最少的事務(所修改的行最少)性能
- mysql > CALL tfer_funds(1,2,300);
- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
- <span class="hilite1">mysql</span> > CALL tfer_funds(1,2,300);
- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
死鎖在任何數據庫系統裏均可能發生,可是對MySQL/InnoDB這種行級鎖數據庫而言可能性相對較少。
能夠經過使用一致的順序來鎖row或table以及讓事務保持儘量短來減小死鎖的頻率。
若是死鎖不容易debug,你能夠向你的程序中添加一些邏輯來處理死鎖並重試事務,但這部分代碼多了之後很難維護
因此,比較好的避免死鎖的方式是在作任何修改以前按必定的順序添加行級鎖,這樣就能避免死鎖:
- CREATE PROCEDURE tfer_funds3
- (from_account INT, to_account INT, tfer_amount NUMERIC(10,2))
- BEGIN
- DECLARE local_account_id INT;
- DECLARE lock_cursor CURSOR FOR
- SELECT account_id
- FROM account_balance
- WHERE account_id IN (from_account, to_account)
- ORDER BY account_id
- FOR UPDATE;
-
- START TRANSACTION;
-
- OPEN lock_cursor;
- FETCH lock_cursor INTO local_account_id;
-
- UPDATE account_balance
- SET balance=balance-tfer_amount
- WHERE account_id=from_account;
-
- UPDATE account_balance
- SET balance=balance+tfer_amount
- WHERE account_id=to_account;
-
- CLOSE lock_cursor;
-
- COMMIT;
- END;
- CREATE PROCEDURE tfer_funds3
- (from_account INT, to_account INT, tfer_amount NUMERIC(10,2))
- BEGIN
- DECLARE local_account_id INT;
- DECLARE lock_cursor CURSOR FOR
- SELECT account_id
- FROM account_balance
- WHERE account_id IN (from_account, to_account)
- ORDER BY account_id
- FOR UPDATE;
-
- START TRANSACTION;
-
- OPEN lock_cursor;
- FETCH lock_cursor INTO local_account_id;
-
- UPDATE account_balance
- SET balance=balance-tfer_amount
- WHERE account_id=from_account;
-
- UPDATE account_balance
- SET balance=balance+tfer_amount
- WHERE account_id=to_account;
-
- CLOSE lock_cursor;
-
- COMMIT;
- END;
設置死鎖ttl: innodb_lock_wait_timeout,默認爲50秒
若是你在一個事務中混合使用InnoDB和非InnoDB表,則MySQL不能檢測到死鎖,此時會拋出「lock wait timeuot」1205錯誤
樂觀所和悲觀鎖策略:
悲觀鎖:在讀取數據時鎖住那幾行,其餘對這幾行的更新須要等到悲觀鎖結束時才能繼續
樂觀所:讀取數據時不鎖,更新時檢查是否數據已經被更新過,若是是則取消當前更新
通常在悲觀鎖的等待時間過長而不能接受時咱們纔會選擇樂觀鎖
悲觀鎖的例子:
- CREATE PROCEDURE tfer_funds
- (from_account INT, to_account INT,tfer_amount NUMERIC(10,2),
- OUT status INT, OUT message VARCHAR(30))
- BEGIN
- DECLARE from_account_balance NUMERIC(10,2);
-
- START TRANSACTION;
-
-
- SELECT balance
- INTO from_account_balance
- FROM account_balance
- WHERE account_id=from_account
- FOR UPDATE;
-
- IF from_account_balance>=tfer_amount THEN
-
- UPDATE account_balance
- SET balance=balance-tfer_amount
- WHERE account_id=from_account;
-
- UPDATE account_balance
- SET balance=balance+tfer_amount
- WHERE account_id=to_account;
- COMMIT;
-
- SET status=0;
- SET message='OK';
- ELSE
- ROLLBACK;
- SET status=-1;
- SET message='Insufficient funds';
- END IF;
- END;
- CREATE PROCEDURE tfer_funds
- (from_account INT, to_account INT,tfer_amount NUMERIC(10,2),
- OUT status INT, OUT message VARCHAR(30))
- BEGIN
- DECLARE from_account_balance NUMERIC(10,2);
-
- START TRANSACTION;
-
-
- SELECT balance
- INTO from_account_balance
- FROM account_balance
- WHERE account_id=from_account
- FOR UPDATE;
-
- IF from_account_balance>=tfer_amount THEN
-
- UPDATE account_balance
- SET balance=balance-tfer_amount
- WHERE account_id=from_account;
-
- UPDATE account_balance
- SET balance=balance+tfer_amount
- WHERE account_id=to_account;
- COMMIT;
-
- SET status=0;
- SET message='OK';
- ELSE
- ROLLBACK;
- SET status=-1;
- SET message='Insufficient funds';
- END IF;
- END;
樂觀鎖的例子:
- CREATE PROCEDURE tfer_funds
- (from_account INT, to_account INT, tfer_amount NUMERIC(10,2),
- OUT status INT, OUT message VARCHAR(30) )
-
- BEGIN
-
- DECLARE from_account_balance NUMERIC(8,2);
- DECLARE from_account_balance2 NUMERIC(8,2);
- DECLARE from_account_timestamp1 TIMESTAMP;
- DECLARE from_account_timestamp2 TIMESTAMP;
-
- SELECT account_timestamp,balance
- INTO from_account_timestamp1,from_account_balance
- FROM account_balance
- WHERE account_id=from_account;
-
- IF (from_account_balance>=tfer_amount) THEN
-
- -- Here we perform some long running validation that
- -- might take a few minutes */
- CALL long_running_validation(from_account);
-
- START TRANSACTION;
-
- -- Make sure the account row has not been updated since
- -- our initial check
- SELECT account_timestamp, balance
- INTO from_account_timestamp2,from_account_balance2
- FROM account_balance
- WHERE account_id=from_account
- FOR UPDATE;
-
- IF (from_account_timestamp1 <> from_account_timestamp2 OR
- from_account_balance <> from_account_balance2) THEN
- ROLLBACK;
- SET status=-1;
- SET message=CONCAT("Transaction cancelled due to concurrent update",
- " of account" ,from_account);
- ELSE
- UPDATE account_balance
- SET balance=balance-tfer_amount
- WHERE account_id=from_account;
-
- UPDATE account_balance
- SET balance=balance+tfer_amount
- WHERE account_id=to_account;
-
- COMMIT;
-
- SET status=0;
- SET message="OK";
- END IF;
-
- ELSE
- ROLLBACK;
- SET status=-1;
- SET message="Insufficient funds";
- END IF;
- END$$
- CREATE PROCEDURE tfer_funds
- (from_account INT, to_account INT, tfer_amount NUMERIC(10,2),
- OUT status INT, OUT message VARCHAR(30) )
-
- BEGIN
-
- DECLARE from_account_balance NUMERIC(8,2);
- DECLARE from_account_balance2 NUMERIC(8,2);
- DECLARE from_account_timestamp1 TIMESTAMP;
- DECLARE from_account_timestamp2 TIMESTAMP;
-
- SELECT account_timestamp,balance
- INTO from_account_timestamp1,from_account_balance
- FROM account_balance
- WHERE account_id=from_account;
-
- IF (from_account_balance>=tfer_amount) THEN
-
- -- Here we perform some long running validation that
- -- might take a few minutes */
- CALL long_running_validation(from_account);
-
- START TRANSACTION;
-
- -- Make sure the account row has not been updated since
- -- our initial check
- SELECT account_timestamp, balance
- INTO from_account_timestamp2,from_account_balance2
- FROM account_balance
- WHERE account_id=from_account
- FOR UPDATE;
-
- IF (from_account_timestamp1 <> from_account_timestamp2 OR
- from_account_balance <> from_account_balance2) THEN
- ROLLBACK;
- SET status=-1;
- SET message=CONCAT("Transaction cancelled due to concurrent update",
- " of account" ,from_account);
- ELSE
- UPDATE account_balance
- SET balance=balance-tfer_amount
- WHERE account_id=from_account;
-
- UPDATE account_balance
- SET balance=balance+tfer_amount
- WHERE account_id=to_account;
-
- COMMIT;
-
- SET status=0;
- SET message="OK";
- END IF;
-
- ELSE
- ROLLBACK;
- SET status=-1;
- SET message="Insufficient funds";
- END IF;
- END$$
5,事務設計指南
- 1,保持事務短小
- 2,儘可能避免事務中rollback
- 3,儘可能避免savepoint
- 4,默認狀況下,依賴於悲觀鎖
- 5,爲吞吐量要求苛刻的事務考慮樂觀鎖
- 6,顯示聲明打開事務
- 7,鎖的行越少越好,鎖的時間越短越好