数据库篇
MySQL 数据库相关
- MySQL 有哪些常见的存储引擎?
- 索引的原理是什么?
- MySQL 三种日志分别起到什么作用?( redoLog , undoLog , binLog )
- 为什么选择 B +树索引?
- 什么情况下会出现索引失效?
- 如何查看执行计划?
- 如何优化 SQL 查询?
- MySQL 主从复制原理?
- 数据库死锁的原因?如何快速定位并解决?
事务隔离级别
- 事务有哪些隔离级别?
- 每种隔离级别会导致什么问题?
- mysql 和 oracle 默认情况下分别采用哪种隔离级别?
- mysql 如何解决幻读的?
1.索引的原理是什么?
B+树:多路平衡查找树
参考1
参考2
参考3
参考4
2.为什么选择B+树索引?
MySQL 选择 B+ 树作为索引的数据结构,主要是因为 B+ 树能够提供更好的查询效率、较快的数据插入和删除速度以及可拓展性,具有以下几点优势:
- 高效的查询效率:B+ 树对于非叶子节点来说,不存储数据,而只存储当前节点分裂后分裂出的子节点的索引,这种设计能够较快地锁定范围,减少查询次数,提高查询效率。
- 顺序访问特性:B+ 树索引的叶子节点存储了所有数据记录的指针,且所有叶子节点按顺序相连,这就可以通过顺序访问来提升查询性能。
- 快速的插入和删除操作:由于B+ 树索引的元素按顺序存储,因此在进行插入和删除操作时不需要进行元素移动,只需要调整相应的指向即可,这将大大降低维护索引的成本。
- 可拓展性:B+ 树能够快速支持范围查询,在数据集增长的情况下也可以很容易地进行扩展,而且它对于分布式支持也非常友好。
- 另外,与其他数据结构相比,B+ 树还具有不易破坏平衡性、支持大型数据集、支持范围查询和支持外部存储器访问等优点,因此成为一种理想的索引结构。
总的来说,MySQL选择B+树作为索引是基于查询效率、快速的插入和删除操作、可拓展性等多方面考虑。B+树索引在数据库中得到广泛应用,已经成为实现高效及高可靠性的关键技术之一。
索引为什么采用B+树,而不用B—树,红黑树?
答案:提升查询速度,首先要减少磁盘IO次数,也就是要降低树的高度。
- 平衡二叉树,红黑树,都属于二叉树。时间复杂度为O(n),当表的数据量千万时,树的深度很深,mysql读取时消耗大量IO,另外,
InnoDB引擎采用页为单位读取,每个节点一页,但是二叉树每个节点存储一个关键词,导致空间浪费
- B-树,非叶子节点存储数据,占用较多空间,导致每个节点指针少很多,无形增加树的深度
- B+树数据都存储在叶子节点,非叶子节点值存储键值+指针,索引树更加扁平,三层深度可以支持千万级表存储,同时叶子节点之间通过链表关联,范围查找更快。
- 红黑树和为什么使用红黑树使用红黑树主要是为了解决链表过长性能低的问题,红黑树是一种接近平衡二叉树,但又不是绝对平衡,结构上是个树形状,是一个有序的结构,在每一个节点上增加一个存储位,表示节点的颜色,可以是红色或者黑色。R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的原理
红黑树是一种自平衡二叉搜索树,能够保持树的高度近似平衡。这种数据结构在插入、删除、查找等基本动态集合操作上具有很好的性能表现,快速、高效地支持查找、插入和删除等基本操作,被广泛应用于诸如内核、虚拟机、数据库、网络路由器和操作系统等领域。
红黑树的结点属性:
- 每个节点是红色或者黑色。
- 根节点是黑色的。
- 每个叶子节点(Nil节点,空节点)是黑色的。
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从任意一个节点到其子树中每个叶子节点的所有路径都包含相同数目的黑色节点。 为了保持红黑树的平衡,任何节点的左右子树高度差不能超过1,否则就需要进行旋转操作(左旋或右旋)。
- 左旋操作:将右节点上移至与父节点同级,该右节点成为该节点的父节点。
- 右旋操作:将左节点上移至与父节点同级,该左节点成为该节点的父节点。
- 红黑树的插入操作的实现过程如下所示:
- 将新节点插入红黑树中,标记其颜色为红色。
- 根据红黑树的五条性质进行判断和操作:
- 如果插入的节点是红色,且插入节点的父节点也是红色,则需要对红黑树进行旋转调整,使其满足红黑树的性质。
- 如果插入的节点是红色节点,且其父节点是黑色节点,则红黑树不需要进行调整。
- 如果插入的节点是黑色节点,则红黑树也不需要进行调整。
- 如果插入的节点是红色节点,且其父节点是黑色节点的情况下,需要进行颜色调整的操作,即将插入的节点颜色设置为黑色,其左右子节点颜色设置为红色。
- 将根节点颜色置为黑色,确保红黑树的性质得到维护。
- 总的来说,红黑树是一种非常高效的自平衡二叉搜索树,其保持二叉搜索树的高度平衡的同时,能够快速、高效地支持查找、插入和删除等基本操作。同时,由于红黑树的特点,它也是一种常用的数据结构,被广泛应用于很多领域中。
红黑树的特性:
- 每个节点或者是黑色,或者是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
旋转和颜色变化规则
- 添加的节点必须为红色
- 变色的情况:当前结点的父亲是红色,且它的叔结点也是红色:
- 把父节点设置为黑色
- 把叔节点设置为黑色
- 把祖父节点设置为红色
- 把当前指针定义到祖父节点,设为当前要操作的
- 左旋的情况:当前父节点是红色,叔节点是黑色,且当前的节点是右子树。
- 以父节点作为左旋。
- 右旋的情况:当前父节点是红色,叔节点是黑色,且当前的节点是左子树。
- 把父节点变成黑色
- 把祖父节点变为红色
- 以祖父节点右旋转
平衡二叉树(AVL)的性质
它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
- 红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
- 平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
3.什么情况下回出现索引失效?
索引失效的几种情况
- 复合索引不按“左前缀”原则,会导致索引失效,例如:index(a,b) where b = 1 and a = 2;
范围后的索引列失效,例如: where a > 2 and b = 1 这就导致b索引列的索引失效
- like %123 或者%123% 这会导致索引失效;可以改为 like 123% ,或者使用覆盖索引,就是select的字段在索引列中 (因为索引B+树是按照索引值
有序排列的,只能根据前缀进行比较)
- 对索引列进行运算,会导致索引失效,例如:where a+1 = 2; 可以改为 where a=1; 还有where round(a)=1,可以创建函数索引index(round(a))
因为索引B+树保存的索引原始值,而不是计算后的值,故不能直接查询比较。若对处理后的结果建立索引,则可以正常使用该索引。
- 索引类型的隐式转换,会导致索引失效,例如:索引age的类型 为varchar比较值为int,如 where age > 10 不会走索引,因为MySQL在遇到字符串和数字
比较的时候,会自动把字符串转为数字,然后再进行比较。相当于为索引age套了个转换函数where CAST(ageAS signed int) > 10,故索引失效。
反之,比较的值为字符串where age > ‘10’ ,则不会出现索引失效。
- or的左右列不都是索引列,会导致索引失效,例如:where a=1 or b=2 ,只有当a和b都是索引列时才可以让索引有效(如果a、b是复合索引也会失效)
(所以一般使用in),or两边为‘>’和‘<’范围查询时,索引也会失效
- is null 和 is not null 会导致索引失效,因为索引列规定是not null的,所以is null 和 is not null 都没有可比行,会全表扫描
- in()括号里面的数量超过一定数量的时候也会导致索引失效,主键临界值是7个,唯一索引和普通索引临界值是4个
- !=、<>、not in会导致索引失效
- 当全表扫描比索引速度快时,索引会失效
- select * from A where A.id in (select B.id from B);
select * from A where exist(select 1 from B where A.id = B.id)
区别:
- 第一个是先把A和B的所有id查出来,然后外层循环是A的id,内层循环是B的id,找到匹配的存到结果集中;循环次数相当于 A.length * B.length;
适用于B的数据量比较小时
- 第二个是循环A的id,每次用A.id去B中查询看是否存在,循环次数相当于 A.length;适用于B的数据量比较大时
4.如何查看执行计划?
explain | 索引优化的这把绝世好剑,你真的会用吗?
5.如何优化sql查询?
谈谈 SQL 优化的经验
- 查询语句无论是使用哪种判断条件等于、小于、大于,WHERE 左侧的条件查询字段不要使用函数或者表达式
- 使用 EXPLAIN 命令优化你的 SELECT 查询,对于复杂、效率低的 sql 语句,我们通常是使用 explain sql来分析这条sql语句,这样方便我们分析,进行优化。
- 当你的 SELECT 查询语句只需要使用一条记录时,要使用 LIMIT 1。不要直接使用 SELECT *,而应该使用具体需要查询的表字段,因为使用 EXPLAIN 进行分析时,
SELECT * 使用的是全表扫描,也 就是 type =all 。
- 为每一张表设置一个 ID 属性。
- 避免在 MHERE 字句中对字段进行 NULL
- 判断避免在 WHERE 中使用!或>操作符
- 使用 BETWEEN AND 替代 IN
- 为搜索字段创建索引
- 选择正确的存储引擎,InnoDB、MyISAM、MEMORY 等
- 使用 LIKE%abc%不会走索引,而使用 LIKE abc%会走索引。
- 对于枚举类型的字段(即有固定罗列值的字段),建议使用 ENUM 而不是 VARCHAR,如性别、星期、类型、类别等。
- 拆分大的 DELETE 或 INSERT 语句,进行批量插入或者批量删除操作,避免一下插入太多数据或者删除太多数据,分批插入,每批插入1000条,事务处理,
插入成功的批次数据不会回滚,失败的批次才会回滚,提升插入大数据的效率
- 选择合适的字段类型,选择标准是尽可能小、尽可能定长、尽可能使用整数。
- 字段设计尽可能使用 NOT NULL
- 进行水平切割或者垂直分割
一条sql执行过长的时间,你如何优化,从哪些方面入手?
- 查看是否涉及多表和子查询,优化Sql结构,如去除冗余字段,是否可拆表等
- 优化索引结构,看是否可以适当添加索引
- 数量大的表,可以考虑进行分离/分表(如交易流水表)
- 数据库主从分离,读写分离
- explain分析sql语句,查看执行计划,优化sql
- 查看mysql执行日志,分析是否有其他方面的问题
一条sql执行很慢,可能是因为什么? 怎么优化?(慢查询优化)
一个 SQL执行的很慢,我们要分两种情况讨论:
- 大多数情况下很正常,偶尔很慢,则有如下原因
- 数据库在刷新脏页,例如redo log写满了需要同步到磁盘。
- 执行的时候,遇到锁,如表锁、行锁。
- 这条SQL语句一直执行的很慢,则有如下原因。
- 没有用上索引:例如该字段没有索引;
- 由于对字段进行运算、函数操作导致索引失效,无法用索引。
- 数据库选错了索引。
日常工作中,MySQL如何优化?
- 分页优化。比如电梯直达,limit 10000, 10 先查找起始的主键id,再通过id>#{value}往后取10条
- 尽量使用覆盖索引,索引的叶子节点中已经包含了要查询的字段,减少回表查询
- SQL优化(索引优化,小表驱动大表,虚拟列,适当增加冗余字段减少连表查询,联合索引,排序优化,慢查询日志explain分析执行计划)
- 设计优化(避免使用NULL,用简单数据类型如int,减少text类型,分库分表)
- 硬件优化(使用SSD减少I/O时间,足够大的网络带宽,尽量大的内存)
百万级别或以上的数据如何删除
关于索引: 由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,
这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建
的索引数量是成正比的。 所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)然后删除其中无用数据(此过程需要不到两分钟)
删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。
你知道哪些数据库结构优化的手段?
- 范式优化: 比如消除冗余(节省空间)
- 反范式优化:比如适当加冗余等(减少join)
- 限定数据的范围:务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。
- 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
- 拆分表:分区将数据在物理上分隔开,不同分区的数据可以指定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,
- 而不必进行全表扫描,明显缩短了查询时间。
- 另外处于不同磁盘的分区也将对这个表的数据传输分散在不同的磁盘I/O,一个精心设置的分区可以将数据传输对磁盘I/O竞争均匀地分散开。对数据量大的时时表
可采取此方法。可按月自动建表分区。
数据量大的情况下怎么优化查询(大数据优化):
- 对查询条件建立索引
- 使用redis或者本地new HashMap进行缓存数据,对一些不经常修改的数据进行初始化缓存
- 分区,分库,分表,主从复制,读写分离,冷热数据处理,硬件方面就是增加数据库的内存
- 分库与分表的目的在于,减小数据库的单库单表负担,提高查询性能,缩短查询时间。
- 通过分表,可以减少数据库的单表负担,将压力分散到不同的表上,同时因为不同表上的数据量少了,起到提高查询性能、缩短查询时间的作用,此外,
可以很大的缓解表锁的问题。
分库与分表带来的分布式困境与应对之策 :
- 数据迁移与扩容问题----一般做法是通过程序先读出数据,然后按照指定的分表策略再将数据写入到各个分表中。
- 分页与排序问题----需要在不同的分表中将数据进行排序并返回,并将不同分表返回的结果集进行汇总和再次排序,最后再返回给用户。
一道场景题:假如你所在的公司选择MySQL数据库作数据存储,一天五万条以上的增量,预计运维三年,你有哪些优化手段(数据量大的问题)?
- 设计良好的数据库结构,允许部分数据冗余,尽量避免join查询,提高效率。
- 选择合适的表字段数据类型和存储引擎,适当的添加索引。
- MySQL数据库主从复制读写分离。
- 分表,减少单表中的数据量,提高查询速度。
- 添加缓存机制,比如Memcached,Apc等。
- 不经常改动的页面,生成静态页面。
- 书写高效率的SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE。
数据库高并发是我们经常会遇到的,你有什么好的解决方案吗?
- 在web服务框架中加入缓存。在服务器与数据库层之间加入缓存层,将高频访问的数据存入缓存中, 减少数据库的读取负担。
- 增加数据库索引,进而提高查询速度。(不过索引太多会导致速度变慢,并且数据库的写入会导致索引的更新,也会导致速度变慢)
- 主从读写分离,让主服务器负责写,从服务器负责读。
- 将数据库进行拆分,使得数据库的表尽可能小,提高查询的速度。
- 使用分布式架构,分散计算压力。
mysql慢查询的12个原因:
(程序员田螺)
- sql没有加索引
- sql索引不生效
- limit深分页问题
- 单表数据量太大
- join或者子查询过多
- in元素过多
- 数据库存在脏页
- order by走文件排序
- 达不到锁
- delete+ in子查询不走索引
- group by使用临时表和文件排序
- 系统或网络资源不够
6.mysql主从复制原理?
主从复制原理
- 可以看到mysql主从复制需要三个线程:master(binlog dump thread)、slave(I/O thread 、SQL thread)
- binlog dump线程: 主库中有数据更新时,根据设置的binlog格式,将更新的事件类型写入到主库的binlog文件中,并创建log dump线程通知slave有数据更新。
当I/O线程请求日志内容时,将此时的binlog名称和当前更新的位置同时传给slave的I/O线程。
- I/O线程: 该线程会连接到master,向log dump线程请求一份指定binlog文件位置的副本,并将请求回来的binlog存到本地的relay log中。
- SQL线程: 该线程检测到relay log有更新后,会读取并在本地做redo操作,将发生在主库的事件在本地重新执行一遍,来保证主从数据同步。
主从复制的优点:
- 实现服务器负载均衡
- 通过复制实现数据的异地备份
- 提高数据库系统的可用性
存在问题:
从数据库具有读log文件的延迟,如何解决?
由于从数据库存在更新从库中SQL数据的延迟,万一主库在从库读取binlog的时候宕机,那么数据可能丢失,主要是由于从库只有一个sql Thread去更新从库数据,
但是主库写压力大,也就是主库会有很多写任务,同时还要有IO线程与从库进行binlog输出,所以复制很可能延时。
解决方法:
- 半同步复制—-解决数据丢失的问题
半同步复制,可以看到当主库进行更新时,在binlog写的过程中,会主动通知Dump进程(输出IO进程)开启,与从库进行数据的同步更新,然后从库会返回一个
ack信号给主库的Dump进程,收到ack确认后,会给用户提交的修改进程发送信号,让其继续执行,当然当从库比较多时,这种方法不能保证全部的从库都进行更新,
如果网络异常或从库宕机,主库压力过大等,都会造成超时,影响客户响应,并行复制可以一定程度上解决类似问题
- 并行复制—-解决从库复制延迟的问题
正常主从复制(异步复制)的方式,也就是主库直接更新数据,但是主从的复制是在主库更新后或者过程中进行,这样显然容易使数据出问题,比如会丢失修改数据等
为什么会主从延迟?
正常情况下,如果网络不延迟,那么日志从主库传给从库的时间是相当短,所以T2-T1可以基本忽略。
最直接的影响就是从库消费中转日志(relaylog)的时间段,而造成原因一般是以下几种:
- 从库的机器性能比主库要差
- 从库的压力大
按照正常的策略,读写分离,主库提供写能力,从库提供读能力。将进行大量查询放在从库上,结果导致从库上耗费了大量的CPU资源,进而影响了同步速度,造成主从延迟。
- 大事务的执行
一旦执行大事务,那么主库必须要等到事务完成之后才会写入binlog。
- 主库的DDL(alter、drop、create)
- 只读节点与主库的DDL同步是串行进行,如果DDL操作在主库执行时间很长,那么从库也会消耗同样的时间,比如在主库对一张500W的表添加一个字段耗费了10分钟,
那么从节点上也会耗费10分钟。
- 从节点上有一个执行时间非常长的的查询正在执行,那么这个查询会堵塞来自主库的DDL,表被锁,直到查询结束为止,进而导致了从节点的数据延迟。
怎么减少主从延迟
主从同步问题永远都是一致性和性能的权衡,得看实际的应用场景,若想要减少主从延迟的时间,可以采取下面的办法:
- 降低多线程大事务并发的概率,优化业务逻辑
- 优化SQL,避免慢SQL,减少批量操作,建议写脚本以update-sleep这样的形式完成。
- 提高从库机器的配置,减少主库写binlog和从库读binlog的效率差。
- 尽量采用短的链路,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输的网络延时。
- 实时性要求的业务读强制走主库,从库只做灾备,备份。(在实际应用场景中,对于一些非核心的场景,比如库存,支付订单等,
需要直接查询主库,其他非核心场景,就不要去查主库了)
主从复制的几种架构方式
- 一主一从,基础的主从结构。
- 主主复制(两个主机在同一等级上,没有主从之分)
- 一主多从,适用于增删改少,查询多的业务。
- 多主一从(Mysql 5.7开始支持),适用于增删改较多,查询少的业务。
- 联级复制
mysql数据库高可用架构:
mysql高可用解决方案都有哪些?
7.数据库死锁原因?如何快速定位并解决?
mysql死锁产生的原因:
order_no是普通索引
select id from t_order where order_no = '10086' for update;
因为 order_no 不是唯一索引,所以行锁的类型是间隙锁,于是间隙锁的范围是(1006, +∞)。
那么,当事务 B 往间隙锁里插入 id = 1008 的记录就会被锁住。因为当我们执行以下插入语句时,会在插入间隙上再次获取插入意向锁。
Insert into t_order(order_no,create_date)values(1008,now());
插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。
而间隙锁与间隙锁之间是兼容的,所以两个事务中 select ... for update 语句并不会相互影响。
案例中的事务 A 和事务 B 在执行完后 select ... for update 语句后都持有范围为(1006,+∞)的间隙锁,而接下来的插入操作为了获取到插入意向锁,
都在等待对方事务的间隙锁释放,于是就造成了循环等待,导致死锁。
为什么间隙锁与间隙锁之间是兼容的?
查询mysql官方文档表明间隙锁在本质上是不区分共享间隙锁或互斥间隙锁的,而且间隙锁是不互斥的,即两个事务可以同时持有包含共同间隙的间隙锁。
这里的共同间隙包括两种场景:其一是两个间隙锁的间隙区间完全一样;其二是一个间隙锁包含的间隙区间是另一个间隙锁包含间隙区间的子集。
间隙锁本质上是用于阻止其他事务在该间隙内插入新记录,而自身事务是允许在该间隙内插入数据的。
也就是说间隙锁的应用场景包括并发读取、并发更新、并发删除和并发插入。
插入意向锁是什么?
注意!插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁。
这段话表明尽管插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作。
如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。
插入意向锁与间隙锁的另一个非常重要的差别是:
尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁
区间内则是可以的)。另外,我补充一点,插入意向锁的生成时机:每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,
如果已加间隙锁,那 Insert 语句应该被阻塞,并生成一个插入意向锁
如何避免死锁?
死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。
只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。
在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:
- 设置事务等待锁的超时时间。
当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。
在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。
当发生超时后,就出现下面这个提示:
ERROR 1205 (HY000):Lock wait timeout exceeded;try restarting transaction;
- 开启主动死锁检测。
主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。
当检测到死锁后,就会出现下面这个提示:
ERROR 12123 (40001):Deadlock found when trying to get lock;try restarting transaction;
上面两种策略是「当有死锁发生时」的避免方式。
我们可以回归业务的角度来预防死锁,对订单做幂等性校验的目的是为了保证不会出现重复的订单,那我们可以直接将 order_no 字段设置为唯一索引列,
利用它的唯一下来保证订单表不会出现重复的订单,不过有一点不好的地方就是在我们插入一个已经存在的订单记录时就会抛出异常。
MySQL遇到过死锁问题吗,你是如何解决的?
我排查死锁的一般步骤是酱紫的:
- 查看死锁日志 show engine innodb status;
- 找出死锁 Sql
- 分析sql加锁情况
- 模拟死锁案发
- 分析死锁日志
- 分析死锁结果
8.mysql三种日志分别起到什么作用?
- 重做日志redo log
- 回滚日志undo log
- 二进制日志binlog(记录所有更改数据的语句(DML,新增,删除,修改),还用于复制,恢复数据库,审计)
- 中继日志relay log
- 重做日志和回滚日志与事务操作息息相关,
- 二进制日志常用于主从复制和数据恢复
- redo log作用:确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性
这一特性。
- undo log作用:保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读
事务是如何通过日志来实现的:
- 事务是基于重做日志文件(redo log)和回滚日志(undo log)实现的。
- 每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。
- 每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。
undo log 主要实现数据库的一致性。
mysql的MVCC:多版本并发控制,在数据库中的实现,就是为了解决读写冲突,他的实现原理主要是依赖记录中的三个隐式字段,undo日志,read view来实现的。
隐式字段:
- DB_TRX_ID:最近修改(修改/插入)事务id:记录创建这条记录/最后一次修改该记录的事务id;
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
- DB_ROW_ID:隐含的自增id(隐藏主键),如果数据表没有主键,产生一个聚簇索引。
9.事务有哪些隔离级别?
读未提交,读已提交,可重复读,串行化
10.每种隔离级别会导致什么问题?
- 读未提交导致脏读,不可重复读,幻读
- 读已提交导致不可重复读,幻读,可以避免脏读
- 可重复读,可以避免脏读和不可重复读,但幻读仍有可能发生
- 可串行化读,在并发情况下,可串行化的读取的结果是一致的,没有什么不同,比如不会发生脏读和幻读;该级别可以防止脏读、不可重复读以及幻读;
- mysql默认级别为可重复读,在可重复读隔离级别下,通过多版本并发控制(MVCC)+间隙锁(Next-key Locking)防止幻读
- 不可重复读的重点是修改,幻读的重点在于新增或者删除,强调范围
- 数据库并发会带来脏读、幻读、丢弃更改、不可重复读这四个常见问题
11.mysql和oracle默认情况下分别采用哪种隔离级别?
mysql可重复读,oracle读已提交
12.mysql如何解决幻读?
mysql解决幻读的方式
-
innoDB的repeatable read可重复读这种隔离级别通过间隙锁+MVCC解决了大部分幻读问题,只有一种特殊的幻读情况无法解决
-
想要解决幻读,可以使用serializable这种隔离级别,或者使用RR也能解决大部分的幻读问题。
-
在RR级别下,为了避免幻读的发生,要么就是使用快照读,要么就是在事务一开始就加锁
-
RR隔离级别下间隙锁才有效,RC隔离级别下没有间隙锁;
-
RR隔离级别下为了解决“幻读”问题,“快照读”依靠MVCC控制,“当前读”通过间隙锁控制
-
间隙锁和行锁合称next-key lock,每个next-key lock 是前开后闭区间
-
间隙锁的引入,可能会导致同样语句锁住更大的范围,影响比并发
mysql行锁,间隙锁,临间锁:
-
行锁 也叫记录锁 锁定的是某一行一级
-
间隙锁 锁定的是记录与记录之间的空隙,间隙锁只阻塞插入操作,解决幻读问题
-
临键锁 nextkeylock 是行锁与间隙锁的并集,是mysql加锁的基本单位
- 原则1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。
- 原则2:查找过程中访问到的对象才会加锁。
- 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
- 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
案例:一张表t id(主键)、c(普通索引)、d 字段 插入数据(0,0,0),(5,5,5),(10,10,10),(15,15,15)
-
update t set d=1 where id = 7 主键索引上的 (5,10)间隙锁
-
update t set d=1 where id = 5 主键索引上的 5行锁
-
update t set d=1 where c = 7 普通索引上的 (5,10)间隙锁
-
update t set d=1 where c = 5 普通索引上的 (0,5]临键锁 (5,10)间隙锁
-
update t set d=1 where c <11 普通索引上的 (0,15]临键锁
-
update t set d=1 where c >=10 普通索引上的 (5,10]临键锁 (10,~]的临键锁
-
update t set d=1 where c >=10 and c <11 普通索引上的 (5,15]临键锁
-
update t set d=1 where id >=10 and id <11 主键索引上的 10行锁 (10,15)间隙锁
分库分表
分库分表相关问题
- 你用过分库分表吗?
- 如何实现单个维度的非sharding-key的查询问题?比如通过userlD作为sharding-key,那么如何实现基于userName进行查询?(映射法、基因法)
- 如何实现多个维度的多个字段非 sharding - key 如何查询?时间、用户名、类别等.…
- 多维度查询需要配合其他查询引擎,那么如何实现数据同步?如何保证双写的一致性?
- 很多情况下并不是一开始就实现分库分表,等我们需要分库分表的时候如何进行数据迁移
1.你用过分库分表吗?
2.如何实现单个维度的非sharding-key的查询问题?比如通过userlD作为sharding-key,那么如何实现基于userName进行查询?(映射法、基因法)
3.如何实现多个维度的多个字段非 sharding - key 如何查询?时间、用户名、类别等.…
4.多维度查询需要配合其他查询引擎,那么如何实现数据同步?如何保证双写的一致性?
5.很多情况下并不是一开始就实现分库分表,等我们需要分库分表的时候如何进行数据迁移
中间件篇
MQ 的相关问题
- 你用过消息队列吗?用了哪个消息队列?
- 在使用 MQ 的时候怎么确保消息100%不丢失?
- 怎么解决消息的重复消费问题?
- 如何实现顺序消息?
- 如何解决引入消息后的事务问题?
1.你用过消息队列吗?用了哪个消息队列?
RabitMQ,RocketMQ,kafka,ActiveMQ,补充Redis
2.在使用 MQ 的时候怎么确保消息100%不丢失?
在了解了消息中间件的机制后,主要从这三个方面来看
- 生产端,不丢失
- MQ服务端,存储本身不丢失
- 消费端,不丢失
延伸:kafka如何解决消息不丢失?
- 生产端
生产端的职责就是,确保生产的消息能够到达MQ服务端,这里我们需要有一个响应来判断本次的操作是否成功
Future send(ProducerRecord record Callback callback)
比如,上面的代码就是通过一个Callback函数,来判断消息是否发送成功,如果失败,我们需要补偿处理
另外,为了提升发送时的灵活性,kafka提供了多种参数,供不同业务自己选择
- 参数acks
该参数标识有多少个分区副本收到消息,才认为本次发送消息是成功的
acks=0,只要发送消息就认为成功,生产端不等待服务器节点的响应
acks=1,表示生产者收到leader分区的响应就认为发送成功
acks=-1,只有当ISR中的副本全部收到消息时,生产端才会认为是成功的,这种配置是最安全的,但是由于同步的节点较多,吞吐量会降低
- 参数retries
表示生产端的重试次数,如果重试次数用完后,还是失败,会将消息临时存储在本地磁盘,服务恢复后再重新发送,建议值retries=3
- 参数retry.backoff.m
消息发送超时或者失败,间隔的重试时间,一般推荐多的设置时间是300毫秒
这里要特别注意一种情况,如果MQ服务没有正常响应,不一定代表消息发送失败,也可能是响应时正好赶上网络抖动,响应超时
当生产端做完这些,一定能保证消息发送成功,但可能发送多次,这样就会导致消息重复。解决消息重复消费的我们后面再讲解决方案
2.MQ服务端
如何保证存储端的消息不丢失呢? 就是确保消息持久化到磁盘,大家很容易想到就是刷盘机制。
刷盘机制分同步刷盘和异步刷盘:
- 生产者消息发过来时,只有持久化到磁盘,MQ的存储端Broker才返回一个成功的ACK响应,这就是同步刷盘。它保证消息不丢失,但是影响了性能。
- 异步刷盘的话,只要消息写入PageCache缓存,就返回一个成功的ACK响应。 这样提高了MQ的性能,但是如果这时候机器断电了,就会丢失消息。
Broker一般是集群部署的,有master主节点和slave从节点。消息到Broker存储端,只有主节点和从节点都写入成功,才反馈成功的ack给生产者。
这就是同步复制,它保证了消息不丢失,但是降低了系统的吞吐量。与之对应的就是异步复制,只要消息写入主节点成功,就返回成功的ack,它速度快,但是会有性能问题
kafka MQ服务端如何保证存储端的消息不丢失?
kafka MQ服务端一般是多分区部署的,也可能会丢失消息,比如:一个分区突然挂掉,那么怎么保证这个分区的数据不丢失,我们会引入副本概念,通过备份来解决这个问题
具体可设置哪些参数?
- 参数 replication.factor
表示分区副本的个数,replicationl.facotr>1 当leader副本挂了,follower副本会被选举为leader继续提供服务
- 参数 min.insync.replicas
表示ISR最少的副本数量,通常设置min.insync.replicas >1,这样才有可用的follower副本执行替换,保证消息不丢失
- 参数unclean.leader.election.enable
是否可以把非ISR集合中的副本选举为leader副本。如果设置为true,而follower副本的同步消息进度落后较多,此时被选举为leader,会导致消息丢失,慎用
3.消费端
消费端要做的是把消息完整的消费处理掉,但是这里有个提交位移的步骤
有的同学,考虑到处理业务耗时比较长,会单独启动线程拉取消息存储到本地内存队列,然后搞个线程池并行处理业务逻辑。
这样设计有个风险,本地消息如果没有处理完,服务器宕机了,会造成消息丢失。
正确做法:拉取消息--》业务处理--》提交消费位移
关于提交位移,kafka提供了集中参数配置
参数 enable.auto.commit 标识消费位移自动提交
如果拉取了消息,业务逻辑还没处理完,提交了消费位移但是消费端却挂了,消费端恢复或其他消费端接管该分片再也拉取不到这条消息,
会造成消息丢失,所以我们通常设置enable.atuo.commit =false ,手动提交消费位移
List messages=consumer.poll();
processMessage(messages);
consumser.commitOffset();
这个方案,会产生另外一个问题,我们来看下这个图
拉取了消息4~消息8,业务处理后,在提交消费位移时,不凑巧系统宕机了,最后的提交位移并没有保存到MQ服务端,下次拉拉取消息时,
依然是从消息4开始拉取,但是这部分消息已经处理过了 ,这样会导致重复消费
3.怎么解决消息的重复消费问题?
如何解决重复消费,避免引发数据不一致?
消息队列是可能发生重复消费的。
- 生产端为了保证消息的可靠性,它可能往 MQ 服务器重复发送消息,直到拿到成功
的 ACK。
- 再然后就是消费端,消费端消费消息一般是这个流程:拉取消息、业务逻辑处理、
提交消费位移。假设业务逻辑处理完,事务提交了,但是需要更新消费位移时,消
费者挂了,这时候另一个消费者就会拉到重复消息了。
kafka解决消息重复消费:
首先,要解决MQ服务端重复消息,kafka在0.11.0版本后,每条消息都有唯一的message id,MQ服务采用空间换时间方式,自动对重复的消息过滤处理,保证
接口的幂等性。
但是这个不能根本上解决消息重复问题,即使MQ服务中存储的消息没有重复,但是消费端是采用拉取的方式,如果重复拉取,也会导致重复消费,如何解决这种场景问题?
- 方案一:只拉取一次(消费者拉取消息后,先提交offset后再处理消息),但是如果系统宕机,业务员处理没有正常结束,后面再也拉取不到这些消息,会导致数据
不一致,该方案很少采用
- 方案二:允许拉取重复消息,但是消费端自己做幂等性控制,保证只成功消费一次,
关于幂等技术方案有很多,我们可以采用数据表或redis缓存存储处理标识,每次拉取到消息,处理前先校验处理状态,再决定是处理还是丢弃消息
如何幂等处理重复消息
幂等处理重复消息,简单来说,就是搞个本地表,带唯一业务标记的,利用主键或者唯一性索引,每次处理业务,先校验一下就好啦。
又或者用redis缓存下业务标记每次看下是否处理过
Kafka 幂等处理重复消息?
- 生产者发送每条数据的时候,里面加一个全局唯一的id,消费到了之后,先根据这个id去比如Redis里查一下,之前消费过吗,如果没有消费过,就处理,
然后这个id写Redis。如果消费过就别处理了。
- 基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。
4.如何实现顺序消息?
如何保证MQ消息是有序的?
有些业务有上下文要求,比如:电商行业的下单,付款,发货,确认收货,每个环节都会发送消息,而消费端拉取并消费消息时,也是希望按正常的状态机流程进行,
所以对消息就有了顺序要求,解决思路:
假设生产者先后产生了两条消息,分别是下单消息(M1),付款消息(M2),M1比M2先产生,如何保证M1比M2先被消费呢。
为了保证消息的顺序性,可以将将M1、M2发送到同一个Server上,当M1发送完收到ack后,M2再发送,这样还是可能会有问题,因为从MQ服务器到服务端,
可能存在网络延迟,虽然M1先发送,但是它比M2晚到,那还能怎么办才能保证消息的顺序性呢?将M1和M2发往同一个消费者,且发送M1后,等到消费端ACK成功后,
才发送M2就得了。消息队列保证顺序性整体思路就是这样啦。比如Kafka的全局有序消息,就是这种思想的体现: 就是生产者发消息时,
1个Topic只能对应1个Partition,一个Consumer,内部单线程消费。但是这样吞吐量太低,一般保证消息局部有序即可。在发消息的时候指定Partition Key,
Kafka对其进行Hash计算,根据计算结果决定放入哪个Partition。这样Partition Key相同的消息会放在同一个Partition。然后多消费者单线程消费指定的Partition。
kafka保证消息有序的方式:
- 该topic强制采用一个分区,所有消息放到一个队列里,这样能达到全局顺序性,但是会损失高并发特性。
- 局部有序,采用路由机制,将同一个订单的不同状态消息存储在一个分区partition,单线程消费,比如kafka就提供了一个接口扩展partitioner,
方便开发人员按照自己的业务场景来定制路由规则。
kafka的路由机制partitioner?
特别注意:虽然保证了单个分片的消息有序,但每个分片的消费者只能是单线程处理,因为多线程无法控制消费顺序,这个可能会损失一些性能。
这里又引出另一个问题如何保证一个队列只能有一个消费端?
另一个问题如何保证一个队列只有一个线程在处理消息?
继续往下看,如果扩容了怎么办?
原来有6个分区,order_id_1的消息在MessageQueue6中,此时扩容一倍,现在12个分区,order_id_1订单后面产生的消息可能路由到
了MessageQueue8中,同一个订单的消息分布在两个分区中,无法保证顺序
我们能做的是,先将存量消息处理完,再扩容,如果是在线业务,可以搞个临时topic,先将消息暂时堆积,待扩容后,按新的路由规则重新发送
顺序消息,如果某条失败了怎么办?会不会一直阻塞?
- 如果失败,不会提交消费位移,系统会自动重试(有重试上限),此时会阻塞后面的消息消费,知道这条消息处理完
- 如果消息达到重试上限,依然失败,会进入死信队列,可以继续处理后面的消息。
消息堆积如何处理?
主要是消费速度跟不上生产速度,从而导致消息堆积,解决思路:
- 可能是刚上线的业务,或者大促活动,流量评估不到位,这是需要增加消费组的机器数量,提升整体消费能力
- 也可能是消费端的问题,正常情况,一条消息处理需要10ms,但是优化不到位或者线上bug,现在要500ms,那么消费端的整体处理速度会下降50倍。
这时,我们就要针对性的排查业务代码。我之前带的团队就有小伙伴出现这个问题呢,当时是数据库的一条sql没有命中索引,导致单条消息处理耗时拉长,进而导致
消息堆积,线上报警,不过凭经验,很快就定位解决了
5.如何解决引入消息后的事务问题?
如何保证数据一致性问题?
为了解耦,引入异步消息机制。先进行本地数据库操作,处理成功后,再发送MQ消息,由消费端进行后续操作。比如:电商订单下单成功后,要通知扣减库存
这两者一定要保证事务操作,否则会出现数据不一致问题,这时候就需要引入事务消息来解决这个问题另外,在消费环节,也可能出现数据不一致情况。我们可以采用
最终一致性原则,增加重试机制。
事务消息是如何实现?
- 生产者产生消息,发送一条半事务消息到 MQ 服务器
- MQ 收到消息后,将消息持久化到存储系统,这条消息的状态是待发送状态。
- MQ 服务器返回 ACK 确认到生产者,此时 MQ 不会触发消息推送事件
- 生产者执行本地事务
- 如果本地事务执行成功,即 commit 执行结果到 MQ 服务器;如果执行失败,发送 rollback。
- 如果是正常的 commit,MQ 服务器更新消息状态为可发送;如果是 rollback,即删除消息。
- 如果消息状态更新为可发送,则 MQ 服务器会 push 消息给消费者。消费者消费完就回 ACK。
- 如果 MQ 服务器长时间没有收到生产者的 commit 或者 rollback,它会反查生产者,然后根据查询到的结果执行最终状态
MQ框架如何实现高吞吐量?
- 消息的批量处理
- 消息压缩,节省传输带宽和存储空间
- 零拷贝
- 磁盘的顺序写入
- page cache 页缓存,有操作系统异步将缓存中的数据刷到磁盘,以及高效的内存读取
- 分区设计,一个topic下面挂着n个分区,每个分区可以对应不同机器消费消息,并发设计
MQ框架如何做到高可用?
以kafka为例
kafka由多个broker组成,每个broker是一个节点。你创建一个topic,这个topic可以划分为多个partition,每个partition存放在不同的broker上,每个
partiton存放一部分数据,每个partition有多个replica副本。
写的时候,leader会负责把数据同步到所有follower上去,读的时候直接读leader上的数据即可
如果某个broker宕机了,没事儿,那个broker上面的partiton在其他机器上都有副本,此时会从follower中重新选举一个新的leader出来,大家继续读写那个
新的leader即可,这既是所谓的高可用性。
如何解决消息队列的延时及过期失效问题?
如何处理消息队列的消息积压问题
消息积压是因为生产者的生产速度,大于消费者的消费速度。遇到消息积压问题时,我们需要先排查,是不是有bug产生了。
如果不是 bug,我们可以优化一下消费的逻辑,比如之前是一条一条消息消费处理的话,我们可以确认是不是可以改为批量处理消息。如果还是慢,
我们可以考虑水平扩容,增加Topic的队列数,和消费组机器的数量,提升整体消费能力。
如果是bug导致几百万消息持续积压几小时。有如何处理呢? 需要解决bug,临时紧急扩容,大概思路如下:
大量消息在mq里积压了几个小时了还没解决
一个消费者一秒是1000条,一秒3个消费者是3000条,一分钟是18万条,1000多万条,所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概
1小时的时间 才能恢复过来,一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:
- 先修复consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉
- 新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量
- 然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时处理,直接均匀轮询写入临时建立好的10倍数量的queue
- 接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据。这种做法相当于是临时将queue资源和consumer资源扩大10倍,
以正常的10倍速度来消费消息。
- 等快读消费完积压的数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息
消息队列过期失效问题
假设你用的是rabbitmq,rabbitmq是可以设置过期时间的,就是TTL,如果消息在queue中积压超过一定时间就会被rabbitmq给清理掉,这个数据就没了,
那这就是第二个坑了, 这就不是说数据会大量积压在mq里,而是大量的数据会直接搞丢
这个情况下,就不是说要增加consumer消费积压的消息,因为实际没啥积压,而是丢了大量的消息,我们可以采取一个方案,就是批量重导
这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入mq里面去,把白天丢的数据给他补回来,也只能这样了
消息队列满了以后该怎么处理?
如果走的是消息积压在mq里,那么如果你很长时间都没有处理掉,此时导致mq都快写满了,咋办,这个还有别的办法么?没有,谁让你第一个方案执行的太慢了,
你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消
费掉所有的消息,然后走第二个方案,在晚上夜深人静的时候去手动查询重导丢失的这部分数据。
Redis
- 用过 Redis 吗? Redis 支持哪些常见的数据结构?
- Redis 的线程模型
- Redis 如何保证数据不丢失的(如何实现持久化)?
- AOF 和 RDB 的实现原理?
- Redis 如何实现高可用?
- 什么是缓存穿透,缓存击穿,缓存雪崩?分别如何预防解决?
分布式锁相关问题
- 用过分布式锁吗?用什么实现的分布式锁?
- 有没有用过基于 redis 分布式锁?有没有用过基于 Zookeeper 的分布式锁?
- 如何给锁设置合理的加锁时间?锁超时了怎么办? Redisson 看门狗的原理?
- Redis 如何解决集群情况下分布式锁的可靠性?
- RedLock 算法的原理?
1.用过 Redis 吗? Redis 支持哪些常见的数据结构?
Redis中一共有5种数据结构:String/hash/list/set/zset
基本数据类型:
- String:String是最常用的一种数据类型,String类型的值可以是字符串、数字或者二进制,但值最大不能超过512MB。使用场景:常规key-value缓存应用。
- Hash:Hash 是一个键值对集合。hash 特别适合用于存储对象。
- Set:Set是一个无序去重的集合。Set 提供了交集、并集等一系列直接操作集合的方法,对于求共同好友、共同关注等功能实现特别方便。
- List:List是一个有序可重复的集合,底层是依赖双向链表实现的。
- SortedSet:有序Set。内部维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。
特殊的数据类型:
- Bitmap:位图,可以认为是一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在Bitmap中叫做偏移量。Bitmap的长度与集合中元素个数无关,
而是与基数的上限有关。
假如要计算上限为1亿的基数,则需要12.5M字节的bitmap。就算集合中只有10个元素也需要12.5M。
- Hyperloglog。HyperLogLog 是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
场景:独立访客统计。HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的。
- Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位、附近的人等。
HyperLogLog、Bitmap的底层都是 String 数据类型,Geospatial 的底层是 Sorted Set 数据类型。
String: set key value, get key
- 介绍:key/value; 二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或 者序列化的对象。一个键最大能存储512MB。
- 常用命令:set,get,strlen,exists,decr,incr,setex等等。
- 应用场景:一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
Hash: hset key field value
- 介绍:hash类似于JDK1.8前的HashMap,内部实现也差不多(数组+链表)。不过,Redis的hash做了更多优化。另外,hash是一个string类型的
field和value的映射表,特别适合用于存储对象,后续操作的时候,可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以hash数据结构来存储用户信息,
商品信息等等。
- 常用命令:hset,hmset,hexists,hget,hgetall,hkeys,hvals等。
- 应用场景: 系统中对象数据的存储。
- 以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果
- 电商的购物车,以用户id为key,商品id为field,商品数量为value
- 添加商品hset cart:1001 10088 1
- 增加数量hincrby cart:1001 10088 1
- 商品总数hlen cart:1001
- 删除商品hdel cart:1001 10088
- 获取购物车所有商品hgetall cart:1001
- 缺点是:过期策略不能用在某个field上,只能用在key上
list
- 介绍:list 即是链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且可以灵活调整链表长度,但是链表的随机访问困难。
许多高级编程语言都内置了链表的实现比如Java中的LinkedList,但是C语言并没有实现链表,所以Redis实现了自己的链表数据结构。
Redis 的 list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
- 常用命令: rpush,lpop,lpush,rpop,lrange,llen等。
- 应用场景: 发布与订阅或者说消息队列、慢查询。
- 关注列表
- 微信或者微博的消息流
LPUSH msg:{诸葛老师-ID} 10018
- 备胎说车发微博,消息ID为10086
LPUSH msg:{诸葛老师-ID} 10086
- 查看最新微博消息
LRANGE msg:{诸葛老师-ID} 0 5
LIST可以很好的完成排队,先进先出的原则。
- 队列
Set 用于去重
- 介绍 :set 类似于 Java 中的HashSet 。Redis 中的set类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,
又不希望出现重复数据时,set是一个很好的选择,并且 set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
可以基于 set轻易实现交集、并集、差集的操作。
- 比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。
这个过程也就是求交集的过程。
- 常用命令:sadd,spop,smembers,sismember,scard,sinterstore,sunion等。
- 应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
- 微信抽奖小程序,sadd key {userId},SRANDMEMBER key [count]/pop [count]
- 点赞,用户关注模型(共同关注列表)
- 统计独立IP
Zset
- 介绍:和 set 相比,sorted set增加了一个权重参数score(double 类型的分数),使得集合中的元素能够按score的大小进行有序排列,还可以通过score
的范围来获取元素的列表。有点像是Java中HashMap和TreeSet的结合体。
- 常用命令:zadd,zcard,zscore,zrange,zrevrange,zrem等。
- 应用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息
(可以理解为按消息维度的消息排行榜)等信息。
- 可以做排行榜应用,取TOP N操作。
- 微博热搜的排行榜
- 带权重的消息队列
Redis 除了做缓存,还能做什么?
- 分布式锁: 通过Redis来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于Redisson来实现分布式锁。
- 限流:一般是通过 Redis + Lua 脚本的方式来实现限流。相关阅读:《我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!》。
- 消息队列:Redis 自带的 list 数据结构可以作为一个简单的队列使用。0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,
有主题和消费组的概念,支持消息持久化以及 ACK 机制。
- 复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过bitmap
- 统计活跃用户、通过 sorted set 维护排行榜。
- …
2.Redis 的线程模型
Redis 为什么是单线程的而不采用多线程方案?
多线程处理会涉及到锁,并且多线程处理会涉及到线程切换而消耗CPU。
采用单线程,避免了不必要的上下文切换和竞争条件。
其次Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。
既然单线程容易实现,而且CPU不会成为瓶颈, 那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)
3.Redis 如何保证数据不丢失的(如何实现持久化)?
redis持久化方式(RDB和AOF)
如何保证 Redis 中的数据不丢失?
- 单机单节点模式
- 使用 AOF 和 RDB 结合的方式, RDB 做镜像全量持久化,AOF 做增量持久化。
- 因为 RDB 会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要 AOF 来配合使用。
4.AOF 和 RDB 的实现原理?
RDB方式
RDB 是Redis默认的持久化方案。RDB持久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个dump.rdb文件。Redis重启会加载dump.rdb文件恢复数据。
RDB持久化的过程(执行SAVE命令除外):
创建一个子进程;
父进程继续接收并处理客户端的请求,而子进程开始将内存中的数据写进硬盘的临时文件;
当子进程写完所有数据后会用该临时文件替换旧的RDB文件。
Redis启动时会读取RDB快照文件,将数据从硬盘载入内存。
通过RDB方式的持久化,一旦Redis异常退出,就会丢失最近一次持久化以后更改的数据。
AOF方式
AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了
数据持久化的实时性,目前已经是Redis持久化的主流方式。
5.Redis 如何实现高可用?
redis高可用架构 三种架构(主从复制,哨兵模式,cluster集群,怎么选举,watchdog)
集群的原理是什么?
- Redis Sentinel(哨兵)着眼于高可用,Sentinel(哨兵)可以监听集群中的服务器,并在master主服务器宕机时 自动从slave服务器中选举出新的服务器
成为master服务器,继续提供服务。
- Redis Cluster(集群)着眼于扩展性,在单个Redis内存不足时,使用Cluster进行分片存储。
哨兵模式
主从模式下,主服务器宕机之后就没有办法进行写操作了,必须手动重新配置,非常麻烦。
哨兵模式可以通过监控redis主从,当主宕机后选举出新的主从而保障服务。
sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。
哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点
发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)
Redis常见使用方式有哪些?
Redis的几种常见使用方式包括:
- 单机版
- Redis主从
- Redis Sentinel(哨兵)
- Redis Cluster
使用场景:
- 单机版:很少使用。存在的问题:1、内存容量有限 2、处理能力有限 3、无法高可用。
- 主从模式:master节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。
- 哨兵模式:master节点挂掉后,哨兵进程会主动选举新的master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模
不是很大,需要自动容错容灾的时候使用。
- Redis cluster:主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有主节点的容量
总和就是Redis cluster可缓存的数据容量。
6.什么是缓存穿透,缓存击穿,缓存雪崩?分别如何预防解决?
缓存穿透,缓存雪崩,缓存击穿,缓存预热,缓存更新,缓存降级的概念以及解决方案
缓存雪崩
- 事前:尽量保证整个 Redis 集群的高可用性,发现机器宕机尽快补上,选择合适的内存淘汰策略。
- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉, 通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程
查询数据和写缓存,其他线程等待。
- 事后:利用 Redis 持久化机制保存的数据尽快恢复缓存
解决方法:在原有的失效时间基础上增加一个随机值,使得过期时间分散一些。
缓存穿透
- 缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中,将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
造成缓存穿透的基本原因有两个:
- 自身业务代码或者数据出现问题。
- 一些恶意攻击、 爬虫等造成大量空命中。
- 布隆过滤器
对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
布隆过滤器的应用之大数据去重:
5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?
通过布隆过滤器,在添加元素之前首先计算元素的hash,然后查询blommer,如果能够匹配则说明可能重复不予添加,否则添加进去,但是这种情况需要考虑
到容错率,因为可能存在哈西碰撞,需要问清楚到低容错率是多少。缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,如果从DB查不到
数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了。
缓存空值,不会查数据库
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,查询不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询压力。
布隆可以看成数据库的缩略版,用来判定是否存在值。启动的时候过滤器是要全表扫描的,数据库数据发生变化的时候会更新布隆过滤器。
布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后
会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询redis和数据库。
- 缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;如果一个查询返回
的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
但是这种方法会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿
- 设置热点数据永久缓存
- 设置互斥锁,单线程重建缓存
比如常见的电商项目中,某些货物成为“爆款”了,可以对一些主打商品的缓存直接设置为永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。
mutex key互斥锁基本上是用不上的,有个词叫做大道至简。
缓存预热
- 缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询
事先被预热的缓存数据!
- 解决思路
- 直接写个缓存刷新页面,上线时手工操作下;
- 数据量不大,可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
缓存更新
- 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,
- 常见的策略有两种: 定时删除和惰性删除
- 定时去清理过期的缓存;
- 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,
第一种的缺点是维护大量缓存的key是比较麻烦的,
第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,
大家可以根据自己的应用场景来权衡。
缓存降级
- 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据
一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
- 以参考日志级别设置预案:
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的 最大阀值,此时可以根据情况自动降级或者人工降级;
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
- 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见
的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
分布式锁相关问题
1.用过分布式锁吗?用什么实现的分布式锁?
redis分布式锁
Synchronized是JVM级别的锁,这里的锁是用于同一进程里面,因为多个线程共同访问某个共享资源,而进行的同步措施,他的前提条件是同一进程内,内存共享;
如果在分布式环境下,是没有办法保证封锁的,此时需要使用分布式锁
Redis为单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,Redis中可以使用SETNX命令实现分布式锁。
将key的值设为value,如果成功则进行下一步的操作,失败则返回。
为了防止代码执行中可能存在的无法释放锁的情况,需要在finally中使用del key命令释放锁,
为了防止死锁,需要给锁设置一个最大有效时间,如果超过,则Redis来帮我们释放锁。
Redis分布式锁实现原理:
set px nx
守护线程,进行 renew
Redis分布式锁实现:先拿setnx 来争抢锁,抢到之后,再用expire(过期)给锁加一个过期时间防止锁忘记了释放。
如果在setnx之后执行expire,之前进程意外crash或者要重启维护了,那会怎么样:
set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!
版本2:使用Redission完成分布式锁
如果有一些代码执行的之间已经超过了这个锁的有效时间,那么redis就会帮我们释放锁,但是这个时候我们并不希望锁被释放,因为该线程执行的操作还没有结束。
可以使用redission对锁进行续命,redission加锁之后会生成另一个线程,这个线程叫watchdog,watchdog默认每间隔10s检查redis主线程是否执行完毕,
如果没有执行完毕则将锁过期时间重置。最后在finally中的获取锁和释放锁是如何保证原子性的?
Redission的原子性依靠lua脚本来实现
如何解决Redis的并发竞争Key问题
所谓Redis的并发竞争Key的问题也就是多个系统同时对一个key进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
但是顺序我们无法控制。推荐一种方案:分布式锁(zookeeper和Redis都可以实现分布式锁)。
(如果不存在Redis的并发竞争Key问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,
生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。
同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的 子节点释放锁。
(使用分布式锁,例如zk,同时加入数据的时间戳,同一时刻,只有抢到锁的客户端才能写入,同时,比较当前数据的时间戳和缓存中数据的时间戳)
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
2.有没有用过基于 redis 分布式锁?有没有用过基于 Zookeeper 的分布式锁?
ZK 和 Redis 的区别,各自有什么优缺点?
- 先说 Redis:
Redis 只保证最终一致性,副本间的数据复制是异步进行(Set 是写,Get 是读,Reids集群一般是读写分离架构,存在主从同步延迟情况),主从切换之后可能有
部分数据没有复制过去可能会丢失锁情况,故强一致性要求的业务不推荐使用 Reids,推荐使用zk。
Redis 集群各方法的响应时间均为最低。随着并发量和业务数量的提升其响应时间会有明显上升(公有集群影响因素偏大),但是极限 qps 可以达到最大且基本无异常
- 再说 ZK:使用 ZooKeeper 集群,锁原理是使用 ZooKeeper 的临时节点,临时节点的生命周期在Client 与集群的 Session 结束时结束。因此如果某个
Client 节点存在网络问题,与ZooKeeper 集群断开连接,Session 超时同样会导致锁被错误的释放(导致被其他线程错误地持有),因此 ZooKeeper 也无法
保证完全一致。ZK 具有较好的稳定性;响应时间抖动很小,没有出现异常。但是随着并发量和业务数量的提升其响应时间和 qps 会明显下降
3.如何给锁设置合理的加锁时间?锁超时了怎么办? Redisson 看门狗的原理?
使用Redission完成分布式锁
如果有一些代码执行的之间已经超过了这个锁的有效时间,那么redis就会帮我们释放锁,但是这个时候我们并不希望锁被释放,因为该线程执行的操作还没有结束。
可以使用redission对锁进行续命,redission加锁之后会生成另一个线程,这个线程叫watchdog,watchdog默认每间隔10s检查redis主线程是否执行完毕,
如果没有执行完毕则将锁过期时间重置。最后在finally中
Redission的的获取锁和释放锁是如何保证原子性的?
Redission的原子性依靠lua脚本来实现
4.Redis 如何解决集群情况下分布式锁的可靠性?
5.RedLock 算法的原理?
并发编程篇
锁相关
- 说一下 synchronized 底层实现原理?
- 说一下 synchronized 、 volatile 、 CAS 的区别?
- synchronized 和 Lock 有什么区别?
- 什么是 CAS , CAS 的原理?
- CAS 有什么缺点?如何解决 CAS 中常见的 ABA 问题?
- AQS 的原理, AQS 的实现过程是什么?
- 有没有用过读写锁 ReentrantReadWriteLock ,说一下 ReentrantReadWriteLock 的原理?
1.说一下 synchronized 底层实现原理?
ReentrantLock 和 Synchronized 的区别?
- Synchronized 是依赖于 JVM 实现的,而 ReenTrantLock 是 API 实现的。
- 在 Synchronized 优化以前,synchronized 的性能是比 ReenTrantLock 差很多的,但是自从 Synchronized 引入了偏向锁,轻量级锁(自旋锁)后,
两者性能就差不多了。
- Synchronized 的使用比较方便简洁,它由编译器去保证锁的加锁和释放。而 ReenTrantLock 需要手工声明来加锁和释放锁,最好在 finally 中声明释放锁。
- ReentrantLock 可以指定是公平锁还是⾮公平锁。⽽ synchronized 只能是⾮公平锁。
- ReentrantLock 可响应中断、可轮询,而 Synchronized 是不可以响应中断的,
synchronized底层实现原理是什么?
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized 是 Java 中的关键字,是一种同步锁。synchronized 关键字可以作用于方法或者代码块。
一般面试时。可以这么回答:
- 反编译后,monitorenter、monitorexit、ACC_SYNCHRONIZED
- monitor 监视器
- Java Monitor 的工作机理
- 对象与 monitor 关联
- 如果 synchronized 作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit,JVM 使用 monitorenter 和 monitorexit 两个指令
实现同步;如果作用 synchronized 作用于方法,反编译可以看到 ACCSYNCHRONIZED 标记,JVM 通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED
来实现同步功能。
- 同步代码块是通过 monitorenter 和 monitorexit 来实现,当线程执行到 monitorenter 的时候要先获得 monitor 锁,才能执行后面的方法。当线程
执行到 monitorexit 的时候则要释放锁。
- 同步方法是通过中设置 ACCSYNCHRONIZED 标志来实现,当线程执行有 ACCSYNCHRONI 标志的方法,需要获得 monitor 锁。每个对象都与一个 monitor
相关联,线程可以占有或者释放 monitor。
2.说一下 synchronized 、 volatile 、 CAS 的区别?
synchronized和volatile的区别是什么?
- volatile是变量修饰符,而synchronized则作用于一段代码或者方法;
- volatile只是在线程内存和main memory(主内存)间同步某个变量的值;而synchronized通过锁定和解锁某个监视器,同步所有变量的值。
显然synchronized要比volatile消耗更多资源;
synchronized关键字三大特性是什么?
- 面试时经常拿synchronized关键字和volatile关键字的特性进行对比,synchronized关键字可以保证并发编程的三大特性:原子性、可见性、有序性,
而volatile关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的synchronized。
- 原子性:一个或多个操作要么全部执行成功,要么全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源。
- 可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。执行synchronized时,会对应执行 lock、unlock原子操作,保证可见性。
- 有序性:程序的执行顺序会按照代码的先后顺序执行。
volatile的特性有哪些?
- 并发编程的三大特性为可见性、有序性和原子性。通常来讲volatile可以保证可见性和有序性。
- 可见性:volatile可以保证不同线程对共享变量进行操作时的可见性。即当一个线程修改了共享变量时,另一个线程可以读取到共享变量被修改后的值。
- 有序性:volatile会通过禁止指令重排序进而保证有序性。
- 原子性:对于单个的volatile修饰的变量的读写是可以保证原子性的,但对于i++这种复合操作并不能保证原子性。这句话的意思基本上就是说volatile
不具备原子性了。
3.synchronized 和 Lock 有什么区别?
Lock和synchronized有什么区别?
- Synchronized 内置的Java关键字,是JVM层面的,Lock 是一个Java类
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
- Synchronized 线程 1(获得锁,如果线程1阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
- Synchronized 不可中断,除非抛出异常或者正常运行完成。Lock是可中断的,通过调用interrupt()方法。
- Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
- 原始构成:sync是JVM层面的,底层通过monitorenter和monitorexit来实现的。Lock是JDK API层面的。(sync一个enter会有两个exit,
一个是正常退出,一个是异常退出,因此不需要手动释放锁)
- 使用方法:sync不需要手动释放锁,而Lock需要手动释放。
- 是否可中断:sync不可中断,除非抛出异常或者正常运行完成。Lock是可中断的,通过调用interrupt()方法。
- 是否为公平锁:sync只能是非公平锁,而Lock既能是公平锁,又能是非公平锁。
- 绑定多个条件:sync不能,只能随机唤醒。而Lock可以通过Condition来绑定多个条件,精确唤醒。
4.什么是 CAS , CAS 的原理?
CAS知道吗? 怎么实现的?
- CAS可以看做是乐观锁的一种实现方式, 全称Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
- CAS涉及到三个属性:
- 需要读写的内存位置V
- 需要进行比较的预期值A
- 需要写入的新值U
- CAS具体执行时,当且仅当预期值A符合内存地址V中存储的值时,就用新值U替换掉旧值,并写入到内存地址V中,否则使用自旋不断获取值进行判断,保证一致性
和并发性,但是比较消耗CPU资源。使用CAS就可以不用加锁来实现线程安全。
当多个线程同时操作一个共享变量时,只有一个线程可以对变量进行成功更新,其他线程均会失败,但是失败并不会被挂起,进行再次尝试,也就是自旋。
Java中的自旋锁就是利用CAS来实现的。
- 原子性保证:CAS算法依赖于rt.jar包下的sun.misc.Unsafe类,该类中的所有方法都是native修饰的,直接调用操作系统底层资源执行相应的任务,
在调用这个类中的CAS方法中,JVM就会若干条系统指令,完整这些指令的过程中是不允许被中断的,所以CAS是一条CUP的原子指令,所以它不会造成数据不一致问题。
5.CAS 有什么缺点?如何解决 CAS 中常见的 ABA 问题?
CAS有哪些问题,如何解决?
- 如果期望的数值和从内存中读取的数值不一样,会一直自旋,开销比较大。引出了ABA问题。
- ABA问题的解决:
所谓ABA问题,就是比较并交换的循环,存在一个时间差,在这个时间差内,比如线程T1将值从A改为B,然后又从B改为A。线程T2看到的就是A,
但是却不知道这个A发生了更改。尽管线程T2 CAS操作成功,但不代表就没有问题。有的需求,只注重头和尾,只要首尾一致就接受。但是有的需求,还看重过程,
中间不能发生任何修改,可以使用AtomicStampedReference,使用stamp添加版本号来解决这个问题。
CAS的优点: 在并发量不是很大时提高效率
6.AQS 的原理, AQS 的实现过程是什么?
AQS知道吗?讲讲
- AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch…。
它维护了一个volatile int state(代表共享资源)状态变量和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
- AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,
获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;
释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。
- 简单来说,AQS就是维护了一个共享资源,然后使用队列来保证线程排队获取资源的一个过程。
- AQS的工作流程: 当被请求的共享资源空闲,则将请求资源的线程设为有效的工作线程,同时锁定共享资源。如果被请求的资源已经被占用了,AQS就用过队列实现了
一套线程阻塞等待以及唤醒时锁分配的机制
7.有没有用过读写锁 ReentrantReadWriteLock ,说一下 ReentrantReadWriteLock 的原理?
线程池相关
- 有哪几类线程池?如何创建线程池?
- 解释一下线程池的核心参数,线程池的执行过程?○如果提交任务时,线程池队列已满,这时候会发生什么?
- 线程池线上参数如何优化?
1.有哪几类线程池?如何创建线程池?
创建线程池有哪几种方式?
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):
- newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。
2.解释一下线程池的核心参数,线程池的执行过程?
线程池的核心参数
- corePoolSize:表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地;
- 表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人;
- 定义忙闲:如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了;
- 工作队列;
- 自定义如何创建线程;
- 自定义任务的拒绝策略:
- 提交任务的线程自己去执行该任务;
- 默认的拒绝策略,会 throws RejectedExecutionException;
- 直接丢弃任务,没有任何异常抛出;
- 丢弃最老的任务。
Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,
这是致命问题。所以强烈建议使用有界队列;
如果线程池处理的任务非常重要,建议自己定义拒绝策略。
一个任务从被提交到被执行,线程池做了哪些工作?
- 向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数,如果不大于,就创建一个核心线程来执行任务。
- 如果大于核心线程数,就会判断缓冲队列是否满了,如果没有满,则放入队列,等待线程空闲时执行任务。
- 如果队列已经满了,则判断是否达到了线程池设置的最大线程数,如果没有达到,就创建新线程来执行任务。
- 如果已经达到了最大线程数,则执行指定的拒绝策略。
3.线程池线上参数如何优化?
合理配置线程池核心线程数(io密集型和cpu密集型)
io密集型任务该如何设置线程池线程数
- 代码查看服务器的核心数
要合理配置线程数首先要知道公司服务器是几核的
代码查看服务器核数:System.out.println(Runtime.getRuntime().availableProcessors());
- 合理线程数配置之CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力
就那些。PU密集型任务配置尽可能少的线程数量:一般公式:CPU核数+1个线程的线程池
- 合理线程数配置之IO密集型
- 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
- IO密集型,即该任务需要大量的IO,即大量的阻塞。
- 在单线程上运IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速
主要就是利用了被浪费掉的阻塞时间。IO密集型时,大部分线程都阻塞,故需要多配置线程数:参考公式:CPU核数 /(1 - 阻系数)
比如8核CPU:8/(1 - 0.9)=80个线程数,阻塞系数在0.8~0.9之间
4.如果提交任务时,线程池队列已满,这时候会发生什么?
有哪些拒绝策略?
当等待队列满时,且达到最大线程数,再有新任务到来,就需要启动拒绝策略。
- AbortPolicy:默认的策略,队列满了丢任务抛出异常
- CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务
- DiscardOldestPolicy:将最早进入队列的任务删,之后再尝试加入队列
- DiscardPolicy:直接丢弃任务,不做任何处理。
分布式篇
分布式理论
- 说说你对 CAP 理论的理解?
- 说说你用过的注册中心,分别使用了什么模型?( AP , CP )
- 说说你对 BASE 理论的理解?
分布式事务相关
- 如何解决分布式事务问题?你用过哪些解决分布式事务的方案?
- 说一下对2PC,3PC协议的理解?
- 有没有用过 SEATA , SEATA 的实现过程是什么?
- 如何基于 MQ 实现最终一致性?
分布式理论
说说你对 CAP 理论的理解?
请讲一个你对CAP理论的理解
- consistency 一致性:指数据在多个副本之间能够保持一致的特性(严格的一致性)
- availability 可用性:指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据)
- partition tolerance 分区容错性:分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障
说说你用过的注册中心,分别使用了什么模型?( AP , CP )
Eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?
Zookeeper保证了CP(C:一致性,P:分区容错性),Eureka保证了AP(A:高可用)
- 当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较
高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,
且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够
恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。
- Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个
Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,
Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:
- Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。
- Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)
- 当网络稳定时,当前实例新的注册信息会被同步到其他节点。因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样
使整个微服务瘫痪
说说你对 BASE 理论的理解?
请将一下BASE理论的三要素
- 基本可用
- 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性,但是,这绝不等于系统不可用
- 比如:
- 响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户响应的查询结果,但是由于出现故障,查询结果的响应时间增加了1~2秒
- 系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的
购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
- 软状态
- 软状态值允许系统中的数据存在中间状态,并认为中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本知己恩进行数据同步的过程存在延时
- 最终一致性
- 强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态,因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而
不需要实时保证系统数据的强一致性
分布式事务相关
如何解决分布式事务问题?你用过哪些解决分布式事务的方案?
分布式事务常见的方案有2PC,3PC,TCC,本地消息表,MQ消息服务,最大努力通知,SAGA事务等等
说一下对2PC,3PC协议的理解?
有没有用过 SEATA , SEATA 的实现过程是什么?
如何基于 MQ 实现最终一致性?
实战篇
- 如何设计接口并保证他们的安全?
- 如何快速定位 CPU 溢出?
- 如何设计实现一个限流组件?
- 如何让系统能抗住预约抢购活动的流量压力?
1.如何设计接口并保证他们的安全?
2.如何设计实现一个限流组件?
3.如何让系统能抗住预约抢购活动的流量压力?
4.如何快速定位 CPU 溢出?