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

MySQL存款与取款表如何防止重复录入?解决重复API请求问题

嘿,这个问题我在做支付类系统的时候踩过不少坑,刚好有几个经过实践验证的方案分享给你,完全能解决重复插入的问题,而且性能比表锁好太多:

方案一:业务唯一键 + INSERT ... ON DUPLICATE KEY UPDATE(最推荐)

这是最常用也最省心的方案,核心思路是给取款/存款记录表设置一个能唯一标识单次请求的联合唯一键,利用MySQL的唯一键约束拦截重复插入,同时配合ON DUPLICATE KEY UPDATE避免报错。

怎么设置唯一键?

对于取款记录来说,唯一键可以由「用户ID + 全局唯一请求ID」组成:

  • 用户ID:标识操作所属的用户;
  • 请求ID:由你的API层生成(比如UUID、雪花ID,甚至前端生成的唯一标识),同一个用户的同一次取款请求,不管触发多少次,请求ID都是相同的。

举个建表示例:

CREATE TABLE withdrawal_records (
    id INT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    request_id VARCHAR(64) NOT NULL COMMENT 'API请求唯一标识',
    amount DECIMAL(10,2) NOT NULL COMMENT '取款金额',
    status TINYINT DEFAULT 1 COMMENT '记录状态:1-成功,2-失败',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    -- 设置联合唯一键
    UNIQUE KEY uk_user_request (user_id, request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

插入逻辑

使用INSERT ... ON DUPLICATE KEY UPDATE语句,当重复插入时,不会新增记录,而是更新指定字段(比如更新最后操作时间),避免报错:

INSERT INTO withdrawal_records (user_id, request_id, amount)
VALUES (1001, 'a1b2c3d4-5678-90ef-ghij-klmnopqrstuv', 500.00)
ON DUPLICATE KEY UPDATE create_time = CURRENT_TIMESTAMP;

这个方案的优点:

  • 完全依赖MySQL的原生约束,代码逻辑简单;
  • 性能高,InnoDB会对唯一键进行行级锁,不会影响其他用户的操作;
  • 天然支持幂等,重复请求不会生成重复记录。
方案二:分布式锁 + 事务(适合无请求ID的场景)

如果你的业务场景没办法生成全局唯一的请求ID,可以用分布式锁来控制同一用户的并发取款请求,确保同一时间只有一个请求能执行插入操作。

实现思路

比如用Redis的SETNX命令,或者MySQL自带的GET_LOCK函数来加锁,锁的粒度建议按「用户ID」来设置(不要用全局锁,否则会影响所有用户的性能):

举个MySQL自带锁的例子:

-- 1. 获取针对用户1001的取款锁,超时时间10秒(防止死锁)
SELECT GET_LOCK('withdrawal_lock_1001', 10) INTO @lock_result;

-- 2. 检查是否获取到锁
IF @lock_result = 1 THEN
    START TRANSACTION;
    -- 执行取款逻辑:比如扣减用户余额 + 插入取款记录
    UPDATE users SET balance = balance - 500.00 WHERE user_id = 1001;
    INSERT INTO withdrawal_records (user_id, amount) VALUES (1001, 500.00);
    COMMIT;
    -- 3. 释放锁
    SELECT RELEASE_LOCK('withdrawal_lock_1001');
ELSE
    -- 未获取到锁,说明当前有并发请求,返回“操作中,请稍后再试”
    SELECT '操作中,请稍后再试' AS msg;
END IF;

注意事项

  • 一定要设置锁的超时时间,防止服务宕机导致锁无法释放;
  • 锁的粒度要尽量细,避免影响其他用户的操作;
  • 如果用Redis锁,要注意锁的续期问题(比如Redisson的看门狗机制)。
方案三:乐观锁 + 事务(兼顾余额一致性)

如果你的取款操作需要同时保证用户余额的一致性(避免超支),可以给用户表加一个版本号字段,利用乐观锁来控制并发,同时间接避免重复插入记录。

实现步骤

  1. 给用户表添加版本号字段:
ALTER TABLE users ADD COLUMN version INT DEFAULT 1 COMMENT '乐观锁版本号';
  1. 事务包裹取款逻辑:
START TRANSACTION;

-- 1. 查询用户当前余额和版本号(加行锁,避免其他事务修改)
SELECT balance, version FROM users WHERE user_id = 1001 FOR UPDATE INTO @current_balance, @current_version;

-- 2. 判断余额是否足够
IF @current_balance >= 500.00 THEN
    -- 3. 更新余额,同时版本号+1(只有版本号匹配才会更新成功)
    UPDATE users SET balance = balance - 500.00, version = version + 1 WHERE user_id = 1001 AND version = @current_version;
    
    -- 4. 检查更新行数,如果是0说明并发冲突,回滚
    IF ROW_COUNT() > 0 THEN
        INSERT INTO withdrawal_records (user_id, amount) VALUES (1001, 500.00);
        COMMIT;
        SELECT '取款成功' AS msg;
    ELSE
        ROLLBACK;
        SELECT '并发冲突,请重试' AS msg;
    END IF;
ELSE
    ROLLBACK;
    SELECT '余额不足' AS msg;
END IF;

这个方案的优点是同时保证了余额的一致性和记录的唯一性,重复请求会因为版本号不匹配而更新失败,自然不会插入重复记录。

总结建议
  • 优先用方案一,只要能生成全局唯一的请求ID,这是最简单高效的方案;
  • 如果你不需要单独的请求ID,或者需要更严格的并发控制,可以考虑方案二;
  • 如果需要兼顾余额一致性,方案三是不错的选择;
  • 绝对不要用MyISAM的表锁,性能差还不支持事务,直接用InnoDB引擎就行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 07:11:00