首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >投票、问卷怎么保证一人一票、刷的票不计入结果:业务幂等、异常票隔离与计票口径

投票、问卷怎么保证一人一票、刷的票不计入结果:业务幂等、异常票隔离与计票口径

原创
作者头像
用户5598620
发布于 2026-09-18 14:00:45
发布于 2026-09-18 14:00:45
1540
举报

导读:做一个评选投票或问卷 H5,上线后最常见的翻车不是页面崩,而是结果不可信:有人用脚本几分钟刷出几百票,两个候选人票数对不上,后台删掉刷票后总数又变了。单纯在网关加 IP 限流挡不住真人拉票和换设备刷量。本文给一套业务侧的正确性设计:以唯一约束保证一人一票、按身份强度分级限流、高并发下计票不错不乱、异常票先进隔离区复核而不是直接删除,让最终票数可解释、可复核、可回放。

一、为什么只做 IP 限流挡不住刷票

网关限流、验证码、设备指纹能挡住一部分机器流量,但它们解决的是"请求别太多",不是"这一票该不该算"。投票业务真正要回答三个问题:

  • 同一个人能不能投第二次(身份唯一性);
  • 来得又快又集中的票,是真实拉票还是脚本(异常识别);
  • 判定可疑的票,怎么处理才不污染结果又不误伤(计票口径)。

IP 会变(公司、校园共用出口 IP)、设备可以伪造、验证码可以打码。所以防刷要分层,而把"一人一票"的正确性压在数据库约束和业务规则上,网关只做第一道削峰。

二、一人一票的根:投票记录的唯一约束

不管前面加多少校验,最后落库必须有一条硬约束兜底:在"活动 + 投票对象 + 投票人唯一标识"上建唯一索引。任何重复提交,数据库这一层都只会进一条。

代码语言:sql
复制
-- 投票人标识按活动配置选取:登录用户用 user_id,弱登录用设备/授权 unionid
CREATE TABLE vote_record (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  activity_id  BIGINT NOT NULL,
  option_id    BIGINT NOT NULL,
  voter_key    VARCHAR(64) NOT NULL,   -- user_id / unionid / 设备指纹,按身份策略定
  id_level     TINYINT NOT NULL,       -- 身份强度:3 登录 2 授权 1 设备 0 匿名
  status       VARCHAR(12) NOT NULL DEFAULT 'valid', -- valid/quarantine/canceled
  ip           VARCHAR(45),
  created_at   DATETIME NOT NULL,
  UNIQUE KEY uk_one_vote (activity_id, option_id, voter_key),
  KEY idx_status (activity_id, status)
);

-- 每个候选的计票表,只统计 valid 票
CREATE TABLE vote_count (
  activity_id BIGINT NOT NULL,
  option_id   BIGINT NOT NULL,
  valid_cnt   INT NOT NULL DEFAULT 0,
  PRIMARY KEY (activity_id, option_id)
);

写入用"插入投票记录 + 计票 +1"放在同一事务,重复插入捕获唯一键冲突后幂等返回,不重复计票。

代码语言:python
复制
def cast_vote(activity_id, option_id, voter):
    try:
        with tx() as conn:
            conn.execute("INSERT INTO vote_record(activity_id,option_id,voter_key,"
                         "id_level,status,ip,created_at) VALUES(%s,%s,%s,%s,'valid',%s,NOW())",
                         activity_id, option_id, voter.key, voter.level, voter.ip)
            conn.execute("UPDATE vote_count SET valid_cnt=valid_cnt+1 "
                         "WHERE activity_id=%s AND option_id=%s", activity_id, option_id)
        return "ok"
    except DuplicateKey:
        return "already_voted"      # 一人一票,重复请求幂等

三、不同登录强度下,怎么定"一个人"

很多活动为了降低参与门槛不强制登录,这时"一个人"有强弱之分,要分级而不是一刀切:

强身份:已登录账号(user_id)或微信授权 unionid,一人一票最可靠; 弱身份:未授权,用设备指纹 + 本地随机标识,允许一票但标记为低可信度; 匿名:连设备标识都拿不到时,只允许在强验证码 + 严格频控下投票,且默认进隔离区复核。

弱身份和匿名票先收、标记、不一定立刻计入:它们正常落库,但 id_level 低,计票时可按活动配置选择"实时计入但加权展示"或"先进隔离、复核后计入",避免门槛过高劝退真实用户,也不让机器票直接冲榜。

四、高并发下票数怎么不丢、不重、能对账

热门投票瞬间上万 QPS,直接每次 UPDATE vote_count +1 会把行锁打热。常见做法是缓存原子计票抗量、异步批量落库,再用投票明细做对账兜底。

代码语言:lua
复制
-- 缓存内对单个候选原子自增,配合活动维度的频控键
local cnt = redis.call('INCR', KEYS[1])
local ip_cnt = redis.call('INCR', KEYS[2])
if ip_cnt == 1 then redis.call('EXPIRE', KEYS[2], ARGV[1]) end
return {cnt, ip_cnt}

但缓存计数只是加速,权威票数永远以 vote_record 明细为准:定时任务按 status='valid' 重算每个候选的总数,与缓存/计票表比对,出现漂移就以明细修正。这样即使缓存重启、消息丢一条,最终结果也能从明细完整重放,不会出现"票数和实际投票人数对不上"。

