MySQL常见问题总结(2)

InnoDB核心特性

Innodb的RR到底有没有解决幻读?

普通查询(快照读)能完全解决,具体的方案就是第一次查询的时候生成快照,后续基于这个快照读取,不会出现幻读

加锁查询/增加/更新/删除(当前读),靠间隙锁基本解决幻读

但是极端情况下,比如在同一个事务里面先快照读,在当前读,会出现幻读现象,因为当前读,拿到的是最新的数据

具体流程:

  • 事务A执行了普通select(快照读)
  • 然后事务B执行了当前读操作(insert刚才查询的记录)
  • 事务A通过select for update 加锁读取,发现和原来结果不一样

原本事务A它一开判断到的是数据不存在,然后再判断一次发现存在,出现了幻读现象

总的来说,RR解决了99% 的幻读场景,但是极端情况下任然会存在,如果想要理论上100%解决,需要使用 Serializable 隔离级别

InnoDB的一次更新事务是怎么实现的?

update操作执行的全流程:

  1. 先把数据读取到内存 Buffer Pool 中
    1. 要更新某行数据,先看内存中有没有,没有的话就从磁盘中加载进来
  1. 写undo log(把旧数据记录下来)
    1. 更新前线记录原来的值,为的就是事务回滚的时候可以恢复,以及MVCC快照读需要用到旧版本数据
    2. 先写内存,后台异步刷盘
  1. 在内存中直接更新数据
    1. 直接修改Buffer Pool中的数据,然后这个数据变成脏页,后台异步刷盘
  1. 写 Redo Log Buffer
    1. 把本次操作记录到redo log 中,不过同样是先写内存,后台异步刷盘的方式
    2. redo log 就是记录你执行的每一条指令,除了查询之外,用来宕机恢复用的
  1. 提交事务
    1. 提交的时候会做几件事
      1. redo log 刷盘
      2. 写binlog(这个用来主从复制的)
      3. 内部两阶段提交,保证redo log 和 binlog 一致
  1. 脏页刷盘
    1. 不过这个不是事务提交就刷盘,而是后台慢慢刷盘
    2. 即使没刷,也不会丢失,因为已经确保redo log 刷盘完成
  1. 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

特点:只锁需要的行

表级锁,表级锁有好几种(都是自动加的)

  1. 意向锁
    • 事务要加行锁前,先给表加个意向锁
    • 作用:快速判断 "这张表能不能直接加表锁"
    • 不会阻塞普通 DML,只和表锁互斥
  1. AUTO-INC 锁
    • 插入自增 ID 时用的表锁
    • 保证 ID 连续
    • 现在基本优化成轻量锁,不会阻塞太久
  1. MDL 元数据锁
    • 只要操作表(select/update/alter)都会加
    • 作用:防止你查询的时候,别人把表结构改了
    • DDL 会拿写 MDL 锁,会阻塞所有读写
  1. 手动加锁(基本不用)
    1. LOCKTABLES t READ;
    2. 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的能力

解决方法:

  1. 改成RC,RC模式下,没有间隙锁,只保留了记录锁
  2. 或者不要使用间隙锁,而是通过其它的手段来保证互斥性

插入意向锁

表示自己打算插入一行记录,专门给 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 自己会解决

  1. 死锁检测,自动回滚
    1. 当检测到有死锁时会挑一个事务进行回滚
  1. 超时自动失败
    1. 默认50秒,超过这个时间事务还没结束会自动回滚

避免死锁的方式:

  1. 固定抢锁顺序
  2. 缩短事务范围
  3. 用RC代替RR, 减少间隙锁、临键锁,死锁明显减少

RC只有记录锁,没有间隙锁、临键锁

如何理解MVCC?

MVCC就是多版本并发控制,就是给数据存储多个版本,读取的时候读取的是旧版本,写的时候写新版本,读写不阻塞

MVCC是专门用来解决 读-写 并发的,读-读不用管,写-写 加锁

MVCC的实现主要靠三样东西,

  1. 每行数据自带三个隐藏字段
  • db_trx_id:最后修改这条数据的事务 ID
  • db_roll_ptr:回滚指针,指向历史版本(undo log)
  • db_row_id:没主键时用的隐藏 ID(不重要)
  1. Undo Log(历史版本链)
    1. 每次修改数据前,需要把旧数据保存到 undo log 中,并通过db_roll_ptr字段串起来形成一条版本链
    2. 你读取操作的时候都是读取对应的undo log ,所以不一定是最新的数据
  1. Read View(可见性判断器)
    1. 用来判断当前来读取的事务应该看见哪一个版本,看不见哪个版本
    2. 具体的规则:
      • 比我早提交的 → 可见
      • 比我晚开始的 → 不可见
      • 正在运行没提交的 → 不可见
      • 我自己改的 → 可见

然后这个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的区别:

  1. 快照读不一样
    • RR是事务第一次查询的时候生成快照,后续的查询一直使用这个快照,所以不会出现不可重复读
    • RC是每一次查询都生成最新的快照,能读取到别人最新提交的数据,但是会出现不可重复读
  1. 锁机制不同(关键 )
    • RR使用 Next-Key Lock,锁的范围大,防止幻读,但是并发度比较低,死锁的概率比较高
    • RC使用 Record Lock,只锁记录,不锁间隙,锁的粒度小,死锁几率比较小

大厂把RR改成RC本质上就是为了更高的并发,因为锁的粒度变小了,很多更新不用等,死锁概率也能大幅度降低(RR的间隙锁容易造成互相等待,RC只锁真实记录,死锁概率小)

对大厂来说很多不可重复读场景不敏感,而且就算出现也是通过乐观锁来解决

什么是数据库事务?

数据库事务就是一组操作要么全部成功,要么全部失败,不能只成功一半

事务的四大特定:ACID

A,原子性,要么都做,要么都不做

C:一致性,数据需要一致

I:隔离性,各个事务之间无不干扰

D:持久性,事务提交了就永久生效

相关推荐
2401_897190552 小时前
mysql数据库性能基准测试工具推荐_使用sysbench进行压力测试
jvm·数据库·python
庞轩px2 小时前
第二篇:String、StringBuilder、StringBuffer深度剖析
java·字符串·stringbuilder·string·stringbuffer·字符串常量池
爱喝水的鱼丶2 小时前
SAP-ABAP: 深入浅出 SAP 经典可执行程序:从零开始掌握
运维·服务器·数据库·sap·abap·开发交流
色空大师2 小时前
【阿里云部署服务问题指南】
java·mysql·阿里云·docker
Rsun045512 小时前
9、Java 外观模式从入门到实战
java·开发语言·外观模式
清心歌2 小时前
TreeSet 深度解析
java·开发语言
迷藏4942 小时前
**RISC-V生态下的嵌入式开发新范式:从指令集到自定义外设的全流程实战**在当前国产化
java·python·risc-v
小松加哲2 小时前
Tomcat 核心原理全解析(含请求流转+组件源码+多应用配置)
java·tomcat·firefox
Lyyaoo.2 小时前
【JAVA基础面经】juc包(java.util.concurrent)
java·开发语言