欢迎光临
我们一直在努力

集中式与分布式怎么选:一次选型复盘,附金仓数据库建表与增删改查实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

一、那个会

先说个会。

去年冬天,去外省看一个项目。会议室坐了二十几个人,投影仪偏色偏得厉害,白底投出来发绿。方案翻到第三页,主讲人放了张图——中间一个圆,四周六个节点。他说,我们要上分布式。

我手里正好有这套系统的监控材料。一天订单三十万笔,数据两个多 T,日增六个 G,峰值 TPS 九百。就这个量,一台好点的机器能跑得打哈欠。
在这里插入图片描述

三个月后,POC 的结论是退回集中式。

跨节点事务的 P99 从三毫秒变成四十多毫秒,压测后半程两阶段提交的锁等待一直冒头,运维还得多管三类组件、十几个进程。评审组最后一轮开会,有人提了一句:省下的钱够买两台更好的机器。就这么定了。

我不反对分布式。我反对的是不看题干就答题。

集中式与分布式这道题,题面不是“哪个先进”,是“你那套业务到底卡在哪”。

二、三个词,得掰开

概念这块我不打算念课本,就说我自己的划法。

集中式,数据一份,或者一份主本加一份备本,读写都落在同一套机器上。想更强就换机器。金仓数据库是这条路——单机、主备、读写分离集群、共享存储集群,形式不同,底子都是“数据放一块、把单机做厚”。

分布式,数据切开,按规则散到多个节点,一个节点管一段,靠多副本和一致性协议保命。想更强就加节点。这里头分原生和中间件两类,差别很大,后面细说。

伪分布式,这个词是韩锋老师先提的,我觉得挺准。买了分布式库,业务按单机跑,一个分片都不用;或者拿租户能力把几十套单机库塞进一个集群,本质是池化。还有一种更常见的,中间件分库分表,分片键随手选的,一条普通查询扫遍所有分片。

要判断真假,问两句就够了:数据真打散了吗,一次业务事务真跨节点了吗。都“是”才算,有一个“否”就是伪的。伪分布式最亏,成本按分布式算,收益按集中式拿。

云原生另算一类。存算分离、按需弹缩,解决的是“弹”,骨架多半还是分布式。前提是你真在云上。自建机房谈弹性,扩的还是那几台物理机。

顺带说一句,这几个词为什么老被混用。因为采购和评审的语境里,“分布式”三个字自带加分项,听起来能扩展、能容灾、技术先进。我参加过一次方案评审,评分表里“是否采用分布式架构”单列一项,占 5 分。这种表一出来,答案其实已经定了,后面讨论的都是怎么把它论证圆。

三、集中式:便宜在哪,贵在哪

总有人说集中式“老”。

这话我不太认同。现在一台双路机架,一百多核、一两个 T 内存、全 NVMe,跑几十万 TPM 很正常。所以“单机扛不住”这句话得追问:是数据装不下,是写入顶到 IO,还是连接把内存吃穿了?三个问题三种解法。一股脑推给架构,是把问题挪走,不是解决。

真正值钱的是 SQL 的自由。多表 JOIN、窗口函数、递归查询、存储过程,全在一个节点里跑完。没有跨节点下推,没有结果归并,也不用先琢磨“这段会不会打到多个分片”。写惯了集中式的人第一次做分片设计会很不适应,脑子里得同时装业务和路由。

事务也省心。本地事务,提交就生效。两阶段提交、全局时钟、最终一致之后的对账,这些活儿统统不存在。

迁移成本低也是实打实的。存量系统从商业库搬过来,改动多在数据类型和少数函数上,业务代码基本不动。金仓数据库对 Oracle 的兼容做得比较细,存量替换里出现频率高,原因就在这儿。

运维半径小。一套主备、一个备份策略、一套监控,一个熟手能兜住。

