目录
事务的概念
CURD不加控制会导致什么问题
什么是事务
为什么需要事务
事务的操作
事务的版本支持
事务的提交方式
事务常见操作方式
事务的隔离级别
如何理解隔离性
四个隔离级别
查看与设置隔离级别
隔离级别的使用
进一步探究读写隔离
了解MVCC
事务的概念
CURD不加控制会导致什么问题
在谈什么是事务之前,让我们先思考一下一个场景,火车票订票系统涉及到储存、管理和查询大量信息,所以肯定要通过用到数据库来进行增删查改,并且订票系统是线上的,所以肯定存在多个用户同时通过各自的客户端访问服务端的,那么问题就来了:如果CURD不加控制会出现什么问题?
当客户端A检测到还有一张票时,将票卖掉了,但在还没来得及更新数据库时,客户端B检测到还有一张票,也将票卖掉了,同一张票居然被卖了两次!从这个例子中我们可以看出,对表的操作不仅要受表的约束,还应该受业务约束。
那么CURD要满足什么属性才能解决上述问题呢?
1. 买票的过程应该是原子的
2. 买票过程中不能相互影响
3. 买完票应该要保证永久有效
4. 买票前后都必须是确定的状态
什么是事务
事务由一组DML语句(数据操作语言)组成,它们之间有一定的逻辑关联,MySQL提供了一种机制保证它们要么全部成功要么全部失败,并且事务可以让不同的客户端看到的数据是不同的。
正如我们前面举的例子,一个MySQL数据库往往不可能只有一个事务在运行,在同一时刻,可能有大量的请求被包装成事务访问数据库,在不加以保护的情况下,如果让它们看到了相同的数据,甚至由多条SQL语句组成的事务执行到一半不想执行了,那就会导致事故。
所以说一个事务不应该仅仅是SQL语句的组合,还必须满足下面的性质:
1. 原子性:一个事务中的所有操作,要么全部完成,要么全部未完成,不存在中间态,如果不能全部完成,就回滚到全部未完成的状态
2. 隔离性:MySQL数据库支持多个并发事务同时对数据进行读写和修改,隔离性防止了事务并发执行时由于交叉执行导致数据不一致。隔离级别分为读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Rpeatable Read)、串行化(Serializable)
3. 持久性:事务执行完毕后,对数据的修改应该是永久的,即使系统故障也不会丢失
4. 一致性:事务执行前和执行后,业务的规则和约束必须要被满足,可以说前面三个性质是实现一致性的前提
为什么需要事务
事务的意义在于简化编程模型,有了事务之后,应用层使用数据库的时候就不需要考虑各种潜在错误和并发问题了。并且事务能够起到解耦合的作用,即使某个操作失败,也不会对系统的其他部分造成影响,增强了系统的可靠性和可维护性。
事务的操作
事务的版本支持
通过查询存储引擎信息,我们可以发现在用得比较多的存储引擎中,innodb是支持事务(transactions)的,而myisam则不支持事务。
事务的提交方式
事务的提交方式分为自动提交和手动提交,我们可以通过以下指令来查看我们当前数据库使用的是哪种方式:
可以看到,当前事务自动提交默认是打开的,那么自动提交的具体表现是什么呢,我们到目前为止还没见过事务长啥样呢,这点后面再提。
我们可以设置事务的提交方式:
现在事务提交方式就被我们设置为手动提交了。
事务常见操作方式
为了方便我们一会观察现象,我们先将事务隔离级别设置为read uncommitted(默认是repeatable read ):
但是我们观察到隔离级别还是显示为默认级别,这个时候我们重启一下mysql客户端就行了:
接下来我们在同一云服务器启动两个mysql客户端a和b来模拟并发访问数据库的情况:
创建测试表:
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;
演示一:事务的开始与回滚
启动事务的两种方式:
-- 方法一
start transaction;
-- 方法二
begin;
-- 在从begin到commit之间的部分都是事务
-- commit后就不能够回滚, 因为已经持久化到磁盘了
commit;
在第一个客户端a:
在第二个客户端b进行查询:
在事务中可以设置保存点,并能够进行定向回滚:
-- 设置保存点
savepoint 保存点名称;
-- 回滚到指定保存点, 没有设置保存点则回滚到事务开始前
rollback to 保存点名称;
演示二:事务未commit时,客户端崩溃
a插入数据后,异常退出:
b在a插入时能看到数据,并且在a异常退出后,我们可以观察到数据自动回滚到了事务开始之前的状态:
演示三:commit后,客户端异常退出
、
客户端b还是能够查到新的记录,说明commit后,数据就被持久化到磁盘中了。
单条SQL语句与事务的关系
在开始验证之前,我们先把自动提交设置为off:
在客户端a把id为2的记录删除,可以观察到,客户端b确实查询到李四的信息被删除了,但当客户端a异常退出时,删除操作被回滚了,李四又回来了:
客户端a:
客户端b:
再来看另一个现象,当客户端a在commit之后才崩溃,客户端b仍然能够查询到修改后的信息,并没有回滚:
a:
b:
最后一个现象,我们现在重新把提交模式设置为自动提交,这次即使我们没有提交,直接让客户端a异常退出,修改还是持久化了:
客户端a:
客户端b:
总结一下,通过前面的现象,我们可以发现:其实我们的单条SQL语句也是一个事务,只不过默认设置的是自动提交,所以我们以前没有注意过而已。
关于事务操作的结论
1. 只要输入begin或start transaction,事务就必须要手动提交,这与是否设置了autocommit无关,autocommit影响的是单条SQL语句
2. 事务可以手动回滚,在操作出现异常时,事务也会自动回滚
3. MySQL中的单条SQL语句被封装为事务,并自动提交(select除外,因为有MVCC的特殊情况)
4. 通过前面的实验,我们验证了事务的原子性和持久性,那么隔离性和一致性呢?
事务的隔离级别
如何理解隔离性
数据库的应用场景就注定了它往往会被许多个客户端并发访问,对上层使用者而言,一个对事务的操作肯定是原子性的,只有执行前和执行后之分。但是从实现原子性的角度,肯定是存在执行中的阶段的,如果不同的事务之间不能相互隔离,即它们会看到和操作同一份数据,那并发访问时就会出现无法预期的错误,所以我们需要让不同的事务相互隔离起来。
四个隔离级别
读未提交(Read Uncommitted):在这种隔离级别下,所有的事务都能看到其他事务未提交的数据,刚刚我们做实验时使用的就是这个级别,在实际生产环境中是不可能使用这个隔离级别的,因为可能出现脏读、幻读、不可重复读等并发问题。
读提交(Read Committed):在这种隔离级别下,所有的事务都只能看到其它事务提交了的数据,这也是大部分数据库的默认隔离级别(但不是MySQL的),这种隔离级别显然比读未提交安全得多,但还是会出现不可重复读、幻读的问题。
可重复读(Repeatable Read):这是MySQL默认的隔离级别,保证了同一个事务在执行中,多次查询时,能读取到相同的数据,但还是会出现幻读的问题(MySQL不会)。
串行化(Serializable):这是事务最高的隔离级别,强制所有事务串行化执行,使之不可能相互冲突,解决了幻读问题,但由于每个读的数据行上都加锁,在实际生产环境中几乎不会使用。
查看与设置隔离级别
-- 查看全局隔离级别
select @@global.tx_isolation;
-- 查看局部隔离级别
select @@session.tx_isolation;
经过查询,我们发现,由于我们在前面把隔离级别设置为了读未提交,所以显示为读未提交,实际上,MySQL的默认隔离级别是可重复读。
所以我们把全局隔离级别重新设置为可重复读,但查看当前会话隔离级别时,还是没变,这是因为修改全局隔离级别后,需要重启客户端才能生效。
可以看到,我们重启客户端后,会话隔离级别就变成了可重复读:
所以说设置全局隔离级别能够影响所有的会话,而会话隔离级别则只能影响当前的会话,在重启之后,隔离级别还是和全局隔离级别一样。
-- 设置全局隔离级别
set global transaction isolation level read uncommitted/read committed/repeatable read/serializable;
-- 设置当前会话隔离级别
set session transaction isolation level read uncommitted/read committed/repeatable read/serializable;
隔离级别的使用
读未提交(Read Uncommitted)
客户端a:
客户端b:
大家注意看,我们在客户端a开始了一个事务,事务中向account表插入了一条数据,但事务还没有提交,而客户端b此刻却能够看到这个没有操作的结果!我们把这样的:事务在执行中读取到其他事务更新但还没有commit的数据叫做脏读。
读未提交几乎没有加锁,虽然效率高,但是有这脏读、不可重复读、幻读等所有问题,所以严重不建议使用这种隔离级别
读提交(Read Committed)
客户端a:
客户端b:
可以看到,在客户端a执行commit事务之前,客户端b是看不到更新了数据的,而提交之后,客户端b顺利看到更新结果:
目前我们感觉读提交好像非常安全呀,那前面提到过的所谓不可重复读是什么呢?让我们看接下来的一次测试:
这次我们让客户端a、b分别启动一个事务:
客户端a:
客户端b:
然后a向account插入一条记录,此时还没有提交,b也无法查到数据更新,这没什么问题,但接下来问题来了:a执行commit事务后,b再次查询,这时能查到结果更新了。这件事是有问题的,因为我们同一个事务对同一张表进行几次查询,居然能查到不一样的值!我们把这样的:同一事务内,同样的读取,不同的时间段,查询到不同的值叫作不可重复读。
可重复读(Repeatable Read)
客户端a:
客户端b:
大家可以发现:客户端b启动事务后,即使其它客户端对数据进行了更新,在这同个事务内还是只能查到原来的结果,也就是说:同一个事务内,无论进行多少次查询,结果都是一样的,这就是可重复读。
其实一般的数据库在可重复读的隔离级别下还是会出现幻读的问题的,但MySQL使用Next-Key锁避免了这点。
串行化(Serializable)
我们测试串行化依旧是启动两个事务:
客户端a:
客户端b:
我们发现,在a的事务commit之前,b的事务想要查询就需要等待,只有当事务先开始的指令执行完后,其他事务的后面的指令才能进行下去,虽然这保证了安全性,但是只能串行化执行,并发性非常差,由于效率低下,我们通常不会采用。
总结
通过前面的学习我们可以发现,隔离级别越高,事务的并发性就越低,所以我们需要在两者之间找到一个平衡点。
隔离级别 | 脏读 | 幻读 | 不可重复读 | 是否加锁 |
读未提交 | 是 | 是 | 是 | 否 |
读提交 | 否 | 是 | 是 | 否 |
可重复读 | 否 | 否 | 否 | 否 |
串行化 | 否 | 否 | 否 | 是 |
进一步探究读写隔离
数据库并发共有三种场景:读读并发、写写并发、读写并发。其中读读并发并不存在任何问题,也不需要并发控制,而写写并发我们通过加锁来保证线程安全,因此MySQL实现读写并发隔离的方式是我们这里主要探究的问题。
了解MVCC
MySQL实现读写并发隔离的方式叫做多版本并发控制(MVCC),这是innodb实现无锁并发隔离的具体方式,用于实现读提交与可重复读。
MVCC为事务分配单向增长的事务ID,为每次修改保存一个版本,版本与与修改该事务的ID相关联,读操作只读取该事务开始前版本的数据,所以说MVCC可以解决以下问题:
1.在读写并发操作数据库时,能够实现读事务不被写事务阻塞,写事务不被读事务阻塞,提高了数据库并发读写的性能。
2.能够解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题。
理解MVCC我们还需要了解两个前置知识:
1.我们的表实际上除了定义的列之外,还存在隐藏的列:最近修改记录的事务id、隐含的自增id(隐藏主键)、回滚指针、删除标志位。
2. 在mysql服务器端分配的内存中,有一块内存缓冲区是用来记录日志数据的。
那么,我们对记录的修改流程就应该是这样的:在修改记录之前,把旧版本写入到undo log中,然后回滚指针指向旧版本在undo log中的地址,然后才是对记录进行修改。后续还有修改操作,就重复这样的流程,这样一来,我们就得到了该记录的版本链,我们可以通过回滚指针在undo log中找到历史版本,进行回滚操作。
我们把这个版本链的一个个版本称为一个个快照,通过版本链与回滚操作,我们可以实现对数据修改的回滚,但是既然说是读写隔离,那读呢?读操作不会对数据造成修改,所以不用保存版本,但并发读写需要考虑的是:应该读哪个版本的数据,是读最新版本呢?还是历史版本?
读最新版本,我们称之为当前读,需要加锁,只能串行化执行:
-- 共享锁
select in share mode;
读历史版本,我们称之为快照读,不需要加锁,因此并发读写性能更高。采用哪种读的方式说到底还是取决于隔离级别是什么。
那么,为什么我们需要隔离级别呢?我们说过,事务操作应该是原子性的,但事务从begin->CURD->commit这个过程中,由于并发执行,增删查改数据的操作可能会交叉执行,所以必须保证不同的事务能看到自己该看到的版本数据。
举个例子:select操作可以选择对数据库某一行的最新版本进行访问,也可以选择对这一行的某个历史版本进行访问;update则对最新版本进行更新。
那么,如何保证不同的事务能看到它们该看到的版本呢?MySQL innodb给出的解决方案是Read View,我们下篇文章来讲。