知识竞赛系统面试高频考点:幂等设计、实时榜单与高并发防作弊

发布时间:2026/10/24 18:49:50
知识竞赛系统面试高频考点:幂等设计、实时榜单与高并发防作弊

简介:这是一套面向知识竞赛组织者与开发者的在线答题系统源码资源,基于VC6.0与MFC框架开发,配套Access数据库,适合需要搭建竞赛平台或进行课程设计、二次开发的技术人员参考。压缩包共61个文件,约7.68MB,以cpp与h源文件为核心,另有obj、pdb、ilk等编译中间文件,以及mdf、ldf数据库文件、bmp与ico界面素材和dsp、dsw工程文件,结构完整可直接编译运行。系统覆盖试题的添加、删除与修改,参赛人员信息录入,在线答题计时与自动评分,并支持按答对数量、答题速度等维度生成动态排名,同时包含通知消息、数据统计与安全隐私等模块。已有395人学习下载,读者可从中获取完整的竞赛系统实现方案、数据库脚本与界面设计思路,便于理解MFC与ADO数据库访问的具体用法,快速完成系统维护、功能扩展或二次开发。

1. 知识竞赛系统面试:从答题到排名,考官到底在盯什么

很多人以为知识竞赛系统的面试就是问“怎么存题目”“怎么算分数”,真坐到对面才发现,考官根本不关心你能不能把题库表建出来。他们盯的是三件事:答题过程怎么保证不崩、排名怎么保证不飘、高并发下怎么保证不丢分。这三件事对应的是消息队列、缓存一致性、分布式锁、幂等设计这些硬考点,而不是增删改查。一份能拿高分的备考笔记,必须先把考点分布摸清楚:答题链路占四成,排名与榜单占三成,防作弊与幂等占两成,剩下的才是基础数据模型。适合已经写过项目、但一被追问“为什么”就卡壳的人。下面按模块拆开讲,每道题都给答题思路,照着练就能用。

2. 答题链路高频考点:从提交到判分,每一步都可能被追问

2.1 答题提交接口的幂等设计

面试官最爱问的第一道题:用户手抖点了两次提交,或者网络重试导致同一份答案发了两遍,你怎么保证不重复计分?这题考的是幂等,不是“加个锁”就完事。

答题思路分三层说。第一层,客户端生成一个全局唯一的提交令牌,随答案一起发到服务端,服务端用 Redis 的SET key value NX EX做占位,占位成功才继续处理,失败直接返回上一次的结果。第二层,数据库层面对(user_id, question_id, exam_id)建唯一索引,作为最后一道防线,防止缓存穿透后重复写入。第三层,如果判分是异步的,消息消费端也要用同一个令牌做去重,保证“至少一次投递”不会变成“至少一次计分”。

import redis import hashlib r = redis.Redis() def submit_answer(user_id, exam_id, question_id, answer, token): # 用提交令牌做幂等键,NX 保证只有第一次能占位成功 idem_key = f"idem:{exam_id}:{user_id}:{token}" # EX 设置过期时间,防止死键堆积 ok = r.set(idem_key, "1", nx=True, ex=300) if not ok: # 已经处理过,直接返回缓存里的结果 return r.get(f"result:{idem_key}") # 真正落库判分 score = judge(question_id, answer) r.set(f"result:{idem_key}", score, ex=300) return score

这段代码的关键在nx=True和ex=300。nx保证原子性占位,ex防止用户一直不重试导致键永久占用。注意判分逻辑要放在占位成功之后,否则占位失败也会白算一次。面试时如果只答“用 Redis 做幂等”,考官会追问“Redis 挂了怎么办”,这时候要补数据库唯一索引兜底,才算答完整。

2.2 判分服务的同步与异步选择

第二道高频题:判分是同步返回还是异步返回?这题没有标准答案,但答错方向直接暴露你没想过流量。

答题思路:客观题、题量小、要求即时出分,走同步,接口里直接算,响应控制在 200ms 内。主观题、题量大、或者有复杂规则(比如多选漏选给一半分),走异步,提交后返回一个task_id,客户端轮询或走长连接拿结果。异步方案要讲清楚消息队列选型、消费失败重试、以及结果回写后怎么通知前端。

