MySQL面试必问(一)🔥
- 作者
- Name
- 青玉白露
- Github
- @white0dew
- Modified on
- Reading time
- 119 分钟
阅读:.. 评论:..
数据库基础知识
数据库三范式(3NF)
简单来说,在关系型数据库汇总,三范式就是设计一个好的数据库所必须要遵循的一些规则。
- 每列原子性(1NF)
比如某一个数据表中有“地址”信息,但是实际业务场景下经常查询“地址”中的城市部分,那么就需要将“地址”拆分成多个部分来保存(否则的话,实际业务场景还需要在地址中进行提取,增加了业务逻辑的复杂度),如下:
- 每列都和主键相关(2NF)
第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。 比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号作为数据库表的联合主键,如下表所示。
但上述的建表方式违反了第二范式,因为这样就产生一个问题:这个表中是以订单编号和商品编号作为联合主键。这样在该表中商品名称、单位、商品价格等信息不与该表的主键相关,而仅仅是与商品编号相关。如何解决?对原来的表进行拆分! 一个订单信息表,一个商品信息表,一个订单项目表。- 每一列都应该与主键列直接相关,而不能间接相关(3NF)
比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。如下面这两个表所示的设计就是一个满足第三范式的数据库表。
第二范式(2NF)和第三范式(3NF)的概念很容易混淆,区分它们的关键点在于, 2NF:非主键列是否完全依赖于主键,还是依赖于主键的一部分; 3NF:非主键列是直接依赖于主键,还是直接依赖于非主键列。 粗略的理解,2NF,处理不合理的复合主键,3NF,处理单主键表的非主键列。
总结一下: 第一范式:具有原子性 第二范式:主键列与非主键列遵循完全函数依赖关系 第三范式:非主键列之间没有传递函数依赖关系 学生管理系统 学生成绩管理系统
事务的特性
绑定在一起作为一个逻辑工作单元的sql语句分组,任何子句失败则回滚到操作前(即一系列子句的合集,是一个操作序列)
事务特性ACID?ACID理论? atomic原子性、consistent一致性、isolation隔离性、durability持久性; 四个:原子(要么功成身退,要么回家种薯)、一致(操作前后的完整性未破坏)、隔离(事务间不影响)、持久(完成后就不会被回滚了); 注:从事务的四个特性可以看出,数据库系统有一个重要的特性就是能够回滚。
分布式事务? 分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。如从招行向工行转账1000元就要用到分布式数据库,因为不能仅调用某一家银行的数据库就完成任务。
TCC分布式事务,在下面的Redis章节中
事务并发的问题?
1、脏读:事务A读取了事务B更新前(事务最后一步是commit,做完才是真正的更新)的数据,然后B回滚操作,那么A读取到的数据是脏数据 2、**不可重复读:**事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一。
3、**幻读:**系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,**就好像发生了幻觉一样,这就叫幻读。**指的就是某个事务在读取某个范围的数据,但是另一个事务又向这个范围的数据去插入数据,导致多次读取的时候,数据的行数不一致。虽然读取同一条数据可以保证一致性,但是却不能保证没有插入新的数据。
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增。 解决幻读需要锁表或者使用next-key、MVCC(存疑)。 上面四种情况的Mysql操作细节如下:链接
事务隔离级别/读未提交/已提交
- READ UNCIMMITTED(未提交读),事务中的修改,即使没有提交,其他事务也可以看得到;
- READ COMMITTED(读已提交),大多数数据库系统的默认隔离级别是提交读,只能看到已经完成的事务的结果,正在执行的,是无法被其他事务看到的。(oracle默认)
- REPEATABLE READ(可重复读),该级别保证了每行的记录的结果是一致的,却无法解决另一个问题,幻读。(可重复读是mysql数据库innodb默认的访问级别,是通过MVCC来解决的)。 4) SERIALIZABLE(序列化),序列化是最高的隔离级别,由于大量加上锁,导致大量的请求超时,因此性能会比较底下,再特别需要数据一致性且并发量不需要那么大的时候才可能考虑这个隔离级别。
锁的概念
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;锁定整个表数据(如MyISAM引擎); 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;锁定整个行数据(如InnoDB引擎); 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般;就是对整个数据库加锁;
以上三者是从锁的粒度来划分的;以下是从加锁的时机来说: **悲观锁:**总认为别人会修改,所以每次拿数据就上锁(小心翼翼型);适合写多读少 **乐观锁:**认为世界和平,只在更新数据时检查一下version,不对劲就回滚;适合读多写少;
InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)。 对于共享锁而言,对当前行加共享锁,不会阻塞其他事务对同一行的读请求,但会阻塞对同一行的写请求。只有当读锁释放后,才会执行其它事物的写操作。 对于排它锁而言,会阻塞其他事务对同一行的读和写操作,只有当写锁释放后,才会执行其它事务的读写操作。
MySQL的行锁又分为共享锁(S锁)和排他锁(X锁)。 一般普通的select语句,InnoDB不加任何锁,我们称之为快照读
select * from test;
通过加S锁和X锁的select语句或者插入/更新/删除操作,我们称之为当前读:
select * from test lock in share mode; select * from test for update; insert into test values(…); update test set …; delete from test …;
**特殊说明:**以上的当前读,读取的都是记录的最新版本。对读取记录都会加锁,除了第一条语句lock in share mode是对记录加S锁(共享锁)外,其他的操作都是加X锁(排他锁)。
索引
索引是数据库管理系统中的一个排序的数据结构,以满足一些查找算法的需求。基本实现是B树或B+树(B+树是B树得特别版本); 索引会造成一定开销,因为修改数据的同时索引也会变动,但是可以提高数据检索的速度;链接 索引的优缺点? 索引最大的好处是提高查询速度; 缺点是更新数据时效率低,因为要同时更新索引; 因此,对数据进行频繁查询建议建立索引;
建立索引的原则
什么情况下需要建索引?
- 经常用于查询的字段
- 经常用于连接的字段(如外键)建立索引,可以加快连接的速度
- 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度
什么情况下不建索引?
- where条件中用不到的字段不适合建立索引,即不常用查询的字段
- 需要经常增删改,索引变动较大
- 区分度不高的字段不适合建立索引,性别等,索引应该建立在区分度高的前提下
索引的数据结构
索引的数据结构主要有B+树和哈希表,对应的索引分别为B+树索引和哈希索引。InnoDB引擎的索引类 型有B+树索引和哈希索引,默认的索引类型为B+树索引。
两者的区别? 哈希索引不支持排序,因为哈希表是无序的。 哈希索引不支持范围查找。 哈希索引不支持模糊查询及多列索引的最左前缀匹配。 因为哈希表中会存在哈希冲突,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定 的,每次查询都是从根节点到叶子节点。
为什么需要hash索引?(AHI,Adapative Hash Index,自适应哈希索引) 1.innodb本身的索引结构是B+tree,而hash索引是innodb存储引擎提供的特性功能 2.innodb存储引擎内部自己去监控索引表,如果监控到某个索引经常用,那么就认为是热数据,然后内部自己创建一个hash索引(只适用“=”的查询,对“in”“<=>”这些范围查询不适用) 3.创建了hash索引后,如果下次又查询到这个索引,那么直接通过hash算法推导出记录的地址,直接一次就能查到数据,比重复去B+tree索引中查询三四次节点的效率高了不少
索引分类
- **主键索引:**名为primary的唯一非空索引,不允许有空值。
- **唯一索引:**索引列中的值必须是唯一的,但是允许为空值。唯一索引和主键索引的区别是: UNIQUE 约束的列可以为null且可以存在多个null值。UNIQUE KEY的用途:唯一标识数据库表中 的每条记录,主要是用来防止数据重复插入。创建唯一索引的SQL语句如下:
- **组合索引:**在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段 时,索引才会被使用,使用组合索引时遵循最左前缀原则。
- 全文索引:只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索 引。 全文索引是将存储在数据库中的大段文本中的任意内容信息查找出来的技术。
索引设计原则及何时失效?
设计原则: 索引列的区分度越高,索引的效果越好。使用性别这种区分度很低的列作为索引,效果就会很差。 尽量使用短索引,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,并且索引高速缓存中的块可以容纳更多的键值,会使得查询速度更快。
有时需要在很长的字符列上创建索引,这会造成索引特别大且慢。使用前缀索引可以避免这个问题。 前缀索引是指对文本或者字符串的前几个字符建立索引,这样索引的长度更短,查询速度更快。
索引不是越多越好,每个索引都需要额外的物理空间,维护也需要花费时间。 利用最左前缀原则。
导致索引失效的情况: 1.对于组合索引,不是使用组合索引最左边的字段(最左前缀匹配),则不会使用索引 如果组合索引为:(name,email) name and email -- 使用索引 name -- 使用索引 email -- 不使用索引
2.以%开头的like查询如 %abc ,无法使用索引;非%开头的like查询如 abc% ,相当于范围查询,会使 用索引
%:百分号匹配零个,一个或多个字符。(相当于平时正则匹配的*) _:下划线符号匹配单个字符。
**假如有这样一列code的值为'AAA','AAB','BAA','BAB' ,**如果where code like '%AB'条件,由于前面是 模糊的,所以不能利用索引的顺序,必须一个个去找,看是否满足条件。这样会导致全索引扫描或者全表扫 描。如果是这样的条件where code like 'A % ',就可以查找CODE中A开头的CODE的位置,当碰到B开头的 数据时,就可以停止查找了,因为后面的数据一定不满足要求。这样就可以利用索引了。
3.查询条件使用or连接,也会导致索引失效 要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
4.查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效;
5.对索引列进行运算 什么叫对索引列进行运算? select * from t where object_id/2=:id; 没有走索引
select * from t where object_id=:id*2;走了索引聚集索引和非聚集索引
前者数据按索引顺序存储,子结点存储真实的物理数据。将数据存储与索引放到了一块,找到索引也就找到了数据; 后者存储指向真正数据行的指针。将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行。 MYisam存储引擎通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因 ;
innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值
B Tree和B+Tree
索引的实现通常用B树和B+树。两者的区别: 1、Btree的关键字和记录是放在一起的(节点等于数据加记录),叶子节点可以看作外部节点,不包含任何信息;B+tree的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。 2、在Btree中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而b+tree中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。
索引实现原理分析(重点) 局部性原理与磁盘预读原理:索引一般以文件形式存储在磁盘上,索引检索需要磁盘I/O操作(极耗费时间)。所以一般以使用磁盘I/O次数来评价索引结构的优劣。 为了提高效率,就要尽量减少磁盘I/O。为了达到这个目的,磁盘每次读取一页大小的数据,通常是4K。
B Tree
先从B-Tree分析,根据B-Tree的定义,每一个节点是记录加上数据(红色部分),可知检索一次最多需要访问h-1个节点(根节点一直在内存中)。两种树的代码实现链接
数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。 为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐(避免去其他页寻找)的,就实现了一个node只需一次I/O。 B-Tree中一次检索最多需要h-1次I/O,渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。 (出度是指一个节点的子节点个数) 综上所述,如果我们采用B-Tree存储结构,搜索时I/O次数一般不会超过3次,所以用B-Tree作为索引结构效率是非常高的。B+Tree
B+树性能分析,只有叶子节点有数据,其他的都是记录,而且叶子节点有指向前后的指针,B树的搜索复杂度为O(h)=O(logdN),所以树的出度d越大,深度h就越小,I/O的次数就越少。由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,从而拥有更好的性能。B+树查找过程如下图所示:
B+Tree恰恰可以增加出度d的宽度,因为每个节点大小为一个页大小,所以出度的上限取决于节点内key和data的大小:dmax=floor(pagesize/(keysize+datasize+pointsize))//floor表示向下取整
B-树和B+树查找过程基本一致。 如上图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。 真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的。
这就可以很明显的看出B+树的优势:
- 单个节点可以存储更多的数据,减少磁盘I/O的次数。
- 查找性能更稳定,因为都是要查找到叶子结点。
- 叶子结点形成了有序链表,便于查询。
同时,由于叶子节点形成了从小到大的链表结构,因此B+树支持单元素查找和范围查找。
为什么说B+-tree比B树更适合实际应用中操作系统的文件索引和数据库索引? B+tree的磁盘读写代价更低:B+tree的内部结点并没有指向关键字具体信息的指针(红色部分),因此其内部结点相对B树更小。 如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。 一次性读入内存中的需要查找的关键字也就越多,相对来说IO读写次数也就降低了;B+tree的查询效率更加稳定:由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引,所以,任何关键字的查找必须走一条从根结点到叶子结点的路。 所有关键字查询的路径长度相同,导致每一个数据的查询效率相当;数据库索引采用B+树而不是B树的主要原因:**B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。 **
MySql
整体架构
Mysql主要分为Server层和存储引擎层: **Server 层:**主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。 存储引擎:主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。(InooDB、Myisam)
Server 层基本组件:主要完成权限检查、SQL语句的分析和优化。 连接器: 当客户端连接 MySQL 时,server层会对其进行身份认证和权限校验。 查询缓存: 执行查询语句的时候,会先查询缓存,先校验这个 sql ,就会直接返回给客户端,如果没有命中,就会执行后续的操作。 **分析器: **没有命中缓存的话,SQL 语句就会经过分析器,主要分为两步,词法分析和语法分析,先 看 SQL 语句要做什么,再检查 SQL 语句语法是否正确 优化器: 优化器对查询进行优化,包括重写查询、决定表的读写顺序以及选择合适的索引等,生成执行计划。 执行器: 首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会根据执行计划去调用引擎的接口,返回结果
执行计划?
通过 explain 命令获取 select 语句的执行计划,了解 select 语句以下信息: 1.表的加载顺序 2.sql 的查询类型 3.可能用到哪些索引,实际上用到哪些索引 4.读取的行数 5.。。。。
查询语句执行流程?
查询语句的执行流程如下:权限校验、查询缓存、分析器、优化器、执行器、引擎。 举个例子,查询语句如下: select * from user where id > 1 and name = '大彬'; 首先检查权限,没有权限则返回错误; 2. MySQL会查询缓存,缓存命中则直接返回,没有则执行下一步; 3. 词法分析和语法分析。提取表名、查询条件,检查语法是否有错误; 4. 两种执行方案,先查 id > 1 还是 name = '大彬' ,优化器根据自己的优化算法选择执行效率最 好的方案; 5. 校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果
日志bin、redo、undo logo
MySQL日志主要包括查询日志、慢查询日志、事务日志、错误日志、二进制日志等。其中比较重要的是 bin log(二进制日志)和 redo log(重做日志)和 undo log(回滚日志)。 bin log 二进制日志(bin log)是MySQL数据库级别的文件,记录对MySQL数据库执行修改的所有操作,不会 记录select和show语句,主要用于恢复数据库和同步数据库。 redo log 重做日志(redo log)是Innodb引擎级别,用来记录Innodb存储引擎的事务日志,不管事务是否提交都 会记录下来,用于数据恢复。当数据库发生故障,InnoDB存储引擎会使用redo log恢复到发生故障前的 时刻,以此来保证数据的完整性。将参数 innodb_flush_log_at_tx_commit 设置为1,那么在执行 commit时会将redo log同步写到磁盘。
1.bin log会记录所有日志记录,包括innoDB、MyISAM等存储引擎的日志;redo log只记录innoDB 自身的事务日志。
- bin log**只在事务提交前写入到磁盘,一个事务只写一次;**而在事务进行过程,会有redo log不断写 入磁盘。
- binlog **是逻辑日志,记录的是SQL语句的原始逻辑;**redo log 是物理日志,记录的是在某个数据页 上做了什么修改;
undo log 除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它保留了 **记录修改前的内容。**通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的 数据,实现MVCC。
SQL语句
exist和in
exists 用于对外表记录做筛选。
EXISTS用于检查子查询是否至少会返回一行数据,该子查询实际上并不返回任何数据,而是返回值True或False。 EXISTS可以和NOT一起使用:NOT EXIST。
exists 会遍历外表,将外查询表的每一行,代入内表查询进行判断。当 exists 里的条件语句能够返回记录 行时,条件就为真,返回外表当前记录。反之如果exists里的条件语句不能返回记录行,条件为假,则 外表当前记录被丢弃。 select a.* from A awhere exists(select 1 from B b where a.id=b.id)
in 是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。 select * from Awhere id in(select id from B) 子查询的表大的时候,使用exists可以有效减少总的循环次数来提升速度;当外查询的表大的时候,使 用IN可以有效减少对外查询表循环遍历来提升速度 ;
int(10)与char(10)?
int(10)中的10表示的是显示数据的长度,而char(10)表示的是存储数据的长度。
nt(M) M表示最大显示宽度。最大有效显示宽度是255。 显示宽度与存储大小或类型包含的值的范围无关。 在 int(M) 中,M 的值跟 int(M) 所占多少存储空间并无任何关系。** int(1)、int(4)、int(10) 在磁盘上都是占用 4 bytes 的存储空间**。
process list?
**show processlist 或 show full processlist **可以查看当前 MySQL 是否有压力,正在运行的 sql,有没有慢 SQL 正在执行。返回参数如下:
id - 线程ID,可以用: kill id; 杀死一个线程,很有用 db - 数据库 user - 用户 host - 连库的主机IP command - 当前执行的命令,比如最常见的:Sleep,Query,Connect 等 time - 消耗时间,单位秒,很有用 state - 执行状态 sleep,线程正在等待客户端发送新的请求 query,线程正在查询或者正在将结果发送到客户端 Sorting result,线程正在对结果集进行排序 Locked,线程正在等待锁 info - 执行的SQL语句,很有用
Mysql索引
归根结底,Mysql索引的问题都是基于B+树!
http://www.360doc.com/content/21/0927/10/62719136_997295723.shtml
索引哪些情况会失效
详解:https://baijiahao.baidu.com/s?id=1722813440785324670&wfr=spider&for=pc
1、like通配符会导致索引失效,注意:”ABC%”不会失效,会走range索引,“% ABC”和“%ABC%”索引会失效;
因为这种方式没法进行B+树的二分查询了,只能全表扫描;
**2、对索引字段进行函数运算/表达式运算(**对索引列运算(如,+、-、*、/) )。
因为索引保存的是索引原始值,函数计算过后自然不一样了,无法走索引。 从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。
3、隐式类型转换,会导致索引失效,例如 age字段类型是int,我们where age = “1”,这样就会触发隐式类型转换。
MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。 走全表扫描: //例子一的查询语句select * from t_user where phone = 1300000001; 这是因为 phone 字段为字符串,所以 MySQL 要会自动把字符串转为数字,所以这条语句相当于: select * from t_user where CAST(phone AS signed int) = 1300000001;也就是对索引使用了函数! 走索引扫描: //例子二的查询语句select * from t_user where id = "1"; 这时因为字符串部分是输入参数,也就需要将字符串转为数字,所以这条语句相当于: select * from t_user where id = CAST("1" AS signed int);索引字段并没有用任何函数,CAST 函数是用在了输入参数
4、查询条件包含or,会导致索引失效。
只要有条件列不是索引列,就会进行全表扫描。 反之,如果都是索引列,那么就不会失效
5、联合索引,查询时的条件列不是联合索引中左边第一个列,索引失效。
原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。 也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。
6、索引字段上使用**(!=或者<>(即不等), not in)时,会导致索引失效,或者is null 以及is not null都可能失效。**
索引不适合哪些场景
1、**索引的选择性(Cardinality)是指索引列中不同值的数目和表的记录数的比值。**假如表里面有1000条数据,表索引列有980个不同的值,这时候索引的选择性就是980/1000=0.98 。索引的选择性越接近1,这个索引的效率很高。 2、性别可以认为是3种,男,女,其他。如果创建索引,查询语句 性别=‘男’的数据,**索引的选择性就是3/1000=0.003。**索引的选择性值很低,对查询提升不大,所以性别建索引意义不大。
进一步分析: 为什么性别不适合建索引呢?因为你访问索引需要付出额外的IO开销,你从索引中拿到的只是地址,要想真正访问到数据还是要对表进行一次IO。假如你要从表的100万行数据中取几个数据,那么利用索引迅速定位,访问索引的这IO开销就非常值了。但如果你是从100万行数据中取50万行数据,就比如性别字段,**那你相对需要访问50万次索引,再访问50万次表,**加起来的开销并不会比直接对表进行一次完整扫描小。
当然凡事不是绝对,**如果把性别字段设为表的聚集索引(但实际上一般是主键),**那么就肯定能加快大约一半该字段的查询速度了。聚集索引指的是表本身中数据按哪个字段的值来进行排序。因此,聚集索引只能有一个,而且使用聚集索引不会付出额外IO开销。当然你得能舍得把聚集索引这么宝贵资源用到性别字段上。 原文链接:https://blog.csdn.net/Win32FanEx/article/details/79513857
数据量少的不适合加索引 更新比较频繁的也不适合加索引 离散性低的字段不适合加索引(如性别)
SQL语句执行时间太长,如何优化?
1、explain** 分析sql语句,查看执行计划,优化sql** 2、优化索引结构,看是否可以适当添加新的索引 3、数量大的表,可以考虑进行分离/分表(如交易流水表) 4、数据库主从分离,读写分离
什么是回表?
对于**非主键索引(除了主键索引都是非主键)查询方式而言,**一共搜索了两棵 B+Tree,第一次搜索 B+Tree 拿到主键值后再去搜索主键索引的 B+Tree,这个过程就是所谓的回表。
InnoDB必须要有,且只有一个聚集索引: (1)如果表定义了主键,则PK就是聚集索引; (2)如果表没有定义主键,则第一个非空唯一索引(not NULL unique)列是聚集索引; (3)否则,InnoDB会创建一个隐藏的row-id作为聚集索引;
什么是索引覆盖?
简单来说,就是索引中已经有了需要查询的列了,这是回表问题的补充!
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。explain的输出结果Extra字段为Using index时,能够触发索引覆盖。
将被查询的字段,建立到联合索引里去。 例子 create table user ( id int primary key, name varchar(20), sex varchar(5), index(name) )engine**=innodb; 第一个sql: select id,name from user where name='shenjian'; 能够命中name索引,索引叶子节点存储了主键id,通过name的索引树即可获取id和name,无需回表,符合索引覆盖,效率较高。 Extra:Using index。 第二个sql: select id,name,sex from user where name=**'shenjian';
能够命中name索引,索引叶子节点存储了主键id,没有储存sex,sex字段必须回表查询才能获取到,不符合索引覆盖,需要再次通过id值扫描聚集索引获取sex字段,效率会降低。 Extra:Using index condition。 如果把(name)单列索引升级为联合索引(name, sex)就不同了。 create table user1 ( id int primary key, name varchar(20), sex varchar(5), index(name, sex) )engine**=**innodb;
可以看到: select id,name ... where name='shenjian'; select id,name,sex ... where name='shenjian'; 单列索升级为联合索引(name, sex)后,索引叶子节点存储了主键id,name,sex,都能够命中索引覆盖,无需回表。 画外音,Extra:Using index。
什么是索引下推?(或称ICP优化)
MySQL 5.6引入了索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表字数。
假设有这么个需求,查询表中“名字第一个字是张,性别男,年龄为10岁的所有记录”。那么,查询语句是这么写的:
mysq> select * from tuser where name like '张 %' and age=10 and ismale=1;
根据前面说的**“最左前缀原则”,该语句在搜索索引树的时候,只能匹配到名字第一个字是‘张’的记录**(即记录ID3),接下来是怎么处理的呢?当然就是从ID3开始,逐个回表,到主键索引上找出相应的记录,再比对age和ismale这两个字段的值是否符合。如下图,需要回表4次。
分库分表
分库分表的概念 水平分库:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。 水平分表:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。 垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。 垂直分表:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。
分库分表可能遇到的问题 事务问题:需要用分布式事务啦 跨节点Join的问题:解决这—问题可以分两次查询实现 跨节点的count,order by,group by 以及聚合函数问题:分别在各个节点上得到结果后在应用程序端进行合并。 数据迁移,容量规划,扩容等问题 ID问题:数据库被切分后,不能再依赖数据库自身的主键生成机制啦,最简单可以考虑UUID 跨分片的排序分页问题(后台加大pagesize处理?)
Innodb体系结构
Mysql两种数据引擎的区别
MYISAM不支持外键和事务,支持表锁,插入数据时,锁定整个表,查表总行数时,不需要全表扫描; INNODB 支持外键和事务,行锁,查表总行数时,全表扫描;
InnoDB,默认存储引擎,事务安全型,缺点是锁表; MyISAM,基于ISAM存储引擎,不支持事务和外键,查询插入速度较高; Memory,将表中的数据放在内存中,提供快速访问,适用于较小的表;
可以说数据库必须有索引,没有索引则检索过程变成了顺序查找,O(n)的时间复杂度几乎是不能忍受的。 我们非常容易想象出一个只有单关键字组成的表如何使用B+树进行索引,只要将关键字存储到树的节点即可。 当数据库一条记录里包含多个字段时,一棵B+树就只能存储主键,如果检索的是非主键字段,则主键索引失去作用,又变成顺序查找了。这时应该在第二个要检索的列上建立第二套索引。
这个索引由独立的B+树来组织。有两种常见的方法可以解决多个B+树访问同一套表数据的问题,一种叫做聚簇索引(clustered index ),一种叫做非聚簇索引(secondary index)。 这两个名字虽然都叫做索引,但这并不是一种单独的索引类型,而是一种数据存储方式。 对于聚簇索引存储来说,行数据和主键B+树存储在一起,辅助键B+树只存储辅助键和主键,主键和非主键B+树几乎是两种类型的树。对于非聚簇索引存储来说,主键B+树在叶子节点存储指向真正数据行的指针,而非主键。
为了更形象说明这两种索引的区别,我们假想一个表如下图存储了4行数据。其中Id作为主索引,Name作为辅助索引。图示清晰的显示了聚簇索引和非聚簇索引的差异。
InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。 若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。第二步使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。MyISM使用的是非聚簇索引(辅助索引),非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。
我们重点关注聚簇索引,看上去聚簇索引的效率明显要低于非聚簇索引,因为每次使用辅助索引检索都要经过两次B+树查找,这不是多此一举吗?聚簇索引的优势在哪?
- 由于行数据和叶子节点存储在一起,这样主键和行数据是一起被载入内存的,找到叶子节点就可以立刻将行数据返回了,如果按照主键Id来组织数据,获得数据更快。
- 辅助索引使用主键作为"指针" 而不是使用地址值作为指针的好处是,减少了当出现行移动或者数据页分裂时辅助索引的维护工作,使用主键值当作指针会让辅助索引占用更多的空间,换来的好处是InnoDB在移动行时无须更新辅助索引中的这个"指针"。也就是说行的位置(实现中通过16K的Page来定位,后面会涉及)会随着数据库里数据的修改而发生变化(前面的B+树节点分裂以及Page的分裂),使用聚簇索引就可以保证不管这个主键B+树的节点如何变化,辅助索引树都不受影响。参考链接
- 聚簇索引适合用在排序的场合,非聚簇索引不适合
- 取出一定范围数据的时候,使用聚簇索引
- 二级索引需要两次索引查找,而不是一次才能取到数据,因为存储引擎第一次需要通过二级索引找到索引的叶子节点,从而找到数据的主键,然后在聚簇索引中用主键再次查找索引,再找到数据
- 可以把相关数据保存在一起。例如实现电子邮箱时,可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚簇索引,则每封邮件都可能导致一次磁盘 I/O。
MVCC
MVCC( Multiversion concurrency control ) ,多版本并发控制,就是同一份数据保留多版本的一种方式,进而实现并发控制。一种处理读写冲突(读写互相不阻塞的问题,每次更新都产生一个新的版本,读的话可以读历史版本)的手段。
在查询的时候,通过read view和版本链(undo log)找到对应版本的数据。 对于高并发场景,MVCC比行级锁更有效、开销更小
Readview
知道怎么实现可重复读的了吧?因为只有事务第一次select才更新read_view,整个事务范围内都不会再更新了,所以读取的内容都是一致的。
MySQL避免幻读?
在快照读情况下,MySQL通过mvcc来避免幻读。 在当前读情况下,MySQL通过next-key来避免幻读(加行锁和间隙锁来实现的)。 next-key包括两部分:行锁和间隙锁。行锁是加在索引上的锁,间隙锁是加在索引之间的。 Serializable 隔离级别也可以避免幻读,会锁住整张表,并发性极低,一般不会使用。
Mysql中使用不同索引的加锁影响:https://blog.csdn.net/weixin_37686415/article/details/114711276
MVCC( Multiversion concurrency control ) 就是同一份数据保留多版本的一种方式,进而实现并发 控制。在查询的时候,通过read view和版本链找到对应版本的数据。
对于使用READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;对于使用SERIALIZABLE隔离级别的事务来说,设计InnoDB的大叔规定使用加锁的方式来访问记录(加锁是啥我们后续文章中说哈);对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。为此,设计InnoDB的大叔提出了一个ReadView的概念,这个ReadView中主要包含4个比较重要的内容:
- m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
- max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
小贴士: 注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
- creator_trx_id:表示生成该ReadView的事务的事务id。小贴士:我们前边说过,只有在对表中的记录做改动时**(执行INSERT、DELETE、UPDATE这些语句时)**才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,**如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;**如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。
- read committed:每次执行select(即读数据)都会创建新的read_view,保证能读取到其他事务已经提交的修 改。 repeatable read:在一个事务范围内,第一次select时更新这个read_view,以后不会再更新,后 续所有的select都是复用之前的read_view。这样可以保证事务范围内每次读取的内容都一样,即 可重复读。
MVCC 实现原理如下: MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。 DB_TRX_ID :当前事务id,通过事务id的大小判断事务的时间顺序。 DB_ROLL_PRT :回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接 在一起构成undo log版本链。 DB_ROLL_ID :主键,如果数据表没有主键,InnoDB会自动生成主键。
快照读和当前读与幻读问题
快照读:读取的是快照版本。普通的SELECT就是快照读。通过MVCC来进行并发控制的,不用加锁。 当前读:读取的是最新版本。** UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE 是当前读**。 快照读情况下,InnoDB通过mvcc机制避免了幻读现象。而mvcc机制无法避免当前读情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。
首先MySQL事务隔离级别默认是RR(可重复读),但是会导致幻读问题。 快照读:简单的select操作,属于快照读,不加锁。(read view) select * from table where ?;
当前读:特殊的读操作,读取记录的最新版本,插入/更新/删除操作,属于当前读,需要加锁。 select * from table where ? lock in share mode; //加共享锁 select * from table where ? for update; // 加排他锁 insert into table values (…); update table set ? where ?; delete from table where ?;
- 幻读是啥?
事务A按照一定条件进行数据读取,期间事务B插入了相同搜索条件的新数据,事务A再次按照原先条件进行读取时(因为每次selcet,RP模式下都会创建新的Read_View,必然会获取最新得数据),发现了事务B 新插入的数据称为幻读。
MVCC不能解决幻读,next-key才可以,具体原因及实例参见下述链接:
https://blog.csdn.net/qq_31930499/article/details/110393988 https://blog.csdn.net/qq_35590091/article/details/107734005 https://draveness.me/mysql-innodb/
其实这里的锁,是**next-key locking(就是一个行锁+范围锁)**实现的。行锁不必说,就是更新的时候锁住这一行,这样别的事务就不能同时进行修改操作了。范围锁(gap lock)锁则是防止插入。
什么是next key lock? 所谓的next key lock就是一个行锁(record lock)+范围锁(gap lock),比如某一个辅助索引(比如上面的class_id),如果它有1,3,5这几个值,那么当我们使用next key lock的锁住class_id=1的时候,实际上锁住了(-无穷,1],或者锁住class_id=3的时候,实际上锁住的是(1,3],也就是一个左开右闭的区间。如果此时别的事务要在这个区间内插入数据,就会被阻塞住。这个锁一直到事务提交才会释放。因此,即使出现了上面图片里面这种情况,也可以保证前后两次去读的内容一致,因为对这个辅助索引上的锁是:“next key lock”,他会锁住一个区间。
但是注意,对于可重复读默认使用的就是next key lock,但是对于“唯一索引”,比如主键的索引,next key lock会降级成行锁,而不会锁住一个区间。因此,如果上面的事务1的update使用的是主键,事务2也使用主键进行插入,那么实际上事务2根本不会被阻塞,可以立即插入并返回。而对于非唯一索引,next key lock则不会降级。
数据库连接池(RAII)
(Resource Acquisition is Initialization,资源获取就是初始化)
- 在内部对象池中,维护一定数量的数据库连接,并对外暴露数据库连接的获取和返回方法。
- 资源重用,由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销
- 更快的系统响应速度,数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池内备用。此时连接池的初始化操作均已完成。
- 新的资源分配手段,**对于多应用共享同一数据库的系统而言,**可在应用层通过数据库连接的配置,实现数据库连接技术。
- 统一的连接管理,避免数据库连接泄露,较较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用的连接,从而避免了常规数据库连接操作中可能出现的资源泄露。
Redis
缓存:数据缓存、页面缓存(key-value的NoSQL数据库) 在开发网站的时候如果有一些数据短时间不会发生变化,而他们还要被频繁访问,为了提高用户的速度和降低网站的负载,就把这些数据放到一个获取速度更快的介质上,该行为就被称为数据的缓存。 该介质可以是文件、数据库、内存,内存经常用于数据缓存。 亿级存储的常见面试题:https://www.kancloud.cn/zatko/redis/2274386
在线Redis练习网站
https://try.redis.io/ 文章:https://www.cnblogs.com/wl-blog/p/15819426.html
基本数据对象及底层结构
字符串对象,底层是简单动态字符串SDC; 链表,底层是压缩列表或双向链表; 集合,底层是整数集合和字典; 有序集合,底层是压缩列表或跳表+字典; 哈希表,底层是压缩列表或字典; Hyperlog,用于基数统计,基本原理是利用hash来估算(布隆过滤器的计数版本),并通过稀疏和稠密存储来控制空间;
如果出现hash冲突,是怎么解决的呢? 插入到尾部; redis在扩容的时候执行 rehash 策略会保留新旧两个 hashtable 结构,查询时也会同时查询两个 hashtable。Redis会将旧 hashtable 中的内容一点一点的迁移到新的 hashtable 中,当迁移完成时,就会用新的 hashtable 取代之前的。当 hashtable 移除了最后一个元素之后,这个数据结构将会被删除。 正常情况下,当 hashtable 中元素的个数等于数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。如果 Redis 正在做 bgsave(持久化) 时,可能不会去扩容,因为要减少内存页的过多分离(Copy On Write)。但是如果 hashtable 已经非常满了,元素的个数达到了数组长度的 5 倍时,Redis 会强制扩容。
既然已有底层结构可以实现list、hash、zset键,为什么还要用ziplist呢? 当然是为了进一步节省内存空间!我们先来看看ziplist是如何压缩的。其内部的数据存储,可以不是都按照32位,而是根据数据的大小进行选择,以免空间浪费:https://blog.csdn.net/zgaoq/article/details/89710600
Redis事务以及ACID
MULTI、EXEC、DISCARD和 WATCH 命令是 Redis 实现事务的的基础。
Redis 事务的执行过程包含三个步骤:- 开启事务;
- 命令入队;
- 执行事务或丢弃;
transaction(事务):并不ACID,只满足:一致性和隔离性两个特性,其他特性是不支持的。 **为什么不满足A?**因为在事务执行过程中错了,Redis没有回滚机制。 简单总结:
- 命令入队(Redis的事务是单线程依次执行的)时就报错,会放弃事务执行,保证原子性;
- 命令入队时没报错,实际执行时报错,不保证原子性;
- EXEC 命令执行时实例故障,如果开启了 AOF 日志,可以保证原子性。
**为什么满足C?**一致性是事务的最终目的,原子性、隔离性、持久性都是为了实现一致性。简单来说,一致性就是为了保证操作前后的数据满足约束条件。https://www.cnblogs.com/wl-blog/p/15819426.html EXEC 执行前,入队报错 事务会被放弃执行,所以可以保证一致性。 EXEC 执行后,实际执行时报错 有错误的执行不会执行,正确的指令可以正常执行,一致性可以保证。 EXEC 执行时,实例故障 实例故障后会进行重启,这就和数据恢复的方式有关了,我们要根据实例是否开启了 RDB 或 AOF 来分情况讨论下。 如果我们没有开启 RDB 或 AOF,那么,实例故障重启后,数据都没有了,数据库是一致的。 如果我们使用了 RDB 快照,因为 RDB 快照不会在事务执行时执行。 所以,事务命令操作的结果不会被保存到 RDB 快照中,使用 RDB 快照进行恢复时,数据库里的数据也是一致的。 如果我们使用了 AOF 日志,而事务操作还没有被记录到 AOF 日志时,实例就发生了故障,那么,使用 AOF 日志恢复的数据库数据是一致的。 如果只有部分操作被记录到了 AOF 日志,我们可以使用 redis-check-aof 清除事务中已经完成的操作,数据库恢复后也是一致的。
满足隔离性吗?Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。 而且还有Watch机制。 WATCH 机制的作用是:在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了。 如果修改了,就放弃事务执行,避免事务的隔离性被破坏。
为什么不满足持久性? 如果 Redis 没有使用 RDB 或 AOF,那么事务的持久化属性肯定得不到保证。 如果 Redis 使用了 RDB 模式,那么,在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,数据丢失,这种情况下,事务修改的数据也是不能保证持久化的。 如果 Redis 采用了 AOF 模式,**因为 AOF 模式的三种配置选项 no、everysec 和 always **都会存在数据丢失的情况。 所以,事务的持久性属性也还是得不到保证。
**消息订阅(Pub/Sub):**实现实时消息系统,即时聊天、群聊等;
Redis为什么高效?
Redis服务器是一个单线程的事件驱动程序,服务器需要处理以下两类事件:
- 文件事件:Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象;服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作,比如连接accept,read,write,close等;
- 时间事件:Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象,比如过期键清理,服务状态统计等。
第一点,基于内存请求操作,速度快; 第二点,单线程,也就不用考虑冲突和线程同步(比如锁和信号量)线程切换等带来的性能损耗。(如果是多核,还可以多起几个Redis进程(需要绑定内核),反正是非关系数据库,而是key-value,只要知道放在哪就好(hash槽)) 第三点,I/O复用,非阻塞;redis使用多路复用(“多路”指的是多个网络连接,“复用”指的是复用同一个线程)技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。 参考(详细解释了高并发的原因): https://www.cnblogs.com/angelyan/p/10450885.html **劣势就在于:**无法发挥多核优势,但是这一点可以通过多进程来解决(新问题,解决分片);
Lazy Free?
若客户端向Redis发送一条耗时较长的命令,**比如删除一个含有上百万对象的Set键,**或者执行flushdb,flushall操作,Redis服务器需要回收大量的内存空间,导致服务器卡住好几秒,对负载较高的缓存系统而言将会是个灾难。为了解决这个问题,**在Redis 4.0版本引入了Lazy Free,将慢操作异步化,**这也是在事件处理上向多线程迈进了一步。
要解决慢操作,**可以采用渐进式处理(比如rehash),即增加一个时间事件,比如在删除一个具有上百万个对象的Set键时,**每次只删除大键中的一部分数据,最终实现大键的删除。但是,该方案可能会导致回收速度赶不上创建速度,最终导致内存耗尽。因此,Redis最终实现上是将大键的删除操作异步化,采用非阻塞删除(对应命令UNLINK),大键的空间回收交由单独线程实现,主线程只做关系解除,可以快速返回,继续处理其他事件,避免服务器长时间阻塞。
Redis在回收对象时,会先计算回收收益,只有回收收益在超过一定值时,**采用封装成Job加入到异步处理队列中,**否则直接同步回收,这样效率更高。回收收益计算也很简单,比如String类型,回收收益值就是1,而Set类型,回收收益就是集合中元素个数。
Redis引入多线程的原因
Redis的瓶颈? redis是基于内存,所以其中一个瓶颈就是内存大小; 另外,redis是单线程,命令的执行过程是:**发送命令、命令排队、命令执行、返回结果。**所以跟网络带宽还有关系,如果分开布置,影响就更大了。 Redis6.0中的多线程,也只是针对网络请求过程采用了多线程,而数据的读写命令仍然是单线程处理的。 但是我们前面也提到过,多路复用的IO模型本质上仍然是同步阻塞型IO模型。 Redis 的瓶颈并不在 CPU,而在内存和网络。 内存不够的话,可以加内存或者做数据结构优化和其他优化等,但网络的性能优化才是大头,网络 IO 的读写在 Redis 整个执行期间占用了大部分的 CPU 时间,如果把网络处理这部分做成多线程处理方式,那对整个 Redis 的性能会有很大的提升。 优化方向:
- 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式。
- **使用多线程充分利用多核,**典型的实现比如 Memcached。
所以总结起来,Redis 支持多线程主要就是两个原因:
- 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核。
- 多线程任务可以分摊 Redis 同步 IO 读写负荷。
那么,在引入多线程之后,如何解决并发带来的线程安全问题呢? 这就是为什么我们前面多次提到的"Redis 6.0的多线程只用来处理网络请求,而数据的读写还是单线程"的原因。 Redis 6.0 只有在网络请求的接收和解析,以及请求后的数据通过网络返回给时,使用了多线程。而数据读写操作还是由单线程来完成的,所以,这样就不会出现并发问题了。
实际上,Redis也并不是单线程的,比如生成RDB文件,就会fork一个子进程来实现,当然,这不是本文要讨论的内容。
string 字符串提供了原子操作incr实现计数,比如帖子访问量、同一IP访问量、点赞数量; hash表可以用来存储用户信息,进行快速修改和查阅;(Redis的hash内部是一个key-map结构,因此可以根据id直接获取批量信息,而不用序列化再反序列化) 使用场景?https://www.cnblogs.com/ottll/p/9470480.html list其实是双端链表,可以实现实现消息实时排序的功能; set集合(不重复的值),可以将好友、关注等存于集合,Redis提供了求交集并集等操作,求共同好友,抽奖等; Sorted set有序集合,带有权重,可以实现学生成绩插入排序,积分排序等;
过期策略与内存淘汰
缓存的一个最基本的概念:数据是会过期的,要么是你自己设置个过期时间,要么是redis自己给干掉。 那么如果设置了过期时间,Redis是如何删除的呢? Redis中过期key的删除策略,分为三种:定时删除、定期删除、惰性删除+内存淘汰机制。 1.定时删除是在设置key的过期时间的同时,会创建一个定时器(timer)。定时器在key的过期时间来临时,**立即执行对key的删除操作。**此种删除策略可以保证过期key会尽可能快的被删除,并释放过期key所占用的内存。 但是此种策略对CPU时间是最不友好的。在过期key比较多的情况下,删除过期key这一行为可能会占用相当一部分CPU时间,在内存不紧张但是CPU时间非常紧张的情况下,将CPU时间用在删除和当前任务无关的过期key上,无疑会对服务器的响应时间和吞吐量造成影响。
2.定期删除是每隔一段时间,程序就会对Redis数据进行一次检查,删除里面的过期key,至于要删除多少过期key,以及要检查多少个db,则是由Redis内部算法决定。Redis内部每隔一段时间执行一次删除过期key的操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。但是限制删除操作执行的时长和频率需要合理地设置,否则可能会演变成定时删除或者惰性删除。
3.惰性删除是定时删除和定期删除的折中处理方案。它放任key过期不管,但是每次获取key时,都会检查取得的key是否过期,如果过期,则删除该key;若没有过期,就返回该key的值。 此策略对CPU时间来说是最友好的,只在取出key时,才对key进行过期检查,即只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的key,不会在删除其他无关的过期key上花费任何CPU时间。 此策略的缺点是对内存是最不友好的。如果一个key已经过期,而这个key又仍然保留在db中,那么只要这个过期key不被删除,它所占用的内存就不会释放。例如,如果db中有非常多的过期key,而这些过期key又恰好没有被访问到的话,那它们也许永远也不会被删除,除非用户手动执行flushdb命令清空,这样会导致大量的无用的脏数据占用大量的
4.如果还是不够用,那么这时内存淘汰机制就出场了——所谓内存淘汰机制,就是当内存不够用的时候,redis采用何种策略对键值进行删除:
- noeviction:只返回错误,不会删除任何key。该策略是Redis的默认淘汰策略,一般不会选用。
- volatile-ttl:将设置了过期时间的key中即将过期(剩余存活时间最短)的key删除掉。
- volatile-random:在设置了过期时间的key中,随机删除某个key。
- allkeys-random:从所有key中随机删除某个key。
- volatile-lru:基于LRU算法,从设置了过期时间的key中,删除掉最近最少使用的key。
- allkeys-lru:基于LRU算法,从所有key中,删除掉最近最少使用的key。该策略是最常使用的策略。
- volatile-lfu:基于LFU算法,从设置了过期时间的key中,删除掉最不经常使用(使用次数最少)的key。
- allkeys-lfu:基于LFU算法,从所有key中,删除掉最不经常使用(使用次数最少)的key。
手写LRU算法:hash+双向链表 https://blog.csdn.net/hahachenchen789/article/details/83339582 LFU算法:unordered_map+set https://leetcode.cn/problems/lfu-cache/
持久化方式
Redis有两种持久化的方式,一种是默认的Snapshoting(快照RDB),另一种是append-only file(aof)。 前者是当redis需要持久化(执行SAVE或BGSAVE或是达到配置文件)时,redis或fork一个子进程,将数据写到磁盘的一个临时文件rdb中,写完之后就将数据库原本的rdb文件替代。 RDB备份条件和命令: 1、执行SAVE命令,在当前线程执行,会卡住 2、执行BGSAVE命令,在后台线程执行,马上返回 3、当符合用户给定的配置条件时Redis会自动将内存中的所有数据进行快照并存储在硬盘上。由两个参数构成:时间和改动的键的个数。当在指定的时间内被更改的键的个数大于指定的数值时就会进行快照。 这种途径优点在于定时备份,Redis效率高,但是缺点就是容易造成数据丢失,例如:5分钟备份一次,但是第8分时宕机了,那么就丢失了后面的3分钟数据。
后者是将执行数据库的每一个指令都追加进日志文件,当redis重启时根据日志文件来重建整个数据库的内容。 AOF机制提供了三种回写策略如下:
原文链接:https://blog.csdn.net/weixin_29749887/article/details/112366967
**Always(同步写回):**命令执行完成,立马同步的将日志写入磁盘 **Everysec(每秒写回):**命令执行完成后,先将日志写入 AOF 文件的内存缓冲区,每隔一秒把缓冲区中内容写入磁盘。 No(操作系统控制的写回):每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。 其实这三中写回策略都无法解决数据丢失的问题,分析如下: **同步写回:**基本不丢失数据,但是每步操作都会有一个慢速的落盘操作,不可避免的影响主线程性能。 每秒写回:采用一秒写一次到 AOF 日志文件中,但是一旦宕机还是会丢失一秒的数据。 **操作系统控制的写回:**在写完缓冲区之后则会写入磁盘,但是数据始终在缓冲区的时间内一旦宕机,数据还是会丢失。
主从模式
高并发基础框架:主从架构,一主多从。即master复制修改,且同步到slave端,slave负责读,即读写分离。 高可用:主从架构部署+哨兵机制任何一个实例宕机,自动会进行主备切换。
为了降低每个redis服务器的负载,我们可以设置多个服务器一起执行任务,做主从模式。 一个master服务器负责写数据,其他的slave服务器负责读数据,主服务器的数据也会自动的同步给其他的从服务器。
设置好slave服务器之后,slave服务器会与master创建链接,master在收到请求之后,会启动一个后台进程,创建当前redis保存数据的快照文件,同时也会收集redis所有的写操作并缓存成文件,master在完成整个镜像的备份之后,会把RDB数据文件发给slave,slave把文件保存到磁盘上,然后把数据加载到内存中,接着master会把所有的缓存的命令发给slave,salve会根据缓存的命令进行数据库重建。
总结一下: 1:slave服务器主动连接到master服务器**(salve从节点会定时检查是否有master需要连接),PSYNC命令。** 2:slave服务器发送SYCN命令到Master服务器请求同步 3:master服务器备份数据库到rdb文件 4:master服务器把rdb文件传输给slave服务器 5:slave服务器先将RDB文件写入磁盘,再从磁盘加载到内存中。 6:完成之后,接下来master服务器把用户所有更改数据的操作,通过命令的方式转发给所有的slave服务器,slave服务器执行master服务器发送过来的命令就可以完成整个同步的流程
哨兵机制
主从复制存在不能自动故障转移、达不到高可用的问题。哨兵模式解决了这些问题。通过哨兵机制可以自动切换主从节点。
**sentinal,**哨兵是redis集群架构中非常重要的一个组件,主要功能如下 (1)集群监控,负责监控redis master和slave进程是否正常工作 (2)消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。 (3)故障转移,如果master node挂掉了,会自动转移到slave node上 (4)配置中心,如果故障转移发生了,通知client客户端新的master地址 PS:设置哨兵,在master node宕机时,自动检测,然后将某个slave node自动切换成master node,进行主备切换,主备切换的事件可能几秒钟或者几分钟。哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作 (1)故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题 (2)即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很不稳定了
哨兵机制工作原理:
- (监听)每个 Sentinel 以每秒钟一次的频率向它所知道的 Master , Slave 以及其他 Sentinel 实例发送一个 PING 命令。
- **(断连)**如果一个实例距离最后一次有效回复 PING 命令的时间超过指定值, 则这个实例会被 Sentine 标记为主观下线。
- **(下线)**如果一个 Master 被标记为主观下线,则正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 是否真正进入主观下线状态。
- **(表决)**当有足够数量的 Sentinel (大于等于配置文件指定值)在指定的时间范围内确认 Master 的确进入了主观下线状态, 则 Master 会被标记为客观下线 。若没有足够数量的 Sentinel 同意 Master已经下线, Master 的客观下线状态就会被解除。 若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
- **(选举)**哨兵节点会选举出哨兵 leader,负责故障转移的工作。
- (转移)哨兵 leader 会推选出某个表现良好的从节点成为新的主节点,然后通知其他从节点更新主节点信息。
哨兵机制: (1)哨兵至少需要3个实例(见下),来保证自己的健壮性 (2)哨兵 + redis主从的部署架构,是不会保证数据零丢失(该宕机还是宕机,见本节备注)的,只能保证redis集群的高可用性 (3)对于哨兵 + redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练
PS:为什么redis哨兵集群只有2个节点无法正常工作? 如果哨兵集群仅仅部署了个2个哨兵实例,master宕机,s1和s2中会选举出一个哨兵来执行故障转移。同时这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2**(2的majority=2,3的majority=2,5的majority=3,4的majority=3),2个哨兵都运行着,就可以允许执行故障转移。但是如果整个M1(master)和S1(sentinal)运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移**,虽然另外一台机器还有一个R1,但是故障转移不会执行。
PPS:经典的3节点哨兵集群 如果M1所在机器宕机了,那么三个哨兵还剩下2个,S2和S3可以一致认为master宕机,然后选举出一个来执行故障转移。同时3个哨兵的majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移。PS1:两种数据丢失的情况 主备切换的过程,可能会导致数据丢失 (1)异步复制导致的数据丢失 因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了 (2)脑裂导致的数据丢失 脑裂,也就是说,某个master所在机器突然脱离正常网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。这个时候,集群里就会有两个master,也就是所谓的脑裂。
此时存在两个不同的master节点,就像一个大脑分裂成了两个。集群脑裂问题中,如果客户端还在基于原来的master节点继续写入数据,那么新的master节点将无法同步这些数据,当网络问题解决之后,sentinel集群将原先的master节点降为slave节点,**此时再从新的master中同步数据,将会造成大量的数据丢失。**旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。如何解决异步复制和脑裂导致的数据丢失? min-slaves-to-write 1 min-slaves-max-lag 10 要求至少有1个slave,数据复制和同步的延迟不能超过10秒。 如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了,通过这种方式上面两个配置可以减少异步复制和脑裂导致的数据丢失。 (1)减少异步复制的数据丢失 有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内 (2)减少脑裂的数据丢失 如果一个master出现了脑裂,跟其他slave断了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求 这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失 上面的配置就确保了,如果跟旧的master跟任何一个slave丢了连接,在10秒后发现没有slave给自己回复确认ack,那么就拒绝新的写请求。因此在脑裂场景下,最多就丢失10秒的数据。
集群
单机、集群、分布式等概念见下: https://www.cnblogs.com/howtobuildjenkins/p/8999441.html 集群其实就是运行多个实例,跟分布式不同的是,集群相当于增加硬件资源来使得整个系统性能更好(利用负载均衡),而分布式则是各个部分之间分门别类,各司其职,IPC通信。
**主从模式受限于主节点的写入能力,集群模式主要通过多个节点存储不同的内容来解决该问题。**原本的主从模式:
进阶的集群模式: 集群主要做两件事:- **自动数据分片,**分配给各个结点;
- **集群中部分节点失效,**也可以继续工作;
Redis cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。Redis cluster采用虚拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。
哈希槽映射
计算公式:slot=CRC16(key)&16383。 优点: 无中心架构,支持动态扩容; 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布; 高可用性。部分节点不可用时,集群仍可用。集群模式能够实现自动故障转移(failover),节点 之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色转换。
缺点: 不支持批量操作(pipeline)。 数据通过异步复制,不保证数据的强一致性。 事务操作支持有限,只支持多 key 在同一节点上的事务操作,当多个 key 分布于不同的节点上时无 法使用事务功能。 key 作为数据分区的最小粒度,不能将一个很大的键值对象如 hash 、 list 等映射到不同的节 点。 不支持多数据库空间,单机下的Redis可以支持到16个数据库,集群模式下只能使用1个数据库空 间。
客户端怎么访问集群?
Redis 集群会计算存储的 key 对应的槽和节点,如果对应的节点不是当前节点的话就会报 moved 重定向 错误,如图所示:
例如:127.0.0.1:7000> set name marklogzhu OK 127.0.0.1:7000> get name "marklogzhu" 127.0.0.1:7000> set name1 java (error) MOVED 12933 127.0.0.1:7002
ask 重定向 除了 moved 错误外,还有一个 ask 错误,它是指槽正在迁移过程中客户端进行访问所以需要重定向到新的节点上去。
redis-cli -c 那么集群的访问要怎么办呢,我们可以使用 redis-cli -c 命令来访问,它可防止moved和ask异常,会自动跳转到对应的 Redis 节点上去继续执行命令,我们来试试看:
[root@VM_0_15_centos redis4]# ./redis-cli -c -p 7003 127.0.0.1:7003> get name -> Redirected to slot [5362] located at 127.0.0.1:7000 "marklogzhu" 127.0.0.1:7000>
哈希算法、一致性、hash槽
前者是不便于集群扩展; 一致性hash算法又会因为数据分配不均匀造成部分节点压力过大;好处就在于可以自行分配节点所容纳的hash槽数量和范围;https://mp.weixin.qq.com/s/Gaf2NAbY3K0ZdEeJzAUIUA 简单hash算法
一致性hash算法 在1997年,麻省理工学院的Karger等人提出了一致性哈希算法,为的就是解决分布式缓存的问题。 在一致性哈希算法中,整个哈希空间是一个虚拟圆环 假设有四个节点Node A、B、C、D,经过ip地址的哈希计算,它们的位置如下 有4个存储对象Object A、B、C、D,经过对Key的哈希计算后,它们的位置如下 对于各个Object,它所真正的存储位置是按顺时针找到的第一个存储节点。例如Object A顺时针找到的第一个节点是Node A,所以Node A负责存储Object A,Object B存储在Node B。一致性哈希算法大概如此,那么它的容错性和扩展性如何呢? 假设Node C节点挂掉了,Object C的存储丢失,那么它顺时针找到的最新节点是Node D。也就是说Node C挂掉了,受影响仅仅包括Node B到Node C区间的数据,并且这些数据会转移到Node D进行存储。
同理,假设现在数据量大了,需要增加一台节点Node X。Node X的位置在Node B到Node C直接,那么受到影响的仅仅是Node B到Node X间的数据,它们要重新落到Node X上。 所以一致性哈希算法对于容错性和扩展性有非常好的支持。但一致性哈希算法也有一个严重的问题,就是数据倾斜。如果在分片的集群中,节点太少,并且分布不均,一致性哈希算法就会出现部分节点数据太多,部分节点数据太少。也就是说无法控制节点存储数据的分配。如下图,大部分数据都在A上了,B的数据比较少。
哈希槽 Redis集群(Cluster)并没有选用上面一致性哈希,而是采用了哈希槽(SLOT)的这种概念。主要的原因就是上面所说的,一致性哈希算法对于数据分布、节点位置的控制并不是很友好。首先哈希槽其实是两个概念,第一个是哈希算法。Redis Cluster的hash算法不是简单的hash(),而是crc16算法,一种校验算法。另外一个就是槽位的概念,空间分配的规则。
一致性哈希的空间是一个圆环,节点分布是基于圆环的,无法很好的控制数据分布。而Redis Cluster的槽位空间是自定义分配的,类似于Windows盘分区的概念。这种分区是可以自定义大小,自定义位置的。
Redis Cluster包含了16384个哈希槽,每个Key通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配。例如机器硬盘小的,可以分配少一点槽位,硬盘大的可以分配多一点。如果节点硬盘都差不多则可以平均分配。所以哈希槽这种概念很好地解决了一致性哈希的弊端。 另外在容错性和扩展性上,表象与一致性哈希一样,都是对受影响的数据进行转移。而哈希槽本质上是对槽位的转移,把故障节点负责的槽位转移到其他正常的节点上。扩展节点也是一样,把其他节点上的槽位转移到新的节点上。 但一定要注意的是,对于槽位的转移和分派,Redis集群是不会自动进行的,而是需要人工配置的。所以Redis集群的高可用是依赖于节点的主从复制与主从间的自动故障转移。
缓存击穿、穿透、雪崩、布隆过滤器
布隆过滤器的原理:其实就是一个一维向量+多个hash函数 当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询Redis和数据库 。 注意,原始的布隆过滤器无法删除,除非是带有计数版本的!即每个位置的1还多保留了一个当前位置的计数器。 注意,布隆过滤器有的,数据库中不一定有(hash冲突);布隆过滤器没有的,数据库一定没有!
Redission分布式锁实现原理
一言以蔽之,先用hash确定访问的节点(集群),在基于lua脚本完成原子性加锁,每隔一段时间延长加锁时间。 在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是用Redisson框架就好了,非常的简便易用。 下面给大家看一段简单的使用代码片段,先直观的感受一下:
注意,这个myLock,就是键值,比如钉钉文档中的docId。
Redission分布式锁的实现原理:
(1)加锁机制 咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。 这里注意,仅仅只是选择一台机器!这点很关键! 紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:为啥要用lua脚本呢? 因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。 为啥lua能保证原子性? Redis使用同一个Lua解释器来执行所有命令,同时,Redis保证以一种原子性的方式来执行脚本:当lua脚本在执行的时候,不会有其他脚本和命令同时执行,这种语义类似于 MULTI/EXEC。从别的客户端的视角来看,一个lua脚本要么不可见,要么已经执行完。
那么,这段lua脚本是什么意思呢? **KEYS[1]**代表的是你加锁的那个key,比如说: RLock lock = redisson.getLock("myLock"); 这里你自己设置了加锁的那个锁key就是“myLock”。 ARGV[1]代表的就是锁key的默认生存时间,默认30秒。 ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1
给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。 如何加锁呢?很简单,用下面的命令: hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1 通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
(2)锁互斥机制 那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢? 很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。 接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的**剩余生存时间。**比如还剩15000毫秒的生存时间。 此时客户端2会进入一个while循环,不停的尝试加锁。 (3)watch dog自动延期机制 客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢? 简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。 (4)可重入加锁机制 那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢? 这时我们来分析一下段lua脚本。
第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。 第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”此时就会执行可重入加锁的逻辑,他会用: incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1 通过这个命令,对客户端1的加锁次数,累加1。 此时myLock数据结构变为下面这样:
(5)释放锁机制 如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。 其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。 如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用: “del myLock”命令,从redis里删除这个key。 然后呢,另外的客户端2就可以尝试完成加锁了。 这就是所谓的分布式锁的开源Redisson框架的实现机制。 (6)上述Redis分布式锁的缺点 其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。 但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。 接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。 此时就会导致多个客户端对一个分布式锁完成了加锁。 这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。 **所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:**在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。如何优化分布式锁的性能?
今天就给大家聊一个有意思的话题:每秒上千订单场景下,如何对分布式锁的并发能力进行优化?
其他思路:库存超卖问题是有很多种技术解决方案的,比如悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作。
常规实现思路:
但是这有个缺陷,一个用户在操作的时候,其他用户只能疯狂轮询加锁,就相当于串行执行了! 在高并发的条件下,这种方式显然比较耗时,这个时候想到java里的ConcurrentHashMap的源码和底层原理,应该知道里面的核心思路,就是分段加锁! 把数据分成很多个段,每个段是一个单独的锁,所以多个线程过来并发修改数据的时候,可以并发的修改不同段的数据。 那么,我为什么不将库存也分段呢? 假如你现在iphone有1000个库存,那么你完全可以给拆成20个库存段,要是你愿意,可以在数据库的表里建20个库存字段,比如stock_01,stock_02,类似这样的,也可以在redis之类的地方放20个库存key。 每个下单请求锁了一个库存分段,然后在业务逻辑里面,就对数据库或者是Redis中的那个分段库存进行操作即可,包括查库存 -> 判断库存是否充足 -> 扣减库存。 ** 这时你得自动释放锁,然后立马换下一个分段库存,再次尝试加锁后尝试处理。**有什么不足?**实现太复杂了,需要自己在业务上实现这个分段。
- 首先,你得对一个数据分段存储,一个库存字段本来好好的,现在要分为20个分段库存字段;
- 其次,你在每次处理库存的时候,还得自己写随机算法,随机挑选一个分段来处理;
- 最后,如果某个分段中的数据不足了,你还得自动切换到下一个分段数据去处理。
Zookeeper实现分布式锁
什么是Zookeeper?
https://mp.weixin.qq.com/s/iHK6ZSnDr7s0IJ6CFk1RlQ https://zhuanlan.zhihu.com/p/59313297
简单来说,Zookeeper是一个开源的分布式协同服务系统,其他应用只要使用Zookeeper提供的接口,就可以实现各种分布式应用。例如:分布式锁、分布式选举,主从切换等等。
从本质上讲,Zookeeper的数据模型是层次模型,如下所示。
总体来说,Znode节点可以分为以下四类。 一个Znode节点可以是持久性的,也可以是临时性的。- 持久性的Znode:创建节点后即使Zookeeper集群宕机,或者Zookeeper客户端宕机,节点也不会丢失。
- 临时性的Znode:Zookeeper客户端宕机或者客户端在指定的超时时间内没有给Zookeeper集群发送消息,那么这个节点就会消失。
Znode节点也可以是顺序性的,所谓的顺序性,就是指每个节点会关联一个唯一的单调递增整数,这个单调递增的整数就是Znode节点名称的后缀,比如:/app1/p_1,/app1/p_2等,由此,Znode又有如下两种分类:
- 持久顺序性的Znode:除了具备持久性的Znode的特性之外,Znode的名称还具备顺序性。
- 临时顺序性的Znode:除了具备临时性的Znode的特性之外,Znode的名称还具备顺序性。
实现分布式锁
基于比较常用的Curator这个开源框架,聊一下这个框架对ZooKeeper(以下简称zk)分布式锁的实现。 参考下图,zk里有一把锁,这个锁就是zk上的一个节点。然后呢,两个客户端都要来获取这个锁,具体是怎么来获取呢?
假设客户端A抢先一步,对zk发起了加分布式锁的请求,这个加锁请求是用到了zk中的一个特殊的概念,叫做**“临时顺序节点”。**简单来说,就是直接在"my_lock"这个锁节点下,创建一个顺序节点,这个顺序节点有zk内部自行维护的一个节点序号。 他会查一下"my_lock"这个锁节点下的所有子节点,并且这些子节点是按照序号排序的,这个时候他大概会拿到这么一个集合: 接着客户端A会走一个关键性的判断,就是说:唉!兄弟,这个集合里,我创建的那个顺序节点,是不是排在第一个啊? 如果是的话,那我就可以加锁了啊!因为明明我就是第一个来创建顺序节点的人,所以我就是第一个尝试加分布式锁的人啊!bingo!加锁成功!大家看下面的图,再来直观的感受一下整个过程。 而客户端b创建的临时顺序节点: 然后查看集合自己不是第一个,所以不能进行加锁。加锁失败以后,客户端B就会通过ZK的API,对他的上一个顺序节点加一个监听器。zk天然(顺序递增啊)就可以实现对某个节点的监听。 如果A的节点被删除了,B获取集合发现自己第一!那么就可以加锁! 其实如果有客户端C、客户端D等N个客户端争抢一个zk分布式锁,原理都是类似的。
- 大家都是上来直接创建一个锁节点下的一个接一个的临时顺序节点
- 如果自己不是第一个节点,就对自己上一个节点加监听器
- 只要上一个节点释放锁,自己就排到前面去了,相当于是一个排队机制。
而且用临时顺序节点的另外一个用意就是,如果某个客户端创建临时顺序节点之后,不小心自己宕机了也没关系,zk感知到那个客户端宕机,会自动删除对应的临时顺序节点,相当于自动释放锁,或者是自动取消自己的排队。
用Curator框架进行加锁和释放锁的一个过程:
很简单,但是背后的原理就是上述的过程。如何保证缓存与数据库的一致性
这里所说的一致性,大多是指数据同时存在缓存和数据库。 1、先删除缓存再更新数据库 进行更新操作时,先删除缓存,然后更新数据库,后续的请求再次读取时,会从数据库读取后再将新数据更新到缓存。 **存在的问题:**删除缓存数据之后,更新数据库完成之前,**这个时间段内如果有新的读请求过来,就会从数据库读取旧数据重新写到缓存中,再次造成不一致,并且后续读的都是旧数据。 **
2、先更新数据库再删除缓存 进行更新操作时,先更新MySQL,成功之后,删除缓存,后续读取请求时再将新数据回写缓存。 存在的问题:更新MySQL和删除缓存这段时间内,请求读取的还是缓存的旧数据,不过等数据库更新完成,就会恢复一致,影响相对比较小。
3、异步更新缓存 数据库的更新操作(binlog)完成后不直接操作缓存,而是把这个操作命令封装成消息扔到消息队列中,然后由Redis自己去消费更新数据,消息队列可以保证数据操作顺序一致性,确保缓存系统的数据正常。
常见面试题
Redis实现点赞、取消功能?
简单来说,用户发起点赞、取消点赞后先存入 Redis 中,再每隔**10min(可设定)**从 Redis 读取点赞数据写入数据库中做持久化存储。点赞、取消点赞是高频次的操作,若每次都读写数据库,大量的操作会影响数据库性能,所以需要做缓存。项目需求需要查看都谁点赞了,所以要存储每个点赞的点赞人、被点赞人,不能简单的做计数。
Redis与Memcache的区别
Mysql与Redis的区别
(1)类型上 从类型上来说,mysql是关系型数据库,关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织,易于维护。redis是缓存数据库,数据存储结构是key-value形式,有string、list、set、zset、hash类型,易于扩展。 (2)作用上 mysql用于持久化的存储数据到硬盘,功能强大,但是速度较慢 redis用于存储使用较为频繁的数据到缓存中,读取速度快,持久化设置有RDB、AOF方式。 (3)需求上 mysql和redis因为需求的不同,一般都是配合使用。推理到redis+mysql,它是内存+磁盘关系的一个映射,mysql放在磁盘,redis放在内存,这样的话,web应用每次只访问redis,如果没有找到的数据,才去访问Mysql。
ElasticSearch
基本原理
https://blog.csdn.net/weixin_36378508/article/details/120671700 倒排索引、分词Term dictionary、Trie字典树(FST、有限状态机)。
- 首先需要经过分词Term,建立倒排索引。
- 在查询的时候,为了能够快速找到想要查询的词语,将所有的分词Term进行排序,也就是所谓的Term Dictionary分词字典。
- 但是如果分词Term太多,那么Term Dictionary也是很大的,会占用较大的内存,为了减小内存占用,加快查询速度,建立了分词索引。它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。
- 为了进一步压缩分词索引,采用FST数据结构来进行状态压缩。
FST(有限状态机
FInite state Transducer
假设我们现在要将mop, moth, pop, star, stop and top(term index里的term前缀)映射到序号:0,1,2,3,4,5(term dictionary的block位置)。 最简单的做法就是定义个Map<string, integer="">,大家找到自己的位置对应入座就好了,但从内存占用少的角度想想,有没有更优的办法呢? 答案就是:FST. FST一种特殊类型的字典树。
⭕️表示一种状态 –>表示状态的变化过程,上面的字母/数字表示状态变化和权重 将单词分成单个字母通过⭕️和–>表示出来,0权重不显示。如果⭕️后面出现分支,就标记权重,最后整条路径上的权重加起来就是这个单词对应的序号。
FST以字节的方式存储所有的term,这种压缩方式可以有效的缩减存储空间,使得term index足以放进内存,但这种方式也会导致查找时需要更多的CPU资源。 (未完待续)https://www.codercto.com/a/44517.html
索引优化
当postion list比较大时,也就是说某一个分词存在多个对应的查询行时(比如男/女,可能每个都对应了一半的数据),为了减小内存消耗,提高速度,可以采用增量编码。 Frame Of Reference:增量编码压缩,将大数变小数,按字节存储 首先,Elasticsearch要求posting list是有序的(为了提高搜索的性能,再任性的要求也得满足),这样做的一个好处是方便压缩。
有点类似于Redis的整数集合的操作(压缩列表的话,每一个节点的存储bit都不一样,而整数集合是相同的)
[1,3,4,7,10]对应的bitmap就是:[1,0,1,1,0,0,1,0,0,1],对应的bit值是1,这样用一个字节就可以代表8个文档id 旧版本(5.0之前)的Lucene就是用这样的方式来压缩的,1亿个文档需要12.5MB的存储空间,这仅仅是一个索引字段。
Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps需要打破这个魔咒就一定要用到某些指数特性: 将posting list按照65535为界限分块,比如第一块所包含的文档id范围在0–65535之间,第二块的id范围是65536–131071,以此类推。再用<商,余数>的组合表示每一组id,这样每组里的id范围都在0~65535内了,剩下的就好办了,既然每组id不会变得无限大,那么我们就可以通过最有效的方式对这里的id存储。
“为什么是以65535为界限?” 程序员的世界里除了1024外,65535=2^16-1,正好是用2个字节能表示的最大数,一个short的存储单位,注意到上图里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大块,用节省点用bitset存,小块就豪爽点,2个字节我也不计较了,用一个short[]存着方便。
为什么用4096来区分大块还是小块呢? 个人理解:都说程序员的世界是二进制的,4096*2bytes = 8192bytes < 1KB, 磁盘一次寻道可以顺序把一个小块的内容都读出来,再大一位就超过1KB了,需要两次读。
联合索引的原理
利用跳表(Skip list)的数据结构快速做“与”运算,或者利用上面提到的bitset按位“与”。 假设有下面三个posting list需要联合索引: 如果使用跳表,对最短的posting list中的每个id,逐个在另外两个posting list中查找看是否存在,最后得到交集的结果。如果使用bitset,就很直观了,直接按位与,得到的结果就是最后的交集。
MongoDB
MongoDB与Mysql的区别
关系型数据库(mysql) 与 非关系型数据库(mongodb)的区别:
Mysql是关系型数据库,主要采用结构化的存储方式。 MongoDB是NoSQL数据库,主要采用Key-Value键值对的方式进行存储,主要应对需要数据项动态增加的业务场景(比如文档业务)。
应用场景
MongoDB的应用场景
1)表结构不明确且数据不断变大 MongoDB是非结构化文档数据库,扩展字段很容易且不会影响原有数据。内容管理或者博客平台等,例如圈子系统,存储用户评论之类的。 2**)更高的写入负载** MongoDB侧重高数据写入的性能,而非事务安全,适合业务系统中有大量“低价值”数据的场景。本身存的就是json格式数据。例如做日志系统。 3)数据量很大或者将来会变得很大 Mysql单表数据量达到5-10G时会出现明细的性能降级,需要做数据的水平和垂直拆分、库的拆分完成扩展,MongoDB内建了sharding、很多数据分片的特性,容易水平扩展,比较好的适应大数据量增长的需求。 4)高可用性 自带高可用,自动主从切换(副本集) 不适用的场景 1)MongoDB不支持事务操作,需要用到事务的应用建议不用MongoDB。 2)MongoDB目前不支持join操作,需要复杂查询的应用也不建议使用MongoDB。
关系型数据库和非关系型数据库的应用场景对比
关系型数据库适合存储结构化数据,如用户的帐号、地址: 1)这些数据通常需要做结构化查询,比如join,这时候,关系型数据库就要胜出一筹 2)这些数据的规模、增长的速度通常是可以预期的 3)事务性、一致性
NoSQL适合存储非结构化数据,如文章、评论: 1)这些数据通常用于模糊处理,如全文搜索、机器学习 2)这些数据是海量的,而且增长的速度是难以预期的, 3)根据数据的特点,NoSQL数据库通常具有无限(至少接近)伸缩性 4)按key获取数据效率很高,但是对join或其他结构化查询的支持就比较差
三种存储引擎
MongoDB支持三种存储引擎,分别是MMAPv1、WiredTiger和Inmemory。
其中,MMAPv1在MongoDB4.0版本已经丢弃,现阶段默认的存储引擎是WiredTiger。 另外,InMemory存储引擎用于将数据只存储在内存中,只将少量的元数据(meta-data)和诊断日志(Diagnostic)存储到硬盘文件中,由于不需要Disk的IO操作,就能获取所需的数据,InMemory存储引擎大幅度降低了数据查询的延迟(Latency)。
WiredTiger和LSM Tree
LSM-Tree并不只是一个数据结构,而是一套算法,定义了三个规则:
- 定义内存数据结构
- 定义磁盘数据结构
- 定义读写过程,或者说是这两种数据结构的转换过程。
LSM Tree设计的初衷就是尽可能让磁盘文件顺序读写,因为对于磁盘而言,顺序读写比随机读写要快得多。
LSM tree 持久化到硬盘上之后的结构称为 Sorted Strings Table (SSTable)。- SSTable 保存了排序后的数据(实际上是按照 key 排序的 key-value 对)。
- 每个 SSTable 可以包含多个存储数据的文件,称为 segment,见下图,是以key作为排序依据的。
- 每个 segment 内部都是有序的,但不同 segment 之间没有顺序关系。
- 一个 segment 一旦生成便不再修改(immutable)(这也是重要的前提)。
LSM-Tree写数据的基本过程如下:
- 当收到一个写请求时,会先把该条数据记录在WAL Log里面,用作故障恢复。
- 当写完WAL Log后,会把该条数据写入内存的SSTable里面**(删除是墓碑标记,更新是新记录一条的数据)**,也称Memtable。注意为了维持有序性在内存里面可以采用红黑树或者跳跃表(mongodb这里是不是就是用的B+树来管理?基于page的管理)相关的数据结构。
- 当Memtable超过一定的大小后,会在内存里面冻结,变成不可变的Memtable,同时为了不阻塞写操作需要新生成一个Memtable继续提供服务。
- 把内存里面不可变的Memtable给dump到到硬盘上的SSTable层中,此步骤也称为Minor Compaction,这里需要注意在L0层的SSTable是没有进行合并的,所以这里的key range在多个SSTable中可能会出现重叠,在层数大于0层之后的SSTable,不存在重叠key。
- 当每层的磁盘上的SSTable的体积超过一定的大小或者个数,也会周期的进行合并。此步骤也称为Major Compaction,这个阶段会真正的清除掉被标记删除掉的数据以及多版本数据的合并,避免浪费空间,注意由于SSTable都是有序的,我们可以直接采用merge sort进行高效合并。
硬盘数据结构
mongodb在磁盘上的目录里面有一个db的目录。然后以数据库的名称为文件夹,文件夹下又分为两个目录:collection和index。每个collection会形成一个物理文件:XXX.wt。 db --db_name1 --db_name2 ----collection ------xxx.wt ----index ------xxx.wt 在往里边插入数据的时候: a. 数据存储以extent为单位。(在wiredtiger引擎中,每个键值对中都是使用bson结构来储存) b. 每个extent包含了一个page header,一个block header,然后就是具体的数据。 c. extent里面具体的数据就是以key-value的形式保存,每个cell或者value都是包含一个cell的元数据,再加上真实的数据 d. 如果真实数据比较大的话,会以一个指针链接的方式指向其他的extent。
内存数据结构
主要结构是BTree,为什么不是B+Tree?因为MongoDB中的仅仅是部分新数据,并不涉及磁盘的IO?
也就是说,wiredTiger在内存中的主要数据结构就是B树(B树的结构可以参考: https://blog.csdn.net/pcgamer/article/details/107156865?spm=1001.2014.3001.5502 )。每个B树的节点称作一个Page,也是wiredTiger调度的基本单位。在B树中,总共分成三种Page: Root Page: 根Page。 Internal Page:这个会存在多层,不保存真正的业务数据,只保存下一层Page节点的地址指针。 Leaf Page:保存真正的业务数据最主要的还是Mysql与MongoDB面向的业务不同,Mysql中范围查询较多,MongoDB较少,它更看重查询单个文件(Key-Value)见:https://www.yisu.com/zixun/494591.html
分片
系统扩充有两种方法:垂直扩展和水平扩展 垂直扩展 提高单台服务器的容量,例如使用更强的cpu,加内存,存储空间。技术的局限限制了单台机器面对高负载的能力。所以垂直扩展有明显的天花板。 水平扩展 将系统的数据和负载分配到多台服务器,根据实际需要添加机器。代价是增加了架构和部署的复杂性。 mongodb 通过 分片 支持水平扩展
mongodb 的分片集群由以下组成:
- 分片 shard: 每个分片包含总量数据的子集,每个分片可以部署为一个副本set ( replicate set).
- mongos: 作为一个查询路由器, 给分片集群向客户端提供一个接口 .
- config servers: 存储集群的元数据和配置.
分片Key
mongodb 使用 shard key 来将数据集中的文档分到不同的shard中。shard key 由一个或多个字段组成,这些字段在数据集的每份文档都有. 尽管不能修改shardkey用的字段,mongodb 4.2 开始,可以修改文档的shardkey的值,除非shardkey用的是 _id 。
mongodb将数据分片为 Chunks。每个Chunk在shardkey上的范围是 [下限, 上限) 。
分片策略
Hash分片 要计算shardkey 对应值的hash值. 每个数据块 chunk 被分配一个hash 后的值的范围 .当使用被hash的索引查询时,mongodb 字段计算hash值。
然而,hash的分布意味着 shardkey的范围查询 不太可能路由到同一个shard, 可能路由到更多的集群。
范围分片 基于shardkey的值将数据分成多个范围,每个 chunk 分配到其中一个范围
MongoDB Change Stream
https://cloud.tencent.com/developer/article/1711794
常见面试题
uuid与自增id的区别?雪花算法?分布式iD?
首先,uuid是无序的,自增id是有序的,为了应对业务场景的变化,通常将主键的值设置为与业务无关的量,也就是本文所讨论的自增id和UUID。 自增ID简单的说就是在向表中插入一条数据时不用自己设置id的值,数据库引擎会自动根据表中的数据的id+1进行填充。相比UUID需要128位进行存储,自增id通常仅需要使用64位的无符号整型(0,18446744073709551615)就可以满足绝大部分的场景需求。
UUID——通用唯一标识码(Universally Unique Identifier)对数据库而已生成的UUID值都近乎随机。(时间戳、mac地址、hash、随机数等) 由于自增id是有序的,索引的用处更大,性能更好,且存储空间要求较小。 但是自增id在不同的数据库可能重复,因此在分布式环境下uuid更适合! 因此:如果数据量非常大需要分库,或者需要更好的安全性,那么使用UUID 对于非敏感数据或者数据量没有大需要分库,使用自增id能节省存储空间并获得更好的性能
其次,雪花算法是推特开源的分布式ID生成算法。一共64bit
- 首位无效符:第一个 bit 作为符号位,因为我们生成的都是正数,所以第一个 bit 统一都是 0。
- **时间戳:**占用 41 bit ,精确到毫秒。41位最好可以表示2^41-1毫秒,转化成单位年为 69 年。
- **机器编码:**占用10bit,其中高位 5 bit 是数据中心 ID,低位 5 bit 是工作节点 ID,最多可以容纳 1024 个节点。
- **序列号:**占用12bit,每个节点每毫秒0开始不断累加,最多可以累加到4095,一共可以产生 4096 个ID。
雪花算法保证: 1.所生成的ID按时间递增 2.整个分布式系统不会有重复的ID
分布式id的解决方案?
我们先看看常见的分布id解决方案以及各自特点的对比 1.UUID 这种方案复杂度最低,但是会影响存储空间和性能 2.利用单机数据库的自增主键,作为分布式ID的生成器,复杂度适中,ID长度较UUID更短,但是受到单机数据库性能的限制,并发量大的时候,该方案也不是最优方案。 3.利用redis,zookeeper的特性来生成id,如:redis的自增命令,zookeeper的顺序节点,这种方案和单机数据库(mysql)性能相比,性能会有所提高,可以适当选用。 **4.雪花算法:**一切问题如果能直接用算法解决,那是最合适的,利用雪花算法可以生成分布式Id,其底层原理就是通过某台机器在某一毫秒内对某一个数字自增,这种方案也能保证分布式架构中的系统id唯一,但是只能保证趋势递增。 下面我们具体看看这些方案的对比:
InnoDB如何解决幻读?
RR隔离级别,MVCC加上next-key lock; https://blog.csdn.net/qq_33330687/article/details/89004462
缓存穿透、缓存雪崩、缓存击穿。
缓存穿透,是指经过缓存层,直接去数据库层查询一个数据库中不一定存在的数据; ** 正常使用缓存查询数据的流程是,依据key去查询value,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。** 如果数据库查询对象为空,则不放进缓存。** 如果每次都查询一个不存在value的key,由于缓存中没有数据,所以每次都会去查询数据库; 当对key查询的并发请求量很大时,每次都访问DB,很可能对DB造成影响; 并且由于缓存不命中,每次都查询持久层,那么也失去了缓存的意义。 ** 解决路径:
- **缓存空值,**即缓存空的value,以免下次遇到同样的key去访问DB,可能造成空间浪费,因此使用较短的过期时间;
- **布隆过滤器,**将数据库中所有key放入布隆过滤器中,即一个key过来,先hash看看布隆过滤器中有没有,有才会进行后续的缓存及数据库,如果没有,直接over;
布隆过滤器可能会误判,如果它说不存在那肯定不存在,如果它说存在,那数据有可能实际不存在; Redis的bitmap只支持2^32大小,对应到内存也就是512MB,误判率万分之一,可以放下2亿左右的数据,性能高,空间占用率及小,省去了大量无效的数据库连接。 布隆过滤器还可以作为推荐去重,链接
缓存雪崩(雪崩),是指在一段时间内,缓存集中过期失效,这样会发生大量的缓存穿透,大量访问直接落到数据库上,对DB造成压力; 解决方式:
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
例如电商环境,对于不同热度的商品,缓存不同的周期,在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。
3.做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
**缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,**由于缓存的构建需要一定时间,当缓存失效后需要重新从数据库中获取数据构建该缓存,**当这个key在失效的瞬间,持续的大并发就穿破缓存,**直接请求数据库,就像在一个屏障上凿开了一个洞。
ps:缓存穿透是由于大量查询一个不存在的key,缓存击穿是大量查询单一个key。
解决方式:
- 使用互斥锁(mutex key):这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。
- "提前"使用互斥锁(mutex key):在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。
- "永远不过期":** 这里的“永远不过期”包含两层意思:**
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。 (2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。
- 资源保护:可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。
以上解决方式的具体代码实现链接
SQL注入
在提交表单时,输入的其实是sql语句,使服务器执行恶意的sql语句,实现无密码登录乃至损坏服务器; https://www.cnblogs.com/myseries/p/10821372.html
布隆过滤器
布隆过滤器可以用于检索一个元素是否在一个集合中。可以肯定不在,但是不能确定一定在,主要目的是减少访问数据库的次数。 它实际上是一个很长的二进制向量(只有一个!)和一系列随机映射函数。
往里边添加key的时候,数组相应位置都要进行置1,因此不能够进行删除,否则你可能会将其他元素的hash值给删除了!如果想删除的话,那就采用计数器的方法,即每一个槽增加1的时候计数器加一,删除的时候减去1即可。 不过这种方式需要维护另外一个容器,占用内存。
我们可以得到一个结论:布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在。 **优点:**优点很明显,二进制组成的数组,占用内存极少,并且插入和查询速度都足够快。 **缺点:**随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。
为什么索引采用B+而不是平衡二叉树?
当记录较多时,采用平衡二叉树就会出现深度较高的情况,这样检索起来O(lgN)的效率较低(涉及磁盘IO),而B+树则是多路平衡树,每个节点可以存储多个数据,通过定位以后,如果在叶节点之前没找到,在相应的叶子节点中通过二叉查找,效率较高。 参考:https://blog.csdn.net/qq_36520235/article/details/94317993
Redis的分布式锁了解嘛?简单说一下?
简单来说就是某一个客户端1获取了锁,为防止死锁就设置一个过期时间;这个时候如果其他的客户端2想要获取这个锁,就循环等待。
这就不得不说到Redission开源框架,解决了锁无法重入的问题,以及高并发情况下的各种故障问题。客户端与Redis Master之间通过Lua(实现原子操作)脚本实现加锁和时间设置,同时判断id是否是对应的锁拥有者,如果锁被占据,则自旋等待;参考:链接
简单参考: https://www.jianshu.com/p/47fd7f86c848 在主从复制及集群的时候,容易卖坑,因为matser在将该锁异步分配给slave的时候,宕机了,造成复制操作终止,这时候其他的客户端加锁成功,但是客户端A也以为自己成功了?! 链接
Redis的常用架构模式?
常有四种架构: 单机:无法高并发; 主从:无法保证高可用,写压力大; 哨兵:主从加哨兵,即可完成faildover; 集群:异步复制,可能数据有误;(涉及hash槽) 参考:链接
为什么不能用uuid作为唯一索引?
主键,唯一索引,联合索引的区别? 主键是一种约束条件,唯一索引是一种索引。
数据库主键,指的是一个列或多列的组合,其值能唯一地标识表中的每一行,通过它可强制表的实体完整性。主键主要是用与其他表的外键关联,以及文本记录的修改与删除。 主键生成的时候会自动生成特殊索引,即主键索引,一种特殊的唯一索引。 最主要区别:1、唯一性索引列允许空值, 而主键索引列不允许为空值。 2、一个表中只有一个主键索引,而唯一索引可以有多个。
联合索引? 联合索引指的是对一张表上的多个列进行索引。也就是说,表上多个列加起来组成一个索引,供快速查询使用。
首先,我们需要看一下联合索引内部的结果:从本质上说,联合索引还是一个B+树,不过联合索引的键值数量不是1, 而是大于等于2. 我们一个两列联合索引假定两个键值分别为key1,key2,则其B+树结构如下图:
叶子上的数字顺序逻辑读取。为什么uuid不能作为主键? uuid具有随机性,且长度较大,因此会造成page分页(页的分裂和合并),且造成索引的存储空间增大,排序较难。 进一步讲解: https://blog.csdn.net/chenwiehuang/article/details/123420278