Browse Source

修改了面经

seamew 1 year ago
parent
commit
ab32931f89
25 changed files with 1147 additions and 7 deletions
  1. 215 0
      面经/问答/K8S.md
  2. 61 0
      面经/问答/Mysql.md
  3. BIN
      面经/问答/assets/056c87751b9dd7b56f4264240fe96d00.png
  4. BIN
      面经/问答/assets/2164474-20210716210057908-1704850787.png
  5. BIN
      面经/问答/assets/2e2b95eebf60b6d03f6c1476f4d7c697.png
  6. BIN
      面经/问答/assets/4ef8691d67eb1eb53217099d0a691eb5.png
  7. BIN
      面经/问答/assets/arch-x-reduce-2.png
  8. BIN
      面经/问答/assets/arch-x-reduce-3.png
  9. BIN
      面经/问答/assets/arch-x-reduce-31-169347359783720.png
  10. BIN
      面经/问答/assets/c8054fbec1b14607b2759ea0d1ddc2f4tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  11. BIN
      面经/问答/assets/https%3A%2F%2Fimg-bed-l.oss-cn-beijing.aliyuncs.com%2Fpic_bed%2Fimage-20210708111308193.png
  12. BIN
      面经/问答/assets/image-20230831172005636.png
  13. BIN
      面经/问答/assets/image-20230905160248241.png
  14. BIN
      面经/问答/assets/image-20230905161245730.png
  15. BIN
      面经/问答/assets/image-20230905164914619.png
  16. BIN
      面经/问答/assets/image-20230905203232434.png
  17. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTg3NTE1,size_16,color_FFFFFF,t_70.png
  18. 264 0
      面经/问答/docker.md
  19. 53 3
      面经/问答/kafka.md
  20. 103 1
      面经/问答/情景设计题.md
  21. 25 0
      面经/问答/操作系统.md
  22. 7 3
      面经/问答/智力问题.md
  23. 311 0
      面经/问答/集合.md
  24. BIN
      面经/项目/assets/4079e08d3ab14eabbecee2926c73b84etplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp
  25. 108 0
      面经/项目/抽奖项目.md

+ 215 - 0
面经/问答/K8S.md

