在数据库中,我们将我们的指定的一些操作设置为一个事务。
事务的四大特性ACID
- Atomocity:原子性,一个事务中的所有操作,要么全部完成,要么全部失败。
- Consistency:一致性,一个或多个事务执行后,原来一致的数据和数据库仍然是一致的。银行转账前后,金额的总额不改变。
- Lsolation:隔离性,数据库开启一个事务时,不会受到其他事务对数据操纵导致数据不一致。多个事务并发互不影响。
- Durability:持久性,事务提交后,对数据库中的数据的修改是永久的。它无法通过回滚或者任何方式来进行恢复。
而事务的ACID是通过
InnoDB日志
和锁
来保证。
- 事务的隔离性是通过
数据库锁+MVVC
的机制实现的。- 事务的持久性通过
redo log(重做日志)
来实现。- 原子性和一致性通过
Undo log(回撤日志)
来实现.
Undo Log保存了事务发生之前的数据的一个版本,为了满足事务的原子性,在事务前备份数据到Undo Log,然后进行数据的修改。如果出现了错误或者用户执行了roll back语句,系统可以利用Undo Log的数据进行恢复。
Redo log确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
事务的隔离级别
MySql5.5后使用的是InnoDB
引擎,它默认隔离级别是可重复读的
。
Orcale默认隔离级别是读已提交
。
MySQL数据库共定义了四种隔离级别:
- Serializable(串行化):可避免脏读、不可重复读、幻像读取情况的发生。
- Repeatable read(可重复读):可避免脏读、不可重复读情况的发生。
- Read committed(读已提交):可避免脏读情况发生。
- Read uncommitted(读未提交):最低级别,以上情况均无法保证。
脏读:事务T1对数据进行修改,但未提交;事务T2进行数据读取,读取到修改但未提交的数据;事务T1进行回滚,取消了数据的修改,事务T2之前读取的数据为无效数据。
不可重复读:事务T2读取了一条数据,事务T1对此数据进行修改并提交,事务T2再次读取此数据。两次读取的数据不一致,数据被修改。
幻像读取:事务T1开启,事务T2对数据进行查询,得到结果一;事务T1对数据进行新增并提交,事务T2再次进行读取,得到结果二。结果二中比结果一中多出一条数据,就和幻像一样,称之为幻像读取,数据被增加。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
不可重复读和幻像读的区别
- 不可重复读是针对同一个数据项的概念,两个事务对同一个数据项进行了修改。
- 幻像读时正对一个数据集的概念,在读取批量数据时,数据的数目发生了变化。
参考数据库的隔离级别
MVVC+读写锁
InnoDB的底层是使用读写锁(共享锁与排他锁)和MVCC(多版本并发控制协议)来进行隔离级别的实现的。它们的实现思想可以简单的做一个解释。
MySQL支持全部4个隔离级别,但在具体实现时,MySQL默认采用RR隔离级别,SQL标准是要求RR解决不可重复读的问题,但是因为MySQL通过next-key lock在RR隔离级别下解决了幻读的问题。
读写锁
首先InnoDB是基于读写锁的,也就是共享锁(S锁)
和排他锁(X锁)
。
在一个事务中,读取某条记录行,会为当前记录行添加一个读锁,其他事务也可以对该记录行添加读锁并且读取,但是不能添加写锁。而修改一条记录时,会为这个记录行添加一个写锁,其他事务不能为其添加任何锁。
- 读锁是一个共享锁,它是对读锁共享的,但是不对写锁共享。
- 写锁是一个排他锁,添加了写锁后就不能添加任何锁。
- 读写锁都只有在事务结束后才会自动释放。
在InnoDB中实现了这种锁机制,并且基于这种锁机制使用了
MVVC
来提升读的效率
两种读取数据方法
在InnoDB中,基于MVVC在不同情况下,会有两种不同的读取数据的方式。
一致性非锁定读
在事务中,查询某一条数据时,这条数据正在被其他事务delete,update
时,读操作并不会等待写锁的释放,而是去读取这条数据之前的快照版本
,也称之为快照读
。
锁定读
在事务中,查询某条数据时,这条数据没有被其它事务delete,update
时,这个时候读取的数据就是当前最新版本的数据,并且会为当前记录行添加读锁,这种读法也叫为当前读
。
此外delete,uptate,insert时的操作也存在着当前读的一种现象,但是会为记录添加写锁。
隔离级别的实现
基于上面的读写锁+MVVC,我们可以很容易的就实现RR(Repeatable Read)
和RC(Read Commited)
读未提交
这种不考虑逻辑性和安全性的隔离级别不会被使用,实现即不加任何锁即可。
读已提交
读已提交的实现在
一致性非锁定读
中,事务开始后,每次查询都会重新生成Read View,这样避免了数据脏读但却无法实现可重复读。在
锁定读
中,读取时会为当前记录添加写锁,其他事务无法获取写锁,不能对该记录行进行修改等操作,所以读未提交和不可重复读都不会发生。可重复读
可重复读的实现在
一致性非锁定读
中,事务开始后,它只会为第一次查询生成Read View,这种方式实现了数据的可重复读。锁定读
利用锁机制实现可重复读。在InnoDB中的RR级别的隔离机制还实现了部分的避免幻读,在RR中,
锁定读时
,使用Next-key lock
的方式来对查询范围内的间隙进行加锁,其他事务不能进行插入事务。一致性非锁定读不会发生幻读现象。串行化:串行化就是让事务不能并发的执行,当有事务进行时,其他事务等待运行,虽然实现了高可靠,但是性能及其低下。
InnoDB的中MVVC原理
MVCC 的实现依赖于:隐藏字段、Read View、Undo log。
在内部实现中,InnoDB 通过数据行的 DB_TRX_ID
和 Read View
来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR
找到 undo log
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View
之前已经提交的修改和该事务本身做的修改。
隐藏字段
DB_TRX_ID(6字节)
:表示最后一次插入或更新该行的事务 id。此外,delete
操作在内部被视为更新,只不过会在记录头Record header
中的deleted_flag
字段将其标记为已删除DB_ROLL_PTR(7字节)
: 回滚指针,指向该行的undo log
。如果该行未被更新,则为空DB_ROW_ID(6字节)
:如果没有设置主键且该表没有唯一非空索引时,InnoDB
会使用该 id 来生成聚簇索引
Read View
Read View
主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”,它是存储在事务中,我们所说的更新快照也就是更新它和undo log。
主要有以下字段:
m_low_limit_id
:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于这个 ID 的数据版本均不可见m_up_limit_id
:活跃事务列表m_ids
中最小的事务 ID,如果m_ids
为空,则m_up_limit_id
为m_low_limit_id
。小于这个 ID 的数据版本均可见m_ids
:Read View
创建时其他未提交的活跃事务 ID 列表。创建Read View
时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids
不包括当前事务自己和已提交的事务(正在内存中)m_creator_trx_id
:创建该Read View
的事务 ID
Undo Log
undo log
主要有两个作用:
- 当事务回滚时用于将数据恢复到修改前的样子
- 另一个作用是
MVCC
,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过undo log
读取之前的版本数据,以此实现非锁定读。
使用事务
begin:事务开启
savepoint:定点回滚
rollback:回滚
begin; //开启事务,或者说是关闭自动提交功能。
insert into test values(5,"lisi");
insert into test values(1,"1");
commit; //提交事务,两个都被保存。
begin; //开启事务
insert into test values(5,"lisi");
insert into test values(1,"1");
rollback; //回滚,两个都未保存。不管语句是否正确。
begin; //开启事务
insert into test values(5,"lisi");
savepoint a; //设置一个定点a
insert into test values(1,"1");
rollback to a; //回滚到定点a