1. 程式人生 > >Mysql高手系列 - 第20篇:異常捕獲及處理詳解(實戰經驗)

Mysql高手系列 - 第20篇:異常捕獲及處理詳解(實戰經驗)

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。

這是Mysql系列第20篇。

環境:mysql5.7.25,cmd命令中進行演示。

程式碼中被[]包含的表示可選,|符號分開的表示可選其一。

需求背景

我們在寫儲存過程的時候,可能會出現下列一些情況:

  1. 插入的資料違反唯一約束,導致插入失敗
  2. 插入或者更新資料超過欄位最大長度,導致操作失敗
  3. update影響行數和期望結果不一致

遇到上面各種異常情況的時,可能需要我們能夠捕獲,然後可能需要回滾當前事務。

本文主要圍繞異常處理這塊做詳細的介紹。

此時我們需要使用遊標,通過遊標的方式來遍歷select查詢的結果集,然後對每行資料進行處理。

本篇內容

  • 異常分類詳解
  • 內部異常詳解
  • 外部異常詳解
  • 掌握樂觀鎖解決併發修改資料出錯的問題
  • update影響行數和期望結果不一致時的處理

準備資料

建立庫: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);

異常分類

我們將異常分為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 ;

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

示例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的執行結果和我們期望的結果不一致的時候,我們需要對這種情況做一些處理,如回滾操作。

示例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);
下單操作涉及到操作上面的賬戶表,我們用儲存過程來模擬實現:
/*刪除儲存過程*/
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步驟:驗證餘額、修改餘額變數、更新餘額。

開啟2個cmd視窗,連線mysql,同時執行下面操作:
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。

示例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判斷更新是否成功,如果成功則記錄訂單資訊並提交事務,否則回滾事務。

驗證結果:開啟2個cmd視窗,連線mysql,執行下面操作:
use javacode2018;
CALL proc4(1001,100,@v_msg);
select @v_msg;
視窗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)
視窗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)

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

也正常。

總結

  1. 異常分為Mysql內部異常和外部異常

  2. 內部異常由mysql內部觸發,外部異常是sql的執行結果和期望結果不一致導致的錯誤

  3. sql內部異常捕獲方式

    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;
  4. ROW_COUNT()可以獲取mysql中insert或者update影響的行數

  5. 掌握使用樂觀鎖(新增版本號)來解決併發修改資料可能出錯的問題

  6. begin end前面可以加標籤,LEAVE 標籤可以退出對應的begin end,可以使用這個來實現return的效果

Mysql系列目錄

  1. 第1篇:mysql基礎知識
  2. 第2篇:詳解mysql資料型別(重點)
  3. 第3篇:管理員必備技能(必須掌握)
  4. 第4篇:DDL常見操作
  5. 第5篇:DML操作彙總(insert,update,delete)
  6. 第6篇:select查詢基礎篇
  7. 第7篇:玩轉select條件查詢,避免採坑
  8. 第8篇:詳解排序和分頁(order by & limit)
  9. 第9篇:分組查詢詳解(group by & having)
  10. 第10篇:常用的幾十個函式詳解
  11. 第11篇:深入瞭解連線查詢及原理
  12. 第12篇:子查詢
  13. 第13篇:細說NULL導致的神坑,讓人防不勝防
  14. 第14篇:詳解事務
  15. 第15篇:詳解檢視
  16. 第16篇:變數詳解
  17. 第17篇:儲存過程&自定義函式詳解
  18. 第18篇:流程控制語句
  19. 第19篇:遊標詳解
  20. 第20篇:異常捕獲及處理詳解
  21. 第21篇:什麼是索引?

mysql系列大概有20多篇,喜歡的請關注一下,歡迎大家加我微信itsoku或者留言交流mysql相關技術!

相關推薦

Mysql高手系列 - 20異常捕獲處理實戰經驗

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 這是Mysql系列第20篇。 環境:mysql5.7.25,cmd命令中進行演示。 程式碼中被[]包含的表示可選,|符號分開的表示可選其一。 需求背景 我們在寫儲存過程的時候,可能會出現下列一些情況: 插入的資料違反唯一約束

Mysql高手系列 - 7玩轉select條件查詢,避免踩坑

這是Mysql系列第7篇。 環境:mysql5.7.25,cmd命令中進行演示。 電商中:我們想檢視某個使用者所有的訂單,或者想檢視某個使用者在某個時間段內所有的訂單,此時我們需要對訂單表資料進行篩選,按照使用者、時間進行過濾,得到我們期望的結果。 此時我們需要使用條件查詢來對指定表進行操作,我們需要了解sq

Mysql高手系列 - 8排序和分頁(order by & limit),存在的坑

這是Mysql系列第8篇。 環境:mysql5.7.25,cmd命令中進行演示。 程式碼中被[]包含的表示可選,|符號分開的表示可選其一。 本章內容 詳解排序查詢 詳解limit limit存在的坑 分頁查詢中的坑 排序查詢(order by) 電商中:我們想檢視今天所有成交的訂單,按照交易額從高到低排序

Mysql高手系列 - 9分組查詢,mysql分組有大坑!

這是Mysql系列第9篇。 環境:mysql5.7.25,cmd命令中進行演示。 本篇內容 分組查詢語法 聚合函式 單欄位分組 多欄位分組 分組前篩選資料 分組後篩選資料 where和having的區別 分組後排序 where & group by & having & order

Mysql高手系列 - 11深入瞭解連線查詢原理

這是Mysql系列第11篇。 環境:mysql5.7.25,cmd命令中進行演示。 當我們查詢的資料來源於多張表的時候,我們需要用到連線查詢,連線查詢使用率非常高,希望大家都務必掌握。 本文內容 笛卡爾積 內連線 外連線 左連線 右連線 表連線的原理 使用java實現連線查詢,加深理解 準備資料 2張表

