事务
从ACID的角度解读InnoDB的事务
原子性
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
为啥需要undo log
因为事务有需要回滚,undo log告诉innodb如何回滚,回滚到什么状态
实现原理 undo Log
->undo log的实现原理
原子性的实现是undo log,InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
持久性
事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响
实现原理 redo log
为啥需要redo log
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
redo log如何保证持久性
当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘(commit时将redo log的数据从buffer pool同步到物理磁盘,但是redolog同步的时机不仅仅只限于commit)。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
redo log日志刷盘性能与数据库数据刷盘性能对比
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
- 刷脏是随机IO,因为每次修改的数据位置随机(每次找修改的点麻烦,需要检索出所在的磁盘页,然后在磁盘页中检索出修改位置),但写redo log是追加操作,属于顺序IO(检索出所在的磁盘页,然后顺着磁盘页往下写)。
- 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入,数据所在的磁盘页都需要重写,而数据的写入往往又伴随着临近磁盘页的挪动,而redo log中只包含真正需要写入的部分,即那些导致数据页发生变动的操作
redo log与binlog都可以恢复数据,有什么不同?
- 作用不同
redo log是用于保证MySQL宕机也不会影响持久性,恢复的是物理数据页;binlog主要用于灾备用于主从复制,并且binlog先于redo log被记录 - 层次不同
redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层实现的,同时支持InnoDB和其他存储引擎。 - 内容不同
redo log是物理日志,记载磁盘Page发生的变动的过程;binlog属于逻辑日志,记载的是导致业务数据发生变动的sql以及对应的时间点、行、列、变动前数据、变动后数据,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。 - 写入时机不同
binlog在事务提交时写入;redo log的写入时机相对多元,除了提交事务,master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。
隔离性
与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
- (一个事务)写操作对(另一个事务)写操作的影响:基于锁机制的并发控制保证隔离性-LBCC
- (一个事务)写操作对(另一个事务)读操作的影响:多版本并发控制保证隔离性-MVCC
基于锁机制的并发控制保证隔离性-LBCC
innodb为了防止两个事务对同一条数据做修改,innodb所有改动表的操作记录都需要执行当前读(update , delete , insert),除了增删改以外,使用共享锁、排他锁读区数据也可以被称为当前读操作,简而言之,当前读操作本质是“数据读取行为的串行化”,所有的改动和读取操作被单线程同步,因此保证读取的数据一定是最新版本
共享锁与排他锁-不同加锁方式的锁
- 共享锁-lock in share mode
如果事务对某行数据加上共享锁之后,可进行读写操作;其他事务可以对该数据加共享锁,但不能加排他锁,且只能读数据,不能修改数据。 某个事物想进行修改数据操作,那他必须等其他事物的共享锁都释放完毕才能进行修改操作set autocommit=0; #开启之后才可以使用 lock in share mode SELECT * FROM order WHERE id = 1 LOCK IN SHARE MODE; #锁住行 SELECT * FROM order WHERE id = 1 or id = 2 LOCK IN SHARE MODE # 不冲突,会阻塞 SELECT * FROM order WHERE id = 1; #不冲突 SELECT * FROM order WHERE id = 1 or id = 2 FOR UPDATE; # 冲突,会阻塞 UPDATE order set price = 1.0 WHERE id = 1; #冲突,会被阻塞
- 排他锁-for update
普通 select 语句默认不加锁,而CUD操作默认加排他锁。
如果事务对数据加上排他锁之后,则其他事务不能对该数据加任何的锁。获取排他锁的事务既能读取数据,也能修改数据。但是对已加锁的数据做不加锁读取时不会阻塞的set autocommit=0; #开启之后才可以使用for update SELECT * FROM order WHERE id = 1 FOR UPDATE; #锁住行 SELECT * FROM order WHERE id = 1 or id = 2 FOR UPDATE; # 冲突,会阻塞 SELECT * FROM order WHERE id = 1 or id = 2 LOCK IN SHARE MODE # 冲突,会阻塞 UPDATE order set price = 1.0 WHERE id = 1; #冲突,会被阻塞 SELECT * FROM order WHERE id = 1; #不冲突
记录锁、间隙锁、临键锁-不同锁定范围的锁
- 记录锁:
对表中的行记录加锁,叫做记录锁,简称行锁。可以使用sql语句select … for update来开启锁,select语句必须为精准匹配(=),不能为范围匹配,且匹配列字段必须为唯一索引或者主键列。也可以通过对查询条件为主键索引或唯一索引的数据行进行UPDATE操作来添加记录锁。 - 间隙锁:
对上面说到的间隙加锁即为间隙锁。间隙锁是对范围加锁,但不包括已存在的索引项。可以使用sql语句select … for update来开启锁,select语句为范围查询,匹配列字段为索引项,且没有数据返回;或者select语句为等值查询,匹配字段为唯一索引,也没有数据返回。
间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。以下是加锁之后,插入操作的例子://假设有一张表最大的id为11 select * from user where id > 15 for update; //插入失败,因为id20大于15,不难理解 insert into user values(20,'20'); //插入失败,原因是间隙锁锁的是记录间隙,改表不存在id=15的临界,故临界点向上查找一直找到11,所以真实的锁定范围是(11,+∞),而13在这个区间中,所以也失败。 insert into user values(13,'13');
- 临键锁:
当我们对上面的记录和间隙共同加锁时,添加的便是临键锁(左开右闭的集合加锁)。为了防止幻读,临键锁阻止特定条件的新记录的插入,因为插入时要获取插入意向锁,与已持有的临键锁冲突。可以使用sql语句select … for update来开启锁,select语句为范围查询,匹配列字段为索引项,且有数据返回;或者select语句为等值查询,匹配列字段为索引项,不管有没有数据返回。
锁定范围
innodb中并没有具体的行锁或者表锁的实现,lock in share mode或者for update都可以锁行或者锁表或者锁定一个范围,这个锁定范围取决于查询条件
明确指定了主键id(=或者in),大部分情况下我们都要避免表锁,通过加锁分析,优化sql可以尽量降低锁的颗粒度有助于性能的提升.
- 锁定行:精确查询并且查询列是一个索引列时,锁定单独行(=,in,查询列有索引)
- 锁定范围:范围查询并且查询列(至少)有一个索引列时,根据最左原则,锁定索引列范围内的所有数据
- 锁定表:范围查询且无法用到索引,此时锁住全表
总结 - 如果查询没有命中索引,则退化为表锁;
- 如果等值查询唯一索引且命中唯一一条记录,则退化为行锁;
- 如果等值查询唯一索引且没有命中记录,则退化为临近结点的间隙锁;
- 如果等值查询非唯一索引且没有命中记录,退化为临近结点的间隙锁(包括结点也被锁定);如果命中记录,则锁定所有命中行的临键锁,并同时锁定最大记录行下一个区间的间隙锁。
- 如果范围查询唯一索引或查询非唯一索引且命中记录,则锁定所有命中行的临键锁 ,并同时锁定最大记录行下一个区间的间隙锁。
- 如果范围查询索引且没有命中记录,退化为临近结点的间隙锁(包括结点也被锁定)。
锁释放时机
- 事务提交后释放锁
- 死锁后数据库系统的各种死锁检测和死锁超时机制生效释放锁
多版本并发控制保证隔离性-MVCC
读一致性问题
一个写操作会对另一个读操作产生的影响:
- 脏读
当前事务读取到其他事务未提交的操作
事务A读取一行数据->事务B改了这行数据,但是还未提交->事务A再次读取这行数据,发现数据不对了 - 不可重复读
当前事务读取到其他事务已提交的数据(这是区分脏读和不可重复读的依据)
事务A读取一行数据->事务B改了这行数据,提交->事务A再次读取这行数据,发现数据不对了 - 幻读
当前事务读取到其他事务新增的数据
事务A查询select * from table where age>10 -> 事务B插入一条age=11的数据 ->事务A查询select * from table where age>10
多了一条数据,事务A会感觉很魔幻,两次读出来的数据不一样
事务隔离级别
隔离级别 | 脏读 | 幻读 | 不可重复读 |
---|---|---|---|
Read Uncommited 读未提交 | 可能 | 可能 | 可能 |
Read Commited 读已提交 | 不可能 | 可能 | 可能 |
repeatable read 可重复读 | 不可能 | 不可能 | |
serializable 串行化 | 不可能 | 可能 | 可能 |
读一致性问题解决方案-快照读
顾名思义,快照读即在事务开始前dui
一致性
读一致性问题解决方案
- LBCC:读取数据时对数据加锁防止其他事务修改
- MVCC:事务在读区数据时对当前数据拍快照,这个快照在当前事务里为这行数据一直提供数据(其他事务可以改,但是改了以后由于当前事务读快照,不会对这个当前事务影响)
行级别锁-共享锁、排他锁
innodb的行锁是靠对行的二级索引和聚集索引加锁实现的。
- 共享锁:又叫读锁一个事务对一个数据加了共享锁,其他事务能读取,但是不能修改。
加锁:select * from student where id=1 lock in share mode
释放锁:commit/rollback - 排他锁:拍他锁不能与其他锁并存,如一个事务获取了一个数据的排他锁,其他事务不能读取/修改这行数据
加锁:delete/update/insert 默认上排他锁,select:select * from student where id=1 FOR UPDATE
表级别锁-意向锁、表锁
事务在准备加排他锁、共享锁前必须先拿到这张表的意向锁。意向锁由引擎自己维护,不是一个具体实现的锁,而是一个标记,用来再给表加锁时迅速判断是否能 成功上锁(如果没有意向锁,表锁得遍历所有数据确定没有一行数据是被行锁的时候才能对表加锁)
锁算法
间隙锁
select * from table where id>4 and id<7 for update 间隙锁将4-7之间的数据给锁了,4和7这两个记录不会加锁(在闭区间加锁),加锁的区间内无法插入数据,这样在读取数据时不会出现幻读(读不进去了)。
如果这张表实际长度只有3,那么真实锁住的范围是(3,正无穷]
innodb正是靠着间隙锁和行锁解决了在“可重复读”级别下对幻读问题的解决。
undo log和redo log
- undo log:事务在做一个操作时,必然会生成一个反向的操作存在undo log中,事务执行一半,一旦遇到需要执行回滚,就按照undo log回滚
- redo log:是InnoDB存储引擎层的日志,又称重做日志文件,用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。在实例和介质失败(media failure)时,redo log文件就能派上用场,如数据库掉电,InnoDB存储引擎会使用redo log恢复到掉电前的时刻,以此来保证数据的完整性。
- bin log:用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
用于数据库的基于时间点的还原。
- 读未提交(Read uncommitted):MySQL 事务隔离其实是依靠锁来实现的,加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的,所以它的性能是最好的,没有加锁、解锁带来的性能开销。但有利就有弊,这基本上就相当于裸奔啊,所以它连脏读的问题都没办法解决。这种事务隔离级别下,select语句不加锁。此时,可能读取到不一致的数据,即“读脏 ”。这是并发最高,一致性最差的隔离级别。
- 读已提交(Read committed):读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据。那脏数据问题迎刃而解了。可避免脏读的发生。在互联网大数据量,高并发量的场景下,几乎不会使用上述两种隔离级别。
- 可重复读(Repeatable read):MySql默认隔离级别。可避免脏读 、不可重复读的发生。
- 串行化(Serializable ):串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。可避免脏读、不可重复读、幻读 的发生。效率最拉垮,也最安全