当前位置:首页 > 科研成果库

科技节组织:现场投票系统,为什么总在你最需要它的时候掉链子?

2026-11-03 08:33:21小研科研成果库7

去年秋天,我蹲在某高校科技节的主席台侧面,手里攥着一台发烫的笔记本。台下黑压压三百多号人,正举着手机对着大屏幕上的二维码猛扫。那是个“最佳展位”的观众投票环节,系统是我熬了两个通宵搭的。前两分钟还风平浪静,第三分钟——页面开始转圈。第五分钟,有个男生扯着嗓子喊:“这玩意儿是不是卡了?”我低头一看监控,数据库连接池直接飙红,错误日志像雪崩一样刷屏。

崩了。真的崩了。而且是在最不该崩的时候。

说实话,这事儿我后来复盘了不下十遍。科技节组织里的技术活儿,最怕的就是这种“看起来简单、实际上全是坑”的环节。投票嘛,不就是点一下按钮?可你想想,三百个人在同一秒点按钮,那能叫“点一下”吗?那叫三百发炮弹同时砸向一台小霸王学习机。

科技节现场观众排队扫码投票拥堵场景科技节现场观众排队扫码投票拥堵场景

那个下午,二维码变成了死胡同

那个下午,二维码变成了死胡同那个下午,二维码变成了死胡同

先说说当时到底发生了什么。我们用的架构很简单:一个云服务器跑着Node.js后端,MySQL存投票记录,前端就是个静态页面。开发环境里我自己测过,一百个并发请求毫无压力,响应时间不到50毫秒。挺美,对吧?

但测试环境和真实场景之间,隔着一道叫做“人类行为”的鸿沟。真实情况是:主持人喊“投票开始”的那一秒,几乎所有人都同时抬手扫码。微信群里的二维码图片被疯狂转发,有人扫了一次没进去就反复刷新。再加上学校Wi-Fi本来就半死不活,大量请求超时重试,相当于每个用户自己给自己叠了三倍流量。

数据库连接池我设了10个。当时觉得够了,毕竟测试时峰值才用掉3个。结果三百个人涌进来,每个请求都要占一个连接,后面的请求就在队列里干等。等超过10秒,前端直接报超时。更骚的是,我没做限流,没做缓存,每个投票请求都老老实实去数据库里插一条记录。三百条并发插入,MySQL的InnoDB引擎锁竞争直接把CPU干到了100%。

你猜现场观众什么反应?先是窃窃私语,然后有人开始笑,最后有人直接走了。主持人站在台上尬聊了三分钟,我只能硬着头皮喊:“大家稍等,系统正在升级!”——其实就是在重启服务。

连接池不是水管,是漏斗

很多人对“数据库连接池”有误解。字面意思,你会觉得它像一根粗水管,池子越大,水流越猛。错了。连接池其实是个漏斗——池子大小只是漏斗的口径,真正决定吞吐量的是下面那根细管子,也就是数据库本身处理请求的速度。

举个例子。你家楼下早餐店只有一口锅,同时来了二十个人点煎饼。你给老板二十个盘子(连接池设20),老板还是得一锅一锅煎。盘子再多,锅就一口。盘子越多,反而越乱,因为老板得来回找盘子、洗盘子。

那怎么办?把“读”和“写”拆开。投票场景里,绝大多数请求是“看看当前票数”,真正的“投票”动作只占一小部分。我当时把每个投票请求都写进数据库,然后查结果也从数据库查。蠢透了。正确做法是:投票动作先扔进消息队列(比如Redis的List或者Kafka),异步慢慢写库;票数展示直接从Redis缓存里读,压根不碰MySQL。这样数据库连接池的压力瞬间降一个数量级。

还有更狠的一招:静态化。投票页面本身是个纯前端页面,完全没必要每次请求都经过后端渲染。我当时为了“实时显示票数”,搞了个每两秒轮询一次接口。三百个人每两秒轮询一次,就是每秒150个请求,全是重复查同一个数据。后来改成WebSocket推送,或者干脆让用户手动点“刷新”才更新——反正科技节投票又不需要毫秒级实时性。你一个展位介绍页,晚三秒看到票数变化,观众根本不在乎。