Mysql高手系列 - 10常用的幾十個函式,收藏慢慢看

這是Mysql系列第10篇。 環境:mysql5.7.25,cmd命令中進行演示。 MySQL 數值型函式 函式名稱 作 用 abs 求絕對值 sqrt 求二次方根 mod 求餘數 ceil 和 ceiling 兩個函式功能相同,都是返回不小於引數的最小整數,即向上取整 floo

Mysql高手系列 - 12子查詢

這是Mysql系列第12篇。 環境:mysql5.7.25,cmd命令中進行演示。 本章節非常重要。 子查詢 出現在select語句中的select語句,稱為子查詢或內查詢。 外部的select查詢語句,稱為主查詢或外查詢。 子查詢分類 按結果集的行列數不同分為4種 標量子查詢(結果集只有一行一列) 列子查

Mysql高手系列 - 13細說NULL導致的神坑,讓人防不勝防

這是Mysql系列第13篇。 環境:mysql5.7.25,cmd命令中進行演示。 當資料的值為NULL的時候,可能出現各種意想不到的效果,讓人防不勝防,我們來看看NULL導致的各種神坑,如何避免? 比較運算子中使用NULL 認真看下面的效果 mysql> select 1>NULL; +--

Mysql高手系列 - 14事務

這是Mysql系列第14篇。 環境:mysql5.7.25,cmd命令中進行演示。 開發過程中,會經常用到資料庫事務,所以本章非常重要。 本篇內容 什麼是事務,它有什麼用? 事務的幾個特性 事務常見操作指令詳解 事務的隔離級別詳解 髒讀、不可重複讀、可重複讀、幻讀詳解 演示各種隔離級別產生的現象 關於隔離級

Mysql高手系列 - 21什麼是索引?

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 這是Mysql系列第21篇。 本文開始連續3篇詳解mysql索引: 第1篇來說說什麼是索引? 第2篇詳解Mysql中索引的原理 第3篇結合索引詳解關鍵字explain 本文為索引第一篇:我們來了解一下什麼是索引? 路人在搞

Mysql高手系列 - 18mysql流程控制語句高手進階

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 這是Mysql系列第18篇。 環境:mysql5.7.25,cmd命令中進行演示。 程式碼中被[]包含的表示可選,|符號分開的表示可選其一。 上一篇儲存過程&自定義函式,對儲存過程和自定義函式做了一個簡單的介紹,但是如

Mysql高手系列 - 19mysql遊標,此技能可用於救火

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 這是Mysql系列第19篇。 環境:mysql5.7.25,cmd命令中進行演示。 程式碼中被[]包含的表示可選,|符號分開的表示可選其一。 需求背景 當我們需要對一個select的查詢結果進行遍歷處理的時候,如何實現呢? 此

Mysql高手系列 - 22深入理解mysql索引原理,連載中

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。 這是Mysql系列第22篇。 背景 使用mysql最多的就是查詢,我們迫切的希望mysql能查詢的更快一些,我們經常用到的查詢有: 按照id查詢唯一一條

Mysql高手系列 - 24如何正確的使用索引?【高手進階】

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。 這是Mysql系列第24篇。 學習索引,主要是寫出更快的sql,當我們寫sql的時候,需要明確的知道sql為什麼會走索引?為什麼有些sql不走索引?sql

Mysql高手系列 - 26聊聊如何使用mysql實現分散式鎖

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。 這是Mysql系列第26篇。 本篇我們使用mysql實現一個分散式鎖。 分散式鎖的功能 分散式鎖使用者位於不同的機器中,鎖獲取成功之後,才可以對共享資源

Mysql高手系列 - 27mysql如何確保資料不丟失的?我們借鑑這種設計思想實現熱點賬戶高併發設計跨庫轉賬問題

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。 這是Mysql系列第27篇。 本篇文章我們先來看一下mysql是如何確保資料不丟失的,通過本文我們可以瞭解mysql內部確保資料不丟失的原理,學習裡面優秀

【搞定Java併發程式設計】20讀寫鎖 --- ReentrantReadWriteLock

上一篇:重入鎖 --- ReentrantLock 詳解(點選檢視) 本文目錄: 1、讀寫鎖的概述 2、讀寫鎖的具體實現 2.1、讀寫狀態的設計 2.2、鎖的獲取 2.2.1、寫鎖的獲取 2.2.2、讀鎖的獲取 2.3、鎖的釋放 2.3.1、寫鎖的釋放 2.3

Maven系列6生命週期和外掛,此看過之後在maven的理解上可以超越同級別90%的人!

maven系列目標:從入門開始開始掌握一個高階開發所需要的maven技能。 這是maven系列第6篇。 整個maven系列的內容前後是有依賴的,如果之前沒有接觸過maven,建議從第一篇看起,本文尾部有maven完整系列的連線。 前面我們使用maven過程中,用到了一些命令,如下: mvn clean mvn

Mysql高手系列 - 4天DDL常見操作彙總

這是Mysql系列第4篇。 環境:mysql5.7.25,cmd命令中進行演示。 DDL:Data Define Language資料定義語言,主要用來對資料庫、表進行一些管理操作。 如:建庫、刪庫、建表、修改表、刪除表、對列的增刪改等等。 文中涉及到的語法用[]包含的內容屬於可選項,下面做詳細說明。 庫的管

Mysql高手系列 - 5天DML操作彙總,確定你都會?

這是Mysql系列第5篇。 環境:mysql5.7.25,cmd命令中進行演示。 DML(Data Manipulation Language)資料操作語言,以INSERT、UPDATE、DELETE三種指令為核心,分別代表插入、更新與刪除,是必須要掌握的指令,DML和SQL中的select熟稱CRUD(增刪