什么是关系型数据库:一次库存超卖事故的内核复盘

各位,我是老张。今天从一次实战出发,往下拆一层。

先说一句结论。什么是关系型数据库?它是用关系模型组织数据、以二维表存储、用 SQL 操作、并靠事务保证一致性的数据库系统。 这句话你背下来了,面试也够用了。

但问题就在这。我见过太多人把这句话背得滚瓜烂熟,一上生产就懵。几年前我参与过一个秒杀系统,用的正是关系型数据库,标准配置。上线第一晚,库存超卖了三百多件。事后复盘,没人答得上来:数据库明明支持事务,为什么还是超了?

所以这篇不重复定义。我要回答的是那句定义背后的问题:这五个字,在你脚下到底是怎么跑起来的。

定义之外,你可能答不上来的三个问题

先把话说直白。搜"什么是关系型数据库",你大概率会得到一份标准答案:关系模型、二维表、主键外键、SQL、ACID,再加上 MySQL、Oracle 这些例子。这些答案挑不出错,也补不出新东西,几十个字就讲完了。定义本身已经没有信息增量的空间了。

真正拉开差距的,是另外几个问题。那张订单表里的"关系",到底约束的是什么?你敲下 COMMIT 之后,磁盘上到底发生了什么?事务说好的隔离性,又是拿什么东西换来的?这几个问题,随便一个答不上来,真出事故的时候就只能干瞪眼。下面逐层拆。

关系型数据库的"关系",到底约束了什么

很多人把"关系"理解成表跟表的外键。这个理解不算错,但太浅。关系模型里的"关系",本质是一张,行是元组,列是属性。表与表之间的联系,靠主键唯一标识一行,靠外键指向另一张表的主键。很多人只看到"能连起来"这一面,其实真正值钱的是"能被约束"。

说白一点,约束才是关系型的命根。类型约束、非空约束、唯一约束、外键约束、检查约束,它们把"数据不能乱"这件事写进了数据库。为什么金融、电信这类系统死磕关系型数据库?因为业务逻辑的一致性,最终要落到数据约束上。

举个最朴素的例子。账户表里余额这一列,声明成数值类型,加一个非负检查。那么任何试图把余额写成负数的操作,会在数据库层直接被拦下来,而不是等到对账那天才发现。

sql 复制代码
CREATE TABLE t_account (
    id       BIGINT PRIMARY KEY,
    user_id  BIGINT NOT NULL,
    balance  NUMERIC(18,2) NOT NULL CHECK (balance >= 0)
);

这段约束看着不起眼,但它是"关系"的第一层含义:数据库替你守住了数据的底线。

一次提交,在磁盘上到底发生了什么

回到那次超卖。要讲清它,得先知道一次事务提交背后发生了什么。事务的四个特性,缩写是 ACID,原子性、一致性、隔离性、持久性。这四个词人人会背,但它们不是免费的,每一个都要靠实打实的机制去换。

先说原子性和持久性。这两个靠的是先写日志,再写数据 。你敲下 COMMIT 的那一刻,数据库不会把数据页直接刷到磁盘,它先把这次改动的日志落盘。以 KES 为例,事务提交的流程是这样的:先把事务相关的 REDO 日志从缓冲区写回磁盘上的日志文件,然后释放这个事务持有的锁,最后才把事务标记为已完成。数据页的落盘是后面 checkpoint 阶段的事。

sql 复制代码
-- 一个最普通的转账事务
BEGIN;
UPDATE t_account SET balance = balance - 100 WHERE id = 1;
UPDATE t_account SET balance = balance + 100 WHERE id = 2;
COMMIT;

这个顺序很关键。日志先落地,就算下一秒机器断电,重启后也能靠日志把没写完的数据重做出来。这就是持久性。反过来,没提交的事务要回滚,也是拿日志去逐步撤销。那超卖跟这个有什么关系?因为原子性管的是"要么全做要么全不做",管不了"两个事务同时读到同一个库存"。那是隔离性的活。

隔离性:怎么让并发的事务"看不见彼此"

超卖的根因,几乎都是隔离性没设对。事务之间相互干扰,会产生四类问题:脏读、不可重复读、幻读,还有更隐蔽的写偏差。为了压制它们,SQL 标准定义了四个隔离级别。

隔离级别 脏读 不可重复读 幻读 实现代价
读未提交 可能 可能 可能 最低
读已提交 不会 可能 可能 较低
可重复读 不会 不会 理论上可能 较高
串行化 不会 不会 不会 最高

很多数据库默认给的是"读已提交"。这个级别下,同一事务里两次读同一行,结果可能不一样。秒杀那种"先查库存再扣减"的写法,恰好踩在这个缝上。

sql 复制代码
-- 危险的写法:查和扣分两步,中间有窗口
SELECT stock FROM t_stock WHERE sku = 'A001';
-- ... 应用层判断 stock > 0 ...
UPDATE t_stock SET stock = stock - 1 WHERE sku = 'A001';

两个请求同时走到查询那一步,都读到库存为 1,都判断"够扣",于是都去扣。超卖就这么来的。正确的做法是把判断和扣减压进一条语句,或者显式加锁把窗口关掉。

底层靠什么实现隔离?两条路。一条是 ,通过两段锁协议保证并发调度的结果可以串行化。另一条是 MVCC,多版本并发控制。MVCC 给数据保留多个版本,读操作去读某个时间点的快照,不用去等写操作。这样读写不互相阻塞,并发能力就上来了。理解了这层,你就明白为什么"关系型数据库是不是过时了"这种问题没有意义。它处理并发一致性的这套机制,是几十年工程打磨出来的,换个存储模型绕不过去。

索引为什么快:B+Tree 和存储引擎

再往下挖一层,就碰到存储引擎了。你写的表和索引,物理上到底放在哪、怎么组织,这是存储引擎管的。拿 KES 来说,它的存储引擎是 heap 表引擎,负责存放用户表和系统表的实际数据以及索引数据。

这个引擎不是孤立的。它跟 WAL 日志、checkpoint、缓冲池、关系缓存这几块是强相关的。你查一条数据,背后是缓冲池在内存和磁盘之间搬运页面;你改一条数据,WAL 负责留痕;checkpoint 负责把脏页刷下去。这几块拼起来,才构成一次完整的读写和事务链路。

那索引为什么能把查询从全表扫描压成几毫秒?主流关系型数据库的索引结构是 B+Tree。它有两条特性最关键。

一是树矮 。B+Tree 一个节点能放下几百个键,三到四层就能撑起上亿行数据,查一次最多几次磁盘 IO。二是叶子节点连成有序链表。范围查询顺着链表扫就行,不用回到根节点重走。

这里有个实战经验。索引不是越多越好。每个索引都会拖慢写入,因为写数据时得同步维护所有索引。见过一张表挂了十几个索引,写入慢得像蜗牛,最后砍掉一半,写入直接翻倍。这个是踩过坑的,不用怀疑。

那到底怎么选:关系型 vs 非关系型

拆完内核,回到选型。这也是搜"什么是关系型数据库"的人真正关心的问题。

维度 关系型数据库 非关系型数据库
数据模型 二维表,强结构 键值、文档、列族、图
事务 完整 ACID 多数只保证单行或最终一致
查询语言 标准 SQL 各家自有 API
扩展方式 垂直扩展为主,可集群 原生水平扩展
适合场景 交易、账务、强一致业务 日志、缓存、海量稀疏数据
国产化适配 有成熟的国产替代(如 KES) 生态以开源社区版本为主
代表产品 KES、Oracle、MySQL Redis、MongoDB、HBase

结论很清楚。要强一致、要复杂关联查询、要事务兜底,选关系型。要很高的写入吞吐、要灵活的 schema、能接受最终一致,选非关系型。

现实中大部分核心系统是关系型打底,非关系型做补充。缓存放 Redis,日志放 ES,账务和订单老老实实放关系型数据库。这不是保守,是权衡。

信创场景下,一个关系型数据库该怎么挑

把视角切到国产化替代。这几年很多单位在做信创改造,问法也从"什么是关系型数据库"变成了"用哪个关系型数据库"。挑国产关系型数据库,我一般看四个维度。

第一看迁移成本。老系统里沉淀了大量 Oracle 的存储过程、内置函数和系统视图,能不能少改甚至不改是关键。KES 支持内核级的兼容模式,数据库类型为 KINGBASE 时,可以选择 ORACLE、MYSQL、SQLSERVER 等不同兼容模式,不同模式下内核支持的能力不一样。说到底,这种内核级兼容图的就是一件事:Oracle 的存储过程少改甚至不改,这一点直接决定迁移工期。

第二看高并发承载。核心业务系统的 OLTP 压力是硬指标,账务、订单这类场景对事务吞吐和响应延迟都很敏感。选型时别只看参数,要看真实业务负载下跑出来的表现。

第三看高可用与数据同步。KES 的主从同步支持半同步或强同步,避免数据丢失。配合 Kingbase FlySync(金仓异构数据同步软件),可以先把源系统和新建系统的数据实时同步,选业务低峰期做割接,把停机时间压到最低。

第四看工程化落地能力。有没有成熟的迁移工具链,有没有可参考的同行业案例。

说到案例,金仓在金融和教育行业有不少落地。金融这边,它的新一代手机银行系统解决方案连续入选金融信创优秀解决方案,把用户中心、账户中心、限额中心等十二大业务做了整合迁移。教育这边,某高校的教务管理系统用这套方案承接了核心业务。这些案例的共性,是从"能用"往"好用"走,靠的是工程化,不是一句口号。

关系型数据库避坑清单

拆到最后,给你三份真金白银换来的教训。

  1. 先查库存再扣减,这种两步写法在秒杀和扣减场景就是事故源头。两个请求同时读到库存 1、都判断够扣,扣两次就超卖;能压成一条带条件的 UPDATE 就压,压不了就显式加锁,别指望默认隔离级别兜底。
  2. COMMIT 返回成功,不等于数据已经安全落盘。日志先于数据页落盘这个顺序,决定了哪些参数动了会丢数据;上线前把"是否等日志刷盘再返回"这类开关的语义摸清楚,别等断电重启才发现日志没落。
  3. 索引别贪多。每次写入都要同步维护所有索引,挂得越多写得越慢;见过一张表挂十几个索引、写入慢得像蜗牛,砍掉一半才恢复,建索引要跟着查询模式走,不能跟着直觉走。

小结

回到开头那个问题。什么是关系型数据库?它是关系模型、二维表、SQL 和 ACID 的组合,但更准确的说法是:**它是一整套用约束守住底线、用日志保证持久、用 MVCC 和锁撑起并发、用 B+Tree 加速查询的系统工程。**它的价值,藏在定义背后的这套机制里。这也是为什么信创选型时,光看"是不是关系型"远远不够,得看内核做得扎不扎实。

落到国产替代,金仓(KES)走的是自主研发这条路。它能帮上什么忙,说白了就三件事:迁移时让 Oracle 的存储过程少改甚至不改,割接时用 Kingbase FlySync 做实时同步、不用停机,运行时靠主从强同步兜底、不丢数据。从"能用"到"好用",靠的就是这些做得扎实,不是一句口号。对正在做信创改造的团队来说,值得认真评估。

你所在的核心系统,目前用的是哪种关系型数据库?迁移时最头疼的是存储过程改写,还是数据同步?欢迎在评论区聊聊,我挑典型的在下篇拆。

我是底层玩家老张。咱不聊概念,只聊源码和压测跑出来的东西。

相关推荐
徐子童5 小时前
介绍MVCC机制
java·mysql·面试题·秋招·并发·mvcc
拒绝内耗。5 天前
高并发系统如何提升吞吐量?从扩容、缓存、异步到数据库优化
架构·高并发
凤山老林5 天前
高并发系统架构设计:Spring Boot 多级限流、库存预扣减与异步下单全链路实战
springboot·高并发·限流·异步
敲代码的嘎仔8 天前
用 Redis 合并写 + DelayQueue 延迟检测,把视频播放进度写库频率降到 1/60
java·开发语言·数据库·redis·缓存·音视频·高并发
敲代码的嘎仔8 天前
互动问答系统实战:两级评论模型、ES 搜索集成、Caffeine 多级缓存全记录
java·开发语言·数据库·elasticsearch·缓存·mybatis·高并发
隐擎fox9 天前
高性能网络爬虫架构设计:基于 Python 的长连接复用与分布式会话池调度实践
分布式·python·网络协议·tcp/ip·高并发·网络爬虫、
Java爱好狂.11 天前
Java初学者如何设计一个高并发系统?
程序员·高并发·架构师·并发编程·java面试·java面试题·java八股文
数据库小学妹13 天前
什么是关系型数据库?零基础用一张订单表讲透原理、SQL与ACID
经验分享·关系型数据库·国产数据库·acid·关系数据库·关系模型·主键外键
爱和冰阔落13 天前
【Linux】epoll 真的是 O(1) 吗?从阻塞 I/O、select/poll 到 LT/ET 与 Reactor 一次讲透
linux·运维·高并发·tcp