Skip to content

Latest commit

 

History

History
596 lines (382 loc) · 31.1 KB

基础知识整理.md

File metadata and controls

596 lines (382 loc) · 31.1 KB

数据库

MySQL 主从切换流程 MySQL 分片与扩容流程

缓存

redis zset 实现redis zset内部实现

skiplist 如何维护每层的节点数量:skiplist 不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。

随机层数如何产生:并不是一个随机函数产生

randomLevel()
    level := 1
    // random()返回一个[0...1)的随机数
    while random() < p and level < MaxLevel do
        level := level + 1
    return level

skiplist 的特性:插入查找都是 log(n) 的复杂度,==支持 range==

redis 哨兵和集群

MQ

Kafka

  1. Kafka 基本架构和原理:https://juejin.im/post/6844903893327937543
  2. 浅谈 Kafka 中 acks 参数对消息持久化的影响:https://my.oschina.net/u/4280438/blog/4523361
  3. Kafka 0.11.0.0 是如何实现 Exactly-once 语义的:https://www.jianshu.com/p/5d889a67dcd3
  4. Kafka 设计解析(八)- Exactly Once 语义与事务机制原理:http://www.jasongj.com/kafka/transaction/

image.png

Consumer Group

image.png

Partition

Kafka 只保证在同一个 partition 内部消息是有序的,在不同 partition 之间,并不能保证消息有序。

image.png

Segment

一个 topic 可以分成若干个 partition。事实上,partition 并不是最终的存储粒度,partition 还可以细分为 segment,一个 partition 物理上由多个 segment 组成。

image.png

如上图,“.index” 索引文件存储大量的元数据,“.log” 数据文件存储大量的消息,索引文件中的元数据指向对应数据文件中 message 的物理偏移地址。其中以 “.index” 索引文件中的元数据 [3, 348] 为例,在 “.log” 数据文件表示第 3 个消息,即在全局 partition 中表示 170410+3=170413 个消息,该消息的物理偏移地址为 348。

Zero Copy

  1. 基于 mmap 的索引
  2. TransportLayer 是 Kafka 传输层的接口。它的某个实现类使用了 FileChannel 的 transferTo 方法,该方法底层使用 sendfile 实现了 Zero Copy。

副本同步和选举⭐️

ISR

Kafka 机制中,leader 将负责维护和跟踪一个 **ISR(In-Sync Replicas)**列表,即同步副本队列,这个列表里面的副本与 leader 保持同步,状态一致。如果新的 leader 从 ISR 列表中的副本中选出,那么就可以保证新 leader 为优选。

所有的副本(replicas)统称为 Assigned Replicas,即 ==AR==。ISR 是 AR 中的一个子集,由 leader 维护 ISR 列表,==follower 从 leader 同步数据有一些延迟(由参数 replica.lag.time.max.ms 设置超时阈值),超过阈值的 follower 将被剔除出 ISR==, 存入 OSR(Outof-Sync Replicas)列表,新加入的 follower 也会先存放在 OSR 中。

HW 和 LEO

image.png

HW

image.png

HW 俗称高水位,HighWatermark 的缩写,==取一个 partition 对应的 ISR 中最小的 LEO 作为 HW==,consumer 最多只能消费到 HW 所在的位置。另外每个 replica 都有 HW,leader 和 follower 各自负责更新自己的 HW 的状态。 ==对于leader 新写入的消息,consumer 不能立刻消费,leader 会等待该消息被所有 ISR 中的 replicas 同步后更新 HW,此时消息才能被 consumer 消费==。这样就保证了如果 leader 所在的 broker 失效,该消息仍然可以从新选举的 leader 中获取。对于来自内部 broker 的读取请求,没有 HW 的限制。

image.png

Kafka 引入了 ISR 的机制,ISR 中都是比较活跃的副本(即使中间有副本同步太慢,也会因为超时被踢出 ISR 进入 OSR),所以同步速度也会非常快。

