目录
1.什么是事务
2.为什么要有事务
3.事务的三个特性是什么
事务的原子性(Atomic):
事务的隔离性(Isolation):
4个隔离级别:
事务的持久性(Durability):
事务的一致性(Consistency):
4.事务的操作和事务的底层原理
Buffer Pool机制
持久性实现原理:redo log 日志(类似Redis里的RDB)
原子性实现原理:undo log 日志
Mysql隔离性的实现原理:锁机制 + MVCC机制:
共享读锁和排他写锁:
MVCC机制:
MVCC机制原理和举例详解:
一个事务是由一条或者多条对数据库操作的SQL语句所组成的一个不可分割的单元,具有原子性。
只有当事务中的所有操作都正常执行完了,整个事务才会被提交给数据库;
如果有部分事务处理失败,那么事务就要回退到最初的状态,因此,事务要么全部执行成功,要么全部失败。
set autocommit 开启事务,一旦开启事务就不是按下回车就会造成持久性修改
执行sql语句 1
执行sql语句 2
执行sql语句 3..........
如果发下sql语句中有执行错误的sql 那么直接调用rollback 退回 rollback
如果没有发现任何执行错误的sql,那么执行commit这个时候才会对数据造成持久性的影响;
开启事务 就会开启 undo log 日志
当需要对数据表执行一系列多个操作的情况下,为了防止这些操作中的部分操作执行成功而另一些操作执行失败,从而导致数据不正确,我们就需要使用事务了。
A C I D
rollback; 原子性的实现原理 ——》Undo log
事务是一个不可分割的整体,事务必须具有原子特性,及当数据修改时,要么全执行,要么全不执行,即不允许事务部分的完成。
4个隔离级别 隔离性实现原理:mysql锁机制 + MVCC机制
当两个或者多个事务并发执行时,为了保证数据的安全性,
将一个事物内部的操作与其它事务的操作隔离起来,
不被其它正在执行的事务所看到。
例如对任何一对事务T1和T2,
对T1而言,T2要么在T1开始之前已经结束,要么在T1完成之后
再开始执行。隔离性使得每个事务的更新在它被提交之前,
对其它事务都是不可见的。
让我们看看事务处理之父Jim Gray对事务隔离性的定义[1]:
Isolation: Concurrently executing transactions see the stored information as if they were running serially (one after another).
持久性的实现原理 redo log
事务完成以后,DBMS保证它对数据库中的数据的修改是永久性的,
即使数据库因为故障出错,也应该能够恢复数据!
如果其他三个特性能够保证,一致性一定可以保证
事务的一致性实现原理 = 原子性原理 + 隔离性原理 + 持久性原理
一个事务执行之前和执行之后,数据库数据必须保持一致性状态。
数据库的一致性状态必须由用户来负责,由并发控制机制实现。
就拿网上购物来说,你只有让商品出库,又让商品进入顾客的购物车才能构成一个完整的事务!由于并发操作带来的数据不一致性包括读脏数据(脏读),不可重复读和虚读(幻读)。
InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
rollback; commit;
undo log中会记录一条和你执行的sql语句意义相反的sql语句,当你rollback时就执行undo log中的sql语句。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
Record Locks(记录锁):在索引记录上加锁。
Gap Locks(间隙锁):在索引记录之间加锁,或者在第一个索引记录之前加锁,或者在最后一个索引记录之后加锁。
Next-Key Locks:在索引记录上加锁,并且在索引记录之前的间隙加锁。它相当于 是Record Locks与Gap Locks的一个结合
共享锁(S):共享(S)用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。·
排他锁(X):用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。
如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
行锁和表锁:
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
· 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最 低,并发度也最高
所谓的mvcc机制就是尽量的不去使用这些锁,一旦这些锁加上之后事务与事务之间就变成了完全的串行执行,在隔离级别中串行化其实就是这样做的。这里又要涉及到乐观锁和悲观锁。
InnoDB的一致性的非锁定读就是通过在MVCC实现的,Mysql的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。MVCC的实现,是通过保存数据在某一个时间点的快照来实现的(如果你配置过虚拟机,那么这个snapshot你一定不陌生),每一个事务只操作自己版本下的数据。因此每一个事务无论执行多长时间看到的数据,都是一样的。所以MVCC实现可重复读。
把这些机制了解清楚之后,我们来结合MVCC来说一下隔离性实现的原理。首先隔离性一共分了4种隔离级别。分别是:未提交读,已提交读,可重复读,和序列化。
未提交读(Read uncommitted):
这种级别,数据库一般都不会用,这里就不讨论了。
已提交读(Read committed):
在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的(Record Locks)。那么已提交读时如何解决脏读问题的?这个就利用了mvcc机制。
只要没有commit那么修改就是自己版本下的数据。
可重复读(Repeatable read):
数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁(Record Locks)的+Mvcc机制
即使commit操作的也是自己版本数据。
序列化(Serializable):所有锁都加了 没有mvcc机制 效率最低
这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论,
(加的锁就是Next-Key Locks)实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。由于InnoDB引擎加的是行锁,并且是加在索引记录上,所以为了不产生幻读在还会加间隙锁。串行。
基本原理
MVCC的实现,通过保存数据在某个时间点的快照来实现的。这意味着一个事务无论运行多长时间,在同一个事务里能够看到数据一致的视图。根据事务开始的时间不同,同时也意味着在同一个时刻不同事务看到的相同表里的数据可能是不同的。
基本特征
每行数据都存在一个版本,每次数据更新时都更新该版本。
修改时Copy出当前版本随意修改,各个事务之间无干扰。
保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
InnoDB存储引擎MVCC的实现策略
在每一行数据中额外保存两个隐藏的列:当前行创建时的版本号和删除时的版本号(可能为空,其实还有一列称为回滚指针,用于事务回滚,不在本文范畴)。这里的版本号并不是实际的时间值,而是系统版本号。每开始新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较。
每个事务又有自己的版本号,这样事务内执行CRUD操作时,就通过版本号的比较来达到数据版本控制的目的。
具体举个例子,不然光说不做确实不好理解:
create table yang(
id int primary key auto_increment,
name varchar(20));
}
假设系统的版本号从1开始.
INSERT
InnoDB为新插入的每一行保存当前系统版本号作为版本号.
第一个事务ID为1;
start transaction;
insert into song values(NULL,'yang') ;
insert into song values(NULL,'long');
insert into song values(NULL,'fei');
commit;
对应在数据中的表如下(后面两列是隐藏列,我们通过查询语句并看不到)
SELECT
InnoDB会根据以下两个条件检查每行记录:
a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
只有a,b同时满足的记录,才能返回作为查询结果.
DELETE
InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.
看下面的具体例子分析:
第二个事务,ID为2;
start transaction;
select * from song; //(1)
select * from yang; //(2)
commit;
假设1
假设在执行这个事务ID为2的过程中,刚执行到(1),这时,有另一个事务ID为3往这个表里插入了一条数据;
第三个事务ID为3;
start transaction;
insert into yang values(NULL,'tian');
commit;
这时表中的数据如下:
然后接着执行事务2中的(2),由于id=4的数据的创建时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,所以id=4的数据行并不会在执行事务2中的(2)被检索出来,在事务2中的两条select 语句检索出来的数据都只会下表:
假设2
假设在执行这个事务ID为2的过程中,刚执行到(1),假设事务执行完事务3后,接着又执行了事务4;
第四个事务:
start transaction;
delete from yang where id=1;
commit;
此时数据库中的表如下:
接着执行事务ID为2的事务(2),根据SELECT 检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行,而id=4的行上面已经说过,而id=1的行由于删除时间(删除事务的ID)大于当前事务的ID,所以事务2的(2)select * from yang也会把id=1的数据检索出来.所以,事务2中的两条select 语句检索出来的数据都如下:
UPDATE
InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间.
假设3
假设在执行完事务2的(1)后又执行,其它用户执行了事务3,4,这时,又有一个用户对这张表执行了UPDATE操作:
第五个事务:
start transaction;
update yang set name='Long' where id=2;
commit;
根据update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID,得到表如下:
继续执行事务2的(2),根据select 语句的检索条件,得到下表:
还是和事务2中(1)select 得到相同的结果。