事务详解

  • 什么是事务
    • 数据库中的事务是指对数据库执⾏⼀批操作,这些操作最终要么全部执⾏成功,要么全部失败,不会存在部分成功的情况
  • 事务的⼏个特性(ACID)
    • 原⼦性(Atomicity) :事务的整个过程如原⼦操作⼀样,最终要么全部成功,或者全部失败,这个原⼦性是从最终结果来看的,从最终结果来看这个过程是不可分割的
    • ⼀致性(Consistency) :⼀个事务必须使数据库从⼀个⼀致性状态变换到另⼀个⼀致性状态
    • 隔离性(Isoladon) :⼀个事务的执⾏不能被其他事务⼲扰。即⼀个事务内部的操作及使⽤的数据对并发的其他事务是隔离的,并发执⾏的各个事务之间不能互相⼲扰
    • 持久性(Durability) :⼀个事务⼀旦提交,他对数据库中数据的改变就应该是永久性的。当事务提交之后,数据会持久化到硬盘,修改是永久性的
  • Mysql中事务操作
    • mysql中事务默认是隐式事务,执⾏insert、update、delete操作的时候,数据库⾃动开启事务、提交或回滚事务
    • 是否开启隐式事务是由变量autocommit控制的
    • 隐式事务:事务⾃动开启、提交或回滚,⽐如insert、update、delete语句,事务的开启、提交或回滚由mysql内部⾃动控制的
    • 显式事务:事务需要⼿动开启、提交或回滚,由开发者⾃⼰控制。start transaction;开启事务,commit;提交,rollback;回滚
  • savepoint关键字
    • 在事务中我们执⾏了⼀⼤批操作,可能我们只想回滚部分数据
    • 我们可以将⼀⼤批操作分为⼏个部分,然后指定回滚某个部分
    • 需要结合rollback to sp1⼀起使⽤,可以将保存点sp1到rollback to之间的操作回滚掉
  • 只读事务
    • 表⽰在事务中执⾏的是⼀些只读操作,如查询,但是不会做insert、update、delete操作,数据库内部对只读事务可能会有⼀些性能上的优化
    • ⽤法如:start transaction read only;
  1. -- savepoint示例
  2. start transaction;
  3. insert into test1 values (1);
  4. savepoint part1;//设置⼀个保存点
  5. insert into test1 values (2);
  6. rollback to part1; //将savepint = part1的语句到当前语句之间所有的操作回滚
  7. commit; //提交事务
  8. -- [1]
  9. select * from test1;
  10. -- 只读事务中执⾏delete会报错
  11. start transaction read only;
  12. select * from test1;
  13. -- Cannot execute statement in a READ ONLY transaction.
  14. delete from test1;
  • 事务中的⼀些问题
    • 脏读:⼀个事务在执⾏的过程中读取到了其他事务还没有提交的数据
    • 读已提交:即⼀个事务操作过程中可以读取到其他事务已经提交的数据
    • 可重复读:⼀个事务操作中对于⼀个读取操作不管多少次,读取到的结果都是⼀样的
    • 幻读:在可重复读的模式下才会出现,其他隔离级别中不会出现
    • 幻读现象例⼦如下
  1. 可重复读模式下,⽐如有个⽤户表,⼿机号码为主键,有两个事物进⾏如下操作
  2. 事务A操作如下: 1、打开事务 2、查询号码为X的记录,不存在 3、插⼊号码为X的数
  3. 据,插⼊报错(为什么会报错,先向下看) 4、查询号码为X的记录,发现还是不存在
  4. (由于是可重复读,所以读取记录X还是不存在的)
  5. 事物B操作:在事务A2步操作时插⼊了⼀条X的记录,所以会导致A中第3步插⼊报错
  6. (违反了唯⼀约束)
  7. 上⾯操作对A来说就像发⽣了幻觉⼀样,明明查询XA中第⼆步、第四步)不存在,但却
  8. ⽆法插⼊成功
  9. 幻读可以这么理解:事务中后⾯的操作(插⼊号码X)需要上⾯的读取操作(查询号码X
  10. 的记录)提供⽀持,但读取操作却不能⽀持下⾯的操作时产⽣的错误,就像发⽣了幻觉⼀
  11. 样。
  • 事务的隔离级别
    • 读未提交:READ-UNCOMMITTED
      • 读未提交情况下,可以读取到其他事务还未提交的数据,多次读取结果不⼀样,出现了脏读、不可重复读
    • 读已提交:READ-COMMITTED
      • 读已提交情况下,⽆法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不⼀样,未出现脏读,出现了读已提交、不可重复读
    • 可重复读:REPEATABLE-READ
      • 可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果⼀致,即可重复读,但是会出现幻读情况
    • 串⾏:SERIALIZABLE
      • 让并发的事务串⾏执⾏(多个事务之间读写、写读、写写会产⽣互斥,效果就是串⾏执⾏,多个事务之间的读读不会产⽣互斥)。串⾏情况下不存在脏读、不可重复读、幻读的问题
    • 四种隔离级别越来越强,会导致数据库的并发性也越来越低
    • 读已提交(READ-COMMITTED)通常⽤的⽐较多