还有个隐性好处,出了性能问题好定位。一条慢 SQL,看执行计划、看等待事件、看 IO,十几分钟能摸到根上。分布式上同样的现象,你得先排除路由、再排除跨节点、最后才轮到 SQL 本身。排查时间翻几倍是常态。

代价也不含糊。

扩容只能换机器。机器的天花板就是系统的天花板,撞上去那天没有缓冲,只能停机割接。现在有些方案用共享存储把几台机器连起来,能缓解一点,但本质还是纵向扩,别指望它解决线性扩展。

故障域集中,单点问题得靠高可用方案补。方案做得糙,切换那天见真章。

最要命的是重负载分析跟交易抢资源。我见过一个系统,交易 P99 一直很漂亮,上了个报表功能,一条没走索引的聚合把 IO 打满,交易当场雪崩。报表挪到备库,立马好了。这不是集中式的错,是部署没想清楚。

高可用这块多说一句。别只盯“切过去要几分钟”这一个数。主备是异步或半同步,切换时没进日志的数据可能丢;共享存储集群是两份实例共用一份数据,切得快、不丢,但对存储链路和网络要求高;读写分离集群夹在中间,能扩读,主库挂了也能接管。成本差不少,拿业务能忍的 RTO、RPO 倒推,比照着产品清单挑靠谱。

四、分布式拿走了什么

分布式解决的是“大”。数据装不下、写入顶不住、要多地多活,这些集中式确实给不了。

代价也真实。我不打分了,按疼的程度往下说。

最疼的是事务。跨节点要么两阶段提交,要么补偿。两阶段提交的锁持有时间变长,协调者一慢,全链路跟着慢;补偿把一致性责任推给业务代码,每一步都得备好逆向操作,还得处理“补偿也失败”。我见过一个下单动作写四张分片表、靠消息队列最终一致的,每天千分之三订单对不上,专门配了两个人对账。

SQL 也没那么自由了。跨片 JOIN 靠广播或者下推加归并,跨片聚合要两阶段汇总,ORDER BY 带 LIMIT 得每个分片各取一批再合并。写得出来,但执行计划跟你想的经常不是一回事。以前一个 DBA 半小时收拾完的慢查询,现在得先看路由、再看分片计划、最后看归并结果。

分片键是埋伏。选错了要重分布,重分布要么停机要么双写,双写就得处理冲突。我参与过一次,一亿两千万行的表,低峰跑了十一个小时,中途回滚重来。从那以后我们定了规矩:分片键建模评审单独过一遍,谁定谁签字。

时延这事容易被忽略。多一跳网络、多一层协调,P99 的抖动比均值难看多了。金融交易、电力调度这类场景,几十毫秒的抖动就是事故。

还有运维。节点数乘组件数,就是你要监控、备份、升级、扩缩容的对象。以前一个人管一套库,现在管一个集群,还得懂一致性协议,懂扩容期间怎么挪流量。人不够,什么架构都白搭。

有个事故我一直记着。某系统做跨分片聚合,一条不带分片键的统计查询,代理层把请求广播到十六个分片,每个分片各扫一遍明细,结果集在中间件内存里归并。平时没事,赶上月末报表,十六路结果一起回来,堆内存直接打满,OOM 重启,交易跟着断了六分钟。后来把这条查询挪到独立分析库,才算消停。

三个误区,评审会上我几乎每次都能碰上。

一个是把读写分离当分布式。读写分离扩的是读,写还是一个节点扛,数据还是一份。它解决“读不过来”,不解决“写不进去”。

一个是把中间件当原生分布式。中间件能路由能归并,但跨库事务、全局一致性、在线扩缩容这些硬骨头,要么不支持,要么得应用配合。用之前先问一句:跨库事务谁兜?

还有一个,把“上了集群”当成“有了高可用”。三个节点挤在一间机房、一个交换机下、共用一台存储。这不叫高可用,叫把鸡蛋放进一个更贵的篮子。

五、动手:在金仓数据库上把订单域跑一遍

