数据库面经

1.事务的特性?事务隔离级别?分别用于解决什么问题?

DML(data manupulation language)数据操纵语言:就是我们经常用到的select,update,insert,delete

DDL(data definition language)数据库定义语言:其实就是在创建表用到的一写sql,比如create,alter,drop(增、删、改)等。

事务包括四大特性:ACID

  • A:原子性Atomicity:事务是最小的工作单元,不可再分,事务的原子性动作必须保证多条DML语句同时成功或者同时失败,一般通过commit和rollback来控制。
  • C:一致性Consistency:执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。
  • I:隔离性Isolation:事务A和事务B之间具有隔离:一个事务所做的修改在最终提交以前对其他事务是不可见的。
  • D:持久性Durability:持久性说的是最终数据必须持久到硬盘文件中,事务才算成功的结束。

这4个特性mysql如何保证实现?

  • MySQL的存储引擎InnoDB使用重做日志(redo log)保证持久性与一致性,回滚日志(undo log)保证原子性,各种锁来保证隔离性。

原子性通过undo log回滚日志实现:MySQL数据库在InnoDB存储引擎中,还使用Undo Log来实现多版本并发控制
当delete一条记录时,undo log中会记录一条对应的 insert记录;
当insert一条记录时,undo log中会记录一条对应的delete记录;
当update一条记录时,undo log中会记录一条对应的update记录;

 MySQL通过原子性、隔离性、持久性来保证一致性。C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。

面试题:MySQL常见日志种类和作用?

- 1. redo 重做日志:
- 作用:确保事务的持久性,防止在发生故障,脏页未写入磁盘。重启数据库会进行redo log执行重做,到达事务一致性
  
- 2. undo 回滚日志
- 作用:保证数据的原子性,记录事务发生之前的数据的一个版本,用于回滚。innodb事务的可重复读和读取已提交 隔离级别就是通过mvcc+undo实现
  
- 3. errorlog 错误日志
- 作用:Mysql本身启动、停止、运行期间发生的错误信息
  
- 4. slow query log 慢查询日志
- 作用:记录执行时间过长的sql,时间阈值可以配置,只记录执行成功

- 5. binlog 二进制日志
- 作用:用于主从复制,实现主从同步
  
- 6. relay log 中继日志
- 作用:用于数据库主从同步,将主库发送来的binlog先保存在本地,然后从库进行回放
  
- 7. general log 普通日志
- 作用:记录数据库操作明细,默认关闭,开启会降低数据库性能
 

隔离级别:

理论上隔离级别包括四个,实际上都是2档起步,主要用于解决脏读,不可重复读,幻读。

  • 脏读:一个事务读到另一个事务尚未提交的数据。
  • 不可重复读:在同一个事务中,多次读取同一个数据时,结果出现不一致。
  • 幻读:在一个事务中使用相同的SQL两次读取,第二次读取到了其他事务新插入的行。

幻读和不可重复度的区别:

  1. 1.前者是一个范围,后者是本身数据内容,从总的结果来看,两者都表现为两次读取的结果不一致
  2. 2.不可重复读注重于数据的修改,而幻读注重于数据的插入。

读未提交:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。

读已提交:允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣。

可重复读:同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。

可串行化:最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰,也就是说,该级别可以防⽌脏读、不可重复读以及幻读。

隔离级别

并发问题

读未提交(Read Uncommitted)

可能会导致脏读、幻读或不可重复读

读已提交(Read Committed)

可能会导致幻读或不可重复读

可重复读(Repeatable Read)

可能会导致幻读

可串行化(Serializable)

不会产⽣⼲扰

Oracle数据库默认的隔离级别是:读已提交(2挡)

Mysql数据库默认的隔离级别是:可重复读(3挡)

如何保证并发安全?

两种方法:

  1. 把隔离级别设置为串行化(serializable)
  2. 乐观锁(版本号)

同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改;mysql的默认隔离级别是可重复读,但是可重复读可能导致幻读,如果要保证绝对的安全只能把隔离级别设置成串行化,这样所有的事务都只能顺序执行,但是性能会下降许多。

第二种方法使用更新的版本控制,维护一个字段作为updateversion,修改时updateversion也作为一个参数传入,在条件语句中添加例如where id= ? and update_version = ? 当然set里面要update_version +1.这样可以控制到每次只能有一个人更新一个版本。

面试题:mysql的可重复读是怎么实现的?

使用MVCC实现的,即Mutil-Version Concurrency Control ,多版本并发控制。关于 MVCC,比较常见的说法如下,包括《高性能 MySQL》也是这么介绍的。

InnoDB在每行记录后面保存两个隐藏的列,分别保存了数据行的创建版本号和删除版本号每开始一个新的事务,系统版本号都会递增。

例如:

-- MVCC新增
begin; -- 假设获取的 当前事务版本号=1
insert into user (id,name,age) values (1,"张三",10); -- 新增,当前事务版本号是1
insert into user (id,name,age) values (2,"李四",12); -- 新增,当前事务版本号是1
commit; -- 提交事务
id name age create_version delete_version
1 张三 10 1 NULL
2 李四 12 1 NULL
-- 上表可以看到,插入的过程中会把当前事务版本号记录到列 create_version 中去!

-- MVCC删除:删除操作是直接将行数据的删除版本号更新为当前事务的版本号
begin; --假设获取的 当前事务版本号=3
delete from user where id = 2;
commit; -- 提交事务
id name age create_version delete_version
1 张三 10 1 NULL
2 李四 12 1 3
-- MVCC更新操作:采用 delete + add 的方式来实现,首先将当前数据标志为删除,然后再新增一条新的数据
begin;-- 假设获取的 当前事务版本号=10
update user set age = 11 where id = 1; -- 更新,当前事务版本号是10
commit; -- 提交事务
id name age create_version delete_version
1 张三 10 1 10
2 李四 12 1 3
1 张三 11 10 NULL
-- MVCC查询操作:
begin;-- 假设拿到的系统事务ID为 12
select * from user where id = 1;
commit; -- 提交事务

查询操作为了避免查询到旧数据或已经被其他事务更改过的数据,需要满足如下条件:

  • 1、查询时当前事务的版本号需要大于或等于创建版本号create_version
  • 2、查询时当前事务的版本号需要小于删除的版本号delete_version,或者当前删除版本号delete_version=NULL

即:(create_version <= current_version < delete_version) || (create_version <= current_version && delete_version-=NULL) ,这样就可以避免查询到其他事务修改的数据,同一个事务中,实现了可重复读!

执行结果应该是:

id name age create_version delete_version
1 张三 11 10 NULL

小结: 

事务开始时刻的版本号会作为事务的版本号,用来和查询到的每行记录的版本号对比。在可重复读级别下,MVCC是如何操作的:

SELECT:必须同时满足以下两个条件,才能查询到(1)只查版本号早于当前版本的数据行。(2)行的删除版本要么未定义,要么大于当前事务版本号。

DELETE:为删除的每一行保存当前系统版本号作为删除版本号。

INSERT:为插入的每一行保存当前系统版本号作为创建版本号。

UPDATE:插入一条新数据,保存当前系统版本号作为创建版本号。同时保存当前系统版本号作为原来数据行删除版本号。

MVCC 只作用于 RC(Read Committed)读已提交 和 RR(Repeatable Read)可重复读 级别,因为 RU(Read Uncommitted)读未提交 总是读取最新的数据版本,而不是符合当前事务版本的数据行。而 Serializable 则会对所有读取的行都加锁。这两种级别都不需要 MVCC 的帮助。

最初我也是坚信这个说法的,但是后面发现在某些场景下这个说法其实有点问题。

举个简单的例子来说:如果线程1和线程2先后开启了事务,事务版本号为1和2,如果在线程2开启事务的时候,线程1还未提交事务,则此时线程2的事务是不应该看到线程1的事务修改的内容的。

但是如果按上面的这种说法,由于线程1的事务版本早于线程2的事务版本,所以线程2的事务是可以看到线程1的事务修改内容的。
 

那究竟是怎么实现的?

实际上,InnoDB 会在每行记录后面增加三个隐藏字段:

DB_ROW_ID:行ID,随着插入新行而单调递增,如果有主键,则不会包含该列。

DB_TRX_ID:记录插入或更新该行的事务的事务ID。

DB_ROLL_PTR:回滚指针,指向 undo log 记录。每次对某条记录进行改动时,该列会存一个指针,可以通过这个指针找到该记录修改前的信息 。当某条记录被多次修改时,该行记录会存在多个版本,通过DB_ROLL_PTR 链接形成一个类似版本链的概念。
数据库面经_第1张图片

 接下来进入正题,以 RR 级别为例:每开启一个事务时,系统会给该事务会分配一个事务 Id,在该事务执行第一个 select 语句的时候,会生成一个当前时间点的事务快照 ReadView,主要包含以下几个属性:

  • trx_ids:生成 ReadView 时当前系统中活跃的事务 Id 列表,就是还未执行事务提交的。
  • up_limit_id:低水位,取 trx_ids 中最小的那个,trx_id 小于该值都能看到。
  • low_limit_id:高水位,生成 ReadView 时系统将要分配给下一个事务的id值,trx_id 大于等于该值都不能看到。
  • creator_trx_id:生成该 ReadView 的事务的事务 Id。
     

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

1)如果被访问版本的trx_id与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

2)如果被访问版本的trx_id小于ReadView中的up_limit_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。

3)如果被访问版本的trx_id大于ReadView中的low_limit_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。

4)如果被访问版本的trx_id属性值在ReadView的up_limit_id和low_limit_id之间,那就需要判断一下trx_id属性值是不是在trx_ids列表中。如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
 

在进行判断时,首先会拿记录的最新版本来比较,如果该版本无法被当前事务看到,则通过记录的 DB_ROLL_PTR 找到上一个版本,重新进行比较,直到找到一个能被当前事务看到的版本。

而对于删除,其实就是一种特殊的更新,InnoDB 用一个额外的标记位 delete_bit 标识是否删除。当我们在进行判断时,会检查下 delete_bit 是否被标记,如果是,则跳过该版本,通过 DB_ROLL_PTR 拿到下一个版本进行判断。

以上内容是对于 RR 级别来说,而对于 RC 级别,其实整个过程几乎一样,唯一不同的是生成 ReadView 的时机,RR 级别只在事务开始时生成一次,之后一直使用该 ReadView。而 RC 级别则在每次 select 时,都会生成一个 ReadView。
 

那MySQL采用的 MVCC 解决了幻读了没有?

幻读:在一个事务中使用相同的 SQL 两次读取,第二次读取到了其他事务新插入的行,则称为发生了幻读。

例如:

  • 1)事务1第一次查询:select * from user where id < 10 时查到了 id = 1 的数据
  • 2)事务2插入了 id = 2 的数据
  • 3)事务1使用同样的语句第二次查询时,查到了 id = 1、id = 2 的数据,出现了幻读。

谈到幻读,首先我们要引入“当前读”和“快照读”的概念,聪明的你一定通过名字猜出来了:

  • 快照读:生成一个事务快照(ReadView),之后都从这个快照获取数据。普通 select 语句就是快照读。
  • 当前读:读取数据的最新版本。常见的 update/insert/delete、还有 select ... for update、select ... lock in share mode 都是当前读。

对于快照读,MVCC 因为因为从 ReadView 读取,所以必然不会看到新插入的行,所以天然就解决了幻读的问题。

而对于当前读的幻读,MVCC 是无法解决的。需要使用 Gap Lock 或 Next-Key Lock(Gap Lock + Record Lock)来解决。

其实原理也很简单,用上面的例子稍微修改下以触发当前读:select * from user where id < 10 for update,当使用了 Gap Lock 时,Gap 锁会锁住 id < 10 的整个范围,因此其他事务无法插入 id < 10 的数据,从而防止了幻读。
 

那经常有人说 Repeatable Read 解决了幻读是什么情况?

SQL 标准中规定的 RR 并不能消除幻读,但是 MySQL 的 RR 可以,靠的就是 Gap 锁。在 RR 级别下,Gap 锁是默认开启的,而在 RC 级别下,Gap 锁是关闭的。

2、mysql的3种锁算法

Record lock:行锁(记录锁),单条索引记录上加锁,锁住的永远是索引,而非记录本身。

Gap lock:间隙锁,在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身

Next-key lock: 临键锁,Record lock 和 Gap lock的结合,即除了锁住记录本身,也锁住索引之间的间隙。

记录锁Record Lock

顾名思义,记录锁就是为某行记录加锁,它封锁该行的索引记录:

-- id 列为主键列或唯一索引列
SELECT * FROM 表名称 WHERE id = 1 FOR UPDATE;

这时候 id 为 1 的记录行会被锁住。

需要注意的是:

  • id 列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁。(行锁在 InnoDB 中是基于索引实现的,所以一旦某个加锁操作没有使用索引,那么该锁就会退化为表锁)
  • 同时查询语句必须为精准匹配=,不能为>、<、like等,否则也会退化成临键锁。

在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:

-- id 列为主键列或唯一索引列
UPDATE SET age = 50 WHERE id = 1;

注意:
1、FOR UPDATE仅适用于InnoDB,且必须在事务处理模块(BEGIN/COMMIT)中才能生效。

2、要测试锁定的状况,可以利用MySQL的Command Mode(命令模式) ,开两个视窗来做测试。

3、Myisam 只支持表级锁,InnerDB支持行级锁 添加了(行级锁/表级锁)锁的数据不能被其它事务再锁定,也不被其它事务修改。是表级锁时,不管是否查询到记录,都会锁定表。

间隙锁Gap Locks

间隙锁基于非唯一索引,它锁定一段范围内的索引记录。间隙锁基于下面将会提到的Next-Key Locking 算法,请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据

SELECT * FROM 表名称 WHERE id BETWEN 1 AND 10 FOR UPDATE;
  • 即所有在(1,10)区间内的记录行都会被锁住,所有id 为 2、3、4、5、6、7、8、9 的数据行的插入会被阻塞,但是 1 和 10 两条记录行并不会被锁住。

  • 除了手动加锁外,在执行完某些 SQL 后,InnoDB 也会自动加间隙锁,这个我们在下面会提到。

临键锁Next-Key Locks

Next-Key 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临建锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁。

假设有如下表:

引擎:InnoDB,隔离级别:Repeatable-Read:table(id PK, age KEY, name)

id age name
1 10 Lee
3 24 Soraka
5 32 Zed
7 45 Talon

该表中 age 列潜在的临键锁有:

(-∞, 10],
(10, 24],
(24, 32],
(32, 45],
(45, +∞],

在事务 A 中执行如下命令:

-- 根据非唯一索引列 UPDATE 某条记录
UPDATE table SET name = Vladimir WHERE age = 24;
-- 或根据非唯一索引列 锁住某条记录
SELECT * FROM table WHERE age = 24 FOR UPDATE;

不管执行了上述 SQL 中的哪一句,之后如果在事务 B 中执行以下命令,则该命令会被阻塞:

INSERT INTO table VALUES(100, 26, 'Ezreal');

明显,事务 A 在对 age 为 24 的列进行 UPDATE 操作的同时,也获取了 (24, 32] 这个区间内的临键锁。

不仅如此,在执行以下 SQL 时,也会陷入阻塞等待:

INSERT INTO table VALUES(100, 30, 'Ezreal');

那最终我们就可以得知,在根据非唯一索引对记录行进行UPDATE \ FOR UPDATE \ LOCK IN SHARE MODE 操作时,InnoDB 会获取该记录行的临键锁 ,并同时获取该记录行下一个区间的间隙锁。

即事务 A在执行了上述的 SQL 后,最终被锁住的记录区间为 (10, 32)。

  • 间隙锁(Gap Locks):间隙锁,锁定一个范围,但不包括记录本身。比如锁定a=5以及其前后2个范围内的数据,也就是将a=3,4,6,7这些行都锁了起来,不包括a=5本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
  • 临键锁(Next-key Locks):锁定一个范围,并且锁定记录本身。比如锁定a=5以及其前后2个范围内的数据,也就是将a=3,4,5,6,7这些行都锁了起来。对于行的查询,都是采用该方法,主要目的是解决幻读的问题


小结

InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。
临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。即,除了锁住记录本身,也锁住索引之间的间隙。
 

注意:

InnoDB 行锁是通过索引上的索引项来实现的。意味者:只有通过索引条件检索数据,InnoDB 才会使用行级锁,否则,InnoDB将使用表锁!

  • 对于主键索引:直接锁住锁住主键索引即可。
  • 对于普通索引:先锁住普通索引,接着锁住主键索引,这是因为一张表的索引可能存在多个,通过主键索引才能确保锁是唯一的,不然如果同时有2个事务对同1条数据的不同索引分别加锁,那就可能存在2个事务同时操作一条数据了。

3.什么是回表?什么情况下mysql innodb会发生回表操作?

Mysql innodb的主键索引是聚簇索引,也就是索引的叶子节点存的是整个单条记录的所有字段值。

不是主键索引的就是非簇集索引,非簇集索引的叶子节点存的是主键字段的值。回表是什么意思?就是你执行一条sql语句,需要从两个b+索引中去取数据。举个例子:

表tbl有a,b,c三个字段,其中a是主键,b上建了索引,然后编写sql语句

SELECT * FROM tbl WHERE a=1

这样不会产生回表,因为所有的数据在a的索引树中均能找到

SELECT * FROM tbl WHERE b=1

这样就会产生回表,因为where条件是b字段,那么会去b的索引树里查找数据,但b的索引里面只有a,b两个字段的值,没有c,那么这个查询为了取到c字段,就要取出主键a的值,然后去a的索引树去找c字段的数据。查了两个索引树,这就叫回表。

索引覆盖就是查这个索引能查到你所需要的所有数据,不需要去另外的数据结构去查。其实就是不用回表。

怎么避免?不是必须的字段就不要出现在SELECT里面。或者b,c建联合索引。但具体情况要具体分析,索引字段多了,存储和插入数据时的消耗会更大。这是个平衡问题。


 

 3.数据库中的左关联和右关联有什么区别?

  • 首先左连接和右连接都是外连接。
  • 连接查询:(表与表之间的查询)
  • 连接查询分为内连接和外连接。二者的区别是有无主副表之分。内连接没有,外连接有。

内连接定义:假设A和B表进行连接,使用内连接的话,凡是A表和B表能够匹配上的记录查询出来,这就是内连接。

外连接定义:假设A和B表进行连接,使用外连接的话,AB两张表中有一张是主表,一张是副表,主要查询主表中的数据,捎带查副表,当副表中的数据没有和主表中的数据匹配上,副表自动模拟出null与之匹配。

                     外连接的最重要的特点是:主表的数据无条件的全部查找出来

内连接分为:

  1. 等值连接(条件是等量关系)                                 select... from..(inner) join...on...
  2. 非等值连接(条件是非等量关系 between ...and)
  3. 自连接(条件是自己连接自己,一张表看做两张表)

外连接分为:

  1. 左外连接(左连接)表示左边的这张表示主表  select... from...left (outer ) join...on...
  2. 右外连接(右连接)表示右边的这张表示主表  select... from...right (outer ) join...on...

数据库面经_第2张图片

数据库面经_第3张图片

数据库面经_第4张图片

数据库面经_第5张图片

那么面试问题来了:

MySQL中联表查询条件WHERE和ON的区别?

        left join 联表查询 on 可以理解为是在两张表中进行条件筛选(即在生成临时中间表的时候进行条件筛选),满足条件的则展示左右拼接的数据记录,不满足条件的,则优先展示左表中的数据,右表中不满足条件的字段为null。

       而where则可以理解为在一张表上进行条件过滤(即,将生成的临时表看做一张表)

假设有两张表:

表1:tab1

id size
1 10
2 20
3 30

表2:tab2

size name
10 AAA
20 BBB
20 CCC

两条SQL:

---sql1
select * form tab1 left join tab2 on (tab1.size = tab2.size) where tab2.name=’AAA’

--- sql2
select * form tab1 left join tab2 on (tab1.size = tab2.size and tab2.name=’AAA’)

第一条SQL的过程:

-- 中间表on条件: 
tab1.size = tab2.size

数据库面经_第6张图片

-- 再对中间表过滤where 条件:
tab2.name=’AAA’

数据库面经_第7张图片

第二条SQL的过程:

-- 中间表on条件: (条件不为真也会返回左表中的记录)
tab1.size = tab2.size and tab2.name=’AAA’

数据库面经_第8张图片

 其实以上结果的关键原因就是left join,right join,full join的特殊性,不管on上的条件是否为真都会返回leftright表中的记录,full则具有left和right的特性的并集。 inner jion没这个特殊性,则条件放在on中和where中,返回的结果集是相同的。

4.MySQL常见的存储引擎?

MySQL5.5之前使用的是MyISAM引擎,5.5以后用的是InnoDB引擎

区别项 InnoDB MYISAM
事务 支持 不支持
锁粒度 行锁,适合高并发 表锁,不适合高并发
是否默认 默认 非默认
支持外键 支持外键 不支持
适用场景 读写均衡,写多读少场景,需要事务 读多写少场景,不需要事务
全文索引 不支持(可以借助插件或者使用ElasticSearch) 支持

InnoDB和MyISAM是mysql数据库的两种存储引擎:

  • InnoDB是聚集索引,支持事务,支持行级锁。这种存储引擎在mysql数据库崩溃之后提供自动恢复机制。(mysql默认的方式)
  • MyISAM是非聚集索引,不支持事务,只支持表级锁。(mysql最常用的方式,但不是默认)

5.MySQL的行锁与表锁,乐观锁和悲观锁的问题?

锁粒度越小,并发支持度越高。

MySQL中有几种锁?

常见的是7种锁,还有一种不常见的预测锁

  • 行锁(Record Locks)属于行级锁,悲观锁

  • 间隙锁(Gap Locks)属于行级锁,悲观锁

  • 临键锁(Next-key Locks)属于行级锁,悲观锁

  • (读)共享锁/(写)排他锁(Shared Locks/Exclusive Locks)属于行级锁,悲观锁

  • 意向共享锁/意向排他锁(Intention Shared Locks/Intention Exclusive Locks)属于表级锁,悲观锁

  • 插入意向锁(Insert Intention Locks)属于特殊的间隙锁,悲观锁

  • 自增锁(Auto-inc Locks)属于表级锁

MySQL中如何划分锁?

  • 按照对数据操作的锁粒度来分:(锁定粒度依次递增)
    • 1.行级锁
    • 2.间隙锁
    • 3.页级锁
    • 4.表级锁
  • 按照锁的共享策略来分:
    • 1.共享锁
    • 2.排他锁
    • 3.意向共享锁
    • 4.意向排他锁
  • 加锁策略上分:
    • 乐观锁
    • 悲观锁
  • 其他:
    • 自增锁

按照对数据操作的锁粒度来分

1. 不同存储引擎使用的锁的类型?

  • MYISAM和MEMORY采用:表级锁(table-level locking)
  • BDB采用:页面锁(page-level locking)或表级锁,默认为页面锁
  • InnoDB支持:行级锁(row-level locking)和表级锁,默认为行级锁

2. 行级锁Record Lock(偏写)

行级锁介绍

行级锁(记录锁)是MySQL中锁定粒度最细的一种锁。表示单个行记录上的锁,行锁一定是作用在索引上的。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。

行级锁可分为:

  • 共享锁
  • 排他锁

行锁的种类

  • 行级锁(Record Locks):单个行记录上的锁。
  • 间隙锁(Gap Locks):间隙锁,锁定一个范围,但不包括记录本身。比如锁定a=5以及其前后2个范围内的数据,也就是将a=3,4,6,7这些行都锁了起来,不包括a=5本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
  • 临键锁(Next-key Locks):锁定一个范围,并且锁定记录本身。比如锁定a=5以及其前后2个范围内的数据,也就是将a=3,4,5,6,7这些行都锁了起来。对于行的查询,都是采用该方法,主要目的是解决幻读的问题

行级锁特点

开销大,加锁慢,会出现死锁。发生锁冲突的概率最低,并发度也最高。

3. 间隙锁Gap Lock

间隙锁介绍

间隙锁,锁定一个范围,但不包括记录本身(它的锁粒度比记录锁的锁整行更大一些,他是锁住了某个范围内的多个行,包括根本不存在的数据),隙锁一定是开区间,比如(3,5)。

GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。该锁只会在隔离级别是RR(可重复读)或者以上的级别内存在。间隙锁的目的是为了让其他事务无法在间隙中新增数据。

4. 临键锁Next-Key Lock

临键锁介绍

是记录锁和间隙锁的结合,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。next-key锁是InnoDB默认的锁,临键锁是是一个左开右闭的区间,比如(3,5]。

next-key lock的效果相当于一个记录锁加一个间隙锁。当next-key lock加在某索引上,则该记录和它前面的区间都被锁定。假设有记录1, 3, 5, 7,现在记录5上加next-key lock,则会锁定区间(3, 5],任何试图插入到这个区间的记录都会阻塞。

record lock、gap lock、next-key lock,都是加在索引上的。假设有记录1,3,5,7,则5上的记录锁会锁住5,5上的gap lock会锁住(3,5),5上的next-key lock会锁住(3,5]。

注意,next-Key锁规定是左开右闭区间!

5. 表级锁(偏读)

表级锁介绍

表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MYISAM与InnoDB都支持表级锁定。

表级锁可分为

  • 表共享读锁(共享锁)
  • 表独占写锁(排他锁)

表级锁特点

开销小,加锁快,不会出现死锁。发生锁冲突的概率最高,并发度也最低。

  • LOCK TABLE my_table_name READ; 用读锁锁表,会阻塞其他事务修改表数据。
  • LOCK TABLE my_table_name WRITE; 用写锁锁表,会阻塞其他事务读和写。

不同存储引擎中的表级锁

  • 在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的 S锁(共享锁/读锁)或者X锁(排他锁/写锁)的,如果想加表级锁需要手动显式地声明。
  • MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MYISAM表显式加锁。

  • 手动增加表锁(读锁/写锁)
lock table 表名称 read/write,表名称2 read/write;
  • 查看表上加过的锁
show open tables;
  • 删除表锁
unlock tables;
  • LOCK TABLES t1 READ:对表t1加表级别的S锁。
  • LOCK TABLES t1 WRITE:对表t1加表级别的X锁。

尽量不用这两种方式去加锁,因为InnoDB的优点就是行锁,所以尽量使用行锁,性能更高

5. 页级锁

页级锁介绍

页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多; 行级冲突少,但速度慢。因此,采取了折中的页级锁,一次锁定相邻的一组记录。BDB引擎默认支持页级锁。

页级锁特点

开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

按照锁的共享策略来分

共享锁和排他锁在MySQL中具体的实现就是读锁和写锁:

  • 读锁共享锁):Shared Locks(S锁),针对同一份数据,多个读操作可以同时进行而不会互相影响                        行锁
  • 写锁排它锁):Exclusive Locks(X锁),当前写操作没有完成前,它会阻断其他写锁和读锁                                   行锁
  • IS锁意向共享锁Intention Shared Lock。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。               表锁
  • IX锁意向排他锁Intention Exclusive Lock。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。            表锁

IS、IX锁是表级锁它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。就是说当对一个行加锁之后,如果有打算给行所在的表加一个表锁,必须先看看该表的行有没有被加锁,否则就会出现冲突。IS锁和IX锁就避免了判断表中行有没有加锁时对每一行的遍历。直接查看表有没有意向锁就可以知道表中有没有行锁。

注意:如果一个表中有多个行锁,他们都会给表加上意向锁,意向锁和意向锁之间是不会冲突的。

1. 共享锁/排他锁

共享锁/排他锁都只是行锁,与间隙锁无关。

  • 共享锁(s锁 读锁) 是一个事务并发读取某一行记录所需要持有的锁。针对同一份数据,多个读操作可以同时进行而不会互相影响;
  • 排他锁(x锁  写锁)是一个事务并发更新或删除某一行记录所需要持有的锁。当前写操作没有完成前,它会阻断其他写锁和读锁;

读锁会阻塞写,但是不会阻塞读。而写锁则会把其他线程的读和写都阻塞

2. 意向共享锁/意向排他锁

意向共享锁/意向排他锁属于表锁,且取得意向共享锁/意向排他锁是取得共享锁/排他锁的前置条件

  • (IS)意向共享锁 Intention Shared Lock:当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
  • (IX)意向排他锁 Intention Exclusive Lock:当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。

IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。就是说当对一个行加锁之后,如果有打算给行所在的表加一个表锁,必须先看看该表的行有没有被加锁,否则就会出现冲突。IS锁和IX锁就避免了判断表中行有没有加锁时对每一行的遍历。直接查看表有没有意向锁就可以知道表中有没有行锁。

共享锁/排他锁与意向共享锁/意向排他锁的兼容性关系:

X IX S IS
X 互斥 互斥 互斥 互斥
IX 互斥 兼容 互斥 兼容
S 互斥 互斥 兼容 兼容
IS 互斥 兼容 兼容 兼容

这四种锁都属于悲观锁,如果一个表中有多个行锁,他们都会给表加上意向锁,意向锁之间都不会发生冲突,排他锁跟谁都冲突

3. 插入意向锁(IIX)

插入意向锁是一种特殊间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作。如果说间隙锁锁住的是一个区间,那么插入意向锁锁住的就是一个

与间隙锁的另一个非常重要的差别是:尽管插入意向锁也属于间隙锁,但两个事务却不能在同一时间内一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。这里我们再回顾一下共享锁和排他锁:共享锁用于读取操作,而排他锁是用于更新删除操作。也就是说插入意向锁、共享锁和排他锁涵盖了常用的增删改查四个动作。

从加锁策略上分:乐观锁和悲观锁

1. 悲观锁

悲观锁 认为对于同一个数据的并发操作,一定是会发生修改的(增删改多,查少),哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

悲观锁用的就是数据库的行锁,认为数据库会发生并发冲突,直接上来就把数据锁住,其他事务不能修改,直至提交了当前事务。

2. 乐观锁

乐观锁 则认为对于同一个数据的并发操作,是不会发生修改的(增删改少,查多)。在更新数据的时候,会采用不断尝试更新的方式来修改数据。也就是先不管资源有没有被别的线程占用,直接去申请操作,如果没有产生冲突,那就操作成功,如果产生冲突,有其他线程已经在使用了,那么就不断地轮询。乐观的认为,不加锁的并发操作是没有事情的。就是通过记录一个数据历史记录的多个版本,如果修改完之后发现有冲突再将版本返回到没修改的样子乐观锁就是不加锁。好处就是减少上下文切换,坏处是浪费CPU时间。

乐观锁相对悲观锁而言,它认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回错误信息,让用户决定如何去做。

乐观锁其实是一种思想,认为不会锁定的情况下去更新数据,如果发现不对劲,才不更新(回滚)。在数据库中往往添加一个version字段(版本号)来实现。乐观锁可以用来避免更新丢失。接下来我们看一下乐观锁在数据表和缓存中的实现。

乐观锁数据表中的实现

利用数据版本号(version)机制是乐观锁最常用的一种实现方式。一般通过为数据库表增加一个数字类型的 “version” 字段,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,返回更新失败。

例子:

-- step1: 查询出商品信息
select (quantity,version) from items where id=100;

-- step2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);

-- step3: 修改商品的库存
update items set quantity=quantity-1,version=version+1 where id=100 and version=#{version};

既然可以用version,那还可以使用时间戳字段,该方法同样是在表中增加一个时间戳字段,和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

需要注意的是,如果你的数据表是读写分离的表,当master表中写入的数据没有及时同步到slave表中时会造成更新一直失败的问题。此时,需要强制读取master表中的数据(将select语句放在事务中)。即:select语句放在事务中,查询的就是master主库了!

乐观锁的锁粒度

乐观锁广泛用于状态同步,我们经常会遇到并发对一条物流订单修改状态的场景,所以此时乐观锁就发挥了巨大作用。但是乐观锁字段的选用也需要非常讲究,一个好的乐观锁字段可以缩小锁粒度。

商品库存扣减时,尤其是在秒杀、聚划算这种高并发的场景下,若采用version号作为乐观锁,则每次只有一个事务能更新成功,业务感知上就是大量操作失败。因为version的粒度太大,更新失败的概率也就会变大。

但是如果我们挑选库存字段作为乐观锁(通过比较库存数来判断数据版本),这样我们的锁粒度就会减小,更新失败的概率也会大大减小。

-- 以库存数作为乐观锁
-- step1: 查询出商品信息
select (inventory) from items where id=100;

-- step2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);

-- step3: 修改商品的库存
update items set inventory=inventory-1 where id=100 and inventory-1>0;

淘宝秒杀、聚划算,跑的就是这条SQL,通过挑选乐观锁,可以减小锁力度,从而提升吞吐。

其他:自增锁AUTO-INC

自增锁(AUTO-INC锁)

  • 在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。这样一个事务在持有 AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。
  • 采用一个轻量级的锁,在为插入语句生成AUTO_INCREMENT修饰的列的值时获取一下这个轻量级 锁,然后生成本次插入语句需要用到的AUTO_INCREMENT列的值之后,就把该轻量级锁释放掉, 并不需要等到整个插入语句执行完才释放锁。

系统变量innodb_autoinc_lock_mode

  • innodb_autoinc_lock_mode值为0:采用AUTO-INC锁。
  • innodb_autoinc_lock_mode值为2:采用轻量级锁。
  • 当innodb_autoinc_lock_mode值为1:当插入记录数不确定是采用AUTO-INC锁,当插入记录数确定时采用轻量级锁

自增锁是一种特殊的表级锁,主要用于事务中插入自增字段,也就是我们最常用的自增主键id。通过innodb_autoinc_lock_mode参数可以设置自增主键的生成策略。防止并发插入数据的时候自增id出现异常。

当一张表的某个字段是自增列时,innodb会在该索引的末位加一个排它锁。为了访问这个自增的数值,需要加一个表级锁,不过这个表级锁的持续时间只有当前sql,而不是整个事务,即当前sql执行完,该表级锁就释放了。其他线程无法在这个表级锁持有时插入任何记录。

6.数据库的索引介绍一下?介绍一下什么时候用Innodb,什么时候用MyISAM

索引的概念:

  • 一种能帮助mysql提高查询效率的数据结构:索引数据结构

索引的实现原理:

  • 通过B+树实现的。通过B+树缩小扫描范围,底层索引进行了排序,分区,索引会携带数据在表中的“物理地址”,最终通过索引检索到数据之后,获取到关联的物理地址,通过物理地址定位到表中的数据,效率是最高的。

索引优点:

  • 大大提高数据查询速度

索引缺点:

  • 维护索引需要耗费数据库资源
  • 索引要占用磁盘空间
  • 当对表的数据进行增删改的时候,因为要维护索引,所以速度收到影响

结合索引的优缺点,得出结论:!数据库表并不是索引加的越多越好,而是仅为那些常用的搜索字段建立索引效果才是最佳的
 

面试题:MySQL什么时候适合创建索引,什么时候不适合创建索引?

1、什么时候适合创建索引?

(1)主键自动建立唯一索引。

(2)频繁作为查询条件的字段。

(3)查询中与其他表关联的字段,外键关系建立索引。

(4)排序字段若通过索引法访问将大大提高排序速度。

(5)查询统计或者分组字段。

2、什么时候不适合创建索引?

(1)频繁更新的字段,因为每次更新不单单是更新了记录,还要更新索引。

(2)表记录太少。

(3)经常增删改的表,因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件数据重复且分布平均的表字段,因此应该只为最经常查询最经常排序的数据列建立索引。

(4)如果某个数据列包含许多重复的内容,为他建立索引就没有太大的实际效果。

 索引的分类

主键索引:PRIMARY KEY
                  设定为主键后,数据库自动建立索引,innodb为聚簇索引,主键索引列值不能有空(Null)
单值索引:又叫单列索引、普通索引
                  即,一个索引只包含单个列,一个表可以有多个单列索引
唯一索引:
                 索引列的值必须唯一,但允许有空值(Null),但只允许有一个空值(Null)
复合索引:
                  即,一个索引可以包含多个列,多个列共同构成一个复合索引!
                  eg: SELECT id (name age) INDEX WHERE name AND age;
全文索引:Full Text (MySQL5.7之前,只有MYISAM存储引擎支持全文索引)
                 全文索引类型为FULLTEXT,在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引可以在Char 、Varchar 上创建。
 

索引的基本操作

主键索引创建

-- 建表语句:建表时,设置主键,自动创建主键索引
CREATE TABLE t_user (
	id VARCHAR(20) PRIMARY KEY,
    name VARCHAR(20)
);

-- 查看索引
SHOW INDEX FROM t_user;

在这里插入图片描述

 单列索引创建(普通索引/单值索引)

-- 建表时创建单列索引:
-- 这种方式创建单列索引,其名称默认为字段名称:name
CREATE TABLE t_user (
	id VARCHAR(20) PRIMARY KEY,
    name VARCHAR(20),
    KEY(name)
);

-- 建表后创建单列索引:
-- 索引名称为:name_index 格式---> 字段名称_index
CREATE INDEX name_index ON t_user(name)

-- 删除单列索引
DROPINDEX 索引名称 ON 表名

在这里插入图片描述

 唯一索引创建

-- 建表时创建唯一索引:
CREATE TABLE t_user2 (
	id VARCHAR(20) PRIMARY KEY,
    name VARCHAR(20),
    UNIQUE(name)
);

-- 建表后创建唯一索引:
CREATE UNIQUE INDEX name_index ON t_user2(name);

在这里插入图片描述

 复合索引创建

-- 建表时创建复合索引:
CREATE TABLE t_user3 (
	id VARCHAR(20) PRIMARY KEY,
    name VARCHAR(20),
    age INT,
    KEY(name,age)
);

-- 建表后创建复合索引:
CREATE INDEX name_age_index ON t_user3(name,age);

-- 复合索引查询的2个原则
-- 1.最左前缀原则
-- eg: 创建复合索引时,字段的顺序为 name,age,birthday
-- 在查询时能利用上索引的查询条件为: 
SELECT * FROM t_user3 WHERE name = ?
SELECT * FROM t_user3 WHERE name = ? AND age = ?
SELECT * FROM t_user3 WHERE name = ? AND birthday = ?
SELECT * FROM t_user3 WHERE name = ? AND age = ? AND birthday = ?
-- 而其他顺序则不满足最左前缀原则:
... WHERE name = ? AND birthday = ? AND age = ? -- 不满足最左前缀原则
... WHERE name = ? AND birthday = ? -- 不满足最左前缀原则
... WHERE birthday = ? AND age = ? AND name = ? -- 不满足最左前缀原则
... WHERE age = ? AND birthday = ? -- 不满足最左前缀原则


-- 2.MySQL 引擎在执行查询时,为了更好地利用索引,在查询过程中会动态调整查询字段的顺序!
-- 这时候再来看上面不满足最左前缀原则的四种情况:
-- 不满足最左前缀原则,但经过动态调整顺序后,变为:name age birthday 可以利用复合索引!
... WHERE name = ? AND birthday = ? AND age = ? 
-- 不满足最左前缀原则,也不能动态调整(因为缺少age字段),不可以利用复合索引!
... WHERE name = ? AND birthday = ? 
-- 不满足最左前缀原则,但经过动态调整顺序后,变为:name age birthday 可以利用复合索引!
... WHERE birthday = ? AND age = ? AND name = ?
-- 不满足最左前缀原则,也不能动态调整(因为缺少name字段),不可以利用复合索引!
... WHERE age = ? AND birthday = ?

数据库面经_第9张图片

面试题:复合索引查询时,字段排列的先后顺序与创建索引时不同,能否成功利用索引查询?

考察点:符合索引的最左前缀原则

-- 假设构成复合索引的字段为 name,age,birthday
-- 则下面那种情况可以使用成功利用复合索引查询?
... WHERE name = ? -- 可以利用
... WHERE name = ? AND age = ? -- 可以利用
... WHERE name = ? AND birthday = ? -- 可以利用
... WHERE name = ? AND age = ? AND birthday = ? -- 可以利用
... WHERE name = ? AND birthday = ? AND age = ? -- 不满足最左前缀原则,但经过动态调整后可以利用
... WHERE birthday = ? AND age = ? AND name = ? -- 不满足最左前缀原则,但经过动态调整后可以利用
... WHERE age = ? AND birthday = ? -- 不满足最左前缀原则,不能动态调整,不能利用复合索引

MySQL索引的数据结构(B+Tree)

-- 建表:
CREATE TABLE t_emp(
	id INT PRIMARY KEY,
    name VARCHAR(20),
    age INT
);

-- 插入数据:插入时,主键无序
INSERT INTO t_emp VALUES(5,'d',22);
INSERT INTO t_emp VALUES(6,'d',22);
INSERT INTO t_emp VALUES(7,'3',21);
INSERT INTO t_emp VALUES(1,'a',23);
INSERT INTO t_emp VALUES(2,'b',26);
INSERT INTO t_emp VALUES(3,'c',27);
INSERT INTO t_emp VALUES(4,'a',32);
INSERT INTO t_emp VALUES(8,'f',53);
INSERT INTO t_emp VALUES(9,'b',13);

-- 查询:自动排序,有序展示(因为主键是有主键索引的,因此会自动排序)

在这里插入图片描述

问题:为什么数据插入时,未按照主键顺序,而查询时却是有序的呢

  • 原因:MySQL底层为主键自动创建索引一旦创建了索引,就会进行排序
  • 实际上这些数据在MySQL底层的真正存储结构变成了下面这种方式:

在这里插入图片描述

问题:为什么要排序呢?

  • 因为排序之后查询效率就快了,比如查询 id = 3 的数据,只需要按照顺序去找即可,而如果不排序,就如同大海捞针,假如100W条数据,可能有时候需要随机查询100W次才找到这个数据,也可能运气好上来第1次就查询到了该数据,不确定性太高!

 原理分析图

在这里插入图片描述

上图这种分层树结构查询效率较高,因为如果我需要查询 id=4的数据,只需要在页目录中匹配,大于3且小于5,则去3对应的page=2中查找数据,这样就不需要从第1页开始检索数据了,大大提高了效率!

从上图可得出,在只有2层的结构下,1page 可以存储记录总数为 1365 * 455 ≈ 62万条,而如果再加1层结构,来存储page层分页目录数据的分页层PAGE的话,那么1PAGE可以存储总page数为:1365 * 1365 ≈ 186万条page,而1PAGE存储的总记录数为 1365 * 1365 * 455 ≈ 8.5 亿条。因此,我们平时使用的话,2层结构就已经足够了!实际上1个页存储的总数据树可能大于理论估计的,因为我们分配name字段的VARCHAR(20)占20个字节,而实际上可能存储的name数据并没有20个字节,可能更小!

 B+树结构分析

查看https://blog.csdn.net/Mcdull__/article/details/118494038第29题

面试题:为什么InnoDB底层使用B+Tree做索引而不用B Tree?

B树每个节点中不仅包含数据的key,还有data数据。而每个页的存储空间是有限的,如果data数据较大时,将会导致每个节点(即一个页16KB)能存储的key的数量减少,当存储数据量很大时,会造成B树的深度较大,增大查询时的磁盘读取I/O次数,进而影响查询效率。(树的深度影响I/O读取次数)

而B+树中数据记录都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只能存储key值信息,这样可以大大增加每个节点(即一个页16KB)能存储的key的数量,进而可以降低树的高度,进而减少磁盘读取I/O次数,提高查询效率。

所以B树和B+树的区别就在于:

  • B+树只有叶子节点存储数据记录
  • B+树非叶子节点只存储键值信息(B树的非叶子也存数据记录)

聚簇索引和非聚簇索引

在表中,聚簇索引实际上就是主要指的是主键索引!如果表中没有主键的话,则MySQL会根据该表生成一个RoleID,拿这个RoleId当做聚簇索引!

聚簇索引:将数据存储与索引放到一起,索引结构的叶子节点保存了每行的数据。例如:上节的分析图中的data层一个单位就是聚簇索引存储数据的例子,主键id 字段就是聚簇索引,上节的分析图就是基于主键索引(聚簇索引)构成的B+树结构!聚簇索引不一定是主键索引,但是主键索引肯定是聚簇索引!

数据库面经_第10张图片

 非聚簇索引将数据与索引分开存储,索引结构的叶子节点指向了数据对应的位置(聚簇索引的值)!非聚簇索引检索数据是在自己的 “树” 上进行查找,例如我们根据表中的非聚簇索引name字段去查找数据时,流程如下图:

数据库面经_第11张图片

再看一张比较正规的分析图:

在这里插入图片描述

 注意:在InnoDB中,在聚簇索引之上创建的索引称之为辅助索引(非聚簇索引),例如:复合索引、单列索引、唯一索引。一个表中只能有1个聚簇索引,而其他索引都是辅助索引!辅助索引的叶子节点存储的不再是行的物理位置,而是主键的值,辅助索引访问数据总是需要二次查找

面试题为什么非聚簇索引(name字段的单列索引)构成的树,其叶子节点存储聚簇索引(主键id),而不直接存储行数据的物理地址呢?

换个方式问:非聚簇索引检索数据时,检索一次本树再去聚簇索引树中检索一次,这样二次检索树结构,那么为什么不直接在非聚簇索引树叶子节点中存放行数据物理地址,这样只需要检索一次树结构就拿到行数据呢?
 

这里画个图方便理解一些:

在这里插入图片描述

从上图得出,在做新增数据时,因为底层是需要基于主键索引进行排序的,那么就可能导致原来某些数据对应的物理地址发生了变化,而这时候由于我们的非聚簇索引树的叶子节点直接存储了数据的物理地址,所以为了保证能获取到数据,还需要同时对非聚簇索引树叶子节点的地址进行一遍更新修改!

​ 同理,如果我们不做插入主键id为4这行记录的操作,而是将其删除的话,这个流程可以自己思考一下!

​ 也就是说:之所以不在非聚簇索引树的叶子节点直接存放行数据的物理地址,是因为,存储数据的物理地址会随着数据库表的CRUD操作而不断变更,为了保证能获取到数据,这时必须要对非聚簇索引树相关叶子节点的地址进行一遍修改!而存主键,主键不会随着CRUD操作发生变化,宁愿多查一次树,也不要再修改一次树的结构!
 

MySQL两种引擎中的(非)聚簇索引

mysql中innodb和myisam对比及索引原理区别:

https://blog.csdn.net/Mcdull__/article/details/115443598

InnoDB中:

InnoDB中使用的是聚簇索引,将主键组织到一颗B+树中,而行数据就存储在该B+树的叶子节点上,若使用WHERE id = 4 这样的条件查找主键,则按照B+树的检索算法即可查找对应的叶子节点,之后获得对应的行数据!
若对使用单列索引(非聚簇索引)的name字段进行搜索,则需要执行2个步骤:

  • 第一步:在辅助索引B+树中检索name,到达其对应的叶子节点后获得该字段对应行记录的主键id!
  • 第二步:使用主键id在主索引B+树中再次执行一次树的检索,最终到达对应的叶子节点并获取到行记录数据!

聚簇索引默认是主键,如果表中没有定义主键,InnoDB会选择一个唯一且非空的索引代替主键作为聚簇索引。而如果也没有这样的唯一非空索引,那么InnoDB就会隐式定义一个主键(类似于Oracle中的RowId)来做为聚簇索引。
如果已经设置了聚簇索引又希望再单独设置聚簇索引,则必须先删除主键,然后添加我们想要的聚簇索引,最后再恢复主键即可!

MYISAM中:

  • MYISAM使用的是非聚簇索引,非聚簇索引的两颗B+树看上去没有什么不同,节点的结构完全一致,只是存储的内容不同,主键索引B+树的节点存储了主键,辅助索引B+树存储了辅助键。
  • 表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指针指向真正的表数据,对于表数据来说,这两个键没有任何差别。
  • 由于索引树是独立的,通过辅助键检索无需再次检索主键索引树!

在这里插入图片描述

 聚簇索引和非聚簇索引的优/劣势

面试题:每次使用非聚簇索检索都需要经过两次B+树查找,看上去聚簇索引的效率明显要低于非聚簇索引,那么聚簇索引的优势何在?

-- 1.由于行数据和聚簇索引树的叶子节点存储在一起,同一页中会有多条行数据,首次访问数据页中某条行记录时,会把该数据页数据加载到Buffer(缓存器)中,当再次访问该数据页中其他记录时,不必访问磁盘而直接在内存中完成访问。
-- 注:主键id和行数据一起被载入内存,找到对应的叶子节点就可以将行数据返回了,如果按照主键id来组织数据,获取数据效率更快!

-- 2.辅助索引的叶子节点,存储主键的值,而不是行数据的存放地址。这样做的好处是,因为叶子节点存放的是主键值,其占据的存储空间小于存放行数据物理地址的储存空间

 面试题:使用聚簇索引需要注意什么?

-- 当使用主键为聚簇索引时,而不要使用UUID方式,因为UUID的值太过离散不适合排序,导致索引树调整复杂度增加,消耗更多时间和资源。

-- 建议主键最好使用INT/BIGINT类型,且为自增,这样便于排序且默认会在索引树的末尾增加主键值,对索引树的结构影响最小(下面主键自增的问题会解释原因)。而且主键占用的存储空间越大,辅助索引中保存的主键值也会跟着增大,占用空间且影响IO操作读取数据!

面试题:为什么主键通常建议使用自增id?

-- 聚簇索引树存放数据的物理地址(xx1,xx2,xx3,xxx5)与索引顺序(1,2,3,5)是一致的,即:
-- 1.只要索引是相邻的,那么在磁盘上索引对应的行数据存放地址也是相邻的。
-- 2.如果主键是自增,那么当插入新数据时,只需要按照顺序在磁盘上开辟新物理地址存储新增行数据即可。
-- 3.而如果不是主键自增,那么当新插入数据后,会对索引进行重新排序(重新调整B+树结构),磁盘上的物理存储地址也需要重新分配要存储的行数据!

在这里插入图片描述

 面试题:索引什么时候失效?

1.使用了or (除非or的列都加上了索引)

2.联合索引 未符合索引字段顺序(最左前缀)

3.like查询 第一个通配符使用的是%,这时候索引是失效的。

4.字符型不加引号 数据库自动转换成数值型 (数据类型不统一)不走索引

5.sql中使用函数,运算操作 

6.对于内容基本重复的列,比如只有1和0,禁止建立索引,因为该索引选择性极差,在特定的情况下会误导优化器做出错误的选择,导致查询速度极大下降

7.mysql扫描全表比索引快 不走索引

eg:
SELECT * FROM t_user WHERE name LIKE 'xx%' -- 可以利用上索引,这种情况下可以拿xx到索引树上去匹配
SELECT * FROM t_user WHERE name LIKE '%xx%' -- 不可以利用上索引
SELECT * FROM t_user WHERE name LIKE '%xx' -- 不可以利用上索引

7、什么是约束?他的分类?MySQL中索引和约束的区别?

定义:为了保证数据的完整性而实现的一套机制,约束是针对表中数据记录的

分类:

  • 非空约束:NOT NULL 保证某列数据不能存储NULL 值;
  • 唯一约束:UNIQUE(字段名) 保证所约束的字段,数据必须是唯一的,允许数据是空值(Null),但只允许有一个空值(Null);
  • 主键约束:PRIMARY KEY(字段名) 主键约束= 非空约束 + 唯一约束 保证某列数据不能为空且唯一;
  • 外键约束:FOREIGN KEY(字段名) 保证一个表中某个字段的数据匹配另一个表中的某个字段,可以建立表与表直接的联系;
  • 自增约束:AUTO_INCREMENT 保证表中新插入数据时,某个字段数据可以依次递增;
  • 默认约束:DEFALUT 保证表中新插入数据时,如果某个字段未被赋值,则会有默认初始化值;
  • 检查性约束:CHECK 保证列中的数据必须符合指定的条件;
     

示例:

create table member(
	id int(10),
	phone int(15) unsigned zerofill,
	name varchar(30) not null,
	constraint uk_name unique(name),
	constraint pk_id primary key (id),
	constraint fk_dept_id foreign key (dept_id,字段2)
	references dept(主表1)(dept_id)
);

面试题:MySQL中索引和约束的区别?

    约束是为了保证表数据的完整性,索引是为了提高查询效率,两者作用不一样!种类也不太一样!

8、数据库设计三大范式?

第一范式:确保每列保持原子性(数据库表中不能出现重复记录,每个字段是原子性的,不能再分)

第二范式:确保表中的每列都和主键相关

第三范式:确保每列都和主键列直接相关,而不是间接相关(不要产生传递依赖)。

9、MySQL查询的指令顺序为?

FROM table_name [as table_alias]

      on

    [left | right | inner join table_name2]  -- 联合查询

    [WHERE ...]  -- 指定结果需满足的条件

    [GROUP BY ...]  -- 指定结果按照哪几个字段来分组

    [HAVING]  -- 过滤分组的记录必须满足的次要条件

     SELECT…

    [ORDER BY ...]  -- 指定查询记录按一个或多个条件排序

    [LIMIT {[offset,]row_count | row_countOFFSET offset}];

--  指定查询的记录从哪条至哪条

10、 MySQL中字段类型CHAR 和 VARCHA 的区别?

char是一种固定长度的类型,varchar是一种可变长度的类型。

  • char如果存入数据的实际长度比指定长度要小,会补空格至指定长度,如果存入的数据的实际长度大于指定长度,低版本会被截取,高版本会报错。
  • varchar类型的数据如果存入的数据的实际长度比指定的长度小,会缩短到实际长度,如果存入数据的实际长度大于指定长度,低版本会被截取,高版本会报错。

char效率会更高,但varchar更节省空间。

11、MySQL中字段类型DATETIME 和 TIMESTAMP的区别?

  • 为什么timestamp只能到2038年
-- MySQL的timestamp类型是4个字节,最大值是2的31次方减1,结果是:
2147483647

-- 转换成北京时间就是: 
2038-01-19 11:14:07

12 、Mybatis中#和$的区别?

  • #可以防止SQL注入,它会将所有传入的参数作为一个字符串来处理。#防止sql注入底层相当于是在操作JDBC时,使用PreparedStatement预编译SQL语句来防止SQL注入
  • $则将传入的参数拼接到sql上去执行,一般用于表名和字段名参数,¥所对应的参数应该由服务器端提供。

所以尽可能不用$

SQL注入案例:
用户在进行登录时,需要验证用户名和密码,对应后台的sql语句为 select * from tableName where username = ‘XXX’ and password = ‘XXX’,XXX为传入的用户名和密码,根据sql返回的结果判断登录是否成功。
假如数据库中存在username为ggqq,password为123456这条记录,此时在登录界面输入用户信息,不出意外会登录成功。但是假如我们在输入时把password的内容改为123456’ or ‘1’ = '1,生成的sql为: select * from tableName where username = ‘ggqq and password =‘123456’ or ‘1’ = ‘1’,这样一来,也会登录成功
 

13、 MySQL大数据量sql分页优化思路?

面试题:线上数据库的一个商品表数据量过千万,做深度分页的时候性能很慢,有什么优化思路?

- 现象:千万级别数据很正常,比如数据流水、日志记录等,数据库正常的深度分页会很慢
- 慢的原因:select * from product limit N,M
- MySQL执行此类SQL时需要先扫描到N行,然后再去取M行,N越大,MySQL扫描的记录数越多,SQL的性能就会越差
 

解决思路:

-- 1、可以使用后端缓存Redis、前端缓存localstorage
-- 2、使用ElasticSearch分页搜索
-- 3、合理使用 mysql 索引
        比如title,cateory被设置为该表的复合索引,可以提高查询效率
        select title,cateory from product limit 1000000,100
-- 4、如果id是自增且不存在中间删除数据,使用子查询优化,定位偏移位置的 id
        这种方式比较耗时,因为需要先检索前1000000行数据,再检索1000000-1000500的目标数据
        select * from oper_log where type='BUY' limit 1000000,100; -- 5秒
        因为id是主键索引,查询速度快,先检索前1000000行记录的id值,并找到第1000000行记录的id值
        select id from oper_log where type='BUY' limit 1000000,1; -- 0.4秒 
       再做一个子查询,因为是主键递增,所以id>=第1000000行记录的id值,这样就相当于跳过扫描前100000行数据,直接从第1000000开始往后检索100条数据
       select * from oper_log where type='BUY' and  id>=(select id from oper_log where type='BUY' limit 1000000,1) limit 100; -- 0.8秒 

14、mysql优化

https://blog.csdn.net/Mcdull__/article/details/116660535  必看

问题:如何做慢 SQL 优化?

首先要搞明白慢的原因是什么:是查询条件没有命中索引?还是 load 了不需要的数据列?还是数据量太大?所以优化也是针对这三个方向来的:

  1. 首先用 explain 分析语句的执行计划,查看使用索引的情况,是不是查询没走索引,如果可以加索引解决,优先采用加索引解决。
  2. 分析语句,看看是否存在一些导致索引失效的用法,是否 load 了额外的数据,是否加载了许多结果中并不需要的列,对语句进行分析以及重写。
  3. 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行垂直拆分或者水平拆分
     

SQL优化语句步骤

SQL优化语句的一般步骤

-1.通过show status命令了解各种SQL的执行频率

-2.定位执行效率较低的SQL语句 慢查询

-3.通过EXPLAIN分析较低SQL的执行计划

-4.通过show profile分析SQL

-5.通过trace分析优化器如何选择执行计划

-6.确定问题并采取相应的优化措施

15、高性能高可用MySQL(主从同步,读写分离,分库分表,去中心化,虚拟IP,心跳机制)

https://blog.csdn.net/Mcdull__/article/details/116949576

16、数据库为什么不推荐使用外键约束?

其实这个话题是老生常谈,很多人在工作中确实也不会使用外键。包括在阿里的JAVA规范中也有下面这一条

【强制】不得使用外键与级联,一切外键概念必须在应用层解决。 **

首先我们明确一点,外键约束是一种约束,这个约束的存在,会保证表间数据的关系“始终完整”。因此,外键约束的存在,并非全然没有优点。
比如使用外键,可以

  • 保证数据的完整性和一致性
  • 级联操作方便
  • 将数据完整性判断托付给了数据库完成,减少了程序的代码量

然而,鱼和熊掌不可兼得。外键是能够保证数据的完整性,但是会给系统带来很多缺陷。正是因为这些缺陷,才导致我们不推荐使用外键,具体如下

性能问题

假设一张表名为user_tb。那么这张表里有两个外键字段,指向两张表。那么,每次往user_tb表里插入数据,就必须往两个外键对应的表里查询是否有对应数据。如果交由程序控制,这种查询过程就可以控制在我们手里,可以省略一些不必要的查询过程。但是如果由数据库控制,则是必须要去这两张表里判断。

并发问题

在使用外键的情况下,每次修改数据都需要去另外一个表检查数据,需要获取额外的锁。若是在高并发大流量事务场景,使用外键更容易造成死锁。

扩展性问题

这里主要是分为两点

  • 做平台迁移方便,比如你从Mysql迁移到Oracle,像触发器、外键这种东西,都可以利用框架本身的特性来实现,而不用依赖于数据库本身的特性,做迁移更加方便。
  • 分库分表方便,在水平拆分和分库的情况下,外键是无法生效的。将数据间关系的维护,放入应用程序中,为将来的分库分表省去很多的麻烦。

技术问题

使用外键,其实将应用程序应该执行的判断逻辑转移到了数据库上。那么这意味着一点,数据库的性能开销变大了,那么这就对DBA(数据库管理员)的要求就更高了。很多中小型公司由于资金问题,并没有聘用专业的DBA,因此他们会选择不用外键,降低数据库的消耗。
相反的,如果该约束逻辑在应用程序中,发现应用服务器性能不够,可以加机器,做水平扩展。如果是在数据库服务器上,数据库服务器会成为性能瓶颈,做水平扩展比较困难。

17.union 和union all的区别?

union:对两个结果集进行并集操作,不包括重复行,同时进行默认的排序规则。

union all:对两个结果集进行并集操作,包括重复行,不进行排序。

https://blog.csdn.net/u010931123/article/details/82425580

你可能感兴趣的:(面经,数据库面经)