豆博客 - 编程
type: channel
link: https://www.douboke.cn/category/bc/
items: CI / CD 全链路：从个人项目到企业级的完整拆解
items: go的单元测试
items: 搞懂otelgrpc库是如何做到跨程序链路追踪的
items: 分布式事务几种方案
items: go实现i18n的几种方法
items: 推荐算法--基于物品的协同过滤
items: gossip协议
items: fx库解决go依赖注入
items: Raft算法
items: Paxos算法

CI / CD 全链路：从个人项目到企业级的完整拆解
type: item
date: 2026-03-03T23:04:31+08:00
description: 完整的CI/CD流水线，会在你掌握的核心主干基础上，补充大量前置门禁、安全合规、发布管控、故障兜底的环节，形成全流程自动化的安全闭环，下面给你按流水线顺序，完整拆解清楚。一、最小可用CI/CD完整步骤这是CI/CD的灵魂，不管流程怎么扩展，核心永远不变，完全可以直接用于个人项目、小型业务：→ 代码提交 → CI（合并主干 → 代码lint → 单元/集成测试 → 代码验证通过） → 持续交付（生产级标准构建 → 预发布环境全量校验 → 版本化推送到Harbor制品库 → 待上线就绪） → 持续部署（持续交付全流程 + 自动审批 + 自动部署到生产服务器）二、完整企业级CI/CD全链路步骤以下是中大型企业核心业务的标准流水线，按执行顺序拆解，补充的都是你之前没提到、但生产环境必做的环节：【前置阶段：代码提交前（CI的隐形起点，提前拦截80%的低级问题）】本地pre-commit钩子：开发者提交代码前，自动在本地跑lint、单元测试、简单格式校验，有问题直接拦在本地，不用推到仓库才发现注意: 当前功能在jetbrain家的产品中没有显式标明, 实际在提交时会自动执行. 但可以通过ide自带的lint和预执行单元测试命令实现, 比写git脚本更简单, 方便, 直接.代码评审（CR）：代码提交后，必须经过团队成员评审通过，才能触发CI流水线，避免单人编写的风险代码进入流程【CI阶段：代码集成与基线验证（核心主干不变，补充企业级门禁）】核心主干：拉取主干代码 → 冲突检测 → 语法lint → 单元测试 → 模块集成测试补充必做环节（全是门禁，不通过直接终止流水线，绝不进入下一环节）：代码质量门禁：扫描代码重复率、圈复杂度、潜在bug、安全坏味道，不达标直接拦截第三方依赖安全扫描（SCA）：检测项目用的开源组件有没有已知高危漏洞（比如log4j漏洞），有风险直接拦截构建缓存优化：缓存不变的依赖包，避免每次构建重复下载，大幅提升流水线速度阶段终点：得到经过验证、可用于生产构建的干净源代码【持续交付阶段：制品标准化与上线就绪】核心主干：生产级构建 → 预发布环境验证 → 推送到Harbor制品库补充必做环节（彻底解决环境差异、安全风险、合规问题，也是你之前疑惑的“持续交付到底干了什么”的核心补充）：严格执行「一次构建原则」：全程只构建一次，这个制品会贯穿预发布测试、入库、生产部署全流程，绝对不会重新构建，从根源避免“测试环境能跑，生产环境跑不起来”的问题制品签名与防篡改校验：给构建好的Docker镜像/程序包打上唯一数字签名，后续所有环节都会校验签名，确保制品没被篡改，守住供应链安全制品二次安全扫描：推入库前，再次扫描制品本身的漏洞（比如镜像里的系统漏洞、恶意文件），有问题直接拦截生产级全量验证：把制品部署到和生产环境1:1的预发布环境，跑端到端全链路测试、性能压测、兼容性测试、安全渗透测试，确保制品在生产环境100%可用版本全生命周期管理：给制品打上不可修改的唯一版本号，和代码提交记录、测试报告、构建日志强绑定，实现任何版本可追溯、出问题可秒级回滚合规审批与审计：自动触发上线审批流（负责人、安全团队审批），全流程所有操作留存审计日志，满足企业合规要求阶段终点：制品完成所有上线前准备，锁在制品库里，随时可以一键安全上线到生产环境，无需任何额外加工【持续部署阶段：自动化上线与故障兜底（核心逻辑不变，补充生产级管控）】核心主干（你已完全掌握）：完整持续交付全流程 + 自动部署到生产服务器补充必做环节（避免自动部署变成“自动故障”，是企业级持续部署的核心）：灰度/金丝雀发布：不直接全量上线，先把1%-5%的流量切到新版本，观察无异常再逐步全量，把故障影响范围降到最低无损上下线与健康检查：先启动新版本实例，自动校验服务健康状态（能否正常响应请求），健康后再逐步切流量，等老版本实例的所有请求处理完再下线，完全不影响用户线上自动化验收：上线后自动跑核心链路冒烟测试，确保线上核心功能正常监控联动与自动回滚：实时对接监控系统，观察新版本的错误率、响应时间、资源占用等核心指标，一旦发现异常，自动触发回滚，瞬间切回上一个稳定版本，无需人工干预上线结果通知：不管成功失败，自动把上线结果、日志、核心指标推送给相关开发、运维人员【后置闭环阶段：全流程归档与持续优化】全流程日志、测试报告、部署记录、审计日志归档留存，用于后续问题排查与合规审计收集本次上线的监控数据、用户反馈，持续优化流水线的效率、拦截率与稳定性关键补充说明没有绝对的“所有步骤”：CI/CD流水线是可定制的，个人小项目、非核心业务，用你梳理的核心主干就完全够用；金融、政务、大型互联网的核心业务，必须把补充环节落地，才能满足安全、合规、高可用的要求。核心本质永远不变：不管流程怎么扩展，CI/CD的核心始终…

