数据存储

MySQL

1. 执行一条 select 语句,期间发生了什么?

  • 连接器:建立连接,管理连接、校验用户身份;

  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;

  • 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;

  • 执行 SQL:执行 SQL 共有三个阶段:

    • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。

    • 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;

    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

2. MySQL 的 NULL 值是怎么存放的?

MySQL 的 Compact 行格式中会用「NULL 值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。

NULL 值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL 值列表,这样可节省 1 字节的空间。

3. MySQL 怎么知道 varchar(n) 实际占用数据的大小?

MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。

4. varchar(n) 中 n 最大取值为多少?

如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。

计算公式:65535 - 变长字段字节数列表所占用的字节数 - NULL 值列表所占用的字节数 = 65535 - 2 - 1 = 65532。

如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL 值列表所占用的字节数 <= 65535。

5. 行溢出后,MySQL 是怎么处理的?

如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。

Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。

Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。

6. 事务隔离级别是怎么实现的?

InnoDB 引擎的默认隔离级别是可重复读。

  • 要解决脏读现象,就要将隔离级别升级到读已提交以上的隔离级别

  • 要解决不可重复读现象,就要将隔离级别升级到可重复读以上的隔离级别。

  • 对于幻读现象。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:

    • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。

    • 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁 + 间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同:

  • 「读提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

  • 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。

这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。

7. MySQL 可重复读隔离级别,完全解决幻读了吗?

  • 对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。

  • 对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。

所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。

要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

8. 为什么 MySQL InnoDB 选择 B+tree 作为索引的数据结构?

  1. B+Tree vs B Tree

B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。

另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。

  1. B+Tree vs 二叉树

对于有 N 个叶子节点的 B+Tree,其搜索复杂度为 O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。

在实际的应用当中, d 值是大于 100 的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 34 层左右,也就是说一次数据查询操作只需要做 34 次的磁盘 I/O 操作就能查询到目标数据。

而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。

  1. B+Tree vs Hash

Hash 在做等值查询的时候效率高,搜索复杂度为 O(1),但是 Hash 表不适合做范围查询。

9. 联合索引

联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。

10. 索引下推

在 MySQL 5.6 之前,只能从 ID(主键值)开始一个个回表,到「主键索引」上找出数据行,再对比 b 字段值。

而 MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

当你的查询语句的执行计划里,出现了 Extra 为 Using index condition,那么说明使用了索引下推的优化。

11. 什么时候需要 / 不需要创建索引?

需要

  • 字段有唯一性限制的,比如商品编码;

  • 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。

  • 经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。

不需要

  • WHERE 条件,GROUP BY,ORDER BY 里用不到的字段,索引的价值是快速定位。

  • 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女。因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。

  • 表数据太少的时候,不需要创建索引。

  • 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree 的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。

12. 有什么优化索引的方法?

  • 前缀索引优化;

使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。

  • 覆盖索引优化;

覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。

  • 主键索引最好自增;

如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。

  • 防止索引失效;

    • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效;

    • 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;

    • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。

    • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

13. 从数据页的角度看 B+ 树

InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。

数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找法的方式进行检索从而提高效率。

为了高效查询记录所在的数据页,InnoDB 采用 B+ 树作为索引,每个节点都是一个数据页。

如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引;如果叶子节点存储的不是实际数据,而是主键值则就是二级索引,一个表中可以有多个二级索引。

在使用二级索引进行查找数据时,如果查询的数据能在二级索引找到,那么就是「索引覆盖」操作,如果查询的数据不在二级索引里,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」。

14. 为什么 MySQL 采用 B+ 树作为索引?

二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn) 降低为 O(n)。

为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。

B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。

但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

  • B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+ 树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O 次数会更少。

  • B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;

  • B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

15. MySQL 单表不要超过 2000W 行,靠谱吗?

索引结构不会影响单表最大行数,2000W 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。

16. 索引失效有哪些?

  • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效;

  • 当我们在查询条件中对索引列使用函数、表达式计算,就会导致索引失效。

  • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。

  • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

17. MySQL 使用 like “%x“,索引一定会失效吗?

如果数据库表中的字段只有主键 + 二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树 (type=index)。

18. count(*) 和 count(1) 有什么区别?哪个性能最好?

count(1)、 count(*)、 count(主键字段) 在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。

所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。

再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。

19. 如何优化 count(*)?

  • 近似值

可以使用 show table status 或者 explain 命令来表进行估算。

  • 额外表保存计数值

当我们在数据表插入一条记录的同时,将计数表中的计数字段 + 1。也就是说,在新增和删除操作时,我们需要额外维护这个计数表。

20. 全局锁是怎么用的?

整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:

对数据的增删改操作,比如 insert、delete、update 等语句; 对表结构的更改操作,比如 alter table、drop table 等语句。 如果要释放全局锁,则要执行这条命令:

unlock tables

当然,当会话断开了,全局锁会被自动释放。

21. 全局锁应用场景是什么?

全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

22. 全局锁又会带来什么缺点呢?

加上全局锁,意味着整个数据库都是只读状态。

那么如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。

23. MySQL 表级锁有哪些?具体怎么用的。

表锁

表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

也就是说如果本线程对学生表加了「共享表锁」,那么本线程接下来如果要对学生表执行写操作的语句,是会被阻塞的,当然其他线程对学生表进行写操作时也会被阻塞,直到锁被释放。

不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能。

元数据锁(MDL)

我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:

  • 对一张表进行 CRUD 操作时,加的是 MDL 读锁;

  • 对一张表做结构变更操作的时候,加的是 MDL 写锁;

MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。

反之,当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。

意向锁

意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突。

意向锁的目的是为了快速判断表里是否有记录被加锁。

AUTO-INC 锁

之后可以在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值,这主要是通过 AUTO-INC 锁实现的。

  • 当 innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁,语句执行结束后才释放锁;

  • 当 innodb_autoinc_lock_mode = 2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。

  • 当 innodb_autoinc_lock_mode = 1:

    • 普通 insert 语句,自增锁在申请之后就马上释放;

    • 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

24. MDL 不需要显式调用,那它是在什么时候释放的?

MDL 是在事务提交后才会释放,这意味着事务执行期间,MDL 是一直持有的。

申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。

25. 行级锁

Record Lock

Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:

  • 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);

  • 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。

Gap Lock

Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。

Next-Key Lock

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。

next-key lock 是包含间隙锁 + 记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。

插入意向锁

一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。

如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。

插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。

插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁。

26. MySQL 是怎么加锁的?

唯一索引等值查询:

  • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」。

  • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」。

非唯一索引等值查询:

  • 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描。在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁。

  • 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。

27. update 没加索引会锁全表?

当我们要执行 update 语句的时候,确保 where 条件中带上了索引列,防止因为扫描全表,而对表中的所有记录加上锁。

我们可以打开 MySQL sql_safe_updates 参数,这样可以预防 update 操作时 where 条件没有带上索引列。

如果发现即使在 where 条件中带上了列索引列,优化器走的还是全表扫描,这时我们就要使用 force index([index_name]) 可以告诉优化器使用哪个索引。

28. MySQL 记录锁 + 间隙锁可以防止删除操作而导致的幻读吗?

在 MySQL 的可重复读隔离级别下,针对当前读的语句会对索引加记录锁 + 间隙锁,这样可以避免其他事务执行增、删、改时导致幻读的问题。

29. MySQL 死锁了,怎么办?

死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。

在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:

  • 设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚。

  • 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。

30. 为什么需要 undo log?

  • 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。

  • 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。

31. 为什么需要 Buffer Pool?

  • 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。

  • 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘 I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。

32. Buffer Pool 缓存什么?

InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的 16KB 的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。

开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。

当我们查询一条记录时,InnoDB 是会把整个页的数据加载到 Buffer Pool 中,将页加载到 Buffer Pool 后,再通过页里的「页目录」去定位到某条具体的记录。

