MySQL全面瓦解14:事务
关于事务
我们在数据库中需要执行一个系列的操作的时候,要保证这个系列执行的连续性和完整性,要么整个系列的执行都成功,要么就全部失败(只要有一个步骤失败,其他均回滚到之前的状态),
保证不会存在一部分成功一部分失败的情况。这就是我们事务的职责。下面举个分苹果的例子:
A同学有3个苹果,B同学有2个苹果,如果A同学给一个苹果给B同学,那么A同学只剩下2个苹果,而B同学有了3个。步骤如下
1 update tname set apples=apples-1 where name = "A"; 2 update tname set apples=apples+1 where name = "B";
当然,这是理想情况下的结果。有可能会出错:A同学减去了一个苹果,然后执行B同学的加苹果的时候,系统宕掉了,B同学没有加成功,A同学的库存中无厘头少了一个苹果。
这时候我们就需要事务支持了,有事务的情况下,就有两种状态,要么都成功,要么都失败。
操作成功:A同学减去一个苹果,B同学增加一个苹果,最终A 2个,B 3个。
操作失败:A同学依旧是3个苹果,B同学依旧是2个苹果。
事务有如下特性(参考官方描述):
2、事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
3、事务用来管理 insert,update,delete 语句。
事务的四个特性(ACID)
一般来说,衡量事务必须满足四个特性:ACID,即 原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
原子性(Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable),下面会详细说明。
持久性(Durability):事务处理结束后,对数据的修改就是永久的,会持久化到硬盘上,即便系统故障也不会丢失。
显示和隐式事务
事务分为显示事务和隐式事务,
隐式事务:事务自动开启、提交或回滚,比如insert、update、delete语句,事务的开启、提交或回滚由mysql内部自动控制的
显示事务:事务需要手动开启、提交或回滚,由开发者自己控制。
自动提交
MySQL中事务默认是隐式事务(即自动提交(autocommit)模式为 ON),执行insert、update、delete操作的时候,数据库自动开启事务、提交或回滚事务。如下所示:
1 mysql> show variables like 'autocommit'; 2 +---------------+-------+ 3 | Variable_name | Value | 4 +---------------+-------+ 5 | autocommit | ON | 6 +---------------+-------+ 7 1 row in set
在自动提交模式下,如果没有start transaction显式地开始一个事务,那么每个sql语句都会被当做一个事务执行提交操作。
通过如下方式,可以关闭autocommit;需要注意的是,autocommit参数是针对连接的,在一个连接中修改了参数,不会对其他连接产生影响。如果你新开一个命令窗口,会恢复到默认值。
1 mysql> set autocommit = 0; 2 Query OK, 0 rows affected 3 4 mysql> show variables like 'autocommit'; 5 +---------------+-------+ 6 | Variable_name | Value | 7 +---------------+-------+ 8 | autocommit | OFF | 9 +---------------+-------+ 10 1 row in set
如果关闭了autocommit,则所有的sql语句都在一个事务中,直到执行了commit或rollback,该事务结束,同时开始了另外一个事务。
特殊操作
在MySQL中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务;比如DDL语句(create table/drop table/alter table)。
不过,常用的select、insert、update和delete命令,都不会强制提交事务。
手动操作事务的两种方式
改变默认事务提交策略
SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1 开启自动提交
这个跟上面的脚本大概一致,先设置为禁止自动提交事务,这样执行的修改并没有真正的到数据库中,等commit 或者 rollback来最终确认,再手动进行事务操作。
1 --1、设置不自动提交事务 2 set autocommit=0; 3 --2、手动进行事务操作 4 commit;|rollback;
提交示例:
1 mysql> set autocommit = 0; 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(5,'初三五班'); 5 Query OK, 1 row affected 6 7 mysql> commit; --提交操作 8 Query OK, 0 rows affected 9 10 mysql> select * from classes; 11 +---------+-----------+ 12 | classid | classname | 13 +---------+-----------+ 14 | 1 | 初三一班 | 15 | 2 | 初三二班 | 16 | 3 | 初三三班 | 17 | 4 | 初三四班 | 18 | 5 | 初三五班 | 19 +---------+-----------+ 20 5 rows in set
回滚示例:
1 mysql> set autocommit = 0; 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(6,'初三六班'); 5 Query OK, 1 row affected 6 7 mysql> rollback; --回滚操作 8 Query OK, 0 rows affected 9 10 mysql> select * from classes; 11 +---------+-----------+ 12 | classid | classname | 13 +---------+-----------+ 14 | 1 | 初三一班 | 15 | 2 | 初三二班 | 16 | 3 | 初三三班 | 17 | 4 | 初三四班 | 18 | 5 | 初三五班 | 19 +---------+-----------+ 20 5 rows in set
常规启用事务
START|BEGIN 开始一个事务
ROLLBACK 事务回滚
COMMIT 事务确认
这是典型的MySQL事务操作,其中start transaction标识事务开始,commit提交事务,将执行结果写入到数据库。如果sql语句执行出现问题,会调用rollback,回滚所有已经执行成功的sql语句。
当然,也可以在事务中直接使用rollback语句进行回滚。:
1 start transaction; --1、开启事务 2 /* 1条或者n条待执行的语句 */ 3 commit;|rollback; --2、手动进行事务操作
提交示例:
1 mysql> start transaction; 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(6,'初三六班'); 5 Query OK, 1 row affected 6 7 mysql> commit; 8 Query OK, 0 rows affected 9 10 mysql> select * from classes; 11 12 +---------+-----------+ 13 | classid | classname | 14 +---------+-----------+ 15 | 1 | 初三一班 | 16 | 2 | 初三二班 | 17 | 3 | 初三三班 | 18 | 4 | 初三四班 | 19 | 5 | 初三五班 | 20 | 6 | 初三六班 | 21 +---------+-----------+ 22 6 rows in set
回滚示例:
1 mysql> start transaction; 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(7,'初三七班'); 5 Query OK, 1 row affected 6 7 mysql> rollback; 8 Query OK, 0 rows affected 9 10 mysql> select * from classes; 11 +---------+-----------+ 12 | classid | classname | 13 +---------+-----------+ 14 | 1 | 初三一班 | 15 | 2 | 初三二班 | 16 | 3 | 初三三班 | 17 | 4 | 初三四班 | 18 | 5 | 初三五班 | 19 | 6 | 初三六班 | 20 +---------+-----------+ 21 6 rows in set
事务保存点的使用
事务保存点(savepoint),指的是对事务执行过程中做位置保存(类似我们打游戏时的存盘点),如果你写了一大堆的语句,但是有部分是你不想回滚的,想保留修改的状态,但是部分是你想回滚的。
这时候使用savepoint是个不错的方法。
1 mysql> start transaction; --开启事务 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(7,'初三七班'); 5 Query OK, 1 row affected 6 7 mysql> savepoint point1; --注意:这边设置了一个存盘点 8 Query OK, 0 rows affected 9 10 mysql> insert into classes values(8,'初三八班'); 11 Query OK, 1 row affected 12 13 mysql> rollback to point1; --记住这个语法,回滚到存盘点,存盘点之后的语句就丢弃了 14 Query OK, 0 rows affected 15 16 mysql> commit; 17 Query OK, 0 rows affected 18 19 mysql> select * from classes; --最后输出,确实只有存盘点之前的成功了 20 +---------+-----------+ 21 | classid | classname | 22 +---------+-----------+ 23 | 1 | 初三一班 | 24 | 2 | 初三二班 | 25 | 3 | 初三三班 | 26 | 4 | 初三四班 | 27 | 5 | 初三五班 | 28 | 6 | 初三六班 | 29 | 7 | 初三七班 | 30 +---------+-----------+ 31 7 rows in set
这边需要注意 savepoint 和 rollback to savepoint 的配合使用。
只读事务的使用
表示在事务中执行的是一些只读操作,如查询,但是不会做insert、update、delete操作,数据库内部对只读事务可能会有一些性能上的优化。
1 start transaction read only;
再只读操作的事务中进行增、删、改操作会报错,如下:
1 mysql> start transaction read only; 2 Query OK, 0 rows affected 3 4 mysql> select * from classes; 5 +---------+-----------+ 6 | classid | classname | 7 +---------+-----------+ 8 | 1 | 初三一班 | 9 | 2 | 初三二班 | 10 | 3 | 初三三班 | 11 | 4 | 初三四班 | 12 | 5 | 初三五班 | 13 | 6 | 初三六班 | 14 | 7 | 初三七班 | 15 +---------+-----------+ 16 7 rows in set 17 18 mysql> insert into classes values(8,'初三八班'); 19 1792 - Cannot execute statement in a READ ONLY transaction. --这边报出异常
事务的脏读、幻读、不可重复读
脏读:读取未提交数据
脏读就是指当一个事务A正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务B也访问这个数据,然后使用了这个脏数据。举个例子
时间顺序 | A事务 | B事务 |
T1 |
开始事务 | |
T2 |
开始事务 | |
T3 |
查询A同学有2个苹果 | |
T4 |
给A同学增加一个苹果(未提交) |
|
T5 |
查询A同学有3个苹果(读脏数据) | |
T6 | 添加苹果操作出现错误,回滚回2个苹果 | |
T7 | 提交事务 |
不可重复读:前后多次读取数据不一致
不可重复读指的是在事务A中先后多次读取同一个数据,读取的结果不一样,因为另外一个事务也访问该同一数据,并且可能修改这个数据,这种现象称为不可重复读。
脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。
时间顺序 | A事务 | B事务 |
T1 |
开始事务 | |
T2 |
开始事务 | |
T3 |
查询A同学有2个苹果 | |
T4 |
给A同学增加一个苹果(未提交) |
|
T5 |
提交事务 | |
T6 | 查询A同学有3个苹果(不可重复读) |
按照正确逻辑,事务B前后两次读取到的数据应该一致,这边一次读到的是2个,一次读到的是3个。
幻读:前后多次读取,数据总量不一致
在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。
时间顺序 | A事务 | B事务 |
T1 |
开始事务 | 开始事务 |
T2 |
第一次查询库存数据有2条 | |
T3 |
给A同学增加一个苹果(增加了一条库存数据) | |
T4 |
提交事务 |
|
T5 |
第二次查询库存数据有3条 |
按照正确逻辑,按照正确逻辑,事务B前后两次读取到的数据总量应该一致
不可重复读和幻读的区别
(1)不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2)幻读是读取了其他事务新增的数据,针对insert与delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
幻读和不可重复读都是指的一个事务范围内的操作受到其他事务的影响了。只不过幻读是重点在插入和删除,不可重复读重点在修改
事务的隔离级别
SQL标准中事务的隔离性(Isolation)定义了四种隔离级别,并规定了每种隔离级别下上述几个(脏读、不可重复读、幻读)问题是否存在。
一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:
隔离级别的分类
注意:幻读只会在 可重复读 级别中才会出现,其他级别下不存在。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交:Read Uncommitted |
✔ | ✔ | x |
读已提交:Read Committed |
× | ✔ | x |
可重复读:Repeatable Read |
× | x | ✔ |
串行化:Serializable |
× |
× | × |
查看|修改 隔离级别
查看当前的隔离级别,默认应该都是可重复读(Repeatable Read):
1 mysql> show variables like 'transaction_isolation'; 2 +-----------------------+-----------------+ 3 | Variable_name | Value | 4 +-----------------------+-----------------+ 5 | transaction_isolation | REPEATABLE-READ | 6 +-----------------------+-----------------+ 7 1 row in set
修改隔离级别:
找到MySQL安装目录中的my.init文件,会看到当前的隔离级别:REPEATABLE-READ。
1 # 隔离级别设置,READ-UNCOMMITTED读未提交,READ-COMMITTED读已提交,REPEATABLE-READ可重复读,SERIALIZABLE串行 2 transaction-isolation=REPEATABLE-READ
修改后重启MySQL即可,下面的各项操作都是在修改了对应的隔离级别之后的操作。
READ-UNCOMMITTED:读未提交
事物A和事物B,事物B未提交的数据,事物A可以读取到,这里读取到的数据叫做“脏数据”,这种隔离级别最低,一般理论上存在,数据库隔离级别大都高于该级别。
举个例子:学校新增加了7班和8班,教务主任进去录入了8班的班级信息,但是该事务并未提交,而8班班主任正好去查看班级信息,发现是存在的,摩拳擦掌准备录入这个班级的学生信息。可是教务主任发现班级信息写错了,应该先录入7班,于是迅速回滚了该事务。
最后8班的信息消失了。出现上述情况,就是我们所说的脏读 ,两个并发的事务,“事务A:查询班级信息”、“事务B:录入班级信息”,事务A读取了事务B尚未提交的数据。
数据基础:
1 mysql> select * from classes; 2 +---------+-----------+ 3 | classid | classname | 4 +---------+-----------+ 5 | 1 | 初三一班 | 6 | 2 | 初三二班 | 7 | 3 | 初三三班 | 8 | 4 | 初三四班 | 9 | 5 | 初三五班 | 10 | 6 | 初三六班 | 11 | 7 | 初三七班 | 12 +---------+-----------+ 13 7 rows in set
时间顺序 | 事务A | 事务B |
---|---|---|
T1 | start transaction; | |
T2 | select * from classes; | |
T3 | start transaction; | |
T4 | insert into classes values(8,'初三八班'); | |
T5 | select * from classes; | |
T6 | select * from classes; | |
T7 | rollback; | |
T8 | commit; |
说明:
事务A-T2:只有7条数据,事务A-T6:有8条数据,事务B-T6并未提交,此时事务A已经看到了事务B插入的数据,出现了脏读。
事务A-T2:只有7条数据,T6-A:有8条数据,查询到的结果不一样,出现不可重复读。
结论:读未提交情况下,可以读取到其他事务还未提交的数据,多次读取结果不一样,出现了脏读、不可重复读
READ-COMMITTED:读已提交
事物A和事物B,事物B提交完的数据,事物A才能读取到
这种隔离级别高于读未提交:即对方事物提交之后的数据,我当前事物才能读取到
这种隔离级别可以避免“脏数据” ,但会导致“不可重复读取”
数据基础跟上面一样
时间顺序 | 事务A | 事务B |
---|---|---|
T1 | start transaction; | |
T2 | select * from classes; | |
T3 | start transaction; | |
T4 | insert into classes values(8,'初三八班'); | |
T5 | select * from classes; | |
T6 | commit; | |
T7 | select * from classes; |
说明:
事务A-T5:只有7条数据,A看不到B新增的第8条数据,说明没有脏读。
事务A-T5:只有7条数据,事务A-T7:读到了8条数据,此时事务B已经提交了事务,事务A读取到了事务B提交的数据,说明可以读取到已提交的数据。
事务A-T5 和 事务A-T7:两次读取的数据结果不一样,说明不可重复读。
结论:读已提交情况下,无法读取到其他事务还未提交的数据,可以读取到其他事务已经提交的数据,多次读取结果不一样,未出现脏读,出现了读已提交、不可重复读。
REPEATABLE-READ:可重复读
可重复读是MySQL默认事务隔离级别
事务A和事务B,事务B提交之后的数据,事务A读取不到,即对方提交之后的数据,还是读取不到
事务B是可重复读取数据,隔离级别高于读已提交,这种隔离级别可以避免“不可重复读取”,达到可重复读取,但是可能导致“幻读”
1 mysql> select * from classes; 2 +---------+-----------+ 3 | classid | classname | 4 +---------+-----------+ 5 | 1 | 初三一班 | 6 | 2 | 初三二班 | 7 | 3 | 初三三班 | 8 | 4 | 初三四班 | 9 | 5 | 初三五班 | 10 | 6 | 初三六班 | 11 | 7 | 初三七班 | 12 | 8 | 初三八班 | 13 +---------+-----------+ 14 8 rows in set
时间顺序 | 事务A | 事务B |
---|---|---|
T1 | start transaction; | |
T2 | start transaction; | |
T3 | insert into classes values(9,'初三九班'); | |
T4 | select * from classes; | |
T5 | commit; | |
T6 | select * from classes; | |
T7 | select * from classes; | |
T8 | commit; | |
T9 | select * from classes; |
说明:
事务A-T4、事务A-T7:读到的是八条数据,事务B-T6:有数据,A事务下读不到B事务产生的数据,说明没有脏读。
事务A-T7:读到的是9条数据,这时事务B已经commit,事务A看不到事务B已提交的数据,A事务下两次读的结果一样,说明可重复读。
事务A-T9:读到的是9条数据。
结论:可重复读情况下,未出现脏读,未读取到其他事务已提交的数据,多次读取结果一致,即可重复读。
SERIALIZABLE:串行
SERIALIZABLE会让并发的事务串行执行。事务A和事务B,事务A在操作数据库时,事务B只能排队等待,这种隔离级别很少使用,吞吐量太低,用户体验差
这种级别可以避免“幻读”,每一次读取的都是数据库中真实存在数据,事务A与事务B串行,而不并发
数据基础同上
时间 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from classes; | |
T3 | start transaction; | |
T4 | insert into classes values(9,'初三九班'); | |
T5 | select * from classes; | |
T6 | commit; | |
T7 | commit; |
按时间顺序运行上面的命令,会发现事务B-T4这样会被阻塞,直到事务A执行完毕T6步骤。
可以看出来,事务只能串行执行了。串行情况下不存在脏读、不可重复读、幻读的问题了。
隔离级别选择注意点
1、读已提交(READ-COMMITTED)通常用的比较多,也是MySQL默认选项。
2、具体选择哪种需要结合具体的业务来选择,隔离级别越高,并发性也低,比如最高级别SERIALIZABLE
会让事物串行执行,并发操作变成串行了,会导致系统性能直接降低
事务典型用法
1 DROP PROCEDURE IF EXISTS t_test; 2 DELIMITER // 3 CREATE PROCEDURE t_test() 4 BEGIN 5 DECLARE t_error INTEGER; 6 DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET t_error = 1; 7 START TRANSACTION; 8 insert into students(studentname,score,classid) VALUE('A',99,3); 9 insert into students(studentname,score,classid) VALUE('A','lala',3); --这边执行错误,两个语句都会被回滚 10 IF t_error = 1 THEN 11 ROLLBACK; -- 有错误回滚 12 ELSE 13 COMMIT; --没错误提交 14 END IF; 15 END //DELIMITER; 16 CALL t_test();
这个是典型的用法,score是decimal类型,这两个语句都会被回滚。
MVCC了解
RR解决脏读、不可重复读、幻读等问题,使用的是MVCC(Multi-Version Concurrency Control)协议,即多版本的并发控制协议。
有多个请求来读取表中的数据时可以不采取任何操作,但是多个请求里有读请求,又有修改请求时必须有一种措施来进行并发控制。不然很有可能会造成不一致。
读写锁,解决上述问题很简单,只需用两种锁的组合来对读写请求进行控制即可,这两种锁被称为:
1、共享锁(shared lock),又叫做 读锁
读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。
2、排他锁(exclusive lock),又叫做 写锁
写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。
通过读写锁,可以做到读读可以并行,但是不能做到写读,写写并行。这边了解一下,后续专门一篇来说明MVCC的原理和实现机制分析。
总结
1、认识ACID(原子性、一致性、隔离性、持久性)特性及其实现原理
2、了解事务的脏读、幻读、不可重复读
3、了解事务的隔离级别以及原理