1. 卡券业务的“百亿级”压力,究竟压在哪里
1.1 券中心的数据模型和核心链路
“爱奇艺卡券”听起来可能不太像重资产系统,真正把券相关的数据模型摆出来以后,你会意识到这其实是非常典型的互联网交易型场景。券的生命周期至少包含这么几层:
- 券模板表:维护满减配置、可用范围、有效期规则,数据量不大,但每次活动投放前会被高频读取。
- 券实例表:用户领取后生成的实际“卡券”,包含券码、状态、锁定订单等。单用户领取量可控,但总量会快速膨胀到数十亿。
- 发券流水表:记录领取、核销、退款、过期、补发等每一次状态变更。这是百亿级数据的重头,也是分析报表最常访问的部分。
- 对账与库存表:用于控制活动库存,支持预扣、回补和每日对账。
早期这套系统用 MySQL 分库分表来支撑,核心思路是“按用户维度分片”。用户领取一张券时,通过 userId 哈希路由到对应的分库,单条写入很快。用户打开“我的卡包”,查询同样落在用户分片上,再配合缓存,体验也没有太大问题。真正让人头疼的,是后台业务方开始频繁跑多维分析:某场活动发了多少张券,其中新用户领了多少,核销率如何,不同端核销占比多少,被退款又退了多少券。这类查询一旦跨了分片,就会变成“把所有分库的数据都捞出来,在应用层临时聚合”,SQL 写起来复杂,执行速度也完全没有保障。
另外一个长期痛点来自数据增长本身。百亿级流水对任何单一大表都是不小的负担,如果在 MySQL 里继续单表保存,超大表的索引重建、归档清理和备份恢复都会越来越慢。分库分表看似解决了容量,却把运维复杂度转移到了业务侧:分片规则要维护,扩容要迁移数据,跨分片事务要引入分布式事务组件,查询还要适配分片键。时间一长,数据库本身的运维同学苦不堪言,业务研发也被各种“路由限制”绑住手脚。
1.2 原架构的三个“硬伤”,不是调参数能解决的
先说容量。分库分表的扩展能力在理论上有上限,但在实际运维里,真正让人担心的是每个分片上的数据增长不均衡。比如热门影视内容的活动券会集中在少数渠道或者少数用户群体里,按 userId 哈希后可能还算均匀,但按活动维度去查库存和核销数据时,热点会集中落在某几个分片。DBA 要频繁处理“某个分片磁盘快满了”的告警,还要加班做在线扩容。
再说实时分析。传统的解法是把线上分库的数据同步到一套独立的数据仓库或分析型数据库,再让报表平台去查询。这看起来没毛病,实际上会引入两条业务链路:线上库负责交易,分析库负责统计。只要同步链路有一点延迟或者数据丢失,运营看到的数据就可能和线上真实状态对不上。业务方经常拿着报表和数据团队“吵架”,而数据团队又很难证明是哪一侧出了问题。还有一个隐蔽问题:仓库里的表结构必须提前设计好。如果业务临时要按一个新的维度去统计,数据开发得先改同步任务,再补数据,根本做不到灵活即查。
最后是运维成本。MySQL 分库分表方案通常还要搭配消息队列做异步解耦,搭配缓存扛峰值,搭配数据同步工具做分析。系统里的组件越多,出故障的概率越高。每次大促前,团队要做全链路压测、容量预估和预案演练,其实有很大一部分精力都消耗在“保证各个组件之间的数据能对得上”这件事上。
所以,单纯把 MySQL 实例换个更强的机器,或者把分片数量再增加一倍,都不解决根本问题。我们需要的是一个能让“交易”和“分析”在同一套数据库内同时跑起来的架构,这也是后来选择 OceanBase 的核心原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. “单库双擎”架构设计:为什么是 OceanBase,以及怎么落地
2.1 单库双擎,在 OceanBase 里到底是什么
先澄清一个概念。“单库双擎”不是指“一个库加两个数据库实例”,从我们的落地经验看,它强调的是用一个统一的数据集群承接业务全量数据,同时在该集群里提供“行存”和“列存”两种引擎能力。
在 OceanBase 的实现里,你可以把一张表或者一个分区设计成行存模式,适合那些频繁按主键点查、小范围更新和插入的场景;也可以把表设计成列存模式,适合那些需要扫描海量行、计算求和、分组聚合的分析场景。对于卡券业务来说,用户领取一张券、核销一张券,本质上是高频的小事务写入,用行存天然合适;而运营侧想要统计“某个时间段内发放了多少券、每个业务线核销了多少张”,需要读取和聚合大量行,列存的压缩率和扫描性能会比行存高出一大截。
这样的做法,和过去“把在线数据库与分析库拆开”的路线有本质区别。以前要保证分析库能查到最新数据,需要额外的同步过程;现在用单库双擎,所有数据都保留在同一个集群中,应用层和报表层面向的是同一份逻辑数据,不再需要担心“两边数据怎么对齐”的问题。复制链路少了,数据一致性的争论自然也少了。
另外需要明确的是,OceanBase 本身还带有分布式多副本机制。我们常说的“单库”,并不意味着把数据放在一台单机上硬扛;底层数据仍然按分区分布在不同节点,并且每个分区都有多个副本。出现节点故障时,系统会自动切换副本,业务基本无感知。作为运维方,我们感知到的是一个对内收敛的集群入口,这比管理十几套 MySQL 分片要清爽太多。
2.2 卡券数据在双擎架构中的建模思路
用这套架构升级,最关键的并不是安装和迁移,而是先把存量表分类清楚:哪些表适合行存,哪些表适合列存,哪些表需要同时满足两类场景。
我们当时给出的一个通用处理原则是:所有作为业务核心“状态机”的表,都优先行存。例如券实例表,写入时创建新记录,用户使用时更新状态,偶尔需要按用户 ID 拉取他名下的券列表,这些都是点查或小范围查询,行存能提供最低的写入延迟和最稳定的查询性能。
流水类明细表,优先考虑列存。 比如发券流水,每天新增几千万条,业务方很少关心其中某一条,反而经常要做全量聚合。列存可以按列读取,只在涉及字段上做扫描和计算,加上列存本身的压缩优势,百亿级流水占用的物理存储也会比行存低不少。用列存跑一次“按活动分组统计核销数量”的查询,过去在 MySQL 分库上可能要等十几秒甚至更久,换到位后基本可以做到秒级出结果。
但实际业务没有这么“二选一”干净。拿“用户最近 20 条发券流水”来说,它天然是一个需要按用户维度的范围查询;可一旦流水表做成列存,点查和范围查询未必划算。为了兼顾两端,实践里我更推荐把流水拆成两类存储:一份是用于线上查询的“近期用户流水”,保留近几个月数据,采用行存;另一份是用于全量分析的“历史归档流水”,采用列存并按照日期分区。这样既不会因为线上表太大拖慢查询,也不会因为分析需求频繁扫行存表导致大量 I/O 开销。
2.3 为什么最终没有继续走“分库分表 + 数仓”的老路
和很多团队一样,我们在早期规划时也讨论过“继续沿用分库分表,把分析需求全部丢给 Hadoop/数仓”的方案。这套方案不是不能用,它最大的问题出在运维和时效上。线上分库和数仓之间需要数据同步任务,而同步任务一旦多起来,还要引入调度平台、监控告警和延迟补偿机制。运营想看秒级实时数据时,如果同步延迟几十秒,他们就会觉得系统“不准”。
从研发效率的角度看,单库双擎还有一个潜在优势:SQL 不再受分片键限制。分库分表下,一条业务查询必须带上 userId 才能路由到正确分片;不带分片键的查询会扫全库,一般都被禁止。但在 OceanBase 的单库架构下,虽然分区表也有分区键,分布式执行引擎却能在所有分区上并行扫描并聚合结果。这意味着研发写查询时,可以更关注业务条件本身,而不是时刻想着“我到底该用哪个字段路由”。迁移之前,这种自由度是想都不敢想的。
提示:这里要澄清一点,不要让研发同学产生“所有 SQL 都不需要关心分区”的误解。单库双擎解决的是不再做物理分库,但它仍然是分布式数据库。高频点查如果完全不带分区键,会触发跨分区的分布式查询,性能大概率不如带分区键的 SQL。所以在写代码时,我们仍然建议把 user_id 或活动 ID 这类高频条件放进 SQL 里,帮助优化器裁剪不必要的分区。
3. 从 MySQL 分库迁移到 OceanBase:全量、增量、灰度与回滚
3.1 迁移前的摸底和准备工作
现网已经有百亿级数据,迁移一定不能抱着“一把梭”的心态。我们第一步做的,是把所有表和查询场景做了一次完整盘点,梳理出哪几张表是核心交易表,哪几张只是日志或流水,哪几张允许在迁移窗口之外临时停止写入。因为卡券业务每天都有人领券、核销,所以所谓的“停机窗口”实际上非常短,整个方案必须支持在线迁移。
摸底时重点关注三个指标:单表数据量、每日新增量、访问峰值 QPS。百亿级流水表基本没办法一次性全量导出导入,需要对存量历史数据分批迁移,通常按时间分区处理。同时,我们还要确认源端 MySQL 开启了 binlog,并且日志格式是 ROW 级别。这直接决定后续做增量同步时能拿到哪些变更记录。如果之前为了省磁盘关掉了 binlog,那么迁移前必须提前打开,否则后续增量数据只能靠应用层补录,整个过程会非常痛苦。
兼容性评估也要提前做。OceanBase 对 MySQL 语法有较高的兼容度,但并非所有周边工具都能无缝切换。我们梳理了业务代码里常见的 SQL 模式,尤其是带 AUTO_INCREMENT 自增主键的表、依赖 LAST_INSERT_ID() 的代码,以及某些特殊的分页写法。对于自增主键,我们在迁移时故意改成了业务内置的分布式 ID 生成器,避免多节点写入时产生冲突。分页写法如果还是继续用“深分页 offset”,在目标库一样会遇到性能问题,所以顺手改成了基于游标的分页方式。
3.2 全量补齐与增量追平
正式迁移时,我们采用“全量 + 增量”双轨方案。先用数据迁移工具把存量历史数据从源端 MySQL 同步到 OceanBase,这个过程只拷贝某一时间点的快照数据。为了保证目标库数据不是“缺一段的旧数据”,同步过程会记录一个起始位点,让后续的增量任务从该位点开始订阅源端的 binlog 变更,持续应用到目标库。
百亿级流水全量同步要考虑限速和断点续传。全速跑可能导致源库的磁盘 I/O 飙升,影响正在运行的线上业务。建议把限速阈值设在源库高峰期峰值的 30% 左右,避开白天业务高峰执行。同时,任务必须能在失败后倒回断点继续跑,绝不能从头来过。数据量越大,“失败后能否续跑”越重要,如果每次失败都要重来,那项目周期会被拖得不可控。
增量追平阶段则要看一个关键指标:同步延迟。刚开始启用增量任务时,目标库和源库存在的时间差可能还有几小时,需要等它逐渐缩小到几秒甚至毫秒级。当增量延迟小于 5 秒时,可以做数据校验。校验不是抽几条数据看看,而是要对关键业务表做总数校验、分片抽样校验和关键字段比对。我们当时写了一套校验脚本:先对比源端库和目标端库中每个逻辑表的总行数,再随机抽几个分片对比字段值。发现差异项后,根据主键逐条回查定位差异原因,大部分是因为全量期间数据发生了更新,增量任务覆盖了,少部分是源端本身存在未清理的脏数据。
3.3 流量灰度切换:先读后写,留好回滚后手
数据追平到秒级之后,就进入最让运维紧张的流量切换阶段。我们分了三步走,每一步都有明确的准入和回滚条件。
第一步,把后台报表和分析系统的只读流量切到 OceanBase。这部分流量不影响线上发券,即使切过去后查询表现不佳,短期影响也可控。切换后主要观察慢查询数量、CPU 利用率和查询响应时间。如果问题较多,可以从 SQL 层面调整索引和查询计划,不必急于切线上交易。
第二步,把线上“非核心写流量”切到新库,例如用户主动删除过期券、历史过期清理等低频写入任务。这一步会暴露真实写场景下的锁竞争和事务问题。比如我们发现“批量核销过期券”的任务一次性更新行数太多,容易和正常核销请求相互等待,因此把批处理拆成了小批量任务,每次只更新几百条,再做延迟一定时间的循环处理。
第三步才是核心的“领券/核销”读写流量切换。为了保证万无一失,我们在切换期间保留了双写机制:新库作为主写入路径,同时把变更异步按原格式写入老库,老库保持只读加备份状态。一旦新库侧出现大规模故障,可以在分钟级内切回老库。双写机制会带来额外的应用改造成本,比较关键的一点是做好幂等控制。我们给每笔发券和核销操作都生成了唯一的 request_id,写入端可能重复提交,但数据库和消费端通过唯一索引去重,从根源保证了数据不一致地重复入账。
注意:双写方案不是永久跑下去的。新库稳定运行一两周后,就应该关掉双写链路,同时使用反向同步工具把切换期间产生的增量数据继续同步到老库,保持老库在一段时间内可用。只切换不同步会造成回滚数据缺失,如果回滚后发现数据丢了一大截,那才是最尴尬的局面。
4. 上线前后的典型问题和避坑经验
4.1 热点券的库存扣减,处理不当容易拖垮数据库
卡券业务和普通交易不一样的地方在于,一场热门活动的库存是集中在一个券模板上的。所有用户同时抢领,数据库层的操作其实是“更新同一个模板的库存”,这个热点很快会成为整个系统的瓶颈。在 MySQL 分库阶段,我们靠 Redis 预扣库存挡住大部分流量,只有真正发券成功时才回写数据库。升级到 OceanBase 后,这个思路依然可用,但需要注意把最终回写数据库的流量控制在合理范围。
实践中我们会把“扣减库存”和“生成用户券实例”拆分:抢领成功先扣 Redis 中的活动库存,随后通过异步队列落库;数据库层只负责扣减真实库存并将状态置为已领取。一次抢券的突发峰值可能是几万到几十万 QPS,但真正打到数据库的写请求被平滑成每秒几百到几千 QPS,这个量级在 OceanBase 集群里完全能够稳定承受。
4.2 大查询突然影响在线业务的排查思路
单库双擎上线后,一个新的隐患浮现了:分析查询和交易查询跑在同一套集群里,万一分析查询占用资源过高,会不会影响线上领券?我们确实遇到过类似告警。运营在后台跑了一张很大范围的报表 SQL,目标表是一张百亿级流水表,一次性扫描了不少分区。当时集群 CPU 曲线突然拉起,发券接口的响应时间也随之小幅波动。
排查时先通过数据库审计视图找到了正在执行的大查询,发现它来自报表用户,并且扫描行数超过预期。问题根源不是 SQL 不支持,而是没有做资源隔离。OceanBase 有多种资源隔离手段:可以为不同业务或用户设置不同的租户资源池,也可以为大查询设置阈值和超时,并限制单个查询的内存使用。我们把报表查询切换到专用的分析型租户,同时限制扫描行数和超时时间,在线交易就再没有被大查询干扰过。
这个实际问题也给我们提了个醒:双擎架构虽然从技术层面拉近了交易和分析的距离,但在运维资源分配上仍必须有“隔离意识”。不是说放在同一套数据库里就可以任意跑大查询,任何分布式系统都经不起无节制的全表扫描。
4.3 慢 SQL 排查和索引优化,要掌握“分场景定位”的方法
迁移完成后的优化工作比想象中多。报表平台切过来后,一开始确实有很多 SQL 比原来在 MySQL 分库时更快了,但也有一些 SQL 执行计划不理想,原因是对 OceanBase 的优化器行为不够熟悉。这里给几条现场实操中比较有用的排查经验:
- 遇到慢 SQL 时,先通过诊断视图查看执行计划,而不是凭经验猜索引。OceanBase 的并行执行能力比较强,有时候没走索引反而会走并行全表扫描,扫描行数多但速度并不慢。
- 给大表加索引看似是通用的解决办法,但在百亿级流水表上执行 DDL 也要挑业务低峰期,并且观察对 CPU 和 I/O 的影响。
- 多表关联查询要特别注意统计信息是否及时更新。表数据量变化剧烈时,旧统计信息可能让优化器选择错误的连接顺序。大促或集中清理任务跑完后,建议手动刷新相关表的统计信息。
- 分页查询尽量改成“上次看到的 ID/时间”这类的键集分页,而不是传统的 offset 翻页,尤其是百亿级表。offset 越大,数据库要扫和丢的行越多,性能自然越来越差。
4.4 备份恢复和容量规划也要用“双擎思维”
升级到 OceanBase 后,备份的对象不再是一个个 MySQL 分片,而是一个逻辑集群。但备份策略的制定要从严,尤其要对超大表做区分处理。核心交易表需要高频增量备份和较低恢复时间目标;流水分析表可以适当放宽备份频率,因为它可以通过历史归档从不同路径恢复。如果所有表都用同一套大而全的备份策略,备份时间和存储成本都会翻倍。
容量规划方面,列存带来的压缩效果非常明显,所以“数据总量”并不等于“存储占用总量”。但列存引擎对内存的消耗可能比行存更敏感,尤其是跑大数据量聚合时。规划 OceanBase 集群节点时,不能只按照源端 MySQL 的磁盘占用去估算,还要把未来分析流量的增长空间预留出来。我们在最初部署时曾把 CPU 估得偏小,上线后发现分析 SQL 持续占资源,只能临时扩容计算节点,整个过程虽然顺利,但能不折腾尽量别折腾。
提示:如果你正准备把类似规模的业务迁到 OceanBase,我建议上线前先搭一个小集群做一次“仿真压测”。压测不能只压交易写场景,也要把运营实际会跑的分析型 SQL 一起放进来,模拟线上混合负载。只看单场景性能的压测,在 HTAP 架构下参考意义有限。
5. 写在最后的小经验
这次“单库双擎”升级,回头看最有价值的不是数据库软件本身,而是整个团队对卡券数据模型做了一次彻底梳理。哪些表能归档、哪些表能列存、哪些表必须行存、哪些查询是真正关键的,这些问题的答案远比“换一个数据库”更值钱。任何架构升级都应该先回答数据形态和访问特征,再选技术和工具。顺序一旦颠倒,后面会不断为最初的粗糙认知买单。
如果让我提炼出一条最想分享的经验,那就是:在做 HTAP 改造或替换分库方案时,永远先分清楚业务场景和执行路径,不要为了“统一”而把不同负载强行塞进同一个入口。单库双擎的价值,是让你有权力选择把哪些场景放在一起,而不是逼你把所有负载混在一起。想清楚这一点,后续的每一步都会顺很多。
