最近看了很多Mysql事务的基础知识,加上自己的一些理解,写下此笔记(也是本人第一篇啊)!!!
PS:排版不太好,见谅了
事务实现原理
Innodb存储引擎为每行记录都自动生成三个隐藏字段:
6字节的事务ID(DB_TRX_ID )
7字节的回滚指针(DB_ROLL_PTR)
隐藏的ID
6字节的事物ID用来标识该行所述的事务,7字节的回滚指针需要了解下Innodb的事务模型。
如图,具体参考博文
首先要知道mysql事务是日志先行的。
redo log
redo log就是保存执行的SQL语句到一个指定的Log文件,当Mysql执行recovery时重新执行redo log记录的SQL操作即可。当客户端执行每条SQL(更新语句)时,redo log会被首先写入log buffer;当客户端执行COMMIT命令时,log buffer中的内容会被视情况刷新到磁盘。redo log在磁盘上作为一个独立的文件存在,即Innodb的log文件,指data文件夹下ib_logfile文件。
undo log
与redo log相反,undo log是为回滚而用,具体内容就是copy事务前的数据库内容(准确的说是与事务相反的sql操作)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),指data文件夹下ibdata文件,即使客户端设置了每表一个数据文件也是如此。
日志详细工作流程如下:
其中innodb_flush_log_at_trx_commit=0|1|2要关注。
注意 事务中sql语句执行出现异常,不会自动回滚,只会报错,需要手动回滚。这和服务器断电不一样的。
另外事务要控制大小,比较小的事务好,因为事务保证ACID牺牲了性能的,并且事务越大,出现异常就丢失的数据多,要进行事务分解。
MVCC
MVCC(Multi-Version Concurrency Control)多版本并发控制,是通过undo log来实现的,通过在每行记录后面保存2个隐藏的列DB_TRX_ID和DB_ROLL_PTR,可以追踪到当前事务下的版本,
MVCC机制保存了数据在某个时间点的快照。
MVCC最大的作用是: 实现了非阻塞的读操作,写操作也只锁定了必要的行。
MYSQL的MVCC 只在 read committed 和 repeatable read 2个隔离级别下工作.
在MVCC的机制下,mysql InnoDB(默认隔离级别)的增删改查变成了如下模式:具体参考博文
SELECT:
1, InnoDB只查找版本早于当前事务版本的数据行(行的系统版本号小于等于事务的系统版本号)
2, 行的删除号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行,在事务开始之前未被删除.
INSERT:
InnoDB 为新插入的每一行保存当前系统版本号做为行版本号。
DELETE:
INNODB 为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE:
InnoDB 为插入的每一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
注意: 上面的读取方式只在InnoDB默认隔离级别下工作,其它的隔离级别会有很大的差异。
事务特性
1.原子性:一个事务(事务中的sql操作)要么全部做完,要么全部不做,不会出现只做一部分的情形,即事务是不可分割的一组操作。如A给B转帐,不会出现A的钱少了,B的钱却没有增加的情况。
2.一致性: 事务的执行前后,保证数据一致性。如转账完成后A减少100,B一定增加100。
3. 持久性:事务一旦提交,数据被永久保存到数据库中;
4. 隔离性:即一个事务在没有完成数据的操作(一般修改、插入、删除)时,即事务没有提交时,其操作结果对其它事务是不可见的。当然这里有个隔离级别的概念,在不同隔离级别下,这里会有不同的表现形式。
事务可恢复性与级联回滚
前提:存储引擎支持事务且事务隔离级别小于Serializable;级联回滚是mysql保证事务一致性和处理事务关系的一种处理机制。
个人对级联回滚理解是这样的:有两个人(两个事务Ti和Tj),一个 好人(Tj),一个坏人(Ti),坏人(Ti)在某个人家里偷了钱(Ti执行读取操作),并将其收入囊中(Ti执行写操作),然后坏人将偷来的钱给好人花(Tj读取了Ti写入的数据项“偷的钱”),最后这个盗窃案被破获了(事务在执行中出现中断),坏人进了监狱(Ti开始回滚),好人也同样被逮捕了(Tj也开始回滚)。可以看出,好人受到了坏人的影响(两个事务之间产生了级联)。因此Ti的回滚造成了Tj的回滚,如果更多事务受到Ti的影响,也会回滚。参考 博文。
事务隔离级别
SQL标准定义了4类隔离级别,包括了一些具体规则(锁机制,MVCC都参与了这些规则的实现),用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
1.Read Uncommitted(读未提交): 在Read Uncommitted策略下,数据库遵循一级封锁协议,只对修改数据的并发操作做限制。一个事务不能修改其他事务正在修改的数据,但可以读取到其他事务中尚未提交的修改,这些修改如果未被提交,将会成为脏数据,Oracle有Ver控制,不会有脏读。
2.Read committed(读已提交):在Read committed策略下,数据库遵循二级封锁协议,只允许读取已经被提交的数据,反过来讲,如果一个事务修改了某行数据且尚未提交,而第二个事务要读取这行数据的话,那么是不允许的。在MySql的InnoDB下,虽然这种操作不被允许,但MySQL不会阻塞住数据的查询操作,而是会查询出数据被修改之前的备份,返回给客户端。MySQL的这种机制称为MVCC(多版本并发控制),就是说数据库在事务并发的过程中对数据维护多个版本,使得不同的事务对不同的数据版本进行读写(MVCC的实现参见引用中的文章)。这样的机制反映在应用中就是,在任何时候对数据库查询总是可以得到数据库中最近提交的数据。为被提交的脏数据被隔离起来,无法被查询到,即防止脏读发生。
3.Repeat Read(可重复读): Repeat Read又比Read Committed更加严格一点,但仍然是在二级封锁协议的范畴,只是读取过程受到更多MVCC的影响。在Read Committed下,允许一个事务中多次相同查询得到不同的结果,就是所谓的不可重复读问题。这在一些应用中是允许的,所以oracle、SQL server上默认这一隔离级别,但MySQL没有,它默认Repeat Read级别。在这一级别下,有赖于MVCC,同一个事务中的查询只能查到版本号不高于当前事务版本的数据,即事务只能看到该事务开始前或者被该事物影响的数据。反过来说,这一级别下,不允许事务读取在该事务开始后新提交的数据。即防止了不可重复读的发生。
依靠上面的机制,已经做到了在事务内数据内容的不变,但是不能保证多次查询得到的数据数量一致。因为在一个事务执行的过程中别的事务完全可以执行数据插入,当插入了刚好符合查询条件的数据时,就会引发数据查询结果集增加,引发幻读。还有一种情况就是,如果一个事务想插入一条数据,而另一个事务已经插入了含有相同主键的数据,那么当前事务也会被阻塞,并最终执行失败,虽然当前事务根本无法查询到这一条数据,这也是一种幻读。
4.Serializable(可串行化): 最强事务隔离机制Serializable,它遵循三级封锁协议,使得所有的事务必须串行化执行,只要有事务在对表进行查询,那么在此事务提交前,任何其他事务的修改都会被阻塞。这解决了一切并发问题,但会造成大量的等待、阻塞甚至死锁,使系统性能降低,一般采用Repeat Read和数据库锁相结合方式来替代它。
并发情况下引发的问题
谈到数据库事务的隔离性,就不能不说数据库受并发影响产生的异常了,一般包含三个数据读问题(脏读,不可重复度,幻读),两个数据写问题。
脏读(dirty read):A事务读取B事务尚未提交的更改数据,并在这个数据基础上操作。如果B事务回滚,那么A事务读到的数据根本不是合法的,称为脏读。在oracle中,由于有version控制,不会出现脏读。
不可重复读(unrepeatable read):A事务读取了B事务已经提交的更改(或删除)数据。比如A事务第一次读取数据,然后B事务更改该数据并提交,A事务再次读取数据,两次读取的数据不一样。
幻读(phantom read):A事务读取了或意识到了B事务已经提交的新增数据。注意和不可重复读的区别,这里是新增,不可重复读是更改(或删除)。这两种情况对策是不一样的,对于不可重复读,只需要采取行级锁防止该记录数据被更改或删除,然而对于幻读必须加表级锁,防止在这个表中新增一条数据。
第一类丢失更新:A事务撤销时,把已提交的B事务的数据覆盖掉。
第二类丢失更新:A事务提交时,把已提交的B事务的数据覆盖掉。
如果想模拟并发,请参考文章:数据库多会话模拟并发,实操过后可以理解的更清楚。
数据库在并发操作下会出现上述这些问题,要解决它就要想办法在执行可能引发问题的操作之前将该操作阻塞住,让它等到合适的时机再执行。那么如何挑选合适的时机阻塞操作的执行,又如何保证在调度过程执行完成后其执行结果与串行执行操作的结果相同呢?这就要了解下三级封锁协议了
三级封锁协议
数据库想要在“合适”的时机阻塞住数据库操作,那么首先要定义好怎么样的时机算是“合适”,因为各个系统支持的业务千差万别,对数据的实时性和有效性的要求也不同。于是数据库理论中就提出了封锁级别的概念,对不同的同步要求采用不同的封锁级别。
三级封锁协议内容如下:
一级封锁协议:事务T在写数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。 一级封锁协议保证事务T是可恢复的,并且可以防止两个事务同时操作(增,删,改)同一数据问题。在一级封锁协议中,如果仅仅是读数据不会加锁的,所以它不能保证可重复读和不读“脏”数据。
二级封锁协议:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后方可释放S锁。 二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。
三级封锁协议 :一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。 三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。
事务隔离性与数据库异常关系(0可能出现,1不会出现)
事务相关的sql语句
查看当前会话隔离级别
select @@tx_isolation
查看系统当前隔离级别
select @@global.tx_isolation
设置当前会话隔离级别
set session transaction isolation level repeatable read
设置系统当前隔离级别
set global transaction isolation level repeatable read
查看当前会话事务是否自动提交
show variables like '%autocommit%'
设置当前会话事务是否自动提交
set autocommit = 0 or 1;0不自动,1自动
具体参考文章:[autocommit = 0与start transaction区别](https://www.cnblogs.com/lhp2012/p/5315928.html)
具体操作
start transaction;
***sql语句**
savepoint [ point_name];
***sql语句**
commit/rollback ;
最后谈一下快照读
快照读就是历史读,也称为为一致性读或一致性非锁定读,与之相对的是非一致性读(锁定读or当前读).
最常用的最基本的select 就是快照读sql,而insert、update、delete、select …for update/lock in share mode等就是当前读,读最新的数据。熟悉不同事务下的快照读,在做数据备份时挺好用的。
快照读在不同事务级别下有不同的表现形式: READ COMMITTED 隔离级别下,每次读取都会重新生成一个快照,所以每次快照都是最新的,也因此事务中每次SELECT也可以看到其它已commit事务所作的更改;REPEATED READ 隔离级别下,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改才会更新快照,因此,只有第一次SELECT之前其它已提交事务所作的更改你可以看到,但是如果已执行了SELECT,那么其它事务commit数据,你SELECT是看不到的。具体参考这里。
参考文章:
[1]: https://www.cnblogs.com/Chenghao-He/p/7693569.html
[2]: http://www.zsythink.net/archives/1436/
[3]: https://blog.csdn.net/hulinku/article/details/79787692