request.required.acks 参数

  • 1:默认值,producer 发送数据到 leader,==leader 写本地日志成功,返回客户端成功;此时 ISR 中的其它副本还没有来得及拉取该消息==,如果此时 leader 宕机了,那么此次发送的消息就会丢失;
  • 0:producer 不停向 leader 发送数据,而不需要 leader 反馈成功消息,这种情况下数据传输效率最高,但是数据可靠性确是最低的。可能在发送过程中丢失数据,可能在 leader 宕机时丢失数据;
  • -1:producer 发送数据给 leader,==leader 收到数据后要等到 ISR 列表中的所有副本都同步数据完成后(强一致性),才向生产者返回成功消息==,如果一直收不到成功消息,则认为发送数据失败会自动重发数据。这是可靠性最高的方案,当然,性能也会受到一定影响。

https://kafka.apache.org/0102/documentation.html#producerconfigs

HW 机制

image.png

image.png

如上图,某个 topic 的某 partition 有三个副本,分别为 A、B、C。A 作为 leader 肯定是 LEO 最高,B 紧随其后,C 机器由于配置比较低,网络比较差,故而同步最慢。这个时候 A 机器宕机,这时候如果 B 成为 leader,假如没有 HW,在 A 重新恢复之后会做同步(makeFollower) 操作,在宕机时 log 文件之后直接做追加操作,而假如 B 的 LEO 已经达到了 A 的 LEO,会产生数据不一致的情况,所以使用 HW 来避免这种情况。 A 在做同步操作的时候,先将 log 文件截断到之前自己的 HW 的位置,即 3,之后再从 B 中拉取消息进行同步。

==如果失败的 follower 恢复过来,它首先将自己的 log 文件截断到上次 checkpointed 时刻的 HW 的位置,之后再从 leader 中同步消息。leader 挂掉会重新选举,新的 leader 会发送 “指令” 让其余的 follower 截断至自身的 HW 的位置然后再拉取新的消息。==

Leader 选举

Kafka 在 ZooKeeper 中为每一个 partition 动态的维护了一个 ISR,这个 ISR 里的所有 replica 都与 leader 保持同步,只有 ISR 里的成员才能有被选为 leader 的可能(通过参数配置 unclean.leader.election.enable=false)。在这种模式下,对于 f+1 个副本,一个 Kafka topic 能在保证不丢失已经 commit 消息的前提下容忍 f 个副本的失败,在大多数使用场景下,这种模式是十分有利的。事实上,==对于任意一条消息,只有它被 ISR 中的所有 follower 都从 leader 复制过去才会被认为已提交,并返回信息给 producer,从而保证可靠性==。但与 “少数服从多数” 策略不同的是,Kafka ISR 列表中副本的数量不需要超过副本总数的一半,即不需要满足 “多数派” 原则,通常,ISR 列表副本数大于等于 2 即可,如此,便在可靠性和吞吐量方面取得平衡。

记录消费进度 Offset

在 consumer 对指定消息 partition 的消息进行消费的过程中,需要定时地将 partition 消息的消费进度 Offset 记录到 ZooKeeper上,以便在该 consumer 进行重启或者其它 consumer 重新接管该消息分区的消息消费权后,能够从之前的进度开始继续进行消息消费。

Follower 副本消息同步的完整流程

首先,Follower 发送 FETCH 请求给 Leader。接着,Leader 会读取底层日志文件中的消息数据,再更新它内存中的 Follower 副本的 LEO 值,更新为 FETCH 请求中的 fetchOffset 值。最后,尝试更新分区高水位值。Follower 接收到 FETCH 响应之后,会把消息写入到底层日志,接着更新 LEO 和 HW 值。

Leader 和 Follower 的 HW 值更新时机是不同的,Follower 的 HW 更新永远落后于 Leader 的 HW。这种时间上的错配是造成各种不一致的原因。

