考试监控大屏为什么不能直接查数据库?WebSocket、Redis与实时数据聚合架构设计

在企业在线考试系统中,监控大屏看起来似乎只是一个"数据展示页面"。

应考5000人、已登录4826人、考试中4712人、已交卷86人、掉线9人、异常19人,再配上参考率、交卷趋势、部门进度和异常考生列表,一块大屏基本就完成了。

于是很多项目第一版都会采用一种最直接的实现方式:

复制代码
浏览器大屏
    ↓
每2秒请求一次接口
    ↓
后台查询数据库
    ↓
COUNT / GROUP BY
    ↓
返回最新统计结果

数据量小时,这套方案完全可以运行。

但当系统真正进入集团企业、煤矿、能源、电力、高校或者大型职业技能竞赛场景,同时考试人数从几百人增加到3000人、5000人甚至更多以后,这种设计很容易出现一个非常尴尬的问题:

监控大屏本来是用来观察考试是否稳定的,结果大屏自己反而成了考试数据库的压力来源。

更加严重的是,正式考试期间数据库本身正在不断处理答案自动保存、考试心跳、交卷、日志、异常记录等业务。如果监控页面再不断执行COUNT、GROUP BY、JOIN和排序统计,就可能让"看考试的人"与"正在考试的人"争抢同一套数据库资源。

所以大型考试系统的实时监控不能简单理解成"前端不断查数据库"。

更加合理的思路应该是:

复制代码
业务事件
   ↓
实时状态聚合
   ↓
Redis实时视图
   ↓
WebSocket推送
   ↓
监控大屏

本文结合大型在线考试业务,并以宏远培训考试系统这类面向企业集中考试、监考中心和数字化大屏的应用场景为例,分析考试实时监控背后的 WebSocket、Redis、状态聚合、数据一致性和异常恢复设计。

需要说明的是,本文中的 WebSocket、Redis 等属于面向该类业务场景的通用架构设计思路,实际项目可以根据系统版本、部署环境和现有技术体系进行调整。


一、为什么大屏直接查数据库,小规模没问题,大规模容易出问题?

假设一场集团考试有5000名考生。

系统需要展示以下数据:

复制代码
应考人数
已登录人数
考试中人数
已交卷人数
掉线人数
异常人数
参考率
交卷率
各部门考试进度
最近异常考生
实时交卷趋势

如果大屏每2秒刷新一次,并且后台分别执行多条SQL:

复制代码
SELECT COUNT(*)
FROM exam_session
WHERE exam_id = ?;

再查询考试中人数:

复制代码
SELECT COUNT(*)
FROM exam_session
WHERE exam_id = ?
  AND status = 'EXAMING';

再查询已交卷:

复制代码
SELECT COUNT(*)
FROM exam_session
WHERE exam_id = ?
  AND status = 'SUBMITTED';

部门进度可能又是一条:

复制代码
SELECT
    org_id,
    COUNT(*) AS total,
    SUM(CASE WHEN status = 'SUBMITTED' THEN 1 ELSE 0 END) submitted
FROM exam_session
WHERE exam_id = ?
GROUP BY org_id;

异常统计再来一次:

复制代码
SELECT COUNT(*)
FROM exam_abnormal_log
WHERE exam_id = ?
  AND create_time >= ?;

一块大屏可能一次刷新就产生十几条SQL。

如果同时还有:

复制代码
集团总部大屏
二级公司监控页面
各考点监控室
监考员操作台
考试管理后台

多个页面同时轮询,数据库收到的就不再是一块屏幕的请求。

而正式考试过程中,数据库还在执行:

复制代码
答案保存
考试状态更新
心跳记录
切屏日志
人脸核验记录
交卷
成绩写入
监考操作

于是:

复制代码
考试业务写入
      +
监控统计查询
      =
共同竞争数据库资源

这才是问题的核心。


二、大屏最危险的SQL通常不是普通查询,而是聚合查询

查一个考生:

复制代码
SELECT *
FROM exam_session
WHERE session_id = ?;

只要索引合理,成本通常比较可控。

但大屏喜欢做的是:

复制代码
COUNT
SUM
AVG
GROUP BY
ORDER BY
趋势统计
多表JOIN

例如要计算各部门实时参考率:

复制代码
SELECT
    o.org_name,
    COUNT(s.id) AS should_count,
    SUM(
        CASE
            WHEN s.status IN ('EXAMING', 'SUBMITTED')
            THEN 1
            ELSE 0
        END
    ) AS joined_count
FROM exam_session s
LEFT JOIN sys_org o
    ON s.org_id = o.id
WHERE s.exam_id = ?
GROUP BY s.org_id, o.org_name;

这种SQL如果每隔两三秒执行一次,就相当于反复计算一个其实只发生少量变化的数据集合。

例如:

复制代码
上一秒考试中:4680人
这一秒考试中:4682人

实际上只变化了两个人。

但传统方式却重新扫描、聚合5000个人的数据。

这在架构上属于:

为了得到2条变化,重新计算5000条状态。

数据量小时感觉不明显。

规模扩大以后,这种浪费就会逐渐暴露出来。


三、真正需要改变的不是SQL,而是思维方式

传统监控思路是:

复制代码
我现在想知道有多少人考试
        ↓
去数据库重新数一遍

实时系统更合理的思路则是:

复制代码
每当有人状态发生变化
        ↓
实时更新统计结果

例如某个考生:

复制代码
未登录
  ↓
已登录
  ↓
考试中
  ↓
已交卷

系统已经知道:

复制代码
考试中 -1
已交卷 +1

那为什么还要重新:

复制代码
SELECT COUNT(*)

扫描所有考生?

这就是实时数据聚合和传统报表查询最大的区别。

可以把它理解成:

复制代码
传统报表:
数据 → 查询 → 计算结果

实时大屏:
事件 → 更新结果 → 直接展示

前者适合历史统计。

后者更加适合考试监控。


四、一套比较合理的考试监控架构

可以把整体架构设计成:

复制代码
                    5000名考生
                         │
        ┌────────────────┼────────────────┐
        ▼                ▼                ▼
      登录             答题             交卷
        │                │                │
        └────────────────┼────────────────┘
                         ▼
                   考试业务服务
                         │
                         ▼
                    状态变化事件
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
         业务数据库              实时聚合服务
                                    │
                                    ▼
                                  Redis
                                    │
                           实时状态 / Counter
                                    │
                                    ▼
                             WebSocket服务
                                    │
              ┌─────────────────────┼─────────────────────┐
              ▼                     ▼                     ▼
          集团大屏               考点大屏              监考操作台

这里数据库和Redis承担的是两个完全不同的角色。

数据库负责最终业务事实。

例如:

复制代码
张三参加了考试
张三的最终答案是什么
张三什么时候交卷
张三最终成绩是多少

而Redis负责:

现在这一秒钟,大屏应该显示什么。

例如:

复制代码
考试中:4712
已交卷:86
掉线:9
异常:19

这两个概念不能混为一谈。


五、Redis不是数据库替代品,而是实时状态视图

这是实时监控架构中特别需要强调的一点。

有人可能认为:

既然用了Redis,考试数据是不是都放Redis?

并不是。

正式考试的答卷、成绩、考试记录、日志等核心数据最终仍应该可靠持久化。

Redis更加适合维护:

复制代码
实时Counter
在线状态
考试状态
心跳时间
临时异常状态
大屏快照

例如:

复制代码
exam:realtime:20260910001

可以使用Hash:

复制代码
should_count      5000
login_count       4826
examining_count   4712
submitted_count   86
offline_count     9
abnormal_count    19

对应Redis命令:

复制代码
HGETALL exam:realtime:20260910001

此时大屏获取6个核心统计数据,不再需要执行6条:

复制代码
COUNT(*)

而只是读取一个已经计算好的结果。


六、考生状态变化以后,Counter应该如何更新?

假设考生张三当前状态:

复制代码
EXAMING

随后完成交卷:

复制代码
EXAMING
    ↓
SUBMITTED

那么系统可以产生事件:

复制代码
{
    "eventType": "EXAM_STATUS_CHANGED",
    "examId": "20260910001",
    "userId": "10086",
    "oldStatus": "EXAMING",
    "newStatus": "SUBMITTED",
    "timestamp": 1789007412000
}

聚合服务收到事件以后,不需要重新统计5000人。

只需要:

复制代码
examining_count - 1

submitted_count + 1

Redis操作可以类似:

复制代码
HINCRBY exam:realtime:20260910001 examining_count -1

HINCRBY exam:realtime:20260910001 submitted_count 1