33. 为什么需要 redo log?

  • 实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;

  • 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。

34. redo log 和 undo log 区别在哪?

  • redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;

  • undo log 记录了此次事务「开始前」的数据状态,记录的是更新之前的值;

所以有了 redo log,再通过 WAL 技术,InnoDB 就可以保证即使数据库发生异常重启,之前已提交的记录都不会丢失,这个能力称为 crash-safe(崩溃恢复)。可以看出来, redo log 保证了事务四大特性中的持久性。

35. 什么是 WAL

为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。

后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 WAL (Write-Ahead Logging)技术。

WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。

36. 产生的 redo log 是直接写入磁盘的吗?

不是的。

实际上, 执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,因为这样会产生大量的 I/O 操作,而且磁盘的运行速度远慢于内存。

所以,redo log 也有自己的缓存—— redo log buffer,每当产生一条 redo log 时,会先写入到 redo log buffer

37. redo log 什么时候刷盘?

  • MySQL 正常关闭时;

  • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;

  • InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。

  • 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘

除此之外,InnoDB 还提供了另外两种策略,由参数 innodb_flush_log_at_trx_commit 参数控制,可取的值有:0、1、2,默认值为 1,这三个值分别代表的策略如下:

  • 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。

  • 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。

  • 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,是专门用来缓存文件数据的,所以写入「 redo log 文件」意味着写入到了操作系统的文件缓存。

38. innodb_flush_log_at_trx_commit 为 0 和 2 的时候,什么时候才将 redo log 写入磁盘?

InnoDB 的后台线程每隔 1 秒:

  • 针对参数 0 :会把缓存在 redo log buffer 中的 redo log ,通过调用 write() 写到操作系统的 Page Cache,然后调用 fsync() 持久化到磁盘。所以参数为 0 的策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失;

  • 针对参数 2 :调用 fsync,将缓存在操作系统中 Page Cache 里的 redo log 持久化到磁盘。所以参数为 2 的策略,较取值为 0 情况下更安全,因为 MySQL 进程的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。

39. redo log 文件写满了怎么办?

如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作。此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动,然后 MySQL 恢复正常运行,继续执行新的更新操作。

所以,一次 checkpoint 的过程就是脏页刷新到磁盘中变成干净页,然后标记 redo log 哪些记录可以被覆盖的过程。

40. 为什么需要 binlog ?

binlog 用于备份恢复、主从复制;

41. redo log 和 binlog 有什么区别?

  1. 适用对象不同:

  • binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;

  • redo log 是 Innodb 存储引擎实现的日志;

  1. 文件格式不同:

  • binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:

    • STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 now 函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;

    • ROW:记录行数据最终被修改成什么样了,不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大;

    • MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;

  • redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了 AAA 更新;

  1. 写入方式不同:

  • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。

  • redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。

  1. 用途不同:

  • binlog 用于备份恢复、主从复制;

  • redo log 用于掉电等故障恢复。

42. 如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗?

不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。

因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。

binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。

43. 主从复制是怎么实现?

MySQL 集群的主从复制过程梳理成 3 个阶段:

  • 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。

  • 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。

  • 回放 Binlog:回放 binlog,并更新存储引擎中的数据。

具体详细过程如下:

  • MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。

  • 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。

  • 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

44. 从库是不是越多越好?

不是的。

因为从库数量增加,从库连接上来的 I/O 线程也比较多,主库也要创建同样多的 log dump 线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽。

所以在实际使用中,一个主库一般跟 2 ~ 3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。

45. MySQL 主从复制还有哪些模型?

  • 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。

  • 异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。

  • 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。

46. binlog 什么时候刷盘?

MySQL 给每个线程分配了一片内存用于缓冲 binlog ,该内存叫 binlog cache,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。

47. 什么时候 binlog cache 会写到 binlog 文件?

MySQL 提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:

  • sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;

  • sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync;

  • sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。

48. 为什么需要两阶段提交?

可以看到,在持久化 redo log 和 binlog 这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致性。这是因为 redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。

49. 两阶段提交的过程是怎样的?

  • prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);

  • commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;

50. 异常重启会出现什么现象?

  • 如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。

  • 如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务。

51. 处于 prepare 阶段的 redo log 加上完整 binlog,重启就提交事务,MySQL 为什么要这么设计?

binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。

所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。

52. 事务没提交的时候,redo log 会被持久化到磁盘吗?

会的。

事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些缓存在 redo log buffer 里的 redo log 也会被「后台线程」每隔一秒一起持久化到磁盘。

也就是说,事务没提交的时候,redo log 也是可能被持久化到磁盘的。

有的同学可能会问,如果 mysql 崩溃了,还没提交事务的 redo log 已经被持久化磁盘了,mysql 重启后,数据不就不一致了?

放心,这种情况 mysql 重启会进行回滚操作,因为事务没提交的时候,binlog 是还没持久化到磁盘的。

53. 两阶段提交有什么问题?

  • 磁盘 I/O 次数高:对于“双 1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。

  • 锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。

54. 为什么两阶段提交的磁盘 I/O 次数会很高?

binlog 和 redo log 在内存中都对应的缓存空间,binlog 会缓存在 binlog cache,redo log 会缓存在 redo log buffer,它们持久化到磁盘的时机分别由下面这两个参数控制。一般我们为了避免日志丢失的风险,会将这两个参数设置为 1:

  • 当 sync_binlog = 1 的时候,表示每次提交事务都会将 binlog cache 里的 binlog 直接持久到磁盘;

  • 当 innodb_flush_log_at_trx_commit = 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘;

可以看到,如果 sync_binlog 和 当 innodb_flush_log_at_trx_commit 都设置为 1,那么在每个事务提交过程中, 都会至少调用 2 次刷盘操作,一次是 redo log 刷盘,一次是 binlog 落盘,所以这会成为性能瓶颈。

55. 为什么事务提交锁竞争激烈?

在早期的 MySQL 版本中,通过使用 prepare_commit_mutex 锁来保证事务提交的顺序,在一个事务获取到锁时才能进入 prepare 阶段,一直到 commit 阶段结束才能释放锁,下个事务才可以继续进行 prepare 操作。

通过加锁虽然完美地解决了顺序一致性的问题,但在并发量较大的时候,就会导致对锁的争用,性能不佳。

56. binlog 组提交

MySQL 引入了 binlog 组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数,如果说 10 个事务依次排队刷盘的时间成本是 10,那么将这 10 个事务一次性一起刷盘的时间成本则近似于 1。

引入了组提交机制后,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个过程:

  • flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘);

  • sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘);

  • commit 阶段:各个事务按顺序做 InnoDB commit 操作;

上面的每个阶段都有一个队列,每个阶段有锁进行保护,因此保证了事务写入的顺序,第一个进入队列的事务会成为 leader,leader 领导所在队列的所有事务,全权负责整队的操作,完成后通知队内其他事务操作结束。

57. redo log 组提交

在 MySQL 5.7 版本中,做了个改进,在 prepare 阶段不再让事务各自执行 redo log 刷盘操作,而是推迟到组提交的 flush 阶段,也就是说 prepare 阶段融合在了 flush 阶段。

这个优化是将 redo log 的刷盘延迟到了 flush 阶段之中,sync 阶段之前。通过延迟写 redo log 的方式,为 redolog 做了一次组写入,这样 binlog 和 redo log 都进行了优化。

58. MySQL 磁盘 I/O 很高,有什么优化的方法?