⭐️at-least-once/at-most-once/exactly-once

  • at-least-once:如果 producer 收到来自 Kafka broker 的确认(ack)或者 acks = all,则表示该消息已经写入到 Kafka。但如果 producer ack 超时或收到错误,则可能会重试发送消息,客户端会认为该消息未写入 Kafka。如果 broker 在发送 Ack 之前失败,但在消息成功写入 Kafka 之后,此重试将导致该消息被写入两次,因此消息会被不止一次地传递给最终 consumer,这种策略可能导致重复的工作和不正确的结果。
  • at-most-once:如果在 ack 超时或返回错误时 producer 不重试,则该消息可能最终不会写入 Kafka,因此不会传递给 consumer。在大多数情况下,这样做是为了避免重复的可能性,业务上必须接收数据传递可能的丢失。
  • exactly-once:即使 producer 重试发送消息,消息也会保证最多一次地传递给最终 consumer。该语义是最理想的,但也难以实现,这是因为它需要消息系统本身与生产和消费消息的应用程序进行协作。例如如果在消费消息成功后,将 Kafka consumer 的偏移量 rollback,我们将会再次从该偏移量开始接收消息。这表明消息传递系统和客户端应用程序必须配合调整才能实现 excactly-once。

Questions:

  • Kafka 选主怎么做的?
  • kafka 与 rabbitmq 区别?
  • kafka 分区怎么同步的?
  • kafka 怎么保证不丢消息的?
  • kafka 为什么可以扛住这么高的 qps?
  • kafka partition broker consumer consumer group topic 等都是啥关系?
  • Kafka 的消费者如何做消息去重?
  • MySQL 去重、Redis 去重、假如场景量极大且允许误判,布隆过滤器也可以
  • Kafka 的 ConsumerGroup

ZooKeeper & etcd

TODO

缓存和数据库一致性问题

  1. 双写又不一致…我该怎么办?:https://mp.weixin.qq.com/s/7b044M3ksi_FPm0U5PlvMg
  2. 如何保证缓存与数据库的双写一致性?: https://mp.weixin.qq.com/s/3Pj5qqGl2MPXGZQkmcrbuA
  3. ⭐️缓存更新的套路:https://coolshell.cn/articles/17416.html

先数据库后缓存(✅)

  1. 先更新数据库再更新缓存:(❌)
    1. 数据库 AB 操作顺序,缓存 BA 顺序
    2. 频繁更新缓存,当缓存不一定用得到
  2. 先更新数据库再删除缓存
    1. Cache Aside Pattern (最常用)(✅)
      1. 读:先缓存,缓存没有再读数据库并更新缓存
      2. 写:写数据库,删除缓存
    2. 为什么是删除缓存而不是更新缓存:其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。
    3. 如果删除缓存失败,缓存里还是老数据,不会被更新(理论上来说,必须要支持事务才能保证一致性,2PC)
    4. 极端情况下会有不一致的问题:比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据(几率非常小)
  3. 通过 binlog 等异步更新缓存
    1. 读:只读缓存
    2. 写:只写数据库,缓存通过异步更新

先缓存后数据库(❌)

  1. 先更新缓存再更新数据库
    1. AB BA 问题(同上)
  2. 删除缓存再更新数据库
    1. 会造成不一致:
      1. 请求 A 进行写操作,删除缓存
      2. 请求 B 查询发现缓存不存在
      3. 请求 B 去数据库查询得到旧值
      4. 请求 B 将旧值写入缓存
      5. 请求 A 将新值写入数据库
    2. 解决办法
      1. **延时双删策略,**更新数据库,删除缓存,sleep xms 再次删除缓存
      2. 操作放到队列里串行化

分布式锁

  1. 分布式锁在存储系统中的技术实践:https://mp.weixin.qq.com/s/X-Ic91s98AFjHMoA7tzYQw
  2. ⭐️再有人问你分布式锁,这篇文章扔给他:https://juejin.im/post/6844903688088059912
  3. ⭐️基于Redis的分布式锁真的安全吗?(上):https://mp.weixin.qq.com/s/1bPLk_VZhZ0QYNZS8LkviA
  4. ⭐️基于Redis的分布式锁真的安全吗?(下):https://mp.weixin.qq.com/s/1HvQJaUKHcAqSa224efNmw
  5. sc_recipes_Locks:https://zookeeper.apache.org/doc/r3.4.9/recipes.html#sc_recipes_Locks
  6. The Chubby lock service for loosely-coupled distributed systems:https://research.google/pubs/pub27897/
  7. 分布式锁的实现之 redis 篇:https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
  8. ⭐️SOFAJRaft-RheaKV 分布式锁实现剖析 | SOFAJRaft 实现原理:https://www.sofastack.tech/blog/sofa-jraft-rheakv-distributedlock/

