目录
前言
一.事务的概念
1.1 什么是事务
1.2 事务的属性
1.3 事务的版本支持
1.4 事务提交方式
1.5 事务的常见操作
1.5.1 准备阶段:
1.5.2 手动演示回滚操作
1.5.3 简单证明原子性
1.5.4 简单证明持久性
1.5.5 begin开启的事务不会受MySQL事务提交方式的影响
1.5.6 MySQL中SQL与事务的关系
1.5.7 总结
二.隔离级别
2.1 隔离级别介绍
2.2 查看和设置隔离性
2.3 操作演示
2.3.1 读未提交
2.3.2 读提交
2.3.3 可重复读
2.3.4 串行化
2.3.5 总结
2.4 一致性
三.解决并发的原理MVCC
3.1 数据库并发的三种场景
3.2 读和写
3.2.1 三个隐藏字段
3.2.2 undo日志
3.2.3 模拟MVCC过程
3.2.4 read view
四.RR和RC的本质区别
MySQL是一个网络服务。大多数情况下,会有很多客户端连接MySQL服务。当多个客户端访问同一个表时,可能会出现问题。
比如:火车票售票系统,当两个客户端同时买票,操作同一张票数表。当客户端A检测还有一张票,将票买掉,但是还没有更新数据库。于此同时,客户端B,也在买票,也检测到还有一张票,客户端B也将票买了。这样就导致一张票被卖了两次。
于是MySQL需要对此现象加以控制。这就是事务解决的问题。
要解决上面的问题,至少需要满足下面的属性(拿买票的过程举例):
事务就是一组DML类的SQL语句,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。
简单来说,事务就是要完成一件事,所用到的所有SQL语句(至少一条)。这些语句要么全部执行成功,要么全部执行失败。即,其中一条执行失败,就全部执行失败。数据立马回滚到执行前的状态。
上面四个属性,可以简称ACID。
事务并不是伴随着数据库系统天生就有的,而是为应用层的服务的。
这样就使得用户在操作数据库时不需要考虑数据的安全问题,简化了编程模型。
事务是存储引擎提供的,在MySQL中只有使用了innodb存储引擎的数据库或者表才支持事务,MyISAM存储引擎不支持事务。
查看数据库存储引擎:
事务提交方式有两种:
查看事务的提交方式:
修改事务的提交方式:
手动启动一个事务:
begin/start transaction;--开始一个事务
...--对表进行操作
commit;--提交事务
设置隔离级别为读未提交,后面在隔离性有详细解释。
注意:设置完global.transaction_isolation,需要重启MySQL。
创建表:
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;
创建保存点:savepoint 保存点名;
回滚:rollback 回滚的保存点;
注意:现在的隔离等级是读为提交。
事务没有执行完发生了错误,会回滚到最开始,相当于这个事务没有发生一样。
当一个事务完成后,操作完的数据会永久保存,系统是否故障。
结论:begin开启的事务,不受MySQL事务提交方式的影响,必须手动commit提交。
结论:在MySQL中没有手动begin开启事务,增删查改都会被MySQL封装成一个事务,即使只有一条SQL语句。
提交方式为:自动提交。
提交方式为:手动提交
正确:
注意点:
再次说明,MySQL是一个网络服务,同一时间可能有很多客户端连接。那么在多个事务在执行多个SQL时,有可能出现问题,比如:多个事务访问同一张表,同一行数据时。
一个事务的时间段可以分为执行前(该事务还没有开始),执行中(该事务正在执行),执行后(该事务已经提交)。
隔离性:保证了事务执行过程中尽量不受干扰。
为了提高效率,执行过程并不是串行化,而是允许事务受到不同程度的干扰,于是就有了一个重要的特征:隔离级别。
注意:隔离级别是针对事务之间的。
隔离基本上都是通过加锁来实现的,不同隔离级别,锁的使用是不同的。常见的有,表锁,行锁,读锁,写锁,间隙锁,Next-key锁(GAP+行锁)。
读提交和可重复读的区别:
有两个事务,事务A和事务B。
事务A修改数据,提交后(commit)。读提交,事务B在提交前(commit)可以看到事务A修改后的数据。可重复读,事务B在提交前(commit)看不到事务A修改后的数据,事务B在提交后,才能看到。
查看隔离性:
设置隔离等级:
这是设置当前会话或者全局隔离级别的语法。
set [session | global] transaction isolation level {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE};
设置当前会话隔离性,另起一个会话,隔离性不会被设置,只影响当前会话。
设置全局隔离级别,另起一个会话,会被影响。
会话隔离级别开始启动时,会和全局隔离级别一样。
设置了全局会话隔离级别,当前会话隔离级别并没有改变,需要重启MySQL才会改变当前会话隔离级别。
修改当前会话隔离级别:
隔离级别时针对事务和事务之间的。
现象:如果隔离级别为读未提交,一个事务A增删改表的内容,未提交事务,在另外一个事务B中可以看到变化的内容。
但是,如果事务A没有提交异常退出了,内容会发生回滚,回滚到事务开始前的内容,原因是事务的原子性。事务A提交了,事务B,提交后也可以看到修改之后的内容,这是应为事务的持久性。
一个事务执行中,读到了另外一个事务的增删改,但是没有commit的数据,这种现象叫做脏读。
现象:两个事务,事务A和事务B,隔离等级为读提交。事务A增删改表的内容,未提交事务,在事务B中,看不到增删改的内容。当事务A提交后,事务B中可以看到增删改的内容。注意事务B没有提交。
问题:此时在事务B中,没有提交事务。但是同样的读取,在同一个事务内,在不同的时间段,读取到的值不同,这种现在叫做不可重复读取。
现象:两个事务,事务A和事务B。事务A增删改表的内容,未提交,事务B看不到修改后的数据;事务A提交后,事务B没有提交,仍然看不到提交的数据;事务B提交后,由于持久化,可以看到修改后的数据。
注意:并不是,同一时段的两个事务,事务A提交的数据,事务B就一定看不到。这个取决于select 的位置。下面详细解释了。
问题:
幻读:在事务只执行中,不同时间段查询,会查找出来新的记录。即,事务A插入一条数据,事务B在未提交前,看到了插入的数据。
但是,在下面的演示中,在事务A向表中插入数据,事务B中在未提交前没有看到插入的数据。这是因为MySQL解决了幻读的问题。
但是,一般的数据库,在事务未提交前能够读到其它数据插入的数据。这是为什么呢?
因为隔离性实现是对数据加锁实现的,而insert数据,插入的数据,在表中并不存在,一般的加锁无法屏蔽这里问题。
而MySQL是如何解决的呢?
MySQL用的是Next-Key锁(GAP+行锁)解决的。
现象:当前有多个事务时,一个事务要增删改表的数据,会发生阻塞。当其它数据全部退出,才能正常进行增删改操作。使得增删改时,事务之间运行是串行的。效率比较低。
但是:如下图,我们发现,查找,并没有串行化,有多个事务同时运行时,查找不会被阻塞,串行化。这是因为查询并不会修改数据。
针对MySQL:
隔离级别 | 脏读 | 幻读 | 不可重复读 |
读未提交【 Read Uncommitted 】
|
有 | 有 | 有 |
读提交【 Read Committed 】
|
没有 | 有 | 有 |
可重复读【 Repeatable Read 】
|
没有 | 没有 | 没有 |
串行化【 serializable 】
|
没有 | 没有 | 没有 |
注意:
当多个事务,同时进行update, insert或者delete时,是会有加锁现象的。即,会发生阻塞。但是select查询和增删改并不冲突,即不会发生阻塞。这是通过读写锁(锁由行锁或者表锁)+MVCC完成的。
这里不好演示,直接给了结论。
对数据库操作实际上就是对数据库进行读和写的操作,并发就是,多个事务同时对数据库进行读和写操作。而数据库正是对在效率和安全方面的考虑,解决读和写并发的问题。
读和读,写和写的问题好处理,但是读和写的问题就比较麻烦,下面主要讨论如何解决读和写并发问题。
多版本并发控制(Multiversion concurrency control),简称MVCC,是一种解决读写冲突的无锁并发控制。
主要是:为事务分配单向增长的事务ID,为每一个修改保存一个版本,版本与事务ID关联,读操作只读改事务开始前的数据的快照(快照,后面有解释,需要先理解,才能理解)。所以MVCC可以为数据库解决以下问题:
注意:没有同时启动的事务,事务的启动一定会有先后的。所以每个事务的ID一定不同。
想要理解MVCC需要知道三个前提知识:
当我们在数据库中创建一个表时,表中的字段不仅会有我们设计的字段,还有有几个隐藏字段。
这里主要介绍三个:
补充:实际上还会有一个删除flag隐藏字段,即记录删除。实际上的删除一个字段,并不是真正的删除,而是将flag设置为删除标记。
演示:
此时:表的全部信息为:
我们目前并不知道创建该记录的事务ID,隐式主键。默认设置成NULL和1。第一条记录之前也没有其它版本,我们设置回滚指针为NULL。
undo日志,实际上时MySQL内存缓冲区里的一段空间,即buffer pool中的一段空间,用来保存日志数据,之后再刷新到磁盘中。
MySQL是一个服务进程,所有操作都需要在内存中完成的。比如:修改数据,先将数据拿到内存中,修改后,再刷新到磁盘中。日志也是一样,先将日志信息写到MySQL内部缓冲区中,之后再刷新到磁盘中。
说明:现在有一个事务,事务ID为10,对student表中的字段做的修改(update),将name从张三,修改成了李四。
过程:
现在又有一个事务11,对student表进行修改(update):将年龄age由28改成38。
注意:修改,修改的是最新记录。
于是这样就是形成了一个基于链表记录的版本链。所谓回滚,就是,用历史数据,覆盖当前数据。
快照:就是undo log中的一个个版本。
注意:只有在增删改动作的时候,才会形成快照。
上面主要讲的是修改数据undo log
如果是delete呢?删除数据了,这个字段就没有了,如何生成这个字段的快照呢?
上面补充的时候说了,在表中还有一个隐藏字段flag,来标记当前字段是否被删除。所以删除数据,并不是将数据真正删除了,而是将flag字段,设置成了删除标记。所以,删除字段也可以生成快照,保存在undo log中。
如果是insert呢?插入数据,之前没有插入数据前面没有历史版本,如果要回滚到插入前怎么办的?
起始在MySQL中由记录数据的log,就像上面一样。还由基于语句的log,记录相反的语句。比如:insert一条数据,在log中就会记录delete的语句。
如果是select呢?
select不会对数据进行修改,所以不需要为select维护多版本。
select读取的时候,是读取的最新数据还是,快照呢?
select既可以读取最新数据,也可以读取快照。
当前读:读取最新数据。增删改,都是当前读,因为需要修改最新的数据。select也可以当前读,(select lock in share mode,select for update),select当前读时,就需要对数据加锁,因为增删改也是当前读,避免数据错误。
快照读:读取快照。增删改是当前读,如果select采用快照读,这样不需要加锁,可以实现读写并发,这就是MVCC的意义所在。
那什么决定了select快照读还是当前读?
隔离级别决定的。不同的隔离级别,访问的数据版本不同。
比如:读提交:每次都可以访问到修改后的数据,当前读。读未提交:访问不到需改,但是未提交的数据,快照读。
事务操作哪个版本是在事务启动时确定的,事务总有先后。事务启动时,会分配一个事务id,通过对比事务id来确定操作哪个快照。
Read view是事务进行快照读操作的时候生产的读视图(read view)。决定了这个select可以看到多少版本数据。这个读视图中,维护了系统当前活跃事务的id。
简单来说就是,在我们某个事务进行快照读时,对该记录创建一个read view,把他当作条件,用来判断当前事务能够看到哪个版本的数据。即,可能时最新版本,也可能是undo log中的版本。
在read view中主要由以下四个成员:
我们读取数据版本链时,能够获得每一个版本对应的事务ID。即:DB_TRX_ID。
我们只需要拿read_view和DB_TRX_ID对比,就可以知道可以读取的id。
最终根据隔离级别,可以获得要读取的id。
Creator_trx_id == DB_TRX_ID || DB_TRX_ID < up_limit_id
在形成read view前已经提交的事务,形成的版本链数据,可以看到。
Up_limit_id <= DB_TRX_ID < low_limit_id
在形成read view时,正在执行的事务id保存在m_ids中。不在m_id中说明在形成快照前提交了,但是仍然在up_limit_id和low_limit_id范围内。
注意:m_ids中的事务id不一定是连续的,可能由中间的事务ID已经提交了。比如:我们有11,12,13,14,15号事务。在形成快照前12,14号事务提交了。形成快照后,m_ids中保存的就是11,13,15。up_limit_id等于11,low_limit_id等于16。
此时如果DB_TRX_ID没有m_ids中,说明已经提交(commit),可以被查询。
如果DB_TRX_ID在m_ids中,说明还没有被提交,不可以被查询,
看下面源代码就懂了。并且可重复读和读提交,是因为read view不同,所以呈现的现象不同。下面有详细介绍。
DB_TRX_ID >= low_limit_id
说明是形成read view后,形成的事务,对数据进行了修改。插入到了版本链中。
比如:此时有11,12,13,14,15号事务,形成read view。up_limit_id等于11,low_limit_id等于16。之后,又开始了一个事务16,并且修改了数据,所以版本链中会有大于low_limit_id的版本数据。
这种数据不能被查看。
对应源码策略:
注意:
MVCC流程演示,方便理解:
假设当前有一条记录:
事务操作:
事务1(id = 1) | 事务2(id = 2) | 事务3(id = 3) | 事务4(id = 4) |
事务开始 | 事务开始 | 事务开始 | 事务开始 |
... | ... | ... | 修改并提交 |
进行中 | 快照读 | 进行中 | |
... | ... | ... |
事务修改了name,将name有张三修改成了李四。
当事务2对某行数据进行快照读,数据库为改行数据形成移和read view读视图。
该read view中保存的数据为:
m_id2 = {1,3};
up_limit_id = 1;
low_limit_id = 5;
creator_trx_id = 2;
版本链为:
只有事务4修改了数据,并在事务2快照读前,提交了事务。
事务2拿着版本链的DB_TRX_ID去跟up_limit_id和low_limit_id和活跃事务ID列表进行比较,判断当前版本是否可以查看。
此时
该read view中保存的数据为:
m_id2 = {1,3};
up_limit_id = 1;
low_limit_id = 5;
creator_trx_id = 2;
DB_TRX_ID = 4
比较步骤:
- DB_TRX_ID(4) < up_limit_id(1) 不小于,判断下一个条件。如果小于可以查看。
DB_TRX_ID(4) >= low_limit_id(5) ? 不大于,判断写一个条件。如果大于,不可以查看,遍历版本链中的下一个结点。 m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务 4 不在当前的活跃事务中。此时就需要隔离级别来决定是否可以查看。隔离级别为读提交,可以查看,可重复读,不可以查看。
RR是可重复度,RC是读提交。
对于表:
设置当前会话的隔离级别为可重复读
介绍一个当前读语法:
select * from 表名 lock in share mode
案例1:
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
begin | 开启事务 | 开启事务 | begin |
select * from student | 快照读 | 快照读 | select * from student |
update student set age=30 where name='张三' | 更新名字为张三的年龄为30 | ||
commit | 提交事务 | ||
select没有读到张三的年龄为30 | select * from student | ||
select可以读到张三的年龄为30 | select * from student lock in share mode |
案例2:
事务A操作 | 事务A描述 | 事务B描述 | 事务B操作 |
begin | 开启事务 | 开启事务 | begin |
select * from student | 快照读,查到年龄为28 | ||
update student set age=30 where name='张三' | 更新名字为张三的年龄为30 | ||
commit | 提交事务 | ||
select可以读到张三的年龄为30 | select * from student | ||
select可以读到张三的年龄为30 | select * from student lock in share mode |
两测试用例的区别:
结论:
针对隔离级别是可重复读(repeatable read)