Raft 协议:选举、日志复制与强一致

Raft 协议 本文是分布式算法科普系列第二篇。上一篇讲了 Distro 协议如何用"去中心化 + 异步同步"实现 AP 模型——写完立刻返回、事后慢慢对齐。这一篇讲它的反面:Raft 如何用"选出一个老板 + 事事多数同意"实现 CP 模型——宁可暂时不可用、绝不返回错误数据。 一、故事:Paxos 太难了,于是有了 Raft 在 Raft 出现之前,分布式共识领域有一个"上古神器"——Paxos。Paxos 由 Leslie Lamport(就是写 LaTeX 的那位)在 1989 年提出,理论正确性无可挑剔,但有一个致命的工程问题:几乎没有人能真正看懂它。 Lamport 在 1998 年发表了一篇补充论文《Paxos Made Simple》,摘要第一句话就是——“The Paxos algorithm, when presented in plain English, is very simple."(用大白话讲,Paxos 其实很简单。)但工程界的反馈很统一:不,它一点也不 simple。 这不是段子,是真实历史。Google 的 Chubby 分布式锁系统在实现 Paxos 的过程中遇到了大量问题,Chubby 的作者 Mike Burrows 有一句著名的吐槽:“世界上只有两种共识算法——Paxos 和那些没人能证明正确的算法。” 2013 年,斯坦福大学的博士生 Diego Ongaro 和导师 John Ousterhout 决定正面解决这个问题。他们的出发点和前面所有人都不一样——把"可理解性"作为算法的首要设计目标,而不是附带的副产品。 Ongaro 从头设计了一个全新的共识算法,刻意把整个协议拆成三个相对独立的模块——领导者选举、日志复制、安全保证——每个模块都可以单独理解。2014 年,他们发表了论文《In Search of an Understandable Consensus Algorithm》(寻找一个可理解的共识算法),Raft 正式诞生。 ...

一月 19, 2023 · 4 分钟 · 803 字 · yaomingye

Distro 协议:去中心化与最终一致

Distro 协议 本文是分布式算法科普系列第一篇。系列面向完全没接触过分布式的业务开发者,用历史故事开场、比喻辅助理解、不涉及数学证明和代码实现。 一、故事:微服务来了,电话号码本怎么办 早年的单体应用,一个进程内部互相调用,不需要"发现"对方——函数调用就行。微服务架构来了,服务实例的数量和位置开始动态变化:扩容加几台、某台机器宕机撤掉、滚动发布换一批新实例。服务 A 要调用服务 B,必须知道此时此刻服务 B 在哪些 IP 和端口上。 最朴素的想法是——搞一个电话号码本。所有服务启动后把自己的地址登记上去,调用方去电话本里查。这个"电话本"就是注册中心。 但问题来了:电话本自己怎么保证不挂?如果只有一个电话本,挂了所有服务都变瞎子。那就多搞几个电话本,每个存一份完整的地址副本。新问题又来了——服务 A 的地址变了,怎么保证所有电话本上写的都一样? 用个比方来理解这个场景: 一个小区有三家传达室,每家都有一本住户登记簿。住户搬家了会通知最近的那家传达室更新记录。有人来访时,随便问哪家传达室都能查到住户的门牌号——哪怕其中一家的登记簿还没来得及更新。 这就是 Distro 协议要解决的问题:在一个多节点集群中,如何让写入请求快速得到响应(高可用),同时保证各节点上的数据最终会变得一致(最终一致)。 二、前置:为什么不能又一致又可用 在深入 Distro 之前,需要先理解一个约束——CAP 定理。 📌 前置知识:CAP 定理说的是,在一个分布式系统中,当网络发生分区(Partition,即节点之间网络不通)时,你只能在一致性(Consistency)和可用性(Availability)之间二选一。网络没出问题时,一致性和可用性可以同时满足。 用电话本的比方说:一号传达室和二号传达室之间电话线断了(网络分区)。此时有人去一号传达室改了一个住户的门牌号(写操作)。一号传达室有两个选择: 选一致性(C):拒绝这个修改请求,因为无法同步给二号传达室。结果:修改失败,但所有传达室的数据保持一致。 选可用性(A):先接受修改,等电话线恢复后再同步给二号传达室。结果:修改成功,但二号传达室暂时还是旧数据。 注册中心这个场景天然更适合选 AP(可用 + 分区容忍)。原因很现实:返回一个略微过期的实例地址(可能已经下线了),调用方最多重试一次换另一个实例;但注册中心如果拒绝查询,整个调用链直接断了。两害相权取其轻。 Raft 协议选了 CP(后面一篇会讲),Distro 协议选了 AP。这就是它们在同一套 Nacos 系统里分工的原因——服务发现走 Distro(AP),配置中心走 Raft(CP)。 三、Distro 的核心设计 3.1 没有主节点 这是 Distro 和 Raft 最根本的区别。Raft 通过选举产生一个主节点(Leader),所有写操作必须经过主节点——主节点把日志复制给从节点,多数确认后提交。如果主节点挂了,必须重新选举,选举期间集群无法写入。 Distro 没有主节点。集群里每个节点都是平等的。写请求可以打到任意一个节点,该节点立刻返回成功,然后异步把变更同步给其他节点。 用一个比方来理解这个差异: Raft 像公司报销流程——所有报销单必须部门经理(Leader)签字才能入账。经理出差了?等着,等他回来或者换新经理。 Distro 像小组共享文档——任何人改了一段,改了就先保存,其他同事打开文档时看到最新版就行。就算有人离线没同步到,等他上线后会自动补上。 去中心化带来的直接好处:没有主节点,就不存在主节点宕机后的"选举窗口"。任何时候任何节点都能处理读写。 3.2 数据分片与一致性哈希 Distro 虽然每个节点都能独立处理写请求,但为了减少冲突和降低同步开销,它对数据做了分片——每个服务实例的注册信息只由一个"负责节点"来权威维护,其他节点虽然也存了这份数据,但只是副本。 分片机制用了一致性哈希。一致性哈希把存储空间组织成一个首尾相连的环(0 ~ 2^32-1)。每个节点在环上占据一个位置,每条数据根据 Key 的哈希值落在环上的某个点,顺时针方向遇到的第一个节点就是这条数据的负责节点。 ...

一月 18, 2023 · 2 分钟 · 376 字 · yaomingye

Redis 高可用架构:主从、哨兵、集群与分片

主从、哨兵、集群与分片——高可用架构全解 一、问题切入:单机 Redis 能走多远 开发环境启动一个 Redis 实例,redis-cli 连上去,SET / GET 一切正常。然后某天线上出了问题: 促销活动期间,Redis 内存飙到 32GB 上限,新的写入被拒绝 服务器宕机,缓存全丢,所有请求直接穿透到 MySQL,服务雪崩 同一个 Key 被几百个并发请求同时修改,客户端频繁收到 READONLY 错误 这些问题指向同一个根因:单机 Redis 有三个硬伤。 硬伤 表现 后果 内存上限 一台机器最多几百 GB 内存,存不下全量数据 频繁淘汰 / OOM 单点故障 进程挂掉 → 整个缓存层不可用 请求穿透到 DB,服务雪崩 写吞吐瓶颈 单机只能处理几万 QPS 的写入,核心在主线程串行执行 促销期间扛不住 Redis 为了解决这三个问题,依次演进出了三种架构模式——主从复制、哨兵模式、集群模式。理解这三者之间的关系,是开发者对接云 Redis 服务的前提。 flowchart TD classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; S[📦 单机 Redis] --> M[📋 主从复制] M --> S2[🔍 哨兵模式] S2 --> C[🔗 集群模式] M --> M1["解决问题: 读扩展 + 数据冗余\n新增问题: 手动故障转移"] S2 --> S2A["解决问题: 自动故障转移\n新增问题: 写仍单点"] C --> C1["解决问题: 写扩展 + 海量数据\n新增问题: 跨槽限制"] class S,M,S2,C process; class M1,S2A,C1 highlight; class S startEnd; 这张演进路线图的含义:后一层架构不是替代前一层,而是叠加。集群模式内置了主从复制和哨兵的部分能力,但三者解决的问题域并不完全相同。下面逐一展开。 ...

一月 17, 2023 · 6 分钟 · 1251 字 · yaomingye

时序数据库在物联网日志系统中的设计与实践

时序数据库在物联网日志系统中的设计与实践:从数据结构到SpringBoot接入 一、一张表撑不住了 📌 前置知识:了解关系型数据库(RDBMS)基本概念,知道什么是SQL、索引、事务。知道什么是磁盘IO,理解顺序IO和随机IO的大致性能差距(约一个数量级)。 某开发者接手了一个物联网项目,设备每秒上报一次数据,1000台设备跑了一周,MySQL 的 iot_logs 表已经几千万行,查询一条设备最近一小时的数据要跑十几秒。加了索引,写入又慢得让人抓狂。这种场景,写过的都懂——关系型数据库不是不能存时序数据,而是它的每一个设计决策都和时序场景的需求背道而驰。 物联网日志(IoT Logs)具有几个典型特征: 写入密集且顺序:数据按时间顺序持续产生,极少更新或删除 查询模式固定:通常按时间范围 + 设备ID聚合查询,很少跨设备做复杂JOIN 数据冷热分明:最近几小时的数据被频繁访问,一周前的数据偶尔查一次 压缩空间巨大:传感器数据变化缓慢,相邻时间点数据高度相似,而 RDBMS 的通用压缩算法根本不认识这种模式 这就引出了一个核心问题:时序数据库(TSDB,Time Series Database)到底在数据结构层面做了哪些改造,让它和经典关系型数据库(RDBMS)产生了本质差异?以及如何在 Spring Boot 项目中把这些 TSDB 用起来? 二、数据结构层面的根本差异 2.1 行式存储 vs 列式存储 RDBMS 以 行(Row) 为单位组织数据,一行数据的各个字段在磁盘上连续存放。这种设计让单行读写非常高效——适合 OLTP 场景下"查一行、改一行"的套路。但面对时序查询时,问题就暴露了:查询"过去一小时内所有传感器的温度平均值",只需要温度这一个列,行式存储却会把湿度、气压、设备状态等几十个列一并从磁盘读进内存——IO 利用率奇低。 TSDB 采用 列式存储(Columnar Storage),将同一列的数据在磁盘上连续存放。查询温度列时,只读取温度相关的数据块,其他列完全不参与 IO。这和 ClickHouse 等 OLAP 引擎的思路一致,但 TSDB 在列式基础上又叠加了时间维度的特殊优化。 flowchart TD subgraph row["📦 行式存储 RDBMS"] direction TB r1["Row1│ts:1000│temp:25.3│hum:68│loc:WH1"] --> r2["Row2│ts:1001│temp:25.4│hum:67│loc:WH1"] r2 --> r3["Row3│ts:1002│temp:25.3│hum:68│loc:WH1"] end subgraph col["📦 列式存储 TSDB"] direction TB c_ts["Col_ts: 1000, 1001, 1002"] --> c_temp["Col_temp: 25.3, 25.4, 25.3"] c_temp --> c_hum["Col_hum: 68, 67, 68"] c_hum --> c_loc["Col_loc: WH1, WH1, WH1"] end row -.-> col classDef default fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; class r1,r2,r3,c_ts,c_temp,c_hum,c_loc default class r1,c_temp highlight ⚠️ 新手提示:不要混淆"列式存储的数据库"和"时序数据库"。列式存储是 TSDB 的技术手段之一,但不是全部。ClickHouse 是列式存储的 OLAP 数据库,但它不是 TSDB——它没有时间维度的特殊处理(如自动分区、自动过期删除)。 ...

一月 16, 2023 · 7 分钟 · 1406 字 · yaomingye

七牛云 OSS + SMTP 邮件:两个轻量级第三方接入实战

七牛云 OSS + SMTP 邮件接入 第1步:目标说明 — 图片上传和发邮件,每个项目的标配 电商项目两个常见的外部依赖:图片存储和邮件通知。商品图、用户头像得有个地方存,登录异常告警、注册欢迎得有个通道发。 Mall 项目用七牛云 OSS 存图片和文件(CDN 加速),用 SMTP 发邮件(Freemarker 模板渲染 HTML 正文),两个接入都不复杂,加起来不超过 200 行代码。本教程从申请凭证到代码封装,两套接入一次讲完。 第2步:前置条件 条件 七牛云 OSS SMTP 邮件 账号 qiniu.com 注册,实名认证 任意邮箱服务(163、QQ邮箱、企业邮箱) 凭证 AccessKey / SecretKey 邮箱地址 + SMTP 授权码 存储空间 在七牛云控制台创建 Bucket 无需 域名 Bucket 绑定 CDN 加速域名 无需 ⚠️ 新手提示:七牛云 OSS 和阿里云 OSS 是竞品,功能几乎一样。Mall 项目选了七牛云,如果公司已经在用阿里云 OSS,代码结构完全能用,换 SDK 即可。接入模式是通用的。 第3步:环境搭建 — 七牛云 OSS 添加 Maven 依赖 <dependency> <groupId>com.qiniu</groupId> <artifactId>qiniu-java-sdk</artifactId> <version>7.4.0</version> </dependency> 配置属性类 @Component @ConfigurationProperties(prefix = "oss.qiniu") @Data public class QiNiuConfig { private String accessKey; // 七牛云 AK private String secretKey; // 七牛云 SK private String bucketPictureName; // 图片 Bucket 名称 private String domainPicture; // 图片 CDN 域名 private String bucketFileName; // 文件 Bucket 名称 private String domainFile; // 文件 CDN 域名 } 图片和文件分两个 Bucket——图片需要 CDN 加速 + 图片处理(裁剪、加水印),文件的访问频率低但需要支持大文件下载,分开管理方便设置不同的生命周期策略。 ...

一月 15, 2023 · 4 分钟 · 846 字 · yaomingye

支付宝支付接入:沙箱调试 + EasySDK + QR 码支付实战

支付宝支付接入 第1步:目标说明 — 支付接入最怕的是什么 不是代码复杂,而是"没法在本地调"——每次测试都要真的扫码付钱,退款、对账、异常场景根本模拟不了。 支付宝提供了沙箱环境(sandbox),完全模拟生产接口的行为,但用的是虚拟账户和虚拟资金。开发人员在沙箱里可以反复测试支付、退款、异常场景,不花一分钱。 Mall 项目对接的是支付宝"当面付"(FaceToFace),生成二维码让用户扫码支付。本教程覆盖:沙箱环境申请 → RSA2 密钥配置 → EasySDK 集成 → QR 码生成 → MockPay 开发模式 → 生产切换。 第2步:前置条件 条件 要求 获取方式 支付宝开放平台账号 已注册并实名 open.alipay.com 沙箱环境 已开通(免费) 开放平台 → 控制台 → 沙箱环境 沙箱应用 自动创建 沙箱环境会自动生成一个测试应用 RSA2 密钥对 2048 位 支付宝密钥生成工具 或 openssl genrsa ⚠️ 新手提示:沙箱环境和正式环境是两套完全独立的系统——沙箱的 APPID、网关地址、密钥、支付宝公钥都和正式环境不同。Sandbox 网关是 openapi-sandbox.dl.alipaydev.com,生产网关是 openapi.alipay.com。切换环境不是改一两个配置项,而是整套凭证都得换。 第3步:环境搭建 添加 Maven 依赖 <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-easysdk</artifactId> <version>2.2.0</version> </dependency> EasySDK 是支付宝官方封装的"开箱即用"SDK。老版 SDK alipay-sdk-java 需要手动构造请求参数、手动验签,代码量是 EasySDK 的 3 ~ 5 倍。EasySDK 一个 Factory.Payment.FaceToFace().preCreate() 就完成预下单。 ...

一月 14, 2023 · 4 分钟 · 655 字 · yaomingye

阿里云短信接入:双 Provider + Mock 验证码实战

阿里云短信接入 第1步:目标说明 — 别在生产环境调试短信 发送短信验证码是登录/注册流程的核心环节。但对接阿里云短信服务时有两个现实问题: 2024 年后个人资质基本申请不到官方短信签名和模板,审核周期长还不一定过 开发调试时不可能真发短信,每条几分钱不说,频繁发送会被运营商拦截 Mall 项目从这两个痛点出发,设计了一套"双 Provider + Mock 开关"的短信架构: 生产环境:用 dysmsapi(阿里云官方短信 SDK),需要企业资质 个人测试:用 dypnsapi(阿里云号码验证服务),个人账号可申请 本地开发:Mock 模式跳过真发,固定验证码 123456 目标是把这套架构讲清楚,读者照着做能在 30 分钟内完成短信接入。 第2步:前置条件 条件 要求 验证/获取方式 阿里云账号 已实名认证 aliyun.com 注册 AccessKey 已创建 RAM 用户,获取 AK/SK 阿里云控制台 → RAM 访问控制 → 创建 AccessKey 签名和模板(dysmsapi) 企业资质,审核通过 阿里云短信服务控制台(个人很难申请) 号码验证服务(dypnsapi) 个人账号可开通 阿里云号码验证服务控制台 ⚠️ 新手提示:dysmsapi 和 dypnsapi 是阿里云的两个不同产品。dysmsapi 是传统短信服务,需要申请签名和模板;dypnsapi 是号码验证服务,提供预置的短信模板(验证码、通知等),个人资质就能用。本教程两种都讲,读者根据自己的资质选一种即可。 第3步:环境搭建 添加 Maven 依赖 <!-- 方案1:官方短信 SDK --> <dependency> <groupId>com.aliyun</groupId> <artifactId>alibabacloud-dysmsapi20170525</artifactId> <version>3.0.0</version> </dependency> <!-- 方案2:号码验证服务 SDK(个人可用) --> <dependency> <groupId>com.aliyun</groupId> <artifactId>alibabacloud-dypnsapi20170525</artifactId> <version>1.0.8</version> </dependency> 两个依赖都加也没问题,项目通过 @ConditionalOnProperty 在运行时选一个生效,不会冲突。 ...

一月 13, 2023 · 5 分钟 · 878 字 · yaomingye

Flyway 数据库迁移:告别手工执行 SQL 脚本

Flyway 数据库迁移 第1步:目标说明 — 从 38 个手工 SQL 脚本说起 Mall 商城项目的 README 里有一句坦诚的自我检讨: SQL 脚本丢在 sql/ 目录手工执行,没有 Flyway / Liquibase。无法追踪某台机器跑过哪些 DDL,回滚靠猜。 打开 sql/feature_1.0.1/ 目录一看——38 个 SQL 文件,命名靠日期: create_table_2024_01_05.sql create_table_2024_01_29.sql alter_table_2024_02_27.sql alter_table_2024_05_12.sql alter_table_2024_09_26.sql ... 每次上线,开发人员手动连上数据库,挑出"这次要跑的"脚本,逐个执行。脚本里还夹杂了手工更新历史数据的 DML: use mall_db; alter table mall_product add column `cover_url` varchar(200) DEFAULT NULL COMMENT '封面图片url'; -- 更新历史数据 update mall_product p inner join mall_product_photo m on p.id = m.product_id set p.cover_url = m.url where m.type=1 and m.is_del=0; -- 别忘了还有分库 use mall_db_order_0; alter table order_trade_item_0 add column `cover_url` varchar(200) ... alter table order_trade_item_1 add column `cover_url` varchar(200) ... use mall_db_order_1; alter table order_trade_item_0 add column `cover_url` varchar(200) ... alter table order_trade_item_1 add column `cover_url` varchar(200) ... 这种模式下会发生什么,写过的人都懂: ...

一月 12, 2023 · 5 分钟 · 1029 字 · yaomingye

Knife4j 接口文档从配置到上线

Knife4j 接口文档 第1步:目标说明 — 打造可交互的 API 文档 后端写完接口,前端过来问"这个参数什么意思"“返回字段有哪些"“能不能让我直接调一下看看效果”——这种场景写过的都懂。 Swagger 就是来解决这个问题的。它能根据代码里的注解自动生成接口文档页面,前端直接在页面上看字段说明、调接口、看返回,不用再追着后端问。而 Knife4j 是 Swagger 的增强 UI,比原生 Swagger UI 好看得多,还支持离线文档导出、全局参数设置、接口排序等实用功能。 本教程基于 Mall 商城项目的真实配置,从零开始搭建一套 Knife4j + Swagger 接口文档,目标是让读者看完就能在自己的项目里用起来。 最终效果:访问 Knife4j 页面,能看到按模块分组的接口列表,点开任意接口能看到请求参数、响应示例,还能直接在页面上填入 Authorization 请求头,在线调试接口。 第2步:前置条件 开始之前,先确认项目环境满足以下条件。 条件 要求 验证命令 JDK 1.8+ java -version Maven 3.6+ mvn -v Spring Boot 2.x 查看 pom.xml 中 spring-boot-starter-parent 版本 现有 Spring Boot Web 项目 已有 Controller 项目中存在 @RestController 类 ⚠️ 新手提示:Knife4j 3.0.2 基于 Springfox 3.0.0,兼容 Spring Boot 2.x。如果是 Spring Boot 3.x 项目,需要使用 knife4j-openapi3-spring-boot-starter 4.x 版本,注解包名也从 io.swagger.annotations 变为 io.swagger.v3.oas.annotations,差异较大,本教程不涉及。 ...

一月 11, 2023 · 6 分钟 · 1108 字 · yaomingye

一个商城项目的结构化日志改造实录

结构化日志改造实录 第1步:目标说明 — 结构化日志到底解决什么问题 某开发者接手了一个 Spring Boot 商城项目的维护。项目跑得挺稳,直到某天凌晨收到告警——短信发送失败了,但翻遍日志找不到任何记录,因为 catch(Exception e) 的块是空的。 这就是非结构化日志的典型场景:日志看似写了,但关键信息全丢了。 结构化日志(Structured Logging)不是一门新技术,而是一种日志编写规范。它的核心目标只有一句话: 让日志既可以被人快速理解,也可以被机器(ELK、Loki、Splunk)精确检索。 本次教程通过一个真实商城项目的日志审计和改造过程,教会读者: 如何识别团队代码中的日志反模式 如何用 SLF4J 的参数化语法替代字符串拼接 如何配置 logback 实现 dev 控制台 + prod 文件持久化 的双环境策略 如何避免异常栈丢失、日志级别混乱等常见坑 完成本教程后,读者能独立完成一个 Spring Boot 项目的日志规范化改造。 第2步:前置条件 — 需要准备什么 开始之前,确保本地环境满足以下条件。 前置项 版本要求 说明 JDK 1.8+ Spring Boot 2.x 编译和运行 Spring Boot 2.x 自带 spring-boot-starter-logging(Logback + SLF4J) Lombok 1.18+ 提供 @Slf4j 注解,免去手写 Logger 声明 Maven 3.6+ 项目构建工具 验证命令: # 检查 JDK java -version # 检查 Maven mvn -version # 检查 Lombok 依赖(在 IDE 中确认 @Slf4j 可用) 📌 前置知识:读者需要了解 Java 异常体系的基本概念(checked / unchecked exception)、Spring Boot 项目的基本结构(Controller → Service → Mapper),以及日志级别 TRACE / DEBUG / INFO / WARN / ERROR 的含义。 ...

一月 10, 2023 · 11 分钟 · 2305 字 · yaomingye
Cat Radio