读书笔记 纳瓦尔宝典 01

阅读原文 抄录或复述标记片段的核心观点 专长无法被教授,但可以被学习。 解释内化 用自己的话+生活案例解读 这里教授和学习处于对立面,教授应该特指不需要额外的付出和思考,只要接受到消息就可以做的事情。比如扫地是可以教授的,简单两句话就可以让一个小孩子拿起扫把扫地。但是将卧室清理的井井有条,使用不同的工具,对于不同的污渍使用不同的方式处理,这种本领是需要下投入时间去学习的,也有人天生有规划好打扫卫生的本领。那这种就是专长,比方说《断舍离》这本书的作者,就是一个收纳大师,所以能写出一个专门教别人收纳的书籍。 关联过去 回忆自己的相关经历 我有哪些专长呢? • 我的文笔很好。从小的作文就常拿高分, 也总是被老师单独拿出来点评。我的同学也很喜欢读我写的作文,有同学说我的文笔风格和一位大师很像。我的朋友作为一个新时代老师,拿着我的高中作文做教学案例。这应该是天赋和从小到大的积累,因为小时候家里条件不好,加上臀肌挛缩导致外八。所以一直自卑,敏感,对身边事物和人的观察比常人更加细致。而讲故事的能力倒是来的不同寻常,小时候小朋友们到学校总是会分享自己周末看的动画片,而我因为家境贫穷没有电视机,但是又不想被别人知道,所以就会假装自己看过相同的动画片而也激情的参与到讨论中。虽然我一部动画片都没有看过,但是故事情节却了如指掌,成了讲故事的好手。 • 我的点子很多。我小时候一直是孩子王,一直能相处各种各样好玩的游戏,让一帮孩子不无聊。到了大学社团,我也是做了话剧社社长,也出谋划策了一些想法。我的想法比较多,身边的人很多是没有办法有新的想法的。这应该也是专长,我之所以想法会比其他人多很多,也是小时候积累的,我从小因为没有电视看,就只能看书,什么书都看,各种奇闻怪谈,科幻科普,心理学成功学,导致知识面比较广。另外,小时候家里比较穷,衣服和房子都很差,所以每天晚上睡觉前都在幻想自己有一个好房子,穿好看的衣服,导致想象力比较丰富。这些遭遇不是正常人能遇到的,所以是我的专长。 规划未来 写一个具体可执行的动作 我有不少想法比较零碎, 一个零碎的想法可能无法产生收益,但足够多的零碎的想法可能就是一笔财富。我的专长是能够有大量的想法,并且文笔也足以清晰的描述。我的缺点是我没有落地这些想法,也没有把零碎的想法扩展成庞大版图。我可能可以把我的缺点改掉,但是眼下更好的是,立即记录我所有零碎的想法。

October 6, 2025 · 1 min

JSON反序列化为HashMap导致线程BLOCK

经验: 不能忽略数据从存储介质转移到

March 25, 2025 · 1 min

奥卡姆剃刀剃不干净不如不剃

如无必要,勿增实体。这个原则在软件开发领域奉为圭臬。 我所在的项目组是从另一个项目中孵化并独立出来的,原先有若干子业务线同属于一个项目组,代码中有一个公用的字段区分不同的业务线,也叫做租户id。 项目组独立出来后,只有一个业务线,所以租户id都固定传入1。这使得租户id的传入成为了一个样板代码。于是在大约两年前,开始了一波去租户id的革命风潮。 租户id的设计是从数据存储阶段就设计好的,所以整个项目组的服务中都有各种各样的强制校验。自然,去租户id的运动最终失败了,部分链路不依赖租户id或者都默认填充1, 部分链路仍然强制校验租户id。而数据库中一直存储这租户id。现在导致租户id不单单是一个模版代码,还是一个无法正常使用的模板代码,因为没有人会知道他们传入的租户id会在那一层被悄悄的篡改为1 尽管租户id已经成为一个无法正常使用但又必须存在的系统寄生虫, 但是整个业务还是可以正常工作的, 偶尔看到的租户id,大家以及熟悉视而不见。 这个时候,N项目登场了。 出于商业上的考量,我们需要新创建一条业务线,复用目前的能力,但是实现细节更简单,未来的发展也明显不同。(一个是线上业务,一个是线下实体业务)。所以,租户是最理想的实现方式。 最早设计的租户id已经无法正常使用了,所以我们需要再给所有的系统,存量数据新增一个崭新的租户id字段,他的目的和最早的租户id一样。然而旧的租户id我们仍然不能把他下线,所以在n项目之后,我们的系统里就会出现两个租户id,有两套截然不同的租户判断和使用方式。

