目录
概念:
事务是一组操作,这些操作要么全部执行成功,要么全部失败回滚。
解释:
1.操作的意思就是一条或多条SQL语句组成,这些语句在逻辑上存在相关性,共同完成一个任务。
2.而操作要么全执行成功,要么全失败回滚的属性就是事务的原子性,除此之外事务还有持久性、隔离性、一致性。
为什么出现事务?
解决用户访问数据库的潜在问题,比如网络异常,服务器宕机,数据不一致,并发冲突,数据丢失。
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中如果发生错误,则会自动回滚到事务开始前的状态,不会对数据库产生任何影响。
持久性(Consistency):事务提交后,对数据库的修改就是永久的,即使系统崩溃也将保存在数据库中。
隔离性(Isolation):数据库允许多个事务同时访问同一份数据,隔离性保证事务在并发执行和串行结果一致。
一致性(Durability):在事务的开始和结束,数据库的完整性约束必须得到保持。
说明:
Engine:存储引擎的名称。
Support:服务器对存储引擎的支持级别,YES表示支持,NO表示不支持,DEFAULT表示数据库默认使用的存储引擎。
Comment:存储引擎的简要说明。
Transaction:表示存储引擎是否支持事务,可以看到MySQL中只有InnoDB存储引擎支持事务。
XA:表示存储引擎是否支持XA事务。
Savepoints:表示存储引擎是否支持保存点。
事务常见的提交方式有两种,分别是自动提交和手动提交。
通过show variables like 命令查看全局变量 autocommit,可以查看事务的自动提交是否被打开:
ON表示自动提交打开,OFF表示自动提交关闭。
用 SET 来改变 MySQL 的自动提交模式。
演示1:事务的开始和回滚
执行下面SQL:
- //1.建表
- //2.查看提交方式
- //3.开始事务
- //4.设置保存点1
- //5.插入数据1
- //6.设置保存点2
- //7.插入数据2
- //8.查看数据
- //9.回滚到保存点2
- //10.查看数据,数据2消失了
- //11.回滚到最初
- //12.查找数据,数据1也消失了
- create table if not exists account(
- id int primary key,
- name varchar(50) not null default '',
- blance decimal(10,2) not null default 0.0
- )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
-
- show variables like 'autocommit';
-
- start transaction;
-
- savepoint save1;
-
- insert into account values (1, '张三', 100);
-
- savepoint save2;
-
- insert into account values (2, '李四', 10000);
-
- select * from account;
-
- rollback to save2;
-
- select * from account;
-
- rollback;
-
- select * from account;
演示结果:rollback可以回滚到指定保存点或事务最开始的状态。
演示2:证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
执行SQL:
1.查看当前会话隔离级别
2.设置当前会话隔离级别为读未提交
3.查看表
4.开始事务
5.插入数据
6.提交事务
7.使用ctrl + 、异常终止MySQL
8.在终端B上查看表
- select @@session.tx_isolation;
- set session transaction isolation level READ UNCOMMITTED;
- select * from account;
- begin ;
- insert into account values(1,'张三',100);
- commit;
-
- //终端B
- select * from account;
-
输出:
异常终止终端A后在终端B仍然可以看到数据,可见commit的数据持久化了:
演示3:begin操作会自动更改提交方式(事务中的所有的SQL语句都需要显示使用COMMIT语句进行提交),不受MySQL是否自动提交影响
执行SQL:
修改自动提交为OFF,开启事务,往表插入数据,使用ctrl + \异常终止MySQL,在终端B查看数据是否回滚。
- select * from account;
- show variables like 'autocommit';
- set autocommit=0;
- show variables like 'autocommit';
- begin;
- insert into account values(2,'李四',10000);
- select * from account;
-
- //终端B
- //终端A崩溃前
- select * from account;
- //终端A崩溃后
- select * from account;
输出:
演示4:证明单条SQL与事务的关系
执行SQL:
- //实验一
- select * from account;
- show variables like 'autocommit';
- set autocommit=0;
- insert into account values(2,'李四',10000);
-
- //终端B
- //终端A崩溃前
- select * from account;
-
- //终端A崩溃后
- select * from account;
-
-
- //实验二
- //终端A
- show variables like 'autocommit';
- set autocommit=1;
- show variables like 'autocommit';
- select * from account;
- insert into account values(2,'李四',10000);
- select * from account;
-
- //终端B
- //终端A崩溃前
- select * from account;
-
- //终端A崩溃后
- select * from account;
做两个实验,一个是自动提交模式,一个是手动提交,验证数据的持久性。
实验一(手动提交),终端A:
实验一,终端B:(可见手动提交下,单条SQL不具有持久性)
实验二(自动提交),终端A:
实验二(自动提交),终端B:
结论总结:
1.begin或者start transaction开始的事务,事务便必须要通过commit提交,才会持久化,与是
否设置set autocommit无关。2.事务可以手动回滚,同时,当操作异常,MySQL会自动回滚。3.InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。4.如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交) 。
5.如果一个事务被提交了(commit),则不可以回退(rollback) 。6.事务执行中可以选择回退到哪个保存点。7.InnoDB 支持事务, MyISAM 不支持事务。
基本概念:
脏读:一个事务在执行过程中读取到了另一个事务未提交的数据。
不可重复读:事务在执行过程中,读取到的某个数据后,另一个事务修改了该数据,导致第一个事务重新读取数据的结果不一致。
幻读:一个事务在读取某个范围的数据时,另一个事务插入了新的数据,导致第一个事务重新读取该范围的数据时,发现有新的数据出现。
读未提交【Read Uncommitted】:所有的事务都可以看到其他事务没有提交的执行结果。相当于没有隔离性,会导致脏读、幻读、不可重复读问题。
读提交【Read Commited】:该隔离级别的大多数数据库的默认隔离级别,在事务中只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,多次select可能结果不一样。
可重复读【Repeatable Read】:MySQL默认的隔离级别,确保同一个事物在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
串行化【Serializable】事务的最高隔离级别,通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。它在每个读的数据行上加上共享锁,可能会导致超时和锁竞争(效率低)。
隔离级别如何实现:通过锁实现,常见锁有表锁、行锁、读锁、写锁、间隙锁(GAP)、Next-Key锁(GAP+行锁)等。
查看全局隔离级别:
SELECT @@global.tx_isolation;
查看当前会话全局隔离级别:
SELECT @@session.tx_isolation;
查看隔离级别(默认同上):
SELECT @@tx_isolation;
设置当前会话或全局隔离级别:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERALIZABLE}
设置全局隔离级别为读未提交,在终端A开启事务并更新指定行,在终端A提交事务前,在终端B看到了未commit的数据(即脏读)。
终端A:
终端B:
终端B的事务执行过程中读到了终端A更新但未commit的数据,属于脏读。
设置全局隔离级别为读提交,在终端A开启事务并更新数据,在终端B开启事务并在终端A事务提交前后读取同一数据读到了不同的数据,即发生了不可重复读的现象。
终端A:
终端B:
在同一事务内,在不同时间段,读取到了不同的值,即不可重复读。
设置全局隔离级别为可重复读,在终端A开启事务,并更新数据,在终端B开启事务,并在终端A事务提交前后读取同一数据读到了相同的数据,即可重复读。将更新数据改成插入数据,再实验一次,发现没有发生读取数据也是读到了相同的数据,即没有发生幻读。
注意:设置隔离级别后要退出mysql客户端重新启动。
终端A:
终端B:
可见在终端A的事务提交前后,在终端B的事务中读取到的数据都是一致的,也即具有可重复读的性质。
倘若将update数据变成插入数据会如何:
终端A:
终端B:
可见还是满足了可重复读,没有出现幻读现象,这是因为MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁 (GAP+行锁)解决的)。
注意:Next-Key锁 就是间隙锁和记录锁的组合。(源自MySQL官网)
设置全局隔离级别为串行化,其中终端A更新数据后,在终端B开启事务,并读取数据,发现阻塞住了,当终端A事务提交后,终端B才看到数据。
终端A:
终端B:
总结:
较低的隔离级别可以提高并发性能,但可能会引入数据一致性问题;而较高的隔离级别可以保证数据一致性,但可能降低并发性能。
不可重复读关注的是修改和删除:读取过的数据,再次读取处理就发现值不一样了。
幻读的重点在于新增:同样的条件,第一次和第二次读出来的记录数不一样。
MySQL默认的隔离级别是可重复读,也是兼顾数据一致性和并发性考虑的较优选择。
事务在执行过程中,将数据库从一个一致的状态转换为另一个一致的状态。
一致性和用户业务逻辑强相关,MySQL提供技术支持,一致性还需要用户业务逻辑做支撑,也就是一致性由用户决定。技术上,有原子性、隔离性、持续性保证一致性。
数据库的并发场景有三种:
读-读:没有问题,不用并发控制。
读-写:有线程安全问题,可能会造成事务隔离性问题,即可能遇到脏读、幻读、不可重复读。
写-写:有线程安全问题,可能会存在更新丢失问题。
数据库的更新丢失问题是指在并发环境下,多个事务同时对同一数据进行更新操作时可能出现的问题。
MVCC(Multi-Version Concurrency Control):多版本并发控制
多版本并发控制是一种用来解决读 - 写冲突的无锁并发控制。
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照,所以MVCC可以为数据库解决下面问题:
并发读写时,读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。
解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题。
DB_TRX_ID:6byte,最近修改事务ID,记录创建这条记录/最后一次修改该记录的事务ID。
DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本
DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引(数据行按照索引的顺序存储在磁盘上,具有相邻的物理位置)。
还有一个flag隐藏字段,记录这数据被更新或删除,记录删除只需标记flag即可。
MySQL中的一段内存缓存区,用来保存日志数据。
假设表中刚插入了一条记录如下,因为不知道创建该记录的事务ID,隐式主键,首条记录也没有回滚指针,所以将他们设置成默认值:
现在执行事务1,对表记录进行修改,修改张三名字为李四。
修改记录前,对记录加行锁,将原本记录拷贝到undo log中,然后写入新的数据,事务ID为1,回滚指针指向副本记录,表示新记录的上个版本就是它。
然后提交事务,释放锁。
事务1执行后:
现在再执行事务2,对表中记录进行修改,将age改成19.
修改记录前,对记录加行锁,将原本记录拷贝并头插入undo log中。
然后修改记录,事务ID为2,回滚指针指向上一个版本的记录。
提交事务2,释放锁。
事务2执行后:
于是我们就有了一个基于链表记录的历史版本链,回滚就是用版本链中的版本覆盖当前数据。
这个历史版本链不仅用于数据回滚,还用于其他事务读取历史版本,所以只有当前记录提交且其他事务与版本链无关的时候,该历史版本链才能删除。
插入和删除的时候如何维护版本链?
删除:将数据拷贝一份放入undo log中,并将该记录的隐藏flag删除字段置为1,这样回滚后flag字段又变成0,相当于删除的数据又恢复了。
插入:新插入的数据没有历史版本,为了回滚操作,新插入的数据拷贝一份放到undo log中,且undo log中的记录的flag标记为1,回滚后就相当于数据被删除了。
上面的一个个版本,我们可以称之为一个个快照。
概念:
快照读:读取历史版本的数据。
当前读:读取当前版本的数据。
事务对数据进行增删改的时候,操作的都是最新记录,即当前读,需要进行加锁保护。
事务在进行select查询的时候,既可能是当前读也可能是快照读,如果是当前读,那也需要进行加锁保护,但如果是快照读,那就不需要加锁,因为历史版本不会被修改,也就是可以并发执行,提高了效率,这也就是MVCC的意义所在。
Read View就是事务进行快照读操作时产生的读视图(Read View)。
在该事务执行的快照读的那一刻,会生成数据库系统当前的快照,记录并维护系统当前活跃事务的ID。(每个事务开启,都分配一个递增的ID,越新的事务,事务ID越大)
Read View在MySQL源码中是一个类,用来可见性判断的,判断当前事务能看到哪个版本的数据。
其基本组成如下:
- class ReadView {
- // 省略...
- private:
- /** 高水位,大于等于这个ID的事务均不可见*/
- trx_id_t m_low_limit_id;
- /** 低水位:小于这个ID的事务均可见 */
- trx_id_t m_up_limit_id;
- /** 创建该 Read View 的事务ID*/
- trx_id_t m_creator_trx_id;
- /** 创建视图时的活跃事务id列表*/
- ids_t m_ids;
- /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
- * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
- trx_id_t m_low_limit_no;
- /** 标记视图是否被关闭*/
- bool m_closed;
- // 省略...
- };
那么MVCC是如何实现的?
结合源码和画图理解:
画图理解:其中id就是下图的DB_TRX_ID
当进行快照读的时候就对比DB_TRX_ID和Read View中的水位线id即可。
当DB_TRX_ID即id等于创建该快照的id或小于up_limit_id那么该记录是可见的。(小于up_limit_id表示事务已提交)。
当id>=low_limit_id说明事务是快照生成后才提交的事务,则事务是不可见的。
当事务id在活跃事务id列表中时,表示事务在进行,也是不可见的。
RR是可重复读,RC是读提交,前面提到,读提交是可以读到已经提交的事务的,是不可重复读的,而可重复读的隔离级别是能在一个事务中保证读取数据的一致性的。
RR和RC级别的不同,本质是Read View生成的时机不同。
在RR级别下某个事务对某条记录的第一次快照读会创建一个快照及Read View,将当前系统活跃的其他事务记录起来,此后再进行快照读的时候,使用的是同一个Read View,所以对之后的修改不可见,实现了读取数据的可重复读。
而在RC级别下,在事务中每一次快照读都会生成并获取最新的Read View,所以别的事务提交了,新的快照读也就马上能看到,所以RC具有不可重复读的问题。