一、MySQL
1.什么是索引、有什么好处
①什么是索引?常见的索引分类有哪些?
索引是存储引擎用于快速找到记录的一种数据结构。
按照叶子节点是否存储了完整的数据行,可以分为聚簇索引和非聚簇索引,
聚簇索引叶子节点存储了完整的行记录数据,非聚簇索引叶子节点没有存储了完整的记录,只存储了索引列和主键/指向数据行的指针;
按照索引键值类型,又可以分为主键索引和二级索引,
在InnoDB引擎中,主键索引就是聚簇索引,其叶子节点中存储了全部数据;二级索引就是非聚簇索引,其叶子节点中存储了索引列的值和主键;
在MyISAM引擎中主键/二级索引都是非聚簇索引,其叶子节点只中存储了索引列和指向完整数据行的指针;
其它的索引还有覆盖索引和联合索引:
覆盖索引:指二级索引中索引列的数据刚好可以满足本次查询所需的数据,无需再通过主键去主键索引中回表查询完整的行记录,这个过程就称为覆盖索引;
联合索引:使用表中多个字段创建的索引;
②使用索引有哪些好处?缺点?何时使用索引?何时不适用?
最大的好处是可以大大提高检索效率,其次可以通过创建唯一索引来保证每一行数据的唯一性;
缺点在于对表中数据进行增删改时,也需要对索引进行维护,降低了增删改操作的效率,除此之外索引本身需要占用物理空间;
当字段有唯一限制或重复度较低时,以及对于那些经常使用where、order by、group by的字段,适合建立索引;
相反,对于那些重复度较高的字段,或是经常更新的字段,则不适合建立索引;
2.索引按数据结构分类、B+树
①InnoDB中索引按数据结构可以分为哪些?
Ⅰ. B+树索引:最常见的索引数据结构,适用于等值、范围和排序查询;
Ⅱ. Full-text全文索引:适用于text文本列的全文搜索,可以使用CREATE FULLTEXT INDEX给文本列创建全文索引;
Ⅲ. 自适应哈希索引:当InnoDB检测到某个索引列的查询频率较高时,可能会自动将其转换为Hash索引,以加速等值查询操作,但缺点是不适用于范围查询和排序查询;
——通常自适应哈希索引由存储引擎自动完成,无需手动干预;
②B+树相比其它结构有什么优点?
Ⅰ. 相比于哈希表:
哈希表虽然在等值查询中速度更快,但哈希表不支持范围查询和排序,B+树则支持;
Ⅱ. 相比于二叉树:
B+树的分支更多,层数更低,查询时的IO次数更少,效率更高;
Ⅲ. 相比于B树:
B+树的非叶子结点只存储索引列和指针,不存储行记录的完整数据,而B树的非叶子结点和叶子节点都存储了行记录的完整数据,因此一个B+树的非叶子节点中存储的记录条数更多,树的层数更低,IO次数更少;
还有就是B+树的叶子节点维护了一条双向链表,因此在范围查询时只需找到一个值然后沿链表遍历即可,而B树则没有;
③B+树索引IO次数一般为多少?
以主键索引来算,一个页的大小为16KB,主键类型一般为int或binInt,大小为4-8B,指针大小一般也为4-8B,所以一个页能存储1k左右的键值,
这样三层的B+树就能存储越10亿条记录,考虑到实际使用中一般页不会存满,所以树的高度一般为2-4层,
又因为根节点是常驻内存的,所以一般IO次数为1-3次;
④既然增加B+树的路数可以降低高度,那么能不能无限增加路数?
不能,因为无限增加路数会导致一个B+树节点中存储的记录条数过多,一次将节点全部读入内存的消耗也会过大;(页的大小也是有限的)
⑤键值为null的记录在B+树如何存放?
首先主键必定非空,因此主键索引中不可能有为Null的键值,而在二级索引中,null值一般被视为最小值,放在B+树叶子节点的最左侧;
3.排查慢SQL、EXPLAIN
①如何排查一条慢SQL?
发现慢SQL:可以通过开启MySQL的慢查询日志,设置好时间阈值,对慢查询命令进行捕获;
分析慢SQL:可以通过EXPLAIN指令来分析SQL语句的执行计划;
优化慢SQL:可以通过索引优化和优化SQL语句来实现;
②EXPLAIN指令分析结果中的关键字段?
首先是id,表示执行顺序,id相同自上而下执行,不同id大的先执行;
然后是type,表示数据扫描类型,执行效率从低到高有:all全表扫描、index全索引扫描、range部分索引扫描、ref查询命中非唯一索引、eq_ref查询命中唯一/主键索引、const查询命中唯一索引且结果只有一条;
然后是key,表示本次查询使用到的索引;
然后是row,表示sql估计的本次扫描的记录行数;
最后是Extra额外信息,using index表示使用了覆盖索引,无需回表查询,using where表示需要回表查询,using filesort表示查询语句中需要排序,但无法利用索引完成排序,using temporary表示使用了临时表保存中间结果;
4.索引优化、SQL优化
①索引优化的手段(如何避免索引失效?索引使用时有哪些注意点?)
避免索引失效(最左匹配原则、范围查询、对索引列进行函数计算、隐式转换):
Ⅰ. 使用联合索引时遵循最左匹配原则,查询的字段应当从索引的最左列开始,不能跳过中间的列,否则会导致索引失效;
Ⅱ. 范围查询中的>、<、like后缀匹配会导致其右侧的索引失效,>=、<=、between、like前缀匹配则不会(等于的那部分生效);
Ⅲ. 在索引列上进行计算或是函数操作会导致索引失效,查询条件中使用or时必须全部字段都建立了索引,否则整个索引也会失效(and有先后顺序,后面的失效不会影响前面,or是没有顺序的,后面的失效会导致前面的也失效);
Ⅳ.对索引列进行隐式转换会导致索引失效;(例如索引字段为字符串类型,查询参数为整型,由于MySQL在比较字符串和整型时,会将字符串转化为整型再进行比较,所以会对索引列使用CAST函数进行转换,因此导致索引失效,而如果反过来,索引列为整型,查询参数为字符串类型,那么会对查询参数使用CAST函数进行转换,此时不会导致索引失效)
Ⅴ.范围查询会导致之后的索引列全部失效;(原因分析:https://blog.csdn.net/km_bandits/article/details/139687694)
Ⅵ.查询条件使用了左模糊匹配或左右模糊匹配;(右模糊匹配不会使索引失效)
Ⅶ. not in会导致索引失效;(因为判断==可以根据大小二分走索引,判断!=在小于/大于时没法判断是在左边还是在右边,也就没法二分走索引)
尽量使用覆盖索引减少回表查询次数等;
让区分度高的字段排在前面等;
②SQL语句优化的手段:
将嵌套子查询改为连接查询(因为子查询产生的虚表中是没有建立索引的,只能执行全表扫描,效率较低);
尽量不使用select * 而是指定查询需要的字段;
5.事务
①事务的四大特性(ACID)?
Ⅰ. 原子性Atomicity:一个事务中的所有操作,要么全部完成,要么全部不完成;——通过undo log实现
Ⅱ. 一致性Consistency:事务操作前后,数据库中的数据应该保持一致性(例如转账事务中,操作前后,转账者和收款者的账户总和应该是不变的);——通过A、I、D实现
Ⅲ. 隔离性Isolation:一个事务在访问数据库时,不会被其他事务干扰,每个事务锁操作的数据空间应当是独立的;——通过MVCC和锁机制实现
Ⅳ. 持久性Durability:一个事务在提交之后,其对数据库进行的修改应当是永久的,即使数据库发生故障也不应当有影响;——通过redo log实现
②并发事务可能会导致哪些问题?
Ⅰ. 脏读:一个事务读到了另一个事务尚未提交的数据;
Ⅱ. 不可重复读:一个事务多次读取同一数据,得到的结果却不一样;
Ⅲ. 幻读:一个事务多次使用相同的查询条件,得到的记录条数却不一样;
Ⅳ. 丢弃修改:两个事务并发地对一个数据进行操作,结果产生了数据覆盖,
(例如两个事物同时对一个为0的变量执行递增操作,首先两个事务先后取到了初始值0,然后各自对0执行递增得到1并修改变量,导致两次递增结果只为1)
③不可重复读和幻读的区别?
不可重复读强调内容修改,幻读强调记录增加或减少;
将两种分为两个概念主要是解决不可重复读和幻读的手段不同;
-
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导致A再读自己的工资时工资变为 2000;这就是不可重复读。
-
例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记 录就变为了5条,这样就导致了幻读。
④事务的隔离级别有哪些?
Ⅰ. 读未提交Read Uncommitted:允许读取尚未提交的数据,可能会导致脏读、幻读或不可重复读;
Ⅱ. 读已提交Read Committed:只允许读取已提交的数据,可以解决脏读;
Ⅲ. 可重复读Repeated Read:保证一个事务对同一字段多次读取的结果是一样的,可以解决脏读、不可重复读,但只能部分解决幻读;——MySQL默认的隔离级别
Ⅳ. 串行化Serailizable:多个事务只能按序逐个执行,可以完全解决幻读问题;
6.MVCC、不可重复读&幻读是如何解决的
①什么是MVCC?原理是什么?
——核心原则:已提交的可见,未提交的不可见
MVCC多版本并发控制,就是通过事务的Read View和记录中的隐藏字段的比对,来控制并发事务访问同一个记录的行为;
具体来说,Read View中有四个重要字段:当前事务id、当前数据库中活跃且未提交的事务列表、列表中的最小事务id、下一个创建的事务的id,
每条记录中有两个隐藏字段:最新修改本条记录的事务id、指向上个版本的记录的指针,
当一个事务访问某条记录时,除了本事务更新的记录总是可见外,
若记录中的事务id小于列表最小事务id,说明更新这条记录的事务已经提交,故可见;
若记录中的事务id大于下一个创建的事务id,说明这是在本事务之后创建的事务更新的记录,故不可见;
若记录中的事务id介于两者之间,则如果其位于活跃且未提交的列表中,说明尚未提交,故不可见,反之则可见;
当这条记录不可见时,事务就会继续判断上一版本的记录是否可见,直到找到对当前事务来说可见的版本或是一直找到最初版本;
读已提交就是在每次读取数据时就生成一个Read View;
可重复读通过只在事务创建时生成一个Read View并一直沿用到事务结束;
②可重复读是如何解决不可重复读问题的?又是如何部分解决幻读的?为什么不能完全解决幻读?
通过MVCC就可以解决不可重复读问题;
对于幻读,有两种解决方案:
对于快照读(普通select语句),MVCC就可以解决部分幻读问题,因为大部分情况下新插入/删除的记录对当前事务来说都是不可见的;
对于当前读(select ...lock in share mode/ for update、update/delete/insert语句),则是采用了记录锁+间隙锁的方式,对指定范围内的记录上锁,从而部分解决了幻读问题;
之所以说只是部分解决了幻读问题,是因为在可重复读隔离级别下仍有可能会发生幻读:
比如一个事务先通过快照读查询记录,然后别的事务添加了一条满足条件的记录并提交,
之后若当前事务对该记录又执行了更新操作,或是改为执行当前读,就会导致两次查询到的记录条数不一样,发生幻读;
解决办法就是在事务开始后先执行当前读给可能发生幻读的记录范围上锁,避免其他并发事务进行修改,从而从根本上避免幻读;
7.MySQL锁
①MySQL中有哪些锁?
Ⅰ. 全局锁:对整个数据库加锁;
Ⅱ. 表级锁:对整张表加锁,实现简单开销小,但锁粒度较大效率低,InnoDB和MyISAM都支持;
Ⅲ. 行级锁:对行记录加锁,行级锁是加在索引上的,锁粒度较小效率高,只有InnoDB支持;
行级锁主要有三种:
记录锁:锁住一条记录,有共享锁和互斥锁之分(又称读锁和写锁),读锁与读锁互相兼容,写锁与所有锁互斥;
——有对应记录才能加记录锁,select ... lock in share mode可以加读锁,select ... for update、update、delete只能加写锁;
间隙锁:只存在于可重复读隔离级别,用于锁住一个范围禁止插入数据,防止幻读的发生,间隙锁可以共存;——没有对应记录就给开区间加间隙锁,因为没有对应记录,所以不存在删改查,防止插入记录即可
临键锁:是记录锁+间隙锁的组合,对记录本身和范围都加锁;
②在执行增删改查操作时分别会加何种锁?(默认是可重复读隔离级别)
——锁在事务中生成,事务提交后释放
Ⅰ. 普通select语句不加锁,
Ⅱ. select ... lock in share mode在不发生幻读的最小范围内加共享记录锁、间隙锁或是临键锁;
select ... for update、update、delete在不发生幻读的最小范围内加互斥记录锁、间隙锁或是临键锁;
Ⅲ. insert会给插入范围加插入意向锁,这种锁互相之间不互斥,也不会阻塞其它事务获得锁,但会被范围内的间隙锁/临键锁阻塞;
注意:
因为行级锁是加在索引上的,如果执行update操作的where查询条件中的字段没加索引,就会导致全表扫描并给所有记录都上锁;
解决办法除了避免写出这类update语句外,可以将sqlsafe_update参数设置为1,防止无索引执行update语句;
③不同索引类型的加锁情况
对于唯一索引:
等值查询:若存在加记录锁,不存在加间隙锁;
范围查询:
④数据库层面是否会发生死锁?如何避免?
上面的操作会出现死锁:因为事务A、B先后给(20,30)加了间隙锁,又先后要对(20,30)范围执行insert操作,这就导致两个事务都阻塞并等待对方释放间隙锁,
满足了互斥、不可剥夺、持有等待、循环等待四个条件,发生死锁;
上面的是利用间隙锁可共存但会阻塞insert操作的特性,触发的死锁,还有一种方式:
事务A先对id=15的记录进行select for update操作,加互斥记录锁;
事务B对id=18的记录进行select for update操作,加互斥记录锁;
事务A对id=18的记录进行update操作,试图获取互斥记录锁,但被阻塞;
事务B对id = 15的记录进行update操作,试图获取互斥记录锁,发生死锁
解决方法:
Ⅰ. 设置等待锁的超时时间:当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数 innodb_lock_wait_timeout
是用来设置超时时间的,默认值时 50 秒;
Ⅱ. 开启主动死锁检测:主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect
设置为 on,表示开启这个逻辑,默认就开启;
8.undo log、redo log、bin log
①什么是undo log回滚日志?
在事务没有提交之前,MySQL会将每次进行增删改操作之前的数据都记录在undo log中,
当事务需要回滚时,就可以通过undo log来实现,undo log保证了事务的原子性;
②什么是redo log重做日志?
redo log是InnoDB存储引擎实现的物理日志,记录每次数据页的修改情况并写入内存缓冲区,当缓冲区被占满一半、或是每隔1s、或是当事务提交时,redo log会被持久化到磁盘上,
这样即使系统崩溃,也可以根据redo log将数据恢复到最新状态,redo log保证了事务的持久性;
——可以使用 Forcing InnoDB Recovery 来进行崩溃恢复
③什么是bin log?
bin log是Server层生成的日志,Server会记录本次事务中执行的所有增删改操作,在事务提交时写入bin log并持久化到磁盘,
主要用于主从复制和数据恢复;
④bin log和redo log的区别?
bin log是Server层实现的日志,所有存储引擎均可用,是全量操作日志,可以进行全量的数据恢复和主从复制,
redo log是InnoDB存储引擎实现的日志,有大小限制,当写满时会边写边擦除,主要用于处理紧急故障恢复;
⑤bin log如何实现主从复制?
将主库的bin log复制到从库,解析为sql语句并执行即可;
⑥为什么要两阶段提交?过程是怎样的?
因为redo log会影响主库的数据,bin log会影响从库的数据,而这两种日志都需要刷盘,如果在刷盘时一个成功一个失败,就会导致主从库数据不一致;
两阶段实际上就是将事务的提交分成两阶段:
首先第一阶段对redo log进行刷盘,成功则进入下一阶段;
第二阶段会对bin log进行刷盘,如果失败则使用undo log对事务进行回滚,如果成功则完成事务的两阶段提交;
(核心:undo log的回滚可以让redo log中的数据状态也回滚,但无法让binlog中的数据状态回滚,所以redo log先刷盘)
9.一条SQL语句在MySQL中的执行过程
Ⅰ. 首先连接器验证用户身份和权限;
Ⅱ. 查询缓存:若缓存命中直接返回,否则进行下一步;
Ⅲ. 分析器,对SQL语句进行词法分析和语法分析,提取SQL语句的关键元素并检查是否存在语法错误;
Ⅳ. 优化器:对SQL语句进行优化,选择最优的执行方案;
Ⅴ. 执行器:查看用户是否有执行权限,然后调用存储引擎提供的接口;
Ⅵ. 存储引擎:按照执行方案执行SQL语句,并返回结果;
10.MySQL行记录的存储
①MySQL 一行记录是怎么存储的?
在常见的COMPACT格式中,一条完整的记录分为额外信息和真实数据两部分,
其中额外信息包括:
变长字段长度列表:逆序存放记录中所有变长字段的真实长度,不大于255KB用一个字节记录即可,大于255KB需要两个字节;
NULL值列表:逆序存放记录中所有可以为NULL的字段是否为NULL,一个字段用一位来表示,不足的补齐1个字节,若所有字段均为非空约束,则可以省略此列表;
记录头信息:记录本条记录是否已被逻辑删除、下一条记录的地址等信息;
真实数据中还有三个隐藏字段:
row_id:如果建表时指定了主键或是唯一约束列就没有此字段,否则会添加此字段来指定某一列作为主键,占用6KB;
trx_id:记录本条记录由哪个事务生成,占用6KB;
roll_ptr:记录上一版本的指针,占用7KB;
②变长字段列表/NULL值列表为什么要逆序存放?
这样的话位置靠前的真实数据更有可能和自己的列表信息存放在同一个CPU Cache Line中,提高Cache命中率;
③varchar(n) 中 n 最大取值为多少?
首先,MySQL 规定除了 TEXT、BLOBs 这种大对象类型之外,其他所有的列(不包括隐藏字段和记录头信息)占用的字节长度加起来不能超过 65535 个字节;
这里我们只考虑单字段记录的情况,由于不包括隐藏字段和记录头信息,故只考虑变长字段长度列表和NULL值列表所占空间即可,
因为单列最大字节数>255KB,故变长字段长度列表占2KB,允许该列为NULL的话,再加上1KB的NULL值列表,
故在数据库表只有一个 varchar(n) 字段且字符集是 ascii 的情况下,varchar(n) 中 n 最大值 = 65535 - 2 - 1 = 65532;
④行溢出后,MySQL 是怎么处理的?
对于Compact行格式:当发生行溢出时,在记录的真实数据处会存放该列的一部分数据,将剩余的数据放在溢出页中,并在真实数据除用20KB存放溢出页的地址;
对于Compressed 和 Dynamic 这两种格式:记录的真实数据处只会用20KB存放溢出页的地址,全部数据都放在溢出页中;
11.MyISAM&InnoDB
①MyISAM 和 InnoDB 有什么区别?
Ⅰ. InnoDB是MySQL5.5.5版本之后的默认存储引擎,支持事务、MVCC、行级锁、外键约束、数据库异常崩溃后的安全恢复,这些MyISAM都不支持;
Ⅱ. 虽然InnoDb和MyISAM都使用B+数作为索引结构,但InnoDB中主键索引采用聚集索引,B+树叶子节点存储实际数据,
而MyISAM中主键索引采用非聚集索引,B+树叶子节点只存储指向完整行记录数据的指针,还需要访问磁盘获取数据,因此InnoDB的读写效率比MyISAM更高;
Ⅲ. MyISAM支持压缩表和空间数据索引,InnoDB不支持;
总体来说,大部分情况下都是选择InnoDB更好,除非不需要InnoDB提供的那些特性;
②InnoDB为什么要使用自增id作为主键?
因为这样在插入新的记录时,可以直接添加到当前索引节点的后续位置,一页写满再开辟新的页,
而如果使用非自增列作为主键,由于主键分布近乎随机,因此新的记录经常会插入到现有索引页中,需要频繁的进行移动记录和分页操作,效率低下,还会产生大量内存碎片;
12.MySQL基础
①说说delete、drop、truncate的异同?
相同点:都用来进行删除操作;
不同点:delete用来删除表中的数据,可以加where条件语句,delete属于DML,在事务中可以通过commit和rollback来选择提交操作或是回滚操作;
truncate删除表中的全部数据,自增id也会重新开始计数,truncate属于DDL,操作立即生效不可回滚,且速度比delete快;
drop则是用来删除整个表,包括其中的数据和索引等都会被删除,drop属于DDL,操作立即生效不可回滚;
②char和varchar有什么区别?
char是定长字符串,在建表时不指定长度则默认为1个字符,指定长度则所有数据均以该长度进行存储,不足长度的以空格填充;
varchar是变长字符串,在建表时必须指定长度,数据以其实际长度进行存储,不过还需要1到2KB在变长字段长度列表中存储其实际长度信息;
通常,若存储的字符串长度基本完全相同,则用char,若长度不确定,则用varchar;
(如果字符串长度超过5000,建议使用text,且应独立出来一张表,用主键来对应,避免影响其它字段索引效率)
③说一说你理解的外键约束?
外键约束主要是为了维护相关联的表之间的数据一致性,
通过在主表中的某个字段建立外键约束,并与子表中的相同字段关联起来,对主表的更新/删除操作进行某种约束,
默认的行为是NO ACTION,即若该记录在子表中也存在,则不允许对主表中的记录进行更新/删除操作;(对子表进行操作不受限制,只约束父表)
还有CASCADE级联行为,即若父表对一条记录进行更新/删除操作,则也对子表中的关联的记录进行相同操作;
(注意:建立外键约束的父表的字段应当建立索引,否则无法对其建立外键约束)
④DECIMAL 和 FLOAT/DOUBLE 的区别是什么?
DECIMAL是定点数,通过以整数的形式存储小数,可以存储精确的小数值,
FLOAT/DOUBLE是浮点数,只能存储近似的小数,有精度损失,
在对精度有要求的场景下,小数应使用DECIMAL类型;
⑤NULL 和 '' 的区别是什么?
Ⅰ. NULL代表一个不确定的值,不能用运算符判断,只能用IS NULL/IS NOT NULL来判断,本身占用一个字节,
而''是空字符串,可以用运算符判断,且不占用空间;
Ⅱ. NULL会影响聚合函数的结果,MAX()、MIN()等聚合函数会忽略NULL,COUNT(列名也会忽略NULL值),但COUNT(*)会将NULL值也算上;
推荐使用0或者""""来代替NULL值作为字段默认值;
⑥SELLECT语句的书写顺序?
⑦SELLECT语句的执行顺序? "SELLECT语句的书写顺序?
二、Redis
1.缓存雪崩是什么?怎么解决?
当大量缓存数据在同一时间过期(失效)或者 Redis服务器 故障宕机时,如果此时有大量的用户请求,因为都无法在 Redis 中处理,于是全部请求都直接访问数据库,造成数据库宕机,进而造成整个系统崩溃,这就是缓存雪崩;
可以看到,发生缓存雪崩有两个原因:
- 原因一:大量数据同时过期。解决方式:
- 均匀设置过期时间:我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证不会出现大量数据在同一时间过期的情况。
- 互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。这样就不会出现同一时间大量请求直接访问数据库的情况;
- 后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
- 原因二:Redis 故障宕机。解决方式:
- 构建高可靠的 Redis 集群:当 Redis 主节点故障宕机时,从节点可以切换成为主节点,继续提供缓存服务,避免了Redis服务器完全宕机;
- 服务熔断机制:当 Redis 故障宕机而导致缓存雪崩问题时,我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,这样就不再继续访问数据库,避免了数据库也宕机,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。这种方式的缺点在于会导致全部业务都无法正常工作;
2.大key、热key、缓存穿透、 缓存击穿、布隆过滤器的原理
①大key
如果一个key对应的value较大,比如String类型超过10kb,其他类型元素数量超过5000个,就认为是大key;
大key会消耗过多的内存和带宽,影响整体性能,
可以利用一些开源工具对RDB文件进行分析找到大key,通过分批次删除元素并最终删除key值;
②热key
如果一个key在一段时间内的访问次数明显高于其它key,就认为它是热key,
热key会占用大量CPU资源和带宽,影响整体性能,
可以利用redis自带的--hotkeys参数或是借助开源软件来找到热key,
处理热key可以使用二级缓存,添加一层本地缓存,缓解Redis缓存的压力;
③缓存穿透:
大量请求查询不存在于缓存和数据库中的数据,导致每次请求都穿透缓存,直接落在了数据库上,导致数据库短时间内接收大量请求而崩溃;
解决办法:在将请求传给数据库之前,先通过布隆过滤器判断查询的key值是否存在于数据库中,不存在的请求直接丢弃;
还可以通过在数据库查不到数据时缓存空对象并设置较短的过期数据来解决;
④缓存击穿:
当某个热key过期了,在其重新载入缓存之前,所有请求都落在了数据库上,导致数据库崩溃;
解决办法:针对热点数据提前进行缓存预热,并设置足够长的过期时间,然后通过后台异步地更新缓存;
除此之外,还可以在热点数据缓存失效期间,给访问数据库和更新缓存的操作加上互斥锁,同一时间只允许一个请求访问数据库和更新缓存,避免大量并发请求同时访问数据库;
⑤布隆过滤器的原理?
布隆过滤器可以用O(1)的时间复杂度判断元素是否存在,其底层就是一个bit数组和若干个哈希函数,当有元素加入时,就通过这几个哈希函数计算元素的哈希值,并将其映射到位数组的对应位置修改为1,
当需要判断元素是否存在时,同样通过这几个哈希函数计算元素的哈希值,得到对应位置位数组的值,如果均为1则认为此元素存在,反之认为不存在;
布隆过滤器的特点:
①判断元素存在可能失误,因为不同哈希值可能映射到数组中的位置相同,使用多个哈希函数也是为了降低误判概率,但判断元素不存在一定不可能失误,因为相同哈希值映射到数组中的位置一定相同,可以通过增长bit数组长度来降低误判率;
②无法删除元素,可以通过将被删除的元素存入Redis的Set中来解决;
3.Redis实现分布式锁
①SET NX PX
可以通过Redis的SET命令结合NX、PX参数来实现,
具体来说,SET命令的参数NX作用是只有key不存在才执行SET操作,利用这一功能:
如果KEY不存在,SET成功,就表示加锁成功;
如果KEY存在,SET失败,就表示加锁失败;
再将键值对的value设为当前客户端的唯一标识,来区别不同客户端之间的锁
最后使用PX参数给锁设置一个过期时间,防止获得锁的客户端异常导致锁无法释放;
解锁的过程就是先先通过键值对的value来判断当前客户端是不是上锁的客户端,再执行del命令删除键值对,为了保证判断操作和删除操作的原子性,可以使用Lua脚本中来实现判断和删除这两步操作;
优点:实现简单,性能也不错,
缺点:首先锁的过期时间不好设置,设置太短可能操作还没有进行完,锁就过期了,设置太长又会影响性能,其次功能不够丰富,不支持可重入、公平锁、读写锁等功能;
(Redission的锁续期:默认过期时间为30s,之后启动一个定时任务Watch Dog每隔10s就对锁的过期时间进行更新,直至其被加锁线程主动释放)
——Redission是一个基于Redis的Java客户端工具,提供了丰富的分布式锁功能:
②Redission的可重入锁
可重入锁底层是基于Redis的Hash数据结构,通过Lua脚本来实现的,
首先从数据结构来看:其外层key是锁名称,value也是一个哈希表,内层key为客户端UUID+线程ID,value是锁的重入次数,初始为0,每重入一次+1(hincrby);
然后其加锁逻辑为:首先判断锁名称是否存在,如果不存在说明锁未被占用,直接存入当前线程的键值对,返回null表示获得锁成功;
如果锁名称已经存在,继续判断其内部key是否和当前线程相同,如果相同说明则执行可重入逻辑,直接令内部value+1表示可重入次数+1,返回null表示获得锁成功;
如果前两个条件都不满足,说明锁已被其他线程获得,则返回当前锁的存活时间表示获取锁失败,当前线程会等待相应时间再去尝试获得锁;
(如果加锁时没有指定锁的过期时间,则Redission会默认将其设置为30s,并且启动一个定时任务Watch Dog每隔10s就对锁的过期时间进行更新,直至其被加锁线程主动释放)
解锁逻辑很简单:先判断是否是加锁的客户端和线程,如果是就将对应锁的键值对删除,同时通知其它线程再次尝试获取锁(获取锁失败的线程都会订阅当前占用锁的线程);
③Redission的公平锁
公平锁在可重入锁的Hash表基础上,添加了一个用于存放等待线程的列表List、以及顺序存放等待队列中每个线程的超时等待时间戳的有序集合ZSet;
公平锁的加锁逻辑变为:
Ⅰ. 首先遍历等待队列,根据zset中的超时时间戳是否小于当前时间戳,剔除那些已经超时等待的线程;
Ⅱ. 然后判断哈希表中锁名称是否存在,如果不存在且等待队列为空,或当前线程就是等待队列中的第一个线程,就直接向Hash表存入当前线程的键值对,获得锁成功,默认设置过期时间为30s;
(如果当前线程是等待队列中的第一个线程,还要从等待队列中移除该线程,同时令zset中每个线程的超时等待时间戳减300s;)
Ⅲ. 如果锁名称已存在,接着判断内部key是否和当前线程相同,如果相同则执行可重入逻辑,直接令内部value+1表示可重入次数+1,获得锁成功;
如果前两个条件都不满足,说明锁已被其他线程获得,获取锁失败,
Ⅳ. 接下来首先判断当前线程是否已经在等待队列中,如果是,就返回当前锁的存活时间,线程阻塞等待相应时间再去尝试获得锁;
Ⅴ. 如果当前线程不在等待队列中,就将当前线程加入到等待队列中,同时计算超时等待时间戳存入zset中;
(超时等待时间戳:每个线程在加入队列时,都会在队列中最后一个线程的ttl的基础上,加上300s超时等待时间+当前时间戳;
线程ttl = zset中的超时时间戳 - 300s - 当前时间戳;)
④Redission的读写锁(读读不互斥,写锁和读、写锁都互斥)
读写锁基于Redis的Hash结构和String结构,通过Lua脚本来实现的,
首先Hash结构的外层key是锁名称,value也是一个哈希表,内层key为客户端UUID+线程ID,内层value是锁的重入次数,初始为0,每重入一次+1,
内部的哈希表中有一个固定的key为mode的键值对,value可以是false、read、write,分别表示锁未被占用、读锁、和写锁,
除此之外,还单独以String键值对的形式记录了每一把锁及其过期时间;
(记录每一把锁的过期时间是为了在有多把读锁,且后加的读锁先主动释放的情况下,也能保证锁名称的过期时间与内部读锁的最长过期时间一致,例如A线程加读锁,过期时间为30s,过了5s后线程B加读锁,过期时间也为30s,此时A线程过期时间变成了25s,但锁名称的过期时间刷新为了30s,此时如果线程B又释放了锁,就会导致锁名称过期时间和仅有的线程A加的读锁过期时间不一致,这时就需要通过单独记录的线程A的读锁的过期时间来修改锁名称的也过期时间为25s)
读锁的加锁逻辑为:
如果锁名称不存在或mode为false,则直接加锁,并以键值对存储当前锁的超时时间;
如果mode为read,则也可以成功获取锁,
如果当前线程没有加过读锁,就在哈希结构内层中添加当前线程和重入次数为1的键值对,
如果当前线程加过读锁,则直接令重入次数+1即可,
之后都以键值对存储当前锁的超时时间;
如果mode为write,则获取锁失败,等待当前锁释放;
写锁的加锁逻辑比较简单:只有当锁名称不存在、或mode为false、或mode为write且当前线程就是占用锁的线程时才能成功加锁,其他情况都加锁失败,需要等待当前锁释放;
⑤分布式锁的其它实现方案,和Redission的对比与方案选型
还可以通过ZooKeeper来实现分布式锁,ZooKeeper底层主要通过创建临时节点来实现,多个客户端去创建同一个临时节点,第一个创建的客户端抢锁成功,释放锁时只需要删除临时节点即可;
ZooKeeper相比Redission优点在于可用性更高,后者可能会因为主节点的锁信息还没有同步到其他节点就宕机了,导致锁信息丢失,还有各节点的时间不同步也可能会导致分布式锁出现问题;
缺点则在于ZooKeeper提供的锁功能较为简单,没有像Redission那样提供公平锁、读写锁等功能,并且性能也不如Redis实现的分布式锁;
我选用Redission是因为对可用性要求没那么高,但需要分布式读写锁来保证本地缓存和分布式缓存的数据一致性;
⑥RedLock是什么?ZooKeeper一定安全吗?
Redis 中的 RedLock就是在多个 Redis 节点上都尝试加锁,超过一半节点加锁成功,并且加锁后的时间要保证没有超过锁的过期时间,才算加锁成功,否则加锁失败, 一般不推荐不使用,因为它为了保证加锁的安全牺牲掉了很多的性能;
ZooKeeper在极端情况下也存在不安全的问题,例如加锁的客户端长时间 GC 导致无法与 ZooKeeper 维持心跳,那么 ZK 就会认为这个客户端已经挂了,于是将该客户端创建的临时节点删除,那么当这个客户端 GC 完成之后还以为自己持有锁,但是它的锁其实已经没有了,因此也会存在不安全的问题;
如果是对可靠性要求非常高的应用,不可以把线程安全的问题全部寄托于分布式锁,而是要在资源层也做一些保护,来保证数据真正的安全;
⑦如果redisson中10秒的时间差是不能接受的 想要让他执行完后立即释放锁需要怎么做?
①执行完操作之后主动执行unlock()方法释放锁;
②在获取锁时使用tryLock()方法指定锁的持有时间为操作执行的预计时间,这样只要等到这个预计时间,无需锁超时就会自动释放;
⑧redisson轮询的线程和业务执行的线程是同一个吗?两个线程之间的数据是如何同步的?如何拿到另一个线程的执行结果?
不是;
可以通过wait()、notify(),或是CountDownLatch来进行同步;
可以通过volatile修饰的共享变量来共享数据;
4.Redis五种基本数据结构、ZSet底层数据结构
①五种基本数据结构
Ⅰ. String:value可以是各种数据类型,底层是SDS简单动态字符串,可以用来缓存JWT token;
(相比于原生的C语言中的字符串,SDS不仅可以存储文本,还能存储图片音频等二进制数据,且SDS中有属性len记录长度,获取字符串长度的时间复杂度只有O(1))
Ⅱ. Hash:value是一个键值对集合,底层是压缩列表或哈希表,适用于需要存储包含多个属性的对象,例如用户信息、商品信息等,还可以用于Redission中的可重入锁;
Ⅲ. List:value是一个列表,底层是压缩列表或双向链表,支持从头部或尾部添加删除元素,可以用来存储各种列表,还可以通过lrange来实现高性能分页查询;
Ⅳ. Set:value是一个无序且不可重复的集合,底层是整数集合或哈希表,适合用来对集合取交集并集、从集合中随机获取元素,例如找共同关注、随机抽奖等场景;
Ⅴ. ZSet:相比Set多了一个权重参数score,可以按照权重对集合元素进行排序,底层是压缩列表或跳表,可用于各种排行榜;
(以上的压缩列表在Redis7.0都被替换成listpack了,元素小于512个时使用前一种数据结构,大于512个用后一种)
常用命令:
String:set、get、incr、decr
Hash:hset、hdel、hget
List:lpush/rpush、lpop/rpop、lrange、lset
Set:sadd、srem、srandmember、spop、sunion、sinter
ZSet:zadd、zrem、zrange/zrevrange
②Redis的ZSet底层是用什么实现的?为什么用跳表而不用红黑树/平衡树?
ZSet底层通过跳表实现,跳表就是一个带有层级的双向链表,通过给部分节点添加额外的指针来分出多个不同跨度的层,使得查询效率达到logn级别;
查询时从跨度最大的最高层开始遍历,通过对比权重和元素值(SDS类型)来判断大小,如果都相等直接返回,如果均小于等于目标值则继续遍历本层的下一个节点,否则返回上一个节点,沿着其指针数组,跳到指向下一层节点的指针往后遍历;
跳表相比红黑树实现更简单,且跳表节点结构更简单内存占用更少(平均每个节点有1.33个指针),
除此之外,跳表做范围查询更简单,只需要找到最小值然后沿着最底层链表遍历即可,红黑树还需要做中序遍历;
——而两者的查询效率接近,平均时间复杂度都为O(logn)
5.Redis持久化
①RDB持久化
RDB持久化就是通过创建快照的方式来获得缓存中的全部数据在某个时间点的副本,并持久化到磁盘中;
这个快照可以用来主从复制或是重启服务器时恢复数据,
RDB持久化时Redis默认的持久化方式,可以在配置中修改每隔多长时间且有多少key被修改时触发RDB持久化;
(手动触发RDB持久化:SAVE会阻塞主线程,BGSAVE在子线程中创建快照,不会阻塞主线程)
②AOF持久化
AOF持久化相比RDB实时性更好,因为每执行一条增删改命令都会写入AOF文件,并根据不同的刷盘策略每执行一条命令、每隔1s或是由操作系统决定何时刷盘;
默认情况下没有开启AOF,可以通过appendonly yes来开启;
AOF重写是什么?
当AOF文件过大时,redis会让子线程通过读取内存中的键值对,来生成一个新的体积更小但数据状态和原来的AOF文件一致的新AOF文件,用来替代原来的AOF文件,
在AOF重写期间执行的增量操作会先存放在内存缓冲区中,待重写完毕再写到新AOF文件的末尾;
③实际生产中用哪种持久化机制?
redis4.0开始支持RDB+AOF混合持久化,在AOF重写时直接将RDB的内容写到AOF文件开头,速度更快,但因为RDB文件存储的是压缩格式,可读性较差;
实际生产中,如果对数据安全性要求不是很高,用RDB就可以了,
如果对数据安全性要求较高,就用RDB+AOF混合持久化;
不推荐单独使用AOF,性能较差;
——恢复时,Redis会优先使用AOF日志进行恢复,因为AOF文件的实时性更好,更不容易遗漏数据,数据完整性更高;
6.Redis单线程、IO多路复用
①为什么单线程的Redis这么快?
首先,由于Redis大部分操作都在内存中完成,且使用的数据结构高效,大部分操作的时间复杂度都为O(1),因此Redis的性能瓶颈不在CPU,而在内存大小或网络带宽,
正因如此,没有采用多线程的必要,采用单线程还可以避免频繁的上下文切换和死锁等问题,
除此之外,Redis采用IO多路复用来实现了单线程处理大量的客户端 Socket 请求;
②什么是IO多路复用
IO多路复用就是通过select、poll、epoll这些多路复用的系统调用,使得单个进程可以监控多个Socket,并在有Socket发生读/写事件时立即进行响应;
具体来说,select/poll的原理是使用线性结构存放要监控的Socket集合,在使用时先将集合从用户态拷贝到内核态中,内核遍历集合并将发生事件的Socket的状态标记为可读/可写,之后再将集合拷贝回用户态,用户态再次遍历集合找出那些可读/可写的Socket,然后进行处理;(整个流程需要两次拷贝两次遍历,当用户越多Socket集合越大,拷贝和遍历的成本就越高,因此实现不了C10K)
而epoll的原理是在内核中维护一棵红黑树来存放要监控的Socket集合,增删连接只需要增删红黑树节点即可,不需要每次都拷贝整个Socket集合,
除此之外epoll还在内核中维护了一个链表来存放发生事件的Socket集合,这样就不需要遍历整个Socket集合来找发生事件的Socket了;
——Socket(套接字):由IP地址和端口号组成,用于唯一标识网络中的一个通信端点;
③用户态和内核态的区别?
CPU处于用户态时执行的是应用程序,只能执行非特权命令,
而当CPU处于内核态下执行的是内核程序,此时可以执行特权命令,例如缺页中断的处理、进程的调度;
会导致进程从用户态切换到内核态的操作:read系统调用、访问内存时发生缺页、整数除以0等;
7.Redis过期删除策略、内存淘汰策略
①Redis对过期数据如何处理?(过期删除策略)
主要有两种方式,
一种是惰性删除,就是只有在访问或是修改key时才对其进行过期检查,如过期则删除(可以选择同步或异步删除),并返回NULL,这种方式对CPU友好,但会出现大量未删除的过期数据,对内存不友好;
第二种是定期删除,就是每隔一段时间就从缓存中随机抽取一批key,对其进行过期检查,删除过期的键值对,如果过期键值对超过一定比例则继续抽取检查,直至比例正常,或是整个流程超过了一定时间,这种方式对内存友好,但CPU负担较大;
(默认配置下,Redis每隔10s从缓存中随机抽取20个key进行过期检查,
如过期则删除,如果本轮抽查的过期key比例超过25%也就是5个,则继续抽查,直至比例低于25%或是循环流程的时间达到上限25ms;)
Redis通常两种方式混用;
②Redis如果内存溢出怎么办?(内存淘汰策略)
当Redis运行内存达到设置的最大运行内存(默认情况下64位操作系统无限制,32位为3G),就会触发内存淘汰机制,主要有8种方式,可以分为三类,
一类是不做内存淘汰,运行内存超过最大设置内存直接报错,这也是默认的内存淘汰策略;
一类是对设置了过期时间的数据进行淘汰:有random随机淘汰、ttl淘汰过期时间最长的、lru淘汰最近最久未使用的、lfu淘汰最不经常使用;
还有一类对全部数据进行淘汰:有random随机淘汰、lru淘汰最近最久未使用的、lfu淘汰最不经常使用;
8.Redis高可用
①主从复制
主从复制就是主服务器执行写操作,从服务器执行读操作,实现读写分离,提高系统并发量;
具体来说,主从服务器首次同步时,主服务器生成RDB文件并传给从服务器,从服务器依据RDB文件进行数据的全量复制,
之后主从服务器之间会维护一个TCP长连接,当主服务器收到了写命令时,也会将写命令异步地通过连接传给从服务器,保证主从服务器的数据一致性,
如果从服务器断连一段时间又重新连接,可以根据从服务器的偏移量查询环形缓冲区,如果可以找到就可以将断连期间主服务器执行的写命令传给从服务器,实现增量复制,如果没有找到就仍旧使用RDB文件进行全量复制;
主从连接的缺点:如果主服务器宕机,只能通过手动选择从服务器来取代主服务器,响应缓慢;
②Redis主从数据不一致时,怎么同步?
主、从服务器数据不一致通常是因为主、从节点间的命令复制是异步进行的;
对于主、从服务器数据不一致:
①首先尽量保证主从节点间的网络连接状况良好,避免因网络延迟导致的数据不一致;
②如果发现从服务器和主服务器有严重的数据不一致,可以通过SLAVEOF NO ONE将从服务器断开,然后重新连接到主服务器上,根据数据复制的进度触发增量同步或是全量同步;
③哨兵
哨兵机制就是在主从复制的基础上,通过新增几个哨兵节点,专门用于监控主服务器的运行状态,当主服务器宕机时,自动选出一个从服务器取代主服务器,保证Redis服务的可用性;
具体来说,哨兵节点会每隔一秒就给所有服务器发送PING命令,如果某个服务器超过一段时间未回复,则本哨兵认为此服务器已下线,称为主观下线,
如果认为该服务器主观下线的数量达到一定数量,就认为这个服务器客观下线,此时哨兵节点会选出其中一个哨兵作为leader,来进行接下来的故障转移:
首先根据每个从节点的优先级、对主服务器数据的复制进度、运行id来选出新的主节点;
然后令其它从节点都指向这个新的主节点,并通知客户主节点的更换;
最后当发生故障的原主节点重新上线时,将其转换为从节点指向新的主节点;
哨兵的缺点:哨兵和主从复制都只能缓解主服务器的读压力,但在高并发场景下写压力也很大,需要缓解;
④集群
集群机制部署多台master同时对外提供读写服务,每台master都配备了若干slave节点并内置了哨兵机制,在master发生故障时自动完成故障转移,保证了集群的高可用性;
同时,通过哈希槽算法让缓存数据均匀地分布在16384个哈希槽中,只要设定好哈希槽和master节点的映射关系,客户请求即可根据路由规则发送到目标master节点上,缓解了高并发场景下单个节点的读写压力;
⑤为什么是16384个哈希槽?
首先节点间通信时发送的心跳数据包中会携带当前节点与所有哈希槽的映射关系,采用16384个哈希槽需要占空间2KB,如果增加哈希槽数量会使得心跳包过大;
其次Redis集群中通常master不太可能超过1000个,16384个哈希槽已经完全够用了;
(具体来说,Redis集群采用了哈希槽算法来将键值对分配给各master节点,总共有2^14=16384个哈希槽,通过对key值计算CRC-16校验码,再对16384取模,将其分配到其中一个哈希槽,再根据哈希槽和master节点的对应关系寻址到对应的master节点进行操作;
——哈希槽算法的优点是当新增或删除节点时,只需要改变哈希槽和master节点的对应关系即可,key值和哈希槽的对应关系保持不变;)
⑥Redis Cluster在扩容/缩容期间可以提供服务吗?
可以,因为Redis Cluster提供了重定向机制,分为两种情况:
如果请求的key对应的哈希槽已经被迁移到其它节点中,就会返回一个MOVED重定向错误,告知客户端当前哈希槽是由哪个新节点负责,客户端会更新哈希槽分配信息,并向新节点发送请求;
如果请求的key对应的哈希槽正在被迁移过程中,就会返回一个ASK重定向错误,告知客户端新节点信息,此时客户端不会更新哈希槽分配信息,而是先向新节点发送一条ASKING命令,询问是否已经完成迁移,如果尚未完成迁移,则还会返回一个TRYAGAIN重试错误,告诉客户端过一段时间再重试,如果已经完成迁移,则客户端就可以向新节点发送请求了;
——ASK重定向和MOVED重定向区别在于前者客户端不会更新哈希槽分配信息,也就是说相同的请求下次还会发给旧节点;
⑦Redis Cluster各节点之间如何通信?
Redis集群在的各节点也会基于Gossip协议进行通信并维护一份集群内各节点的状态信息,Gossip消息包括:
MEET消息可以将新节点加入到集群中,
PING/PONG消息可以用来检查各节点的存活状态;
9.Redis事务——不重要,鸡肋功能,实际开发很少用,了解一下和MySQL的事务做一下对比即可
事务相关命令:MULTI、EXEC、DISCARD、WATCH,类似于MySQL事务中的begin、commit、rollback;
Redis 事务不支持原子性——因为Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的;
(为什么不支持回滚:Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中)
Redis事务有RDB+AOF持久化机制,但因为AOF在no和everysec刷盘策略下仍可能发生数据丢失,而always性能太差,所以Redis事务也不完全保证持久性;
不过Redis事务不支持原子性的问题可以通过Lua脚本来部分解决;
Lua脚本和Redis事务的区别:
①Redis事务是将一组命令打包成一个集合来执行,而Lua脚本实际上执行的是一段Lua代码,只是效果相当于多条命令;
②正因如此,Lua脚本的性能比Redis事务更好,因为减少了网络交互次数;
③Lua脚本可以实现复杂逻辑,Redis事务只是简单的命令组合;
④Lua脚本的原子性更好,不过也不能完全保证原子性,因为如果 Lua 脚本运行时出错并中途结束,出错之前执行的命令是无法被撤销的,不能实现回滚;
Lua脚本代码示例:
if redis.call(""get"",KEYS[1]) == ARGV[1] then
return redis.call(""del"",KEYS[1])
else
return 0
end
10.Redis双写一致性(缓存和数据库间的数据一致性)的解决方案
持久化能确保Redis中的数据存到磁盘中的一致性,Redis双写一致性则是指当磁盘的数据修改之后如何保证Redis中的数据同步修改;
具体来说有以下几种解决方案:
①旁路缓存模式
读操作首先读本地缓存,若命中直接返回,未命中则查分布式缓存,若命中则写入本地缓存之后再返回,若未满足则查询数据库;
写操作时,先写入数据库,然后删除两级缓存中的数据;
这种方案缓存一致性较强,适合读多写少的场景,但缺点在于:
Ⅰ. 首次请求数据必定不在缓存中,解决办法是可以对热点数据进行缓存预热,当对热点数据进行修改时,可以在热点数据缓存失效期间,给访问数据库的操作设置互斥锁,同一时间只允许一个请求访问数据库,避免大量并发请求同时访问数据库;
Ⅱ. 如果写操作频繁的话,由于缓存频繁被删除,导致缓存命中率会比较低,解决办法是可以在进行写操作时,更新数据库之后也更新缓存,并给更新缓存的操作加一个分布式锁,同一时间只允许一个线程更新缓存;
(因为更新数据库然后更新缓存有数据不一致的风险,更新缓存的操作可能会因为线程切换等延迟导致旧数据覆盖新数据:例如线程A更新数据库为100,然后更新缓存操作发生延迟,之后线程B更新数据库为80,然后更新缓存为80,之后线程A延迟结束更新缓存为100,此时数据库为80,缓存为100,加了分布式锁可以保证线程A先更新缓存)
Ⅲ. 仍有数据不一致的风险:例如在高并发场景下,A线程读取某个不在缓存中的数据,从数据库中读到数据100,在将数据写入缓存的过程中发生延迟,此时线程B将数据库中的该数据更新为80,由于缓存中不存在故不需要删除,之后线程A完成将数据100写入缓存的操作,此时缓存为100,数据库为80;由于写缓存的速度远快于写数据库,因此上述情况发生的可能性不大,但仍有可能;
(
如果删除缓存失败了怎么办?
另起一个订阅服务,监控Mysql的binlog,当binlog变化时,尝试删除缓存,
当删除失败时,就生成一个用于执行删除操作的消息,异步地继续尝试删除缓存,如果重试了一定次数仍然没能成功删除,就将消息发送到死信队列,等待进一步排查和处理;
可以先删除缓存,再更新数据库吗?
不可以,发生数据不一致的可能性很高,例如:线程A将某数据由80修改为100,先删除缓存,然后在更新数据库过程中,线程B读取该数据,缓存为空故读取数据库得到80并写入缓存,之后线程A完成更新数据库,此时缓存为80,数据库为100,数据不一致;
由于写数据库的速度远慢于写缓存,因此上述情况发生的可能性很大;
如何解决旁路缓存方案仍可能存在的数据不一致问题?
如果一致性要求不高,可以给缓存都设置较短的过期时间,这样即使不一致影响也不大,但缺点是会降低缓存命中率;
如果一致性要求较高,则可以采用延迟双删的方案;
)
②延迟双删模式
写操作:先删除缓存,然后更新数据库,之后延迟等待一段时间,再次删除缓存;
Ⅰ. 先删除缓存再更新数据库:是因为延迟双删的整个流程时间较长,尤其是延迟等待的时间,而进行第一次删除有可能提前实现数据的一致性,例如线程A进行写操作,首先删除缓存,然后更新数据库中的数据,之后延迟等待,此时线程B再进行读操作,就可以从数据库中获得新数据并放入缓存,这样就在线程A尚未完成整个延迟双删操作前实现了数据的最终一致性;
(如果没有第一次删除,则缓存中的旧数据可能会存放到线程A完成整个延迟双删流程执行完第二次删除缓存;
为什么不是更新数据库->第一次删除->等待->第二次删除?
这样也可以,个人感觉这样可以增大提前实现数据一致性的可能性,因为只需要单个线程自己完成更新数据库和删除缓存操作,就可能实现数据一致性,不需要其它线程再进行一次删除缓存操作;
)
Ⅱ. 延迟等待的时间:不小于从数据库中读取数据并写入缓存的时间,这样可以确保从数据库中读取的旧数据都已经写入缓存,
除此之外,还要考虑不大于缓存的过期时间,以及根据缓存的更新频率进行调整,
如果数据库采用了主从分离,则还需要考虑主库、从库数据同步的时间;
Ⅲ. 再次删除缓存就是为了确保删除写入缓存中的旧数据,这样下次读取的就是最新的数据;
延迟双删的优点:数据一致性相较旁路缓存方案更高,虽然不能保证强一致性,但可以达到最终一致性的要求;
(强一致性:任意时刻各节点的数据都是一致的;
最终一致性:各节点数据有时会不一致,但随着时间的迁移,各节点的数据总是向着一致的方向变化;)
缺点:多了一次删除缓存操作和等待时间,开销较大,不适合对性能要求较高的场景;
③读写穿透模式
当需要进行写操作时,如果cache中不存在,则直接写数据库,如果cache中存在,则写入cache中,由cache自行同步写入数据库,
这种方案很少用,主要Redis没有提供写入数据库的功能;
④异步缓存模式
当需要进行写操作时,直接写入cache,之后异步更新数据库,
这种方案的写性能很好,但数据一致性较差,因为cache可能还没写入数据库就宕机了,通常在消息队列和InnoDB的Bufferpool中用到;
三、ElasticSearch
1.倒排索引、正排索引
① 倒排索引?
就是首先用分词器将每个文档分成多个单词,然后将单词与所有包含这个单词的文档的id、单词出现频率、单词位于文档中的位置映射起来,
多个单词的集合就称为单词字典,与之映射的信息列表称为倒排列表,这两者就组成了倒排索引,
当进行检索时直接遍历单词字典即可找到所有包含这个单词的文档;
②正排索引?
正排索引将文档id与此文档内的所有单词及其出现频率、位置映射起来,
当进行检索时需要依次遍历每个文档内的每个单词;
③ 倒排索引&正排索引对比
倒排索引的优点是检索效率高,缺点是维护索引的成本都很高,当新增数据时,整个单词字典及其对应的倒排列表可能都需要改变;
正排索引的优点是维护成本低,新增数据时只需要添加一个文档id即可,缺点是检索效率低;
——Elasticsearch会自动根据字段的类型和使用情况来选择适合的索引方式。通常情况下,文本类型的字段会被设置为倒排索引,而数值类型的字段则会被设置为正排索引
2.分词器
①什么是分词器?常见的有哪些?
分词器负责对文档内容进行分词,建立单词字典,
常用的非中文分词器有:
Standard Analyzer:标准分词器,也是默认的分词器,作用是将英文转换成小写,中文只支持单字切分;
Simple Analyzer:简单分词器,通过非英文字符来分割文本信息,英文转换成小写,非英文不进行切分;
常见的中文分词器有:
IK Analyzer:包括两种分词模式——ik_max_word:细粒度切分模式,对文本进行最细粒度的切分,尽可能多地拆分出词语,ik_smart:智能模式,粗粒度切分模式,已经切分出的词语不会被其它词语再占有;
② 分词器原理?
首先处理原始文本,比如去掉html标签等,然后按照分词器规则切分单词,最后对切分后的单词进行处理,包括转小写、切除停用词、添加近义词等;
3.ElasticSearch常用数据类型
①ElasticSearch常用数据类型有哪些?
有关键字keyword,常见的数值类型、布尔类型、日期类型,非结构化文本text等
②es建表时字段类型的选择
用于精确匹配的字段使用keyword,用于范围查询的字段使用数值类型(如updated at、created at),用于分词模糊匹配的字段使用text;
③keyword和数值类型有什么不一样吗,为什么keyword不适合范围查询、数值类型不适合精确匹配?
首先,keyword属于字符串,会建立倒排索引,倒排索引由字典树(or FST?)+倒排列表组成,前者非常适合精确匹配,后者按docId在跳表中有序存储,可以使用跳表二分查找,因此在多条件查询结果合并时也非常高效(好像还有使用bitmap来合并的);
——字典树非常适合精确匹配,但如果要进行范围查询,就只能将其变为多个精确匹配再合并结果了(例如范围为1-50,就分成50个精确匹配),如果范围很大就效率非常低了;
而数值类型则是通过kd-tree来存储的,这是一个专为范围查询设计的数据结构,但缺点是叶子节点中的数据是按照value排序而不是按docId排序的,因此当需要对多个列表进行合并时,其效率就远不如keyword那样可以使用跳表二分查找了;
——kd-tree适合范围查询,精确匹配则不如keyword
④keyword和text的区别?
区别主要在于keyword是精确匹配,不会进行分词,而text是关键词搜索,会对查询条件和字段本身都进行分词;
4.Mapping
①什么是Mapping?
类似于数据库中的表结构,用来定义文档中各字段的名称、数据类型、是否采用索引、使用什么分词器等;
② Mapping有哪些?你是怎么定义Mapping的?
Mapping分为动态Mapping和显式Mapping:动态Mapping就是根据待索引数据自动建立索引,自动定义映射类型(插入数据时也会自动推断Mapping信息,所以不需要指定Mapping),
显式Mapping就是手动设置各字段的属性;
(如果想要某个字段不被索引,可以在Mapping中将其index属性设为false)
我是手动定义映射关系的,将需要全文搜索的字段设为text类型,采用ik_smart智能分词器,需要精确值的字段设为keyword,其他的按照各自数据类型进行设置;
5.ElasticSearch常见的查询语句
①常见的查询语句有哪些类型?
Term查询:精确匹配查询,不对查询条件做分词,只有和查询条件精确匹配的文档才会被搜索到;
Match查询:全文搜索,对查询条件进行分词,对每个单词进行逐个查询,最后汇总得到的结果;
Multi-Match查询:单条件多字段查询,可以设置每个字段的权重;
Range查询:范围查询,查询满足条件范围内的所有数据;
Bool查询:多条件多字段查询,其内部可以包含Term查询、Match查询等查询语句,并可以根据must、should、must not、filter等查询子句来算分和过滤;
②bool查询的四种子句
must:必须全部满足的查询条件,且参与计算分值;
must_not:必须全部不满足的查询条件,不参与计算分值;
should:需要部分满足的查询条件,参与计算分值,具体需要满足几个条件由minimum_should_match参数决定(当bool查询中不包含filter和must子句时,此参数默认为1,即should子句中至少需要满足一个条件,当存在filter或must子句时,此参数默认为0,表示should子句中的查询条件可以全部不满足,不过如果有满足的可以提高评分)
filter:必须全部满足的查询条件,但不参与计算分值;
③filter查询和query查询的区别
Ⅰ. 在bool查询中,must和should属于query查询,会参与计算分值,且查询结果需要按照相关分数进行排序,
filter和must_not则属于filter查询,不会参与计算分值,查询结果也不需要按分值排序,因此filter的执行效率更高;
Ⅱ. filter查询的结果会被缓存,而query查询结果不会被缓存;
因此,对于不需要计算相关性打分的场景,应当使用filter替代must提高查询效率;
④es几种模糊查询的区别——match、wildcard、fuzzy
match:分词匹配检索,哪些词能查到取决于分词器;
wildcard:通配符检索,功能和SQL中的like模糊匹配一样,前后都可以拼接*,表示匹配0到多个任意字符;
fuzzy:纠错检索,对输入条件有一定的纠错能力;
6.ES分片和副本
分片(shard)——类似Redis的Cluster
当有大量的文档时,由于内存的限制、磁盘处理能力不足、无法足够快的响应客户端的请求等,一个节点可能不够。这种情况下,数据可以分为较小的分片。每个分片放到不同的服务器上。
可以通过指定路由字段和映射规则来将对应的文档路由到指定分片中,进行操作时带上路由字段即可直接查询所在分片,如果没有带上路由字段,则会遍历操作全部分片,并将各分片的结果组合在一起。
为提高查询吞吐量或实现高可用性,可以使用分片副本。
副本是一个分片的精确复制,每个分片可以有零个或多个副本。ES中可以有许多相同的分片,其中之一被选择更改索引操作,这种特殊的分片称为主分片。
当主分片丢失时,如:该分片所在的数据不可用时,集群将副本提升为新的主分片
7.ES优化
①profile分析es语句
在请求中添加"profile": true即可开启,主要关注query字段中每个查询操作,以及breakdown中每个细分操作的执行次数和耗时,找出导致慢查询的操作,
breakdown主要包含:
next_doc、advance:表示确定下一个匹配文档所需的时间,后者是前者的低级版本;
8.ES与MySQL、Redis的对比
ElasticSearch的数据存储在磁盘中(不过提供了大量优化和缓存机制),由于采用了倒排索引,关键词搜索的性能非常高,但没有事务机制,可能会出现脏数据,不适合存放原始数据;
MySQL的数据存储在磁盘中,由于采用了B+树作为索引,需要遵循最左匹配原则,关键词搜索(模糊匹配)的性能较低,且当数据量过大、查询条件较多时,B+树层数也会增多,导致查询速度也会变慢,但由于提供了事务机制,可以保证没有脏数据,适合存放原始数据;
Redis的数据存储在内存中,读写性能极高,但不适用于存储大量数据,通常用来作为缓存使用;
MySQL存储原始数据,写数据先向MySQL中写,之后异步将MySQL的变更写入ES中,
查询数据先查ES,如果需要绝对准确的数据,可以再用主键id回查MySQL;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界