go的单元测试
type: item
date: 2026-03-02T23:12:37+08:00
description: 在写自己项目和公司项目里面都没实际编写过单元测试, 即使要也是使用AI自动编写, 说实在的, AI编写单元测试真是一把好手. 这个文章只是梳理一下go单元测试的各种库, 以便后续需要手动编写时候能快速找到对应的库.基础测试用例（Unit Test）核心：通过编写原生代码调用目标函数，验证返回结果是否与预期一致、执行是否报错。Go 原生支持：使用 testing 包，函数名以 Test 开头，通过 t.Error/t.Errorf 标记失败。Fuzz 测试（模糊测试）核心：测试鲁棒性（Robustness），通过自动生成随机 / 边界输入（如异常字符串、超大整数），发现函数可能的崩溃、 panic 或逻辑漏洞。Go 原生支持：1.18+ 版本内置，函数名以 Fuzz 开头。go-sqlmock核心：模拟数据库交互，无需真实数据库连接。验证执行的 SQL 语句（如查询 / 更新语法、参数）和预期响应（如模拟返回的行数据、影响行数）是否匹配。用途：隔离数据库依赖，快速测试数据访问层代码。gomonkey核心：函数 / 方法打桩（Stub）工具，可在运行时替换函数实现（包括全局函数、成员方法，全局变量）。用途：隔离外部依赖（如替换网络调用、文件 I/O），让测试不依赖真实环境。Ginkgo核心：BDD（行为驱动开发）风格的测试框架，提供更语义化的测试组织方式（如 Describe/Context/It 结构），搭配 Gomega 断言库使用。优势：测试用例更易读、易维护，适合复杂业务逻辑的测试组织。Goconvey同上gomock核心：Go 官方维护的 Mock 框架，根据 interface 自动生成 Mock 实现类。可在测试中设置预期的方法调用（如参数匹配、调用次数），并返回指定结果。用途：解耦依赖的 interface，验证代码是否正确调用了依赖的方法。Testify各大测试库几乎都会引用, 包含很多包, 其中 assert最为常用, 更多包查看: https://github.com/stretchr/testify注意事项http框架中, 应当分为handle service repository三层, 其中repo层应当有 interface.go 用于标记数据库操作需要实现的函数. 以便切换数据库和单元测试时方便go-sqlmock / 手动实现接口. 还方便 handle 和 service 层调用时 gomock 自动生成