分布式锁的特点:互斥性、可重入性、超时、高可用、支持阻塞和非阻塞、支持公平锁和非公平锁 实现方法:MySQL、ZK(Curator)、Redis(RedLock)、...

MySQL 实现

ZK 实现

image.png

Curator 加锁流程:

  1. 首先进行可重入的判定:这里的可重入锁记录在 ConcurrentMap<Thread, LockData> threadData 这个 Map 里面,如果 threadData.get(currentThread) 是有值的那么就证明是可重入锁,然后记录就会加 1。我们之前的 Mysql 其实也可以通过这种方法去优化,可以不需要 count 字段的值,将这个维护在本地可以提高性能。
  2. 然后在我们的资源目录下创建一个节点:比如这里创建一个 /0000000002 这个节点,这个节点需要设置为 ==EPHEMERAL_SEQUENTIAL== 也就是临时节点并且有序。
  3. 获取当前目录下所有子节点,判断自己的节点是否位于子节点第一个。
    1. 如果是第一个,则获取到锁,那么可以返回。
    2. 如果不是第一个,则证明前面已经有人获取到锁了,那么需要获取自己节点的前一个节点。/0000000002 的前一个节点是 /0000000001,我们获取到这个节点之后,再上面注册 Watcher(这里的 watcher 其实调用的是 object.notifyAll(),用来解除阻塞)。
  4. object.wait(timeout) 或 object.wait():进行阻塞等待这里和我们第 3.b 步的 watcher 相对应。

​ 解锁流程:

  1. 首先进行可重入锁的判定:如果有可重入锁只需要次数减 1 即可,减 1 之后加锁次数为 0 的话继续下面步骤,不为 0 直接返回。
  2. 删除当前节点。
  3. 删除 threadDataMap 里面的可重入锁的数据。

这里之所以要创建一个 EPHEMERAL_SEQUENTIAL 节点,然后注册自己前一个节点的 watcher,而不是 watch 同一个节点的原因:

  1. 防止“惊群效应”,唤醒所有的 watcher 去抢夺锁
  2. 公平

Redis 实现

Redission

