当前位置: 首页 > news >正文

大考阅卷高并发下数据库架构平滑演进实践

先给结论:大考阅卷这类业务,真正的难点不是“数据库性能不够”,而是“流量来得又猛又集中,数据库架构却不能在关键节点随便重构”。如果你们的系统也面临周期性高并发,比如考试阅卷、报名抢课、成绩查询、活动秒杀,那么数据库架构的平滑演进方案,比单纯调高配置更重要。这篇文章以学科网大考阅卷场景为背景,拆解从单库单表到读写分离、分库分表、队列异步化的一步步实践,重点说清楚怎么切换才不出事故,怎么回滚才算安全,以及高峰期到底该盯哪些指标。

适合谁看?负责线上业务后端或数据库运维的工程师,准备做架构升级但担心稳定性的人,还有那些“系统平时很稳,一到考试就报警”的团队。这篇文章不提供万能架构,但会给一套可以照着检查、照着验证的方法。

1. 大考阅卷高并发的真实压力点在哪里

先说清楚业务模型。大考阅卷不是普通的 CRUD 系统,它有几个非常明显的特征:周期性极强、峰值极高、状态流转复杂、数据一致性要求严格。平时可能一天几千个请求,大考一到,可能一瞬间涌进来几十万次提交,而且这些请求不是均匀分布的。

1.1 阅卷业务的流量特征和普通电商不一样

电商的秒杀,用户点一次按钮,后面是库存扣减和订单创建,链路短,可以大量依赖前置拦截。阅卷场景不一样,考生交卷、答案上传、图片切分、题目分发、阅卷教师端的拉取任务、打分提交、异常卷标记、成绩合成,每一步都落在数据库上。

流量模型上有几个关键差异:

  • 读多写少,但写又是强依赖。阅卷教师不停拉取任务、切换考生、提交分数,任务表被反复读写。
  • 高峰期不是几分钟,而是持续数小时。有些科目阅卷会连续开放几天,白天晚上都有教师在线,压力曲线不会快速回落。
  • 系统不允许“丢数据”。考生分数如果因为数据库写入失败丢了,后面没法解释。
  • 热点集中在少数几张表。任务表、答卷表、分数表天然是热点,靠单库单表很难撑住。

很多人一说到高并发,第一反应就是“上缓存”“上队列”,但阅卷场景没那么简单。答案图片、题目内容这些可以缓存,但阅卷教师提交的分数、仲裁结果、复查状态这些绝对不能只放缓存,必须落库。也就是说,缓存解决了读放大,但写放大仍然压在数据库上。

1.2 最容易出问题的不是数据库本身,而是链路里的连接池和慢查询

我见过很多次大考前的压测,数据库 CPU 还没有满,应用层先崩了。原因很常见:连接池满了。

应用服务每次操作都要从连接池拿一个数据库连接,数据库的并发连接数是有限的。压力一起来,请求排队等待连接,等待时间超过应用层超时,前端就开始报错。这时候数据库负载看起来可能只有 60%,但业务已经大面积超时。

所以排查高并发问题,不能只盯数据库的 CPU、内存,还要看:

  • 活跃连接数是不是已经接近连接池最大值。
  • 应用等待数据库连接的平均耗时是多少。
  • 是否存在大量慢 SQL 长期占着连接不释放。
  • 是否有长事务,导致连接池被“吃住”。

这几个指标往往比 CPU 更重要。大考阅卷这种读多写多的场景,一旦连接池被打满,后面所有请求都要排队,雪崩就是几十秒的事情。

2. 数据库架构演进的几个阶段:为什么不能一步到位

现在来看架构演进。很多团队会陷入一个误区:既然未来要分库分表,干脆一开始就把所有表拆好。我的建议是不要这么干。分库分表会显著提高开发和查询成本,如果业务没有到那个量级,反而浪费人力,还容易引入分布式事务问题。

数据库架构演进没有银弹,只有按阶段走。

2.1 第一阶段:单库单表,先保证业务能闭环

最早期的阅卷系统通常就是一套单体应用,配一个 MySQL 实例。所有业务表都在同一个库里,事务简单,开发速度快,适合业务验证期。

这个阶段不用过度设计。只要做到:

  • 合理建立索引,特别是任务表、答卷表上的联合索引。
  • 控制单表数据量,定期归档历史考试数据。
  • 避免一个大事务里写入过多数据。
  • 提前把慢查询日志打开。

单库单表的瓶颈在数据量和连接数。当一次大考的答卷记录达到千万级,任务表和分数表不断膨胀时,即使加了索引,查询也会开始变慢。这是第一个必须演进的信号。

2.2 第二阶段:读写分离,把读压力先卸掉

阅卷业务有一个天然特点:读操作可以接受轻微延迟。教师端拉取任务列表、查看考生答卷、查询已批阅数量,这些请求从从库读完全没问题。

读写分离怎么做才稳?

  • 主库只负责写操作:任务创建、分数提交、状态更新。
  • 从库负责查询:任务列表、统计报表、阅卷进度。
  • 通过中间件或者应用层数据源路由,把读写请求分发到不同实例。
  • 关键点是主从延迟监控。大考期间如果从库延迟超过几秒,教师端看到的“已批阅数量”就可能不准,容易以为系统卡了。

读写分离能解决读放大,但解决不了写热点。当所有阅卷教师都在提交分数,分数表的主键写入还是集中在一个主库上,写锁和磁盘 IO 会逐渐成为瓶颈。这就需要考虑把数据拆开。

2.3 第三阶段:分库分表,按考试和考生维度切分

分库分表的目的是把数据分散到多个实例,降低单库单表的热点压力。阅卷场景的分片键选择很关键。

我的建议是优先按考生 ID 或答题任务 ID 分片,不要按时间分片。原因很简单:

  • 大考阅卷是持续多天的,按时间分片会把当天提交的分数全部打到一个库上,热点问题没有解决。
  • 按考生 ID 分片,同一个考生对应的答卷、成绩、任务记录都在同一个分片内,天然的隔离性更好。
  • 阅卷时经常要查“某个考生的完整答题情况”,按考生 ID 分片能避免跨库聚合。

分库之后,原本简单的关联查询会被打断。比如“某道题被多少教师批阅过”“某考点的平均分”这类跨分片统计,直接 SQL 查不了,需要提前落汇总表,或者通过异步任务计算。

这里要注意,分库分表不是为了让每个请求都更快,而是为了让高并发请求能分散到多个数据库实例上。如果把 1000 万条记录分成 10 个库,每个库只有 100 万条,单库的压力自然下降。代价是架构复杂度上升,事务从本地事务变成分布式事务,查询需要路由和聚合。

2.4 第四阶段:异步队列和缓存兜底,不只是减少数据库压力

分库分表之后,数据库压力降下来了,但还有一个问题:写入的突发性。阅卷教师往往在某个时间段集中提交分数,比如上午 10 点到 11 点之间,提交量是其他时间的几倍。如果你让应用直接同步写库,数据库扛得住,但连接和事务可能扛不住。

所以第四阶段要引入异步化:

  • 提交分数请求先进入消息队列,应用层立即返回“提交成功”。
  • 后台消费任务批量写入数据库,或者分批提交。
  • 队列堆积量作为关键指标,消费速度必须大于生产速度。

异步化要注意一个常见坑:消息丢失。阅卷分数不能丢,所以消息队列要做好持久化,消费端要保证业务幂等。也就是说,同一个提交请求如果被重复消费,后一次不会重复加分。一般通过业务主键或状态机来保证幂等。

缓存的使用也要有边界。适合缓存的:

  • 考试基本信息、题目内容、评分标准。
  • 教师端首页的统计数字。
  • 热门的考生答卷只读快照。

不适合缓存的:

  • 教师刚提交的分数。
  • 仲裁记录、异常卷状态。
  • 任何需要严格一致性的审核数据。

我之前遇到过团队把所有查询都加缓存,结果教师提交分数后,刷新页面还是旧数据,引发大量投诉。缓存永远只能兜底读,不能替数据库承载写逻辑的最终一致性。

3. 平滑演进的核心方法:切换不是“换库”,而是“搬家”

数据库架构演进最怕的不是写代码,而是切换过程。尤其大考阅卷这种业务,你不可能说“停服两小时,我们把数据迁移一下”。所以平滑演进的核心思路是:旧系统和新系统并行运行,数据双写,灰度放量,随时能回滚。

3.1 双写与回放:新旧库并存期的数据一致性怎么做

双写的思路很简单:新数据同时写入旧库和新库,读流量先继续走旧库,等新库数据追平并稳定一段时间后,再把读流量切到新库。

双写有几个细节必须处理:

  • 双写不是简单地在业务代码里同时写两个数据源,而是要处理“新库写成功、旧库写失败”和“旧库写成功、新库写失败”两种异常。
  • 我的做法是:主写入仍然写旧库,新库通过监听旧库的 binlog,异步同步新增和修改。业务代码不用大改,数据一致性由同步任务保证。
  • 如果必须由业务代码双写,则要引入“通过新旧两个数据源确认一致”的补偿任务。比如每隔 5 分钟扫一次对账表,找出差异,以旧库为准回放数据到新库。

这里有个容易被忽略的点:旧库在迁移期间不能做破坏性变更。比如你不能一边同步 binlog,一边把旧库的某张表结构改成新结构,否则 binlog 解析可能会挂掉,或者同步出来的数据对不上。

3.2 灰度开关:按考试、按地区、按科目逐步放量

即使数据同步没问题,也不能在某天大考前直接切换全部流量。需要做流量灰度。

灰度怎么设计?阅卷业务有几个天然维度:

  • 按考试:先迁移一个普通的模拟考试,再迁移省级统考,最后才上大考。
  • 按科目:同一个考试里,可以先切数学,再切语文。
  • 按地区:如果系统按考区部署,可以直接按考区灰度。
  • 按用户:选择一部分教师账号走新库,观察核心功能是否正常。

灰度开关建议放在配置中心,而不是写死在代码里。因为切换期间可能要秒级调整,改代码、重新发布根本来不及。开关可以是一个 boolean 变量,也可以是一个路由规则表达式,比如“地区 = '某市' AND 科目 = '数学',读流量走新库”。

灰度期间要对比两套数据源返回的结果,看有没有数量不一致、状态不一致的问题。我在实践中的经验是:不要只对比最终数据,要对比操作日志。比如教师在旧库新建了一条任务,新库如果晚了几秒才同步出来,会造成短时间“灰度的教师看到了数据,未灰度的看不到”,这种问题只有通过日志对比才能发现。

3.3 校验工具:迁移后怎么确认数据没丢

切换之前,必须要做全量校验。否则上线以后发现某道题的成绩丢了,业务事故就不是小问题了。

常用的校验方式:

  • 表行数对比。两套库中同一张表的总行数是否一致,这是最基础的一层。
  • 关键字段汇总对比。比如总分 SUM、最大 ID、最小 ID、计数,几个聚合值一对比就能发现明显差异。
  • 抽样明细对比。对 ID 取模,抽样一部分详细比对所有字段。
  • 变更时间窗增量对比。迁移期间同步任务处理的最后一条 binlog 位置,和业务侧实际产生的最新数据位置要进行对齐。

校验工具不要放在业务服务内部,应该独立部署。因为校验逻辑复杂,如果写进业务服务里,容易影响线上链路,也容易互相干扰。

还有一个细节:校验不是跑一次就完了。大考阅卷迁移期间会有大量新数据写入,建议设置周期任务,比如每 10 分钟跑一次增量校验,直到切换窗口结束。

3.4 回滚预案:每个阶段都要能退回去

很多团队做完切换,没有准备回滚方案,结果上线后发现问题只能硬着头皮修。这在阅卷场景是不能接受的。

平滑演进的回滚预案要包含三层:

  • 代码层:应用服务保留切换开关,发现新库有问题,直接把流量切回旧库。
  • 数据层:双写期间不能停掉写旧库的任务,保留旧库作为权威数据源。
  • 同步层:如果新库已经承接了部分写流量,回滚时要先把新库新增的数据同步回旧库,再切流量。

最稳的回滚就是:从头到尾都保留“旧库是主数据源”的状态,新库一直是备胎。等到大考过去,系统稳定运行一段时间,才正式把旧库降级为归档库,或者直接下线。

4. 高并发阅卷场景的关键参数和架构配置参考

架构演进落地时,具体参数怎么配,是开发同学最常问的问题。这里给的是通用配置思路,实际要以你的环境和压测为准。不要凭空调参数,也不要照搬别人的最优值。

4.1 连接数、线程池、超时时间:先从参数上找瓶颈

数据库连接池是最容易产生问题的点。以常见的连接池配置为例:

参数建议思路说明
maximum-pool-size不要超过数据库 max_connections 的 70%要给运维、监控、批量任务留连接
minimum-idle建议等于 maximum-pool-size 的一部分大考场景峰值起来速度太快,空闲连接太少会突刺
connection-timeout建议 1000ms 到 3000ms超过 3 秒还没拿到连接,应用层基本已经超时
max-lifetime建议小于数据库 wait_timeout避免被数据库主动断开后继续使用
validation-timeout建议几百毫秒校验连接不要占用太长时间

线程池大小也不能盲目调大。线程太多,数据库连接被占满,CPU 上下文切换反而变高。一般建议线程池核心线程数从 20 到 50 起步,通过压测慢慢调整。

超时时间也要分层。应用层到数据库的连接超时、SQL 执行超时、前端接口响应超时,要设计成“数据库超时 < 应用层超时 < 用户等待超时”。如果反过来,数据库已经超时了,应用层还在等,前端已经报错,用户那边看到的还是“加载中”,这时候错误信息对排查很不友好。

4.2 分库分表键:为什么按考生 ID 分片更稳

分库分表的核心是分片键。选错分片键,后面的查询和事务都会很难搞。

阅卷场景的分片键,我强烈建议按考生 ID 或者任务 ID。按时间分片只在归档场景有用,在高并发实时读写场景基本不推荐。原因我之前说过:流量会集中在当天时间的分片上,其他分片空转。

按考生 ID 分片还有一个优势:同一个考生的全部数据被天然聚合到同一个分片。查“某考生的答卷、分数、仲裁记录”时,不需要跨库查询,一条路由就完成了。

分片数量怎么定?不要一次性分 1024 个表,容易管理不过来。一般建议先按未来 2 年的数据容量预估分片数,比如每个分片控制在 500 万行以内,再根据当前数据量倒推分片数量。分片数最好是 2 的幂,方便路由算法取模。

分片路由要固定,不能频繁变更。如果你一开始按考生 ID 取模分成 16 个库,后来发现不够要扩到 32 个库,旧数据的路由就全部失效。解决方式一般有两种:

  • 使用一致性哈希,减少扩容时需迁移的数据量。
  • 在路由层维护分片映射表,但要增加一次查询开销。

我的建议是:初期设计时多留一点余量,宁可前 6 个月分片数稍多,也不要 3 个月后就扩容。分片扩容在数据量大的情况下会非常消耗时间。

4.3 缓存策略:哪些数据适合缓存,哪些绝不能缓存

缓存不是为了“快”而加,而是为了给数据库减负。阅卷场景中,读多写少的稳定数据是最适合缓存的。

我一般会把阅卷系统的缓存分成三类:

  • 热点基础数据:考试配置、科目信息、题目列表,可以缓存 30 分钟到 24 小时,变化极少。
  • 轻动态数据:教师阅卷数、已完成任务数,这种数据允许延迟 1 到 5 秒,可以缓存,但要有失效机制。
  • 强一致数据:已提交的分数、仲裁结果、异常卷状态,绝对不能只读缓存。

有的团队喜欢把教师端首页的任务统计结果直接缓存 10 分钟,结果教师提交分数后,首页还显示“未批阅”,造成误解。更稳妥的方式是:提交分数后,让统计缓存立刻失效,或者使用较短过期时间,比如 30 秒。

缓存雪崩也要提前预防。大考阅卷高峰期,如果所有缓存同时过期,大量请求会同时穿透到数据库,很危险。建议将过期时间设置成基础值加随机偏移量,避免同一秒钟集中失效。

5. 大考高峰期的稳定性保障流程

架构演进完成之后,真正决定成败的是大考当天的稳定性保障。不管平时压测多漂亮,考试高峰期还是会暴露各种意外。

5.1 大考前三天该做的检查清单

大考前三天不是新功能的开发窗口,而是检查窗口。我会按下面的清单逐项确认:

  • 数据库连接池最大值、活跃连接数、慢 SQL 阈值是否调整到位。
  • 主从延迟监控是否正常,告警阈值是否合理。
  • 消息队列积压量、消费速度、失败重试队列是否有人盯。
  • 分库分表路由规则是否经过一次线上灰度验证。
  • 缓存预热脚本是否准备好,大考开始前手动跑一次。
  • 回滚开关是否在配置中心可见,操作人员是否知道怎么用。
  • 磁盘空间、慢查询日志空间、binlog 保留时间是否足够。

不要在大考前做大的结构变更。如果需要调整索引,提前 2 周做,并且观察一段时间。大考前三天只做参数调整和容量确认。

5.2 高峰期监控看什么指标

大考当天,我建议把监控面板分成四个区域:

  • 数据库层:活跃连接数、CPU、磁盘 IO、内存、主从延迟、慢 SQL 数量。
  • 应用层:QPS、平均响应时间、P99 延迟、线程池活跃线程数、连接池等待时间。
  • 中间件层:消息队列积压数量、消费失败数、缓存命中率、缓存穿透数。
  • 业务层:阅卷任务提交成功率、任务状态流转是否正确、有无异常卷卡住。

关键不是监控的指标多,而是告警要能定位问题。比如“教师端打开任务列表慢”,你要能从上到下看到:接口调用到了哪个服务,服务查了哪个表,走了缓存还是数据库,数据库响应时间是多少。如果缺乏链路追踪,高峰期排查会非常被动。

5.3 出问题时的排查链路:从现象到恢复

阅卷高峰期如果出了问题,先不要急着改代码,按顺序排查:

  1. 先看现象是什么:页面打不开、接口超时、提交失败、数据不一致,还是批量任务堆积。
  2. 再看资源是否打满:数据库连接数、CPU、内存、磁盘 IO、带宽。
  3. 再看慢 SQL:有没有出现平时没见过的 SQL,或者某条 SQL 的执行计划突然变了。
  4. 再看中间件:队列积压是否在上升,缓存命中率是否大幅下降。
  5. 最后看反复出现的错误日志:比如死锁、连接超时、主键冲突。

我遇到过一类典型问题:切换分库分表后,某个查询条件没有带分片键,导致中间件广播到所有分片执行,数据库 CPU 瞬间飙升。表面上看是数据库性能不行,实际是应用层路由设计问题。这种问题如果只盯数据库负载,很难定位,必须回到应用日志看 SQL 是否走了全分片扫描。

恢复的顺序和排查相反:先恢复业务。如果只是一个查询接口有问题,可以先加缓存兜底;如果涉及写入,评估是否要打开回滚开关;如果数据库短时间扛不住,可以降级掉非核心接口,优先保障阅卷提交和分数保存。

6. 演进过程中的典型踩坑和复盘

最后说几个我在类似演练和复盘里反复看到的坑。这些坑不是从网上抄的,是我认为任何做数据库架构演进的团队都可能遇到的真实场景。

6.1 典型问题一:迁移期间数据不一致

迁移期间最常见的问题就是新旧库数据对不上。表面原因通常是同步任务延迟或丢消息,实际原因往往更复杂:

  • binlog 解析程序崩溃后,没有可靠的重置位点,导致漏掉一段时间的数据。
  • 新库有自增主键,旧库也有自增主键,双写时两边生成的主键不一致,导致后续关联数据全对不上。
  • 业务代码里有“先删后插”的逻辑,同步任务只监听了 update 和 insert,没有处理 delete,结果两边数据差异越来越大。

解决办法是:迁移期间不要相信同步任务“应该没问题”,必须每隔一段时间就做一次全量校验。校验发现差异,优先以旧库为准回放数据,不要直接手改。

6.2 典型问题二:切换后性能反而下降

有的团队迁移完数据、切换完流量,发现新库性能反而还不如旧库。原因通常不是数据库变差了,而是查询方式变了。

单库的时候,你可以随意 JOIN,可以在任何字段上加索引。分库分表之后,很多 JOIN 失效了,跨分片查询会在中间层聚合,性能自然下降。如果新的查询直接查询没有分片键的表,中间件会广播到所有分片,然后合并结果,这种写法在大表上非常伤。

解决思路是:

  • 把关联查询改成两次查询,在应用层做数据拼接。
  • 对常用查询提前构建宽表或汇总表。
  • 严格约束业务查询必须带上分片键,不允许全分片扫描。

6.3 典型问题三:监控看起来正常,业务却超时

这个坑最折磨人。数据库各项指标看起来都正常,连接数没有满,CPU 也不高,但是业务接口就是大量超时。

后来排查发现,问题往往出在应用层线程池。比如某个线程池的核心线程数太小,队列满了以后,新的任务直接进入拒绝策略,看起来是接口超时,实际是线程池拒绝。数据库没问题,应用层先撑不住了。

所以大考阅卷这种高并发场景,监控不能只看数据库。一定要把应用层的线程池活跃数、队列大小、拒绝次数一起监控起来。如果出现大量拒绝或排队,数据库再稳也没有用。

6.4 给类似业务团队的几个建议

如果你正在做类似的数据库架构演进,我有几个个人建议:

第一,不要在高峰期前一个月才启动大改造。数据库架构演进至少要预留一个完整的考试周期做验证。上一场模拟考试切换到新架构,跑完没问题,下一场大考再用,这是最稳妥的节奏。

第二,每一步演进都要有明确的触发条件。单库单表先跑,当慢查询和连接数开始成为瓶颈,再上读写分离;读写分离后,写热点明显了,再分库分表。每一步都不提前,也不拖延。

第三,架构切换要有人专门负责。不要“部署完就完了”。切换窗口内,必须有专人盯监控,盯消息队列,盯数据校验结果。出了小问题随时切回去,不要等到业务受损才处理。

数据库架构演进这件事,真正考验人的不是写出多牛的分库分表代码,而是整个切换过程是否可控、可回滚、可验证。把双写、灰度、校验、回滚这几件事做扎实,即使大考阅卷流量再猛,系统也能稳稳接住。

http://www.cnnetsun.cn/news/4239934.html

相关文章:

  • macOS 开源 Spotlight 替代方案:原生快速文件搜索工具实践指南
  • Python实战:从零构建学生管理系统,掌握CRUD与数据持久化
  • Qwen3.8实战:从API接入到本地部署与推理加速
  • 北岳恒山与悬空寺:绝壁之上的道化山河
  • MATLAB数学建模实战:从数据预处理到算法优化的核心技巧
  • NOIP2008 ISBN校验题精讲:从规则落地到工程化思维
  • AI生物技术情报简报实战:用LLM分析EGFR耐药文献全流程
  • 大模型本质是上下文预测引擎:AI应用开发与部署实践
  • Ansible控制节点配置与云服务自动化实战指南
  • 172张工业车间人员检测数据集:YOLOv8微调与部署实战
  • 数模竞赛多元线性回归实战:从数据诊断到模型检验全流程解析
  • 动态规划去重技巧:从蓝桥杯真题解析本质不同上升子序列计数
  • 半导体制冷杯DIY全解析:TEC选型、散热设计与PID温控实战
  • 保姆级教程:茉莉花 Zotero 插件 30 分钟搞定知网元数据抓取与 PDF 大纲
  • 网盘下载速度慢到 KB 级?这款免费油猴脚本本地解析直链,9 大网盘通吃,四步十分钟上手
  • Mac版Navicat试用到期怎么办?免费脚本快速重置恢复14天
  • 玻璃脏污目标检测数据集:工业视觉质检实战指南
  • 电力高空作业安全带检测数据集:VOC/YOLO双格式与YOLOv8实战
  • Coze记忆功能全解析:让智能体真正记住用户
  • 微盘源码K线修复与余额宝会员等级系统部署全攻略
  • Grok无字幕看懂数学视频?拆解多模态与推理融合的技术链路
  • 架构与设计演化:大型系统不停机现代化改造路径
  • 中医药知识图谱问答系统项目实战:Neo4j建模与Python问答实现
  • MATLAB仿真报童问题:从理论到实战的库存优化指南
  • 坑洼检测不是图像分类:道路语义理解与轻量化部署实战
  • 数模竞赛相关性分析实战:MATLAB与SPSS核心操作与结果解读
  • YOLOv8遥感小目标检测实战:NWPU VHR-10与DOTA数据集改进与训练全解析
  • ROS 2四足机器人单腿逆运动学实战:从关节坐标到运动控制
  • AI模型罗盘:从ReAct到Agent的工程化选型与评测方法
  • 基于DETR的智能冰箱物品识别:训练、部署与zip解压避坑全攻略