把业务逻辑写进数据库中?这不是上个世纪的"存储过程架构"吗?
在微服务、Serverless、大数据、分布式集群盛行的今天,这个老派思路,为什么还值得认真谈一次?
📚 背景知识:什么是业务逻辑?业务逻辑在哪运行?
在开发一个系统时,我们大致可以把代码分为三类:
- 表示层逻辑(UI逻辑) :界面展示与交互行为。
- 业务逻辑(Domain Logic) :系统核心规则,例如"库存不足不能下单"。
- 数据访问逻辑(Data Access) :对数据库的读写操作。
常规架构中,业务逻辑通常由应用层处理:
前端 → 应用服务(业务逻辑) → 数据库(存数据)
而把业务逻辑写进数据库
是指:
- 把部分或全部业务规则直接写入数据库中,例如使用存储过程(Stored Procedure) 、触发器(Trigger) 、自定义函数(Function) 和 约束(Constraint) 等特性。
- 应用层只负责参数传递、UI 展示。
🏛 历史背景:IBM IMS 系统与"数据库中的业务逻辑"
把业务逻辑写进数据库
的做法并非新生事物,其历史可追溯到 20 世纪 60 年代。1966 年,IBM 推出了 IMS(Information Management System) ,这是一套为美国阿波罗登月计划设计的数据库管理系统,其架构强调:
- 数据和事务逻辑紧密结合:应用程序通过调用数据库事务来实现完整的业务操作
- 逻辑写在数据库端:通过"程序化数据库"方式执行复杂流程
- 高性能与一致性:IMS 以事务安全和高吞吐著称,在银行、电信等领域广泛使用
IMS 的成功,使"胖数据库"的理念深入人心。后来,随着关系型数据库和 PL/SQL 的出现,存储过程成为实现复杂业务逻辑的标准工具。
IMS 系统发明的 Trigger 和 Transaction 等一系列现代数据库的默认机制,被后人总结为
ACID
特性,直到今天,它也还是最值得信赖的数据保障机制。
🛠 架构演进:胖数据库的回归
在 90 年代至 2000 年代初期,很多系统采用"胖数据库、瘦应用"的架构:
- 使用 SQL 和 PL/pgSQL 写出大量逻辑
- 应用层作为 UI 或接入层存在
后来,后端语言和框架发展迅速,业务逻辑转移到了服务层,数据库变成了"纯存储层"。
但在某些系统中,数据库仍然承担着关键业务逻辑职责,尤其在:
- 银行、证券、保险等高一致性系统
- ERP、库存、订单系统
- 多系统共享数据库的场景
✨ 为什么考虑把业务逻辑写进 PostgreSQL?
✅ 更强的事务一致性
- 数据库内部逻辑天然支持事务(BEGIN/COMMIT/ROLLBACK)
- 无需依赖应用框架或中间件控制一致性
✅ 性能更高
- 减少前后端之间的往返调用
- 可以在一次事务中完成校验+更新+插入
✅ 可复用、统一管理
- 多个系统调用数据库中的存储过程,不用重复实现逻辑
✅ 数据完整性保障
- 使用触发器/约束可强制规则执行,防止数据脏写
📊 PostgreSQL 实战示例:下订单流程
🔹 场景:用户下订单
需求如下:
- 检查库存是否足够
- 库存足够则创建订单并扣减库存
- 不足则报错拒绝
- 自动记录日志
- 保证所有步骤原子性
我们将使用 PostgreSQL 的以下功能实现:
FUNCTION
+DO
块实现事务控制TRIGGER
实现日志记录CHECK
限制库存不能为负FOREIGN KEY
确保用户和商品合法
🗃 表结构:
SQL
-- 用户表,记录用户信息
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
-- 商品表,记录库存和商品信息
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
stock INT NOT NULL CHECK (stock >= 0)
);
-- 订单表,记录每笔用户下单信息
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id),
product_id INT REFERENCES products(id),
quantity INT NOT NULL,
created_at TIMESTAMP DEFAULT now()
);
-- 日志表,用于记录订单操作行为
CREATE TABLE order_log (
id SERIAL PRIMARY KEY,
order_id INT,
action TEXT,
log_time TIMESTAMP DEFAULT now()
);
⚙ 存储过程(函数)实现业务逻辑:
SQL
-- place_order 函数:检查库存,创建订单,扣减库存
CREATE OR REPLACE FUNCTION place_order(
p_user_id INT,
p_product_id INT,
p_quantity INT
) RETURNS VOID AS $$
DECLARE
available_stock INT;
BEGIN
-- 锁定该商品行,避免并发冲突
SELECT stock INTO available_stock FROM products
WHERE id = p_product_id FOR UPDATE;
IF available_stock < p_quantity THEN
RAISE EXCEPTION '库存不足';
END IF;
-- 扣减库存
UPDATE products SET stock = stock - p_quantity WHERE id = p_product_id;
-- 创建订单
INSERT INTO orders(user_id, product_id, quantity)
VALUES (p_user_id, p_product_id, p_quantity);
END;
$$ LANGUAGE plpgsql;
⚡ 自动记录日志的触发器:
sql
-- 触发器函数:在每次订单插入后,自动写入日志表
CREATE OR REPLACE FUNCTION log_order_insert()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO order_log(order_id, action)
VALUES (NEW.id, '创建订单');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 创建触发器
CREATE TRIGGER trg_log_order
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION log_order_insert();
📩 调用示例:
SQL
-- 调用下单函数:用户1购买商品101的2件
SELECT place_order(1, 101, 2);
如果库存不足,将自动抛出异常并终止事务;否则,订单创建、库存更新和日志写入都在一个事务中完成。
✅ 与应用层逻辑的对比(以 Node.js 为例)
以"下订单"流程为例,下面是将业务逻辑写在 Node.js 应用层的常规写法:
📦 Node.js 中传统写法(Express + ORM 示例)
js
// 下订单:应用层业务控制
app.post('/order', async (req, res) => {
const { userId, productId, quantity } = req.body;
const product = await db.products.findOne({ where: { id: productId } });
if (product.stock < quantity) {
return res.status(400).json({ error: '库存不足' });
}
await db.sequelize.transaction(async (t) => {
await db.products.update({ stock: product.stock - quantity }, { where: { id: productId }, transaction: t });
const order = await db.orders.create({ userId, productId, quantity }, { transaction: t });
await db.order_log.create({ orderId: order.id, action: '创建订单' }, { transaction: t });
});
res.json({ success: true });
});
🆚 与数据库侧逻辑对比
- 复杂度:Node.js 中需手动管理事务、依赖 ORM;而数据库侧只需调用一个函数即可
- 一致性:数据库事务天然封装,应用层需开发者小心处理异常回滚
- 复用性:Node.js 每个服务都需实现逻辑,数据库中只需调用同一函数
特性 | 写在数据库中 | 写在应用中(Node.js) |
---|---|---|
性能 | 🔼 减少往返 | 🔽 多次 SQL 请求 |
事务一致性 | 🔼 数据库内事务 | 🔽 ORM 或手动控制 |
可复用性 | 🔼 多端共享函数 | 🔽 每端都要实现 |
调试测试 | 🔽 较难 | 🔼 成熟工具 |
版本管理 | 🔼 Flyway 管理 .sql 脚本 | 🔼 Git、CI |
微服务适配 | 🔽 拆分受限 | 🔼 模块化更好 |
✅ 结论:如果业务规则稳定、调用频繁、对一致性和性能要求极高,可以考虑写在数据库。
如果业务变动频繁、协作团队偏向前后端协作开发,写在 Node.js 等应用层会更灵活易维护。
🧰 补充一下 PostgreSQL 中触发器与函数基本用法
📌 自定义函数(Function)
- 使用
CREATE FUNCTION
定义 - 支持
RETURNS
返回值(VOID、INT、TABLE等) - 支持变量声明、条件判断、循环控制
- 支持事务语义与异常处理
- 示例:
SQL
CREATE OR REPLACE FUNCTION add_numbers(a INT, b INT) RETURNS INT AS $$
BEGIN
RETURN a + b;
END;
$$ LANGUAGE plpgsql;
📌 触发器(Trigger)
- 使用
CREATE TRIGGER
声明 - 触发时机:
BEFORE
/AFTER
/INSTEAD OF
- 支持
FOR EACH ROW
(逐行)或FOR EACH STATEMENT
(一次) - 常与函数配合使用:触发某操作时自动调用函数
- 示例:记录用户表插入行为
SQL
CREATE OR REPLACE FUNCTION log_user_insert()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_log(table_name, action)
VALUES ('users', 'insert');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_user_insert
AFTER INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION log_user_insert();
📆 版本控制与维护建议
- 所有数据库函数、触发器、初始数据放入 SQL 脚本(建议按模块拆分)
- 使用 Flyway、Sqitch 或 Liquibase 等工具进行数据库迁移管理
- 存储过程内避免过度业务嵌套逻辑,只保留核心流程判断
- 复杂规则使用 UDF 拆解,保持函数职责清晰
- 编写数据库层级单元测试(pgTAP、utPLSQL)
☑ 哪些场景适合把业务写进 PostgreSQL?
✅ 适合:
- 高并发订单、库存等写密集型业务
- 业务规则稳定但对一致性要求高(如财务、支付)
- 多前端系统共用同一业务逻辑的场景(如后台、App、小程序)
❌ 不适合:
- 多服务拆分(微服务)架构中跨库逻辑
- 规则频繁变化、动态性高的业务(如策略系统)
- 研发团队缺乏数据库编程经验的情况
🔄 总结
数据库不仅仅是存储器,它也可以是规则的守门人。
通过 PostgreSQL 的强大特性(事务、触发器、函数、约束等),我们可以:
- 将部分核心逻辑转移到数据库中
- 提高性能与一致性
- 降低重复实现和维护成本
这种思路适用于对数据一致性要求极高、规则变化不频繁的系统。虽然不是银弹,但在对的场景下,它是值得考虑的架构武器之一。
"数据库是被低估的计算平台。"
📎 附录:我们真的需要"大数据"和"微服务"吗?
在开发实践中,我们常常接触到"互联网标签"------大数据、微服务、分布式、高可用、弹性扩展、容器化、CI/CD、DAU 过亿......这些术语令人眼花缭乱,也容易让人误以为它们是每个项目的"标配"。
但现实往往不是这样:
- 绝大多数开发者 一生中所参与的系统,DAU 不会超过十万,甚至连万级都难达到。
- 系统复杂度的瓶颈更多来自不清晰的逻辑、不一致的数据模型、不规范的协作流程,而不是流量或分布式架构。
- 很多系统盲目引入 Kafka、Redis、微服务拆分、容器集群,最后反而增加了维护成本、降低了可靠性。
因此,回归本质------用最简单的技术满足业务需要,用最接近数据的方式表达规则和保障一致性
,是对工程效率和质量的最大尊重。
如果你的项目只是一个日活几千的后台管理系统、一个内网的流程平台、一个局域网的协作工具------那把逻辑写进 PostgreSQL,不仅够用,还可能是最优解。
技术选型的重点不在于潮流,而在于恰当。