微服务数据库的核心矛盾
单体应用可以通过数据库事务轻松保证数据一致性。而在微服务架构中,每个服务拥有独立的数据库,传统的ACID事务跨越服务边界时变得不可行。这是微服务数据库设计最核心的挑战:如何在保证服务独立部署的同时,维护跨服务数据的最终一致性。
模式1:每服务独立数据库
每个微服务拥有自己的数据库,其他服务只能通过API访问数据,绝不直接查询其他服务的数据库。
-- 订单服务数据库(orders_db)
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL, -- 不是外键!只是存储用户ID
status VARCHAR(20) NOT NULL,
total_amount DECIMAL(12,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 用户服务数据库(users_db)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL
);
-- 订单服务需要用户姓名时,调用用户服务API
-- 而非执行跨库JOIN:SELECT u.name FROM users_db.users u ...
优点:服务可以独立扩展和部署,可以为不同服务选择最适合的数据库类型。 挑战:跨服务查询需要API调用,报表类查询复杂度上升。
模式2:CQRS(命令查询职责分离)
CQRS将读操作(Query)和写操作(Command)分离到不同的数据模型,解决读写需求差异大的问题:
-- 写模型(规范化,保证写入一致性)
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL
);
CREATE TABLE order_items (
id UUID PRIMARY KEY,
order_id UUID REFERENCES orders(id),
product_id UUID NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL
);
-- 读模型(反规范化,为查询优化的物化视图)
CREATE TABLE order_summaries (
order_id UUID PRIMARY KEY,
user_id UUID NOT NULL,
user_name VARCHAR(200) NOT NULL, -- 冗余存储
user_email VARCHAR(255) NOT NULL, -- 冗余存储
status VARCHAR(20) NOT NULL,
item_count INT NOT NULL,
total_amount DECIMAL(12,2) NOT NULL,
created_at TIMESTAMP NOT NULL,
last_updated TIMESTAMP NOT NULL
);
-- 通过事件异步更新读模型
CREATE OR REPLACE FUNCTION refresh_order_summary(p_order_id UUID)
RETURNS VOID AS $$
BEGIN
INSERT INTO order_summaries
SELECT
o.id, o.user_id, u.name, u.email, o.status,
COUNT(oi.id), SUM(oi.quantity * oi.unit_price), o.created_at, NOW()
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN users u ON o.user_id = u.id -- 此处读模型可合并数据
WHERE o.id = p_order_id
GROUP BY o.id, u.name, u.email
ON CONFLICT (order_id) DO UPDATE SET
status = EXCLUDED.status,
total_amount = EXCLUDED.total_amount,
last_updated = EXCLUDED.last_updated;
END;
$$ LANGUAGE plpgsql;
模式3:Saga模式处理分布式事务
当一个业务操作需要跨多个服务保证一致性时(如下单需要扣减库存、创建订单、扣减积分),Saga模式将其拆分为一系列本地事务,每步失败时执行补偿操作:
-- Saga协调器状态表
CREATE TABLE saga_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
saga_type VARCHAR(100) NOT NULL,
current_step VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL, -- running/completed/compensating/failed
payload JSONB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE saga_events (
id BIGSERIAL PRIMARY KEY,
saga_id UUID REFERENCES saga_instances(id),
step VARCHAR(100) NOT NULL,
event_type VARCHAR(50) NOT NULL, -- started/completed/failed/compensated
event_data JSONB,
occurred_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 示例:创建订单Saga步骤
-- Step1: 锁定库存 → 失败: 释放库存
-- Step2: 创建订单 → 失败: 取消订单 + 释放库存
-- Step3: 扣减积分 → 失败: 恢复积分 + 取消订单 + 释放库存
-- Step4: 发送通知 → 失败: 仅记录日志(通知可重试,不需要回滚前序步骤)
模式4:发件箱模式(Outbox Pattern)
确保本地数据库操作与消息发布的原子性,防止"数据已写入但消息未发出"的不一致:
-- 在业务表所在同一数据库创建发件箱表
CREATE TABLE outbox_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
aggregate_id UUID NOT NULL,
event_type VARCHAR(100) NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
published_at TIMESTAMP -- NULL=待发布
);
-- 在同一事务中写入业务数据和发件箱事件
BEGIN;
UPDATE orders SET status = 'confirmed' WHERE id = $1;
INSERT INTO outbox_events (aggregate_id, event_type, payload)
VALUES ($1, 'OrderConfirmed', '{"orderId": "...", "userId": "..."}');
COMMIT;
-- 独立进程轮询发件箱并发布到消息队列
-- SELECT * FROM outbox_events WHERE published_at IS NULL ORDER BY created_at LIMIT 100;
使用Erdmates设计微服务数据库边界
在微服务架构中,正确划定服务边界(即有界上下文)是成功的关键。Erdmates帮助团队可视化每个服务的数据模型,清晰标注服务间的数据所有权,以及事件流向关系。在架构评审时,通过ERD快速发现服务间的数据耦合问题,提前调整边界划分。
微服务数据库设计没有"正确答案",只有"当前场景下的最优权衡"。从最简单的每服务独立数据库开始,在实际遇到性能瓶颈或一致性问题时,再逐步引入CQRS、Saga等复杂模式。