文章目录
- MySQL
-
- 基础概念
-
- 1、数据库三大范式是什么?
- 2、MySQL执行的流程是怎样的?
- 3、B / B+ 树?
- 4、 MySQL一行记录是怎么存储的?
- 索引
-
- 5、索引的分类
- 6、联合索引的最左匹配原则
- 6.1、联合索引的索引下推
- 7、联合索引的范围查询
- 8、什么时候需要 / 不需要创建索引?
- 9、有什么优化索引的方法?
- 10、哪些情况下导致索引失效?
- 11、哪种count性能最好?
- 事务
-
- 12、事务的四大特性?
- 13、InnoDB引擎通过什么技术保证事务的特性的呢?
- 14、事务并发会引发什么问题?
- 15、事务的隔离级别有哪些?
- 16、介绍下MySQL的事务日志。
- 17、什么是MVCC?
- 18、事务的隔离级别都是怎么实现的?
- 19、如何解决幻读?
- 锁
-
- 20、MySQL都有哪些锁?
- 21、MySQL怎么加行级锁的?
- 22、MySQL什么情况下会导致死锁?怎么解决?
- 日志
-
- 23、undo log、redo log、bin log各自的作用是什么?有什么不同?
-
- undo log 与redo log
- redo log 与 bin log
- 24、为什么需要缓存池buffer pool?
- 25、为什么需要两阶段提交,提交过程是怎样的,有什么问题?
- 26、MySQL读写分离,主从怎么同步,同步延时问题怎么解决?
- SQL优化
-
-
- 26、如何定位SQL以及优化SQL语句的性能问题?
- 27、大表数据查询怎么优化?
- Redis
-
- 基本概念
-
- 数据结构
-
- 2、Redis包含哪些数据类型?使用场景是什么?
- 3、五种场景的Redis数据类型底层都是怎么实现的?
- Redis线程网络模型
-
- 4、Redis是单线程的吗?
- 5、Redis网络IO处理模式是怎样的?
- Redis持久化
-
- 6、Redis如何实现数据不丢失?
- 7、RDB快照如何实现?
- 8、AOF日志如何实现?
- 9、混合持久化如何实现?
- Redis缓存设计
-
- 10、如何避免缓存穿透、缓存雪崩、缓存击穿、?
-
- 11、数据库与缓存如何保持一致性?
- 12、Redis的过期删除策略是什么?
- 13、Redis的内存淘汰策略有哪些?
- Redis主从、切片集群
-
- 14、Redis主从复制原理,有什么缺陷?
- 15、Redis主从模式下的哨兵机制是怎么实现的?
- 16、Redis的分片集群
- Redis实战
MySQL
基础概念
1、数据库三大范式是什么?
- 第一范式:强调列的原子性,即数据库表的每一列是不可分割的原子数据项。
- 第二范式:实体的属性完全依赖于主关键字。
- 第三范式:任何非主属性不依赖于其他非主属性。
2、MySQL执行的流程是怎样的?
- 通过TCP连接到MySQL连接器。客户端通过TCP连接发送连接请求到MySQL连接器,连接器会对该请求进行权限验证及连接资源分配。
- 查询缓存。(MySQL先会在query cache中查找缓存数据,缓存数据以 key - value,保存,key为SQL,value,SQL查询结果。如果缓存命中,MySQL不会进行解析查询语句,而是直接返回value给客户端)。如果没有命中缓存,就要往下继续执行,等待执行完毕后,查询结果就会被存入缓存中。由于MySQL的缓存命中率比较低,在MySQL8.0版本开始,直接将查询缓存这个阶段删掉了。
- 解析SQL。MySQL解析器对SQL语句进行词法分析,识别出关键字,构建出SQL语法树,随后进行语法分析,判断SQL是否合法。
- 执行SQL。SQL执行流程包含
- prepare,预处理阶段。主要完成检测查询的表或者字段是否存在,完成select * 中的符号替换为表中所有的列。
- optimize,优化阶段。优化器基于查询成本的考虑,会对SQL查询语句的执行方案进行优化。
- execute,执行阶段。根据优化器生成的执行方案,从存储引擎读取记录,返回给客户端。
3、B / B+ 树?
因为数据库读取数据为磁盘中访问,较为耗时,为了提供数据访问效率,需要尽可能地减少数据访问的层级。
- B - Tree(多路平衡查找树)
一颗最大度数为5(5阶)的B树,每个节点最多存储4个key(5个数据范围,由4个 数据值构成,因此B树每个节点的数据数目比指针数(分支)少1),5个指针(5个分支)。
- B+树:树的变种,所有的元素都会出现在叶子节点上,非叶子节点数据只起到索引的作用,且所有的叶子节点会构成一个单向链表。
- MySQL的B+树对经典B+树做了进一步优化,在原B+树基础上,增加了一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的B+树,提供区间访问性能。
4、 MySQL一行记录是怎么存储的?
以MySQL默认的存储引擎InnoDB为例:
我们每创建一个database,都会在/var/lib/mysql/
目录里边创建一个以database为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。目录中包含:
- db.opt:存储当前数据库默认字符集和字符校验规则。
- t_order.frm:存放表结构定义信息。
- t_order.ibd:存放表数据信息,从 MySQL 5.6.6 版本开始MySQL 中每一张表的数据都存放在一个独立的 .ibd 文件,称为独占表空间文件。
表空间由段(segment)、区(extent)、页(page)、行(row)组成。
- 行 - row:每条记录按行存放
- 页 - page:记录按行存放,但是数据库以页为单位进行读取,默认每个页大小为16KB,即最多保证16KB的连续存储空间。
- 区(extent):InnoDB存储引擎采用B+树组织数据,B+树中通过双向链表链接每一行的数据,如果以页为单位来分配存储空间,则链表中相邻的两个页之间的物理位置并不是连续的,导致磁盘查询时产生大量随机I/O,造成性能低下。为了解决随机I/O的问题,让链表中相邻的页的物理位置也相邻,这样就可以使用顺序 I/O 了。具体而言,在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。
- 段(segment):表空间由各个段组成,一般分为:
- 索引段:存放B+树的非叶子节点的区的集合
- 数据段:存放B+树的叶子节点的区的集合
- 回滚段:存放回滚数据的区的集合。
MySQL中每一行的数据,常用Cmpact行格式存储:
一条完整的记录分为:
- 额外信息
- 1、变长字段长度列表:只出现在数据表有变长字段的情况,用于存放变长字段数据的实际长度。
- 2、NULL值列表 :只出现在存在允许为null的字段的表,NULL值列表采用二进制的bit位表示数据是否为空,位的值为1,代表为null,为0,代表为非空。必须用整个字节(8个位)表示,不足在高位补0。
- 3、记录头信息,包含:
- delete_mask:标识此条数据是否被删除。
- next_record:下一条记录位置。
- record_type:记录类型,0表示普通记录,1表示B+树非叶子节点,2表示最小记录,3表示最大记录。
- 真实数据
- 1、隐藏字段
- row_id:如果没有设置主键,则生成row_id为主键。
- trx_id:事务id
- roll_pointer:回滚指针,记录上一个版本的指针。
- 2、数据字段:普通列字段。
索引
5、索引的分类
-
按「数据结构」
分类:B+tree索引、Hash索引、Full-text索引。
-
按「物理存储」
分类:聚簇索引(主键索引)、二级索引(辅助索引)。
- 主键(聚簇)索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
- 二级(辅助)索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。
-
按「字段特性」
分类:主键索引、唯一索引、普通索引、前缀索引。
- 主键索引:建立在主键字段上,一个表最多一个,不允许空值,通常在创建表时一块创建。
- 唯一索引:建立在UNIQUE字段,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
- 普通索引:建立在普通字段.
- 前缀索引:对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。
-
按「字段个数」分类:单列索引、联合索引。
- 通过将多个字段组合成一个索引,该索引就被称为联合索引。
6、联合索引的最左匹配原则
比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name),创建联合索引的方式如下:CREATE INDEX index_product_no_name ON product(product_no, name);
联合索引(product_no, name) 的 B+Tree的非叶子节点用两个字段的值作为 B+Tree 的 key 值,查询数据时,先按 product_no 字段比较,在 product_no 相同的情况下再按 name 字段比较。因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循**「最左匹配原则」**,联合索引会失效,这样就无法利用到索引快速查询的特性了。
比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引(因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要):
where a=1;
where a=1 and b=2 and c=3;
where a=1 and b=2;
但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
where b=2;
where c=3;
where b=2 and c=3;
6.1、联合索引的索引下推
在使用联合索引时,如果出现大量需要回表的情况,索引下推可以帮助进行优化,将过滤条件放在引擎层进行处理
比如:使用一张用户表tuser,表里创建联合索引(name, age)。
如果现在有一个需求:检索出表中名字第一个字是张,而且年龄是10岁的所有用户。那么,SQL语句是这么写的:
select * from tuser where name like '张%' and age=10;
使用索引下推,会将找到的以张开头的name字段再根据age =10,进行过滤筛选, 拿到所有符合的id,再回表拿到所有的结果。
不使用索引下推,则会根据第一个条件搜索到后,返回给server层,server层再根据第二个条件筛除。
索引下推的下推其实就是指将部分上层(服务层)负责的事情,交给了下层(引擎层)去处理。
7、联合索引的范围查询
Q1: select * from t_table where a > 1 and b = 2,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?
Q1 这条查询语句只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引,因为在符合 a > 1 条件的二级索引记录的范围里,b 字段的值是无序的。
Q2: select * from t_table where a >= 1 and b = 2,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?
对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的,因此会用到b字段的联合索引。
Q3: SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?
Q4: SELECT * FROM t_user WHERE name like ‘j%’ and age = 22,联合索引(name, age)哪一个字段用到了联合索引的 B+Tree?
Q3、Q4是与Q2一样的。
联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
8、什么时候需要 / 不需要创建索引?
索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:增加存储开销,增加索引维护成本。
什么时候适用索引?
- 字段有唯一性限制的,比如商品编码;
- 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。
- 经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。
什么时候不需要创建索引?
- 字段中存在大量重复数据
- 表数据太少
- 经常更新的字段不用创建索引
9、有什么优化索引的方法?
- 前缀索引优化:可以减小索引字段大小,但也有局限性,比如
order by
无法使用前缀索引,无法把前缀索引用作覆盖索引。
- 通过建立联合索引,覆盖索引。减少回表查询。
- 主键最好自增,减少页分裂。
- 索引最好设置为非空,存在null值会导致优化器做索引选择时更加复杂。
- 防止索引失效。
10、哪些情况下导致索引失效?
- 使用 != 或者<>导致索引失效。
- 对索引使用左或者左右模糊匹配也就是
like %xx
或者like %xx%
这两种方式都会造成索引失效。因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。
- 对索引使用函数或者表达式计算。
- 对索引进行隐式类型转换
- 联合索引非最左匹配
- where字句中的or,如果or前边的条件是索引列,而在or后的条件列不是索引列,索引会失效。
11、哪种count性能最好?
count() 是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,count()函数作用是统计符合查询条件的记录中,函数指定的参数不为 NULL 的记录
有多少个。
1 这个表达式就是单纯数字,它永远都不是 NULL,所以count(1)就是在统计表中所有的记录数。因此有:
count(1) = count(*)
count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。
所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。
事务
12、事务的四大特性?
- 原子性:事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
- 一致性:事务完成时,必须使所有数据保持一致。
- 隔离性:数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性:事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
13、InnoDB引擎通过什么技术保证事务的特性的呢?
redo log 再执行日志,即数据改变时先写改变的日志。确保数据的持久性,即便发送错误也可根据redo log再次执行。
undo log 回滚日志,用于记录数据被修改前的信息。
- 原子性:通过undo log(回滚日志)来保证原子性,事务中途失败了,根据undo log回滚。
- 持久性:通过redo log(再执行日志)保证持久性,数据修改如果失败了,根据redo log,再次执行修改。
- 隔离性:通过MVCC(多版本并发控制)和锁机制,保证事务执行过程中不受并发影响。
- 一致性:通过原子性、持久性和隔离性来保证事务执行结果的一致性。
14、事务并发会引发什么问题?
- 脏读:一个事务读取到另外一个事务还没有提交的数据。
- 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称为不可重复读。
- 幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在了,好像出现了“幻影”。
15、事务的隔离级别有哪些?
当多个事务并发执行时可能会遇到「脏读、不可重复读、幻读」的现象,这三个现象的严重性排序如下:
- 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;
- 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;
- 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
- 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
针对不同的隔离级别,并发事务时可能发生的现象也会不同:
16、介绍下MySQL的事务日志。
-
redo log : redo log 不是随着事务的提交才写入的,而是在事务执行过程中,便开始写入redo中。防止发生故障的时间点,尚有脏页未写入磁盘。在重启MySQL服务时,会根据redo log进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。
-
undo log:undo log 用来回滚行记录到某个版本。事务未提交之前,Undo 保存了未提交之前的版本数据,undo log中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在MySQL innodb 存储引擎中用来实现多版本并发控制。
-
bin log。 MySQL的 binlog 是记录所有数据库表结构变更(例如 CREATE、ALTER TABLE)以及表数据修改(INSERT、UPDATE、DELETE)的二进制日志。binlog 不会记录 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改。MySQL binlog 以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。
17、什么是MVCC?
MVCC (多版本并发控制)的实现,是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
MVCC实现的原理基于MySQL表中的隐藏字段:row_id(隐藏主键id)、trx_id(事务id)和roll_ptr(回滚指针),回滚指针指向这条记录的上一个版本,记录在undo log中。根据事务id和回滚指针,就可以定位到undo log的版本链,读取相应版本的视图(ReadView)。
版本访问规则:
MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, …},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。在进行SELECT操作时,根据数据行快照的TRX_ID与TRX_ID_MIN和TRX_ID_MAX之间的关系,从而判断数据行快照是否可以使用:
- TRX_ID < TRX_ID_MIN,表示该数据行快照是在当前所有未提交事务之前进行更改的,因此可以使用。
- TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。
- TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:
- 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
- 可重复读:都不可以使用。
快照读与当前读
1、 快照读:MVCC中的select操作的是快照中的数据,不需要加锁。
2、当前读:MVCC中对数据进行修改的操作(增删改)需要进行加锁,从而读取最新的数据。
18、事务的隔离级别都是怎么实现的?
- 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
- 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
- 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同。
- 可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。
- 读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。
19、如何解决幻读?
MySQL InnoDB 引擎的可重复读隔离级别(默认隔离级),根据不同的查询方式,分别提出了避免幻读的方案:
-
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。在可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
-
针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
举例了两个发生幻读场景的例子。
第一个例子:
time1:事务A查询id为5的记录,发现查询不到。
tme2:事务B插入id为5的记录。
time3:事务B提交了事务。
time4:事务A插入id为5的记录时,发现已经有了记录,发生幻读。
第二个例子:
time1:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
time2:事务 B 往插入一个 id= 200 的记录并提交;
time3:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。
锁
20、MySQL都有哪些锁?
-
全局锁:全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。被锁期间,数据库是只读的。
-
表级锁:锁住整张表。
- 表锁:
- 表共享读锁:客户端1执行
lock table score read;
表score会变为只读状态,此时客户端1如果要增删改表score会报错,客户端2如果要增删改表score会进入等待状态,等待客户端1解锁后,客户端2的修改语句才会提交。
- 表独占写锁:客户端1执行:
lock tables score write;
客户端1会独占表score的读写,其他客户端的读写会被堵塞。
- 元数据锁(MDL):元数据锁的加锁过程是系统自动控制的,无需显式使用,在访问一张表的时候自动加上。
- 对一张表进行 CRUD 操作时,加的是 MDL 读锁(共享);
- 当对表结构进行变更操作时,加的是元数据锁中的写锁(排他)。
- 读锁与读锁之间不互斥,因此可以多线程操作同一张表。 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
- 意向锁:
意向锁的目的是为了快速判断表里是否有记录被加锁。
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables … read)和独占表锁(lock tables … write)发生冲突。
-
行级锁:行级锁每次操作对应的行数据,发生冲突概率最低,并发度最高,InnoDB的索引是基于索引组织的,行锁是通过对索引上的索引加锁来实现的,而不是对记录加的锁。
- 行锁(record lock,记录锁):对每条记录的加的锁,可分为:
- 共享锁(S,读):允许一个事务读取一行,阻止其他事务获取排他锁,读读共享,读写互斥。
- 排他锁(X,写):允许获取了排他锁的事务更新数据,阻止其他事务获取共享锁和排他锁,即读写互斥、写写互斥。
- 在执行增删改的时候,自动加排他锁。执行select时,默认不加任何锁,但可以通过语句手动加锁。
- 间隙锁(Gap锁):只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
* 临键锁(Next-Key Lock锁):是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
21、MySQL怎么加行级锁的?
行级锁加锁规则比较复杂,不同的场景,加锁的形式是不同的。
加锁的对象是索引,加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是左开右闭区间,而间隙锁是前开后开区间,临键锁next key,相当于在间隙锁的锁范围基础上,增加锁定了区间的右边界,即next-key,对应的记录锁
但是,next-key lock 在一些场景下会退化成记录锁或间隙锁。
那到底是什么场景呢?总结一句,在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁。
- 唯一索引等值查询。
- 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」。
- 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」。
- 唯一索引范围查询
- 针对「大于或者大于等于」的范围查询,因为存在等值查询的条件,那么如果等值查询的记录是存在于表中,那么该记录的索引中的 next-key 锁会退化成记录锁。
- 针对「小于或者小于等于」的范围查询,要看条件值的记录是否存在于表中:
- 当条件值的记录不在表中,那么不管是「小于」还是「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。
- 当条件值的记录在表中,如果是「小于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 next-key 锁;如果「小于等于」条件的范围查询,扫描到终止范围查询的记录时,该记录的索引 next-key 锁不会退化成间隙锁。其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。
- 非唯一索引等值查询:用非唯一索引进行等值查询的时候,因为存在两个索引,一个是主键索引,一个是非唯一索引(二级索引),所以在加锁时,同时会对这两个索引都加锁,但是对主键索引加锁的时候,只有满足查询条件的记录才会对它们的主键索引加锁。
- 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁。
- 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。
- 非唯一索引范围查询:非唯一索引范围查询,索引的 next-key lock 不会有退化为间隙锁和记录锁的情况,即非唯一索引范围查询都是加的临键锁。
- 没有加索引的查询:如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞。
因此,在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。
22、MySQL什么情况下会导致死锁?怎么解决?
time1阶段:事务A加间隙锁,范围(20, 30)
time2阶段:事务B加间隙锁,范围(20, 30)
间隙锁的意义只在于阻止区间被插入,因此是可以共存的。
一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享和排他的间隙锁是没有区别的,他们相互不冲突,且功能相同,即两个事务可以同时持有包含共同间隙的间隙锁。
time3阶段:事务A尝试给id为25的位置加插入意向锁,但是发现事务B在(20,30)间设置了间隙锁,加锁失败,阻塞,等待事务B释放间隙锁。
time4阶段:事务B尝试给id为26的位置加插入意向锁,但是发现事务A在(20,30)间设置了间隙锁,加锁失败,阻塞,等待事务A释放间隙锁。
事务A和事务B相互等待对方释放锁,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
日志
23、undo log、redo log、bin log各自的作用是什么?有什么不同?
- undo log 主要有两大作用:
- 记录事务执行前的状态,实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
- 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
- redo log主要有两大作用:
- 记录事务执行结束的状态,保障事务的持久性,事务提交后如果发生了崩溃,可以在重启后通过redo log恢复事务的执行。
- redo log 的写入方式使用了追加操作,redo log 并不是直接写入磁盘,而是先写入redo log buffer,后续再将redo log buffer中的redo log顺序写入到磁盘,刷盘时机包含(MySQL正常关闭】redo log buffer写入量大于容量一半,InnoDB后台线程每隔1秒,将redo log buffer持久化到磁盘,每次提交事务时,也可选择参数将redo log buffer落盘),将MySQL的写操作从磁盘的[随机写]变成了顺序写,提升了执行性能。
- bin log主要用于备份恢复和主从恢复。MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作。
undo log 与redo log
undo log记录的是事务提交前的数据状态,redo log记录的是事务提交之后的数据状态
redo log 与 bin log
1、适用对象不同:
binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
redo log 是 Innodb 存储引擎实现的日志;
2、写入方式不同:
binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
redo log 是循环写,日志空间大小是固定(redo log buff有固定大小),全部写满就从头开始,保存的是未被刷入磁盘的脏页日志。
3、用途不同:
binlog 用于备份恢复、主从复制;
redo log 用于掉电等故障恢复。
24、为什么需要缓存池buffer pool?
Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。
- 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
- 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存、自适应哈希索引、锁信息等等。
25、为什么需要两阶段提交,提交过程是怎样的,有什么问题?
事务提交后,redo log 和 binlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,即redo log与bin log只有其中一个成功了。redo log 会决定主库的数据状态,而bin log会决定从库的数据状态,redo log与bin log只有一个刷盘成功,就会导致主从不一致的情况发生。
为了解决这个问题,MySQL内部是分prepare和commit两个阶段完成事务X的提交。
- prepare 阶段:将 XID(事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘
- commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit。
在两阶段提交的机制下,可以通过比对bin log中有没有redo log中记录事务id,来决定是回滚事务还是提交事务,如果有就提交事务,如果没有就回滚事务。这样就可以保证 redo log 和 binlog 这两份日志的一致性了。
虽然两阶段提交解决了日志一致性问题,但它也有问题:
- 磁盘 I/O 次数高:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。
- 锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。
26、MySQL读写分离,主从怎么同步,同步延时问题怎么解决?
通过设置一个MySQL主库(负责数据写入和更新)操作和多个从库(负责数据查询),通过主从同步机制,将查询的业务需求分摊给从库,减轻主库的负担,从而提高数据库的使用性能。
主从同步的流程:
1、master在每个事务更新数据完成之前,将(增删改)操作记录的以追加的方式写入到binlog文件中。
2、slave开启一个I/O Thread,与master建立连接,主要工作就是读取master的bin log。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的就是将master的写入到转发日志 relay log。
3、slave的 SQL线程会读取转发日志 relay log,并顺序执行日志中的SQL事件,从而使得从库中的数据与主库保持一致。
上边的主从同步机制上,从库的数据与主库中最新的数据之间不可避免地存在延迟,短暂的延时是可以接受的,但一些情况下,这种延迟会非常严重:
- 1、查询业务需求为主,导致从库压力太大,SQL执行变慢。
- 2、主库执行了比较耗时的事务,传导到从库延迟更高。
针对上述原因,可以采用:
- 一主多从,分担从库压力。
- 做业务分类,实时性要求较高的重要业务直接走主库查询。
- 采用半同步机制,主库只需要等待至少一个从库接收到并写到 Relay Log 文件即可,主库不需要等待所有从库给主库返回 ACK。主库收到这个 ACK 以后,才能给客户端返回 “事务完成” 的确认。
- 采用并行复制,从库开启多个线程并行读取relay log中不同库的日志,然后并行重放到对于的库中。
SQL优化
26、如何定位SQL以及优化SQL语句的性能问题?
使用MySQL提供的SQL性能分析工具:
- 慢查询日志:记录了所有执行时间超过了指定参数的所有SQL语句的日志。通过语句
show variables like 'slow_query_log';
可以查看慢查询日志的开启状态。
- profile详情:使用profile详情可以让我们了解SQL时间都耗费到哪些去了。
- 使用explain执行计划,即在SQL语句前加
explain
和desc
即可分析该条sql语句的执行情况,是否使用索引的情况,索引中字节数,连接类型。
- 查看MySQL的
慢查询日志
,定位到SQL的id。
- 使用
profile
详情,可以查看查询的具体的时间花费在哪里。
- 使用
explain
或者desc
+ SQL语句,可以分析该SQL语句的执行计划,其中有两个字段是值得我们关注的:
- type:是否使用索引以及使用的索引类型,字段值效率从高到低依次是:
- const:对主键或者唯一索引进行等值查询
- eq_ref: 通常发生在关联查询中,关联的条件是表的主键或者唯一非空索引。
- ref:对于非聚簇索引的等值查询一般都为ref。
- ref_or_null:在ref的继承上还需要查到为null的结果,由于查询null值需要扫描整个索引树的行信息,所以会比ref慢。
- index_merge:会将多个结果合并为一个,统一回表查询。
- range:任意索引的范围查询,包括like、between、>、<等
- index:全表扫描,没有走索引,但是可以在索引树上拿到需要的结果。
- all:全表扫描,不能在索引树上拿到所有查询结果。
- extra:额外信息,
- using index(覆盖索引)
- using index condition(索引下推)
- using where(没有索引下推)
- using temporary(使用了临时表)
- using filesort(查询到后需要额外排序)。
- 查看表中数据行是否过大,考虑是否进行分库分表
- 查看数据库所在服务器占用,考虑升级服务器性能等。
SQL优化操作:
1、批量插入,主键顺序插入,避免多次连接数据库,使用load指令大批量插入数据。
2、主键优化:满足业务需求情况下,尽量降低主键长度、保证主键自增。
3、需要排序的业务场景,即使用order by的语句,通过建立合适的索引,联合索引遵循最左前缀法则,注意排序的顺序,默认为升序。
4、需要分组查询的语句,即使用group by的语句也可通过建立联合索引,加快查询速度。
5、limit优化(超大分页优化),MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
使用子查询的方式,先定位到limit的位置,然后再返回。
例如:SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
6、update语句一定要根据索引定位,避免全表扫描,临键锁升级为表锁。
27、大表数据查询怎么优化?
- 构建索引
- 通过redis缓存
- 主从赋值、读写分离
- 分库分表,核心思想是将数据分散存储,使得单一数据库/表的数据量变小来缓解单一数据库性能问题,从而提升数据库整体性能。
Redis
基本概念
1、为什么用Redis作为MySQL的缓存?
主要是因为Redis具备高性能和高并发两种特性。
- 高性能:从MySQL中访问数据,是从硬盘读取,而使用Redis是从内存中读取,效率更高。
- 高并发:单台Redis的QPS每秒处理请求的次数是MySQL的10倍,直接访问Redis能够承受的请求数是远远高于直接访问MySQL的。
数据结构
2、Redis包含哪些数据类型?使用场景是什么?
- String类型,应用场景包含:缓存对象、分布式锁、共享session信息。
- List类型,应用场景包含:消息队列。(但是有两个问题:1、是生产者需要自行实现全局唯一ID,2、是不能以消费形式消费数据)
- Hash类型:缓存对象、购物车等。
- Set类型:唯一性且需要聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
- ZSet类型:唯一性且有排序需求的场景,比如排行榜、电话和姓名排序。
- BitMap:二值状态统计的场景,比如签到、判断用户登录状态,连续签到用户总数等;
- HyperLogLog:海量数据基数统计的场景,比如百万级网页UV统计。
- GEO:存储地理位置信息的场景,比如滴滴叫车。
- Stream:消息队列,相比与基于List类型实现的消息队列,Stream可以自动生成全局唯一性消息ID ,并且支持以消费组的形式消费数据,每个订阅的消费组都会收到消息的发送。
3、五种场景的Redis数据类型底层都是怎么实现的?
-
string 底层是由SDS(简单动态字符串)实现的,它包含了字符串的长度len,以申请的空间数alloc,头类型flag和实际存放数据的字符数组buf,相比于C语言传统的字符数组,有如下优点:
- 可以在O(1)时间内获取字符串长度
- 支持动态扩容,并通过内存预分配机制,减少内存分配次数。
- 根据长度读取字符串内容,而非结束标志’/0’,因此是二进制安全 的。
-
list底层是由quicklist实现,quicklist中每个节点都是一个ZipList,采用ZipList,ZipList的空间利用率更高,采用多个小的ZipList连接,可以加快内存申请的效率,找到多个小的连续内存空间,要比找到一大块连续内存空间容易的多。
-
hash底层 默认由ZipList实现,高版本采用listpack,ZipList中相邻的两个entry分别保存field和value。但数据量较大时,会采用Dict实现。
-
Set底层基于Dict实现,filed存放Set的数据,而value存放null。当存放的数据都是整数类型时,采用整数集合IntSet实现。
-
ZSet有序表,基于Dict与skiplist实现。
Redis线程网络模型
4、Redis是单线程的吗?
Redis的核心功能,对于数据库键值的增删改查指令的解析、执行、结果发送是单线程执行的,因为Redis本身是对于内存的操作,执行效率很快,使用多线程要考虑线程安全,线程切换开销,反而影响其效率,但是对于一些非核心功能,如文件关闭、AOF刷盘、内存释放、网络IO请求处理用到了多线程以提高效率。
Redis的核心功能:接收客户端指令请求,解析请求,进行数据读写等操作,发送数据给客户端,这个过程是由一个线程(主线程)来完成的,因此可以说Redis的核心功能是单线程实现的。
但是Redis整体的实现不是单线程的。
- Redis在2.6版本会启动2个后台线程,分别处理关闭文件、AOF刷盘这两个任务。
- Redis在4.0版本之后,新增了一个新的后台线程,用来异步释放Redis内存,也就是lazyfree线程。
- Redis6.0版本以后,采用了多个I/O线程来处理网络请求。
之所以将关闭文件、AOF刷盘、释放内存这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果放在主线程完成,容易发生阻塞。
5、Redis网络IO处理模式是怎样的?
第一步:Redis初始化:
- 首先调用epoll_create()创建一个epoll对象,调用socket()创建一个服务端socket。
- 然后调用bind()绑定服务端端口号,调用listen()监听该socket。
- 调用epoll_ctl()将listen socket加入到epoll,同时注册连接事件处理函数。
第二步:事件循环函数:
初始化完成后,主线程进入到一个事件循环函数,主要会做以下事情:
- 首先,调用处理发送队列函数,查看是否有任务需要发送。如果有发送任务,通过write函数将客户端发送缓冲区里的数据发送处理,如果这一轮没有发送后,会注册写处理函数,等待epoll_wait发现后再次处理。
- 接着,调用epoll_wait函数,等待事件到来:
- 如果是连接事件,调用accept()获取已经连接的socket,调用epoll_ctl将已经连接的socket加入到epoll,注册读事件处理函数。
- 如果是读事件到来,则调用读事件处理函数,调用read()获取客户端发送的数据,解析命令,处理命令,将客户端对象添加到发送队列,将执行结果写到发送缓存区,等待发送。
- 如果是写事件到来,则会调用写事件处理函数,调用write()函数将客户端发送缓存区里的数据发送出去,如果这一轮数据,没有发送完,就会继续注册写事件处理函数,等待epoll_wait发现可写后再处理。
Redis持久化
6、Redis如何实现数据不丢失?
Redis的读写操作都是在内存中,所以Redis性能才会高,但是Redis宕机、重启,内存中的数据就会丢失,为了保证内存中的数据不丢失,Redis实现了数据持久化机制,将数据存储到磁盘,这样在Redis重启时就能够从磁盘中恢复原有的数据。
Redis共有以下三种数据持久化方式:
- RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘。
- AOF日志:每执行一条写操作命令,就把该命令以追加的方式写到一个文件里;
- 混合持久化方式,Redis4.0以后,集成了AOF和RDB各自的优点。
7、RDB快照如何实现?
Redis提供了两个命令来生成RDB文件,分别是save和bgsave。
- save命令在主线程执行生成RDB文件,会阻塞主线程,此期间,不可再执行Redis指令。
- bgsave命令,即后台保存,执行bgsave命令,会fork()创建子进程,复制父进程的页表,但是父子进程的页表指向的物理内存还是一个,此时如果执行读操作,父子进程是互不影响的,但是如果执行写操作则会为被修改的数据创建一份副本,然后bgsave子进程会把该副本写入到RDB文件。
极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍!!!。因此Redis在使用时应该预留内存。
8、AOF日志如何实现?
AOF概念:
AOF(append only file):Redis在执行一条写操作命令后,就会将该命令以追加的方式写入到一个文件里,然后Redis重启时,会读取文件记录的命令,逐一执行命令,来进行数据的恢复。
AOF触发重写:
AOF日志随着执行的写操作命令越来越多,文件大小越来越大,当大小超过所设定的阈值后,Redis会启用AOF重写机制,在没有使用重写机制前,假设前后执行了「set name xiaolin」和「set name xiaolincoding」这两个命令的话,就会将这两个命令记录到 AOF 文件。在使用重写机制后,就会读取 name 最新的 value(键值对) ,然后用一条 「set name xiaolincoding」命令记录到新的 AOF 文件,之前的第一个命令就没有必要记录了,因为它属于「历史」命令,没有作用了。
AOF重写过程:
Redis重写AOF过程是由后台子进程bgwriteaof
来完成的。子进程进程AOF重写期间,主进程可以继续出来命令请求,避免阻塞主进程。重写子进程对内存是只读的,把内存数据的键值对转换为一条命令,再将命令记录到重写日志(新的AOF文件)。
如果在重写AOF日志中,主进程修改了已经存在的数据,会发生写时复制。Redis设置了一个AOF重写缓冲区,该缓冲区在bgwriteaof
子进程之后开始使用,在重写AOF期间,当Redis执行完一个写命令之后,它会同时将这个写命令写入到AOF缓冲区和AOF重写缓冲区。当子进程完成AOF重写工作后,会向主进程发生一个信号。主进程收到信号后,会将AOF重写缓冲区中的所有内容追加到新的AOF文件中,使得新旧两个AOF文件所保存的数据库状态一致,新的AOF文件进行改名,覆盖现有的AOF文件。
9、混合持久化如何实现?
- RDB的优点是只保存内存中记录的数据,恢复快,文件小,但是有数据丢失风险,安全性不足。RDB的频率也不好把握,频率太低,丢失的数据越大,频率太高则会影响性能。
- AOF的优点是丢失数据少,较为安全,但是AOF文件通常比较大,而是数据恢复速度慢。
- Redis4.0提出了混合使用AOF和RDB,集成各自的优点。当开启了混合持久化时,在AOF重写日志时,fork出来的重写子进程会先将与主线程共享的内存数据以RDB方式写入到AOF文件,然后主线程处理操作的命令会被记录在重写缓冲区,重写缓冲区里的增量命令会以AOF方式写入到AOF文件,写入完成后,通知主进程将新的含有RDB格式和AOF格式的AOF文件替换旧的AOF文件。
也就是说,混合持久化时,AOF文件的前半部分是RDB格式的全量数据,后半部分是以AOF格式的增量数据
这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。
Redis缓存设计
10、如何避免缓存穿透、缓存雪崩、缓存击穿、?
缓存穿透
缓存穿透是指客户端请求的数据在缓冲中和数据库中都不存在
,这样缓存永远不会生效,这些请求都会打到数据库中。
缓存穿透的发生一般有两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
常见的方案有三种:
- 1、限制非法请求,在API入口处,判断请求参数是否合理,判断是否为恶意请求。
- 2、缓存空值,并设置一个较短的TTL。后序请求就会命中缓存,但同时为了避免大量这样的缓存,通过设置较短的TTL,自动清除他们。
- 3、使用布隆过滤器,判断数据是否存在。在写入数据到数据库时,使用布隆过滤器做个标记,用户请求过来时,业务判断缓存失效后,通过查询布隆过滤器快速判断数据是否存在。如果不存在,就不用查询数据库。
缓存雪崩
大量缓存数据在同一时间过期(失效)时
,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
对于缓存雪崩问题:我们可以采用以下方案解决:
- 1、将缓冲失效时间TTL随机打乱,降低缓存集体失效的概率。
- 2、利用Redis集群,提高服务的可用性
- 3、给缓存业务添加降级限流策略
- 4、给业务添加多级缓存。
缓存击穿
缓存击穿问题也称为热点key
问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了
(热点key失效),无数的请求访问会在瞬间给数据库带来巨大的冲击,并且多个线程都会尝试进行缓存重建,导致服务器性能紧张。但实际上只需要一个线程重建缓存就足够了
缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。
常见的解决方法如下:
- 1、不给热点key设置TTL, 由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
- 2、给重建缓存的过程加锁,其他没有锁的线程阻塞,保证同一时间只有一个业务线程重建缓存。但是互斥锁导致多个线程等待,依然会造成性能影响。
- 3、基于逻辑过期策略,给缓存设置逻辑过期的字段
expire
,第一个发现缓存过期的线程拿到锁,开启一个新的线程,执行缓存重建过期,而自己直接返回过期的缓存。其余没有拿到锁的线程在缓存重建成功之前,都直接返回过期缓存,不阻塞。这种方式是弱一致性的,但是性能会比较好。
11、数据库与缓存如何保持一致性?
常见的缓存更新策略包含:
- Cache Aside (旁路缓存)策略
- Read/Write Through(读穿/写穿)策略
- Write Back(写回)策略
实际开发中Redis和MySQL的更新策略都是Cache Aside(旁路缓存)策略。
Cache Aside(旁路缓存)策略是最常用的,应用程序直接与数据库、缓存交互,并负责对缓存的维护,该策略又可以细分为读策略和写策略。
写数据时,先更新数据库,再更新缓存,并发时可能会有问题:
为了避免上边这种缓存与数据的不一致,写数据时,更新数据库后,执行了删除缓存。
那么为什么是先更新数据库,再删除缓存呢?
因为对缓存的操作要比对数据库的操作更快,先更新缓存再更新数据库,数据不一致的时间差更大,造成请求从数据库和缓存中结果不一致的概率更高!而先更新数据库,再删除缓存,删除缓存的时间更快,中间的时间差更小。
12、Redis的过期删除策略是什么?
每当我们对一个key设置了TTL时,Redis构造该key的数据时,会额外存储一个过期字典(expire dict),当我们查询一个key,Redis首先检查key是否存在于过期字典中。如果不存在,则正确读取,如果存在,则会检查key的过期时间与当前时间进行比对,检测key是否过期。
Redis采用的过期删除策略是惰性删除 + 定期删除
惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
为此Redis会使用定期删除策略,配合上边的惰性删除策略。
定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
定期删除有两种工作模式:
- 在Redis的initServer()函数中,会按照server.hz的频率来执行过期key的清理,模式为SLOW,默认每秒10次。
- Redis的每个事件循环前回调用beforeSleep()函数,执行过期key的清理,模式为FAST。
- SLOW执行低频但清理更加彻底。FAST模型执行高频,但执行时机较短。
13、Redis的内存淘汰策略有哪些?
内存淘汰策略执行的时机:
Redis会在处理客户端命令的方法processCommand()中,如果发现OOM,会触发内存淘汰机制。
Redis支持8种不同的策略进行key的淘汰,默认的淘汰策略是:
1、noeviction,不淘汰任何key,但是内存满的时候不允许写入新数据。
其他常见内存淘汰策略,通常分为对全体key处理和对设置了TTL的key处理。
2、包括按照TTL优先淘汰将要过期key。
3-4、对全体key,随机淘汰,对设置了TTL的key随机淘汰。
5-6、基于LRU算法对全体key或者设置了TTL的key进行淘汰。
7-8、基于LFU算法对全体key或者设置了TTL的key进行淘汰。
LRU(least recently used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大,淘汰的优先级越高。
LRU是基于时间的,时间敏感,最近的,新出现的热点key,更有可能被常驻到内存中,而那些长时间没有使用的key则更有可能被淘汰掉。
LFU(least frequently used),最少频率使用。会统一每个key的访问频率,值越小,淘汰优先级越高。
LFU是频率敏感的,更适合保存包含有多个热点key,某个key上一次访问已经是昨天中午12:00,并且在昨天12:00的5分钟内,高频访问了200次,如果今天中午还有这样的需求,按照LFU的策略,昨天的热key会常驻在内存中。
Redis主从、切片集群
14、Redis主从复制原理,有什么缺陷?
Redis主从复制实现方案是采用多台Redis服务器,一主多从,且主从服务之间采用读写分离的模式,主服务器可以进行读写操作,当发生写操作时,自动将写操作同步给服务器,而从服务器一般是只读模式,并接收主服务器同步过来的写操作命令。
由于主从服务器之间的命令复制是异步进行的,所以无法实现主从数据的强一致性。
主从服务器数据同步的原理如下:
15、Redis主从模式下的哨兵机制是怎么实现的?
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复,哨兵的主要作用包括服务器状态监控、自动故障恢复、通知。
-
服务器状态监控
- 哨兵基于心跳机制监测服务器状态,每隔1秒向集群的每个实例发送ping命令;如果某个哨兵,发现实例未在规定时间内响应,则认为实例主观下线,当超过指定数量(一般大于哨兵总数一半)的哨兵都主观认为该实例下线,则该实例节点客观下线。
- 一旦监测到master节点客观下线,第一个发现master节点主观下线的哨兵需要在slave中选择一个作为新的master。 选举的依据:
- 判断slave节点与master节点断开的时间长短,超过指定值的slave直接排除。
- 判断slave节点的优先级值,值越小,优先级越高。
- 判断slave节点运行的id大小,id越小,优先级越高。
-
自动故障恢复
- 被选中的slave,执行slave of no one,成为新的master。
- 通知所有其他slave节点,执行slave of 新的master。
- 修改发生故障的master节点的配置,添加slave of 新的master。
16、Redis的分片集群
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
这些哈希槽怎么被映射到具体的 Redis 节点上的呢?有两种方案:
- 平均分配: 在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
- 手动分配: 可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。
Redis实战