二十、异常捕获及处理详解

摘要:
插入失败。插入或更新数据超过了字段的最大长度。操作失败。更新会影响行数和预期结果。本节介绍异常的分类。内部异常的详细信息。外部异常的详细信息。抓住乐观的锁。解决并发数据修改问题。使现代化当受影响的行数与预期结果不一致时,准备数据进行处理。/*创建数据库javacode2018*/drop数据库(如果存在javacode2018);

代码中被[]包含的表示可选,|符号分开的表示可选其一。

需求背景

我们在写存储过程的时候,可能会出现下列一些情况:

  1. 插入的数据违反唯一约束,导致插入失败

  2. 插入或者更新数据超过字段最大长度,导致操作失败

  3. update影响行数和期望结果不一致

遇到上面各种异常情况的时,可能需要我们能够捕获,然后可能需要回滚当前事务。

本文主要围绕异常处理这块做详细的介绍。

此时我们需要使用游标,通过游标的方式来遍历select查询的结果集,然后对每行数据进行处理。

本篇内容

  • 异常分类详解

  • 内部异常详解

  • 外部异常详解

  • 掌握乐观锁解决并发修改数据出错的问题

  • update影响行数和期望结果不一致时的处理

准备数据

  /*建库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 ;

  验证:

    DELETE FROM test1;

    CALL proc1(1,1);  

    上面先删除了test1表中的数据,然后调用存储过程proc1,由于test1表中的a字段是主键,插入第二条数据时违反了a字段的主键约束,mysql内部抛出了异常,导致第二条数据插入失败,最终只有第一条数据插入成功了。

    上面的结果和我们期望的不一致,我们希望要么都插入成功,要么失败。  

  示例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内部抛出的错误,而是由于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);
  

    下单操作涉及到操作上面的账户表,我们用存储过程来模拟实现:    
二十、异常捕获及处理详解第1张二十、异常捕获及处理详解第2张
 1 /*删除存储过程*/
 2 DROP PROCEDURE IF EXISTS proc3;
 3 /*声明结束符为$*/
 4 DELIMITER $
 5 /*创建存储过程*/
 6 CREATE PROCEDURE proc3(v_user_id int,v_price decimal(10,2),OUT v_msg varchar(64))
 7   a:BEGIN
 8     DECLARE v_available DECIMAL(10,2);
 9 
10     /*1.查询余额,判断余额是否够*/
11     select a.available into v_available from t_funds a where a.user_id = v_user_id;
12     if v_available<=v_price THEN
13       SET v_msg='账户余额不足!';
14       /*退出*/
15       LEAVE a;
16     END IF;
17 
18     /*模拟耗时5秒*/
19     SELECT sleep(5);
20 
21     /*2.余额减去price*/
22     SET v_available = v_available - v_price;
23 
24     /*3.更新余额*/
25     START TRANSACTION;
26     UPDATE t_funds SET available = v_available WHERE user_id = v_user_id;
27 
28     /*插入订单明细*/
29     INSERT INTO t_order (price) VALUES (v_price);
30 
31     /*提交事务*/
32     COMMIT;
33     SET v_msg='下单成功!';
34   END $
35 /*结束符置为;*/
36 DELIMITER ;
View Code

    上面过程主要分为3步骤:验证余额、修改余额变量、更新余额。  

    开启2个cmd窗口,连接mysql,同时执行下面操作:

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

  

    上面出现了非常严重的错误:下单成功了2次,但是账户只扣了100。

    上面过程是由于2个操作并发导致的,2个窗口同时执行第一步的时候看到了一样的数据(看到的余额都是1000),然后继续向下执行,最终导致结果出问题了。

    上面操作我们可以使用乐观锁来优化。

    乐观锁的过程:用期望的值和目标值进行比较,如果相同,则更新目标值,否则什么也不做。  

    乐观锁类似于java中的cas操作,这块需要了解的可以点击:详解CAS

    我们可以在资金表t_funds添加一个version字段,表示版本号,每次更新数据的时候+1,更新数据的时候将version作为条件去执行update,根据update影响行数来判断执行是否成功,优化上面的代码,见示例2。  

  示例2:对示例1进行优化。  

    创建表:    
二十、异常捕获及处理详解第1张二十、异常捕获及处理详解第4张
 1 DROP TABLE IF EXISTS t_funds;
 2 CREATE TABLE t_funds(
 3   user_id INT PRIMARY KEY COMMENT '用户id',
 4   available DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '账户余额',
 5   version INT DEFAULT 0 COMMENT '版本号,每次更新+1'
 6 ) COMMENT '用户账户表';
 7 
 8 DROP TABLE IF EXISTS t_order;
 9 CREATE TABLE t_order(
10   id int PRIMARY KEY AUTO_INCREMENT COMMENT '订单id',
11   price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '订单金额'
12 )COMMENT '订单表';
13 delete from t_funds;
14 /*插入一条数据,用户id为1001,余额为1000*/
15 INSERT INTO t_funds (user_id,available) VALUES (1001,1000);
16 创建存储过程:
View Code    
    创建存储过程:    