RedLock

  1. 获取当前时间(毫秒数)
  2. 按顺序依次向 N 个 Redis 节点执行获取锁的操作,为了保证在某个 Redis 节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间,它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个 Redis 节点获取锁失败以后,应该立即尝试下一个 Redis 节点
  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第 1 步记录的时间。如果客户端从大多数 Redis 节点(>= N/2+1)成功获取到了锁,==并且获取锁总共消耗的时间没有超过锁的有效时间 (lock validity time),那么这时客户端才认为最终获取锁成功==;否则,认为最终获取锁失败。
  4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第 3 步计算出来的获取锁消耗的时间。
  5. 如果最终获取锁失败了(可能由于获取到锁的 Redis 节点个数少于 N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有 Redis 节点发起释放锁的操作

分布式锁的安全问题

==GC 的 STW、时钟发生跳跃、长时间的网络 I/O==

阿里云的实现

  1. 严格互斥性

    存储场景的使用方式

image.png

​ 越界场景

image.png

​ 解决方法:存储系统引入 IO Fence,操作 Storege 的时候带上 seq,如果当前 seq 小于 Storage 的 seq,就拒绝

image.png

  1. ==可用性==

    通过==心跳==来保证锁的健壮性,针对异常的用户进程持续占据锁的场景,为了保证锁最终可以被调度,提供了可以安全释放锁的会话加黑机制。==将会话加黑后,心跳将不能正常维护==,最终导致会话过期,锁节点被安全释放。

  2. ==切换效率==

    同时结合具体的业务场景,例如守护进程发现锁持有进程挂掉的场景,提供锁的 CAS 释放操作,使得进程可以零等待进行抢锁。比如利用在锁节点中存放进程的唯一标识,强制释放已经不再使用的锁,并重新争抢,该方式可以彻底避免进程升级或意外重启后抢锁需要的等待时间。

分布式系统

一致性

顺序一致性 线性一致性:https://www.yuque.com/littleneko/note/1507354

Paxos/Raft

  1. 分布式系统的事务处理:https://coolshell.cn/articles/10910.html

延迟任务系统

  1. Java 延时任务方案: https://juejin.im/post/6844904121787482119
  2. 有赞延迟队列设计: https://tech.youzan.com/queuing_delay/
  3. Redis ZSet 的几种使用场景: https://zhuanlan.zhihu.com/p/147912757
  4. 千万级延时任务队列如何实现,看美图开源的-LMSTFY: https://www.chainnews.com/articles/332847148440.htm

方案分析

  1. 线程池
  2. Java DelayQueue
  3. 数据库
  4. ==Redis==
    1. ==ZSet==

有赞的实现:

image.png

对于多实例部署的情况下多个 Timer 消费的问题,可以使用分布式锁解决

  1. expire event
  2. 消息队列
    1. kafka
    2. RabbitMQ
    3. ==RocketMQ==:本身支持延迟消息
  3. ==时间轮==

image.png

时间轮过大的解决方案:层级时间轮

image.png

负载均衡

TODO

服务发现

  1. 聊聊微服务的服务注册与发现:http://jm.taobao.org/2018/06/26/%E8%81%8A%E8%81%8A%E5%BE%AE%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%9C%8D%E5%8A%A1%E6%B3%A8%E5%86%8C%E4%B8%8E%E5%8F%91%E7%8E%B0/
  2. 聊一聊微服务架构中的服务发现系统:https://mp.weixin.qq.com/s/IhsLvbhr8-jwg4nW-P7CRQ

熔断

TODO

GC算法

  1. 《垃圾回收的算法与实现》.中村成洋 , 相川光 , 竹内郁雄 (作者). 丁灵 (译者)
  • 标记清除
  • 引用计数
  • 复制
  • 标记压缩
  • 保守式 GC
  • 分代垃圾回收
  • 增量式垃圾回收

Java

并发编程

  • 线程安全性
  • volatile
  • 原子变量
  • CAS

数据结构实现

Map/ConcurrentMap

如何扩容

JVM

内存模型

GC

Golang

内存模型

  1. The Go Memory Model: https://golang.org/ref/mem

Goroutine 调度

  1. ⭐️Go 语言设计与实现 - 6.5 调度器:https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/
  2. Go 为什么这么“快”:https://mp.weixin.qq.com/s/ihJFa5Wir4ohhZUXVSBvMQ

image.png

image.png

goroutine 是怎么调度的? goroutine 和 kernel thread 之间是什么关系?

GC

  1. ⭐️Go 语言设计与实现 - 7.2 垃圾收集器:https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
  2. 关于Golang GC的一些误解--真的比Java算法更领先吗?:https://mp.weixin.qq.com/s/eDd212DhjIRGpytBkgfzAg

栈空间管理

  1. https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-stack-management/

逃逸分析 Go 语言的逃逸分析遵循以下两个不变性:

  • ==指向栈对象的指针不能存在于堆中==;

  • ==指向栈对象的指针不能在栈对象回收后存活==;

image.png

分段栈和连续栈

栈扩容和栈缩容

编译器会在 cmd/internal/obj/x86.stacksplit 函数中==为函数调用插入 runtime.morestack 运行时检查==,它会在几乎所有的函数调用之前检查当前 Goroutine 的栈内存是否充足,如果当前栈需要扩容,我们会保存一些栈的相关信息并调用 runtime.newstack 创建新的栈。 如果要触发栈的缩容,新栈的大小会是原始栈的一半,不过如果新栈的大小低于程序的最低限制 2KB,那么缩容的过程就会停止。

数据结构实现

channel

  1. https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/

数据结构 hchan struct

  1. qcount、dataqsiz、buf、sendx、recv 构建底层的循环队列
  2. sendq 和 recvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,这些等待队列使用双向链表 runtime.waitq 表示,链表中所有的元素都是 runtime.sudog 结构

发送数据

  1. lock
  2. Send
    1. 当存在等待的接收者时,通过 runtime.send 直接将数据发送给阻塞的接收者;
      1. 拷贝数据
      2. ==将等待接收数据的 Goroutine 标记成可运行状态 Grunnable 并把该 Goroutine 放到发送方所在的处理器的 runnext 上等待执行,该处理器在下一次调度时就会立刻唤醒数据的接收方==;
    2. 当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;

image.png

  1. 当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据;

接收数据

  1. lock

  2. Recv

    当存在等待的发送者时,通过 runtime.recv 直接从阻塞的发送者或者缓冲区中获取数据;

image.png

​ 当缓冲区存在数据时,从 Channel 的缓冲区中接收数据;

image.png

​ 当缓冲区中不存在数据时,等待其他 Goroutine 向 Channel 发送数据;

Concurrency

  1. https://golang.org/doc/effective_go.html#concurrency

==Do not communicate by sharing memory; instead, share memory by communicating.==

Channels: https://golang.org/doc/effective_go.html#channels

A leaky buffer The tools of concurrent programming can even make non-concurrent ideas easier to express. Here's an example abstracted from an RPC package. The client goroutine loops receiving data from some source, perhaps a network. To avoid allocating and freeing buffers, it keeps a free list, and uses a buffered channel to represent it. If the channel is empty, a new buffer gets allocated. Once the message buffer is ready, it's sent to the server on serverChan.

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not.
        select {
        case b = <-freeList:
            // Got one; nothing more to do.
        default:
            // None free, so allocate a new one.
            b = new(Buffer)
        }
        load(b)              // Read next message from the net.
        serverChan <- b      // Send to server.
    }

