当单机 PostgreSQL 撑不住时:Citus 分布式实战

你的数据库快爆了 凌晨 3 点,报警短信把你吵醒:数据库 CPU 100%,响应超时。 你揉着眼睛打开监控,订单表已经 2 亿行,每次查询都在全表扫描。加索引?早加过了。分表?业务代码要大改。 DBA 说:“该分库分表了。” 你打开《一致性实战(四)》,看到分片路由、跨分片查询聚合、Saga 事务补偿… 头皮发麻。 有没有更简单的方案? 有。让我介绍 Citus——PostgreSQL 的分布式扩展。它让分片对应用几乎透明,大部分 SQL 不用改,ORM 和 sqlx 可以直接用。 Citus 是什么 想象你有一个图书馆,书越来越多,一个管理员忙不过来了。 传统方案(手动分片): 你把书分到 4 个房间,每个房间一个管理员。但你得记住每本书在哪个房间,跨房间借书要自己协调。 Citus 方案: 还是 4 个房间 4 个管理员,但前台有一个总调度。你只跟总调度说话,它自动知道书在哪,跨房间的事它帮你协调。 这就是 Citus 的架构: ┌─────────────────────────────────────────────────────────┐ │ 应用程序 │ │ (sqlx, Diesel, 任何 PG 客户端) │ └─────────────────────┬───────────────────────────────────┘ │ 普通 PostgreSQL 协议 ▼ ┌─────────────────────────────────────────────────────────┐ │ Coordinator (协调器) │ │ 看起来就是一个普通 PostgreSQL │ │ • 解析 SQL │ │ • 路由到正确的分片 │ │ • 聚合跨分片结果 │ └───────┬─────────────┬─────────────┬─────────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Worker1 │ │ Worker2 │ │ Worker3 │ │ (分片0-3)│ │ (分片4-7)│ │(分片8-11)│ └─────────┘ └─────────┘ └─────────┘ 关键点:应用只连接 Coordinator,它看起来、用起来就是一个普通 PostgreSQL。 五分钟上手 1. 启动 Citus 集群 用 Docker Compose 快速启动一个本地集群: # docker-compose.yml services: coordinator: image: citusdata/citus:12.1 container_name: citus_coordinator ports: - "5432:5432" environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: mydb healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 networks: - citus-network worker1: image: citusdata/citus:12.1 container_name: citus_worker1 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 networks: - citus-network worker2: image: citusdata/citus:12.1 container_name: citus_worker2 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 networks: - citus-network networks: citus-network: driver: bridge 启动集群并等待健康检查通过: ...

December 11, 2025 · 9 min · 1819 words · Nanlong

从单机到分布式:一致性实战(四)数据分片:跨分片事务一致性

从这里开始 经过缓存优化后,小明的二手书平台性能有了很大提升。但新的问题出现了:数据量太大,单个数据库快撑不住了。 订单表已经有上亿条数据,每次查询都很慢,添加索引也无济于事。磁盘空间也快满了,单机存储已经到达瓶颈。 DBA 看了监控后说:“是时候分库分表了。” 小明决定按 user_id 将订单数据分到 4 个数据库分片中: user_id % 4 = 0 → shard_0 user_id % 4 = 1 → shard_1 user_id % 4 = 2 → shard_2 user_id % 4 = 3 → shard_3 分片后,单个用户的订单查询飞快了。但很快遇到了新问题: 问题一:跨分片查询 “给我查最近 7 天所有用户的订单总额。“运营说。 这意味着要查询所有 4 个分片,然后合并结果。 问题二:跨分片事务 “用户 A(在 shard_0)想把一本书转让给用户 B(在 shard_1)。” 这涉及两个分片的数据修改,如何保证原子性? 问题三:分片键变更 “用户要改手机号,但我们是按手机号分片的…” 分片键变更意味着数据要迁移到另一个分片。 这就是数据分片带来的一致性挑战。 分片基础 分片路由 首先实现一个分片路由器: use sqlx::PgPool; use std::collections::HashMap; use std::sync::Arc; /// 分片路由器 pub struct ShardRouter { shards: Vec<Arc<PgPool>>, shard_count: usize, } impl ShardRouter { pub fn new(shard_pools: Vec<PgPool>) -> Self { let shard_count = shard_pools.len(); Self { shards: shard_pools.into_iter().map(Arc::new).collect(), shard_count, } } /// 根据分片键获取分片 pub fn get_shard(&self, shard_key: i64) -> &PgPool { let shard_index = (shard_key as usize) % self.shard_count; &self.shards[shard_index] } /// 获取所有分片(用于跨分片查询) pub fn all_shards(&self) -> &[Arc<PgPool>] { &self.shards } /// 根据分片键计算分片索引 pub fn shard_index(&self, shard_key: i64) -> usize { (shard_key as usize) % self.shard_count } } 基本的分片读写 pub struct ShardedOrderRepository { router: ShardRouter, } impl ShardedOrderRepository { /// 写入订单(路由到对应分片) pub async fn create_order(&self, order: &Order) -> Result<(), Error> { let pool = self.router.get_shard(order.user_id); sqlx::query!( r#" INSERT INTO orders (id, user_id, book_id, amount, status, created_at) VALUES ($1, $2, $3, $4, $5, NOW()) "#, order.id, order.user_id, order.book_id, order.amount, order.status, ) .execute(pool) .await?; Ok(()) } /// 查询用户订单(单分片查询) pub async fn get_user_orders(&self, user_id: i64) -> Result<Vec<Order>, Error> { let pool = self.router.get_shard(user_id); let orders = sqlx::query_as!(Order, "SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC", user_id ) .fetch_all(pool) .await?; Ok(orders) } } 问题一:跨分片查询 当查询条件不包含分片键时,需要查询所有分片。 ...

December 11, 2025 · 13 min · 2588 words · Nanlong

数据库演进史(四):分布式数据库——当单机不够用

前情回顾 前两篇我们看到了数据库世界的两极: 关系型:ACID 事务、SQL 标准、但单机天花板 NoSQL:无限扩展、灵活 Schema、但牺牲一致性 这像是一个权衡三角(注意:这不是 CAP 定理,CAP 讨论的是分布式系统中一致性/可用性/分区容错的取舍): 一致性(C) /\ / \ / \ /______\ 可扩展性(S) 可用性(A) 传统关系型:优先 C 和 A,牺牲 S(单机架构) 传统 NoSQL:优先 S 和 A,牺牲 C(最终一致性) 2012 年,Google 发表了 Spanner 论文,证明了一个惊人的事实: 三者可以兼顾——代价是巨大的工程复杂度。 ...

December 9, 2025 · 6 min · 1266 words · Nanlong