Mysql系列的目標是:經過這個系列從入門到全面掌握一個高級開發所須要的所有技能。html
這是Mysql系列第20篇。java
環境:mysql5.7.25,cmd命令中進行演示。mysql
代碼中被[]包含的表示可選,|符號分開的表示可選其一。sql
咱們在寫存儲過程的時候,可能會出現下列一些狀況:微信
遇到上面各類異常狀況的時,可能須要咱們可以捕獲,而後可能須要回滾當前事務。併發
本文主要圍繞異常處理這塊作詳細的介紹。函數
此時咱們須要使用遊標,經過遊標的方式來遍歷select查詢的結果集,而後對每行數據進行處理。優化
建立庫:javacode2018
spa
建立表:test1,test1表中的a字段爲主鍵。3d
/*建庫javacode2018*/ drop database if exists javacode2018; create database javacode2018; /*切換到javacode2018庫*/ use javacode2018; DROP TABLE IF EXISTS test1; CREATE TABLE test1(a int PRIMARY KEY);
咱們將異常分爲mysql內部異常和外部異常
當咱們執行一些sql的時候,可能違反了mysql的一些約束,致使mysql內部報錯,如插入數據違反惟一約束,更新數據超時等,此時異常是由mysql內部拋出的,咱們將這些由mysql拋出的異常統稱爲內部異常。
當咱們執行一個update的時候,可能咱們指望影響1行,可是實際上影響的不是1行數據,這種狀況:sql的執行結果和指望的結果不一致,這種狀況也咱們也把他做爲外部異常處理,咱們將sql執行結果和指望結果不一致的狀況統稱爲外部異常。
test1表中的a字段爲主鍵,咱們向test1表同時插入2條數據,而且放在一個事務中執行,最終要麼都插入成功,要麼都失敗。
/*刪除存儲過程*/ DROP PROCEDURE IF EXISTS proc1; /*聲明結束符爲$*/ DELIMITER $ /*建立存儲過程*/ CREATE PROCEDURE proc1(a1 int,a2 int) BEGIN START TRANSACTION; INSERT INTO test1(a) VALUES (a1); INSERT INTO test1(a) VALUES (a2); COMMIT; END $ /*結束符置爲;*/ DELIMITER ;
上面存儲過程插入了兩條數據,a的值都是1。
mysql> DELETE FROM test1; Query OK, 0 rows affected (0.00 sec) mysql> CALL proc1(1,1); ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY' mysql> SELECT * from test1; +---+ | a | +---+ | 1 | +---+ 1 row in set (0.00 sec)
上面先刪除了test1表中的數據,而後調用存儲過程
proc1
,因爲test1表中的a字段是主鍵,插入第二條數據時違反了a字段的主鍵約束,mysql內部拋出了異常,致使第二條數據插入失敗,最終只有第一條數據插入成功了。上面的結果和咱們指望的不一致,咱們但願要麼都插入成功,要麼失敗。
那咱們怎麼作呢?咱們須要捕獲上面的主鍵約束異常,而後發現有異常的時候執行rollback
回滾操做,改進上面的代碼,看下面示例2。
咱們對上面示例進行改進,捕獲上面主鍵約束異常,而後進行回滾處理,以下:
/*刪除存儲過程*/ DROP PROCEDURE IF EXISTS proc2; /*聲明結束符爲$*/ DELIMITER $ /*建立存儲過程*/ CREATE PROCEDURE proc2(a1 int,a2 int) BEGIN /*聲明一個變量,標識是否有sql異常*/ DECLARE hasSqlError int DEFAULT FALSE; /*在執行過程當中出任何異常設置hasSqlError爲TRUE*/ DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE; /*開啓事務*/ START TRANSACTION; INSERT INTO test1(a) VALUES (a1); INSERT INTO test1(a) VALUES (a2); /*根據hasSqlError判斷是否有異常,作回滾和提交操做*/ IF hasSqlError THEN ROLLBACK; ELSE COMMIT; END IF; END $ /*結束符置爲;*/ DELIMITER ;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;
當有sql異常的時候,會將變量
hasSqlError
的值置爲TRUE
。
mysql> DELETE FROM test1; Query OK, 2 rows affected (0.00 sec) mysql> CALL proc2(1,1); Query OK, 0 rows affected (0.00 sec) mysql> SELECT * from test1; Empty set (0.00 sec)
上面插入了2條同樣的數據,插入失敗,能夠看到上面
test1
表無數據,和指望結果一致,插入被回滾了。
mysql> DELETE FROM test1; Query OK, 0 rows affected (0.00 sec) mysql> CALL proc2(1,2); Query OK, 0 rows affected (0.00 sec) mysql> SELECT * from test1; +---+ | a | +---+ | 1 | | 2 | +---+ 2 rows in set (0.00 sec)
上面插入了2條不一樣的數據,最終插入成功。
外部異常不是由mysql內部拋出的錯誤,而是因爲sql的執行結果和咱們指望的結果不一致的時候,咱們須要對這種狀況作一些處理,如回滾操做。
咱們來模擬電商中下單操做,按照上面的步驟來更新帳戶餘額。
DROP TABLE IF EXISTS t_funds; CREATE TABLE t_funds( user_id INT PRIMARY KEY COMMENT '用戶id', available DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '帳戶餘額' ) COMMENT '用戶帳戶表'; DROP TABLE IF EXISTS t_order; CREATE TABLE t_order( id int PRIMARY KEY AUTO_INCREMENT COMMENT '訂單id', price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '訂單金額' ) COMMENT '訂單表'; delete from t_funds; /*插入一條數據,用戶id爲1001,餘額爲1000*/ INSERT INTO t_funds (user_id,available) VALUES (1001,1000);
/*刪除存儲過程*/ DROP PROCEDURE IF EXISTS proc3; /*聲明結束符爲$*/ DELIMITER $ /*建立存儲過程*/ CREATE PROCEDURE proc3(v_user_id int,v_price decimal(10,2),OUT v_msg varchar(64)) a:BEGIN DECLARE v_available DECIMAL(10,2); /*1.查詢餘額,判斷餘額是否夠*/ select a.available into v_available from t_funds a where a.user_id = v_user_id; if v_available<=v_price THEN SET v_msg='帳戶餘額不足!'; /*退出*/ LEAVE a; END IF; /*模擬耗時5秒*/ SELECT sleep(5); /*2.餘額減去price*/ SET v_available = v_available - v_price; /*3.更新餘額*/ START TRANSACTION; UPDATE t_funds SET available = v_available WHERE user_id = v_user_id; /*插入訂單明細*/ INSERT INTO t_order (price) VALUES (v_price); /*提交事務*/ COMMIT; SET v_msg='下單成功!'; END $ /*結束符置爲;*/ DELIMITER ;
上面過程主要分爲3步驟:驗證餘額、修改餘額變量、更新餘額。
USE javacode2018; CALL proc3(1001,100,@v_msg); select @v_msg;
mysql> SELECT * FROM t_funds; +---------+-----------+ | user_id | available | +---------+-----------+ | 1001 | 900.00 | +---------+-----------+ 1 row in set (0.00 sec) mysql> SELECT * FROM t_order; +----+--------+ | id | price | +----+--------+ | 1 | 100.00 | | 2 | 100.00 | +----+--------+ 2 rows in set (0.00 sec)
上面出現了很是嚴重的錯誤:下單成功了2次,可是帳戶只扣了100。
上面過程是因爲2個操做併發致使的,2個窗口同時執行第一步的時候看到了同樣的數據(看到的餘額都是1000),而後繼續向下執行,最終致使結果出問題了。
上面操做咱們能夠使用樂觀鎖來優化。
樂觀鎖的過程:用指望的值和目標值進行比較,若是相同,則更新目標值,不然什麼也不作。
樂觀鎖相似於java中的cas操做,這塊須要瞭解的能夠點擊:詳解CAS
咱們能夠在資金錶t_funds
添加一個version
字段,表示版本號,每次更新數據的時候+1,更新數據的時候將version做爲條件去執行update,根據update影響行數來判斷執行是否成功,優化上面的代碼,見示例2。
對示例1進行優化。
DROP TABLE IF EXISTS t_funds; CREATE TABLE t_funds( user_id INT PRIMARY KEY COMMENT '用戶id', available DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '帳戶餘額', version INT DEFAULT 0 COMMENT '版本號,每次更新+1' ) COMMENT '用戶帳戶表'; DROP TABLE IF EXISTS t_order; CREATE TABLE t_order( id int PRIMARY KEY AUTO_INCREMENT COMMENT '訂單id', price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '訂單金額' )COMMENT '訂單表'; delete from t_funds; /*插入一條數據,用戶id爲1001,餘額爲1000*/ INSERT INTO t_funds (user_id,available) VALUES (1001,1000);
/*刪除存儲過程*/ DROP PROCEDURE IF EXISTS proc4; /*聲明結束符爲$*/ DELIMITER $ /*建立存儲過程*/ CREATE PROCEDURE proc4(v_user_id int,v_price decimal(10,2),OUT v_msg varchar(64)) a:BEGIN /*保存當前餘額*/ DECLARE v_available DECIMAL(10,2); /*保存版本號*/ DECLARE v_version INT DEFAULT 0; /*保存影響的行數*/ DECLARE v_update_count INT DEFAULT 0; /*1.查詢餘額,判斷餘額是否夠*/ select a.available,a.version into v_available,v_version from t_funds a where a.user_id = v_user_id; if v_available<=v_price THEN SET v_msg='帳戶餘額不足!'; /*退出*/ LEAVE a; END IF; /*模擬耗時5秒*/ SELECT sleep(5); /*2.餘額減去price*/ SET v_available = v_available - v_price; /*3.更新餘額*/ START TRANSACTION; UPDATE t_funds SET available = v_available WHERE user_id = v_user_id AND version = v_version; /*獲取上面update影響行數*/ select ROW_COUNT() INTO v_update_count; IF v_update_count=1 THEN /*插入訂單明細*/ INSERT INTO t_order (price) VALUES (v_price); SET v_msg='下單成功!'; /*提交事務*/ COMMIT; ELSE SET v_msg='下單失敗,請重試!'; /*回滾事務*/ ROLLBACK; END IF; END $ /*結束符置爲;*/ DELIMITER ;
ROW_COUNT()
能夠獲取更新或插入後獲取受影響行數。將受影響行數放在v_update_count
中。而後根據
v_update_count
是否等於1判斷更新是否成功,若是成功則記錄訂單信息並提交事務,不然回滾事務。
use javacode2018; CALL proc4(1001,100,@v_msg); select @v_msg;
mysql> CALL proc4(1001,100,@v_msg); +----------+ | sleep(5) | +----------+ | 0 | +----------+ 1 row in set (5.00 sec) Query OK, 0 rows affected (5.00 sec) mysql> select @v_msg; +---------------+ | @v_msg | +---------------+ | 下單成功! | +---------------+ 1 row in set (0.00 sec)
mysql> CALL proc4(1001,100,@v_msg); +----------+ | sleep(5) | +----------+ | 0 | +----------+ 1 row in set (5.00 sec) Query OK, 0 rows affected (5.01 sec) mysql> select @v_msg; +-------------------------+ | @v_msg | +-------------------------+ | 下單失敗,請重試! | +-------------------------+ 1 row in set (0.00 sec)
能夠看到第一個窗口下單成功了,窗口2下單失敗了。
再看一下2個表的數據:
mysql> SELECT * FROM t_funds; +---------+-----------+---------+ | user_id | available | version | +---------+-----------+---------+ | 1001 | 900.00 | 0 | +---------+-----------+---------+ 1 row in set (0.00 sec) mysql> SELECT * FROM t_order; +----+--------+ | id | price | +----+--------+ | 1 | 100.00 | +----+--------+ 1 row in set (0.00 sec)
也正常。
異常分爲Mysql內部異常和外部異常
內部異常由mysql內部觸發,外部異常是sql的執行結果和指望結果不一致致使的錯誤
sql內部異常捕獲方式
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;
ROW_COUNT()
能夠獲取mysql中insert或者update影響的行數
掌握使用樂觀鎖(添加版本號)來解決併發修改數據可能出錯的問題
begin end
前面能夠加標籤,LEAVE 標籤
能夠退出對應的begin end,能夠使用這個來實現return的效果
mysql系列大概有20多篇,喜歡的請關注一下,歡迎你們加我微信itsoku或者留言交流mysql相關技術!
原文出處:https://www.cnblogs.com/itsoku123/p/11645952.html