这样复杂度从:

复制代码
扫描5000人

变成:

复制代码
更新2个Counter

人数进一步扩大时,这种设计的价值会更加明显。


七、但是直接HINCRBY还有一个大坑:重复事件

假设交卷事件因为网络或者消息重试,被处理了两次。

第一次:

复制代码
examining_count -1
submitted_count +1

第二次又执行:

复制代码
examining_count -1
submitted_count +1

最后大屏人数就错了。

于是可能出现:

复制代码
实际交卷:1000人
大屏显示:1001人

考试继续进行以后,误差还可能越来越大。

因此实时Counter更新必须建立在:

状态转换幂等

基础之上。

不能只看:

复制代码
事件告诉我 +1

而应该先确认:

复制代码
这个人的状态是不是第一次
从EXAMING变成SUBMITTED?

八、比较可靠的办法:保存考生当前状态

例如Redis中同时维护:

复制代码
exam:candidate:status:{examId}

Hash结构:

复制代码
10001 = EXAMING
10002 = SUBMITTED
10003 = EXAMING
10004 = OFFLINE

处理张三事件时:

复制代码
oldState = Redis当前状态
newState = 事件中的状态

如果:

复制代码
oldState == newState

说明重复事件。

直接忽略。

如果:

复制代码
oldState = EXAMING
newState = SUBMITTED

才真正执行Counter变化。

伪代码:

复制代码
public void changeStatus(
        Long examId,
        Long userId,
        ExamStatus newStatus) {

    ExamStatus oldStatus =
            redisRepository.getStatus(examId, userId);

    if (oldStatus == newStatus) {
        return;
    }

    redisRepository.decrease(
            examId,
            oldStatus
    );

    redisRepository.increase(
            examId,
            newStatus
    );

    redisRepository.setStatus(
            examId,
            userId,
            newStatus
    );
}

但这里还有一个问题:

如果两个请求并发执行怎么办?

所以生产环境通常还需要保证这一组状态转换操作的原子性。

可以使用Redis Lua脚本或者其他原子操作机制完成。


九、Redis Lua脚本为什么适合实时状态切换?

例如逻辑可以抽象成:

复制代码
读取旧状态
     ↓
判断是不是重复事件
     ↓
旧状态Counter -1
     ↓
新状态Counter +1
     ↓
更新人员状态

这几步如果分别发送Redis命令,中间可能被其他请求插入。

Lua脚本则可以把整组操作作为一次原子执行。

示意逻辑:

复制代码
local oldStatus =
    redis.call(
        'HGET',
        KEYS[1],
        ARGV[1]
    )

local newStatus = ARGV[2]

if oldStatus == newStatus then
    return 0
end

if oldStatus then
    redis.call(
        'HINCRBY',
        KEYS[2],
        oldStatus,
        -1
    )
end

redis.call(
    'HINCRBY',
    KEYS[2],
    newStatus,
    1
)

redis.call(
    'HSET',
    KEYS[1],
    ARGV[1],
    newStatus
)

return 1

这样才比较接近一个可靠的:

复制代码
Real-Time State Machine

十、考试状态最好设计成状态机,而不是几个Boolean字段

早期系统有时喜欢这样设计:

复制代码
is_login
is_exam
is_submit
is_offline

这种设计很快会出现:

复制代码
is_exam = true
is_submit = true

到底代表什么状态?

更加合理的方式是定义主状态:

复制代码
public enum ExamSessionStatus {

    NOT_LOGIN,

    LOGGED_IN,

    EXAMING,

    SUBMITTED,

    FINISHED
}

其中:

复制代码
掉线
异常
切屏
人脸异常

不一定应该全部变成主状态。

因为一个考生完全可能:

复制代码
当前仍然处于EXAMING

同时存在:
切屏异常
人脸异常

所以更加合理的数据模型是:

复制代码
Main Status
+
Realtime Flags

例如:

复制代码
{
    "status": "EXAMING",
    "online": true,
    "abnormal": true,
    "abnormalTypes": [
        "WINDOW_SWITCH"
    ]
}

这比把所有状态揉成一个字段更加灵活。


十一、掉线人数为什么特别容易统计错?

"掉线"看起来简单,其实并不是一个业务按钮触发的状态。

用户不会告诉服务器:

我现在掉线了。

服务器只能根据:

