事务详解
- 什么是事务
- 数据库中的事务是指对数据库执⾏⼀批操作,这些操作最终要么全部执⾏成功,要么全部失败,不会存在部分成功的情况
- 事务的⼏个特性(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;
-- savepoint示例start transaction;insert into test1 values (1);savepoint part1;//设置⼀个保存点insert into test1 values (2);rollback to part1; //将savepint = part1的语句到当前语句之间所有的操作回滚commit; //提交事务-- [1]select * from test1;-- 只读事务中执⾏delete会报错start transaction read only;select * from test1;-- Cannot execute statement in a READ ONLY transaction.delete from test1;
- 事务中的⼀些问题
- 脏读:⼀个事务在执⾏的过程中读取到了其他事务还没有提交的数据
- 读已提交:即⼀个事务操作过程中可以读取到其他事务已经提交的数据
- 可重复读:⼀个事务操作中对于⼀个读取操作不管多少次,读取到的结果都是⼀样的
- 幻读:在可重复读的模式下才会出现,其他隔离级别中不会出现
- 幻读现象例⼦如下
可重复读模式下,⽐如有个⽤户表,⼿机号码为主键,有两个事物进⾏如下操作事务A操作如下: 1、打开事务 2、查询号码为X的记录,不存在 3、插⼊号码为X的数据,插⼊报错(为什么会报错,先向下看) 4、查询号码为X的记录,发现还是不存在(由于是可重复读,所以读取记录X还是不存在的)事物B操作:在事务A第2步操作时插⼊了⼀条X的记录,所以会导致A中第3步插⼊报错(违反了唯⼀约束)上⾯操作对A来说就像发⽣了幻觉⼀样,明明查询X(A中第⼆步、第四步)不存在,但却⽆法插⼊成功幻读可以这么理解:事务中后⾯的操作(插⼊号码X)需要上⾯的读取操作(查询号码X的记录)提供⽀持,但读取操作却不能⽀持下⾯的操作时产⽣的错误,就像发⽣了幻觉⼀样。
- 事务的隔离级别
- 读未提交: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,不过这个需要重启服务
/*删除函数*/DROP FUNCTION IF EXISTS fun1;/*声明结束符为$*/DELIMITER $/*创建函数*/CREATE FUNCTION fun1(v_max_a int) RETURNS int BEGIN /*⽤于保存结果*/ DECLARE v_total int DEFAULT 0; /*创建⼀个变量,⽤来保存当前⾏中a的值*/ DECLARE v_a int DEFAULT 0; /*创建⼀个变量,⽤来保存当前⾏中b的值*/ DECLARE v_b int DEFAULT 0; /*创建游标结束标志变量*/ DECLARE v_done int DEFAULT FALSE; /*创建游标*/ DECLARE cur_test1 CURSOR FOR SELECT a,b from test1 wherea<=v_max_a; /*设置游标结束时v_done的值为true,可以v_done来判断游标是否结束了*/ DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=TRUE; /*设置v_total初始值*/ SET v_total = 0; /*打开游标*/ OPEN cur_test1; /*使⽤Loop循环遍历游标*/ a:LOOP /*先获取当前⾏的数据,然后将当前⾏的数据放⼊v_a,v_b中,如果当前⾏⽆数据,v_done会被置为true*/ FETCH cur_test1 INTO v_a, v_b; /*通过v_done来判断游标是否结束了,退出循环*/ if v_done THEN LEAVE a; END IF; /*对v_total值累加处理*/ SET v_total = v_total + v_a + v_b; END LOOP; /*关闭游标*/ CLOSE cur_test1; /*返回结果*/ RETURN v_total; END $/*结束符置为;*/DELIMITER ;
游标中有个指针,当打开游标的时候,才会执⾏游标对应的select语句,这个指针会指向select结果中第⼀⾏记录。当调⽤fetch 游标名称时,会获取当前⾏的数据,如果当前⾏⽆数据,会触发NOT FOUND异常。当触发NOT FOUND异常的时候,我们可以使⽤⼀个变量来标记⼀下,如下代码:DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=TRUE;当游标⽆数据触发NOT FOUND异常的时候,将变量v_down的值置为TURE,循环中就可以通过v_down的值控制循环的退出。如果当前⾏有数据,则将当前⾏数据存到对应的变量中,并将游标指针指向下⼀⾏数据,如下语句:fetch 游标名称 into 变量列表;
-- 写个存储过程,遍历test2、test3,将test2中的a字段和test3中的b字段任意组合,插⼊到test1表中/*删除存储过程*/DROP PROCEDURE IF EXISTS proc1;/*声明结束符为$*/DELIMITER $/*创建存储过程*/CREATE PROCEDURE proc1() BEGIN /*创建⼀个变量,⽤来保存当前⾏中a的值*/ DECLARE v_a int DEFAULT 0; /*创建游标结束标志变量*/ DECLARE v_done1 int DEFAULT FALSE; /*创建游标*/ DECLARE cur_test1 CURSOR FOR SELECT a FROM test2; /*设置游标结束时v_done1的值为true,可以v_done1来判断游标cur_test1是否结束了*/ DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done1=TRUE; /*打开游标*/ OPEN cur_test1; /*使⽤Loop循环遍历游标*/ a:LOOP FETCH cur_test1 INTO v_a; /*通过v_done1来判断游标是否结束了,退出循环*/ if v_done1 THEN LEAVE a; END IF; BEGIN /*创建⼀个变量,⽤来保存当前⾏中b的值*/ DECLARE v_b int DEFAULT 0; /*创建游标结束标志变量*/ DECLARE v_done2 int DEFAULT FALSE; /*创建游标*/ DECLARE cur_test2 CURSOR FOR SELECT b FROM test3; /*设置游标结束时v_done1的值为true,可以v_done1来判断游标cur_test2是否结束了*/ DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done2=TRUE; /*打开游标*/ OPEN cur_test2; /*使⽤Loop循环遍历游标*/ b:LOOP FETCH cur_test2 INTO v_b; /*通过v_done1来判断游标是否结束了,退出循环*/ if v_done2 THEN LEAVE b; END IF; /*将v_a、v_b插⼊test1表中*/ INSERT INTO test1 VALUES (v_a,v_b); END LOOP b; /*关闭cur_test2游标*/ CLOSE cur_test2; END; END LOOP; /*关闭游标cur_test1*/ CLOSE cur_test1; END $/*结束符置为;*/DELIMITER ;
- 总结
- 游标⽤来对查询结果进⾏遍历处理
- 游标的使⽤过程:声明游标、打开游标、遍历游标、关闭游标
- 游标只能在存储过程和函数中使⽤
- 游标只能在存储过程和函数中使⽤
异常捕获及处理详解
- 内部异常:当我们执⾏⼀些sql的时候,可能违反了mysql的⼀些约束,导致mysql内部报错,如插⼊数据违反唯⼀约束,更新数据超时等,此时异常是由mysql内部抛出的,我们将这些由mysql抛出的异常统称为内部异常
-- a字段为主键,模拟Mysql内部异常-- 重点是:DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE;-- 当有sql异常的时候,会将变量hasSqlError的值置为TRUE。/*删除存储过程*/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 ;
- 外部异常:当我们执⾏⼀个update的时候,可能我们期望影响1⾏,但是实际上影响的不是1⾏数据,这种情况:sql的执⾏结果和期望的结果不⼀致,这种情况也我们也把他作为外部异常处理,我们将sql执⾏结果和期望结果不⼀致的情况统称为外部异常
-- 模拟电商中下单操作-- 2个窗同时执⾏第⼀步的时候看到了⼀样的数据,然后继续向下执⾏,最终导致结果出问题-- 可以使⽤乐观锁来优化/*删除存储过程*/DROP PROCEDURE IF EXISTS proc3;/*声明结束符为$*/DELIMITER $/*创建存储过程*/CREATE PROCEDURE proc3(v_user_id int,v_price decimal(10,2),OUT v_msgvarchar(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 ;
- 掌握乐观锁解决并发修改数据出错的问题:⽤期望的值和⽬标值进⾏⽐较,如果相同,则更新⽬标值,否则什么也不做
-- 创建表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 '⽤户账户表';-- ROW_COUNT()可以获取更新或插⼊后获取受影响⾏数。将受影响⾏数放在v_update_count中-- 然后根据v_update_count是否等于1判断更新是否成功,如果成功则记录订单信息并提交事务,否则回滚事务/*删除存储过程*/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 fromt_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 ;
- 总结
- 异常分为mysql内部异常和外部异常
- 内部异常由mysql内部触发,外部异常是sql的执⾏结果和期望结果不⼀致导致的错误
- sql内部异常捕获⽅式
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET hasSqlError=TRUE; - ROW_COUNT()可以获取mysql中insert或者update影响的⾏数
- 掌握使⽤乐观锁(添加版本号)来解决并发修改数据可能出错的问题
- begin end前⾯可以加标签,LEAVE 标签可以退出对应的begin end,可以使⽤这个来实现return的效果