一,前言
大家好,我是小墨。
这一章我们继续mysql的学习-----mysql事务。事务(transaction)定义为一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务。而mysql事务作为mysql的重点,工作中会频繁使用到,本文尽量深入浅出的讲全,通过联系mysql的锁,版本控制等内容讲解全,与大家分享。
二,事务基础
这一节我们介绍下mysql如何使用。
我们要知道事务是一个工作单元,我们需要把对mysql的一些操作同时作为一个工作单元进行操作,如果操作失败了还需要回退处理。那么事务过程分为:
- 开启事务:Start Transaction
- 事务结束:End Transaction
- 提交事务:Commit Transaction
- 回滚事务:Rollback Transaction
用于控制事务处理分为两种:自动提交,非自动提交,mysql控制语句如下:
- SET AUTOCOMMIT=0 禁止自动提交
- SET AUTOCOMMIT=1 开启自动提交
解释一下:set autocommit=0, 这个命令会将这个线程的自动提交关掉。 意味着如果你只执行一个select语句, 这个事务就启动了, 而且并不会自动提交。 这个事务持续存在直到你主动执行commit 或 rollback 语句, 或者断开连接
我们建议开启自动提交,默认mysql也是自动提交。使用显示语句方式启动事务。操作如下:
- BEGIN 或者 start transaction 开始一个事务
- ROLLBACK 事务回滚
- COMMIT 事务确认
举例如下:(菜鸟教程)
mysql> use RUNOOB;
Database changed
mysql> CREATE TABLE runoob_transaction_test( id int(5)) engine=innodb; # 创建数据表
Query OK, 0 rows affected (0.04 sec)
mysql> select * from runoob_transaction_test;
Empty set (0.01 sec)
mysql> begin; # 开始事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into runoob_transaction_test value(5);
Query OK, 1 rows affected (0.01 sec)
mysql> insert into runoob_transaction_test value(6);
Query OK, 1 rows affected (0.00 sec)
mysql> commit; # 提交事务
Query OK, 0 rows affected (0.01 sec)
mysql> select * from runoob_transaction_test;
+------+
| id |
+------+
| 5 |
| 6 |
+------+
2 rows in set (0.01 sec)
三,事务的基本特性和隔离级别
事务的基本特性:ACID
- A:atomic 原子性,一个事务中的操作要么全部成功,要么全部失败
- C:consistent 一致性,数据库总是从一个一致性的状态转换到另外一个一致性的状态,意思是前后变化的状态一致,中间sql执行有问题也不会影响前后的mysql数据。
- I:isolation 隔离性 ,一个事务的修改在最终提交前,对其他事务是不可见的
- D:duration 持久性,一旦事务提交,所做的修改就会永久保存到数据库中
以上是事务的具体特性,但是我们要注意到如果有多个事务之间并行处理该怎么处理呢?这时候引出事务的隔离级别,事务隔离级别分为: 读未提交(read uncommitted) 、读提交(read committed) 、 可重复读(repeatable read) 和串行化(serializable )
对于这四个级别怎么区分其实很简单,只需要注意两点:
- 事务操作有时间差距,当一个事务操作修改了数据,提交前或者提交后是否要给另一个事务看到
- 事务操作时需要执行select 操作查看数据,那多个事务之间查看数据是否要隔离
根据以上两点即可区分:
由第一点区分:
- 事务提交前可以查看变更: 读未提交(read uncommitted)
- 事务提交后可以查看变更:读提交(read committed)
由第二点区分:
- 多个事务之间查看数据都不隔离,即总是查看当前数据库的最新数据:读未提交(read uncommitted) 、读提交(read committed)
- 一个事务执行过程中看到的数据, 总是跟这个事务在启动时看到的数据是一致的:可重复读(repeatable read),当然为提交的事务对其他不可见
- 串行化(serializable ):串行化, 顾名思义是对于同一行记录, “写”会加“写锁”, “读”会加“读锁”。 当出现读写锁冲突
的时候, 后访问的事务必须等前一个事务执行完成, 才能继续执行,这里不加描述。
而我们mysql默认的事务级别为:可重复读
举例《丁奇mysql45讲》,我们对比下这四种事务区别
- 若隔离级别是“读未提交”, 则V1的值就是2。 这时候事务B虽然还没有提交, 但是结果已经被A看到了。 因此, V2、 V3也都是2。
- 若隔离级别是“读提交”, 则V1是1, V2的值是2。 事务B的更新在提交后才能被A看到。 所以,
V3的值也是2。- 若隔离级别是“可重复读”, 则V1、 V2是1, V3是2。 之所以V2还是1, 遵循的就是这个要求:
事务在执行期间看到的数据前后必须是一致的。- 若隔离级别是“串行化”, 则在事务B执行“将1改成2”的时候, 会被锁住。 直到事务A提交后,
事务B才可以继续执行。 所以从A的角度看, V1、 V2值是1, V3的值是2
四,ACID实现原理
mysql要实现事务,让一段时间的对mysql的多个操作作为一个工作业务,让mysql执行,我们谈谈如何实现ACID:
- A原子性:我们要让一次操作如果提交成功就全部成功,失败了就全部失败。失败由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql,成功则由mysql写入数据。
- C一致性:一般由代码层面来保证,我们使用时常会使用spring事务来操作mysql事务,万一失败会捕捉Exception使数据一起回滚。
- I隔离性由MVCC来保证,这里下一节介绍
- D持久性由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复。 举例:假如某个时刻数据库崩溃,在崩溃之前有事务A和事务B在执行,事务A已经提交,而事务B还未提交。当数据库重启进行 crash-recovery 时,就会通过Redo log将已经提交事务的更改写到数据文件,而还没有提交的就通过Undo log进行roll back。 这里后面有机会再补一篇文章讲一讲
五,事务隔离性原理(MVCC),幻读
我们这里先思考下为什么多事务下可以怎么处理读取和修改数据之间的问题,我们在java写代码时经常为了防止多线程写使用了锁机制,但是在这里呢?如果我们使用mysql锁来控制的话,让事务只能串行运行,我们要注意到mysql操作时多语句操作需要读写IO,还有可能其他业务操作的等待,这样会导致我们加了行锁后阻塞了多个事务操作,导致mysql效率极大降低,因此mysql使用多版本控制来解决。
MVCC:Multiversion concurrency control 多版本并发控制。一种并发控制的方法. 就是 同一份数据临时保留多版本的一种方式,,实际上保存了数据在某个时间点的快照
这里我们来科普下mysql如何实现多版本并发控制:InnoDB里面每个事务有一个唯一的事务ID, 叫作transaction id。 它是在事务开始的时候向InnoDB的事务系统申请的, 是按申请顺序严格递增的,每行数据也都是有多个版本的。 每次事务更新数据的时候, 都会生成一个新的数据版本。在实现上, InnoDB为每个事务构造了一个数组, 用来保存这个事务启动瞬间, 当前正在“活跃”的所有事务ID。 “活跃”指的就是, 启动了但还没提交。
这一段比较晦涩,我们举个例子来讲,还是用了《丁奇mysql45讲》例子
做如下假设:
1. 事务A开始前, 系统里面只有一个活跃事务ID是99;
2. 事务A、 B、 C的版本号分别是100、 101、 102, 且当前系统里只有这四个事务;
3. 三个事务开始前, (1,1) 这一行数据的row trx_id是90。
这样, 事务A的视图数组就是[99,100], 事务B的视图数组是[99,100,101], 事务C的视图数组是
[99,100,101,102]。
然后我们使用mysql默认隔离级别:可重复读,看看如何进行数据使用。
- 我们看例子,注意三个事务执行语句顺序分别为:C:SET K=K+1 ---> B:SET K=K+1 ---> A:SET K=K+1
- 注意我们假设A、 B、 C的版本号分别是100、 101、 102,事务A的视图数组就是[99,100], 事务B的视图数组是[99,100,101], 事务C的视图数组是[99,100,101,102]。k(1) 这一行数据的row trx_id是90
- 事务C进行数据更新,此时K(1)--->(2) row trx_id为102(标记上事务C的版本号)
- 第二个有效更新是事务B, 此时K(2)--->(3), 这时候, 这个数据的最新版本(即row trx_id) 是101, 而102又成为了历史版本
- 在事务A查询的时候, 其实事务B还没有提交, 但是它生成的K(3)这个版本已经变成当前版本了
- A读数据时,读取K(3) row trx_id = 101,大于自己的事务版本号100,在可重复读级别下,不读取,找上个历史版本
- 读取K(2) row trx_id = 102,大于自己的事务版本号100,不读
- 读取K(1) row trx_id = 90,小于自己的事务版本号100,读取出来。
以上就是mysql可重复读的读取和判断过程。对于可重复读, 一个事务只需要在启动的时候声明说, “以我启动的时刻为准, 如果一个数据版本是在我启动之前生成的, 就认;如果是我启动以后才生成的, 我就不认, 我必须要找到它的上一个版本”
那么我引入下图,对于各种隔离级别会发生的一些情况,根据以上过程我们其实可以自己来解释为啥会发生这种情况。
我举例分析下幻读为啥会在可重复读发生:
幻读指的是一个事务在前后两次查询同一个范围的时候, 后一次查询看到了前一次查询没有看到的行。
最简单的例子就是:
- A事务 select * from table where id = 1;(主键)
- B事务 insert into table values (1,1);
- A事务 insert into table values (1,1);
- select * from table where id = 1;
因为B事务插入了一条id为1的数据导致虽然之前A查出来id=1无,然后想插入发生主键冲突,这里其实是因为我们MVVC中多版本控制中并不能控制新增的数据。如果需要避免这种情况得加入锁,下篇文章再介绍。
六,总结
我们在这篇文章总结了事务过程,包括事务的基础,来龙去脉等,这里底层其实还跟锁挂上关系,我们下文在介绍。希望各位看完有收获,欢迎评论和点赞。
参考文章: