You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Oracle中如何防止并发INSERT操作及竞态条件下基于表A许可行数验证表B插入行数

解决Oracle并发INSERT的竞态条件问题

哥们,你遇到的这个问题我之前也碰到过——就是Oracle并发插入时的竞态条件,你的原SQL之所以失效,核心原因是没有锁定任何资源,所有并行请求都在读取同一个数据快照,结果大家都觉得“还没到限制”,然后都插进去了。针对你这个场景(基于表A的每行限制,控制表B的用户级插入行数),我给你几个靠谱的解决方案:

核心思路:通过锁定实现串行化处理

要解决竞态问题,必须让并发请求串行处理——也就是每个请求都要等前一个处理完,再重新统计行数。最稳妥的方式是锁定表A中对应的活动行,因为它是限制规则的源头,锁定它就能让所有针对同一活动的请求排队。

方案一:PL/SQL块+行级锁(推荐)

这个方案把“锁定规则源→统计当前行数→判断插入”放在同一个事务里,完全避免了竞态:

DECLARE
    v_limit          NUMBER;
    v_current_count  NUMBER;
    v_offer_id       OFFER.OFFER_ID%TYPE := :offer_id;
    v_user_id        OFFER.USER_ID%TYPE := :user_id;
    v_campaign_id    OFFER.CAMPAIGN_ID%TYPE := :campaign_id;
    v_start_date     OFFER.START_DATE%TYPE := :start_date;
    v_end_date       OFFER.END_DATE%TYPE := :end_date;
    v_request_status OFFER.请求状态%TYPE := :request_status;
BEGIN
    -- 第一步:锁定表A中对应的活动行,强制并发请求串行
    SELECT ISSUE_LIMIT_PER_USER
    INTO v_limit
    FROM CAMPAIGN
    WHERE CAMPAIGN_ID = v_campaign_id
    FOR UPDATE; -- 这行是关键!其他请求会在这里等待,直到当前事务提交/回滚

    -- 第二步:在锁保护下,准确统计该用户当前已插入的行数
    SELECT COUNT(OFFER_ID)
    INTO v_current_count
    FROM OFFER
    WHERE CAMPAIGN_ID = v_campaign_id
      AND USER_ID = v_user_id; -- 注意:必须加USER_ID过滤,因为限制是**每个用户**的

    -- 第三步:判断是否允许插入
    IF v_current_count < v_limit THEN
        INSERT INTO OFFER (OFFER_ID, USER_ID, CAMPAIGN_ID, START_DATE, END_DATE, 请求状态)
        VALUES (v_offer_id, v_user_id, v_campaign_id, v_start_date, v_end_date, v_request_status);
        COMMIT;
    ELSE
        -- 超过限制,抛出自定义异常提示
        RAISE_APPLICATION_ERROR(-20001, '该活动下您的申请次数已达上限');
        ROLLBACK;
    END IF;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        ROLLBACK;
        RAISE_APPLICATION_ERROR(-20002, '对应的活动不存在');
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
/

为什么这个方案有效?

  • FOR UPDATE会锁定CAMPAIGN表中CAMPAIGN_ID=:campaign_id的行,其他并发请求执行到这一步时会自动等待,直到当前事务完成(提交/回滚),从根本上避免了“同时读旧数据”的问题。
  • 在锁的保护下,统计OFFER表的行数是实时的,不会出现多个请求同时得到“行数不足”的错误结果。

方案二:用INSERT ... SELECT结合锁(适合纯SQL场景)

如果不想写PL/SQL,也可以把锁定和插入合并到一条SQL里,利用SELECT ... FOR UPDATE来锁定资源:

INSERT INTO OFFER (OFFER_ID, USER_ID, CAMPAIGN_ID, START_DATE, END_DATE, 请求状态)
SELECT :offer_id, :user_id, :campaign_id, :start_date, :end_date, :request_status
FROM DUAL
WHERE EXISTS (
    SELECT 1
    FROM CAMPAIGN c
    WHERE c.CAMPAIGN_ID = :campaign_id
      AND (SELECT COUNT(OFFER_ID)
           FROM OFFER o
           WHERE o.CAMPAIGN_ID = c.CAMPAIGN_ID
             AND o.USER_ID = :user_id) < c.ISSUE_LIMIT_PER_USER
    FOR UPDATE -- 锁定CAMPAIGN行,确保并发串行
);

注意:

这个方案的核心还是FOR UPDATE锁定CAMPAIGN行,不过如果插入失败(超过限制),SQL不会抛出异常,只是插入0行,所以需要在应用层判断插入的行数来处理结果。

为什么你的原方案失效?

你的原SQL是先查询OFFER的行数,再插入,但这个查询是快照读(Oracle默认的隔离级别是READ COMMITTED,每个查询都读当前已提交的数据快照)。并发时,多个请求同时执行查询,看到的都是“还没到限制”的快照,然后都执行插入,最终导致超量。而UPDATE场景下有效,是因为UPDATE会锁定被更新的行,并发请求会排队,不会出现同时修改的情况。

额外注意点

  1. 一定要过滤USER_ID:从你的示例数据看,限制是“每个用户每个活动最多插1条”,所以统计行数时必须加上USER_ID=:user_id的条件,不然会统计所有用户的行数,逻辑就错了。
  2. 及时提交事务:锁持有时间越长,并发性能越差,所以事务要尽量短,处理完就提交。
  3. 异常处理:要考虑活动不存在、插入失败等异常情况,避免事务挂起或数据不一致。

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

火山引擎 最新活动