复制代码
最后心跳时间

判断。

例如客户端每隔一定时间发送:

复制代码
HEARTBEAT

服务器记录:

复制代码
lastHeartbeat = 10:30:16

如果到:

复制代码
10:30:46

仍然没有新心跳,就可以根据配置规则判定:

复制代码
OFFLINE

但这里同样不建议每次心跳都:

复制代码
UPDATE exam_session
SET last_heartbeat = NOW();

如果5000人每5秒一次:

复制代码
5000 ÷ 5
=
1000次心跳/秒

如果这些心跳全部写数据库,反而会制造大量没有必要的数据库写入。

更加适合的是:

复制代码
Heartbeat
   ↓
Redis
   ↓
更新TTL / LastHeartbeat

正式业务数据库只在必要时保存关键状态或者最终异常记录。


十二、WebSocket真正解决的问题是什么?

如果Redis已经保存了实时数据,为什么还需要WebSocket?

因为大屏还需要知道:

数据什么时候发生变化。

最简单方案仍然是:

复制代码
setInterval(() => {
    fetch('/api/exam/realtime')
}, 2000);

这仍然属于Polling。

即使数据放在Redis里,也意味着:

复制代码
没有变化
→ 仍然请求

只有1个人变化
→ 仍然请求全部数据

WebSocket则把模式改变为:

复制代码
客户端问服务器

变成:

复制代码
服务器主动通知客户端

架构变成:

复制代码
Redis状态变化
      ↓
Realtime Aggregator
      ↓
WebSocket
      ↓
Browser

当:

复制代码
submitted_count
86 → 87

服务器主动推送:

复制代码
{
    "type": "EXAM_REALTIME_UPDATE",
    "examId": "20260910001",
    "submitted": 87,
    "examining": 4711
}

前端收到以后直接修改页面。


十三、但是千万不要"一个事件推一次大屏"

这是另一个常见误区。

假设5000名考生正在考试。

每一个考生:

复制代码
登录
开始考试
保存答案
发送心跳
切换窗口
交卷

都会产生事件。

如果每收到一个事件就立即向大屏推送一次,那么WebSocket本身也可能形成大量无意义消息。

尤其是:

复制代码
Heartbeat

完全没有必要每次都刷新大屏。

更加合理的办法是:

事件实时处理,大屏合并推送。

例如内部一分钟可能处理数万个细粒度事件,但WebSocket可以按照:

复制代码
200ms
500ms
1秒

这样的可配置窗口进行聚合。

例如500毫秒内发生:

复制代码
考试中 -12
已交卷 +10
掉线 +2

只推一次:

复制代码
{
    "examining": 4700,
    "submitted": 96,
    "offline": 11
}

而不是连续推送24条消息。

这叫:

复制代码
Event Coalescing

或者可以简单理解为:

业务事件细粒度,UI刷新粗粒度。


十四、大屏第一次打开的时候怎么办?

WebSocket还有一个非常容易被忽略的问题。

假设监考员:

复制代码
10:35

才打开监控大屏。

他之前没有接收到任何WebSocket消息。

难道页面从0开始等事件?

显然不行。

因此实时页面一般需要:

复制代码
Snapshot + Delta

模式。

第一次连接:

复制代码
GET /api/exam/realtime/snapshot

得到:

复制代码
{
    "examId": "20260910001",
    "should": 5000,
    "login": 4890,
    "examining": 4300,
    "submitted": 570,
    "offline": 12,
    "abnormal": 26,
    "version": 18752
}

随后建立WebSocket。

之后只接收增量变化:

复制代码
Snapshot
   ↓
WebSocket Delta
   ↓
WebSocket Delta
   ↓
WebSocket Delta

这样即使监控端中途刷新或者重新打开,也能马上得到完整数据。


十五、WebSocket断线以后,怎么保证大屏没有漏消息?

监控室网络也可能断。

例如:

复制代码
10:30:01
WebSocket断开

10:30:08
重新连接

这7秒钟可能发生:

复制代码
120人交卷
8人掉线
3个异常

如果客户端重新连接以后只是继续接收新消息,那么中间这批状态就丢了。

因此重新连接以后最稳妥的处理通常不是:

复制代码
继续接着猜

而是:

复制代码
重新获取Snapshot

例如:

复制代码
WebSocket断线
      ↓
自动重连
      ↓