面试官最想听的是你对“用户等多久会烦躁”的判断。一般超过 1.5 秒的等待,用户就会怀疑系统卡了。所以同步判分只做轻量计算,重逻辑一律异步。如果考官追问“异步怎么保证用户看到分数”,答:提交后前端展示“判分中”,用task_id每 500ms 轮询一次,最多轮询 10 次,超时提示“稍后在成绩页查看”。这样既不让用户干等,也不给服务端压力。

2.3 答题过程中的断线续答

第三道题很实战:用户答到第 8 题突然断网,重新进来怎么恢复进度?这题考的是状态存储和恢复策略。

答题思路:每答一题,客户端本地先存一份,同时异步上报服务端。服务端用user_id + exam_id作为 key,存一个答题进度对象,包含已答题号、已选答案、剩余时间。恢复时优先读服务端,服务端没有再用本地。注意剩余时间不能只靠客户端算,否则改本地时间就能作弊,服务端要记录开考时间戳,恢复时用服务端时间重新计算剩余秒数。

提示:进度存储别用关系库一行行更新,用 Redis Hash 存整个进度对象,一次读写搞定,还能顺带设置和考试时长一致的过期时间。

3. 排名与榜单高频考点:实时榜、最终榜、并列名次怎么答

3.1 实时排名的数据结构选型

排名题第一问:实时榜单怎么实现?很多人张口就是“数据库 order by”,考官一听就知道没做过高并发。

答题思路:用 Redis 的有序集合 ZSET。ZADD更新分数,ZREVRANGE取前 N 名,ZREVRANK查某人排名,全是 O(log N)。分数相同时按提交时间早的排前面,可以把分数和时间戳拼成一个复合分值,比如score * 10^10 - timestamp,这样分数高的自然靠前,同分时时间早的靠前。

# 更新用户分数,exam:1001:rank 是榜单键 ZADD exam:1001:rank 950 user:2001 # 取前 10 名,WITHSCORES 带分数 ZREVRANGE exam:1001:rank 0 9 WITHSCORES # 查 user:2001 的排名,从 0 开始算 ZREVRANK exam:1001:rank user:2001

参数说明:ZADD的分数是 double,注意精度,分数差距小于 1e-10 会出问题,所以分数最好用整数。ZREVRANGE的起止索引是闭区间,取前 10 是0 9。如果榜单要分页,用ZREVRANGE key start stop配合页码算偏移。面试时如果被问“ZSET 底层是什么”,答跳表加哈希表,跳表保证范围查询快,哈希表保证 O(1) 查分数。

3.2 最终榜单与实时榜单的差异处理

第二问:考试结束后,最终榜单怎么定?实时榜和最终榜能一样吗?

答题思路:不能一样。实时榜追求快,允许短暂不一致;最终榜追求准,必须等所有判分完成、所有异步任务落库后再生成。做法是考试结束后发一个“结算”消息,消费端把所有答题记录聚合,算出最终分数,再写入一个独立的最终榜单键。最终榜单生成后加个标记位,前端读榜时先看标记位,有最终榜就读最终榜,没有才读实时榜。

面试官想听的是你对“结算时机”的把控。如果判分有异步任务,结算必须等所有任务完成,可以用一个计数器记录待完成任务数,归零时触发结算。别小看这个细节,很多考生答“考试结束就生成最终榜”,考官会追问“异步判分还没完怎么办”,这时候就露馅了。

3.3 并列名次的处理逻辑

第三问:两个人分数一样,排名怎么算?这题考的是业务理解和细节。

答题思路:先明确业务规则。常见两种:一种是并列,都算第 5 名,下一个是第 7 名;另一种是打破并列,按提交时间早的排前面。面试时先反问“业务上要求并列还是打破”,再给方案。如果并列,ZSET 里分数相同,ZREVRANK返回的排名是任意的,需要额外查同分人数来修正。如果打破并列,就用前面说的复合分值。

注意:并列名次在发奖场景下很敏感,第 5 名并列两个,奖品怎么发?面试时主动提这个业务点,考官会觉得你考虑得全。

4. 防作弊与高并发易错点:这些题一错就丢分

4.1 易错点一:用数据库自增做主键还沾沾自喜

错在哪:答题记录表用自增 ID,高并发下自增锁竞争严重,插入变慢。为什么错:知识竞赛的答题写入是高频操作,自增主键在并发插入时会产生锁等待,QPS 上不去。怎么答对:用雪花算法生成分布式 ID,或者用user_id + exam_id + question_id做联合主键,避免单点自增。面试时如果被问“为什么不用自增”,把锁竞争和分库分表扩展性两点讲清楚。

