You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Postgres高并发下如何高效更新单条匹配行实现一次性验证码分配

表结构优化方案
  • 保留原有核心字段,可根据数据规模调整字段类型避免溢出:
    • MappingId:优先用BIGINT自增主键,适配千万级以上验证码存储需求
    • Code:按验证码规则设置对应字符串/数值类型即可
    • DeviceId:按设备ID规则设置类型,默认值设为NULL表示未被领取
  • 新增部分索引,仅索引未被领取的验证码记录,大幅缩小索引体积、提升查询效率,避免全表/全索引扫描:
CREATE INDEX idx_mapping_unclaimed ON "MappingTable" ("MappingId") WHERE "DeviceId" IS NULL;

如果需要进一步避免回表开销,可创建覆盖索引:

CREATE INDEX idx_mapping_unclaimed_cover ON "MappingTable" ("MappingId") INCLUDE ("Code") WHERE "DeviceId" IS NULL;
查询语句优化方案

核心使用PostgreSQL原生的SKIP LOCKED语法,直接跳过已被其他事务锁定的行,完全消除锁等待问题,单条语句保证原子性,不会出现重复领取、竞态条件问题:

UPDATE "MappingTable" 
SET "DeviceId" = $1 
WHERE "MappingId" = (
    SELECT "MappingId" 
    FROM "MappingTable" 
    WHERE "DeviceId" IS NULL
    LIMIT 1
    FOR UPDATE SKIP LOCKED
)
RETURNING "Code";

其中$1为传入的待绑定设备ID参数。如果偏好CTE写法,也可以使用如下等价语句,性能差异极小:

WITH unclaimed_code AS (
    SELECT "MappingId"
    FROM "MappingTable"
    WHERE "DeviceId" IS NULL
    LIMIT 1
    FOR UPDATE SKIP LOCKED
)
UPDATE "MappingTable" m
SET "DeviceId" = $1
FROM unclaimed_code
WHERE m."MappingId" = unclaimed_code."MappingId"
RETURNING m."Code";

该方案默认使用PostgreSQL标准的读已提交隔离级别即可,无需调整更高隔离级别带来额外开销。

原方案性能问题根因

原有写法未加行锁跳过逻辑,高并发场景下大量请求会同时查询到同一条未被领取的MappingId,后续更新时多个事务争抢同一行锁,导致90%以上的请求处于锁等待状态,因此QPS无法提升。加入SKIP LOCKED后,每个请求会自动跳过被其他事务持有的行,不会产生等待。

进阶优化建议

如果生产环境峰值QPS超过2万,可在此基础上做分桶优化:

  • 给MappingTable新增bucket_id字段,预生成验证码时随机分配0N的桶号(N可按并发规模设置为10100)
  • 部分索引加上桶号维度
  • 领取时随机生成一个桶号作为查询条件,进一步缩小查询范围、降低行冲突概率

优化后常规场景压测QPS可轻松达到数千到数万级别,完全满足百万级设备的验证码领取需求。


内容的提问来源于stack exchange,提问作者Steve Johnson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 22:36:01