我们知道事务在提交的时候,需要将 binlog 和 redo log 持久化到磁盘,那么如果出现 MySQL 磁盘 I/O 很高的现象,我们可以通过控制以下参数,来 “延迟” binlog 和 redo log 刷盘的时机,从而降低磁盘 I/O 的频率:

  • 设置组提交的两个参数: binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 参数,延迟 binlog 刷盘的时机,从而减少 binlog 的刷盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但即使 MySQL 进程中途挂了,也没有丢失数据的风险,因为 binlog 早被写入到 page cache 了,只要系统没有宕机,缓存在 page cache 里的 binlog 就会被持久化到磁盘。

  • 将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000),表示每次提交事务都 write,但累积 N 个事务后才 fsync,相当于延迟了 binlog 刷盘的时机。但是这样做的风险是,主机掉电时会丢 N 个事务的 binlog 日志。

  • 将 innodb_flush_log_at_trx_commit 设置为 2。表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache,专门用来缓存文件数据的,所以写入「 redo log 文件」意味着写入到了操作系统的文件缓存,然后交由操作系统控制持久化到磁盘的时机。但是这样做的风险是,主机掉电的时候会丢数据。

59. 详解更新一条记录的流程

具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程如下:

  1. 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:

    • 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;

    • 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。

  2. 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:

    • 如果一样的话就不进行后续更新流程;

    • 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;

  3. 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。

  4. InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘 I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。

  5. 至此,一条记录更新完了。

  6. 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。

  7. 事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交):

    • prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;

    • commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);

60. 详解 Buffer Pool

Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。

Buffer Pool 以页为单位缓冲数据,可以通过 innodb_buffer_pool_size 参数调整缓冲池的大小,默认是 128 M。

Innodb 通过三种链表来管理缓页:

  • Free List (空闲页链表),管理空闲页;

  • Flush List (脏页链表),管理脏页;

  • LRU List,管理脏页 + 干净页,将最近且经常查询的数据缓存在其中,而不常查询的数据就淘汰出去。

InnoDB 对 LRU 做了一些优化,我们熟悉的 LRU 算法通常是将最近查询的数据放到 LRU 链表的头部,而 InnoDB 做 2 点优化:

  • 将 LRU 链表 分为 young 和 old 两个区域,加入缓冲池的页,优先插入 old 区域;页被访问时,才进入 young 区域,目的是为了解决预读失效的问题。

  • 当「页被访问」且「 old 区域停留时间超过 innodb_old_blocks_time 阈值(默认为 1 秒)」时,才会将页插入到 young 区域,否则还是插入到 old 区域,目的是为了解决批量数据访问,大量热数据淘汰的问题。

可以通过调整 innodb_old_blocks_pct 参数,设置 young 区域和 old 区域比例。

在开启了慢 SQL 监控后,如果你发现「偶尔」会出现一些用时稍长的 SQL,这可因为脏页在刷新到磁盘时导致数据库性能抖动。如果在很短的时间出现这种现象,就需要调大 Buffer Pool 空间或 redo log 日志的大小。

61. MySQL 三范式

  • 第一范式要求数据库表中的每一列都是不可分割的原子数据项。这意味着每一列的数据都应该是单一的值,而不是列表或集合。

  • 第二范式在满足第一范式的基础上,要求所有非主键属性完全依赖于主键,而不是部分依赖。解决的是复合主键的部分依赖问题。

  • 第三范式在满足第二范式的基础上,任何非主键属性不依赖于其他非主键属性。解决的是非主键属性之间的传递依赖问题。

在满足第一范式的基础上,若这张表只有一个主键,那么它必定满足第二范式。

62. MySQL 实现分布式锁

MySQL 可以通过在 select 语句后增加 for update 来获取排它锁,从而实现分布式锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁,因此获得排它锁的线程即可获得分布式锁。

63. 慢 SQL 优化

  • 索引优化:索引是提高查询效率的重要手段,可以通过 explain 命令查看 SQL 语句的执行计划,找到慢查询的原因,然后对相应的字段添加索引。

  • 优化 SQL 语句:常见的 SQL 优化技巧包括避免使用通配符查询、使用 JOIN 代替嵌套 SELECT、减少子查询等。

  • 分析表结构:创建索引、垂直分割表等。创建合适的索引可以加快查询速度,但同时会增加写入数据的时间。垂直分割表可以将表根据不同的功能、访问模式分为多个表,避免查询全部字段和频繁更新次数相同的字段造成的性能问题。

  • 查看监控和报警:使用数据库监控来实时监控数据库的性能,包括查询时间、锁等待时间、缓存命中率等。配置报警规则,当某些关键性能指标(如查询时间、CPU 使用率、内存使用率等)超过阈值时,触发报警,及时发现和解决性能问题。

64. 如何防止 SQL 注入

使用预编译语句:GORM 使用 database/sql 的参数占位符来构造 SQL 语句,这可以自动转义参数,避免 SQL 注入数据。

65. InnoDB 有 Hash 索引吗

InnoDB 不支持用户定义的 Hash 索引,但通过 Adaptive Hash Index 提供了类似的优化功能,用于加速特定查询。用户无需手动管理 AHI,InnoDB 会根据查询模式自动调整和优化。

66. int(1) 和 int(10) 的区别

在 MySQL 中,int(1) 和 int(10) 的区别在于,int(1) 和 int(10) 本身没有区别,但是加上 (M) 值后,会有显示宽度的设置。这个值不是存储数据的大小,而是显示数据的长度。如果你想要存储数据的大小,可以使用 char(10) 或者 varchar(10)。

67. 字符串做主键索引的危害

  • 字符串长度大,占用空间大,不利于查询和排序。

  • 字符串作为主键,会导致索引文件变大,降低查询效率。

68. utf8 varchar 最长是多少

utf8 编码下,MySQL 中的 varchar 最长长度是 21844 个字符

69. MySQL 乐观锁实现方式

MySQL 的乐观锁是通过使用版本号或时间戳来实现的。

UPDATE products
SET price = 12.99,
    version = version + 1
WHERE id = 1
  AND version = 0;

在上述 SQL 语句中,我们将 price 更新为 12.99,并将 version 增加 1。同时,我们使用 WHERE 子句来限制更新只能在 id 为 1 且 version 为 0 的记录上进行,这是为了确保在更新期间没有其他并发操作修改了该记录。

如果更新成功,则表示没有其他并发操作修改了该记录。如果更新失败,则表示有其他并发操作修改了该记录,这时可以选择重试操作或执行其他逻辑以适应该冲突。

70. MyISAM 和 InnoDB 的区别有哪些

  1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

  2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;

  3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的;

  4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

  5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

71. 读写 MySQL 超时

  • 调整连接超时设置:MySQL 有两个主要的超时设置,分别是 wait_timeout 和 interactive_timeout。这两个设置决定了连接在多长时间没有活动后会被关闭。

  • 优化查询和事务:如果查询或事务的执行时间超过了连接的超时时间,可以考虑对查询进行优化,例如添加索引、优化查询语句等。另外,可以将长时间运行的查询或事务拆分为多个较小的操作,以减少单个操作的执行时间。

  • 使用合适的连接池:通过使用连接池,可以避免频繁地创建和关闭连接,提高连接的复用率。连接池可以自动管理连接的超时时间,并在需要时重新创建新的连接。

72. 数据库分表

https://zhuanlan.zhihu.com/p/569961814

73. MySQL 索引如何存储?每个索引一个 B+ 树,还是多个索引放一个 B+ 树?

每个索引都对应一个独立的 B+ 树。每个索引节点包含多个关键字和对应的指针,关键字按照顺序排列,指针指向下一层的节点或者叶子节点。叶子节点存储了索引的值以及对应的数据行的指针。这种结构可以高效地支持单个值的查找、范围查询和排序操作。

74. MySQL 叶子节点中存的是什么数据?

MySQL 的叶子节点存储的是具体数据或者主键 KEY。

在 B+ 树索引中,叶子节点是存储实际数据的节点。每个叶子节点包含一定范围的键值和对应的数据块的指针 (或者是数据本身) 。叶子节点之间通过双向指针连接,形成一个有序的链表。通过这些指针,可以在树中进行快速的搜索和定位,以找到包含特定键值的叶子节点。

75. B+ 树的范围查找怎么做的?

  1. 从根节点开始,按照 B+ 树的搜索算法,找到第一个大于或等于范围的起始值的叶子节点。这可以通过在每个内部节点上进行二分查找来实现。

  2. 从起始叶子节点开始,顺序遍历叶子节点,找到所有在范围内的值。由于 B+ 树的叶子节点是按照关键字顺序链接的,所以可以通过遍历叶子节点来找到范围内的所有值。

  3. 如果范围的结束值大于当前叶子节点的最大关键字,继续遍历下一个叶子节点,重复步骤 2,直到找到所有满足范围条件的值。

B+ 树的范围查找是基于关键字的有序性进行的,因此可以快速定位到范围的起始位置,并按顺序获取范围内的值。这使得 B+ 树在范围查询方面非常高效。

76. 如果只有原子性能保持一致性吗

两个操作是原子操作,即要么全部执行成功,要么全部失败,如果发生故障(如系统挂起、断电等),就不会出现只从 A 账户扣款但没有给 B 账户加款的情况。

然而,仅仅保证了原子性,并不意味着能保证系统的一致性。一致性是指数据要满足预设的一系列约束和规则。若一个转账操作,一致性则要求在转账操作完成后,A 账户和 B 账户的总金额应该和转账操作前保持一致。但是,如果在执行转账操作的同时,同时有其他的操作(如 A 账户有一笔 100 元的存款),那么仅仅依靠原子性,是无法保证数据的一致性的。

77. MySQL 索引类型

MySQL 的索引主要有以下几种类型:

  1. 主键索引 (PRIMARY KEY) :用于唯一标识表中的每一行数据。一个表只能有一个主键索引,且主键索引的值不能为 NULL。

  2. 唯一索引 (UNIQUE) :保证索引列的值在表中是唯一的,可以有多个唯一索引。

  3. 普通索引 (INDEX) :最基本的索引类型,用于加快查询速度。可以在一个表中创建多个普通索引。

  4. 全文索引 (FULLTEXT) :用于在文本字段上进行全文搜索。

78. undo log 与 redo log 里面记录的是物理数据还是逻辑数据

undo log 记录的内容是逻辑数据。如果某个事务发生了回滚,undo log 就会发生作用,这个过程通常被称为「反向」操作。undo log 记录了一个事务执行所做的每一步修改,这样在需要撤销的时候,就可以利用 undo log 中记录的信息,将数据状态回滚到事务开始之前。

redo log 记录的是物理数据。也就是说,它保存了数据库中的实际更改。当系统崩溃或其他故障发生时,redo log 能够通过重做(redo)之前的操作,将数据恢复到故障发生时的状态。它是数据库恢复的一个重要手段。

79. 单纯的 SELECT 会加锁吗

  • 单纯的 SELECT 语句不会对表进行加锁。在 MySQL 中,SELECT 语句默认使用共享锁 (也称为读锁) ,它允许多个会话同时读取同一张表的数据。

  • 使用了 FOR UPDATE 子句,则会对查询的行进行加锁,此时会获取排他锁 (也称为写锁) 。排他锁会阻塞其他会话的读取和写入操作,直到当前会话释放锁为止。

80. MySQL 自增 id 能回滚吗

MySQL 的自增 ID 在事务回滚后是不会回退的。当一个事务中插入了数据并回滚后,虽然插入的数据被删除了,但是自增 ID 的值仍然会自增。

81. MySQL 内连接和左外连接的区别

  • 内连接 (INNER JOIN) : 内连接返回两个表中满足连接条件的行,即只返回两个表中连接字段相等的行。它只返回符合条件的交集部分。

  • 左外连接 (LEFT JOIN) : 左外连接返回左表中的所有记录和右表中连接字段相等的记录。即左表的所有行都会被返回,而右表中没有匹配的行则会填充为 NULL。

82. 主从数据库在对主库写的时候加锁会不会锁从库

当我们在主库进行写操作(更新,插入,删除等)时,会对相关数据加锁,以保证数据的一致性和完整性。这种锁只在主库生效,并不会影响到从库。

主库完成写操作后,会通过数据库的复制机制将这些变更写入日志文件,然后再将这些日志复制到从库。在从库上重放这些日志,完成与主库的数据同步。

83. 分库分表怎么实现数据的平滑迁移

  1. 设计分库分表方案:根据业务需求和数据特点,设计合适的分库分表方案。常见的策略可以用哈希。

  2. 创建新的数据库和表结构:根据分库分表方案,在新的数据库中创建分片并设计相应的表结构。确保新的数据库和表结构与原始数据库保持一致。

  3. 数据同步:将原始数据库中的数据逐步同步到新的数据库中。可以采用增量同步和全量同步相结合的方式。增量同步可以通过开启增量数据单向同步,将数据从旧库同步到新库。全量同步则是将历史数据从旧库全量同步到新库。

  4. 切换读写操作:当数据同步完成后,可以将读写操作切换到新的数据库上。这可以通过修改应用程序的连接配置或者使用中间件来实现。

  5. 监控和验证:在切换完成后,需要对新的数据库进行监控和验证,确保数据迁移过程中没有出现问题,并且新的数据库能够正常工作。可以通过监控指标、日志和测试来进行验证。

84. 一张表里有班级学号分数,用 SQL 求每个班级分数前三的学生

SELECT 班级, 学号, 分数
FROM (
  SELECT 班级, 学号, 分数,
    ROW_NUMBER() OVER(PARTITION BY 班级 ORDER BY 分数 DESC) as rn
  FROM student_info
) t
WHERE rn <= 3

在这个查询中,首先我们在内部查询中使用窗口函数 ROW_NUMBER(),对每个分区(这里的分区就是班级)的记录进行排序(按分数降序)并编号。然后,在外部查询中,我们筛选出每个班级分数前三的学生。

85. 联合索引的存储结构

联合索引的实现主要基于 B+ 树数据结构。在创建联合索引时,MySQL 会按照定义的列顺序对数据进行排序和存储。具体过程如下:

  1. 排序:在创建联合索引时,系统首先根据索引列的顺序(从左到右)对表中的数据进行排序。例如,对于联合索引 (b, c, d),数据会首先按 b 列排序,如果 b 相等,则按 c 排序,再按 d 排。

  2. 数据页结构:每个数据页存储了联合索引的所有列的值以及对应的主键值。数据页内部是有序的,形成单向链表,数据页之间则组成双向链表。这种结构使得在查询时可以快速定位到所需的数据。

  3. 查询过程

    • 当执行查询时,MySQL 会首先在索引页中查找最小值记录,进而定位到对应的数据页。

    • 在数据页内部,系统会依次比较每个字段的值,直到找到匹配的记录。例如,在查找某个班级、学生和科目的成绩时,系统会依次根据班级、学生姓名和科目名称进行查找。

86. 为什么 MySQL 要借助数据库引擎而不是自己实现

MySQL 的架构采用了可插拔的存储引擎设计,这意味着开发者可以根据特定的应用需求选择合适的存储引擎,如 InnoDB、MyISAM 等。这种设计使得 MySQL 能够灵活适应不同的使用场景(如 OLTP 和 OLAP),并且不需要对整个数据库系统进行重大修改。

87. 深度翻页问题

标记记录法

  1. 在第一次查询时,获取最大的 ID 值 (或其他唯一标识列)。

  2. 在后续查询时,使用 ID > 标记值 的条件进行查询,并更新标记值。

  3. 这样每次只需要查询比标记值大的数据,可以大幅提升性能。

分批次查询

这种方法是将原本一次性查询的数据,分批次进行多次查询。每次查询一个合适的数量,然后将结果合并。比如每次查询 1000 条,那么第一次查询 0-1000 条,第二次查询 1000-2000 条,以此类推。这样每次查询的偏移量都不会太大,可以保证性能。

88. 哈希索引适合什么场景

  • 等值查询:哈希索引在处理等值查询(如 =IN)时表现优越,因为它可以通过哈希函数直接定位到数据位置,查询时间复杂度为 O(1)。

  • 高频率的单值查找:在需要频繁查找特定键值的场景中,哈希索引能够提供快速的访问速度。

然而,哈希索引不支持范围查询和排序操作,因此在需要这些功能的场景中就不适用。例如,哈希索引无法处理 ><BETWEEN 等条件查询,也无法用于 ORDER BY 操作。

Redis

1. Redis 和 Memcached 有什么区别?

共同点

  • 都是基于内存的数据库,一般都用来当做缓存使用。

  • 都有过期策略。

  • 两者的性能都非常高。

不同点

  • Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;

  • Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;

  • Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;

  • Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持;

2. 为什么用 Redis 作为 MySQL 的缓存?

  • Redis 具备高性能、低延迟(存在于内存)

  • Redis 能够分布式高可用(哨兵、集群机制)

  • Redis 提供丰富的数据结构

3. Redis 是单线程吗?

Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程

  • Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;

  • Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key 等命令,会把这些删除操作交给后台线程来执行。当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,我们应该使用 unlink 命令来异步删除大 key。

关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列

  • BIO_CLOSE_FILE

  • BIO_AOF_FSYNC

  • BIO_LAZY_FREE

4. Redis 采用单线程为什么还这么快?

  • Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU。

  • Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

  • Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

5. Redis 6.0 之前为什么使用单线程?

CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络 I/O 的限制

使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。

6. Redis 6.0 之后为什么引入了多线程?

Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理。

Redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据,并不会以多线程的方式处理读请求。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。

Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程:

  • Redis-server : Redis 的主线程,主要负责执行命令;

  • bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF 刷盘任务、释放内存任务;

  • io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。

7. Redis 如何实现数据不丢失?

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;

  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;

  • 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RDB 的优点;

8. AOF 日志是如何实现的?

Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。

9. AOF 为什么先执行命令,再把数据写入日志呢?

  • 避免额外的检查开销:先将写操作命令记录到 AOF 日志里,再执行该命令,如果当前的命令语法有问题,那么不进行命令语法检查就会将该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。

  • 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。

10. AOF 写回策略有几种?

  • Always,每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;

  • Everysec,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;

  • No,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

11. AOF 日志过大,会触发什么机制?

AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。

在使用重写机制后,就会读取 key 最新的 value

12. 重写 AOF 日志的过程是怎样的?

重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的

  • 避免堵塞主进程

  • 发生写时复制,复用加锁保证数据安全

重写过程中,主进程依然可以正常处理命令,为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。

在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。

主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

  • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;

  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

13. RDB 快照是如何实现的呢?

RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据

在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以

14. RDB 做快照时会阻塞线程吗?

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;

  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;

15. RDB 在执行快照的时候,数据能修改吗?

如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。

16. 为什么会有混合持久化?

  1. 启动时加载 RDB 文件:Redis 启动时首先加载 RDB 文件,这样可以快速恢复大部分数据。

  2. 追加 AOF 日志:在加载 RDB 文件后,继续通过 AOF 文件记录的操作日志来恢复最近的数据变更。

混合持久化优点:

  • 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

混合持久化缺点:

  • AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;

  • 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

17. Redis 如何实现服务高可用?

  • 主从模式

  • 哨兵模式

  • 切片集群模式

18. 集群脑裂导致数据丢失怎么办?

由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

解决方案

  • min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。

  • min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。

19. Redis 使用的过期删除策略是什么?

Redis 使用的过期删除策略是「惰性删除 + 定期删除」这两种策略配和使用。

20. 什么是惰性删除策略?

不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。

惰性删除策略的优点:

  • 因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。

惰性删除策略的缺点:

  • 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。

21. 什么是定期删除策略?

每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期 key。

定期删除策略的优点:

  • 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。

定期删除策略的缺点:

  • 难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。

22. Redis 持久化时,对过期键会如何处理的?

RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。

  • RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。

  • RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:

    • 如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;

    • 如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。

AOF 文件分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段。

  • AOF 文件写入阶段:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。

  • AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中。

23. Redis 主从模式中,对过期键会如何处理?

当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。

从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

24. Redis 内存满了,会发生什么?

在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。

  • 在 64 位操作系统中,maxmemory 的默认值是 0

  • 在 32 位操作系统中,maxmemory 的默认值是 3G,因为 32 位的机器最大只支持 4GB 的内存

25. 如何避免缓存雪崩?

大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增。

  • 将缓存失效时间随机打散:我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。

  • 缓存预热:在系统启动或者缓存失效时,提前将一些热点数据加载到缓存中,避免在缓存失效的瞬间,大量请求直接打到数据库。

  • 设置缓存不过期:我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。

26. 如何避免缓存击穿?

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮

  • 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

27. 如何避免缓存穿透?

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增

  • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

  • 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。

28. 如何设计一个缓存策略,可以动态缓存热点数据呢?

通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。

  • 先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;

  • 同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;

  • 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。

在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。

29. Cache Aside(旁路缓存)策略

写策略的步骤:

  • 先更新数据库中的数据,再删除缓存中的数据。

读策略的步骤:

  • 如果读取的数据命中了缓存,则直接返回数据;

  • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

Cache Aside 策略适合读多写少的场景,不适合写多的场景

30. Redis 如何实现延迟队列?

使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。

31. Redis 的大 key 有什么影响

一般而言,下面这两种情况被称为大 key:

  • String 类型的值大于 10 KB;

  • Hash、List、Set、ZSet 类型的元素的个数超过 5000 个;

大 key 会带来以下四种影响:

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。

  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量。

  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。

  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多。

32. 如何删除大 key?

  1. 分批次删除

  2. 异步删除

33. Redis 管道有什么用?

使用管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。

但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞。

要注意的是,管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能。

34. Redis 事务支持回滚吗?

Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果

35. 为什么 Redis 不支持事务回滚?

  • 他认为 Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中。

  • 不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。

36. 如何用 Redis 实现分布式锁的?

Redis 的 SET 命令有个 NX 参数可以实现「key 不存在才插入」,所以可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;

  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

37. 基于 Redis 实现分布式锁有什么优缺点?

基于 Redis 实现分布式锁的优点:

  • 性能高效(这是选择缓存实现分布式锁最核心的出发点)。

  • 实现方便。选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。

  • 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。

基于 Redis 实现分布式锁的缺点:

  • 超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。

    • 我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。当主线程执行完成后,销毁续约锁即可。

  • Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

38. Redis 如何解决集群情况下分布式锁的可靠性?

为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。

39. Redis 内存淘汰策略

针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。

在设置了过期时间的数据中进行淘汰:

  • volatile-random:随机淘汰设置了过期时间的任意键值;

  • volatile-ttl:优先淘汰更早过期的键值。

  • volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;

  • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;

在所有数据范围内进行淘汰:

  • allkeys-random:随机淘汰任意键值;

  • allkeys-lru:淘汰整个键值中最久未使用的键值;

  • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。

40. Redis 是如何实现 LRU 算法的?

Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。

当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 X 个值(此值可配置),然后淘汰最久没有使用的那个。

Redis 实现的 LRU 算法的优点:

  • 不用为所有的数据维护一个大链表,节省了空间占用;

  • 不用在每次数据访问时都移动链表项,提升了缓存的性能;

但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。

41. Redis 是如何实现 LFU 算法的?

头部中记录 ldt 和 logc

  • ldt 用来记录 key 的访问时间戳;

  • logc 用来记录 key 的访问频次,它的值越小表示使用频率越低,越容易淘汰,每个新加入的 key 的 logc 初始值为 5。logc 会随时间推移而衰减。

