InnoDB核心特性
Innodb的RR到底有没有解决幻读?
普通查询(快照读)能完全解决,具体的方案就是第一次查询的时候生成快照,后续基于这个快照读取,不会出现幻读
加锁查询/增加/更新/删除(当前读),靠间隙锁基本解决幻读
但是极端情况下,比如在同一个事务里面先快照读,在当前读,会出现幻读现象,因为当前读,拿到的是最新的数据
具体流程:
- 事务A执行了普通select(快照读)
- 然后事务B执行了当前读操作(insert刚才查询的记录)
- 事务A通过select for update 加锁读取,发现和原来结果不一样
原本事务A它一开判断到的是数据不存在,然后再判断一次发现存在,出现了幻读现象
总的来说,RR解决了99% 的幻读场景,但是极端情况下任然会存在,如果想要理论上100%解决,需要使用 Serializable 隔离级别
InnoDB的一次更新事务是怎么实现的?
update操作执行的全流程:
- 先把数据读取到内存 Buffer Pool 中
-
- 要更新某行数据,先看内存中有没有,没有的话就从磁盘中加载进来
- 写undo log(把旧数据记录下来)
-
- 更新前线记录原来的值,为的就是事务回滚的时候可以恢复,以及MVCC快照读需要用到旧版本数据
- 先写内存,后台异步刷盘
- 在内存中直接更新数据
-
- 直接修改Buffer Pool中的数据,然后这个数据变成脏页,后台异步刷盘
- 写 Redo Log Buffer
-
- 把本次操作记录到redo log 中,不过同样是先写内存,后台异步刷盘的方式
- redo log 就是记录你执行的每一条指令,除了查询之外,用来宕机恢复用的
- 提交事务
-
- 提交的时候会做几件事
-
-
- redo log 刷盘
- 写binlog(这个用来主从复制的)
- 内部两阶段提交,保证redo log 和 binlog 一致
-
- 脏页刷盘
-
- 不过这个不是事务提交就刷盘,而是后台慢慢刷盘
- 即使没刷,也不会丢失,因为已经确保redo log 刷盘完成
- binlog 记录完整事务
两阶段提交:
redolog 先写盘,并标记为prepare,然后binlog写盘,如果成功就把redolog标记为commit
能保证两者都成功才算提交
总的来说就是靠redolog保证数据不丢,靠undolog保证可回滚,靠两阶段提交保证主从一致性
InnoDB和MyISAM有什么区别?
事务:
- InnoDB 支持事务,支持提交和回滚
- MyISAM 不支持事务,写完立马生效,不能回滚
锁粒度:
- InnoDB:行锁,只锁某一行,并发高
- MyISAM:表锁,锁的粒度很大,并发差
索引结构:
- InnoDB采用聚簇索引:索引和数据放在一起,叶子节点存储的就是整行数据
- MyISAM采用非聚簇索引:索引存储的是数据的地址,索引和数据分开存储
外键:
- InnoDB 支持外键
- MyISAM 不支持
表总行数:
- InnoDB 不存储表行数, count (*) 要扫描全表
- MyISAM专门存储了总行数, count (*) 极快
清空表:
- InnoDB需要一行行删除,速度慢
- MyISAM 直接重建表,速度极快
安全性:
- InnoDB 宕机可恢复
- MyISAM 宕机直接损坏数据
Innodb加索引,这个时候会锁表吗?
看版本,
5.6之前会锁表,读写都做不了
5.6之后,引入了后台加索引的操作,只有开始和结束会加锁,中间不影响读写,后台默默创建索引
InnoDB如何解决脏读、不可重复读和幻读的?
脏读、不可重复读 靠MVCC解决
幻读 靠MVCC+间隙锁解决
脏读就是读取到别人没提交的数据,RR/RC靠MVCC的可见性判断(ReadView)直接隔绝,只能看到别人提交的数据
不可重复读就是同一个事务中,两次查询结果不一致,RR通过第一次查询生成ReadView,后续都是基于这个快照进行查询,别人提交的修改你也看不见
幻读就是同一个查询突然多/少了几行,解决思路,快照读用MVCC,RR级别下因为整个数据用同一个ReadView,可以很好的解决;当前读,每次select for update/增删改都会加上间隙锁,基本上能解决
但是如果同一个事务先快照读再当前读就会出现幻读
InnoDB支持哪几种行格式?
四种,就是决定着数据如何存储
REDUNDANT → COMPACT → DYNAMIC → COMPRESSED
冗余→紧凑→动态→压缩
现在默认用的是动态
InnoDB中的表级锁、页级锁、行级锁?
InnoDB 不支持页级锁,MyISAM 才支持
行级锁,锁的是索引,不是数据本身,说白了就是先根据数据找到对应的id,如何把这条记录锁起来,本质上和锁数据效果一样,只不过人家认的是id
常见的是 UPDATE ... WHERE id=1
特点:只锁需要的行
表级锁,表级锁有好几种(都是自动加的)
- 意向锁
-
- 事务要加行锁前,先给表加个意向锁
- 作用:快速判断 "这张表能不能直接加表锁"
- 不会阻塞普通 DML,只和表锁互斥
- AUTO-INC 锁
-
- 插入自增 ID 时用的表锁
- 保证 ID 连续
- 现在基本优化成轻量锁,不会阻塞太久
- MDL 元数据锁
-
- 只要操作表(select/update/alter)都会加
- 作用:防止你查询的时候,别人把表结构改了
- DDL 会拿写 MDL 锁,会阻塞所有读写
- 手动加锁(基本不用)
-
- LOCKTABLES t READ;
- LOCKTABLES t WRITE;
页级锁:按照页为维度去锁,一次锁好几条,innoDB 没有
常见误区:行级锁当你的条件没有执行索引的时候会直接锁表?
不对,没指定索引的时候会全表扫描出符合条件的记录Id,把这些Id都锁上,只是锁住多条,本质上还是行锁,不是表锁
MySQL只操作同一条记录,也会发生死锁吗?
会
当出现两个事务操作同一条记录的时候有可能会发生死锁
原因就是加锁的顺序反了
当更新一条记录的时候,MYSQL会同时锁住这条记录的主键索引和普通索引
UPDATE 表 SET ... WHERE name = 'xxx';
操作的是name,所以会先锁name,再尝试去锁主键
UPDATE 表 SET ... WHERE id = 15;
操作的是主键,会先锁主键,再尝试锁name
出现死锁的例子
update my_table set name = 'tom',age = 22 where name = "tom111";
先锁住name,然后再根据查询出来的id去锁主键
select * from my_table where id = 15 for update;
update my_table set age = 33 where name like "tom%";
先锁id,再锁对应的name
这个时候就出现了互相占有且等待的情况,解决方法就是统一加锁的顺序,先锁id,再锁普通索引
MySQL中的事务隔离级别?
脏读:读到别的事务没提交的数据
不可重复读:一个事务内,两次查询结果不一样(被别人 update 了)
幻读:一个事务内,突然多出来 / 少了一行(被别人 insert /delete 了)
MySQL 一共是有四种事务隔离级别
RU,读未提交
- 能读取到别人还没有提交的数据
- 脏读、不可重复读、幻读都有
- 基本不同
RC,读已提交
- 只能读取到别人提交的数据
- 解决了脏读,但是没解决不可重复读和幻读
RR,可重复读
- 解决了脏读和不可重复读
- 没有解决幻读,是默认的隔离界别
- InnoDB 通过MVCC+间隙锁基本解决幻读,但是无法100%
串行化
- 解决脏读,不可重复读,幻读
- 几乎没有并发能力
undolog会一直存在吗?什么时候删除?
不会一直存在,会被 purge 线程自动删除
但是具体什么时候能被删除,需要分两种
undolog 有两种, insert undo log(插入产生的) 和 update/delete undo log(更新、删除产生的)
insert undo log 用来回滚的,只要事务一提交,别人就不需要看你插入前的版本,可以直接被删除
update/delete undo log 不但用来回滚,还用来给MVCC提供快照历史版本,只要还有事务需要读取这个旧数据就不能删除,必需等所有需要读取它的事务都结束才能删
删除的过程就是,purge 线程查看当前这个undo log 对应的旧版本的数据是否还有事务需要读取,没有的话就可以删除
判断是否还有事务需要读取就是通过判断所有活跃事务中,最早的 ReadView 如果还比当前 undo log 对应的版本年轻,就说明可以当前undo log 可以删除(也就是旧的可以删除,新的需要留着)
需要注意的是,长事务会导致undo log 堆积,比如你开了一个事务执行了一些select 之后就放着,这个过程中,任务的delete/update产生的undo log 都不能删除,因为他们的版本比较年轻,可能会被用到
当前读和快照读有什么区别?
所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读。
SELECT * FROM xx_table WHERE ...
和快照读相对应的另外一个概念叫做当前读,当前读就是读取最新数据,所以,加锁的 SELECT ,或者对数据进行增删改都会进行当前读
SELECT * FROM xx_table LOCK IN SHARE MODE;
SELECT * FROM xx_table FOR UPDATE;
INSERT INTO xx_table ...
DELETE FROM xx_table ...
UPDATE xx_table ...
只有RR和RC才会使用快照读
高并发情况下自增主键会不会重复,为什么?
不会,因为表锁有一个**AUTO-INC,**每次主键自增的时候就是通过这个锁来确保唯一分配,即使高并发也不会重复
它的工作原理就是,当一个事务尝试向一个包含自增表中插入一条或者多条记录的时候,InnoDB 就会申请一个**AUTO-INC锁,**确保插入期间没有其它事务可以插入
当然这里都是针对单表,多表就不一定了
介绍下InnoDB的锁机制?
锁主要分两种:S锁 和 X锁
共享锁S = 读锁
SELECT ... LOCK IN SHARE MODE;
特点:大家都能读,但是都不能改
排他锁X = 写锁
加锁方式:UPDATE/DELETE/INSERT 自动加,或 SELECT ... FOR UPDATE
加锁方式:UPDATE/DELETE/INSERT 自动加,或 SELECT ...更新
特点:只有我能读能改,别人都不能读不能改
并且X和任何锁都是互斥的,无论是X还是S
意向锁 IS/IX
意向锁就是提前打个招呼,表示自己要给表上的一些字段加锁
比如:
- IS表示准备加S锁
- IX表示准备加X锁
意向锁的作用只有一个:快速判断能否给整张表加某种锁
避免别人正在对某些行加锁,自己也错误加锁
意向锁之间都是兼容的,IS和IX兼容,因为只是一个意向,不是真的读写锁,但是后面真正加S和X的时候就会互斥
行锁三兄弟:记录锁、间隙锁、临建锁
记录锁 Record Lock
- 锁某一条索引记录
- 只锁这一行
- 条件必须是精确匹配唯一索引(=)
- 比如:
where id = 10
间隙锁 Gap Lock
- 锁两个索引之间的空隙
- 不锁记录,只锁 "中间空位"
- 目的:防止别的事务插入数据,避免幻读
临键锁 Next-Key Lock
- 记录锁 + 间隙锁 合体
- 锁当前记录 + 前面的间隙
- InnoDB 默认的行锁算法
- 可解决幻读问题
间隙锁是很容易出现死锁现象的,因为间隙锁本身是不会互斥的,也就是一个线程加了间隙锁(1,10),另一个线程同样可以加(1,10),间隙锁只阻塞插入操作
问题来了
事务A锁住(1,10),尝试插入7
事务B同样锁住(1,10),尝试插入7
那么就出现死锁了,两个事务都因为被对方的间隙锁阻塞了插入7的能力
解决方法:
- 改成RC,RC模式下,没有间隙锁,只保留了记录锁
- 或者不要使用间隙锁,而是通过其它的手段来保证互斥性
插入意向锁
表示自己打算插入一行记录,专门给 insert 使用的
如果两个事务要插入同一条ID,就会阻塞
AUTO-INC 锁(自增锁)
给自增主键用的表锁。
- 旧版:表级锁,插入时锁全表,并发差
- 新版:可以设置成连续模式,提高并发
- 保证自增 ID 不重复、不乱序
具体的优化就是,当能确定插入行数的时候,会通过预先获取一批Id的方式,避免长时间加锁
当不确定具体插入行数的时候,就依然使用原来的抢锁思路
介绍一下InnoDB的数据页,和B+树的关系是什么?
数据页是磁盘读写数据的最小包裹,默认大小16KB
无论你一次是读取多少,InnoDB 从磁盘读取的时候都是按照页为单位进行读取,一页包含多条记录
B+ 树是InnoDB 的索引结果,主要用来快速的定位数据的位置
B+树的节点就是页,一个节点 = 一个数据页
不过叶子节点存储的是真实地数据,而非叶子节点存储的是主键值+指向下一层数据页的地址
乐观锁与悲观锁如何实现?
悲观锁就是提前加锁地思想,提前避免并发问题
begin;
select * from goods where id=1 for update; -- 上锁!
update goods set stock=2 where id=1;
commit; -- 解锁
比较适合写多且要数据强一致性地场景
乐观锁就是不加锁,但是修改完要提交地时候会判断数据是否变了
-- 1. 查询数据和版本号
select stock, version from goods where id=1;
-- 2. 修改时,必须版本号一致才更新
update goods
set stock=2, version=version+1
where id=1 and version=1;
适合需要读多写少地场景
什么是InnoDB的页分裂和页合并
页分裂,这一页满了,要插入新的数据的时候,需要把这一页拆成两页再写入
比如原本已经存储了Id 1、2、4、5、6
现在又来了一个 id = 3,但是放不下了
MySQL 会拆分成两页
- 旧页:1、2
- 新页:3、4、5、6
这个过程就是页分裂
后果:
- 要新开页、移动数据、修改指针
- IO 高、慢、耗性能
- 会产生磁盘碎片
页合并,一个页地数据被不断的删除,到最后只剩下一点,MySQL 为了提高利用率会自动进行合并,把空页回收掉
后果:
- 同样要移动数据、调整指针
- 频繁删除 → 频繁合并 → 性能抖动
避免页分裂,使用自增Id,不要使用UUID
避免页合并,尽量逻辑删除,减少使用物理删除
什么是ReadView,什么样的ReadView可见?
ReadView 就是一个可见性判断器,它能决定你当前事务,能看见哪个版本的数据,看不见哪个
可以这样理解,就是当你的事务开始的时候,数据库拍下的一张快照
它里面主要记录了:
- up_limit_id:当前最小的活跃事务 ID
- low_limit_id:下一个要分配的事务 ID
- trx_ids :当前还没提交的活跃事务列表
- creator_trx_id:你自己的事务 ID
说人话就是判断一条数据能不能被当前事务看见
判断规则:
- 在我开始前就提交的 → 能看见
- 在我开始后才提交 / 没提交的 → 看不见
什么是排他锁和共享锁?
共享锁(S 锁 / 读锁)
- 加锁:SELECT ... LOCK IN SHARE MODE
- 规则:可读不可写,多人可同时加共享锁,但都不能加排他锁。
排他锁(X 锁 / 写锁)
- 加锁:SELECT ... FOR UPDATE / UPDATE/DELETE/INSERT
- 加锁: 选择 ...用于更新 / 更新/删除/插入
- 规则:独占,加了排他锁后,其他人任何锁都不能加。
一句话口诀 读共享,写独占 写与写互斥,读写也互斥
什么是事务的2阶段提交?
事务地2阶段提交就是为了让redo log 和 binlog 之间地数据保持一致性,避免主从同步的时候出现主从不一致的 情况
- redo log:InnoDB 自己的日志,崩溃恢复用,保证 "事务不丢"
- binlog:MySQL 服务层日志,主从同步、数据恢复用
数据更新的时候需要保证两个日志都写成功,才算事务真正完成
两阶段提交:
第一阶段:prepare阶段
- 执行 SQL
- 把 redo log 写到磁盘,标记为
prepare - binlog 还没写
第二阶段:commit
- 把 binlog 刷到磁盘(关键一步)
- 再把 redo log 标记为
commit - 事务真正完成
判断redo log 和 binlog 一致性就是通过XID
两个文件都有对应的XID,判断的时候直接看能不能对上
什么是数据库的锁升级,Innodb支持吗?
锁升级就是当有很多行锁的时候,为了方便管理直接升级为表锁
Innodb 不支持,Innodb的目的是为了高并发
数据库乐观锁的过程中,完全没有加任何锁吗?
不是,乐观锁一般是用在update场景,虽然没有显式加锁,但是update本身有锁
乐观锁和悲观锁都有加锁,那乐观锁的优势在哪?
乐观锁可以通过CAS判断,只有在修改数据的那一瞬间才加锁,锁的粒度小,并发比较好
数据库扫表任务如何避免出现死循环
一般不会这么问,但是确实回出现这个问题
SELECT * FROM case_event
WHERE STATE = 'INIT'
ORDER BY ID
LIMIT 200;
比如上面这个代码,看着没什么问题
但是假设在处理191~300这部分数据的时候出错了,也就是扫描出来之后,不能顺利的改变它的状态,那么后续不是每次都还是扫出这一批处理不了的数据吗
所以解决方法,额外传入一个游标,记录每一批的最大下标
SELECT * FROM case_event
WHERE STATE = 'INIT' AND id > #{lastMaxId}
ORDER BY ID
LIMIT 200;
数据库死锁如何解决?
死锁:两个事务互相想要拿到对方拥有的锁,然后进入阻塞等待的状态
死锁原因:
- 多个事务加锁顺序相反(A 等 B,B 等 A)
- 事务执行太慢,锁持有太久
- 一次操作数据太多,锁范围变大
- RR 隔离级别下间隙锁、临键锁更容易死锁
不需要自己解决,MySQL 自己会解决
- 死锁检测,自动回滚
-
- 当检测到有死锁时会挑一个事务进行回滚
- 超时自动失败
-
- 默认50秒,超过这个时间事务还没结束会自动回滚
避免死锁的方式:
- 固定抢锁顺序
- 缩短事务范围
- 用RC代替RR, 减少间隙锁、临键锁,死锁明显减少
RC只有记录锁,没有间隙锁、临键锁
如何理解MVCC?
MVCC就是多版本并发控制,就是给数据存储多个版本,读取的时候读取的是旧版本,写的时候写新版本,读写不阻塞
MVCC是专门用来解决 读-写 并发的,读-读不用管,写-写 加锁
MVCC的实现主要靠三样东西,
- 每行数据自带三个隐藏字段
db_trx_id:最后修改这条数据的事务 IDdb_roll_ptr:回滚指针,指向历史版本(undo log)db_row_id:没主键时用的隐藏 ID(不重要)
- Undo Log(历史版本链)
-
- 每次修改数据前,需要把旧数据保存到 undo log 中,并通过db_roll_ptr字段串起来形成一条版本链
- 你读取操作的时候都是读取对应的undo log ,所以不一定是最新的数据
- Read View(可见性判断器)
-
- 用来判断当前来读取的事务应该看见哪一个版本,看不见哪个版本
- 具体的规则:
-
-
- 比我早提交的 → 可见
- 比我晚开始的 → 不可见
- 正在运行没提交的 → 不可见
- 我自己改的 → 可见
-
然后这个RR和RC生成Read View的方式不同
- RR,事务第一次查询时生成Read View,之后全程用这个(解决不可重复读)
- RC,每次查询都生成一个新的Read View(存在不可重复读)
快照读和当前读是不同的
快照读,读取到的可能是历史版本的数据
SELECT * FROM t;
当前读,读取到的是最新的数据,需要加锁
SELECT ... FOR UPDATE;
UPDATE; DELETE; INSERT;
为什么MySQL默认使用RR隔离级别?
因为 MySQL 要保证默认行为更安全、更符合传统数据库直觉:
- 一个事务里多次查询结果一致
- 尽量避免幻读
- 主从复制更兼容
所以官方默认选 RR。
为什么默认RR,大厂要改成RC?
MySQL 默认的RR安全性更高,数据一致性更强,大厂改成RC是为了更高并发和更少死锁
RR和RC的区别:
- 快照读不一样
-
- RR是事务第一次查询的时候生成快照,后续的查询一直使用这个快照,所以不会出现不可重复读
- RC是每一次查询都生成最新的快照,能读取到别人最新提交的数据,但是会出现不可重复读
- 锁机制不同(关键 )
-
- RR使用 Next-Key Lock,锁的范围大,防止幻读,但是并发度比较低,死锁的概率比较高
- RC使用 Record Lock,只锁记录,不锁间隙,锁的粒度小,死锁几率比较小
大厂把RR改成RC本质上就是为了更高的并发,因为锁的粒度变小了,很多更新不用等,死锁概率也能大幅度降低(RR的间隙锁容易造成互相等待,RC只锁真实记录,死锁概率小)
对大厂来说很多不可重复读场景不敏感,而且就算出现也是通过乐观锁来解决
什么是数据库事务?
数据库事务就是一组操作要么全部成功,要么全部失败,不能只成功一半
事务的四大特定:ACID
A,原子性,要么都做,要么都不做
C:一致性,数据需要一致
I:隔离性,各个事务之间无不干扰
D:持久性,事务提交了就永久生效