下面全是代码。语法在测试机上跑过,表名改改能用。环境是金仓数据库(KingbaseES),客户端用自带的 ksql,默认端口 54321。

5.1 连上去,建库建模式

— 命令行连接(生产上别把密码写在命令里,用 .kql 或环境变量)
ksql –U app_user –W –d orderdb –h 10.20.30.11 –p 54321

— 建库与建模式
CREATE DATABASE orderdb;
CREATE SCHEMA ods;
SET search_path TO ods, public;

5.2 建表:分区、约束、索引、注释一次到位

订单主表按月做范围分区。坑先说在前面:分区表的主键和唯一约束必须带上分区键,不然建不出来。所以主键是 (order_id, created_at),唯一键是 (order_no, created_at)。这也意味着单号唯一性不能只靠数据库,得配上应用层校验。

CREATE TABLE ods.ods_order (
order_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
order_no VARCHAR(32) NOT NULL,
tenant_id VARCHAR(16) NOT NULL,
customer_id BIGINT NOT NULL,
order_status SMALLINT DEFAULT 0 NOT NULL,
total_amount NUMERIC(18,2) DEFAULT 0 NOT NULL,
discount_amount NUMERIC(18,2) DEFAULT 0 NOT NULL,
pay_amount NUMERIC(18,2) DEFAULT 0 NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
remark VARCHAR(500),
CONSTRAINT pk_ods_order PRIMARY KEY (order_id, created_at),
CONSTRAINT uk_ods_order_no UNIQUE (order_no, created_at),
CONSTRAINT ck_ods_order_amt CHECK (pay_amount >= 0),
CONSTRAINT ck_ods_order_stat CHECK (order_status BETWEEN 0 AND 9)
)
PARTITION BY RANGE (created_at)
(
PARTITION p202601 VALUES LESS THAN (DATE '2026-02-01'),
PARTITION p202602 VALUES LESS THAN (DATE '2026-03-01'),
PARTITION p202603 VALUES LESS THAN (DATE '2026-04-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);

明细表我没建外键,故意的。分区表上的外键约束限制不少,跨分区引用会让分区裁剪失效,删父表数据时还可能锁住整个分区。我们的做法是应用层保证,外加每天跑一次孤儿数据核对,比硬扛外键灵活。

CREATE TABLE ods.ods_order_item (
item_id BIGINT GENERATED BY DEFAULT AS IDENTITY,
order_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
qty INTEGER NOT NULL,
unit_price NUMERIC(18,2) NOT NULL,
amount NUMERIC(18,2) NOT NULL,
created_at TIMESTAMP NOT NULL,
CONSTRAINT pk_ods_order_item PRIMARY KEY (item_id),
CONSTRAINT ck_ods_item_qty CHECK (qty > 0)
);

CREATE INDEX idx_item_order ON ods.ods_order_item (order_id);

CREATE INDEX idx_order_cust ON ods.ods_order (customer_id, created_at);
CREATE INDEX idx_order_stat ON ods.ods_order (order_status, created_at);

COMMENT ON TABLE ods.ods_order IS '订单主表,按创建时间月度范围分区';
COMMENT ON COLUMN ods.ods_order.pay_amount IS '应付金额,已扣减优惠,单位:元';
COMMENT ON TABLE ods.ods_order_item IS '订单明细表';

索引不是越多越好。这两条照着实际查询写的:一条服务“某客户最近订单”,一条服务“按状态捞待处理单”。上线三个月我们删了两个建了没用上的索引,写入 TPS 反而涨了 8%。

5.3 增

单条插入,自增列不写:

INSERT INTO ods.ods_order
(order_no, tenant_id, customer_id, order_status,
total_amount, discount_amount, pay_amount, remark)
VALUES ('SO2026110600012', 'T007', 88421003, 0,
1280.00, 80.00, 1200.00, '首单立减');

批量插入一条语句搞定,比循环单条快一个数量级:

INSERT INTO ods.ods_order
(order_no, tenant_id, customer_id, order_status,
total_amount, discount_amount, pay_amount)
VALUES ('SO2026110600013', 'T007', 88421004, 0, 399.00, 0.00, 399.00),
('SO2026110600014', 'T007', 88421005, 0, 268.50, 18.50, 250.00),
('SO2026110600015', 'T008', 88421006, 0, 99.00, 0.00, 99.00);

历史数据搬迁用 INSERT … SELECT,走同一套分区路由:

INSERT INTO ods.ods_order_his
(order_id, order_no, tenant_id, pay_amount, created_at)
SELECT order_id, order_no, tenant_id, pay_amount, created_at
FROM ods.ods_order
WHERE created_at >= DATE '2026-01-01'
AND created_at < DATE '2026-02-01'
AND order_status = 3;

幂等写入用 MERGE。这是集中式最舒服的地方之一,一条语句完成“有则更新、无则插入”,不用先查再判,也没有跨节点协调成本:

MERGE INTO ods.ods_order t
USING (SELECT 'SO2026110600012' AS order_no, 88421003 AS customer_id) s
ON (t.order_no = s.order_no)
WHEN MATCHED THEN
UPDATE SET t.pay_amount = t.pay_amount – 100.00,
t.discount_amount = t.discount_amount + 100.00,
t.updated_at = CURRENT_TIMESTAMP
WHEN NOT MATCHED THEN
INSERT (order_no, tenant_id, customer_id, order_status, pay_amount, created_at)
VALUES (s.order_no, 'T007', s.customer_id, 0, 0.00, CURRENT_TIMESTAMP);

5.4 查

日常最重的一条:某客户近三个月金额最高的三笔订单,顺便校对付费金额和明细合计对不对。CTE 加窗口函数,一个节点里跑完:

WITH stat AS (
SELECT o.order_id,
o.order_no,
o.customer_id,
o.pay_amount,
SUM(i.amount) AS item_amount,
ROW_NUMBER() OVER (PARTITION BY o.customer_id
ORDER BY o.pay_amount DESC) AS rn
FROM ods.ods_order o
JOIN ods.ods_order_item i ON i.order_id = o.order_id
WHERE o.created_at >= DATE '2026-08-01'
AND o.order_status IN (1, 2)
GROUP BY o.order_id, o.order_no, o.customer_id, o.pay_amount
)
SELECT order_no, customer_id, pay_amount, item_amount,
pay_amount – item_amount AS diff
FROM stat
WHERE rn <= 3
ORDER BY customer_id, pay_amount DESC
FETCH FIRST 100 ROWS ONLY;

这条 SQL 搬到分布式上至少改三处。JOIN 得保证两张表同分布键,不然退化成广播;ROW_NUMBER() 里 PARTITION BY customer_id 要求同一客户落在同一分片,不然排名会错;FETCH FIRST 得每个分片各取 100 行再归并。写是能写,但每一处都得先想清楚路由。

看完 SQL 一定要看执行计划:

EXPLAIN ANALYZE
SELECT count(*), sum(pay_amount)
FROM ods.ods_order
WHERE created_at >= DATE '2026-11-01'
AND created_at < DATE '2026-12-01';

正常情况下只扫 p202603 一个分区。要是看到全分区扫描,八成是条件写坏了——分区裁剪只认分区键上的常量或者可下推的表达式。下面这种写法是经典反例,to_char 包一层,裁剪直接失效,几千万行白扫:

— 反例:不要这么写
WHERE to_char(created_at, 'YYYYMM') = '202611'

— 应该这么写
WHERE created_at >= DATE '2026-11-01' AND created_at < DATE '2026-12-01'

还有个小细节。分区表上的全局索引和本地索引不是一回事,建索引时得想清楚查询是不是总带分区键。总带的,本地索引就够;不带分区键的查询多,才考虑全局索引,但维护代价高,分区滚动的时候尤其明显。

5.5 改

状态机流转,带上时间窗,别开大事务:

UPDATE ods.ods_order
SET order_status = 4,
updated_at = CURRENT_TIMESTAMP
WHERE order_status = 1
AND created_at < CURRENT_TIMESTAMP – INTERVAL '7' DAY;

按外部数据批量修正金额,用 EXISTS 关联,一次成型:

UPDATE ods.ods_order o
SET pay_amount = o.pay_amount – d.refund_amt,
updated_at = CURRENT_TIMESTAMP
WHERE EXISTS (SELECT 1
FROM ods.tmp_refund d
WHERE d.order_id = o.order_id
AND d.batch_no = 'R20261106001');

这种没带分片键的关联更新,集中式里就是一条普通 UPDATE;到了分布式上,tmp_refund 要么做成广播表,要么改成按订单号逐批驱动,写法完全变了。

顺带一个经验,批量更新一定分批,五万行左右提交一次。我见过一条 UPDATE 扫了四百万行、跑在没设超时的会话里,锁等了二十分钟,把上游的定时任务全堵住了。后来加了时间窗和分批提交,同样的活儿七十秒跑完。

5.6 删

删除得分清三种手段,代价差好几个数量级:

— 精确删除,走索引
DELETE FROM ods.ods_order_item
WHERE order_id IN (SELECT order_id
FROM ods.ods_order
WHERE order_status = 9
AND created_at < DATE '2025-01-01');

— 清空临时表,无条件、不可回滚、速度最快
TRUNCATE TABLE ods.tmp_refund;

— 清理整月历史:元数据操作,秒级完成
ALTER TABLE ods.ods_order DROP PARTITION p202601;

这里有个我付过学费的教训。早些年做数据清理,直接 DELETE FROM 一张三千万行的表,跑了四个多小时,回滚段一路涨,中途差点把库撑爆,最后杀掉会话重来。后来所有大批量清理一律改两步:先按分区或者按批次挪走,再 DROP PARTITION,或者分批 DELETE 加 COMMIT。判据很简单,单次 DELETE 过十万行就该换手段。

5.7 事务

金仓数据库默认读已提交。交易类业务够用,报表类需要整个查询看到同一个快照,显式抬到可重复读:

BEGIN;

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

UPDATE ods.ods_order
SET order_status = 2, updated_at = CURRENT_TIMESTAMP
WHERE order_id = 1000000001;

SAVEPOINT sp_item;

UPDATE ods.ods_order_item
SET amount = qty * unit_price
WHERE order_id = 1000000001;

— 明细校验不通过,只回滚这一段,主单的更新保留
ROLLBACK TO SAVEPOINT sp_item;

COMMIT;

保存点这个能力,在分布式里要打问号。跨分片事务能不能局部回滚,看产品实现,很多场景下只能整体回滚再重试。这就是“本地事务”四个字的含金量。

5.8 同一段业务,换个架构要改多少

把上面这些操作平移到分布式,改动量大致是这样:

环节集中式做法分布式要做的事
主键 自增列,库内保证 全局唯一 ID(雪花或号段服务)
建表 直接建,随便加索引 先定分片键,索引要带分片键前缀
关联查询 任意 JOIN 同分布键 JOIN,否则广播或改写成两次查询
聚合排序 一次算完 两阶段聚合,LIMIT 要各分片取数再归并
幂等写入 MERGE 一条语句 按分片键路由后本地 MERGE,跨分片要补偿
事务 BEGIN … COMMIT 分布式事务或最终一致 + 对账
历史清理 DROP PARTITION 按分片逐片清理,注意路由元数据
扩容 换机器 数据重分布,双写或停写窗口

这张表我建议直接投在选型会上。很多时候争的不是技术细节,是没人把“改造成本”四个字具体化。

六、什么时候该上

标准列几条,丑话说前头:同时成立两条以上才值得认真评估,一条都不占就别讨论了。

数据量三年内稳定超过 10TB 还在涨——注意是“稳定超过”,不是“预计会到”。

写入峰值把单机打穿——这个得实测。同样硬件、你的写入模型,能跑到多少 TPS,写满之后多出来的那部分,才是分布式要接的。

并发过万而且以短事务为主——不过先看连接池和中间件能不能扛,别急着换架构。

业务天然可分——按租户、按区域、按用户 ID 天然隔离,分片键不用硬凑。这条比数据量重要。我见过几百 G 却特别适合分布式的,也见过十个 T、全是大表关联、根本切不开的。

多地多活或者单元化的硬要求——集中式给不了。

团队里有人扛得住分布式运维——最容易被忽略,也是失败率最高的原因。

反过来,如果理由是“怕以后不够用”“领导觉得先进”“别人都上了”,那多半不该上。

还有一点,别只看现在的量,看三年的曲线。有的业务数据平稳,三年涨两成;有的半年翻一倍。前者按现状选,后者得按两年后的量选。这个判断比任何架构对比都实在。

有个反例我一直记着。客户系统八百多个 G,上了三节点分布式,运维两年,扩过一次容,加节点那天停了四小时。我问他值不值,他想了半天说,当初买台好机器可能省一半事。这话我没敢在评审会上讲。

七、我现在更常推荐的,其实是组合

落地效果好的方案,基本都不是二选一。

交易主链路集中式,主备加读写分离,读多写少的查询全走备库。历史数据分区表按月滚,过了保留期的 DROP PARTITION 直接带走。报表分析另起一套,独立的分析库,增量靠同步工具送。要多地的,按业务单元垂直切,单元内部还是集中式,单元之间用消息解耦。

每一层干自己擅长的事。集中式管强一致和复杂查询,读写分离管读扩展,分区管生命周期,分析库管重计算。扩容也不用整体推翻,哪层先撞墙先扩哪层。

迁移这块提醒一句,工具能转语法,转不了语义。视图、存储过程、自定义函数、序列、触发器,这几样最容易出问题。惯例是工具跑一遍,人工再过一遍执行计划,专挑原本走索引、迁完变全表的语句。

集中式的高可用别只盯主备。共享存储集群能把切换压到几十秒,读写分离集群能把读能力线性拉起来,这两条在框架内就够用,不用动数据分布。

够不够用得压出来,不能靠感觉。

我至少盯五个数:峰值 TPS 下的 P99;CPU 和 IO 的稳态水位,我个人压在 70% 以内;活跃连接数和内存;锁等待与死锁次数;检查点或者日志切换频率。前三个管日常峰值,后两个管突发。

模型也得真。读写比例、一个事务几条 SQL、热点怎么分布,这三样跟生产差太远,曲线再漂亮也没用。我见过均匀分布压出来的满分答卷,上线碰着真实热点,一天就顶不住。

备份也提一句。集中式的备份策略简单,全量加增量,恢复演练一个月做一次就够。分布式做一致性备份麻烦得多,各分片的备份时间点得对齐,不然恢复出来是一堆对不上的数据。这块经常被漏在方案之外。

八、收尾

三件事,说完就完。

选之前把数拿出来。数据量、写入峰值、并发模型、增长曲线。这四个数没有,评审会就是聊天。

复杂度这笔账算清楚。集中式把复杂度留在库里,你专心写 SQL;分布式把复杂度摊到架构、开发、运维上,换扩展性。你愿意在哪层付钱,这才是真问题。

代码不会骗人。同一个订单域的增删改查,集中式二十行 SQL,分布式一套分片规则加一套一致性方案。自己写一遍,就知道团队接不接得住。
在这里插入图片描述

金仓数据库是集中式的路线。不是短板,是定位。该问的不是“它是不是分布式”,是“我这套业务,需不需要为扩展性提前付那些代价”。

赞(0)
未经允许不得转载:171主机测评 » 集中式与分布式怎么选:一次选型复盘,附金仓数据库建表与增删改查实践
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址