文章目录
1.事务的概念
是数据库的最小工作单元,体现为一条或多条DML的集合,这个集合中所有的语句要么都成功,要么都失败。
在MySQL的4种引擎中,只有InnoDB支持事务(其余三种为MyISam、bdb、memory)。
2.事务的特性
2.1.四大特性-原子性、一致性、隔离性、持久性
- 原子性:一个事务中的所有操作都是一个整体,要么都成功,要么都失败。
- 一致性:数据在事务操作前后必须都要满足业务规则的约束。比如一个账户有90块钱,但是支付100成功了,余额就是-10。这样就没有满足业务的要求。
- 隔离性:数据库允许并发DML,在并发时如果多个事务同时操作同一条数据,就会导致数据不正确,这时候就需要隔离性,即事务与事务之间是相互隔离的,对同一个数据进行操作时,后一个事务必须等到前一个事务commit或rollback后才能执行。
- 持久性:每个事务执行结束后对数据的修改都是永久的,即便系统发生故障,数据库重启后,数据也会恢复原来的状态。
不管是原子性还是隔离性、持久性,最终的目的都是为了实现一致性。
2.2.持久性如何保证
通过redo log 和 双写缓冲来实现。
- redo log
我们在操作数据的时候,MySQL会将数据先保存到buffer pool里面,并记录redo log。如果在从buffer pool刷到硬盘之前就出现了故障导致MySQL服务宕机。在下一次MySQL启动的时候,就会从redo log中读取内容,再写入到磁盘,保证数据的完整性。 - 双写缓冲(double write buffer)
在部分页写入的情况下,redo log是不能保证数据正常恢复的,这是时候就需要使用到双写缓冲。
什么叫部分页写入?
数据库保存数据的单元叫做页(page),在InnoDB中的page size一般是16k,而操作系统写文件的单位是4k,所以对应的一次innoDB页flush到磁盘,需要写入4个块。如果在完全写入完成之前(例如只写入了1个块)发生了宕机,这种情况就叫部分页写入。
部分页写入会造成页损坏,此时再使用redo log就无法恢复宕机前的数据了。
双写缓冲的作用
简单的理解,就是一个备份作用。MySQL将脏数据flush到磁盘时,会将数据复制到double write buffer中。
MySQL宕机恢复后,如果页损坏无法从redo log恢复数据,就会从双写缓冲中恢复。
2.3.事务的常用语句
-- 关闭自动提交
set autocommit = 0;
-- 开启事务
begin;
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;
-- 嵌套事务中的保存存储点和回滚到储存点
save point pointname;
rollback to pintname;
2.4.事务的隐式提交
开启事务后,在同一事务中再次输入begin;就会触发上一个事务的隐式提交。
如果没有输入rollback回滚,或commit提交,事务会在超时和退出会话时隐式回滚。
2.5.事务的隔离级别
MySQL的InnoDB实现了SQL标准的4中隔离级别,用来限定事务内那些改动是其他事务可见的,哪些是不可见的。分为以下4种:
- 未提交读 read uncommited (RU):一个事务可以读到其他事务未提交的数据,这种现象就叫做脏读,除非要做特殊的业务处理,否则不建议使用这种隔离级别。
- 已提交读 read commited (RC):一个事务只能读取到其他事务已经提交的数据。
- 可重复读 repeatable read (RR):一个事务在提交前,无论其他事务对数据做了什么修改,也无论其他事务是否提交,当前事务始终读取到的都是事务开始时读取的数据。这是MySQL的默认隔离级别。
- 串行化 serializable:读写都会加锁,读取数据时加表级共享锁,写入数据时加表级排它锁。这种级别会导致InnoDB并发大幅度下降,发生大量超时和锁竞争。
3.并发事务可能遇到的问题
数据库的并发场景一共有3中:读读、读写、写写。
其中读读不需要并发控制,另外两种如下。
3.1.读写冲突
读写冲突包含以下三种,脏读、幻读、不可重复读。
- 脏读:RU导致脏读,参考第5点读未提交。
- 幻读:重点在于新增,即同一个事务中,同样的条件下两次读取出的数据条数不一致。
- 不可重复读:重点在于修改和删除,同一个事务中,同样的条件两次读取出的数据不一致。
注:幻读和不可重复读的定义参照SQL92标准。
3.2.写写冲突
写写冲突主要是更新丢失问题:
多个事务同时操作同一行数据,每个事务都基于最初读取的数据做修改,最后提交的事务会覆盖掉前面其它事务提交的更新的数据。
第一类更新丢失:
由回滚造成的数据覆盖,下面以用户转账取款为例:
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | 开始事务 |
T2 | 查询余额为1000元 | 查询余额为1000元 |
T3 | 汇入100元,当前余额1100元 | |
T4 | 提交事务 | |
T5 | 取出100元,当前余额为900元 | |
T6 | 回滚事务,余额恢复为1000 |
第二类更新丢失:
由提交造成的数据覆盖,同样以转账取款为例:
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | 开始事务 |
T2 | 查询余额为1000元 | 查询余额为1000元 |
T3 | 汇入100元,当前余额1100元 | |
T4 | 提交事务 | |
T5 | 取出100元,当前余额为900元 | |
T6 | 提交事务,余额修改为900 |
3.1.如何解决事务并发造成的问题
3.1.1.解决一致性的问题
InnoDB中可以通过配置不同的数据库隔离等级来解决并发访问下的一致性问题。
隔离级别与一致性问题对应关系
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | √ | √ | √ |
已提交读 | × | √ | √ |
可重复读 | × | × | √(InnoDB为×) |
串行化 | × | × | × |
隔离级别是如何解决一致性问题的呢?
可以通过MySQL实现的多版本并发控制(MVCC),锁(LBCC)这两种机制来解决。
LBCC–解决写写冲突
针对并发的问题,往往可以想到解决方案就是加锁。MySQL同样可以使用锁来解决。比如我们在读数据时锁定要操作的数据,不允许其他事务修改。比如串行化在读写数据都会加锁。
这里的锁可以选择悲观锁和乐观锁两种。
MVCC–解决读写冲突
可以想到的是,每次操作都去加锁,必要会极大的影响到数据操作的效率。所以引入了另外一种方案,多版本并发控制即MVCC。
MVCC实现了在当前事务第一次读取数据时,创建一个快照,后面不管其他事务对这个数据做了任何的修改,再次读取的数据时从快照中读取,保证每一次读取都与第一次保持一致。
两种解决方案往往是组合使用的,一般可以组合成MVCC+悲观锁,MVCC+乐观锁。
3.1.2.当前读和快照读的概念
根据上面的两种并发事务解决方案,MySQL在查询数据有两种方式。
- 当前读:读取的是记录的最新版本数据,读取时还需要保证其他事务不能修改当前记录,读的时候需要加锁。例如给select加上共享锁或排它锁,以及增、删、改操作都属于当前读。
- 快照读:不加锁的读就是快照读,基于多版本的读取方式,读取的不一定是最新数据,也可能是之前的历史版本数据。MVCC解决的读写不冲突,实际上就是快照读。
4.MVCC的实现原理
上面提到了MVCC实际上就是提供了一个快照读的机制来解决读写冲突,那快照的生成依据是什么呢?要解决这个问题,我们先了解一些概念:
隐式字段
数据行除了显示创建的字段之外,还有一些隐式的字段,例如:
- DB_ROW_ID:没有显示创建主键索引时,InnoDB创建一个自增的字段,并以这个字段为基础创建一个聚簇索引作为主键,6byte。
- DB_TRX_ID:事务ID,记录创建或修改当前这条数据时的事务id。
- DB_ROLL_PTR:回滚指针,指向当前记录的上一个版本(即undo日志)。
MVCC使用到的就是DB_TRX_ID和DB_ROLL_PTR。
undo日志
用来保存当前记录的旧版本,如果就版本的数量大于1个,就会形成一个旧记录链表,使用回滚指针进行关联。链首为分别对应最新的旧记录和最老的旧记录。
活跃事务ID
每个事务开启时都会分配一个ID,这个ID是递增的。在做增、删、改操作时,这个活跃的事务ID会写入到DB_TRX_ID这个隐藏列中。
-- 查询当前索引活跃事务ID
select trx_id from INFORMATION_SCHEMA.INNODB_TRX;
MVCC多版本的体现
我们在更新一行记录时,新纪录会保存到表中,旧记录会保存到undo日志中。这里的新旧两个记录就是当前更新的这行数据的两个版本,用事务ID做区分。
4.1.Read View的生成
Read View就是事务在快照读的时候,维护当前系统中所有活跃事务的ID,并根据事务ID和Read View的可见性算法,生成的一个读视图。
可以简单的理解,Read View维护了以下的三个全局属性:
- 创建快照的事务id(creator_trx_id)
- 当前所有活跃事务列表(ids)
- 当前所有活跃事务的最小值(up_limit_id)
- 尚未分配的下一个事务id,即当前活所有跃事务最大值+1(low_limit_id)
当前是指的快照生成的时间点,这三个全局属性生成后对当前的快照,不会再改变。
4.1.1.可见性算法
可见性算法是用来判断当前检索出的行数据是否是可见的,流程如下:
- 首先获取当前行数据的事务ID,记作DB_TRX_ID。
- 如果DB_TRX_ID < up_limit_id,表示这一行数据是在生成快照之前提交的,返回可见。
- 如果DB_TRX_ID>=low_limit_id,表示这一行数据在生成快照时还没有创建,返回不可见。
- 如果 up_limit_id < DB_TRX_ID < low_limit_id,就看DB_TRX_ID有没有在ids中,如果在表示这行数据在生成快照时,还没有提交,返回不可见,如果不在则返回可见。
- 此外,如果查询出的行数据的事务id,就是当前创建快照的事务id(creator_trx_id),无论如何都是可见的。
对于不可见的行记录,根据当前记录的DB_ROLL_PTR指针,在undo log中取出最近一次旧记录,用这个记录的事务id再做一次上面的条件判断。直到找到一个对快照读可见的旧记录或DB_ROLL_PTR指向空为止。
4.2.RR和RC级别下的Read View
两者的生成时机不同:
RR是在某个事务中第一次快照读时生成快照,在事务提交前每一次快照读都是读的第一次生成的快照。这种方式避免了不可重复读和幻读。
RC是在某个事务中,每次快照读都会生成一个快照,只解决了脏读的问题。