[{"content":"我是个喜欢发朋友圈的人，对身边发生的事情，对一些事情的看法、感想，或者自己在某个时刻的内心想法，都忍不住去做一些文字输出。\n今年在朋友圈输出了很多内容，做一下汇总，大家可以看个乐呵。\n2024.2.24\n过年期间，从身边的人总结出一些想法。\n婚恋市场实际上发展得挺畸形，互联网的快速发展抹平了很多信息差，导致一些落后低俗的观念开始普及，比如彩礼。\n女生家庭想要一种平稳的保障，或者是想要男方的诚意，或是尊重某种地域习俗，或者是从众心理、面子，于是对车、房、彩礼等东西都有了一定的要求。\n但是对于一个普通家庭来说，如果财务状况并不理想，想要全方位的满足这种要求，可能需要付出很大的代价。\n比如本身没有能力购房，但是举全家之力付个首付，相当于提前拥有了和目前财务状况所不匹配的资产，房贷这个负担要么是父母承受，要么是结婚双方背负，为以后的婚姻生活埋下了一个定时炸弹。\n有的男生，在这种情况下，干脆选择不恋爱不结婚，造成的结果就是结婚意愿普遍下降。\n2024.2.19\n补了下春节档电影《热辣滚烫》。\n电影本身没有什么大的亮点，演技、台词都很小品化，剧情一般，笑点无聊，5 分不能再多了。\n一个噱头减肥营销就能引起这么多人的共鸣，只能说国内观众太容易共情了。\n我想起了十一年前王家卫为了拍《一代宗师》，到处走访调研，准备了整整 10 年。\n张震为了这部电影练了三年八极拳，甚至成为了国内武术比赛的冠军。\n而他仅仅是电影中的一个配角，出场时间甚至没超过 10 分钟，即使这样，人家也没搞这些东西出来作为电影营销的方式。\n只能说中国电影在这么下去真的没救了。\n2024.8.17\n生意的本质就是信息差，技术厉害的人很多，怎么让别人知道你很厉害，是一个人综合能力的体现。\n2024.8.25\n面对镜头，把自己的想法表述出来，这是一件看起来简单，实际做起来挺困难的一件事儿。\n所以大多数人都只是内容消费者，而不是内容生产者。\n有的时候内心想法很丰富，但是真正说出来的时候，却发现和自己心里想的完全不一样。\n2024.9.1\n关于和领导谈薪的一些想法：\n首先需要摒弃一种思维，那就是我埋头努力工作，那么老板一定会看到，然后感动了他，老板主动来找我加薪！\n不可能！绝对不可能！你不主动提，那么大概率公司永远不太可能会给你加薪的，当然一些晋升和薪酬制度比较完善的大厂除外。\n在这种情况下，需要抓住机会，瞅准时机，向老板提出自己的想法。\n首先，有功劳说功劳，比如你负责的项目，有什么样的技术难点，解决了什么难题，为公司带来了什么样的收益和价值。\n没有功劳说苦劳，比如自己干了多少脏活累活，别人不愿意干的我主动干，能力不够态度来凑嘛。\n没有苦劳说疲劳，比如自己一直加班，没有个人生活，为公司付出了很多等等。\n这是第一点，没有功劳说苦劳，没有苦劳说疲劳。\n第二点，就是卖惨。\n比如自己压力大，又是单身狗，没钱谈女朋友。\n或者说自己要买房，房价太贵，或者说自己背负高额房贷，压力巨大。\n又或者父母身体不好，需要补贴家用，自己是家里唯一的经济支柱。\n一些情况可以适当的夸大，但也不能太假。\n总之，会哭的孩子有奶吃，在职场不要做一个默默无闻，埋头苦干的老实人。\n2024.9.28\n最近在工作中的一些感悟：\n在工作中，任何有意义的成长，一定是带着痛苦的。刚开始在了解范围有限的情况下，做一些事情反而觉得轻松。\n但是到了一定的阵痛期，会发现寸步难行，思考的方面更多了，做起来会觉得非常困难。但只要我们熬过这些困难的时候，才会让自己发生质的变化。\n一个工程师的绝对能力，不仅仅在技术层面得以展现，而是在这个基础之上，发表自己有建设性的想法，去构建团队内、公司内，甚至行业内的影响力。\n2024.10.5\n在一些热门景点，总能看到很多直播的人，这不禁让我想到，或许每个人心中都埋藏着一个成为网红的梦想？\n信息越来越透明的情况下，看着别人成功，会误以为自己好像也行。\n但是，任何在自媒体领域展现自己并且获得成绩的人，本身在自己的领域就是佼佼者。\n自媒体无法帮我们完成从零到一的积累，只能完成从一到一百的影响力扩散。\n2024.10.9\n有时候 Github 上看到很多轮子，都是一些地址在北欧国家的程序员搞出来的，比如挪威，瑞典之类。\n真的佩服北欧的这些程序员，一天天的，哪里有这么多的精力去造各种轮子。而且还能够维护得很好。\n不得不感慨，在一个福利保障高度完善的社会体系中，没有那么大的就业和生存压力，反而能够激发人无穷的创造力。\n2024.10.17\n最近一直在想，就算我技术再牛逼，做出了一个很厉害的产品，我有什么样的渠道可以扩散到足够多的人群，并且去进行售卖盈利，有多少人愿意付费？\n答案是很悲观的，在我现有的粉丝基数上，我没有足够的能力去宣传和运营一个产品，并让它持续盈利。\n这不禁让我想到，创业过程中，用最厉害的技术，做出一个好的产品，这反而是相对来说最简单的事情。\n产品想要发展、盈利，一是需要足够的人脉、粉丝基数，二是需要有推广、运营，得靠钱来砸。\n我一直告诫自己不要把自己局限在技术圈子里，因为技术有时候并不重要。\n于是我做了很多方面的尝试，无论是自媒体还是知识付费，都像是某种预演，让我在当前的能力范围内把一些事情做到最好，这或许能够为以后做更多的事情而铺垫。\n2024.10.20\n曾经的我是一个偏内向的人，有时候跟别人说话，声音甚至都会有点发抖。\n我想，一个有正常逻辑思维的人，无论内向与否，都应该具有表述清楚自己想法的能力。\n在最近半年录制视频的过程中，我最开始仍然显得有一些局促，尽管已经做了十足的准备，在说话时却仍然经常打结、中断。但是经过这段时间的刻意训练，面对镜头的我变得坦然了许多。\n我也一直很喜欢线上或是线下做一些公开的技术分享，现在面对几十人，甚至上百人做一些分享，也不会有丝毫紧张的情绪了。\n这几年以来我也坚持不懈的写文章，写得越多，自己的思维也会更加活跃，表达的欲望也多了。\n有时候我们内心的想法非常丰富，但是当真正表达的时候，会发现根本张不了口，或是自己说出来的话，完全达不到自己想象的那种效果。\n或许，在任意场合、任何时候，表达清楚自己的观点，本就是一项需要刻意练习的能力。\n2024.11.1\n一个人想要在长期获得倍数，甚至指数级的成长，本就是一件极其困难的事情，因为有时候这并不取决于个体本身。\n家庭环境、受教育程度、个人觉悟、行业选择、社会环境、时代趋势等等，都在深刻的影响着我们，有的甚至是我们终生都无法摆脱的桎梏。\n一个人想要堕落或平庸，却又非常的容易，黄赌毒、交友不慎、选错职业，选错婚姻、行业没落等等，每一个因素都能够摧毁一个人。\n这个社会，给普通人的陷进太多，给普通人上升的渠道又太少，社会资源集中在少数地区，少部分人手中。\n作为时代的一粒尘埃，改变阶级或许困难，但是努力一次，追求一次，也不枉此生了。\n2024.11.5\n在一个上诈下愚的氛围中。\n事实可以被扭曲，真相可以被掩盖，情绪可以被煽动，这样固然可以转化一部分矛盾，但是没有理性，不讲公义的情绪，本就是一把双刃剑，煽动容易，失控也容易。\n一部分群体，在正常的环境下被漠视、践踏，但是在集体癫狂中，却又能够轻而易举的获得存在感和使命感。特别是在一些崇高口号的加持下，一些不合常理的行为，便能够越过道德的界限，变成理直气壮的正义。\n2024.11.6\n我们有时候总是把得不到的想象成最美好的样子，但实际上并不存在那么完美的人或事。\n社交媒体的美好瞬间，只是别人希望你看到的，或只是某一时刻的状态，甚至是小心翼翼的伪装。\n在这种环境下，发表真实想法，展现真实状态，就难能可贵了。\n我一直喜欢在各个平台发表自己天马行空的想法，有可能并不完全正确，但绝对真实。\n我也和大多数人一样，会迷茫，会犯蠢，会判断失误，也拥有每一个普通人的世俗欲望，喜欢钱，喜欢美女，追求满足自己欲望的物质。\n但身处于一个焦虑的时代，认识到自己的不完美，拒绝无意义的精神内耗，做真实的自己，或许是一种挺好的选择。\n2024.11.8\n刚开始写开源，重要的是摒弃害羞，怕丢人的心态，可能自认为自己做的东西很垃圾，开源出去之后怕被别人指点、嘲笑。\n但是我们要朝着长远的利益去看，任何项目在早期可能都是非常不完善，甚至是小儿科的，但是任何小事都经不起长期的坚持，只要能够日益完善，一定能够做的更好。\n有的时候需要锻炼自己的强大心态，针对别人毫无意义的诋毁，最好的处理办法可能是一笑置之，坚持去做自己喜欢，自己擅长的事情。\n那些喜欢在网络上诋毁别人的人，大概率自己本身就是一个没有什么能力的人，所以针对这种人，我们要保持良好的心态，不要被别人影响。\n2024.12.19\n有时候工作中会遇到一些自己不那么喜欢，甚至厌恶的事情，但是我仍然尝试让自己去做好。\n直到它变成了我擅长的事情。\n后面再做任何事情，我都会想，自己不喜欢不擅长的事情都能做好了，还有什么工作搞不定呢？\n过渡到其他领域也一样，我是一个不擅言辞的人，但是我仍然尝试多去说，多去写。\n面对十个人做过一次 talk，后面就算面对上百人，只要有类似的经历在，就会完全不怯场，有底气和信心去做好。\n","date":"2024-12-21T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E8%BF%99%E4%B8%80%E5%B9%B4%E5%9C%A8%E6%9C%8B%E5%8F%8B%E5%9C%88%E7%9A%84%E7%A2%8E%E7%A2%8E%E5%BF%B5/","title":"这一年在朋友圈的碎碎念"},{"content":"工作 今年是继续在数据库行业摸爬打滚的一年，比起刚入行时的懵懂，现在有了一些自己定位问题、解决问题的能力，对 Postgres 的源码也更加熟悉了。\n但终究来说数据库是一个太复杂的领域，了解得越多反而越发现自己的无知，时常信心会受到打击和摧毁，甚至自我怀疑，然后又给自己鼓劲重拾信心，如此循环往复，在折磨中不断学习和提升。\n今年的工作主要集中在数据库云原生的适配方面，做了各种 feature 开发、bug 修复、测试等等，协助系统稳定上线运营。\n还参与了两次线下的 talk。\n以及一次我司直播技术分享。\n国内数据库行业大致已经进入到了存量淘汰的阶段，在这样的一个趋势下，对我们个人来说有太多的不确定性，未来只能说走一步看一步了。\n开源项目 今年在开源项目方面没有投入太多的精力，因为项目已经趋于稳定，功能也比较完善，大多是一些常规迭代更新，根据一些社区 issue 反馈来驱动。\nrosedb 今年共更新了 6 个版本：\n目前累计有 4.6k star\n今年还即兴开源了一个小的 rust 练习项目，效果没想到还挺不错，帮助了一些人去学习和实践 rust，并且这个项目还很幸运的上了一次 Github Trending。\n未来在开源项目这方面应该会维持现状，佛系更新吧。如果社区有人反馈就进行更新和维护，目前暂时也没有新的想法和灵感去搞新的开源项目了。\n知识服务 《从零实现 KV 存储》是去年更新的课程，这一年多的时间里也帮助了很多同学锻炼提升了技术能力，并且拿到了心仪的 offer，好评也非常多。\n课程详情：https://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g\n《从零实现分布式 KV》也在今年初更新完结了，这应该也是学习分布式系统，手写 raft 算法的首选教程。\n课程详情：https://av6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5b\n《从零实现 SQL 数据库》是我从去年底就开始准备的课程了，然后准备了大半年的时间，在今年九月发布，也在前几天更新完毕。\n这个课程应该是目前全网唯一使用 Rust 手写一个数据库的教程了，非常的硬核有挑战，也帮助到了很多同学学习和上手使用 Rust，提升自己的底层基础，以及系统设计能力。\n课程详情：https://icnyamgobd0u.feishu.cn/docx/AbXZdEbY0obdcTxF0FKcx4Pqnyf\n后续在知识服务这方面没有太多明确的计划，可能会根据大家的反馈去进行动态调整。持续提供优质内容，能够帮助到大家学习真正硬核的技术，是我永恒不变的初衷。\n庆幸的是这的确帮助到了很多同学，每次有同学私信我说项目给与了他们很大的帮助，比如找到了一份满意的工作，都是我非常开心的时刻。\n自媒体 今年一些业余时间在自媒体的运营方面花费了不少，主要是录制和剪辑视频。这个看起来挺简单的事，其实做起来远比自己想象的要辛苦不少。\n刚开始的时候，会不太适应，比如面对镜头说话断断续续，需要提前将自己想说的内容记下来，然后反复的练习。后面慢慢熟悉了，面对镜头坦然了许多，自己的语言表达能力得到了很大的提升。\n视频主要是在 B 站发布，不知不觉今年已经累计发布了二十多个视频，粉丝接近 8K。\n最高的一个视频播放是 3w，其实算是做的比较差的了，主要是视频还不够有趣味性，风格比较单一。\n俗话说就是不太会整活儿，只能先慢慢坚持拍下去，后续尽量尝试让视频的风格更加多样化。\n公众号今年中规中矩，还是没能突破一万粉丝。主要是写了 Postgres 源码学习系列，以及开源项目经验分享系列，还有其他的一些关于学习、职场、生活等零散的内容。\n在自媒体方面的尝试，主要是想拓宽自己的发展路径，不让自己局限于单纯的写代码，而且还能够锻炼自己的表达能力，构建自己的个人影响力和粉丝圈子。后续如果想做更多事情的话，必须得有一个有影响力的个人 IP 作为基础。\n生活 生活方面，今年下半年开始重拾了网球这个为数不多的爱好。想到自己也快奔三了，不希望看到自己变成一个油腻大叔模样，还是想尽量保持身材。\n并且平时坐着的时间太多了，身体健康方面确实需要重视起来，后续应该会把锻炼和网球运动作为常态，有一个健康的身体才是所有事情中最重要的。\n2024 年总体来说，工作、开源、知识服务、自媒体这几个方面平稳发展，有了更多的积累，并且尝试了自己不那么擅长的事情，取得了一些初步成果。\n但有时候也会觉得有一种无力感，想要寻求更大的突破，发现异常的困难。比如现在 B 站粉丝只有 8k，如何达到 8w？假如工作之外能月入 1w，如何更进一步，达到 5w？从 1 到 100 或许简单，但是从 0 到 1 的起步，却是最困难的。\n我自己是一个始终不满足于现状的人，到了一个台阶，便会想方设法的到一个更高的台阶。但是取得多大的成就，个人的努力只是一方面，有时候运气、趋势、大环境发挥的作用远比个人因素大得多。\n只能说，做好自己所热爱的事情，不给自己留下遗憾就很好了。\n","date":"2024-12-17T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/2024-%E5%B9%B4%E7%9A%84%E5%9B%9E%E9%A1%BE%E4%B8%8E%E6%80%BB%E7%BB%93/","title":"2024 年的回顾与总结"},{"content":"前几天刚好完成了 rust 手写数据库课程的命令行工具，有一个交互式的客户端，瞬间成就感拉满了！\n不枉我写这个项目接近一年的时间，觉得之前的努力都没有白费哈哈。\n在这个客户端当中，可以创建表：\n可以增删改查数据：\n可以进行常见的 sql 查询，比如\nOrder By Limit 和 Offset Projection 投影 Join 语句 Agg 聚集函数 Group By 分组 并且还支持 ACID 的事务操作：\n也可以查看当前表的信息：\n《从零实现 SQL 数据库》是我今年开始搞的，最初也只是想着试试看能不能做，但是后来帮助了一些同学，他们都从中学习到了很多，这也让我一路坚持到了现在。\n课程虽然实现的只是一个非常简单的数据库，但是麻雀虽小五脏俱全，数据库内核的各个模块基本都实现了。\n相信通过这样一个项目，对编程基础、rust 上手、项目实战、系统设计等等能力都会上一个台阶！\n现在课程项目接近尾声了，代码量（加注释）接近 6000 行，是非常不错的适合上手和实践 rust 的项目。\n课程的详细目录如下：\n感兴趣的同学可以进这个课程详情链接查看：\nhttps://icnyamgobd0u.feishu.cn/docx/AbXZdEbY0obdcTxF0FKcx4Pqnyf\n","date":"2024-12-05T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rust-%E6%89%8B%E5%86%99%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E6%88%90%E5%B0%B1%E6%84%9F/","title":"Rust 手写数据库的成就感！"},{"content":"平时不太关注 TIOBE INDEX 的排名，但是今天刚好看到了，发现 Go 语言都已经排在第七了。\n几年前我刚开始学习的时候，Go 语言其实受关注和欢迎程度没有这么高，但是近些年来一路狂飙，稳坐前十，成为了主流编程语言之一。\nGo 在云计算、容器化和微服务架构中的应用越来越广泛。\n而且云原生的基础设施 Kubernetes 和 Docker 等技术的普及，Go 语言凭借其高效的执行性能和简洁的代码结构，成为开发者构建云原生系统的首选语言。\n未来几年，Go 语言应该会继续向前，稳居前十应该没什么问题，看什么时候能够超过 JavaScript 了。\n其实另一个值得关注的是 Rust，Rust 的上升速度也非常惊人，去年还在 20 名左右徘徊，今年这时候的排名已经来到第 14 位了。\n我在这几年接触和使用 Rust 的经历中，也非常看好 Rust 的发展。\nRust 的内存安全性和高性能，其实也逐步成为开发高性能系统、嵌入式应用和区块链技术的理想选择。\n主要是 Rust 语言避免了传统 C/C++ 中存在的许多内存管理问题，且在性能上几乎不输给这些底层语言，这使得它在需要高效并且稳定的应用中逐渐取代了传统的技术栈。\n当然，C 和 C++ 的基本盘还是很大，Rust 作为一门相对较年轻的语言，其实能做到这种程度已经非常不错了。\n到明年，Rust 或许能够超过前面的几个，首次进入到前十当中，拭目以待吧哈哈！\n我自己的课程也是优先支持 Go 和 Rust 这两种语言，目前看来是挺正确的选择。\n目前《从零实现 KV 存储》支持 Rust 和 Go 两种语言，手把手教学，只需要了解基础的语法知识，即可学会一个硬核实战项目！\n课程详情链接：\nhttps://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g\n《从零实现 SQL 数据库》支持 Rust，使用 Rust 手写一个数据库系统，超级硬核，Rust 实战项目首选，并且还有 Rust 零基础的入门讲解，就算没有 Rust 基础也可以来学习。\n课程详情链接：\nhttps://w02agegxg3.feishu.cn/do\n","date":"2024-11-16T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E5%88%B0%E6%98%8E%E5%B9%B4rust-%E5%92%8C-go-%E9%83%BD%E5%B0%86%E7%A8%B3%E5%B1%85%E5%89%8D%E5%8D%81/","title":"到明年，Rust 和 Go 都将稳居前十"},{"content":"前面的几篇文章主要讲述了：\nrose 聊开源—1 你为什么需要一个开源项目\nrose 聊开源—2 如何快速上手一个开源项目\nrose 聊开源—3 如何开始写自己的开源项目\nrose 聊开源—4 如何推广自己的开源项目\n这一篇来跟大家聊一聊我自己在这几年的开源项目折腾历程中，一些收获与做得不太好的地方，给大家一些参考和启示。\n此篇文章过后，这个系列暂告一段落，因为经其他读者反馈，可能大部分人并没有自己的开源项目，或者说也是刚起步，在这方面的经验并不是很多。 前面几篇文章介绍的一些经验、项目推广等内容，还不能够立即用得上。所以有兴趣的同学，可以按照我之前的介绍去开始参与开源，或者写自己的开源项目。 当大家有了更多的经验之后，后续如果有其他的感想，我会不定期再次对这个系列进行更新。\n1、项目现状 我在这几年的开源历程中，主要是维护了两个 Go 语言的开源项目，分别是 rosedb 和 lotusdb，目前项目的关注度，完善程度其实还是不错的了，star 数量也都上千，并且多次登上过 Github Trending 榜单。\n对于个人开源项目来说，能够做到这种规模，也算是不小的成就了吧。\n除了这两个项目之之外，还有其他的一些组件，比如：\nwal 预写日志组件\ndiskhash 基于磁盘的哈希表\n还有一些其他的 tutorial 项目。\n比如针对数据库学习路线，项目推荐等，有 database-learning。\n针对 go 和 rust 的学习路线，也有对应的两个开源项目：\nmini-bitcask，是 rosedb 的迷你版本，也是一个教学性质的项目。\n还有 rust 的练手项目，rust-practice\n还有一些早期的项目，比如 grpc-demo，当初学习 gRPC 的时候写的一些小的练习。\nalgo-learn，当初刷 leetcode 的一些题解记录。\n2、对自身技术能力的提升 折腾这么多项目，其实对自己最直观的提升就是技术方面。\n针对一些学习类的项目，其实我们能够在学习的时候做一些记录，写一些文章，然后记忆模糊的时候能够随时拿出来温习，是很不错的学习方式，也能够锻炼自己总结归纳的能力。\n然后去折腾一些偏实战的开源项目，需要去解决各种问题，比如从理论到实践的鸿沟跨越，需要去进行架构设计，系统设计。\n并且一些代码细节方面需要仔细考究，对代码规范，单元测试，可扩展，简洁易读等方面都有很高的要求。\n经过这方面的折腾，也增强了我自己的代码洁癖，这一习惯也延续到了工作当中。\n3、对职业发展的影响 毫不隐瞒的说，其实有了开源项目，给了我的履历非常大的加成，当然这其实也有大环境、以及运气等因素的共同影响。\n因为有了开源项目，我在找工作的过程当中能够有更多的底牌，因为这是别人了解你的一种很直观的方式。开源项目代码都是公开的，一个人的技术能力能够马上得到体现，这其实就是最大的技术能力背书，不需要其他任何的虚名。\n在开源项目的长期维护和运营过程当中，我也找到了自己所热爱的事情，能够有机会持续的去折腾，也是挺不错的一种状态了。\n4、维护社群，个人影响力 目前我维护了 rosedb\u0026amp;lotusdb 的两个开源社区群，共计有 800+ 人，slack 虽然不太活跃，但是也有 150+ 人数。\n而且我也靠着在开源项目的运营，产出了一些相关的内容，发布在了自己的公众号、B 站上面，进一步完善了我的个人 ip，我自己在这方面的影响力也得到了提升。\n更重要的是，这是一段很宝贵的经历，注定给我留下很深的印象。\n5、talk 能力、写作能力 还有就是我也会写一些相关的文章，以及录制一些视频去讲述我自己的开源项目，在这个过程中我也锻炼了自己的写作能力。\n并且我也在一些平台，无论是线上还是线下，分享了很多关于开源项目，以及我自身的开源经历，这进一步提升了我面对公众的 talk 能力。\n这些软技能，虽然在短期内看不到什么效果，但确实是在潜移默化的影响着我，让我能够成为一个更加全面的人。\n6、做得不好的地方 当然，其实在这几年的开源项目历程中，也有做的不是很好的地方。\n最遗憾的一点便是对于开源项目团队维护得很差，这其实是有多方面的原因吧。\n因为外部贡献者，每个人的目的、状态、思维都是不一样的，很难去保持长期的合作关系，大多数人都只是在短期之内有一定的参与热情，但是久而久之可能因为各种原因就搁置了。\n我其实也尝试去采用各种办法去推动一个长期合作的开源贡献团队，但最核心的一点是，如果没有金钱维系的话，每个人都会产生倦怠的情绪，靠热情是无法持久的。\n当然还有一点就是项目本身的上限并不高，创新性比较有限，就算做到极致，也只能是一个无法盈利的简单项目，没有前瞻性的优势，同样也无法让其他贡献者保持长期的参与意愿。\n只不过，我认为这倒也无可厚非，因为我们折腾的项目，本身也只是符合我们自己当前阶段的状态，以及技术和认知能力，能够在这个范围之内尽力做到最好，就已经很不错了。\n","date":"2024-11-15T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rose-%E8%81%8A%E5%BC%80%E6%BA%905-%E6%8A%98%E8%85%BE%E5%BC%80%E6%BA%90%E8%BF%99%E4%BA%9B%E5%B9%B4%E7%9A%84%E6%94%B6%E8%8E%B7/","title":"rose 聊开源—5 折腾开源这些年的收获"},{"content":"前面的三篇内容，分别讲述了：\nrose 聊开源—1 你为什么需要一个开源项目 rose 聊开源—2 如何快速上手一个开源项目 rose 聊开源—3 如何开始写自己的开源项目 今天这一篇，来和大家简单聊一聊如何推广自己的开源项目。\n其实很多技术人，对于营销、推广这类词汇带着天生的滤镜，单纯的认为技术就是最核心的，其他都是扯淡，其中尤以 Linus 的那句 \u0026ldquo;Talk is cheap, show me the code\u0026rdquo; 最为知名，成为了很多搞技术的人推崇备至的理念。\n但我想说的是，talk 不仅不 \u0026ldquo;cheap\u0026rdquo;，它有时候比技术更加重要。\n就像在职场中一样，你的工作能力非常出色，能够高质量的完成很多高难度的任务，但是如果你无法清楚的表达自己所做的事情，那么，别人有可能是并不知道的。\n所以，我们有时候会看到，一些技术不那么厉害的人，靠着写 PPT，能够成功忽悠领导，升职加薪。\n当然，这是一个不太恰当的例子，只是为了说明 talk 的重要性，并不是鼓励让大家都去搞假大空的汇报式工作。\n我们还是需要在脚踏实地的基础之上，然后适当性的去提升 talk 能力。这样的话，一是可以清楚表达自己的想法，让自己所做的事情能够扩散到整个团队之中。二是在这个基础之上，更进一步，当你的技术实力和演讲能力都非常出众的情况下，便可以建立自己在公司内，甚至是整个行业内的威信。\n回到我们的开源项目，其实是同样的道理，在目前开源项目众多的情况下，为了不让自己的项目石沉大海，让自己的开源项目看起来更有含金量，更有关注度。那么我们就不能只埋头开发，天真的以为只要是好的就一定能够吸引到别人，但是酒香也怕巷子深，特别是现在很多项目同质化比较严重的情况下。\n所以，在我的经验看来，结合我自己的亲身实践，想要推广自己的开源项目，可以从以下几个点入手。\n一是，我们可以自己去写文章推广。比如我们做出了一个自认为有价值的项目，那么可以去写文章介绍自己的项目。\n文章应该尽可能的涵盖到各个平台，比如常见的知乎、公众号、掘金、思否、博客园等等，然后可以转发社群，朋友圈，尽可能的让更多的人看到你的项目。\n也可以尝试在英文圈去推广，比如 Medium、X、Reddit、Hacker News、Quora 等，所以建议大家的项目尽量使用英文 README，英文的注释，避免出现中文，和更广阔的开源群体接轨。\n二是，可以去寻求其他人的合作。比如我们的文章，可以投稿到其他的平台，有很多关于 Github 精选、Github 项目推荐等公众号平台，而且不乏一些流量较大的平台，他们的粉丝基数更大，可以向更多的人群扩散。\n除此之外，可以向一些行业内稍有流量的人进行推广合作，也就是俗称的大 V，或许别人帮助介绍一下，就会有非常大的流量，当然这一般都是有偿的。\n三，降低项目的理解门槛，以及使用门槛。为什么一些刷题、面经、算法项目在 Github 上面非常火爆？一个很大的原因是这类题材面向的人群非常广，大家理解和学习起来没有太大的难度，非常容易上手。\n如果你的项目是比较小众的，本身覆盖的人群不是很广，那么一定要在降低门槛这方面做得更好。\n比如你可以写文章，仔细讲解透彻你的开源项目的各种细节，用通俗易懂的方式让更多的人能够理解。也可以录制视频，去讲解自己在项目过程中的一些关键设计、项目的亮点、代码流程等等。\n总之，一个原则就是让别人有一个全方位了解你项目的方式，可以快速上手项目，可以大致理解项目的各种细节。\n四，可以参加一些开源相关的活动，以及一些线上或是线下的 talk 等，近年来其实都有很多开源项目相关的活动，规模有大有小，可以选择适合自己的去进行讲述，可以介绍自己的项目，也可以去介绍自己写开源项目的一些契机，心路历程等等。\n比如 Go 语言的有「Go 夜读」，Rust 的有「Rust 中文社区」等等。\n五，集中一段时间持续发力造势，争取能够上 Github Trending。\n需要提前说明的是，这一点其实是可遇不可求的，因为是否能登上 Github Trending，以及就算上了 Trending，能够获得多少关注，其实都是不确定的。\nGithub Trending 筛选的是近一段时间内（大概一周左右）比较活跃的项目，活跃包括 commit、PR、issue、star、fork 数量等等。如果在一周之内，你的项目每天都能够维持几十 star 数量的上涨，那么就有不小的概率登上 Trending。\nGithub Trending 是很多全球开发者每天都会关注的列表，如果能够在这个榜单上，那么你的项目会获得更大程度的曝光。\n六、最重要的一点，其实还是回归项目本身，专注于持续维护和打造一个更高质量的项目，只有持续的发光发热，才能够有更多的机会将其呈现到更多的人面前。\n否则，无论多好的项目，多好的 idea，偶尔的热情肯定敌不过长久的坚持。\n最后需要说明一点，我们不遗余力的去推广自己的开源项目，其实并不是满足自己的虚荣心，或者出于炫耀的心态。更多的是给自己一些心里暗示，寻求一些正向反馈，让自己能够坚持下去。\n刚开始做开源的时候，其实最缺少的就是一种反馈，如果我们的项目无人问津，那么持续维护的热情也会受到很大的影响，极有可能在短暂的挣扎之后彻底失去信心和希望。\n","date":"2024-11-13T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rose-%E8%81%8A%E5%BC%80%E6%BA%904-%E5%A6%82%E4%BD%95%E6%8E%A8%E5%B9%BF%E8%87%AA%E5%B7%B1%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","title":"rose 聊开源—4 如何推广自己的开源项目"},{"content":"在前面的两节内容中，我分别介绍了：\n开源项目能够带给我们的好处，为什么需要一个开源项目\n如何快速上手一个开源项目，参与到其他开源社区中的一些注意事项等等\n今天是这个系列的第三篇，来简单聊聊如何开始写自己的开源项目。\n首先需要说明的是，无论是参与到已有的开源项目中，还是自己去写开源项目，都是很不错的方式，并没有绝对的哪个好哪个不好。\n我们只需要在当前的状态之下，找到适合自己的参与方式就好了。\n如果要写自己的开源项目，一般可以从这几个方面去入手：\n一是有了好的商业契机，想要创业，并且将自己的项目开源出去，寻求更多的关注度。 但是读者中估计符合这种情况的比较少，我自己也还没有达到这种高度，后面有机会再聊。\n二是在自己的日常工作中，发现了一些非常不错的点子，觉得这个 idea 有比较大的价值，所以本着锻炼自己的需要，自己折腾了一些相关的组件，然后将一些内容开源出来。\n三是自己在日常工作或开发中，用到了一些第三方的开源组件，对这个组件本身比较熟悉了。并且愈发的觉得组件本身可能存在非常多的问题，有很大的优化空间，在这种情况下，有的人会选择回馈上游社区，帮助开源社区进行迭代。 当然也有的是另起炉灶，重新基于自己的新的思路、设计，去开启新的开源项目。\n四是完全出于自己的学习需求，比如自己的一些学习笔记，学习心得，刷题方法，刷题记录等等，这一类开源项目一般戏称 README 项目，当然我自己倒没有觉得有任何的不妥，不同的开源项目本身有自己不同的受众。 并没有说谁的项目技术更硬核，就更牛逼，只要自己的项目对自己是有帮助的，或是对受众群体有帮助，就是一个非常不错的有开源价值的项目。\n五是出于自己的折腾心态，想要去搞一个项目，这应该符合大多数人的情况。 无论是去用新的语言去重复造一个轮子，还是基于自己的一些奇妙的想法，亦或是基于某些学术上的论文去实现等等。我最开始搞 rosedb 的时候就是这样一种状态，当时看到了一篇写的不错的论文，然后就自己去实现了。\n以上这几点，就是在我看来，常见的去搞自己的开源项目的一些点，大家可以根据自己的需求、实际情况，然后去挑选自己感兴趣的领域进行切入。\n如果你已经开始写自己的开源项目，那么恭喜你，你已经比大多数的开发者拥有了一个更加吸引人的特点。 它可能在短期内并不会带给你任何的收益、成就、提升，但是只要能够长期坚持下去，一定能够从中获得一些回报。\n最后我想再提一点，那就是在刚开始写自己的开源项目的时候，需要注意的一点心理上的调整。\n最重要的是要摒弃害羞，怕丢人的心态，可能自认为自己做的东西很垃圾，开源出去之后怕被别人指点、嘲笑。但是我们要朝着长远的利益去看，任何项目在早期可能都是非常不完善，甚至是小儿科的，但是任何小事都经不起长期的坚持，只要能够日益完善，一定能够做的更好。\n有的时候需要锻炼自己的强大心态，针对别人毫无意义的诋毁，最好的处理办法可能是一笑置之，坚持去做自己喜欢，自己擅长的事情。\n那些喜欢在网络上诋毁别人的人，大概率自己本身就是一个没有什么能力的人，所以针对这种人，我们要保持良好的心态，不要被别人影响。 我们可以时常在心里暗示自己，我把自己的东西开源出来，能够帮助到别人，并且提升自己，针对那些诋毁者，你又有什么拿得出手的成就呢？\n","date":"2024-11-09T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rose-%E8%81%8A%E5%BC%80%E6%BA%903-%E5%A6%82%E4%BD%95%E5%BC%80%E5%A7%8B%E5%86%99%E8%87%AA%E5%B7%B1%E7%9A%84%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","title":"rose 聊开源—3 如何开始写自己的开源项目"},{"content":"分享两点自己最近在工作中的一些感悟与思考。\n1 两年前我进入数据库内核开发的时候，刚开始确实有点痛苦，感觉非常的困难，哪哪都不熟悉，对数据库的整体模块、架构，一些设计细节都感觉非常模糊，尽管有一些 KV 数据库的经验，但是总体来说还是太少了，不足以应对数据库这么庞杂的系统。\n好在自己还算是坚持，加上向一些 nice 的同事请教，以及 leader 的协助，能够慢慢的开始去上手做一些简单的事情。两年多过去了，我在工作中也做了很多，主导了一些独立的功能上线。\n但是，随着我在数据库这方面了解的东西越来越多，成就感却没有那么足，反而觉得自己不知道的东西更多了。因为我现在做的事情，都是比较独立的 feature，涉及到的模块很多，慢慢的发现有时候做一件事情感觉寸步难行。\n有时候因为一个功能牵扯到的模块很多，需要不停的学习，然后再去应用并解决现有的问题。\n当然也因为自己的一些洁癖，比如为了写一行代码，我可能会去看 100 行，甚至 500 行代码，仔细去看别人是怎么写的。有时候为了一个函数或者变量的命名，为了确保能够和现有的代码风格保持一致，都会纠结一会儿该怎么取一个合适的名字。\n久而久之，在做事情的时候，刚开始甚至会觉得有一些心情受挫。\n只不过，转念一想，我的这种情况应该还算是正常的，估计很多人都会经历。\n今天 leader 给我分享了一些事情，比如说他刚开始带的一些人，在前面的一两年也都没有深入到内核开发，都是从一些比较边缘的事情开始做起来，然后慢慢的熟悉，我目前的学习节奏还是非常快的了。\n确实是这样，有时候我们看到某某同事怎么这么厉害，因为我们看到的只是结果，并没有看到别人成长的过程，每个人都会经历很多职场阵痛期。\n在工作中，任何有意义的成长，一定是带着痛苦的。刚开始在了解范围有限的情况下，做一些事情反而觉得轻松。但是到了一定的阵痛期，会发现寸步难行，思考的方面更多了，做起来会觉得非常困难。 但只要我们熬过这些困难的时候，才会让自己发生质的变化。\n2 第二点是我最近在思考的一个问题，那就是如何在职场中寻求更大的突破？ 大多数人的状态，其实都是埋头默默做自己的事情，稍微好点的，能够在遇到问题的时候和别人讨论，然后有进展就反馈，中规中矩的去完成自己的目标，我目前的状态大概也是这样。\n这种状态持续久了之后，我开始去思考，怎么才能够更进一步，达到一个更高的层次？ 今天和 leader 交流了一下，得到了一些经验，以及我自己也有了一些总结。\n观察一下职场中更加厉害的人，他们都有什么样的特点？除了专业技能强之外，比较重要的一点是他们能够多方面，甚至全方位的参与到很多事情当中，并且不停地发表自己的看法，能够 cover 住很多东西。\n我觉得想要寻求更进一步的突破的话，确实需要在团队中建立这样的个人影响力。\n在做好自己事情的基础之上，能够去参与到其他的事务当中，比如能够去 review 别人的代码，参与到别人的需求讨论中，而不是秉持一种事不关己的心态。\n当我们能够越来越多的参与到其他事情当中，我们会在团队中树立自己的影响力，当别人第一想到这个问题的时候，能够下意识的和你关联起来。\n久而久之，参与的东西越来越多，就越可能有机会去承担更多的责任，成为独当一面的中坚力量，甚至更进一步，当你能够 cover 住团队内大多数事情的时候，其实就具备了成为 team leader 的能力。\n当然，能力越大，责任越大，反过来是一样的，想要承担更多的责任，必须要有对应的能力。我觉得，首先能够在自己的能力范围之内，去参与到其他事情当中，并且发表自己的看法。\n在这个基础之上，尽管自己有可能在某些方面能力不足，但是可以选择性的参与，就算有不懂的也没有什么关系，重要的是能够有参与进去的意愿，也不要羞于发表。这样会促使我们去不停的学习，参与的事情越来越多，我们的能力也会越来越强。\n一个工程师的绝对能力，不仅仅在技术层面得以展现，而是在这个基础之上，发表自己有建设性的想法，去构建团队内、公司内，甚至行业内的影响力。\n","date":"2024-09-27T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E6%9C%80%E8%BF%91%E8%81%8C%E5%9C%BA%E4%B8%AD%E7%9A%84%E4%B8%A4%E7%82%B9%E6%84%9F%E6%82%9F%E4%B8%8E%E6%80%9D%E8%80%83/","title":"最近职场中的两点感悟与思考"},{"content":"今天是自己的 28 岁生日，毕业都工作五年了，时光荏苒啊。\n我在前几年从来没有年龄方面的焦虑，心态上一直都挺不错的，老是觉得现在的自己和几年前好像并没有太大的区别。\n在工作之中，也会觉得自己是一个新人，需要不停的学习和向别人请教，但是随着工作时间越来越长，经历变得丰富起来，经验也有一些积累，心态也随之发生了一些悄然的变化。\n在工作和生活中，也遇到了越来越多比我年纪更小的人，甚至是一些 00 后，突然发觉，自己好像真的不是那么年轻了。\n28 岁的我现在是什么状态？\n工作五年，还是在互联网行业摸爬打滚，工作内容经历了很多的变化，目前做着自己还算喜欢的事情，但是这样的状态能够持续多久呢？\n没有人能够知道，包括我自己，目前互联网行业总体并不乐观，每个人都如履薄冰，裁员、失业、工作机会骤减、内卷严重，这些声音不绝于耳。\n但是又有什么办法呢，已经上了这样一条船，想要切换赛道，其实难度也并不小。特别是没有家庭支撑，没有资源、背景的情况下，想要改变就更困难了。\n我也带着这样的焦虑，于是在近两年开始规划着其他可能的出路，尝试做了一些内容。\n比如做自媒体，这算是对无背景的年轻人还算不错的一条路子，目前主要运营了三个平台，公众号 8K 粉丝，B 站 7K 粉丝，知乎 5K 粉丝。\n总体来说比较平淡，时常会觉得累，容易陷入创作瓶颈。有时候还有一些不切实际的幻想，比如自己粉丝破 10W，甚至 20W，成为小网红，哈哈。但是尝试做过的都知道，想要进一步突破，又谈何容易呢。\n尝试的另一个路子是做副业，目前成绩也是中规中矩，远远达不到让自己可以躺平的状态，想要进一步发展也是困难重重。一方面是自己的人脉资源，粉丝基数都挺少的，导致传播的范围比较小，另一方面是自己的内容比较小众，受众面窄，但是技术含量又挺高的，导致我需要花费大量的时间去准备。\n在开源方面，这两年投入的精力变少了很多。前几年还可以靠着自己的一腔热血，持续的去更新，但是现在，一是几个项目的状态基本成型，仅需要偶尔维护；二是做开源根本无法赚钱，面向应用层估计好点，但是一些基础组件，在当前我的能力范围内，基本不太可能做出实现盈利的产品。\n很多事情，甚至是绝大多数事情，能够持续向前推动，背后一定是有物质上的激励，仅仅依靠情怀，或者热情，真的很难维持。\n目前的状态大致是这样，当然也有一些其他的打算。\n比如人脉积累到了一定的程度，技术水平得到了更大的提升，或许可以尝试自己做出一些产品，或者是成系统的服务？\n但具体是什么方向，我还没有想法，做基础软件，回报周期太长，对自身，对团队的素质要求高，做应用层面的东西，自己又不太熟悉，并且竞争也会普遍加剧，如果没有特别大的粉丝基数，或者是一个新颖独创的想法，也很难成功。\n按照我目前的能力和状态，这个目标估计在短期内是无法实现的了。\n28 岁，的确也是到了谈婚论嫁的时候，时常看到朋友圈，隔三差五的，某些同学或同事结婚了，组件了新的家庭。但是我自己，在这方面估计还是遥遥无期。\n在大城市待久了，偶尔也会觉得孤独，城市的繁华，灯红酒绿，好像跟我们普通人并没有太大的关系。难以在这样的大城市中获得一席之地，想要扎根下来的话，房价太高，仅仅凭工资收入积累，基本不太可能完成购房这么大的目标。\n回二线城市吧，又有点不太甘心，况且工作机会，薪资待遇方面也会差不少，有一点不太敢迈出这一步，至少现在还不是好的时机。\n心态总是这样反反复复，时而迷茫，时而挣扎，或许这就是我们这个阶段的年轻人的常态吧。\n","date":"2024-09-23T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E4%B8%8D%E7%9F%A5%E4%B8%8D%E8%A7%8928-%E5%B2%81%E4%BA%86/","title":"不知不觉，28 岁了"},{"content":"在前面的一篇开源项目系列中，主要介绍了目前开源项目蓬勃发展的态势，并且拥有一个开源项目，对我们个人履历、职业发展等都有非常多的好处。\n这一次就来跟大家分享一下，面对一个开源项目，我们应该如何上手，快速看懂项目的源代码，并且能够参与到开源项目的社区中。\n首先，如果我们通过 Github 找到了一些项目，如何判断这个项目是否符合我们的需求，是否值得我们花时间去投入呢?\n在我看来，有几个比较重要的点需要关注。\n一是项目是否对新人友好，是否有比较完善的文档和上手教程。其实一般做的比较规范的项目，都会有这部分内容，会在项目的 README 文档中，说明上手项目的整体流程，以及如何参与到社区当中等等。\n如果连这些基本的内容都没有的话，说明社区的建设并不是很好，或者说只是一些年久失修的项目，想要上手的话难度会很大。\n二是，如果想要参与到开源项目当中的话，则需要关注这个项目是否接受外部贡献者。因为有的项目实际上是公司内部在维护，对外部贡献者可能并没有完善的流程、规范等等。\n第三点，可以关注是否有适合的 Good First Issue，一般来说，开源社区会将一些适合新人上手的 Issue 标记为 Good First Issue，从这些 Issue 开始上手会是一个不错的选择。\n在这个基础之上，如果你已经选择了一个不错的开源项目，如何快速上手呢？我认为搞懂下面几个问题是比较关键的。\n首先，面对一个开源项目，其实最基础的问题，就是搞懂这个项目的基本背景，就是这个项目是基于什么契机建立的，比如是为了解决什么实际的问题，或者是对某个系统进行优化，或者是使用新的语言进行重写等等。\n通过这些背景，需要搞懂项目主要的功能是什么，用来解决什么样的问题。\n其次，需要搞懂项目的大致架构，当然在初期不太可能将每个模块都能够完全搞明白，特别是如果一个项目比较庞大的情况下，但是尽量搞清楚架构图中每个模块的大致功能，先从宏观上有个大致的了解。\n这个理论背景的基础之上，我们需要动手实践起来。\n比如，首先需要能够编译并且运行这个项目，这是最基础的一个步骤。并且针对项目当中的一些核心流程和方法，能够去进行调试，一步一步的去查看执行过程中的状态转换。\n比如针对一个数据库项目，或者存储引擎的项目，其实最基础的流程就是，建立表，并且向表中插入数据，然后能够从表中读取数据，这是一个最基础的步骤。\n当然，这个链路当中可能又涉及到了很多的不同的内部组件和方法等等，需要我们在调试的过程中去慢慢熟悉。\n以上是我对如何入门开源项目的一点建议，我在 B 站也发布了一个视频，使用一个 Rust 项目作为示例，用上面我提到的步骤，一步步调试 Rust 项目，向大家详细演示了如何搞懂一个开源项目。\n感兴趣可以观看下：\nhttps://www.bilibili.com/video/BV1Qc4YevEVy\n","date":"2024-09-11T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rose-%E8%81%8A%E5%BC%80%E6%BA%902-%E5%A6%82%E4%BD%95%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B%E4%B8%80%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","title":"rose 聊开源—2 如何快速上手一个开源项目"},{"content":"继去年发布《从零实现 KV 存储》和《从零实现分布式 KV》两门课程之后，今天再次给大家带来一个全新的课程，使用 Rust，手写一个 SQL 数据库系统，超级硬核，前所未有！\n课程详情，以及如何购买，都在飞书云文档中，复制下面的链接打开即可：\nhttps://w02agegxg3.feishu.cn/docx/OxwGdeM30oss7vxEG5AcUn4unEc\n下面是课程内容的详细介绍：\n本课程将会使用 Rust 从零实现一个完整的 SQL 数据库系统，将会由浅到深、由易到难，循序渐进的带着大家去实现，从第一行代码开始，均有完整的代码实现演示。\n无论你是后端研发，中间件开发，基础架构开发，甚至是 DBA，能够手写一个完整的 SQL 数据库系统，都是你突破技术发展瓶颈的有效途径。\n本课程将会详细介绍架构设计 ，原理剖析，再到源码实现，让你深度掌握数据库底层，具备解决大量生产级数据库问题的能力，助力成为高端技术人才！\n学习完本课程，你至少可以收获：\n入门 Rust 语言，课程针对 Rust 零基础的同学上手，专门讲解了 Rust 中最常用的基础语法，能够应对大多数开发需求 巩固 Rust 语法基础，并且可以使用 Rust 搭建一个完整的、超级硬核的实战项目 锻炼编程基本功，数据库是基础软件领域最为复杂，工程细节极为考究的项目，可以从中学习到数据库内核构造，完全掌握一个数据库的整体执行流程 简历上的一个硬核项目，一个脱颖而出的亮点，和别人拉开差距，帮助你在职场上获得更大的突破 课程作者 我的网名叫 roseduan，是开源项目rosedb、lotusdb 的作者，目前总共超过 6k star，目前担任数据库内核研发的职位，主要研究 Postgres 数据库，在数据库内核开发方面有丰富的经验。\n我的 Github 主页：https://github.com/roseduan\n我也开源过一些 Rust 项目，并且上过 Github 全球 Trending 榜单，在 Rust 方面也有多年的开发经验。\n同时，我也是**《从零实现 KV 存储》和《从零实现分布式 KV》**这两门课程的作者，在教学方面也有很多的心得，能够帮助大家梳理知识重点，理清前后脉络，用最快的速度，最稳的节奏帮助你学懂、学透全部知识点。\n对这两门课程感兴趣的同学，也可以点这里查看对应的课程详情 0 从零实现 KV 存储—关于本课程 0. 从零实现分布式 KV—关于课程\n课程形式 课程内容是文档+视频。\n文档是一小节内容的概述，主要描述这一小节内容学什么。\n视频是详细的理论讲解+代码实现。\n课程目录 第一部分售价 400\n第二部分售价 400\n可以单独购买第一部分，觉得不错，再购买第二部分\n也可以两部分一起购买，更快解锁全部内容，第二部分内容后面会陆续发布，敬请期待。\n第一部分 第一部分售价 400，代码量在 3000 行左右，包含内容：\nRust 基础回顾，主要讲解 Rust 常用基础语法和最重要的所有权、引用等概念，Rust 零基础也可以上手！ 使用最常见的 SQL 语句搭建项目的代码结构 磁盘存储引擎 MVCC 事务完整实现 完整目录如下：\n第二部分 第二部分售价 400，陆续更新中，大致目录如下\n适用人群 这个课程对以下同学应该都非常的合适，包括但不限于：\n入门并巩固 Rust 基础，课程内容几乎涵盖了 Rust 大多数常用基础，例如数据类型，match 表达式匹配，函数，闭包，结构体，泛型，Trait，所有权，借用，生命周期，错误处理，智能指针等等 增加 Rust 项目经验的同学，如果学习了一些 Rust 的基础知识，但是苦于没有项目经验，想要进一步巩固自己的知识，自己写一个数据库是一个很合适的实战项目 想要巩固基本功的同学，基本功对一个程序员来说非常重要，数据库是任何开发者必学的基本技能之一。自己写一个数据库，更加深入理解一个复杂的系统，是你和别人拉开差距的关键 想要深入学习数据库的内部构造，彻底搞懂数据库系统基本原理，包括 SQL 解析、查询执行、存储引擎、事务等 突破职业瓶颈，理解一个复杂系统的设计与实现，提升自己的技术能力，架构设计能力 前置知识 学习本课程，不需要任何其他前置知识（当然了解一些 SQL 和 Rust 的基础更好）。\n课程当中涉及到的一些内容，例如 Rust 零基础入门，数据库基础知识等，我将会详细的为大家讲解，前期先跟着视频中敲代码，是完全可以学会的。\n是否可以试看 可以，课程的第四节和第七节内容可以试看\n04 从零实现 SQL 数据库—数据库架构\n07 从零实现 SQL 数据库—Planner 实现\n如果没有完整上下文的话，试看的具体内容细节你可能并不会特别明白。 试看主要是让同学们了解课程的大致结构，和我的讲课风格等等。\n课程目前的进度 课程目前第一部分内容已经更新完毕，目前可单独购买第一部分！\n第二部分预计 2024 年底更新完毕，准备好了我会发布。\n课程评价 我之前已经发布过两门课程，课程受到了很多好评。\n有的同学，根据课程内容，自己写开源项目，获得了上千 star！\n有的同学在面试中使用课程项目，毕业即获得年薪 30w+ offer！\n可以在这两篇文章中了解详情：\n从零实现 KV 存储—捷报频传\n课程学员故事—介绍两个开源项目\n从零实现 SQL 数据库这个课程更加硬核，会比之前的项目更具含金量！\n如何购买 https://w02agegxg3.feishu.cn/docx/OxwGdeM30oss7vxEG5AcUn4unEc\n咨询服务 购买后，我会拉你进课程用户专属的飞书群，我会亲自为你解答学习过程中的疑难杂症，保证你肯定能够完全学会课程中的内容！\n其他 Q\u0026amp;A 购买后在什么平台学习？\n课程内容都在飞书云文档，购买成功后，为你开启对应的权限，然后可以在线观看\n课程时长？代码量？\n课程的视频总时长大概在 15 小时左右，代码量大概在 3000 行\n只学了第一部分，能把项目写到简历上吗？\n当然可以，其实到第十一节，就已经是能够独立运行，比较迷你的数据库了，只是功能比较单一，也可以写到简历上\n如何获取项目中的代码？\n购买成功后，可以到课程专属飞书用户群公告中下载\n没有任何数据库或者 Rust 基础，能学会吗？\n当然可以，本课程首先就针对 Rust 零基础的同学，讲解了 Rust 大多数基础语法。对数据库的内容也会在每一个小节进行讲解，并且有代码实现，完全可以学会。\n购买后是否有有效期？\n没有，购买后永久持有，无限次观看。\n可以退款吗\n虚拟内容服务，一经购买，概不退款，多谢支持与理解。\n","date":"2024-08-29T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rust-%E6%89%8B%E5%86%99-sql-%E6%95%B0%E6%8D%AE%E5%BA%93/","title":"Rust 手写 SQL 数据库"},{"content":"感受 到 2024 年中，我刚好毕业五年了，五年的时间对我们来说，可能说长也不算长，说短也不算短。我们从一个涉世未深的学生，逐渐转变为一个稍微有一些社会阅历的打工人。\n毕业的这五年以来，其实我的工作方面，也有了很大的一些变化。\n今天跟大家聊一聊，我自己毕业五年以来，我自己的一些工作内容方面的一些经历，以及我自己的一些感想看法等等。\n可能很多朋友也知道，我大学并不是计算机专业，而是一个文科的专业，因为我之前高中是一个文科生，然后到大学的话，学的专业跟计算机是没有任何关系的。\n我是在大三的开始去自学计算机，毕业的时候，找到了一份在上海的 Java 开发的工作。\n其实上海这座城市，也是我毕业的时候很想去的一座城市，内心有一种没有任何原因的憧憬。然后比较幸运的找到了在上海的一份工作。\n刚毕业的时候我做的是 Java 业务开发，然后那个时候工资也就几千块，跟一些什么进大厂的科班同学肯定是没法比的。\n那个时候我技术其实也比较的菜，刚工作的时候，也花了很长的时间去适应。对于一些真实的线上的项目环境并不是特别的了解，也花了比较长时间才慢慢的去适应工作的节奏。\n也正是因为我觉得自己的一些基础能力比较薄弱，我在业余的时间不断的去学习，学习了很多计算机方面的基础知识，比如说操作系统，数据库，数据结构，算法等等。\n基本上一些基础知识或多或少都学习了一些，然后我也学习了很多新的语言，比如说 Python、Go。\n通过 Go 语言的话，我开源了自己人生的第一个开源项目，其实之前也分享过很多这方面的内容。\n目前 rosedb 这个开源项目是在 Go 语言存储引擎这个垂直领域，算是小有名气的一个开源项目吧，也是被很多的一些生产环境所使用。\n我觉得正是那个时候的这样一种不断学习、折腾的一种心态，才导致我后面的一些工作内容不断的经历了一些转型和变化。我觉得是跟我当时爱折腾的一种心态是分不开的。\n在我毕业一年半的时候，就是做 Java 业务开发一年半以后，然后我跳槽到了 b站，做的是 Go 语言的后端业务开发，后面又转到了分布式存储相关的岗位。\n然后是我毕业三年以后，也就是 2022 年年中，由于我自身在数据库这方面兴趣和经验的一些积累，并且在三年前的话，数据库行业其实发展还是挺不错的，也有些机会，比较幸运的就转到了数据库内核开发的一个方向，最近两年都是在数据库行业。\n这就是我工作五年以来在工作内容方面经历的诸多变化的一个历程，主要是从 Java 业务开发到 Go 语言开发，再到分布式存储，再到数据库，经历了非常曲折的一个变化。\n当然这个变化的过程，一方面是我自己的兴趣，另一方面也是由于其他的一些大环境的一些不可控的因素而共同导致的结果。\n其实这也是我觉得毕业这五年以来最大的一个感受，变化是每时每刻都存在的，我们的一些工作内容有可能随着我们自己的兴趣发生变化，也有可能随着其他的一些环境的因素发生变化。\n比如说你跳槽了，业务方向不一样了，或者是转了语言，其实都有可能，或者是你自己的一些组织架构发生了一些调整，导致你的工作内容也发生了很多变化，都是很正常的。\n所以说对于很多刚毕业没多久，或者说还没有毕业的同学，一定要有一种拥抱变化的一种心态。我们的工作内容可能随时跟随着很多因素去变化，所以说这个时候，不要把自己局限在一个小的范围之内。\n我觉得还是要秉持一种更开放的心态，面对变化的时候，能够主动的去调整我们的心态，我觉得是比较重要的。\n拥抱变化，其实也是我毕业这五年以来最大的一个感想。\n还有一个感想就是在目前形势并不是特别好的情况下，我觉得要和别人拉开差距，最有效的一种方式，就是去做别人做不到，或者是难以坚持的事情。\n比如说我最开始学习 Go 语言之后，自己看了很多资料、课程、项目等，很多人可能也仅仅局限于看过而已，并没有去把它付出实践。\n由于各种机缘巧合，也可能由于自己的兴趣，然后我去做了很多项目方面的一些尝试，所以我觉得一定要去做别人想不到的事情，或者是坚持不下去的事情。\n比如说我们每个人都会学习数据库、操作系统，可能对于大部分人来说，仅仅局局限于学习过，比如说看过一些视频，或者说看过一些教材，可能好点了的会做一些笔记之类的。\n但是我觉得真正的要和别人拉开差距的话，你得去做别人做不了的事情。比如说你自己去写一个比较迷你的操作系统，或者说你自己去写一个比较迷你的数据库系统等等。\n特别是在当前的就业形势并不是特别好的情况下，想要继续在这方面去发展的话，一定要有这样的一种意识。\n这也是我这几年以来最大的一些感想吧。\n迷茫 当然其实每个人可能都会有自己的痛苦、迷茫等等，其实我也会有。\n我们背井离乡来到了一座大城市，然后像北京上海这样高昂的房价，让人觉得望而却步，想要在这里立足下来，其实是非常困难的。\n对于我们普通人来说，在没有其他外力的加持下，很难在这样的大城市立足，当然你可能有了亲朋好友的一些帮助，然后拼了命，在北京上海的这样大城市付了个首付，但是你可能要背负很长时间的一个房贷的压力。\n我觉得对于目前的就业形势来说的话，我是不太想去背负这样的压力的可能就是对于未来的一些就业环境持比较悲观的一种态度，难看到能够在这个行业有机会持续的做下去，可能还是比较困难的。\n大部分人跟我的想法是比较类似的，对未来的一些形势可能并不太乐观，我觉得这也是很多人迷茫的原因，我跟大家是同样的。\n当然还有比如说对于我们程序员群体来说，可能并没有什么丰富的业余活动，社交的圈子也比较窄，如果又是一个比较内向的人的话，可能有的时候在这样一个大城市当中，会觉得非常的孤独，就是没有什么知心的朋友。亲朋好友可能也隔得比较远，没有什么太多的联系。\n我觉得这可能是每一个打工人可能都会有的一种孤独感吧。\n","date":"2024-08-26T20:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E6%AF%95%E4%B8%9A%E4%BA%94%E5%B9%B4%E5%90%8E%E7%9A%84%E6%84%9F%E5%8F%97%E4%B8%8E%E8%BF%B7%E8%8C%AB/","title":"毕业五年后的感受与迷茫"},{"content":"去年开始，陆续有很多同学开始学习我的课程，半年多的时间过去了，他们从中收获很多，最近这段时间捷报频传，都凭借项目丰富了自己的项目经历，提升了自己的技术能力，在面试中也是披荆斩棘。\n很多同学自发的给我反馈，表示课程很赞，甚至是唯一能捡起来的练手项目：\n项目面试\u0026amp;求职 也有同学将项目用于面试，拿到大厂实习 offer：\n甚至有的同学在求职中，项目发挥关键作用，刚毕业就能拿下 30w+ 的 offer！\n其实每个有收获的同学，都更应该感谢自己，能够坚持做好一个硬核项目，本身就不是一件容易的事情。\n在大环境不好，就业糟糕的情况下，其实我们唯一能做的，就是不断提升自己的能力，去做别人做不到的事情，这样才能够和别人拉开差距，凸显自己的优势。而不是整天自怨自艾，无所事事，这根本改变不了什么。 其实，静下心来做好一件事，就已经能够超越很多人了，只要努力，结果也一定不会太差。\n课程详情 对课程感兴趣的同学，可以进这个链接查看课程详情：\nhttps://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g\n","date":"2024-08-12T13:19:33Z","permalink":"https://blog.roseduan.cn/p/%E7%AE%80%E5%8E%86%E4%B8%8A%E5%86%99%E4%BB%80%E4%B9%88%E9%A1%B9%E7%9B%AE%E6%A0%A1%E6%8B%9B%E8%83%BD%E6%8B%BF%E5%88%B0-30w-%E7%9A%84-offer/","title":"简历上写什么项目，校招能拿到 30w 的 offer"},{"content":"FDW 概述 FDW，即 Foreign Data Wrapper，是 PostgreSQL 中的一项关键特性，通过接入 fdw，用户可以直接通过 SQL 语句访问各种外部数据源。\n在 Postgres 中，FDW 有很多应用场景，比如：\n1. 跨数据库查询： 在 PostgreSQL 数据库中，我们可以通过 FDW 直接请求和查询其他 PostgreSQL 实例，或是其他数据库如 MySQL、Oracle、DB2、SQL Server 等。\n2. 数据整合： 当我们需要从不同数据源整合数据时，例如 RESTFUL API、文件系统、NoSQ L数据库以及流式系统等，FDW 能够帮助我们轻松实现这种跨来源的数据整合。\n3. 数据迁移： 利用 FDW，我们可以高效地将数据从旧系统迁移到新的 PostgreSQL 数据库中。\n4. 实时数据访问： 通过 FDW，我们能够访问外部实时更新的数据源。\n常见的 FDW PostgreSQL 支持非常多常见的 FDW，能够直接访问多种类型的外部数据源。例如，可以连接并查询远程的PostgreSQL，或者其他主流的 SQL 数据库如 Oracle、MySQL、DB2 以及 SQL Server。同时，PostgreSQL FDW 也具备灵活的接口，支持用户自定义外部访问方式。\n此外，对于 NoSQL 数据库，以及实时数据库如 InfluxDB、消息队列如 Kafka、文档型数据库如 MongoDB 等等都能通过FDW实现数据访问。\n常见的文本格式数据，如 CSV、JSON、Parquet 和 XML，也可以通过 FDW 轻松访问。大数据组件如 Elasticsearch、BigQuery，以及 Hadoop 生态系统中的 HDFS 和 Hive 等等都可以通过 FDW 实现无缝集成。\nFDW 基本使用 FDW机制由四个核心组件构成：\n1. Foreign Data Wrapper： 特定于各数据源的库，定义了如何建立与外部数据源的连接、执行查询及处理其他操作。\n2. Foreign Server： 在本地PostgreSQL中定义一个外部服务器对象，对应实际的远程或非本地数据存储实例。\n3. User Mapping： 为每个外部服务器设置用户映射，明确哪些本地用户有权访问，并提供相应的认证信息，如用户名和密码。\n4. Foreign Table： 在本地数据库创建表结构，作为外部数据源中表的映射。对这些外部表发起的 SQL 查询将被转换并传递给相应的 FDW，在外部数据源上执行。\n以 postgres_fdw 为例，下面是一个 fdw 的基础使用方法：\n创建插件和 Foreign Server\n1 2 3 4 5 6 7 8 9 10 创建插件 test=# create extension postgres_fdw; CREATE EXTENSION 创建 Foreign Server CREATE SERVER foreign_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host \u0026#39;127.0.0.1\u0026#39;, port \u0026#39;8001\u0026#39;, dbname \u0026#39;postgres\u0026#39;); 创建 User Mapping 和外部表\n1 2 3 4 5 6 7 8 9 10 11 创建 User Mapping CREATE USER MAPPING FOR gpadmin SERVER foreign_server; 创建外部表 CREATE FOREIGN TABLE foreign_table ( val int ) SERVER foreign_server OPTIONS (schema_name \u0026#39;public\u0026#39;, table_name \u0026#39;t2\u0026#39;); FDW 实现原理 在 PostgreSQL 的内核代码中，FDW 访问外部数据源的操作接口主要通过 FdwRoutine 这一结构体进行定义。任何接入外部数据源的插件都可以根据自身需要去实现这些接口。\n接口函数大致分为多个类别，包括但不限于扫描、修改、分析外部表等等。例如，扫描外部表相关接口定义了如何扫描外部表，常见的操作包括开始扫描（ BeginForeignScan，主要进行准备工作）、执行扫描（IterateForeign Scan，从扫描中获取数据）、重新扫描（RescanForeignScan）以及结束扫描（EndForeignScan）等。\n此外，还有用于修改数据的外部表接口，支持对数据进行 insert、delete、updat e等操作，以及 explain 和 analyze 等外部表接口。\n如下图，这是一个file_fdw 插件实现 FdwRoutine 的示例，这里仅实现了一些基础的扫描操作接口（如BeginForeignScan、IterateForeignScan 等），以及用于表分析的 AnalyzeForeignTable 接口。\n在 PostgreSQL 的执行过程中，这些接口函数会在 planner 或 executor 阶段被调用。尤其是当 executor 需要依赖外部服务插件访问数据时，它会通过插件提供的数据访问接口来获取数据。这使得 FDW 能够与 PostgreSQL 的Parser、Planner 以及 Rewriter 等组件能够无缝协作。\n在需要访问外部数据源时，我们只需定义好相应的数据访问接口，就能直接获取数据，并按照PostgreSQL的标准流程进行后续处理。在执行过程中，执行器会分解为几个阶段进行：\n1.首先进入 初始化阶段，核心任务是执行外部表扫描 ExecInitForeignScan。在这个阶段，主要是定义了一些外部扫描的接口，并调用 FdwRoutine 中用户自定义的接口，从而进行扫描前的准备。\n2.紧接着是执行查询阶段 ，此时会调用 ExecuteForeignScan 方法。在这个方法中，我们主要需指定 ForeignNext 来获取下一条数据，并定义 ForeignRecheck 来检验数据元组的可见性。\n3.最后进入结束查询阶段 ，即执行 EndForeignScan，该阶段主要负责资源清理工作。若系统检测到存在 FDWRoutine，就会利用用户自定义的 EndForeignScan 函数来释放资源。\n以上就是FDW整体的实现流程。接下来，为了更深入地了解FDW的工作机制，我们将深入探讨FDW的源码。\nFDW源码解析 FDW 支持的数据类型众多，但在此我们以常见的 Postgres_fdw 为例，剖析其源码实现，同样可帮助理解其他 FDW的源码逻辑。\n首先，我们需要定义 FdwRoutine。前文提到了 FdwRoutine 主要负责定义外部数据扫描的接口，接口需要自定义实现外部扫描的方法。\n访问外部数据源\n定义好 FdwRoutine 之后，开始访问并扫描外部数据源。在 Postgres_fdw 中，流程也就是进入 BeginForeignScan 阶段。这一阶段主要是获取我们先前定义的外部表实例和用户信息，然后初始化并获取一个连接到远端数据源。\n执行查询阶段\n获取连接后，执行查询，即进行 IterateForeignScan 阶段。这个过程的逻辑是创建一个游标迭代器(cursor)，并从cursor中持续获取数据。\n当全部数据迭代或扫描完成后，我们会释放连接并关闭 cursor 等资源，通过自定义的 EndForeignScan 阶段完成。\ninsert操作\n对于 insert 操作，例如，在本地 PostgreSQL 数据库中修改Web数据源，增加一条数据，需要访问插入Web数据的接口。此操作先进入BeginForeignInsert 阶段，任务是构造SQL语句，通过预处理语句进行初始化，做好插入准备。\n之后，进入 ExecuteForeignInsert 阶段，执行数据插入，主要通过预处理语句传递参数，然后发送SQL到远端执行。\n最后，EndForeignInsert 阶段负责收尾和资源清理。\n更新/删除操作\n更新和删除操作的逻辑与插入类似。首先进入BeginDirectModify阶段，进行数据修改前的准备，如构建查询语句、获取连接等。随后执行修改操作，主要通过发送参数和查询到远端来执行。\n","date":"2024-08-07T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/postgres-%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E7%95%AA%E5%A4%96%E7%AF%87fdw-%E8%AF%A6%E8%A7%A3/","title":"Postgres 源码学习番外篇—FDW 详解"},{"content":"从大三自学编程开始，到现在，大学毕业竟然都五年了，这期间我学习了很多新的语言，这些学习历程带给了我非常多的收获，今天就简单给大家分享下。\n我的学习大概分为了三个阶段。\n第一阶段，在大学自学期间，我选择的是当时工作就业机会最多的 Java，这次自学，带给我最大的收获就是给了我一份工作。\n尽管这份工作并不那么尽善尽美，薪资也比较低，但对于当时的我来说已经是非常欣慰了，甚至让我觉得幸运，能够去往自己毕业时梦寐以求的城市，上海。\n当时互联网还算是非常不错的了，就业形势一片大好，欣欣向荣的感觉，每个人都是那么的有朝气。我也一样，初到上海的我，心中总是充满激情与干劲，甚至满含理想。\n只是后来每况愈下，到现在，甚至有一份稳定的工作都成了奢求，每个人的信心应该都收到了不同程度的打击。\n之前其实也提到过，我从 Java 转到了 Go 语言，具体细节在之前旧文提到过了，感兴趣的同学可以去翻一翻，这里不再赘述。\n毕业大概一年半，学习 Go 带给我的收获更多了，由此进入了第二阶段。\n我收获了一份新的工作，从一个小公司，到一个具有更高知名度的互联网公司，增长了一些见识，认识了一些优秀的人。\n当然最重要的是，培养了自己的兴趣，收获了自己的开源项目，并且小有名气。我也从业务方向转到了基础架构方向，在技术方面的收获就更多了。\n我也认识了很多志同道合的朋友，让我发现有很多人都跟我有同样的兴趣，甚至类似的背景。当然也认识了更多优秀的人，给了我更多向前的动力。\n这期间还有一个收获，那就是我的个人 ip 影响力更大了。 我的公众号增长的萌芽阶段，其实就是我开源了 RoseDB 之后（第一个开源项目），从那以后，几年的时候一直都保持着缓慢但稳定的增长。\n虽然现在粉丝也不算多，但也还算是积累了一些影响力。个人 ip 在前期确实是需要很多的积累，有时候看到别人的一个爆款就很涨粉，但是在一个爆款之前，别人往往已经积累了很多年，所以倒也不是很眼红。\n到了 2022 年，姑且认为是我学习发展的第三阶段，我从基础架构再次转向，误打误撞的进入了数据库行业，这时候我学习了 C 和 Rust。\n这期间的学习更像是半自动半被迫的，因为数据库实在是太复杂了，这里压力迫使我不得不去学习。\n我在刚入职不久之后，因为工作上的需要，维护的一个组件是使用 Rust 编写的，所以我就开始了学习 Rust，这次学习带给我的收获便是，让我第一次真正的接触到了数据库内核，了解到了它的复杂与困难，当然也从中得到了很多的锻炼。\n然后因为我们的数据库是 C 写的，所以实际上这两年的工作，也时刻伴随着学习的过程。\n这期间的学习，带给我的收获，不仅仅是让我对数据库有了更多的了解，更深的认知，而且在技术方面也有了更多的积累，让我在工作之中处理一些难题的时候，会更加的从容不迫。\n好了，这便是我从大学自学编程，到现在毕业五年时间的学习历程，大致分为了三个阶段，每个阶段都有很多的偶然性，以及我自己都无法预料的结果。\n但是总结下来，这期间其实有一些我认为始终没变的东西，最重要的便是我始终保持开放的心态，乐于去接受新的事物，并且始终保持学习的状态。\n我始终以我 Github 上的签名来提醒我自己，Stay hungry, stay foolish\n","date":"2024-07-30T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E6%AF%95%E4%B8%9A%E4%BA%94%E5%B9%B4%E5%AD%A6%E4%B9%A0%E6%96%B0%E7%9A%84%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E7%BB%99%E6%88%91%E5%B8%A6%E6%9D%A5%E4%BA%86%E4%BB%80%E4%B9%88/","title":"毕业五年，学习新的编程语言给我带来了什么"},{"content":"前面提到了 Postgres 中的数据文件是被换分为了多个 page，每个 page 的大小默认是 8 KB。当我们向表中插入数据的时候，就需要从这些 page 中找到一个能够放得下这条数据的 page。\n因为数据文件 page 的组织是无序的，元组的插入也是无序的，所以如果依次遍历查找满足条件的 page，可能会非常的低效，Postgres 中使用 FSM（Free Space Map） 来进行查找，加速找到适合插入的 page 的过程。\nFSM（Free Space Map），即空闲空间映射，其目的主要是快速定位一个有足够空间容纳插入元组的文件页。\n我们需要保证这个映射空间尽可能小，并且辅以一个高效的数据组织方式，这样才能够快速的检索。\n在 Postgres 中，一个 page 默认的大小是 8KB，默认情况下一个文件的大小是 1GB，所以能够最多容纳 131072 个 page。\n如果我们采用一个 32 位 int 类型来表示一个 page 的空闲空间的话，当然是没问题的。但是如果 page 很多的话，每个 page 都需要 32 位来表示空闲空间的值。\nFSM 也是需要物理存储的，为了在搜索的时候，能够更加快速，我们需要保证 FSM 占用的空间尽可能的少，所以在 Postgres 中采用了分类别的方式，将空闲空间的大小以 32 为步长，分为了 256 个区间：\n这样一个 8KB 的 page 的空闲空间大小，使用一个 uint8 类型就可以表示了，由 4 个字节变成了 1 个字节。\n如果 page 大小超过了 8KB，例如是 32KB，那么步长也随之增加：32KB / 256\n表示空闲空间的数据信息，存储到了在磁盘文件上，以 _fsm 结尾。文件的数据存储方式和 heap page 类似，也是采用了 page 的方式，page 的内部结构和前面提到的 heap page 类似，也有对应的 page header。\n存储的内容则比 heap page 简单很多，主要是两个属性：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /* * Structure of a FSM page. See src/backend/storage/freespace/README for * details. */ typedef struct { /* * fsm_search_avail() tries to spread the load of multiple backends by * returning different pages to different backends in a round-robin * fashion. fp_next_slot points to the next slot to be returned (assuming * there\u0026#39;s enough space on it for the request). It\u0026#39;s defined as an int, * because it\u0026#39;s updated without an exclusive lock. uint16 would be more * appropriate, but int is more likely to be atomically * fetchable/storable. */ int fp_next_slot; /* * fp_nodes contains the binary tree, stored in array. The first * NonLeafNodesPerPage elements are upper nodes, and the following * LeafNodesPerPage elements are leaf nodes. Unused nodes are zero. */ uint8 fp_nodes[FLEXIBLE_ARRAY_MEMBER]; } FSMPageData; fp_next_slot：从堆中搜索的起始位置 fp_nodes：空闲空间的数据（uint8 类型） 解决了空闲空间占用的问题，接下来就是空闲空间的数据如何组织的问题。其实这可以理解为是一个从无序的数组中，找到一个大于等于给定值的元素。\n所以 Postgres 中使用了堆这个数据结构来存储空闲空间的大小，堆的叶子节点对应的是 page 的空闲大小，堆顶元素是最大的元素，当查找是，从堆顶元素进入，依次和其子节点进行对比，一直到达叶子节点。\n查找的时候，还需要注意一个问题，那就是如果每次都从堆的顶层节点进入，那么有可能不同的进程找到的 page 是一样的。\n为了提升并发性能，让不同的进程尽量找到不同的 page，这样能够避免锁竞争。\n所以在查找的时候，记录了一个下次开始查找的下标值，如果该下标处的值不满足条件，则跳转到其右边的那个节点，然后从右边的节点的父节点开始查找，以此类推。\n还需要注意一个问题，一个 FSMPage 有可能存不下所有的 heap page 的空闲空间大小。\n所以实际上在存储的时候，会将空闲空间大小存储到不同的 FSM Page 中，那么不同 fsm page 中的空闲空间数据，又怎么维护成一个堆结构呢？\n实际上是使用了多层结构，将不同 fsm page 的数据维护成了多个 level 层级的关系。\n","date":"2024-06-30T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/postgres-%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-5fsm-%E7%A9%BA%E9%97%B2%E7%A9%BA%E9%97%B4%E6%98%A0%E5%B0%84/","title":"Postgres 源码学习 5—FSM 空闲空间映射"},{"content":"wal 的由来 wal 是我去年写的一个小组件，主要用于 LSM Tree 或者 Bitcask 的预写日志文件，以及任意的 append-only 文件读写都可以使用，第一次发布是 2023.6.13，刚好开源一年了：\nrosedb 和 lotusdb 将其作为重要的底层日志文件存储组件使用，这个通用的组件简化了 rosedb 和 lotusdb 的一部分代码，使项目整体更加简洁。 一年过去了，wal 同时也被很多其他的开源/闭源项目所使用（生产环境），对这个小组件我还是比较满意的，整体代码的质量还不错，代码理解起来也比较简单。\n之前写过一篇文章，简单介绍了如何使用 wal 构建一个极简的 KV 存储模型，以及我还在 Go 夜读社区分享过关于 wal 的设计和实现，结合这些资料看懂源代码应该没有什么困难。 使用 wal 构建你自己的 KV 存储 Go 夜读：高性能预写日志（Write Ahead Log）的设计与实现\n总体来说 wal 就是一个 append-only 的日志文件，提供了基础的读写方法，基本上参考了 leveldb 的预写日志文件的格式。\n写是一直 append 的，写完之后会得到一个表示数据位置的结构体，通过这个结构可以读取到对应的数据。\n这次对 wal 的优化 之前对整个 wal 文件进行遍历的时候，如果 value 比较小，那么会多次重复读取 value 所属的 block，这样的话效率比较低，而且是完全没必要的。 之前的策略是加上了一个 block cache 来缓解这个问题。\n但是细想，一个 block 读上来之后，如果 value 仍然在当前 block 中，那么可以重复利用这个 block，不用再去读取文件了。 在这个思路之下，对 wal 的读取进行了优化，主要是去掉了 block cache，并且如果 value 比较小的话，会直接重复利用当前 block，避免重复读取。\n优化之后的效果还是比较明显的，在我的机器上，遍历 1.8G 的数据，花了 5 s 左右，之前是 20s，遍历读取的性能提升在 4-5 倍左右。\n带来的一个好处便是，rosedb 的启动速度会得到提升，因为 rosedb 在启动的时候，会加载所有的 wal 文件进行索引的构建。\n","date":"2024-06-15T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E8%BF%91%E6%9C%9F%E5%AF%B9-wal-%E7%BB%84%E4%BB%B6%E7%9A%84%E6%80%A7%E8%83%BD%E6%8F%90%E5%8D%87/","title":"近期对 wal 组件的性能提升"},{"content":"我自己从最开始开源 rosedb/lotusdb，以及一些其他组件，折腾开源也有很多年了，这次想写一个关于开源项目的系列，结合我自己的实际经历，讲讲开源项目的开发、参与、维护、流程规范、收获、盈利等内容，希望这个系列对大家有所帮助。\n在当今快速发展的技术领域，开源项目越来越受到开发者和企业的重视，近几年开源在国内的势头其实也发展得非常好，涌现出了非常多优秀的开源项目、开源社区，以及日益增长的开源开发者和爱好者。\n开源甚至是一些公司的商业策略以及赖以生存的根本，比如国内开源各方面做的最好的 TiDB，还有很多其他优秀的开源项目。\n对于我们个人来说，参与或维护开源项目不仅可以提升个人的技术水平，还能带来诸多好处，这一次来和大家简单探讨为什么你需要一个开源项目，以及它如何在个人成长和职业发展中发挥关键作用。\n技术提升\n开源能够带给我们非常多的好处，对于技术人来说，最直观的就是技术能力方面的提升了。\n通过开发项目解决实际问题，并且在开源维护的过程当中，阅读别人的代码，能够见识到不同的人的代码，以及编码风格，可以从别人写的好的地方去学习。\n我自己开源了一些关于存储引擎方面的开源项目，让我能够在这方面一直深入研究，不断提升在这方面的能力。\n履历\n开源项目是一种展示技术能力的最佳方式之一。\n比如在简历中列出你参与的开源项目以及你的贡献，能够直观地向招聘者展示你的实际能力和项目经验，这比任何证书和成绩单都更具说服力。\n深入参与一个比较知名的开源项目，或者自己运营一个有影响力的开源项目，都会使自己的个人履历增光添彩。\n人脉\n开源社区汇聚了全球各地的优秀开发者，通过参与开源项目，有机会结识这些志同道合的技术爱好者。\n无论是项目讨论、技术交流，还是合作开发，这些互动都将极大地拓展你的人脉圈。\n强大的人脉不仅对技术提升有帮助，还能在职业发展中带来更多机遇。\n比如我自己就通过开源项目结识了很多志同道合的朋友，并且还和一些人维持了不错的关系。\n团队协作\n一个项目开源出去之后，你肯定不希望只是自己一个人玩儿，如果有后续有很多的人参与进来，你将学会如何在团队中有效协作，这包括代码规范、版本控制、任务分配、沟通协调等多方面的技能。\n这些团队协作经验对于你在未来的职场中适应团队工作模式、提升工作效率具有重要意义。\n表达能力\n参与开源项目不仅需要写代码，还需要撰写文档、报告问题和提出建议，这些活动有助于提升你的书面表达能力。\n同时，在开源社区中，你也需要通过讨论和交流来表达自己的想法，这对于口头表达能力也是一种锻炼。这些表达能力在技术分享、会议演讲和项目汇报中都非常重要。\n在职场工作这么多年，我发现大多数人的表达能力其实并不算太好，能够把一件事情透彻的讲清楚，并且让别人能够理解，是一件并不容易的事情。\n技术管理\n如果你主导或维护一个开源项目，你将接触到项目管理的方方面面。\n这包括制定项目规划、管理任务进度、协调团队工作、处理项目风险，技术选型等。\n这些技术管理经验不仅对个人能力提升有帮助，还能为未来担任技术领导角色打下坚实的基础。\n对求职的帮助\n目前很多公司岗位在招聘的时候，都把拥有开源项目，或者参与开源项目作为了一个重要的加分项。\n因为参与或维护开源项目意味着候选人至少有很多的优势，比如：\n实际项目经验：开源项目中的实际开发经验可以证明候选人的技术能力和解决问题的能力。 自主学习能力：参与开源项目通常需要自主学习和探索新技术，这表明候选人具有较强的自我驱动力和学习能力。 团队协作能力：在开源项目中，候选人需要与来自不同背景的开发者协作，这展示了其良好的沟通和协作能力。 责任感和毅力：开源项目通常需要长时间的投入和持续的维护，参与者需要具备高度的责任感和毅力。 说回我自己，我上一次找工作的时候，实际上我的开源项目对我的帮助非常大，在面试的过程当中，给了我很多的信心，并且也证明了自己的能力。\n所以，无论是出于技术提升、职业发展，还是个人成长的考虑，参与或者维护开源项目都是一个明智的选择。\n","date":"2024-06-05T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rose-%E8%81%8A%E5%BC%80%E6%BA%901-%E4%BD%A0%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E4%B8%80%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","title":"rose 聊开源—1 你为什么需要一个开源项目"},{"content":"前面一节主要从宏观上了解 Postgres 表数据文件的组织方式，接下来我们深入到一个表文件的 page 内部，查看 page 的具体结构表示。\n存储在磁盘上的一个表数据文件，内部切分为了多个 page，每个 page 默认的大小是 8KB，为了从磁盘上读取数据的效率，每次从文件中读取数据的时候，都是以 page 作为基本单位。\n文件页中的每个 Page 被赋予了一个连续递增的唯一的编号，叫做 BlockNumber。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* * BlockNumber: * * each data file (heap or index) is divided into postgres disk blocks * (which may be thought of as the unit of i/o -- a postgres buffer * contains exactly one disk block). the blocks are numbered * sequentially, 0 to 0xFFFFFFFE. * * InvalidBlockNumber is the same thing as P_NEW in bufmgr.h. * * the access methods, the buffer manager and the storage manager are * more or less the only pieces of code that should be accessing disk * blocks directly. */ typedef uint32 BlockNumber; 我们可以通过 Postgres 的插件 pageinspect 来查看一个 page 的内部结构和状态。\n1 2 3 4 5 postgres=# postgres=# postgres=# create extension pageinspect; CREATE EXTENSION postgres=# create table t as select generate_series(1,100)a; 我这里创建了一个对应的插件，并且创建了一个表。\n然后可以通过 pageinspect 插件的一些函数查看表所属的 page 的数据信息：\n1 2 3 4 5 postgres=# select * from page_header(get_raw_page(\u0026#39;t\u0026#39;, 0)); lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid ------------+----------+-------+-------+-------+---------+----------+---------+----------- 1/8CA839B0 | 0 | 0 | 824 | 1792 | 8192 | 8192 | 4 | 0 (1 row) get_raw_page 是插件实现的方法，接收两个参数，分别是表名和 page 编号；page_header 方法则可以获取到 page 的 Header 头部信息。\n可以看到获取到的字段和下图的 PageHeader 结构基本一致。\n每个 page 主要由页头、内容、special 三部分组成，大致物理存储结构如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /* * +----------------+---------------------------------+ * | PageHeaderData | linp1 linp2 linp3 ... | * +-----------+----+---------------------------------+ * | ... linpN | | * +-----------+--------------------------------------+ * | ^ pd_lower | * | | * | v pd_upper | * +-------------+------------------------------------+ * | | tupleN ... | * +-------------+------------------+-----------------+ * | ... tuple3 tuple2 tuple1 | \u0026#34;special space\u0026#34; | * +--------------------------------+-----------------+ * ^ pd_special */ typedef struct PageHeaderData { /* XXX LSN is member of *any* block, not only page-organized ones */ PageXLogRecPtr pd_lsn; /* LSN: next byte after last byte of xlog * record for last change to this page */ uint16 pd_checksum; /* checksum */ uint16 pd_flags; /* flag bits, see below */ LocationIndex pd_lower; /* offset to start of free space */ LocationIndex pd_upper; /* offset to end of free space */ LocationIndex pd_special; /* offset to start of special space */ uint16 pd_pagesize_version; TransactionId pd_prune_xid; /* oldest prunable XID, or zero if none */ ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* line pointer array */ } PageHeaderData; 页头部分其实是这个 page 的一些元数据信息，由 PageHeaderData 结构体表示，主要有如下内容：\npd_lsn：xlog（WAL） 在当前 page 的最后一次修改的日志记录\npd_checksum：文件页对应的校验和，保护文件页内容\npd_flags：page 的一些状态信息，取值有如下几种\n#define PD_HAS_FREE_LINES 0x0001 /* are there any unused line pointers? / #define PD_PAGE_FULL 0x0002 / not enough free space for new tuple? / #define PD_ALL_VISIBLE 0x0004 / all tuples on page are visible to * everyone / #define PD_VALID_FLAG_BITS 0x0007 / OR of all valid pd_flags bits */\nPD_HAS_FREE_LINES：是否还有未使用的 linp 指针\nPD_PAGE_FULL：页面已满，剩余的空间无法容纳新的 Tuple\nPD_ALL_VISIBLE：page 所有的 tuple 都是可见的\nPD_VALID_FLAG_BITS：全部有效的 pd_flags 标记位\npd_lower：该 page 内空闲空间的起始位置\npd_upper：该 page 内空闲空间的结束位置\npd_special：存储一些特定的信息，比如 BTree 索引会用到\npd_pagesize_version：存储页面大小和版本信息\npd_prune_xid：page 中可删除的最旧的事务 ID\npd_linp：即前面注释中标注的 linp 1 linp 2 linp 3 \u0026hellip; Linp n，是一个数组，用来标识 page 内一条数据的位置偏移，使用结构体 ItemIdData 表示。\nItemIdData 结构体主要有三个字段：\n1 2 3 4 5 6 typedef struct ItemIdData { unsigned lp_off:15, /* offset to tuple (from start of page) */ lp_flags:2, /* state of line pointer, see below */ lp_len:15; /* byte length of tuple */ } ItemIdData; lp_off 占 15 位，表示数据在 page 的偏移\nlp_flags 占 2 位，表示状态，取值有这几种：\n/* * lp_flags has these possible states. An UNUSED line pointer is available * for immediate re-use, the other states are not. / #define LP_UNUSED 0 / unused (should always have lp_len=0) / #define LP_NORMAL 1 / used (should always have lp_len\u0026gt;0) / #define LP_REDIRECT 2 / HOT redirect (should have lp_len=0) / #define LP_DEAD 3 / dead, may or may not have storage */\nLP_UNUSED：表示此空间空闲，未被使用\nLP_NORMAL：指向实际的偏移位置\nLP_REDIRECT：不指向实际数据，而是一个跳转，指向其他的 ItemIdData，用于 HOT（Heap Only Tuple）\nLP_DEAD：数据已经被删除\nlp_length：数据的长度\n从前面的 page 结构描述中可以得知，一条 Tuple 在插入到 page 当中的时候，是无序的，所以 Postgres 中最常用的表组织方式叫做 Heap，意为杂乱的，无顺序的。\n这种数据组织的方式，其实可以非常高效的读取、插入、删除表中的一行数据，因此 Postgres 的 Heap 表结构其实适用于 OLTP 的场景。\n当读取数据的时候，可以根据 BlockNumber 确定 page 编号，以及页内偏移 OffsetNumber 确定数据在 page 内的位置，使用结构体 ItemPointerData 表示一条数据的物理存储位置。\n1 2 3 4 5 typedef struct ItemPointerData { BlockIdData ip_blkid; OffsetNumber ip_posid; } 参考资料\nhttps://www.postgresql.org/docs/14/storage-page-layout.html\nhttps://www.interdb.jp/pg/pgsql01/03.html\n","date":"2024-06-02T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/postgres-%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-4%E8%A1%A8%E6%96%87%E4%BB%B6-page-%E7%BB%93%E6%9E%84%E6%A6%82%E8%A7%88/","title":"Postgres 源码学习 4—表文件 Page 结构概览"},{"content":"前面一节说到，在 Postgres 的 VFD 机制之上，我们可以避开打开文件数量的系统限制，通过 VFD 可以进行打开、读写、关闭、删除文件等操作，简单来说就是 VFD 为我们提供了一个抽象，屏蔽了操作系统文件描述符的接口，后续我们对文件的 open、close，以及 CRUD 操作都在 VFD 的基础之上。\n文件类型和文件块 要了解 Postgres 的存储管理，需要先对 Postgres 的表文件的组织方式、类型有一个简单的了解。\nPostgres 中的表文件可能会非常大，在物理存储上会将表文件拆分为多个，每一个表文件通过 segno 来区分。\n在 Postgres 的数据目录中，表文件的存储格式为 base/\u0026lt;database oid\u0026gt;/\u0026lt;table relfilenode\u0026gt;。\n在 base 目录下，存储了不同 Database 的数据，例如在我的当前环境中，当前的数据库名为 rose，其 oid 为 24582。\n所以在 Postgres 数据目录的 base 目录下，就会有对应的 Database 目录，目录名称就是 Oid：\n我在当前数据库中有一个表，其名为 articles，我们可以通过 pg_class 表查询到对应的表 relfilenode。\n可以看到表对应的 relfilenode 是 24588，所以可以到 24582 这个数据库目录中，查看所有表数据相关的文件：\nimg\n可以看到表数据文件被分为了\n24588 24588.1 24588.2 24588.3 24588.4 24588.5\n每一个数据文件被称为一个数据段（Segment）文件，符号 . 后就是段号（segno），第 0 个分段文件没有段号。\n除了数据文件，还有 24588_fsm 表示的是表的空闲空间映射文件，记录每个文件的空闲空间大小，24588_vm 即 VisibilityMap 文件，记录的是每个文件页的可见状态。\n在源码中，定义了枚举 ForkNumber 来表示每种文件的类型。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* * Stuff for fork names. * * The physical storage of a relation consists of one or more forks. * The main fork is always created, but in addition to that there can be * additional forks for storing various metadata. ForkNumber is used when * we need to refer to a specific fork in a relation. */ typedef enum ForkNumber { InvalidForkNumber = -1, MAIN_FORKNUM = 0, FSM_FORKNUM, VISIBILITYMAP_FORKNUM, INIT_FORKNUM, /* * NOTE: if you add a new fork, change MAX_FORKNUM and possibly * FORKNAMECHARS below, and update the forkNames array in * src/common/relpath.c */ } ForkNumber; MAIN_FORKNUM：表数据文件 FSM_FORKNUM：空闲空间映射文件 VISIBILITYMAP_FORKNUM：文件页可见性 INIT_FORKNUM：主要用于 UNLOGGED 表\n在分配和读取数据文件的时候，为了效率，一般会以块为单位， 在 Postgres 中默认块大小是 8KB，可以在系统初始化时设置。\n1 2 3 4 5 6 7 8 /* Size of a disk block --this also limits the size of a tuple. You can set it bigger if you need bigger tuples (although TOAST should reduce the need to have large tuples, since fields can be spread across multiple tuples). BLCKSZ must be a power of 2. The maximum possible value of BLCKSZ is currently 2^15 (32768). This is determined by the 15-bit widths of the lp_off and lp_len fields in ItemIdData (see include/storage/itemid.h). Changing BLCKSZ requires an initdb. */ #define BLCKSZ 8192 还有另一个参数是 RELSEG_SIZE，表示一个文件中的最大块数量，使用这个参数和 BLOCKZ，就能够计算出每个数据文件的最大值，目前默认是 1GB（131072 * 8192 / 1024 / 1024 / 1024 = 1GB）。\n1 2 3 4 5 6 7 8 9 10 11 12 /* RELSEG_SIZE is the maximum number of blocks allowed in one disk file. Thus, the maximum size of a single file is RELSEG_SIZE * BLCKSZ; relations bigger than that are divided into multiple files. RELSEG_SIZE * BLCKSZ must be less than your OS\u0026#39; limit on file size. This is often 2 GB or 4GB in a 32-bit operating system, unless you have large file support enabled. By default, we make the limit 1 GB to avoid any possible integer-overflow problems within the OS. A limit smaller than necessary only means we divide a large relation into more chunks than necessary, so it seems best to err in the direction of a small limit. A power-of-2 value is recommended to save a few cycles in md.c, but is not absolutely required. Changing RELSEG_SIZE requires an initdb. */ #define RELSEG_SIZE 131072 存储管理器 在 Postgres 中，在对表文件管理和操作时，提供了存储管理器（SMGR）的抽象，由于历史原因，早期的系统中，可能存在不同的存储系统，比如磁盘（magnetic disk）、索尼光盘（Sony WORM optical disk jukebox）、持久化主存（persistent main memory）等。\n但是目前在操作系统层面，已经提供了文件系统的抽象，所以存储管理器其实已经没有存在的必要了，但是 Postgres 依然选择将其保留，主要是认为这层抽象并没有什么其他的影响。\n所以目前只保留了基于磁盘的存储管理，实际上底层还是调用的操作系统的文件系统。\nimg\n在源代码中也体现的很明显，两个文件，smgr.c 主要是存储管理器，而 md.c 就是磁盘的存储管理。\n存储管理器类似一个顶层抽象，具体的存储管理操作都交给了磁盘存储管理器。\n磁盘存储管理器 在 Postgres 中，一个表会有多个文件，对于表中打开的文件的管理，使用结构体 SMgrRelationData 来表示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 /* * smgr.c maintains a table of SMgrRelation objects, which are essentially * cached file handles. An SMgrRelation is created (if not already present) * by smgropen(), and destroyed by smgrclose(). Note that neither of these * operations imply I/O, they just create or destroy a hashtable entry. * (But smgrclose() may release associated resources, such as OS-level file * descriptors.) * * An SMgrRelation may have an \u0026#34;owner\u0026#34;, which is just a pointer to it from * somewhere else; smgr.c will clear this pointer if the SMgrRelation is * closed. We use this to avoid dangling pointers from relcache to smgr * without having to make the smgr explicitly aware of relcache. There * can\u0026#39;t be more than one \u0026#34;owner\u0026#34; pointer per SMgrRelation, but that\u0026#39;s * all we need. * * SMgrRelations that do not have an \u0026#34;owner\u0026#34; are considered to be transient, * and are deleted at end of transaction. */ typedef struct SMgrRelationData { /* rlocator is the hashtable lookup key, so it must be first! */ RelFileLocatorBackend smgr_rlocator; /* relation physical identifier */ /* pointer to owning pointer, or NULL if none */ struct SMgrRelationData **smgr_owner; /* * The following fields are reset to InvalidBlockNumber upon a cache flush * event, and hold the last known size for each fork. This information is * currently only reliable during recovery, since there is no cache * invalidation for fork extension. */ BlockNumber smgr_targblock; /* current insertion target block */ BlockNumber smgr_cached_nblocks[MAX_FORKNUM + 1]; /* last known size */ /* additional public fields may someday exist here */ /* * Fields below here are intended to be private to smgr.c and its * submodules. Do not touch them from elsewhere. */ int smgr_which; /* storage manager selector */ /* * for md.c; per-fork arrays of the number of open segments * (md_num_open_segs) and the segments themselves (md_seg_fds). */ int md_num_open_segs[MAX_FORKNUM + 1]; struct _MdfdVec *md_seg_fds[MAX_FORKNUM + 1]; /* if unowned, list link in list of all unowned SMgrRelations */ dlist_node node; } SMgrRelationData; 其中比较关键的字段有：\nmd_num_open_segs：记录每个 FORK 类型当前已经打开的文件数量 md_seg_fds：针对每个 FORK 类型打开文件的 VFD 信息\nSMgrRelationData 会使用一个进程私有的哈希表来保存，每次需要打开一个表的文件时，可以首先从这个哈希表中查找，如果找到的话直接返回，这样同一个表只需要维护一个结构体即可，有利于保持对表文件操作的一致性。\n哈希表的 key 是 SMgrRelationData 结构体的第一个属性 smgr_rlocator：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 typedef struct RelFileLocator { Oid spcOid; /* tablespace */ Oid dbOid; /* database */ RelFileNumber relNumber; /* relation */ } RelFileLocator; /* * Augmenting a relfilelocator with the backend ID provides all the information * we need to locate the physical storage. The backend ID is InvalidBackendId * for regular relations (those accessible to more than one backend), or the * owning backend\u0026#39;s ID for backend-local relations. Backend-local relations * are always transient and removed in case of a database crash; they are * never WAL-logged or fsync\u0026#39;d. */ typedef struct RelFileLocatorBackend { RelFileLocator locator; BackendId backend; } RelFileLocatorBackend; smgr_rlocator 的类型是 RelFileLocatorBackend，locator 是一个三元组，分别是 \u0026lt;tablespaceid，database id，relation id\u0026gt;，backend 只对临时表有用，对普通表来说，其值一般是 -1。\n在磁盘存储管理器中，其实主要就是对 SMgrRelationData 的管理，涉及到对其创建、打开、关闭、删除、扩展等操作。\n创建表文件 在函数 mdcreate 中实现，一个数据表会有很多个数据文件，这里的创建表文件指的是传入给定的 tablespace id、database id、relation id，以及文件类型（ForkNumber），创建第一个物理文件，后续其他新的文件会在写入数据的时候动态扩展。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 TablespaceCreateDbspace(reln-\u0026gt;smgr_rlocator.locator.spcOid, reln-\u0026gt;smgr_rlocator.locator.dbOid, isRedo); path = relpath(reln-\u0026gt;smgr_rlocator, forknum); fd = PathNameOpenFile(path, _mdfd_open_flags() | O_CREAT | O_EXCL); if (fd \u0026lt; 0) { int save_errno = errno; if (isRedo) fd = PathNameOpenFile(path, _mdfd_open_flags()); if (fd \u0026lt; 0) { /* be sure to report the error reported by create, not open */ errno = save_errno; ereport(ERROR, (errcode_for_file_access(), errmsg(\u0026#34;could not create file \\\u0026#34;%s\\\u0026#34;: %m\u0026#34;, path))); } } pfree(path); _fdvec_resize(reln, forknum, 1); mdfd = \u0026amp;reln-\u0026gt;md_seg_fds[forknum][0]; mdfd-\u0026gt;mdfd_vfd = fd; mdfd-\u0026gt;mdfd_segno = 0; if (!SmgrIsTemp(reln)) register_dirty_segment(reln, forknum, mdfd); 所以 mdcrete 的逻辑其实比较简单，如上，主要就是创建第一个文件，通过 PathNameOpenFile 方法创建（或打开）文件并获得文件描述符，然后存储到 SMgrRelation 的 md_seg_fds 数组中。\n打开表文件 在函数 smgropen 中，注意这个方法并不会实际去打开文件，只会初始化一些状态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 /* * smgropen() -Return an SMgrRelation object, creating it if need be. * * This does not attempt to actually open the underlying file. */ SMgrRelation smgropen(RelFileLocator rlocator, BackendId backend) { RelFileLocatorBackend brlocator; SMgrRelation reln; bool found; if (SMgrRelationHash == NULL) { /* First time through: initialize the hash table */ HASHCTL ctl; ctl.keysize = sizeof(RelFileLocatorBackend); ctl.entrysize = sizeof(SMgrRelationData); SMgrRelationHash = hash_create(\u0026#34;smgr relation table\u0026#34;, 400, \u0026amp;ctl, HASH_ELEM | HASH_BLOBS); dlist_init(\u0026amp;unowned_relns); } /* Look up or create an entry */ brlocator.locator = rlocator; brlocator.backend = backend; reln = (SMgrRelation) hash_search(SMgrRelationHash, \u0026amp;brlocator, HASH_ENTER, \u0026amp;found); /* Initialize it if not present before */ if (!found) { /* hash_search already filled in the lookup key */ reln-\u0026gt;smgr_owner = NULL; reln-\u0026gt;smgr_targblock = InvalidBlockNumber; for (int i = 0; i \u0026lt;= MAX_FORKNUM; ++i) reln-\u0026gt;smgr_cached_nblocks[i] = InvalidBlockNumber; reln-\u0026gt;smgr_which = 0; /* we only have md.c at present */ /* implementation-specific initialization */ smgrsw[reln-\u0026gt;smgr_which].smgr_open(reln); /* it has no owner yet */ dlist_push_tail(\u0026amp;unowned_relns, \u0026amp;reln-\u0026gt;node); } return reln; } 如果哈希表 SMgrRelationHash 为空，则初始化\n从哈希表中查找表对应的 SMgrRelation 结构\n如果没找到的话，则初始化一个对应的表文件管理结构体（reln） 然后调用磁盘存储管理器（md.c）中的 smgr_open 进行初始化 md 中的 smgr_open 其实也非常简单，只是对 md_num_open_segs 属性进行了初始化。\n1 2 3 4 5 6 7 8 9 10 /* * mdopen() -Initialize newly-opened relation. */ void mdopen(SMgrRelation reln) { /* mark it not open */ for (int forknum = 0; forknum \u0026lt;= MAX_FORKNUM; forknum++) reln-\u0026gt;md_num_open_segs[forknum] = 0; } 关闭表文件 通过函数 mdclose 实现，主要是对每个 Fork 类型的文件，查询 md_seg_fds 中保存的文件描述符信息，然后通过 FileClose 方法关闭文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* * mdclose() -Close the specified relation, if it isn\u0026#39;t closed already. */ void mdclose(SMgrRelation reln, ForkNumber forknum) { int nopensegs = reln-\u0026gt;md_num_open_segs[forknum]; /* No work if already closed */ if (nopensegs == 0) return; /* close segments starting from the end */ while (nopensegs \u0026gt; 0) { MdfdVec *v = \u0026amp;reln-\u0026gt;md_seg_fds[forknum][nopensegs 1]; FileClose(v-\u0026gt;mdfd_vfd); _fdvec_resize(reln, forknum, nopensegs 1); nopensegs--; } } 删除表文件 在 smgr 中，删除表文件的方法是 smgrdounlinkall，主要会将表文件先关闭（mdclose）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* * create an array which contains all relations to be dropped, and close * each relation\u0026#39;s forks at the smgr level while at it */ rlocators = palloc(sizeof(RelFileLocatorBackend) * nrels); for (i = 0; i \u0026lt; nrels; i++) { RelFileLocatorBackend rlocator = rels[i]-\u0026gt;smgr_rlocator; int which = rels[i]-\u0026gt;smgr_which; rlocators[i] = rlocator; /* Close the forks at smgr level */ for (forknum = 0; forknum \u0026lt;= MAX_FORKNUM; forknum++) smgrsw[which].smgr_close(rels[i], forknum); } 然后遍历每个 Fork 类型，删除其中的每个文件。\n1 2 3 4 5 6 7 for (i = 0; i \u0026lt; nrels; i++) { int which = rels[i]-\u0026gt;smgr_which; for (forknum = 0; forknum \u0026lt;= MAX_FORKNUM; forknum++) smgrsw[which].smgr_unlink(rlocators[i], forknum, isRedo); } 实际的删除文件流程都在 md.c 中的 mdunlinkfork 方法，删除表文件的时候，会立即删除除了 MAIN_FORK 第 0 个分段文件（Segment）之外的其他文件。\n对于 MAIN_FORK 的 0 个文件，会将其保留防止其他表重用文件名，并且将文件内容截取（truncate）到 0，然后发送请求给 checkpointer 进程，让其在下一次 checkpoint 之后再实际删除该文件，关键代码如下：\n1 2 3 4 5 6 7 /* Prevent other backends\u0026#39; fds from holding on to the disk space */ ret = do_truncate(path); /* Register request to unlink first segment later */ save_errno = errno; register_unlink_segment(rlocator, forknum, 0 /* first seg */ ); errno = save_errno; 扩展表文件 在对数据表文件进行写入时，如果表文件空间不够了，那么需要扩展表文件的大小，通过函数 mdextend 实现。\n在扩展时，会以 BLOCK 为单位，每次扩展一个或多个 BLOCK。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 v = _mdfd_getseg(reln, forknum, blocknum, skipFsync, EXTENSION_CREATE); seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); Assert(seekpos \u0026lt; (off_t) BLCKSZ * RELSEG_SIZE); if ((nbytes = FileWrite(v-\u0026gt;mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_EXTEND)) != BLCKSZ) { if (nbytes \u0026lt; 0) ereport(ERROR, (errcode_for_file_access(), errmsg(\u0026#34;could not extend file \\\u0026#34;%s\\\u0026#34;: %m\u0026#34;, FilePathName(v-\u0026gt;mdfd_vfd)), errhint(\u0026#34;Check free disk space.\u0026#34;))); /* short write: complain appropriately */ ereport(ERROR, (errcode(ERRCODE_DISK_FULL), errmsg(\u0026#34;could not extend file \\\u0026#34;%s\\\u0026#34;: wrote only %d of %d bytes at block %u\u0026#34;, FilePathName(v-\u0026gt;mdfd_vfd), nbytes, BLCKSZ, blocknum), errhint(\u0026#34;Check free disk space.\u0026#34;))); } if (!skipFsync \u0026amp;\u0026amp; !SmgrIsTemp(reln)) register_dirty_segment(reln, forknum, v); 大致逻辑是会获得对应的文件描述符，然后通过 FileWrite 方法写入 block 数量大小的数据。\n写完之后，会标识该文件是 dirty 的，然后发送请求到 checkpointer 进程（register_dirty_segment），让其在下一次发生 checkpoint 的时候将文件内容落盘。\n读写表文件 读写表文件中的 block 主要是通过 mdread/mdwrite 方法。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /* * mdread() -Read the specified block from a relation. */ void mdread(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, void *buffer) { off_t seekpos; int nbytes; MdfdVec *v; /* If this build supports direct I/O, the buffer must be I/O aligned. */ if (PG_O_DIRECT != 0 \u0026amp;\u0026amp; PG_IO_ALIGN_SIZE \u0026lt;= BLCKSZ) Assert((uintptr_t) buffer == TYPEALIGN(PG_IO_ALIGN_SIZE, buffer)); TRACE_POSTGRESQL_SMGR_MD_READ_START(forknum, blocknum, reln-\u0026gt;smgr_rlocator.locator.spcOid, reln-\u0026gt;smgr_rlocator.locator.dbOid, reln-\u0026gt;smgr_rlocator.locator.relNumber, reln-\u0026gt;smgr_rlocator.backend); v = _mdfd_getseg(reln, forknum, blocknum, false, EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY); seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); Assert(seekpos \u0026lt; (off_t) BLCKSZ * RELSEG_SIZE); nbytes = FileRead(v-\u0026gt;mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_READ); } 读取的时候，会传入表文件的 SMgrRelation 结构体，以及文件类型 forknum 和块号 blocknum，然后通过 FileRead 方法将数据读取到指定的 buffer 中。\nmdwrite 的方法和读取基本类似，主要是将指定的 buffer 内容通过 FileWrite 方法写入到对应的文件中。\n1 2 3 4 5 6 7 8 v = _mdfd_getseg(reln, forknum, blocknum, skipFsync, EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY); seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); Assert(seekpos \u0026lt; (off_t) BLCKSZ * RELSEG_SIZE); nbytes = FileWrite(v-\u0026gt;mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_WRITE); 注意这里写入之后，并不会等待操作系统刷盘之后再返回，而是直接返回，将刷盘的请求交给了 checkpointer 进程，由此来提高数据写入的效率，而数据的崩溃恢复、一致性，是通过 WAL 来保证的。\n","date":"2024-05-26T16:51:56+08:00","permalink":"https://blog.roseduan.cn/p/postgres-%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-3postgres-%E5%AD%98%E5%82%A8%E7%AE%A1%E7%90%86%E5%99%A8/","title":"Postgres 源码学习 3—Postgres 存储管理器"},{"content":"发现一本不错的书籍，名叫《Build Your Own Database From Scratch》，也就是从零实现一个你自己的 SQL 数据库，书中有完整的代码演示，用 Go 语言实现。\n大致看了下这本书，感觉还是非常不错的，内容主要分为了两个部分。\n实现磁盘 B+ 树 第一部分实际上是去构建一个基于磁盘的 KV 存储引擎，这里是使用的磁盘 B+ 树作为数据存储和组织的方式。 针对这一部分，也是循序渐进，分为了几个不同的 part，分别涉及到 B+ Tree 的大致结构，节点的插入、删除，以及数据查询，空闲列表等。\n书中有完整的代码演示和丰富的图例，帮助理解。\nKV 之上的 SQL 数据库 第二部分，是在前面实现的 B+ 树 KV 存储引擎之上，去构建一个迷你的 SQL 数据库，当然支持的语法比较有限，只涉及到简单的一些 CRUD 的语法。\n并且在 KV 之上支持了事务的特性，然后对数据表进行解析、存储，总体来说是一个比较完整的资料。 当然，唯一的缺点是这本书是英文的，没有中文版，但书中都是使用了一些专业性强的术语，并没有太多生僻的词句，理解起来应该不难。\n这种类型的资料其实对学习来说就已经非常不错了，比一些不成系统的资料、博眼球的垃圾文章要好很多。 但是看起来可能非常的枯燥，并且没有人解答疑惑的话，也比较难以坚持下去。\n这里推荐一下我的教程《从零实现 KV 存储》，从第一行代码开始，实现一个完整的 KV 存储引擎，并且兼容 Redis 数据结构和协议，有详细的视频讲解和代码演示。 课程详情可以看这里：https://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g\n付费教程最大的好处是，可以和志同道合的朋友一起学习，互相监督，共同进步，并且我专门负责答疑，在学习的过程当中，有任何疑问都可以随时咨询，能够确保你能够完全学懂！\n最近有很多同学，给我反馈，通过《从零实现 KV 存储》课程项目丰富了自己的简历，拿到了非常不错的 offer，在就业环境不佳的情况下，我们唯一能做的就是多学习充实自己，拉开和别人的差距！\n","date":"2024-05-24T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E6%8E%A8%E8%8D%90%E4%B8%80%E6%9C%AC%E6%89%8B%E5%86%99%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E4%B9%A6%E7%B1%8D/","title":"推荐一本手写数据库的书籍"},{"content":" 完整代码：https://github.com/rosedblabs/rust-practice/tree/main/mvcc\n事务及 MVCC 数据库的事务是一个经久不衰的话题，相信大家都已经耳熟能详了，事务是指数据库中单一逻辑工作单元的操作集合。这些操作要么全部成功执行，要么全部失败，从而确保数据库的一致性和完整性。\n事务有 ACID 四个特性，分别是：\n原子性（Atomicity）：事务应该被视为不可分割的最小执行单元。这意味着事务中的所有操作要么全部完成，要么全部失败回滚。如果事务中的任何一部分操作失败，整个事务将会被回滚到初始状态，不会留下部分执行的结果。 一致性（Consistency）：事务在执行前后，数据库应该保持一致性状态，不会破坏数据库的完整性。 隔离性（Isolation）：事务的执行应该相互隔离，使得每个事务感觉自己在操作数据库时是独立的。这意味着并发执行的事务之间不应该相互影响，即使它们同时访问相同的数据。这样可以防止并发执行时出现数据不一致或者丢失的问题。 持久性（Durability）：一旦事务被提交，其结果应该是永久性的。即使系统崩溃或发生故障，已经提交的事务所做的改变也应该被永久保存在数据库中，不会丢失。 其中原子性一般是通过预写日志来保证，持久性是通过预写日志和存储管理完成，隔离性常用的方式有多版本并发控制（MVCC）、两阶段锁等等。\n隔离性是这其中稍微复杂的，其他的几个特性其实都不难理解，这篇文章将会使用 Rust 代码，实现一个最基础的 MVCC 事务，让一些重要概念不仅仅存在于理论层面，而是让大家上手实践，这样才能够加深理解并完全掌握。\n我们知道数据库的隔离性其实又分为了四种，分别是\n读未提交（Read Uncommitted） 读提交（Read Committed） 可重复读（Repeatable Read） 串行化（Serializable） 总体来说 MVCC 是实现事务隔离性的手段，通过 MVCC 可以很方便的实现可重复读。\nMVCC 是建立在数据多版本的基础之上的，当写入一条数据的时候，会带上一个版本号，这个版本号一般是事务的唯一标识，修改数据的时候，不会直接原地去修改数据，而是新增一条新的数据，并且带上一个新的版本号。\n这样一条数据实际上就会在物理存储上存在多个版本，当读取数据的时候，会找到第一个满足条件的数据并返回。\n这样的好处是：读不会阻塞写，写也不会阻塞读， 最大限度提升了数据库的并发性能。\n如下所示，横轴表示数据库中的 key，竖轴表示事务开始的时间顺序。\n1 2 3 4 5 6 7 Time 5 4 a4 3 b3 x 2 1 a1 c1 d1 a b c d Keys 事务 T2 启动的时候，它看到的值是 a=a1，c=c1，d=d1。\n随后又有几个事务修改了 a（将 a1 改为 a4），新增了 b=b3，并且删除了 d。\n这时候事务 T5 开始，它看到的值是 a=a4，b=b3，c=c1。\n可以看到对数据的修改并不是原地的，而是新增加一个版本的数据，删除数据的时候，其实也不是真正的将其删除掉，而是通过标记的方式。\n接下来通过具体的代码来进行更进一步的了解。\n需要说明的是，出于演示的便利性，代码全部使用了基于内存的数据结构，所以严格意义上来说并不满足事务的原子性、持久性，但这并不妨碍我们去理解 MVCC 的精髓。\n事务定义 事务的定义使用一个单独的结构体来表示，分别包含了存储数据的底层 KV，事务的版本号，以及活跃事务列表。\nactive_xid 是一个事务私有的数据结构，保存了在这个事务启动时，其他活跃的（未提交）事务集合，这是实现可重复读的关键，保证就算其他事务提交了，也不会影响到当前的事务。\n1 2 3 4 5 6 7 8 9 // MVCC 事务 pub struct Transaction { // 底层 KV 存储引擎 kv: Arc\u0026lt;Mutex\u0026lt;KVEngine\u0026gt;\u0026gt;, // 事务版本号 version: u64, // 事务启动时的活跃事务列表 active_xid: HashSet\u0026lt;u64\u0026gt;, } 再来看一下事务的具体操作。\n开启事务 首先是开启事务，这里是申请一个全局唯一的事务版本号，并且保存当前活跃事务列表。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 开启事务 pub fn begin(kv: Arc\u0026lt;Mutex\u0026lt;KVEngine\u0026gt;\u0026gt;) -\u0026gt; Self { // 获取全局事务版本号 let version = acquire_next_version(); let mut active_txn = ACTIVE_TXN.lock().unwrap(); // 这个 map 的 key 就是当前所有活跃的事务 let active_xid = active_txn.keys().cloned().collect(); // 添加到当前活跃事务 id 列表中 active_txn.insert(version, vec![]); // 返回结果 Self { kv, version, active_xid, } } 写入数据 然后是写入数据，需要判断当前写入的 key 是否和其他的事务发生了冲突，如果是的话，则需要返回错误，通知调用者进行重试。\n这里判断冲突主要的逻辑是扫描最后一个 key 及其对应的版本，并判断其可见性：\n如果最后一个 key 可见，说明是当前事务自己写入的，或者是比自己更早的已经提交的事务的写入的，可见。 如果 key 的版本号存在于活跃事务列表中（其他事务修改了，并且没提交），或者版本号比自身大（有新的事务修改了数据并提交），说明存在并发写入冲突。 如果没有冲突，则写入数据，也分为了两个步骤，一是记录了当前 version 写入了哪些 key（主要保证回滚时可以将数据删除掉），二是实际向存储引擎写入对应的数据。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 fn write(\u0026amp;self, key: \u0026amp;[u8], value: Option\u0026lt;Vec\u0026lt;u8\u0026gt;\u0026gt;) { // 判断当前写入的 key 是否和其他的事务冲突 // key 是按照 key-version 排序的，所以只需要判断最近的一个 key 即可 let mut kvengine = self.kv.lock().unwrap(); for (enc_key, _) in kvengine.iter().rev() { let key_version = decode_key(enc_key); if key_version.raw_key.eq(key) { if !self.is_visible(key_version.version) { panic!(\u0026#34;serialization error, try again.\u0026#34;); } break; } } // 写入 TxnWrite let mut active_txn = ACTIVE_TXN.lock().unwrap(); active_txn .entry(self.version) .and_modify(|keys| keys.push(key.to_vec())) .or_insert_with(|| vec![key.to_vec()]); // 写入数据 let enc_key = Key { raw_key: key.to_vec(), version: self.version, }; kvengine.insert(enc_key.encode(), value); } 可以看到写入数据的时候，需要将原始的 key 加上当前版本号。\n提交事务 提交事务的逻辑比较简单，因为所有需要写入的数据实际上都已经写入完成了，这时候只需要将这个事务的版本号从全局的活跃事务列表中删除就可以了。\n1 2 3 4 5 6 // 提交事务 pub fn commit(\u0026amp;self) { // 清除活跃事务列表中的数据 let mut active_txn = ACTIVE_TXN.lock().unwrap(); active_txn.remove(\u0026amp;self.version); } 这样后续新开启的事务可以看到这个事务的修改，而其他未提交的事务，仍然看不到这个事务的修改。\n回滚事务 回滚事务的逻辑和提交事务有些类似，唯一的区别是，需要将写入的数据重新删除掉。\n这样做的目的是确保事务的一致性，因为事务回滚之后，原来已经写入的数据不能对后续的事务可见，所以需要全部删除掉。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 回滚事务 pub fn rollback(\u0026amp;self) { // 清除写入的数据 let mut active_txn = ACTIVE_TXN.lock().unwrap(); if let Some(keys) = active_txn.get(\u0026amp;self.version) { let mut kvengine = self.kv.lock().unwrap(); for k in keys { let enc_key = Key { raw_key: k.to_vec(), version: self.version, }; let res = kvengine.remove(\u0026amp;enc_key.encode()); assert!(res.is_some()); } } // 清除活跃事务列表中的数据 active_txn.remove(\u0026amp;self.version); } 读取数据 读取数据的逻辑比较简单，因为数据 key 是按照 version 排序的，所以只需要从后往前遍历，找到第一个可见的记录，就是当前事务能够得到的对应 key 的结果。\n需要说明的是，这里的处理逻辑稍微有点冗余，因为直接遍历了整个 map 去查找对应的 key，但实际上因为我们写入的 key 都是有序的，所以只需要前缀遍历即可。\n1 2 3 4 5 6 7 8 9 10 11 // 读取数据，从最后一条数据进行遍历，找到第一条可见的数据 pub fn get(\u0026amp;self, key: \u0026amp;[u8]) -\u0026gt; Option\u0026lt;Vec\u0026lt;u8\u0026gt;\u0026gt; { let kvengine = self.kv.lock().unwrap(); for (k, v) in kvengine.iter().rev() { let key_version = decode_key(k); if key_version.raw_key.eq(key) \u0026amp;\u0026amp; self.is_visible(key_version.version) { return v.clone(); } } None } 总结 MVCC 的核心是“多版本”，即同一个 key 在写入、更新、查询的时候，都会和当前版本号相关，通过事务的版本号来判断数据的可见性。\n并且存储的时候，同一个 key 在物理上会有多份，分别对应不同的版本，本质上是一种通过空间冗余的方式来提升并发性能，让每一个事务都“看起来像”是单独执行，不受其他并发事务的影响，做到了读写互不阻塞。\n通过上面的理论知识讲解和代码实战，相信你能够对 MVCC 理论有了一个更加深入的理解了。\n完整代码地址：https://github.com/rosedblabs/rust-practice/tree/main/mvcc\n","date":"2024-05-12T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rust-%E7%BB%83%E6%89%8B%E9%A1%B9%E7%9B%AE-3-%E5%AE%9E%E7%8E%B0-mvcc-%E5%A4%9A%E7%89%88%E6%9C%AC%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6/","title":"Rust 练手项目 3 - 实现 MVCC 多版本并发控制"},{"content":" 本文完整代码：https://github.com/rosedblabs/rust-practice\n表达式解析、计算是一种基本和常见的任务，例如最常见的算术表达式，计算的方法有很多，比如逆波兰表达式、LL、LR 算法等等。\n这一次介绍一种最简单的、容易理解的基于运算符优先级的算法来完成这个任务。\n基于运算符优先级的算法叫做 Precedence Climbing，它本质上是一种递归下降解析表达式的方法，通过递归地处理运算符和操作数来解析表达式，并根据运算符的优先级和结合性来确定表达式的计算顺序。\n这种算法的核心思想是利用运算符的优先级进行“爬升”（Climbing），以决定表达式的结构和计算顺序。\n首先我们做一些约束，由于运算符众多，我们可以支持几种最常用的：\n+ 加 - 减 * 乘 / 除 ^ 幂 并且我们知道，幂运算的优先级是最高的，其次是 * 和 /，优先级最低的是 + 和 -。\n所以约定其运算符的优先级分别为 3（^）、2（* /）、1（+ -）\n1 2 3 4 5 2 + 3 ^ 2 * 3 + 4 |---------------| : prec 1 |-------| : prec 2 |---| : prec 3 约定优先级的主要作用是在计算的时候，需要根据优先级来确定计算的顺序。\n确定了优先级的问题，第二个问题是结合性，运算符的结合性其实也是确定的，例如加法是左结合的，这意味着 2 + 3 + 4 等价于 (2 + 3) + 4，而幂运算是右结合的，这意味着 2 ^ 3 ^ 4 实际上等价于 2 ^ (3 ^ 4)。\n最后还需要注意一个问题，那就是子表达式，也就是用括号包裹的部分，这部分实际上是需要单独进行计算的，并且比运算符的优先级更高。\n其实也很容易理解，比如 2 * (3 + 5) * 7，尽管 * 的优先级比 + 高，但是需要先计算括号内的部分。\n确定了这些需求，我们再来看如何用 Rust 代码来进行实现。\n首先我们需要将表达式进行解析，也就是词法分析的阶段，将一个表达式解析为不同的 Token，下面是约定的几种 Token：\n1 2 3 4 5 6 7 8 9 10 11 12 // Token 表示，数字、运算符号、括号 #[derive(Debug, Clone, Copy)] enum Token { Number(i32), Plus, // 加 Minus, // 减 Multiply, // 乘 Divide, // 除 Power, // 幂 LeftParen, // 左括号 RightParen, // 右括号 } 然后定义了一个 Tokenizer 结构体，主要是利用 Peekable 接口将表达式解析为不同的 Token：\n1 2 3 4 5 // 将一个算术表达式解析成连续的 Token // 并通过 Iterator 返回，也可以通过 Peekable 接口获取 struct Tokenizer\u0026lt;\u0026#39;a\u0026gt; { tokens: Peekable\u0026lt;Chars\u0026lt;\u0026#39;a\u0026gt;\u0026gt;, } 然后自定义实现了一个 Iterator，让解析后的 Token 可以通过迭代器进行返回。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 impl\u0026lt;\u0026#39;a\u0026gt; Iterator for Tokenizer\u0026lt;\u0026#39;a\u0026gt; { type Item = Token; fn next(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt; { // 消除前面的空格 self.consume_whitespace(); // 解析当前位置的 Token 类型 match self.tokens.peek() { Some(c) if c.is_numeric() =\u0026gt; self.scan_number(), Some(_) =\u0026gt; self.scan_operator(), None =\u0026gt; return None, } } } 假如我们的表达式是 2 + 3 ^ 2 * 3 + 4，实际上解析后的 Token 就是：\n1 2 3 4 5 6 7 8 9 Token::Number(2) Token::Plus Token::Number(3) Token::Power Token::Number(2) Token::Multiply Token::Number(3) Token::Plus Token::Number(4) 拿到 Token 之后，进入到了语法分析的阶段，需要根据每个表达式的含义，以及其优先级，计算对应的结果。\n首先定义一个方法，计算单个 Token 以及子表达式，这只存在两种情况，分别是 Number 这个 Token，以及带括号的子表达式。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 fn compute_atom(\u0026amp;mut self) -\u0026gt; Result\u0026lt;i32\u0026gt; { match self.iter.peek() { // 如果是数字的话，直接返回 Some(Token::Number(n)) =\u0026gt; { let val = *n; self.iter.next(); return Ok(val); } // 如果是左括号的话，递归计算括号内的值 Some(Token::LeftParen) =\u0026gt; { self.iter.next(); let result = self.compute_expr(1)?; match self.iter.next() { Some(Token::RightParen) =\u0026gt; (), _ =\u0026gt; return Err(ExprError::Parse(\u0026#34;Unexpected character\u0026#34;.into())), } return Ok(result); } _ =\u0026gt; { return Err(ExprError::Parse( \u0026#34;Expecting a number or left parenthesis\u0026#34;.into(), )) } } } 这里其实比较好理解，如果是 Number 直接返回，如果是子表达式，则重新调用计算表达式的方法进行计算。\n然后是另一个核心的方法计算表达式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 fn compute_expr(\u0026amp;mut self, min_prec: i32) -\u0026gt; Result\u0026lt;i32\u0026gt; { // 计算第一个 Token let mut atom_lhs = self.compute_atom()?; loop { let cur_token = self.iter.peek(); if cur_token.is_none() { break; } let token = *cur_token.unwrap(); // 1. Token 一定是运算符 // 2. Token 的优先级必须大于等于 min_prec if !token.is_operator() || token.precedence() \u0026lt; min_prec { break; } let mut next_prec = token.precedence(); if token.assoc() == ASSOC_LEFT { next_prec += 1; } self.iter.next(); // 递归计算右边的表达式 let atom_rhs = self.compute_expr(next_prec)?; // 得到了两边的值，进行计算 match token.compute(atom_lhs, atom_rhs) { Some(res) =\u0026gt; atom_lhs = res, None =\u0026gt; return Err(ExprError::Parse(\u0026#34;Unexpected expr\u0026#34;.into())), } } Ok(atom_lhs) } 这个方法中核心的逻辑可以分几个步骤来理解：\n一是使用了 min_prec 参数控制当前层级的优先级，如果表达式的优先级小于 min_prec 则直接跳出循环，返回当前的值。\n比如 2 * 3 + 4，* 会先解析到，然后 + 运算符的优先级明显比 * 更低，会直接返回当前值 3。\n二是如果运算符的结合性是左边的话，则下一次迭代的 min_prec 需要递增。\n比如表达式是 2 * 3 * 4，解析到第二个 * 的时候，* 的优先级本来是 2，但它是左结合的，所以此时 min_prec 是 3，会直接跳出循环，所以实际上会先计算 2 * 3。\n最后是得到了运算符两边的值，就可以进行计算了，这里是根据运算符的实际含义来进行的：\n1 2 3 4 5 6 7 8 9 10 11 // 根据当前运算符进行计算 fn compute(\u0026amp;self, l: i32, r: i32) -\u0026gt; Option\u0026lt;i32\u0026gt; { match self { Token::Plus =\u0026gt; Some(l + r), Token::Minus =\u0026gt; Some(l - r), Token::Multiply =\u0026gt; Some(l * r), Token::Divide =\u0026gt; Some(l / r), Token::Power =\u0026gt; Some(l.pow(r as u32)), _ =\u0026gt; None, } } 这就是根据运算符优先级来进行表达式计算的整体流程，这个算法看起来还是非常简洁优雅的，非常巧妙的利用优先级来解决运算的顺序和结合等问题。\n完整的代码也只有 200 多行，比较适合用来练手，通过这个项目，可以学习到：\n一个优雅、简洁的表达式计算的算法 解决类似写一个计算器的面试问题 Rust 基础数据类型、枚举、结构体基本用法 函数、递归 match 表达式 自定义 Result 错误处理 迭代器的常见用法 next、peekable 等 自定义迭代器 Option 使用 最后附上项目地址： https://github.com/rosedblabs/rust-practice 对你有帮助的话，欢迎给个 star ⭐️ 哦！\n","date":"2024-05-02T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rust-%E7%BB%83%E6%89%8B%E9%A1%B9%E7%9B%AE-2-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E8%AE%A1%E7%AE%97/","title":"Rust 练手项目 2 - 表达式计算"},{"content":"之前写过一个 Go 语言的 mini-bitcask，实现了一个基于 bitcask 存储模型的极简 KV 存储引擎。 可以结合之前的文章食用：https://mp.weixin.qq.com/s/s8s6VtqwdyjthR6EtuhnUA\n这次重新用 Rust 实现了一个版本，代码量和之前的差不多，包含了常用的方法，例如 Set、Get、Delete、Scan、PrefixScan、Merge。\n项目地址：https://github.com/rosedblabs/rust-practice/tree/main/mini-bitcask-rs\nSet\n1 2 3 4 5 6 7 8 9 pub fn set(\u0026amp;mut self, key: \u0026amp;[u8], value: Vec\u0026lt;u8\u0026gt;) -\u0026gt; Result\u0026lt;()\u0026gt; { let (offset, len) = self.log.write_entry(key, Some(\u0026amp;value))?; let value_len = value.len() as u32; self.keydir.insert( key.to_vec(), (offset + len as u64 - value_len as u64, value_len), ); Ok(()) } Set 逻辑比较直观简洁，写入磁盘日志，并且更新内存索引结构。\nGet\nGet 则是先从内存中获取索引，再从磁盘中获取 Value。\n1 2 3 4 5 6 7 8 pub fn get(\u0026amp;mut self, key: \u0026amp;[u8]) -\u0026gt; Result\u0026lt;Option\u0026lt;Vec\u0026lt;u8\u0026gt;\u0026gt;\u0026gt; { if let Some((value_pos, value_len)) = self.keydir.get(key) { let val = self.log.read_value(*value_pos, *value_len)?; Ok(Some(val)) } else { Ok(None) } } Delete\ndelete 的逻辑和 Set 类似，只是写入了一个空的值，并且从内存中对应的 key 移除。\n1 2 3 4 5 pub fn delete(\u0026amp;mut self, key: \u0026amp;[u8]) -\u0026gt; Result\u0026lt;()\u0026gt; { self.log.write_entry(key, None)?; self.keydir.remove(key); Ok(()) } Scan\nscan 功能主要借助了 Rust 自带的内存数据结构 BTreeMap 的迭代器进行实现，非常简洁和方便。\n1 2 3 4 5 6 7 8 9 10 11 12 13 impl\u0026lt;\u0026#39;a\u0026gt; Iterator for ScanIterator\u0026lt;\u0026#39;a\u0026gt; { type Item = Result\u0026lt;(Vec\u0026lt;u8\u0026gt;, Vec\u0026lt;u8\u0026gt;)\u0026gt;; fn next(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt; { self.inner.next().map(|item| self.map(item)) } } impl\u0026lt;\u0026#39;a\u0026gt; DoubleEndedIterator for ScanIterator\u0026lt;\u0026#39;a\u0026gt; { fn next_back(\u0026amp;mut self) -\u0026gt; Option\u0026lt;Self::Item\u0026gt; { self.inner.next_back().map(|item| self.map(item)) } } Merge\nmerge 的逻辑其实也比较简单，将内存中的数据全部重写，并且替换旧的文件即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 pub fn merge(\u0026amp;mut self) -\u0026gt; Result\u0026lt;()\u0026gt; { // 创建一个新的临时用于用于写入 let mut merge_path = self.log.path.clone(); merge_path.set_extension(MERGE_FILE_EXT); let mut new_log = Log::new(merge_path)?; let mut new_keydir = KeyDir::new(); // 重写数据 for (key, (value_pos, value_len)) in self.keydir.iter() { let value = self.log.read_value(*value_pos, *value_len)?; let (offset, len) = new_log.write_entry(key, Some(\u0026amp;value))?; new_keydir.insert( key.clone(), (offset + len as u64 - *value_len as u64, *value_len), ); } // 重写完成，重命名文件 std::fs::rename(new_log.path, self.log.path.clone())?; new_log.path = self.log.path.clone(); // 替换现在的 self.log = new_log; self.keydir = new_keydir; Ok(()) } 通过这个简单的项目，可以学习到 Rust 的大多数基础语法，例如：\n数据类型，数组、整型等 match 表达式 函数 结构体 错误处理 迭代器 Iterator 和 DoubleEndedIterator 文件读写操作 BufWriter 和 BufReader 单元测试撰写 项目地址：https://github.com/rosedblabs/rust-practice/tree/main/mini-bitcask-rs\n觉得有帮助的话请不用吝啬你的 Star ⭐️ 哦！\n","date":"2024-04-29T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/rust-%E7%BB%83%E6%89%8B%E9%A1%B9%E7%9B%AE-1-mini-bitcask/","title":"Rust 练手项目 1 - mini-bitcask"},{"content":"操作系统中的文件 数据库的本质其实就是用来存储数据的，所以免不了和文件系统、存储进行交互，万丈高楼平地起，存储一般是一个数据库的最底层，Postgres 在存储的文件管理方面也有很多的设计与抽象。\n在操作系统层面，提供了一些文件操作相关的系统调用（fopen、fclose、fsync 等），我们作为上层使用者，可以直接通过 C 语言库进行调用即可（Postgres 使用 C 语言编写）。\n具体和文件系统的交互我们并不关心，操作系统打开文件之后，会在进程的控制块中维护一些打开文件的相关信息，并返回一个文件描述符，后续我们与文件的交互都通过文件描述符进行。\n操作系统能够打开多少文件，是有限制的，一个是系统级限制，指的是在内核中可以打开多少文件，可以通过命令 sysctl fs.file-max 查看。另一个是用户级限制，为了不让某个进程打开太多的文件，进而消耗所有的资源，对单个进程能打开文件也有限制，可以通过 ulimit -n 命令查看。\nPostgres 的 VFD 作用 Postgres 数据库在运行的过程当中，可能会打开非常多的文件，比如数据表对应的文件，元数据表文件，以及一些在 SQL 运行时打开的临时文件，例如排序、哈希表所需的文件。\n所以有非常大的概率超过单个进程打开文件数量的限制，为了解决这个问题，Postgres 设计了 VFD（虚拟文件描述符）机制，主要是将实际的操作系统文件描述符维护到一个 LRU 缓存中，通过切换打开的方式，规避了进程打开文件数量的限制。\n如果一个进程打开的文件数目达到了限制，则暂时关闭最久未使用的文件，保存其状态，待下次重新打开。\nVFD 的基本工作方式 Postgres 主要通过一个进程私有的数组来维护 VFD，名为 VfdCache。\n1 2 3 4 5 6 /* * Virtual File Descriptor array pointer and size. This grows as * needed. \u0026#39;File\u0026#39; values are indexes into this array. * Note that VfdCache[0] is not a usable VFD, just a list header. */ static Vfd *VfdCache; VfdCache 数组的第一个元素不存储任何数据，仅作为头部使用，下面是 vfdCache 的初始化逻辑，会在 backend 进程启动的时候调用，大致的逻辑就是为 VfdCache 数组分配内存。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /* * InitFileAccess --- initialize this module during backend startup * * This is called during either normal or standalone backend start. * It is *not* called in the postmaster. * * Note that this does not initialize temporary file access, that is * separately initialized via InitTemporaryFileAccess(). */ void InitFileAccess(void) { Assert(SizeVfdCache == 0); /* call me only once */ /* initialize cache header entry */ VfdCache = (Vfd *) malloc(sizeof(Vfd)); if (VfdCache == NULL) ereport(FATAL, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg(\u0026#34;out of memory\u0026#34;))); MemSet((char *) \u0026amp;(VfdCache[0]), 0, sizeof(Vfd)); VfdCache-\u0026gt;fd = VFD_CLOSED; SizeVfdCache = 1; } 如果需要打开一个文件，那么会首先在 VfdCache 数组中查找空闲的虚拟文件描述符，主要是通过 nextFree 指针进行查找，如果当前没有空闲的 vfd 了，那么会启动扩容机制，初始情况下，VfdCache size 是 32，每次扩容为原来的 2 倍。\nVfd 扩容和分配的逻辑都在方法 AllocateVfd 中。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 static File AllocateVfd(void) { Index i; File file; DO_DB(elog(LOG, \u0026#34;AllocateVfd. Size %zu\u0026#34;, SizeVfdCache)); Assert(SizeVfdCache \u0026gt; 0); /* InitFileAccess not called? */ if (VfdCache[0].nextFree == 0) { /* * The free list is empty so it is time to increase the size of the * array. We choose to double it each time this happens. However, * there\u0026#39;s not much point in starting *real* small. */ Size newCacheSize = SizeVfdCache * 2; Vfd *newVfdCache; if (newCacheSize \u0026lt; 32) newCacheSize = 32; /* * Be careful not to clobber VfdCache ptr if realloc fails. */ newVfdCache = (Vfd *) realloc(VfdCache, sizeof(Vfd) * newCacheSize); if (newVfdCache == NULL) ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg(\u0026#34;out of memory\u0026#34;))); VfdCache = newVfdCache; /* * Initialize the new entries and link them into the free list. */ for (i = SizeVfdCache; i \u0026lt; newCacheSize; i++) { MemSet((char *) \u0026amp;(VfdCache[i]), 0, sizeof(Vfd)); VfdCache[i].nextFree = i + 1; VfdCache[i].fd = VFD_CLOSED; } VfdCache[newCacheSize - 1].nextFree = 0; VfdCache[0].nextFree = SizeVfdCache; /* * Record the new size */ SizeVfdCache = newCacheSize; } file = VfdCache[0].nextFree; VfdCache[0].nextFree = VfdCache[file].nextFree; return file; } 拿到虚拟文件描述符之后，会调用 C 库函数 open 实际去打开文件，并且将一些文件状态维护到 Vfd 结构体中，这个结构体主要存储的是虚拟文件描述符的一些信息，也就是存储到 VfdCache 数组中的结构。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct vfd { int fd; /* current FD, or VFD_CLOSED if none */ unsigned short fdstate; /* bitflags for VFD\u0026#39;s state */ ResourceOwner resowner; /* owner, for automatic cleanup */ File nextFree; /* link to next free VFD, if in freelist */ File lruMoreRecently; /* doubly linked recency-of-use list */ File lruLessRecently; off_t fileSize; /* current size of file (0 if not temporary) */ char *fileName; /* name of file, or NULL for unused VFD */ /* NB: fileName is malloc\u0026#39;d, and must be free\u0026#39;d when closing the VFD */ int fileFlags; /* open(2) flags for (re)opening the file */ mode_t fileMode; /* mode to pass to open(2) */ } Vfd; Vfd 结构体中，主要通过 nextFree、lruMoreRecently、lruLessRecently 指针将 vfd 维护到不同的队列里面。\n每次新打开一个文件，都会将该 vfd 通过 lruMoreRecently 和 lruLessRecently 指针，维护这个双向链表，每次关闭一个 VfdCache 中的文件，都会将其从链表中删除。\n每次查找空闲的 VfdCache 的时候，都会通过 nextFree 链表进行查找。\n以访问文件为例，首先会判断文件是否打开，如果没有打开的话，则打开文件并且将其放到最近使用的链表中。\n主要的逻辑在函数 LruInsert 中，在实际打开文件之前，会尝试关闭最久未使用的文件。\n然后会通过系统调用打开文件，并且获取到实际的文件描述符（fd），将其保存到 vfdP 结构中。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 static int LruInsert(File file) { Vfd *vfdP; Assert(file != 0); DO_DB(elog(LOG, \u0026#34;LruInsert %d (%s)\u0026#34;, file, VfdCache[file].fileName)); vfdP = \u0026amp;VfdCache[file]; if (FileIsNotOpen(file)) { /* Close excess kernel FDs. */ ReleaseLruFiles(); /* * The open could still fail for lack of file descriptors, eg due to * overall system file table being full. So, be prepared to release * another FD if necessary... */ vfdP-\u0026gt;fd = BasicOpenFilePerm(vfdP-\u0026gt;fileName, vfdP-\u0026gt;fileFlags, vfdP-\u0026gt;fileMode); if (vfdP-\u0026gt;fd \u0026lt; 0) { DO_DB(elog(LOG, \u0026#34;re-open failed: %m\u0026#34;)); return -1; } else { ++nfile; } } /* * put it at the head of the Lru ring */ Insert(file); return 0; } 如果文件已经是打开状态，那么会先从链表中删除，然后将其插入到最近使用的链表中。将 Vfd 加入到链表中，代码如下，可以看到主要是通过维护 lruMoreRecently 和 lruLessRecently 这两个指针，将当前 vfd 加入到链表的头部。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void Insert(File file) { Vfd *vfdP; Assert(file != 0); DO_DB(elog(LOG, \u0026#34;Insert %d (%s)\u0026#34;, file, VfdCache[file].fileName)); DO_DB(_dump_lru()); vfdP = \u0026amp;VfdCache[file]; vfdP-\u0026gt;lruMoreRecently = 0; vfdP-\u0026gt;lruLessRecently = VfdCache[0].lruLessRecently; VfdCache[0].lruLessRecently = file; VfdCache[vfdP-\u0026gt;lruLessRecently].lruMoreRecently = file; DO_DB(_dump_lru()); } 而 Delete 方法则描述的是将一个 vfd 从链表中删除。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static void Delete(File file) { Vfd *vfdP; Assert(file != 0); DO_DB(elog(LOG, \u0026#34;Delete %d (%s)\u0026#34;, file, VfdCache[file].fileName)); DO_DB(_dump_lru()); vfdP = \u0026amp;VfdCache[file]; VfdCache[vfdP-\u0026gt;lruLessRecently].lruMoreRecently = vfdP-\u0026gt;lruMoreRecently; VfdCache[vfdP-\u0026gt;lruMoreRecently].lruLessRecently = vfdP-\u0026gt;lruLessRecently; DO_DB(_dump_lru()); } 小结 Postgres 中的 VFD，即虚拟文件描述符，主要是为了能够规避操作系统中最大打开文件数的限制，采用切换打开的方式，维护了一个链表，将最近打开的文件维护到链表头部，最久未使用的文件放置到链表尾部。\n访问文件的时候，会从 VfdCache 数组中查找空闲的虚拟文件描述符，如果找到的话，则直接使用，否则分配新的 VfdCache 空间。\n在打开文件的时候，会尝试关闭最久未使用的文件，将位置留给最新打开的文件。\n通过这种方式，Postgres 可以打开远超过系统和进程限制的文件数量，是一个非常精妙的设计。\n","date":"2024-04-18T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/postgres-%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-2postgres-%E7%9A%84-vfd-%E6%9C%BA%E5%88%B6/","title":"Postgres 源码学习 2—Postgres 的 VFD 机制"},{"content":"docker 环境 这里我使用了一个纯净的 Ubuntu 环境来进行演示，为了方便，使用了 docker。\n如果你有其他的物理机，或者云服务器，都是可以的，Postgres 支持多种平台编译，如果你是非 Ubuntu 环境，可以自行查阅相关的资料进行编译安装，步骤都是大同小异的。\n我使用了 Ubuntu 20.04 版本的镜像作为演示：\n使用镜像启动容器：\n1 docker run -itd --name \u0026lt;container-name\u0026gt; --privileged \u0026lt;image id\u0026gt; 进入环境：\n1 docker exec -it \u0026lt;container-name | container id\u0026gt; /bin/bash 创建用户 最好不要在 root 用户下编译和安装 Postgres，这里我们可以新建一个用户\n1 useradd \u0026lt;username\u0026gt; -m -s /bin/bash 切换到新的用户环境中。\n1 su \u0026lt;username\u0026gt; 安装依赖 安装 Postgres 编译所需的依赖（这里是摘取了 Greenplum 的安装依赖，可能包含了一些没必要安装的，但肯定是涵盖了 Postgres 需要的依赖，所以全部安装上也没啥问题）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 sudo apt-get update sudo apt-get install -y \\ bison \\ ccache \\ cmake \\ curl \\ flex \\ git-core \\ gcc \\ g++ \\ inetutils-ping \\ krb5-kdc \\ krb5-admin-server \\ libapr1-dev \\ libbz2-dev \\ libcurl4-gnutls-dev \\ libevent-dev \\ libkrb5-dev \\ libpam-dev \\ libperl-dev \\ libreadline-dev \\ libssl-dev \\ libxerces-c-dev \\ libxml2-dev \\ libyaml-dev \\ libzstd-dev \\ locales \\ net-tools \\ ninja-build \\ openssh-client \\ openssh-server \\ openssl \\ pkg-config \\ python3-dev \\ python3-pip \\ python3-psycopg2 \\ python3-psutil \\ python3-yaml \\ zlib1g-dev 执行编译 拉取 Postgres 的源代码，并进入到 postgres 代码目录中。\n如果是拉取最新版本的代码，可以从 Github 上获取：\n1 git clone https://github.com/postgres/postgres.git 如果想要获取对应版本的源代码，则可以从 Postgres 官网中下载：\n地址：https://www.postgresql.org/ftp/source/\nPostgres 有非常多的编译选项，详情可以参考官方文档：https://www.postgresql.org/docs/current/install-make.html#CONFIGURE-OPTIONS\n我们这里只使用最简单的编译方式即可。\n1 CFLAGS=-O0 ./configure --prefix=/home/roseduan/pg-install --enable-debug 我们关闭了编译器的优化，方便后续的调试，并且打开了 debug 模式。\n--prefix 指定编译后的二进制目录的位置，这里不指定也是可以的，默认是在 /usr/local 下面。\nConfigure 之后，如果没有错误产生的话，则执行编译并安装：\n1 make -s -j`nproc` install 编译安装之后，得到了二进制目录，可以将 bin 目录加入到 PATH 环境变量中，如果嫌麻烦，可以加入到 $HOME 目录中的 .bashrc 或者 .zshrc（取决于你的 sh 是什么），这样下次登录就不用重复设置了。\n1 export PATH=/\u0026lt;posgres-install-dir\u0026gt;/bin:$PATH 初始化 DB 上述步骤完成后，可以使用 init 命令来初始化 postgres 的数据目录。\n1 pg_ctl -D \u0026lt;pg 数据目录路径\u0026gt; init 初始化完成后，直接启动 postgres 的服务即可。\n1 pg_ctl -D pg-data start 启动之后，可以查看 postgres 的进程状态。\n也可以通过 psql 命令连接到数据库中：\n1 psql postgres 如何 Debug 有了源码环境之后，其实 Debug 调试就比较简单。\n使用 psql 登录之后，后台会启动一个工作进程来服务于这个客户端的请求，可以通过 pg_backend_pid() 方法查看进程 id。\n这里我的进程 id 是 1857，直接通过 gdb -p 1857 即可对该进程进行 Debug。\n我们可以在 gdb 中设置一个断点，比如 Postgres 的简单查询命令都会走 exec_simple_query 方法，可以直接对这个方法打断点，然后在客户端任意执行一个 select 语句，就会到 gdb 的断点中了：\n参考资料 https://www.postgresql.org/docs/current/installation.html\n","date":"2024-04-16T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/postgres-%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-1postgres-%E6%BA%90%E7%A0%81%E7%BC%96%E8%AF%91%E5%92%8C-debug/","title":"Postgres 源码学习 1—Postgres 源码编译和 debug"},{"content":"我自己就是从业务自学转入数据库内核研发岗位的，根据自己的经历，简单总结了一下入门数据库/存储相关的学习路线、学习资料、项目书籍推荐等，大家可以参考。\n并且需要强调的是，这些内容其实并不只是为了想要转入数据库/存储方向的同学。\n就算是业务后端开发，以及云原生等方向，数据库、分布式都是必备的基础内容，并且存储也能够学习到很多的一些操作系统基础知识，所以我觉得都是有必要去进行学习的。\n必看课程 CMU-15445 和 CMU-15721\nhttps://www.youtube.com/@CMUDatabaseGroup\n这两个不用多说，经典的数据库入门教程，由数据库的大佬 Andy Pavlo 亲自授课。\n可以了解到数据库的基本概念，例如存储、BufferPool 缓冲池、索引、优化器、执行器、事务等。\n15445 的实验部分是基于其开源的教学项目 bustub，补全其中几个重要的部分，这个项目是 C++ 写的，如果对 C++ 不熟悉的话，那么我觉得实验部分可以暂时跳过，有多余的精力再来搞，毕竟我们是来学数据库的，而不是学 C++ 的。\n存储项目实践 学习数据库课程的同时，顺便可以了解下存储方面的内容，存储是数据库必不可缺的重要组成部分。\n例如 B+ 树、bitcask、LSM Tree，以及 LSM Tree 的优化 Wisckey，找几篇文章看看，了解下基本概念，或者直接看看对应的最权威的论文。\n然后自己去实践写一个，例如写一个简单的 bitcask、B+ 树存储引擎，或者 LSM 存储引擎。\n在我的经验看来，这几类存储引擎，实现的难度大致是 B+树 \u0026gt;= LSM \u0026gt; bitcask，bitcask 可以看做是一个简化版的 LSM Tree。\n对于 B+ 树，学习的资料有\nBoltDB，Go 语言写的一个 B+ 树单机存储引擎，在生产环境中广泛应用 https://cstack.github.io/db_tutorial，一个简单的从零开始写数据库的教程，类似 SQLite，C 语言实现 LSM Tree，可以参考以下资料\nleveldb，Google 开源的单机存储引擎，Go 语言也有一个对应的实现 goleveldb leveldb-rs，Rust 实现的 leveldb mini-lsm，一个从零实现极简的 LSM 教程，Rust 实现 bitcask 可参考资料：\nrosedb，基于 bitcask 的单机 KV 引擎 mini-bitcask，一个极简的 bitcask 教程 之所以推荐写存储类的实战项目，主要是因为存储层的 KV 一般比较好实现，同时又能够了解到一些数据库的基本设计理念。\n强烈推荐👍🏻\n如果想要一个完整实现 KV 存储的代码实践教程，可以参考我的《从零实现 KV 存储》的教程，从第一行代码开始的视频实战教程，使用 Go 和 Rust 两种语言分别实现。\n事务/MVCC 这部分网上的资料比较多，可以看看事务的一些基本概念 ACID，然后看看如何去实现的，可以借鉴其他数据库例如 MySQL、PostgreSQL，关于事务实现原理分析这方面的文章比较多。\n概念了解差不多之后，可以自己动手实现，例如可以在自己写的存储项目的基础上，加上事务的功能，保证事务原子性、隔离性，以及并发读写的性能，自己上手撸肯定比只了解理论好很多。\nCMU 15445 和 15721 课程都讲到了事务的一些基本概念，可以参考 https://15445.courses.cs.cmu.edu/fall2019/schedule.html#oct-23-2019\n其他的一些部分，例如 parser、执行器、优化器、向量化等等，比较复杂，自己从头搞一个的难度比较大，我觉得可以简单看看资料，了解一下基本概念，工作之中再针对性的查漏补缺。\n当然如果你对某个部分特别感兴趣的话，比如优化器之类的，也可以多去了解然后自己实践，我这里推荐存储和事务的实现，是因为相对来说比较容易上手。\n其他方面的项目实践这里也推荐一些资料：\nGo 语言实现的 sql 解析器 https://github.com/xwb1989/sqlparser TiDB 的 mysql 解析器 https://github.com/pingcap/tidb/tree/master/pkg/parser Rust 实现的 sql parser https://github.com/sqlparser-rs/sqlparser-rs Meta 开源维护的通用数据库执行框架 Velox https://github.com/facebookincubator/velox 向量化执行框架 Arrow https://github.com/apache/arrow 分布式 这部分内容首推 MIT 6824（现已改名为 6.5840），分布式系统入门的首选课程。 https://www.youtube.com/@6.824/videos\n有精力的话可以跟着把实验部分做完。lab 是实现一个除成员变更之外的 raft 共识算法，并且基于 raft 实现一个容错的分布式 KV 系统。\n强烈推荐👍🏻\n如果对课程内容并不熟悉的话，初次上手做 lab 可能会比较痛苦，并且自己搞也可能会比较浪费时间。\n可以看看我的《从零实现分布式 KV》教程，从零开始，基于 6824 的 lab，写一个基于 raft 的分布式 KV，教程内容完全涵盖了 6824 的 lab 部分。\n这个学完了之后，可以挑战下 PingCAP 的 talent plan 中的 TinyKV，它和 6824 的实验部分比较类似，实现一个基于 raft 的分布式 KV 存储系统，难度会比 6824 更大，lab 的代码框架是现成的，只需要往里面添加内容即可，测试也比较完备。 https://github.com/talent-plan/tinykv\n如果还有时间的话，可以再上一个台阶，挑战下 PingCAP talent plan 的 TinySQL 项目，主要是实现一个简单的分布式数据库项目，有完备的文字教程，只是难度略大。 https://github.com/talent-plan/tinysql\n工作或者实习 当然，其实最好的办法，还是能够直接参与到工作实践当中，这样学习起来是最快的，可以向 leader 请教，和同事交流等。\n如果自身又没有太多经验的话，可以试试那些愿意接纳转数据库内核的公司，这可能会要求你有其他亮眼的东西了，比如基础比较扎实，折腾过自己的项目之类的。能把上面提到的这些东西认真学习下，完成个 60% 左右，我觉得应对一些面试就应该没有太大的问题了。\n为了帮助你更高效的学习，我还整理了一份数据库开发的学习资料，数据库的各个方面都涉及到了，例如 SQL、优化器、执行引擎、存储等等，包含一些优质的书籍、论文、视频课程、博客等，还有一些优质的教学类项目。 总计十几页的 PDF，一次性送给你，方便提升学习效率。 还有一些关于数据库方面的优质 PDF 书籍，可以参考学习： 这份学习资料 PDF 和所有的书籍都可以在我的公众号领取，后台回复关键字数据库。 ","date":"2024-04-13T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E8%B6%85%E7%BA%A7%E7%A1%AC%E6%A0%B8%E7%9A%84%E6%95%B0%E6%8D%AE%E5%BA%93/%E5%88%86%E5%B8%83%E5%BC%8F%E5%AD%98%E5%82%A8%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF/","title":"超级硬核的数据库/分布式存储学习路线"},{"content":"语法基础 官方的 rust book https://doc.rust-lang.org/book/title-page.html 通过例子学 Rust https://rustwiki.org/zh-CN/rust-by-example/index.html 官方 rustlings 小练习 https://rustlings.cool/ Rust 语言圣经 https://course.rs/basic/intro.html Google 出的 Rust 教程 https://google.github.io/comprehensive-rust/welcome.html Rust 程序设计语言 https://kaisery.github.io/trpl-zh-cn/title-page.html Learn Rust Easy 一本中文入门书 https://rustycab.github.io/LearnRustEasy/ 语法进阶 官方 rust 死灵书 https://doc.rust-lang.org/nomicon/intro.html Rust 原子操作和锁 https://marabos.nl/atomics/ Rust 原子和锁—中文翻译 https://atomics.rs/about-book.html 写各种链表练习 Rust https://rust-unofficial.github.io/too-many-lists/index.html 项目实战 构建兼容 Redis 协议的 KV 存储 https://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g 实现一个简单的 DNS 服务 https://github.com/EmilHernvall/dnsguide/tree/master 构建一个简单的微服务 https://www.goldsborough.me/rust/web/tutorial/2018/01/20/17-01-11-writing_a_microservice_in_rust/ 写一个简单的网页应用 https://www.sheshbabu.com/posts/rust-wasm-yew-single-page-application/ Rust 构建一个文本编辑器 https://www.flenker.blog/hecto/ Tokio 异步编程 官方文档：https://tokio.rs/tokio/tutorial https://rust-book.junmajinlong.com/ch100/00.html Tokio 中文文档：https://tokio-zh.github.io/document/ 项目实战：mini-redis https://github.com/tokio-rs/mini-redis 数据结构和算法 https://github.com/TheAlgorithms/Rust 书籍 推荐 《Rust 程序设计》 《Rust 权威指南》 《Rust 实战》 《深入理解 rust 并发编程》 其他 《Command-Line Rust》 《Rust Atomics and Locks》 《Rust for Rustaceans》 《System Programing with Rust》 《Rust 编程之道》 Github 不太好上传大 PDF 文件，以上提到的所有书籍都可以在我的公众号「roseduan写字的地方」领取，关注后回复关键字 Rust 即可。\n","date":"2024-04-09T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E5%85%A8%E7%BD%91%E6%9C%80%E8%AF%A6%E7%BB%86%E7%9A%84-rust-%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99%E4%B9%A6%E7%B1%8D%E5%AE%9E%E6%88%98%E9%A1%B9%E7%9B%AE/","title":"全网最详细的 Rust 学习资料、书籍、实战项目"},{"content":" 本文在 B 站有对应的视频，视频中有更详细的讲解，求个三连~ https://www.bilibili.com/video/BV13J4m157K5\n我自己在毕业的前 2 年，其实都是做的后端业务开发，后来才转到了数据库内核开发，结合我自己的实际经历，跟大家聊一聊这两种不同类型的岗位都有什么区别。 我会分别从编程语言、难易程度、工作流程、市场就业、薪资情况、稳定性方面进行全方位对比，最后也会对两种方向的选择给出自己的建议。\n编程语言 我们常说的后端开发，主要是实现服务端的业务流程，并且对前端的增删改查请求进行正确的响应，当然也需要保证后端服务的高可用、高并发等特性。\n后端开发也比较注重开发效率，因为产品经理的需求有可能是快速变化的，需要后端开发人员进行快速响应，协助快速上线产品，不断试错迭代。\n后端开发常用的语言一般都是带 GC 的自动内存管理的语言，这类语言将程序员从繁琐的内存管理中释放出来，让其更加专注于实现业务逻辑，常用的有 PHP、Java、Go、Python、Ruby 等等。\n当然这些语言其实也有对应的框架，帮助快速去构建业务流程，比如 Java 的 Spring 系列、PHP 的 Laravel、Python 的 Django 等等。\n数据库内核开发，注重的是性能、数据安全、分布式、可扩展等等，数据库性能问题是一个经久不衰的话题，有各种各样的性能评测榜单，比如 TPCH、TPCC、TPCDS、ClickBench 等等。\n所以在编程语言的选择上，以不带 GC 的编程语言居多，C、C++ 占了大多数，因为这类编程语言性能高，没有自动垃圾回收带来的性能抖动。目前国内的数据库内核，基本上分为了两个系列，一个是魔改 Mysql，Mysql 是 C++ 写的，另一个是魔改 Postgres，而 pg 则是 C 语言写的。\n当然近些年来 Rust 的发展非常迅猛，在系统级编程领域也有了很多的应用，很多新兴的数据库都陆续采用了 Rust。\n难易程度 后端开发的难度，一是主要体现在对业务逻辑本身的理解上，因为有的业务流程冗长，繁琐，理解起来其实并不是特别的容易；二是体现在对系统本身的高可用、高并发等方面，需要有一定的系统设计能力。\n数据库内核开发的难度，主要体现在对数据库内核本身的学习和理解之上，因为数据库是一个非常复杂的系统，涉及到非常多的组件，比如解析器、执行器、事务、存储引擎、缓存、分布式等等。\n比如常见的 Postgres，发展了接近 30 年，是成百上千位世界上最顶级的程序员精心维护的项目，目前的代码量在 100w 行左右，注释都有 30w 行，所以不太可能在短时间内完全理解，有时候甚至花个两三年的时间，也只是刚刚入门。\n想要学习一个现有的数据库系统就已经非常复杂了，如果还要在其之上去做一些功能开发、性能优化，则难度会继续提升。\n所以后端开发和数据库内核开发各有各的难度，总体来说的话应该是数据库内核开发更难一点，学习上手的周期也会更长。\n工作流程 后端开发的工作流程，基本上是产品经理提出需求，然后后端开发人员进行开发之前的需求对齐、调研、接口设计，然后是编码、文档撰写、和前端联调对接，然后是到测试环节，有专门的测试人员对需求进行功能性测试，如果发现 bug 则需要修复，然后是需求完成并上线。\n后端开发的流程有时候会比较繁琐，因为涉及到的上下游比较多，比如产品经理、测试、其他业务部门、甚至运营等等，可能会花费很多的时间在开会上，特别是在大厂这种情况更为突出。\n而数据库内核开发，虽然上下游的需求方并没有那么多，但是开发的流程可能会非常长，如果一个需求比较大的话，则更是由一个团队来持续推进开发。数据库内核的需求一般来自使用者、业务场景、或者是老板的想法等等，产品经理在其中起到的作用比较小，像一些初创公司，甚至没有专门的产品经理。\n内核开发其实是花了更多的时间聚焦在功能开发本身，并且开发完成后，测试的工作也会非常漫长，往往需要自己编写单元测试，通过回归测试，混沌测试等一系列流程，最终才能够上线。\n市场就业 在工作机会方面，应该是后端开发要多于数据库内核开发岗位的，这其实比较明显，目前国内搞数据库的公司大大小小有两三百家，但有招聘需求的估计并不是很多，当然一些大厂有内部的自研数据库服务，所有也会有一些相关的招聘需求 ，比如字节、阿里、华为、腾讯等等。\n后端开发的岗位相对来说是更多的，但是人也更多，因为其门槛更低，竞争也比较激烈。目前的就业环境总体来说也比不上前几年，僧多粥少，面试的难度较大，找不到工作的人还是挺多的。\n薪资情况 薪资情况其实并不太好说，受各方面的影响因素其实是比较大的，这里只能给出一个预估给大家参考。\n我们就以校招的薪资来作为一个基准，对于后端开发来说，大厂的薪资应该是比较有代表性了，目前大厂后端开发，校招的薪资区间大概在 20k-25k 左右，可能会受到学历、实习经历、具体部门等诸多因素的影响，但大致是这个区间。\n数据库内核开发，在 2023 年，本科毕业的薪资大概是 25k-30k 左右，如果是硕士学历，则有可能更高。\n稳定性 两种类型的岗位其实都有各自的不稳定因素。\n后端开发可能是需要面临如何应对年轻人的冲击，如何应对大家常说的 35 岁危机，并且面临日益严峻的就业环境，其实压力是非常大的，一方面不得不学习新的技术，拼了命去卷算法、八股文，但是在日常工作中可能又根本用不上，陷入面试造火箭工作拧螺丝的窘境。\n数据库内核开发，主要是就业面很窄，能够选择的跳槽的机会是有限的，学习的周期又很长，非常容易中途被一些其他的因素打断，导致半途而废。 但是只要在数据库内核方面积累了足够的经验，那么你的技术护城河其实就打造出来了，不那么容易被替代，像我们公司，30 多岁不仅不会担心被裁员，甚至是团队不可或缺的中坚力量。\n并且，从数据库内核想要转到其他的方向，比如分布式存储，甚至后端开发，应该比后端开发转到数据库内核要容易得多。\n如何选择 结合前面我说的一些对比，其实大家对于如何选择应该也有自己的打算了，我觉得还是需要结合自己的兴趣，以及技术能力等情况做出自己的选择。\n对技术有一定追求的，并且想要一直在技术这个领域深耕的，我觉得数据库内核开发是一个不错的方向。如果想要求稳，只是为了能够有一份工作，然后持续的在互联网行业赚点钱糊口，那么可以选择后端开发。\n还有的同学是目前是后端开发，但是想要转入到数据库内核的，这种其实也没有什么问题，多尝试自己感兴趣的方向总是好的。不至于温水煮青蛙，一直待在自己的舒适圈里，不寻求任何变化，可能就不会有什么进步。\n","date":"2024-04-02T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91%E5%92%8C%E6%95%B0%E6%8D%AE%E5%BA%93%E5%86%85%E6%A0%B8%E5%BC%80%E5%8F%91%E6%9C%80%E5%85%A8%E6%80%BB%E7%BB%93%E8%AF%A6%E7%BB%86%E5%AF%B9%E6%AF%94/","title":"后端开发和数据库内核开发最全总结，详细对比！"},{"content":"Go 语言语法 基础语法 数据类型 变量声明 错误处理 函数，多返回值 接口/结构体 进阶 泛型 数据结构 array slice map heap 并发 goroutine、channel、context 反射 在线资源 https://www.runoob.com/go/go-tutorial.html https://www.w3cschool.cn/go/ Go by Example 中文版 https://gobyexample-cn.github.io/ 基础书籍 Go 程序设计语言 https://book.douban.com/subject/27044219/ Go 并发编程实战 进阶书籍： Go 语言设计与实现 https://draveness.me/golang/ Go 语言高级编程 https://chai2010.cn/advanced-go-programming-book/ Go 语言原本 https://golang.design/under-the-hood/ 巩固基础，项目推荐 数据库/存储小项目 https://www.bilibili.com/video/BV1qW421c7kY 极客兔兔 https://github.com/geektutu/7days-golang https://github.com/muesli/cache2go 常用框架 web 框架、orm 框架：gin、echo、gorm https://echo.labstack.com/docs/quick-start https://gin-gonic.com/zh-cn/docs/ 从零设计一个 web 框架 https://github.com/astaxie/build-web-application-with-golang/tree/master 书籍 《Go Web 编程》 微服务 微服务 gRPC https://grpc.io/docs/languages/go/quickstart/ 《gRPC 与云原生应用开发》 go-zero https://github.com/zeromicro/zero-examples https://github.com/Mikaelemmmm/go-zero-looklook Kratos https://github.com/go-kratos/examples https://github.com/go-kratos/beer-shop 实战项目：写一个极简 RPC 框架 https://github.com/zehuamama/tinyrpc 面试突击 https://github.com/lifei6671/interview-go https://www.topgoer.cn/docs/gomianshiti/mianshiti https://www.topgoer.cn/docs/interview/interview-1dks7os61lo44 https://github.com/mao888/golang-guide 其他书籍、资料 《Head First Go》 《Go 语言学习笔记》 《Go 网络编程》 《Go 语言从入门到进阶实战》 《Mastering Go》中文翻译 https://www.bookstack.cn/read/Mastering_Go_ZH_CN/README.md Go 语言常见面试题 https://www.topgoer.cn/docs/gomianshiti/mian1 Uber 出品的 Go 语言编码风格指南 https://github.com/uber-go/guide/blob/master/style.md Awesome-go go 语言相关的资料、三方库列表 https://awesome-go.com/ 书籍领取方式 文中提到的所有书籍，都可以直接领取： 公众号「roseduan写字的地方」，后台回复「Go」\n","date":"2024-03-24T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/go-%E8%AF%AD%E8%A8%80%E6%9C%80%E5%85%A8%E5%AD%A6%E4%B9%A0%E8%B7%AF%E5%BE%84%E8%B5%84%E6%96%99%E6%B1%87%E6%80%BB/","title":"Go 语言最全学习路径、资料汇总！"},{"content":"今天给大家分享一下我从大学自学编程，然后毕业四年多以来，学习过的编程语言，然后也会对这些编程语言做一个简单的比较，最后也会给出我的学习建议。\n我在 B 站录制了本文的视频，有更多的详细内容：https://www.bilibili.com/video/BV1Xt421V7zW\n学习历程 我最开始接触编程，是在大一的时候，学的是 C 语言，因为那个时候 C 语言是计算机系的第一门课程了吧，我虽然不是计算机系的，但是也能从其他同学那里打听到一些信息，于是自己也开始学习 C 语言了。\n买了一本 C 语言的书，是谭浩强的《C 语言程序设计》，这本书总体来说一言难尽，然后买了第二本是《C Primer Plus》，跟着大致敲了一遍代码。只是当时计算机基础很差，学习断断续续，很不成系统。\n到了大三，为了能够毕业时找到工作，开始自学 Java，Java 当时还是非常火爆的，培训班、网上的资料铺天盖地的都是 Java 相关的，我那时候还学习了 Java Web，做了一个类似淘宝的商城项目，自己写了简单的前端，所以用到了 Html/CSS 、JavaScript。 商城系统/XX 管理系统那个时候还是很流行的练手项目（以致于后来有一个梗，面试官说你们项目组人还挺多的\u0026hellip;\u0026hellip;）。\n不像现在，人手都是高并发、 CMU 15445、MIT 6824 这些项目和课程，只能说越来越卷了。\n毕业之后，在工作中有一些比较繁琐、重复劳动的工作，比如生成一些随机数据的 Execl 文件，根据字段自动生成 sql，文本内容替换等等，所以学习了 Python，用 Python 写这类小工具非常适合，能够大大提升工作效率。\n工作业余时间我还自学了 Go 语言，开始用 Go 写开源项目，围绕存储引擎写了很多开源项目和小轮子，比如 rosedb、lotusdb、wal、diskhash、mini-bitcask 等等。 然后还找到了 Go 后端开发的工作，去了 B 站做后端开发，也算是实现了从 Java 到 Go 的转变，从那以后就没怎么接触 Java 生态了。\n后来我又转到了数据库方向，这个方向整体还是 C 和 C++ 比较多，所以又重新开始了学习 C 语言。同时工作中偶尔也接触到了 C++，但我对 C++ 了解比较一般，不算特别熟悉。\n在工作中维护的组件是 Rust 写的，并且我本身也对 Rust 比较感兴趣，所以就学习了 Rust，并且实现了一个 Bitcask 存储模型的 Rust 语言实现，相当于是 rosedb 的 Rust 版本（只不过没有开源），对熟悉基础语法、Rust 上手实践的帮助还是挺大的。\n这就是我毕业这几年以来的编程语言学习历程，涉及到\n前端：Html/CSS、JavaScript C C++ Java Python Go Rust 到目前为止，我使用得比较频繁的语言是：\n开源项目的维护主要是用 Go 语言 自己的兴趣 Rust 语言，业余时间会写一些有意思的项目 工作上 C/C++/Rust 编程语言比较 基本上比较主流的后端语言我都接触到了，这里简单谈谈自己的看法吧，\nJava 比较适合用来写一些企业级后端业务，大一统的后端框架 Spring 全家桶，业务开发的效率比较高，安卓开发也用的较多，移植性好，面 向对象、泛型，自动内存回收。Java 总体来说是一门综合实力挺强的一门语言。\nGo 的杀手锏是高效易用的并发，比如 goroutine 协程，channel 通道，比较适合用来构建云原生微服务、网络编程、中间件等等，kubernetes 和 docker 也是 Go 比较知名的项目了。\nPython 比较简洁，主要场景目前主要在机器学习、网络爬虫、当然也有一些 web 开发，比如 Django、Flask 框架，也比较适合用来写自动化测试。\nC/C++ 目前主要是在系统级编程领域发挥重要作用，执行效率高，性能好，但是也相对复杂，手动管理内存。主要应用于系统级软件，比如操作系统，数据库，驱动程序，嵌入式等等。\nRust 是近些年来崛起的比较火热的语言之一了，具备内存安全、高性能的特点，在系统级编程领域对 C/C++ 发起了挑战，现在很多新兴的数据库软件都开始使用 Rust 了，比如 neon、risingwave、databend、influxdb、surrealdb，同时也在区块链、web3 方面有应用。\n学习建议： 1、不要给自己设限，通常情况下我们会给自己打上一个标签，比如 Java 程序员、Go 程序员、C++ 程序员等等，程序员不应该和一门语言绑定在一起，可以根据自己的兴趣去学习自己感兴趣的内容。工作之后，更多的是根据实际情况去进行转换，学习新的语言以及技术栈，而不是一成不变。\n2、如果你只是为了快速上手，然后找到一份工作的话，在这种功利目的下，可以按照市场需求去学习，比如 Java、Go、C++ 都非常的不错，当 然目前相对来说 Java 是最卷的了，Rust 也是近些年来非常不错的语言，也建议去上手学习。\n3、针对在校学生，建议学好 C 语言，C 语言是贴近系统层的语言，对自己的计算机基础有非常大的帮助，不要觉得 C 语言很无趣，这个世界上最伟大的一批软件，Linux、Nginx、Redis、SQLite、Postgres、Git、Vim、curl 都是使用 C 语言写的。有了 C 语言的基础，再去学其他的语言会容易很多。\n4、针对已经工作的同学，其实最好是在一门语言上多花时间，最好能够精通，也就是先纵向发展，培养自己的核心竞争力。然后在此基础之上，结合自己的兴趣以及实际情况，去学习一门新的语言其实上手就会非常快了。\n最后，编程语言具体的学习方式，比如如何快速上手一门语言，就后面再分享了，感谢大家。\n","date":"2024-03-18T20:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E7%BB%86%E6%95%B0%E8%87%AA%E5%B7%B1%E5%AD%A6%E4%B9%A0%E8%BF%87%E7%9A%84%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E9%99%84%E5%AD%A6%E4%B9%A0%E5%BB%BA%E8%AE%AE/","title":"细数自己学习过的编程语言，附学习建议"},{"content":"大家好，今天给大家分享一些使用 Go 语言编写的数据库/存储项目。\n因为我的两个存储引擎开源项目 rosedb 和 lotusdb 都是使用 Go 语言编写的，所以这几年在这方面也有很多的积累，今天就把自己压箱底的干货分享给大家。\n首先需要说明的是，像 TiDB、CockroachDB、etcd 这些大型的项目我就不再列举了，因为这些项目都耳熟能详，公开的资料也很多，不需要我再多说什么了。\n今天想给大家分享的是，一些比较小众的，代码量在 1w 行以内，适合大家去学习上手的一些项目。如果你想入门数据库/存储领域，这些项目其实都非常适合你去研究学习，能够让你对数据库/存储领域有一个更深入的了解。如果你是学习了 Go 语言，想找一个小项目来练手，这些项目也是非常适合你。\n这次分享的项目主要分为了两个大的类型，一是一些基础类型的教程，二是一些比较完整的项目。 最后也会分享我自己推荐的学习方式。\n基础类型的教程 第一个是我自己写的 mini-bitcask 教程 https://github.com/rosedblabs/mini-bitcask，300 多行代码实现了一个极简的 bitcask 存储引擎，可以看做是 rosedb 的 mini 版本，对于你学习存储引擎的原理和实现有很大的帮助。我之前还专门写了一篇文章来介绍这个项目，可以结合起来观看效果更佳。 两百行代码实现基于 paxos 的分布式 KV https://github.com/openacid/paxoskv，也有一个专门讲解的博客文章，非常值得学习。 从零开始写时序数据库 https://github.com/chenjiandongx/mandodb。这个项目是一个时序数据库，作者从零开始写了一个简单的时序数据库，代码量不大，适合新手学习。 Go 语言实现的易于学习的 sql 数据库 https://github.com/qw4990/NYADB2，参考了很多 boltdb 的实现。 1k 行代码的极简分布式 kv 数据库 https://github.com/geohot/minikeyvalue，并且用于了生产环境。 进阶类型的项目 关系型数据库 关系型数据库这里推荐几个我觉得还不错的，但是关系型 DB 难度肯定比 KV 更大，因为关系型 DB 包含了多个组件比如 parser、执行器、事务、存储等模块，感兴趣的同学可以参考。\nhttps://github.com/chaisql/chai，嵌入式 SQL 数据库，兼容 Postgres 的 sql，支持持久化存储。\nhttps://github.com/codenotary/immudb，支持文档、SQL、KV 的多模数据库。\nhttps://github.com/auxten/go-sqldb，简单的 sql 数据库，使用 B+ 树存储数据，实现了 parser 和 executor。\nhttps://github.com/rqlite/rqlite，基于 sqlite 的分布式数据库，可以认为是 raft+sqlite。\nhttps://github.com/hashicorp/go-memdb，hashicorp 的内存型嵌入式数据库，比较轻量级。\nKV 数据库 Bitcask rosedb https://github.com/rosedblabs/rosedb，基于 bitcask 的 KV 存储引擎，轻量级，支持 WriteBatch、TTL、Scan 等功能，目前是被应用到了生产环境中。\nnutsdb https://github.com/nutsdb/nutsdb，同样也是基于 bitcask 的 KV 存储引擎，支持类似 Redis 的数据结构，国人开发和维护。\nB+Tree boltdb https://github.com/etcd-io/bbolt, Go 语言领域知名的存储引擎，B+ 树实现，支持一写多读的事务，广泛运用于生产环境，etcd 就是使用了 boltdb 作为持久化存储引擎。\nLSM Tree goleveldb https://github.com/syndtr/goleveldb，leveldb 的 Go 语言实现，学习 LSM Tree 实现细节的好项目。\nbadger https://github.com/dgraph-io/badger，wisckey 的实现，LSM Tree KV 分离。\npebble https://github.com/cockroachdb/pebble，CockroachDB 的底层存储引擎，目前 Go 领域最难的 KV 存储引擎了，设计非常精细，代码量也比较大，主要参考了 RocksDB。\nHybrid（LSM+BPTree） lotusdb https://github.com/lotusdblabs/lotusdb，结合 LSM 和 B+Tree 的存储引擎，架构较为新颖。\n学习建议 最后，针对 KV 数据库的学习，这里给出我的一些小的建议。\n从存储模型上来说，主流的 KV 存储模型有两种，分别是 B+Tree 和 LSM Tree，当然后来也出现了很多基于此的变种和优化，但最基本的还是这两个。\nbitcask 可以看做是一个简化版的 LSM Tree，它大致只包含 LSM 中的 wal 和 memtable 组件，LSM 中最复杂的 SSTable 组件被省略了。\n所以在学习上，建议先从 bitcask 学起，可以参考我的那个 mini-bitask 教程，结合文章，很容易就能够理解了，代码也只有 300 多行。\n然后再看看我的 rosedb 项目，就基本上能够理解 bitcask 存储模型了。\n有了这个基础之后，可以再学习 B+树或者 LSM Tree，倒也不用两个都学，可以挑选一个自己感兴趣的去学习。\nB+树的话就去看 boltdb，网上也有很多 boltdb 源码解析的文章。LSM Tree 的话推荐 goleveldb，结合 leveldb 一些资料和原理，应该理解起来也不难。\n当然，看代码学习也只是迈出了第一步，想要更加深入的话，比如自己去撸一个出来，就需要花费更多的时间和精力了。\n","date":"2024-03-06T21:07:56+08:00","permalink":"https://blog.roseduan.cn/p/go-%E8%AF%AD%E8%A8%80%E6%95%B0%E6%8D%AE%E5%BA%93/%E5%AD%98%E5%82%A8%E9%A1%B9%E7%9B%AE%E6%8E%A8%E8%8D%90/","title":"Go 语言数据库/存储项目推荐"},{"content":" 本文选自《从零实现分布式 KV》课程的加餐文章。 从零开始，手写基于 raft 的分布式 KV 系统，课程详情可以看这里：https://av6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5b\n在简历上如何写这个项目？ 项目概述\n基于 MIT 6824 课程 lab 框架，实现一个基于 raft 共识算法、高性能、可容错的分布式 KV 存储系统，保证系统的一致性和可靠性。\n设计细节\n设计基于 Raft 一致性算法的分布式系统架构。 支持分布式数据存储和检索的 KV 存储引擎，采用 Raft 协议确保数据的强一致性。 实现数据分片和自动故障转移机制，以实现系统的高可用性和容错性。 使用 Go 语言编写，工程级代码可靠性和简洁性。 结果\n参照 Raft 论文使用 Golang 实现了领导选举、日志同步、宕机重启和日志压缩等主要功能。熟悉 Raft 算法的基本原理和实现细节，熟悉 Golang 并发编程和分布式调试。 实现了一个高性能的分布式键值存储系统，保证数据的一致性和可靠性。 通过所有代码测试，在负载测试中表现出良好的性能和稳定性，能够有效地应对并发访问和故障情况。 可能的面试问题\u0026amp;回答 以下我们每个节点统称为 Peer，面试官可能会叫节点、副本(Replica)、Node 等等术语，记得和面试官对齐就好。\nRaft 主要在什么场景中使用？ 通常有两种用途：\n元信息服务，也称为配置服务（configuration services）、分布式协调服务（coordinator services）等等。如 etcd。用以追踪集群中元信息（比如副本位置等等）、多副本选主、通知（Watch）等等。 数据复制（Data replication）。如 TiKV、CockroachDB 和本课程中的 ShardKV，使用 Raft 作为数据复制和冗余的一种手段。与之相对，GFS 使用简单的主从复制的方法来冗余数据，可用性和一致性都比 Raft 要差。 注：在分布式系统中，数据指的是外界用户存在系统中的数据；元数据指的是用户维护集群运转的内部信息，比如有哪些机器、哪些副本放在哪里等等。\nRaft 为了简洁性做了哪些牺牲（即有哪些性能问题）？ 每个操作都要落盘。如果想提高性能可能需要将多个操作 batch 起来。 主从同步数据较慢。在每个 Leader 和 Follower 之间只允许有一个已经发出 AppendEntries；只有收到确认了，Leader 才能发下一个。类似 TCP 中的“停等协议”。如果写入速度较大，可能将所有的 AppendEntries Pipeline 起来性能会好一些（即 Leader 不等收到上一个 AppendEntries 的 RPC Reply，就开始发下一个） 只支持全量快照。如果状态机比较小这种方式还可以接受，如果数据量较大，就得支持增量快照。 全量快照同步代价大。如果快照数据量很大，每次全量发送代价会过高。尤其是如果 Follower 本地有一些较老的快照时，我们只需要发增量部分即可。 难以利用多核。因为 log 只有一个写入点，所有操作都得抢这一个写入点。 Raft 在选举时是不能正常对外提供服务的，这在工程中影响大吗？ 不太大，因为只有网络故障、机器宕机等事件才会引起宕机。这些故障的发生率可能在数天到数月一次，但 Raft 选主在秒级就能完成。因此，在实践中，这通常不是一个问题。\n有其他不基于 Leader 的共识协议吗？ 原始的 Paxos 就是无主的（区别于有主的 MultiPaxos）。因此不会有选举时的服务停顿，但也有代价——每次数据同步时都要达成共识，则数据同步代价会更大（所需要的 RPC 更多，因为每次同步消息都是两阶段的）。\n论文提到 Raft 只在非拜占庭的条件下才能正常工作，什么是拜占庭条件？为什么 Raft 会出问题？ “非拜占庭条件”（Non-Byzantine conditions）是指所有的服务器都是“宕机-停止”（ fail stop）模型（更多模型参见这里）：即每个服务器要么严格遵循 Raft 协议，要么停止服务。例如，服务器断电就是一个非拜占庭条件，此时服务器会停止执行指令，则 Raft 也会停止运行，且不会发送错误结果给客户端。\n拜占庭故障（Byzantine failure）是指有些服务器不好好干活了——可能是代码因为有 bug，也可能是混入了恶意节点。如果出现这种类型节点，Raft 可能会发送错误的结果给客户端。\n通常来说，Raft 的所有节点都期望部署在一个数据中心吗？ 是的。跨数据中心的部署可能会有一些问题。有些系统，如原始的 Paxos（由于是 Leaderless）可以跨数据中心部署。因为客户端可以和本地的 Peer 进行通信。\n如果发生网络分区，Raft 会出现两个 Leader ，即脑裂的情况吗？ 不会，被分到少数派分区的 Leader 会发现日志不能同步到大多数节点，从而不能提交任何日志。一种优化是，如果一个 Leader 持续不能联系到多数节点，就自动变为 Follower。\n当集群中有些 Peer 宕机后，此时的“多数派”是指所有节点的多数，还是指存活节点的多数？ 所有节点的多数。比如集群总共有五个 Peer，则多数派永远是指不低于 3 个 Peer。\n如果是后者，考虑这样一个例子。集群中有五个 Peer，有两个 Peer 被分到一个分区，他们就会认为其他三个 Peer 都宕机了，则这两个 Peer 仍然会选出 Leader ，这明显是不符合预期的。\n选举超时间隔选择的过短会有什么后果？会导致 Raft 算法出错吗？ 选举超时间隔选的不好，只会影响服务的可用性（liveness），而不会影响正确性（safety）。\n如果选举间隔过小，则所有的 Follower 可能会频繁的发起选举。这样，Raft 的时间都耗在了选举上，而不能正常的对外提供服务。\n如果选举间隔过大，则当老 Leader 故障之后、新 Leader 当选之前，会有一个不必要的过长等待。\n为什么使用随机超时间隔？ 为了避免多个 Candidate 一直出现平票的情况，导致一直选不出主。\nCandidate 可以在收到多数票后，不等其余 Follower 的回复就直接变成 Leader 吗？ 可以的。首先，多数票就足够成为主了；其次，想等所有票也是不对的，因为可能有些 Peer 已经宕机或者发生网络隔离了。\nRaft 对网络有什么假设？ 网络是不可靠的：可能会丢失 RPC 请求和回复，也可能会经历任意延迟后请求才到达。\n但网络是有界的（bounded）：在一段时间内请求总会到达，如果还不到达，我们就认为该 RPC 丢失了。\nvotedFor 在 requestVote RPC 中起什么作用？ 保证每个 Peer 在一个 Term 中只能投一次票。即，如果在某个 term 中，出现了两个 Candidate，那么 Follower 只能投其中一人。\n且 votedFor 要进行持久化，即不能说某个 Peer 之前投过一次票，宕机重启后就又可以投票了。\n即使服务器不宕机，Leader 也可能会下台吗？ 是的，比如说 Leader 所在服务器可能 CPU 负载太高、响应速度过慢，或者网络出现故障，或者丢包太严重，都有可能造成其他 Peer 不能及时收到其 AppendEntries，从而造成超时，发起选举。\n如果 Raft 进群中有过半数的 Peer 宕机会发生什么？ Raft 集群不能正常对外提供服务。所有剩余的节点会不断尝试发起选举，但都由于不能获得多数票而当选。\n但只要有足够多的服务器恢复正常，就能再次选出 Leader，继续对外提供服务。\n请简单说说 Raft 中的选举流程？ 所有的 Peer 都会初始化为 Follower，且每个 Peer 都会有一个内置的选举超时的 Timer。\n当一段时间没有收到领导者的心跳或者没有投给其他 Candidate 票时，选举时钟就会超时。\n该 Peer 就会由 Follower 变为 Candidate，Term++，然后向其他 Peer 要票（带上自己的 Term 和最后一条日志信息）\n其他 Peer 收到请求后，如果发现 Term 不大于该 Candidate、日志也没有该 Candidate 新、本 Term 中也没有投过票，就投给该 Term 票。\n如果该 Peer 能收集到多数票，则为成为 Leader。\n如果所有 Peer 初始化时不为 Follower、而都是 Candidate，其他部分保持不变，算法还正确吗？ 正确，但是效率会变低一些。\n因为这相当于在原来的基础上，所有 Peer 的第一轮选举超时是一样：同时变为 Candidate。则谁都要不到多数票，会浪费一些时间。之后就又会变成原来的选举流程。\n如何避免出现网络分区的 Peer 恢复通信时将整体 Term 推高？ 问题解释：如果某个 Peer （我们不妨称其为 A）和其他 Peers 隔离后，也就是出现了网络分区，会不断推高 Term，发起选举。由于持续要不到其他 Peer 的票，因此会持续推高 Term。一旦其之后某个时刻恢复和其他 Peer 的通信，而由于 Term 是 Raft 中的第一优先级，因此会强迫当前的 Leader 下台。但问题是，由于在隔离期间日志被落下很多，Peer A 通常也无法成为 Leader。最终结果大概率是原来的 Leader 的 Term 被拉上来之后，重新当选为 Leader。有的人也将这个过程形象的称之为“惊群效应”。\n解决办法：PrevVote。每次 Candidate 发起选举时，不再推高 Term，但是会拿着 Term+1 去跟其他 Peer 要票，如果能要到合法的票数，再去推高 Term（Term+1）。而如果能要到多数票，其实就保证该 Candidate 没有发生网络隔离、日志是最新的。如果要不到多数票，就不能推高 Term，这样会保证发生了网络隔离的 Peer 不会一直推高自己的 Term。\nRaft 和 Paxos 有什么区别？ 首先，Raft 和 Paxos 都是共识协议，而所有的共识协议在原理上都可以等价为 Paxos，所以才有共识协议本质上都是 Paxos 一说。\n如 Raft 论文中提到的，Raft 是为了解决 Paxos 理解和实现都相对复杂的问题。将共识协议拆成两个相对独立的过程：领导者选举和日志复制，以降低理解和实现的复杂度。当然，如果要想工程可用，Raft 的优化也是无止境的大坑，也并非像论文声称的那么简单。因此，有人说，Raft 看起来简单只是因为论文叙述的更清楚，而非算法本身更为简洁。\nRaft 其实是和 Multi-Paxos 等价，因为 Paxos 只解决单个值的共识问题。\nRaft 和 Paxos 的角色分法也不太相同，Raft 的每个 Peer 都可以有 Leader，Candidate 和 Follower 三种状态；而 Paxos 是将系统分为 Proposer，Acceptor 和 Learner 三种角色，实现时可以按需组合角色。\n在 Paxos 中，一旦某个日志在多数节点存在后就可以安全的提交；但在 Raft 中，不总是这样，比如一条日志在多数节点中存在后，但不是当前 Leader 任期的日志，也不能进行直接提交；而只能通过提交当前任期的日志来间接提交。\n在Paxos 在选举时，Leader 可能需要借机补足日志，但 Raft 中选举过程完全不涉及日志复制（这也是 Raft 进行拆分的初衷）。这是因为 Raft 只允许具有最新日志的 Candidate 成为 Leader，而 Paxos 不限制这一点。\n在 Paxos 中，允许乱序 commit 日志，而 Raft 只允许顺序提交。\n在 Paxos 中，每个 Peer 的 term 是不一致的，全局自增的；在 Raft 中 term 是每个 Peer 独立自增的，但需要对齐。\n更多区别，可以参考文末给出的资料。\nRaft 在工程中有哪些常见的优化？ 由于领导者选举是个低频操作，主要 IO 路径优化还是集中在日志同步流程上。\nbatch：Leader 每次攒一批再刷盘和对 Follower 进行同步。降低刷盘和 RPC 开销。 pipeline：每次发送日志时类似 TCP 的“停-等”协议，收到 Follower 确认后才更新 nextIndex，发送后面日志。其实可以改成流水线式的，不等前面日志确认就更新 nextIndex 继续发后面的。当然，如果后面发现之前日志同步出错，就要回退 nextIndex 重发之前日志——而原始版本 nextIndex 在同步阶段是单调递增的。 并行 append：Leader 在 append 日志到本地前，就先发送日志给所有 Follower。 请简单描述基于 raft 的分布式 KV 系统的架构？ 一个基于 raft 的分布式 KV 系统，实际上是由一组使用 raft 算法进行状态复制的节点组成。客户端会选择将请求发送到 Leader 节点，然后由 Leader 节点进行状态复制，即发送日志，当收到多数的节点成功提交日志的响应之后，Leader 会更新自己的 commitIndex，表示这条日志提交成功，并且 apply 到状态机中，然后返回结果给客户端。\n以上是单个 raft 集群的分布式 KV 系统架构。\n如果系统中数据量较大，一个 raft 集群可能无法承受大量的数据，性能也会受到影响。因此还基于此设计了可分片的分布式 shardkv 系统。shardkv 由多个 raft 集群组成，每个集群负责一部分 shard 数据。\nShard 到 raft 集群的映射关系，保存在独立的配置服务中。\n分布式系统中读数据的流程是什么样的，如何优化？ 为了保证线性一致性，目前的实现是利用了 raft 算法，将读请求传入到 raft 并进行状态复制，这样能够保证读到的数据一定是最新的。\n但是由于读请求也进行了一次日志复制，执行效率会受到影响，业界常用的两种优化方式是 ReadIndex 和 LeaseRead。\nhttps://cn.pingcap.com/blog/linearizability-and-raft/\nhttps://www.sofastack.tech/blog/sofa-jraft-linear-consistent-read-implementation/\n客户端发送请求的时候，如何知道集群中的 Leader 节点是哪个？ 在没有任何前置条件的情况下，客户端会轮询集群中的每个节点并发送请求，如果非 Leader 节点收到请求，会返回一个错误给客户端。客户端然后挑选下一个 server 进行重试，直到得到了正确的响应。\n然后会将 Leader 节点的 id 保存起来，下次发送请求的时候，优先选择这个节点发送。\n如果 raft 集群的 Leader 节点发生故障，客户端如何处理？ 对于一个可容错的分布式 KV 系统，需要能够应对这种故障发生，并且在多数节点正常的情况下，需要依然提供服务。\n得益于 raft 共识算法的特性，在某个节点故障后，其他节点会由于收不到心跳消息而超时，并重新发起选举。\n所以客户端会在得不到正常响应的时候轮询重试，直到 raft 集群中的 Leader 节点重新选举完成并提供正常服务。\n如何处理客户端的重复请求？ 如果客户端的请求已经提交，但是 server 返回的过程中结果丢失，那么客户端会发起重试，导致这个请求在状态机中被执行了两次，会违背线性一致性。\n因此我们需要保证客户端的请求只能被状态机应用一次，我们可以维护一个去重哈希表，客户端 ID + 命令 ID 组成一个唯一的标识符，如果判断到命令是已经被执行过的，则直接返回对应的结果。\nShardkv 的问题：为什么需要对分布式 KV 系统进行分片？ 一是单个 raft 集群实际存储数据的引擎是单机的，能够存储的数据量有限。二是在不分区的情况下，所有数据的读写请求都会在一个分片中，这在并发量较大的情况下可能存在一定的瓶颈。\n如果对数据做了分区，那么不同分区之间的数据读写请求是可以并行的，这能够较大的提升 KV 系统的并发能力。\nShardkv 的配置怎么保存？ Shardkv 的配置是单独保存在一个服务中，客户端会向这个服务发起请求，查询 key 所属的 shard 应该在哪个 raft 集群中，并向这个集群发起请求。\n配置服务也需要高可用特性，因为配置服务如果发生故障不可用的话，那么整个分布式 kv 服务都会无法提供服务，因此也使用 raft 算法保证高可用，构建了一个单 raft 集群来存储配置信息。\nShard 数据如何迁移？ 启动一个后台定时任务，定期从配置服务中获取最新的配置，如果检测到配置发生变更，则变更对应 shard 的状态，标记为需要进行迁移。\n同时启动另一个后台定时任务，定期扫描 shard 的状态，如果检测到需要进行迁移的 shard，则发送消息，通过 raft 模块进行同步。然后在 Leader 节点中处理 shard 迁移的请求，将 shard 数据从原所属的 raft 集群中迁移到新的集群中。\nShard 迁移的时候，客户端的请求会受到影响吗？ 如果客户端请求的 key 所属的 shard 并没有在迁移中，那么可以正常提供服务。\n否则，说明客户端请求的 key 在迁移中，则返回错误，让客户端进行重试。\n如果有并发的客户端请求和 shard 迁移请求，应该怎么处理？ 客户端请求和 shard 迁移请求的确存在并发情况，如果处理顺序不一致，会违背线性一致性。\n我们将 shard 迁移的请求也传入到 raft 模块进行同步，这样和客户端的请求是一致的，利用 raft 的一致性来保证两种不同请求的先后顺序，前面的执行结果一定对后续的请求可见。\n如果某个 Shard 已经迁移了，那么它还会占存储空间吗？ 不会，我们实现了 shard 清理的完整流程，会启动一个后台定时任务，定期扫描 shard 的状态，如果检测到 shard 是需要进行清理的，则也会发送 shard 清理消息进行处理。\n参考资料 Paxos vs Raft：https://ics.uci.edu/~cs237/reading/Paxos_vs_Raft.pdf TiKV Raft 的优化：https://zhuanlan.zhihu.com/p/25735592 Raft FAQ：https://pdos.csail.mit.edu/6.824/papers/raft-faq.txt ","date":"2024-03-04T22:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E5%88%86%E5%B8%83%E5%BC%8F-kv-%E9%9D%A2%E8%AF%95%E6%B1%87%E6%80%BB/","title":"分布式 KV 面试汇总"},{"content":"自从去年 11 月份上线《从零实现分布式 KV》课程以来，得到了一些朋友的支持。 目前三个多月过去了，在我们的共同努力下，《从零实现分布式 KV》课程已经全部完结！\n课程详情，以及购买方式，可查看这个链接：https://av6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5b\n课程包含实现一个分布式 KV 系统的全部流程，从 raft 共识算法，到分布式 KV 的实现，再到 shardkv 的实现，都有详细的讲解和代码实现。\nraft 算法部分，从论文讲解，再到具体的代码实现，包含 raft 的核心模块，例如 Leader 选举、日志复制、日志压缩。分布式 KV 部分，基于 raft 实现了完整的分布式系统，并且实现了分片的分布式 KV，分片迁移、分片清理等内容。\n我们按照循序渐进的方式，将内容拆分成了独立的部分，总共 35 小节内容，让大家能够一步一个台阶，从零开始掌握分布式理论和系统实践！\n可以毫不谦虚的说，这应该是目前全网最详细的 raft 和分布式 KV 教程了！\n以下是课程的完整目录：\n看到我们做的教程真正的能够帮助到别人，其实是一件很开心的事情。\n也希望能够帮助到更多的人，让大家能够更深入的理解分布式理论和实践，给自己的简历上增加一个亮点，并且为自己的职业发展提供更多的可能。\n强烈推荐感兴趣的同学可以入手学习起来，假期想充充电的话，这就是和别人拉开差距的最好时机，或者可以假期好好玩玩，之后再冲刺一波！\n","date":"2024-01-27T10:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F-kv%E8%AF%BE%E7%A8%8B%E5%AE%8C%E7%BB%93/","title":"从零实现分布式 KV——课程完结！"},{"content":"自从去年 11 月份上线《从零实现分布式 KV》课程以来，得到了一些朋友的支持。\n课程详情：https://av6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5b\n目前课程按照原定计划稳步更新中，目前已经更新到了第 20 节，raft 部分的实现已经全部完成。\nraft 这一部分，是分布式理论的重要基础，我们采用了循序渐进的方式，从论文讲解，然后再到代码实现，构造了完整的 raft 共识算法。\n主要分为了四个部分：\nLeader 选举 日志复制 日志持久化 日志压缩 每一部分都有详细的理论讲解，并且拆分成了多个小节，每个小节都有对应的代码实现，可以说是保姆级的手撕 raft 教程了。\n下面是课程的详细目录：\n后续会持续更新分布式 KV 和 shardkv 部分，预计 2024.1 月底前就能更新完成。\n想要上车的同学，请直接联系我，有其他任何疑问都可以进行咨询。\n","date":"2024-01-07T22:51:56+08:00","permalink":"https://blog.roseduan.cn/p/%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F-kv%E8%AF%BE%E7%A8%8B%E6%9B%B4%E6%96%B0/","title":"从零实现分布式 KV——课程更新"},{"content":"工作 去年六月份加入到现在的公司，目前已经一年多了，今年全年的时间，逐步深入的参与到数据库内核的一些 feature 开发中来，做了非常多的事情，包括：\n支持数据库中的并发创建索引（Create Index Concurrently 和 Reindex Concurrently），主要是参考了 Postgres 的做法，达到的基本效果是在索引创建和重建期间，不阻塞数据表的 DML 操作 为数据库引入了 pgvector 和 kafka fdw 插件，支持向量搜索和 Kafka 外部数据源 还有一个重要的工作，在数据库中新增了 Create/Alter/Drop Task 的功能，主要是支持定时任务的执行 当然还有最重要的，今年下半年开始参与，也是目前还在进行中的，那就是参与到公司数据库产品的存算分离中来，我们对传统的基于 Postgres 的 MPP 数据库的架构进行了大幅的调整，原来的 Postgres 进程充当了计算节点，不存储数据，支持计算集群 warehouse 的创建、水平扩展、高可用；将元数据进行了拆分，数据存储部分进行了全新设计，接入了对象存储，支持多种文件存储格式。 目前也在设计和开发云原生架构下的一个重要功能，那就是 Time Travel， 支持访问一个数据表的任意时刻的历史数据。 总体来说，相较于去年，在内核方面有了更多的积累，当然数据库内核本身非常庞大、复杂、对技术能力要求较高，想要继续深入，需要花费不少的时间，希望在明年能够投入更多的时间去研究。\n也非常感激我现在的 Team Leader，虽然他平时各种繁杂事务缠身，但总是耐心地给了我很多的帮助，不遗余力的引导我去解决工作中遇到的一些难题。\n开源 今年在开源项目的运营方面，时间主要集中在年中的几个月，虽然投入精力比较有限，但是做出的改动却非常大。\nrosedb 开始进入全新的 V2 版本，这主要是由于自己在数据库方面积累的知识越多，不得不重新开始审视自己之前做过的事情。 发现了很多 rosedb 之前的一些问题，并进行了一次大的重构。\n主要是将原来 rosedb 之上的 Redis 数据结构的支持去掉了，并且重写了数据文件部分，目前 rosedb 专注于成为一个轻量级的 KV 存储引擎。\n今年也收获了一些用户，开始有人将 rosedb 部署到生产环境中使用，看到自己写出的项目能够去解决实际的问题，是一种很不错的感觉。 star 数从去年的 3.5k 涨到了现在的 4.2k：\nLotusDB 也一样，将原来的架构进行了大刀阔斧的改造，基本是完全重写了一遍，也发布了 V2 版本。\n由于个人的精力有限，LotusDB 的重构工作，是在和两位社区小伙伴的共同努力下完成的。\nLotusDB 的 star 也从去年的 1k 到现在的 1.9k，基本上翻了一倍。\n明年针对 LotusDB 项目，我会打算组织团队运营，制定一些规范化的开发流程、文档、Release 日志、CI、Code Review 等等，就算做慢一点也没关系，主要是能够持续的运营下去。\n今年也开源了两个新的项目，都是在重构 rosedb 和 LotusDB 的时候，抽取了一些通用的组件开发然后开源的。\n一个是 WAL，Write Ahead Log，也就是预写日志，写这个组件的时候，是在重构 rosedb 期间，考虑到写日志是一个通用的需求，不仅 rosedb 会用到，LotusDB 也会用到，其他的基于 LSM 或者 Bitcask 的存储引擎，可能都会有这个需求。\n于是我在五一假期期间，花了几个小时，写出了一个最初的版本开源了出去，后面也是按照实际需求在更新这个项目。\n第二个是 diskhash，基于磁盘的哈希表，主要考虑到像 Bitcask 这种存储引擎，重启的时候需要去全量加载索引，数据量很大的话重启很耗时，于是我开发了基于磁盘的哈希索引结构，花了某个周末的一天时间开发然后开源。\n但是后面并没有接入到 rosedb 中，因为改造起来比较繁琐，比我预想的麻烦得多，而且想到 rosedb 的定位，还是让它专注成为一个轻量、适合少量数据的引擎。\n对于大数据量就可以使用 LotusDB，于是将 diskhash 接入到了 LotusDB 里面，使其成为了目前唯一一个支持 BTree 和 Hash 双索引的 KV 数据库。\n在开源方面，其实今年有非常多的想法，但是无奈时间和精力有限，很多想法都搁置了。\n比如我重构 rosedb 的时候，将原来的 Redis 数据结构拆了出来，原本是打算出一个新的开源项目，在 KV 之上去构建 Redis 的数据结构，并且支持 Redis 协议，底层就可以接入不同的 KV，比如 RoseDB、LotusDB、Pebble、Badger 等等，但是一直没时间去继续推进。\n还有一个想法是基于 LotusDB 做一个搜索引擎项目，主要是对一个现有的项目进行改造，并且更换存储引擎，但是也搁置了。\n还有其他的一些事情，比如写一个 Rust 的版本 WAL，运营 LotusDB 公众号，都因为没有太多的时间投入而全都搁置。\n这些事情看明年的具体情况去抽取一些继续做了，如果有同学有兴趣做这些项目的话，可以联系我，我会给与你帮助与支持。\n教程 今年还写了两个教程（付费），一个是《从零实现 KV 存储》。\n主要是将我过去这几年，在存储引擎方面的知识进行了总结和回顾，并且从零开始，实现了一个兼容 Redis 数据结构和 Redis 协议的数据库，当然这个项目主要是出于教学的目的。\n教程也帮助到了非常多的人，有的人拿去面试，获得了不错的 offer，有的人将学到的项目进行增强完善，开源出去也获得了成百上千 star，给自己的履历增加了一个很大的亮点。\nhttps://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3gw02agegxg3.feishu.cn\n第二个教程是《从零实现分布式 KV》，和知名博主「青藤木鸟」进行合作，将自己之前的分布式相关的经验进行了系统的总结，并且基于 MIT 6824 课程，专注于代码实现层面。\n对我自己来说也是一次总结输出，也希望这个教程能够帮助到更多的人，特别是这几年互联网就业环境非常不理想的情况之下。\nhttps://av6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5bav6huf2e1k.feishu.cn\n好了，这就是我在 2023 年做的一些事情，目前毕业已经四年多了，虽然做了不少自己感兴趣的事儿，但是对一些事情仍然感觉到迷茫、疑惑，甚至焦虑，或许这是人生之路上无法避免的吧。\n","date":"2023-11-23T14:23:11Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84-2023-%E5%B9%B4%E5%85%B3%E4%BA%8E%E5%B7%A5%E4%BD%9C%E5%BC%80%E6%BA%90%E5%89%AF%E4%B8%9A/","title":"我的 2023 年，关于工作、开源、副业"},{"content":"上一回说到，在从业务转到基础架构岗位之后，我开发了自己的第二个开源项目 LotusDB，并且因为一些原因，我在 2022 年中，也就是去年上海解封之后，开始打算看看新的机会了。\n这一次跳槽我倾向的是各种基础架构的岗位，但具体做什么，其实并没有明确的想法，毕竟去年的就业环境其实就已经不太好了，能够顺利找到一份工作就非常不错了。于是我开始在上海投递简历，接连面试了好几家公司，比较幸运的拿到了其中一些数据库内核开发的岗位机会。\n我虽然凭借两个开源项目，有一些存储方面的基础知识，但是对于关系型数据库、分布式数据库，了解的东西甚少，基础其实也比较薄弱，但是得益于公司急需数据库内核方面的人，就算没有太多经验的也可以培养，于是我就比较幸运的转到了数据库内核。\n在面试的过程当中，我的两个开源项目对我的帮助非常大，很多面试官都比较的感兴趣，面试的内容也主要围绕开源项目的一些设计细节来展开，所以其实现在回过头来看，幸亏当初我不懈的坚持、折腾，终于等来了这次机会，并且抓住了这次机会。\n所以这里也稍微感慨一下，如果你对某个领域感兴趣，那么可以坚持下去，坚持不懈的折腾，尽管在前期是可能没有任何收益，甚至会感觉到痛苦、懈怠，但是当一段时间之后，如果面对一些非常不错的机会，没有一些前期积累的话，你是很难抓住这种机会的，只能眼睁睁看着它溜走。\n转到数据库内核之后，我开始了这方面的学习，以及适应新的工作，在开源项目上面花的时间就比较少了。\n但是在今年初对开源项目做了一个非常大的变动，那就是随着我自己在这方面积累的知识越来越多，我开始重新审视自己做过的东西，发现了很多的问题，于是开始了一次次重构，将 RoseDB 和 LotusDB 都更新到了 V2.0 的版本，在架构上做出了非常大的改动，几乎就是完全重写了。\n这其实就是开源的魅力之一，不仅仅能够积累自己的影响力，还能够促进自己的工作，然后自己的技术能力和知识有了更多的积累之后，又不断地反哺开源，不断完善开源项目。\n今年又发生了一些重要的事情，比如我开始更多的深入到数据库内核开发当中，并且开始撰写自己的课程，把自己所学的知识传播给更多的人，让自己的经历能够影响和激励更多的人。\n我的编程故事，写到这里，就暂时告一段落了。这一次系列文章，主要是回顾了自己工作四年多以来的一些经历，从自学编程转码，到转行基础架构和数据库内核，然后折腾自己的开源项目，算是一个复盘了。\n我的编程故事—7 第二个开源项目\n我的编程故事—6 转岗 \u0026amp; rosedb持续维护\n我的编程故事—5 Java 到 Go，开源 rosedb\n我的编程故事—4 第一份工作\n我的编程故事—3 秋招之旅\n我的编程故事—2 决定自学\n我的编程故事—1 上大学了\n当然后面还会时刻回顾自己走过的路，也希望我的经历能够让大家若有所思，不断激励着你前行。\n","date":"2023-11-20T18:29:33Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84%E7%BC%96%E7%A8%8B%E6%95%85%E4%BA%8B8-%E6%95%B0%E6%8D%AE%E5%BA%93%E5%86%85%E6%A0%B8/","title":"我的编程故事—8 数据库内核"},{"content":"在众望所归之下，前两天终于出了一个全新的课程《从零实现分布式 KV》，大家的学习热情都非常高涨，其中有很多同学都问到了一个共同的问题，那就是这个课程和我之前的《从零实现 KV 存储》有什么区别呢？\n这一次就专门给大家解释一下。\n其实说起来也比较简单，《从零实现 KV 存储》实现的是一个单机 KV 存储引擎，何为单机？一般指的是在一个 server 上的单个进程里运行的数据库，其主要解决的问题是数据如何存储到持久化存储介质中，比如最常见的磁盘。\n所以我们会设计存储到磁盘上的数据会怎么进行组织，磁盘上的文件格式是什么样的，然后会考虑怎么才能够更加高效的从磁盘读取数据，减少磁盘 IO 次数。所以单机存储引擎更加专注于数据存储到磁盘的具体实现方法，并且要尽量保证数据不丢失。\n常见的单机 KV 存储模型有 B+ 树、LSM 树、Bitcask，使用这些模型实现的单机 KV 引擎有 LevelDB、RocksDB、BoltDB、Badger、Pebble、RoseDB 等等。\n那么《从零实现分布式 KV》 课程又实现的什么呢？\n分布式 KV，其重点在于分布式。前面说到了，单机 KV 是在一个 server 上运行的，如果这个 server 出现了故障，或者磁盘损坏了导致了数据丢失等情况，那么这个数据库一是不能够响应用户的请求，二是存储在其中的数据有可能损坏，并且如果我们没有备份的话，数据就永远丢失了，会造成比较严重的后果。\n所以分布式就能够比较好的解决这个问题，利用最朴素的思想，不要把鸡蛋放在同一个篮子里。既然数据存储在一个 server 上有非常大的问题，那么我们将数据拷贝出来，存储到不同的 server 上不就好了？\n这样每个 server 上的一份数据一般叫做一个副本（Replica），如果一个 server 出现了故障，还有其他的数据副本可以继续使用。\n但是数据有了多个副本之后，随之而来又带来了新的问题，那就是写数据的时候，应该写到哪个副本里面？还是全部的副本都写一遍？读数据的时候，应该从哪个副本去读？如果副本之间的数据不一致了怎么办？\n等等，这些问题抽象出了一个新的概念，那就是共识，即让多个副本之间协调一致，统一对外提供服务，并且保证数据的完整一致性，我们需要一些手段来让多个副本之间达成共识，这一般称之为共识算法，常见的有 Paxos 和 Raft，而我们课程中实现的是 Raft 算法。\n有了共识算法之后，我们可以在这个基础之上构建分布式、高可用的系统，而课程中实现的是最常见的分布式 KV 系统，每个 server 之上，都会使用 Raft 共识算法来保证多个副本之间的一致性，然后每个 server 本地都会维护一个存储数据的单机 KV，这个单机 KV 一般叫做状态机。\n常见的分布式 KV 系统有 TiKV、ETCD、FoundationDB 等等。\n所以现在大家应该就清楚了，分布式 KV 重点在于分布式算法，以及分布式系统的设计与实现，并且只是用到了单机 KV 来存储本地数据，而存储数据、磁盘数据组织的具体逻辑，是交给了单机 KV 去负责。\n这两个课程的学习有先后顺序吗？\n这也是问的比较多的问题，实际上并没有先后顺序，所以先学哪个都是可以的，也都能够学懂，彼此都是独立的内容。\n最后，感谢大家的支持，希望这个课程能够对大家有所帮助，附上课程链接，想要购买者可查看：\n《从零实现 KV 存储》\nhttps://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g\n《从零实现分布式 KV》\nhttps://av6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5b\n","date":"2023-11-19T10:15:33Z","permalink":"https://blog.roseduan.cn/p/%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0-kv-%E5%92%8C%E5%88%86%E5%B8%83%E5%BC%8F-kv-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB/","title":"从零实现 KV 和分布式 KV 有什么区别？"},{"content":"今年初上线了我的第一个课程《从零实现 KV 存储》，广受好评，在这段时间里，有非常多的同学咨询我是否有意向去开设一个分布式 KV 的课程，毕竟之前的课程只专注于单机 KV 的实现，对于想要更进一步了解分布式存储的同学来说，分布式系统的理论和实践也是必不可少的。\n于是我联合知名博主「青藤木鸟」一起，经过几个月的精心打磨，为大家带来了这个全新的课程，希望能够帮助到大家深入理解分布式理论，深入实践分布式系统的设计与实现，不断提升自己，增强自身的技术竞争力。\n以下是此课程的详情，大家也可以直接进这个链接查看课程的内容以及购买方式：\nhttps://av6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5bav6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5b\n本课程基于 MIT 6.5840（前 6.824，主要复用了课程实验的代码框架和测试），参考各种资料，结合我们的一些工业界经验和多次实现相关实验的经验，按易于理解的方式，拆成循序渐进的模块，每节只关注一小部分，以图文、代码和视频的形式呈现。\n本课程会手把手教你如何弄懂一个共识协议，以及基于共识协议的分布式 KV 的方方面面、各种细节；也会教你如何组织和写出漂亮的工程代码。分布式系统是当今主流互联网系统的基础架构，而共识协议又是其中的典型代表和基石中的基石。\n学习本课程，能让你对分布式系统所面临的问题、所使用的技能有一个全面和深入的认识。\n关于作者 青藤木鸟，计算机本硕，有多年大厂、外企和创业公司的 infra 从业经验。专注大规模数据系统，包括分布式系统、数据库、存储和数据处理。以“木鸟杂记”的网名在知乎、b 站和公众号等平台，进行分布式系统和数据库相关知识持续输出。写过几十万字来翻译和分享分布式、数据库方面的经典书籍 DDIA。《分布式数据库论坛》创建者。现有维护专栏：《DDIA学习会》和 《系统日知录》。\n**roseduan，**数据库内核开发工程师（Greenplum 和 Postgres 内核），专注于数据库、分布式、存储引擎等领域，有着丰富的实践经验，知名开源存储引擎 rosedb 和 lotusdb 的作者，累计约 6k star。\n学习方式 建议按照章节顺序来看，每章先看文档内容，然后再看对应的视频，跟着实现代码，加深理解。如果你对一些前置知识和某些章节很熟悉，也可以快速跳到你关心的章节来看。\n也可以结合 MIT 6.824 的 lecture 进行辅助学习：\nhttps://www.youtube.com/@6.824。\n其他的参考资料，我们会列在每节的文末（文中也会在相关文字直接关联一些超链接），大家可以按需自行取用。\n课程大纲 课程主要分为三部分内容：\nraft 共识算法的实现 基于 raft 的分布式 KV 基于 multi raft 的 shardkv 以及一些附录内容，比如分布式调试、并发编程等，这些技术在课程中会经常使用到，基础薄弱者可以多学习一下。\n之后还会根据根据同学们的需求和反馈添加一些加餐内容。\n课程大致目录如下：\n试看内容 \\01. Raft 论文演绎\n\\04. PartA 状态转换\n适用人群 这个课程对以下同学应该都非常的合适，包括但不限于：\n想入门数据库内核的同学，分布式 KV 是现代分布式数据库中必不可少的重要组成部分，它为数据库提供数据存储、高可用性、横向扩展等保障，并且具备高性能和可伸缩性，学习课程可以帮助理解分布式数据库的技术核心。 想入门分布式存储的同学，从零实现一个分布式系统，加强对分布式相关知识的理解，而不仅仅限于理论知识，掌握如何构建、调试和优化分布式应用。 增加 Go 项目经验的同学，如果学习了一些 Go 的基础知识，但是苦于没有项目经验，想要进一步巩固自己的知识，这个项目将会非常硬核，用于面试也会极具亮点。 想要巩固基本功的同学，基本功对一个程序员来说非常重要。但是平常的一些课程，例如编程语言、数据结构、算法、多线程编程、分布式理论等，学完了之后总是没有太多的使用场景来实践，没过多久就忘记了。这个课程当中涉及到手写共识算法 raft、代码组织、代码命名、分布式理论、并发编程、并发调试等知识，可以帮助你打牢基本功。 做毕业设计，对于即将毕业的同学，苦于无法找到一个合适的毕设项目，基于这个项目做一些衍生处理（比如可视化）应该会让老师眼前一亮。 前置知识 本课程只需要对 Golang 有一个最基本的了解：懂基本语法，能看懂代码。不需要其他特别的前置知识，录屏的视频会带着大家一步步敲代码。如果有大家有觉得铺垫不清楚的地方，随时给我们反馈，我们会增加相关前置章节。\n更新进度 目前已经更新了 7 节内容，预计后续每周将会更新 1-2 节，大概 2024.3 月更新完毕。\n常见 Q\u0026amp;A 购买后在什么平台学习？\n课程内容都在飞书云文档，购买成功后，为你开启对应的权限，所有内容都可以在线观看\n课程时长？代码量？\n以下皆为当前预估，以最后课程完成时状态为准。 课程视频时长：Raft 部分大概 6 小时、分布式 KV 部分大概 10 小时。 课程代码：自己需要写的部分，Raft 部分和 KV 部分大概各 1k 行；总 repo（包含测试代码、其他工具性代码）大概 10k 行。\n课程代码是什么语言实现的？\n课程基于 MIT 6824（现已更名为 6.5840） 的实验代码，因此是由 Go 语言实现的。后续根据同学需求和我们的精力有可能会支持其他的语言，比如 Rust 和 C++ 等。\n课程和 MIT 6824 有什么区别，或者联系？\n与 MIT 6.824 的联系是，课程复用了实验代码的框架和测试，因此课程内容和公开课 Lab 基本对应。 与 MIT 6.824 的区别是，本课程更侧重代码实践，只专注 Raft 和分布式 KV 代码的实现。为此我们通过详尽的前置知识铺垫、手敲代码录屏、代码级文档、多种答疑形式来确保你能在代码的级别理解 Raft 这个共识算法和基于 Raft 的分布式 KV 的方方面面。\n如何获取项目中的代码？\n购买成功后将会开启代码 pull 的权限，也可以到课程专属飞书用户群中下载。\n文档和视频对应的吗？\n涉及到写代码的部分，都会有对应的视频。\n没有任何分布式系统和存储的基础，能学会吗？\n当然可以，本课程只需要熟悉 Go 的语言基础就可以了。其他的内容，例如论文讲解、架构设计、代码组织，都会在课程中详细讲述，你也可以在文档中、用户群中进行咨询，完全可以学会。\n购买后是否有有效期？\n没有，购买后永久持有，无限次观看。\n感谢大家一如既往的支持，也希望这个课程能够真正的帮助到大家，想要购买的同学，添加我下面的微信，有任何疑问都可以进行咨询。\n最后再附上课程详情链接：\nhttps://av6huf2e1k.feishu.cn/docx/JCssdlgF4oRADcxxLqncPpRCn5b\n","date":"2023-11-14T10:15:33Z","permalink":"https://blog.roseduan.cn/p/%E9%87%8D%E7%A3%85%E5%87%BA%E7%82%89%E4%BB%8E%E9%9B%B6%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F-kv/","title":"重磅出炉！从零实现分布式 KV！"},{"content":"上一回说到，我从业务开发转到了分布式存储的岗位，前后主要参与了组内的数据库代理（DB Proxy）和分布式 KV 存储组件的开发和维护。从一个纯粹的 CRUD 业务开发转到了自己梦寐以求的基础架构岗位，心里还是非常兴奋的。\n这段时期内主要接触和学习了公司内生产级的分布式 KV 和单机存储引擎都是什么样子的，给自己涨了很多见识，以及学习了很多这方面的专业知识。\n这一段时间也激发了我新的创造灵感，让我开始了自己的第二个开源项目 LotusDB。\n在一天上班的路上，我突发奇想，在存储引擎方向，能不能结合 B+ 树和 LSM 树的优势，毕竟一个是读稳定，一个是写吞吐更好的，有没有什么项目是结合了这两个 idea 的呢？\n有了这个想法之后，我便开始了初期的调研，主要是在网上搜索相关的内容，发现了有一些类似的概念，例如在 usenix 上有一篇名为 SLM-DB 的论文和我的想法大致是类似的。\nhttps://www.usenix.org/system/files/fast19-kaiyrakhmet.pdf\n有了这个理论支撑之后，我便有了更多的底气，然后在 Github 上也没有找到同类型的项目，看起来是没有人去做的。\n于是我召集了几个志同道合的同学，加我总计四人，开始了这个项目的设计和编码，最终项目的 demo 版本在 1 个月左右的时间就做出来了，主要是复用了之前 RoseDB 的一些代码，并且利用了一些现成的组件，在最短的时间内将我最初的想法进行了实践和验证。\n当然，这个项目一开始就是开源的，初期也获得了很多的关注，我之前在公众号还专门写过文章去进行介绍，然后也上过几次 Github Trending 榜单。\n后来架构方面也进行了一些演进，目前已经到了最新的 V2.0 版本，star 也已经 1.8k 了，只是目前这个项目到生产环境还有一段的距离，后面会持续进行迭代。\n在工作方面，算是中规中矩的进行着，到了 2022 年上半年的时候，在上海经历了几个月严重的疫情肆虐，开始了很长一段时期的居家办公。\n2022 年中，我在第二家公司已经待了一年办左右，解封之后，长期的居家办公让我身心俱疲，加上其他的一些原因，让我有了看看新机会的想法。于是我开始了又一次跳槽，转到了数据库内核开发的岗位，这一次找工作的细节，留待下次再细说。\n","date":"2023-10-25T18:29:33Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84%E7%BC%96%E7%A8%8B%E6%95%85%E4%BA%8B7-%E7%AC%AC%E4%BA%8C%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","title":"我的编程故事—7 第二个开源项目"},{"content":"经过差不多两个月的重构，在社区小伙伴的共同协作努力下，LotusDB V2.0.0版本正式发布！\nLotusDB 项目地址：https://github.com/lotusdblabs/lotusdb\nLotusDB 是用 Go 语言编写的 KV 数据库，它采用 KV 分离的思想，融 LSM Tree 和 B+ Tree 存储模型为一体，适合大规模的 KV 数据存储，相较于 Go 语言领域知名的 KV 存储项目 badger 和 bbolt，LotusDB 具有更先进的设计架构。\n这次我们通过两个月的重构，将 LotusDB 原来 V1 版本的代码基本上重新实现了一遍，目的在于提高简洁度。\nLotusDB 的整体架构实现了全新升级，目的在于提升读写和压缩性能，下面是之前 V1 版本的架构图：\n这是重构后的 V2 版本的架构图：\n相对于 V1 版本，V2 版本主要有以下更新：\n新版本的所有磁盘存储（包括预写日志 WAL 和 Value Log）均直接采用 wal 组件（https://github.com/rosedblabs/wal）实现，代码屏蔽了底层管理文件读写的操作，更加简洁易懂 新版本通过对 key 哈希分片的方式构建多个 Index 和 Value Log 对象，实现了 Index 和Value Log 的并发读写，读写和压缩性能相对于 V1 提升了 2~3 倍，这也是本次重构架构上最大的更新 TODO：另一大重要更新是将会引入新的基于磁盘的 Hash 索引，这样 LotusDB 将会支持磁盘 BTree 和 Hash 两种类型的索引，满足更多样化的场景（预计在下一次 Release 发布） 新版本舍弃了 Column Family（CF） 的设计，目的在于使架构更加简洁，原来跨 CF 的操作并不能保证原子性，如果后续有需求的话将会重新设计这个 feature 毫不夸张的说，这是目前 Go 语言领域架构最先进的 KV 存储引擎项目，其优势主要体现在：\n结合了 LSM 和 B+ 树存储模型的优点，B+ 树读性能稳定，而 LSM 写吞吐高，LotusDB 在这基础上做了一个巨大的改动，就是采用 KV 分离的思想，完全舍弃掉 LSM 中的 SST 文件，改由 B+ 树来存储索引，而 value 存放则参考了 Wisckey 和 bitcask 模型的设计，以追加写的方式存储到单独的 value log 文件中，充分利用顺序 IO 的优势 更加均衡和快速的读/写性能，写入 LotusDB 的数据不需要在磁盘上排序，追加写的 Value Log 设计在写入过程中减少了磁盘磁头的移动，因此即便是完全无序的数据，LotusDB 依然能够保持较高的吞吐量。同时 LotusDB 的读写性能更加均衡，而不会像bbolt 那样读写性能差距极大，适合读写都非常频繁的业务场景 比典型的 LSM 低得多的读放大，LotusDB 采用 KV 分离的思路，从 B+ 树中获取 Key 对应 Value 在磁盘中的存储地址后，即可直接一步读取 Value 数据，显著降低了传统 LSM 存储模型带来的读放大问题 更大规模的数据存储，和 LotusDB 的姊妹项目 RoseDB 相比，LotusDB 是一个基于磁盘索引的 KV 存储引擎，而 RoseDB 是 Bitcask 存储模型全内存索引，因此数据存储规模基本不受到内存大小的影响，因此更加适合大规模的数据存储 支持崩溃恢复，LotusDB 实现了 WAL 机制，在将 KV 数据刷盘前就将事务日志进行持久性存储，数据库崩溃后依然能够从 WAL 中读取事务日志，保证事务的原子性 数据并发式刷盘 LotusDB 通过将 key 进行哈希分片的机制，建立多个 Index 和 Value Log 对象，每一组 Index \u0026amp; Value Log 对象对应一个数据片，当 memory table 中的数据需要刷盘时，多个对象可以通过并发的方式实现刷盘，大大提升了刷盘的吞吐量 以下是 LotusDB 的简单使用示例，大家可以上手体验！\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package main import \u0026#34;github.com/lotusdblabs/lotusdb/v2\u0026#34; func main() { // 指定设置 options := lotusdb.DefaultOptions options.DirPath = \u0026#34;/tmp/lotusdb_basic\u0026#34; // 打开数据库 db, err := lotusdb.Open(options) if err != nil { panic(err) } defer func() { _ = db.Close() }() // 写入键值对 key := []byte(\u0026#34;KV store engine\u0026#34;) value := []byte(\u0026#34;LotusDB\u0026#34;) putOptions := \u0026amp;lotusdb.WriteOptions{ Sync: true, DisableWal: false, } err = db.Put(key, value, putOptions) if err != nil { panic(err) } // 读取键值对 value, err = db.Get(key) if err != nil { panic(err) } println(string(value)) // 删除键值对 err = db.Delete(key, putOptions) if err != nil { panic(err) } // 启动Value Log压缩 err = db.Compact() if err != nil { panic(err) } } 致谢\n在 LotusDB 的重构过程中，非常感谢社区的各位小伙伴的参与和积极贡献，特别是 @燕小七 和 @akiozihao 表现最为积极活跃，为 LotusDB 2.0 的重构做了很多工作，再次表示感谢，也希望能够继续参与。\n同时 @燕小七 同学成为了 LotusDB 的第一位 Committer，后续我们希望能够培养更多的 Committer 和 Maintainer，充分发挥社区的作用，共同打造和完善 LotusDB 这个最先进的 KV 存储引擎！\n同时也非常欢迎大家能够参与进来，目前我们只是发布了第一个版本，后续将会持续迭代，能够让大家发挥的空间巨大，感兴趣的可以加我微信私聊，我会将你拉到开发者群当中。\n","date":"2023-09-14T09:57:33Z","permalink":"https://blog.roseduan.cn/p/go-%E8%AF%AD%E8%A8%80-kv-%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E-lotusdb-2.0-%E9%87%8D%E7%A3%85%E5%8F%91%E5%B8%83/","title":"Go 语言 KV 存储引擎 LotusDB 2.0 重磅发布！"},{"content":"上一次说到，毕业一年多之后，我经历了一次跳槽，从 Java 也转到了 Go 语言，从事普通的后端开发工作。\n在工作之余，我还是会在自己的业余时间写写 rosedb 项目，当然这仅仅是一些兴趣而已，并且在 Github 基本没有获得任何的关注。\n后来，我在自己的公众号上写了一篇文章，名为我写了一个数据库。。。，稍微有点标题党，但也不算太偏题，就是这篇文章带来了一些初始流量的积累。\n那时候我的公众号也就几百人，写出来之后，我就在很多的群里都转发，期望获得更多的曝光，可能是因为这个标题比较吸引人，还是获得了不少的关注，并且承蒙不少人的支持，点了一些 star，这样 rosedb 的关注后面就越来越多了。\n基于此，我还在 B 站录制了一个系列视频，讲述了这个项目的大致结构，以及一些设计的要点。\n然后我还到 Go 夜读做了一期分享，讲述了这个项目的大致设计和源码。\nhttps://www.bilibili.com/video/BV1ih411h7yC\n几波操作一下来，rosedb 就吸引了更多人的关注，后面就频繁的登上了 Github 的 Trending 榜单，当时我还专门写了文章做个纪念。\nrosedb 连续两天上榜\n然后在 21 年 6 月份的时候，也就是开源七个月之后，rosedb 的 star 数就到了 1000。\n七个月，从零到一千\n获得了这些关注之后，对于我自己其实是非常大的鼓励，当然也感觉到很意外，完全没想到会达到这样的效果，这也刺激了我在这个领域去深耕，继续钻研。\n当然我的工作还是普通的后端开发，直到有一次，我参与了公司内部的一次技术分享，让我了解到公司的基础架构部，是有在做分布式 KV 相关的内容的，这也引起了我极大的兴趣，于是想着能不能内部转岗过去，关于这次转岗的经历，我之前也写过文章记录，这里就不再赘述了。\n转岗记\n最近其实也有很多人咨询我，关于如何从业务开发转到其他基础架构相关方向的，但是我的经历具有很大的偶然性，并不具有特别的参考价值，因为当时以我自身的水平，如果专门出去找存储相关的工作的话，可能还是很困难的。\n但还是可以提取一些通用的建议给更多有同样需求的人，首先就是兴趣非常重要，这能够驱使你即使在下班后，或者其他闲暇时间能够投入更多的精力来做自己感兴趣的事情，在做这些事情的事情，在前期可能是见不到任何成效的，并且可能也并没有什么直接的收益。\n但是如果能够凭借兴趣和热爱坚持下去，或者也能看到一些曙光。\n然后比较重要的就是寻找正向反馈，比如我在做出 rosedb 项目之后，不遗余力的去宣传，尽可能的去获取更多的关注，当有更多的人关注到我的项目，我就能获得更多的成就感，才能够更好的坚持下去。\n现在回过来看，如果没有当时的坚持，或许 rosedb 也不会发展到现在，我也不会转到存储，然后一步步到现在做数据库内核。\n所以如果你有自己感兴趣的事情，尽可能去折腾和尝试，并且不断宣传出去让更多人知道，获得正向反馈，这样你的职业方向或许就能够得到更多、更好的发展。\n欲知我转岗到分布式存储之后的事情，以及 rosedb 的后续，且听下回分解。\n","date":"2023-08-27T18:43:33Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84%E7%BC%96%E7%A8%8B%E6%95%85%E4%BA%8B6-%E8%BD%AC%E5%B2%97-rosedb%E6%8C%81%E7%BB%AD%E7%BB%B4%E6%8A%A4/","title":"我的编程故事—6 转岗 \u0026 rosedb持续维护"},{"content":"上一回说到，在工作的闲暇之余，我学习了很多的东西，其中就包括了 Go 语言，那时候并没有明确的目的，也没有什么转行转语言的想法，可能仅仅是对技术的兴趣吧！\n学习之余，我发现了一些有意思的项目，比如 Go 语言领域知名的 boltdb，当时觉得非常的牛逼，特别是对还只会 CRUD 的我来说。于是心里盘算着能不能自己也写一个类似的东西，然后就发现了 bitcask 论文，具体这个论文是怎么被我找到的，我已经记不清了，可能是随便搜索的时候，点击了某个链接进去的。\n了解到 bitcask 存储模型之后，又知道了其他的一些基础的东西，比如 B+ Tree、LSM Tree，并且还了解到 nutsdb，也是一个国人维护的开源项目，这坚定了我也可以自己写一个的念头，别人都可以做到，我为什么不行呢？\n当时是我毕业工作后的一年多，在 10 月的国庆节假期期间，我决定看是否自己能够写出来。\n但踌躇满志的我很快便栽了跟头，论文的内容其实理解起来并不难，但当真正上手去写的时候，会发现完全无从下手，好几天下来也没憋出几行代码。后来我意识到是自己的实践能力还不是很足，于是对 boltdb、nutsdb 的代码细读了一下，花了大概一个多月的时间，然后在 12 月的时候，又花了一个月的时间写出了 rosedb 的第一个版本然后开源出去了。\n从这个图也可以看到其实 rosedb 就是从 2020 年底开始的，当然那时候开源出去，并没有什么影响力，仅仅是觉得有趣而已，代码质量也很一般，也就没什么关注的人了。\n也是在那时候，我有了跳槽的打算，毕竟已经毕业一年半了，想着能不能看看新的机会，试试换个更大点的平台。在找工作的时候，我的简历上写了 Java 和 Go 两种，都还算是比较熟悉了，所以 Java 和 Go 相关的工作岗位都在看。\n最后还是比较幸运的通过了哔哩哔哩的面试，在其中一个部门担任普通的后端开发职位，面试的过程当中，我开源的 rosedb 还算是一个不错的亮点，面试官可能觉得我还是一个对技术有热情，比较喜欢实践的人，有一定的加分。\n现在回过头来看，有一些运气使然，因为我在找工作的时候，偏偏遇到了 Go 语言相关的开发工作，这让我可以在以后的工作当中对 Go 更加熟悉，也可以继续在 rosedb 现有的基础之上，在工作之余，靠着自己多技术的一些热忱，然后把这个项目持续的完善下去。\n如果不是这样的话，假如我继续从事 Java 相关的工作，rosedb 可能并不会发展成现在的样子，因为我的工作并不是 Go 相关的，这个项目可能没多长时间就慢慢的置之不理了。\n总之，我找到了一份互联网中厂的 Go 开发岗位，从 Java 转到了 Go 语言，并且 rosedb 项目也刚开始开源出去，虽然并没有获得什么关注。但还是会在业余时间慢慢的维护，预知后面我是如何转到存储岗位，并且 rosedb 是如何获得进一步发展的，且听下回分解。\n","date":"2023-08-26T18:43:33Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84%E7%BC%96%E7%A8%8B%E6%95%85%E4%BA%8B5-java-%E5%88%B0-go%E5%BC%80%E6%BA%90-rosedb/","title":"我的编程故事—5 Java 到 Go，开源 rosedb"},{"content":"自从三月份上线我的课程《从零实现 KV 存储》以来，陆陆续续得到一些朋友的支持，在这期间，也涌现出了一些优秀的学员，他们有的人基于课程，搞出了自己的开源项目，并且得到了非常不错的发展，这里以下面的这两个项目为例给大家分享下。\nFlyDB\n项目地址：https://github.com/ByteStorage/FlyDB\n第一个项目叫 FlyDB，这个学员之前跟我提过，跟着课程然后自己搞项目，但是没有给我说项目地址，我也就没在意了。\n但是今天突然刷到了，然后看了看代码结构，发现有点眼熟，然后我再看贡献者列表的时候突然就想起来了。\n一问，果然是这个学员的项目。\n更让我惊讶的是，项目看起来非常的不错，文档写的也很漂亮，代码质量也很不错，并且在课程的基础之上，加入了很多扩展的内容，比如集群，项目 star 也有 500+。\nCouloyDB\nhttps://github.com/Kirov7/CouloyDB\n第二个项目叫 CouloyDB，这个同学做的这个项目我是知道的，之前他还在课程的用户群里面说过，也是自己加上了很多内容，比如事务的完善。\n今天在看到这个项目的时候，发现还在维护，并且也有一百多 star，看起来还是非常不错的。\n这两个项目都是课程学员开源出去的，看起来我的课程对大家的学习有了很大的帮助，能够帮助大家激发兴趣，做自己热爱的事情，也是我做这个课程的初衷。\n我也很开心和惊喜，甚至有点惶恐，现在的年轻人太生猛了，有了更多的学习经验和资料，就能够少走很多我之前走过的弯路，真是教会徒弟，饿死师傅啊！\n希望大家能够给与这些项目更多的支持，比如点点 star，或者感兴趣的话也可以参与进去，因为这两个项目目前看起都处于比较早期的阶段，能够做的事情应该还是挺多的。\n当然这次列举的两个项目仅仅是我知道的，或许还有一些我不知道的学员做的项目，反正大家加油干就是了！\n最后，再召集一下还在犹豫要不要上车的同学，跟着课程学习完，并且完全学懂是肯定没问题的，并且你自己感兴趣的话，也可以自己多花时间，或许你也可以像上面的两个学员一样，做出属于自己的开源项目。\n课程详情点击这里：\nhttps://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g\n","date":"2023-08-02T13:19:33Z","permalink":"https://blog.roseduan.cn/p/%E5%AD%A6%E5%91%98%E6%95%85%E4%BA%8B%E4%B8%A4%E4%B8%AA%E4%B8%8A%E7%99%BE%E6%98%9F%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/","title":"学员故事，两个上百星开源项目"},{"content":"这篇文章将主要描述，如何使用我最近新开发的 WAL（Write Ahead Log）构建属于你自己的 KV 存储引擎。\nwal 地址：https://github.com/rosedblabs/wal\n什么是 WAL？ wal，即 Write Ahead Log，通常叫做预写日志，在一般的数据库或者存储系统中，是为了预防崩溃恢复而存在的，以传统的 LSM 和 Bitcask 存储引擎为例，数据首先进入存储引擎时，会先写到 WAL 中，然后再更新内存索引，LSM 一般是跳表，而 Bitcask 一般是哈希表，当然你也可以选择其他的内存数据结构。\n这样当系统重启时，会通过重放 wal 日志来构建内存数据结构中的内容。\n在 Bitcask 存储引擎中，有一个非常特殊的地方在于，预写日志 wal 和实际存储数据的日志文件，其实就是同一个文件，这样便带来一个极大的好处，那就是我们可以直接基于 wal 构建出一个轻量、快速、简单可靠的 KV 存储引擎。\n而在 LSM 存储引擎中，会稍微复杂点，因为其后还有 SSTable 这一大块内容，所以本文将会简单起见，只介绍下如何构建 Bitcask 存储，当然如果你在 LSM 中使用了 Wisckey 这样的优化技术后，也可以使用 wal 来存储 kv 分离之后的 Value Log 文件。\nWAL 的由来 最开始想开发这个项目，其实主要是想到要重构 rosedb 和 lotusdb，然后这其中有很多重复的内容，rosedb 的数据文件可以用 wal 来存储，lotusdb 中 Memtable 对应的预写日志，和 Value Log 也可以用 wal 来存储。\n因为这几种类型它们的存储格式都是一样的，即日志追加（append only）。所以我将这个公共的部分单独提取出来，形成了一个新的项目。\nWAL 的大致结构 然后我们再来看一下 wal 项目的大致结构，一个 wal 实例，其实分为了多个文件，每个文件叫做一个 Segment，这个 Segment 具体有多大，是可以在启动时配置的，默认是 1GB。\nSegment 文件是分为了多个旧的文件，和一个当前活跃的文件，新写入的数据，会写到活跃的 Segment 文件中。\n一个 Segment 文件内部，又分为了 n 个等分的 block 块，每一个 block 块的大小是 32 KB。block 写的是变长的 chunk 数据，一个 chunk 主要是有固定的 7 字节的头部，以及其后的实际的用户存储的数据。每个 chunk 都分为了四种类型，分别是 FULL、FIRST、MIDDLE、LAST，这主要是借鉴了 Leveldb/RocksDB 中的 wal 的设计。\n数据在写入到 wal 中后，会得到一个 ChunkPosition，这个 Position 是描述数据在 wal 中的位置信息，你可以直接使用这个位置信息从 wal 中通过 Read 方法读取到写入的数据。\n如何基于 wal 构建 KV 存储 从前面的描述中，可以看出，wal 其实就是由多个 Segment 文件组成，支持日志追加写数据，并且可以从中读数据的一个服务。\n这几天集中优化了一下 wal 的读写性能，目前的读写速度很快，并且几乎不怎么占据内存。\n有了这个 wal 组件之后，我们再基于此构建一个 Bitcask 存储引擎，将会变得极其的简单。\n首先，我们要做的就是选择一个内存数据结构，比如 B-Tree、跳表、红黑树、哈希表等等都是可以的，只要是能够存储一个 KV 值即可。\n用户写入数据，实际就是先写入到 wal 中，写到 wal 之后，你会得到一个位置信息 ChunkPosition，然后把 Key+ChunkPosition 存储到内存数据结构中即可。\n然后是读数据，直接根据 Key 从内存数据结构中获取到对应的 ChunkPosition，然后根据这个位置从 wal 中读取到实际的 Value 即可。\n最后是重启数据库，需要调用 wal 中的 NewReader 方法，这个方法可以遍历 wal 中的所有数据，并返回 Key+ChunkPosition 信息，你只需要把这个数据再次存放到内存数据结构中就可以了。\n这几个主要的步骤一完成，一个最基础的 KV 存储引擎就构建起来了，当然你还可以基于此做很多的完善和优化。\n好了，这就是基于 wal 这个组件来构建你自己的 KV 存储引擎的大致流程，大家可以自己去尝试动手写一下，对自己的实战能力提升应该还是很大的。如果项目对大家有帮助的话，可以给个 star 支持下哦！\n","date":"2023-07-31T13:24:33Z","permalink":"https://blog.roseduan.cn/p/%E4%BD%BF%E7%94%A8-wal-%E6%9E%84%E5%BB%BA%E4%BD%A0%E8%87%AA%E5%B7%B1%E7%9A%84-kv-%E5%AD%98%E5%82%A8/","title":"使用 WAL 构建你自己的 KV 存储"},{"content":" 本文是《从零实现 KV 存储》课程的面试要点总结，相当于只要你学习了课程，以下提到的内容都是你自己完成的。对课程感兴趣的同学可以进这个链接查看详情：https://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g\n在简历上如何写这个项目？ 项目概述\n基于 Bitcask 模型，兼容 Redis 数据结构和协议的高性能 KV 存储引擎 设计细节\n采用 Key/Value 的数据模型，实现数据存储和检索的快速、稳定、高效 存储模型：采用 Bitcask 存储模型，具备高吞吐量和低读写放大的特征 持久化：实现了数据的持久化，确保数据的可靠性和可恢复性 索引：多种内存索引结构，高效、快速数据访问 并发控制：使用锁机制，确保数据的一致性和并发访问的正确性 编程语言：采用 Go/Rust（根据实际情况写 Go 或者 Rust） 编写，兼顾高性能以及编码简洁性 结果\n性能方面：相较于其他同类型的存储引擎，读写性能稳定快速，相较于 redis，能够基本在一个数量级，但是节省了大量的内存空间\n例如 leveldb、bolt、badger、sled 等等，做个简单的对比 可靠性：依赖数据文件的持久化特性，确保了数据的可靠存储和恢复，降低数据丢失的风险\n简洁直观的用户 API，支持内嵌式的基础 Put/Get/Delete 等接口，也可以通过 HTTP 接口进行数据访问，也可以通过 redis client 进行直接访问\n可能的面试问题\u0026amp;回答 你做的这个项目能简单介绍一下吗 我开发的项目是一个基于 Bitcask 存储模型的 KV 数据库。bitcask 是一种高性能的持久化存储引擎，其基本原理是采用了预写日志的数据存储方式，每一条数据在写入时首先会追加写入到数据文件中，然后更新内存索引，内存索引存储了 Key 和 Value 在磁盘上的位置，读取数据时，会先从内存中根据 key 找到对应 Value 的位置，然后再从磁盘中取出实际的 Value。\n基于这种模型，其读写性能都非常高效快速，因为每次写入实际上都是一次顺序 IO 操作，然后更新内存，每次读取也是直接从内存中找到对应数据在磁盘上的位置。\n为什么会做这个项目 这个问题的答案因人而异，可以根据自己的情况来回答，例如：\n对数据库存储系统实现的好奇心，看到了对应的 Bitcask 的论文，想要自己去实现 弥补 redis 的缺陷，因为 redis 是一种基于内存的数据库，在数据量较大的情况下，对内存的压力会非常大，而 Bitcask 可以规避这个缺点，显著降低内存使用量 参加数据库比赛，针对性的设计了一种存储引擎 现有的存储引擎例如基于 B+ 树，读性能稳定，但是写数据是随机 IO，性能较差，LSM Tree 写性能优秀，但是读性能不稳定，读放大、写放大、空间放大问题严重；而 Bitcask 存储模型的读写性能则非常的稳定快速。\n有哪些适用场景 缓存系统\nKV 数据库可用作缓存系统的后端存储，以提供快速的数据访问和响应能力。由于 Bitcask 存储模型具有高性能和低读写放大的特性，它适合存储频繁访问的热数据，提供快速的缓存读取操作。\n日志存储\nKV 数据库可以作为日志存储系统使用，将日志数据持久化到磁盘上的日志文件中。Bitcask 存储模型的追加写入方式使得日志的写入操作非常高效，确保了日志的可靠存储和后续分析。\nKey 小 Value 大的 KV 数据存储\nBitcask 将 key 和对应的索引都维护在了内存当中，这样如果 key 较小的话，那么内存当中能够维护的数据量就更多，并且 Value 是在磁盘上存储的，因此可利用磁盘更大的空间来存储 Value。\n优缺点是什么 优点\n读写低延迟 这是由于 Bitcask 存储模型文件的追加写入特性，充分利用顺序 IO 的优势。\n高吞吐量，即使数据完全无序 写入的数据不需要在磁盘上排序，Bitcask 的日志结构文件设计在写入过程中减少了磁盘磁头的移动。\n能够处理大于内存的数据集，性能稳定 数据访问涉及对内存中的索引数据结构进行直接查找，这使得即使数据集非常大，查找数据也非常高效。\n一次磁盘 IO 可以获取任意键值对 内存索引数据结构直接指向数据所在的磁盘位置，不需要多次磁盘寻址来读取一个值，有时甚至不需要寻址，这归功于操作系统的文件系统缓存以及 WAL 的 block 缓存。\n性能快速稳定 写入操作最多需要一次对当前打开文件的尾部的寻址，然后进行追加写入，写入后会更新内存。这个流程不会受到数据库数据量大小的影响，因此性能稳定。\n备份简单 在大多数系统中，备份可能非常复杂。Bitcask 通过其只追加写入一次的磁盘格式简化了此过程。任何按磁盘块顺序存档或复制文件的工具都将正确备份或复制 Bitcask 数据库。\n批处理操作可以保证原子性、一致性和持久性 支持批处理操作，这些操作是原子、一致和持久的。批处理中的新写入操作在提交之前被缓存在内存中。如果批处理成功提交，批处理中的所有写入操作将持久保存到磁盘。如果批处理失败，批处理中的所有写入操作将被丢弃。\n即一个批处理操作中的所有写入操作要么全部成功，要么全部失败。\n缺点\n所有的 key 必须在内存中维护 始终将所有 key 保留在内存中，这意味着您的系统必须具有足够的内存来容纳所有的 key。\n启动速度受数据量的影响 数据库启动时，会加载所有的数据，并且会重放所有的操作，以此来构建内存索引，如果数据量较大，这个过程可能会非常漫长\n磁盘上产生了无效的数据，如何清理 实现了 Bitcask 论文中提到的 Merge 方案，Merge 实际上就是对磁盘数据空间进行清理的操作，其基本执行流程是遍历所有的数据，并将有效的数据进行重写，然后使用新的文件替换旧的文件，以此达到回收空间的效果。\n追问：\nMerge 的过程会阻塞读写操作吗\n不会，Merge 实际上是在新的目录打开了新的 Bitcask 进程实例，这个实例和原目录上运行的实例互不冲突，Merge 的时候，只会读取原 Bitcask 实例的索引数据结构，判断数据是否有效，并不会对原来的实例产生任何影响，并且原实例的写入会写到新的文件中，不会参与到 Merge 过程中，所以对写入也没有影响。 Merge 过程万一很漫长，中途挂了怎么办\n在具体实现中，会在 Merge 结束之后，在磁盘文件中写入一个 Merge 完成的标识，只有当有这个标识的时候，我们才认为一次 Merge 是完整的，否则 Merge 都是不完整的，直接删除掉 Merge 的数据目录即可。 写操作是如何保证原子性的 采用了预写日志的方式，和其他大多数系统一样，WAL 通常是保证事务原子性和持久性的关键，在 Bitcask 存储模型中，比较特殊的是 WAL 文件本身就是数据文件，所以天然可以保证原子性，我们在写入的时候加上了一个完成的标识，并且给每一批次的数据都附了一个全局递增的 id，只有全部提交完成了，这个批次的数据才算完成，否则都不会进行加载。\n和 LevelDB、BoltDB、Redis 的区别 LevelDB 是经典的 LSM Tree 存储模型，其基本架构大致分为了 WAL、memtable、SSTable，数据写入首先会到 wal 中保证持久化，然后更新到内存的 memtable 中，如果 memtable 满了，则 flush 到磁盘的 sstable 中。\n读数据会从 memtable 查找，如果没找到，则从磁盘上的多级 sstable 中查找，读性能不稳定。\nBoltDB 是 B+ 树存储模型，读性能稳定，但是写入是随机 IO，性能较差。\nRedis 是一种纯内存的数据结构服务，也可以持久化到磁盘中，但其实际上是一种面向内存的 KV 存储，数据量受到内存容量的影响。\n而 Bitcask 存储模型，写性能和 LSM 模型相当，读性能也很稳定，读写都是一次磁盘 IO 操作即完成，并且相较于 Redis，Value 是不会存储到内存中，节省了内存空间，并且性能能够和 Redis 维持在一个数量级。\n做了哪些改进 内存索引限制\n前面说到，Bitcask 的一大缺点就是所有的 key 都必须在内存中维护，这样数据库中能存储的数据量就受到了内存容量的限制，而我的项目中，创造性的使用了持久化的 B+ 树来作为索引数据结构，这样就可以将索引存储到磁盘上，突破了内存容量的限制。但是这样带来的一个副作用便是读性能会下降，因为原来是直接从内存中就能到获取到 Value 的位置，但是使用 B+ 树存储索引的话，还需要从磁盘 B+ 树中获取索引，然后再到磁盘中获取 Value，相当于多了几次磁盘 IO 操作 内存索引锁粒度优化\n在我的最初的项目中，内存索引是单个数据结构，并且为了保证并发安全，这个结构的读写都需要加锁，如果在大数据量下，所有的数据都会竞争这把锁，所以我将索引进行了分区，并通过哈希函数将 key 映射到不同的索引结构当中，大大减少了并发冲突 启动速度优化\n为了避免在重启的时候全量加载所有的数据来构建内存索引，我的项目中实现了 Bitcask 论文中提到的 Hint 文件，Hint 文件实际上就是一个 key+索引的文件，它不存储 Value，相较于原始文件容量会小很多，这样重启加载的时候，直接加载这个 Hint 文件 追问 1：Hint 文件是在什么时候生成的呢 Merge 的时候生成的，Merge 时实际上所有的数据都是有效的，这个时候只需要依次存储对应的 key 和索引数据 追问 2：Hint 文件的格式是什么 和数据文件是一样的，都采用了日志追加的方式 持久化策略优化\n在最开始的设计中，默认的刷盘策略是交给了操作系统来调度，这样的好处是性能很好，但是存在丢失数据的风险，在实际环境中，我们可以再提供另一个选项，用户可以设置积累到多少个字节之后，再进行一次持久化。这相当于提供了一个折中的方案，相较于之前的要么每条都持久化，要么完全交给操作系统，这样能够让用户自己灵活配置。 数据文件布局优化\n我还参考了 LevelDB 和 RocksDB 的 WAL 文件的格式，将数据文件的组织形式改为 block（32KB） 的方式，加速数据读写的效率。 兼容 HTTP 协议\n单纯的 KV 接口大多只能在嵌入式的场景中使用，但无法作为远程调用服务使用，所以我在存储引擎的基础之上，加上了 HTTP 的访问接口，这样便可以将存储引擎作为 HTTP 服务使用 兼容 Redis 数据结构和协议\n原生的 KV 接口能够满足的需求比较有限，而 Redis 支持了多种数据结构，比如 String、List、Hash、Set、ZSet，满足了多样化的需求。于是我在 KV 的接口之上，兼容了 Redis 的数据结构，并且兼容了 Redis 的通信协议 RESP，这样一是可以满足多样需求，二是可以让用户无缝切换到我的存储项目中 注：以上是我个人能够想到的一些东西，但在实际场景中，可能问的问题会更多，大家如果有相关的经验，可以在这里留言分享！\n","date":"2023-07-06T08:56:33Z","permalink":"https://blog.roseduan.cn/p/%E7%A1%AC%E6%A0%B8%E9%A1%B9%E7%9B%AE-kv-%E5%AD%98%E5%82%A8%E8%BD%BB%E6%9D%BE%E6%8B%BF%E6%8D%8F%E9%9D%A2%E8%AF%95%E5%AE%98/","title":"硬核项目 KV 存储，轻松拿捏面试官！"},{"content":"上一回说到，我的秋招之旅非常的平淡，几经波折，找到了一份普普通通的工作，毕业后就去新的城市，新的公司入职了。\n我的编程故事—3 秋招之旅\n从上大学的时候，我就非常向往大城市，也说不上来为什么，或许只是心中的一种执念。\n当然，毕业后还是如愿去了上海。\n刚到上海的时候，租了一个小房间，房租是一千块，已经算是非常便宜了，那时候刚毕业的工资也就几千块。\n我的第一份工作是 Java 业务开发，公司也不大，做的是电子签名相关的业务。刚工作的时候，其实并不是非常适应，可能是由于自己太菜的缘故，很多东西无法上手，有点完全不知所措。\n第一是面对庞杂的代码，加上对业务的不熟悉，对一些小的需求甚至也无法理解，写代码更是无处下手。\n第二是自己对一些基础工具使用不熟练，基础知识差，比如面对 git 产生了冲突也不知道应该怎么办。\n那个时候的学习能力，以及社交能力，都无法让我在短时间内解决工作当中遇到的一些问题。\n当然，所有的问题到最后都有解决的办法，我当时是有老员工带，虽然害怕问一些低级的问题，但到了不得不问的阶段，也只能腆着脸问了。\n所以尽管我做的很慢，但至少态度还算端正，只能算一个兢兢业业，中规中矩，毫无特长的员工。\n或许正因为如此，工作上不懂的越多，让我在业余的时间里抓紧了学习的节奏。\n我那时候在一些付费的平台上买了很多的课程，数据结构，Java，Python，并发编程等等，周末的时间基本全在学习，甚至在上班的公交车上也不忘学习。\n工作了半年之后，即 2020 年初，疫情开始从武汉蔓延至全国，春节假期也因此而延长了一周。\n疫情后，我们居家办公了一段时间，那时候有了更多的业余时间，开始学习 Go 语言了。\n至于为什么会开始学 Go，我已记不清了，或许只是因为当时比较火热，加上已经学习了 Python，就想挑一个有趣的来玩一玩。\n在学习期间看到了一个使用 Go 语言写单机 KV 数据库的论文 bitcask，然后就看到了一些对应的项目，比如 nutsdb，boltdb 等等。\n这对当时还只会 CRUD 的我大受震撼，心里直呼这真是太牛逼了。\n于是想着能不能自己也尝试写一个出来，这也是当时我写 rosedb 的启蒙。\n欲知我是如何写出第一个开源项目，然后离职从 Java 转 Go 的，且听下回分解。\n","date":"2023-06-28T18:43:33Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84%E7%BC%96%E7%A8%8B%E6%95%85%E4%BA%8B4-%E7%AC%AC%E4%B8%80%E4%BB%BD%E5%B7%A5%E4%BD%9C/","title":"我的编程故事—4 第一份工作"},{"content":"RoseDB V2 重构的第一个版本发布了！\nRoseDB 是一个基于 Bitcask 存储模型，轻量、快速、可靠的 KV 存储引擎。Bitcask 存储模型的设计主要受到日志结构化的文件系统和日志文件合并的启发。\n感兴趣可参考 Bitcask 论文：https://riak.com/assets/bitcask-intro.pdf\nRoseDB 存储数据的文件使用预写日志（Write Ahead Log）进行了重新设计，这些日志文件是具有 block 缓存的只追加写入（append-only）文件。\nwal: https://github.com/rosedblabs/wal\n我将原来 rosedb 中的 Redis 数据结构和协议拆分了出去，后面会单独形成一个项目，方便接入不同的存储引擎，比如 rosedb、badger、pebble、levledb 等等。\n所以 rosedb 只专注于单机存储引擎的功能，目前处于积极维护状态，欢迎大家 issue 或者贡献，点点 star ⭐️。\n项目地址：https://github.com/rosedblabs/rosedb\n以下是 RoseDB 的一些简单介绍和使用示例：\n主要特点 优势 读写低延迟\n这是由于 Bitcask 存储模型文件的追加写入特性，充分利用顺序 IO 的优势。高吞吐量，即使数据完全无序\n写入 RoseDB 的数据不需要在磁盘上排序，Bitcask 的日志结构文件设计在写入过程中减少了磁盘磁头的移动。\n能够处理大于内存的数据集，性能稳定\nRoseDB 的数据访问涉及对内存中的索引数据结构进行直接查找，这使得即使数据集非常大，查找数据也非常高效。\n一次磁盘 IO 可以获取任意键值对\nRoseDB 的内存索引数据结构直接指向数据所在的磁盘位置，不需要多次磁盘寻址来读取一个值，有时甚至不需要寻址，这归功于操作系统的文件系统缓存以及 WAL 的 block 缓存。\n性能快速稳定\nRoseDB 写入操作最多需要一次对当前打开文件的尾部的寻址，然后进行追加写入，写入后会更新内存。这个流程不会受到数据库数据量大小的影响，因此性能稳定。\n崩溃恢复快速\n使用 RoseDB 的崩溃恢复很容易也很快，因为 RoseDB 文件是只追加写入一次的。恢复操作需要检查记录并验证CRC数据，以确保数据一致。\n备份简单\n在大多数系统中，备份可能非常复杂。RoseDB 通过其只追加写入一次的磁盘格式简化了此过程。任何按磁盘块顺序存档或复制文件的工具都将正确备份或复制 RoseDB 数据库。\n批处理操作可以保证原子性、一致性和持久性\nRoseDB 支持批处理操作，这些操作是原子、一致和持久的。批处理中的新写入操作在提交之前被缓存在内存中。如果批处理成功提交，批处理中的所有写入操作将持久保存到磁盘。如果批处理失败，批处理中的所有写入操作将被丢弃。即一个批处理操作中的所有写入操作要么全部成功，要么全部失败。\n缺点 所有的 key 必须在内存中维护\nRoseDB 始终将所有 key 保留在内存中，这意味着您的系统必须具有足够的内存来容纳所有的 key。\n快速上手 基本操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package main import \u0026#34;github.com/rosedblabs/rosedb/v2\u0026#34; func main() { // 指定选项 options := rosedb.DefaultOptions options.DirPath = \u0026#34;/tmp/rosedb_basic\u0026#34; // 打开数据库 db, err := rosedb.Open(options) if err != nil { panic(err) } defer func() { _ = db.Close() }() // 设置键值对 err = db.Put([]byte(\u0026#34;name\u0026#34;), []byte(\u0026#34;rosedb\u0026#34;)) if err != nil { panic(err) } // 获取键值对 val, err := db.Get([]byte(\u0026#34;name\u0026#34;)) if err != nil { panic(err) } println(string(val)) // 删除键值对 err = db.Delete([]byte(\u0026#34;name\u0026#34;)) if err != nil { panic(err) } } 批处理操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 创建批处理 batch := db.NewBatch(rosedb.DefaultBatchOptions) // 设置键值对 _ = batch.Put([]byte(\u0026#34;name\u0026#34;), []byte(\u0026#34;rosedb\u0026#34;)) // 获取键值对 val, _ := batch.Get([]byte(\u0026#34;name\u0026#34;)) println(string(val)) // 删除键值对 _ = batch.Delete([]byte(\u0026#34;name\u0026#34;)) // 提交批处理 _ = batch.Commit() 完整代码可查看 examples 示例代码。\n","date":"2023-06-14T09:57:33Z","permalink":"https://blog.roseduan.cn/p/rosedb-v2-%E7%89%88%E6%9C%AC%E5%8F%91%E5%B8%83/","title":"rosedb v2 版本发布！"},{"content":"上次说到，我大三大四就是在日日夜夜的自学中度过的。\n我的编程故事—2 决定自学\n那个时候底子差，没正经上过计算机科班的课程，所以选择了普遍流行的 Java 开发，主要学习的是 Java 基础语法以及一些简单的 Spring 项目。\n大四的时候，开始正式秋招了，一个涉世未深的大学生，面对这阵仗多少有些措手不及。最开始的时候，本校有一些公司来进行校招宣讲，但我们是一个工科学校，材料、机械相关的专业很多，所以计算机相关的公司很少，要么也是一些小公司。\n那个时候才意识到，一个好的学校，就是一个好的平台，优秀的企业、互联网大厂基本上只会在好的学校有宣讲会，一些稍差的学校根本不会有这种机会。\n想起来我刚上大学的时候，甚至自以为是，觉得好的大学和差的大学实际上没有太大的区别，基本上靠自我驱动去学习，临近毕业的时候，才发现当时的想法是多么的幼稚。\n只不过在本校的时候，面了一个 Java 开发的岗位，面试官不专业，问的问题也很业余，但是这次经历给了我一些信心，就算是非计算机专业的，好像也并不是很重要，面试官基本上不会在乎这个。\n后来就没办法了，本校的机会太少，我就将目光瞄向了其他的学校，我们附近有一些好一点的学校，比如电子科技大学、西华大学，后来就去其他的学校参加校招。\n大致的结果就是，基本上都失败了，大厂根本看不上我的简历，而且我的底子太差，数据结构算法的知识很匮乏，笔试的题目也做不出来几个。\n但很幸运的是，在自己的不懈坚持下，面试通过了两家在上海的公司，都算不上大，小有知名度，而且做的是传统软件，薪资也比较低。\n但对于当时的我来说，已经很欣慰了，毕竟是一个纯自学选手。那时候互联网环境应该还不错，当然后面就感觉慢慢的走下坡路了，要是把当时我的水平放到现在，不太可能能够找到工作，所以也算是搭上了末班车。\n拿到 offer 之后，也没有停下来放飞自我了，反而因为在面试中有很多的挫败感，顿感焦虑，一是自己面试失败的经历太多了，感觉自己很菜，二是很羡慕一些拿到大厂 offer 的同学，薪资非常的高。\n所以在短暂休息之后，业余时间仍然在学习，补充一些自己缺乏的知识，并且仍然在参加校招，虽然校招已经接近尾声了。在后来的面试中，心态就比较放松，因为已经有确定的 offer 了，所以发挥的还可以，又拿到了两个成都的小公司的 offer，于是我选择了其中的一个， 就直接去开始实习了。\n后来实习结束，正式毕业，没有选择继续留在实习的公司，去了上海开始正式工作。\n这便是我的校招经历，当然远比不上那些披荆斩棘，斩获各个大厂 offer 的科班同学，或许正是因为见识到了别人的强悍，刺激到了我很多，才激励我在以后的日子里从不停止学习。\n关于我在上海的第一份工作的情况，待下次再细说。\n","date":"2023-05-02T12:10:33Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84%E7%BC%96%E7%A8%8B%E6%95%85%E4%BA%8B3-%E7%A7%8B%E6%8B%9B%E4%B9%8B%E6%97%85/","title":"我的编程故事—3 秋招之旅"},{"content":"上一回说到，大一大二在无忧无虑中度过，那的确是非常美妙的一段时光，可能很多同学的这个阶段都差不多吧。\n到了大三，班上同学的状态就不太一样了，因为临近毕业，可能很多人都在心中盘算以后的出路了。\n有的人决定考研、考证，或者考公务员之类的，于是也开始准备起来了，天天泡图书馆学习，早起晚归。有的人依旧躺平，完全不会考虑以后的事，依旧在寝室中玩游戏，虚无度日。\n而我，在当时的环境中，其实很迷茫，因为当时的学习成绩并不太好，并且对本专业的工作、学习等都不是非常的感兴趣。\n想要跨专业考研到计算机专业，这个只是想过，但是很快就打消念头了，一是因为难度太大，大学期间我并没有系统学习过计算机专业的课程，二是这个专业的竞争非常激烈，想要考上个好大学的概率更低了。\n当然也想过继续躺平，啥也不管，反正离毕业还是有一段时间的，但是躁动的内心已不允许我继续沉寂下去，必须得让自己有一个奔头，不至于在毕业的时候啥也没有。\n其实我自学编程很早就开始了，大一的时候还自己买了一本 C 语言的书籍进行学习，当时想的是能够转到学校的计算机专业，但是因为各种限制，也失败了，于是学习也是断断续续，并不系统。\n到了大三的时候，我开始决定系统性的自学了，选择了 Java，这是当时（2018 年前后）很多人的共同选择，工作机会也是比较多的。\n当时其实也想过去上个培训班，但是一是当时没钱，培训费用一般得上万了，二是我觉得离毕业找工作其实还有接一年多的时间，这个期间我觉得我是有能力可以学会找到一份工作的。\n至于其他的，完全没想过，比如找工作的时候，怎么和面试官说我不是这个专业的，怎么和其他的计算机专业的竞争，工作机会多不多，能不能找到相关的工作，这些问题心里完全没谱。\n但是既然决定了，就一往无前，我在图书馆找了一些 Java 基础相关的书籍，每天看看书，然后跟着敲代码，学习基础知识，然后也花了一点钱，买了一些实战的课程，然后跟着敲。\n于是从大三开始，我的大学生活就是，每天还是会上课，因为必须得保证不挂科，才能够顺利毕业拿到毕业证，否则就会比较麻烦。当然我只是保证能够基本学会，考试能够通过就行，也不追求太好的成绩了。\n然后在没上课的时间，基本上都花在了图书馆，夜以继日的学习，那个时候其实还是非常欠缺经验的，学习了一堆后来我觉得并没什么用的东西，而有用的东西却又没有意识到应该学习，这算是非常大的一个缺憾了。\n总之，大三大四就是这样，在自学编程中度过，然后就到了毕业找工作的时候。\n待续。\n","date":"2023-04-15T19:42:33Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84%E7%BC%96%E7%A8%8B%E6%95%85%E4%BA%8B2-%E5%86%B3%E5%AE%9A%E8%87%AA%E5%AD%A6/","title":"我的编程故事—2 决定自学"},{"content":"毕业到现在，三年多的时间里，接触过很多编程语言，这篇文章简单来聊一聊，最后也有自己的一些思考和看法，Enjoy！\n之前发过一个朋友圈，简单罗列了一下自己之前写过的语言，感觉主流的编程语言自己都接触过了，今天就来详细谈一谈。\n一些读者都知道，我是在大学期间自学编程，我的专业是国际经济与贸易，然后看到本校的计算机专业课程，他们最开始学习的语言就是 C，于是便开始接触到了 C 语言。\n但那时候我根本没有任何计算机的底子，C 又偏偏是一个很难的语言，对计算机基础的要求比较高，所以是浅尝辄止，只是把《C Primer Plus》这本书简单学习了一遍，也没有任何的项目实践和深入学习。\n然后到了大三的时候，迫于找实习和工作的压力，从那时起开始系统性的学习 Java 了。\n至于为什么选择 Java，稍微工作几年的同学都知道，Java 当时还是挺火的，语法容易学，对计算机基础的要求并不是特别高，然后培训班铺天盖地的都是 Java 的课程，网上的 Java 教学视频、学习资源等也有很多。\n当时学 Java，人手一个电商/博客项目，有点像现在，学习数据库的，都得学习 CMU-15445、MIT-6824，然后还要自己参与或者手撸一个数据库项目一样（只能说越来越卷了。。。）\n当时写一些商城项目，前端页面也是自己写，所以学习了一些 Html/CSS 和 JavaScript 基础，搭配 Java 的 JSP，简单的页面就能自己撸出来了，当然现在估计都没人用 JSP 了。\n大学毕业参加工作，第一份工作便顺理成章的找了 Java，毕竟其他的咱也不会。\n工作有时候会遇到一些重复机械的活，于是当时就想能不能通过一些脚本，帮助自己提升工作效率。于是简单学习了 Python，写了一些小工具，例如自动生成 SQL 定义，提取数据库的数据转成 Excel，等等。\n到了 2020 年中，大概毕业后一年的样子，业余的时间开始学 Go，Go的语法还是挺简单的，上手很快，rosedb 就是从那个时候开始的。\n后来换了工作，找到的职位刚好是用 Go 的，所以便完成了从 Java 到 Go 的跨越，至此 Java 便不怎么接触了。用 Go 最开始也是写业务，然后转到了基础架构方向，用 Go 写了一段时间的数据库中间件。\n然后就是接触到了分布式 KV 存储，公司的这个存储项目是用 C++ 写的，于是便开始学习了一些 C++，当然这个项目我参与并不是很多，然后就离职了，所以我对 C++只是很浅显的了解。\n后来到了数据库行业做内核开发，开始用 C，于是便重操旧业，开始复习大学期间最开始接触的 C 语言。\n最近，又因为工作的一些要求，开始学习 Rust，刚接触 Rust 的确不太适应，但是也能够感受到这门语言的强大和灵活。\n于是总结一下，从最开始接触编程到现在，我已经写过 Html/CSS、JavaScript、Java、Python、Go、C++、C、Rust。\n工作之后才发现，完全有可能根据工作的改变，而去学习并且切换到新的语言，这是无法避免的，所以还在学校的同学，不用去纠结自己到底应该学哪门编程语言，能够深入任何一门语言，以后再学习新的语言，都会非常的快。\n这也是一些大厂面试，并不怎么看重语言的原因之一。\n而对于工作之余，想要学习新的语言的同学，可以想想自己学习的目的是什么，例如你想要做什么样的产品，然后再据此去选择一门新的语言。\n我之前也在 V2EX 上发表过类似的看法：\n接触到多个语言，能够让自己不会成为一个语言控，能够比较清晰的认识到每个语言诞生的背景，以及它的适用场景，总之会更加包容的看待一些问题。\n对于语言学习的经验，简单分享一下，如果你有明确的目标，这样学习肯定是最好的。比如你学一门新的语言，就是为了看懂公司的项目，那么可以一边看一边学，并且能够接一些小的需求，逐渐深入。\n如果学一门语言并没有太明确的目的，只是为了兴趣，或者说给以后的自己多一个选择，那么可以找一个基础的教程，跟着敲代码，然后自己参与到项目中去。可以是自己折腾的项目，也可以是别人的开源项目，总之，实践起来才是最快的学习方法。\n当然工作的这几年，接触了比较多的语言，有一个非常不好的方面，就是对大多数语言的认知都是浮于表面，没有更加深入的去学习。\n最好的状态还是能够精通一两门，所以我后续希望能够在 Go 和 Rust 方向有更深入的理解。\n最后再说一句，PHP 是世界上最好的语言（手动狗头）。\n","date":"2023-04-10T18:36:33Z","permalink":"https://blog.roseduan.cn/p/%E9%82%A3%E4%BA%9B%E5%B9%B4%E6%88%91%E5%86%99%E8%BF%87%E7%9A%84%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/","title":"那些年，我写过的编程语言"},{"content":"周末来一篇水文放松放松。\n说起来，我高中的时候还是个文科生呢，其实初中的时候，我文理科的成绩都挺好的，但是一上高中后，不太适应学习节奏，成绩全面下滑，还被老师批评了。\n当然比如数学物理化学等学科下滑得相对来说更严重，所以在高一的时候，选择文理的时候，我选择了一个自己还稍微好点的文科。当时也是没办法，总不能选择一个自己没把握，成绩又很烂的学科吧。\n现在回过来想其实挺遗憾的，要是能重来，我要选理科！\n后面才慢慢的适应学习节奏，成绩也才好起来了一些。\n高考的时候，文科选专业非常的受限制，比如理科可以选择一些文科的专业，但很蛋疼的是文科肯定不能选择理科的专业。\n当时计算机专业很火的，大家一窝蜂的去读计算机，我看了下但是却又没法选择，虽然很气但是也没法改变了。\n所以就很无奈，只能选择了一个看起来高大上的专业—国际经济与贸易。\n可能很多人高考结束后选专业的时候，都差不多，根本不知道自己喜欢什么，也不知道自己以后想做什么，也没有什么人给与辅导。\n有的人是看什么火就选什么，比如理科的计算机，文科的金融、贸易等等。\n还有就是看名字，觉得高大上很洋气，所以就选了，比如我选择的国际经济与贸易，后来我问同寝室的同学，好多就是看这个名字高大上才选择的。\n还有的人选专业就是摆烂心态，就像抽签一样，随便点到哪个就是哪个。\n上大学的时候，自由散漫，完全卸去了高中时期的压力，大一大二时期都过得很开心，没啥烦恼没啥压力，但同时也没钱。\n那个时候也根本没什么目标，不知道以后想做什么，能做什么，每天上完课之后，就出去玩，骑着自行车到处玩，或是到网球场打球。\n对本专业的课程也没什么兴趣，非常的枯燥，到期末的时候才临时抱佛脚，拿着课本看着老师给出的重点，才慢慢看，或者直接在考试的时候作弊，蒙混过关。\n可能很多人的大一大二都差不多，特别是在一些普通的学校里，学习环境也很一般，同寝室的人，下课之后就是玩游戏，根本不可能想着去学习。\n说出来可能都不可思议，有的人大学整整四年，在图书馆待的时候加起来不超过两个小时，而且去图书馆还不是去学习的。\n总之，大一大二，对大多数人来说，包括我，是最无忧无虑的一段时光。\n然后到了大三，情况就开始有一些不一样了。。。\n待续。\n","date":"2023-03-29T09:36:33Z","permalink":"https://blog.roseduan.cn/p/%E6%88%91%E7%9A%84%E7%BC%96%E7%A8%8B%E6%95%85%E4%BA%8B1-%E4%B8%8A%E5%A4%A7%E5%AD%A6%E4%BA%86/","title":"我的编程故事—1 上大学了"},{"content":"精雕细琢了几个月的时间之后，《从零实现 KV 存储》课程终于上线了，打造此课程的目的，是看到很多人想要入行数据库内核或者分布式存储，但是苦于没有一个很好的实战项目来进行系统性的学习。\n结合自己从 Java 业务开发转行数据库内核的经历，我认为从零实现 KV 存储是一个很好的起点，希望这个课程可以帮助到一部分同学。\n本课程是从零实现一个完整的 KV 存储项目，比起其他的项目，一个很大的不同在于，我会在视频当中，带着大家从第一行代码开始编写，而不是对着代码进行讲解，或者让你自己去看。\n并且本课程将会使用 Rust 和 Go 两种语言实现。\n通过每一行代码的编写，你会对整个系统了如指掌，这样对自己基本功的锻炼、对编程能力的提升都是很大的，下面是关于本课程的一些基本情况：\n关于我 我的网名叫 roseduan，是开源项目rosedb、lotusdb的作者，目前约累计 4.6k star，在存储引擎方面有自己的一些实践经验。\n我的 Github 主页：https://github.com/roseduan\n课程形式 课程提供了两种展现的方式，文档+视频。\n文档中描述了一小节内容的基本流程，例如数据读写流程、删除流程等，以及一些代码细节，主要是方便你随时温习，在手机端也能够很好的观看。\n另一个是视频，在视频当中，我会专注于代码细节，带着你一行一行的来完成每一个章节，你可以先跟着我把代码敲一遍，然后自己不断的反思学习，提出自己的思考，以及可能的优化思路，这样才能够不断的提升自己。\n课程的每一篇文档开头，都有一个对应的视频，建议你先通过观看视频的方式，跟着我把代码写完。\n课程的视频演示中，将使用 Rust 和 Go 两种语言来实现，一次付费购买，相当于可以用两种语言来学习实现一个 KV 数据库。\n课程内容 课程大致分为了几个部分\n一是 KV 存储基本功能的实现，包含数据读写、Merge、WriteBatch 等 二是一些常见的优化策略，主要包含内存索引、文件 IO、Merge 操作的优化 三是对 Redis 数据结构以及 Redis 协议的支持 以及其他的例如数据备份、测试、HTTP 接口接入等等。 课程目录如下：\n如果你在学习的过程当中，发现有任何问题，或者可以补充的内容，都可以提出来，如果合理的话，我会再加入到课程内容中。\n适用人群 这个课程对以下同学应该都非常的合适，包括但不限于：\n想入门数据库的同学，存储对一个数据库来说是必不可少的，存储也是一个数据库当中非常重要的模块，通过自己动手写 KV 存储可以掌握一些数据库的基本概念，以及数据库的基本设计思路 想入门分布式存储的同学，单机 KV 存储是分布式存储岗位的必备，可以了解单机存储引擎的设计思路，优化思路等，增强自己对存储引擎的深入理解 增加 Go 项目经验的同学，如果学习了一些 Go 的基础知识，但是苦于没有项目经验，想要进一步巩固自己的知识 增加 Rust 项目经验的同学，这个和 Go 类似，课程支持了两种语言，所以想要进一步巩固自己的 Rust 基础知识，这个课程也很合适 想要巩固基本功的同学，基本功对一个程序员来说非常重要，但是平常的一些课程，例如操作系统，学完了之后总是没有太多的使用场景，没过多久就忘记了，这个课程当中涉及到操作系统、文件、磁盘等知识，可以帮助你打牢基本功 做毕业设计，对于尚未毕业的同学，苦于无法找到一个合适的毕设项目，这个项目应该会让导师们眼前一亮 前置知识 学习本课程，不需要什么特别的前置知识，只需要熟悉 Go 或者 Rust 的基础语法即可，课程当中涉及到的一些内容，我将会详细的为大家讲解，前期先跟着视频中敲代码，是完全可以学会的，没有任何障碍。\n是否可以试看 可以，课程的前两节可以试看：\n01 从零实现 KV 存储—初识 KV 数据库\n02 从零实现 KV 存储—bitcask 论文详解\n课程目前的进度 课程目前更新到了第七节，KV 引擎的数据读写接口都定义好了，后续每周将会更新 1-2 节内容。\n如何购买 售价：450 元\n课程提供了文档+视频，并且使用 Rust 和 Go 两种语言实现，在市面基本上没有同类型的课程。学习一个硬核的项目，为自己的职业发展提供更多的可能，我自己就是从业务 Java 后端转到数据库内核的，我的启蒙项目就是自己从零实现了一个 KV 引擎，所以对于这一点我是深有体会。\n想要购买的同学，请加我 vx（vx号：kiss_duan），或扫描下面的二维码，有其他任何疑问都可以咨询。\n购买成功后，将会为你开启对应的权限。\n购买后是否有有效期？\n没有，购买后永久持有，无限次观看。\n咨询服务\n购买后，我会拉你进课程用户专属的飞书群，我会亲自为你解答学习过程中的疑难杂症，保证你肯定能够完全学会课程中的内容！\nPS. 课程主页详情请点击这里：https://w02agegxg3.feishu.cn/docx/Ktp3dBGl9oHdbOxbjUWcGdSnn3g\n","date":"2023-03-06T10:15:33Z","permalink":"https://blog.roseduan.cn/p/%E4%BD%BF%E7%94%A8-rust-%E5%92%8C-go-%E4%BB%8E%E9%9B%B6%E6%9E%84%E5%BB%BA-kv-%E5%AD%98%E5%82%A8/","title":"使用 Rust 和 Go 从零构建 KV 存储"},{"content":"我自己就是从业务自学转入数据库内核研发岗位的，根据自己的经历，简单总结了一下入门数据库相关的学习路线、学习资料、项目书籍推荐等，大家可以参考。\n必看课程 CMU-15445 和 CMU-15721\nhttps://www.youtube.com/@CMUDatabaseGroup\n这两个不用多说，经典的数据库入门教程，由数据库的大佬 Andy Pavlo 亲自授课。可以了解到数据库的基本概念，例如存储、BufferPool 管理、索引、优化器、执行器、事务、MVCC 等。\n15445 的实验部分是基于其开源的教学项目 bustub，补全其中几个重要的部分，这个项目是 C++ 写的，如果对 C++ 不熟悉的话，那么我觉得实验部分可以暂时跳过，有多余的精力再来搞，毕竟我们是来学数据库的，而不是学 C++ 的。\n存储小项目 学习教学课程的同时，顺便可以了解下存储方面的内容，例如 B+ 树，bitcask，LSM Tree，以及 LSM Tree 的优化 Wisckey，不用专门去学，找几篇文章看看，了解下基本概念，或者直接看看论文。\n然后自己去实践写一个，例如写一个简单的 bitcask、B+ 树存储引擎，或者 LSM 存储引擎。\n之所以推荐写存储类的小项目，主要是因为存储层的 KV 一般比较好实现，同时又能够了解到一些数据库的基本设计理念。\n这里推荐下我的两个项目：\nhttps://github.com/flower-corp/rosedb\nhttps://github.com/flower-corp/lotusdb\n事务/MVCC 这部分网上的资料比较多，可以看看事务的一些基本概念 ACID，然后看看如何去实现的，可以借鉴其他数据库例如 MySQL、PostgreSQL，关于事务实现原理分析这方面的文章比较多。\n概念了解差不多之后，可以自己动手实现，例如可以在自己写的存储项目的基础上，加上事务的功能，保证事务原子性、隔离性，以及并发读写的性能，自己上手撸肯定比只了解理论好很多。\n其他的一些部分，例如 parser、执行器、优化器、向量化等等，比较复杂，自己从头搞一个的难度比较大，我觉得可以简单看看资料，了解一下基本概念，工作之中再针对性的查漏补缺。\n当然如果你对某个部分特别感兴趣的话，比如优化器之类的，也可以多去了解然后自己实践，我这里推荐存储和事务的实现，是因为相对来说比较容易上手。\n分布式 这部分内容首推 Mit.6824，分布式系统入门的首选课程。\nhttps://www.youtube.com/@6.824/videos\n有精力的话可以跟着把实验部分做完。\n然后可以挑战下 PingCAP 的 talent plan 中的 TinyKV，它和 6824 的实验部分比较类似，实现一个基于 raft 的分布式 KV 存储系统，难度比较大，但是代码框架已经搭好了，只需要往里面添加内容即可，测试也比较完备。\nhttps://github.com/talent-plan/tinykv\n如果还有时间的话，可以再上一个台阶，挑战下 PingCAP talent plan 的 TinySQL 项目，主要是实现一个简单的分布式数据库项目，有完备的文字教程。\nhttps://github.com/talent-plan/tinysql\n工作或者实习 当然，其实最好的办法，还是能够直接参与到工作实践当中，这样学习起来是最快的，可以向 leader 请教，和同事交流等。\n如果自身又没有太多经验的话，可以试试那些愿意接纳转数据库内核的公司，这可能会要求你有其他亮眼的东西了，比如基础比较扎实，折腾过自己的项目之类的。能把上面提到的这些东西认真学习下，完成个 60% 左右，我觉得应对一些面试就应该没有太大的问题了。\n为了帮助你更高效的学习，我还整理了一份数据库开发的学习资料，数据库的各个方面都涉及到了，例如 SQL、优化器、执行引擎、存储等等，包含一些优质的书籍、论文、视频课程、博客等，还有一些优质的教学类项目。\n总计十几页的 PDF，一次性送给你，方便提升学习效率。\n还有一些关于数据库方面的优质 PDF 书籍，可以参考学习：\n这份学习资料 PDF 和所有的书籍都可以在这里领取：\nhttps://github.com/rosedblabs/database-learning\n","date":"2023-02-01T10:15:33Z","permalink":"https://blog.roseduan.cn/p/%E6%95%B0%E6%8D%AE%E5%BA%93/%E5%AD%98%E5%82%A8%E5%AD%A6%E4%B9%A0%E8%B7%AF%E5%BE%84%E6%8E%A8%E8%8D%90/","title":"数据库/存储学习路径推荐"},{"content":"澳网 2022 的年初是美好的，因为纳达尔在澳网夺冠了，为这一刻，我等待了十年。从 2012 年开始关注纳达尔，见证了他的一个又一个巅峰，其实从 2010 年之后，纳达尔的大满贯全都是法网和美网了，澳网和温网总是望尘莫及，于是我特别的期待他能够再拿一次澳网（温网概率太小了）。\n其实扪心自问，为什么如此喜欢纳达尔？我也没有明确的原因，可能他满足了自己内心幻想的那个少年吧。\n每个人的内心或许都藏着另一个自己，这个自己可能跟现实中的完全不一样，现实中的我们，可能羞怯、懦弱、自卑，但是幻想中的自己，我们会变得勇敢、无畏、骄傲。\n我特别喜欢纳达尔年轻的时候，留着一头长发，穿着一身海盗衫，风度翩翩，青春无敌，那是最好的时代。\n只可惜，网球在国内太小众，身边几乎没什么人关注，2022 年澳网夺冠的这一份喜悦，只能自己一个人享受。\n封闭 上海的 3 月~ 6 月是魔幻的，最开始身边住得远一些的同事都申请居家了，后来公司通知全部居家，当时想应该只是日常的疫情反复，没几天就过去了。\n居家期间小区通知会封闭两天，让我们自己出去采购物资，谁知这是三个月内的最后一次出小区。\n从那时起，第一次感觉到基本生存都受到了威胁，每顿饭都得省着点吃，希望能多撑几天，一些物资比如纸巾、饮用水也是省着用。每天早上六点多，在睡眼朦胧中打开手机 APP 抢菜，而且大概率还是抢不到的。\n其实对于孤身在外的年轻人还好，可以用 APP 抢，和邻居交换等等，但想想那些上有老下有小的，或者生病的人，还有线下实体行业的从业人员，他们有些人所背负的压力是巨大的，社区的物资救济只是杯水车薪，很多人可能因此而崩溃。\n最后总算是熬过来了，当然这个城市和每个人都付出了巨大的代价。\n这是一段不应该被遗忘的历史。\n工作 上海 6 月解封后，入职了一家新的公司，辗转到了数据库行业，看似是巧合，却又像是水到渠成，从最开始写业务，到业余时间折腾存储引擎，到转入公司的基础架构做分布式存储，然后到数据库。\n工作内容和我所想象的还是不太一样，更加的有难度，产品是基于开源的 greenplum 数据库，会更多的研究 PostgreSQL 的源码，这期间还因为工作所需学习并使用了 Rust 语言。\n我很看好也比较喜欢 Rust，今年应该会投入更多的时间学习并实践。还是希望能够在数据库领域有更多的沉淀吧，毕业的这三年多，做的事情很杂乱，接触的编程语言也很多，但是没有深入到某个领域去研究，对于职业生涯的长期发展感觉还是不太好的。\n数据库行业这两年挺火热，但后面会如何发展却不得而知，在这个垂直领域，想要在几百家公司中生存下来，还是有挑战的。都说选择比努力更重要，但是我们又如何在最开始的时候验证自己的选择呢，或许身在局中的我们，只能尽力而为，去相信自己的选择。\n运营 在开源项目这方面，今年投入的精力有限，一是因为上半年封控期间，居家办公效率比较低，精力消耗严重；二是下半年换了工作，更多的时间花在了工作方面，毕竟对数据库的一些基础还是不太熟悉的。\nGithub 从 6 月份开始基本上就没更新了：\n两个项目的状态：\n今年不知道有没有时间去驱动这两个开源项目了，感觉项目功能方面已经不会有太大的改变了，定位还是很简单的 KV 存储引擎，或许应该可以在易用性、文档建设等方面着手，降低使用者的门槛。\n知乎在下半年也几乎没有更新了。\n公众号这段时间主要更新了 CMU-15445 的学习笔记，这是入门数据库的必学课程了，后续应该会坚持更新完毕，其他还有一些职场、生活感悟相关的文章。\n感情 因为一些机缘巧合，遇到了一个对的人，经历了这魔幻的 2022 年，这是最幸运的事情，我特别喜欢一句诗：\n草在结它的种子\n风在摇它的叶子\n我们站着 不说话\n就十分美好\n这是我期待的爱情最好的模样，谢谢你让我感受到这种美好。\n新冠 在 12 月中旬加入了“小阳人”的大军，这是无法避免的，新冠三年恰好贯穿了自己毕业后的这三年，经历了太多的辛酸苦辣，幸好这一切都慢慢结束了。\n在历史的潮流中，在这个环境下，有时候我们真的是身不由己，我们更多的只是被动的接受，无法参与到历史的抉择之中去，很多人可能会像我一样在心理上备受煎熬。\n但无论怎样，还是要努力的生活，希望可以慢慢恢复到正常的状态，2023 年大家一起加油！\n","date":"2023-01-14T14:23:11Z","permalink":"https://blog.roseduan.cn/p/2022-%E5%B9%B4%E5%B0%8F%E7%BB%93/","title":"2022 年小结"}]