logc 增加操作并不是单纯的 + 1,而是根据概率增加,如果 logc 越大的 key,它的 logc 就越难再增加。

所以,Redis 在访问 key 时,对于 logc 是这样变化的:

  • 先按照上次访问距离当前的时长,来对 logc 进行衰减;

  • 然后,再按照一定概率增加 logc 的值

42. String

内部实现

String 类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。

  • SDS 不仅可以保存文本数据,还可以保存二进制数据。

  • SDS 获取字符串长度的时间复杂度是 O(1)。

  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。

应用场景

  • 缓存对象

  • 常规计数

  • 分布式锁(NX)

  • 共享 Session 信息

43. List

内部实现

List 类型的底层数据结构是由双向链表或压缩列表实现的:

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;

  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

应用场景

消息队列

  1. 如何满足消息保序需求?

List 本身就是按先进先出的顺序对数据进行存取的。

  1. 如何处理重复的消息?

  • 每个消息都有一个全局的 ID。

  • 消费者要记录已经处理过的消息的 ID。

我们需要自行为每个消息生成一个全局唯一 ID,生成之后,我们在用 LPUSH 命令把消息插入 List 时,需要在消息中包含这个全局唯一 ID。

  1. 如何保证消息可靠性?

当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。

为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。

44. List 作为消息队列有什么缺陷?

List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。

要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现。

45. Hash

内部实现

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;

  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

应用场景

  • 缓存对象

  • 购物车

46. Set

Set 类型和 List 类型的区别如下:

  • List 可以存储重复元素,Set 只能存储非重复元素;

  • List 是按照元素的先后顺序存储元素的,而 Set 则是无序方式存储元素的。

内部实现

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries 配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;

  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。

应用场景

  • 点赞

  • 共同关注

  • 抽奖活动

46. Zset

有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。

内部实现

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;

  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

ZSet 使用跳表的主要原因是为了实现有序集合的快速访问和范围查询。跳表通过层级结构和索引节点,提供了快速查找和范围查询的能力,并且具有较小的空间占用。这使得 ZSet 能够高效地处理有序集合的各种操作。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

应用场景

  • 排行榜

  • 电话、姓名排序

47. BitMap

内部实现

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

应用场景

  • 签到统计

  • 判断用户登录态

  • 连续签到用户总数

47. HyperLogLog

HyperLogLog 提供不精确的去重计数,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数

应用场景

  • 百万级网页 UV 计数

48. GEO

内部实现

GEO 本身并没有设计新的底层数据结构,而是直接使用了 ZSet 集合类型。

GEO 类型使用 GeoHash 编码方法实现了经纬度到 ZSet 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 ZSet 元素的权重分数。

这样一来,我们就可以把经纬度保存到 ZSet 中,利用 ZSet 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。

应用场景

  • 滴滴打车

  • 美团附件商家召回

48. Stream

支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

  • 消息保序:XADD/XREAD

  • 阻塞读取:XREAD block

  • 重复消息处理:Stream 在使用 XADD 命令,会自动生成全局唯一 ID;

  • 消息可靠性:内部使用 PENDING List 自动保存消息,使用 XPENDING 命令查看消费组已经读取但是未被确认的消息,消费者使用 XACK 确认消息;

  • 支持消费组形式消费数据

49. Redis 基于 Stream 消息队列与专业的消息队列有哪些差距?

  • Redis 本身可能会丢数据;

  • 面对消息挤压,内存资源会紧张;

51. 如何避免大 Key

最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。

52. Redis 主从复制

主从复制共有三种模式:全量复制、基于长连接的命令传播、增量复制。

主从服务器第一次同步的时候,就是采用全量复制,此时主服务器会两个耗时的地方,分别是生成 RDB 文件和传输 RDB 文件。为了避免过多的从服务器和主服务器进行全量复制,可以把一部分从服务器升级为「经理角色」,让它也有自己的从服务器,通过这样可以分摊主服务器的压力。

第一次同步完成后,主从服务器都会维护着一个长连接,主服务器在接收到写操作命令后,就会通过这个连接将写命令传播给从服务器,来保证主从服务器的数据一致性。

如果遇到网络断开,增量复制就可以上场了,不过这个还跟 repl_backlog_size 这个大小有关系。

如果它配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。所以为了避免这种情况的频繁发生,要调大这个参数的值,以降低主从服务器断开后全量同步的概率。

53. 怎么判断 Redis 某个节点是否正常工作?

Redis 判断节点是否正常工作,基本都是通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。

Redis 主从节点发送的心态间隔是不一样的,而且作用也有一点区别:

  • Redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数 repl-ping-slave-period 控制发送频率。

  • Redis 从节点每隔 1 秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量,目的是为了:

    • 实时监测主从节点网络状态;

    • 上报自身复制偏移量, 检查复制数据是否丢失, 如果从节点数据丢失, 再从主节点的复制缓冲区中拉取丢失数据。

54. 主从复制架构中,过期 key 如何处理?

主节点处理了一个 key 或者通过淘汰算法淘汰了一个 key,这个时间主节点模拟一条 del 命令发送给从节点,从节点收到该命令后,就进行删除 key 的操作。

55. Redis 是同步复制还是异步复制?

Redis 主节点每次收到写命令之后,先写到内部的缓冲区,然后异步发送给从节点。

replication buffer 是主节点(Master)用来暂时存储发送给从节点(Slave)的数据的缓冲区。每个从节点都有自己独立的 replication buffer

56. 主从复制中两个 Buffer(replication buffer 、repl backlog buffer) 有什么区别?

replication buffer 、repl backlog buffer 区别如下:

  • 出现的阶段不一样:

    • repl backlog buffer 是在增量复制阶段出现,一个主节点只分配一个 repl backlog buffer;

    • replication buffer 是在全量复制阶段和增量复制阶段都会出现,主节点会给每个新连接的从节点,分配一个 replication buffer;

  • 这两个 Buffer 都有大小限制的,当缓冲区满了之后,发生的事情不一样:

    • 当 repl backlog buffer 满了,因为是环形结构,会直接覆盖起始位置数据;

    • 当 replication buffer 满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制。

57. 为什么会出现主从数据不一致?

之所以会出现主从数据不一致的现象,是因为主从节点间的命令复制是异步进行的,所以无法实现强一致性保证(主从数据时时刻刻保持一致)。

58. 如何如何应对主从数据不一致?

  • 尽量保证主从节点间的网络连接状况良好,避免主从节点在不同的机房。

  • 可以开发一个外部程序来监控主从节点间的复制进度。具体做法:

    • 我们可以开发一个监控程序,先用 INFO replication 命令查到主、从节点的进度,然后,我们用 master_repl_offset 减去 slave_repl_offset,这样就能得到从节点和主节点间的复制进度差值了。

    • 如果某个从节点的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从节点连接进行数据读取,这样就可以减少读到不一致数据的情况。

59. 主从切换如何减少数据丢失?

异步复制同步丢失

Redis 配置里有一个参数 min-slaves-max-lag,表示一旦所有的从节点数据复制和同步的延迟都超过了 min-slaves-max-lag 定义的值,那么主节点就会拒绝接收任何请求。

假设将 min-slaves-max-lag 配置为 10s 后,根据目前 master->slave 的复制速度,如果数据同步完成所需要时间超过 10s,就会认为 master 未来宕机后损失的数据会很多,master 就拒绝写入新请求。这样就能将 master 和 slave 数据差控制在 10s 内,即使 master 宕机也只是这未复制的 10s 数据。

那么对于客户端,当客户端发现 master 不可写后,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间(等 master 恢复正常)后重新写入 master 来保证数据不丢失。

集群产生脑裂数据丢失

  • min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。

  • min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果主从同步的延迟超过 x 秒,主节点会禁止写数据。

我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。

