尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

大考阅卷高并发下数据库架构平滑演进实践 先给结论大考阅卷这类业务真正的难点不是“数据库性能不够”而是“流量来得又猛又集中数据库架构却不能在关键节点随便重构”。如果你们的系统也面临周期性高并发比如考试阅卷、报名抢课、成绩查询、活动秒杀那么数据库架构的平滑演进方案比单纯调高配置更重要。这篇文章以学科网大考阅卷场景为背景拆解从单库单表到读写分离、分库分表、队列异步化的一步步实践重点说清楚怎么切换才不出事故怎么回滚才算安全以及高峰期到底该盯哪些指标。适合谁看负责线上业务后端或数据库运维的工程师准备做架构升级但担心稳定性的人还有那些“系统平时很稳一到考试就报警”的团队。这篇文章不提供万能架构但会给一套可以照着检查、照着验证的方法。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 出问题时的排查链路从现象到恢复阅卷高峰期如果出了问题先不要急着改代码按顺序排查先看现象是什么页面打不开、接口超时、提交失败、数据不一致还是批量任务堆积。再看资源是否打满数据库连接数、CPU、内存、磁盘 IO、带宽。再看慢 SQL有没有出现平时没见过的 SQL或者某条 SQL 的执行计划突然变了。再看中间件队列积压是否在上升缓存命中率是否大幅下降。最后看反复出现的错误日志比如死锁、连接超时、主键冲突。我遇到过一类典型问题切换分库分表后某个查询条件没有带分片键导致中间件广播到所有分片执行数据库 CPU 瞬间飙升。表面上看是数据库性能不行实际是应用层路由设计问题。这种问题如果只盯数据库负载很难定位必须回到应用日志看 SQL 是否走了全分片扫描。恢复的顺序和排查相反先恢复业务。如果只是一个查询接口有问题可以先加缓存兜底如果涉及写入评估是否要打开回滚开关如果数据库短时间扛不住可以降级掉非核心接口优先保障阅卷提交和分数保存。6. 演进过程中的典型踩坑和复盘最后说几个我在类似演练和复盘里反复看到的坑。这些坑不是从网上抄的是我认为任何做数据库架构演进的团队都可能遇到的真实场景。6.1 典型问题一迁移期间数据不一致迁移期间最常见的问题就是新旧库数据对不上。表面原因通常是同步任务延迟或丢消息实际原因往往更复杂binlog 解析程序崩溃后没有可靠的重置位点导致漏掉一段时间的数据。新库有自增主键旧库也有自增主键双写时两边生成的主键不一致导致后续关联数据全对不上。业务代码里有“先删后插”的逻辑同步任务只监听了 update 和 insert没有处理 delete结果两边数据差异越来越大。解决办法是迁移期间不要相信同步任务“应该没问题”必须每隔一段时间就做一次全量校验。校验发现差异优先以旧库为准回放数据不要直接手改。6.2 典型问题二切换后性能反而下降有的团队迁移完数据、切换完流量发现新库性能反而还不如旧库。原因通常不是数据库变差了而是查询方式变了。单库的时候你可以随意 JOIN可以在任何字段上加索引。分库分表之后很多 JOIN 失效了跨分片查询会在中间层聚合性能自然下降。如果新的查询直接查询没有分片键的表中间件会广播到所有分片然后合并结果这种写法在大表上非常伤。解决思路是把关联查询改成两次查询在应用层做数据拼接。对常用查询提前构建宽表或汇总表。严格约束业务查询必须带上分片键不允许全分片扫描。6.3 典型问题三监控看起来正常业务却超时这个坑最折磨人。数据库各项指标看起来都正常连接数没有满CPU 也不高但是业务接口就是大量超时。后来排查发现问题往往出在应用层线程池。比如某个线程池的核心线程数太小队列满了以后新的任务直接进入拒绝策略看起来是接口超时实际是线程池拒绝。数据库没问题应用层先撑不住了。所以大考阅卷这种高并发场景监控不能只看数据库。一定要把应用层的线程池活跃数、队列大小、拒绝次数一起监控起来。如果出现大量拒绝或排队数据库再稳也没有用。6.4 给类似业务团队的几个建议如果你正在做类似的数据库架构演进我有几个个人建议第一不要在高峰期前一个月才启动大改造。数据库架构演进至少要预留一个完整的考试周期做验证。上一场模拟考试切换到新架构跑完没问题下一场大考再用这是最稳妥的节奏。第二每一步演进都要有明确的触发条件。单库单表先跑当慢查询和连接数开始成为瓶颈再上读写分离读写分离后写热点明显了再分库分表。每一步都不提前也不拖延。第三架构切换要有人专门负责。不要“部署完就完了”。切换窗口内必须有专人盯监控盯消息队列盯数据校验结果。出了小问题随时切回去不要等到业务受损才处理。数据库架构演进这件事真正考验人的不是写出多牛的分库分表代码而是整个切换过程是否可控、可回滚、可验证。把双写、灰度、校验、回滚这几件事做扎实即使大考阅卷流量再猛系统也能稳稳接住。
返回列表