4.2 易错点二:排行榜用数据库 order by 还觉得没问题

错在哪:每次查榜都select * from score order by score desc limit 10,数据量一大就慢查询。为什么错:排序无法命中索引时走全表扫描,几千人同时看榜直接拖垮数据库。怎么答对:用 Redis ZSET 扛读,数据库只做持久化,榜单读请求全部走缓存。如果考官追问“缓存和数据库不一致怎么办”,答:写分数时先写数据库,再更新 ZSET,用消息队列保证最终一致,读榜只读 ZSET。

4.3 易错点三:判分逻辑写在接口里还觉得挺清晰

错在哪:判分规则和接口耦合,改一个规则要动接口代码,回归测试全量跑。为什么错:知识竞赛的判分规则经常变,比如多选从“全对得分”改成“漏选得一半”,硬编码在接口里就是灾难。怎么答对:把判分规则抽成策略模式,每种题型一个策略类,接口只负责调度。面试时补一句“规则配置化,运营后台可改”,考官会高看一眼。

4.4 易错点四:忽略答题时间的服务端校验

错在哪:剩余时间只在前端倒计时,服务端不校验,用户改本地时间就能多答题。为什么错:这是安全漏洞,不是功能问题。怎么答对:服务端记录开考时间戳和考试时长,每次提交答案时校验当前时间 - 开考时间 <= 考试时长,超时直接拒绝。如果考官问“网络延迟导致提交晚了一秒怎么办”,答:允许一个小的宽限期,比如 3 秒,宽限期内提交仍有效,但记录异常供风控排查。

4.5 易错点五:消息队列消费失败直接丢弃

错在哪:异步判分消息消费失败没有重试,用户分数丢了。为什么错:消息队列的“至少一次”语义要求消费端必须幂等且可重试,直接丢弃等于数据丢失。怎么答对:消费失败进死信队列,人工介入或定时重试,重试时用幂等键去重。面试时如果被问“重试多少次”,答:一般 3 次,间隔递增,超过进死信队列并告警。

5. 答题技巧与押题思路:一套能直接套用的答题框架

面试知识竞赛系统,最怕的是东一榔头西一棒子。我后来总结了一个四步框架,每次遇到系统设计题都强制走一遍:先问业务场景,再定数据模型,然后拆读写链路,最后补异常和边界。这套框架能覆盖八成以上的追问。

第一步,问业务场景。别急着答技术方案,先确认考试规模、并发量、是否实时出分、有没有防作弊要求。比如“这场考试是 100 人还是 10 万人”,直接决定你用不用消息队列和分布式缓存。面试官抛出题目后,你反问一句“大概什么量级”,他会觉得你靠谱。

第二步,定数据模型。题库表、答题记录表、榜单表,三张核心表先画出来。答题记录表用联合唯一索引防重,榜单表可以不存在数据库里,直接 Redis ZSET。这一步要讲清楚每张表的读写比例,题库读多写少,答题记录写多读少,榜单读多写少。

第三步,拆读写链路。写链路:提交答案 → 幂等校验 → 判分 → 写记录 → 更新榜单。读链路:查题目 → 查进度 → 查榜单。每条链路标出瓶颈点,写链路的瓶颈在判分和榜单更新,读链路的瓶颈在榜单查询。瓶颈点就是你要加缓存、加队列的地方。

第四步,补异常和边界。断线怎么办、重复提交怎么办、判分失败怎么办、榜单并列怎么办、缓存挂了怎么办。每个异常给一个兜底方案,不用多复杂,但要有。

押题方向上,优先准备三道题:设计一个支持 10 万人同时答题的知识竞赛系统;实时榜单怎么实现,同分怎么处理;答题提交的幂等和防作弊怎么做。这三道题练熟,基本覆盖核心考点。另外准备一个自己项目里的翻车经历,比如“当时榜单用数据库排序,压测直接崩了,后来换 ZSET 才扛住”,面试官对血泪经验比对标准答案更感兴趣。

提示:答题时多用“我会先…再…最后…”的句式,让考官跟上你的思路。别一上来就堆技术名词,先讲清楚为什么选这个方案,再讲怎么实现。

从那以后我每次准备面试,都强制自己把每个系统设计题按这四步写一遍,写完再对着镜子讲一遍,讲不顺的地方就是没想透的地方。希望帮到你。

本文还有配套的精品资源,点击获取