备战秋招面试,微信搜索公众号【TechGuide】关注更多新鲜好文和互联网大厂的笔经面经。
作者@TechGuide
点赞再看,养成习惯,您动动手指对原创作者意义非凡🤝
当你的才华还撑不起你的野心时,你应该静下心去学习 。🤝点赞再看,养成习惯🤝 |
---|
文章目录
- 前言
- 正文
- 一. 数据库中有哪些索引类型以及数据结构? 最左匹配原则?
- 二. 索引失效的情况
- 三. hash索引与B+树如何选用
- 四. 有哪些数据库引擎,各自的区别
- 五. 一条sql语句的查询流程
- 六. 怎么对一条查询语句(包括索引)调优
- 七. 数据库的三大范式和事务的四大特性(含隔离级别)
- 八. MySQL四种隔离机制的底层实现(如何解决幻读 )
- 九. MySQL有哪几种锁,分别怎么实现
- 十. 优化数据库的方案
- 十一. MySQL主从复制的原理?
- 十二. MySQL如何保证事务的特性
- 十三. MySQL和Redis的区别(各自使用的场景以及原因)
- 十四. redis的五大数据结构
- 十五. redis为什么快?
- 十六. Redis雪崩、击穿、穿透
- 十七. Redis缓存淘汰策略
- 十八. Redis持久化策略
前言
近些年来,随着微服务架构的发展,人们对数据库在分布式、高并发场景下的要求越来越高,可以说不同数据库针对不同应用场景,其底层的数据结构和算法,也愈加精巧且复杂,但是,归根结底,数据库也只是要实现数据持久化存储的一个工具而已,从这个角度出发,其和“记账本”是没有两样的,引用oracle官方对数据库的定义:
A database is an organized collection of structured information, or data, typically stored electronically in a computer system. A database is usually controlled by a database management system (DBMS).
正文
在开始梳理之前,带着问题去复习知识,往往是最行之有效的学习方法之一,因为这样很容易让你把握住重点,并且试图在阅读中找出关联,融会贯通,先思考一下,上面列举的这些问题你是否已经理解透彻。
如果对于上面的问题,你都可以侃侃而谈的话,说明你对数据库的基础掌握比较扎实,确实也没有读下去的必要了。但是如果没有的话,欢迎和我一起梳理一遍这些知识点,希望你有所收获。
一. 数据库中有哪些索引类型以及数据结构? 最左匹配原则?
作用:
使用索引的意义在于通过缩小一张表中需要查询的记录/行的数目来加快搜索的速度。
概念:
普通索引包含一个表中列的值和值所在行数据的物理地址,并且这些值存储在一个数据结构(此索引)中。索引的本质是一种适应特定应用场景的数据结构。 比如,以二叉树作为索引的数据库可以用下图示意。需要注意的是,索引本身也很大,不可能全部存储在内存中,一般以索引文件的形式存储在磁盘上。
关于索引的命令:
CREATE [UNIQUE] INDEX indexName ON mytable(username(length));
//创建索引,如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length。
ALTER table tableName ADD [UNIQUE] INDEX indexName(columnName)
//修改表结构(添加索引)
DROP INDEX [indexName] ON mytable;
//删除
SHOW INDEX FROM table_name\G
//查看,可以通过添加 \G 来格式化输出信息。
EXPLAIN SELECT * FROM `t_mobilesms_11` WHERE userid='2222'
//查看执行计划,包括索引(key)利用情况
索引类型:
- 从物理存储角度分:
- 聚集索引: 正文内容按照一个特定维度排序存储,这个特定的维度就是聚集索引。所以一张表只有一个聚集索引。
聚集索引一般是表中的主键索引
,如果表中没有显示指定主键,则会选择表中的第一个不允许为NULL的唯一索引
,如果还是没有的话,就采用Innodb
存储引擎为每行数据内置的6字节ROWID
作为聚集索引。聚集索引是索引和数据的集合体,不但决定了数据的顺序,在叶节点还存储数据行。
- 非聚集索引:非聚集索引索引项顺序存储,但索引项对应的内容却是随机存储的。
建立两列以上的联合索引
,即可查询复合索引里的列的数据而不需要进行回表
二次查询。当sql语句的所求查询字段(select列)和查询条件字段(where子句)全都包含在一个索引中 (联合索引),可以直接使用索引查询而不需要回表查询(覆盖索引),回表查询是指通过普通索引找到主键id,再根据主键id回到聚集索引(通常为主键索引)中确定行记录。
-
从逻辑角度分:
- 主键索引(值唯一,不能为空) - 普通索引 - 唯一索引(值唯一,可以为空) - 联合索引
主键索引与唯一索引区别:
- 主键列在创建时,已经默认为
空值 + 唯一索引
了。 - 主键可以被其他表引用为外键,而唯一索引不能。
- 一个表最多只能创建一个主键,但可以创建多个唯一索引。
普通索引与唯一索引区别:
- 对于普通索引和唯一索引来说,查询性能无差别。对于
普通索引
来说,查找到满足条件的第一个记录(5,'张三')
后,需要查找下一个记录,直到碰到第一个不满足k=5
条件的记录。对于唯一索引
来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。 - 从更新过程来说,由于change buffer的加成,当要更新的目标页不在内存中时,普通索引因为减少随机 IO 的访问,对更新性能的提升很明显
- 大部分场景普通索引都优于唯一索引(在业务代码保证唯一性的前提下),但如果业务模式是更新完后马上查询,此时普通索引+change buffer会起副作用
当需要更新
一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB
会将这些更新操作缓存在 change buffer
中,这样就不需要从磁盘中读入这个数据页了。在下次查询
需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer
中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
虽然名字叫作 change buffer
,实际上它是可以持久化的数据。也就是说,change buffer
在内存中有拷贝,也会被写入到磁盘上。
将 change buffer
中的操作应用到原数据页,得到最新结果的过程称为 merge
。除了访问这个数据页会触发 merge
外,系统有后台线程会定期 merge
。在数据库正常关闭(shutdown)的过程中,也会执行 merge
操作。
显然,如果能够将更新操作先记录在 change buffer
,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool
的,所以这种方式还能够避免占用内存,提高内存利用率。change buffer
用的是 buffer pool
里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size
来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool
的 50%。
对于唯一索引
来说,所有的更新操作都要先判断这个操作是否违反唯一性约束
。而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer
了。
change buffer 的使用场景:
对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer
的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。
因为 merge
的时候是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer
记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。
-
从底层实现角度分:
- B+树索引 - Hash索引 - Full-Text全文索引 - R-Tree索引
【关于底层数据结构的细节可以参考我写的这篇文章】
关于联合索引的测试的理解:
利用索引中的附加列,您可以缩小搜索的范围,但使用一个具有两列的索引 不同于使用两个单独的索引。复合索引的结构与电话簿类似,人名由姓和名构成,电话簿首先按姓氏对进行排序,然后按名字对有相同姓氏的人进行排序。如果您知道姓,电话簿将非常有用;如果您知道姓和名,电话簿则更为有用,但如果您只知道名不姓,电话簿将没有用处。
所以说创建复合索引时,应该仔细考虑列的顺序。对索引中的所有列执行搜索或仅对前几列执行搜索时,复合索引非常有用;仅对后面的任意列执行搜索时,复合索引则没有用处。
多个单列索引在多条件查询时优化器会选择最优索引策略,可能只用一个索引,也可能将多个索引全用上! 但多个单列索引底层会建立多个B+索引树,比较占用空间,也会浪费一定搜索效率,故如果只有多条件联合查询时最好建联合索引!
可以简单理解为,当创建(a,b,c)联合索引时,相当于创建了(a)单列索引,(a,b)联合索引以及(a,b,c)联合索引。想要索引生效的话,只能使用 a和a,b和a,b,c三种组合;当然,a,c组合也可以,但实际上只用到了a的索引,c并没有用到!
最左匹配(前缀)原则:
以底层B+树为例,当树节点的数据项是复合的数据结构,比如查询条件是(name,age,sex)
的时候,b+树是按照从左到右的顺序来建立搜索树的,此时对于每一个数据项包含多个关键字,并且对于单一的key,后面的key是有序的。
比如当(张三,20,F)
这样的数据来检索的时候,b+树会优先比较name
来确定下一步的搜索方向,如果name
相同再依次比较age
和sex
,最后得到检索的数据;但当(20,F)
这样的没有name
的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name
就是第一个比较因子,必须要先根据name
来搜索才能知道下一步去哪里查询。比如当(张三,F)
这样的数据来检索时,b+树可以用name
来指定搜索方向,但下一个字段age
的缺失,所以只能把名字
等于张三的数据都找到,然后再匹配性别
是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
总结:
1、需要加索引的字段,要在where条件中
2、数据量少的字段不需要加索引;因为建索引有一定开销,如果数据量小则没必要建索引(速度反而慢)
3、避免在where子句中使用or来连接条件,因为如果俩个字段中有一个没有索引的话,引擎会放弃索引而产生全表扫描。
4、联合索引比对每个列分别建索引更有优势,因为索引建立得越多就越占磁盘空间,在更新数据的时候速度会更慢。另外建立多列索引时,顺序也是需要注意的,应该将筛选条件严格的索引放在前面,这样筛选力度会更大,效率更高。
二. 索引失效的情况
1,用or连接的其中一个查询字段无索引,或者联合索引中使用or
2. 以%开头的like模糊查询
3. 如果列类型是字符串,那一定要在条件中将数据使用单引号引用起来,否则不会使用索引
4. 如果MySQL预计使用全表扫描要比使用索引快,则不使用索引
5. 判断索引列是否不等于某个值时。‘!=’
操作符。
select * from SoftWareDetailInfo where SoftUseLine != 0
6. 对索引列进行运算。这里运算包括+-*/
等运算。也包括使用函数
。
select * from SoftWareDetailInfo where SoftUseLine +0= 0
select * from SoftWareDetailInfo where count(SoftUseLine) = 0
7. 索引字段进行判空查询时。也就是对索引字段判断是否为NULL时。语句为is null 或 is not null
。
select * from SoftWareDetailInfo where CreateTime is null
8. 范围列可以用到索引(联合索引必须是最左前缀),但是范围列后面的列无法用到索引
三. hash索引与B+树如何选用
hash索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。Memory引擎默认使用的是此种索引。
存储引擎对所有的索引列计算出一个哈希码,将哈希码存储在索引中,同时哈希表中保存每个数据行的指针。
hash索引的特点:
- hash索引中只有hash值和行数的指针,因此无法直接使用索引来避免读取行,但是因为这种索引读取快,性能影响不明显。
- hash索引不是按照索引值顺序存储,无法使用于排序。
- 不支持部分列匹配查找,这里面是使用索引列的全部内容来计算哈希值,例如(A,B)两列一起建索引,单纯使用A一列,那么就无法使用索引,B-Tree索引的话,因为支持匹配最左前缀,所以这种情况适用性偏好。
- 哈希索引只支持等值查询,包括
=、in()、<=>,
不支持where age > 10
这种范围查询。- 哈希冲突很多的话,维护索引操作的代价也很高
B+树索引和哈希索引的明显区别是:
-
如果是
等值查询
,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;当然了,这个前提是,键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据; -
从示意图中也能看到,如果是
范围查询
检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索; -
同理,哈希索引也没办法利用索引完成
排序
,以及like ‘xxx%’
这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询); -
哈希索引也不支持多列联合索引的
最左匹配规则
; -
B+树索引的关键字检索
效率稳定
,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈希碰撞
问题。
四. 有哪些数据库引擎,各自的区别
查看搜索引擎的命令
SHOW ENGINES
SHOW VARIABLES LIKE 'storage_engine';
输出:
主要存储引擎:
在MySQL中,只有HEAP/MEMORY
引擎表才能显式支持哈希索引(勘误:InnoDB也支持全文索引)。
MyISAM存储引擎:
- 表级锁
- 不支持事务
- 索引为非聚簇性索引
- 适合查询比较频繁的场景,插入,更改比较少,无需事务的场景
InnoDB储存引擎:
- 同时使用表级锁和行级锁
- 支持事务和外键
- 索引为聚簇索引,会有回表操作
- 适合并发性较高,更改表比较频繁的,安全要求较高的场景
总结:
(1)如果要提供提交、回滚、崩溃恢复能力的事物安全(ACID兼容)能力,并要求实现并发控制,InnoDB是一个好的选择
(2)如果数据表主要用来插入和查询记录,则MyISAM引擎能提供较高的处理效率
如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将(3)数据保存在内存中的Memory引擎,MySQL中使用该引擎作为临时表,存放查询的中间结果
(4)如果只有INSERT和SELECT操作,可以选择Archive,Archive支持高并发的插入操作,但是本身不是事务安全的。Archive非常适合存储归档数据,如记录日志信息可以使用Archive
使用哪一种引擎需要灵活选择,一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求,使用合适的存储引擎,将会提高整个数据库的性能。
五. 一条sql语句的查询流程
大致查询流程:
- 客户端发送一条查询给服务器。
- 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。
- 服务器端的分析器进行SQL解析、预处理,再由优化器生成对应的执行计划。
- MySQL根据优化器生成的执行计划,再调用存储引擎的API来执行查询。 将结果返回给客户端。
关于优化器的补充:
MySQL使用基于成本的查询优化器
(Cost-Based Optimizer,CBO)。它会尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最少的一个。
优化器会根据优化规则对关系表达式进行转换,这里的转换是说一个关系表达式经过优化规则后会生成另外一个关系表达式,同时原有表达式也会保留,经过一系列转换后会生成多个执行计划,然后CBO
会根据统计信息和代价模型(Cost Model)计算每个执行计划的Cost
,从中挑选Cost最小的执行计划
。
sql具体查询语句详解:
所有的查询语句都是从 FROM 开始执行的。在实际执行过程中,每个步骤都会为下一个步骤生成一个虚拟表,这个虚拟表将作为下一个执行步骤的输入。
序号 | sql命令 | 描述 |
---|---|---|
1 | FORM | 对FROM的左边的表和右边的表计算笛卡尔积。产生虚表VT1 |
2 | ON | 对虚表VT1进行ON筛选,只有那些符合的行才会被记录在虚表VT2中。 |
3 | JOIN | 如果指定了OUTER JOIN(比如left join、 right join),那么保留表中未匹配的行就会作为外部行添加到虚拟表VT2中,产生虚拟表VT3, rug from子句中包含两个以上的表的话,那么就会对上一个join连接产生的结果VT3和下一个表重复执行步骤1~3这三个步骤,一直到处理完所有的表为止。 |
4 | WHERE | 对虚拟表VT3进行WHERE条件过滤。只有符合的记录才会被插入到虚拟表VT4中。 |
5 | GROUP BY | 根据group by子句中的列,对VT4中的记录进行分组操作,产生VT5.with CUBE ROLLUP: 对表VT5进行cube或者rollup操作,产生表VT6. |
6 | HAVING | 对虚拟表VT6应用having过滤,只有符合的记录才会被 插入到虚拟表VT7中。 |
7 | SELECT | 执行select操作,选择指定的列,插入到虚拟表VT8中。 |
8 | DISTINCT | 对VT8中的记录进行去重。产生虚拟表VT9. |
9 | ORDER BY | 将虚拟表VT9中的记录按照<order_by_list>进行排序操作,产生虚拟表VT10. |
10 | LIMIT | 取出指定行的记录,产生虚拟表VT11, 并将结果返回。 |
六. 怎么对一条查询语句(包括索引)调优
- 在经常需要进行检索的字段上创建索引
- 避免在索引上使用计算
- 使用预编译查询
这里解释一下,数据库在接受到sql语句之后,需要词法和语义解析,优化sql语句,制定执行计划。这需要花费一些时间。但是很多情况,我们的一条sql语句可能会反复执行,或者每次执行的时候只有个别的值不同。如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则效率就明显不行了。为了解决上面的问题,于是就有了预编译,预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化。一次编译、多次运行,省去了解析优化等过程。
它的优点是,预编译阶段可以优化 sql 的执行,并且可以防止SQL注入问题。
- 调整Where子句中的位置
- 尽量将多条SQL语句压缩到一句SQL中
- 用
where
字句替换HAVING字句(having一般用在分组后) - 使用表的别名
- 用
union all
替换union (对两个结果集进行并集操作,保留重复行,不进行排序) - 只在必要的情况下才使用事务
begin tran —— commit
- 用变长字段
varchar/nvarchar
代替char/nchar
- (以下对SELECT语句)
- 任何地方都不要使用
select * from t
,用具体的字段列表代替“*”,不要返回用不到的任何字段 - 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
- 别用like的前置%,别用in
- (以下非select语句)
- 如果只更改1、2个字段,不要Update全部字段,否则频繁调用会引起明显的性能消耗,同时带来大量日志
- 使用了
ROWID
高效删除重复记录
DELETE FROM EMP E
WHERE E.ROWID >
(SELECT MIN(X.ROWID) FROM EMP X WHERE X.EMP_NO = E.EMP_NO);
其他更详细内容,可以参考这里。
七. 数据库的三大范式和事务的四大特性(含隔离级别)
教材中范式的定义:
符合某一种级别的关系模式的集合,表示一个关系内部各属性之间的联系的合理化程度。
- 第一范式1NF (确保每列保持原子性)
符合1NF的关系中的每个属性都不可再分(每个字段不需要再次拆分以达到更好的数据库性能)。以下关系违反1NF。
2. 第二范式2NF (确保表中的每列都和主键相关)
仅仅符合1NF的设计,仍然会存在数据冗余过大
,插入异常
,删除异常
,修改异常
的问题。
码:关系中的某个属性或者某几个属性的组合,用于区分每个元组(可以把“元组”理解为一张表中的每条记录,也就是每一行)。
设 K 为某表中的一个属性或属性组,假如当 K 确定的情况下,该表除 K 之外的所有属性的值也就随之确定,那么 K 就是码。
如何确定码? 画出所有函数依赖关系。
2NF在1NF的基础之上,消除了非主属性对于码的部分函数依赖。
第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中,避免了冗余。消除这些部分函数依赖,只有一个办法,就是将大数据表拆分成两个或者更多个更小的数据表,在拆分的过程中,要达到更高一级范式的要求,这个过程叫做”模式分解“。
分解后的函数依赖关系如下:
3. 第三范式3NF
需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。如果有,可以拆分成多张表,并用外键连接。
3NF在2NF的基础之上,消除了非主属性对于码的传递函数依赖。
说明:
三大范式是递进的关系,不要切割来看,三个都符合的数据库才有可能是一个结构合理的关系型数据库。
加深对范式的理解可以阅读这篇文章【强推】
2)事务四大特性(ACID)和隔离级别(以及对应解决的问题)
四大特性:
- 原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
- 一致性(Consistency)
一致性是指一个事务执行之前和执行之后,数据库都必须处于一致性状态,比如在转账例子中,不能存在A减少,B没增加的情况。
- 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
- 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
隔离级别(括号内指存在或已解决的问题)
- Read-Uncommited (脏读)
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。
在实际引用场景中不允许脏读出现,但是此隔离级别下数据库的并发性能是最好的。
SET @@session.transaction_isolation = 'READ-UNCOMMITTED';
- Read Commited (不可重复读)
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。实质就是在同一事务内,对同一个数据的查询可能返回多个不同的结果,这些结果可能是其他事务操作提交更改的。
一个事务只能读取另一个已经提交的事务,但是多次读取可能会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。
比较:脏读是指read uncommited data, 而不可重复读是指read different data。
结合一下实际测试用例理解。
//1.准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,再准备一张测试表 test ,写入一条测试数据并调整隔离级别为 READ COMMITTED ,任意一个终端执行即可。
SET @@session.transaction_isolation = 'READ-COMMITTED';
create database test;
use test;
create table test(id int primary key);
insert into test(id) values(1);
//2.登录 mysql 终端 1,开启一个事务,将 ID 为 1 的记录更新为 2 ,并确认记录数变更过来。
begin;
update test set id = 2 where id = 1;
select * from test; -- 此时看到一条记录为 2
//3.登录 mysql 终端 2,开启一个事务后,查看表中的数据。
use test;
begin;
select * from test; -- 此时看一条 ID 为 1 的记录
//4.登录 mysql 终端 1,提交事务。
commit;
//5.切换到 mysql 终端 2。
select * from test; -- 此时看到一条 ID 为 2 的记录
完美解决了脏读的问题,但不可重复读的问题依然存在。
- Repeatable read(存在幻读问题)
该隔离级别是 MySQL 默认的隔离级别,在同一个事务里, select 的结果是事务开始时间点的状态(具体与MVCC相关,后文有详述),因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks
、间隙锁
机制来避免幻读。
这就说明,一个事务只能读到另一个已经提交的事务修改过的数据,但是第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。
幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁,锁定检索范围为只读,这样就避免了幻读。【百度百科原文】
不可重复读和幻读的区别:
不可重复读的重点是修改:同样的条件, 你读取过的数据, 再次读取出来发现值不一样了
幻读的重点在于新增或者删除:同样的条件, 第1次和第2次读出来的记录数不一样
- SERIALIZABLE(序列化,解决幻读)
在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,以上3种隔离级别都允许对同一条记录进行读-读、读-写、写-读的并发操作,如果我们不允许读-写、写-读(但可以读-读)的并发操作,从而避免了脏读、不可重读复读和幻读问题。
总结:
READ_UNCOMMITTED 读未提交:最低级别,一个事务可以读取另一个未提交事务的数据。幻想读、不可重复读和脏读都允许。
READ_COMMITTED 读已提交:一个事务要等另一个事务提交后才能读取数据。允许幻想读、不可重复读,不允许脏读。
REPEATABLE_READ 可重复读:在开始读取数据(事务开启)时,不再允许修改操作。允许幻读,不允许不可重复读和脏读。
SERIALIZABLE 可串行化:最高级别,在该级别下,事务串行化顺序执行。幻想读、不可重复读和脏读都不允许。
八. MySQL四种隔离机制的底层实现(如何解决幻读 )
比较透彻的理解mvcc 、间隙锁的工作机制。
理解MySQL-InnoDB的四种隔离机制的底层实现需要先弄清楚三个完全陌生的概念:undo log日志
、版本链
和readview
。
概念说明:
对于InnoDB存储引擎,聚簇索引记录中都包含两个“隐藏”的字段,事务id
(trx_id)和版本指针
(roll_pointer)
trx_id
:每次对某条聚簇索引记录进行改动时,都会把对应的事务id
赋值给trx_id隐藏列。roll_pointer
:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志
中,然后这个pointer隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息,如此,这些undo日志
就可以形成一条记录链如下图,即为版本链
。
ReadView
中主要包含一个包含当前系统中所有活跃(uncommited)的读写事务id
的列表,把这个列表命名为为m_ids。
阅读完原文后,做一下总结,注意名词可以有出入,但是原理和工作机制要理解透彻。
关于四种隔离机制底层实现的总结:
对于使用READ UNCOMMITTED
隔离级别的事务来说,直接读取记录的最新版本;
对于使用SERIALIZABLE
隔离级别的事务来说,使用加锁的方式来访问记录。
对于使用READ COMMITTED 和 REPEATABLE READ
隔离级别的事务来说,就需要用到我们上边所说的版本链。
- 如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。【此行勘误】
- 如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
根据以上判定规则,READ COMMITTED和REPEATABLE READ机制的区别就在于何时生成ReadView
,READ COMMITTED 在每次读取数据前都生成一个ReadView(当前读),而REPEATABLE READ只有第一次读取数据时生成一个ReadView(快照读),这样可以保证每次查询版本链和ReadView(m_ids)比较得到的可见性事务基本没变化,可重复读。
所谓的MVCC
(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD
、REPEATABLE READ
这两种隔离级别的事务在执行普通的SELECT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同
,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复这个ReadView就好了。
九. MySQL有哪几种锁,分别怎么实现
基本介绍:
MyISAM
和 MEMORY
存储引擎采用的是表级锁
(table-level locking)
BDB 存储引擎采用的是页面锁(page-level locking),但也支持表级锁
InnoDB
存储引擎既支持行级锁
(row-level locking),也支持表级锁
,但默认情况下是采用行级锁。
默认情况下,表锁和行锁都是自动获得的, 不需要额外的命令。
1.乐观锁与悲观锁:
对于悲观锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(适用于多写)
对于乐观锁,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。(适用于多读)
- 版本号机制:提交版本必须大于记录当前版本才能执行更新的乐观锁策略。
- CAS算法:是一种无锁算法,基于硬件原语实现,能够在不使用锁的情况下实现多线程之间的变量同步。jdk中的
java.util.concurrent.atomic
包中的原子类就是通过CAS来实现了乐观锁。
2.共享锁和排他锁(读锁和写锁)
共享锁和排它锁是悲观锁的不同的实现,都属于悲观锁的范畴。
共享锁S又称读锁 (read lock),是读取操作创建的锁。其他用户可以并发读取数据,也可以重复上共享锁,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到所有共享锁已释放。
排他锁X又称写锁 (write lock): 若事务 1 对数据对象A加上排他锁,事务 1 可以读A也可以修改A,其他事务不能再对A加任何锁,直到事物 1 释放A上的锁。这保证了其他事务在事物 1 释放A上的锁之前不能再读取和修改A。排它锁会阻塞所有的排它锁和共享锁。附上关于事务的常用命令。
意向共享锁(读锁 IS ),事务想要获取一张表的几行数据的共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(写锁 IX),事务想要获取一张表中几行数据的排它锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
意向锁的主要用途是为了表达某个事务正在锁定一行或者将要锁定一行数据。e.g:事务A要对一行记录r进行上X锁,那么InnoDB会先申请表的IX锁,再锁定记录r的X锁。在事务A完成之前,事务B想要来个全表操作,此时直接在表级别的IX就告诉事务B需要等待而不需要在表上判断每一行是否有锁。
意向排它锁存在的价值在于节约InnoDB对于锁的定位和处理性能
。另外注意了,除了全表扫描以外意向锁都不会阻塞。
3.粒度锁比较:
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
存储引擎(Memory, MyISAM)通过总是一次性同时获取所有需要的锁以及总是按相同的顺序获取表锁来避免死锁。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。最大程度的支持并发,同时也带来了最大的锁开销。InnoDB有三种行锁的算法:
-
Record Lock
:单个行记录上的锁 -
Gap Lock
:间隙锁,锁定一个范围,而非记录本身 -
Next-Key Lock
:结合Gap Lock和RecordLock,锁定一个范围,并且锁定记录本身。主要解决的问题是REPEATABLE READ隔离级别下的幻读。
# Transaction-A
mysql> set autocommit = 0;
mysql> update innodb_lock set v='1001' where id=1;
mysql> commit;
# Transaction-B
mysql> update innodb_lock set v='2001' where id=2;
Query OK, 1 row affected (0.37 sec)
mysql> update innodb_lock set v='1002' where id=1; //行锁影响
Query OK, 1 row affected (37.51 sec)
在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步获得的,这就决定了在 InnoDB 中发生死锁是可能的。
页面锁: 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。举例来说, 假如 user 表中只有 101 条记录, 其 userid 的值分别是 1,2,…,100,101, 下面的 SQL:
Select * from user where userid > 100 for update;
是一个范围条件的检索,InnoDB 不仅会对符合条件的 userid 值为 101 的记录加锁,也会对userid 大于 101(但是这些记录并不存在)的“间隙”加锁,防止其它事务在表的末尾增加数据。在执行这个 select 查询语句的时候,会将对应的索引访问条目进行上排他锁。
使用场景:为了让自己查到的数据确保是最新数据,并且查到后的数据只允许自己来修改的时候,需要用到 for update
子句。
InnoDB 使用间隙锁的目的,为了防止幻读,以满足串行化隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了 userid 大于 100 的任何记录,那么本事务如果再次执行上述语句,就会发生幻读。
Select * from user where userid > 100 lock in share mode ;
in share mode
子句的作用就是将查找到的数据加上一个 share 锁,这个就是表示其他的事务只能对这些数据进行简单的select 操作,并不能够进行 DML 操作,允许其他的事务也对该数据上S锁,但是不能够对该数据进行修改。
使用场景:为了确保自己查到的数据没有被其他的事务正在修改,也就是说确保查到的数据是最新的数据,并且不允许其他人来修改数据。但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据 使用了 in share mode
的方式上了 S 锁。
for update 和 lock in share mode 的区别:
前一个上的是排他锁(X 锁),一旦一个事务获取了这个锁,其他的事务是没法在这些数据上执行for update
;后一个是共享锁,多个事务可以同时的对相同数据执行lock in share mode
。
具体实现概述:
InnoDB 行锁是通过给索引上的索引项加锁来实现的,这一点 MySQL 与 Oracle 不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB 这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,当索引失效时,InnoDB 将使用表锁!【待补充】
十. 优化数据库的方案
【拓展】
1.SQL语句优化
1)应尽量避免在 where 子句中使用!=或<>
操作符,否则将引擎放弃使用索引而进行全表扫描。
2)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
3)很多时候用 exists 代替 in 是一个好的选择。
4)用Where子句替换HAVING 子句,因为HAVING只会在检索出所有记录之后才对结果集进行过滤。
5)编程手段防止SQL注入:使用JDBC PreparedStatement
按位插入或查询;正则表达式过滤(非法字符串过滤);
2.索引优化
查看索引优化部分。
补充:
什么是索引下推?
索引下推(index condition pushdown )简称ICP
,在Mysql5.6的版本上推出,用于优化查询。
- 在不使用
ICP
的情况下,在使用非主键索引(又叫普通索引或者二级索引)进行查询时,存储引擎
通过索引检索到数据,然后返回给MySQL服务器
,服务器然后判断数据是否符合条件 。 - 在使用
ICP
的情况下,如果存在某些被索引的列的判断条件时,MySQL服务器
将这一部分判断条件传递给存储引擎
,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器 。
索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。
SELECT * from user where name like '陈%' and age=20
- 5.6之前的版本是没有索引下推这个优化的,会忽略
age
这个字段,直接通过name
进行查询,在(name,age)
这课树上查找到了两个结果,id
分别为2,1,然后拿着取到的id
值一次次的回表查询,因此这个过程需要回表两次。 - 5.6版本添加了索引下推这个优化,
InnoDB
并没有忽略age
这个字段,而是在索引内部就判断了age
是否等于20,对于不等于20的记录直接跳过,因此在(name,age)
这棵索引树中只匹配到了一个记录,此时拿着这个id去主键索引树中回表查询全部数据,这个过程只需要回表一次。
根据explain
解析结果可以看出Extra的值为Using index condition
,表示已经使用了索引下推。
3.数据库结构优化
1)范式优化: 比如消除冗余(节省空间…)
2)反范式优化:比如适当加冗余等(减少join)
3)拆分表: 分区将数据在物理上分隔开,不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,而不必进行全表扫描,明显缩短了查询时间,另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘I/O,一个精心设置的分区可以将数据传输对磁盘I/O竞争均匀地分散开。对数据量大的时时表可采取此方法。可按月自动建表分区。
4)拆分其实又分垂直拆分和水平拆分:
案例: 简单购物系统暂设涉及如下表: 1.产品表(数据量10w,稳定) 2.订单表(数据量200w,且有增长趋势) 3.用户表 (数据量100w,且有增长趋势) 以mysql为例讲述下水平拆分和垂直拆分,mysql能容忍的数量级在百万静态数据可以到千万
垂直拆分: 解决问题:表与表之间的io竞争 不解决问题:单表中数据量增长出现的压力 方案: 把产品表和用户表放到一个server上 订单表单独放到一个server上
水平拆分: 解决问题:单表中数据量增长出现的压力 不解决问题:表与表之间的io争夺 方案: 用户表通过性别拆分为男用户表和女用户表 订单表通过已完成和完成中拆分为已完成订单和未完成订单 产品表 未完成订单放一个server上 已完成订单表和男用户表放一个server上 女用户表放一个server上。
4.服务器硬件优化
提升数据库服务器硬件配置(提升硬盘配置,换SSD固态硬盘),或者搭建数据库集群。
十一. MySQL主从复制的原理?
master主机开启bin-log
功能,日志文件用于记录数据库的读写增删。需要开启3个线程,master IO线程
,slave开启 IO线程
SQL线程
,
- slave通过IO线程连接master,并且请求某个
bin-log
,position
之后的内容。 - master服务器收到slave IO线程发来的日志请求信息,io线程去将
bin-log
内容,position
返回给slave IO线程。 - slave服务器收到
bin-log
日志内容,将bin-log
日志内容写入relay-log
中继日志,创建一个master.info
的文件,该文件记录了master ip 用户名 密码 master bin-log名称,bin-log position
。 - slave端开启SQL线程,实时监控
relay-log
日志内容是否有更新,解析文件中的SQL语句,在slave数据库中去执行。
MySQL默认使用基于语句(复执行sql语句)的复制,当基于语句的复制会引发问题的时候就会使用基于行(复制sql执行结果)的复制,MySQL会自动进行选择。
在MySQL主从复制架构中,读操作可以在所有的服务器上面进行,而写操作只能在主服务器上面进行。主从复制架构虽然给读操作提供了扩展,可如果写操作也比较多的话(多台从服务器还要从主服务器上面同步数据),单主模型的复制中主服务器势必会成为性能瓶颈。
Mysql主从复制的过程 (复习一遍,有点重复了·)
- Slave上面的IO进程连接上Master,并请求从指定日志文件的指定位置之后的日志内容。
- Master接收到来自Slave的IO进程的请求后,负责复制的IO进程会根据请求信息读取日志指定位置之后的日志信息,返回给Slave的IO进程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息所读到的Master端的bin-log文件的名称以及bin-log的位置。
- Slave的IO进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的bin-log的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的告诉Master从何处开始读取日志。
- Slave的Sql进程检测到relay-log中新增加了内容后,会马上解析relay-log的内容成为在Master端真实执行时候的那些可执行的内容,并在自身执行。
半同步复制:
使用半同步复制。在主库commit之前,需要先将binlog
同步到从库,主库可以设置同步binlog
的过期时间,在binlog
复制到从库之后,从库后续会自行重放中继日志
。不过这样也增加了客户端的延迟。
十二. MySQL如何保证事务的特性
【拓展】
结论:
redo log
重做日志用来保证事务的持久性undo log
回滚日志保证事务的原子性- undo log+redo log保证事务的一致性
- 锁(共享、排他)用来保证事务的隔离性
ACID其他的三个属性都是为了保证一致性而存在。
redo log
InnoDB通过Force Log at Commit
机制来实现持久性,当commit时,必须先将事务的所有日志写到重做日志文件进行持久化,待commit操作完成才算完成。为了确保每次日志都写入重做日志文件,在每次将日志缓冲写入重做日志文件后,InnoDB存储引擎默认都需要调用一次fsync(刷入磁盘)操作。
undo log
为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为Undo Log),然后进行数据的修改。如果出现了错误或者用户执行了 ROLLBACK
语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。undo log实现多版本并发控制(MVCC)来辅助保证事务的隔离性。
所以,如果整个事务执行的过程系统崩溃或者断电了,在系统重启的时候,恢复机制会将redo log中已提交的事务重做,保证事务的持久性;而undo log中未提交的事务进行回滚,保证事务的原子性。
说明:
回滚日志不同于重做日志,它是逻辑日志,对数据库的修改都逻辑的取消了。当事务回滚时,它实际上做的是与先前相反的工作。对于每个INSERT,InnoDB存储引擎都会完成一个DELETE;对于每个UPDATE,InnoDB存储引擎都会执行一个相反的UPDATE。
另一个注意点是,当记录的是对 delete和 update操作产生的 undo log(update undo log)时,该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待 purge线程进行最后的删除。当记录的是对 insert操作产生的 undo log时,可以提交后删除。
十三. MySQL和Redis的区别(各自使用的场景以及原因)
1)类型上
从类型上来说,mysql是关系型数据库,redis是缓存数据库
2)作用上
mysql用于持久化的存储数据到硬盘,功能强大,但是速度较慢
redis用于存储使用较为频繁的数据到缓存中,读取速度快
3)需求上
mysql和redis因为需求的不同,一般都是配合使用。
4)存放位置
数据存放位置MySQL:数据放在磁盘
Redis:数据放在内存
5)适合存放数据类型
Redis适合放一些频繁使用,比较热的数据,因为是放在内存中,读写速度都非常快,一般会应用在下面一些场景:排行榜(sorted set)、计数器、消息队列推送、好友关注、粉丝.
关于redis事务的补充:
- 一次性:一次执行多个redis操作命令,相当于打包的批量执行脚本
- 顺序性:顺序执行
- 排他性:在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
但是redis事务不具备原子性(收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行),并且无隔离级别。
十四. redis的五大数据结构
Redis有5个基本数据结构:string
(动态字符数组)、list
(双向链表)、hash
、set
和zset
。以下关于必知命令。
注意:redis命令不区分大小写·。
redis数据库基本操作:
SELECT 3 #选择第几个redis数据库
DBSIZE #查看数据库容量
keys * #查看所有redis键名
flushdb #清空当前数据库
flushall #清空所有数据库
EXISTS keyname #判断是否存在
EXPIRE keyname 10 #设置十秒后过期
type keyname
expire ireader 60 # 1表示设置成功,0表示变量ireader不存在
ttl ireader # 还有50秒的寿命,返回-2表示变量不存在,-1表示没有设置过期时间
del ireader # 删除成功返回1
redis事务:
multi
set a aaa、
exec //取消DISCARD WATCH可监控key值,若改变,则取消执行EXEC
string必知命令:
set money 100
get money
setrange ireader 28 wooxian #指定位置替换
getrange ireader 28 34
strlen ireader
append ireader .hao
incrby money 20
decrby money 20
incr money
decr money
setex keyname 30 "hello hong" #带过期时间设置
setnx keyname “hello somebody” #set if not exist
set lock 4854857 ex 3000 nx #redis分布式锁使用的加锁语句
mset k1 k2 k3 v1 v2 v3
mget k1 k2 k3
getset k1 v4
List必知命令:
lpush list one
rpush list two
lpop list
rpop list
lrange list 0 -1 #取出全部内容
lindex list -1 #下标取值
lrem list 1 one #移除一个one
ltrim list 1 3 #截取并保留的范围
linsert list before one three #one前插入three
llen list
hash必知命令:
hash底层原理:
在实现结构上hash使用二维结构,第一维是数组,第二维是链表,hash的内容key和value存放在链表中,数组里存放的是链表的头指针。通过key查找元素时,先计算key的hashcode,然后用hashcode对数组的长度进行取模定位到链表的表头,再对链表进行遍历获取到相应的value值。
扩容 当hash内部的元素比较拥挤时(hash碰撞比较频繁),就需要进行扩容。扩容需要申请新的两倍大小的数组,然后将所有的键值对重新分配到新的数组下标对应的链表中(rehash)。如果hash结构很大,比如有上百万个键值对,那么一次完整rehash的过程就会耗时很长。这对于单线程的Redis里来说有点压力山大。所以Redis采用了渐进式rehash
的方案。它会同时保留两个新旧hash结构,在后续的定时任务以及hash结构的读写指令中将旧结构的元素逐渐迁移到新的结构中。这样就可以避免因扩容导致的线程卡顿现象。
缩容 Redis的hash结构不但有扩容还有缩容,从这一点出发,它要比Java的HashMap要厉害一些,Java的HashMap只有扩容。缩容的原理和扩容是一致的,只不过新的数组大小要比旧数组小一倍。
hset ireader go fast
hmset ireader java fast python slow #一次设置多个键值对
hget ireader go
hmget ireader go python
hgetall ireader
hkeys ireader
hvals ireader
hdel ireader java #可删除多个
hexists ireader go
set和sorted set必知命令:(共同好友 关注)
补充:HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同
一个内部值。
sorted set(zset)底层实现hash和跳跃列表。其中每一个元素value有一个权重score,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
sadd set "666"
smembers set #全部元素
sismember set "666" #判断是否有
scard set #元素个数
srem set "666"
spop set #随机弹出
zadd key score member
zadd ireader 4.0 python #通过zadd指令可以增加一到多个value/score对,score放在前面
zcard ireader
zrem ireader go pytho #可删除多个
> zscore ireader python
"5"
> zrangebyscore ireader 0 5
1) "go"
2) "java"
3) "python"
> zrangebyscore ireader -inf +inf withscores
1) "go"
2) "1"
3) "java"
4) "4"
5) "python"
6) "5"
有序集合对象的编码可以是ziplist
或者skiplist
。同时满足以下条件时使用ziplist
编码:
- 元素数量小于128个
- 所有member的长度都小于64字节
ziplist
编码的有序集合使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个保存score。ziplist
内的集合元素按score从小到大排序,score较小的排在表头位置。
skiplist
编码的有序集合底层是一个命名为zset
的结构体,而一个zset结构同时包含一个字典
和一个跳表
。跳表按score从小到大保存所有集合元素。而字典则保存着从member到score的映射,这样就可以用O(1)的复杂度来查找member对应的score值。虽然同时使用两种结构,但它们会通过指针来共享相同元素的member和score,因此不会浪费额外的内存。
十五. redis为什么快?
(io模型 + 基于内存 + 线程模型)
结论:
多路复用IO拓展:
Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。【IO多路复用在Linux下包括了三种,select、poll、epoll,关于linux中的IO模型】
Reactor模型(事件驱动)事件处理器。以select为例,select的调用会阻塞到有文件描述符可以进行IO操作或被信号打断或者超时才会返回。
select将监听的文件描述符分为三组,每一组监听不同的需要进行的IO操作。readfds是需要进行读操作的文件描述符,writefds是需要进行写操作的文件描述符,exceptfds是需要进行异常事件处理的文件描述符。这三个参数可以用NULL来表示对应的事件不需要监听。
当select返回时,每组文件描述符会被select过滤,只留下可以进行对应IO操作的文件描述符。
总结以上就是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。
纯内存操作
单线程
单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案,省去切换上下文的开销。
简单高效的数据结构
十六. Redis雪崩、击穿、穿透
十七. Redis缓存淘汰策略
当内存空间不够时,redis会根据设置的淘汰策略将一部分数据删除,淘汰策略大致可以作用于全范围数据或者仅作用于设置了过期时间的数据。
淘汰策略 | 描述 |
---|---|
volatile-ttl | 表示在设置可过期时间的键值对中,根据过期时间的先后进行淘汰数据,越早被过期的数据,越先被淘汰。 |
volatile-random | 在设置了过期时间的键值对中,随机淘汰数据。 |
volatile-lru | 会根据lru算法进行数据的淘汰 |
volatile-lfu | 会根据lfu算法进行数据的淘汰 |
allkeys-random | 在全部的键值对数据中,进行数据的随机淘汰。 |
allkeys-lru | 在全部的键值对数据中,根据lru算法进行数据的淘汰。 |
allkeys-lfu | 在全部的键值对数据中,根据lfu算法进行数据的淘汰。 |
最大缓存、主键失效、淘汰机制等参数都是修改redis.config文件来配置的。
redis在实际删除失效主键时又有两种方式:
- 消极方法(passive way)
在主键被访问时如果发现它已经失效,那么就删除它。redis在实现GET、MGET、HGET、LRANGE等所有涉及到读取数据的命令时都会调用 expireIfNeeded,它存在的意义就是在读取数据之前先检查一下它有没有失效,如果失效了就删除它。
- 积极方法(active way)
周期性地探测,发现失效就删除。消极方法的缺点是,如果key 迟迟不被访问,就会占用很多内存空间,所以才有积极方式。
- 主动删除:
当内存超过maxmemory限定时,触发主动清理策略,该策略由启动参数的配置决定
十八. Redis持久化策略
- RDB 持久化
RDB 持久化(也称作快照持久化)是指将内存中的数据生成快照保存到磁盘里面,保存的文件后缀是 .rdb。rdb 文件是一个经过压缩的二进制文件,当 Redis 重新启动时,可以读取 rdb 快照文件恢复数据。
RDB 文件是一个单文件的全量数据,很适合数据的容灾备份与恢复,通过 RDB 文件恢复数据库耗时较短,通常 1G 的快照文件载入内存只需 20s 左右。
- AOF 持久化
AOF 会把 Redis 服务器每次执行的写命令记录到一个日志文件中,当服务器重启时再次执行 AOF 文件中的命令来恢复数据。
默认情况下 AOF 功能是关闭的,Redis 只会通过 RDB 完成数据持久化的。开启 AOF 功能需要
redis.conf
文件中将appendonly
配置项修改为yes
AOF 文件的写入流程可以分为以下 3 个步骤:
命令追加(append):将 Redis 执行的写命令追加到 AOF 的缓冲区 aof_buf
文件写入(write)和文件同步(fsync):AOF 根据对应的策略将 aof_buf
的数据同步到硬盘
文件重写(rewrite):定期对 AOF 进行重写,从而实现对写命令的压缩。 手动调用重写日志bgrewriteaof
。
- 优缺点对比
持久化方式 | 优点 | 缺点 |
---|---|---|
RDB | a.是一个压缩过的非常紧凑的文件,保存着某个时间点的数据集,适合做数据的备份、灾难恢复 b.可以最大化 Redis 的性能,在保存 RDB 文件,服务器进程只需 fork 一个子进程来完成 RDB 文件的创建,父进程不需要做 IO 操作 c.与 AOF 持久化方式相比,恢复大数据集的时候会更快 | a.RDB 的数据安全性是不如 AOF 的,保存整个数据集是个重量级的过程,根据配置可能要几分钟才进行一次持久化,如果服务器宕机,那么就可能丢失几分钟的数据 b.Redis 数据集较大时,fork 的子进程要完成快照会比较耗费 CPU 和时间 |
AOF | a.数据更完整,安全性更高,秒级数据丢失(取决于 fsync 策略,如果是 everysec,最多丢失 1 秒的数据) b.AOF 文件是一个只进行追加的命令文件,且写入操作是以 Redis 协议的格式保存的,内容是可读的,适合误删紧急恢复 | a.对于相同的数据集,AOF 文件的体积要远远大于 RDB 文件,数据恢复也会比较慢 b.根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。不过在一般情况下, 每秒 fsync 的性能依然非常高 |