重新拉取完整实时快照
      ↓
恢复增量推送

这样大屏最多短暂延迟,不会长期显示错误数据。


十六、多台应用服务器以后,WebSocket还会遇到什么问题?

单机开发环境中:

复制代码
Browser
   ↓
Server A

非常简单。

但正式系统可能有:

复制代码
Server A
Server B
Server C

考生交卷请求可能落到:

复制代码
Server A

而监控大屏WebSocket连接在:

复制代码
Server C

此时Server C怎么知道Server A刚刚发生了状态变化?

这就是多节点实时通信问题。

可以增加一层:

复制代码
Redis Pub/Sub
Redis Stream
MQ
其他事件总线

例如:

复制代码
Server A
   ↓
产生状态事件
   ↓
Realtime Channel
   ↓
Server C
   ↓
WebSocket
   ↓
监控大屏

于是应用服务器不再依赖:

复制代码
我自己有没有处理这个请求

而是订阅整个考试的实时事件。


十七、Redis中的统计数据会不会有一天和数据库不一致?

会。

任何实时缓存系统如果运行足够长时间,都应该考虑:

复制代码
事件丢失
重复事件
程序异常
Redis重启
网络分区
人工修改

因此不能认为:

Redis显示4712人,就永远是真理。

真正的最终事实仍然应该来自:

复制代码
Database

所以成熟一点的架构还需要:

复制代码
Reconciliation

也就是对账。

例如按照配置周期执行:

复制代码
SELECT status, COUNT(*)
FROM exam_session
WHERE exam_id = ?
GROUP BY status;

然后和Redis比较:

复制代码
Database:

EXAMING   4712
SUBMITTED 86


Redis:

EXAMING   4711
SUBMITTED 87

发现不一致以后:

复制代码
报警
+
修正实时视图
+
记录异常日志

注意这里数据库聚合查询并没有被彻底消灭。

区别在于:

以前每2秒查一次。

现在可能只在初始化、恢复、定期校验以及最终统计时查询。

数据库压力完全不是一个量级。


十八、Redis挂掉以后,大屏是不是就彻底没数据了?

不应该。

Redis保存的是:

复制代码
实时视图

而不是唯一业务事实。

所以Redis异常恢复以后,系统应该能够根据数据库重新构建。

例如:

复制代码
Redis异常
   ↓
服务恢复
   ↓
查询exam_session
   ↓
重新计算5000名考生状态
   ↓
重建Realtime Snapshot
   ↓
恢复WebSocket推送

这也是为什么不建议只在Redis保存:

复制代码
考生最终交卷状态

而数据库什么都没有。

正确关系应该是:

复制代码
数据库
=
Source of Truth

Redis
=
Real-Time Projection

可以翻译成:

数据库管"事实",Redis管"现在怎么看事实"。


十九、异常考生列表不能只维护一个数字

大屏可能显示:

复制代码
异常人数:19

但监考员真正需要的是:

复制代码
哪19个人?

点击以后应该看到类似:

考生 单位 当前状态 异常类型 时间
张三 机电部 考试中 多次切屏 10:31:22
李四 安全部 考试中 心跳异常 10:32:05
王五 生产部 考试中 人脸异常 10:32:17

因此Redis不仅可以保存Counter,还可以维护:

复制代码
exam:abnormal:{examId}

其中保存:

复制代码
userId
abnormalType
lastTime
level

大屏点击:

复制代码
异常人数 19

以后直接读取异常集合。

与此同时,最终需要留档的异常记录仍然写入数据库,用于后续考试复核和审计。


二十、监考操作台为什么又比普通大屏复杂一步?

普通大屏主要属于:

复制代码
Read

但监考中心还可能需要:

复制代码
提醒考生
标记异常
强制抓拍
强制交卷
移交复核

这时候数据流就是双向的:

复制代码
                  WebSocket
考试系统 ─────────────────────→ 监考端

考试系统 ←───────────────────── 监考端
                  Command

例如:

复制代码
{
    "command": "FORCE_SUBMIT",
    "examId": "20260910001",
    "userId": "10086"
}

但这里需要特别注意:

WebSocket不能代替权限校验。

监考员发出强制交卷命令以后,后台仍然应该检查:

复制代码
当前操作人是谁
是否拥有监考权限
是否能够管理本场考试
目标考生是否属于授权范围
考试当前是否允许执行该操作

