事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个特性,简称 ACID。
原子性(atomicity):一个事务应该是一个不可分割的工作单位,事务中包括的操作要么都成功,要么都不成功。
一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据在事物未提交前对并发的其他事务是隔离的,并发执行的各个事务之间不能互相影响。
持久性(durability):一个事务一旦成功提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
隔离性
事务隔离级别解决问题
1. 脏读
脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。
2. 不可重复读
对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。
3. 幻读
幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。
4. 可重复读
可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。
事务隔离级别
SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:
读未提交(Read Uncommitted):解决更新丢失问题。如果一个事务已经开始写操作,那么其他事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现,即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据不需要加 S 锁。
读已提交(Read Committed):解决了脏读问题。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。这可以通过“瞬间共享读锁”和“排他写锁”实现, 即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁,当数据读取完成后立刻释放 S 锁,不用等到事物结束。Oracle默认隔离级别
可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻读数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。Mysql默认使用该隔离级别。这可以通过“共享读锁”和“排他写锁”实现,即事物需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁,当数据读取完成并不立刻释放 S 锁,而是等到事物结束后再释放。MySQL默认隔离级别
MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决时不彻底的。
串行化(Serializable):解决了幻读的问题的。提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
隔离级别的实现
MVCC
多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交和可重复读取隔离级别的实现。
MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。
InnoDB实现MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:
1)隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。
2)基于undo log的版本链:前面说到每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。
- 在每一行数据中额外保存两个隐藏的列:当前行创建时的版本号和删除时的版本号(事务的id值),每次数据更新时都更新该版本。
- 修改时Copy出当前版本随意修改,各个事务之间无干扰。
- 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
当前读和快照读
读到的数据可能是历史数据,不是数据库最新的数据。这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。
当前读:像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。当前读实际上是一种加锁的操作,是悲观锁的实现。
快照读:像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
实现原理
主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。
隐式字段
Mysql每行记录都有隐式字段
- DB_TRX_ID
6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR
7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
- DB_ROW_ID
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
- 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
undoLog
事务对记录修改的时候,会生成undoLog,就是当前行的副本,并且会将行的事务id加一(自增),并且回滚指针指向undoLog,另一个事务修改的时候,行数据拷贝到undolog中,发现该行记录已经有undolog了,最新的undoLog指针会指向前一个,形成版本链。
insert undo log:代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃。
update undo log:事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。
purge:为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
Read View
主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undolog里面的某个版本的数据。
InnoDB为每一个事务构造了一个数组m_ids用于保存一致性视图生成瞬间当前所有活跃事务(开始但未提交事务)的ID,将数组中事务ID最小值记为低水位m_up_limit_id,当前系统中已创建事务ID最大值+1记为高水位m_low_limit_id,构成如图所示:
一致性视图下查询操作的流程如下:
1、 当查询发生时根据以上条件生成ReadView,该查询操作遍历Undo log链,根据当前被访问版本(可以理解为Undo log链中每一个记录即一个版本,遍历都是从最新版本向老版本遍历)的DB_TRX_ID,如果DB_TRX_ID小于m_up_limit_id,则该版本在ReadView生成前就已经完成提交,该版本可以被当前事务访问。DB_TRX_ID在绿色范围内的可以被访问
2、若被访问版本的DB_TRX_ID大于m_up_limit_id,说明该版本在ReadView生成之后才生成,因此该版本不能被访问,根据当前版本指向上一版本的指针DB_ROLL_PT访问上一个版本,继续判断。DB_TRX_ID在蓝色范围内的都不允许被访问
3、若被访问版本的DB_TRX_ID在[m_up_limit_id, m_low_limit_id)区间内,则判断DB_TRX_ID是否等于当前事务ID,等于则证明是当前事务做的修改,可以被访问,否则不可被访问, 继续向上寻找。只有DB_TRX_ID等于当前事务ID才允许访问橙色范围内的版本
4、最后,还要确保满足以上要求的可访问版本的数据的delete_flag不为true,否则查询到的就会是删除的数据。
如何解决幻读
- 使用串行化读的隔离级别
- MVCC+next-key locks:next-key locks由record locks(索引加锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)
原子性
InnoDB实现回滚,靠的是undo log。当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
当事务提交之后,undo不会被马上删除,而是放入待删除队列,由purge线程来判断是否删除和处理。
在mysql5.6之前,undo只存在于共享表空间中,之后的版本中,则可以配置为独立的文件。
undo内部默认128个回滚段槽(rseg slot),每个rseg slot内部有1024个回滚段(rollback segment)。其中:
slot0——共1个,预留给系统表空间
slot1~slot31——共31个,预留给临时表空间
slot32~slot127——共96个,预留给undo独立表空间
持久性
Mysql是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。
事务提交前直接把数据写入磁盘.
redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
mysql的恢复策略是:
1.恢复时,先根据redo重做所有事务(包括未提交和回滚了的)
2.再根据undo回滚未提交的事务。
redo日志会随着时间推移而越来越大,为了提升redo的恢复性能,引入了checkpoint机制,在恢复的时候,只需要从checkpoint的位置往后恢复即可。
一致性
重做日志,回滚日志以及锁技术就是实现事务的基础。
- 事务的原子性是通过 undo log 来实现的
- 事务的持久性是通过 redo log 来实现的
- 事务的隔离性是通过 (读写锁+MVCC)来实现的
日志
WAL
WAL 是 Write Ahead Log 的缩写,也就是当我们更新数据库数据时,首先将数据写入日志,然后再写入至真正的 DB 文 件中,其目的是为了解决数据库写入效率问题
日志两阶段提交保证数据一致性
重做日志(redo log)
确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
redo log在数据准备修改前写入缓存中的redo log中,然后才对缓存中的数据执行修改操作;而且保证在发出事务提交指令时,先向缓存中的redo log写入日志,写入完成后才执行提交动作。
回滚日志(undo log)
保存了事务发生之前的数据的一个版本,可以用于回滚
undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
二进制日志(binlog)
记录 MySQL Table 中的逻辑操作,比如更新了哪条数据,对这条数据做了哪些更新,可用于恢复因误删库导 致的数据丢失
用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
binlog 的日志格式主要有两种,Statement 和 Row(推荐)
两阶段提交的目的就是让 redo log 和 binlog 在逻辑上保持一致
慢查询日志
慢查询日志记录了执行超过一个阈值的SQL语句,通过该日志,可以帮助定位存在问题的SQL语句,并进行优化。通过参数long_query_time设置这个阈值,默认10,单位秒。
查询日志
查询日志记录了所有对MySQL数据库请求的信息,无论这些请求是否得到了正确的执行,默认文件名为:主机名.log。因为SQL的量大,默认是不开启的。一些特殊情况(如排除故障)可能需要临时开启一下。
错误日志
错误日志文件对MySQL的启动,运行,关闭过程进行了记录,该文件不仅记录了所有的错误信息,也记录了一些告警信息或正确的信息。通过命令show variables like ‘log_error’;可以定位错误日志文件位置。