@@ -0,0 +1,215 @@
+## 什么是K8S
+
+Kubernetes 是一个可移植的、可扩展的开源平台,用于管理容器化的工作负载和服务,可促进声明式配置和自动化。 Kubernetes 拥有一个庞大且快速增长的生态系统。Kubernetes 的服务、支持和工具广泛可用。
+
+![image-20230905160248241](assets/image-20230905160248241.png)
+
+## pod
+
+容器的本质是进程,Kubernetes就是操作系统。
+
+在一个OS当中,进程并非独自运行的,而是以进程组的方式,有原则的组织在一起。
+
+Kubernetes当中,将“进程组”的概念映射到了容器技术中。
+
+为什么需要这个“组”的概念??
+
+应用之间往往具有密切的协作关系,类似于“进程和进程组”。
+
+关于如何妥善处理成组调度的问题?
+
+* Mesos有一个资源囤积(resource hoarding)机制,当所有设置了Affinity约束的任务都到达之后,才开始对他们进行统一调度
+* Google Omega论文,提出乐观调度处理冲突的方法,即:先不管这些冲突,而是通过回滚机制在出现冲突后解决问题
+
+Kubernetes中得到了妥善解决,调度器**统一按照Pod而非容器的资源需求进行计算**。
+
+但是如果仅仅这样,那么Kubernetes可以在调度器层面解决 容器应用之间的紧密关系,而不一定要把Pod设置为最基本单位?
+
+引出Pod在Kubernetes项目中更加重要的意义,即:**容器设计模式**。
+
+* **Pod只是一个逻辑概念,Kubernetes真正处理的,还是宿主机OS上的Namespace和Cgroups,物理上并没有存在一个所谓的Pod的边界或者隔离环境。**
+
+* Pod,是一组共享了某些资源的容器。
+
+* **Pod当中所有的容器,共享的是同一个Network Namespace,并且可以声明共享同一个Volume。**
+
+**在Pod当中,容器是对等的关系**,而如果是用简单用docker run共享,则必然会有容器先启动,进而产生拓扑关系。
+
+### **Infra容器**(新版本叫Init容器)
+
+为了实现这样的对等关系,Pod的实现需要一个中间容器,叫做`Infra`容器。
+
+**它永远是首先被创建的容器,其他用户定义的容器通过Join Network Namespace方式,与Infra关联起来。**
+
+Infra容器一定要占据非常少的资源,所以它使用一个非常特殊的image,叫**`k8s.gcr.io/pause`**,(称为**pause 镜像**)它是用汇编语言编写、永远处于暂停状态,解压后也只有100~200KB左右。
+
+Pod 的生命周期和 Infra 容器一致,而与其中的用户定义容器无关。
+
+![image-20230905161245730](assets/image-20230905161245730.png)
+
+要为Kubernetes开发一个网络插件时,应该**重点考虑如何配置这个Pod的Network Namespace,而不是每一个用户容器如何使用你的网络配置**。
+
+对于**共享Volume**而言,只需要**把Volume的定义设计在Pod层级**即可:**一个 Volume 对应的宿主机目录对于 Pod 来说就只有一个**,Pod 里的容器只要声明挂载这个 Volume,就一定可以共享这个 Volume 对应的宿主机目录。
+
+## pod配置文件config
+
+**特殊的Projected Volume**
+
+它们存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。这些特殊 Volume 的作用,**是为容器提供预先定义好的数据。**
+
+一共支持四种Volume:
+
+1. Secret: 保密数据
+
+把 Pod 想要访问的加密数据,存放到 Etcd 中。然后,就可以通过在 Pod 的容器里挂载 Volume 的方式,访问到这些 Secret 里保存的信息。
+
+经典场景:存放数据库的Credential信息:
+
+2. ConfigMap: 配置文件
+
+ConfigMap 保存的是不需要加密的、应用所需的配置信息,其他用法与Secret相同。
+
+用ConfigMap来保存一个java应用的配置文件ui.properties:
+
+3. Downward API:  Pod元数据
+
+让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息。
+
+4. ServiceAccountToken: 服务账户,通过验证
+
+> **疑问:现在有了一个 Pod,我能不能在这个 Pod 里安装一个 Kubernetes 的 Client,这样就可以从容器里直接访问并且操作这个 Kubernetes 的 API 了呢?**
+>
+> **答案是:可以,但是需要解决API Server的授权问题。**
+
+`Service Account` 对象的作用,就是 Kubernetes 系统内置的一种“服务账户”,它是 **Kubernetes 进行权限分配的对象。**
+
+ Service Account 的授权信息和文件,实际上**保存在它所绑定的一个特殊的 Secret 对象里的**。这个特殊的 Secret 对象,就叫作**ServiceAccountToken**。
+
+**任何运行在 Kubernetes 集群上的应用,都必须使用这个 ServiceAccountToken 里保存的授权信息,也就是 Token,才可以合法地访问 API Server**。
+
+Kubernetes为了方便使用,为用户提供一个默认的服务账户(default Service Account),任何一个运行在 Kubernetes 里的 Pod,都可以直接使用这个默认的 Service Account,而无需显示地声明挂载它。
+
+## Replicaset
+
+`ReplicaSet`是kubernetes中的一种副本控制器,简称`rs`,主要作用是控制由其管理的pod,使pod副本的数量始终维持在预设的个数。它的主要作用就是保证一定数量的Pod能够在集群中正常运行,它会持续监听这些Pod的运行状态,在Pod发生故障时重启pod,pod数量减少时重新运行新的 Pod副本。**官方推荐不要直接使用ReplicaSet,用Deployments取而代之**,Deployments是比ReplicaSet更高级的概念,它会管理ReplicaSet并提供很多其它有用的特性,最重要的是Deployments支持声明式更新,声明式更新的好处是不会丢失历史变更。所以Deployment控制器不直接管理Pod对象,而是由 Deployment 管理ReplicaSet,再由ReplicaSet负责管理Pod对象。
+
+
+
+Replicaset核心作用在于用户创建指定数量的pod副本,并确保pod副本一直处于满足用户期望的数量, 起到多退少补的作用,并且还具有自动扩容缩容等制。
+Replicaset控制器主要由三个部分组成:
+1、**用户期望的pod副本数**:用来定义由这个控制器管控的pod副本有几个
+2、**标签选择器**:选定哪些pod是自己管理的,如果通过标签选择器选到的pod副本数量少于我们指定的数量,需要用到下面的组件
+3、**pod资源模板**:如果集群中现存的pod数量不够我们定义的副本中期望的数量怎么办,需要新建pod,这就需要pod模板,新建的pod是基于模板来创建的。
+
+## Deployment
+
+为了更好地解决服务编排的问题,k8s在V1.2版本开始,引入了deployment控制器,值得一提的是,这种控制器并不直接管理pod,
+
+而是通过管理replicaset来间接管理pod,即:deployment管理replicaset,replicaset管理pod。所以deployment比replicaset的功能更强大。
+
+![image-20230905164914619](assets/image-20230905164914619.png)
+
+deployment的主要功能有下面几个:
+
+- 支持replicaset的所有功能
+- 支持发布的停止、继续
+- 支持版本的滚动更新和版本回退
+
+## StatefulSet
+
+作为一个后端工程师,因为负责的大部分项目都是`Web`服务这类的“无状态应用”,在平时工作中接触到的最常用的`Kubernetes`控制器是`Deployment`,但是`Deployment`只适合于编排“无状态应用”,它会假设一个应用的所有 `Pod`是完全一样的,互相之间也没有顺序依赖,也无所谓运行在哪台宿主机上。正因为每个`Pod`都一样,在需要的时候可以水平扩/缩,增加和删除`Pod`。
+
+但是并不是所有应用都是无状态的,尤其是每个实例之间有主从关系的应用和数据存储类应用,针对这类应用使用`Deployment`控制器无法实现正确调度,所以`Kubernetes`里采用了另外一个控制器`StatefulSet`负责调度有状态应用的`Pod`,保持应用的当前状态始终等于应用定义的所需状态。
+
+## node affinity
+
+pod绑定节点最简单的方法是使用 nodeSelector,但它比较简单粗暴,使用起来不能灵活调度,这个在后续版本中也会慢慢过时,所以我们一般用 nodeAffinity来实现这些需求。
+
+Node Affinity
+ Affinity 翻译成中文是“亲和性”,它对应的是 Anti-Affinity,我们翻译成“互斥”。这两个词比较形象,可以把 pod 选择 node 的过程类比成磁铁的吸引和互斥,不同的是除了简单的正负极之外,pod 和 node 的吸引和互斥是可以灵活配置的。
+
+Affinity的优点:
+
+匹配有更多的逻辑组合,不只是字符串的完全相等
+ 调度分成软策略(soft)和硬策略(hard),在软策略下,如果没有满足调度条件的节点,pod会忽略这条规则,继续完成调度。
+ 目前主要的node affinity:
+
+requiredDuringSchedulingIgnoredDuringExecution
+ 表示pod必须部署到满足条件的节点上,如果没有满足条件的节点,就不停重试。其中IgnoreDuringExecution表示pod部署之后运行的时候,如果节点标签发生了变化,不再满足pod指定的条件,pod也会继续运行。
+
+requiredDuringSchedulingRequiredDuringExecution
+ 表示pod必须部署到满足条件的节点上,如果没有满足条件的节点,就不停重试。其中RequiredDuringExecution表示pod部署之后运行的时候,如果节点标签发生了变化,不再满足pod指定的条件,则重新选择符合要求的节点。
+
+preferredDuringSchedulingIgnoredDuringExecution
+ 表示优先部署到满足条件的节点上,如果没有满足条件的节点,就忽略这些条件,按照正常逻辑部署。
+
+preferredDuringSchedulingRequiredDuringExecution
+ 表示优先部署到满足条件的节点上,如果没有满足条件的节点,就忽略这些条件,按照正常逻辑部署。其中RequiredDuringExecution表示如果后面节点标签发生了变化,满足了条件,则重新调度到满足条件的节点。
+
+## Toleration
+
+节点亲和性 是 Pod的一种属性,它使 Pod 被吸引到一类特定的节点(这可能出于一种偏好,也可能是硬性要求)。 **污点(Taint)** 则相反——它使节点能够排斥一类特定的 Pod。
+
+**容忍度(Toleration)** 是应用于 Pod 上的。容忍度允许调度器调度带有对应污点的 Pod。 容忍度允许调度但并不保证调度:作为其功能的一部分, 调度器也会评估其他参数。
+
+污点和容忍度(Toleration)相互配合,可以用来避免 Pod 被分配到不合适的节点上。 每个节点上都可以应用一个或多个污点,这表示对于那些不能容忍这些污点的 Pod, 是不会被该节点接受的。
+
+## service
+
+Service是一种抽象的对象,它定义了一组Pod的逻辑集合和一个用于访问它们的策略,一个Serivce下面包含的Pod集合一般是由**Label Selector**来决定的。假如我们后端运行了3个副本,这些副本都是可以替代的,因为前端并不关心它们使用的是哪一个后端服务。尽管由于各种原因后端的Pod集合会发生变化,但是前端却不需要知道这些变化,也不需要自己用一个列表来记录这些后端的服务,Service的这种抽象就可以帮我们达到这种解耦的目的。
+
+
+
+> Node IP:Node节点的IP地址
+>  Pod IP:Pod的IP地址
+>  Cluster IP:Service的IP地址
+
+首先,Node IP是Kubernetes集群中节点的物理网卡IP地址(一般为内网),所有属于这个网络的服务器之间都可以直接通信,所以Kubernetes集群外要想访问Kubernetes集群内部的某个节点或者服务,肯定得通过Node IP进行通信(这个时候一般是通过外网IP了)
+
+然后Pod IP是每个Pod的IP地址,它是Docker Engine根据docker0网桥的IP地址段进行分配的(我们这里使用的是flannel这种网络插件保证所有节点的Pod IP不会冲突)
+
+最后Cluster IP是一个虚拟的IP,仅仅作用于Kubernetes Service这个对象,由Kubernetes自己来进行管理和分配地址,当然我们也**无法ping这个地址**,他没有一个真正的实体对象来响应,他只能结合Service Port来组成一个可以通信的服务。
+
+![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTg3NTE1,size_16,color_FFFFFF,t_70.png)
+
+## Ingress
+
+ingress翻译过来是入口的意思,k8s希望ingress是整个集群流量的入口,引入ingress后整个请求如下图所示
+
+![image.png](assets/c8054fbec1b14607b2759ea0d1ddc2f4tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+### 为什么需要Ingress资源
+
+由于K8S集群拥有强大的副本控制能力,Pod随时可能从一个节点上被驱逐到另一个节点上,或者直接销毁再来一个新的。
+
+然而伴随着Pod的销毁和重生,Pod的IP等信息不断地在改变,此时使用K8S提供的Service机制可以解决这一问题,Service通过标签选定指定的Pod作为后端服务,并监听这些Pod的变化。
+
+在对外暴露服务时,使用Service的NodePort是一个方法,但还会有以下几个问题
+
+### 问题1 - 如何管理端口
+
+当需要对外暴露的服务量比较多的时候,端口管理的问题便会暴露出来。并且service只支持4层代理,也就是只能根据ip+端口
+
+此时的一个处理方案是使用一个代理服务(例如Nginx)根据请求信息将请求转发到不同的服务上去。
+
+### 问题2 - 如何管理转发配置
+
+每当有新服务加入,都需要对该服务的配置进行修改、升级,在服务数量逐渐变多后,该配置项目会变得越来越大,手工修改的风险也会逐渐增高。
+
+那么需要一个工具来简化这一过程,希望可以通过简单的配置动态生成代理中复杂的配置,最好还可以顺手重新加载配置文件。
+
+K8S刚好也提供了此类型资源。
+
+## 健康检查
+
+健康检查(health check)是用于检测应用实例是否正常工作,对应用状态的监控,保障业务高可用的一种机制。
+
+k8s健康检测主要分为以下三种:
+
+- 存活性探测(Liveness probes) :主要是探测应用是否还活着。如果检测到应用没有存活就杀掉当前pod并重启。
+- 就绪性探测(Readiness probes):只要是探测应用是否准备好接受请求访问,如果检测应用准备好了,就把请求流量放进来;反之,则把应用节点从注册中心拿掉。
+- 启动探测(Startup Probes):对于旧应用需要更长的启动时间,这时候既不想重启应用也不想让请求访问进来,可以设置启动探测给足够的启动时间保证应用启动成功。
+
+![image-20230905203232434](assets/image-20230905203232434.png)
+
+> **二者不能相互替代,根据实际情况,配合使用。只配置了readiness是无法触发容器重启的;只配置了liveness,可能应用还没准备好,导致请求失败,status是running,Ready是0/1。**

+ 61 - 0
面经/问答/Mysql.md

@@ -152,6 +152,25 @@ MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种数
 
 如果有的话,插入操作就会发生**阻塞**,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个**插入意向锁**,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
 
+## 加锁规则
+
+唯一索引等值查询:
+
+- 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会**退化成「记录锁」**。
+- 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会**退化成「间隙锁」**。
+
+非唯一索引等值查询:
+
+- 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后**在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁**。
+- 当查询的记录「不存在」时,**扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁**。
+
+非唯一索引和主键索引的范围查询的加锁规则不同之处在于:
+
+- 唯一索引在满足一些条件的时候,索引的 next-key lock 退化为间隙锁或者记录锁。
+- 非唯一索引范围查询,索引的 next-key lock 不会退化为间隙锁和记录锁。
+
+其实理解 MySQL 为什么要这样加锁,主要要以避免幻读角度去分析,这样就很容易理解这些加锁的规则了。
+
 ## InnoDB 和 MyISAM 引擎的区别
 
 InnoDB和MyISAM是MySQL数据库中常见的两种存储引擎,它们有一些重要的区别。
@@ -447,6 +466,11 @@ show global variables like 'slow_query_log_file'
 
 * 索引列用 or 连接时会导致索引失效:如果条件中有or,只要其中一个条件没有索引,其他字段有索引也不会使用。
 
+## mysql分库分表历史数据如何查询
+
+* 直接进行迁移:记录时间戳,使用定时任务去扫描
+* 平滑迁移:只插入,旧表数据不动,插入时候直接加上扩容倍数。查询直接分批查询。
+
 ## mysql分库分表物理层面优化
 
 * IO瓶颈
@@ -541,3 +565,40 @@ select * from t1 where id > 300000 order by id limit 10, 10
 ```mysql
 select * from t1 where id > (select id from t1 order by id limit 300000, 1) limit 10
 ```
+
+## MySQL中 IS NULL、IS NOT NULL、不等于, 能用上索引吗?
+
+**MySQL中决定使不使用某个索引执行查询的依据就是成本够不够小,如果null值很多,还是会用到索引的。**
+
+## MySQL允许在唯一索引字段中添加多个NULL值
+
+ 在sql server中,唯一索引字段不能出现多个null值 在mysql 的innodb引擎中,是允许在唯一索引的字段中出现多个null值的。 根据NULL的定义,**NULL表示的是未知,因此两个NULL比较的结果既不相等,也不不等,结果仍然是未知。**根据这个定义,多个NULL值的存在应该不违反唯一约束,所以是合理的,在oracel也是如此。
+
+## mysql索引为什么不能为null
+
+会引起歧义,就不允许
+
+## 为什么是2千万?
+
+假设
+
+- 非叶子节点内指向其他页的数量为 x
+- 叶子节点内能容纳的数据行数为 y
+- B+ 数的层数为 z
+
+一页是16K,**页又一些File Header (38 byte)、Page Header (56 Byte)、Infimum + Supermum(26 byte)、File Trailer(8byte), 再加上页目录,大概 1k 左右。** 剩下 15k 用于存数据,主键我们假设是 Bigint (8 byte), 而页号也是固定的(4Byte)那么索引页中的一条数据也就是 12byte。
+
+所以非叶子节点内指向其他页的数量为 x , x=15*1024/12≈1280 行。
+
+叶子节点和非叶子节点的结构是一样的,同理,能放数据的空间也是 15k。
+
+但是叶子节点中存放的是真正的行数据,这个影响的因素就会多很多,比如,字段的类型,字段的数量。每行数据占用空间越大,页中所放的行数量就会越少。
+
+这边我们暂时按一条行数据 1k 来算,那一页就能存下 15 条,Y = 15*1024/1000 ≈15。
+
+算到这边了,是不是心里已经有谱了啊。
+
+根据上述的公式,Total =x^(z-1) *y,已知 x=1280,y=15:
+
+- 假设 B+ 树是两层,那就是 z = 2, Total = (1280 ^1 )*15 = 19200
+- 假设 B+ 树是三层,那就是 z = 3, Total = (1280 ^2) *15 = 24576000 (约 2.45kw)

BIN
面经/问答/assets/056c87751b9dd7b56f4264240fe96d00.png


BIN
面经/问答/assets/2164474-20210716210057908-1704850787.png


BIN
面经/问答/assets/2e2b95eebf60b6d03f6c1476f4d7c697.png


BIN
面经/问答/assets/4ef8691d67eb1eb53217099d0a691eb5.png


BIN
面经/问答/assets/arch-x-reduce-2.png


BIN
面经/问答/assets/arch-x-reduce-3.png


BIN
面经/问答/assets/arch-x-reduce-31-169347359783720.png


BIN
面经/问答/assets/c8054fbec1b14607b2759ea0d1ddc2f4tplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


BIN
面经/问答/assets/https%3A%2F%2Fimg-bed-l.oss-cn-beijing.aliyuncs.com%2Fpic_bed%2Fimage-20210708111308193.png


BIN
面经/问答/assets/image-20230831172005636.png


BIN
面经/问答/assets/image-20230905160248241.png


BIN
面经/问答/assets/image-20230905161245730.png


BIN
面经/问答/assets/image-20230905164914619.png


BIN
面经/问答/assets/image-20230905203232434.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIxMTg3NTE1,size_16,color_FFFFFF,t_70.png


+ 264 - 0
面经/问答/docker.md

@@ -0,0 +1,264 @@
+## 什么是docker
+
+说实话关于 Docker 是什么并太好说,下面我通过四点向你说明 Docker 到底是个什么东西。
+
+- **Docker 是世界领先的软件容器平台。**
+- **Docker** 使用 Google 公司推出的 **Go 语言** 进行开发实现,基于 **Linux 内核** 提供的 CGroup 功能和 namespace 来实现的,以及 AUFS 类的 **UnionFS** 等技术,**对进程进行封装隔离,属于操作系统层面的虚拟化技术。** 由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。
+- **Docker 能够自动执行重复性任务,例如搭建和配置开发环境,从而解放了开发人员以便他们专注在真正重要的事情上:构建杰出的软件。**
+- **用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。**
+
+## Docker 容器的特点
+
+- **轻量** : 在一台机器上运行的多个 Docker 容器可以共享这台机器的操作系统内核;它们能够迅速启动,只需占用很少的计算和内存资源。镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。
+- **标准** : Docker 容器基于开放式标准,能够在所有主流 Linux 版本、Microsoft Windows 以及包括 VM、裸机服务器和云在内的任何基础设施上运行。
+- **安全** : Docker 赋予应用的隔离性不仅限于彼此隔离,还独立于底层的基础设施。Docker 默认提供最强的隔离,因此应用出现问题,也只是单个容器的问题,而不会波及到整台机器。
+
+## docker和虚拟机对比
+
+传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
+
+![img](assets/2e2b95eebf60b6d03f6c1476f4d7c697.png)
+
+![img](assets/4ef8691d67eb1eb53217099d0a691eb5.png)
+
+- **容器是一个应用层抽象,用于将代码和依赖资源打包在一起。** **多个容器可以在同一台机器上运行,共享操作系统内核,但各自作为独立的进程在用户空间中运行** 。与虚拟机相比, **容器占用的空间较少**(容器镜像大小通常只有几十兆),**瞬间就能完成启动** 。
+- **虚拟机 (VM) 是一个物理硬件层抽象,用于将一台服务器变成多台服务器。** 管理程序允许多个 VM 在一台机器上运行。每个 VM 都包含一整套操作系统、一个或多个应用、必要的二进制文件和库资源,因此 **占用大量空间** 。而且 VM **启动也十分缓慢** 。
+
+通过 Docker 官网,我们知道了这么多 Docker 的优势,但是大家也没有必要完全否定虚拟机技术,因为两者有不同的使用场景。**虚拟机更擅长于彻底隔离整个运行环境**。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。而 **Docker 通常用于隔离不同的应用** ,例如前端,后端以及数据库。
+
+
+
+就我而言,对于两者无所谓谁会取代谁,而是两者可以和谐共存。
+
+![img](assets/056c87751b9dd7b56f4264240fe96d00.png)
+
+## docker底层原理
+
+Docker 技术是基于 LXC(Linux container- Linux 容器)虚拟容器技术的。
+
+> **LXC,其名称来自 Linux 软件容器(Linux Containers)的缩写,一种操作系统层虚拟化(Operating system–level virtualization)技术,为 Linux 内核容器功能的一个用户空间接口。它将应用软件系统打包成一个软件容器(Container),内含应用软件本身的代码,以及所需要的操作系统核心和库。通过统一的名字空间和共用 API 来分配不同软件容器的可用硬件资源,创造出应用程序的独立沙箱运行环境,使得 Linux 用户可以容易的创建和管理系统或应用容器。**
+
+LXC 技术主要是借助 Linux 内核中提供的 CGroup 功能和 namespace 来实现的,通过 LXC 可以为软件提供一个独立的操作系统运行环境。
+
+**cgroup 和 namespace 介绍:**
+
+- **namespace 是 Linux 内核用来隔离内核资源的方式。** 通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。Linux namespaces 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的进程拥有独立的全局系统资源,改变一个 namespace 中的系统资源只会影响当前 namespace 里的进程,对其他 namespace 中的进程没有影响。
+
+- **CGroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物力资源 (如 cpu memory i/o 等等) 的机制。**
+
+**cgroup 和 namespace 两者对比:**
+
+两者都是将进程进行分组,但是两者的作用还是有本质区别。namespace 是为了隔离进程组之间的资源,而 cgroup 是为了对一组进程进行统一的资源监控和限制。
+
+## namespace
+
+每个进程在 Linux 系统中都拥有自己的命名空间(namespace).Namespace是对全局系统资源的一种封装隔离,使得处于不同namespace的进程拥有独立的全局系统资源,改变一个namespace中的系统资源只会影响当前namespace里的进程,对其他namespace中的进程没有影响。
+
+### Linux内核支持的namespaces
+
+目前,Linux内核里面实现了7种不同类型的namespace。
+
+```shell
+名称        宏定义             隔离内容
+Cgroup      CLONE_NEWCGROUP   Cgroup root directory (since Linux 4.6)
+IPC         CLONE_NEWIPC      System V IPC, POSIX message queues (since Linux 2.6.19)
+Network     CLONE_NEWNET      Network devices, stacks, ports, etc. (since Linux 2.6.24)
+Mount       CLONE_NEWNS       Mount points (since Linux 2.4.19)
+PID         CLONE_NEWPID      Process IDs (since Linux 2.6.24)
+User        CLONE_NEWUSER     User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)
+UTS         CLONE_NEWUTS      Hostname and NIS domain name (since Linux 2.6.19)
+```
+
+> **注意:** 由于Cgroup namespace在4.6的内核中才实现,并且和cgroup v2关系密切,现在普及程度还不高,比如docker现在就还没有用它,所以在namespace这个系列中不会介绍Cgroup namespace。
+
+当一个namespace中的所有进程都退出时,该namespace将会被销毁。当然还有其他方法让namespace一直存在,假设我们有一个进程号为1000的进程,以ipc namespace为例:
+
+1. 通过mount --bind命令。例如mount --bind /proc/1000/ns/ipc /other/file,就算属于这个ipc namespace的所有进程都退出了,只要/other/file还在,这个ipc namespace就一直存在,其他进程就可以利用/other/file,通过setns函数加入到这个namespace
+2. 在其他namespace的进程中打开/proc/1000/ns/ipc文件,并一直持有这个文件描述符不关闭,以后就可以用setns函数加入这个namespace。
+
+### UTS
+
+UTS Namespace 用来隔离系统的**主机名、hostname 和 NIS 域名**。
+
+- UTS namespace就是进程的一个属性,属性值相同的一组进程就属于同一个namespace,跟这组进程之间有没有亲戚关系无关
+- clone和unshare都有创建并加入新的namespace的功能,他们的主要区别是:
+  - unshare是使当前进程加入新创建的namespace
+  - clone是创建一个新的子进程,然后让子进程加入新的namespace
+- UTS namespace没有嵌套关系,即不存在说一个namespace是另一个namespace的父namespace
+
+### IPC
+
+IPC 就是在不同进程间传递和交换信息。IPC Namespace 使得容器内的所有进程,进行的数据传输、共享数据、通知、资源共享等范围控制在所属容器内部,对宿主机和其他容器没有干扰。
+
+### mount
+
+Mount Namespace 用来隔离文件系统的挂载点,不同的 Mount namespace 拥有各自独立的挂载点信息。在 Docker 这样的容器引擎中,Mount namespace 的作用就是保证容器中看到的文件系统的视图。
+
+### PID
+
+PID namespaces用来隔离进程的 ID 空间,使得**不同容器里的进程 ID 可以重复,相互不影响**。
+
+PID namespace可以嵌套,也就是说有父子关系,在当前namespace里面创建的所有新的namespace都是当前namespace的子namespace。父namespace里面可以看到所有子孙后代namespace里的进程信息,而子namespace里看不到祖先或者兄弟namespace里的进程信息。
+
+### network
+
+network namespace用来隔离网络设备, IP地址, 端口等. 每个namespace将会有自己独立的网络栈,路由表,防火墙规则,socket等。
+
+每个新的network namespace默认有一个本地环回接口,除了lo接口外,所有的其他网络设备(物理/虚拟网络接口,网桥等)只能属于一个network namespace。每个socket也只能属于一个network namespace。
+
+当新的network namespace被创建时,lo接口默认是关闭的,需要自己手动启动起
+
+标记为"local devices"的设备不能从一个namespace移动到另一个namespace,比如loopback, bridge, ppp等,我们可以通过ethtool -k命令来查看设备的netns-local属性。
+
+### user
+
+User namespace用来隔离user权限相关的Linux资源,包括user IDs and group IDs,keys , 和capabilities.
+
+## cgroup简介
+
+cgroup和namespace类似,也是将进程进行分组,但它的目的和namespace不一样,namespace是为了隔离进程组之间的资源,而cgroup是为了对一组进程进行统一的资源监控和限制。
+
+cgroup分v1和v2两个版本,v1实现较早,功能比较多,但是由于它里面的功能都是零零散散的实现的,所以规划的不是很好,导致了一些使用和维护上的不便,v2的出现就是为了解决v1中这方面的问题,在最新的4.5内核中,cgroup v2声称已经可以用于生产环境了,但它所支持的功能还很有限,随着v2一起引入内核的还有cgroup namespace。v1和v2可以混合使用,但是这样会更复杂,所以一般没人会这样用。
+
+用于:
+
+- **将线程分组**
+- **对每组线程使用的多种物理资源进行限制和监控**
+
+## cgroup 
+
+cgroup 有以下几个关键概念:
+
+- 任务(Task):系统中的一个进程、线程。
+  - 在 cgroup v2 当中,进/线程与cgroup的关系如下:
+  - 所有 cgroup 组成一个**树形结构**(tree structure),
+  - 系统中的**每个进程都属于且只属于**某一个 cgroup;
+  - 一个**进程的所有线程**属于同一个 cgroup;
+  - 创建子进程时,继承其父进程的 cgroup;
+  - 一个进程可以被**迁移**到其他 cgroup;
+  - 迁移一个进程时,**子进程(后代进程)不会自动**跟着一起迁移;
+- **控制组(Control Group)**:**Cgroups进行资源监控和限制的基本单位**,可以监控一个或多个task。**控制组是有树状结构关系的,子控制组会继承父控制组的属性(资源配额,限制等)。**控制组 指明了资源的配额限制,一个进程可以加入到某个控制组,也可以迁移到另一个控制组中.
+- **子系统(Sub-system)**:可以跟踪或限制控制组使用该类型物理资源的内核组件。也叫**资源控制器(Controller)**。
+  - 控制器的所有行为都是具有**层级传递性**的,**如果一个 cgroup 启用了某个控制器,那这个 cgroup 的 sub-hierarchy 中所有进程都会受控制。**
+- **层级(Hierarchy)**:由控制组组成的树状结构。通过被挂载到文件系统中形成。`mount | grep "type cgroup"`可以查看。**层级**是作为**控制组的根目录**,来绑定controller,来达到对资源的控制。
+
+> 注意:
+>
+> 1. 每个层级需要绑定 controller 来进行资源控制
+> 2. 系统中可以存在多个层级,整个 Cgroups 的结构应该是多个树状结构
+> 3. 子控制组分配的资源不能超过父控制组分配的资源
+
+相互之间的关系:
+
+1. 同一个**层级**可以附加绑定一个或多个`controller`
+2. `controller`是可以同时附加到多个`hierarchy`,但是一个已经附加到某个层级的controller不能附加到其他含有其他controller的层级上,也就是说**绑定多个层级后,这些层级都是只有唯一controller。**
+3. **一个任务不能存在于同一个层级的不同控制组,但是一个任务可以存在于不同层级中的多个控制组中**
+4. 系统每创建一个层级时,该系统上的所有任务都会默认加入到这个层级的根控制组
+5. **fork 或 clone 一个子任务时,会自动加入到父任务的控制组中**,允许子任务移动到其他控制组, 没有限制
+
+注意(v1版本):
+
+- 一个Task在每个Hierarchy中只能属于一个Control Group
+- 一个Sub-System只能附加于一个Hierarchy
+- 一个Hierarchy可以附加于多个Sub-System
+
+## 资源控制模型
+
+![img](assets/https%3A%2F%2Fimg-bed-l.oss-cn-beijing.aliyuncs.com%2Fpic_bed%2Fimage-20210708111308193.png)
+
+### **Version1 to Version2**
+
+在Linux 2.6.24当中首次发行了 cgroups 的实现,各种 cgroup controller 之间的不协调导致了 cgroup hierarchy 管理变得非常复杂。
+
+由于Version 1当中的问题,Linux 3.10开始设计一个全新的的cgroups实现来解决问题,最终在Linux 4.5版本中官方发行,即Version 2。
+
+Cgroups v2 希望完全取代 Cgroups v1, 但是为了兼容,v1 并没有被移除,而且很多场景下都会作为系统的默认设置,当前的系统中可以同时使用 Cgroups v2 和 v1,但是一个 controller 只能选择一个版本使用。
+
+### v1 - v2 **切换**
+
+在当前,系统中默认使用的是 cgroup v1。
+
+重新启动内核时,添加一个参数`systemd.unified_cgroup_hierarchy=1`, 如:
+
+```
+grubby --update-kernel=ALL --args=systemd.unified_cgroup_hierarchy=1
+```
+
+------
+
+与 v1 不同,cgroup **v2 只有单个层级树**(single hierarchy)。 用如下命令挂载 v2 hierarchy:
+
+```bash
+# mount -t <fstype> <device> <dir>
+$ mount -t cgroup2 none $MOUNT_POINT
+```
+
+> cgroupv2 文件系统 的 magic number 是 0x63677270 (“cgrp”)。
+
+**兼容性:**
+
+- 所有**支持 v2 且未绑定到 v1 的控制器,会被自动绑定到 v2** hierarchy,出现在 root 层级中。
+- **v2 中未在使用的控制器**(not in active use),可以绑定到其他 hierarchies。
+
+这说明我们能以完全后向兼容的方式,**混用 v2 和 v1 hierarchy**。
+
+### **Version 1**
+
+### **介绍**
+
+`Controller`即Cgroups的资源控制器,用于独立控制一种资源。
+
+| 控制器     | 用途                                                         |
+| ---------- | ------------------------------------------------------------ |
+| blkio      | 限制cgroups中task的块设备io                                  |
+| cpu        | 限制控制组下所有任务对 CPU 的使用                            |
+| cpuacct    | 自动生成控制组中任务对 CPU 资源使用情况的报告                |
+| cpuset     | 为控制组中任务分配独立 CPU(针对多处理器系统) 和内存          |
+| devices    | 控制任务对设备的访问                                         |
+| freezer    | 挂起或恢复 控制组 中的任务                                   |
+| memory     | 限制控制组的内存使用量,自动生成任务对内存的使用情况报告     |
+| pids       | 限制控制组中进程可以派生出的进程数量                         |
+| net_cls    | 通过使用等级识别符(classid)标记网络数据包,从而允许 Linux 流量控制程序(Traffic Controller, TC) 识别从具体 cgroup 中生成的数据包 |
+| net_prio   | 限制任务中网络流量的优先级                                   |
+| perf_event | 可以对控制组中的任务进行统一的性能测试                       |
+| huge_tlb   | 限制对 Huge page 的使用                                      |
+| rdma       | 限制 RDMA/IB 资源                                            |
+
+### **Version 2**
+
+Version 2 的**新变化**:
+
+1. `Cgroups v2` 提供`a unified hierarchy against which all controllers are mounted`
+2. `no Internal process` ,除了root cgroup之外,process 只能存在于leaf node(即 **不能包含`child cgroups`的cgroups**)之中。
+3. 通过`cgroup.controllers`和`cgroup.subtree_control`来指定`Active cgroups`
+4. 移除`tasks`文件。`cpuset`controller使用的`cgroup.clone_chiledren`被移除。
+5. `cgroup.events`提供对于empty cgroup的增强版提醒机制
+
+cgroup V2当中支持的Controller:
+
+- cpu
+- cpuset
+- freezer
+- hugetlb
+- io
+- memory
+- perf_event
+- pids
+- rdma
+
+## 联合文件系统
+
+### 1、什么是UnionFS
+
+联合文件系统(Union File System):2004年由纽约州立大学石溪分校开发,它可以把多个目录(也叫分支)内容联合挂载到同一个目录下,而目录的物理位置是分开的。UnionFS允许只读和可读写目录并存,就是说可同时删除和增加内容。UnionFS应用的地方很多,比如在多个磁盘分区上合并不同文件系统的主目录,或把几张CD光盘合并成一个统一的光盘目录(归档)。另外,具有写时复制(copy-on-write)功能UnionFS可以把只读和可读写文件系统合并在一起,虚拟上允许只读文件系统的修改可以保存到可写文件系统当中。
+
+写时复制:copy-on-write,简写为 CoW,也叫隐式共享,是一种提高资源使用效率的资源管理技术。它的思想是:如果一个资源是重复的,在没有对资源做出修改前,并不需要立即复制出一个新的资源实例,这个资源被不同的所有者共享使用。当任何一个所有者要对该资源做出修改时,复制出一个新的资源实例给该所有者进行修改,修改后的资源成为其所有者的私有资源。通过这种资源共享的方式,可以显著地减少复制相同资源带来的消耗,但是这样做也会在进行资源的修改时增加一部分开销。
+
+### 2、docker的镜像rootfs,和layer的设计
+
+任何程序运行时都会有依赖,无论是开发语言层的依赖库,还是各种系统lib、操作系统等,不同的系统上这些库可能是不一样的,或者有缺失的。为了让容器运行时一致,docker将依赖的操作系统、各种lib依赖整合打包在一起(即镜像),然后容器启动时,作为它的根目录(根文件系统rootfs),使得容器进程的各种依赖调用都在这个根目录里,这样就做到了环境的一致性。
+
+**Docker镜像的设计中,引入了层(layer)的概念**,也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量rootfs(一个目录),这样应用A和应用B所在的容器共同引用相同的Debian操作系统层、Golang环境层(作为只读层),而各自有各自应用程序层,和可写层。启动容器的时候通过UnionFS把相关的层挂载到一个目录,作为容器的根文件系统。
+
+需要注意的是,rootfs只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:**这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。**
+

+ 53 - 3
面经/问答/kafka.md

@@ -218,6 +218,25 @@ Kafka从0.11.x版本开始引入这种分配策略,它主要有两个目的:
 - RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的
 - Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。
 
+## Pulsar
+
+Pulsar 是下一代云原生分布式消息流平台,最初由 Yahoo 开发 ,已经成为 Apache 顶级项目。
+
+Pulsar 集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。
+
+Pulsar 的关键特性如下(摘自官网):
+
+- 是下一代云原生分布式消息流平台。
+- Pulsar 的单个实例原生支持多个集群,可跨机房在集群间无缝地完成消息复制。
+- 极低的发布延迟和端到端延迟。
+- 可无缝扩展到超过一百万个 topic。
+- 简单的客户端 API,支持 Java、Go、Python 和 C++。
+- 主题的多种订阅模式(独占、共享和故障转移)。
+- 通过 Apache BookKeeper 提供的持久化消息存储机制保证消息传递 。
+- 由轻量级的 serverless 计算框架 Pulsar Functions 实现流原生的数据处理。
+- 基于 Pulsar Functions 的 serverless connector 框架 Pulsar IO 使得数据更易移入、移出 Apache Pulsar。
+- 分层式存储可在数据陈旧时,将数据从热存储卸载到冷/长期存储(如 S3、GCS)中。
+
 ## 为什么选择使用kafka
 
 1. **实时数据流处理**: 您的项目需要处理煤矿大数据的实时采集、传输、存储和分发,其中实时性是关键。Kafka作为一个分布式流处理平台,能够高效地处理大量的实时数据流。结合HBase,Kafka可以将实时数据快速传输到HBase存储层,并在整个数据处理流程中保持低延迟。
@@ -228,7 +247,7 @@ Kafka从0.11.x版本开始引入这种分配策略,它主要有两个目的:
 
 综上所述,选择使用Kafka是出于其在实时数据流处理、高吞吐量、可扩展性、数据流治理以及与HBase的结合方面的优势。
 
-### 为什么选择kafka2
+### 为什么选择kafka
 
 |            | Kafka                                                        | RocketMQ                                                     | RabbitMQ                                                     |
 | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
@@ -258,7 +277,7 @@ Kafka从0.11.x版本开始引入这种分配策略,它主要有两个目的:
 7. 消息事务特性,与本地业务同个事务,本地消息落库;消息投递到服务端,本地才删除;定时任务扫描本地消息库,补偿发送。
 8. MQ得伸缩性和可扩展性,如果消息积压或者资源不够时,如何支持快速扩容,提高吞吐?可以参照一下 Kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了吗。
 
-### Zookeeper 在 Kafka 中的作用
+## Zookeeper 在 Kafka 中的作用
 
 Zookeeper在Kafka中扮演着关键的角色,它是Kafka集群中的一个核心组件,用于管理、协调和维护整个Kafka集群的状态。以下是Zookeeper在Kafka中的主要作用:
 
@@ -275,7 +294,7 @@ Zookeeper在Kafka中扮演着关键的角色,它是Kafka集群中的一个核
 1. **简化架构**: 在以前的版本中,Kafka使用了Zookeeper来管理元数据、选举、存储偏移量等,这导致了Kafka集群的架构比较复杂。采用KRaft可以使得整体架构更加简化,因为KRaft将元数据和状态的管理集成到Kafka本身中,减少了对外部依赖。
 2. **一致性和可靠性**: KRaft基于Raft一致性算法,提供了更强的一致性保证。这可以帮助Kafka更好地处理元数据和状态的管理,从而提高整个系统的可靠性。
 3. **降低维护成本**: 使用Kafka自身来管理元数据和状态,可以减少对Zookeeper的依赖,从而降低了维护的复杂性。此外,Zookeeper和Kafka的不同版本之间可能会出现不兼容性问题,这也增加了维护和升级的难度。
-4. **性能优化**: KRaft被设计为适合于Kafka的使用场景,因此它可以通过优化来提升一些性能指标。对于元数据管理和状态同步等方面,KRaft可以更好地满足Kafka的需求。
+4. **性能优化**: KAFKA与zk之间需要网络通信去请求,如果网络超时或者抖动,会导致kafka异常
 
 ## Raft
 
@@ -417,3 +436,34 @@ Candidate 在等待投票回复的时候,可能会突然收到其它自称是
 Raft 保证所有 committed 日志都已经被**持久化**,且“**最终**”一定会被状态机apply。
 
 注:这里的“最终”用词很微妙,它表明了一个特点:Raft 保证的只是集群内日志的一致性,而我们真正期望的集群对外的状态机一致性需要我们做一些额外工作,这一点在《线性一致性与读性能优化》一章会着重介绍。
+
+## kafka优化
+
+* 去除zk,优化
+* 没有实现延迟队列,可以参考rocket mq
+
+RocketMQ 实现延迟队列的原理是基于消息的存储和消费控制。
+
+当生产者发送消息时,会根据设置的延迟级别将消息存储在对应的延迟消息队列中。延迟消息队列是一个特殊的存储区域,用于存放需要延迟发送的消息。
+
+在 RocketMQ 的 Broker 端,延迟消息会以不同的延迟级别分别存储在具有不同持久化特性的文件中。这样可以根据消息的延迟级别和存储特性对消息进行管理,以便在合适的时间发送给消费者。
+
+同时,RocketMQ 的消费者会启动并订阅延迟消息,但实际消费延迟消息的时间是由 RocketMQ 控制的。在消息存储时,Broker 会根据消息的延迟级别和存储特性来计算出消息的过期时间。当消息达到过期时间后,才会交给消费者进行消费。
+
+需要注意的是,RocketMQ 的延迟队列并不是精确的实时延迟,而是近似延迟。这是因为 RocketMQ 在处理延迟消息时,并不会对每一条消息都进行实时的延迟计算和触发。相反,它使用了一种定时检查和触发机制来控制延迟消息的发送。这也是为了保证系统的高吞吐量和性能。
+
+综上所述,RocketMQ 实现延迟队列的原理是通过将延迟消息存储在特定的延迟消息队列中,并在过期时间到达后交给消费者进行消费。延迟消息的存储和触发机制保证了延迟消息的可靠性和近似延迟。
+
+* 没有实现顺序消费
+
+RocketMQ 可以通过添加消息队列的方式来实现顺序消费。在 RocketMQ 中,每个主题(Topic)下可以包含多个消息队列(Message Queue),每个消息队列都是独立的。如果需要保证顺序消费,需要将同一业务的消息发送到同一个 Message Queue 中。
+
+以下是实现顺序消费的方法:
+
+1. 按照业务进行分组:将同一业务的消息发送到同一个 Group 的消息队列中。
+2. 使用顺序生产者:RocketMQ 提供了顺序生产者(OrderProducer)来保证发送到同一个 Message Queue 中的消息顺序发送。在构造消息时,可以指定消息的业务 ID,RocketMQ 会根据该 ID 将消息发送到同一个 Message Queue 中。
+3. 使用顺序消费者:RocketMQ 提供了顺序消费者(OrderConsumer)来保证同一个 Message Queue 内的消息按照顺序被消费。在订阅消息时,需要设置消费模式为 Orderly,并实现 MessageListenerOrderly 接口,在消息处理完成后需要手动调用 ack 方法通知服务器消息已被成功消费。
+
+需要注意的是,在使用顺序消费的情况下,建议同时设置消费者数量为1,否则可能会导致顺序混乱。此外,即使使用了顺序生产者和顺序消费者,仍然可能出现无法保证消息完全按照顺序进行处理的情况,例如网络问题、服务故障等原因。因此,在使用顺序消费时,应该仔细设计消息的生产和消费流程,并进行充分测试,以确保消息能够按照预期顺序进行处理。
+
+* 没有支持云原生,类似Pulsar支持云原生,kafka在添加结点的时候需要全量复制,导致耗时过长。

+ 103 - 1
面经/问答/情景设计题.md

@@ -334,4 +334,106 @@ demo:将1min分为4个小窗口,每个小窗口能处理25个请求。通过
   * 一致性哈希
 * 最小响应时间
 * 随机
-* 粘性
+* 粘性
+
+## 降级和熔断
+
+### 基本的容错模式
+
+> 常见的容错模式主要包含以下几种方式
+
+- 主动超时:Http请求主动设置一个超时时间,超时就直接返回,不会造成服务堆积
+- 限流:限制最大并发数
+- 熔断:当错误数超过阈值时快速失败,不调用后端服务,同时隔一定时间放几个请求去重试后端服务是否能正常调用,如果成功则关闭熔断状态,失败则继续快速失败,直接返回。(此处有个重试,重试就是弹性恢复的能力)
+- 隔离:把每个依赖或调用的服务都隔离开来,防止级联失败引起整体服务不可用
+- 降级:服务失败或异常后,返回指定的默认信息
+
+![img](assets/arch-x-reduce-2.png)
+
+### 服务降级
+
+> 由于爆炸性的流量冲击,对一些服务进行有策略的放弃,以此缓解系统压力,保证目前主要业务的正常运行。它主要是针对非正常情况下的应急服务措施:当此时一些业务服务无法执行时,给出一个统一的返回结果。
+
+#### 降级服务的特征
+
+- 原因:整体负荷超出整体负载承受能力。
+- 目的:保证重要或基本服务正常运行,非重要服务延迟使用或暂停使用
+- 大小:降低服务粒度,要考虑整体模块粒度的大小,将粒度控制在合适的范围内
+- 可控性:在服务粒度大小的基础上增加服务的可控性,后台服务开关的功能是一项必要配置(单机可配置文件,其他可领用数据库和缓存),可分为手动控制和自动控制。
+- 次序:一般从外围延伸服务开始降级,需要有一定的配置项,重要性低的优先降级,比如可以分组设置等级1-10,当服务需要降级到某一个级别时,进行相关配置
+
+#### 降级方式
+
+- 延迟服务:比如发表了评论,重要服务,比如在文章中显示正常,但是延迟给用户增加积分,只是放到一个缓存中,等服务平稳之后再执行。
+- 在粒度范围内关闭服务(片段降级或服务功能降级):比如关闭相关文章的推荐,直接关闭推荐区
+- 页面异步请求降级:比如商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,可以进行降级;
+- 页面跳转(页面降级):比如可以有相关文章推荐,但是更多的页面则直接跳转到某一个地址
+- 写降级:比如秒杀抢购,我们可以只进行Cache的更新,然后异步同步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
+- 读降级:比如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。
+
+#### 降级预案
+
+在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
+
+- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
+- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
+- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
+- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
+
+#### 服务降级分类
+
+- 降级按照是否自动化可分为:自动开关降级(超时、失败次数、故障、限流)和人工开关降级(秒杀、电商大促等)。
+- 降级按照功能可分为:读服务降级、写服务降级。
+- 降级按照处于的系统层次可分为:多级降级。
+
+#### 自动降级分类
+
+- 超时降级:主要配置好超时时间和超时重试次数和机制,并使用异步机制探测回复情况
+- 失败次数降级:主要是一些不稳定的api,当失败调用次数达到一定阀值自动降级,同样要使用异步机制探测回复情况
+- 故障降级:比如要调用的远程服务挂掉了(网络故障、DNS故障、http服务返回错误的状态码、rpc服务抛出异常),则可以直接降级。降级后的处理方案有:默认值(比如库存服务挂了,返回默认现货)、兜底数据(比如广告挂了,返回提前准备好的一些静态页面)、缓存(之前暂存的一些缓存数据)
+- 限流降级: 当我们去秒杀或者抢购一些限购商品时,此时可能会因为访问量太大而导致系统崩溃,此时开发者会使用限流来进行限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:排队页面(将用户导流到排队页面等一会重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)
+
+#### 服务降级需考虑的问题
+
+- 核心服务或非核心服务。
+- 是否支持降级,及其降级策略。
+- 业务放通场景,极其策略。
+
+### 服务熔断
+
+> 在学习服务熔断时,有必要区分下如下几个相关的概念。
+
+- 服务雪崩
+
+多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C有调用其他的微服务,如果整个链路上某个微服务的调用响应式过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统雪崩,所谓的”雪崩效应”
+
+- 断路器
+
+“断路器”本身是一种开关装置,当某个服务单元发生故障监控(类似熔断保险丝),向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延。乃至雪崩。
+
+- 服务熔断
+
+熔断机制是应对雪崩效应的一种微服务链路保护机制,当整个链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回”错误”的响应信息。
+
+- Hystrix
+
+Hystrix是一个用于分布式系统的延迟和容错的开源库。在分布式系统里,许多依赖不可避免的调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整个服务失败,避免级联故障,以提高分布式系统的弹性。
+
+#### 熔断流程
+
+1. 基本的断路器模式
+
+![img](assets/arch-x-reduce-3.png)
+
+它有两个基本状态(close和open)和一个基本trip动作:
+
+- close状态下, client向supplier发起的服务请求, 直接无阻碍通过断路器, supplier的返回值接直接由断路器交回给client.
+- open状态下,client向supplier发起的服务请求后,断路器不会将请求转到supplier, 而是直接返回client, client和supplier之间的通路是断的
+- trip: 在close状态下,如果supplier持续超时报错, 达到规定的阀值后,断路器就发生trip, 之后断路器状态就会从close进入open.
+
+2. 扩展的断路器模式
+
+基本的断路器模式下,保证了断路器在open状态时,保护supplier不会被调用, 但我们还需要额外的措施可以在supplier恢复服务后,可以重置断路器。一种可行的办法是断路器定期探测supplier的服务是否恢复, 一但恢复, 就将状态设置成close。断路器进行重试时的状态为半开(half-open)状态。
+
+![image-20230831172005636](assets/image-20230831172005636.png)
+

+ 25 - 0
面经/问答/操作系统.md

@@ -1,3 +1,28 @@
+## 虚拟内存和物理内存的区别
+
+虚拟内存和物理内存是计算机系统中两个重要的概念,它们有以下区别:
+
+1. 定义和功能:
+   - 物理内存(也称为主存)是计算机实际的硬件内存,用于存储正在运行的程序和数据。
+   - 虚拟内存是一种在物理内存和磁盘之间建立的抽象层,通过将部分数据或代码存储到磁盘上,以提供更大的可用内存空间。
+2. 大小:
+   - 物理内存的大小是硬件决定的,通常以字节为单位。
+   - 虚拟内存的大小取决于操作系统和文件系统的设计,可以比物理内存更大。
+3. 访问速度:
+   - 物理内存是基于硬件直接访问的,因此具有非常高的访问速度。
+   - 虚拟内存中的数据需要从磁盘加载到物理内存中才能访问,因此访问速度较慢。
+4. 可见性:
+   - 物理内存对处理器和其他硬件设备可见,是直接访问的对象。
+   - 虚拟内存对处理器和应用程序可见,而底层的物理细节对其是透明的。
+5. 管理方式:
+   - 物理内存的管理由操作系统负责,包括分配、回收等。
+   - 虚拟内存的管理也由操作系统负责,它通过页面置换算法(如LRU)来决定将哪些虚拟页面从磁盘加载到物理内存中。
+6. 可用空间:
+   - 物理内存的可用空间受硬件限制,一旦用尽,就无法再存储更多的程序或数据。
+   - 虚拟内存的可用空间通常比物理内存大得多,因为它可以使用磁盘上的空间作为扩展。
+
+总结来说,物理内存是计算机实际的硬件内存,直接访问速度快,大小有硬件限制;而虚拟内存是一种抽象层,通过在磁盘和物理内存之间进行数据交换,提供了更大的可用内存空间,但访问速度较慢。虚拟内存由操作系统管理,可以比物理内存更大,并使用页面置换算法来优化内存使用。
+
 ## 虚拟内存
 
 **虚拟内存**是逻辑存在的内存,他的主要作用的简化内存管理。

+ 7 - 3
面经/问答/智力问题.md

@@ -14,9 +14,7 @@
 在短的一段燃尽的瞬间,再在另一段中间随便点燃一个点,使得香保持四倍燃烧速度即可。
 香全部燃尽时就是15分钟。
 
-3. 一根不均匀的绳子,全部烧完需要1个小时,问怎样烧能计时1个小时15分钟
-
-取出三条绳子。
+3. 三根不均匀的绳子,全部烧完需要1个小时,问怎样烧能计时1个小时15分钟
 
 1、同时点燃“第一根的两头”和“第二根的一头”,第一根烧完时间过了“30分钟”;
 
@@ -24,6 +22,12 @@
 
 3、第二根烧完后马上点燃第三根绳子的两头,当第三根烧完时间又用了“30分钟”。加起来总共=30+15+30=75分钟=一个小时十五分钟。
 
+4. 两个不均匀的绳子,全部烧完需要1个小时,问怎样烧能计时1个小时15分钟
+
+1、同时点燃“第一根的两头”和“第二根的一头”,第一根烧完时间过了“30分钟”;
+
+2、第一根烧完后马上点燃第二根的另一头,到第二根烧完时间又过了“15分钟”;
+
 ## 大数问题
 
 1. 在100G文件中找出出现次数最多的100个IP

+ 311 - 0
面经/问答/集合.md

@@ -105,6 +105,11 @@ RHi()函数是不同于H()的哈希函数,用于同义词发生地址冲突时
 
 ### 4. 建立公共溢出区:将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中。
 
+## hashmap1.8死循环
+
+还有可能卡在**at java.util.HashMap$TreeNode.balanceInsertion(HashMap.java:2229)**
+可能**Node节点转换为TreeNode结点异常**,红黑树再平衡的时候会导致死循环
+
 ## HashTable如何保证线程安全
 
 HashMap是非同步的,没有对读写等操作进行锁保护,是线程不安全的。
@@ -196,3 +201,309 @@ Java 7 中 `ConcurrentHashMap` 的存储结构如上图,`ConcurrnetHashMap` 
 2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
 3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
 4. 如果是链表,遍历查找之。
+
+### 扩容方法
+
+```java
+//新增元素时,也就是在调用 putVal 方法后,为了通用,增加了个 check 入参,用于指定是否可能会出现扩容的情况
+//check >= 0 即为可能出现扩容的情况,例如 putVal方法中的调用
+private final void addCount(long x, int check){
+    ... ...
+    if (check >= 0) {
+        Node<K,V>[] tab, nt; int n, sc;
+        //检查当前集合元素个数 s 是否达到扩容阈值 sizeCtl ,扩容时 sizeCtl 为负数,依旧成立,同时还得满足数组非空且数组长度不能大于允许的数组最大长度这两个条件才能继续
+        //这个 while 循环除了判断是否达到阈值从而进行扩容操作之外还有一个作用就是当一条线程完成自己的迁移任务后,如果集合还在扩容,则会继续循环,继续加入扩容大军,申请后面的迁移任务
+        while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
+            int rs = resizeStamp(n);
+            // sc < 0 说明集合正在扩容当中
+            if (sc < 0) {
+                //判断扩容是否结束或者并发扩容线程数是否已达最大值,如果是的话直接结束while循环
+                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
+                    break;
+                //扩容还未结束,并且允许扩容线程加入,此时加入扩容大军中
+                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
+                    transfer(tab, nt);
+            }
+            //如果集合还未处于扩容状态中,则进入扩容方法,并首先初始化 nextTab 数组,也就是新数组
+            //(rs << RESIZE_STAMP_SHIFT) + 2 为首个扩容线程所设置的特定值,后面扩容时会根据线程是否为这个值来确定是否为最后一个线程
+            else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
+                transfer(tab, null);
+            s = sumCount();
+        }
+    }
+}
+
+
+
+//扩容状态下其他线程对集合进行插入、修改、删除、合并、compute等操作时遇到 ForwardingNode 节点会调用该帮助扩容方法 (ForwardingNode 后面介绍)
+final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
+    Node<K,V>[] nextTab; int sc;
+    if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
+        int rs = resizeStamp(tab.length);
+        //此处的 while 循环是上面 addCount 方法的简版,可以参考上面的注释
+        while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {
+            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
+                sc == rs + MAX_RESIZERS || transferIndex <= 0)
+                break;
+            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
+                transfer(tab, nextTab);
+                break;
+            }
+        }
+        return nextTab;
+    }
+    return table;
+}
+
+//putAll批量插入或者插入节点后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容会调用到这个方法
+private final void tryPresize(int size) {
+    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
+    int sc;
+    //如果不满足条件,也就是 sizeCtl < 0 ,说明有其他线程正在扩容当中,这里也就不需要自己去扩容了,结束该方法
+    while ((sc = sizeCtl) >= 0) {
+        Node<K,V>[] tab = table; int n;
+        //如果数组初始化则进行初始化,这个选项主要是为批量插入操作方法 putAll 提供的
+        if (tab == null || (n = tab.length) == 0) {
+            n = (sc > c) ? sc : c;
+            //初始化时将 sizeCtl 设置为 -1 ,保证单线程初始化
+            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
+                try {
+                    if (table == tab) {
+                        @SuppressWarnings("unchecked")
+                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
+                        table = nt;
+                        sc = n - (n >>> 2);
+                    }
+                } finally {
+                    //初始化完成后 sizeCtl 用于记录当前集合的负载容量值,也就是触发集合扩容的阈值
+                    sizeCtl = sc;
+                }
+            }
+        }
+        else if (c <= sc || n >= MAXIMUM_CAPACITY)
+            break;
+        //插入节点后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容会进入到下面这个 else if 分支
+        else if (tab == table) {
+            int rs = resizeStamp(n);
+            //下面的内容基本跟上面 addCount 方法的 while 循环内部一致,可以参考上面的注释
+            if (sc < 0) {
+                Node<K,V>[] nt;
+                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
+                    break;
+                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
+                    transfer(tab, nt);
+            }
+            else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
+                transfer(tab, null);
+        }
+    }
+}
+```
+
+> **说明:总的来说**
+>
+> 1. **在调用 addCount 方法增加集合元素计数后发现当前集合元素个数到达扩容阈值时就会触发扩容 。**
+>
+> 2. **扩容状态下其他线程对集合进行插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode 节点会触发扩容 。**帮助该桶进行扩容
+> 3. **putAll 批量插入或者插入节点后发现存在链表长度达到 8 个或以上,但数组长度为 64 以下时会触发扩容  。**
+>
+> **注意:桶上链表长度达到 8 个或者以上,并且数组长度为 64 以下时只会触发扩容而不会将链表转为红黑树 。**
+
+```java
+//调用该扩容方法的地方有:
+//java.util.concurrent.ConcurrentHashMap#addCount        向集合中插入新数据后更新容量计数时发现到达扩容阈值而触发的扩容
+//java.util.concurrent.ConcurrentHashMap#helpTransfer    扩容状态下其他线程对集合进行插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode 节点时触发的扩容
+//java.util.concurrent.ConcurrentHashMap#tryPresize      putAll批量插入或者插入后发现链表长度达到8个或以上,但数组长度为64以下时触发的扩容
+private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
+    int n = tab.length, stride;
+    // 计算每条线程处理的桶个数,每条线程处理的桶数量一样,如果CPU为单核,则使用一条线程处理所有桶
+    // 每条线程至少处理16个桶,如果计算出来的结果少于16,则一条线程处理16个桶
+    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
+        stride = MIN_TRANSFER_STRIDE; // subdivide range
+    if (nextTab == null) {            // 初始化新数组(原数组长度的2倍)
+        try {
+            @SuppressWarnings("unchecked")
+            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
+            nextTab = nt;
+        } catch (Throwable ex) {      // try to cope with OOME
+            sizeCtl = Integer.MAX_VALUE;
+            return;
+        }
+        nextTable = nextTab;
+        //将 transferIndex 指向最右边的桶,也就是数组索引下标最大的位置
+        transferIndex = n;
+    }
+    int nextn = nextTab.length;
+    // 新建一个占位对象,该占位对象的 hash 值为 -1 该占位对象存在时表示集合正在扩容状态,key、value、next 属性均为 null ,nextTable 属性指向扩容后的数组
+    // 该占位对象主要有两个用途:
+    // 1、占位作用,用于标识数组该位置的桶已经迁移完毕,处于扩容中的状态。
+    // 2、作为一个转发的作用,扩容期间如果遇到查询操作,遇到转发节点,会把该查询操作转发到新的数组上去,不会阻塞查询操作。
+    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
+    // 该标识用于控制是否继续处理下一个桶,为 true 则表示已经处理完当前桶,可以继续迁移下一个桶的数据
+    boolean advance = true;
+    // 该标识用于控制扩容何时结束,该标识还有一个用途是最后一个扩容线程会负责重新检查一遍数组查看是否有遗漏的桶
+    boolean finishing = false; // to ensure sweep before committing nextTab
+    // 这个循环用于处理一个 stride 长度的任务,i 后面会被赋值为该 stride 内最大的下标,而 bound 后面会被赋值为该 stride 内最小的下标
+    // 通过循环不断减小 i 的值,从右往左依次迁移桶上面的数据,直到 i 小于 bound 时结束该次长度为 stride 的迁移任务
+    // 结束这次的任务后会通过外层 addCount、helpTransfer、tryPresize 方法的 while 循环达到继续领取其他任务的效果
+    for (int i = 0, bound = 0;;) {
+        Node<K,V> f; int fh;
+        while (advance) {
+            int nextIndex, nextBound;
+            // 每处理完一个hash桶就将 bound 进行减 1 操作
+            if (--i >= bound || finishing)
+                advance = false;
+            else if ((nextIndex = transferIndex) <= 0) {
+                // transferIndex <= 0 说明数组的hash桶已被线程分配完毕,没有了待分配的hash桶,将 i 设置为 -1 ,后面的代码根据这个数值退出当前线的扩容操作
+                i = -1;
+                advance = false;
+            }
+            // 只有首次进入for循环才会进入这个判断里面去,设置 bound 和 i 的值,也就是领取到的迁移任务的数组区间
+            else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
+                bound = nextBound;
+                i = nextIndex - 1;
+                advance = false;
+            }
+        }
+        if (i < 0 || i >= n || i + n >= nextn) {
+            int sc;
+            // 扩容结束后做后续工作,将 nextTable 设置为 null,表示扩容已结束,将 table 指向新数组,sizeCtl 设置为扩容阈值
+            if (finishing) {
+                nextTable = null;
+                table = nextTab;
+                sizeCtl = (n << 1) - (n >>> 1);
+                return;
+            }
+            // 每当一条线程扩容结束就会更新一次 sizeCtl 的值,进行减 1 操作
+            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
+                //(sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT 成立,说明该线程不是扩容大军里面的最后一条线程,直接return回到上层while循环
+                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
+                    return;
+                // (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT 说明这条线程是最后一条扩容线程
+                // 之所以能用这个来判断是否是最后一条线程,因为第一条扩容线程进行了如下操作:
+                //    U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
+                // 除了修改结束标识之外,还得设置 i = n; 以便重新检查一遍数组,防止有遗漏未成功迁移的桶
+                finishing = advance = true;
+                i = n; // recheck before commit
+            }
+        }
+        else if ((f = tabAt(tab, i)) == null)
+            // 遇到数组上空的位置直接放置一个占位对象,以便查询操作的转发和标识当前处于扩容状态
+            advance = casTabAt(tab, i, null, fwd);
+        else if ((fh = f.hash) == MOVED)
+            // 数组上遇到hash值为MOVED,也就是 -1 的位置,说明该位置已经被其他线程迁移过了,将 advance 设置为 true ,以便继续往下一个桶检查并进行迁移操作
+            advance = true; // already processed
+        else {
+            synchronized (f) {
+                if (tabAt(tab, i) == f) {
+                    Node<K,V> ln, hn;
+                    // 该节点为链表结构
+                    if (fh >= 0) {
+                        int runBit = fh & n;
+                        Node<K,V> lastRun = f;
+                        // 遍历整条链表,找出 lastRun 节点
+                        for (Node<K,V> p = f.next; p != null; p = p.next) {
+                            int b = p.hash & n;
+                            if (b != runBit) {
+                                runBit = b;
+                                lastRun = p;
+                            }
+                        }
+                        // 根据 lastRun 节点的高位标识(0 或 1),首先将 lastRun设置为 ln 或者 hn 链的末尾部分节点,后续的节点使用头插法拼接
+                        if (runBit == 0) {
+                            ln = lastRun;
+                            hn = null;
+                        }
+                        else {
+                            hn = lastRun;
+                            ln = null;
+                        }
+                        // 使用高位和低位两条链表进行迁移,使用头插法拼接链表
+                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
+                            int ph = p.hash; K pk = p.key; V pv = p.val;
+                            if ((ph & n) == 0)
+                                ln = new Node<K,V>(ph, pk, pv, ln);
+                            else
+                                hn = new Node<K,V>(ph, pk, pv, hn);
+                        }
+                        //setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法
+                        //使用 volatile 方式的 putObjectVolatile 方法,能够将数据直接更新回主内存,并使得其他线程工作内存的对应变量失效,达到各线程数据及时同步的效果
+                        //使用 volatile 的方式将 ln 链设置到新数组下标为 i 的位置上
+                        setTabAt(nextTab, i, ln);
+                        //使用 volatile 的方式将 hn 链设置到新数组下标为 i + n(n为原数组长度) 的位置上
+                        setTabAt(nextTab, i + n, hn);
+                        //迁移完成后使用 volatile 的方式将占位对象设置到该 hash 桶上,该占位对象的用途是标识该hash桶已被处理过,以及查询请求的转发作用
+                        setTabAt(tab, i, fwd);
+                        //advance 设置为 true 表示当前 hash 桶已处理完,可以继续处理下一个 hash 桶
+                        advance = true;
+                    }
+                    //该节点为红黑树结构
+                    else if (f instanceof TreeBin) {
+                        TreeBin<K,V> t = (TreeBin<K,V>)f;
+                        //lo 为低位链表头结点,loTail 为低位链表尾结点,hi 和 hiTail 为高位链表头尾结点
+                        TreeNode<K,V> lo = null, loTail = null;
+                        TreeNode<K,V> hi = null, hiTail = null;
+                        int lc = 0, hc = 0;
+                        //同样也是使用高位和低位两条链表进行迁移
+                        //使用for循环以链表方式遍历整棵红黑树,使用尾插法拼接 ln 和 hn 链表
+                        for (Node<K,V> e = t.first; e != null; e = e.next) {
+                            int h = e.hash;
+                            //这里面形成的是以 TreeNode 为节点的链表
+                            TreeNode<K,V> p = new TreeNode<K,V>
+                                (h, e.key, e.val, null, null);
+                            if ((h & n) == 0) {
+                                if ((p.prev = loTail) == null)
+                                    lo = p;
+                                else
+                                    loTail.next = p;
+                                loTail = p;
+                                ++lc;
+                            }
+                            else {
+                                if ((p.prev = hiTail) == null)
+                                    hi = p;
+                                else
+                                    hiTail.next = p;
+                                hiTail = p;
+                                ++hc;
+                            }
+                        }
+                        //形成中间链表后会先判断是否需要转换为红黑树:
+                        //1、如果符合条件则直接将 TreeNode 链表转为红黑树,再设置到新数组中去
+                        //2、如果不符合条件则将 TreeNode 转换为普通的 Node 节点,再将该普通链表设置到新数组中去
+                        //(hc != 0) ? new TreeBin<K,V>(lo) : t 这行代码的用意在于,如果原来的红黑树没有被拆分成两份,那么迁移后它依旧是红黑树,可以直接使用原来的 TreeBin 对象
+                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
+                        (hc != 0) ? new TreeBin<K,V>(lo) : t;
+                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
+                        (lc != 0) ? new TreeBin<K,V>(hi) : t;
+                        //setTabAt方法调用的是 Unsafe 类的 putObjectVolatile 方法
+                        //使用 volatile 方式的 putObjectVolatile 方法,能够将数据直接更新回主内存,并使得其他线程工作内存的对应变量失效,达到各线程数据及时同步的效果
+                        //使用 volatile 的方式将 ln 链设置到新数组下标为 i 的位置上
+                        setTabAt(nextTab, i, ln);
+                        //使用 volatile 的方式将 hn 链设置到新数组下标为 i + n(n为原数组长度) 的位置上
+                        setTabAt(nextTab, i + n, hn);
+                        //迁移完成后使用 volatile 的方式将占位对象设置到该 hash 桶上,该占位对象的用途是标识该hash桶已被处理过,以及查询请求的转发作用
+                        setTabAt(tab, i, fwd);
+                        //advance 设置为 true 表示当前 hash 桶已处理完,可以继续处理下一个 hash 桶
+                        advance = true;
+                    }
+                }
+            }
+        }
+    }
+}
+```
+
+#### 总结
+
+* 设计方面
+
+  * 基于sizeCtI共享变量,通知各线程当前哈希桶的状态;基于transferindex共享变量,重新划分区间,保证每一个子区间最多只有一个线程进行扩容;
+
+  * 基于双table + 标记节点,保证扩容过程中get操作的不受扩容影响;
+
+* 实现方面
+
+  * 共享变量用volatile修饰,保证线程间的可见性;
+  * sizeCtl、transferlndex采用自旋+CAS进行修改,保证原子性
+  * 节点的迁移和标记采用synchronized关键字加锁,保证原子性;

BIN
面经/项目/assets/4079e08d3ab14eabbecee2926c73b84etplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp


+ 108 - 0
面经/项目/抽奖项目.md

@@ -0,0 +1,108 @@
+## 项目介绍
+
+该项目是学校项目课设,主要应用在促销,抽奖秒杀领域的一个基础模块,2H4G服务器基础配置为kafka,redis,mysql,后端,压测最高100TPS,QPS大约800左右。
+
+## 分布式事务如何解决
+
+1.其实是这样的,使用对于跨库的事务处理,一种是分布式事务,另外一种就是基于MO+任务调度补偿的方式,完成最终一致性。
+2.那么鉴于抽奖系统的实时性要求,从用户流程体验上,希望更加流畅,支撑更大的并发量,而不是对整个流程添加过多的事务,降低性能。因为事务来说,是一种集中化的竟态,所以这部分设计上采用最终一致性的方式进行处理,而不是直接添加大块的事务。同时对于单kev的秒杀,还采用了滑块分段锁的方式进行处理,所以整人流程来看,都是希望是去中心化的,提高吞吐量的。
+
+## mq如何保证等幂性
+
+对于寡等性处理流程;
+
+1. 使用Redis缓存对于重复的MO进行消费记录,,一般记录个12小时,短一点也可以。
+2. 消费前如果缓存没有记录,可以查询数据库,消费过进行缓存记录。
+3. 最终重要的,一股对于金、订单、支付等场景,必须使用数据库防重字段做强一致性拦截处理,避免重复消费造成资损和客诉
+4. 一般来说都是更新和插入,使用数据库主键或者version字段进行去重
+
+## 系统优化
+
+这个一个商品活动秒杀的实现方案,最开始的设计是基于一个活动号ID进行锁定,秒杀时锁定这个ID,用户购买完后就进行释放,但在大量用户抢购时,出现了秒杀分布式锁后的业务逻辑处理中发生异常,释放锁失败,导致所有的用户都不能再拿到锁,也就造成了有商品但不能下单的问题事故处理:优化独占竟态为分段静态,将活动ID+库存编号作为动态锁标识。当前秒杀的用户如果发生锁失败那么后面的用户可以继续秒杀不受影响。而失败的锁会有worker进行补偿恢复,那么最终会避免超卖以及不能售卖学习总结:核心的技术实现需要经过大量的数据验证以及压测,否则各个场景下很难评估是否会有风险。当然这也不是唯一的实现方案,可以根据不同的场景有不同的实现处理.
+
+## 表设计
+
+1. 先介绍业务;抽奖系统作为营销活动平台中的一个环节,承接着活动玩法、积分消耗、奖品发放等系统的纽带,帮助整个业务完成用户的活跃。 
+2. 后阐述领域;作为一个战略设计的一环,战术实现上要尽可能做到职责隔离,对应系统的具体实现上要拆分出;活动、算法、规则、策略、用户、订单等领域。 
+3. 引入表设计;根据领域驱动中对各个模块的定义,设计数据库表,也就对应了活动表、抽奖策略配置表、准入规则引擎表、用户抽奖单记录表、以及配合这些表数据结构运行的其他表,如:记录用户的参与次数等。
+
+## 如何评估QPS
+
+首先需要根据业务提供的推广规模、渠道、人数,来评估。- 这里前面按照28法则评估过。 - 假如系统有1000万用户,那么每天来点击页面的占比20%,也就是200万用户访问。 - 假设平均每个用户点击50次,那么总用有1亿的PV - 一天24个小时,平均活跃时间段算在5个小时内【24*20%】,那么5个小时预计有8000万点击,也就是平均每秒4500个请求。 - 4500是一个均值,按照电商类峰值的话,一般是3~4倍均值量,也就是5个小时每秒18000个请求【QPS=1.8万】
+
+## 为什么要选redis,redis 主从集群下潜在的锁失效问题怎么考虑怎么解决。
+
+Redis作为一种高性能的内存数据库,其提供的分布式锁机制可以满足高并发场景下的锁控制需求。相比于传统的基于数据库的锁机制,Redis分布式锁具有更高的性能和更好的可扩展性。
+
+同时,Redis还提供了多种锁实现方式,如SETNX、SET、NX、PX等,可以根据具体的业务场景选择最适合的方式。 
+
+在Redis主从集群下,由于主从节点之间的数据同步存在一定的延迟,可能会导致锁的失效问题。为了解决这个问题,可以采用以下几种方式: 
+
+- 使用RedLock算法:RedLock是一种多实例分布式锁算法,可以在Redis集群中实现更安全的锁机制,避免单点故障和网络分区问题。
+- 设置适当的超时时间:在设置锁的过期时间时,可以适当增加一些缓冲时间,避免因主从同步延迟导致锁失效。 
+- 使用Redis Sentinel进行故障转移:通过配置Redis Sentinel进行主从切换,确保锁服务的高可用性。
+
+## zookeeper 作为分布式锁优缺点
+
+Zookeeper作为一种高可用的分布式协调服务,其提供的分布式锁机制可以满足高并发场景下的锁控制需求。相比于Redis分布式锁,Zookeeper分布式锁具有以下优缺点:
+
+* 优点:   
+  * 可以避免锁的失效问题:Zookeeper采用基于ZAB协议的分布式一致性算法,可以保证分布式锁的强一致性,避免因主从同步延迟导致锁失效问题。  
+  * 支持更复杂的锁机制:Zookeeper提供了两种锁实现方式:共享锁和排他锁,可以根据具体的业务场景选择最适合的方式。 
+  * 可以与其他Zookeeper服务集成:Zookeeper还提供了诸如分布式队列、命名服务等功能,可以与分布式锁一起使用,构建更完整的分布式应用系统。 
+* 缺点:  
+  * 性能相对较低:Zookeeper采用基于ZAB协议的分布式一致性算法,需要进行多次网络通信和数据同步,相比于Redis分布式锁,性能相对较低。  
+  * 部署和维护成本较高:Zookeeper需要部署专门的服务器集群,需要进行一定的配置和维护工作,相比于Redis分布式锁,部署和维护成本较高。
+
+## 还有没有其他方案-回答mysql,问 mysql 做分布式锁的优缺点
+
+MySQL作为一种传统的关系型数据库,其提供的分布式锁机制可以满足一定程度上的锁控制需求。相比于Redis和Zookeeper分布式锁,MySQL分布式锁具有以下优缺点: 
+
+* 优点:  
+  * 易于部署和维护:MySQL已经广泛应用于各种应用场景中,部署和维护相对较为简单。  
+  * 支持更复杂的锁机制:MySQL提供了多种锁实现方式,如行锁、表锁、读锁、写锁等,可以根据具体的业务场景选择最适合的方式。 
+* 缺点:  
+  * 性能较低:MySQL采用基于磁盘的存储方式,相比于Redis和Zookeeper,性能较低。  
+  * 可扩展性较差:由于MySQL采用基于磁盘的存储方式,其可扩展性较差,难以应对高并发场景下的锁控制需求。  
+  * 存在单点故障问题:MySQL采用主从复制的方式进行数据同步,存在单点故障问题,可能导致锁失效问题。
+
+## 如果redis宕机导致incr失败怎么办
+
+可以把 Redis 滑块锁的 key 的有效期设置为活动结束时间,这样即使宕机重启,key 也没有被释放。
+
+## 为什么不能使用自增主键
+
+如果使用自增主键,会导致无法复杂均衡。导致尾部热点问题。
+
+* 导致分片不均匀
+* 导致自增锁和插入锁
+* 导致合并数据发生冲突
+* 导致不同节点数据冲突
+
+## UUID为什么不能作为主键
+
+* UUID是随机无序产生的,插入新数据时会导致索引的分裂和磁盘碎片,影响查询性能
+* UUID不太容易理解
+
+## 雪花算法
+
+![ae.png](assets/4079e08d3ab14eabbecee2926c73b84etplv-k3u1fbpfcp-zoom-in-crop-mark1512000.awebp)
+
+雪花算法原理就是生成一个的64位比特位的 long 类型的唯一 id。
+
+- 最高1位固定值0,因为生成的 id 是正整数,如果是1就是负数了。
+- 接下来41位存储毫秒级时间戳,2^41/(1000*60*60*24*365)=69,大概可以使用69年。
+- 再接下10位存储机器码,包括5位 datacenterId 和5位 workerId。最多可以部署2^10=1024台机器。
+- 最后12位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成2^12=4096个不重复 id。
+
+雪花算法有以下几个优点:
+
+- 高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
+- 基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
+- 不依赖第三方库或者中间件。
+- 算法简单,在内存中进行,效率高。
+
+雪花算法有如下缺点:
+
+- 依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。
+- 时间跨度只有69年,可以优化一下算法生成,生成时间戳减去系统上线的时间。