最近看了很多关于事务问题的博客,感觉看的好混乱,没有一个整体的架构来谈事务;
所以就根据自己的见解谈一谈关于事务的问题。
1.事务四大特征(ACID)
众所周知,事务的四大特性即原子性,持久性,隔离性和一致性,
一致性是事务的最终目的,而原子性,持久性,隔离性则是一致性的保证。
下面我们就分别谈谈事务是怎么具体通过原子性,持久性和隔离性实现一致性的
1.1原子性
原子性:事务中的所有操作,要么一起*成功*,要么一起*失败*,没有别的状态。
那么事务怎么保证一起成功或者失败呢?
InnoDB通过会把一个事务中所作的修改(即:insert,delete,update),备份到undo log(回滚日志)中,保存在磁盘,当事务执行失败,通过回滚日志进行回滚,保证了事务原子性。
1.2持久性
持久性:事务一旦提交成功,那么对数据库的修改就是永久性的,
即使系统故障也不会造成影响。
我们设想很简单,事务提交的时候把事务的更改刷新到磁盘不就行了?
但是,存在两个问题,一个就是磁盘I/O是很耗时的,那么InnoDB是怎么解决的呢?
所以InnoDB提供一个Buffer Pool(缓冲池),缓冲池包含磁盘部分数据页的映射,
当从数据库读取数据的时候,先从缓冲池读取,如果没有则从磁盘读取放入缓冲池冲;
当向数据库写入数据时,先写入缓冲池,然后定期刷新到磁盘。(即刷脏)
第二个问题就来了,如果缓冲池数据还没有来得及刷新到磁盘,服务器故障,那么持久性怎么保证?
参考undo log,我们增加了redo log,我愿称之为前滚日志。当事务提交时,我们先把记录同步在redo log中,保存在磁盘中,再更新Buffer Pool即可。如果服务器故障,我们可以用前滚日志来恢复数据。
那么既然都是刷磁盘,为啥要记录redo log而不是直接刷脏呢?
主要原因是redo log比刷脏快很多:
第一点是,redo log是追加操作日志,是顺序IO;而刷脏是随机IO,
因为每次更新的数据不一定是挨着的,也就是随机的。
第二点是,刷脏是以数据页(Page)为单位的(即每次最少从磁盘中读取
一页数据到内存,或者最少刷一页数据到磁盘),MySQL默认页大小是16KB,
对一个页上的修改,都要整个页都刷到磁盘中;而redo log只包含真正的需
要写入磁盘的操作日志。
MySQL还有一个记录操作的日志,叫binlog,它是基于MySQl服务器层面的,主要作用就是备份数据和主从同步。那么,binlog 和 redo log之间的一致性怎么保证呢?
Mysql是通过二阶段提交事务来保证数据一致性:
第一阶段提交:将redo log提交到磁盘,将状态改为"prepare",binlog不做操作;
第二阶段提交:生成事务操作的binlog并写入磁盘;
调用引擎的事务提交,将redo log状态改为"commit",事务提交完成;
1.3隔离性
隔离性:多个并发事务之间相互隔离,互不影响。
这里有一个隔离级别的概念:读未提交,读已提交,可重复读(MySQl默认隔离级别),串行化。
一般来说,隔离级别越高并发性能越低。
四个隔离级别可能造成的问题:
读未提交:一个事务读取了别的事务未提交的数据,可能造成脏读;
读已提交:一个事务连续两次读到的行数据不一致,别的事务在中间
对数据进行过操作,可能造成不可重复读;
可重复读:一个事务连续两次读到的区间数据行数不同,别的事务在
对区间数据进行了增删,可能造成幻读;
串行化:最高隔离级别,但是并发性能最低。
一般我们两个或多个事务同时操作一个资源时,会出现以下情况:读读操作,写写操作,读写操作,那么我们谈谈MysQL是如何处理这三种情况的:
读读操作:事务互不影响,可以并行,我们有一个共享锁处理这种情况,也称读锁;
写写操作:事务必定互相影响,不可并行,那么我们有一个排他锁处理,也称写锁;
读写操作:是我们最为常见的情况,势必互相影响,不可并行,如果我们用排他锁,
那么就会严重影响性能。所以,我们有了MVCC多版本并发控制来处理读写操作。
2.MVCC实现原理
MVCC的目的就是为了实现读写并行操作。那么它是怎么实现的?
MVCC是通过在每行后面增加两个隐藏列和undo log(回滚日志)来实现的:
InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。
这两个列, 一个保存了行的创建时间,一个保存了行的过期时间,
当然存储的并不是实际的时间值,而是系统版本号。
以上片段摘自《高性能Mysql》这本书对MVCC的定义。
这里我们要理解三个概念:
1.系统版本号:每执行一次事务,系统版本号都会递增,这是基于表的,可以称为事务id;
2.创建时间:一次事务如果修改了某一行,那么就保存这次的事务id作为创建时间,可以称为行版本号;
3.过期时间:同理,我们可以称为上一个行版本号;
当我们执行增删改查的时候MVCC是怎么操作的呢?
2.1select
select需要满足两个条件:
行版本号<=事务id
确保读取到的行/数据,要么是事务开始之前存在的,要么是事务自身插入/修改的;
2.上一个行版本号要么未定义,要么>当前事务id
确保事务开始之前,行未删除;
2.2insert
保存当前事务id作为行版本号;
2.3delete
保存当前事务id作为上一个行版本号;
2.4update
可以理解为先delete再insert;
以上这种方式(MVCC)我们称为快照读,读写不冲突,每次读取快照数据,但是不是最新的数据。
快照读:最简单的select操作,属于快照读,不加锁
select * from table where id = 1;
还有一种当前读,读写冲突,但是最新数据。
当前读:特殊的读与增删改操作,属于当前读,会读取数据库原本的数据,加锁
select * from table where xxx lock in share mode; (读/共享锁)
select * from table where xxx for update;(写/排他锁)
insert into table values()
update table set xxx where xxx
delete form table where xxx
以上就是我的一些简单理解,参考
《高性能SQL》(第三版)
https://www.jianshu.com/p/081a3e208e32
https://www.cnblogs.com/jimoer/p/13528278.html