游标详解

  • 游标定义:是处理数据的⼀种⽅法,为了查看或者处理结果集中的数据,游标提供了
    在结果集中⼀次⼀⾏遍历数据的能⼒。游标只能在存储过程和函数中使⽤。
  • 游标作⽤:相当于⼀个指针,这个指针指向select的第⼀⾏数据,可以通过移动指针来遍历后⾯的数据
  • 游标使⽤步骤
    • 声明游标:这个过程只是创建了⼀个游标,需要指定这个游标需要遍历的select查询,声明游标时并不会去执⾏这个sql
    • 打开游标:打开游标的时候,会执⾏游标对应的select语句
    • 遍历数据:使⽤游标循环遍历select结果中每⼀⾏数据,然后进⾏处理
    • 关闭游标:游标使⽤完之后⼀定要关闭。
  • 游标执⾏过程详解
    • 声明游标:DECLARE 游标名称 CURSOR FOR 查询语句;,⼀个begin end中只能声明⼀个游标
    • 打开游标:open 游标名称;
    • 遍历游标:fetch 游标名称 into 变量列表;
      • 取出当前⾏的结果,将结果放在对应的变量中,并将游标指针指向下⼀⾏的数据
      • 当调⽤fetch的时候,会获取当前⾏的数据,如果当前⾏⽆数据,会引发mysql内部的NOT FOUND错误
    • 关闭游标:close 游标名称;,游标使⽤完毕之后⼀定要关闭
  • 单游标⽰例
    • 可能有问题:This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary
    • 解决办法1:执⾏ SET GLOBAL logbintrustfunctioncreators = 1;,不过 重启了就失效了,且有主从复制的时候从机必须要设置,不然会导致主从同步失败
    • 解决办法2:在my.cnf⾥⾯设置 log-bin-trust-function-creators=1,不过这个需要重启服务
  1. /*删除函数*/
  2. DROP FUNCTION IF EXISTS fun1;
  3. /*声明结束符为$*/
  4. DELIMITER $
  5. /*创建函数*/
  6. CREATE FUNCTION fun1(v_max_a int)
  7. RETURNS int
  8. BEGIN
  9. /*⽤于保存结果*/
  10. DECLARE v_total int DEFAULT 0;
  11. /*创建⼀个变量,⽤来保存当前⾏中a的值*/
  12. DECLARE v_a int DEFAULT 0;
  13. /*创建⼀个变量,⽤来保存当前⾏中b的值*/
  14. DECLARE v_b int DEFAULT 0;
  15. /*创建游标结束标志变量*/
  16. DECLARE v_done int DEFAULT FALSE;
  17. /*创建游标*/
  18. DECLARE cur_test1 CURSOR FOR SELECT a,b from test1 where
  19. a<=v_max_a;
  20. /*设置游标结束时v_done的值为true,可以v_done来判断游标是否结束了*/
  21. DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=TRUE;
  22. /*设置v_total初始值*/
  23. SET v_total = 0;
  24. /*打开游标*/
  25. OPEN cur_test1;
  26. /*使⽤Loop循环遍历游标*/
  27. a:LOOP
  28. /*先获取当前⾏的数据,然后将当前⾏的数据放⼊v_a,v_b中,如果当前⾏⽆数据,
  29. v_done会被置为true*/
  30. FETCH cur_test1 INTO v_a, v_b;
  31. /*通过v_done来判断游标是否结束了,退出循环*/
  32. if v_done THEN
  33. LEAVE a;
  34. END IF;
  35. /*对v_total值累加处理*/
  36. SET v_total = v_total + v_a + v_b;
  37. END LOOP;
  38. /*关闭游标*/
  39. CLOSE cur_test1;
  40. /*返回结果*/
  41. RETURN v_total;
  42. END $
  43. /*结束符置为;*/
  44. DELIMITER ;
  • 游标过程详解
  1. 游标中有个指针,当打开游标的时候,才会执⾏游标对应的select语句,这个指针会指向
  2. select结果中第⼀⾏记录。
  3. 当调⽤fetch 游标名称时,会获取当前⾏的数据,如果当前⾏⽆数据,会触发NOT FOUND
  4. 异常。
  5. 当触发NOT FOUND异常的时候,我们可以使⽤⼀个变量来标记⼀下,如下代码:
  6. DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=TRUE;
  7. 当游标⽆数据触发NOT FOUND异常的时候,将变量v_down的值置为TURE,循环中就可以
  8. 通过v_down的值控制循环的退出。
  9. 如果当前⾏有数据,则将当前⾏数据存到对应的变量中,并将游标指针指向下⼀⾏数据,
  10. 如下语句:
  11. fetch 游标名称 into 变量列表;
  • 嵌套游标⽰例
  1. -- 写个存储过程,遍历test2test3,将test2中的a字段和test3中的b字段任意组合,插⼊到test1表中
  2. /*删除存储过程*/
  3. DROP PROCEDURE IF EXISTS proc1;
  4. /*声明结束符为$*/
  5. DELIMITER $
  6. /*创建存储过程*/
  7. CREATE PROCEDURE proc1()
  8. BEGIN
  9. /*创建⼀个变量,⽤来保存当前⾏中a的值*/
  10. DECLARE v_a int DEFAULT 0;
  11. /*创建游标结束标志变量*/
  12. DECLARE v_done1 int DEFAULT FALSE;
  13. /*创建游标*/
  14. DECLARE cur_test1 CURSOR FOR SELECT a FROM test2;
  15. /*设置游标结束时v_done1的值为true,可以v_done1来判断游标cur_test1是否结束
  16. 了*/
  17. DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done1=TRUE;
  18. /*打开游标*/
  19. OPEN cur_test1;
  20. /*使⽤Loop循环遍历游标*/
  21. a:LOOP
  22. FETCH cur_test1 INTO v_a;
  23. /*通过v_done1来判断游标是否结束了,退出循环*/
  24. if v_done1 THEN
  25. LEAVE a;
  26. END IF;
  27. BEGIN
  28. /*创建⼀个变量,⽤来保存当前⾏中b的值*/
  29. DECLARE v_b int DEFAULT 0;
  30. /*创建游标结束标志变量*/
  31. DECLARE v_done2 int DEFAULT FALSE;
  32. /*创建游标*/
  33. DECLARE cur_test2 CURSOR FOR SELECT b FROM test3;
  34. /*设置游标结束时v_done1的值为true,可以v_done1来判断游标cur_test2是否
  35. 结束了*/
  36. DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done2=TRUE;
  37. /*打开游标*/
  38. OPEN cur_test2;
  39. /*使⽤Loop循环遍历游标*/
  40. b:LOOP
  41. FETCH cur_test2 INTO v_b;
  42. /*通过v_done1来判断游标是否结束了,退出循环*/
  43. if v_done2 THEN
  44. LEAVE b;
  45. END IF;
  46. /*将v_a、v_b插⼊test1表中*/
  47. INSERT INTO test1 VALUES (v_a,v_b);
  48. END LOOP b;
  49. /*关闭cur_test2游标*/
  50. CLOSE cur_test2;
  51. END;
  52. END LOOP;
  53. /*关闭游标cur_test1*/
  54. CLOSE cur_test1;
  55. END $
  56. /*结束符置为;*/
  57. DELIMITER ;
  • 总结
    • 游标⽤来对查询结果进⾏遍历处理
    • 游标的使⽤过程:声明游标、打开游标、遍历游标、关闭游标
    • 游标只能在存储过程和函数中使⽤
    • 游标只能在存储过程和函数中使⽤