March 17, 2025 · 1 min

Concurrenthashmap写入加锁阻塞

我们有一个服务会往数据库中写入一系列的规则,并且查询请求是否命中了这些规则。 规则的数据结构非常复杂, 拓展性很强, 劣势直接使用这种数据结构进行查询性能很差。 为此,我们设计了一个定时任务, 定期的将写入时的mysql数据转化为能更加快速读取的redis缓存数据结构。(我们的规则生效允许有一定的延时, 所以定时任务可以满足要求) 每个请求会读取该请求所需要的规则,同时也会读取全局规则。 规则都存储在redis中,所以大量请求读取全局规则会导致热点key问题。 为了解决热点key问题,我们为这些全局规则做了服务上的本地缓存。(全局规则的数量很少,一共只会占用不到1mb的内存)。 进行压力测试时,发现大量的读请求阻塞在concurrenthashmap的compute方法上, 导致80%的请求超时失败,服务无法正常提供功能。 原因是使用的本地缓存,在读取redis上值为空时写入空,这会导致每个本地缓存读取请求都触发一次缓存更新操作(从redis读取)并写入底层的concurrenthashmap中,而concurrenthashmap的写入需要加锁并且性能很差,导致请求被阻塞。业务原因是,全局规则有可能不存在,所以redis上也不存在相应的值。 最终,通过读取不到规则时设置一个对应的空规则而不是null,解决了该问题。

February 19, 2025 · 1 min

内存对齐

/// Align downwards. Returns the greatest x with alignment `align` /// so that x <= addr. The alignment must be a power of 2. pub fn align_down(addr: usize, align: usize) -> usize { if align.is_power_of_two() { addr & !(align - 1) } else if align == 0 { addr } else { panic!("`align` must be a power of 2"); } } /// Align upwards. Returns the smallest x with alignment `align` /// so that x >= addr....

January 29, 2025 · 1 min

GC与TP999

最近我开发了一个接口,这个接口需要从 Redis 上读取一系列规则,判断请求是否满足这些规则。服务使用 Java 开发,垃圾回收器选择了 G1。由于请求量很大,且用户对性能要求较高,查询延迟(TP999)必须尽可能低。 每次请求最多会读取 5 条规则,也有可能读取不到规则。每条规则的形式是一个 Map,且比较大,每个 Map 大约 1 到 2 KB。规则数量非常庞大,并且会有更新操作,因此将所有规则全部做成本地缓存是不现实的。我们估算,大部分请求会读取 2 到 3 条规则,只有极少部分请求会读取 5 条规则(我们通过布隆过滤器来快速判断并拦截那些会读取 5 条规则的请求,从而减少查询时间)。 为了尽量减少查询延迟,我们考虑过一种方案,即提前将请求可能用到的所有规则(最多 5 条)通过 mget 一次性加载到内存中。这样,无论请求最终读取多少条规则,都能以较低的延迟完成查询,虽然加载 5 条规则的时间略长一点。但这种做法的缺点是,无论请求最终需要多少条规则,都会预先加载 5 条规则,这些规则的生命周期仅限于一次请求。如果请求量很大,频繁的内存分配可能会导致 GC 增加,甚至触发 G1 的 MixedGC,从而引发 STW(Stop-the-World)。 如果不提前加载规则,每次请求最多需要读取 5 条规则时,就会调用 5 次 Redis 请求,导致查询延迟增大,TP999 的表现就不好了。 这个问题困扰了我一段时间,不过现在我已经想通了。我的结论是,“提前加载所有需要的规则”。因为 GC 问题可以通过水平扩容来解决,而 TP999 的问题,只有通过提前加载所需的规则才能根本解决。 使用 Java 这种有垃圾回收机制的语言,追求单机的极致性能其实是一个伪命题。虚拟机和垃圾回收器的存在意味着,不管怎么优化,也不可能达到顶级稳定的性能。Java 的优势在于减少了开发的复杂度,避免了过多的性能调优工作。对于单机性能,我们只需要确保性能不至于太差,而在流量压力大的情况下,可以通过水平扩容来解决。如果为了 GC 优化而不提前加载规则,那么每个请求的 TP999 延迟很难通过外部手段来解决,也无法通过水平扩容来解决。 因此,我认为,业务开发中应该进行性能优化,但优化要适度。因为业务代码可能会在半年内被重构或删除,而根据用户体验法则,响应时间超过 150ms 会让用户明显感到不适。如果真的需要追求极致性能,应该优先考虑选择高性能的语言和框架,而不是在现有的技术栈上做过度的调优。