这两个配置项组合后的要求是,主节点连接的从节点中至少有 N 个从节点,「并且」主节点进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主节点就不会再接收客户端的写请求了。

60. 主从如何做到故障自动切换?

主节点挂了 ,从节点是无法自动升级为主节点的,这个过程需要人工处理,在此期间 Redis 无法对外提供写操作。

可以使用哨兵在发现主节点出现故障时,由哨兵自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性。

61. 为什么要有哨兵

Redis 在 2.8 版本以后提供的哨兵(Sentinel)机制,它的作用是实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。

哨兵一般是以集群的方式部署,至少需要 3 个哨兵节点,哨兵集群主要负责三件事情:监控、选主、通知。

哨兵节点通过 Redis 的发布者/订阅者机制,哨兵之间可以相互感知,相互连接,然后组成哨兵集群,同时哨兵又通过 INFO 命令,在主节点里获得了所有从节点连接信息,于是就能和从节点建立连接,并进行监控了。

  1. 第一轮投票:判断主节点下线

当哨兵集群中的某个哨兵判定主节点下线(主观下线)后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。

当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。

  1. 第二轮投票:选出哨兵 leader

某个哨兵判定主节点客观下线后,该哨兵就会发起投票,告诉其他哨兵,它想成为 leader,想成为 leader 的哨兵节点,要满足两个条件:

  • 第一,拿到半数以上的赞成票;

  • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

  1. 由哨兵 leader 进行主从故障转移

选举出了哨兵 leader 后,就可以进行主从故障转移的过程了。该操作包含以下四个步骤:

  • 第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点,选择的规则:

    • 过滤掉已经离线的从节点;

    • 过滤掉历史网络连接状态不好的从节点;

    • 将剩下的从节点,进行三轮考察:优先级、复制进度、ID 号。在每一轮考察过程中,如果找到了一个胜出的从节点,就将其作为新主节点。

  • 第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;

  • 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;

  • 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;

62. 数据库和缓存如何保证一致性?

旁路一致性保证。

针对「先删除缓存,再更新数据库」方案在「读 + 写」并发请求而造成缓存不一致的解决办法是「延迟双删」。

63. 如何保证两个操作都能执行成功?

我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。

  • 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。

  • 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

64. Redis 使用的协议

Redis 底层使用的通信协议是 RESP(Redis Serialization Protocol 的缩写),此协议只适用于 Redis 客户端 - 服务端 之间的通信, Redis 集群中节点间通信使用的是 Gossip 协议。

67. Redis 集群有多少个槽

Redis 集群默认情况下有 16384 个槽。这是因为 Redis 使用哈希槽(hash slots)来分片数据,并将数据分布在多个节点上。每个槽可以保存一个键值对,因此 Redis 集群最多可以保存 16384 个键值对。

68. Redis 如何解决 hash 结构的冲突

Redis 在哈希结构中使用了链地址法来解决冲突。具体实现中,Redis 使用了链表和跳表(Skip List)这两种数据结构来存储冲突的键。当链表过长时,Redis 会将链表转换为跳表,以提高查找效率。

69. Redis 保证 incr 命令原子性的原理是什么?

因为 Redis 是单线程的。

71. 用 Redis ZSet 实现排行榜先用分数再用时间排序怎么实现?

如果是用 41bit 表示时间戳,22bit 表示积分的话,那么 score 的组成就是这样的: 0(最高位不用)|0000000 00000000 0000000(22bit 表示积分)|0 00000000 00000000 00000000 00000000 00000000(41bit 表示时间戳)

因为排序首先按积分排再按时间排,所以积分在高位,时间戳在低位,这样不管时间戳的值是多少,积分越大,64bit 表示的数值就越大。

但我们需要的是按时间升序排,也就是最先达到 xx 积分的用户排在最前面,所以我们不能单纯的使用 41bit 存储时间戳,而应该是存储一个随时间流逝而变小的数值。

由于排行榜都会有一个周期,如周榜是一周,月榜是一个月,所以我们使用 41bit 存储的是一个周期的结束时间 yyy-MM-dd 23:59:59 对应的时间戳与用户积分更新时间的时间戳的差值,这个值会随着时间的推移而变小,而且不会出现负数的情况,刚好能够达到目的。

72. 判断 Key 是否存在

要判断 Redis 中的 Key 是否存在,可以使用 EXISTS 命令。EXISTS 命令用于检查给定的 Key 是否存在于 Redis 中。它的基本语法如下:

EXISTS KEY_NAME

如果 Key 存在,命令返回整数 1;如果 Key 不存在,命令返回整数 0。

73. Redis 的扩容方式

  1. 水平扩容 (分区) :通过增加更多的 Redis 服务器来分摊数据和负载。数据户端实现将被分散 (分片) 存储到多个 Redis 实例中,每个实例只存储整个数据的一部分。这种方式需要在客分片逻辑,Redis Cluster 提供了自动分片和高可用性的解决方案。

  2. 垂直扩容:通过增加单个 Redis 服务器的硬件资源 (如 RAM、CPU 等) 来提升其性能和容量。这种方式的缺点是硬件资源是有上限的,无法无限扩容。

74. Redis 如何实现乐观锁

通过 WATCH 命令配合 MULTIEXEC 来模拟乐观锁的行为。核心的想法是:我们“监视”一个变量,然后在执行事务前检查监视的变量是否已经发生改变。如果变量发生了改变,我们就放弃执行事务。

  1. 监控键:使用 WATCH 命令监控需要保护的键。

  2. 开启事务:使用 MULTI 命令开启事务。

  3. 执行操作:在事务内执行一系列命令。

  4. 提交事务:使用 EXEC 命令提交事务。如果监控的键在此期间被修改,事务将失败。

75. Redis 渐进式 Rehash

重新哈希/扩容操作会占用大量的 CPU 和内存资源,如果一次性完成该操作,可能会导致 Redis 在一段时间内阻塞,无法对外提供服务。

在渐进式 rehash 中,Redis 会同时维持新旧两个哈希表,新的哈希表是旧的哈希表大小的两倍。Redis 在对哈希表执行任何操作时,会顺带将旧哈希表中的一部分键值对移动到新哈希表中。具体移动的数量由服务器的配置决定。

假设服务器配置了每次把 100 个键值对从旧哈希表移动到新哈希表,Redis 每次执行一个哈希表操作,都会顺带完成这个移动操作;当旧哈希表的所有键值对都移动到新哈希表后,Redis 就释放旧哈希表的内存空间,渐进式 rehash 操作完成。

这种渐进方式的好处是将高负载的操作分摊到了一个时间段内,避免了 Redis 在进行哈希表扩容的时候服务暂停,提高了 Redis 的高可用性。

76. 怎么用 Bitmap 统计用户登录情况

使用 Bitmap 统计用户一年登录情况的基本思路是利用 Redis 的 Bitmap 数据结构,将每个用户的登录状态映射到一个特定的位上。以下是具体的步骤和实现方法:

基本概念

Bitmap 是一种高效的数据结构,能够以极小的内存占用来表示大量的二值状态(如登录与未登录)。在 Redis 中,每个用户的登录状态可以用一个比特位(bit)来表示,1 表示已登录,0 表示未登录。

实现步骤

  1. 定义 Key: 为每个用户的登录状态定义一个唯一的 Key,通常可以采用以下格式:

   login_status:{user_id}

例如,用户 ID 为 1001 的用户,其 Key 可以是 login_status:1001

  1. 设置登录状态: 每当用户登录时,使用 SETBIT 命令将对应的位设置为 1。假设用户在一年中的第 n 天登录,可以这样设置:

   SETBIT login_status:1001 n 1

这里,n 从 0 到 364 表示一年中的每一天。

  1. 检查登录状态: 要检查某个特定日期用户是否登录,可以使用 GETBIT 命令:

   GETBIT login_status:1001 n

