Lifelong Learning

进步不能停

深入浅出 Go-GC

&&& Golang GC流程 Go 如何启动 GC Go 触发 GC 有三种情况: 主动调用 runtime.gc() 可能会触发 Gc 当分配对象时可能会触发 Gc 守护协程定时Gc runtime/mgc.go const ( // 根据堆分配内存情况,判断是否触发 GC gcTriggerHeap gcTriggerKind = iota // 定时触发 GC gcTriggerTime // 手动触发 GC gcTriggerCycle } func (t gcTrigger) test() bool { // ... switch t.kind { case gcTriggerHeap: // ... // 如果是堆内存分配导致的 GC,会 Check 当前堆内存的使用情况 trigger, _ := gcController.trigger() return atomic.Load64(&gcController.heapLive) >= trigger case gcTriggerTime: // 如果是守护协程 GC,则会check当前距离上一次 GC 是否已经达到 2 min if gcController.gcPercent.Load() < 0 { return false } lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime)) return lastgc != 0 && t.now-lastgc > forcegcperiod case gcTriggerCycle: // ... // 如果是手动触发 GC,check 前一轮 GC 有没有结束 return int32(t.n-work.cycles) > 0 } return true } Gc - Start Golang的标记清扫算法的关键点就是标记这一步,其采用三色标记法,从 Root 对象开始进行可达性分析,关键步骤如下: ...

September 19, 2025 · 小石堆

大模型调用的流式输出解析

从流式输出到服务端推送技术再到 Java 的 WebFlux 大模型流式输出 像 ChatGPT 这样的网页,我们不难发现问出问题后,大模型吐字是一段接一段的,但我们传统的 Http 请求,一般是每次获取一段数据就要再次发起请求一次。这是一种耗费资源的方式,简言之就是 Http 轮询(短轮询和长轮询 Comet),所以服务端主动推送数据的计数就应运而生。 服务端主动推送技术 这里抛开 Http 轮询计数,主要涉及到了 SSE 和 WebSocket,其实 SSE 和 WebSocket 都是服务于 “实时” 二字的。 SSE 协议 SSE(Server Send Events),顾名思义,服务端发送事件,是指服务端能够主动给客户端发送消息。其基于 Http 协议,需要按照 SSE 协议规范在消息响应体中填充数据,如果需要 SSE 协议,则需要 Http 长连接(默认),并且将请求中的 content-type 设置为 text/event-stream。 其原理实际上是在建立好的 Http 连接上,于客户端协商,返回的类型不为一次性的数据包,而是返回一个 Stream。而基于这个 Strem,服务器可以不断的往内部填入数据,客户端也可以依次接受数据。 其实 SSE 是比较常见的,因为很多时候,只需要服务器推送给客户端,而客户端不需要给服务器发送内容,比如说在一个常用开源容器监控系统 Dozzle 中,就能看到其身影。可以类比,如果一个系统,类似比赛的看板或者日志的看板,就比较适合用 SSE 协议。 因为 SSE 并非一个完全新的协议,而是使用了 Http 协议的功能,并定义一系列规范,所以 SSE 的优点就是: 轻量级(并非全新协议)(相较于 WebSocket) 基于 Http,基本上所有的浏览器都支持、 支持断开重连 缺点: ...

September 14, 2025 · 小石堆

Java 并发串讲

串讲内容 从 Java 的并发的手段出发: Synchorized Volatile ReentrantLock 原子操作类 再到并发的底层 JMM 内存可见性和有序性 再到常见并发工具类 ConcurrentHashMap & HashTable JUC 并发安全手段 Java 的并发一直是面试的考点,这里并发安全手段主要整理了 Synchorized 关键字、Volatile 关键字、ReentranLock、原子操作类这四个 Synchorized 特性: 支持可重入 支持偏向锁、轻量级锁、重量级锁 非公平 Sync 关键字主要作用于方法,作用是标记某个对象或类的某个方法,在同一时刻只能有一个线程进行操作。其主要用法先了解下: // 1. 作用于代码块 synchorized(lock){ } // 2. 作用于普通方法 public synchorized void xxxx(){ } // 3. 作用于静态方法 public static synchorized void xxxx(){ } 上面的代码块,从上到下,Sync 关键字加锁的粒度也是逐渐增大,对于 1 号,锁的粒度是括号里的 lock;对于 2 号,锁的粒度是调用这个方法的对象;对于 3 号,锁的粒度是这个 Class。 ...

September 9, 2025 · 小石堆

Java ArrayList & Go Slice 扩容

Java 中的 ArrayList 类似 Golang 里的 Slice,都是动态数组 ArrayList 什么是 ArrayList ArrayList 是 Java 中最常见的动态数组之一,他实现了 List 接口,底层基于数组实现,但能自动扩容,可以理解为变长数组的一种。 List<String> list = new ArrayList<>(); list.add("zhangsan"); list.add("lisi"); System.out.println(list); ArrayList 的扩容原理 当创建一个 ArrayList 的时候,默认初始的容量为 10,其内部实现类似 Object[] elementData = new Object[10] 当元素超过容量时,arraylist 就会触发扩容机制,其触发条件为 if(size == elementData.length) 扩容策略如下 newCapacity = oldCapacity + (oldCapacity + 1),简言之就是扩容为原来的 1.5 倍。 这里涉及到扩容过程的主要函数是 grow() private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity + 1); if(newCapacity - minCapacity < 0) { newCapacity = minCapacity; } // 创建新数组,复制旧数据 elementData = Arrays.copyOf(elementData, newCapacity) } 这里的扩容规则实际上并没有 Go 的复杂,但是要理解 minCapacity ...

