招新平台是如何解决高并发判题问题的?

在设计一个招新平台答题系统时,有一个典型需求:

同一道题提交人数越多,得分越低(衰减计分)。同时要保证高并发下的分数正确性。

一、业务场景 & 技术挑战

典型需求 包括:

  1. 用户高并发提交题解
  2. 只有 首次正确提交 才计入计数
  3. 答题人数越多,分数越低(例如对数衰减)
  4. 分数必须计算准确,不能出现并发错误
  5. 衰减后的分数必须可靠写入数据库
  6. 系统需要具备足够的高并发吞吐能力

二、整体实现思路

用户提交答案
    ↓
后端校验答案是否正确
    ↓
Redis Lua(原子):
    SETNX 判断是否首次正确提交
    INCR 计数
	返回计数
    ↓
Java 计算衰减得分
    ↓
MySQL 落库:
    - 用户得分(幂等写)
    - 题目统计(乐观锁 + 重试)

三、Redis的Lua脚本实现计数原子性

-- KEYS[1] = 用户是否答对标记 key
-- KEYS[2] = 题目提交人数计数 key

-- 防重复:首次答对才能进入计分逻辑
local firstSolved = redis.call('SETNX', KEYS[1], 1)
if firstSolved == 0 then
    return -1  -- -1 表示已提交过,不再得分
end

-- 原子计数
local count = redis.call('INCR', KEYS[2])

return count

这段代码执行了两个逻辑:

  1. 构造题目-用户key ,如果该key存在说明用户之前以及答对该题
  2. 如果是新用户答对,更新计数

四、Java 端:幂等写用户得分

INSERT INTO solve_record (user_id, question_id, degree)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE degree = VALUES(degree);

无则插入,有则修改

五、Java 端:更新题目(乐观锁 + 重试)

先用分数衰减算法计算出当前得分(略)
题目更新采用版本号法 实现:

UPDATE issue
SET solved_count = solved_count + 1,
    version = version + 1
    current_score = score
WHERE question_id = ?
  AND version = ?;

如果版本不一致,说明出现并发冲突 ,需要重试:

public void updateQuestionSolvedCount(Long questionId) {
    int maxRetry = 1; // 重试 1 次

    for (int i = 0; i <= maxRetry; i++) {

        // 1. 查询当前版本
        Issue issue = IssueMapper.selectById(questionId);
        int version = stat.getVersion();

        // 2. 尝试更新
        int rows = questionStatMapper.updateSolvedCount(questionId, version);

        if (rows == 1) {
            return;  // 成功
        }

        // 3. 冲突 → 重试
        if (i == maxRetry) {
            throw new RuntimeException("Version conflict, retry failed");
        }

        // 休息一下避免忙等
        try { Thread.sleep(10); } catch (Exception ignore) {}
    }
}