<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>后端 on Zampo Blog</title><link>https://blog.cpdd.fyi/categories/%E5%90%8E%E7%AB%AF/</link><description>Recent content in 后端 on Zampo Blog</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Wed, 15 Jul 2026 12:00:00 +0800</lastBuildDate><atom:link href="https://blog.cpdd.fyi/categories/%E5%90%8E%E7%AB%AF/index.xml" rel="self" type="application/rss+xml"/><item><title>MySQL 开始分库分表时，别急着换国产数据库：先问完这五句</title><link>https://blog.cpdd.fyi/posts/mysql-sharding-domestic-database-five-questions/</link><pubDate>Wed, 15 Jul 2026 12:00:00 +0800</pubDate><guid>https://blog.cpdd.fyi/posts/mysql-sharding-domestic-database-five-questions/</guid><description>&lt;p&gt;一个 MySQL 集群开始不好伺候，团队通常会经历同一串动作：加只读副本、拆热点表、按用户 ID 分库、路由写进中间件、跨库查询单独做服务。&lt;/p&gt;
&lt;p&gt;最开始都觉得还能扛。&lt;/p&gt;
&lt;p&gt;直到某次需求只是“查一下用户在所有店铺的订单”，代码评审里先出现的不是 SQL，而是“这个表到底落在哪个库”“能不能接受异步汇总”“跨库事务谁兜底”。这时候，“换国产数据库”会突然变成一个很有诱惑力的选项。&lt;/p&gt;
&lt;p&gt;但它不是止痛药。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;国产数据库不是让 MySQL 原地变强；它是在替你接走一部分扩展、可用性和兼容性问题，同时把另一部分复杂度交还给你。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇不列厂商，也不讲谁排第几。只把一个只用过 MySQL 的开发者最该问的五句话讲清楚：它是什么，能干什么，你为什么可能需要它，代价落在哪里，最后又到底能换回什么。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;【配图占位 1】单库 → 应用层分片 → 多节点数据层。突出“复杂度没有消失，只是换了落点”。详见配图 brief。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="1-这是啥"&gt;1. 这是啥&lt;/h2&gt;
&lt;p&gt;一句话：&lt;strong&gt;国产数据库是一组由本土厂商交付的关系数据库产品；对应用来说仍然是表、SQL、索引和事务，对团队来说换的是兼容边界、扩展方式和运维模型。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;别把“国产”理解成一种新的数据库原理。&lt;/p&gt;
&lt;p&gt;你不会因为换了它，就突然不用写索引、不用看慢查询、不用处理死锁。业务代码照样要连数据库，ORM 照样会生成 SQL，线上照样会有大事务和误删风险。它首先是一个产品与交付选择，不是一种脱离关系数据库常识的新技术。&lt;/p&gt;
&lt;p&gt;真正容易误解的是后半句。&lt;/p&gt;
&lt;p&gt;MySQL 用久了，很多人默认的世界观是：一份数据主要待在一台主库上；容量不够，先加机器；读不够，拉副本；再不够，就由应用决定数据去哪张表、哪个库。&lt;/p&gt;
&lt;p&gt;而你考虑换的这类产品，常常会把“数据能不能放到多台机器上”“一台机器故障后谁接手”“应用看到的还是不是一张表”这些事，往数据库内部收一部分。&lt;/p&gt;
&lt;p&gt;这不代表它替你做完了架构。&lt;/p&gt;
&lt;p&gt;它只是把原来散在路由层、同步任务、脚本和人工操作里的规则，换成了数据库自己的规则。规则放的位置变了，责任没有消失。&lt;/p&gt;
&lt;h2 id="2-干什么的"&gt;2. 干什么的&lt;/h2&gt;
&lt;p&gt;和单机 MySQL 相比，开发者真正能感知到的变化，通常不是某个宣传名词，而是下面几件很具体的事。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据可以跨机器放。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;以前一张大表顶到单机磁盘、CPU 或写入上限，你的动作往往是拆库拆表。拆完以后，&lt;code&gt;orders_17&lt;/code&gt; 和 &lt;code&gt;orders_18&lt;/code&gt; 在应用眼里已经不是一张表了：路由要算，聚合要补，运维要记住每个库的容量。&lt;/p&gt;
&lt;p&gt;有些数据库可以把一张逻辑表拆成多个数据分区，放到不同机器上。应用仍然面对一张表，数据到底在哪台机器，由系统按规则处理。你少写了一层分片路由，至少不用每次业务查询都先问“落点在哪”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;事务可能跨节点。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分库之后，最难受的不一定是查询，而是本来一句 &lt;code&gt;BEGIN&lt;/code&gt; 能解决的事情，忽然要面对多个库。你可以改成最终一致，也可以上补偿和对账；这都是工程方案，但每种都要把失败路径补齐。&lt;/p&gt;
&lt;p&gt;支持跨节点事务的数据库，能让一部分原本跨库的写入仍在数据库事务语义里完成。这里的关键词是“一部分”。数据一旦不在同一个节点，协调、网络往返和副本确认都会进账单。它能让你少造轮子，不会让物理成本归零。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;高可用的动作更靠近数据库。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;单主、主从、故障切换、读写漂移，这些词 MySQL 团队都不陌生。差别在于，有些产品会把副本、故障检测、切换和数据重建做得更内聚。应用不必为每一个节点故障临时拼一套流程。&lt;/p&gt;
&lt;p&gt;但“更内聚”不等于“你不用管”。副本放在哪个故障域、切换时能丢多少数据、连接池多久感知、下游能不能承受短暂失败，仍然是你的系统问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;扩容不再总是一次业务改造。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;分库分表最贵的部分，往往不是第一次拆，而是第二次、第三次扩容。库数量从 16 变成 32 时，路由规则、历史数据、离线任务、报表口径都可能要动。&lt;/p&gt;
&lt;p&gt;如果数据库能把数据分布与重平衡接住，扩容会更像一次受控的基础设施操作，而不是让业务团队改一轮代码。注意，是“更像”，不是“点一下按钮就结束”。热点、容量、水位和迁移窗口还是要盯。&lt;/p&gt;
&lt;p&gt;这些能力可以压成一句：&lt;strong&gt;它想把你已经塞进应用层的数据分布问题，往数据库里收回去。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="3-我为什么要用它"&gt;3. 我为什么要用它&lt;/h2&gt;
&lt;p&gt;不是因为“分布式听起来更高级”，更不是因为某个采购词。&lt;/p&gt;
&lt;p&gt;你该考虑它，通常是因为 MySQL 的使用方式已经开始反过来塑造业务代码，而且这种塑造越来越贵。&lt;/p&gt;</description></item><item><title>我追着 Redis 超时查了一圈，最后是 1200 万 field 的 Hash 卡住了实例</title><link>https://blog.cpdd.fyi/posts/redis-bigkey-timeout-postmortem/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0000</pubDate><guid>https://blog.cpdd.fyi/posts/redis-bigkey-timeout-postmortem/</guid><description>&lt;h1 id="我追着-redis-超时查了一圈最后是-1200-万-field-的-hash-卡住了实例"&gt;我追着 Redis 超时查了一圈，最后是 1200 万 field 的 Hash 卡住了实例&lt;/h1&gt;
&lt;p&gt;接口开始一批批变慢，Redis 超时告警也跟着冒出来。&lt;/p&gt;
&lt;p&gt;这种时候，人很容易把锅先扣给连接池：连接是不是借不出来了，应用是不是堆了一堆等待 Redis 的 goroutine。接着再去翻慢查询，怀疑是不是哪条业务请求突然把数据库拖住了。&lt;/p&gt;
&lt;p&gt;这两步没有错。但它们都在应用侧找“谁等得太久”。这次真正该问的问题是：Redis 在那段时间里，究竟被哪一条命令占住了？&lt;/p&gt;
&lt;p&gt;最后钉住问题的，是一个 Hash。它有 12,102,218 个 field。&lt;/p&gt;
&lt;p&gt;不是连接池先坏了。是这个 Hash 先把 Redis 实例堵住，连接池和慢接口只是后面跟着出现的受害者。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Redis 的 Key 设计，不能只问“能不能放进去”；还要问“任何一次读、删、迁移它时，实例要被占住多久”。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/redis-bigkey-timeout-postmortem/cover.png" alt="告警扩散路径示意图"&gt;&lt;/p&gt;
&lt;h2 id="先别急着扩容先看谁把实例占住了"&gt;先别急着扩容，先看谁把实例占住了&lt;/h2&gt;
&lt;p&gt;排查一开始还是按常规路径走：连接池、调用链、慢查询。&lt;/p&gt;
&lt;p&gt;连接池图能告诉你“有很多请求正在等”；慢查询能告诉你“业务链路某一段变长了”。但两张图都不等于根因。尤其当超时集中指向同一组 Redis 实例时，继续在应用侧猜，会把排查带偏。&lt;/p&gt;
&lt;p&gt;应该把视线收回 Redis 自己的延迟。&lt;/p&gt;
&lt;p&gt;公开案例里，业务侧先观察到访问 Redis 超时，&lt;code&gt;HKEYS&lt;/code&gt; 平均响应约 200 ms，最大超过 500 ms。实例监控则显示，平时响应约 0.1 ms，异常时出现约 70 ms 的突刺，最高约 100 ms。[1]&lt;/p&gt;
&lt;p&gt;这两组数字放在一起，事情就清楚了：不是所有请求都慢，而是某些时刻有命令占住了实例。后面的命令排队，客户端看到的时间就被拉长。&lt;/p&gt;
&lt;p&gt;这也是 Redis 超时最容易被误判的地方。你看到的是调用方等了 500 ms；真正消耗实例的，可能只是一段 70 ms 到 100 ms 的执行窗口。对一个本来以亚毫秒响应为常态的实例来说，这已经够把后面一串请求堵住。&lt;/p&gt;
&lt;p&gt;别把“平均延迟还可以”当成没事。对在线服务更危险的，往往是那根偶尔冒出来、却把队列压长的尖刺。&lt;/p&gt;
&lt;h2 id="慢的不是-redis是你让它一次做得太多"&gt;慢的不是 Redis，是你让它一次做得太多&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;HKEYS&lt;/code&gt; 的语义很直接：取出一个 Hash 的全部 field。问题不在命令名字，而在“全部”两个字。&lt;/p&gt;</description></item><item><title>你写了个日志系统选了 InnoDB，然后发现写入越来越慢</title><link>https://blog.cpdd.fyi/posts/lsm-vs-btree-storage-engine-decision/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0000</pubDate><guid>https://blog.cpdd.fyi/posts/lsm-vs-btree-storage-engine-decision/</guid><description>&lt;p&gt;你做了一个日志系统。&lt;/p&gt;
&lt;p&gt;数据量每天 500GB，全是 APP 行为埋点。你选了 MySQL InnoDB，因为团队最熟。跑了一个月，写入越来越慢，到了夜里还要锁表清理。DBA 说：要不试试换 RocksDB？&lt;/p&gt;
&lt;p&gt;你换了一版。写入快了很多，但查询偶尔会卡一下，有时候 p99 飙到 200ms+。老板问为什么，你说「Compaction 抖了一下」。&lt;/p&gt;
&lt;p&gt;然后你开始琢磨：InnoDB 和 RocksDB，底层不都是「树」吗？怎么差异这么大？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;B+Tree 和 LSM-Tree 不是「哪个更快」的问题，是「你把写放大的代价付在什么时候」的问题。&lt;/strong&gt; 前者付在每次写入，后者付在后台合并。你的业务承受哪种代价，就选哪种设计。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="先解决一个问题btree-在写入时到底发生了什么"&gt;先解决一个问题：B+Tree 在写入时到底发生了什么&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://blog.cpdd.fyi/images/lsm-vs-btree-storage-engine-decision/inline-01-btree-split.png" alt="B+Tree 页分裂示意图"&gt;&lt;/p&gt;
&lt;p&gt;大多数后端工程师都用过 MySQL InnoDB，知道数据存在索引里，索引是 B+Tree。但到底怎么「写进去」的，大部分人没细想过。&lt;/p&gt;
&lt;p&gt;B+Tree 的结构简化版是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有数据行（value）只存在叶子节点&lt;/li&gt;
&lt;li&gt;内部节点只存 key，不存数据，作用是导航&lt;/li&gt;
&lt;li&gt;叶子节点之间用双向链表连接，方便范围扫描&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当你 INSERT 一行数据，InnoDB 做的事是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 root 开始，顺着内部节点往下走到对应叶子页&lt;/li&gt;
&lt;li&gt;把这一页从磁盘读到 Buffer Pool（如果不在内存里）&lt;/li&gt;
&lt;li&gt;在页内的有序位置插入&lt;/li&gt;
&lt;li&gt;如果页满了，分裂成两页，重新分布键值&lt;/li&gt;
&lt;li&gt;如果分裂传到上层，级联更新内部节点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;问题出在第 4 步。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你用自增 ID 做 PK 时，新插入的数据永远往最右侧叶子节点写，只有在最右侧满了才触发分裂，而且只影响右边界。&lt;/p&gt;
&lt;p&gt;但是你用了 UUID 做 PK——新人常犯的经典错误——或者业务 key 本身就是分散的，那么每次插入的数据会随机落到任意一个叶子节点。&lt;strong&gt;每次插入都可能触发一次页分裂。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;页分裂的代价是什么？InnoDB 默认页大小是 16KB。你写入一行 128 字节的数据，如果触发了页分裂，InnoDB 要读旧页、写两页，刷盘的数据可能是 32KB+。你写的是 128 字节，硬盘却干了 32KB 的活。&lt;strong&gt;写放大。&lt;/strong&gt;&lt;/p&gt;</description></item></channel></rss>