二十、异常捕获及处理详解第1张二十、异常捕获及处理详解第6张
 1 /*删除存储过程*/
 2 DROP PROCEDURE IF EXISTS proc4;
 3 /*声明结束符为$*/
 4 DELIMITER $
 5 /*创建存储过程*/
 6 CREATE PROCEDURE proc4(v_user_id int,v_price decimal(10,2),OUT v_msg varchar(64))
 7     a:BEGIN
 8     /*保存当前余额*/
 9     DECLARE v_available DECIMAL(10,2);
10     /*保存版本号*/
11     DECLARE v_version INT DEFAULT 0;
12     /*保存影响的行数*/
13     DECLARE v_update_count INT DEFAULT 0;
14 
15 
16     /*1.查询余额,判断余额是否够*/
17     select a.available,a.version into v_available,v_version from t_funds a where a.user_id = v_user_id;
18     if v_available<=v_price THEN
19       SET v_msg='账户余额不足!';
20       /*退出*/
21       LEAVE a;
22     END IF;
23 
24     /*模拟耗时5秒*/
25     SELECT sleep(5);
26 
27     /*2.余额减去price*/
28     SET v_available = v_available - v_price;
29 
30     /*3.更新余额*/
31     START TRANSACTION;
32     UPDATE t_funds SET available = v_available WHERE user_id = v_user_id AND version = v_version;
33     /*获取上面update影响行数*/
34     select ROW_COUNT() INTO v_update_count;
35 
36     IF v_update_count=1 THEN
37       /*插入订单明细*/
38       INSERT INTO t_order (price) VALUES (v_price);
39       SET v_msg='下单成功!';
40       /*提交事务*/
41       COMMIT;
42     ELSE
43       SET v_msg='下单失败,请重试!';
44       /*回滚事务*/
45       ROLLBACK;
46     END IF;
47   END $
48 /*结束符置为;*/
49 DELIMITER ;
View Code

    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内部异常和外部异常

  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的效果

  

免责声明:文章转载自《二十、异常捕获及处理详解》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇RTT学习之线程管理基于Jmeter+maven+Jenkins构建性能自动化测试平台下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

FineReport连接多维数据库示例及操作

1. 描述 FineReport连接多维数据库,首先要通过数据连接将多维数据库与FineReport连接起来,然后在数据连接的基础上新建多维数据库XMLA数据集,用于模板设计。 2.XMLA数据连接 2.1描述 多维数据库(Multi Dimensional Database,MDD),顾名思义,就是指将数据存储在多个维度中,简单来说,就是将数据存放在一个...

DFS学习

1、前言     学习数据结构的时候就已经了解了DFS原理,但是一直没有详细的复习和代码实现。再刷题的过程中发现很多题目都使用DFS,因此回头再好好梳理一下DFS的思路以及代码实现。 2、算法理论 2.1 理论知识 深度优先搜索算法(英语:Depth-First-Search,简称DFS)是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能...

CPF 入门教程

CPF C#跨平台桌面UI框架 系列教程 CPF 入门教程(一) CPF 入门教程 - 数据绑定和命令绑定(二) CPF 入门教程 - 样式和动画(三) CPF 入门教程 - 绘图(四)  CPF 入门教程 - 设计器和模板库的使用(五) CPF 入门教程 - 控件布局(六)  CPF 入门教程 - 属性和事件(七) CPF 入门教程 - 各个控件介绍(八...

Jmeter做接口测试

Jmeter操作手册以获取微信公众号access_token为例 一、操作步骤 创建集合,集合名可以是项目名称,这里改为“微信公众号测试” 创建一个线程组,命名为获取access_token 创建一个请求,命名为获取access_token 在接口文档里面获取信息,填写协议类型、服务器名称或ip、HTTP请求方法、路径,端口号默认,因为是GEt方...

InfluxDB 安装以及使用

InfluxDB InfluxDB简介:       InfluxDB 是一个开源分布式时序、事件和指标数据库。使用Go语言编写,无需外部依赖。其设计目标是实现分布式和水平伸缩扩展。        它有三大特性:        1. Time Series (时间序列):你可以使用与时间有关的相关函数(如最大,最小,求和等);        2. Metr...

python socket编程介绍

一、概述 socket 通常被称作“套接字”,用于描述IP地址和端口,是通讯链的句柄,应用程序通过socket向网络发送请求或者回应网络的请求。 socket起源于UNIX,在linux、UNIX中“一切皆文件”,对于文件用 打开、读写、关闭 模式来操作。socket就是该模式的一个实现,是一种特殊的文件。一些socket函数就是对其进行的操作(读IO、写...