搞懂otelgrpc库是如何做到跨程序链路追踪的
type: item
date: 2026-02-21T00:50:00+08:00
description: 基础grpc是基于http/2.0协议的, otelgrpc库中大量出现metadata可以简单理解成http请求中的header核心文件go.opentelemetry.io\contrib\instrumentation\google.golang.org\grpc\otelgrpc@v0.65.0\stats_handler.go整个trace功能的核心都在这个文件里面.删除了import和注释, 删除了无用函数. 用$1 $2代表参数1 参数2// TagRPC 给每一次rpc调用附加信息 func (h *clientHandler) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context { // 提取info.FullMethodName (/GreetService/SayHello) // name是判断FullMethodName如果第一个字符是/就去掉第一个字符返回, 不是直接返回FullMethodName. 第二个返回是[]attribute.KeyValue{semconv.RPCMethod(name)} // 路径: go.opentelemetry.io\contrib\instrumentation\google.golang.org\grpc\otelgrpc@v0.65.0\internal\parse.go:20 // name: GreetService/SayHello // attrs: 有一个key为rpc.method的值 name, attrs := internal.ParseFullMethod(info.FullMethodName) // attrs类型attribute.KeyValue类型, 一个key可以有多个value. 这里用semconv.RPCSystemNameGRPC再添加rpc.system.name的key attrs = append(attrs, semconv.RPCSystemNameGRPC) if record { // 创建一个新的切片, 以避免错误识别到metrics所使用的相同attrs切片中 spanAttributes := make([]attribute.KeyValue, 0, len(attrs)+len(h.SpanAttributes)) spanAttributes = append(append(spanAttributes, attrs...), h.SpanAttributes...) ctx, _ = h.tracer.Start( ctx, name, trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes(spanAttributes...), ) } // 1. inject内部用metadata获取$1 ctx的内容. // 2. 把上面h.tracer.Start创建的spanid、traceid注入到propagators. // 3. 以key为traceparent, value为00-{traceID}-{spanID}-01 格式来填入. // 4. 用metadata再把md包装一下返回ctx return inject(context.WithValue(ctx, gRPCContextKey{}, &gctx), h.Propagators) } 先来总结一下TagRPC做了哪些事情吧用pb里面定义的service名拼接调用的rpc函数名. 如果第一个是/就去掉赋值给name, 然后给attrs赋值一个key是rpc.method value是未去除/的name给attrs添加key是rpc.system.name value是grpc判断filter是否为空, 不为空用filter过滤一下info创建一个新的spanAttrs避免和metrics冲突创建一个子spanmetrics代码这里跳过...把子span返回的ctx包裹上gctx, 然后提取内容注入给全局propagator(如果没有应当会新建一个)结束, 执行HandleRPC核心就是inject添加了tranceparent的metadata. 然后在服务端接收, 然后otel.GetTextMapPropagator().Extract() 提取传递来的span, 他会自动new成一个trace.SpanContext返回, 然后需要手动trace.SpanContextFromContext(ctx) 提取成span. 进而就可以使用tracer.Start()…

分布式事务几种方案
type: item
date: 2026-02-16T01:26:00+08:00
description: 在了解方案前先了解一下几个理论理论CAPC consistency 一致性: 所有节点在同一时间访问时, 拿到的均为最新数据A availability 可用性: 向非故障节点发起请求后, 每一次请求都能在有限时间内获得合理的响应（而非错误或超时）P partition tolerate 分区容错性: 部分节点挂了, 依旧能正常对外提供服务在分布式系统下,P是前提,仅能实现CP/APCP 一致性 + 分区容错: 当网络分区发生时，为了保证数据一致，系统会拒绝服务或报错，牺牲可用性（A）。适用于银行转账、交易系统AP 可用性 + 分区容错: 当网络分区发生时，系统保证可用，但允许读取旧数据，牺牲强一致性（C），满足最终一致性。适用于大多数Web应用、社交媒体CA 一致性 + 可用性: 仅可在单机环境下成立,常用数据库如mysql oracle等BASEBASE是下面几个单词的缩写, 此理论是互联网实践而来,是对CAP理论的演变和权衡结果.Basically Available 基本可用: 在系统发生故障或高负载时, 必须保证系统核心功能可用, 非核心功能使用降级熔断等策略保证--基本可用Soft-state 软状态: 允许系统存在短暂、无锁、可过渡的中间状态，节点间数据暂时不一致, 但不影响整体可用性.​ 与硬状态(ACID事务, 实时锁定)相对Eventually Consistent 最终一致性: BASE理论强调最终一致性, 即在有限时间和有穷步后, 结果最终一致.一致性的三个状态一致性强调结果, 即数据最后能否一致强一致性: 系统写了什么, 读出来就是什么弱一致性: 不保证多久后读取数据是最新的, 甚至永远可能读不到最新值. 都不保证最终一致性: 现在不一定一致, 但在有限时间后一定一致幂等性同一个操作执行多次, 结果相同常见方案XA 标准化分布式事务这是对2PC 的标准化实现, 是X/Open 组织制定的、跨数据库/消息队列的分布式事务工业级标准协议. 流程参见2PC支持XA 的有MySQL、Oracle、PostgreSQL、SQL Server、RocketMQ、Kafka 等几乎所有主流中间件2PC 两阶段提交借用XA 的角色定义: AP 业务应用(Application Program), TM 事务管理器(Transaction Manager), RM 参与者(Resource Manager)AP 向TM 发起全局事务TM向RM A, B发送要执行的SQlRM A, B开启事务, 执行业务SQL, 两个执行成功/失败均向TM 返回TM 根据返回决定向RM 发送commit或rollback优点: 强一致, 开发简单. 适用于金融等系统缺点: 性能差, 协调者节点挂了会导致锁无法释放(可用心跳包等方法解决), 极端情况下会导致一部分commit, 一部分rollback3PC 三阶段提交CanCommit: 仅检查库存, 锁.PreCommit: 所有节点的Can 均返回成功时, 向所有节点发送PreCommit, 此步骤开启事务但不提交.DoCommit: 所有节点的PreCommit 均返回成功时,向所有节点发送DoCommit.3PC 为所有节点增加了超时决策逻辑, 在PreCommit 后超时可选自动Abort/DoCommit, 在CanCommit 超时自动Abort.极端情况下仍有可能导致协调者只给部分节点发送了DoCommit, 剩下节点可能Abort/DoCommit.生产几乎不用AT 自动事务模式 (Seata)官方链接: https://seata.apache.org/zh-cn/docs/overview/what-is-seata 你可以阅读官方文档获取更详细的说明三个角色TM（Transaction Manager）: 业务应用里的事务发起方TC（Transaction Coordinator）: Seata Server, 管理全局事务状态、协调各分支提交/回滚、维护全局锁RM（Resource Manager）: 由 Seata 代理数据源实现, 自动拦截 SQL、生成日志、执行回滚一阶段执行流程开启本地事务, 解析SQL 的操作(update等)、表（product），条件（where name = 'TXC'）等相关的信息查询前镜像：根据SQL定位到记录, 将记录保留一份执行业务 SQL, 将执行后的结果当做后镜像: 根据前镜像的结果，通过 主键 定位数据将前后镜像和业务SQL 组成一条回滚日志记录, 插入到UNDO_LOG 表中向TC 申请指定表的指定主键的记录的全局锁如果申请到全局锁, 本地事务提交(携带undo log一块). 如果拿锁失败则继续尝试, 超出尝试范…

