为什么在子查询中使用“SELECT .. FOR UPDATE”时 PostgreSQL 会返回陈旧数据?

问题描述

问题陈述@H_502_2@

在 Postgresql 10.7 中,我有一个查询,它在子选择中使用“SELECT .. FOR NO KEY UPDATE SKIP LOCKED”并聚合另一个表中的行。 在聚合表中使用 select 和 insert 同时执行两个事务我能够观察到状态,当插入成功时,但 select 没有获取它并且未处理。

重现步骤@H_502_2@

数据库设置:

CREATE TABLE customer (id SERIAL PRIMARY KEY);
CREATE TABLE transaction (id SERIAL PRIMARY KEY,customer_id INT REFERENCES customer(id),amount NUMERIC(15,2) NOT NULL);
CREATE TABLE balance (customer_id INT REFERENCES customer(id),2) NOT NULL,transaction_id INT NOT NULL REFERENCES transaction(id));
CREATE TABLE customer_balance_update (customer_id INT REFERENCES customer(id) UNIQUE,is_balance_updated BOOLEAN NOT NULL DEFAULT FALSE);

CREATE OR REPLACE FUNCTION customer_balance_update_trigger() RETURNS TRIGGER AS $function$
BEGIN
    IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
        INSERT INTO customer_balance_update(customer_id,is_balance_updated)
        VALUES (NEW.customer_id,TRUE)
        ON CONFLICT (customer_id) DO UPDATE SET is_balance_updated = EXCLUDED.is_balance_updated;
    END IF;
    RETURN NULL;
END;
$function$ LANGUAGE plpgsql;

CREATE TRIGGER customer_balance_update_trigger
    AFTER INSERT OR UPDATE
    ON balance
    FOR EACH ROW
EXECUTE PROCEDURE customer_balance_update_trigger();

CREATE OR REPLACE FUNCTION transaction_insert_trigger() RETURNS TRIGGER AS $function$
BEGIN
    IF (TG_OP = 'INSERT') THEN
        INSERT INTO balance(customer_id,amount,transaction_id)
        VALUES (NEW.customer_id,NEW.amount,NEW.id);
    END IF;
    RETURN NULL;
END;
$function$ LANGUAGE plpgsql;

CREATE TRIGGER transaction_insert_trigger
    AFTER INSERT
    ON transaction
    FOR EACH ROW
EXECUTE PROCEDURE transaction_insert_trigger();

INSERT INTO customer(id) VALUES (1);
INSERT INTO customer_balance_update(customer_id,is_balance_updated) VALUES (1,TRUE);

测试用例:

请注意,在每次执行之前,请确保 balance 表为空,并且 customer_balance_update 有 1 个客户的行,其中 is_balance_updated = TRUE

我有 Java 应用程序,它有 2 个线程:第一个用于选择,第二个用于插入。 在第一个线程中,我有以下伪代码(它作为一个事务执行,最后提交):

SELECT c.customer_id,COALESCE(SUM(b.amount),0) AS balance
FROM (SELECT customer_id AS customer_id
      FROM customer_balance_update
      WHERE is_balance_updated = TRUE AND customer_id = 1
      FOR NO KEY UPDATE SKIP LOCKED) AS c
LEFT JOIN balance b ON c.customer_id = b.customer_id
GROUP BY c.customer_id

// store selected balance if result not empty

// if balance present execute this
UPDATE customer_balance_update 
SET is_balance_updated = FALSE 
WHERE customer_id = 1

在第二个线程中,我有以下伪代码(它作为另一个事务执行,最后提交):

INSERT INTO transaction(customer_id,amount) VALUES (1,1)

当两个线程完成时,我会做以下检查:

SELECT is_balance_updated FROM customer_balance_update WHERE customer_id = 1

我认为我可以观察到三种情况:

  1. 一个线程中的 SELECT 速度更快,并且我已经锁定了 customer_balance_update 行,因此获取的余额为 0.0 并且带有 INSERT 的线程只有在第一个线程提交后才会完成。在这种情况下,稍后选择的 is_balance_updated 标志应该为 true。
  2. 在第二个线程中插入更快,在第一个线程中,在执行选择时,我跳过了锁定的行,没有选择任何内容在这种情况下,提取的余额将为 null,稍后选择的 is_balance_updated 标志应为 true。
  3. 第二个线程中的 INSERT 甚至在第一个线程中的 select 之前执行,因此我选择了 1.0 金额的余额。稍后选择的 is_balance_updated 标志应为 false。

但是,我能够观察到第四个案例:

  • is_balance_updatedfalse 并且在第一个线程余额中捕获的是 0.0

有人知道这是怎么可能的吗?

实际上,当我将 SELECT .. FOR NO KEY UPDATESELECT ... FROM balance 拆分为两个连续查询时,我无法重现上述问题。

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)