September 3, 2025 · 小石堆

浅谈 IO 多路复用

之前面试准备过这里的八股,但觉得理解不够深,故打算再探深些 五种 IO 模型 阻塞 IO 非阻塞 IO IO 多路复用 信号驱动 IO 异步 IO 阻塞 IO 最简单,最易懂 厨子做菜,做不好服务生也等着,等到菜做好再上菜。 不难看出,效率很低,但是实现简单,对应到计算机世界中,这里用 C 语言的 read() 函数去举例(虽然 C 语言烂的一坨:) 用户态调用 read(fd, buf, count) 软中断切入内核态,内核根据 fd 查文件描述符表,找到对应的内核文件描述符 检查页缓存,是不是存在数据(之前用过,缓存还在) 缓存没有,触发磁盘 IO,等待数据,进程阻塞挂起 DMA 数据 Copy 完成,触发硬件中断,进而内核把页缓存的数据 copy_to_user 到用户空间 buf 上面就是一次完整的阻塞 IO,我们可以看出,在用户态触发 read() 后,后面的逻辑基本上都是交由内核态进程去接管的,但是该用户态进程此时卡死在这里,如果 fd 对应的是网络请求,那一个 fd 就得拉一个进程接管,假设上万个请求,那系统中会有上万个阻塞进程,所以不适合并发规模较大的场景。 非阻塞IO 厨子做菜,服务生不断轮询反问厨子有没有做好。 这一点对应到 CPU 就是进程不断调用 read(),如果数据准备好了,read()出数据,如果数据没有准备好,read()出 fasle,得到 false 的进程就去继续调用 read()。 while true: data = read(fd) // 非阻塞调用 if data == EAGAIN or data == EWOULDBLOCK: // 没有数据,立刻返回错误码 do_other_work() // 去做其他事情 else: process(data) // 一旦有数据就处理 break 也就是说,用户线程需要不断询问内核数据是否准备好,虽然能及时获取到数据,但是在没有获取到数据的时候,不会主动交出 CPU 资源,而一直占用 CPU。 ...

August 11, 2025 · 小石堆

Kotlin + SpringBoot 踩坑实录

最近在做 Kotlin 项目的时候,由于没有系统性学习过Kotlin,遇到了一个坑,当我的 Kotlin 项目的某个模块,使用 @Autowried 注解注入的时候,明明初始化过对象的成员变量,但是获取的时候,仍然为 Null。在排查完问题后,最终写下此篇,内容主要涉及到了 Kotlin 的 final 和 Spring 的代理机制。 问题复现 @Component open class ApiGw { private var endPoint:String? = null @PostConstruct open fun init() { endPoint = "初始化 endPoint" println(endPoint) println("this class" + this.javaClass.toString()) } @Cacheable(cacheNames = ["userCache"], key = "#id") fun printEndpoint(id: Long) { println(endPoint) println("this class" + this.javaClass.toString()) } } @RestController class ControllerA { @Autowired private lateinit var api: ApiGw @GetMapping("/hello") fun sayHello(): String { api.printEndpoint(123) return "Hello from Kotlin Spring Boot!" } } 上面的代码,调用 sayHello() 的时候,我预期 endPoint 的输出为 “初始化 endPoint”。因为在 @PostConstruct 的作用下,endPoint 已经被初始化过了。但是结果并非如此: ...