go实现i18n的几种方法
type: item
date: 2026-02-12T00:33:11+08:00
description: 哔哔叨去年上班时候也去想过实现i18n,但是由于进度要求就没有去查看别的库什么的,而是通过定义const枚举常量返回给前端,再让前端根据常量从json文件中找到对应的文本输出.这种方法导致的就是耦合性较强,同样带来的还有前端首屏加载负担大.好处是给后端减少了在内存中加载大量文本的问题.因此现在看到相关视频来看看go实现i18n的几个方法官方非标准库实现官方有实现golang.org/x/text包,但由于是非go的标准库,所以用例和注释可能会缺少,导致有使用成本# 先安装包 go get golang.org/x/text/language go get golang.org/x/text/message # 安装cli go install golang.org/x/text/cmd/gotext@latestproject/main.gopackage main import ( "golang.org/x/text/language" "golang.org/x/text/message" "strings" ) //go:generate gotext -srclang=zh -dir=./i18n/locales/ update -out=./i18n/catalog.go -lang=zh,en func main() { temp := &strings.Builder{} p := message.NewPrinter(language.MustParse("zh")) p.Print("test") p.Println("this is test string") p.Printf("this is test string1") p.Sprint("this is test string2") p.Sprintf("this is test string3") p.Sprintln("this is test string4") p.Fprint(temp, "this is test string5") p.Fprintln(temp, "this is test string6") p.Fprintf(temp, "this is test string7") }项目根目录执行 gotext -srclang=zh -dir=./i18n/locales extract -lang=zh,en . -srclang代表项目原始语言 -dir要把生成的文件放在哪 -lang代表要生成的语言有哪些 现在目录结构如下 project │ go.mod │ go.sum │ main.go │ └─i18n └─locales ├─en │ out.gotext.json │ └─zh out.gotext.json可以看到生成了/i18n/locales/en /i18n/locales/zh文件夹和两个out.gotext.json文件.我们将两个out.gotext.json文件复制为messages.gotext.json, 然后随便打开一个可以看到只有1,3,7的文本被提取出来, 即Printf Sprintf Fprintf函数的文本会被提取.我们现在在json文件中把translation字段填上对应文本然后执行命令 gotext -srclang=zh -dir=./i18n/locales/ update -out=./i18n/catalog.go -lang=zh,en或 go generate这时候生成了 i18n/catalog.go 文件,我们需要手动修改package为i18n然后在main中新增引用 _ "alitracing/i18n" 再次运行代码输出: testthis is test string // 忽略此行 这是测试字符串1我们忽略输出的第一行,这时候发现文本已经被替换了.要在项目中运用思路如下:用gin拦截器获取前端请求头的accept-language在拦截器中使用language的matcher自动匹配语言,如果没有就返回t的第一个语言t := []language.Tag{ language.English, // en language.Chinese, // zh } matcher := language.NewMatcher(t) tag, _ := language.MatchStrings(matcher, "zh-CN") t1, _ := tag.Base() fmt.Println(t1.String()) // zh将tag放进gin上下文中传递,在需要的地方实例化printer然后用xxxxf函数打印go-i18n待写...