January 25, 2025 · 1 min

DcVm的对象模型

我在用Rust写一个JVM,整个项目从三个月前开始准备,查阅了大量的资料,也写了不少代码做试验,直到最近才走上正规,进入了稳定的开发阶段。 其中最困扰我的是如何设计DcVm的内存模型来表达Java的对象,我在之前没有用C++、Rust这种无GC的语言系统性的写过大项目,也没有过编写虚拟机或者语言解释器的经验,这部分着实花了我很久才找到一点头绪。 一开始的时候,我打算照搬KiVM的设计,KiVM是用C++编写的JVM,使用了一种简化的oop-klass模型。然而我在使用Rust编写时,发现照搬oop-klass模型实在是太复杂了,非常难以实现。oop-klass模型使用了大量的继承关系,并且oop-klass之前有引用的关系。官方也提到这种模型的实现里面有历史原因,比如markOop不是一个oop。我希望DcVM是一个简单的、优先考虑可读性而不是性能的教学用的JVM(未来打算进行可视化),所以我不想在新实现的时候还要背负hotspot这种成熟的、性能高的的JVM的复杂实现。 然后我又去研究了rjvm的设计,rjvm使用Rust编写的Jvm,它的实现是类似与解释性语言的虚拟机实现,在底层通过复杂的指针操作,将类型信息直接写入内存,这在写简单的解释型语言的虚拟机时是个场景的做法,但是Java的字节码里是有类型信息的,底层这种大量的指针操作使得代码很不直观,作者也对这部分实现不满意。 百思不得其解时,我读到了Writing Interpreters in Rust: a Guide这边教程,这个教程并不是写JVM的,而是教如何写一个解释型语言的虚拟机实现的。代码很好,让我学到了很多rust的使用方式,我尝试使用其中TaggedPtr, FatPtr, ScopterPtr, TaggedScopterPtr, RawPtr各种Ptr来表示我的底层实现。我发现这太复杂了,我大部分实现都在思考用什么Ptr,这些ptr在教程里的好处是在虚拟机实现的时候写代码在编译期能拿到类型,但这只使用与解释型语言,因为他们的类型是固定的几种,而Java是可以自定义类型的。 最终,我决定简化oop-klass模型,将oop始作数据的存储区,将klass始作算法的存储区,DcVM的类型分为三类:原始类型、instance和Array, 原始类型是int、long这些,而Instance是自定义的类型,每个instanceOop会指向一个instanceKlass(虚拟机运行时同一个类可以很多个oop, 但只能有1个klass), Array是个递归的结构(里面可以存Array, Instance或原始类型)。整体的实现目前没有使用裸指针,先用typed_arena做了简单的内存分配(后面要自己写内存分配器和垃圾回收), 所以代码里有大量的引用的生命周期参数。目前我简单的将所有的生命周期都写成一样的,后续对生命周期理解深入了之后需要做一下优化。

December 2, 2024 · 1 min

虚伪与憨厚

陈丹青说他那个年代的人都很憨,现在人不够憨,一开口就知道你这个人想要骗我。 的确如此,虚伪的人越来越多。虚伪的定义太哲学,以致于说道虚伪,并没有什么特别的感受,只是有一个这个人不是很好的印象。 把虚伪具体点讲,一个人一张口就知道他是想要骗我。虚伪的人的赞美,就像他抽烟后吐出的烟圈,虚无缥缈,毫无穿透力,你能感觉的到他虚伪的赞美是无力的,只能在屋子里转悠,进入不了任何人的内心。 这个资本的世界,虚伪的人越来越多,总是想着去欺骗,去忽悠别人,把真相隐藏起来,不真诚。和别人对话目的性太强,就是想要获得利益,没有利益的事不做,只想着去收割其他人。 希望在这种世界里,我仍能够保持纯真,不愿意做虚伪的人,虚伪的人活不长久也不会过的开心,每天勾心斗角,也不会有真正的朋友。

