├── .gitignore ├── README.md ├── cache ├── README.md └── img │ ├── cache_before_db.png │ ├── db_before_cache.png │ └── db_remove_cache.png ├── database ├── .DS_Store ├── README.md ├── img │ ├── .DS_Store │ ├── index.png │ ├── next_key_lock.png │ ├── rr-phantom-read-example.png │ └── transaction.png ├── indexes.md └── transaction.md ├── gc ├── README.md ├── algorithm.md ├── g1.md ├── img │ ├── cms_gc.png │ └── gc_roots.png └── java_cms.md ├── golang ├── README.md ├── goroutine.md ├── img │ ├── lock_competition_two_ways.png │ └── lock_pattern.png └── mutex.md ├── img ├── dingtalk_group.jpg └── good_job.png ├── microservice ├── README.md ├── availability.md ├── general.md ├── img │ └── chain-timeout.png └── timeout.md ├── mq ├── Kafka.md ├── README.md └── img │ ├── ack_timeout.jpeg │ ├── decouple1.jpeg │ ├── decouple2.jpeg │ ├── delay_third_directly.jpeg │ ├── delay_third_using_mq.jpeg │ ├── kafka_available_performance.png │ ├── mq_overview.jpeg │ ├── overview.jpeg │ └── too_many_msg.jpeg ├── pattern └── README.md ├── redis ├── .DS_Store ├── availability.md ├── data_structure.md ├── expired.md ├── img │ ├── availability.png │ ├── data_structure.png │ ├── expired.png │ ├── io_model.png │ ├── persistence.png │ ├── pipeline.png │ └── value_object.png ├── io_model.md ├── persistent.md └── pipeline.md └── sharding ├── README.md └── rewrite_sql.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea 18 | 19 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 什么是八股文? 4 | 5 | 就是日经题目,考察各种框架、基础知识的题目。这一类题目几乎可以说是有标准答案,因此我准备用这个仓库来收集这一类的题目,并且给出答案。 6 | 7 | 你所要做的就是,下载下来,而后一篇篇背过去。拿出当年读书背书的劲头来,理解不理解都不要紧,背熟了就可以,能够把八股文面试应付过去就可以。 8 | 9 | 做这个的动机,有几个原因: 10 | 1. 现在的面试主要就是八股文+算法; 11 | 2. 很多人本身知识是具备的,但是如何回答,如何组织语言还可以进一步加强; 12 | 3. 网上的很多面经题,就是列举题目,给出答案,组织不是很合理; 13 | 14 | # 如何阅读? 15 | 16 | 我会按照主题来划分,每一个主题下,分成几个部分: 17 | 1. 基本回答:这一部分背出来,基本上就过关了; 18 | 2. 扩展点:这一部分是为了面试亮点的,同时也是一个引导面试官思路的点; 19 | 3. 可能问法:同一个知识点,不同面试官问的问题都不太一样;又或者,当你和面试官聊到什么的时候,可以将话题引申过来这里; 20 | 21 | # 如果你希望讨论别的主题 22 | 23 | 一般来说,你可以发issue或者discussion,将自己遇到的面试题目发出来。 24 | 25 | 如果我知道,就会先给一个粗略的回答,但是这个回答并不是经过整理的。 26 | 27 | 只有经过整理的才会出现在这个repository。 28 | 29 | # 教的不是技术! 30 | 31 | 我一定要强调的一点是,这里并不是真的专研技术,因为背是背不来技术的。 32 | 33 | 这里只是帮助你提高一点获得好的工作的可能性。 34 | 35 | # 授权 36 | 37 | 非收费性质的个人复制、分发不受限制。 38 | 39 | 基于此仓库的衍生,都需要保持对个体非盈利使用的免费与开放。 40 | 41 | 商业使用请获得我的授权。 42 | -------------------------------------------------------------------------------- /cache/README.md: -------------------------------------------------------------------------------- 1 | # 缓存 2 | 3 | 解析:缓存的面试其实分成两大块: 4 | - 缓存的基本理论 5 | - 缓存中间件的应用 6 | 7 | 这里我们讨论缓存的一些基本理论,缓存中间件 Redis 等,在对应的中间件章节里面看里面查看。 8 | 9 | 缓存的基本理论,目前来说考察比较多的是: 10 | - 缓存和 DB 一致性的问题 11 | - 缓存模式 12 | - 缓存穿透、缓存击穿、缓存雪崩 13 | 14 | ### 缓存和 DB 一致性问题 15 | 16 | 为了方便讨论,这里就将问题简化为 DB 和缓存一致性。也就是更新只需要更新 DB 和缓存。 17 | 18 | 首先要记住,缓存一致性的问题根源于两个原因: 19 | - 不同线程并发更新 DB 和缓存; 20 | - 即便是同一个线程,更新 DB 和更新缓存是两个操作,容易出现一个成功一个失败的情况; 21 | 22 | 缓存和 DB 一致性的问题可以说是无最优解的。无论选择哪个方案,总是会有一些缺点。 23 | 24 | 最常用的是三种必然会引起不一致的方案,这三种方案大同小异。面试的时候要记住为什么它们会引起不一致。这三种方案都是有一个显著特征,就是如果缓存是会过期的,那么它们最终都会一致。 25 | 26 | 1. 先更新 DB,再更新缓存。不一致的情况: 27 | 1. A 更新 DB,DB中数据被更新为1 28 | 2. B 更新 DB,DB中数据被更新为2 29 | 3. B 更新缓存,缓存中数据被更新为2 30 | 4. A 更新缓存,缓存中数据被更新为1 31 | 5. 此时缓存中数据为1,而DB中数据为2。这种不一致会一直持续到缓存过期,或者缓存和DB再次被更新,并且被修改正确; 32 | ![](./img/db_before_cache.png) 33 | 1. 先更新缓存,再更新 DB。不一致的情况; 34 | 1. A 更新缓存,缓存中数据被更新为1 35 | 2. B 更新缓存,缓存中数据被更新为2 36 | 3. B 更新 DB,DB中数据被更新为2 37 | 4. A 更新 DB,DB中数据被更新为1 38 | 5. 此时缓存中数据为2,但是DB 中数据为1。这种不一致会一直持续到缓存过期,或者缓存和DB再次被更新,并且被修改正确; 39 | ![](./img/cache_before_db.png) 40 | 1. 先更新 DB,再删除缓存。不一致的情况; 41 | 1. A 从数据库中读取数据1 42 | 2. B 更新数据库为2 43 | 3. B 删除缓存 44 | 4. A 更新缓存为1 45 | 5. 此时缓存中数据为1,数据库中数据为2 46 | ![](./img/db_remove_cache.png) 47 | 48 | 所以本质上,没有完美的解决方案,或者说仅仅考虑这种更新顺序,是不足以解决缓存一致性问题的。 49 | 50 | 与这三个类似的一个方案是利用 CDC 接口,异步更新缓存。但是本质上,也是要忍受一段时间的不一致性。比如说典型的,应用只更新 MySQL,然后监听 MySQL 的 binlog,更新缓存。 51 | 52 | 而如果需求强一致性的话,那么比较好的方案就是: 53 | - 第一个是负载均衡算法结合 singleflight 54 | - 第二个是分布式锁。严格来说,分布式锁的方案,我一点都不喜欢,毫无技术含量 55 | 56 | 第一个方案,稍微有点奇诡。我们可以考虑对 key 采用哈希一致性算法来作为负载均衡算法,那么我们可以确保,同一个 key 的请求,永远会落到同一台实例上。然后结合单机 singleflight,那么可以确保永远只有一个线程更新缓存或者 DB,自然就不存在一致性问题了。 57 | 58 | 这个方案要注意的是在哈希一致性算法因为扩容,或者缩容,或者重新部署,导致 key 迁移到别的机器上的时候,会出现问题。假设请求1、2都操作同一个 key: 59 | - 请求1被路由到机器 C 上 60 | - 扩容,加入了 C1 节点 61 | - 请求2被路由到了 C1 节点上 62 | - (以先写DB为例)请求1更新DB 63 | - 请求2更新DB,请求2更新缓存 64 | - 请求1更新缓存 65 | 66 | 在这种情况下,你不管怎么搞都会出现不一致的问题。那么可能的解决方案就是: 67 | - 要么在部署 C1 之前,在 C 上禁用缓存 68 | - 要么在部署 C1 之后,先不使用缓存,在等待一段时间之后,确保 C 上的迁移key的请求都被处理完了,C1 再启用缓存 69 | 70 | 分布式锁的方案就没什么好说的了,咔嚓一把分布式锁一了百了。分布式锁适用于写请求特别少的例子,因为读是没有必要加分布式锁的。读完全没有必要加分布式锁,即便此时有人正在更新缓存或者 DB,当前的请求要么读到更新前的,要么读到更新后的,不会有什么问题。 71 | 72 | 注意我说的写,是写缓存的写。也就是说,如果要是缓存过期,然后用 DB 的数据更新缓存,同样要参与抢夺这个分布式锁。 73 | 74 | 另外,一个可行的分布式锁方案的优化是在单机上引入 singleflight,确保一个实例针对一个特定的 key 只会有一个线程去参与抢全局的分布式锁。 75 | 76 | 注意!前面的这些方案,我们都有一个基本的假设,就是更新 DB 和更新缓存两个步骤都会成功。但是很显然这个假设是站不住脚的,也就是说,真正寻求强一致性,还要进一步解决更新 DB 和更新缓存一个成功一个失败的问题。 77 | 78 | 这里,也就是只有三个选项: 79 | - 追求强一致性,选用分布式事务; 80 | - 追求最终一致性,可以引入重试机制; 81 | - 如果可以使用本地事务,那么应该是:开启本地事务-更新DB-更新缓存-提交事务 82 | 83 | 然后一个问题是:我用了分布式事务,我还需要分布式锁吗?答案是,要的。因为分布式事务既解决不了多个线程同时更新的问题,也解决不了一个线程更新,一个线程从数据库读数据刷缓存的问题。 84 | 85 | ### 缓存模式 86 | 87 | 缓存模式主要是所谓的 cache-aside, read-through, write-through, write-back, refresh ahead 以及更新缓存使用到的 singleflight 模式。这些模式并不是银弹,如果说某个模式优于其它的模式,这就是在扯淡了。因此,选择何种缓存模式,也就是一个业务层面上考虑的问题,大多数时候,选取任何一种模式都不会有问题。 88 | 89 | - write-back:这个稍微有点意思。因为标准的 write-back 是在缓存过期的时候,然后再将缓存刷新到 DB 里面。因此,它的弊端就是,在缓存刷新到 DB 之前,如果缓存宕机了,比如说 Redis 集群崩溃了,那么数据就永久丢失了;但是好处就在于,因为过期才把数据刷新到 DB 里面,因为读写都操作的是缓存。如果缓存是 Redis 这种集中式的,那么意味着大家读写的都是同一份数据,也就没有一致性的问题。但是,如果你设置了过期时间,那么缓存过期之后重新从数据库里面加载的同时,又有一个线程更新缓存,那么两者就会冲突,出现不一致的问题; 90 | - refresh ahead 这种其实就是前面说的利用 CDC 的方案 91 | 92 | ### 缓存异常场景 93 | 94 | 缓存穿透、击穿和雪崩,本质上就是一个问题:缓存没起效果。只不过根据不起效的原因进行了进一步的细分。 95 | 96 | 我一直觉得这三个东西的命名特别沙雕,因为穿透和击穿在中文语境下区别就不大,也不知道是哪个卧龙凤雏搞出来的名字。 97 | 98 | 其实,这三个就是描述了三种场景: 99 | - 你数据库本来就没数据 100 | - 你数据库有,但是缓存里面没有 101 | - 你缓存本来有,但是突然一大批缓存集体过期了 102 | 103 | 数据库本来就没数据,所以请求来的时候,肯定是查询数据库的。但是因为数据库里面没有数据,所以不会刷新回去,也就是说,缓存里面会一直没有。因此,如果有一些黑客,一直发一些请求,这些请求都无法命中缓存,那么数据库就会崩溃。 104 | 105 | 如果数据库有,但是缓存里面没有。理论上来说,只要有人请求数据,就会刷新到缓存里面。问题就在于,如果突然来了一百万个请求,一百万个线程都尝试从数据库捞数据,然后刷新到缓存,那么数据库也会崩溃。 106 | 107 | 缓存本来都有,但是过期了。一般情况下都不会有问题,但是如果突然之间几百万个 key 都过期了,那么接下来的请求也几乎全部命中数据库,也会导致数据库崩溃。 108 | 109 | 110 | ## 面试题 111 | 112 | ### 如何解决缓存和 DB 的一致性问题 113 | 分析:日经题,每次面,但凡简历上出现了缓存两个字眼,就会问。甚至于,只要面试官公司用了缓存,他们就会问。 114 | 115 | 缓存和 DB 数据一致性的问题,先说结论:没有完美的方案,无非就是取舍问题。 116 | 117 | 一般的思路就是,先指出,先更新 DB 还是先更新缓存,还是更新DB之后删除缓存的方案,本质上都有一致性的问题,从而强调,仅仅依靠缓存和DB是做不到一致性的,要结合别的组件。 118 | 119 | 然后回答两个方案: 120 | - 分布式锁方案:该方案亮点在于强调可以引入单机 singleflight 来减少锁竞争; 121 | - 结合负载均衡算法,可以利用哈希一致性负载均衡算法和单机上的 singleflight 122 | 123 | 两个方案的本质都是确保只有一个线程操作特定的 key 124 | 125 | 答:缓存和 DB 的一致性问题,没有什么特别好的解决方案,主要就是一个取舍的问题。 126 | 127 | 如果能够忍受短时间的不一致,那么可以考虑只更新 DB,等缓存自然过期。大多数场景其实没有那么强的一致性需求,这样做就够了。 128 | 129 | 进一步也可以考虑先更新 DB 再更新缓存,或者先更新缓存再更新 DB,或者更新 DB 之后删除缓存,都会有不一致的可能,但是至少不会比只更新 DB 更差。 130 | 131 | 另外一种思路是利用 CDC 接口,比如说监听 MySQL 的binlog,然后更新缓存。应用是只更新 MySQL,丝毫不关心缓存更新的问题。(引导面试官问 CDC 问题,或者 MySQL binlog,或者说这种模式和别的思路比起来有什么优缺点) 132 | 133 | 至于说其它的比如说 cache-aside, write-through, read-through, write-back 也对一致性问题,毫无帮助。(引导面试官问之后几个 pattern) 134 | 135 | 如果追求强一致性,那么可行的方案有两个: 136 | - 利用分布式锁。在读上没必要加锁,在写的时候加锁。(面试官可能会进一步问,为什么读不需要加锁,很简单,在同一个时刻,有人更新数据,有人读数据,那么读的人,读到哪个数据都是可以的。如果写已经完成,那么读到的肯定是新数据,如果写没有完成,读到的肯定是老数据)。(刷亮点)一种可行的优化方案,是在单机上引入 singleflight。那么更新某个 key 的时候,同一个实例上的线程自己竞争一下,决出一个线程去参与抢全局分布式锁。在写频繁的时候,这种优化能够有些减轻分布式锁的压力。(实际上,写频繁的还要追求强一致性,还要用缓存简直无力吐槽) 137 | - 另外一个方案是利用负载均衡算法和 singleflight。可以选择一种负载均衡算法,即一个 key 只会被路由到同一个实例上。比如说使用一致性哈希算法。结合 singleflight,那么可以确保全局只有一个线程去更新数据,那么自然就不存在一致性的问题了。(开始进一步讨论这种方案的细节,也可以等面试官问。面试官要是水平高,他自然就会意识到在扩容缩容,或者重启的时候,会有问题。要是面试官水平不高,他是意识不到的,哈哈哈,所以可以用来试探一下面试官的斤两) 138 | 139 | 这种方案的问题在于,在扩容、缩容、或者重启的时候,因为会引起 key 迁移到别的实例上,所以可能出现不一致的问题。在这种情况下,可以考虑两种方案。第一种方案是扩容或者缩容的时候在原本实例上禁用这些迁移 key 的缓存;另外一种方案是目标实例先不开启读这些迁移 key 的缓存,等一小段时间,确保原本实例上的这些迁移 key 的请求都被处理完了,然后再开启缓存。 140 | 141 | #### 类似问题 142 | - 在使用了缓存的时候,你先更新缓存还是先更新 DB 143 | - 要是先更新缓存成功,再更新 DB 失败会怎样 144 | - 要是先更新DB成功,但是更新缓存失败会怎样 145 | - 更新 DB 之后,删除缓存的做法有什么弊端?怎么解决这种弊端? 146 | 147 | ### singleflight 是什么 148 | 分析:本质上来说,singleflight 只是一个设计模式,你可以用于缓存,也可以用于别的方面。 149 | singleflight 是指,如果多个线程(协程)去做同一件事,那么我们可以采用 singleflight 模式,从多个线程里面挑出来一个去做,其余的线程就停下来等结果。 150 | 151 | singleflight 在缓存里面主要用在更新缓存上,防止在缓存未命中的时候,多个线程同时访问 DB,给 DB 造成巨大的压力。 152 | 153 | 答:singleflight 是一种设计模式,使用这种设计模式,可以在我们更新缓存的时候,控制住只有一个线程去更新缓存。 154 | 155 | 不过,这里面强调的一个线程只是指同一个 key 只会有一个线程。因为我们并不会说更新不同的 key 也共享一个更新线程。 156 | 157 | (亮点,要解释 singleflight 只在单机层面上,而不是在全局上)另外一个是,在分布式环境下,我们只做单机层面上的控制。也就是说,如果有多台机器,我们会保证一个机器只有一个线程去更新特定一个 key 的缓存。比如说,针对 key1,如果有三台机器,那么最多会有三个线程去更新缓存。 158 | 159 | 不做全局的原因很简单,在分布式环境下,数据库至少要能撑住这种多台机器同时发起请求的负载。而做全局的 singleflight 本质上就是利用分布式锁,这个东西非常消耗性能。 160 | 161 | (如果是 Go 语言)在 Go 上,标准库直接提供了 singleflight 的支持。(这里要防面试官让你手写一个) 162 | 163 | #### 类似问题 164 | - 为什么用 singleflight 165 | - 为什么 singleflight 只在单机层面上应用 166 | - 如果要在全局层面上应用 singleflight,怎么搞?其实就是加一个分布式锁,没什么花头 167 | -------------------------------------------------------------------------------- /cache/img/cache_before_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/cache/img/cache_before_db.png -------------------------------------------------------------------------------- /cache/img/db_before_cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/cache/img/db_before_cache.png -------------------------------------------------------------------------------- /cache/img/db_remove_cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/cache/img/db_remove_cache.png -------------------------------------------------------------------------------- /database/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/database/.DS_Store -------------------------------------------------------------------------------- /database/README.md: -------------------------------------------------------------------------------- 1 | # 数据库 2 | 3 | 数据库的面试题分成很多。 -------------------------------------------------------------------------------- /database/img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/database/img/.DS_Store -------------------------------------------------------------------------------- /database/img/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/database/img/index.png -------------------------------------------------------------------------------- /database/img/next_key_lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/database/img/next_key_lock.png -------------------------------------------------------------------------------- /database/img/rr-phantom-read-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/database/img/rr-phantom-read-example.png -------------------------------------------------------------------------------- /database/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/database/img/transaction.png -------------------------------------------------------------------------------- /database/indexes.md: -------------------------------------------------------------------------------- 1 | # 数据库索引 2 | 3 | 基本回答: 4 | 5 | 索引是为了加快数据查询的一种数据结构。 6 | 7 | 从数据结构角度出发,索引分为B树索引,B+树索引,哈希索引和位图索引。 8 | 在MySQL上,主要是采用B+树索引,B树索引在NoSQL上使用较多,哈希索引在KV数据库上较为常见。 9 | 10 | 从形态上来说,可以分成覆盖索引,前缀索引,全文索引,联合索引,唯一索引和主键索引。(下面的定义,可以直接一股脑说出来,也可以等面试官问) 11 | 1. 覆盖索引其实是指我们查询的列全部命中了索引; 12 | 2. 前缀索引是指只利用了数据前几个字符的索引,如果前面几个字符区分度不好的话,不建议使用前缀索引; 13 | 3. 全文索引现在比较少用,一般推荐使用别的中间件来完成,例如ES(小心这一步,可能咔嚓把话题引过去了ES上); 14 | 4. 联合索引是指多个列组成一个索引。创建的时候我们会考虑把区分度好的索引放在前面,因为MySQL遵循最左前缀匹配原则;(这里可能会问你啥是最左前缀匹配原则) 15 | 5. 唯一索引是指数据库里面要求该索引值必须要唯一,我们一般用于业务唯一性保证; 16 | 6. 主键索引是比较特殊的索引,一般它的叶子节点要么存储了数据,要么存储了指向数据的指针。MySQL的innodb引擎存储的是数据,MyISAM放的是数据的地址;(这里也会引过去聚簇索引与非聚簇索引) 17 | 18 | 从是否存储数据的角度,又可以分为聚簇索引和非聚簇索引,MySQL的主键就是聚簇索引,每张表唯一一个,非聚簇索引的数据本质上存储的是主键。(面试官可能从这里被引导过去聚簇索引与非聚簇索引) 19 | 20 | 而对于MySQL的innodb来说,它的行锁是利用索引来实现的,所以如果查询的时候没有索引,那么会导致表锁。(这一句可能引导面试官问你锁和事务的问题,如果不熟悉锁和事务,请不要回答)。 21 | 22 | ![知识点](./img/index.png) 23 | 24 | ## 扩展点 25 | 26 | ### 什么是最左匹配原则 27 | 28 | 分析:单单回答最左前缀匹配原则是很简单,但是没有亮点。亮点在,最左前缀匹配大概是如何运作的。之所以只需要回答”大概“如何运作,是因为详细回答太难,面试官没读过源码也搞不清楚,犯不着。 29 | 30 | 答案:最左前缀匹配原则是指,MySQL会按照联合索引创建的顺序,从左至右开始匹配。例如创建了一个联合索引(A,B,C),那么本质上来说,是创建了A,(A,B),(A,B,C)三个索引。之所以如此,因为MySQL在使用索引的时候,类似于多重循环,一个列就是一个循环。在这种原则下,我们会优先考虑把区分度最好的放在最左边,而区分度可以简单使用不同值的数量除以总行数来计算(distinct(a, b, c)/count(*))。 31 | 32 | ### 数据库支持哈希索引吗 33 | 34 | 分析:很少有面试官会在数据库面试里边聊哈希索引,因为这个东西很罕见,用法也比较奇诡。不过这也是一个优雅装逼的点。 35 | 36 | 答案:哈希索引是利用哈希表来实现的,适用于等值查询,如等于,不等于,IN等,对范围查询是不支持的。我们惯常用的innodb引擎是不支持用户自定义哈希索引的,但是innodb有一个优化会建立自适应哈希索引。 37 | 所谓的自适应哈希索引,是指innodb引擎,如果发现二级索引(除了主键以外的别的索引)被经常使用,那么innodb会给这个索引建立一个哈希索引,加快查询。所以从本质上来说,innodb的自适应哈希索引是一个对索引的哈希索引。 38 | 39 | 关键:等值查询,对索引的哈希索引 40 | 41 | #### 如何引导 42 | 1. 在前面回答了哈希索引之后,就直接跳过来这里,例如“哈希索引在KV数据库上比较常见,不过innodb引擎支持自适应哈希索引,它是..." 43 | 44 | ### 聚簇索引和非聚簇索引的区别 45 | 46 | 分析:这其实是一个很简单的问题,但也是一个很能装逼的问题。聚簇索引和非聚簇索引的区别,只需要回答,他们叶子节点是否存储了数据。但是要答出亮点,就要多回答两个点:第一,MySQL的非聚簇索引存储了主键;第二,覆盖索引不需要回表。 47 | 48 | 答案:聚簇索引是指叶子节点存储了数据的索引。MySQL整张表可以看做是一个聚簇索引。因为非聚簇索引没有存储数据,所以一般是存储了主键。于是会导致一个回表的问题。即如果我们查询的列包含不在索引上的列,这会引起数据库先根据非聚簇索引找出主键,而后拿着主键去聚簇索引里边捞出来数据。而根据主键找数据会引起磁盘IO,性能大幅度下降。这就是我们推荐使用覆盖索引的原因。 49 | 50 | 关键点:聚簇索引存了数据,非聚簇索引要回表 51 | 52 | #### 如何引导过来这里? 53 | 1. 聊到了覆盖索引与回表的问题,话术可以是”一般用覆盖索引,在不使用覆盖索引的时候,会引起回表查询,这是因为MySQL的非聚簇索引...“; 54 | 2. 聊到如何计算一次查询的开销。这个比较少见,因为一般的面试官也讲不清楚一次MySQL查询时间开销会在哪里; 55 | 3. 前面基本回答,回答了聚簇索引之后直接回答这部分 56 | 4. 聊到了B+树的叶子节点可以存放什么,或者聊到了索引的叶子节点可以存放什么 57 | 5. 是不是查询一定会引起回表?这其实是考察覆盖索引,所以在谈及了覆盖索引之后可以聊这个聚簇索引和非聚簇索引的点 58 | 59 | ### MySQL为什么使用B+树索引 60 | 61 | 分析:实际上就是为了考察数据结构,B+树的特征,而且能够根据B+树的特征,理解MySQL选择B+树的原因。面试官可能同时希望你能够横向比较B+树、B树、平衡二叉树,红黑树和跳表 62 | 直接背这几种树的基本特征是比较难的,所以我们可以只回答关键点。关键点就是三个,和二叉树比起来,B+树是多叉的,高度低;和B树比起来,它的叶子节点组成了一个链表;第三点,是一个角度很清奇的点,就是查询时间稳定,可预测。和跳表的比较比较奇诡,要从MySQL组织B+树的角度出发。 63 | 64 | 答案:MySQL使用B+树主要就是考虑三个角度: 65 | 1. 和二叉树,如平衡二叉树,红黑树比起来,B+树是多叉树,比如MySQL默认是1200叉树,同样数据量,高度要比二叉树低; 66 | 2. 和B树比起来,B+树的叶子节点被连接起来,形成了一个链表,这意味着,当我们执行范围查询的时候,MySQL可以利用这个特性,沿着叶子节点前进。而之所以NoSQL数据库会使用B树作为索引,也是因为它们不像关系型数据库那般大量查询都是范围查询; 67 | 3. B+树只在叶子节点存放数据,因此和B树比起来,查询时间稳定可预测。(注:这是一个高级观点,就是在工程实践中,我们可能倾向于追求一种稳定可预测,而不是某些数据贼快,某些数据唰一下贼慢) 68 | 4. B+树和跳表比起来,MySQL将B+树节点大小设置为磁盘页大小,这样可以充分利用MySQL的预加载机制,减少磁盘IO 69 | 70 | 关键点:高度低,叶子节点是链表,查询时间可预测性,节点大小等于页大小 71 | 72 | 73 | #### 如何引导过来这里? 74 | 1. 面试官直接问起来; 75 | 2. 你们聊起了树结构,聊到了B树和B+树,话术一般是“因为B+树和B树比起来,有...的优点,索引MySQL索引主要是使用B+树的; 76 | 3. 聊到了范围查询或者全表扫描,你可以从B+树的角度来说,这种扫描利用到了B+树叶子节点是链表的特征; 77 | 78 | ### 为什么使用自增主键 79 | 80 | 分析:这是一个常考点,从根源上来说,是为了考察你对数据库如何组织数据的理解。问题在于,数据库如何组织数据其实是一个很难的问题,所以一般情况下,不需要回答到非常底层的地步。 81 | 82 | 答案:MySQL的主键是一个**聚簇索引**,即它的叶子节点存放了数据。 83 | 在使用自增主键的情况下,会保证树的分裂照着单方向分裂的,这会大概率导致物理页的分裂也是朝着单方向进行的,即连续的。 84 | 在不使用自增主键的情况下,如果在已经满的页里面插入,会导致MySQL页分裂,虽然逻辑上页依旧是连续的,但是物理页已经不连续了。 85 | 如果在使用机械硬盘的情况下,会导致范围查询经常导致机械硬盘重新定位,性能差。 86 | 87 | 关键点:单方向增长,物理页连续 88 | 89 | #### 如何引导过来这里? 90 | 1. 面试官可能直接问你 91 | 2. 你在基本回答那里,回答到"聚簇索引"的时候,主动说起,为什么我们要使用自增索引。话术可能是"MySQL的主键索引是聚簇索引,每张表一个,所以我们一般推荐使用自增主键,因为自增主键会保证树单方向分裂..." 92 | 3. 聊起数据库表结构设计,你们公司推荐使用自增主键,你可以主动说,我们公司是强制要求使用自增主键的,因为... 93 | 4. 聊起数据库表结构设计,你有一些特殊的表,没有使用自增主键,你可以说"我们大多数表都是使用自增主键的,因为...但是这几张表我们没有使用,因为xxx(结合你们的业务特征回答)",慎用 94 | 5. 聊到树结构的特征。比如说面试官其实面你的是数据结构,而不是数据库,但是你们聊到了树,就可以主动提起。因为大部分树,比如说红黑树,二叉平衡树,B树,B+树都有一个调整树结构的过程,所以可以强行引过来; 95 | 6. 聊起分库分表设计,主键生成的时候,可以提起生成的主键为什么最好是单调递增的。这个问题其实和为什么使用自增主键,是同一个问题; 96 | 7. 评价为什么使用UUID来作为主键生成策略会很糟糕 97 | 98 | ### 索引有什么缺点 99 | 100 | 分析:面试官就是为了吓你,出其不意攻其不备。又或者面试官问为啥不在所有列上建立索引,或者问为什么不建立多一点索引 101 | 102 | 答案:索引的维护是有开销的。在增改数据的时候,数据库都要对应修改索引;而如果索引过多,以至于内存没法装下全部索引,那么会导致访问索引本身都会触发IO。所以索引不是越多越好。比如为了避免数据量过大,某些时候我们会使用前缀索引。 103 | 104 | #### 如何引导过来这里 105 | 1. 面试官直接问了 106 | 2. 你在基本回答那里回答了前缀索引,之后可以说”使用前缀索引是为了节省空间,因为索引本身的维护是有开销的,除了空间开销,在数据更新的时候..." 107 | 3. 在回答完什么时候索引之后可以直接说 108 | 109 | ### 什么是索引下推 110 | 111 | 分析:这个题更加无聊,因为它对于你的实际工作帮助可以说没有了。前面那些点理解清楚,还可以说有助于自己设计索引,这个就可以说,完全没用。回答这个问题的关键点在于,要和联合索引、覆盖索引一起讨论。因为他们体现的都是一个东西:即尽量利用索引数据,避免回表。 112 | 113 | 答案:索引下推是指将于索引有关的条件由MySQL服务器下推到引擎。例如按照名字存取姓张的,like "张%"。在原来没有索引下推的时候,即便在用户名字上建立了索引,但是还是不能利用这个索引。而在支持**索引下推的引擎上**,引擎就可以利用名字索引,将数据提前过滤,避免回表。目前innodb引擎和MyISAM都支持索引下推。索引下推和覆盖索引的理念都是一致的,尽量避免回表。 114 | 115 | ### 使用索引了为什么还是很慢? 116 | 117 | 分析:这又是属于违背直觉的问题,本质上考察的是你对索引,和MySQL执行过程的理解。记住一个核心点,**索引只能帮你快速定位到数据**,而定位到数据之后的事情,比如说把读数据,比如说写数据,这都是要时间的。尤其是要考虑事务机制,锁竞争的问题。 118 | 119 | 答案:索引只能帮助定位数据,但是**从索引定位到数据,到返回结果,或者更新数据,都需要时间**。尤其是在事务中,索引定位到数据之后,可能一直在等待锁。如果别的事务执行时间缓慢,那么即便你用了索引,这一次的查询还是很慢。本质上是因为,MySQL 的执行速度是受到很多因素影响的,准确来说,索引只是大概率能够加速这个过程而已。 120 | 121 | 另外要考虑,数据库是否使用错了索引。如果我们的表上面创建了多个索引,那么就会导致 MySQL 选择使用了不那么恰当的索引。在这种时候,我们可以通过数据库的 Hint 机制提示数据库走某个索引。 122 | 123 | 关键字:锁竞争 124 | 125 | #### 类似问题 126 | - 为什么我定义了索引,查询还是很慢?这个问题有一个陷阱,即他没说我用到了索引,也就是说,你定义了索引,但是可能MySQL没用;也可能用了,但是卡在锁竞争那里了 127 | 128 | #### 如何引导 129 | - 在前面聊到使用索引来优化的时候,可以提一嘴这个,即并不是说使用了索引就肯定很快 130 | 131 | ### 什么时候索引会失效? 132 | 133 | 分析:索引失效这个说法有点误导人,准确的说法是,为什么我明明定义了索引,但是MySQL却没有使用索引?关键点是权衡,即下面的第二个理由。 134 | 135 | 答案:没有使用索引主要有两大类原因,一种是自己 SQL 没写好,例如: 136 | 1. 索引列上做了计算 137 | 2. like 关键字用了前缀匹配,例如”%abc“。注意的是,后缀匹配是可以用索引; 138 | 3. 字符串没有引号导致类型转换 139 | 140 | 另一种,则是 MySQL 判断到使用索引的代价很高,比如说要全索引扫描并且回表,那么就会退化成为全表扫描。数据库数据量的大小和数据分布,会影响MySQL的决策。 141 | 142 | #### 类似问题 143 | - 为什么我定义了索引,查询还是很慢?没用 or 锁竞争 144 | - 为什么我定义了索引,MySQL 却不用? -------------------------------------------------------------------------------- /database/transaction.md: -------------------------------------------------------------------------------- 1 | # 数据库事务 2 | 3 | 事务是指多个数据库操作组成一个逻辑执行单元,满足 ACID 四个条件。 4 | 5 | A是指原子性,即这些操作要么全部成功,要么全部不成功,不存在中间状态; 6 | C是指一致性,数据库从一个状态转移到另外一个状态,数据完整性约束不变。在分布式语境下,这个很多时候是指数据如果存储了多份,那么每一份都应该是一样的。(后面分布式语境,要小心一点,因为这一步,可能会让面试官准备考察分布式事务) 7 | I是指隔离性,一个事务的执行不会影响另外一个事务; 8 | D是指持久性,已提交对数据库的修改,应该永久保留在数据库中。(装逼点)而实际上MySQL的事务,如果设置不当,可能出现事务已经提交,但是并没有被持久化。(这一点是为了加分的,你需要记住后面的《事务提交了但是数据没有保存》) 9 | 10 | 在MySQL上,innodb引擎支持事务,但是MyISAM不支持事务。(这个是为了引导面试官问两个引擎的区别) 11 | 12 | innodb 引擎是通过MVCC来支持事务的。(到这一步,停下来,接下来,面试官极大概率问你什么是MVCC) 13 | 14 | 关键点:ACID,innodb 通过 MVCC 支持事务 15 | 16 | ![数据库事务](./img/transaction.png) 17 | 18 | ## 扩展点 19 | 20 | ### 什么是 MVCC 21 | 22 | 分析:MVCC算是一个常考的点,而且是一个能考察得很细很深入的点,这里我们尽量将话题控制在一个难度适中的地步。当然,如果你是DBA,那么你应该一路往下探讨直到源码层。这个问题问得非常的大,所以我们只需要回答要点就可以。而后,面试官如果想继续了解你的水平,他就会根据他感兴趣的要点问下去。这里的难点就是,要点太多……要背很多。 23 | 24 | 答案:MVCC,多版本并发控制。`innodb`引擎主要是通过`undo log` 和事务版本号来实现多版本,利用锁机制来实现并发控制。 25 | 26 | (接下来仔细解释`undo log`和版本号的运作机制,其中`undo log`是为了引导面试官继续问相关的问题,如`redo log`,`bin log`。) 27 | 28 | `innodb`引擎会给每张表加一个隐含的列,存储的是事务版本号。当修改数据的时候,会生成一条对应的`undo log`,`undo log`一般用于事务回滚,里面含有版本信息。简单来说可以认为`undo log`存储了历史版本数据。每当发起查询的时候,`MySQL` 依据隔离级别的设置生成`Read View`,来判断当前查询可以读取哪个版本的数据。例如,在已提交读的隔离级别下,可以从`undo log`中读取到已经提交的最新数据,而不会读取到当前正在修改尚未提交的事务的数据。 29 | 30 | 而锁机制,对于 innodb 来说,有多个维度: 31 | 1. 从独占性来说,有排他锁和共享锁; 32 | 2. 从锁粒度来说,有行锁和表锁; 33 | 3. 从意向来说,有排他意向锁和共享意向锁; 34 | 4. 从场景来说,还可以分为记录锁,间隙锁和临键锁; 35 | 36 | 分析:到这里停下来,上面这一番回答,基本上什么都点到了,接下来就是等提问了。这一堆回答,涉及到了很多知识点,可以考察的非常多: 37 | 1. `undo log`, `redo log`, `binlog` 38 | 2. 隔离级别 39 | 3. 各种锁,其中又以记录锁、间隙锁和临键锁比较有亮点 40 | 4. Read View 41 | 42 | 关键点:多版本 = undo log + 事务版本号,并发控制=各种锁 43 | 44 | #### 如何引导 45 | 1. 讨论数据库事务隔离级别 46 | 47 | ### 能够解释一下MySQL的隔离级别吗? 48 | 49 | 分析:考察基本的知识点。如果只是背出来各种隔离级别和对应存在的问题,那么就达标了。刷亮点如何刷呢?一个是结合 MVCC 来阐述MySQL是如何支持的;一个是讨论 snapshot isolation。前者比较中规中矩,后者比较多是秀知识面。我们分成这两个思路,前面都类似,就是总结各种隔离级别。 50 | 51 | 数据库的隔离级别有四种: 52 | 1. 未提交读:事务可以读取另外一个事务没有提交的数据。 问题:脏读,不可重复读,幻读 53 | 2. 提交读:事务只能读取到另外一个已经提交的事务数据。 问题: 不可重复度,幻读 54 | 3. 重复读:事务执行过程查询结果都是一致的,innodb 默认级别。 问题: 幻读 55 | 4. 串行化:读写都会相互阻塞 问题: 56 | 57 | MVCC 方向: 58 | innodb 引擎利用了 `Read View` 来支持提交读和重复读。`Read View`里面维护这三个变量: 59 | 1. up_limit_id:已提交事务ID + 1 60 | 2. low_limit_id:最大事务ID + 1 61 | 3. txn_ids:当前执行的事务ID 62 | 63 | 提交读这个级别,默认读取是不加锁的,只有修改才会加锁。简单来说,已提交读,是每次查询都生成一个新的`Read View`,所以永远都能看到已经提交的事务。 64 | 65 | 可重复读则是在第一次查询生成`Read View`之后,后面的查询都是使用这个`Read View`。 66 | 67 | snapshot isolation 方向: 68 | 69 | innodb 引擎的可重复读隔离级别,要比定义的隔离级别更加严苛一点。一般的可重复读,无法解决幻读的问题。比如说原本你事务里面查询订单信息,这个时候又插入了一个新的订单,那么这种时候,幻读就会导致我们下一个查询就会查询到这条记录。但是 innodb 引擎的隔离级别并不会出现这个问题。 70 | 71 | 因为 innodb 引擎使用了临键锁,在“当前读”,也就是写的时候,锁住了记录之间的空档,防止插入数据。(这里面,不需要解释临键锁,等面试官提问) 72 | 73 | #### 如何引导 74 | 1. 前面聊到了MVCC提到隔离级别,机会合适就可以主动发起进攻 75 | 76 | ### InnoDB 的 Repeatable Read 隔离级别有没有解决幻读 77 | 78 | 先说答案:解决了(在官方文档的暧昧中),但是又没有完全解决(在头脑清醒的开发者眼中)。 79 | 80 | 如上文所言,官方文档中表述 InnoDB 用临键锁 (next-key lock) 解决了幻读的问题,临键锁工作在 RR 隔离级别下,设置隔离级别为 RC 会导致 GAP 锁失效,继而导致没有临键锁。这是 InnoDB 自我定义其 RC 存在幻读,而 RR 可以避免幻读的描述。 81 | 82 | InnoDB 作为一个优等生,在[隔离级别定义](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads)要求 RR 不需要避免幻读的情况下,宣称自己实现了这个功能。但实际上受到了限制: 83 | 84 | 85 | - 对于仅包含连续相同快照读语句的事务,MVCC 避免了幻读,但是这种场景临键锁没有用武之地,而官方文档重点强调是临键锁的实际避免了幻读,所以 InnoDB 肯定觉得自己做到了更多。 86 | - 对于仅包含连续相同当前读语句的事务,第一个当前读会加临键锁,会阻塞别的事物的修改,也避免了幻读。 87 | - 但是对于快照都和当前读语句交错的事务,第一个快照读后其它事务仍可以修改并提交内容,当前事务的后续当前读就会读到其他事务带来的变更。导致可以造出一些印证 InnoDB 没有解决幻读问题的例子。 88 | 89 | ![](./img/rr-phantom-read-example.png) 90 | 91 | #### 参考资料 92 | 93 | - 官方文档-[InnoDB 宣称使用临键锁解决幻读](https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html) 94 | - To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking 95 | - 官方文档-[InnoDB 定义临键锁为 Record lock plus gap lock](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-next-key-locks) 96 | - A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record. 97 | - 官方文档-[InnoDB 临键锁工作在 RR 下](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-next-key-locks) 98 | - By default, InnoDB operates in REPEATABLE READ transaction isolation level 99 | - 官方文档-[InnoDB 可设置隔离级别为 RC 以关闭 Gap lock](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-gap-locks) 100 | - Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED 101 | - 官方文档-[幻读(幻影行)定义](https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html) 102 | - The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. 103 | - Wikipedia [幻读定义](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Repeatable_reads) 104 | - A phantom read occurs when, in the course of a transaction, new rows are added or removed by another transaction to the records being read. 105 | - Wikipedia [Serializable 隔离级别可以避免幻读](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Phantom_reads) 106 | - However, at the lesser isolation levels (than Serializable), a different set of rows may be returned the second time (phantom read happens) 107 | - MySQL [BUG #63870](https://bugs.mysql.com/bug.php?id=63870) 关于当前读附带一点幻读的讨论 108 | - 正经讨论 - [Innodb 中 RR 隔离级别能否防止幻读](https://github.com/Yhzhtk/note/issues/42) 109 | 110 | ### 什么是共享锁,排它锁 111 | 112 | 分析:概念题,答完顺便回答意向排他锁,意向共享锁,刷一波 113 | 114 | 答案:共享锁指别的事务可以读,但是不可以写。排他锁,是指别的事务既不可以读也不可以写。与之非常类似的是,意向共享锁和意向排他锁,事务在获取共享锁或者排他锁之前,要先获得对应的意向锁。意向锁是数据库自己加的,不需要干预。 115 | 116 | (下面这段可能比较绕,记不住就算) 117 | 排它锁和其它三种都互斥;(X排斥一切) 118 | 意向排它锁和意向锁兼容;(IX 兼容 I) 119 | 共享锁和共享锁、意向共享锁兼容;(S 兼容 S) 120 | 121 | ![lock and i lock](https://pic4.zhimg.com/80/v2-37761612ead11ddc3762a4c20ddab3f3_720w.jpg) 122 | 123 | ### 什么是记录锁,临键锁,间隙锁 124 | 125 | 分析:概念题,可以点出来记录锁和行锁的关系,并且指明一下行锁是在索引项上加的。 126 | 127 | 答案: 128 | 1. 记录锁:锁住一行,所以叫做记录锁,也是行锁; 129 | 2. 间隙锁:锁住记录之间的间隔,或者索引之前的范围,或者所以之后的范围。只在重复读级别产生,(可以在前面隔离级别的地方提) 130 | 3. 临键锁(Next key lock):记录锁和间隙锁的组合,即锁住记录,又锁住了间隙 131 | 132 | ![记录锁和间隙锁](./img/next_key_lock.png) 133 | 134 | 135 | ### innodb 引擎和 MyISAM 引擎的区别 136 | 137 | 分析:很多人陷入的一个误区,就是死记硬背所有的区别,面试的时候一紧张,又忘了。其实大可不必,记住几个关键点就可以了,因为面试官不一定就把所有的点都记得。说实在,这个问题完全就是为了面试而面试,因为在当前大家选择`MySQL`一般都默认使用`innodb`引擎的时候,讨论这个区别没有太大实际意义。万一不幸的是你们公司用的是MyISAM引擎,那就要仔细回答,方方面面照顾到。 138 | 139 | 答案:innodb 引擎和 MyISAM 最大的区别是事务、索引、锁支持。 140 | 1. innodb 引擎支持事务,而 MyISAM 不支持; 141 | 2. innodb 引擎的主键索引的叶子节点存放的是数据本身,而MyISAM存储的是数据的地址,需要再一次寻址; 142 | 3. innodb 支持行锁,而MyISAM 只支持表锁,因此`innodb`支持的并发粒度更细更高; 143 | 144 | 一般来说,在不使用事务,数据修改少而读多的时候,又或者机器比较差的时候,用MyISAM比较合适。 145 | 146 | ### 为什么事务提交了但是数据没有保存 147 | 148 | 分析:这个问题呢,因为它和ACID的特性有冲突,所以是一个装逼点。一般不做 DBA,没踩过这一类的坑的人,比较容易忽略这一点。在前面提到 ACID 的 D 的时候,如果你记得这个,就可以主动说。整体来说,这是一个稍微高级一点的话题,所以要把握尺度,对这方面了解比较深刻,就一定要刷一波;如果感觉对面的面试官了解不深,也可以刷一波。 149 | 150 | 答案:在MySQL的innodb引擎中,事务提交后,必须将数据刷盘到磁盘上,如果在事务提交之后,没来得及刷到磁盘,就会出现事务已经提交,但是数据丢失了。(回到这一步你要开始判断,如果你是主动聊的,那就停下来,等面试官追问;如果这是面试官问的,那就接着答细节)MySQL的innodb引擎,事务提交的关键是将`redo log`写入到`Log buffer`,而后MySQL调用`write`写入到`OS cache`,只有到最后操作系统调用`fsync`的时候,才会落到磁盘上。 151 | 152 | (为了方便记忆,记住这个过程:`commit` -> `log buffer` -> `OS cache` -> `fsync`) 153 | (下面这一段是可选) 154 | 数据库有一个参数 `innodb_flush_log_at_trx_commit` 可以控制刷盘的时机: 155 | 1. 0,写到`log buffer`, 每秒刷新; 156 | 2. 1,实时刷新; 157 | 3. 2,写到`OS cache`, 每秒刷新 158 | 159 | (接下来步入终极装逼环节,为了表达我们对这个问题的深刻理解,对OS的一般理解,我们得扩充一下回答面,慎用) 160 | 161 | Redis的`AOF`机制也面临类似的问题,即`AOF`也不是立刻刷盘,而是写入到了`OS cache`,等到缓冲区填满,或者`Redis`决定刷盘才会刷到磁盘。而`redis`有三种策略控制,`always` 永远, `everysec` 每秒, `no` 不主动。默认情况下`everysec`,即有一秒钟的数据可能丢失。 162 | 163 | (最后升华一下主题) 164 | 对于大多数要和磁盘打交道的系统来说,都会面临类似的问题,要么选择性能,要么选择强持久性。 165 | 166 | 关键字:提交不等于落盘了,`fsync` 167 | 168 | #### 如何引导 169 | 1. 从`Redis` AOF 引过来,两边讨论的都是同一个主题; 170 | 2. 回答`ACID`的时候引导过来; 171 | 3. 讨论磁盘 IO 的时候看情况; 172 | 4. 讨论操作系统文件系统的时候,看情况; 173 | 174 | 核心就是,涉及到了`OS cache`,`fsync`等点,就可以引导来这边。 175 | 176 | 177 | ### 什么是redo log, undo log 和 binlog 178 | 179 | 分析:概念题。最好的回答是用`undo`, `redo`, `binlog`来讲述清楚事务与回滚,主从同步复制。这里我们做一个简要的回答,把精髓答出来。 180 | 181 | 答案: 182 | 1. `redo log` 是`innodb`引擎产生的,主要用于支持MySQL事务,MySQL会先写`redo log`,而后在写`binlog`。`redo log`可以保证即使数据库异常重启,数据也不会丢失 183 | 2. `undo log` 是`innodb`引擎产生的,主要时候用于解决事务回滚和MVCC。数据修改的时候,不仅记录`redo log`,也会记录`undo log`。在事务执行失败的时候,会使用`undo log`进行回滚; 184 | 3. `binlog` 主要用于复制和数据恢复,记录了写入性的操作。`binlog`分成基于语句,基于行和混合模式三种复制模式。 185 | 186 | (扩展点1,阐述两阶段提交) 187 | 因为`redo log`生成到`binlog`写入之间有一个时间差,所以为了保证两者的一致性,MySQL引入了两阶段提交: 188 | 1. Prepare阶段,写入`redo log`; 189 | 2. Commit阶段,写入`binlog`,提交事务; 190 | 191 | (扩展点2,阐述一下的刷盘时机) 192 | 1. `binlog` 刷盘可以通过`sync_binlog`参数来控制。0-系统自由判断,1-commit刷盘,N-每N个事务刷盘 193 | 2. `redo log`刷盘可以通过参数`innodb_flush_log_at_trx_commit`控制。0-写入`log buffer`,每秒刷新到盘;1-每次提交;2-写入到`OS cache`,每秒刷盘; 194 | 195 | 196 | ## Reference 197 | [一文理解MySQL MVCC](https://zhuanlan.zhihu.com/p/29150809) 198 | [innodb中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) 199 | -------------------------------------------------------------------------------- /gc/README.md: -------------------------------------------------------------------------------- 1 | # GC (垃圾回收) 2 | 3 | ## GC 的考察点 4 | GC 在特定的语言里面,是一个极其重要的面试知识点,比如说 Golang 和 Java。 5 | 6 | GC 的面试,主要是从三个维度进行考察: 7 | 1. GC 的算法:例如引用计数,标记清扫,标记整理等,纯粹从算法的层面考察大家; 8 | 2. GC 的实现:例如 JVM 中 HotSpot 实现的 CMS,G1等,重点考察这些实现的具体步骤,部分情况下会涉及细节; 9 | 3. 实践:集中在,DEBUG 和调优。DEBUG是指,实践中是否遇到多 GC 相关的问题,如果遇到了,怎么解决的;调优则是发现实际中的 GC 的效果不理想,如何优化的问题; 10 | 11 | 因此 GC 要如何复习,才能面试顺利呢? 12 | 1. 背熟算法; 13 | 2. 背熟具体实现的步骤,部分重点实现要深挖细节。要熟记影响这些实现的参数,同时可以结合自己公司内部配置来记忆参数和理解; 14 | 3. 准备案例,包括各种奇诡问题,优化案例。这里要注意的是,**如果你亲自遇到过,那么就用你亲自遇到过**,如果没有,用你同事遇到的问题;依旧没有,就用公司出过的问题;完全没遇到,就准备网上的案例; 15 | 16 | 那么常见的面试失误在哪里呢? 17 | 1. 算法和实现不能区分清楚; 18 | 2. 并行和并发不能区分清楚; 19 | 3. 实现之间混淆; 20 | 4. 遗漏了不同实现的参数配置及其影响; 21 | 5. 未提前准备好各种案例; 22 | 23 | 那怎么样才能在 GC 面试里面刷出来亮点?—— 与众不同 24 | 1. 别人不知道的,我知道; 25 | 2. 别人知道的,我知道更多细节; 26 | 3. 结合实际 27 | 4. 结合内存分配器 28 | 5. 横向比较 29 | 30 | 前两条很好理解,后一条如何理解呢?要知道,面试官面 GC,不管是面算法,面实现还是面调优,他就是想确认你能不能解决 GC 的问题。所以结合实际能够让他知道,你确实是知道如何解决 GC 问题的。 31 | 32 | 第四点则是一个盲点。就是大部分的人只关注过GC,但是没有关注过不同垃圾回收器,其内存是如何被分配的。比如说 CMS 采用了空闲链表法来管理空闲内存,就是一个很有特色的点。 33 | 34 | 35 | -------------------------------------------------------------------------------- /gc/algorithm.md: -------------------------------------------------------------------------------- 1 | # GC 算法 2 | 分析:算法可以从多个维度进行分析。 3 | 4 | ## 总览 5 | 6 | ### 收集器与回收器 7 | 8 | 首先,算法可以分成两个大问题 9 | 1. 如何找到存活对象:基本上就是两类,引用计数和标记(也叫做跟踪式,可达性分析); 10 | 2. 怎么回收空间:复制,整理或者清扫。复制是指直接将存活对象拷贝到另外一块内存区域;压缩是指,将存活对象挪到一起;清扫实际上,大多数时候,就是做一些标记,标记内存可用; 11 | 12 | 这里要强调一下清扫。清扫明面上是指,我将垃圾扫掉,实际上是指将内存返回给内存分配器。那么问题就来了,内存分配器怎么维护这些空闲内存?基本上这里又是两种选择,一种是位图,一种是空闲链表法。相比之下,采用复制的算法,基本上就只需要维护寥寥几个地址。 13 | 14 | 如果笛卡尔积一下,就有了: 15 | 1. 引用计数-复制,引用计数-整理,引用计数-清扫; 16 | 2. 标记-复制,标记-整理,标记-清扫; 17 | 18 | 不过引用计数我们面试比较少遇到,它在实际中用得也不多,标记类用得比较多。 19 | 20 | ### 并发与并行 21 | 22 | 算法又有并发和并行之分。这里提到的并发和并行,在GC这个特定的语义下,含义稍微有点区别。 23 | 24 | 这里我们说的并发,其实是指 GC 线程和应用线程,一起运行。就是一边GC,一边对外服务。并行则是指多个GC线程一起干活。 25 | 26 | 那么,要记住,多核 CPU 之下,并发往往意味着并行。即,GC 线程和应用线程是并行的,GC线程和GC线程也是并行的,应用线程和应用线程也是并行的。 27 | 28 | 但是并行不一定意味着并发。例如 Java 里面的 Parallel New,就是GC线程并行收集,这个就不是并发的,因为没有应用线程此时也在运转。 29 | 30 | 典型的并发 GC: 31 | 1. Java 上 HotSpot 实现的 CMS,G1,ZGC 32 | 2. Golang GC; 33 | 34 | 典型的并行 GC:HotSpot 带 Parallel 关键字的,Old Parallel, Parallel New 35 | 36 | #### 类似问题 37 | - 并发 GC 是并行 GC吗?多核 CPU 上就是,否则就不是 38 | - 并行 GC 一定是并发 GC吗?不是 39 | 40 | ### 分代 41 | 42 | 不是所有 GC 都有分代的!! 43 | 44 | 分代主要是基于分代假说,大部分刚分配的对象会在短时间内死掉,越是生存时间长的对象,越不容易死掉 45 | 46 | (类比“**幼儿夭折**”和“**老不死**”) 47 | 48 | ![HotSpot分代的效果](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/img/jsgct_dt_003_alc_vs_srvng.png) 49 | 50 | 典型的分代 GC:HotSpot 的大部分GC都是 51 | 52 | ### 增量回收 53 | 54 | 增量回收核心在于,回收的时候并不是将整个堆,或者整个分代回收掉,而是只回收部分。实施增量回收核心就在于避免在一次GC中消耗太多资源,典型的就是 G1 采用了增量回收来避免停顿时间超长。 55 | 56 | 典型的增量回收GC:HotSpot G1 57 | 58 | ## 面试题 59 | 60 | ### 你了解 XXX 算法吗? 61 | 分析:这就是送分题,考察基本概念。这一大类题目,要想回答出来亮点,可以指出哪些垃圾回收器使用了该算法,然后指出这种算法的优缺点(可以进行一些横向比较)。更进一步,如果能够记住并且理解,可以结合内存分配器来一起说。因为特定的算法,限制住了内存分配器的实现。 62 | 63 | 记住步骤: 64 | 1. 基本流程 65 | 2. 优缺点 66 | 67 | 下面我们一个个算法说过去。 68 | 69 | #### 标记-复制 70 | 71 | (首先回答基本流程)标记-复制算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,将存活的对象复制到另外一块内存。Java的Serial New, Parallel New 和 G1 都是采用了标记复制算法。 72 | 73 | (讨论优缺点)该算法的优点是,复制会保证我们能够得到一块连续的内存,可以采用高效率的内存分配方案(叫做bump-the-pointer,其实就是指针移动)。缺点则是内存利用率不高,极端情况下,我们只能利用一半内存,另外一半内存要作为复制的目标内存。而且复制也是一个消耗极大的过程。 74 | 75 | ![指针碰撞](https://img-blog.csdnimg.cn/20201130155644621.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3JkX3dfY3Nkbg==,size_16,color_FFFFFF,t_70#pic_center) 76 | 77 | ##### 类似问题 78 | - 复制算法,只能使用一半内存吗?这个问题是说,我们在使用复制算法的时候,要留出一部分空间来装复制的对象。比如说,我们有1G内存,复制算法是不是只能使用 500 M,剩下的500M作为装存活对象的空间。并不是,假如说我们存活对象占比10%,例如我100M对象,存活的有10M。那么我就只需要10M来装存活对象。回到这个 1G 的例子,这意味着我可以用 800 M,第一次回收,有 80M 存活,我丢到一个 100M 的块里边;第二次它是 (800+100) * 10% = 90 M 存活对象,还剩下 100 M,非常完美放下。这就是 JVM 里面为啥是两个 Survivor。因此用复制算法,内存利用率也可以超过50%。 79 | ![注意一个Eden两个](https://img-blog.csdn.net/20170518171044703?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc3RlZF96eHo=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) 80 | 81 | #### 标记-整理 82 | 83 | (首先回答基本流程)标记-整理算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,将存活的对象全部挪到一侧。这个过程类似于标记-复制。比较有名的采用了这个算法的就是 HotSpot 里面的 Serial Old 和 Parallel Old。 84 | 85 | (讨论优缺点)该算法的优点是,整理复制会保证我们能够得到一块连续的内存,可以采用高效率的指针碰撞技术来分配内存。缺点则是整理过程非常耗时,涉及到了大量对象移动。比如 CMS 在启用压缩之后,这个过程是 STW 的,导致 GC 停顿时间特别长。 86 | 87 | (讨论改进方案,亮点)有一种改进思路,是不进行全量整理,而是部分整理,即每次 GC 只会整理一部分,作为 GC 停顿时间和内存碎片的一种权衡。 88 | 89 | #### 标记-清扫 90 | 91 | (首先回答基本流程)标记-清扫算法,在GC开始的时候,会从 GC root 出发标记,沿着对象的引用链,标记存活的对象。在标记完成之后,垃圾回收器会把空闲内存交回内存分配器。最有名的标记清扫垃圾回收器是 CMS 回收器。 92 | 93 | (讨论优缺点)该算法的优点是,清扫的过程比复制和整理要快很多。但是带来的缺点是,内存分配要更加复杂,并且空闲内存不再是一个连续的块。比如说 CMS 就采用了空闲链表来管理空闲内存(这里可能引导过去聊CMS 空闲内存管理,不熟悉就忽略)。带来严重的内存碎片问题。 94 | 95 | ##### 如何引导 96 | - 从操作系统内存管理聊过来 97 | 98 | #### 引用计数 99 | 100 | 引用计数是在对象里面维护一个计数,标记有多少对象使用到了该对象。如果计数为0,就表明该对象可以被回收了。 101 | 102 | 该算法的优点是实现简单,GC过程很快。缺点则是,循环引用难以解决。所谓的循环引用,是指多个对象之间互相引用,最终行成一个环,因此它们的计数永远都不会为0。Swift 的垃圾回收就是采用了引用计数(赫赫有名的ARC),还有很多智能指针也是用引用计数来实现的。 103 | 104 | 还有一个缺点,则是整个开销和引用变更次数成正比。 105 | 106 | (注意,如果记得住,可以接着回答**如何解决循环引用**) 107 | 108 | ### 如何解决引用计数中的循环引用问题? 109 | 分析:难题。其实大多数情况下,面试官问出来这个问题,也没指望我们能回答出来,就是抱着万一你知道的心态。当然,如果你面的语言,就是用了引用计数来做GC的话,那么这会比较重要。对于 Java,golang 开发来说,稍微知道一点就可以。 110 | 111 | 答案:一般是有三种策略: 112 | 1. 采用特殊引用。例如使用 weak reference,弱引用。这一类的做法是用户需要自己显式管理自己的引用,在出现循环引用的地方,将一部分引用修改为 weak reference,从而所谓的 strong reference 就不再组成环; 113 | 2. 采用后备的追踪式收集器。一般来说,是把可能出现环的对象单独处理,用追踪式的收集器标记一遍,这些就是存活对象。(Python就是这种策略,不了解算法细节) 114 | 3. 采用试探删除策略。该方法类似于图里面去除环的算法,尝试把某些引用删掉。如果删掉之后别的对象的计数变为0,那么说明这些对象只有环内的引用,因此是可回收对象。 115 | 116 | #### 如何引导 117 | - 在聊到了引用计数的时候。这个是一个比较安全的亮点,就是认真研究过循环引用处理方案的面试官不多 118 | - 在聊到特殊引用的时候,可以讲一下特殊引用在引用计数里面的应用。 119 | 120 | #### 类似问题 121 | - 追踪式垃圾回收器如何解决循环引用问题?这个问题其实是吓人的,因为追踪式的一般都是使用三色法来追踪对象,天然就解决了,可以参考后面的三色标记法面试题 122 | 123 | ### 引用计数和可达性分析的优缺点? 124 | 分析:根据两者的基本特征来回答就可以。这个问题比较罕见。 125 | 126 | 答案:引用计数最大的优点就是实现简单、GC 很快,整体开销被平摊到了整个应用生命周期内,对并发 GC 支持比较好。缺点则是循环引用难以解决,整体开销和引用变更次数成正比,比较大。 127 | 128 | 可达性分析则是会在 GC 过程中引入 STW,难以实现,并发 GC 的实现特别困难。优点则是可达性分析只和存活对象数量有关,开销较小。并且可达性分析解决循环引用的问题非常容易 129 | 130 | #### 如何引导 131 | - 无论是讨论了引用计数还是可达性分析,都可以做一个总结 132 | 133 | #### 类似问题 134 | - XXX 为什么用引用计数/可达性分析?总结它们的优缺点,指出就是不同人在不同场景下的权衡。 135 | 136 | 137 | ### 什么是三色标记法 138 | 分析:考察基本算法。答出一般步骤就可以。如果要刷亮点,就要回答并发标记流程,并发的情况下,可能误把回收对象标记为存活。 139 | 140 | 答案:三色标记法是指在标记过程中将对象标记为黑色、灰色或者白色。黑色代表存活对象,灰色代表正在标记中,白色表示死亡对象; 141 | 1. 最开始的时候,所有的对象都是白色的; 142 | 2. 而后从GC root 出发,首先将对象标记为灰色,其次将其引用对象标记为灰色,再把自己从灰色变为黑色; 143 | 144 | ![三色标记法](https://pic2.zhimg.com/v2-5fe8ea45e2518ca19cfeb31558160fb1_b.webp) 145 | 146 | (引出误标记的话题)这个标记过程可以和应用线程并发运行,不过这个时候可能存在一个问题,就是可能一个对象被标记为黑色(即存活),但是随后应用线程更新了指向它的引用,它变成了死对象。这个时候,标记结束之后,该对象依旧会被认为还存活着(活死人,假阴性)。 147 | 148 | (这里我们补充一下如何解决循环引用)使用三色标记法能够天然解决循环引用的问题,因为循环引用的一端,必然会被先染成了黑色,这时候就直接跳过,而不会重复染色,导致循环。 149 | 150 | #### 类似问题 151 | - 三色标记法怎么解决循环引用问题?其实这个问题有点鸡肋,一般了解一点三色标记法的人都不会问这个。因为三色标记法里面,循环引用就不是一个问题。 152 | - 标记为黑色的对象一定是存活对象吗?并发下就不是,非并发下就是 153 | 154 | ### 为什么使用并发 GC? 155 | 156 | 分析:并发 GC 意味着应用线程不停,减少停顿时间。 157 | 158 | 答案:并发 GC 有很显著的优势,即在整个回收过程中,大部分情况下,应用依旧可以对外服务,仅仅需要在特定的时间节点上 STW,整体停顿时间很多。 159 | 160 | 不过并发 GC 一般实现复杂,而且吞吐量不如并行 GC。(这里尝试引导面试官,进一步问,为什么并发 GC 吞吐量不如并行 GC) 161 | 162 | ### 为什么并发 GC 的吞吐量一般比并行 GC 要低? 163 | 164 | 分析:考察并发 GC 实现上的难点。核心就在于并发 GC 要额外引入别的数据结构和步骤来处理,在并发过程中,引用的变更。例如回收过程中,应用创建了新的对象,修改了原本的对象。我们这里使用具体的例子来总结。 165 | 166 | 答案:主要在于,并发 GC需要引入额外的数据结构和步骤来处理并发过程中,应用线程修改过的对象。(如果自己不熟悉后面的这些,就不要说)例如在 Java CMS 回收器,就引入了预清理和再标记步骤,G1 引入 SATB (snapshot at the beginning)了。这些都会占据更多的 CPU 资源。 167 | 168 | ### 并发 GC 和并行 GC 比起来有什么优缺点? 169 | 170 | 分析:考察的是这两大类 GC 的设计初衷。并发 GC 设计初衷是为了不停下应用线程,也是为了降低停顿时间。而并行 GC 则是纯粹为了加快 GC 速度。 171 | 172 | 因此,并发 GC的优点是应用不停,停顿时间短;并行 GC则是吞吐量大。 173 | 174 | 答案:并发 GC 优点在于整个 GC 过程中,大多数时候应用不需要停下来,因此应用能够平稳运行,整个STW的时间也短。 175 | 176 | 并行 GC 则专注在吞吐量,停顿时间会比并发GC长。 177 | 178 | (给出选择建议)对于互联网应用这种强调停顿时间的应用来说,一般选择并发 GC;而对于批处理之类的应用,则可以使用并行 GC。 179 | 180 | #### 类似问题 181 | - 并发 GC 性能比并行 GC 好?错 182 | - 并行 GC 性能比并发 GC 好?错 183 | - 什么时候用并发 GC? 184 | - 什么时候用并行 GC? 185 | 186 | ### 什么是安全点? 187 | 188 | 分析:这个问题其实并没有非常标准的回答,理论上也应该很少有人关注,不过我被问过几次,姑且放这里。一般来说,只有JVM 才会面这个问题。别的语言应该虽然有类似的概念——比如说 golang,但是面试没遇到过。 189 | 190 | 答案:安全点是指在这个时间点上,引用关系不会被改变,常见的安全点有方法调用和循环。GC一般要从安全点开始,当准备 GC 的时候,会等待所有的线程都到达安全点,这就是 STW 的实现方式。但是也有别的问题,需要利用到安全点,例如我们尝试 dump 整个堆栈。 191 | 192 | #### 如何引导 193 | - 在谈到 STW 的时候,可以聊起是怎么进入 STW 状态的,就是依靠安全点 194 | 195 | ### 如何判断一个对象存活? 196 | 分析:一般来说,面试官问出这个问题,其实是希望你回答什么标记过程啥的。但是呢,这个问题的答案,准确来说,是问的引用计数和可达性分析,所以先回答可达性分析,直接命中面试官的下怀,然后再补充引用计数。 197 | 198 | 答案:这主要有两种手段,可达性分析和引用计数。 199 | 200 | 可达性分析目前主流是采用三色标记法,三色标记法巴拉巴拉(接上面什么是三色标记法),标记结束之后白色的对象就是死掉的对象。在并发标记的时候,黑色的对象是可能存活对象,但是并不能确保一定存活(亮点也在这里,就是并发三色标记的活死人问题); 201 | 202 | 引用计数,则要简单很多。计数为 0 就是死掉了,不过考虑到循环引用的问题,应该说,除了循环引用之外的引用数量是0,代表已经死了(亮点在要解释循环引用的特殊之处,它们虽然计数不为0,但是已经死掉了)。 203 | 204 | #### 类似问题 205 | - 如何知道一个对象可以被回收了? 206 | - 引用计数不为0,对象一定活着吗? 207 | 208 | ### GC root 是什么? 209 | 分析:本来这个问题,应该和具体的实现结合在一起来考察的。比如说,准确的问法是,我用 CMS + Parallel New,那么在 Full GC 的时候,GC root 包含哪些?不过很多时候面试官都不严谨,所以我们可以从一般原则上回答,然后举个例子。这个问题,在具体语言的 GC 上还会进一步分析。 210 | 211 | 答案:GC root,顾名思义,是指在GC启动的时候必然存活的一组对象。一般来说,GC root 包含: 212 | 1. 栈上对象,于 Java 来说,还包含本地方法栈; 213 | 2. 全局对象,如常量池; 214 | 3. 非收集部分指向收集部分的引用。常见于分代 GC 和增量式GC中 215 | 216 | ### Minor GC 是什么?Major GC 是什么? 217 | 分析:茴香豆的茴字有几种写法的问题。 218 | 219 | 答案:Minor GC 是指年轻代的垃圾回收,Major GC 是指 Full GC。 220 | 221 | ### 为什么要分代? 222 | 分析:其实分代不是最开始就有的,而是大家观察到了分代假说的两个现象之后,才有了分代的设计。那么分代究竟是为什么引入呢?很简单,就是既然新对象很容易就死掉,老对象很难死掉,那我们就分开着两个,然后新对象朝生夕死,这样就可以每次都回收大量的空间。而老对象待着的地方,我就可以少回收,反正也回收不到东西,只在确实没空间了我再回收。所以分代,核心就是为了提高 GC 效率。 223 | 224 | 但是还有一个难点,就是为啥有的 GC 实现是不分代的。这个问题的答案可以作为我们回答的亮点。 225 | 226 | 答:(首先回答分代假说,这是从理论上直接回答了这个问题)分代是基于分代假说,即新对象很容易死,老对象不容易死。(下面点出核心,就是为了效率)因此如果我们采用分代,依据存活时间来将对象放到不同的内存区域,那么在回收的时候,就可以只回收年轻代,或者一起回收。这样一来,回收效率高,(效率高的两个方面)一方面是停顿时间短(也可以说是资源消耗低),一方面是能够回收更多的内存。 227 | 228 | (下面我们指出并不是所有的 GC 实现都是分代的,作为一个亮点)但是并不是所有的 GC 都是分代,例如Java 的 ZGC,golang GC 都不是分代的。绝大部分情况下,分代都要比不分代效率高,但是分代带来的了额外的问题: 229 | 1. 实现难度高,分代 GC 的实现难度,要比非分代高一个量级; 230 | 2. 较难配置和优化,所有的分代 GC 都面临一个问题,就是各个分代的大小该如何确定; 231 | 232 | #### 类似问题 233 | - 为什么有些 GC 没有采用分代 234 | 235 | ### 对象死了就立刻会被回收吗? 236 | 237 | 答案:并不是。只有触发了 GC 才会被回收。 238 | 239 | ### 对象死了一定会被回收吗? 240 | 241 | 答案:并不是,有些语言设计了复活机制,那么它可以在被回收之前,重新活过来(也就是又有了新的引用指向它)。比如说 Java。 -------------------------------------------------------------------------------- /gc/g1.md: -------------------------------------------------------------------------------- 1 | # G1 垃圾回收器 2 | 3 | 分析:CMS 和 G1 都可以被认为是近年面试考察的高频考点。G1 的复习也类似于 CMS 的复习,重点在于捋清楚其中的步骤。而后为了刷出亮点,可以尝试在部分细节上下功夫。 4 | 5 | G1 的几个基本概念要捋清楚: 6 | 1. Region。这个可以说是和 CMS 根源上不同设计理念的体现。总体来说,虽然 CMS 曾经也是支持增量式回收的,但是做得不如 G1 彻底。G1是彻底的增量式回收,原因就在于,它不是每次都回收全部的内存,而是挑一部分 Region 出来。之所以只挑选一部分出来,核心也就是为了控制停顿时间。 7 | 2. Garbage First:也就是 G1 名字的由来。是指,每次回收的时候,回收器会从 Region 里面挑出一些比较脏的来回收。注意这里面有两个,**挑出一些** 和 **比较脏**。这揭示了两个问题:第一个,G1是增量式回收的;第二,G1 优先挑选垃圾最多的。 8 | 9 | 这里给出一个理解 G1 算法的思路: 10 | 11 | G1 的目标是控制住停顿时间。那么我们怎么控制停顿时间?一种比较好的思路就是,我每次回收只回收一小部分内存。例如说我有一个 32G 的堆,我每次只回收 4 个G。那么如果原来你停顿时间是32秒,回收 4G 就只需要5秒。 12 | 13 | 进一步你就会想,如果是我来设计这个 G1,我要想做到这一步,我该怎么搞?我能不能先把堆分成四部分,每次回收其中的一部分? 14 | 15 | 答案是可以的。然后你就又会遇到问题,有些人可能想回收三分之一的堆,那你怎么办?加个参数控制?比如说启动的时候让用户指定把堆分成多少分? 16 | 17 | 那么问题又来了,用户也不知道该分成多少份才能恰好满足自己希望的停顿时间。 18 | 19 | 这个时候你就会考虑,能不能让用户把他希望的停顿时间告诉你,你自己来决定回收多大的一块。 20 | 21 | 到了这一步,你又会发现一个问题,即便用户告诉你期望停顿时间要控制在一秒内,于是你提前把堆分成了三十二份,但是因为应用的负载不是静态的,导致你每次回收一份,也会经常超出期望。 22 | 23 | 这个时候,你就会想,我这提前划分好感觉不太靠谱,能不能动态划分呢? 24 | 25 | 所以问题的根源就是怎么做到动态划分,比如说一会分成三十二份,一会分成六十四份。这个问题难在哪里?难在怎么知道不回收的部分,有哪些引用指向了被回收部分。如果直接动态划分,就没法子维护整个信息。 26 | 27 | 那么,你就会想到,我能不能先把堆划分得很细碎,比如说,我直接把堆分成1024份,每一份自己维护一下别人引用自己内部对象的信息?然后当回收的时候,我就从里面挑。比如说这次回收,预计一秒内只能回收128份,那我就挑128份出来回收掉;下一次能更惨,只能回收64份,所以我就挑64份来回收。 28 | 29 | 这就是 G1 的基本思想。 30 | 31 | 这就是抓住 G1 的核心。G1 的后面的一切,都是因为分成了那么多小份,然后每一次要挑出来一部分回收。 32 | 33 | 然后我们从这一点出发,看一下 G1 的各种奇技淫巧。 34 | 35 | 首先 Region 我们已经说过了,就是为了能够保证 GC 期间灵活选择而不得不划分的。 36 | 37 | 那么 RSet(记忆集)又是拿来干啥?用来记录别的 Region 引用本 Region 内部对象的结构体。为什么要记录?不记录的话不知道这个 Region 内部的对象是不是活着。 38 | 39 | 那么怎么理解 G1 的两种模式? 40 | 41 | 我们再考虑一下,我想要挑出来一部分 Region 来回收,我是随机挑吗?当然不是,我希望尽可能回收脏的 Region。那么什么 Region 比较脏? 42 | 43 | 显然是放着年轻代对象的 Region 比较脏。因为对象朝生夕死,所以想当然的我们会说我们优先挑年轻代的 Region 就可以了。 44 | 45 | 那么问题来了,你不能一直挑年轻代,你总要挑老年代的,不然老年代岂不是永远不回收了? 46 | 47 | 所以我们会想到,启动一个后台线程,扫描这些老年代的 Region,看看脏不脏。要是很多已经很脏了,我们就把这部分老年代的 Region 回收掉。 48 | 49 | 这就是 G1 的 Young GC、Mixed GC 和全局并发标记循环的来源了。 50 | 51 | 这里面还有几个细节,我们沿着思路继续思考下去。 52 | 53 | 首先一个,并发标记循环,意味着应用也在运行,如果我在标记过程中,引用被修改了,怎么办?这就是引入了 SATB( snapshot at the beginning)。这个名字就有点让人误解,会以为 G1 真的记了一个快照,其实不是的。简单来说,可以理解为这个机制记录了在并发期间引用发生过修改的对象。在最终标记阶段,会处理这些变更。 54 | 55 | 其次,如果我要是 Mixed GC 太慢,还没来得及回收老年代也满了,怎么办?这个问题和 CMS 是一样。那么 CMS 是怎么解决的?退化为 Serial Old GC。很显然,G1 也是一样处理的。(从这个角度来说,可以理解 Serial Old 是一个后备回收器,只要你 CMS 或者 G1 崩了,那就是它顶上) 56 | 57 | 前面我们还提到,就是要挑出脏的,那么什么才是脏的,那就是要算一下,里边活着的对象还有多少。要是一个活着的对象都没了,这个 Region 是不是可以直接回收了?都不用复制存活对象了。这就是并发循环标记最后一步,把发现的一个存活对象都没了的 Region,脏得彻底的 Region 直接收回。 58 | 59 | 还有一个点,其实算是优化,而不算是本质。就是并发标记循环会复用 Young GC 的结果。在 Young GC 的初始标记完成后,一边是 Young GC 继续下去,一边是并发循环标记。 60 | 61 | 接下来我们想,每次挑出来多少个才是合适呢?之前我们已经揭露了,静态划分是不行的,因为要根据程序动态运行来决定挑多大一块内存来回收。因此我们肯定不能用参数或者直接写死,而是要实时计算。 62 | 63 | 那么怎么计算呢?这个细节,面试基本不会考。大概的原理是考察最近的几次 G1 GC 的情况,大概推断这一次 G1 至多回收多少块。有点像是根据最近几次 GC 的情况,来猜测这一次 GC 回收每一块 Region 需要多长时间,然后算出来。核心在于,根据最近几次情况来推断。 64 | 65 | G1 的面试总体上来说不如 CMS 常见。原因很简单,对于大多数应用来说,4G 的堆就足够了。在这个规模上,G1 是并不比 CMS 优秀的。而且 CMS 因为应用得多,所以懂得原理调优的人比 G1 多。 66 | 67 | ## 面试问题 68 | 69 | ### 什么是 Region? 70 | 71 | 分析:基本概念题,可以从为什么需要 Region 的角度来作为亮点。 72 | 73 | 答案:G1 将整个内存划分成了一个个块,通过这种块,可以控制每次回收的时候只回收一定数量的块,来控制整个停顿时间(这就是引入Region的目标)。 74 | 75 | 有三类 Region: 76 | 1. 年轻代 Region; 77 | 2. 老年代 Region; 78 | 3. Humongous Region,用于存放大对象(这是一个不同于 CMS 的地方。CMS 是使用老年代来存放大对象的); 79 | 80 | ![Region](https://upload-images.jianshu.io/upload_images/2579123-b5f52615c38aa31b.png?imageMogr2/auto-orient/strip|imageView2/2/w/478/format/webp) 81 | 82 | 每一个 Region 归属哪一类并不是固定不变的(这是一个很容易让人误解的地方),也就是说,在某一个时间点,一个 Region 可能是放着年轻代对象,另一个时间点,可能放着老年代对象。 83 | 84 | (我们稍微提及 Region 内部内存是如何分配的)为对象分配内存就比较简单了,Region 内部通过指针碰撞分配内存。为了减少并发,每一个线程,会从 Region 里面申请一大块内存,这块内存叫做 TLAB(thread local allocation buffer),每一个线程自己内部再分配。 85 | 86 | #### 类似问题 87 | - 年轻代的 Region 能不能给老年代用?能,在回收清空了这个 Region之后,就可以分配给老年代了 88 | - Region 有哪几类? 89 | - Region 怎么分配内存? 90 | - 什么是 TLAB?有些面试官好像会把这个东西记成 TLB(thread local buffer) 91 | 92 | ### 什么是 CSet?Collection Set 93 | 分析:基本概念题。刷亮点落在一个很容易误解的地方 94 | 答案:在每一次 GC 的时候,G1 会挑选一部分的 Region 来回收,这部分 Region 就被称为 CSet。不过要注意的是,在 Young GC的时候,是选择全部年轻代的 Region 的,G1 是通过控制所能允许的 年轻代 Region 数量来控制Young GC 的停顿时间。 95 | 96 | (后边这一点很容易让人误解,总以为是分配了一大堆年轻代 Region,然后 Young GC 只回收其中一部分,其实并不是,而是说,当 G1 觉得我只能一次性回收 50 个年轻代的 Region,那么当分配了 50 个年轻代 Region 之后,就会触发 Young GC) 97 | 98 | ### G1 的具体步骤 99 | 100 | 分析:基本考察。如果直接问步骤,那么大概率是问 MIXED GC。但是从回答的角度,要交代清楚 Young GC 和 Mixed GC。既然谈及了 Mixed GC,就要谈到并发标记循环。最后以 Mixed GC 失败,退化为Serial Old结束。 101 | 102 | 我们会把亮点放在与 CMS 横向比较上。G1 的很多步骤,都和 CMS 是类似的。通过这种比较,我们能够看到一些这一类并发 GC 在处理一些问题上的共同点。 103 | 104 | 所以接下来的回答,但凡是涉及到了和 CMS 的部分,都可以成为亮点。 105 | 106 | 答案:G1 的具体来说,可以分成 Young GC, Mixed GC 两个部分。 107 | 108 | 1. 初始标记,该步骤标记 GC root;(什么是 GC root 可以看 [GC 算法](./algorithm.md) 109 | 2. 根区间扫描阶段:该阶段简单理解,就要扫描 Young GC 之后的 Survivor Regions 的引用,和第一步骤的 GC root,合在一起作为扫描老年代 Regions 的根,这一个步骤,在 CMS 里面是没有的; 110 | 3. 并发标记阶段 111 | 4. 重新标记阶段 112 | 5. 清扫阶段:该阶段有一个很重要的地方,是会把完全没有存活对象的 Region 放回去,后边可以继续使用。清扫阶段也有一个及其短暂的 STW,而 CMS 这个步骤是完全并发的; 113 | 114 | 在标记阶段结束之后,G1 步入评估阶段,就是利用前面标记的结果,看看回收哪些 Region。G1 会根据近期的 GC 情况来判定要回收多少个 Region,在达到期望停顿时间的情况下,尽可能回收多的 Region。 115 | 116 | 而 G1 会优先挑选脏的 Region 来回收。 117 | 118 | #### 类似问题 119 | - 并发标记循环步骤 120 | - Mixed GC 步骤 121 | 122 | ### G1 什么时候会触发 Full GC 123 | 分析:其实和 CMS 类似,核心都是在老年代尝试分配内存的时候,找不到足够的空间,就会退化为 Full GC。那么问题来了,什么时候会尝试分配对象到老年代?—— 年轻代对象晋升。这和 CMS 又不同,CMS 中还有可能是大对象直接分配到老年代。那么 G1 的大对象分配到哪里?分配到了 Huge Regions。那么万一 G1 也没有 Region 来容纳这个大对象,会不会也开始 Full GC?答案是会的。所以总结下来就是两个: 124 | 1. 分配对象到老年代的时候,老年代没有足够的内存。这基本上就是对象晋升失败; 125 | 2. 分配大对象失败; 126 | 127 | 答:主要是两个时机: 128 | 1. 对象晋升到老年代的时候,没有足够的空间来容纳,也就是并发模式失败,要进行 Full GC 129 | 2. 分配大对象的时候,没有足够的空间来容纳,也会触发 Full GC 130 | 131 | (尝试回答如何解决 Full GC,作为一个亮点)对于前者来说,要避免这种情况, 就是要确保 Mixed GC 执行得足够快足够频繁。因此可以通过调整堆大小,提前启动 Mixed GC,或者调整并发线程数来加快 Mixed GC。至于后者,则没什么好办法,只能是加大堆,或者加大 Region 大小来避免。 132 | 133 | (总结和 CMS 的相同点)基本上,G1 触发 Full GC 和 CMS 触发 Full GC 是类似的,核心都在于并发模式失败,老年代找不到空间。所不同的是 G1 有专门的存放大对象的 Region,所以这一点会稍微有点不同。 134 | 135 | ### CMS 和 G1 的区别 136 | 137 | 分析:这个问题就很宽泛,可以从多个角度去回答。 138 | 1. 从两者内存管理的角度去回答 139 | 2. 从适用场景去回答 140 | 3. 回收模式 141 | 142 | 也可以聊具体步骤上的差异。但是一般来说问这种区别,更加多是希望讨论一些特征上的差异。步骤上的差异虽然也算是差异,不过可能不太符合期望而已。 143 | 144 | 答案:CMS 和 G1 都是并发垃圾回收器,但是它们在内存管理,适用场景上都有很大的不同。 145 | 1. CMS 的内存整体上是分代的,使用了空闲链表来管理空闲内存;而 G1 也用了分代,但是内存还被划分成了一个个 Region,在 Region 内部,使用了指针碰撞来分配内存; 146 | 2. 在适用场景下,G1 在大堆的表现下比 CMS 更好。G1 采用的是启发式算法,能够有效控制 GC 的停顿时间; 147 | 3. 回收模式上,G1 有 Young GC 和 Mixed GC。Mixed GC 是同时回收老年代和年轻代; 148 | 149 | #### 类似问题 150 | 151 | - 为什么 G1 会比 CMS 更适合大堆?启发式算法能够比较准确控制停顿时间 152 | 153 | ### 在并发标记期间,G1 是怎么处理这阶段发生变化的引用? 154 | 155 | 分析:考察并发的固有问题,就是如果这个过程应用的引用发生了变化,G1 是如何处理的。在 CMS 里面,我们说了,CMS 是用卡表,也就是卡表 + 预清理 + 重标记来完成的,核心是利用写屏障来重新把卡表标记为脏,在预清理和重标记阶段重新处理脏卡。 156 | 157 | G1 里面则不同,它用的是 SATB,但是也利用了写屏障。它的处理机制,可以归结为,当引用变更的时候,会把这个变更作为一条 log 写到一个 buffer 里面。在重标记阶段重新这些 log。 158 | 159 | 这个机制,亮点可以横向对比 CMS,也有一个比较出其不意的角度,就是横向对比 Redis 的 BG save。基本上都是一样的。 160 | 161 | 先是产生一个快照,然后再把并发期间的修改丢到日志里面,在最后重新处理一下日志。 162 | 163 | 答案:G1 采用了所谓的 SATB。G1 利用写屏障,会把并发标记期间被修改的引用都记录到一个 log buffer 里面,再重标记阶段重新处理这个 log。 164 | 165 | (和 CMS 对比)这个机制和 CMS 是比较像的,差别在于 CMS 是把内存对应的卡表标记为脏,并且引入预清理阶段,在预清理和重标记阶段处理这些脏卡。 166 | 167 | (和 Redis BG save 对比,抽象一下) 这种 snapshot + change log 的机制,在别的场景下也能看到。比如说在 Redis 的 BG Save 里面,Redis 在子进程处理快照的时候,主进程也会记录这期间的变更,存放在一个日志里面。后面再统一处理这些日志。 168 | 169 | -------------------------------------------------------------------------------- /gc/img/cms_gc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/gc/img/cms_gc.png -------------------------------------------------------------------------------- /gc/img/gc_roots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/gc/img/gc_roots.png -------------------------------------------------------------------------------- /gc/java_cms.md: -------------------------------------------------------------------------------- 1 | # CMS 垃圾回收器 2 | 3 | ## 总览 4 | 分析:首先要从概念上捋清楚,CMS 是 hotspot 虚拟机上的一个实现,并不是某种算法。它使用的算法是标记-清扫。但是它有一个特殊的模式,就是可以开启压缩,那么会在回收的时候清扫之后再压缩内存,减少内存碎片。这就是所谓的标记-清扫-压缩算法。 5 | 6 | CMS 的面试,大多数时候来说,就是面一下算法的大概流程,并不会涉及特别多的细节——这些细节真的真的很多。 7 | 8 | 那么我们在面试 CMS 垃圾回收器的时候,要怎么刷出亮点?就是补充细节。 9 | 10 | 例如,大多数人只能说出,CMS 有初始标记-并发标记-再标记-清扫四个步骤。那么你可以补充预清理阶段。 11 | 12 | 这种补充细节是有很多个维度的。 13 | 14 | 另外一个刷亮点的地方,就是补充正向的内存分配。说清楚 CMS 使用了空闲链表来管理空闲内存,也使用了 TLB(Thread local buffer) 来分配内存。 15 | 16 | 另外一些面试题就是针对某个步骤来抠细节,或者针对使用过的数据结构来抠细节。 17 | 18 | 此外就是要熟练掌握一些 CMS 的控制参数,这些参数会影响CMS的效果和性能。本质上来说,这一类的问题,属于CMS的调优范围。 19 | 20 | 但是大体上来说,能够完全背下来流程,那么通过 CMS 垃圾回收器其实是不难的。 21 | 22 | ## 面试题 23 | 24 | ### 你了解 CMS 吗? 25 | 26 | 分析:基础题。这道题基本上面试官就是指望你回答出来 CMS 的基本特征,基本流程。 27 | 28 | 基本流程我们采用六步来回答,在一般意义上的四步里面引入额外的两步,并发预清理和并发重置。 29 | 30 | 并发预清理位于并发标记和最终标记之间。 31 | 32 | 并发重置则是最后一个步骤。 33 | 34 | 35 | 回答:(先回答基本特征)CMS 是一个基于标记-清扫(这是一个钓鱼的说法,因为面试官可能会怀疑你不知道它的压缩过程)算法的并发垃圾回收器,主要用于老年代回收。(这里就会引来其它问题,比如年轻代怎么回收,年轻代的回收和 CMS 是如何合作的) 36 | 37 | (回答基本流程)CMS 的基本流程可以分成六个步骤(同样,这个步骤也没涉及到压缩步骤,是因为压缩步骤是一个可选的步骤,这里我们可以等面试官来问): 38 | 1. 初始标记:主要是扫描 GC root (点明 GC root,这样面试官可能会进一步问你 CMS 里面的 GC root 有哪些),这个过程是 STW 的; 39 | 2. 并发标记:这个过程应用程序可以继续运行; 40 | 3. 并发预清理:该步骤主要是为了处理在并发标记阶段应用线程修改过的引用,减少后面重新标记的停顿时间; 41 | 4. 重新标记:STW 的,将并发阶段修改过的引用进行校正; 42 | 5. 并发清理:主要就是将空闲内存还给 CMS 的空闲链表(这里我们提及了空闲链表,是为了引来相关的问题)。(后面这一段,是点明一个问题,因为很多面试官其实不了解这个,不用指望他会问)如果在这个阶段,又有对象被分配到老年代,那么会被放到特定的链表的位置,因而不会被回收。 43 | 6. 并发重置:重置GC阶段使用的数据结构,以备下一次使用(这里我们没说有什么数据结构,也是因为如果面试官不了解就不会问,问了就可以继续说) 44 | 45 | (注意,我们这里额外讨论了两个阶段,并发预清理和并发重置。其中并发预清理是重点,所以我们在回答的时候指出了引入这个阶段的目的,是为了减少重新标记的停顿时间) 46 | 47 | #### 类似问题 48 | - CMS的执行步骤? 49 | - CMS使用了什么算法? 50 | - CMS 有几次停顿?是哪几次? 51 | 52 | (下面我们分不同的阶段来看看面试官会怎么问) 53 | 54 | ### CMS 的 GC root 有什么? 55 | 分析:之前在[总览](./algorithm.md)那里我们讨论过 GC root 大概有些什么。这里则是进一步细化了,在CMS这个回收期下,GC root 有一些什么。 56 | 57 | 其实我们大概都能够猜到主要的几个,比如说线程本身,线程栈上引用,本地方法栈上引用。然后还有一些稍微细想大概也能猜到的,比如说加载到的类,它持有的静态对象。 58 | 59 | 按照 Eclipse 的文档,它划分得非常详细: 60 | 61 | ![gc root 详细](./img/gc_roots.png) 62 | 63 | 我们先分个类: 64 | 1. Java 线程相关的:Java线程本身,Java的线程栈 65 | 2. 本地方法相关的:本地方法栈上变量,本地方法全局变量,本地方法局部变量 66 | 3. 类相关:系统类,类的静态变量 67 | 4. finalize 相关:finalize 是 JVM 管理的,所以也是 gc root 68 | 5. 同步相关:处于同步阻塞的对象。因为 JVM 的实现基本上都是把阻塞队列丢到一个本地(NATIVE)队列里面,所以它也不得不成为一个 gc root; 69 | 6. 其它 70 | 71 | 其实这几个子类,稍微想想都能理解,为什么它们会成为 gc root。 72 | 73 | 回答这个问题,入门是要答出1,2;进阶是要答出3,4,5。 74 | 75 | 所以亮点落在后面三个。 76 | 77 | 还有一个更加进阶的高级回答,是回答 CMS 如何处理年轻代指向老年代的引用。之前我们说过,跨代引用也会成为 gc root。比如说在 CMS 和 Parallel New 的组合之下,如果触发年轻代 GC,那么老年代指向年轻代的引用,是被记录在记忆集(卡表是它的实现)里面的,在 GC 启动的时候要扫描记忆集,找出这种跨代引用。 78 | 79 | 但是 CMS 是没有记录年轻代指向老年代的引用的。这意味着,在 CMS 启动的时候,它必须扫描整个年轻代才能找到所有年轻代指向老年代的引用。 80 | 81 | 这听起来就很可怕。但是考虑到我们有年轻代 GC,所以实际上 CMS 会等一段时间,如果这段时间出发了年轻代 GC,那么 CMS 扫描整个年轻代就变成了只需要扫描 Survivor 了。 82 | 83 | 所以我们的回答分成三段,依次递进。 84 | 85 | 答案:gc root 有很多。(先回答最常见的,线程和本地方法)最常见的就是线程本身和线程栈上对象(这是指引用),与之类似的是本地方法相关的,包括本地方法栈上对象,本地方法全局变量;(回答后面三种比较人知道的)而后还有系统类和类的静态变量,finalize 相关的,以及处于同步状态的对象。(一般是不需要解释为什么这三种也是 gc root,面试官如果没有提前了解的话,他也一时想不到为啥) 86 | 87 | (高高级,讨论跨代)总而言之, gc root 可以理解为,所有的指向回收区域的外部引用。 88 | 89 | 比如说年轻代回收,需要扫描卡表找到老年代指向年轻代的引用;G1垃圾回收需要找到指向 Region 的外部引用。 90 | 91 | (重点描述 CMS,当然如果你们之前的话题是G1,那就是重点描述G1,依次类推)而 CMS 则比较不一样,它因为没有记录年轻代指向老年代的引用,所以 CMS 需要扫描整个年轻代才能找到跨代引用。因此,CMS 会在准备启动 GC 之前等待一小段时间,看看这段时间内是否发生了年轻代 GC。如果发生了,那么 CMS 就只需要扫描 Survivor 区了。(这里也是埋伏笔,因为后面 GC 还可能问到,为什么我们要设置一些参数,以迫使 CMS 期间触发一些年轻代 GC) 92 | 93 | (再进阶)该参数是`XX:CMSWaitDuration`,一般设置为覆盖一个年轻代GC周期。但是设置过长可能导致老年代空间完全耗尽。(这里我们只说完全耗尽,然后看面试官会不会问 promotion failed 之类的问题,他可能进一步会询问这会出现什么问题,但是这里,我们就暂时停下来,参考后面的**CMS 来不及回收老年代会发生什么事情**) 94 | 95 | #### 类似问题 96 | - CMS 如何处理年轻代?年轻代是用 Parallel New 来回收的,CMS 会利用年轻代 GC的结果来减少初始标记和再标记两个停顿过程 97 | 98 | ### CMS 并发标记阶段,如果引用发生了变更会怎样? 99 | 100 | 分析:这个要分开来说。首先 CMS 是只回收老年代和永久代的,那么年轻代对象之间的引用变更,CMS 是不会管的。因为后面 CMS 在最终的重标记阶段,还要重新扫描年轻代,所以这个阶段不用管。 101 | 102 | 而其它引用变更,大体上又分为两类,一个是年轻代引用指向老年代的引用变更,一个是老年代内部之间的引用变更。无论是这两种情况的哪一种,CMS 只会把内存标记为 Dirty,后面预清理阶段会进行处理。 103 | 104 | 答案:如果是年轻代之间对象应用发生变更,那么 CMS 在这个阶段不会做什么(这里引导面试官问你,最终 CMS 是在哪里处理的)。否则,CMS 会把内存标记为 Dirty,在预清理阶段和重标记阶段再次处理。 105 | 106 | #### 类似问题 107 | - CMS 并发标记阶段,创建了新对象,会发生什么?什么也不会发生,因为 CMS 不管年轻代,只是在后面重标记阶段重新标记年轻代对象; 108 | - CMS 并发标记阶段,如果老年代引用发生了变化,会发生什么?CMS会重新把这块内存标记为 dirty,预清理和重标记阶段再处理一遍; 109 | - CMS 并发标记阶段,如果年轻代指向老年代的引用发生了变化,会发生什么?和上面一样,把这块内存标记为脏,预清理和重标记阶段再来一遍; 110 | 111 | ### CMS 的预清理阶段做了什么? 112 | 113 | 分析:这个阶段看起来比较花里胡哨,具体来说,它可以是单独的预清理(Pre-clean)阶段,也可以是指预清理和可中断预清理两个阶段。 114 | 115 | 预清理的目标是降低停顿时间。它是为了解决并发标记阶段引用变化问题而引入的。从前面也可以看出来,CMS 一个重要的特征就是并发标记阶段,会发生应用变更。非年轻代的引用变更,CMS 会把内存标记为 Dirty。 116 | 117 | 如果我们考虑一下,要是没有预清理阶段,那么重标记阶段就需要全部 Dirty 的内存再标记一遍,如果应用很繁忙,那么可能很多内存都需要重新标记。 118 | 119 | 因此引入了并发的预清理阶段,这一个阶段就是处理这些 Dirty 的内存块。然后发现,这个预清理也是并发的,如果这个时候也有引用变更,岂不是又产生了新的 Dirty 内存块?是的,但是因为这个过程比较快,所以理论上产生的 Dirty 块要比较少。后面的可中断的预处理阶段,也是这么一个过程,但是为了避免无线套娃,所以它是可中断的。 120 | 121 | 答案:预清理阶段主要是处理在并发阶段标记为 Dirty 的内存块(具体来说,是卡表里面的卡被标记为 Dirty)。提前处理了这些 Dirty 内存块,那么重标记阶段就可以少处理一点内容,减轻停顿时间。 122 | 123 | 预清理阶段有一个可中断的预清理。该阶段是不断循环,循环内的步骤类似预清理阶段,不过除了处理 Dirty 的内存块,还会扫描年轻代,找到指向老年代的引用。 124 | 125 | (进阶,讨论中断的条件)这个阶段是可以被中断的,中断的条件有三个: 126 | 1. 循环次数达到阈值; 127 | 2. 执行时间达到了阈值; 128 | 3. 新生代的 Eden 的内存使用率达到了阈值; 129 | 130 | (这三个分别是三个参数控制,不过不太重要,不需要记住参数名字,前两个很好理解容易记忆,后一个记不住也没关系) 131 | 132 | #### 类似问题 133 | - 什么是预清理? 134 | - 什么是可中断预清理? 135 | - 什么时候会中断预清理? 136 | - 为什么要中断? 137 | 138 | ### 重标记阶段,做了什么? 139 | 140 | 分析:重标记阶段主要是找出来并发阶段修改的引用,再一次处理一遍。 141 | 142 | 所以重标记不得不重新扫描 GC root、年轻代和 Dirty 内存块。 143 | 144 | 注意的是,重标记不会重新完全标记一遍,而是尽量缩小了标记范围。 145 | 146 | 答案:重标记阶段主要是为了处理并发阶段发生变更的引用。该阶段主要是重新扫描 GC root,年轻代和 Dirty 块。 147 | 148 | (我们稍微讨论一下重标记停顿时间来源)大多数时候,该阶段的停顿时间主要是由扫描年轻代和 Dirty 造成的。因此为了降低这个阶段的停顿时间,我们可以调整预清理的参数,尽可能多在预清理阶段处理完,也可以开启参数`CMSScavengeBeforeRemark`,强制在重标记之前执行一次年轻代GC,以降低GC时间。 149 | 150 | #### 类似问题 151 | - 怎么降低重标记的停顿时间 152 | - 为什么要在重标记之前执行一次年轻代GC 153 | 154 | ### CMS 来不及回收老年代会发生什么事情 155 | 156 | 分析:这其实考察的是 CMS的一个异常情况。回忆一下,什么时候会触发 CMS?除了第一次以外,其他时候都是JVM觉得应该GC了,就会触发GC。它会控制住GC频率,确保 CMS GC 发生期间,不会出现老年代空间不足的问题。 157 | 158 | 但是很显然,总有意外,于是就会出现 concurrent mode failure. 比如耳熟能详的 promotion failure。 159 | 160 | 那么在这种情况下,会出现什么问题呢? 161 | 162 | 当然就是整个 CMS 会失败,退化为串行GC。它使用的是古老的 Serial Old 来GC,整个过程都是 STW 的。 163 | 164 | 答案:会出现 concurrent mode failure(并发模式失败),于是退化为 Serial Old 来GC,GC 时间会猛增。 165 | 166 | (接下来我们讨论一下为什么会出现这种情况,作为进一步解释,亮点一。这也是一个单独可能出现的面试题)一般出现是两种情况,年轻代 GC 提升对象失败,或者超大对象直接分配在老年代失败。更深层次的原因可能是年轻代 GC 太频繁,导致对象被迅速提升到老年代,也可能是老年代碎片太严重,导致找不到足够大的连续内存来容纳对象。 167 | 168 | (接下来讨论解决方案,亮点二。这也本身就是一个面试题)我们需要尽量避免这种出现情况: 169 | 1. 大多数时候,最简单的方案是增加堆的大小; 170 | 2. 其次我们可以调整老年代和年轻代的比例,但是调大老年代的比例意味着年轻代GC更加频繁,治标不治本; 171 | 3. 让 CMS GC的时候整理内存,即触发压缩过程。即设置`-XX:UseCMSCompactAtFullCollection`和`-XX:CMSFullGCBeforeCompaction=5`参数。这个过程会导致更长的STW时间(压缩的过程是STW的);同时`CMSFullGCBeforeCompaction`这个参数,过小会导致频繁的内存压缩,性能很差; 172 | 4. 设置`-XX:CMSInitiatingOccupancyFraction=70`和`-XX:+UseCMSInitiatingOccupancyOnly`参数,即让 JVM 永远在使用量达到我们设置的阈值的时候就开始CMS(这里是70%) 173 | 174 | #### 类似问题 175 | - 如何解决 CMS 内存碎片?还能咋的,只能是开启压缩了。主要是解释`CMSFullGCBeforeCompaction`参数过大过小会有什么问题; 176 | - `CMSInitiatingOccupancyFraction`参数有什么作用?告诉JVM**第一次**CMS在使用了多少内存后开始GC,如果配合`UseCMSInitiatingOccupancyOnly`则是让JVM永远使用我们设置的比率,而不必自适应计算 177 | - 什么是 concurrent mode failure?有什么后果 178 | - 什么是 promotion failure?有什么后果 179 | - 过早 promotion 会有什么后果?容易导致 Full GC,容易导致 promotion failure 180 | 181 | ### CMS 为什么会有内存碎片 182 | 183 | 分析:综合考察内存正向分配和GC回收。这道题还是比较有水平的。首先要先揭示CMS的内存管理,核心就是两点:标记清扫和空闲链表。 184 | 185 | 我个人认为是标记清扫这种特性决定了空闲链表这种管理空间内存的方式。 186 | 187 | 答案:原因在于 CMS 使用了标记-清扫的内存回收算法,和空闲链表的内存管理方式。每一次 CMS GC 之后,幸存的对象会把连续内存划分成一段段, 188 | 189 | ![空闲内存被分成一段一段](./img/cms_gc.png) 190 | 191 | 多次 GC 之后,就会导致所有的空闲内存都很小,以至于明明还有很多空闲内存,但是却找不到任何一块足够大的内存来存放新对象。这就是内存碎片。 192 | 193 | 194 | (其实这里还有一种可能,是面试官问的其实是并发过程中产生的浮动碎片,其实就是该被回收的没被回收,但是大多数时候,是指这个清扫导致的内存碎片化) 195 | 196 | (接下来我们讨论解决方案,在前面提到过,就是`UseCMSCompactAtFullCollection` 和 `-XX:CMSFullGCBeforeCompaction` 参数)我们可以通过设置`UseCMSCompactAtFullCollection` 和 `-XX:CMSFullGCBeforeCompaction`参数来控制 CMS 执行压缩,并且控制在多少次 GC 触发一次压缩。 197 | 198 | 199 | #### 如何引导 200 | - 讨论到正向的内存管理 201 | - 讨论到操作系统内存管理 202 | - 讨论到指针碰撞 203 | - 讨论到为什么要压缩 204 | 205 | #### 类似问题 206 | - 为啥老年代明明还有空闲内存,却触发了 Full GC?碎片太多,找不到足够大的连续的空闲内存;又或者 CMS 预估到接下来这点空闲内存不够了,需要提前触发GC(时间间隔轮询做这种判断) 207 | - CMS 为什么要压缩 208 | 209 | ### 什么时候会触发 Full GC? 210 | 211 | 分析:考察触发 Full GC的点。在 CMS的语境下,则是考察 CMS 触发的时机。一般人的回答都是从promotion failed这种角度,虽然也对,但是不够完整,严格意义上来说,CMS 的触发,有两种方式,一种是定时轮询,判断要不要 GC(这种其实严格来说,不是 Full GC,是 Major GC。不过国内的语境之下,经常会把 Major GC 和 Full GC 混为一谈);一种就是刚才提到的,`promotion failed` 这种不得不触发的时机。前者我们叫做主动触发,后者叫做被动触发。 212 | 213 | 主动触发的面试亮点在于,要阐述清楚,CMS 是如何轮询的,有什么参数可以控制;被动触发则是列举场景。 214 | 215 | 主动式轮询,有一个参数控制时间间隔。而后,老年代使用率达到阈值,触发GC;开启了回收永久代的时候,永久代使用率达到阈值,也触发GC。 216 | 217 | 被动触发其实很容易记住,就是对象尝试分配到老年代的时候,如果发现老年代找不到一个内存来放对象,那么就会触发GC,这个时候都是退化为串行的 Serial GC。 218 | 219 | 答案:CMS 触发时机有两种,主动触发和被动触发。 220 | 221 | 主动触发是指,CMS 会轮询,判断当前是否需要 GC。(基本回答)CMS 会在老年代内存使用达到一个阈值的时候,开始 GC;又或者在开启了回收永久代的时候,永久代内存使用率触发阈值,触发GC。(进阶)还有两个参数`CMSInitiatingOccupancyFraction` 和 `UseCMSInitiatingOccupancyOnly` 都设置了的话,那么 CMS 的阈值就会使用设置的值,否则是使用默认值。(据我了解是会动态调整) 222 | 223 | 被动触发,核心在于对象分配到老年代的时候找不到空闲内存来容纳(这时候,可能是充满了碎片,所以没有连续的足够大的内存来容纳,也可能是确实是都用完了)。而对象分配到老年代,就是晋升失败,或者大对象直接分配失败。 224 | 225 | (后面可以将话题引导到 promotion failed 的后果,就是面试题**CMS 来不及回收老年代会发生什么事情**) 226 | 227 | #### 类似问题 228 | - CMS 是否回收永久代?CMS 可以用于回收永久代,但是参数得配置开启 229 | - CMS 什么时候会退化为 Serial GC? 230 | 231 | #### 如何引导 232 | - 反正讨论到了 CMS,Full GC 都可以聊这个 233 | - 可以结合 G1 垃圾回收器,来对比两者的触发时机 234 | 235 | ### CMS 采用的空闲链表方式管理内存,存在什么缺点? 236 | 237 | 分析:本质上,这应该算是内存管理的面试题,因为别的,例如操作系统,如果也采用了空闲链表,那么也会有这个问题。 238 | 239 | 就是碎片和内存分配低效两个问题。 240 | 241 | 答案:主要有两个方面,内存碎片和内存分配低效。 242 | 243 | (如果之前没有聊过内存碎片,这里可以补充一下什么叫做内存碎片)CMS 的内存管理方式会导致这么一个问题,内存被分割为一个个零散的小块,这些小块可能太小,以至于无法容纳任何对象,导致内存利用率低。 244 | 245 | 而空闲链表这种管理方式,导致内存分配的时候,需要查找可用内存,然后再分配,效率极低。 246 | 247 | #### 类似问题 248 | - 为什么CMS分配内存比较慢 249 | - CMS用了指针碰撞技术吗?显然没有 250 | 251 | #### 如何引导 252 | - 聊到空闲链表都可以说 253 | 254 | ### CMS 有什么缺点 255 | 256 | 分析:CMS 的缺点是极为明显的。最容易想到的就是内存碎片问题,不过很少有人会回答道内存碎片带来的影响。 257 | 258 | 另外就是 CMS 会出现并发模式失败的可能,也就是退化为 Serial GC 的可能性。 259 | 260 | 最好就是 CMS 的停顿时间,比 G1 之类的长。它的两次标记,初始标记和重标记,都和年轻代存活对象有关,因此为了避免 CMS 的停顿时间,我们会尝试开启一个参数: 261 | `CMSScavengeBeforeRemark` 在重标记之前执行依次年轻代 GC,这样能显著降低 GC 停顿时间。也因此 CMS 不适用于大堆(有些人说 6G 以上,有些人说 8G 以上就不要用 CMS了,我觉得是跟应用特征相关) 262 | 263 | 答案:CMS主要有三个缺点: 264 | 1. 内存碎片。这导致 CMS 内存使用率比较低(总有一些碎片无法被利用),(补充空闲链表,作为亮点)而且 CMS 使用了空闲链表来管理空闲内存,导致内存分配相比指针碰撞,要慢很多; 265 | 2. CMS 存在并发模式失败的可能,这会导致 CMS 退化为 Serial GC,性能极差;(这里可能面试官问什么时候会导致并发模式失败) 266 | 3. CMS 不适用大堆。CMS 的停顿时间相比 G1 还是比较长的,(补充一下重标记的问题,引导面试官问重标记问题)我们可以通过开启`CMSScavengeBeforeRemark`来确保重标记之前,能够触发一次 Young GC,来减轻 CMS 的停顿时间 267 | 268 | #### 如何引导 269 | - 讨论到并发模式失败 270 | - 讨论到重标记 271 | - 讨论到空闲链表 272 | 273 | #### 类似问题 274 | - CMS 适合什么场景 275 | 276 | ## Reference 277 | [Oracle documents - CMS](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html) 278 | [Java gc roots](https://help.eclipse.org/2021-06/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html&cp=37_2_3) 279 | [CMS](https://juejin.cn/post/6844903740864987149) -------------------------------------------------------------------------------- /golang/README.md: -------------------------------------------------------------------------------- 1 | # Go 语言 2 | 3 | ## 面试题 4 | 5 | ### Go 语言时间精度问题 6 | 7 | 在 Go 语言里面,和时间相关的中间件或者框架都需要注意时间精度的问题。 8 | 9 | 我们用阻塞超时的例子来给大家描述时间精度可能源自哪些方面。例如说,我们在使用并发等待队列的时候,入队的超时时间设置了一秒,那么你究竟什么时候会收到超时错误,是不确定。这是因为: 10 | - 时钟精度。时钟并不能在恰好一秒的时候就发出时钟中断; 11 | - 阻塞:如果 goroutine 本身已经被阻塞了,那么从收到时钟中断,到唤醒 goroutine,到返回超时错误,需要一段时间。在 Go 里面还多了一个开销,就是返回超时错误意味着 goroutine 需要被调度到 P 上面执行。但是如果这个时候 P 忙着处理其它 goroutine,那么这个 goroutine 还需要等待 P 的调度(虽然 goroutine 本身是处于一种可以被调度的状态)。而造成阻塞的原因有很多: 12 | - 和 channel 交互,例如发数据到满了的 channel,或者从一个空的 channel 里面接受数据 13 | - 竞争锁,别的 goroutine 持有锁一直没有释放,那么 goroutine 就会一直被阻塞 14 | 15 | -------------------------------------------------------------------------------- /golang/goroutine.md: -------------------------------------------------------------------------------- 1 | # goroutine —— 协程 2 | 3 | ## 面试题 4 | 5 | ### 进程、线程和协程的不同? 6 | 7 | 分析:这种很明显的是一种逐步演化的路径。也就是`进程-线程-协程`总体而言可以看做是一种演化路线。 8 | 9 | 审视这个演化,就会发现它是朝着更轻量的方向演进的。于是结合需求和计算机的发展,就会发现:业务越来越复杂,计算机越来越强大,但是我们需要的确是越来越细粒度的资源分配。 10 | 11 | 进程演进到线程,共享了内存,但是线程可以被CPU单独调度;线程到协程,内存使用量更少了,多个协程绑定到一个线程,相当于大家平分了这个线程的 CPU。 12 | 13 | 以上这一段吹牛,如果面试记得,可以跟面试官聊。不记得就算了。 14 | 15 | 答案:(首先是标准答案) 16 | 1. 进程是资源分配的最小单位,而线程是 CPU 调度的单位,一个进程可以有多个线程。因为同一个进程内的线程共享了堆内存,所以在经常会引起并发编程问题; 17 | 2. 协程比线程更轻量级。线程的创建和销毁、调度还需要陷入到内核中,而协程可以认为完全是依赖于用户空间创建、销毁和调度的。同时协程相比线程,占据的资源更加小。 18 | 19 | (其次,开始引申)目前来说,很多语言都开始尝试支持协程,主要是因为现在的很多业务都是短平快,或者是 IO 密集的,相比之下,线程也过于重了。 20 | 21 | 最有名的就是`goroutine`(实际上也不是最有名吧,不过都面试 go 了,就说最有名了),此外还有`kotlin`协程,`python`的协程。 22 | 23 | ### 类似问题 24 | - 为什么要引入协程?看后面**为什么引入协程**,其实这两个问题基本上一样; 25 | 26 | 27 | ### 为什么要引入协程? 28 | 29 | 分析:`goroutine` 的引入,本质上还是为了规避一个问题:我又想有并发,但是我又不想陷入到内核里面去。于是就有了这个 `goroutine` 的东西。 30 | 31 | 该面试题的核心,就是协程“轻量”。这种轻量体现在两方面: 32 | 1. 所需要的资源更少 33 | 2. 创建销毁和调度更轻量,并不需要陷入内核 34 | 35 | 其实个人看法是大规模应用协程随着需求和计算机性能发展而来的大趋势。需求上,要求我们有更高的并发。而大多数并发执行的任务都是短平快,单独一个线程划不来,即便是使用线程池,也会带来频繁切换上下文的问题;而计算机性能提高,使得我们可以将整个计算机资源划分为更细粒度进行分配,两者叠加就是协程的出现。这一段算是个人体会,面试慎用。 36 | 37 | 答案:很简单,因为我们需要一个更轻量的东西来取代线程。(开始聊自己理解的起源)当前绝大多数系统处理的任务都是非常短平快的,或者是 IO 密集这种频繁触发上下文切换的任务。这导致我们如果使用线程,就不得不面临线程频繁切换,陷入内核的问题——这是一个极大的开销。 38 | 39 | 因此我们需要一个比线程更加轻量的东西。这个东西要具备两个特征: 40 | 1. 占有的资源小——我们都是小任务,不需要那么多资源; 41 | 2. 创建销毁和调度消耗少——小任务,还时常阻塞,所以调度一定要快要轻; 42 | 43 | 结合在一起就是`goroutine`了。 44 | 45 | #### 类似问题 46 | - 为什么有了线程池还是要有`goroutine`:线程池只是减轻了创建和销毁的开销,但是线程本身还是占有很多资源,上下文调度依然很重 47 | 48 | ### 怎么避免`goroutine`泄露? 49 | 50 | 分析:如果你不知道`goroutine`什么时候会结束,就不要使用`goroutine`。这是核心原则。讲完这个原则之后,可以讲一些如何做到“知道goroutine”何时结束。 51 | 52 | 大体上就是两个方向: 53 | 1. 超时控制 54 | 2. 信号通知。这一步基本上就是利用`channel` 55 | 56 | 这两个方向,基本上都离不开要使用`select`来配合。 57 | 58 | 然后刷两点可以回答”如何发现`goroutine`泄露“。 59 | 60 | 答案:避免`goroutine`泄露的核心原则是"Never start a goroutine without knowning when it will stop"(用英文会显得你比较专业,记不住可以替换为对应的中文)。 61 | 62 | 归根结底,就是要有办法控制住结束掉自己开启的`goroutine`。大体上有两类做法: 63 | 1. 超时控制,主要利用`context.Timeout`的特性; 64 | 2. 主动发信号给`goroutine`关闭。一般是要利用到`channel`的特性; 65 | 66 | 两种做法基本都要配合`select`特性来。要么是业务正常结束,退出`goroutine`,要么是超时,或者收到关闭信号,异常退出。 67 | 68 | (开始讨论如何发现`goroutine`泄露)如果`goroutine`都不是自己开启的,那肯定是没得办法了。只能通过`runtime.NumGoroutine()`方法监控`goroutine`的数量来判断有没有泄露。如果`goroutine`一直在上涨,而且数量也很多,说明泄露很严重。而如果只是轻微泄露,比如说一万个`goroutine`里面泄露了十个,是很难看出来的。 69 | 70 | (后面可以进一步引申,跳到**`goroutine`泄露的典型场景**) 71 | (这个问题也可以针对`goroutine`泄露的典型场景来回答,比如说小心使用`channel`,正确使用`mutex`,防止业务一直阻塞等,不过略等于啥也没说) 72 | 73 | #### 类似问题 74 | - 如何发现`goroutine`泄露了 75 | 76 | ### `goroutine`泄露的典型场景 77 | 78 | 分析:这个问题答案来自煎鱼大佬的文章[跟读者聊 Goroutine 泄露的 N 种方法,真刺激!](https://blog.csdn.net/EDDYCJY/article/details/115535237) 79 | 80 | PS:煎鱼大佬的文章都很浅显易懂,即便是难题也能说得很容易理解,大家可以多读读,他有一个公众号《脑子进煎鱼了》,可以关注。 81 | 82 | 记住,至少背下来里面的一个例子,防着面试官让你手写一个`goroutine`泄露的例子。 83 | 84 | 然后面试官让你看一段代码,如果有锁,就要怀疑死锁;如果有`channel`就要怀疑`goroutine`泄露; 85 | 86 | 其实`channel`的代码坑极多,在`channel`里面进一步讨论。 87 | 88 | 答:有: 89 | - `channel`发送不接收 90 | - `channel`接收不发送 91 | - `nil channel` 92 | - 慢等待 93 | - 互斥锁忘记解锁 94 | - 同步锁使用不当 95 | 96 | (前面这种回答太干了,你可以加点例子来刷亮点)例如在 RPC 调用,或者数据库查询里面,如果没有设置超时时间,那么大量 goroutine 会被阻塞,直到拿到响应。 97 | 98 | (同样聊排查作为亮点)排查主要用`runtime.NumGoroutine`或者`pprof`工具。`pprof`会返回所有带有堆栈跟踪的`goroutine`列表。 99 | 100 | #### 类似问题 101 | - 可以写一个 `goroutine`泄露的例子吗? 102 | - 或者面试官给你一段代码,让你看有什么问题 103 | 104 | ## Reference 105 | [跟读者聊 Goroutine 泄露的 N 种方法,真刺激!](https://blog.csdn.net/EDDYCJY/article/details/115535237) 106 | -------------------------------------------------------------------------------- /golang/img/lock_competition_two_ways.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/golang/img/lock_competition_two_ways.png -------------------------------------------------------------------------------- /golang/img/lock_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/golang/img/lock_pattern.png -------------------------------------------------------------------------------- /golang/mutex.md: -------------------------------------------------------------------------------- 1 | # Mutex 2 | 3 | ## 面试题 4 | 5 | ### `mutex`是如何加锁的 6 | 7 | 分析:也是考察`mutex`的实现原理。基本上就是围绕`自旋-FIFO`来说的。简单理解就是,`mutex`先尝试自旋,自旋不行就所有`goroutine`步入`FIFO`,先到先得。 8 | 9 | 在大多数的锁实现里面——不仅仅是 Go 的 mutex 都是有套路的: 10 | 11 | ![](./img/lock_pattern.png) 12 | 13 | 所以基本上就是两步: 14 | - 自旋加锁,所谓的自旋也就是 CAS 操作,将锁从无锁状态修改为加锁状态。自旋这个过程一般可以可通过控制自旋的次数或者时长来控制; 15 | - 自旋失败之后就进入队列,等待释放锁的时候被唤醒; 16 | 17 | 但是 Go 有一点特殊,即 Go 有所谓的正常模式和饥饿模式。为了理解这个问题,要先看这么一个问题: 18 | ![](./img/lock_competition_two_ways.png) 19 | 20 | 如果锁此时已经被释放了,那么你作为一个设计者,你会把锁给谁? 21 | 22 | - 给 G2:毕竟我们要保证公平,先到先得是规矩,不能破坏 23 | - G1 和 G2 竞争:保证效率。G1 肯定已经占着了 CPU,所以大概率能够拿到锁 24 | 25 | 所谓的正常模式,就是 G1 和 G2 竞争的模式。这种模式下,G1 拿到锁的概率远远大于 G2,因为此时 G1 是占据着 CPU 的。从这个角度来说,这也是为什么 Go 设计成 G1 和 G2 竞争的模式,因为这样可以避免 goroutine 的调度。 26 | 27 | 那么问题就成了,G2 可能等来等去一直抢不到锁,每次都被新来的抢走锁。为了解决这个问题,Go 就引入了所谓的饥饿模式。在饥饿模式下,锁必然会被交给 G2。 28 | 29 | 当等待队列为空的时候,或者 G2 等待的时间不够 1ms,就退出饥饿模式。 30 | 31 | 答案:`mutex`加锁大概分成两种模式: 32 | 1. 首先 mutex 会尝试自旋直接加锁。如果自旋失败,那么 goroutine 就会被加入到等待队列里面等待唤醒; 33 | 2. 如果队头 goroutine 被唤醒,那么要看此时 mutex 的状态。如果此时 mutex 处于饥饿状态,那么锁会别直接移交给队头 goroutine;如果此时是正常状态,那么队头 goroutine 和最新的请求锁的 goroutine 进行竞争 34 | 3. 在正常模式下,因为最新请求锁的 goroutine 此时占据着 CPU,那么它大概率能够拿到锁,那么队头的 goroutine 等待 1ms 都拿不到锁,mutex 就会进入饥饿状态 35 | 4. 等到队列中的 goroutine 都被清空了,或者队头等待时间不够 1ms,就会退出饥饿状态 36 | 37 | (讨论一下公平性的问题)所以从严格意义上来说,它并不是公平锁,因为在正常状态下,一个新的请求锁的`goroutine`和等待的`goroutine`一起竞争锁。而严格意义的公平应该是永远遵循 `FIFO`。 38 | 39 | 40 | #### 类似问题 41 | - Go mutex 的两种状态? 42 | - 什么是饥饿状态? 43 | - 什么时候进入饥饿状态,什么时候退出饥饿状态? 44 | - mutex 是不是公平锁?显然不是 45 | 46 | 47 | ## Reference 48 | [mutex is more fair](https://news.ycombinator.com/item?id=15096463) 49 | [这可能是最容易理解的 Go Mutex 源码剖析](https://segmentfault.com/a/1190000039855697) 50 | -------------------------------------------------------------------------------- /img/dingtalk_group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/img/dingtalk_group.jpg -------------------------------------------------------------------------------- /img/good_job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/img/good_job.png -------------------------------------------------------------------------------- /microservice/README.md: -------------------------------------------------------------------------------- 1 | # 微服务面试题 2 | 3 | - [超时控制](timeout.md) 4 | - [可用性](availability.md) 5 | - [其它](general.md) -------------------------------------------------------------------------------- /microservice/availability.md: -------------------------------------------------------------------------------- 1 | # 微服务可用性 2 | 3 | 微服务可用性这个部分,也就是通俗意义上说的服务治理部分。大体上在一般人的概念里面,它主要包括: 4 | - 熔断 5 | - 限流 6 | - 降级 7 | - 隔离 8 | - 重试 9 | - 超时控制 10 | 11 | ## 面试题目 12 | 13 | ### 怎么保证微服务的可用性? 14 | 15 | 分析:这个问题就非常宽泛。因为可用性的保证是一个巨大的话题,手段主要就是: 16 | - 隔离 17 | - 熔断 18 | - 限流 19 | - 降级 20 | 21 | #### 类似问题 22 | - 你怎么保证 XXX 服务的可用性? 23 | - 你做了一些什么?or 你们公司做了一些什么? 24 | 25 | ### 26 | -------------------------------------------------------------------------------- /microservice/general.md: -------------------------------------------------------------------------------- 1 | # 其它 2 | 3 | > 微服务细碎的问题就放在这里讨论 4 | 5 | 微服务的面试题,大体上可以分成: 6 | 1. 微服务本身的面试题。包括微服务的概念,为什么要微服务之类的; 7 | 2. 微服务治理的面试题。主要就是服务注册与发现,熔断限流之类的; 8 | 3. 微服务架构的面试题。微服务有什么部分,每个部分有什么用处,以及怎么设计; 9 | 4. 微服务选型的面试题。包括选择微服务框架,以及选择微服务依赖的第三方中间件的问题; 10 | 11 | ## 面试题目 12 | ### 微服务和 RESTful 的区别 13 | 14 | 分析:这个东西很多人都聊过,但是很多人都没说清楚。我给个“最终”答案:微服务是一种架构,而 RESTful API 是符合**REST**设计风格的 Web API。所以从这个角度来说,这两者根本不具备可比性。那么为什么面试官要问这个问题呢?大概率是他们公司内部有并存的微服务应用,和 RESTful 应用。 15 | 16 | 我们先来分析 RESTful,它是一种遵守了**REST**设计风格的 Web API,显然也有不遵守 REST 的。这部分我们一般就是简单描述为 Web 服务。 17 | 18 | 而微服务,一般是指以 RPC 为通信,结合了整个微服务治理的架构模式。实际上,微服务作为一种架构模式,其落地的选择是有很多的,除了这里说的 RPC,还有一种很重要的实现手段就是基于 Web API 的实现方式。 19 | 20 | 我们可以总结来说,微服务落地,最基本的通信的角度来说,可以是 RPC,也可以是 HTTP。而在 HTTP 之下,有一个子分类就是 RESTful。因此,微服务和 RESTful 的区别,核心就是: 21 | 1. 微服务是架构模式; 22 | 2. RESTful 是指符合 REST 规范的 Web API; 23 | 3. 微服务可以用 RESTful 来实现; 24 | 25 | 这里就基本上讨论清楚了,不过我们还可以刷一个亮点:RPC 本身也是可以用 RESTful 来实现的。于是我们可以加上第四点:有些微服务虽然是基于 RPC 来构建的,但是 RPC 本身又可以是用 RESTful 来实现的。 26 | 27 | 沿着这个思路: 28 | 1. 微服务用什么实现?RESTful 或者 RPC 都可以; 29 | 2. RPC 用什么实现?直接基于 TCP 或者基于 HTTP 都可以。 30 | 31 | 答案: 32 | 1. 微服务是架构模式; 33 | 2. RESTful 是指符合 REST 规范的 Web API; 34 | 3. 微服务可以用 RESTful 来实现; 35 | 4. (亮点)微服务的另外一种实现,是利用 RPC 来实现;而 RPC 可以直接基于 TCP 来实现,也可以基于 HTTP 来实现,所以也可以用 RESTful 来实现;(这里可能会引起面试官的兴趣,就是问你 RPC 有哪些实现思路) 36 | 37 | (最后总结)微服务和 RESTful 总体来说是两种维度的东西。 38 | 39 | (这里还有一个可能,就是面试官问你,RPC 和 RESTful 的区别,以为第四点我们聊到了微服务可以是 RPC 也可以是 RESTful) 40 | 41 | ### RPC 和 RESTful 的区别 42 | 43 | 分析:RPC 和 RESTful 的区别,前面的问题**微服务和 RESTful 的区别**我们已经提到了。 RPC 名字叫做远程过程调用,是一种远程通信协议。但凡是协议,就会有落地。那么 RPC 的落地就很百花争鸣了,不过主流就是两个流派:基于 HTTP 的和基于 TCP 的。前者的代表是 gRPC,后者的代表是 Dubbo。基于 HTTP 的,如果要是它的 API 设计也符合 REST 设计风格,那么就可以说,它是基于 RESTful 的。 44 | 45 | 后面我们可以稍微聊一下这两种实现方式的优劣对比,作为一个亮点。 46 | 47 | 答案:RPC 是远程通信协议,它的实现可以是基于 HTTP 的,也可以是基于 TCP 的。而 RESTful 是符合 REST 设计风格的 Web API。因此,如果一个 RPC 是基于 HTTP 的,并且 HTTP 的 API 设计是符合 REST 设计风格的,那么就可以说这个 RPC 是基于 RESTful 的。 48 | 49 | 它们也是两个不同维度的东西。一般来说,基于 TCP 的 RPC 实现更加复杂,但是可以从 TCP 层面上优化,因此性能会更好。而基于 HTTP 的则是实现非常简单,目前,基于 HTTP2 协议实现的 RPC 在性能上也很优秀,对于大部分应用来说,并不会触及它的性能上限(这里面是一个很大的误区,有很多人实际中,根本不考虑自己的实际情况,就使劲朝着高性能的角度去选型,其实大多数时候,我们都是和复杂度本身做斗争,而不是和性能作斗争)。 50 | 51 | #### 类似问题 52 | - RPC 实现思路。这个其实是一个很复杂的问题,不过只有在面试 RPC 中间件开发的时候才会涉及 53 | 54 | ### 微服务划分的粒度该如何确定? 55 | 分析:微服务划分粒度也可以说是模块划分粒度,其实是一个没准确答案的问题。很多时候,架构师划分模块自己都说不清楚为什么这么划分。 56 | 57 | 大体上的思路有几种: 58 | - 按照 DDD 理论进行划分:这一类使用 DDD 中的限界上下文(bounded context)作为微服务的边界。也就是一个限界上下文内的自然就是一个微服务 59 | - 按照敏捷团队理论:敏捷强调两个披萨团队,那么微服务的粒度就是一个团队的能力边界 60 | - 按照复杂度理论:一个人能完全理解微服务的细节(或者说模块细节) 61 | - 按照组织架构划分:这个就完全和技术没什么关系了,组织架构怎么设计,然后按照组织架构来分配系统模块 62 | 63 | 就我个人经验来看,没有哪个人是纯粹按照其中的一条标准来进行划分的。如果有人声称自己是按照 DDD 来划分,那就是在吹牛。因为模块划分,或者说微服务拆分,本身就是一个分蛋糕的过程,这个过程难免伴随组织架构调整。 64 | 65 | 如果是一个有人事权的 CTO 宣称自己能够按照 DDD 理论来分那还有点可能,不然就都是吹牛。 66 | 67 | 回答的时候可以回答标准的 DDD 理论思路。 68 | 69 | 答案:一般来说,微服务的划分可以利用 DDD 理论,一个限界上下文定义了微服务的边界,那么只需要按照限界上下文来进行划分就可以了。 70 | 71 | 不过那是理想情况。正常来说在真的划分微服务的时候还要考虑: 72 | - 团队规模:微服务的边界不能超出团队的维护能力; 73 | - 复杂度:最好微服务划分到一个人能够完全理解其中细节的地步。或者说,两三个人合在一起能够将服务的细节交代清楚的地步; 74 | - 组织结构:如果不能够调整组织结构,那么组织结构会极大限制微服务划分,也就是康威定律,有什么样的组织架构就有什么样的系统; 75 | 76 | 一般服务拆分都需要比较强力的实权管理层来推进。 77 | 78 | ### 既然有了模块化,为什么还要微服务? 79 | 80 | 分析∶要回答这个问题,要理解模块化和微服务化的本质区别是什么。它们的本质区别 81 | 就是是否独立部署。其余的诸如微服务划分之类的这种东西,是完全可以照搬过去模块化。一般来说,我个人认为模块化应该是微服务化的前置条件。也就是如果模块化都没搞定,贸然上微服务,多半没有好结果。 82 | 83 | 所以,为什么要微服务化,就落在我们为什么非要独立部署上来,或者说独立部署究竟有什么好处。 84 | 85 | - 首先我们想到,这是一种非常的隔离机制。因为独立部署直接就是物理隔离了。那么一个服务崩溃,或者服务所在的机器崩溃,都不会影响另外一个服务; 86 | - 其次是微服务的独立升级。在模块化之下,如果要升级一个服务,或者修复一个 BUG,那需要找到所有使用这个模块的应用,挨个升级一遍。而在微服务之后,只需要维护者自己重新部署一下就可以。当然这也是一种双刃剑,因为很可能升级有BUG,然后导致别人的服务就莫名其妙崩溃了 87 | - 易于横向扩展。单体模块化应用比起来,只需要扩展有需要的服务,而不必整个应用都横向扩展。 88 | 89 | 此外还有一些点要澄清一下∶ 90 | 91 | - 可靠性问题。这个其实是存疑的。因为虽然理论上来说,微服务独立部署天然提供了强隔离机制,以至于某些服务出故障,并不会影响别的服务。但是别忘了这里面很重要的一点微服务是独立部署的,微服务之间通信是网络通信,网络通信比本地调用脆弱多了。所以实际上,可靠性反而是下降的; 92 | - 代码易维护这也是一个存疑的问题。大多数人的模块化,并没有单独分出去作为一个代码仓库,而是依旧集中在一个巨大的仓库里面,所以会带来这种错觉。实际上,模块化里面有一个重要的步骤就是做到代码的物理隔离。比如说 order 的代码在一个仓库,User的在一个仓库。如果这个代码拿出去独立部署,那就是微服务。如果通过依赖来引入,那就是模块化; 93 | - 控制复杂度∶没得说, 和上面差不多。模块化也是通过分而治之, 来控制复杂度1 94 | 的,和微服务一样。所以实际上两者都能起到同样的效果。而且,微服务可以细分,模块也同样可以细分; 95 | - 可测试性∶同上; 96 | 97 | 98 | ### 微服务和 SOA 对比 99 | 100 | -------------------------------------------------------------------------------- /microservice/img/chain-timeout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/microservice/img/chain-timeout.png -------------------------------------------------------------------------------- /microservice/timeout.md: -------------------------------------------------------------------------------- 1 | # 超时控制 2 | 3 | 分析:超时控制在微服务里面,是一个咋一看很容易,但是讨论起来又可以在平凡之中刷出足够亮点的话题。 4 | 5 | 超时控制最基本,也就是大多数人能够想到的超时控制也就是 A 调用 B,然后超时是 3s 这种超时。那么想要把超时控制回答好,那么就需要深入理解这几个话题: 6 | - 为什么要做超时控制?需要举例子说明超时控制没做,会有什么影响; 7 | - 超时控制怎么做?这里面我们会进一步细分单次调用的超时控制,以及在这个基础上的全链路超时控制是如何实现的; 8 | - 超时之后能否中断正在执行的业务逻辑; 9 | 10 | ## 面试题 11 | 12 | ### 为什么要做超时控制? 13 | 分析:如果单纯从理论上来说,那么可以总结为客户端总是希望在预期的时间内拿到响应的,而不管这个响应是正常响应还是异常响应。那么超时就是为了保证这一点。在超时控制里面还有一种比较特殊的形态,即链路超时控制。例如在 A -> B -> C -> D 的时候在整条链路上设置一个超时时间。例如在起点 A 处设置一个超时时间 1s,那么意味着在 A 收到请求到 A 从 B 中拿到响应的整条链路,超时时间不能超过 1s。这种链路超时控制,也可以看作是为了让调用者在确定的时间段内拿到响应。只不过这个调用者就是用户本身。 14 | 15 | 从另外一个角度来说,超时也是为了节省资源。我们可以预期的是,在超时之后,后续的步骤应该会被中断掉,并且及时释放资源,例如 TCP 连接,数据库连接之类的。 16 | 17 | 18 | 所以总结,超时控制的目标: 19 | - 让调用者能够在预期的时间内拿到响应。而在链路超时控制里面,则是希望用户可以在确定的时间内拿到响应,提升用户体验。 20 | - 及时释放资源 21 | 22 | 为了增强说服力,可以额外举例子,两种做法带来的后果: 23 | - 例如在 APP 首屏的请求里面设置整个链路超时时间不超过 100ms,这会迫使后端去优化程序性能以满足这种性能要求 24 | - 缺乏超时控制(或者超时时间设置得不对)会引起资源泄露。例如在微服务调用里面,如果没有超时控制,那么发起调用的 goroutine 会被阻塞,直到拿到响应。又或者在 DB 查询里面,没有超时控制,在数据库性能偶然抖动的情况下,可能导致数据库连接消耗干净,同时引起 goroutine 泄露。 25 | 26 | 注意,这里我们说 goroutine 泄露,是 Go 专属的。Java 之类的语言,依赖的是线程,那么缺乏超时控制会不会导致线程长期阻塞,则取决于语言细节,以及框架的线程模型。 27 | 28 | 可以认为,最基础的回答就是答出两条核心目标,刷亮点则在于解释如果超时控制设置不当会有什么后果。对于 Go 语言开发来说,这也是一个典型的可能引起 goroutine 泄露的场景。 29 | 30 | 答:(基本回答)超时控制一般是为了两件事: 31 | - 确保调用者能够在预期的时间内拿到响应,即便是一个超时响应也好过完全没有响应。例如 APP 首页等关键 API 上,在链路超时控制(这里是一个引导,因为面试官可能接下来就是面你链路超时控制机制了)里面,设置整条链路的超时时间不超过 100ms。 32 | - 及时释放资源。例如在 RPC 调用里面或者数据库查询里面,设置超时时间。如果没有设置的话,那么 goroutine 或者连接之类的东西一直被占用,引起资源泄露,即 goroutine 泄露或者连接无法被复用(在这个地方要小心,如果面试官捕捉到了 goroutine 泄露这几个关键字,那么可能会进一步考察 goroutine;如果面试官捕捉到了连接复用几个关键字,那么可能会进一步面连接复用的原理)。 33 | 34 | #### 类似问题 35 | - 超时如果没有设置,或者值没有设置好,会出现什么问题?要注意,超时没设置,或者设置过长,会浪费资源,典型例子就是 goroutine 长时间得不到释放(或者 TCP 链接长时间被占用)。但是过短也不行,因为过短意味着你会经常超时,而没有办法拿到正常的响应 36 | 37 | 38 | ### 如何计算超时时间? 39 | 分析:这个问题是指,假如说你要发起一个 RPC 调用,A->B,那么设置多长的超时时间比较好。这个东西,要从两个角度考虑: 40 | - 从用户体验的角度考虑 41 | - 从开发者的角度 42 | 43 | 从政治正确的角度来说,那么肯定是从用户体验的角度来考虑的。也就是说如果产品经理觉得用户只能等待一秒,那么意味着你的接口的超时时间就应该是一秒。一秒内,不管你有没有处理完毕,拿到正常的响应,你都要明确告诉用户。也就是用户体验上的一个原则:一个坏消息都好过没有消息。这也就是说,超时时间完全不是由我们研发人员控制的,而是纯粹的一种用户体验设计上的事情。 44 | 45 | 当时实际上,受制于产品经理的水平,以及技术架构的缺陷,我们并不能总是站在用户体验的角度考虑超时时间。这种情况下,超时时间就只能是依赖于研发人员去确认了。 46 | 47 | 对于一个已经上线的接口来说,例如 B。那么 A 调用 B 的超时时间可以设置为 99 线,或者 999 线。这取决于公司的可用性目标。不过有两个值是肯定不能用的,中位数或者平均值。中位数意味着一半的请求会超时,而平均值则是接近一半的请求会超时。 48 | 49 | 对于一个没有上线的接口来说,因为不知道 99 线或者 999 线,那么这个时候就可以通过压测来确定这两个值。如果无法压测,那么只能根据代码来手动分析。例如说如果一个数据库查询大概是 10ms, 而 B 接口发起了三个数据库查询,那么至少要 30ms,从容估算的可以取 100ms。 50 | 51 | 这个东西很像计算限流阈值。所以你可以在面试的时候顺便引导过去限流。应该说,跟响应有关的数据基本上都可以通过这种方式来确定。 52 | 53 | 最后要强调一下超时时间只是一个建议值,例如说设置了1秒钟作为超时时间,那么可能1.2秒才最终超时。这就引申出来时间精确度的问题,时间精确度这个是和语言相关的。 54 | 55 | 答案:计算超时时间可以考虑从两个角度考虑: 56 | - 从用户体验的角度考虑,也是最佳方案(不得不吐槽,这就是政治正确的说法,面试官绝对不敢说不对)。那么可以要求产品经理给出具体的超时参数,比如说用户能够最多容忍这个接口多长时间内返回数据 57 | - 否则的话,我们研发人员可以考虑自己确定超时时间。如果是调用一个已有的接口,那么可以利用观测到的响应时间数据,选用 99 线或者 999 线来作为超时时间。(稍微刷一下亮点)但是不能选用中位数或者平均值之类的,因为这意味着有相当多的请求会超时。而如果一个接口还没有上线,那么可以考虑压测来确定超时时间(这里你可以稍微提一下,限流的阈值也可以通过压测来确定。那么话题就可能引向限流)。如果连压测也做不了(这并不稀奇,能够随时执行压测的公司是凤毛麟角),那么只能通过代码分析来确定超时时间。例如说 B 接口上有三次数据库查询,如果每次数据库查询的时间是 10ms,那么就可以估计 B 数据库查询就要花掉 30ms,再预留一些 buffer,就可以认为调用 B 的超时时间在 100ms。 58 | 59 | (如果你记得你使用的语言,或者你面试的语言时间精度问题,就在这里接着刷亮点)大多数时候,我们并不能预期系统或者中间件的超时处理是很精确的,这主要是源于巴拉巴拉(转到讨论不同语言的时间精度问题) 60 | 61 | #### 相关问题 62 | - 如何确定限流阈值 63 | - 时间精度问题 64 | 65 | ### 怎么实现超时控制? 66 | 分析:一般来说,其实不太会直接问怎么实现超时控制,一般都是问怎么实现链路超时控制。 67 | 68 | 我们的回答也分成两块: 69 | - 和语言相关的进程内超时控制。例如在 Go 里面基本上就是利用 context.Context 来实现进程内超时控制; 70 | - 全链路超时控制; 71 | 72 | 重点放在全链路超时控制上。全链路超时控制核心要解决: 73 | - 计算剩余超时时间 74 | - 跨进程传递剩余超时时间 75 | 76 | 首先我们看一个例子: 77 | 78 | ![链路超时](./img/chain-timeout.png) 79 | 80 | 基本步骤是: 81 | - 客户端最开始的时候,链路超时时间是 3s。一般在 BFF 里面会设置好整个链路的超时时间,而后逐步传递; 82 | - 客户端自己业务花掉了 0.5s,那么链路剩余超时时间就是 (3-0.5) = 2.5s 了; 83 | - 客户端准备发起 RPC 调用,RPC 客户端将剩余超时时间编码进去请求里面,传递到服务端; 84 | - RPC 服务端收到请求之后,发现剩余超时时间是 2.5s,但是网络传输花了 0.5s(怎么知道花了 0.5s),那么剩余超时时间就是 2s; 85 | - RPC 服务端将请求和剩余超时时间都交给用户的业务代码,业务执行花掉 0.5s,那么还剩下 1.5s; 86 | - 接下来,不管服务端要继续调用 RPC 服务,还是要调用 http 接口,都要将 1.5s 继续传递下去; 87 | 88 | 所以可以注意到,整个链路超时控制,其实沿着整条调用链路,将剩余超时时间一层一层传递下去,每一个步骤都要严格控制自己的超时时间不能超过整个链路的剩余超时时间。 89 | 90 | 在进程间,或者说跨网络传递超时时间主要依赖于采用的通信协议。例如说如果我们的微服务是依赖于 gRPC 的,那么这个链路剩余超时时间是在 gRPC 底层的 HTTP 头部中传递。而如果我们采用的是 Dubbo 协议,那么链路剩余超时时间是在 Dubbo 协议的 attachment 部分传递的。(虽然 Dubbo 协议本身有一个超时的字段,但是好像并没有用来做链路超时控制,存疑)。如果是其它协议,那么就要看具体的协议设计了。一般来说都是在协议的元数据中传递。如果你知道面试官公司用的是什么协议,那么你可以提前了解一下面试官使用的协议是否支持链路超时控制,如果支持的话,那么它的剩余超时时间是怎么在网络之间传递。 91 | 92 | 在这种超时控制下,还有一个细节,就是客户端调用服务端的时候,谁来控制这个超时? 93 | - 在客户端计时:也就是客户端自己有一个计时器,时间到了就认为超时了,而后不会再管服务端的响应 94 | - 客户端和服务端同时计时:也就是客户端有一个计时器,时间到了就给用户代码一个超时响应;而服务端也有自己的计时器,如果服务端超时了,那么服务端将直接返回一个超时响应(其实也可以什么响应都不返回,因为服务端可以认为客户端早就已经返回了)。相比之下,因为服务端也维持住了计时器,所以超时之后服务端可以及时释放资源(主要是连接) 95 | 96 | 唯一要注意的就是,超时控制是不能仅仅在服务端这边控制的。假如说只在服务端这边有一个计时器,然后超时了,那么服务器返回的超时响应,很可能在半路就丢掉了,而客户端根本没有收到。 97 | 98 | 另外一个问题是传递的超时时间,究竟是什么? 99 | - 第一种传递的方案就是直接传递剩余超时时间,也就是我们在图里面画的,1.5s 这种。不过大多数时候不是直接传这种字符串,而是传数字,例如以毫秒作为的数字。这种方案的缺陷在于难以计算网络传输时间。如果面试问到怎么计算,那么标准答案依旧是测试,在平均请求大小的情况下,完全复刻线上环境,测试两个节点之间通信的延迟。一般来说,除非是跨城市机房通信,或者跨国通信,否则相比超时时间,网络传输时间基本可以忽略笔记; 100 | - 第二种传递的方案就是传递超时时间戳。超时时间戳一般来说传递 64 位数字就可以。但是如果想要使用更少字节来传输,那么可以考虑使用毫秒数,并且该毫秒数不需要从 1970 年开始计数,而是可以用最近的,比如说从 2020 年开始的。这种方案并没有什么缺陷,只不过如果时钟不同步的问题比较严重的话,那么可能导致超时控制不准。但是一般超时时间至少都有几十毫秒,很少有时钟不同步会在几十毫秒这个量级; 101 | 102 | 总结起来,如果你想要回答好这个问题,就要讲清楚: 103 | - 链路超时控制的大概流程 104 | - 链路超时控制在哪一边控制 105 | - 剩余超时时间是怎么传输的 106 | - 剩余超时时间在网络中传输,传输的究竟是什么? 107 | 108 | 回答: 109 | 超时控制大体上分成两种,一种是进程内的超时控制,一种是链路超时控制。进程内的超时控制是和语言相关的,例如说在 Go 里面就是采用了 context.Context 来控制超时。 110 | 111 | 而链路超时比较复杂。(先把整个流程大概说一下)举例来说,在调用链路 A -> B -> C 中,如果 A 设置了整个链路的超时时间是 3s,如果 A 本身花了 0.5s 调用,然后发起调用 B,网络传输花了 0.5s,那么 B 收到请求之后剩余的超时时间就剩下 2s,如果 B 本身花费了 0.5s,而后发起调用 C,如果网络传输也花了 0.5s,那么 C 收到请求之后剩余超时时间只有 1s 了。 112 | 113 | 一般来说,对于不支持链路超时控制的微服务框架来说,它们只需要在客户端维持一个计时器。在超时之后直接返回一个超时响应。而支持链路超时控制的微服务框架,那么可以选择同时在客户端和服务端计时。(这里不要继续回答下去,等面试官问为什么要在服务端也启动一个计时器,核心就是为了释放连接,不需要将相应写回给客户端)。 114 | 115 | (开始讨论细节,也就是刷亮点了,首先讨论怎么传递链路超时控制)在链路超时控制中,关键的一点是每一次发起调用的时候,都需要计算剩余超时时间,而后将剩余超时时间传递给服务端。(开始展示自己的知识面广)一般来说,RPC 协议里面都会预留一个部分,用于传递链路元数据,链路剩余超时时间就属于链路元数据的一部分。例如在 gRPC 里面,链路剩余超时时间是在 HTTP 协议的头部中传递的。 116 | 117 | (继续讨论传输的内容)大部分情况下,剩余超时时间都是传递毫秒数,而后服务端收到请求之后减去网络传输时间(这里依旧是引导面试官问怎么确定网络传输时间,也就是在分析中说的,测试,注意要跟面试官强调剩余超时时间一般都很大,那么只要不是跨大陆或者跨国通信,那么基本可以忽略不计),得到的就是服务端剩余超时时间。 118 | 119 | 另外一种做法是传递超时时间戳,(这里是刷亮点)超时时间戳可以选在从 2020 年开始的时间戳,并且以毫秒为单位,这样四个字节就够了。(紧接着继续刷亮点,讨论时钟同步问题)这种方案的隐患在于如果时钟不同步的话,超时控制会不准确(本来就不准确,那么面试官就可能问你为什么超时控制不准确,你就可以看**如何计算超时时间**中的讨论)。不过相比超时时间几百毫秒,时钟不同步的那一点点误差,完全可以忽略不计。 120 | 121 | #### 相关问题 122 | - 本地超时控制是怎么做的?如果从本质上来说,都是依赖于时钟中断,剩下的就是不同语言机制就不太一样了 123 | - 什么是全链路超时控制? 124 | - 怎么在链路中传递超时时间?要回答两个点,不同协议是怎么传的和传递的究竟是什么,注意刷亮点 -------------------------------------------------------------------------------- /mq/Kafka.md: -------------------------------------------------------------------------------- 1 | # 你是否了解 Kafka 2 | 3 | 分析: 如果只是宽泛地谈 Kafka,那么回答的点就要围绕 Kafka 的几个组成来。这个部分不必谈及“为什么 Kafka 高性能” “为什么 Kafka 高可用”等问题。因为按照一般的惯例,接下来就会聊这个话题。总跳回答的思路就是介绍一下 Kafka 的基本原理,几个主要概念。后面详细的内容,等后面面试官来提问。 4 | 5 | ![Kafka知识点](./img/kafka_available_performance.png) 6 | 7 | 答:Kafka 是一个基于发布订阅模式的消息队列中间件。它由 Producer, Consumer, Broker 和 Partition 几个组成。 8 | 9 | Kafka 里面的每一个消息都属于一个主题,每一个主题都有多个 Partition。Partition 又可以使用主从复制模式,即 Partition 之间组成主从模式。这些 Partition 均匀分布在 Broker 上,以保证高可用。(这里点到了高可用,引导面试官探讨 Kafka 高可用)。每一个 Partition 内消息是有序的,即分区顺序性。(这一句是为了引出后面如何保证消息有序性) 10 | 11 | Producer 依据负载均衡设置,将消息发送到 Topic 的特定 Partition 下;(后面面试官可能会问负载均衡策略) 12 | 13 | Consumer 之间组成了 Consumer Group,可以有多个 Consumer Group 消费同一个 Topic,互相之间不会有影响。Kafka 强制要求每个 Partition 只能有一个 Consumer,并且 Consumer 采取拉模式,消费完一批消息之后再拉取一批(尝试引出来后面的拉模型的讨论); 14 | 15 | 一个 Kafka 集群由多个 Broker 组成,每个 Broker 上存放着不同 Topic 的 Partition; 16 | 17 | ![Topic,Broker 和 Partition](https://pic2.zhimg.com/80/v2-17a2d36445a764081b45e012397291bd_720w.jpg) 18 | 19 | ## 扩展点 20 | 21 | ### Kafka 的高性能是如何保证的? 22 | 23 | 分析:必考题。高性能的影响因素有很多,但是常考的就是顺序写 + 零拷贝。在这个问题之下,我们只需要罗列出来各个点,但是不做深入解释。在罗列完之后,我们重点对其中某些点做详细说明,一般我建议用零拷贝做深入阐释。当然,最好是记得所有的点,包括它们的细节,不过这样一回答,没有三五分钟答不完。 24 | 25 | 答:Kafka 高性能依赖于非常多的手段: 26 | 1. 零拷贝。在 Linux 上 Kafka 使用了两种手段,mmap (内存映射,一般我都记成妈卖批,哈哈哈) 和 sendfile,前者用于解决 Producer 写入数据,后者用于 Consumer 读取数据; 27 | 2. 顺序写:Kafka 的数据,可以看做是 AOF (append only file),它只允许追加数据,而不允许修改已有的数据。(后面是亮点)该手段也在数据库如 MySQL,Redis上很常见,这也是为什么我们一般说 Kafka 用机械硬盘就可以了。有人做过实验(的确有,你们可以找找,我已经找不到链接了),机械磁盘 Kafka 和 SSD Kafka 在性能上差距不大; 28 | 3. Page Cache:Kafka 允许落盘的时候,是写到 Page Cache的时候就返回,还是一定要刷新到磁盘(主要就是mmap之后要不要强制刷新磁盘),类似的机制在 MySQL, Redis上也是常见,(简要评价一下两种方式的区别)如果写到 Page Cache 就返回,那么会存在数据丢失的可能。 29 | 4. 批量操作:包括 Producer 批量发送,也包括 Broker 批量落盘。批量能够放大顺序写的优势,比如说 Producer 还没攒够一批数据发送就宕机,就会导致数据丢失; 30 | 5. 数据压缩:Kafka 提供了数据压缩选项,采用数据压缩能减少数据传输量,提高效率; 31 | 6. 日志分段存储:Kafka 将日志分成不同的段,只有最新的段可以写,别的段都只能读。同时为每一个段保存了偏移量索引文件和时间戳索引文件,采用二分法查找数据,效率极高。同时 Kafka 会确保索引文件能够全部装入内存,以避免读取索引引发磁盘 IO。(这里有一点很有意思,就是在 MySQL 上,我们也会尽量说把索引大小控制住,能够在内存装下,在讨论数据库磁盘 IO 的时候,我们很少会计算索引无法装入内存引发的磁盘 IO,而是只计算读取数据的磁盘 IO) 32 | 33 | (批量操作+压缩的亮点)批量发送和数据压缩,在处理大数据的中间件中比较常见。比如说分布式追踪系统 CAT 和 skywalking 都有类似的技术。代价就是存在数据丢失的风险; 34 | (数据压缩的亮点)数据压缩虽然能够减少数据传输,但是会消耗更过 CPU。不过在 IO 密集型的应用里面,这不会有什么问题; 35 | 36 | (下面是零拷贝详解) 37 | 一般的数据从网络到磁盘,或者从磁盘到网络,都需要经过四次拷贝。比如说磁盘到网络,要经过: 38 | 39 | ![四次拷贝](https://miro.medium.com/max/840/0*Q6eoQ-19bq-qkm_Y) 40 | 41 | 1. 磁盘到内核缓冲区 42 | 2. 内核缓冲区到应用缓冲区 43 | 3. 应用缓冲区到内核缓冲区 44 | 4. 内核缓冲区到网络缓冲 45 | 46 | 零拷贝则是去掉了第二和第三。(之所以叫零拷贝,并不是说完全没有拷贝,而是指没有CPU参与的拷贝,DMA的还在)。 47 | 48 | ![零拷贝](https://miro.medium.com/max/700/0*es45Nv-ea2WDtI0n) 49 | 50 | (这一段可选,因为比较冷僻)如果在 Linux 高版本下,而且支持 DMA gather copy,那么内核缓冲区到 51 | 52 | ![零拷贝](https://miro.medium.com/max/700/0*XJNUTI5QoiCzSbxE) 53 | 54 | Kafka 利用了两项零拷贝技术,mmap 和 sendfile。前者是用于解决网络数据落盘的,Kafka 直接利用内存映射,完成了“写入操作”,对于 Kafka 来说,完成了网络缓冲区到磁盘缓冲区的“写入”,之后强制调用`flush`或者等操作系统(有参数控制)。(继续补充细节,如果自己是JAVA开发并且记得的话)Java 提供了`FileChannel`和`MappedByteBuffer`两项技术来实现 mmap。 55 | 56 | `sendfile`是另外一种零拷贝实现,主要解决磁盘到网络的数据传输。操作系统读取磁盘数据到内存缓冲,直接丢过去`socket buffer`,而后发送出去。很多中间件,例如 `Nignx`, `tomcat` 都采用了类似的技术。 57 | 58 | 关键字:零拷贝,顺序写,缓冲区,批量,压缩,分段存储 59 | 60 | 61 | #### 类似问题 62 | - 什么是零拷贝 63 | - 为什么顺序写那么快? 64 | - Kafka 为什么那么快? 65 | - 66 | 67 | #### 如何引导 68 | - 讨论到了零拷贝技术 69 | - 讨论到了顺序写技术 70 | 71 | ### Kafka 的 ISR 是如何工作的? 72 | 73 | 分析:考察的是 Partition 同步问题。回答这个问题的时候,要清晰解释清楚,一个 Producer 写入一条消息,到 Partition 同步完成的步骤。亮点在于说清楚,这些步骤如果出现问题会导致什么结果。这里还牵涉到三个基本概念,ISR,HW,LEO。容易混淆的两个时间点,一个是消息写入成功,一个是消息同步成功。这是两个不同的东西。同样,另外一个容易混淆的是,ISR 里面的分区数据和主分区还是有差别的,也就是说,我们认为从分区与主分区保持同步,并不是严格的。 74 | 75 | 答:ISR 是分区同步的概念。Kafka 为每个主分区维护了一个 ISR,处于 ISR 的分区意味着与主分区保持了同步(所以主分区也在 ISR 里面)。 76 | 77 | 当 Producer 写入消息的时候,需要等 ISR 里面分区的确认,当 ISR 确认之后,就被认为消息已经提交成功了。ISR 里面的分区会定时从主分区里面拉取数据,如果长时间未拉取,或者数据落后太多,分区会被移出 ISR。ISR 里面分区已经同步的偏移量被称为 LEO(Log End Offset),最小的 LEO 称为 HW(高水位,high water,这个用木桶来比喻就很生动,ISR 里面的分区已同步消息就是木板,高水位就取决于最短的那个木板,也就是同步最落后的),也就是消费者可以消费的最新消息。 78 | 79 | ![LEO 和 HW](https://img-blog.csdnimg.cn/20200706235345430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FqaWFueWluZ3hpYW9xaW5naGFu,size_16,color_FFFFFF,t_70) 80 | 81 | 当主分区挂掉的时候,会从 ISR 里面选举一个新的主分区出来。 82 | 83 | (下面我们进一步解释一下 Producer 写入消息) 84 | 我们在 Producer 里面可以控制 ACK 机制。Producer 可以配置成三种: 85 | 1. Producer 发出去就算成功; 86 | 2. Producer 发出去,主分区写入本地磁盘就算成功; 87 | 3. Producer 发出去,ISR 所有的分区都写入磁盘,就算成功; 88 | 89 | 其性能依次下降,但是可靠性依次上升。 90 | 91 | (如果记得,可以补上这个说明)因为 ISR 里面包含了主分区,也就是说,如果整个 ISR 只有主分区,那么全部写入就退化为主分区写入。所以在可靠性要求非常高的情况下,我们要求 ISR 中分区不能少于三个。该参数可以在 Broker 中配置(min.insync.replicas) 92 | 93 | (回答到这里,我们基本上就说清楚了 ISR 的基本机制。下面我们横向对比一下 ISR 机制与别的主从同步机制。很明显的,就是 Producer 这种发送策略,是否等待同步完成,在很多中间件上都能看到,随便挑一个出来就可以。我这里总结一下: 94 | 95 | ISR 的同步机制和其它中间件机制也是类似的,在涉及主从同步的时候都要在性能和可靠性之间做取舍。通常的选项都是: 96 | 1. 主写入就认为成功 97 | 2. 主写入,至少一个从写入就认为成功; 98 | 3. 主写入,大部分从库写入就认为成功(一般“大部分”是可以配置的,从这个意义上来说,2和3可以合并为一点); 99 | 4. 主写入,所有从库写入就认为成功; 100 | 101 | 而“写入”也会有不同语义: 102 | 1. 中间件写到日志缓存就认为写入了; 103 | 2. 中间件写入到系统缓存(page cache)就认为写入了; 104 | 3. 中间件强制刷新到磁盘(发起了 fsync)就认为写入了; 105 | 106 | 都是性能到可靠性的取舍。) 107 | 108 | (在面试的时候,可以考虑回答完 ISR 之后,将上面的总结说出来,可以借用 MySQL,Redis, ZK 来说明,这算是一般规律) 109 | 110 | 111 | #### 类似问题 112 | - Kafka GC 时间过长会导致什么问题?可能导致分区被踢出去 ISR。 113 | - Kafka 是如何保证可靠性的?(除了 ISR 以外,还要强调一下 Partition 是分布在不同 Broker 上,以避免 Broker 宕机导致 Topic 不可用 114 | - 如何提高 Kafka 的可靠性 115 | - 如何提高 Kafka 吞吐量?可靠性和吞吐量在这里就是互斥的,调整参数只能提高一个,降低另外一个。 116 | 117 | ### 什么时候分区会被移出 ISR? 118 | 119 | 分析:考察 ISR 的特点,要理解 Kafka 如何维护 ISR 的。其实就两个参数控制,一个是落后多少消息,一个是多久没同步。比较有亮点的是能够清楚答出是哪两个参数,另外一个刷亮点的机会,就是说清楚它们过大过小都会有什么影响。 120 | 121 | 答案:当分区触发两个条件中的任何一个时,都会被移除出 ISR。 122 | 1. 消息落后太多,这个是参数`replica.lag.max.messages` [0.9.0后被移除](http://kafka.apache.org/documentation/#upgrade_9_breaking) 123 | 2. 分区长时间没有发起`fetch`请求,由参数`replica.lag.time.max.ms`控制。 124 | 125 | (刷亮点,点出影响因素,后面面试官跟你探讨这些因素怎么影响的)基本上,除非是新的 Broker,否则几乎都是由网络、磁盘IO和GC引起的,大多数情况下,是负载过高导致的。 126 | 127 | (点出过大过小的影响)这两个参数,过小会倒是 ISR 频繁变化,过大会导致可靠性降低,存在数据丢失的风险。 128 | 129 | (如果你知道你们公司的配置)我们公司的配置是 XXX 和 XXX。 130 | 131 | 关键字:落后多少消息,多久没同步 132 | 133 | #### 如何引导 134 | - 谈到 Kafka 可靠性可用性 135 | 136 | >补充: 为什么kafka要将`replica.lag.max.messages`删除? 137 | > 138 | > 因为这个参数本身很难给出一个合适的值。以默认的值4000为例,对于消息流入速度很低的主题(比如TPS为10),这个参数就没什么用;对于消息流入速度很高的主题(比如TPS为2000),这个参数的取值又会引入ISR的频繁变动(ISR 需要在Zookeeper中维护)。所以从0.9x版本开始,Kafka就彻底移除了这一个参数。 139 | 140 | ### Kafka 的负载均衡策略有哪些? 141 | 142 | 分析:一般考察的是 Producer 怎么把消息分到对应的 Partition 上。理论上来说就是两种,一个是轮询,一个是 Hash 取余。这取决于 Key 是否为 Null。但是我们可以结合实际中的一些现实场景,来做一些扩展说明。特别是 Hash 取余。这种问题,列举有什么策略一类的面试题,其实单纯列出来,只能说是合格,要想回答好,就需要针对性地结合自己的经历,重点分析某些策略的优劣。所以我们的回答会先简单介绍有哪些策略,后面会重点落在 Hash 取余上,着重分析 Hash Key 对负载均衡的问题。 143 | 144 | 最后我们将话题引到 Partition 负载均衡与消费者负载均衡不匹配的问题上。它是指,我们的消息的确分布均匀了,但是处理不同的消息可能有快有慢,在极端情况下,可能处理慢的消息都在特定的 Partition 上,因此导致某个消费者负载奇高,而其余的消费者却没有什么负载。 145 | 146 | 答案:一般来说有两种,一种是轮询,即 Producer 轮流挑选不同的 Partition;另外一种是 Hash 取余,这要求我们提供 Key。 147 | (接下来,我们讨论 Key 的选择对 Partition 负载的影响,主要是为了体现自己用 Kafka 解决不同问题的思路) 148 | 149 | Key 的选取,大原则上是采用业务特征 ID,或者业务特征的某些字段拼接而成。比如说,我们可以考虑按照用 Order ID(可以替换成自己项目里面的某些业务的ID)作为 Key,这意味某个订单的消息肯定落在特定的某个 Partition 上,这就保证了针对该订单的消息是有序的(这里面间接提到了有序性的问题,体现了自己对于 Partition 的理解)。 150 | 151 | (说一下 Hash 策略的风险)但是 Hash 策略下,如果 Key 设置不当,可能会导致某些 Partition 承载了大多数的流量。比如说按照商家 ID 来作为 Key,那么可能某些热点商家,大卖家,其消息就集中在某个 Partition 上,导致负载不均衡。 152 | 153 | (我们升华一下这个问题,就是这些负载策略实际上都只考虑 Partition 的负载,而没有考虑 Consumer 的负载,为了进一步凸显自己对负载均衡的理解) 154 | 无论是轮询,还是 Hash,都无法解决一个问题:它们没有考虑 Consumer 的负载。例如,我们可以用 Hash 策略均匀分布了消息,但是可能某些消息消费得慢,有些消息消费得快。假如说非常不幸我们消费得慢的消息都落在某个 Partition,那么该 Partition 的消费者和别的消费者比起来,消费起来就很慢,带来很大的延迟,甚至出现消息堆积。 155 | 156 | 关键字:轮询,Hash,Key 的选取 157 | 158 | #### 类似的问题 159 | - 如何选取 Hash Key 160 | - 你们是如何设置 Producer 推送消息到哪个 Partition 的? 161 | 162 | #### 如何引导 163 | - 在介绍 Kafka 的时候 164 | - 讨论到 Hash Key 选择的时候。其实不仅仅是 Kafka,所有基于 Hash 的负载均衡算法,都会有类似的问题。所谓的 Hash 冲突,也就是这个问题。 165 | - 聊到了消息有序性的时候 166 | 167 | ### 为什么 Kafka 的从 Partition 不能读取? 168 | 169 | 分析:考察 Kafka 消费者拉取消息的特点。这个问题的背景是,一般的主从模式,从服务器都可以提供读服务,但是 Kafka 的从 Partition 是不能提供读服务的。所以这也是一个违背一般规律的问题。我们回答的点就要从 Kafka 本身消息存储和消费者消费特点两方面回答。为了方便记忆,以及清楚解释这个问题,我们可以对比 MySQL 的主从模式,假设 Kafka 允许读从 Partition 的数据,会发生什么。 170 | 171 | 答:首先是 Kafka 自身的限制,即 Kafka 强制要求一个 Partition 只能有一个 Consumer,因此 Consumer 天然只需要消费主 Partition 就可以。 172 | 173 | 那么假如说 Kafka 放开这种限制,比如说有多个 Consumer,分别从主 Partition 和从 Partition 上读取数据,那么会出现一个问题:即偏移量如何同步的问题。例如一个 Consumer 从 Partition A 读取了 0- 100 的消息,那么另外一个 Consumer 从 Partition B 上读取,就只能读取 100 之后的数据。那么 Kafka 就需要在不同的 Partition 之间协调这个已读取偏移量。而这是分布式一致性的问题,难以解决。 174 | 175 | MySQL 的主从模式比起来,并没有这种问题,即 MySQL 不需要进行类似偏移量的协商。 176 | 177 | 而从另外一个角度来说,Kafka 的读取压力是远小于 MySQL 的,毕竟一个 Topic,是不会有特别多的消费者的。并且 Kafka 也不需要支持复杂查询,所以完全没必要读取从 Partition 的数据。 178 | 179 | 关键字:偏移量 180 | 181 | ### 为什么 Kafka 在消费者端采用了拉(PULL)模型? 182 | 分析:考察拉模型的特点。回答的关键点在于,对比拉模型和推模型,在 MQ 这种场景下的优缺点。加分点在于明确指出什么 MQ 使用了什么模型。 183 | 184 | 答:采用拉模型的核心原因在于,消费者的消费速率不同。在拉模型之下,消费者自己消费完毕就自己再去拉去一批,那么这种速率是由消费者自己控制的,所需要的控制信息也是由消费者自己保存的。而采用推模型,就意味着中间件要和消费者就速率问题进行协商,否则容易导致要么推送过快,要么推送过慢的问题。 185 | 186 | 推模型的一个极大的好处是避免竞争,例如在多个消费者拉同一主题的消息的时候,就需要保证,不同消费者不会引起并发问题。而 Kafka 不会有类似的问题,因为 Kafka 限制了一个 Partition 只能有一个消费者,所以拉模型反而更加合适。 187 | 188 | 关键字:谁控制,并发竞争 189 | 190 | ### 分区过多会引起什么问题? 191 | 192 | 分析:这应该属于大厂题,因为一般的公司是不会遇到这种问题的。那么分区过多会引起什么问题,要从 Producer、Consumer 和 Broker 三者考虑。注意这里说的分区过多,一般都是指主分区本身就很多,而不是指我一个主分区有一千个从分区。后者,要从 ISR 的角度去分析。不过基本上不会面这个问题,面到了就怼回去,谁家的主分区会有一千个从分区。 193 | 194 | 答:对于 Producer 来说,它采用的是批量发送的机制,那么分区数量多的话,就需要消耗大量的内存来维护这些缓存的消息。同时,也增大了数据丢失的风险。 195 | 196 | 对于 Consumer 来说,分区数量多意味着要么部署非常多的实例,要么开启非常多的线程,无论是哪一种方案,都是开销巨大。 197 | 198 | 对于 Broker 来说,分区特别多而对应的 Broker 数量又不足的话,那么意味着一个 Broker 上分布着大量的分区,那么一次宕机就会引起 Kafka 延时猛增。同时,每一个分区都要求 Broker 开启三个句柄,那么会引起 Broker 上的文件句柄被急速消耗,可能导致程序崩溃。还要考虑到,Kafka 虽然采用了顺序写,但是这是指在一个分区内部顺序写,在多个分区之间,是无法做到顺序写的。 199 | 200 | (注意,对于 Broker 来说,如果你的集群规模非常大,以至于虽然有一万个分区,但是每个 Broker 上只有寥寥几个分区,那么分区数量对 Broker 来说是没影响的。我们这里的讨论,都是建立在我一个 Broker 上放了很多分区的基础上) 201 | 202 | (分区数量和性能的关系类似一个二次函数,随着分区增长会慢慢变好,但是到达一个临界点之后,就会开始衰退) 203 | #### 类似问题 204 | - 分区数量是不是越多越好?显然不是; 205 | - 分区数量越多,是不是吞吐量越高?显然不是; 206 | - 能不能通过增加分区数量来提高 Kafka 性能?注意,这个是可以的,但是要注意把握度,就是不能无限增加; 207 | - Topic 过多会引起什么问题?其实差不多是同一个问题,Topic 多意味着分区多,而且通常伴随的是每个 Topic 的数据量都不大; 208 | 209 | ### 如何解决 Topic 的分区数量过多的问题? 210 | 211 | 分析:这个问题其实挺无解的。因为如果你确实有那么多消息需要消费,或者说写入压力很大的话,你就需要那么多分区。所以你很难说我削减分区。不过有一些措施能够缓解这个问题。 212 | 213 | 答:增加 Broker,确保 Broker 上不会存在很多的分区。这可以避免 Broker 上文件句柄数量过多,顺序写退化为随机写,以及宕机影响范围太大的问题。 214 | 215 | 其次可以考虑拆分 Topic 并且部署到不同的集群。(这里要注意,Topic 如果拆了但是没有增加 Broker,也没有部署额外的 Kafka 集群,那么其实还是没啥用) 216 | 217 | 当然,如果分区的写入负载其实并不大,那么可以考虑削减分区的。(尝试引出削减分区的话题,这是一个鱼钩,因为kafka本身不支持削减分区) 218 | 219 | #### 类似问题 220 | - 如何解决 Topic 太多的问题?这个问题稍微有点不同,我们考虑的就不是拆分 Topic 而是合并 Topic了。增加 Broker 有点效果,但是没有分区数量多那么有效。核心就在于, Topic 多伴随的都是每个 Topic 数据不多。 221 | 222 | #### 如何引导 223 | - 聊到了分区数量是不是越多越好 224 | 225 | ### 如何确定合适的分区数量? 226 | 227 | 分析:典型的计算容量题。所不同的是,分区数量会影响两端,因此要同时考虑 Producer 的效率和 Consumer 的效率。 228 | 229 | 答:使用 Kafka 提供的压测工具来测试。一般来说,我们对于某个特定的 Topic,其消息大小是能够从业务上推断出来的,也就是我们不存在说一个 Topic,某些消息特别长,某些消息特别短。大部分的消息长度都在相差不多的范围内。 230 | 231 | 因此我们可以控制写入一个分区的 TPS,观察同步延时和消息是否积压(消费端的消费数据,例如99线等也可以)。 232 | 233 | #### 类似问题 234 | - 如何确定消费者数量?要注意,消费者最多最多就是和分区数量一样,其它就是压测了。 235 | 236 | #### 如何引导 237 | - 聊到了分区数量过多的问题 238 | 239 | ### 如何保证消息有序性?方案有什么缺点? 240 | 241 | 分析:考察分区内部有序的特点。消息被投递到某个分区里面,是有序的,但是分区之间是没有顺序的。回答这道题,还要回答出来一个要点,即这并不意味着我们只能使用一个分区,而是可以考虑在发消息的时候主动指定分区,确保业务上要求顺序的消息都被投递到同一个分区中。 242 | 243 | 答:Kafka 要做到消息有序,只需要将消息都投递到同一个分区里面。因为 Kafka 的设计确保了一个分区内部的消息是有序的。但是,这并不是说,我们只能拥有一个分区,而是我们可以从业务上,将相关的消息都扔到了一个分区。例如按照用户 ID 来选择分区,确保用户相关的某些消息都在同一个分区内部。(点出缺点)类似的方案都要注意分区负载,例如热点用户产生了大量的消息,都被积压在该分区。 244 | 245 | #### 类似问题 246 | - Topic 为了保证消息有序性,我们会考虑只使用一个 Partition,你有什么改进方案? 247 | 248 | ### Kafka能不能重复消费? 249 | 250 | 分析:这个问题,其实不是指我们之前提到的,消费者如何避免重复消费中的那种因为超时引起的重复消费。面试官想问的是,我能不能主动重复消费。比如说,我程序有BUG,消费的时候出错了。等我修复完 BUG 之后,我打算重新消费一遍,有没有办法做到。所以准确来说是,消费者能不能消费历史上的消息。这里刷亮点的在于点出,消息保存时间,因为超过消息保存时间,就真找不着了。 251 | 252 | 答:能。Kafka 的分区用 offset 来记录消费者消费到哪里了。因此我们可以考虑指定 offset 来消费,比如指定一个很久之前的 offset。 253 | 254 | 一些场景之下,我们会更加倾向于指定时间节点,那么可以先根据时间戳找到 offset,然后再从 offset 消费。 255 | 256 | 不过要注意的是,有些时候,这些消息可能已经被归档(删除——一般都不会直接删除,而是丢到一个别的地方放起来,以防万一)了,那么这一类的消息,就确实是没法子重复消费了。 257 | 258 | ### Rebalance 发生时机 259 | 分析:考察 Rebalance 的基本特性。Rebalance 本质就是给消费组的消费者分配任务的过程。记住这个本质之后,那么 rebalance 的过程触发时机,就是 Topic、分区和消费者三个人的事情了。举个例子,就好比分苹果,无论是苹果数量,还是小朋友数量发生了变化,你都要重新来一遍。问这个问题,基本上是为了引出 rebalance 的过程,以及 rebalance 造成的影响。 260 | 261 | 答: 262 | 1. Topic 或者分区的数量变化(苹果数量变化,例如增加新的分区) 263 | 2. 消费者数量变化(加入或者退出)。这个又可以细分为两个:一个是消费超时(max.poll.interval.ms),一个是心跳超时(session.timeout.ms) 264 | 265 | 类似问题 266 | - 什么时候会 Rebalance? 267 | - 消费者加入或者退出会有啥影响? 268 | - 扩容(指增加分区)会有什么影响?这两个都是引起 rebalance 269 | 270 | ### rebalance 的过程 271 | 分析:典型的过程题目。一般来说,结合具体的场景来回答步骤,容易记忆也容易理解。这里我们可以围绕着因为消费者变化引起的 rebalance 来回答。实际上,不同原因引起的 rebalance 过程是有一些差异的,不过这些差异不涉及根本,所以没必要在这里纠结。如果能够根据不同情况来回答步骤,那自然是好的。这里就是简化一下。 272 | 273 | 答:以新的消费者加入为例,这个步骤可以分成以下几步: 274 | 1. 新的消费者向协调者上报自己的订阅信息; 275 | 2. 协调者强制别的消费者发起一轮 rebalance,上报自己的订阅信息; 276 | 3. 协调者从消费者中挑选一个 leader,注意这里是挑选了消费者中的 leader; 277 | 4. 协调者将订阅信息发给 leader,让 leader 来制作分配方案; 278 | 5. leader 上报自己的方案; 279 | 6. 协调者同步方案给别的消费者 280 | 281 | (更加简单的记忆方式是:挑选 leader -> leader 出方案 -> 同步方案,就很像一堆同事说我来搞负责解决这个问题,然后老板挑了一个卷王,说你出个方案,老板看了方案很满意,交代其它同事说按照这个方案执行) 282 | 283 | ### rebalance 有啥影响 284 | 285 | 分析:结合前面的 rebalance 的过程,也可以看到,rebalance 对消费者的影响是最大的,因为在 rebalance 的过程中,都不能消费。前面的一大堆的 rebalance 问题,实际上对于一个增删改查工程师来说,就是为了引出这个话题。 286 | 287 | 答: 288 | 1. 重复消费:如果在消费者已经消费了,但是还没提交,这个时候发生了 rebalance,那么别的消费者可能会再一次消费; 289 | 2. 影响性能:rebalance 的过程,一般是在几十毫秒到上百毫秒。这个过程会导致集群处于一种不稳定状态中,影响消费者的吞吐量; 290 | 291 | #### 如何引导 292 | - 在前面聊到过程,就可以主动聊有什么影响 293 | 294 | ### 如何避免 rebalance? 295 | 分析:前面我们说过,要么是 Topic 或者分区变化引起,要么是消费者变化引起,这两个都会导致 rebalance。所以如何避免,就是如何避免出现这两种变化。 296 | 297 | 答:首先,Topic 或者分区变化,引起 rebalance 是无法避免的,因为一般都是因为业务变化引起的。比如说,随着流量增加,我们要增加分区。 298 | 299 | 能够避免的就是防止消费者出现消费超时或者心跳超时。消费超时可以增大`max.poll.interval.ms` 参数,避免被协调者踢掉。或者优化消费逻辑,使得消费者能够快速消费,拉取下一批消息。 300 | 301 | 而心跳超时,也可以通过增大`session.timeout.ms`来缓解。 302 | 303 | (可以进一步分析这两个参数增大的弊端) 304 | 但是这两个参数增大,都可能导致,消费者真的出了问题,但是协调者却迟迟没有感知到的问题。 305 | 306 | #### 如何引导 307 | - 聊到 rebalance 的影响 308 | 309 | ### 为什么 Kafka 不支持减少分区? 310 | 311 | 分析:Kafka 增加分区是可以的,但是减少分区是不能的。这个问题,只要想一下,减少分区要怎么实现,就能得出结论。它的核心难点是,减少的这个分区上的数据怎么处理?比如说,能不能分给别的分区,要分怎么分?对应的消费者怎么处理?在回答了为什么不能减少之后,给出一个可能的解决方案。 312 | 313 | 答:主要还是在于,难以处理分区上的数据。假如说,我们要支持 Kafka 支持减少分区,那么我们就要考虑第一个问题,这个分区上的数据,该怎么办?大多数情况下,我们不能直接丢掉,那么只能考虑重新分配给其它的分区。于是就涉及到,如何分配,以及对其余分区的影响的问题了。 314 | 315 | 总体来说,减少分区的复杂度,远比增加分区的复杂度大,但是收益是小的。一方面,有别的手段来解决类似的问题,另一方面,大多数的场景,都是增加分区,而不是减少分区。 316 | 317 | 假如我们要实现类似的功能,可以考虑两种方案: 318 | 1. 创建一个完全一样的 Topic,然后分区数量少一点,等老的 Topic 消费完就直接下线,只留下这个新的 Topic; 319 | 2. 考虑在写入分区的时候,不再写入特定的分区,可以通过业务来控制,也可以通过负载均衡机制来控制;其缺点是,这个没用的分区会长期存在,并没有在事实上删除它; 320 | 321 | 322 | ## References 323 | [图解Kafka高可用机制](https://zhuanlan.zhihu.com/p/56440807) 324 | 325 | [Kafka高性能原理](https://zhuanlan.zhihu.com/p/105509080) 326 | 327 | [Why Kafka is fast](https://preparingforcodinginterview.wordpress.com/2019/10/04/kafka-3-why-is-kafka-so-fast/) -------------------------------------------------------------------------------- /mq/README.md: -------------------------------------------------------------------------------- 1 | # 消息队列 2 | 3 | 分析:如果不考察特定的消息中间件,那么就是考察一般的消息队列的理论。 4 | 5 | ![消息队列概览](./img/mq_overview.jpeg) 6 | 7 | 基本上就是围绕以上这些考点。因为不考察具体的消息中间件的原理,反而不太好回答,因为这些问题都是要在实际中遇到过,才能有比较深刻的体会。所以下面的很多回答,都是使用了我自己的例子,读者要进行相应的替换,提前准备好。 8 | 9 | 要理解这些问题,我们要先理解分布式调用语义: 10 | 11 | 1. 至少一次语义:是指消费者至少消费消息一次。这意味着存在重复消费的可能,解决思路就是幂等; 12 | 2. 至多一次语义:是指消费者至多消费消息一次。这意味着存在消息没有被消费的可能,基本上实际中不会考虑采用这种语义,只有在日志采集之类的,数据可以部分缺失的场景,才可能考虑这种语义; 13 | 3. 恰好一次语义:最严苛的语义,指消息不多不少恰好被消费一次; 14 | 15 | 绝大多数情况下,我们追求的都是**至少一次**语义,即生产者至少发送一次,可能重复发送;消费者至少消费一次,可能重复消费(虽然去重了,但是我们也认为消费了,只不过这个消费啥也没干)。结合之下,就能发现,只要解决了消费者重复消费的问题的,那么生产者发送多次,就不再是问题了。 16 | 17 | 有时候面试官也会将这种去重之后的做法称为“恰好一次”,所以面试的时候要注意一下,是可以用重试+去重来达成恰好一次语义的。 18 | 19 | 理解了这些,我们就解决了数据一致性的问题,即生产者一定发出去了消息,或者消费者一定消费了消息。 20 | 21 | 还有一些问题,我们会在具体的消息队列中间件上讨论,例如如何保证高可用,如何保证高性能。这些都是具体消息中间件相关的。 22 | 23 | 关键字:**一致性**,**幂等**,**顺序** 24 | 25 | ![大概模式](./img/overview.jpeg) 26 | 27 | ## 问题 28 | 29 | ### 你用消息队列做什么? 30 | 31 | 分析:也就是消息队列的一般用途。这个要结合自己的实际经验来回答,重点强调一下某个方面。尤其是削峰,这种涉及处理高并发大数据的点。千万要记得提前准备例子,用来支撑你的说明。 32 | 33 | 答案: 34 | 35 | 1. 解耦:将不同的系统之间解耦开来。尤其是当你在不希望感知到下游的情况。(后面我们用一个一对多的项目来进一步说明,这是一个很常见的场景,可以自行替换自己的例子)通常而言,当我需要对接多个系统,需要告知我的某些情况的时候,但是我又不知道究竟有多少人关心,以及他们为啥关心的时候,就会考虑采用消息队列来通信。(这个例子是我的例子)例如我们退款,会用消息队列来暴露我们的退款信息,比如说退款成功与否。很多下游关心,但是实际上,我们退款部门根本不关心有谁关心。在不使用消息队列的时候,我们就需要一个个循环调用过去; 36 | 37 | 2. 异步:是指将一个同步流程,利用消息队列来拆成多个步骤。(下面是我准备的一个刷亮点的方面,我们从事件驱动设计上来讨论)这个特性被广泛应用在事件驱动之中。(下面是我退款的例子)比如说在退款的时候,退款需要接入风控,多个款项资金转移,这些步骤都是利用消息队列解耦,上一个步骤完成,发出事件来驱动执行下一步; 38 | 39 | 3. 削峰:(这是最大的考点,而且如果你的简历里面有类似电商之类的经历,那么就很可能追问下去,接连考秒杀啥的)削峰主要是为了应对突如其来的流量。一般来说,数据库只能承受每秒上千的写请求,如果在这个时候,突然来了几十万的请求,那么数据库可能就会崩掉。消息队列这时候就起到一个缓冲的效果,服务器可以根据自己的处理能力,一批一批从消息队列里面拉取请求并进行处理。 40 | 41 | 关键字:**解耦**,**异步**,**削峰** 42 | 43 | ![直接调用](./img/decouple1.jpeg) 44 | 45 | ![依赖中间件解耦](./img/decouple2.jpeg) 46 | 47 | #### 类似问题 48 | 49 | - 消息队列有什么作用 50 | - 你为什么用消息队列 51 | - 为什么不直接调用下游,而要使用消息中间件?这个基本上就是回答解耦,异步也勉强说得上,不过要点在解耦。 52 | - 或者,面试官直接问道三个特性的某个,你是如何使用的 53 | 54 | #### 如何引导 55 | 56 | - 讨论到秒杀 57 | - 讨论到事件驱动 58 | 59 | ### 消息队列有什么缺点? 60 | 61 | 分析:典型的反直觉题。因为我们只会说消息队列怎么怎么好,很少有人会思考使用消息队列会带来什么问题。 62 | 63 | 答: 64 | 65 | 1. 可用性降低:引入任何一个中间件,或者多任何一个模块,都会导致你的可用性降低。(所以这个其实不是MQ的特性,而是所有中间件的特性) 66 | 2. 一致性难保证:引入消息队列往往意味着本地事务不可用,那么就容易出现数据一致性的问题。例如业务成功了,但是消息没发出去; 67 | 3. 复杂性上升:复杂性分两方面,一方面是消息队列集群维护的复杂性,一方面是代码的复杂性 68 | 69 | (升华主题)几乎所有的中间件的引入,都会引起类似的问题。 70 | 71 | 关键字:**可用性**,**一致性**,**复杂性** 72 | 73 | #### 如何引导 74 | - 前面说完消息队列的好处之后,直接就可以接缺点 75 | 76 | ### 消息中间件你用了什么?为什么选择它? 77 | 78 | 分析:这个问题这里只能给出一般的答题思路。这是典型的[为什么使用 A?](../pattern/README.md)的问题。所以我们延续这种思路来回答。 79 | 80 | 答案: 81 | 82 | 1. 你所了解的消息中间件有哪些; 83 | 2. 你所使用的是哪个,具有什么特点; 84 | 3. 拿另外一个消息中间件做对比。如果你知道面试官所在的公司,用的是什么消息中间件,那么就用他使用的来做对比。如果你不知道,建议使用`Kafka`; 85 | 4. 你的业务特征。从业务特征推导出来,你的选型是合适的。这里要注意的是,对于一般的业务来说,可能使用什么消息中间件没有本质的区别。这种情况下,不必硬找原因,自己说点自己的理解就可以了,比如说文档全,社区完善,之前用过啥的; 86 | 5. (可选)这一步,如果你了解你们公司的集群是如何部署的,比如说`Kafka`是怎么部署的,有多少`Partition`,你可以接着聊。如果你们的消息队列启用了某种特殊的功能,务必说出来,并且给出使用理由; 87 | 6. (可选)不足之处,目前你们的消息队列面临什么的问题,你有什么改进的想法。一般来说,如果你只是一个增删改查工程师,那么这方面你可能了解不多,那可以从自己的使用经历来说,比如说出过什么问题; 88 | 89 | ### 如何保证消息消费的幂等性? 90 | 91 | 分析:这个问题有别于“如何保证消息只会发送一次”。消息消费幂等,意味着发送方可能发送了多次,或者消费中间出了什么问题,导致了重复消费。单纯的消息中间件并不能保证。例如,当网络超时的时候,中间件完全不知道,消费者消费了没有,成功还是失败。如果不重试,那么就可能没消费;如果重试,就可能重复消费。这里有些人可能会回答`ACK`机制,其实这是不对的,`ACK`机制并不能保证幂等。因为你`ACK`了,你并不能确保中间件一定能收到。看后面**什么情况导致重复消费**。 92 | 93 | ![ACK 机制无法确保幂等性](./img/ack_timeout.jpeg) 94 | 95 | 答案:保证幂等性,主要依赖消费者自己去完成。一般来说,一条消息里面都会带上标记这一次业务的某些特征字段。核心就是利用这些字段来去重。比如说,最常见的是利用数据库的唯一索引来去重,要小心的就是,采用 `check - doSomething` 模式可能会有并发问题。 96 | 97 | 另外一种就是利用 Redis。因为你只需要处理一次,所以不必采用分布式锁的模式,只需要将超时时间设置得非常非常长。带来的不利影响就是Redis会有非常多的无用数据,而且万一真有消息在 Redis 过期之后又发过来,那还是会有问题。 98 | 99 | #### 类似问题 100 | 101 | - 如何保证消息只会被消费一次? 102 | - 如何保证消息消费恰好一次语义? 103 | 104 | 关键字:**去重**,**Redis Set**, **唯一索引** 105 | 106 | ### 什么情况导致重复消费? 107 | 108 | 分析:理解什么情况会导致重复消费,就能够理解如何保证消息一定会发出来,以及如何确保消息肯定会被发送。回答这个问题要从生产者、消息中间件、消费者三个层面上来说。 109 | 110 | 答案: 111 | 112 | 1. 从生产者到消息中间件之间,生产者可能重复发送。例如生产者发送过程中出现超时,因此生产者不确定自己是否发出去了,重试; 113 | 2. 消息中间件到消费者,也可能超时。即消息中间件不知道消费者消费消息了没有,那么重试就会引起重复消费。消息中间件不知道消费者消费消息了没有,又有两种子情况: 114 | 115 | 2.1 消息传输到消费者的时候超时; 116 | 117 | 2.2 消费者确认的时候超时; 118 | 119 | 关键字:**超时** 120 | 121 | ### 如何保证生产者只会发送消息一次? 122 | 123 | 分析:这个就很难做到只发送一次。这个问题,可以拆成两个问题:如何保证消息一定发出去了?其次是如何保证只发了一次? 124 | 125 | 第一个问题:如何保证消息一定发出去了?这个问题一般是指这么一个场景,我前面做了一大堆业务操作,最终我需要发一个消息。比如说我前面创建一个订单,我后面一定要发出去一个消息。这本质上是一个分布式事务问题,所以我们可以考虑用分布式事务来解决;其次还可以考虑用重试来确保。 126 | 127 | 答案:这个问题,可以拆成两个问题:如何保证消息一定发出去了?其次是如何保证只发了一次? 128 | 129 | 对于第一个问题来说,可以考虑分布式事务,或者重试机制。 130 | 131 | 开启分布式事务需要消息中间件的支持。 132 | 133 | 超时机制,核心就是超时处理 + 查询。如果在消息发送明确得到了失败的响应,那么可以考虑重试,超过重试次数就需要考虑人手工介入。 134 | 135 | 另外一种是超时处理,即你也不知道究竟成功了没。为了防止这种问题,可以考虑在发送消息之前,插入一条数据库待发送消息记录,这个插入要和前面的业务逻辑绑在一起作为一个本地事务。在发送成功、失败或者超时都标记对应的记录。带来的问题就是增加数据库的负担,并且后面更新记录的时候,依旧可能失败,最终还是无法保证生产一次。 136 | 137 | 而后开启一个定时任务,扫描超时的记录,然后重新发送。如果消息中间件支持查询,那么可以考虑查询一下上一次的究竟成功没有,否则就只能直接重试。 138 | 139 | 第二个问题:如何确保只发送一次? 140 | 141 | 从前面来看,分布式事务天然就能保证只发送一次。而超时机制,则完全无法保证。 142 | 143 | (升华主题)其实我们追求的并不是消息恰好发送一次,而是消息至少发送一次,依赖于消费端的幂等性来做到恰好一次语义。 144 | 145 | #### 类似问题 146 | 147 | - 如何保证消息一定发出去了? 148 | - 如何保证消息的可靠性传输? 149 | - 如何保证消息的数据一致性?分生产者和消费者两方来回答 150 | - 能不能依赖于消息ID来做重复消费的去重? 151 | 152 | #### 如何引导 153 | - 聊到如何确保消费一次。 154 | 155 | ### 如何保证消息顺序 156 | 157 | 分析:这个问题,说实在的,是一个很巨大的问题。它可能指: 158 | 159 | 1. AB两台机器,A机器在实际上先于B机器发出来的消息,那么消费者一定先消费 160 | 2. 同一个业务(例如下单),先发出来的消息(例如创建订单消息)一定比后面发的消息(例如支付消息)先被消费 161 | 3. 不同业务,先发出来的消息的(例如支付消息)一定比后面发的消息(例如退款)先被消费。(实际中,支付和退款差不多都是分属两个部门) 162 | 163 | 第一个问题,涉及的是时钟问题,可以忽略,除非面到了中间件设计中时序处理的问题。 164 | 165 | 第二个问题,可以理解为同一个主题(topic),前后两条消息。这是一般意义上的消息顺序。 166 | 167 | 第三个问题,可以理解为不同主题(topic),前后两条消息。这是业务上的消息顺序。 168 | 169 | 所以,我们的回答就围绕:同一个业务的消息,如何保证顺序。 170 | 171 | 如何保证顺序,有两方面的要求,一方面是投递的顺序,一方面是消费的顺序。同一个业务的消息可能在不同机器上产生,那么投递的顺序,要是正确的;同一个业务的消息,可能被多个消费者消费,那么它们被消费的时候,也要是顺序的。 172 | 173 | 这又可以分成两个方向回答,一个是消息中间件天然支持,一个是中间件没法支持,只能自己搞点东西来支持,做法类似于 TCP 的滑动窗口协议,要停下来等待前面的消息都到了,再丢出来。 174 | 175 | 等到具体的中间件的时候,我们会进一步讨论这个问题。 176 | 177 | 回答:一般原则上,保证消息顺序需要保证生产者投递过来的顺序是对的,消费者消费的顺序也是对的。大多数情况下,我们可以让相关消息都发送到同一个队列里面。例如,对于`Kafka`而言,我们可以让要求顺序的消息,都丢到同一个`Partition`里面。重要的是,`Kafka`的机制保证了,一个`Partition`只会有一个线程来消费,从而保证了顺序。 178 | 179 | (要补充说明,`Partition`可以替换为队列等你所用的中间件的类似的概念)这里指的同一个`Partition`是指,相关的消息都到一个`Partition`,而不是指整个`topic`只有一个`Partition`。 180 | 181 | (补充缺点)带来的问题是要慎重考虑负载均衡的问题,否则容易出现一个`Partition`拥挤的问题。而且它还限制了,同一个`Partition`只能被一个线程消费,而不能让一个线程取数据而后提交给线程池消费。 182 | 183 | 有些时候,我们需要自己重新排序收到的消息,比如说消息中间件不支持指定发送目的地(队列),或者消息属于不同的主题。(属于不同主题,就只能用这个方法) 184 | 185 | 在收到乱序之后的消息,可以选择两种做法: 186 | 187 | 1. 将消息转储到某个地方,例如扔过去数据库,放到`Redis`或者直接放在内存里面; 188 | 2. 让消息中间件过一段时间再发过来,期望再发过来的时候,前置消息已经被处理过了; 189 | 190 | 每次收到消息,都要检查是不是可以处理这个消息了,可以的话才处理。 191 | 192 | (要考虑周详,容错问题)这种问题下,可能出现的是,某个前置消息一直没来,那么应该有一个告警机制。还要考虑到,这种已经被处理过的消息,是可以被清理掉的,以节省资源。 193 | 194 | 而如果我们要求不同主题的消息有顺序,例如某个业务会依次产生 A,B 两个消息,它们属于不同主题。那么这种情况下,第二种方法才能达到保证消息顺序的目的。 195 | 196 | #### 类似问题 197 | - 如何保证业务消息的顺序?要根据消息是否属于同一主题来回答,两个方面都要回答 198 | - 如何保证管理消息的依赖性? 199 | - 如何确保某个消息一定会先于别的消息执行? 200 | 201 | #### 如何引导 202 | 203 | - 在谈论到`Kafka`的`Partition`的时候(或者讨论到类似的东西的时候,可以简单提及可用这种性质来解决顺序问题); 204 | 205 | ### 消息积压怎么办? 206 | 207 | 分析:消息积压,核心就在于生产者太快而消费者太慢。解决思路要么控制生产者的发送速率,要么提高消费者的效率。一般我们不太会倾向于控制发送者的速率,所以解决问题的思路就变成了如何提高消费者效率。提高消费者的效率,要么提高消费单条消息的效率,要么是增加消费者的数量。 208 | 209 | ![消息积压解决思路](./img/too_many_msg.jpeg) 210 | 211 | 答案:整体上有两个思路: 212 | 213 | 1. 增加集群规模。不过这个只能治标,缓解问题,但是不能解决问题; 214 | 2. 加快单个消息的消费速率。例如原本同步消费的,可以变成异步消费。把耗时的操作从消费的同步过程里面摘出去; 215 | 3. 增加消费者。例如`Kafka`中,增加`Partition`,或者启用线程池来消费同一个`Partition`。 216 | 217 | (刷亮点)其实消息积压要看是突然的积压,即偶然的,那么只需要扩大集群规模,确保突然起来的消息都能在消息中间件上保存起来,就可以了。因为后续生产者的速率回归正常,消费者可以逐步消费完积压的消息。如果是常态化的生产者速率大于消费者,那么说明容量预估就不对。这时候就要调整集群规模,并且增加消费者。典型的就是,`Kafka`增加新的`Partition`。 218 | 219 | #### 类似问题 220 | 221 | - 生产者发送消息太快怎么办? 222 | - 消费者消费太慢怎么办? 223 | - 怎么加快消费者消费速率? 224 | 225 | ### 如何实现延时消息? 226 | 227 | 分析:其实这是一个消息队列相关的问题。有些消息队列天然就支持延时消息,有些消息队列就是不支持。这里我们当然只讨论不支持延时消息的消息队列。这也可以从两个角度来说,一个是生产者延时发送,一个是消费者延时消费。从核心上来说,延时消息就是消费者要在一段时间后消费,至于是生产者先发,消费者存着不消费,还是生产者直接等到时间点再发,就是看需求和设计了。 228 | 229 | 答案:在消息中间件本身不支持延时消息的情况下,大体上有两种思路: 230 | 231 | 第一种思路是消息延时发送。生产者知道自己的消息要延时发送,可以考虑先存进去代发消息列表,而后定时任务扫描,到达时间就发送;也可以生产者直接发到一个特殊的主题,该主题的消费者会存储下来。等到时间到了,消费者再投递到准确的主题; 232 | 233 | 第二种思路是消息延时消费。消费者直接收到一个延时消息,发现时间点还没到,就自己存着。定时任务扫描,到时间就消费。 234 | 235 | 如果是消费者和生产者自己存储延时消息,那么意味着每个人都需要写类似的代码来处理延时消息。所以比较好的是借助一个第三方,而第三方的位置也有两种模式: 236 | 1. 第三方位于消息队列之前,第三方临时存储一下,后面再投递; 237 | 2. 第三方位于某个特殊主题之后,生产者统一发到该特殊主题。第三方消费该主题,临时存储,而后到点发送到准确主题; 238 | 239 | 240 | -------------------------------------------------------------------------------- /mq/img/ack_timeout.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/ack_timeout.jpeg -------------------------------------------------------------------------------- /mq/img/decouple1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/decouple1.jpeg -------------------------------------------------------------------------------- /mq/img/decouple2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/decouple2.jpeg -------------------------------------------------------------------------------- /mq/img/delay_third_directly.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/delay_third_directly.jpeg -------------------------------------------------------------------------------- /mq/img/delay_third_using_mq.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/delay_third_using_mq.jpeg -------------------------------------------------------------------------------- /mq/img/kafka_available_performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/kafka_available_performance.png -------------------------------------------------------------------------------- /mq/img/mq_overview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/mq_overview.jpeg -------------------------------------------------------------------------------- /mq/img/overview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/overview.jpeg -------------------------------------------------------------------------------- /mq/img/too_many_msg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/mq/img/too_many_msg.jpeg -------------------------------------------------------------------------------- /pattern/README.md: -------------------------------------------------------------------------------- 1 | # 答题思路 2 | 3 | 这是我总结的一些答题思路。这种思路跟具体的知识点没有关系,只是一种组织答案的手段。 4 | 5 | 其实,本质上来说,这些答题思路,实际上也就是我日常分析问题的思路 6 | 7 | ## 为什么使用A? or 为什么不使用 A 8 | 9 | 这是一个最常见的模式,一般是面试官问你,你为什么使用了某项技术。举例来说,消息队列属于百家争鸣,于是很可能面试官就是要问你“为什么你用`Kafka`?”。 10 | 11 | 有些同学要懵逼了,我咋知道,我进来就是用`Kafka`。 12 | 13 | 这显然是陷入误区了。这个问题,面试官其实不是想问你为什么选择了`Kafka`,而是你是否了解`Kafka`的特性。所以,你的回答就应该是,`Kafka`有什么特性,别的消息中间件有什么特性,你的业务有什么特性,所以你选择`Kafka`。 14 | 15 | 回答类似于这种“你为什么不用A”或者“你为什么用A”,答题的思路都是: 16 | 1. A 有什么特点 17 | 2. 类似产品 B C D 有什么特点 18 | 3. 我的业务,或者我的场景是什么特点 19 | 4. 所以我选择 A 20 | 21 | 这个思路其实也就是一般做技术调研的思路。 22 | 23 | 这里有一个小技巧,如果你清楚知道你面试那家公司用的是哪个产品,那么你就要深入学习这个产品,而后用来和你的选择做对比。举例来说,你们用的是`Kafka`,然后你了解到面试官公司用的是`ActiveMQ`,那么你在回答“为什么使用`Kafka`”的时候,就重点和`ActiveMQ`作为对比。 24 | 25 | 26 | ## 你是如何解决X问题的? 27 | 28 | 这个问题也很常见。一般是为了考察你在解决问题的时候,有没有权衡和思考。很多同学会说,这个方案是大佬设计的,我哪里知道? 29 | 30 | 不好意思,面试官就是想让你说出来,大佬是如何决策的。 31 | 32 | 其实这个问题和 `为什么使用A` 是类似的,或者说差不多的。不过就是话术上要转变一下。 33 | 34 | 一般的思路是: 35 | 1. 业界的一般做法,有 A B C,它们都有啥特点 36 | 2. 我的场景是怎样的 37 | 3. 所以选择 A 38 | 39 | 这里面比较能刷出亮点的是,横向比较某两个候选方案。注意的是,这个候选方案是你排除了明显不合理之后,看起来都行的方案,然后你要着重描述你选择某个方案的理由。 -------------------------------------------------------------------------------- /redis/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/redis/.DS_Store -------------------------------------------------------------------------------- /redis/availability.md: -------------------------------------------------------------------------------- 1 | # Redis 高可用 2 | 3 | 分析:Redis 高可用分成两个点,Redis Cluster 和 Redis Sentinel。这里要区别两种模式解决的痛点: 4 | - Redis Sentinel 是纯粹的高可用方案,采用的是主从复制; 5 | - Redis Cluster 部署的是对等(peer-to-peer)节点,解决了高可用问题,同时,也解决了**单机瓶颈**; 6 | 7 | 这里面有一个很容易误解的点,就是 Redis Cluster 里面的每个节点,都可以是一个是**主从**集群,在高端操作下,Redis Cluster 就是由多个主从集群构成的。 8 | 9 | 所以,Redis Cluster 解决高可用是要从两个角度来回答的,一个是对等节点,一个是节点是主从集群。另外一个考点在于,Cluster 的槽分配和迁移。迁移理解了,就理解了扩容和缩容。 10 | 11 | Redis Sentinel 模式要注意有两个集群,一个是存放了 Redis 数据的集群,一个是监控这个数据集群的哨兵集群。于是就需要理解哨兵集群之间是如何监控的,如何就某件事达成协议,以及哨兵自身的容错。 12 | 13 | ![Redis 高可用](./img/availability.png) 14 | 15 | 答案:Redis 高可用有两种模式,Sentinel 和 Cluster。 16 | 17 | Sentinel 本质上是主从模式,与一般的主从模式不同的是,主节点的选举,不是从节点完成的,而是通过 Sentinel 来监控整个集群模式,发起主从选举。因此本质上 Redis Sentinel 有两个集群,一个是 Redis 数据集群,一个是哨兵集群。 18 | 19 | Redis Cluster 集成了对等模式和主从模式。Redis Cluster 由多个节点组成,每个节点都可以是一个主从集群。Redis 将 key 映射为 16384 个槽(slot),均匀分配在所有节点上。(这里有两个点,先不说,一个是怎样的均匀才是均匀,一个是客户端怎么查询,坐等面试官问) 20 | 21 | 两种模式下的主从同步都有全量同步和增量同步两种(引导面试官询问两种同步模式细节),一般情况下,我们应该尽量避免全量同步(钓鱼,面试官接着就会问为什么,或者全量同步有啥缺点,或者如何避免) 22 | 23 | (简单提及一下如何选择) 24 | 一般而言,如果数据量和复杂并不大的时候,想要保证高可用,就采用 Redis Sentinel;如果负载很大,或者说触及了 Redis 单机瓶颈,那么应该采用 Redis Cluster 模式。 25 | 26 | #### 相关问题 27 | - 你了解 Redis Sentinel 模式么 28 | - 你了解 Redis Cluster 模式么 29 | 30 | 31 | ## 扩展点 32 | 33 | ### Redis 主从之间是如何同步数据的? 34 | 35 | 分析:考察一般的数据同步模式。这里要回答出亮点,就要回到全量同步和增量同步(PSYNC)。而实际上从服务器发起的 PSYNC 既可能触发全量同步,也可能触发增量同步,所以我们不用 PSYNC 术语,以免搞混。而要理解增量复制,首先就要理解全量复制。为了方便记忆,我们记住核心点,就是如果同步的起始点命令,还在主服务器的缓冲队列上,那就是增量同步,如果不在,那就是全量同步。 36 | 37 | 答案:分成两种,全量同步和增量同步。全量同步的步骤是: 38 | 1. 从服务器发起同步,主服务器开启 BG SAVE,生成 BG SAVE 过程中的写命令也会被放入一个缓冲队列; 39 | 2. 主节点生成 RDB 文件之后,将 RDB 发给从服务器; 40 | 3. 从服务器接收文件,**清空本地数据**,再入 RDB 文件;(这个过程会忽略已经过期的 key,参考过期部分的讨论) 41 | 4. 主节点将缓冲队列命令发送给从节点,从节点执行这些命令; 42 | 5. 从节点重写 AOF; 43 | 44 | 这时候已经同步完毕,之后主节点会源源不断把命令同步给从节点。 45 | 46 | ( 主生成 RDB 和 缓冲命令, 发给从,从加载 RDB,执行缓冲命令,重写 AOF ) 47 | 48 | (先分析这种全量同步面临的问题,而后引出增量同步) 49 | 从上面的步骤可以看出来,全量同步非常重,资源消耗很大,而且,大多数情况下,从服务器上是存在大部分数据的,只是短暂失去了连接。如果这个时候又发起全量同步,那么很容易陷入到无休止的全量同步之中。 50 | 51 | 因此 Redis 引入了增量同步。增量同步的依赖于三个东西: 52 | 1. 服务器ID:用于标识 Redis 服务器ID; 53 | 2. 复制偏移量:主服务器用于标记它已经发出去多少;从服务用于标记它已经接收多少(从服务器的比较关键); 54 | 3. 复制缓冲区:主服务器维护的一个 1M 的FIFO队列,近期执行的写命令保存在这里; 55 | 56 | 从服务器将自己的复制偏移量发给主服务器,如果主服务器发现,该偏移量还在复制缓冲区,那么就执行增量复制,将偏移量后面的命令同步给从服务器;否则执行全量同步; 57 | 58 | (其实就是,从服务器记录了一下自己同步到哪里,然后找主服务器同步,主服务器一看,这个数据还在缓冲区,ok,可以增量同步) 59 | 60 | #### 如何引导 61 | - 讨论到无论是 Cluster 还是 Sentinel 的主从同步的时候 62 | 63 | #### 相关问题 64 | - Redis 全量同步是如何进行的 65 | - Redis 的增量同步是如何进行的 66 | - 你了解 Redis 的复制缓冲区吗(or 复制积压缓冲区)? 67 | 68 | ### Redis 如何决定是使用全量同步还是增量同步? 69 | 70 | 分析:考察同步的知识点。前面提到过了一点,这里系统总结一下。其实从一般的认知里面去推断,也能推出来。 71 | 72 | 答案: 73 | 1. 从服务器发现自己从来没有同步过,那么执行全量同步; 74 | 2. 从服务器发起同步命令(PSYNC),但是主服务器发现从服务器上次同步的对象不是自己,(服务器ID不匹配),于是执行全量同步; 75 | 3. 从服务器发起同步命令(PSYNC),主服务器发现偏移量太古老了,数据已经不在复制缓冲区了,全量同步; 76 | 4. 从服务器发起同步命令(PSYNC),主服务器发现偏移量对应的数据还在复制缓冲区,执行增量同步; 77 | 78 | (上面记不住,就简单记忆下面这个) 79 | 当且仅当,从服务器从相同的主服务器里面同步,偏移量对应的命令还在缓冲区,执行增量同步。 80 | 81 | #### 类似问题 82 | - PSYNC 一定发起增量同步么?NO 83 | - 增大、缩小复制缓冲区有什么影响?影响你触发全量同步的概率 84 | 85 | ### Redis 服务器重启可能引发什么问题? 86 | 87 | 分析:考察同步。前面提到过服务器ID,这个ID准确说是服务器运行时ID,它在重启后会变化。而结合主从同步的问题,我们会发现,服务器运行时ID变化会触发全量同步。 88 | 89 | 答案:服务器重启,分成主服务器重启和从服务器重启。 90 | 91 | 对于从服务器来说,因为重启会使它丢失了上一次同步的主服务器的ID,所以只能发起全量同步; 92 | 93 | 对于主服务器重启来说,因为服务器ID发生变化,所有的从服务器都需要执行全量同步; 94 | 95 | (刷亮点,论述一种不会变更服务器ID的重启方式) 96 | 针对这种情况,Redis 引入了一种安全重启机制,这种机制下重启不会变更服务器ID,可以避免全量同步 97 | 98 | #### 如何引导 99 | - 聊到服务器ID的时候就可以 100 | - 聊到 Redis 性能调优 101 | - 聊到可能触发全量同步的情况 102 | 103 | #### 类似问题 104 | - 你了解安全重启么? 105 | - 服务器ID变更会出现什么问题? 106 | - 如何避免全量同步? 107 | 108 | ### Redis 主从之间网络不稳定可能引发什么问题? 109 | 110 | 分析:考察同步。大多数和主从有关的问题,几乎都是围绕全量同步来做文章的。网络连接不稳,导致主从同步失败。亮点在于,要结合 Redis 的超时机制来回答。 111 | 112 | 答案:主从之间网络不稳定可能引起三种情况: 113 | 1. 短暂网络抖动,那么从服务器可以通过 ACK 机制重新补充丢失的数据(参考后面的心跳机制); 114 | 2. 超时,但是从服务器发过来的偏移量还在缓冲区,增量复制; 115 | 3. 超时,偏移量不在缓冲区,全量复制; 116 | 117 | (为了方便记忆,把时间轴想象成三部分:未超时,超时但是复制缓冲还在,超时没救了) 118 | 119 | #### 如何引导 120 | - 心跳机制可以谈起 121 | - 聊到全量同步 122 | - 聊到避免全量同步 123 | 124 | ### 全量同步有什么缺点 125 | 126 | 分析:考察全量同步。核心在于领悟全量同步的开销,要从非常具体的 CPU,内存,磁盘 IO,网络传输,以及潜在可能失败导致无休止的全量同步几个角度回答,最后点出因为这么多缺点,所以需要引入增量同步。 127 | 128 | 答案:全量同步是利用 BG SAVE 来完成的,所以 129 | 1. 从 CPU 和 内存的角度来说,会发起`fork`系统调用,在单机内存很大的时候,这会引起很大的延迟,并且因为 COW 的原因,引发大量的缺页中断; 130 | 2. BG SAVE 的文件写入到磁盘,会增大磁盘负载; 131 | 3. BG SAVE 在网络中传输,会导致短时间内网络负载飙升; 132 | 4. 更重要的是,因为全量同步非常复杂,这段时间可能从服务器再次和主服务器失去连接。等下次重连的时候,又触发一遍全量同步,循环往复; 133 | 134 | 也因此引入了增量同步。 135 | 136 | (如果记得,继续回答**如何避免全量同步**) 137 | 138 | ### 如何避免全量同步 139 | 140 | 分析:前面已经分析过了,这里再总结一下。先从怎么触发全量同步开始聊起,然后得出避免的方法,逻辑分明。 141 | 142 | 答案:引发全量同步的几个原因有: 143 | 1. 主服务器宕机重连 144 | 2. 主服务器没有安全启动 145 | 3. 主从同步超时,导致缓冲区溢出(就是偏移量对应的数据不再缓冲区了) 146 | 4. 从服务器重启 147 | 148 | 这些情况大部分是避免不了。能做的大概就是两件事: 149 | 1. 主服务器使用安全重启机制,避免ID变化; 150 | 2. 增大复制缓冲区 151 | 3. 调大超时 152 | 153 | 然后就是加强网络建设了(这是一句屁话,因为网络不可用,你软件是没办法的) 154 | 155 | #### 如何引导 156 | - 但凡聊到全量同步代价很高,就可以跳过来这里 157 | 158 | #### 类似问题 159 | - 什么时候会触发全量同步 160 | 161 | ### Redis 的心跳机制是怎样的? 162 | 163 | 分析:Redis 的心跳机制,关键点在于,它是一个类似于 TCP 协议的 ACK 机制。所以我们结合 TCP 的 ACK 机制来回答。 164 | 165 | 答案:Redis 的心跳机制,是两个方向的,一个是主服务器向从服务器发送心跳,用于检测网络和从服务器存活; 166 | 167 | 另外一个是从服务器像主服务器发送 REPLCONF ACK,这个 ACK 会带上自身的复制偏移量。因此,如果服务器发现从服务器的偏移量比较落后,可以将丢失的数据重新补上。 168 | 169 | (开始结合 TCP 来讨论)这就是类似于 TCP 的 ACK 机制。ACK 会告诉发送端下一次期望的数据报,而后发送端进行重发。 170 | 171 | (进一步装逼,不熟悉滑动窗口协议的请忽略)不过 TCP 引入了滑动窗口协议,因此可以简单处理失序报文,但是 Redis 的同步,是要求严格的顺序的,并且从服务器并不具备处理失序命令的能力。 172 | 173 | #### 如何引导 174 | - 聊到了 TCP 的ACK机制 175 | 176 | 177 | ### Sentinel 是如何监控主从集群的? 178 | 179 | 分析:考察 Sentinel 模式的基本特点。核心就是主观下线 -> 客观下线 -> 主节点故障转移。 180 | 181 | 答案:Sentinel 本身有三个定时任务(重要): 182 | 1. 获取主从结构信息,所以能够做到主从结构动态更新; 183 | 2. 获取其它 Sentinel 节点的看法; 184 | 3. 对主从节点的心跳检测; 185 | 186 | 整个过程可以理解为:首先 Sentinel 获取了主从结构的信息,而后向所有的节点发送心跳检测,如果这个时候发现某个节点没有回复,就把它标记为**主观下线**;如果这个节点是主节点,那么 Sentinel 就询问别的 Sentinel 节点主节点信息。如果大多数都 Sentinel 都认为主节点已经下线了,就认为主节点已经**客观下线**。 187 | 188 | 当主节点已经**客观下线**,就要步入故障转移阶段。故障转移分成两个步骤,一个是 Sentinel 要选举一个 leader,另外一个步骤是 Sentinel leader 挑一个主节点。 189 | 190 | Sentinel leader 选举是使用 raft 算法的,(这里犯不着描述具体步骤,等后面他如果有兴趣,就会问你细节。大部分情况下是没有的,因为 RAFT 算法很复杂) 191 | 选举出 leader 之后,leader 从健康从节点之中依据 <优先级, 偏移量, 服务器ID> 进行排序,挑出一个节点作为主节点。 192 | 193 | (在这里可以刷一波,是因为这个排序方式有点违背直觉,正常我们可能会认为优先选择偏移量最大的,也就是数据最新的,而不是优先级最大的,一个很奇怪的点,猜不出原因) 194 | 195 | 找出主节点之后,Sentinel 要命令其它从节点连接新的主节点,同时保持对老的主节点的关注,在它恢复过来之后把它标记为从节点,命令它去同步新的主节点。 196 | 197 | (这个过程,比较复杂的是各种参数配置和权衡,我们只讨论一个参数,`parallel-syncs`,这部分是刷亮点的部分) 198 | 199 | 因为可能存在多个从节点,因此我们需要控制同时进行控制转移的从节点的数量,也就是`paralle-syncs`参数。该参数如果设置过小,会导致故障转移时间很长;但是如果该参数设置过大,会导致多数从节点不可用。 200 | 201 | #### 如何引导 202 | - 如果聊到了 RAFT 算法,可以用 Sentinel leader 选举作为举例 203 | 204 | #### 相关问题 205 | - 主观下线是什么? 206 | - 客观下线是什么? 207 | - Sentinel 如何发现节点故障? 208 | - Sentinel 如何选举主节点? 209 | 210 | ### 为什么会发生脑裂?有什么危害?如何解决? 211 | 212 | 分析:考察脑裂。脑裂不是只有 Redis 才有的,而是所有的主从模式都会有类似的问题。比如说 Zookeeper,所以可以结合 zookeeper 来说。 213 | 214 | 答案:(首先点明)大部分主从模式都会遇到脑裂问题。它的根源在于,当我们把一个主节点标记位从节点之后,它自己认为自己还是主节点。如果这个时候客户端还是连上了这个主节点,那么就会导致在错误的主节点上执行了写命令,导致数据不一致。 215 | 216 | zookeeper 也有类似的问题。彻底解决这个问题其实不太可能(确实不太可能,分布式环境下),只能尽量缓解。在 Redis 里面有一个参数,控制主节点至少要有多少个从节点才会接受写请求,把这个值设置比较大,能够缓解问题。 217 | 218 | (吹牛逼了,这一段我也不是很有把握肯定对,但是没关系,吹出来就是加分,毕竟你深入思考了)上面的参数是无法根绝这个问题的。因为事情就可能那么凑巧,恰巧整个集群一分为二,然后两边各有一个主节点,然后都认为自己是主节点,而且从节点数也达标。这时候,如果将参数设置为超过一半,那么就可以避免这个问题。 219 | 220 | 脑裂也是现在制约主从模式的一个很大的问题,因此最近涌现出来了很多的对等集群。 221 | 222 | #### 如何引导 223 | - zookeeper 脑裂 224 | - 单纯探讨主从模式 225 | - 聊到了对等模式 226 | 227 | #### 相关问题 228 | - 主从模式有什么缺点 229 | 230 | ### Redis 为什么不直接使用普通的 Master-Slave 模式,而是要引入 Sentinel ? 231 | 232 | 分析:这个问题也有一点强行解释的意味。这个问题源于这么一种朴素的认知,就是其实从服务器完全可以自己发起选举,选出一个 leader 来,也就是主节点。Sentinel 却是引入了哨兵,由哨兵选出哨兵 leader,由哨兵 leader 选出一个主节点。但是在 Cluster 里面,主节点是直接由从节点选举出来的。强行解释,没啥好说的。 233 | 234 | 答案:(可能)是出于性能考虑。Sentinel 可以单独部署,那么 Sentinel 在选举 leader,挑选主节点的时候,并不会影响到 Redis 数据集群的性能。 235 | 236 | ### Redis Cluster 是如何运作的? 237 | 238 | 分析:详细讨论Cluster的机制。一般人只会回答到槽分配那一步,但是在面试后的时候,答得比较好的,要回答的到槽迁移。槽迁移就是发生在扩容缩容,因此不必纠结于扩容还是缩容,理解了槽迁移机制就可以。之前在数据结构部分,我们谈到Redis的渐进式 rehash, 说过Redis一个显著特征是大量运用延迟策略,而槽的迁移也是这种策略。 239 | 240 | 答案:Redis Cluster 主要是利用 key 的哈希值,将其分成 16384 个槽,而后每个槽被分配到不同的服务器上。这些服务器,本身也是一个主从模式的主服务器。 241 | 242 | (先回答,请求路由问题) 243 | Redis Cluster 是peer-to-peer,每个节点都能提供读写服务。在这种情况下,如果客户端请求的某个 key 不在该服务器上,该服务器就会返回一个`move`错误,让客户端再一次请求正确的服务器(类似于HTTP的重定向)。 244 | 245 | (记得就提及智能客户端) 246 | 可见,在这种情况下,如果我们能够在客户端维持一份槽映射表,我们的就不必经过这么一份转发,这就是所谓的智能路由。(不要继续讨论,等问) 247 | 248 | (接下里讨论槽迁移) 249 | 但是,分布式环境下,可能会扩容缩容。因此槽就会出现迁移,从一台服务器挪到另外一台服务器。 250 | 251 | Redis 提供了槽迁移的命令,主要步骤就是让目标节点准备好接收,源节点准备迁移。热后小批量迁移key。 252 | 253 | (讨论迁移过程中key的访问) 254 | 因此在迁移过程中,一个槽的部分 key 可能在源节点,一部分在目标节点。因此如果请求过来,打到源节点,源节点发现已经迁移了,就会返回一个 ASK 错误,这个错误会引导客户端直接去访问目标节点。 255 | 256 | #### 相关问题 257 | - 当槽在迁移过程中,我一个 key 过来,会如何? 258 | - Redis Cluster 是如何分片的? 259 | 260 | ### smart client 是什么? 261 | 262 | 分析:考察基本的智能客户端的概念。对于智能客户端来说,关键点在于,维护了槽映射关系,并且在收到`move`错误的时候更新,其次则是对每一个节点,创建了一个线程池。使用智能客户端性能提高很大,但是也存在映射关系落后于真实关系的问题。 263 | 264 | 答案:智能客户端是指,客户端将槽到服务器的映射关系维持在内存中,并且在收到`move`错误的时候更新信息。如果在使用连接池的情况下,它会对每一个主节点建立一个池。这种模式,极大减少了`move`错误发生的概率,并且即便真的发生了槽迁移,也很快就能修正自己的映射关系。 265 | 266 | #### 相关问题 267 | - 你用了智能客户端吗?(不管用不用,反正先把特点答了) 268 | - 有什么缺点?(稍微滞后一点) 269 | -------------------------------------------------------------------------------- /redis/data_structure.md: -------------------------------------------------------------------------------- 1 | # Redis 数据结构 2 | 3 | 分析:这是`Redis`必考题。考察`Redis`数据结构有两大类问法,一种是直接问你某种数据结构的特征,一种是问你某个场景下应该使用什么数据结构。本质上,两者考察的都是同一个东西,即你是否了解某种数据结构。在复习这个模块的时候,要从表象和底层实现两个角度去学习,熟记于心。你要注意区分,你要回答的是 Redis 值对象的数据结构(表象),还是底层实现。大多数情况下,你应该从表象出发,即值对象的角度出发,而后讨论每一种值对象的可能的底层实现。如果记不住全部的底层实现,可以只讨论重点的几个。先看图: 4 | 5 | ![值对象](./img/value_object.png) 6 | 7 | ![底层数据结构](./img/data_structure.png) 8 | 9 | 总体回答的讨论就是“某种值对象-有什么实现-某种实现的特点”。 10 | 11 | 答案:从使用的角度来说,Redis 有五种数据结构(注意,我们这里按照第一张图来回答,即从值对象的角度出发) 12 | 13 | - 字符串对象,即我们设置的值是一个简单的数字或者字符串; 14 | - 列表对象,即值是一个列表,存储多个元素。从底层实现上来说,有`ziplist`和`linkedlist`两种实现; 15 | - 字典对象,即值本身就是一个字典。从底层实现上来说,有`ziplist`和`hashtable`俩中实现; 16 | - 集合对象,即值是一个集合(Set)。底层有`intset`和`hashtable`两种实现; 17 | - 有序集合对象,即值是一个有序的集合。底层有`ziplist`和`skiplist`两种实现; 18 | 19 | 分析:注意,回到这一步差不多就可以了。接下来,如果面试官想要继续探讨这些问题,他就会问底层实现了。讨论底层实现的时候,要把注意力放在横向比较上。因为一个很显然的问题,就是上面的不同对象类型,都有多种底层实现。 20 | 另外一个点是,很多时候考察为什么 Redis 那么高效,除了一般的那些理由,也可以说 Redis 设计了非常多的数据结构,并且选择合适的数据结构,来减少缓存加快效率。 21 | 22 | ## 扩展点 23 | 24 | ### Redis 使用的字符串有什么特点? 25 | 26 | 分析:考察底层数据结构特点。 27 | 28 | 答案:Redis 使用的字符串,叫做 SDS。SDS 的特点是: 29 | 30 | 1. 直接存储了字符串长度,可以常量时间获得长度; 31 | 2. SDS 采用了预分配和懒回收的策略来分配内存,可以减少内存分配次数; 32 | 33 | #### 类似问题 34 | - SDS 有什么特点 35 | - SDS 和 C 字符串比起来有什么优点 36 | - Redis 为什么不直接使用 C 字符串 37 | 38 | #### 如何引导 39 | 可以考虑在前面回答字符串对象的时候主动谈起。 40 | 41 | 42 | ### Redis 的 hashtable 是如何实现的 43 | 44 | 分析:考察底层数据结构hashtable的实现。刷亮点的机会,在于回答出来扩容,即 rehash 的过程。注意的是, Redis 的渐进式 rehash 是很有特色的。要结合各自语言的 hashtable 实现来做交叉对比。例如,对于 Java 的开发者来说,可以比较 Redis 的 rehash 和 Java HashMap 的 rehash 过程;对于 golang 来说,Redis 的 rehash 过程和 map 的底层实现理念接近,也可以一并说起来。 45 | 46 | 而后要再一次点出,采用渐进式 rehash 的优缺点,即采用渐进式 rehash 会时总的开销增大,但是这种开销被平摊到了每次访问数据中,是一种取舍。 47 | 48 | 在回答完毕之后,为了万无一失,可以再一次提起,就是说字典除了可以用哈希表实现,也可以用`ziplist`来实现。 49 | 50 | 答案:(关于哈希算法这一段,可以作为候选,因为哈希算法名字很难记)Redis 采用 MurmurHash2 (么么哈希),该算法效率高,随机性好,可以减少冲突可能。 51 | 52 | Redis的哈希表是采用了拉链法来解决冲突,在冲突的时候,会将元素加在表头,以加快速度。 53 | 54 | Redis 的扩容比较有特色,采用的是渐进式 rehash。即 Redis 实际上维持了新旧两张表,迁移发生的时候,Redis 并不是直接把数据迁移到新的表,而是在后续删改查的时候逐步挪过去。 55 | 56 | (以下是和特定语言进行比较,看你的语言里面 rehash 是如何实现的,Java 版本)Java 里面的 rehash 则不一样,而是一次性迁移的。 57 | (golang 版本)go 语言的 map 实现理念很接近。golang 的 map 的扩容也是渐进式的,也是在访问数据过程中逐步完成迁移。 58 | 59 | 所以,当查找某个 key 的时候,大概是先去原表里面找,找不到就去新表里面找。如果在原表找到了,就同时执行迁移逻辑。 60 | 61 | (亮点第二弹,结合 Redis Cluster 的重新分配来做横向比较,不熟悉重新分片不建议使用,或者你可以拼一把,就是可能面试官也不太了解重新分片,所以不会问细节)这种渐进式的思想在 Redis 里面还体现在重新分片上。Redis 的重新分片也是一边迁移数据,同时对外提供服务。在找一个 key 的时候,也是先找源节点,找到了同时迁移,找不到就说明迁移到了目标节点。 62 | 63 | (总结,升华主题,强调一下渐进式的改进并不是毫无缺点的。这也是回答“为什么使用渐进式 rehash ”)总体而言,渐进式 rehash 可以带来更平滑的响应时间。但是渐进式的 rehash 也会带来总体开销比一次性迁移开销大的缺点。 64 | 65 | 分析:这个话题是很能体现你对数据结构的理解。特别是要点出 rehash 过程的优缺点。在数据库索引那里,我说过,工程学上,我们会倾向于选择可预测性, rehash 也是这种思想的体现。即渐进式的 rehash 不会出现说因为哈希表扩容导致响应时间猛增的问题。 66 | 67 | 关键点:拉链法,MurmurHash2(么么哈希),渐进式 rehash 68 | 69 | #### 类似问题 70 | - 为什么要使用渐进式 rehash 71 | - 渐进式 rehash 有什么缺点?(总开销大,每次查询都比一次性 rehash要慢) 72 | - Redis 的哈希表扩容有什么特色 73 | - Redis 的哈希表如何扩容 74 | 75 | #### 如何引导 76 | - 讨论到你的语言的 map 的底层实现的时候,你可以用 Redis 作为横向对比 77 | 78 | 79 | ### Redis 里面的 ziplist 是如何实现的?有什么用? 80 | 81 | 分析:考察底层数据结构。`ziplist`在 Redis 里面是一个很重要的结构,要紧紧围绕`ziplist`节省内存,结构紧凑,搜索快速的特点来回答。要想答出特色来,要先回答`ziplist`的基本特点,要把重点放在`ziplist`增删改数据时候的行为,关键点在于两个,一个是数据移动,一个是连锁更新。 82 | 83 | 答案:`ziplist`是一个很特殊的列表,它的内存类似数组那样是连续,但是每个元素的大小却不相同。`ziplist`通常用于单个数据小,并且数据量不多的情况。在 Redis 里面,`ziplist`用于组成有序集合,字典和列表三种值对象。 84 | 85 | (回答第一个要点,元素移动) 86 | `ziplist`能够在`O(1)`的时间内完成对头尾的操作(因为`ziplist`记录了首尾节点),但是一般的增删改查,都是`O(N)`的。这是因为`ziplist`是一个连续内存的结构,找到位置`i`,需要从头部开始遍历,而在**增删**的时候需要将位置`i`之后的元素移动(增往后移动,删往前移动)。 87 | 88 | (回答第二个要点,连锁更新) 89 | 尤其是,因为`ziplist`的节点存储了前一个节点的长度`prelen`,所以,当前一个节点发生变更的时候,就需要更新长度`prelen`。而 Redis 为了节约内存,`prelen`有一个字节和五个字节两种长度。举例来说,假设前一个节点,最开始的长度是254,而后更新成了256,那么当前节点原本一个字节能够放下`prelen`,不得不扩展到五个字节。假如说当前节点最开始长度也是254,那么`prelen`扩展到五个字节之后就变成了258,当前节点的后一个节点,就不得不跟着扩展。 90 | 91 | 这就是所谓的连锁更新,它使得一个增删改操作,最坏的时候是`O(N^2)`。这也是为什么`ziplist`只适合放置小数据,少数据的原因。(从这里也可以解释为什么前面那些编码,都是限制数据小于64字节,并且数量少于512) 92 | 93 | 关键点:内存连续,数据移动,连锁更新, 94 | 95 | #### 类似问题 96 | - 如何往`ziplist`里面插入或者删除一个元素?(考察数据移动和连锁更新) 97 | - 什么时候会触发连锁更新? 98 | - `ziplist`在最糟糕的情况下性能如何?(考察连锁更新) 99 | - 为什么数据量大的时候不用`ziplist`?(考察`ziplist`的特点,特别是增删改查的行为) 100 | - `ziplist`的操作效率是多少?(O(N),一般情况下,只要不是操作头尾,即PUSH,POP之类的操作,都是) 101 | - 删除会引起连锁更新么?(当然可能!增删改都可能!) 102 | - 为什么使用`ziplist`?(考察`ziplist`特点) 103 | - 什么情况下使用`ziplist`? (同上,单个数据小,数据量也少) 104 | 105 | #### 如何引导 106 | - 讨论到了有序列表的时候 107 | - 讨论到 ArrayList, LinkedList 的时候。`ziplist`的实现不同于这两种,所以可以扩展到这里 108 | 109 | ### Redis 的整数集合(intset)是什么?有什么特色 110 | 111 | 分析:考察底层数据结构,核心就在于理解`intset`的**升级不降级**的特性。 112 | 113 | 答案:`intset`是一个数组结构,用于存储整数类型,里面的元素是唯一的。它可以存放16、32、64位的整数。如果元素位数变大,那么就会触发升级过程。例如原本存储的元素都是16位整数,现在插入一个32位的整数,那么 Redis 需要按照32位重新计算内存大小,并且分配内存,迁移原本的数据,而后将新数据插入。有一点需要注意的是,Redis并不支持降级。 114 | 115 | #### 类似问题 116 | - 如果我有一个小的整数数据集想要放到 Redis,Redis会用什么结构来存储? 117 | - Redis 的`intset`是否支持降级? 118 | 119 | ## 总结 120 | 121 | Redis 的问题大同小异,套路就是: 122 | 1. 你用过XXX结构么? 123 | 2. 如果我想存储XXX特点的数据,你会用什么? 124 | 3. 如果我想存储XXX特点的数据,Redis 会用什么结构? 125 | 126 | 而后就是前面列举的几个有很强个性的数据结构的实现原理。在复习的时候,一定要记住值对象和底层实现的关系。大概的思路就是“值对象——支持的数据结构——数据结构特点” 127 | 128 | 还要把握住不同底层实现切换的逻辑。比如说字典,底层可能是`ziplist`和`hashtable`,那么要把握住两个点: 129 | 1. 什么时候会从`ziplist`转化到`hashtable` 130 | 2. 怎么转化 131 | 132 | 第二个问题`怎么转化`其实很好回答,所有的底层实现转换,都是遍历老的实现的数据,一个个迁移过去。例如`ziplist`迁移`hashtable`,就是遍历`ziplist`,对里面每一个元素做哈希,放到对应的位置。 133 | 134 | Redis 还有一个设计理念,就是先凑合,不行再升级。最开始 Redis 总是选择能够节省内存的,紧凑的数据结构,后面发现不行了,再来升级。 135 | 136 | 137 | -------------------------------------------------------------------------------- /redis/expired.md: -------------------------------------------------------------------------------- 1 | # Redis 过期处理 2 | 3 | 分析:Redis 对过期键值对的处理,可以说是日经题。核心就是懒惰删除+定期删除。前者很容易记忆,后者很容易忽略。而要刷亮点,要从 RDB, AOF 和 主从复制三个不同处理策略上着手。 4 | 5 | ![过期策略](./img/expired.png) 6 | 7 | 答案: Redis 删除过期键值对,主要依赖于两种方式,定期删除和懒惰删除。 8 | 9 | 定期删除是指 Redis 会定期遍历数据库,检查过期的 key 并且执行删除。它的特点是随机检查,点到即止。它并不会一次遍历全部过期 key,然后删除,而是在规定时间内,能删除多少就删除多少。这是为了平衡 CPU 开销和内存消耗。 10 | 11 | (扩展点一,和 JVM 的 G1 垃圾回收器做对比,它们的类似点在于部分回收)这有点类似于 JVM 的 G1 垃圾回收器,G1 也是挑选一部分 Region 来回收,不过 G1 主要是为了平衡停顿时间。 12 | 13 | Redis 的另外一个删除策略是懒惰删除,即如果在访问某个 key 的时候,会检查其过期时间,如果已经过期,则会删除该键值对。 14 | 15 | (扩展点二,结合 RDB, AOF 和 主从复制来回答) 16 | 17 | 如果 Redis 开启了持久化和主从同步,那么 Redis 的过期处理要复杂一些。 18 | 1. 在 RDB 之下,加载 RDB 会忽略已经过期的 key;(RDB 不读) 19 | 2. 在 AOF 之下,重写 AOF 会忽略已经过期的 key;(AOF 不写) 20 | 3. 主从同步之下,从服务器等待主服务器的删除命令;(从服务器啥也不干) 21 | 22 | (扩展点三,讨论不同版本从服务器对过期 key 的处理策略) 23 | 如果 Redis 开启了主从同步,那么从库对过期 key 的处理,不同版本有不同策略。对于写来说,从库都是等主库的删除命令,但是对于读来说: 24 | - 在 3.2 之前,Redis 从服务器会返回过期 key 的值,仿佛没有过期一样 25 | - 在 3.2 之后,Redis 从服务器会返回NULL,和主库行为一致 26 | 27 | (扩展点四,讨论 3.2 版本之前,读取从库过期 key 的策略) 28 | 在 3.2 之前,可以使用`TTL`命令来判断 key 究竟有没有过期。 29 | 30 | 31 | #### 类似问题 32 | - 为什么 Redis 定时删除策略不删除全部过期的 key? 33 | - Redis 的定时删除策略是怎样的? 34 | - Redis key 过期之后,还能读到数据吗? 35 | - 为什么有时候 key 已经过期了,但是还能读到数据? 36 | - 如何解决 Redis 从库 key 过期依然返回数据的问题? -------------------------------------------------------------------------------- /redis/img/availability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/redis/img/availability.png -------------------------------------------------------------------------------- /redis/img/data_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/redis/img/data_structure.png -------------------------------------------------------------------------------- /redis/img/expired.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/redis/img/expired.png -------------------------------------------------------------------------------- /redis/img/io_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/redis/img/io_model.png -------------------------------------------------------------------------------- /redis/img/persistence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/redis/img/persistence.png -------------------------------------------------------------------------------- /redis/img/pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/redis/img/pipeline.png -------------------------------------------------------------------------------- /redis/img/value_object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flycash/interview-baguwen/163f3382437a5ada78359e46c13c599c605f7de9/redis/img/value_object.png -------------------------------------------------------------------------------- /redis/io_model.md: -------------------------------------------------------------------------------- 1 | # IO 模型 2 | 3 | 分析:所有的 IO 模型,考来考去就是一句话,**IO多路复用**。因为操作系统就那么一回事,你要高性能,就没啥选择,反正别问,问就是`IO 多路复用`。那么为什么大家还问呢?因为`IO 多路复用`大体上大家都是差不多的,但是细节上就五花八门。回答 Redis 的 IO 模型,亮点可以从两个角度刷,一个是和`memcache`的比较;一个是从 6.0 支持多线程角度刷。 4 | 5 | ![IO 模型](./img/io_model.png) 6 | 7 | 核心就是四个组件。 8 | 9 | 答案:Redis 采用的是 IO 多路复用模型,核心分成四个组件: 10 | 1. 多路复用程序 11 | 2. 套接字队列 12 | 3. 事件分派器 13 | 4. 事件处理器。事件处理器又可以分成三种,连接处理器,请求处理器,回复处理器。 14 | 15 | (事件处理器怎么记住这三个呢?按照“发起连接——发送请求——发回响应”三个步骤,刚好对应三个处理器) 16 | 17 | (大概描述一下各个组件是怎么配合的,大致就是生产者——消费者模式) 18 | 19 | 多路复用程序会监听不同套接字的事件,当某个事件,比如发来了一个请求,那么多路复用程序就把这个套接字丢过去套接字队列,事件分派器从队列里边找到套接字,丢给对应的事件处理器处理。 20 | 21 | ![Redis IO 多路复用](https://img-blog.csdnimg.cn/20200614190842638.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1Nla3lfZmVp,size_16,color_FFFFFF,t_70) 22 | 23 | 24 | (扩展点一,讨论 6.0 引入的多线程模型) 25 | 26 | Redis 这种模型的瓶颈在于从套接字中读写数据。因此在 6.0 中引入了异步 IO 线程,专门负责读取 IO 数据。在这种模型之下,相当于主线程监听到套接字事件,找到一个 IO 线程去读数据,之后主线程根据命令,找到对应的事件处理器,执行命令。写入响应的时候,也是交给了 IO 线程。这就是相当于,有一个线程池,只负责读写数据,主线程负责轮询和执行命令。 27 | 28 | ![Redis 多线程模型](https://pic3.zhimg.com/80/v2-4bd6569139472aaf4423540dd303e61a_1440w.jpg) 29 | 30 | (扩展点二,讨论`memcache`的 IO 模型) 31 | 32 | `memcache` 的 IO 模型本质上是 IO 多路复用。所不同的是,`memcache` 的 IO 多路复用是多线程的,并且命令的执行也是多线程的。`memcache`的`acceptor`线程监听到套接字事件之后,丢给`workers`线程,线程负责读写数据并且执行命令。 33 | 34 | ![memcache IO 模型](https://upload-images.jianshu.io/upload_images/6302559-7b933753b04ac9bb.png?imageMogr2/auto-orient/strip|imageView2/2/w/856/format/webp) 35 | 36 | (扩展点三,结合扩展点二,进一步比较 Redis 多线程和 `memecache` 多线程) 37 | 在 Redis 6.0 支持多线程 IO 之后,两者的 IO 模型看上去其实差别不是特别大了。根本的差距,在于 Redis 依旧是只有一个单线程来执行命令,但是 `memcache` 是各自线程执行各自的命令。 38 | 39 | #### 类似问题 40 | - Redis 为什么引入多线程模型? 41 | - Redis 一定是单线程的吗? 42 | - Redis 是如何保证高性能的?(这里只讨论了一个点,还有别的点) 43 | 44 | #### 如何引导 45 | - 讨论 Redis 为什么那么高效 46 | - 讨论到多路复用 47 | - 讨论到 IO 模型 48 | - 讨论到“Redis一定是单线程的吗”这种问题 49 | - 讨论到了`memcache` 和 Redis 的区别 50 | 51 | ## References 52 | [Redis 多线程解密](https://segmentfault.com/a/1190000039223696) -------------------------------------------------------------------------------- /redis/persistent.md: -------------------------------------------------------------------------------- 1 | # Redis 的持久化机制 2 | 3 | 分析:Redis 持久化机制,其实还是比较简单的,就是 RDB 和 AOF 两种,掌握各自的特点和适用场景,然后背一下 AOF 重写的流程,差不多就可以了。如果要刷出亮点,在于两个点,COW 和 `fsync`的时机。前者是和操作系统相关,`fork` 调用有关;后者是横向对比其它中间件的持久化机制,比如说MySQL 的 `redolog`, `binlog` 都有类似的机制。如果直接问起来持久化机制,可以只先回答大概,把关键知识点点到就可以,后面等面试官来挖。 4 | 5 | 进一步,我们可以拿 Redis 的 AOF 机制和 MySQL 的 `binlog` 进行对比,它们都记录的是中间执行步骤。而 MySQL 的`mysqldump`就非常接近 `RDB`,也是一种快照保存方案。 6 | 7 | ![Redis 持久化](./img/persistence.png) 8 | 9 | 答案: Redis 的持久化机制分成两种,RDB 和 AOF。 10 | 11 | RDB 可以理解为是一个快照,直接把 Redis 内存中的数据以快照的形式保存下来。因为这个过程很消耗资源,所以分成 SAVE 和 BG SAVE 两种。(后面这个,是点出来 COW,如果你无法理解 COW 机制,就不要回答)BG SAVE的核心是利用`fork` 和 `COW` 机制。 12 | 13 | AOF 是将 Redis 的命令逐条保留下来,而后通过重放这些命令来复原。我们可以通过重写 AOF 来减少资源消耗。(重写AOF,这个是钓鱼,是为了引导后面的两个话题,为什么要重写,以及如何重写) 14 | 15 | #### 如何引导 16 | 17 | 这里面有一个很出人意料的引导的点,就是分布式锁。看后面**使用 Redis 来作为分布式锁,会有什么问题?** 18 | 19 | ## 扩展点 20 | 21 | ### BG SAVE是如何工作的? 22 | 23 | 分析:明面上是考察BG SAVE,实际上是考察 COW。所以亮点就在于把 COW 说个大概。思路就是从`fork`系统调用谈起,谈到 COW,再谈到 COW 内部的大概步骤。另外一个方向是结合 Java 的`CopyOnWrite`数据结构一起聊。 24 | 25 | 答案:BG SAVE 是为了解决 SAVE 资源消耗过多的问题(这一句是点出目标)。BG SAVE核心是利用`fork`系统调用,复制出来一个子进程,而后子进程尝试将数据写入文件。这个时候,子进程和主进程是共享内存的,当主进程发生写操作,那么就会复制一份内存,这就是所谓的 COW。COW 的核心是利用缺页异常,操作系统在捕捉到缺页异常之后,发现他们共享内存了,就会复制出来一份。(这里,如何发现共享内存,检查的是页表项,记不住没关系,认怂就可以。关键点在于缺页异常。) 26 | 27 | (下面是结合 Java `CopyOnWrite` 来阐述,不熟悉 Java 这一类数据结构的请忽略。我猜测其它语言或者工具也会有类似的机制,可以挑一个深入描述,作为一种对比) 28 | Java 里面也有一大类数据结构,利用了 COW 这种思想,例如 `CopyOnWriteArrayList`,当里面元素变更的时候,就会复制出来一个新的。它特别适合那种大多数情况只是读,只有小部分可能是写的场景。 29 | 30 | (进一步升华,引导下去下面的**COW 缺陷**)如果 Redis 的数据也是读多写少,那么 COW 就很高效。这也是一种典型的空间换取时间策略。 31 | 32 | #### 如何引导 33 | 34 | - 操作系统里面聊到了进程,`fork`系统调用 35 | - Java 里面聊到了`CopyOnWrite`之类的数据结构 36 | 37 | #### 类似问题 38 | 39 | - 为什么引入 BG SAVE? 40 | - RDB 是如何运作的?先回答 SAVE,而后回答 BG SAVE。 41 | - COW 是如何运作的 42 | 43 | ### COW 有什么缺陷? 44 | 45 | 分析:考察 COW 的特点。其实这个问题有点故意找茬没事找事的感觉。COW 就两个问题,一个是写多的时候,缺页异常会非常多,如果物理内存紧张,会引发大量的物理页置换。另外一个就是,COW 的存在,导致 Redis 无法完全利用内存,总要留出来一部分给 COW 使用。 46 | 47 | 答案:有两个缺点: 48 | 49 | 1. 引发缺页异常。如果物理内存紧张,还会引起大量的物理页置换; 50 | 2. COW 的存在,导致我们需要预留一部分内存出来,Redis 无法全部利用服务器的内存; 51 | 52 | (这个时候我们可以进一步讨论,这一段讨论只是为了展示你对于 COW 的理解)一般来说,最极端情况是所有内存复制一遍,那么 Redis 最多利用一半的内存,考虑到操作系统本身的开销,那么一半都不到。不过如果愿意冒险的话,可以设置超过一半。例如,不考虑操作系统开销,如果自己的 Redis 读多写少,在整个 BG SAVE 过程,最多复制 10% 的内存,那么就可以给 Redis 分配 80% 的内存。这种搞法,糟糕的情况下,会引发大量的物理页置换,性能下降。所以,很少有人这么使用。 53 | 54 | #### 如何引导 55 | 56 | - 前面聊到 BGSAVE 就可以指出来 57 | 58 | #### 类似问题 59 | 60 | - COW 会引发什么问题? 61 | - 频繁写的 Redis,在使用 BG SAVE 的时候会有什么问题 62 | - BG SAVE 有什么缺陷? 63 | 64 | ### 为什么启用了 AOF 还是会丢失数据? 65 | 66 | 分析:又是一道违背一般常识的问题,考察的就是刷盘时间,和数据库“为什么事务提交了,数据却丢了”一个性质。所以两边可以交叉对比来回答。 67 | 68 | 答案:原因在于 AOF 的数据只写到了缓存,还没有写到磁盘。 AOF 有三个选项可以控制刷盘: 69 | 70 | 1. always: 每次都刷盘 71 | 2. everysec: 每秒,这意味着一般情况下会丢失一秒钟的数据。而实际上,考虑到硬盘阻塞(见后面**使用 everysec 输盘策略有什么缺点),那么可能丢失两秒的数据。 72 | 3. no: 由操作系统决定 73 | 74 | 他们的数据保障逐渐变弱,但是性能变强。 75 | 76 | (开始升华主题,横向对比)所有依赖于`fsync`系统调用落盘的中间件都会碰到类似的问题。例如`redolog`, `binlog`。而且,在`redolog`如果提交事务之后,没有及时落盘,而此时数据库崩掉,就会出现事务已经提交,但是数据依旧丢失的问题。 77 | 78 | #### 如何引导 79 | 80 | - 前面聊到 MySQL 的`redolog`,`binlog`; 81 | - 聊到`fsync`话题; 82 | 83 | ### 使用 everysec 策略刷盘有什么缺点? 84 | 85 | 分析:这是为了考察所谓的刷盘阻塞。就是当你每秒刷一次的时候,可能会出现,数据太多,或者硬盘阻塞,你无法在一秒钟内刷完数据。 86 | 87 | 答案:使用 everysec 会面临一个刷盘阻塞的问题。如果数据太多,或者硬盘阻塞,导致一秒钟内无法把所有的数据都刷新到磁盘。Redis 如果发现上一次的刷盘还没结束,就会检查,距离上一次刷盘成功多久了,如果超过两秒,那么 Redis 会停下来等待刷盘成功。 88 | 89 | 因此使用 everysec 可能导致丢失两秒数据,而且在同步等待的时候,Redis 的其它请求都被阻塞。 90 | 91 | #### 如何引导 92 | 93 | - 一般我建议在前面聊到了刷盘策略的时候说 94 | 95 | #### 类似问题 96 | 97 | - 什么是硬盘阻塞 98 | - 如果磁盘负载(或者 IO 负载)太大,会有什么问题? 99 | 100 | ### 为什么 AOF 要引入重写的机制? 101 | 102 | 分析:考察 AOF 特点。核心就是 AOF 逐条记录命令,导致 AOF 文件非常巨大,其次就是,AOF 记录的命令是可以合并的。我们用一个例子来辅助记忆后面的 AOF 命令合并这一个点。 103 | 104 | 答案:AOF 是逐条记录 Redis 执行命令的,这会导致 AOF 文件快速膨胀。在使用 AOF 恢复数据的时候,异常缓慢。从另外一个角度来说,我们也不需要真的逐条记录 Redis 的命令,一些命令是可以合并的。举例来说,假如我们 Redis 记录了用户ID到用户名字的数据,那么某个用户先更新自己的用户名为AAA,后面更新为BBB,实际上,我们只需要记录最后一条更新为BBB的。又比如说,Redis 先插入了一条数据AAA,后面又删除了AAA,这个时候我们可以两条都不记录。 105 | 106 | (刷亮点)MySQL `binlog` 类似于 AOF,但是并没有重写机制,因为 MySQL 可以混用 `mysqldump` 和 `binlog` 来恢复数据。(看后面**Redis 如何利用 RDB 和 AOF 恢复数据**) 107 | 108 | ### AOF 重写是怎么运作的? 109 | 110 | 分析:考察 AOF 重写的特点。这里面有一个误区,就是直觉上,我们以为 AOF 重写是读已有的 AOF 文件,然后尝试合并里面的记录。实际上不是的,AOF 重写非常类似于 RDB,只不过是输出格式不一样。但是 AOF 重写还要解决一边在重写,一边又有新的 AOF 的问题。面试的亮点就在于回答清楚后面的一边重写 AOF,一边又有新的 AOF 来了怎么处理。 111 | 112 | 答案:重写 AOF 整体类似于 RDB。它并不是读已经写好的 AOF 文件,然后合并。而是类似于 RDB,直接`fork`出来一个子进程,子进程按照当前内存数据生成一个 AOF 文件。在这个过程中,Redis 还在源源不断执行命令,这部分命令将会被写入一个 AOF 的缓存队列里面。当子进程写完 AOF 之后,发一个信号给主进程,主进程负责把缓冲队列里面的数据写入到新 AOF。而后用新的 AOF 替换掉老的 AOF。这里可以看出来,最后这个步骤是比较耗时的,同时 Redis 也处于一种无法执行别的命令的状态。(据我所知是这样的,就是处理缓冲队列的数据的时候,类似于 GC 的 STW 过程,无法对外服务,这也是一个亮点,很少有人会考虑最后这个缓冲队列处理,是不是会导致无法执行用户命令) 113 | 114 | (这里还有一个刷亮点的地方,但是是只适用于 Java 方向,熟悉 G1 垃圾回收期的同学。在 G1 里面也有一个类似的缓冲队列)这种机制,在别的地方也可以看到。比如说 G1 回收器,使用了SATB技术,在开始的时候记录了一个快照,而 GC 过程的引用变更都会丢到一个缓冲队列,在再标记阶段重新处理。 115 | 116 | (升华主题,点出 AOF 这种方案的固有缺陷.)类似于 AOF 这种记录变更的技术,都要面临类似的问题,也就都需要考虑合并与重写的机制。 117 | 118 | ### RDB 和 AOF 该如何选择? 119 | 120 | 分析:考察 RDB 和 AOF 的优缺点。 121 | 122 | 答案:选择的原则是: 123 | 124 | 1. 如果数据不能容忍任何丢失,或者只能容忍少量丢失,那么用 AOF; 125 | 2. 否则 RDB,即一般的数据备份和容灾,RDB就够了; 126 | 127 | 遇事不决 AOF,反正 RDB 可以的,AOF 肯定也可以。 128 | 129 | #### 类似问题 130 | 131 | - 为什么 Redis 要搞 RDB 和 AOF 两种机制? 132 | - 要想保证丢失数据最少,应该使用哪种? AOF 133 | - 只是出于数据备份和容灾,用哪种? 134 | 135 | ### Redis 如何利用 RDB 和 AOF 恢复数据? 136 | 137 | 分析:一句话的事情,非常简单,不过我们可以结合MySQL数据恢复来做比较。 138 | 139 | 答案:原则就是,有 AOF 用 AOF,没有就用 RDB。(AOF>RDB,你可以进一步解释为什么)这是因为 AOF 的数据在大概率的情况下,是要比 RDB 新的。这和 MySQL 的数据恢复有点不同。 MySQL 的 `mysqldump` 类似于 RDB,而`binlog` 类似于 AOF。MySQL 是可以用 `mysqldump` 的文件来恢复,而后从`binlog`里面找出后续变更,从而恢复数据。 140 | 141 | (进一步深化,其实我也说不清楚为毛`binlog`没有类似的机制,我觉得也可以考虑有的)我觉得这也是为什么`binlog`没有类似于 AOF 重写机制的一个原因。 142 | 143 | #### 类似问题 144 | 145 | - 为什么 Redis 恢复数据优先使用 AOF 数据 146 | 147 | ### 使用 Redis 来作为分布式锁,会有什么问题? 148 | 149 | 分析:这是一个很偏门冷僻的问题,在分布式锁里面可能会问到。我们假定你能正确使用 Redis 命令来写一个分布式锁,那么你还需要考虑这个场景:一个线程抢到了分布式锁,然后这个锁没有持久化,然后 Redis 崩了,很快又重启了,结果下一个线程立马就拿到了锁,这个时候就会出现你代码万无一失,但是分布式锁还是被多个线程拿到了的问题。我感觉很少人会考虑这个点,就暂且留着。 150 | 151 | 答案:要考虑分布式锁持久化的问题。假定我一个线程拿到了分布式锁,那么如果这个锁没有被持久化,那么如果 Redis 崩溃立刻重启,那么下一个线程立马就能拿到锁。 152 | 153 | 所以在考虑这种场景下,万无一失的方案,就是开启 AOF 持久化,并且将刷盘时机设置成`always`。 154 | -------------------------------------------------------------------------------- /redis/pipeline.md: -------------------------------------------------------------------------------- 1 | # Redis Pipeline 2 | 分析:Redis Pipeline 的面试主要停留在为什么 Pipeline 快这个核心要点,围绕着这个核心要点考察 Pipeline 的原理。同时还会结合考察如何在 Redis Cluster 里面使用 Pipeline,以及和批量命令的区别。 3 | 4 | 所以需要首先掌握 Pipeline 的大概原理。 5 | 6 | Pipeline 的原理其实不难: 7 | 8 | ![Redis Pipeline](./img/pipeline.png) 9 | 10 | 它的核心要点: 11 | 1. 应用代码会持续不断的把请求发给 Redis Client; 12 | 2. Redis Client 会缓存这些命令,等凑够了一批,就发送命令到 Redis 服务端; 13 | 3. Redis Server 收到命令之后进行处理,并且在处理完这一波命令之前,所有的响应都被缓存在内存里面; 14 | 4. Redis Server 处理完了 Pipeline 发过来的一批命令,而后返回响应给 Redis Client; 15 | 5. Redis Clinet 接收响应,并且将结果递交给应用代码; 16 | 6. 如果此时还有命令,Redis Client 会继续发送剩余命令 17 | 18 | 先来回顾一下普通的一个命令从发送出去到收到响应,要做些什么: 19 | 1. Redis Client 发起系统调用 send(),将命令发送给 Redis Server 20 | 2. 命令在网络中传输 21 | 3. Redis Server 发起系统调用 read(),从网络中读取命令 22 | 4. Redis Server 发起系统调用 send(),返回响应 23 | 5. 响应在网络中传输 24 | 6. Redis Client 发起系统调用 read(),读取响应 25 | 26 | 总结下来,整个过程就是四次系统调用,而后一个在网络中传输命令和响应——可以看做是一个 RTT。 27 | 28 | 那么假如有 N 个命令,这里就需要 N * 4 次系统调用,并且需要 N * RTT。 29 | 30 | 如果在 Pipeline 里面,如果恰好是 N 个命令发送一波,那么只需要四次系统调用加一个 RTT。但是这一个 RTT 和原本的单个 RTT 比起来是要慢一点的,因为这里要发送的数据量要多。 31 | 32 | 因此我们可以总结,使用 Pipeline 性能比较好就在于两点: 33 | - 减少系统调用,避免内核态切换 34 | - 减少了 RTT 35 | 36 | 而开销我们也能看出来:对于 Redis Client 来说,需要额外的内存缓存命令;对于 Redis Server 来说,需要额外的内存来缓存响应。 37 | 38 | 这两者的缓存,都跟 N 的大小有关。因此控制 N 的大小就比较关键了,N 同时影响着系统调用的数量和缓存所需的内存大小,两者需要权衡折中。此外,如果 N 很大,导致很难凑够 N 个命令,那么客户端的命令就会长期缓存,而没能够及时发送到服务端。 39 | 40 | 使用 Pipeline 的时候,服务端在收到命令之后会先缓存,因此 Redis Server 可能会把好几个不同客户端发过来的命令混在一起。 41 | 42 | 但是 Redis Server 保证了来自同一个 Pipeline 的命令会被顺序执行。只不过这并不意味着这 N 个命令是原子的。如果中间有任何一个命令失败,那么后续的命令将不会被执行,而已经被执行的命令,也不会回滚。 43 | 44 | 那么和批量处理命令比起来有什么不同? 45 | - 从系统调用的角度来说,没什么不同 46 | - 批量命令里面一批命令,必然是同种命令,比如说都是 Get,或者都是 Set;而 Pipeline 则不是,任何命令都可以通过 Pipeline 来发送 47 | 48 | ## 面试题 49 | ### Redis Pipeline 的原理 50 | 分析:这里基本上就是回答 Pipeline 的实现机制。在答基本步骤的过程中,可以有意识引导面试官问 N 的取值,N 对内存的影响。 51 | 52 | 最后简单总结一下 Pipeline 为什么快。 53 | 54 | 答案:Redis Pipeline 的原理是: 55 | 1. 应用代码会持续不断的把请求发给 Redis Client; 56 | 2. Redis Client 会缓存这些命令,等凑够了 N 个,就发送命令到 Redis 服务端。而 N 的取值对 Pipeline 的性能影响比较大(引导询问 N 的取值); 57 | 3. Redis Server 收到命令之后进行处理,并且在处理完这 N 个命令之前,所有的响应都被缓存在内存里面。这里也可以看到,N 如果太大也会额外消耗 Redis Server 的内存(这里引导讨论内存消耗这个弊端); 58 | 4. Redis Server 处理完了 Pipeline 发过来的一批命令,而后返回响应给 Redis Client; 59 | 5. Redis Clinet 接收响应,并且将结果递交给应用代码; 60 | 6. 如果此时还有命令,Redis Client 会继续发送剩余命令; 61 | 62 | (刷亮点,也是引导)Redis Pipeline 减少了网络 IO,也减少了 RTT,所以性能比较好。 63 | 64 | #### 类似问题 65 | - 为什么 Redis Pipeline 在实时性上要差一点?主要就是命令和响应都会被缓存,而不是及时返回。 66 | 67 | ### Redis Pipeline 有什么优势? 68 | 分析:如果直接回答性能比较好,那么就基本等于没说。这个问题本质上其实是“为什么 Redis Pipeline 性能好”。 69 | 70 | 结合之前我们的分析,可以看到无非就是两个原因:网络 IO 和 RTT。 71 | 72 | 这里可以稍微讨论一下,批处理命令如 mget 和 mset 其实也是具备这两个优点的 73 | 74 | 答:Redis Pipeline 相比普通的单个命令模式,性能要好很多。 75 | 76 | 单个命令执行的时候,需要两次 read 和 两次 send 系统调用,加上一个 RTT。如果有 N 个命令就是分别乘以 N。 77 | 78 | 但是在 Pipeline 里面,一次发送,不管 N 多大,都是两次 read 和两次 send 系统调用,和一次 RTT。因而性能很好。 79 | 80 | (刷亮点,准备引导面试官问和批处理命令的区别)实际上 mget 之类的批量命令,相比单个命令分别执行,也是只需要两次 read 和两次 send 系统调用,和一次 RTT。和 Pipeline 比起来没啥区别。 81 | 82 | ### Redis Pipeline 和 mget 的不同点 83 | 84 | 分析:虽然问的是不同点,但是一般回答都是相同点和不同点一起说。 85 | 相同点: 86 | - 减少网络 IO 和 RTT,性能好 87 | - Redis Cluster 对这两种用法都不太友好 88 | 89 | 不同点: 90 | - Redis Pipeline 可以执行任意的命令,而 mget 之类的只能是执行同种命令; 91 | - Redis Pipeline 的命令和响应都会被缓存,因此实时响应上不如 mget; 92 | - Redis Pipeline 和 mget 都会受到批次大小的影响,但是相比之下 Redis Pipeline 更加严重,因为它消耗内存更多; 93 | 94 | 这里分析完之后,可以进一步分析两者的使用场景。其中 Redis Cluster 的问题属于引导,不必全部回答出来。 95 | 96 | 答:Redis Pipeline 和 mget 之类的批量命令有很多地方都很相似,比如说: 97 | 98 | - 减少网络 IO 和 RTT,性能好(注意,这里可能面试官会问,它是如何减少 IO 和 RTT,也就是我们前面讨论优势的地方) 99 | - Redis Cluster 对这两种用法都不太友好(这个是引导,准备讨论 Redis Cluster 需要的特殊处理) 100 | 101 | 但是具体来说,Redis Pipeline 使用场景和 mget 不太一样: 102 | - Redis Pipeline 可以执行任意的命令,而 mget 之类的只能是执行同种命令; 103 | - Redis Pipeline 的命令和响应都会被缓存,因此实时响应上不如 mget; 104 | - Redis Pipeline 和 mget 都会受到批次大小的影响,但是相比之下 Redis Pipeline 更加严重,因为它需要缓存命令和响应,消耗更大; 105 | 106 | (刷亮点,讨论什么时候用 Redis Pipeline)在频繁读写的情况下,使用 Redis Pipeline 都是能受益的。但是如果是追求实时响应的话,那么就不要使用 Redis Pipeline,因为 Redis Pipeline 的机制导致请求和响应会被缓存一小段时间。这种实时场景,只能考虑批量处理命令 107 | #### 类似命令 108 | - 什么时候选择 Redis Pipeline 109 | - 什么时候选择 mget 110 | 111 | ### 如何在 Redis Cluster 上使用 Redis Pipeline 112 | 113 | 分析:首先,一般的说法都是 Redis Cluster 上无法使用批量命令和 Pipeline。这种说法其实没什么大问题,但是实际上我们可以考虑在客户端上做一些改造,使得在 Redis Cluster 上也能使用 Pipeline。 114 | 115 | 核心要点: 116 | - 知道 Redis Cluster 上槽的分布。假如说 Redis Cluster 有 ABC 三个节点,并且 0-5000 槽在 A 上,5001-10000 槽在 B 上,10001-16384 在 C 上; 117 | - 我们在每个节点上都创建一个 Pipeline 118 | - 当请求过来,比如说找 key1, key2, key3 119 | - 这时候,根据 key1, key2, key3 知道对应的槽,假说 key1 槽是 100,key2 槽是2000,key3 槽是6000 120 | - 根据槽找到对应的节点。key1 和 key2 对应 A,key3 对应 B 121 | - 找到在该节点上创建的 Pipeline,发送命令。key1 和 key2 通过 A 上的 Pipeline 发送,key3 上的通过 B 的 Pipeline 来发送 122 | 123 | 所以本质上这个过程,就是手动将 key 按照槽分布到不同节点,然后使用不同节点上的 Pipeline。 124 | 125 | 答:一般来说,Redis Pipeline 是针对节点,所以会说无法在 Redis Cluster 上使用 Pipeline。这种说法是指,我们无法创建一个 Pipeline,连上整个 Redis Cluster。 126 | 127 | 但是实际上,我们可以通过创建多个 Pipeline 分别连上每一个节点来在 Redis Cluster 上使用 Pipeline。 128 | 129 | 这种使用方式的核心在于:当我们收到一个请求时,要能计算出来它的槽。而后根据槽找到对应的节点。然后将请求发送到该节点对应的 Pipeline 上。 130 | 131 | #### 类似问题 132 | - Redis Cluster 上能不能使用 Pipeline? -------------------------------------------------------------------------------- /sharding/README.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | 分析:分库分表的面试题还是属于比较复杂的。这种复杂体现在两方面,一方面是分库分表从理论上来说就很复杂;另外一方面这种复杂度也体现在实践上。 3 | 4 | 从理论上来说,则是要掌握分库分表中间件的设计原理: 5 | - [改写 SQ](./rewrite_sql.md)L:给出一个逻辑 SQL 和相应的分库分表规则,你应该能够说清楚它的目标库和目标表 6 | - 执行 SQL:重点要考虑 7 | - 目标 SQL 能不能并发执行 8 | - 修改数据类的查询,例如说 INSERT、UPDATE、DELETE 之类的语句,如果目标 SQL 有多个,那么如果出现部分失败的问题该怎么解决 9 | - 事务怎么处理 10 | - 处理结果集:这个过程和前面改写 SQL 是很密切相关的,要考虑 11 | - 排序和分页 12 | - 聚合函数 13 | 14 | 从实践中来说,则要掌握: 15 | - 为什么要分库分表:这个问题要结合读写分离、分区表来综合回答;更进一步则要说清在具体的业务场景下,应该分库还是分表,还是说都要;高端点则要将分库分表和其它中间件进行一个横向比较,站在一个集群模式的角度去讨论这个问题 16 | - 实际中如何分库分表: 17 | - 怎么挑选分库分表的键 18 | - 主键怎么生成:不同策略的优缺点和潜在问题 19 | - 从单裤单表开始分库分表,要怎么做 20 | - 怎么计算分库分表应该要分多少库、多少表 21 | - 如果中途发现分库分表需要扩容,怎么扩容: 22 | - 怎么做数据迁移 23 | - 怎么做数据校验 24 | - 怎么切换流量 25 | - 怎么做好回滚:即万一中间任何一个步骤出问题,能不能回滚,怎么回滚 26 | - 分库分表的性能问题 27 | - 排序查询性能问题以及优化方案 28 | - 分页查询性能问题以及优化方案 29 | - 非典型查询,是指不符合分库分表初衷的查询。例如在订单表中,典型的分库分表方案是使用买家 ID,那么如果卖家要查询自己卖了多少单,就很难查询 30 | 31 | 你们在背八股文的时候记得按照顺序看下去,因为有些内容是存在先后依赖关系的,比如说你先看改写 SQL 的内容,你就很难理解后面执行 SQL 和合并结果集的内容。 32 | 33 | 如果你们有兴趣,可以考虑报名我在极客时间开设的实战训练营,在里面的直播主题课分库分表里面,我比较详细讨论了分库分表中间件设计的弯弯绕绕。 34 | 35 | 在面试的时候,如果你不是真的面试中间件研发岗位,那么面试重点会侧重在实践中。但是如果你想面试万无一失,那么你应该同时掌握理论,并且能够自己设计并且实现一个简单的分库分表中间件。 36 | 37 | -------------------------------------------------------------------------------- /sharding/rewrite_sql.md: -------------------------------------------------------------------------------- 1 | # 改写 SQL 2 | 分析:改写 SQL 是分库分表的第一个核心步骤。改写 SQL 的目标是生成物理 SQL,或者说目标 SQL。改写 SQL 主要受到两方面的影响: 3 | - 查询本身 4 | - 分库分表规则 5 | 6 | 举个典型例子来说,SELECT * FROM user_tab WHERE id < 10000 这条语句: 7 | - 如果是哈希分库分表,那么大概率是广播,即将查询发到全部表上 8 | - 如果是范围分表,那么就可以根据范围来计算出准确的目标表 9 | 10 | 而如果是 SELECT avg(age) FROM user_tab,那么不管是范围还是哈希分库分表,都要改写为 SELECT count(id), sum(age) FROM user_tab,然后在处理结果的时候根据 count 和 sum 来求平均值。 11 | 12 | --------------------------------------------------------------------------------