推荐算法--基于物品的协同过滤
type: item
date: 2026-01-18T18:47:00+08:00
description: 基于物品的协同过滤Item base collaborative filtering,这个算法的核心逻辑是: 你喜欢物品A,根据其他喜欢A的用户喜欢的物品B来计算相似度,然后推荐.ItemCF核心算法1. 定义用户交互矩阵U = { "user1": { "item1": 1, "item2": 1, "item3": 0, }, "user2": { "item1": 1, "item2": 0, "item3": 1, }, "user3": { "item1": 1, "item2": 0, "item3": 1, } }2. 计算物品相似度实际时你应当只计算有相同交互物品的用户的其他物品相似度.使用下面的余弦相似度公式计算:$$ w_{ij} = \frac{C[i][j]}{\sqrt{|N(i)| \cdot |N(j)|}} $$C[i] [j]: 物品i物品j共同交互用户数量,遍历每个用户的物品,两两配对然后加一,这就是现实了共现矩阵(统计两个或多个元素在特定上下文内同时出现频率的二维矩阵)|N(i)|和物品i有交互的用户总数,|N(j)|一样.# 实现共现矩阵 collective_matrix = {} # 物品被操作计次 item_operate = {son_name: 0 for _,item in U.items() for son_name in item.keys()} for index, v in U.items(): for ci,item in enumerate(v.keys()): if ci == len(v.keys())-1: break collective_matrix[item] = {name: 0 for name in v.keys()} break for pk in U.keys(): for item in U[pk].keys(): if U[pk][item] != 0: item_operate[item] +=1 for name,v in U.items(): # 获取item长度 l = len(v) # 获取所有key all_keys = list(v.keys()) for i in range(l): # 如果用户没有交互此物品,跳过 if v[all_keys[i]] == 0: continue # user交互的物品总数减去已经遍历过的物品数 for j in range(l-i-1): if v[all_keys[i]] == v[all_keys[i+j+1]]: collective_matrix[all_keys[i]][all_keys[i+j+1]] += 1 # 输出 print(collective_matrix) print(item_operate) # 计算两个物品的相似度 similar_dict = {} for item_parent in collective_matrix.keys(): for son_key in (collective_matrix[item_parent]).keys(): # 默认没有C[i][i]这种情况 if son_key is item_parent: continue # 如果没有人交互i和j两个物品,跳过 if collective_matrix[item_parent][son_key] == 0: continue # 开始计算不同物品的相似度 if item_parent not in similar_dict: similar_dict[item_parent] = {} similar_dict[item_parent] |= { son_key: collective_matrix[item_parent][son_key]/math.sqrt(item_operate[item_parent] * item_operate[son_key]) } print(similar_dict) # 输出 {'item1': {'item1': 0, 'item2': 1, 'item3': 2}, 'item2': {'item1': 0, 'item2': 0, 'item3': 0}} {'item1': 3, 'item2': 1, 'item3': 2} {'item1': {'item2': 0.5773502691896258, 'item3': 0.8164965809277261}}用余弦相似度公式再次确认是否正确:$$ w_{ij} = \frac{1}{\sqrt{3 \cdot 1}} \approx 0.577 $$分子C[i] [j]…