The server loop receives each message from the client, processes it, and returns the buffer to the free list.

func server() {
    for {
        b := <-serverChan    // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
        case freeList <- b:
            // Buffer on free list; nothing more to do.
        default:
            // Free list full, just carry on.
        }
    }
}

The client attempts to retrieve a buffer from freeList; if none is available, it allocates a fresh one. The server's send to freeList puts b back on the free list unless the list is full, in which case the buffer is dropped on the floor to be reclaimed by the garbage collector. (The default clauses in the select statements execute when no other case is ready, meaning that the selects never block.) This implementation builds a leaky bucket free list in just a few lines, relying on the buffered channel and the garbage collector for bookkeeping.

for循环中的坑

// 这段代码并不会如预期的那样输出0-9的数字,
// 而是会输出0-9不确定的数字
// 因为for循环中的i这个变量的应用会重复使用
func main() {
    fin := make(chan int, 10)
	a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    for i := range a {
        go func() {
            fmt.Println(i)
            fin <- 1
        }()
    }
    for i := 0; i < 10; i++ {
        <-fin
    }
}

// 正确的代码应该是如下所示,给func加个参数
func main() {
    fin := make(chan int, 10)
	a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    for i := range a {
        go func(i int) {
            fmt.Println(i)
            fin <- 1
        }(i)
    }
    for i := 0; i < 10; i++ {
        <-fin
    }
}

// 或者这样写
func main() {
    fin := make(chan int, 10)
	a := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    for i := range a {
        i := i
        go func() {
            fmt.Println(i)
            fin <- 1
        }()
    }
    for i := 0; i < 10; i++ {
        <-fin
    }
}

其他问题

Q1: 数组和切片有什么不同? A1: 数组大小不可改变,切片大小可变

image.png

Q2: 切片 append A2: 如下图(需要扩容和不需要扩容两种情况)

image.png

Q3: 切片扩容 A3: 扩容就是为切片分配一块新的内存空间并将原切片的元素全部拷贝过去:

  • 如果期望容量大于当前容量的两倍就会使用期望容量;

  • 如果当前切片的长度小于 1024 就会将容量翻倍;

  • 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

Q4: new 和 make 的区别 A4:

  • make 的作用是初始化内置的数据结构,也就是我们在前面提到的切片、哈希表和 Channel;
  • new 的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针;

image.png

其他

zk,etcd 一致性保证

Links

  1. https://www.cnblogs.com/liangsonghua/p/www_liangsonghua_me_38.html
  2. 写在19年初的后端社招面试经历(两年经验): 蚂蚁 头条 PingCAP: https://segmentfault.com/p/1210000018065974/read