五、刷来的票怎么处理:隔离复核,而不是直接删

直接删除可疑票有两个问题:误判了无法恢复,删除瞬间总数跳变容易引发争议。更稳妥的是引入隔离状态,把"计票"和"判定"解耦:

  • 命中明确规则(同一 voter_key 重复、黑名单设备):重复票天然被唯一约束挡掉;
  • 命中可疑特征(同 IP/设备段速率异常、时间间隔机械、新注册账号集中投同一候选):票落库但置为 quarantine,不计入 valid_cnt;
  • 隔离票进入复核队列,按规则自动放行或人工确认,确认刷票置 canceled,误判则恢复为 valid 并补计票。
代码语言:python
复制
def is_suspicious(voter, recent):
    if recent.ip_rate(voter.ip, window=60) > LIMIT_PER_MIN:   return True
    if recent.device_rate(voter.device_fp, window=300) > LIMIT_DEVICE: return True
    if voter.level <= 1 and recent.new_account_ratio() > 0.8: return True
    return False

def settle_quarantine(rec, decision):
    if decision == "confirm_cheat":
        update_status(rec.id, "canceled")
    elif decision == "restore":
        with tx() as c:
            updated = c.execute("UPDATE vote_record SET status='valid' "
                                "WHERE id=%s AND status='quarantine'", rec.id)
            if updated:
                c.execute("UPDATE vote_count SET valid_cnt=valid_cnt+1 "
                          "WHERE activity_id=%s AND option_id=%s", rec.activity_id, rec.option_id)

对外展示可以同时给出"实时票数(含待核)"和"有效票数(已核)"两个口径,活动结束以有效票为准,全过程有记录、可申诉。

踩坑清单

  • 只在网关限流、库里不加唯一约束:绕过限流就能重复投票,一人一票必须靠唯一索引兜底。
  • 用 IP 当唯一身份:共用出口 IP 会误伤一片真人,IP 只能做频控维度,不能做投票人主键。
  • 缓存计票不当权威:缓存丢数据就永久错票,最终结果必须能从投票明细重算。
  • 可疑票直接物理删除:误判无法恢复、总数突变引争议,应使用隔离状态复核。
  • 不区分登录强度:强制登录劝退用户、全匿名又放水,应按身份强度决定是否实时计票。
  • 投票和计票不在同一事务:会出现有票无数或一票多数,明细与计数要同进同退、定时对账。

工程落地建议

投票、问卷这类活动建议沉淀一套通用"投票中心":活动配置里声明身份策略(是否强制登录)、频控阈值、可疑规则与计票口径;接入方只负责页面和候选管理。明细、计票、隔离三类表分离,实时数走缓存、权威数走明细重算,复核动作全部留痕。活动结束后冻结计票表并归档明细,既方便复盘真实拉新效果,也能在结果被质疑时逐条回放。

上线复盘清单

  • 活动+候选+投票人标识上有唯一索引,重复票幂等返回
  • 投票明细与计票在同一事务,失败一起回滚
  • 按登录/授权/设备/匿名分级,弱身份票有标记和计票策略
  • 网关与业务双层频控,IP、设备、账号多维限速
  • 可疑票进隔离区,复核后才计入或作废,全程留痕
  • 定时按明细重算票数并与计票表对账,支持结果回放
  • 对外区分实时票与有效票口径,结束后冻结归档

常见问题

Q:投票活动怎么真正做到一人一票?

A:以登录账号或授权标识作为投票人主键,在"活动+候选+投票人"上建唯一索引,重复提交被数据库幂等挡掉;未强制登录时用设备标识做弱身份并降低其计票可信度,IP 只用于频控不作为唯一身份。

Q:发现有人刷票,直接删掉那些票可以吗?

A:不建议直接删除,应把命中异常规则的票置为隔离状态、暂不计入有效票,再经规则或人工复核确认后作废或恢复,避免误判无法挽回,也让最终票数可解释、可申诉。

Q:高并发投票时页面上的票数怎么保证不错?

A:实时数可用缓存原子自增抗并发,但权威票数以投票明细为准,定时按有效票明细重算并与计票表对账修正,即使缓存重启或消息丢失也能完整重放结果。

结语

以上为投票、问卷类轻应用的防刷与计票正确性实践,频控阈值和可疑规则需按活动热度与参与门槛调整。核心是把"请求削峰"交给网关、把"一人一票"交给唯一约束、把"结果可信"交给明细重算与隔离复核,票数才经得起复盘和质疑。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么只做 IP 限流挡不住刷票
  • 二、一人一票的根:投票记录的唯一约束
  • 三、不同登录强度下,怎么定"一个人"
  • 四、高并发下票数怎么不丢、不重、能对账
  • 五、刷来的票怎么处理:隔离复核,而不是直接删
  • 踩坑清单
  • 工程落地建议
  • 上线复盘清单
  • 常见问题
    • Q:投票活动怎么真正做到一人一票?
    • Q:发现有人刷票,直接删掉那些票可以吗?
    • Q:高并发投票时页面上的票数怎么保证不错?
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档