并记录审计日志。

否则实时通信通道反而可能变成越权操作入口。


二十一、以宏远培训考试系统为例,监考大屏真正应该解决什么?

从企业正式考试场景来看,监控大屏的价值并不是"做几个漂亮图表"。

真正的问题是:

一场几千人的考试正在进行,管理人员能不能快速判断考试现在是否正常?

例如宏远培训考试系统的监考中心业务场景中,可以围绕:

复制代码
应考
登录
考试中
已交卷
掉线
异常

形成实时状态视图。

集团总部可以查看整体参考进度,下属单位可以在权限范围内查看本单位考试情况;监考人员可以进一步查看异常考生,并结合考试日志进行处理。

这比简单显示:

复制代码
考试人数:5000

更加有价值。

因为监考真正关心的是:

复制代码
还有多少人没有进入考试?

哪些单位参考率明显偏低?

一分钟内为什么突然出现大量掉线?

哪个考点连续发生异常?

交卷是否出现集中堵塞?

考试结束以后还有多少人没有成功提交?

这实际上已经不是普通"数据看板"。

而是:

Real-Time Exam Observability

即考试过程的实时可观测能力。


二十二、私有化考试环境为什么更需要做好这套架构?

企业、煤矿、电厂以及部分政务场景经常采用:

复制代码
局域网
内网
专网
私有云
本地服务器

部署考试系统。

这种环境的优势之一是系统资源可控,但也意味着项目方需要根据:

复制代码
实际人数
服务器配置
数据库性能
网络结构
监考功能

进行容量规划。

宏远培训考试系统面向内网、外网以及私有化部署场景时,如果正式考试电脑端与监控中心同时运行,就更加需要避免大屏统计影响考生答题业务。

从架构角度可以把优先级明确为:

复制代码
第一优先级:
答题与答案保存

第二优先级:
交卷与考试状态

第三优先级:
监考与异常处理

第四优先级:
实时统计展示

任何情况下,都不能为了让大屏每秒刷新几个数字,而降低正式考试数据链路的稳定性。


二十三、大屏实时架构最好监控哪些指标?

当系统上线以后,不应该只监控:

复制代码
CPU
内存

还要观察实时链路本身。

例如:

指标 关注的问题
Online WebSocket Connections 当前监控连接数量
Event Input QPS 每秒考试状态事件数量
Aggregation Latency 事件到聚合完成的延迟
Redis OPS Redis实时操作压力
WebSocket Push Latency 状态变化到大屏显示的延迟
Event Backlog 是否存在事件积压
Reconnect Count 是否频繁发生WebSocket断线
Snapshot Load Time 大屏首次加载速度
Reconciliation Difference Redis与数据库是否出现偏差
DB Dashboard Queries 大屏是否重新大量访问数据库

最后一个指标其实非常有意思。

如果架构改造以后:

复制代码
WebSocket很多
Redis OPS很多

但是:

复制代码
Dashboard DB Query

仍然非常高,就意味着系统可能只是表面上加了一层Redis,大屏底层仍然不断查数据库。

这种改造意义就比较有限。


二十四、六个特别常见的实时大屏设计误区

实际项目中,可以把几种实现方式放在一起比较:

设计方式 看起来的问题 真正的问题
每秒直接查数据库 实现简单 聚合SQL持续压库
加Redis但仍不断查DB 有缓存 缓存没有成为实时视图
使用WebSocket但每次先查DB 实时推送 只是把轮询换成主动查询
每个事件立即推大屏 延迟低 消息量巨大、页面频繁重绘
Redis Counter直接HINCRBY 性能高 重复事件造成统计漂移
只信Redis不做对账 架构简单 异常后数据无法证明准确

真正比较完整的方案应该同时解决:

复制代码
事件
状态
聚合
推送
幂等
恢复
对账

而不是只引入某一个技术名词。


二十五、最终可以形成怎样的一套实时监控链路?

综合前面的设计,完整架构可以抽象成:

复制代码
                         考生端
                           │
       ┌───────────────────┼───────────────────┐
       ▼                   ▼                   ▼
      登录                心跳                交卷
       │                   │                   │
       └───────────────────┼───────────────────┘
                           ▼
                      Exam Service
                           │
                     Business Event
                           │
               ┌───────────┴───────────┐
               ▼                       ▼
            Database              Event Channel
         最终业务事实                   │
                                       ▼
                              Realtime Aggregator
                                       │
                         ┌─────────────┴─────────────┐
                         ▼                           ▼
                    Candidate State              Counter
                         │                           │
                         └─────────────┬─────────────┘
                                       ▼
                                     Redis
                                       │
                              Realtime Snapshot
                                       │
                                       ▼
                               WebSocket Gateway
                                       │
                  ┌────────────────────┼────────────────────┐
                  ▼                    ▼                    ▼
              集团监控大屏          考点监控大屏          监考操作台

同时旁边还应该存在一条:

复制代码
Database
   ↓
Reconciliation
   ↓
Redis Snapshot

负责:

复制代码
初始化
定期校验
异常恢复
最终对账

这样才能把实时性和可靠性结合起来。


二十六、总结:实时监控的核心不是WebSocket,而是"不重新计算已经知道的事情"

考试监控大屏为什么不应该频繁直接查询数据库?

并不是因为:

数据库性能一定很差。

而是因为系统其实已经知道:

复制代码
谁登录了
谁进入考试了
谁掉线了
谁交卷了
谁发生异常了

这些业务事件已经在系统中真实发生过。

如果每次大屏刷新时再把所有数据重新扫描一遍,相当于:

系统不断重新计算自己刚刚已经知道的结果。

更加合理的方式应该是:

复制代码
考试行为
   ↓
产生状态事件
   ↓
幂等状态转换
   ↓
实时聚合
   ↓
Redis维护Realtime View
   ↓
WebSocket合并推送
   ↓
监控大屏

数据库继续承担:

复制代码
最终数据持久化
历史记录
考试结果
审计证据
数据恢复

Redis承担:

复制代码
当前状态
实时Counter
心跳
实时快照

WebSocket承担:

复制代码
服务器主动推送

聚合服务负责:

复制代码
把大量细粒度考试事件
转换成大屏真正需要的数据

而定期对账机制负责:

复制代码
证明实时数据没有越跑越偏

对于几百人的普通考试,这套架构可能显得稍微复杂。

但当场景变成:

复制代码
3000人
5000人
集团多考点
多人监考
异常抓拍
实时交卷
集中监控

以后,它解决的就不仅仅是"大屏卡不卡"。

更加重要的是:

监控系统本身不能成为正式考试系统的负担。

以宏远培训考试系统这类面向企业集中培训考试、私有化部署和多级组织管理的系统为例,监考大屏真正有价值的地方,也不是把几个数字做得多炫,而是让集团管理员、考试负责人和监考人员在不影响正式考试业务的前提下,实时知道考试进行到了哪里、哪里发生了异常、哪些问题需要马上处理。

从工程角度看,这套架构最终可以归纳成四个词:

事件驱动、增量聚合、实时推送、最终对账。

WebSocket和Redis只是实现这些目标的技术手段。

真正决定一块考试监控大屏能不能在几千人同时考试时依然稳定、准确、可信的,是背后的整套实时数据架构。

相关推荐
风哥2号1 小时前
数据库教程FGMT02‑生产环境Linux+Oracle19c安装配置与项目实战
linux·数据库·ffmpeg
九皇叔叔5 小时前
【第二章】Redis基础入门:Redis是什么、为什么快以及核心特性详解
数据库·redis·缓存
云和恩墨6 小时前
删库不跑路,回滚极速触达——zData S“旁路日志”持续保护原理深剖
数据库
MetaLite6 小时前
SpringBoot接口分层规范-外网网关内部服务与参数边界
java·数据库·spring boot
Wang's Blog8 小时前
Java框架快速入门: Spring Security+OAuth2之元注解简化权限表达式
java·数据库·spring
蓝速科技10 小时前
医院导诊 AI 数字人一体机场景适配与落地指南丨蓝速科技
运维·数据库·人工智能·科技·自然语言处理·技术分享
QYR-分析10 小时前
重轨受电弓行业深度报告:市场格局、技术迭代与发展前景
大数据·数据库·人工智能
泡泡鱼(敲代码中)11 小时前
MySQL基础学习笔记:从数据模型到DDL全掌握
开发语言·数据库·笔记·学习·mysql
l1t11 小时前
DeepSeek总结的chdb-core v26.7.3发版说明
数据库·clickhouse·oracle