在线投票系统高并发架构Redis缓存消息队列示意图在线投票系统高并发架构Redis缓存消息队列示意图

别信“上云就万事大吉”的鬼话

别信“上云就万事大吉”的鬼话别信“上云就万事大吉”的鬼话

出事后有人跟我讲:“你买的是最便宜的云服务器吧?换台贵的。”说实话,这话对了一半。云服务器确实有弹性伸缩,但你得自己配。默认配置下,一台2核4G的机器跑MySQL,并发上到200就喘。我后来打开云监控一看,出问题那几分钟,CPU和内存都还有余量,瓶颈全在数据库连接和磁盘IO上。你就算换台16核64G的,连接池不调、缓存不加,照样崩。

还有个误区:以为用了“云数据库”就自带高并发。并不是。云数据库给你的是运维层面的高可用,不是性能层面的无限扩展。你写个全表扫描的查询,再贵的云数据库也得跪。我见过有团队在科技节现场用云函数跑投票统计,每次请求都去遍历整个MySQL表——云函数确实能一秒弹出一百个实例,但每个实例都去扫表,数据库直接被打成筛子。

那正确的姿势是什么?把请求挡在数据库前面。具体分三层:第一层,CDN或者Nginx缓存静态页面,连后端都不碰;第二层,用Redis做投票计数和限流,每个用户ID一分钟只能投一次,超了直接返回“您已投过”;第三层,消息队列削峰,投票动作进队列,后台慢慢消费。这样哪怕瞬间来一千个人,数据库看到的只是每秒十几条的匀速写入,舒服得很。

对了,还有个细节容易被忽略:二维码本身的承载能力。我们当时把二维码印在展板上,旁边还贴了个“扫码投票”的提示。结果好多人扫了之后跳出来一个微信登录页,登录完还要填手机号——直接走了一半。科技节现场的老人小孩都有,任何多余的交互都是流失。后来改成微信内直接授权,一键投票,转化率立马上去了。你看,技术问题有时候不只是技术。

如果再来一次,我会怎么干?

现在让我重新组织一个科技节的投票环节,我会这么做:

先做个压测。用k6或者JMeter模拟300个并发用户,别偷懒,不要手动点几下就算完。压测的时候把网络延迟也加上,模拟现场Wi-Fi的抖动。然后看瓶颈到底在哪——是连接池不够?是SQL写得太烂?还是静态资源没缓存?

接着把架构简化到极致。投票页面完全静态化,托管在OSS或者Vercel上,后端只提供一个极简的API:POST /vote {userId, boothId}。这个API第一件事是查Redis里有没有这个用户的投票记录(用SETNX实现分布式锁),有就直接返回重复投票;没有就给boothId对应的计数器加一,同时丢一条消息到RabbitMQ。消费端每隔一秒批量写一次MySQL。整个过程,MySQL的连接数最多保持在个位数。

最后,准备Plan B。我在主席台旁边放了一台旧手机,开着热点,里面装了个本地版的投票统计小工具——万一云端彻底挂了,就让大家连热点扫码,数据存手机本地,事后合并。这招土,但真救命。

说回那个下午。我重启完服务,改了连接池参数,临时加了个Redis缓存,总算让投票在二十分钟内跑完了。但大屏幕上的实时票数排行榜一直没敢再开,怕再次触发查询风暴。最后颁奖的时候,我听见一个学生嘀咕:“这投票系统还不如用微信接龙。”扎心,但没毛病。科技节组织里,技术不是用来炫技的,是用来让活动顺顺当当办完的。那些花里胡哨的实时大屏、炫酷动画,抵不过一个“扫码秒投、投完就走”的朴素体验。

所以下次你要搞科技节,记住:投票系统崩溃不是因为技术不够先进,是因为你太相信“一切都会按测试环境走”。人类行为才是最大的变量。你永远不知道现场会有多少个人同时自拍打卡、微信视频、疯狂刷新——就像你永远不知道食堂阿姨打菜时手抖的幅度。