MySQL事务问题(基于InnoDB存储引擎)

        最近看了很多关于事务问题的博客,感觉看的好混乱,没有一个整体的架构来谈事务;
所以就根据自己的见解谈一谈关于事务的问题。

一、事务的四大特征(ACID)

事务的四大特性即原子性,持久性,隔离性和一致性, 一致性是事务的最终目的,而原子性,持久性,隔离性则是一致性的保证。

下面我们就分别谈谈事务是怎么具体通过原子性,持久性和隔离性实现一致性的:

1.原子性

原子性:事务中的所有操作,要么一起成功,要么一起失败,没有别的状态。 那么事务怎么保证一起成功或者失败呢?

InnoDB通过会把一个事务中所作的修改(即:insert,delete,update),备份到undo log(回滚日志)中,保存在磁盘,当事务执行失败,通过回滚日志进行回滚,保证了事务原子性。

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",事务提交完成;

3.隔离性

隔离性:多个并发事务之间相互隔离,互不影响。 这里有一个隔离级别的概念:读未提交,读已提交,可重复读(MySQl默认隔离级别),串行化。 一般来说,隔离级别越高并发性能越低。

四个隔离级别可能造成的问题:

读未提交:一个事务读取了别的事务未提交的数据,可能造成脏读; 读已提交:一个事务连续两次读到的行数据不一致,别的事务在中间 对数据进行过操作,可能造成不可重复读; 可重复读:一个事务连续两次读到的区间数据行数不同,别的事务在 对区间数据进行了增删,可能造成幻读; 串行化:最高隔离级别,但是并发性能最低。

一般我们两个或多个事务同时操作一个资源时,会出现以下情况:读读操作,写写操作,读写操作,那么我们谈谈MysQL是如何处理这三种情况的:

读读操作:事务互不影响,可以并行,我们有一个共享锁处理这种情况,也称读锁; 写写操作:事务必定互相影响,不可并行,那么我们有一个排他锁处理,也称写锁; 读写操作:是我们最为常见的情况,势必互相影响,不可并行,如果我们用排他锁, 那么就会严重影响性能。所以,我们有了MVCC多版本并发控制来处理读写操作。

二、MVCC实现原理(Multi-Version ConCurrency Control,即多版本控制协议)

MVCC的目的就是为了实现读写并行操作。那么它是怎么实现的?
MVCC是通过在每行后面增加两个隐藏列和undo log(回滚日志)来实现的:

InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。 这两个列, 一个保存了行的创建时间,一个保存了行的过期时间, 当然存储的并不是实际的时间值,而是系统版本号。

以上片段摘自《高性能Mysql》这本书对MVCC的定义。

这里我们要理解三个概念:

1.系统版本号:每执行一次事务,系统版本号都会递增,这是基于表的,可以称为事务id;

2.创建时间:一次事务如果修改了某一行,那么就保存这次的事务id作为创建时间,可以称为行版本号;

3.过期时间:同理,我们可以称为上一个行版本号;

当我们执行增删改查的时候MVCC是怎么操作的呢?

1.select

select需要满足两个条件:

1.行版本号<=事务id
确保读取到的行/数据,要么是事务开始之前存在的,要么是事务自身插入/修改的;
2.上一个行版本号要么未定义,要么>当前事务id
确保事务开始之前,行未删除;

2.insert

保存当前事务id作为行版本号;

3.delete

保存当前事务id作为上一个行版本号;

4.update

可以理解为先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》(第三版)

MySQL事务 - 简书
MySQL是如何实现事务的ACID - 纪莫 - 博客园

你可能感兴趣的:(mysql,java)