August 9, 2025 · 小石堆

《深入理解分布式系统》

第一章 Why we need? 以前我理解分布式系统只是简单的认为其作用是分散单机压力,而实际上是肤浅的,分布式系统有以下特点: 扩展性:现在大量数据密集型计算,而对于单机,当数据量增大到一定程度时,单机就无法扩展了,此时,需要分布式来扩展存储节点 可用性:多节点的存在,使得系统可以在规定数量节点宕机的情况下仍然保持提供正常服务。5个 9 的可用性。 高性能:现如今计算机发展早已突破摩尔定律的限制,单机性能不够时,我们可以往上堆料,但是单机的性能仍然是有上限的,而分布式可以简单的拓展系统中的计算节点,从而实现性能的扩展。 必要性:对于一些天然的场景,生来就是分布式系统,比如说跨行转账,两个行的数据库形成了天然的数据隔离,此时,需要分布式系统设计来保证这个场景下的安全。 第二章 两将军问题和拜占庭将军问题所引出的分布式难题! 在分布式系统中:节点是不可靠的,因为节点会宕机;网络是不可靠的,因为网络可能会延迟到达、不到达以及重复到达;时钟是不可靠的,因为多个节点之间的时钟难以同步。 第三章 分布式数据基础 现在大多业务都是数据密集型业务,从单机扩展到分布式的数据存储,既然有存储备份,就会有复制的概念 复制 Why we need replica? 假设系统中目前存在三个节点,为了保证三节点的一致性,replica 就是必然的,这里涉及到了三种复制模型。 单主复制 单主复制是比较简单的模型,例如之前实现的 Raft 就是单主复制模型,对于单主复制,又分为以下两个类型 同步复制:同步复制是指主节点在收到客户端日志后,将日志复制给从节点后,再确认其复制成功后,再将此次请求视为一次成功的请求,最后返回给客户端 异步复制:异步复制是指主节点在收到客户端日志后,在本机上完成操作后,立刻响应客户端,至于日志复制,则由后台异步发送给从节点 上面两种复制方式分别代表了可靠性和效率。 当实现同步的单主复制系统时,一次客户端请求可能会带来较高的延迟,因为需要所有的从节点响应后,才算一次完整的请求。 而异步的方式,虽然能极快的响应客户端请求,但是却无法确保从节点都复制了日志,例如上图,如果两个从节点都无法完成正常的复制,而主节点确正常响应了客户端请求,那么会大大降低系统的可用性。 单主复制的优缺点 优点: 单主复制系统设计简单,容易理解 单主复制的系统,友好高读低写场景,可以将读请求分散到其余从节点上,即使从节点压力过大,单主复制系统也易于扩展 由于其他节点并不处理写请求,只有 Leader 需要考虑并发请求,因此只要 Leader 保证自己的操作顺序,则其他节点亦能保证 缺点: 不难发现,单主复制只有 One Leader,当 Leader 节点宕机后,整个系统会陷入不可用的状态,这个不可用的时长和选主 or Leader恢复时长紧密相关 单主复制的写请求只由 Leader 节点来承担,所以写请求的性能瓶颈由 Leader 节点决定 多主复制 既然单主写拉跨,那就也由多个节点来承担写请求 多主复制系统在一定程度上分散了写请求的压力,但是不难看出,系统是容易出现不一致的。 eg. 例如上图,按照图示,整个系统最后的 x 值应该为3,但是我们设想一种情况,当 x = 1 的日志复制给主节点 1 和主节点 2 后,从节点的 x = 1 请求由于网络延迟还在路上,此时主节点 2 发起了 x = 3 的请求,这个请求成功的复制到了所有节点,次后,x = 1 的请求由来到了从节点上,导致两个主节点 x = 3,从节点 x = 1。此时系统产生不一致性。 通过上面的例子,我们不难发现,多主分布式系统要解决的第一个难题就是数据冲突(虽然单主也会发生数据冲突,但易于解决) ...

May 24, 2025 · 小石堆