如果返回 1,则表示该用户在第 n 天登录。

  1. 统计登录次数: 使用 BITCOUNT 命令可以统计用户在一年中登录的总次数:

   BITCOUNT login_status:1001

这将返回该用户在一年内登录的总天数。

77. Redis 分布式锁方案

七种方案!探讨Redis分布式锁的正确使用姿势 分布式解锁为什么需要保证原子性

78. Redis 热点数据解决方案

  • 本地缓存,将热 Key 提前加载到本地内存中

  • 热点 Key 限流,读命令通过添加从节点解决,写命令添加限流器

MongoDB

1. MongoDB 的优势有哪些

  • 面向文档的存储:以 JSON 格式的文档保存数据。

  • 任何属性都可以建立索引。

  • 复制以及高可扩展性。

  • 自动分片。

  • 丰富的查询功能。

  • 快速的即时更新。

2. 为什么在 MongoDB 中使用 "Object ID" 数据类型

"ObjectID" 数据类型用于存储文档 id

3. MongoDB 中的“Namespace”是什么?

MongoDB 在集合中存储 BSON(二进制交换和结构对象表示法) 对象。集合名称和数据库名称的连接称为命名空间。

4. MongoDB 中的分片是什么?

跨多台机器存储数据记录的过程称为分片。它是一种 MongoDB 方法,以满足数据增长的需求。它是数据库或搜索引擎中的数据的水平分区。每个分区都称为切分或数据库切分。

对象存储

1. 何为对象存储?

对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。

2. MinIO 基础概念

  • Object:存储到 MinIO 的基本对象,如文件、字节流,Anything…

  • Bucket:用来存储 Object 的逻辑空间。每个 Bucket 之间的数据是相互隔离的。对于客户端而言,就相当于一个存放文件的顶层文件夹。

  • Drive:即存储数据的磁盘,在 MinIO 启动时,以参数的方式传入。Minio 中所有的对象数据都会存储在 Drive 里。

  • Set:即一组 Drive 的集合,分布式部署根据集群规模自动划分一个或多个 Set ,每个 Set 中的 Drive 分布在不同位置。一个对象存储在一个 Set 上。(For example: {1…64} is divided into 4 sets each of size 16.)

一个对象存储在一个 Set 上

一个集群划分为多个 Set

一个 Set 包含的 Drive 数量是固定的,默认由系统根据集群规模自动计算得出

一个 Set 中的 Drive 尽可能分布在不同的节点上

3. MinIO 的数据高可靠

Minio 使用了 Erasure Code 纠删码和 Bit Rot Protection 数据腐化保护这两个特性,所以 MinIO 的数据可靠性做的高。

Minio 纠删码可以在丢失一半的盘的情况下,仍可以保证数据安全。 而且 Minio 纠删码是作用在对象级别,可以一次恢复一个对象

4. SeaweedFS 特点

SeaweedFS 最初作为一个对象存储来有效地处理小文件。中央主服务器(master)只管理文件卷(volume),而不是管理中央主服务器中的所有文件元数据,它允许这些卷服务器管理文件及其元数据。这减轻了中央主服务器的并发压力,并将文件元数据传播到卷服务器,允许更快的文件访问 (只需一个磁盘读取操作)。每个文件的元数据只有 40 字节的磁盘存储开销。使用 O(1) 磁盘读取。

5. SeaweedFS Master 原理

Master Server 集群之间的副本同步是基于 Raft 协议。

Master Server 保存整个 Volume Server 的拓扑信息,结构: Topology -> Data Center-> Rack -> Data Node -> Volume。可以通过 Data Center-> Rack -> Data Node 查找一个 Data Node 上的所有 Volume 信息。

另外,也需要根据 Volume 去查找它所在的所有节点,所以 Leader Master 还维护着另一个结构: Topology -> Collection -> VolumeLayout-> Data Node List 其中,Collection 对 VolumeLayout 的组织是按备份方式分类区分,VolumeLayout 保存每个 volume id 到其具体位置 Data Node List 的映射,同时保存了 Volume 的可读写性质。

Leader Master 跟各个 Volume Server 通过心跳保持连接, Volume Server 通过心跳将本地的卷信息 (增、删、过期等) 上报给 Leader Master。

然后 Leader Master 再将 Volume 的位置信息通过 gRPC 同步 (KeepConnected) 给其余 Master。所以, 当查询一个 Volume 的具体位置时,Leader Master 直接从本地的 Topology 中读取, 非 Leader Master 则从本地 Master Client 的 vidMap 中获取数据。

当要上传文件时,Leader Master 还负责分配一个全局唯一且递增的 id 作为 file id。

6. SeaweedFS Volume 原理

volume server 是文件实际存储的位置, 对文件的组织是按如下格式进行的:Store -> DiskLocations -> Volumes -> Needles

其中 DiskLocations 对应不同的目录, 每个目录中有很多 Volumes, 每个 Volume 实际是一个硬盘文件 .dat,其中分段保存一批上传的业务文件, 为了快速定位一个业务文件在 .dat 中的位置, 每个 Volume 对应个 .idx 文件, 用于保存所有业务文件在 .dat 文件中的偏移量和文件大小。为加快查询索引速度, Volume Server 启动时会将 .idx 读到 LevelDb 中,虽然一个业务文件在多个 Volume Server 作为主备, 但各 Volume Server 是对等的,平等接收外部请求, 当收到上传业务文件后, 会将文件传到其它机器。

ElasticSearch

1. Elasticsearch 的搜索过程是怎样的?

Elasticsearch 的搜索过程可以分为两个主要阶段:查询阶段(Query)获取阶段(Fetch)

  • 查询阶段

    1. 当接收到搜索请求时,Elasticsearch 会确定该请求命中的分片(主分片或副本分片)。

    2. 每个分片在本地执行查询,并将结果存储在一个有序的优先队列中。

    3. 查询结果被发送到协调节点,协调节点会整合所有分片的结果并生成一个全局排序列表。

  • 获取阶段

    1. 协调节点根据全局排序列表,向各个分片请求具体的文档数据。

    2. 最终,路由节点将这些文档返回给客户端。

2. 为什么使用倒排索引?

倒排索引是一种高效的文本检索结构,它将文档中的单词映射到包含该单词的文档列表。相比于正排索引(按文档存储单词),倒排索引在搜索时能显著提高查询速度,因为它可以快速定位到包含特定关键词的文档,减少了需要扫描的文档数量。这种结构特别适合于全文搜索场景。

3. Elasticsearch 如何处理大数据量的聚合?

Elasticsearch 使用 基数聚合(Cardinality Aggregation) 来处理大数据量的聚合。基数聚合基于 HyperLogLog(HLL)算法,能够高效地计算字段的唯一值数量。它通过对输入数据进行哈希处理,并根据哈希结果中的位数进行概率估算,从而得到基数。该方法的优点是可以配置精度,以控制内存使用,适用于数十亿的唯一值。

4. Elasticsearch 的集群架构是怎样的?

Elasticsearch 的集群架构由多个节点组成,每个节点可以扮演不同的角色,包括:

  • 主节点:负责集群的管理和状态维护。

  • 数据节点:存储数据和执行数据相关的操作,如索引和搜索。

  • 协调节点:负责接收客户端请求,并将请求分发到相应的数据节点,最终将结果返回给客户端。

这种架构设计使得 Elasticsearch 能够扩展并处理大规模的数据集。

5. Elasticsearch 中的分片是什么?

分片是 Elasticsearch 中用于存储和管理数据的基本单位。每个索引可以被划分为多个分片,每个分片都是一个独立的 Lucene 索引。分片的目的是为了提高数据的可扩展性和查询性能。通过将数据分散到多个分片上,Elasticsearch 能够并行处理查询和索引操作,从而提升整体性能。分片可以是主分片或副本分片,副本分片用于提供冗余和提高查询性能。

Last updated