异常捕获及处理详解

  • 内部异常:当我们执⾏⼀些sql的时候,可能违反了mysql的⼀些约束,导致mysql内部报错,如插⼊数据违反唯⼀约束,更新数据超时等,此时异常是由mysql内部抛出的,我们将这些由mysql抛出的异常统称为内部异常
  1. -- a字段为主键,模拟Mysql内部异常
  2. -- 重点是:DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;
  3. -- 当有sql异常的时候,会将变量hasSqlError的值置为TRUE
  4. /*删除存储过程*/
  5. DROP PROCEDURE IF EXISTS proc2;
  6. /*声明结束符为$*/
  7. DELIMITER $
  8. /*创建存储过程*/
  9. CREATE PROCEDURE proc2(a1 int,a2 int)
  10. BEGIN
  11. /*声明⼀个变量,标识是否有sql异常*/
  12. DECLARE hasSqlError int DEFAULT FALSE;
  13. /*在执⾏过程中出任何异常设置hasSqlError为TRUE*/
  14. DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;
  15. /*开启事务*/
  16. START TRANSACTION;
  17. INSERT INTO test1(a) VALUES (a1);
  18. INSERT INTO test1(a) VALUES (a2);
  19. /*根据hasSqlError判断是否有异常,做回滚和提交操作*/
  20. IF hasSqlError THEN
  21. ROLLBACK;
  22. ELSE
  23. COMMIT;
  24. END IF;
  25. END $
  26. /*结束符置为;*/
  27. DELIMITER ;
  • 外部异常:当我们执⾏⼀个update的时候,可能我们期望影响1⾏,但是实际上影响的不是1⾏数据,这种情况:sql的执⾏结果和期望的结果不⼀致,这种情况也我们也把他作为外部异常处理,我们将sql执⾏结果和期望结果不⼀致的情况统称为外部异常
  1. -- 模拟电商中下单操作
  2. -- 2个窗同时执⾏第⼀步的时候看到了⼀样的数据,然后继续向下执⾏,最终导致结果出问题
  3. -- 可以使⽤乐观锁来优化
  4. /*删除存储过程*/
  5. DROP PROCEDURE IF EXISTS proc3;
  6. /*声明结束符为$*/
  7. DELIMITER $
  8. /*创建存储过程*/
  9. CREATE PROCEDURE proc3(v_user_id int,v_price decimal(10,2),OUT v_msg
  10. varchar(64))
  11. a:BEGIN
  12. DECLARE v_available DECIMAL(10,2);
  13. /*1.查询余额,判断余额是否够*/
  14. select a.available into v_available from t_funds a where a.user_id
  15. = v_user_id;
  16. if v_available<=v_price THEN
  17. SET v_msg='账户余额不⾜!';
  18. /*退出*/
  19. LEAVE a;
  20. END IF;
  21. /*模拟耗时5秒*/
  22. SELECT sleep(5);
  23. /*2.余额减去price*/
  24. SET v_available = v_available - v_price;
  25. /*3.更新余额*/
  26. START TRANSACTION;
  27. UPDATE t_funds SET available = v_available WHERE user_id =
  28. v_user_id;
  29. /*插⼊订单明细*/
  30. INSERT INTO t_order (price) VALUES (v_price);
  31. /*提交事务*/
  32. COMMIT;
  33. SET v_msg='下单成功!';
  34. END $
  35. /*结束符置为;*/
  36. DELIMITER ;
  • 掌握乐观锁解决并发修改数据出错的问题:⽤期望的值和⽬标值进⾏⽐较,如果相同,则更新⽬标值,否则什么也不做
  1. -- 创建表
  2. DROP TABLE IF EXISTS t_funds;
  3. CREATE TABLE t_funds(
  4. user_id INT PRIMARY KEY COMMENT '⽤户id',
  5. available DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '账户余额',
  6. version INT DEFAULT 0 COMMENT '版本号,每次更新+1'
  7. ) COMMENT '⽤户账户表';
  8. -- ROW_COUNT()可以获取更新或插⼊后获取受影响⾏数。将受影响⾏数放在
  9. v_update_count
  10. -- 然后根据v_update_count是否等于1判断更新是否成功,如果成功则记录订单信息并提交事务,否则回滚事务
  11. /*删除存储过程*/
  12. DROP PROCEDURE IF EXISTS proc4;
  13. /*声明结束符为$*/
  14. DELIMITER $
  15. /*创建存储过程*/
  16. CREATE PROCEDURE proc4(v_user_id int,v_price decimal(10,2),OUT v_msg
  17. varchar(64))
  18. a:BEGIN
  19. /*保存当前余额*/
  20. DECLARE v_available DECIMAL(10,2);
  21. /*保存版本号*/
  22. DECLARE v_version INT DEFAULT 0;
  23. /*保存影响的⾏数*/
  24. DECLARE v_update_count INT DEFAULT 0;
  25. /*1.查询余额,判断余额是否够*/
  26. select a.available,a.version into v_available,v_version from
  27. t_funds a where a.user_id = v_user_id;
  28. if v_available<=v_price THEN
  29. SET v_msg='账户余额不⾜!';
  30. /*退出*/
  31. LEAVE a;
  32. END IF;
  33. /*模拟耗时5秒*/
  34. SELECT sleep(5);
  35. /*2.余额减去price*/
  36. SET v_available = v_available - v_price;
  37. /*3.更新余额*/
  38. START TRANSACTION;
  39. UPDATE t_funds SET available = v_available WHERE user_id =
  40. v_user_id AND version = v_version;
  41. /*获取上⾯update影响⾏数*/
  42. select ROW_COUNT() INTO v_update_count;
  43. IF v_update_count=1 THEN
  44. /*插⼊订单明细*/
  45. INSERT INTO t_order (price) VALUES (v_price);
  46. SET v_msg='下单成功!';
  47. /*提交事务*/
  48. COMMIT;
  49. ELSE
  50. SET v_msg='下单失败,请重试!';
  51. /*回滚事务*/
  52. ROLLBACK;
  53. END IF;
  54. END $
  55. /*结束符置为;*/
  56. DELIMITER ;
  • 总结
    • 异常分为mysql内部异常和外部异常
    • 内部异常由mysql内部触发,外部异常是sql的执⾏结果和期望结果不⼀致导致的错误
    • sql内部异常捕获⽅式
      DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;
    • ROW_COUNT()可以获取mysql中insert或者update影响的⾏数
    • 掌握使⽤乐观锁(添加版本号)来解决并发修改数据可能出错的问题
    • begin end前⾯可以加标签,LEAVE 标签可以退出对应的begin end,可以使⽤这个来实现return的效果