September 18, 2024 · 1 min

Ld脚本的简单入门

SECTIONS { . = 0x80000; /* Kernel load address for AArch64 */ .text : { KEEP(*(.text.boot)) *(.text .text.* .gnu.linkonce.t*) } .rodata : { *(.rodata .rodata.* .gnu.linkonce.r*) } PROVIDE(_data = .); .data : { *(.data .data.* .gnu.linkonce.d*) } .bss (NOLOAD) : { . = ALIGN(16); __bss_start = .; *(.bss .bss.*) *(COMMON) __bss_end = .; } _end = .; /DISCARD/ : { *(.comment) *(.gnu*) *(.note*) *(.eh_frame*) } } __bss_size = (__bss_end - __bss_start)>>3; 每个链接脚本都需要有SECTIONS,每个SECTIONS里面可以定义多个secname,比如示例里的....

July 28, 2024 · 1 min

为什么HashMap的大小都是2的幂次方

分为两个问题,一个是为什么创建HashMap时的容量都是2的幂次方,第二个是为什么扩容时是扩大两倍。 容量为2的幂次方 目的是为了更快的取模,假设容量为N,哈希值是hash,如果N是2的幂次方,则hash % N = hash & (N - 1) 2的幂次方的二进制表示,只有最高位是1,其他位都是0,2的幂次方减一的二进制表示则全部是1。 hash & (N -1)本质上是只保留hahs的末尾几位,得到的结果一定小于N, 在0到N-1之间。 扩容扩大两倍 扩大两倍可以保证原有的元素要么保持在原来的索引上,要么移动到原索引加N的位置,可以保证数据在扩张后的分布不会变得更差。 首先map的容量都是2的幂次方,那么扩大两倍,本质上就是在取模的时候多保留一位,那么原来的哈希值如果上一位是0,则扩容后取模的结果不会变,如果上一位是1,那么扩容后取模的结果就是二进制数最前面加上1 在二进制最高位+1,相当于十进制里+N,比如从16扩大到32,那么相当于本来取4位,变成取5位,取5位相当于在原来的索引上+16 举个例子: hash值是110010,此时map的大小是16,那么取后4位,0010做索引,也就是2 扩大两倍后(总大小是32),取后5位,也就是10010做索引,结果是18 扩大两倍后会不会有在扩容前没有hash冲突的两个hash值,扩容后hash冲突了 按照上文的结论,扩容后,原来hash值要么还在原来的索引上,要么在原来的索引+N的位置上(N是扩容前的大小) 两个索引(在扩容前不冲突)在扩容后只可能有3种情况: 两个索引位置都不变,那么扩容后也不会有冲突 两个索引的位置都+N,那么也不会有冲突 一个索引变化,另一个不变化 第三种情况,有没有可能出现,扩容前不冲突,扩容后冲突的情况。因为扩容后,索引要么不变,要么+N,如果想要扩容后发生冲突,那么需要两个hash值,为hash1,hash2 假设 hash1与hash2在扩容前不冲突,扩容后冲突。 则一定有: hash1在扩容后+N,hash2扩容后不移动 扩容前hash2的索引正好比hash1大N 扩容前取模运算保留低位的n位,而扩容后保留n+1位, 因为hash1在扩容后+N,说明hash1的第n+1位是1,而hash2扩容后不移动,说明hash2的第n+1位是0, 扩容前hash2的索引正好比hash1大N, 就需要hash2的索引的低n-1位和hash1的索引的低n-1位相同。并且hash2的索引的第n位是1,hash1的索引的第n位是0 那么我们会得到 n+1 n 相同的低位 hash1: 1 0 XXX hash2: 0 1 XXX 因为hash1和hash2的低n+1位不完全一致,所以扩容后取n位hash1和hash2不可能落到一个索引上,假设不成立。

June 26, 2024 · 1 min