0

问题陈述

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

重现步骤

数据库设置:

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), amount NUMERIC(15,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标志应该为真。
  2. 在第二个线程中插入更快,在第一个线程中进行选择时,我跳过了锁定的行,没有选择任何内容。在这种情况下,获取的余额将是null,后来选择is_balance_updated的标志应该是真的。
  3. 甚至在第一个线程中的选择之前执行了第二个线程中的 INSERT,因此我选择了余额与1.0金额。后来选择is_balance_updated的标志应该是假的。

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

  • is_balance_updated的,并在第一个线程余额中捕获0.0

有谁知道这怎么可能?

SELECT .. FOR NO KEY UPDATE实际上,当我SELECT ... FROM balance分成两个顺序查询时,我无法重现上述问题。

4

0 回答 0