Mysql系列第十九講 異常捕獲及處理詳解

qwer1030274531發表於2020-10-12

準備資料

建立庫:javacode2018

建立表:test1,test1表中的a欄位為主鍵。

/*建庫javacode2018*/drop database if exists javacode2018;create database javacode2018;/*切換到javacode2018庫*/use javacode2018;DROP TABLE IF EXISTS test1;CREATE TABLE test1(a int PRIMARY KEY);123456789

異常分類

我們將異常分為mysql內部異常和外部異常

mysql內部異常

當我們執行一些sql的時候,可能違反了mysql的一些約束,導致mysql內部報錯,如插入資料違反唯一約束,更新資料超時等,此時異常是由mysql內部丟擲的,我們將這些由mysql丟擲的異常統稱為內部異常。

外部異常

當我們執行一個update的時候,可能我們期望影響1行,但是實際上影響的不是1行資料,這種情況:sql的執行結果和期望的結果不一致,這種情況也我們也把他作為外部異常處理,我們將sql執行結果和期望結果不一致的情況統稱為外部異常。

Mysql內部異常

示例1

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 ;1234567891011121314

上面儲存過程插入了兩條資料,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)123456789101112

上面先刪除了test1表中的資料,然後呼叫儲存過程proc1,由於test1表中的a欄位是主鍵,插入第二條資料時違反了a欄位的主鍵約束,mysql內部丟擲了異常,導致第二條資料插入失敗,最終只有第一條資料插入成功了。

上面的結果和我們期望的不一致,我們希望要麼都插入成功,要麼失敗。

那我們怎麼做呢?我們需要捕獲上面的主鍵約束異常,然後發現有異常的時候執行rollback回滾操作,改進上面的程式碼,看下面示例2。

示例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 ;1234567891011121314151617181920212223242526

上面重點是這句:

DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;1

當有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)12345678

上面插入了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)1234567891011121314

上面插入了2條不同的資料,最終插入成功。

外部異常

外部異常不是由mysql內部丟擲的錯誤,而是由於sql的執行結果和我們期望的結果不一致的時候,我們需要對這種情況做一些處理,如回滾操作。

示例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 '賬戶餘額') 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);12345678910111213

下單操作涉及到操作上面的賬戶表,我們用儲存過程來模擬實現:

/*刪除儲存過程*/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 ;123456789101112131415161718192021222324252627282930313233343536

上面過程主要分為3步驟:驗證餘額、修改餘額變數、更新餘額。

開啟2個cmd視窗,連線mysql,同時執行下面操作:

USE javacode2018;CALL proc3(1001,100,@v_msg);select @v_msg;123

然後執行:

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)12345678910111213141516

上面出現了非常嚴重的錯誤:下單成功了2次,但是賬戶只扣了100。

上面過程是由於2個操作併發導致的,2個視窗同時執行第一步的時候看到了一樣的資料(看到的餘額都是1000),然後繼續向下執行,最終導致結果出問題了。

上面操作我們可以使用樂觀鎖來最佳化。

樂觀鎖的過程:用期望的值和目標值進行比較,如果相同,則更新目標值,否則什麼也不做。

樂觀鎖類似於java中的cas操作,這塊需要了解的可以點選:詳解CAS

我們可以在資金錶t_funds新增一個version欄位,表示版本號,每次更新資料的時候+1,更新資料的時候將version作為條件去執行update,根據update影響行數來判斷執行是否成功,最佳化上面的程式碼,見示例2。

示例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);123456789101112131415

建立儲存過程:

/*刪除儲存過程*/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 ;12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849

ROW_COUNT()可以獲取更新或插入後獲取受影響行數。將受影響行數放在v_update_count中。

然後根據v_update_count是否等於1判斷更新是否成功,如果成功則記錄訂單資訊並提交事務,否則回滾事務。

驗證結果:開啟2個cmd視窗,連線mysql,執行下面操作:

use javacode2018;CALL proc4(1001,100,@v_msg);select @v_msg;123

視窗1結果:

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)1234567891011121314151617

視窗2結果:

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)1234567891011121314151617

可以看到第一個視窗下單成功了,視窗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)123456789101112131415


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/30239065/viewspace-2726215/,如需轉載,請註明出處,否則將追究法律責任。

相關文章