gossip协议
type: item
date: 2025-08-31T22:01:00+08:00
description: Anti-Entropy熵是物理学上的一个概念，代表杂乱无章，而反熵就是在杂乱无章中寻求一致。本质上，反熵是一种通过异步修复实现最终一致性的方法。--钝悟节点周期性随机选择节点,交换全量数据消除差异.周期不会结束.若运行时间较长会导致传递数据过大.状态Susceptible(易感): 节点还没接收到这条消息Infective(传染性的): 节点已经持有这条消息，并且会周期性地向其他节点传播全量消息过程1. 节点周期性触发节点 A 每隔固定时间 T (例如 5 秒) 执行 Anti-Entropy随机选择一个邻居节点 B (fan-out=1,此时只会选择一个邻居节点)2. 交换状态节点 A 将自己完整状态发送给 B节点 B 将自己完整状态发送给 A3. 计算差异A 接收到 B 的状态 → 找出 B 拥有但自己缺失的部分 → 更新自己B 接收到 A 的状态 → 找出 A 拥有但自己缺失的部分 → 更新自己4. 重复执行节点 A 等待下一个周期，再随机选择一个节点执行同样操作所有节点独立、周期性地执行这个流程优点最终一致性确定节点周期性全量或差异同步，理论上所有节点最终必然一致。容错能力强即使某轮传播失败，下轮周期仍会补救，适应网络不稳定。修复遗漏节点无论之前哪个节点没收到消息，周期性同步最终都会把缺失数据同步过去。适合大规模网络同步不依赖单轮成功，概率模型收敛分析简单可靠。缺点传播速度相对慢周期性触发可能导致新消息扩散比 Rumor-Mongering 慢。带宽消耗大全量或差异同步比单条消息增量传播消耗更多网络资源。实现稍复杂需要周期触发、状态摘要或差异计算(摘要和差异是现代实践,论文中没有)，以及节点间同步机制。Rumor-Mongering仅传播最新数据. 每次传播都随机选择节点进行传播, 如果节点已经持有消息(即传播失败), 或因网络超时传播失败, 计数器+1, 达到阈值后停止.由于是增量传播,数据包小,不会无限制传播.状态Susceptible(易感): 节点还没接收到这条消息Infective(传染性的): 节点已经持有这条消息，并且会周期性地向其他节点增量传播消息。Removed: 当收到被传播节点持有此消息,或传播次数超过阈值时,停止传播这条消息过程论文传播停止是因为全网络节点已感染有点难理解,这里用工程实践版的1. 收到消息节点 A 收到消息更新 infected 列表标记这个消息已知2. 选择目标节点 A 从自己的邻居列表中随机选择几个节点 (fan-out数量)3. 尝试发送消息对每个目标节点 B：如果 B 没有收到过消息 M:B 接收消息 M,并在下一轮也会对自己的邻居进行 rumor-mongering 传播节点 A 的计数器 不增加如果 B 已经持有消息 M:节点 A 的计数器 +1 (表示一次失败传播)如果 B 网络超时:节点节点 A 的计数器 +1 (表示一次失败传播)4. 检查停止条件节点 A 对消息 M 的传播计数器达到阈值 (例如 3):节点 A 停止传播消息 M节点 A 不再尝试发送 M 给其他节点5. 下一轮传播对于尚未停止的节点,重复 1-3节点每轮随机选择邻居,尝试传播消息,直到自己的计数器达到阈值优点快速传播新消息节点收到新消息就立即传播,消息扩散速度快低带宽开销（增量传播）只发送新消息,不发送全量状态实现简单逻辑只需随机选择邻居并推送消息,无需全局状态或复杂同步表概率性最终一致性随着轮次增加,高概率网络大部分节点最终会收到消息缺点潜在不一致问题节点 A 向节点 B 传播消息 M，但因为网络超时/丢包, B 没有收到 → 本轮传播失败A 后续随机选择节点传播时 可能没再次选择 BA 的失败计数器达到阈值 → 停止传播如果其他节点也因为随机选择没选择到 B,由于Rumor-mongering不会向Anti-Entropy主动周期性和邻居节点同步, 所以 B 在下次被传播前缺失了这条消息结果：网络中存在少数节点缺失该消息 → 高概率最终一致,但不是强一致性可靠性受网络不稳定影响大网络超时或节点离线可能导致消息传播失败,且节点不会自动重新尝试收敛不可预测消息最终传播到全网的时间依赖于随机性和阈值,不保证精确收敛不能保证强一致性适合快速扩散场景,但不能保证每个节点最终状态完全一致因此现代实践中先使用Rumor-mongering快速传播,再使用Anti-Entropy保证最终一致性通信模式Push节点A全量推送自己所有数据给B节点,B节点对比差异更新自己Pull节点A将自己的key,version推送给BB对比本地数据,增量推送(包含key,value,version)比A新的数据给AA对比更新自己Push-Pull节点A将自己的key,version推送给BB对比本地数据,增量推送(包含k…

fx库解决go依赖注入
type: item
date: 2025-08-31T13:29:00+08:00
description: Fx is the backbone of nearly all Go services at Uber.----https://uber-go.github.io/fx/index.htmlfx和wire选择wire每次启动前需要手动使用CLI编译才能调用依赖注入.这在启动时保证了更好的性能.fx则是在启动时进行依赖注入,因此启动性能不会很好,但是在启动完成后运行时的性能是无影响的.在考虑到业务对于启动时时长并不敏感,且wire如果新增/删除依赖对象需要再次执行cli生成,最终选择了fx.基础函数介绍fx.Provide用于提供需要的依赖对象.这个函数不会一定执行,只有当有调用此依赖对象时才会被执行.package main import ( "fmt" "go.uber.org/fx" ) type A struct{} func NewA() A { fmt.Println("A is running") return A{} } type B struct { } func NewB(p A) B { fmt.Println("B is running") return B{} } func main() { app := fx.New( fx.Provide(NewA), fx.Provide(NewB), ) app.Run() } A函数提供了1,如果B不依赖A函数返回的1,那么A函数就不会执行.注意: 此时B没有被引用,因此只是将对应类型告知给了container,并没有实例化对象// 控制台输出如下,并没有打印 running [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() [Fx] PROVIDE main.A <= main.NewA() [Fx] PROVIDE main.B <= main.NewB() [Fx] RUNNINGfx.Invoke此函数提供的对象一定会被执行,如果你有一个函数没有被依赖,但是必须要他被执行,那就使用Invoke// 此时新增C类型 type C struct { } func NewC(b B) C { fmt.Println("C is running") return C{} } // 再将main修改成这样 func main() { app := fx.New( fx.Provide( NewA, NewB, ), fx.Invoke(NewC), ) app.Run() } 如果没有Invoke C那么Provde的A,B两个函数就不会被执行. 但是现在C一定会被执行,且依赖1,2.那么A和B也就会被执行.// 控制台输出如下,可以看到是根据依赖自动实例化对象的 [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() [Fx] PROVIDE main.A <= main.NewA() [Fx] PROVIDE main.B <= main.NewB() [Fx] INVOKE main.NewC() [Fx] BEFORE RUN provide: main.NewA() [Fx] RUN provide: main.NewA() in 0s [Fx] BEFORE RUN provide: main.NewB() [Fx] RUN provide: main.NewB() in 0s [Fx] RUNNING A is running B is running C is runningfx.Annotate和fx.As这两个函数通常是一块出现的,用于将一个函数的返回,强行转换成另一个接口package main import ( "fmt" "go.uber.org/fx" ) type Animal interface { Say() } type A struct{} func (receiver A) Say() { fmt.Println("i am a") } func NewA() A { fmt.Print…

Raft算法
type: item
date: 2025-08-24T21:07:00+08:00
description: 前置请先看完 http://www.kailing.pub/raft/index.html 后继续查看文章角色leaderfollowercandidate每个节点同时只扮演一个角色,允许切换Leader领导者,接收客户端请求,使用日志条目复制数据到Follower维护状态term: 当前任期号log[]: 日志条目数组,每个条目包含客户端指令和产生该条目的任期号commitIndex: 已被集群多数节点复制且可以提交的日志最大索引（所有≤该索引的日志均可应用到状态机）lastApplied: 已应用到状态机的日志最大索引（记录本地状态机执行到哪条日志）nextIndex[]: 对每个 follower 维护一个值，表示下一次向该 follower 发送日志的起始索引matchIndex[]: 对每个 follower 维护一个值，表示该 follower 已成功复制的最大日志索引Follower跟随者,被动接受Leader的日志复制请求和心跳包维护状态term: 当前任期号（与 leader 保持一致，或在发现更高任期时更新）log[]: 日志条目数组（与 leader 同步的日志副本）commitIndex: 已被集群多数节点复制且可以提交的日志最大索引（由 leader 通过心跳同步）lastApplied: 已应用到状态机的日志最大索引（记录本地状态机执行到哪条日志）votedFor: 当前任期内投票给的节点 ID（用于避免重复投票，保证每个任期仅投一票）超时计时器：用于检测 leader 是否失联（若超时未收到 leader 心跳，会转换为 candidate 发起选举）Candidate候选者,当Follower在一定时间没有接收到Leader心跳包后将从follower切换到candidate进行选举请求日志复制作用: 专门用于向 follower 同步新的日志条目内容: 包含 leader 的任期号（term）、前一条日志的索引和任期（用于 follower 校验匹配）、需要同步的日志条目（可能是一条或多条）、leader 当前的commitIndex（用于同步提交状态）心跳包作用：用于维持leader的地位,定时发送,防止follower因超时触发新的选举。内容：格式与日志复制请求基本一致，但不包含任何新的日志条目（可以理解为空日志的复制请求），同样会携带 leader 的term和commitIndex投票请求作用: 竞选leader内容: 等待150ms-300ms随机时间过期后为自身term+1,给自己投一票,然后向其他节点发送投票请求,多数派同意后变成leader成员变更请求作用: 动态增删节点,避免脑裂问题执行流程客户端向Leader发送请求Leader接受到请求,使用日志复制将数据同步到Follower多数派Follower处理完成响应给LeaderLeader收到多数派响应,修改自身CommitIndex,返回响应给客户端Leader等待下次心跳包进行同步提交信息如果在同步提交信息前挂掉了怎么办?此时Follower变成Candidate进行选举,注意,只有日志与多数派相同的Candidate才会被选举为Leader新的Leader当选,任期+1,立刻触发日志同步步骤如下向所有follower发送包含自身日志信息的同步请求（基于nextIndex初始化值）当follower返回响应时,NewLeader会通过matchIndex记录每个follower已复制的最大日志索引当NewLeader发现某条日志的matchIndex已满足多数派已复制（即超过半数 follower 的matchIndex≥该日志索引），就会直接将自己的commitIndex更新为该日志索引此时之前Leader未同步提交的信息将会被标记提交Reference[]: http://www.kailing.pub/raft/index.html "Raft 算法动画演示"[]: https://chatgpt.com/ "ChatGPT"[]: https://www.doubao.com/ "豆包"

Paxos算法
type: item
date: 2025-08-24T18:56:00+08:00
description: 介绍There is only one consensus protocol, and that’s “Paxos” — all other approaches are just broken versions of Paxos—— Mike Burrows角色proposal(提议者),发起提案acceptor(接受者),决策提案learner(学习者),当提案被通过,会被学习者学习,学习者不参与提案的发起和抉择[!IMPORTANT]一个agent允许扮演多个角色提案一次提案通过多轮决策最终决定一个唯一值.每个提案均会有instance_id,下面用i代替每个agent均会带有promised_rnd: 已承诺决策ID,下面用prnd代替accepted_rnd: 已接受决策ID,下面用arnd代替accepted_v: 已接受决策值,下面用av代替传递的rnd不仅会有round_id,还会携带proposal的唯一标识实现理想情况假设现有4个节点(agent),其中A为proposal,BCD为Acceptorproposal A向BCD发送信息: i=1,rnd=(1,A)收到多数派返回结果: i=1,prnd=(1,A),arnd=0,av=nilA发现prnd和自己的rnd相同,因此开始Phase2,发送: i=1,rnd=(1,A),v="G"接收到多数派节点返回信息: i=1,arnd=1,av="G",prnd=1理想情况下提案结束.并发写冲突X向多数派BC发送: i=1,rnd=(1,X)X收到多数派返回: i=1,prnd=(1,X),arnd=0,av=nilY向多数派CD发送: i=1,rnd=(1,Y)Y收到多数派返回: C: i=1,prnd=(1,X),arnd=0,av=nilD: i=1,prnd=(1,Y),arnd=0,av=nilY查询多数派返回的prnd != rnd,rnd+1继续发送请求 i=1,rnd=(2,Y) 给BCY收到多数派返回: i=1,prnd=(2,Y),arnd=0,av=nilX此时进行Phase2发送请求: i=1,rnd=(1,X),v="G"B节点返回: i=1,prnd=(1,X),arnd=1,av="G" B节点更新成功C节点判断prnd!=rnd返回: i=1,prnd=(2,Y),arnd=0,av=nil C节点更新失败Y进行Phase2发送请求: i=1,rnd=(2,Y),v="J"Y收到多数派返回: i=1,prnd=(2,Y),arnd=2,av="J"此时X重新进行Phase1,由于上面被拒绝,这次rnd增加为3,发送请求: i=1,rnd=(3,X)X收到多数派返回:B: i=1,prnd=(3,X),arnd=1,av="G"C: i=1,prnd=(3,X),arnd=2,av="J"X发现多数派响应中,有两个接受的值,av="G" av="J"因此,这次不能写入自己的值,而是进行修复.由于C节点的arnd最新,考虑到paxos算法的约定: 只接收rnd大于自己的请求,如果av="G"已经被多数派确认,那么av="J"一定不会被写入. 所以将av="J"确定为要修复值X向BC节点进行Phase2请求: i=1,rnd=(3,X),v="J"X接收到多数派响应: i=1,prnd=(3,X),arnd=3,av="J"提案结束最终值J被确定极端情况--rnd无限提升X向BC发送i=1,rnd=1X收到多数派成功返回Y向CD发送i=1,rnd=1失败(这里没画出来),提升rnd后发送i=1,rnd=2Y收到多数派成功返回X向BC发送i=1,rnd=1,v="X",B节点成功,C节点失败X提升rnd,向BC发送i=1,rnd=3,收到成功响应Y向CD发送i=1,rnd=2,v="Y",D节点成功,C节点失败Y提升rnd .....无限重试...Muti-Paxos将多个提案的Phase1合并到一个请求中发送Fast-Paxos逻辑一步到位直接发送Phase2请求,其中rnd=0,这样可以保证冲突时安全退回,因为classic-paxos的rnd一定>=1如果Fast-Paxos冲突,退化到Classic-Paxos处理为什么增加多数派数量引用xp大佬的图除X,Y外,中间五个节点从左到右命名为ABCDEX使用rnd=0占用了ABC.因为rnd同为0,Y只占据了DE.Y退化至classic-paxos.此时CDE的arnd都为0,Y无法判断哪个值被多数派接受,如果选择CDE则最终值为y0,但实际x0为多数派接受.此时应当修改多数派从n/2+1改为n*(3/4)+1,这样接受X的多数派数量为4个,Y不论如何去选都至少有两个节点av为x0…