问题陈述
在 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
我认为我可以观察三种情况:
- 第一个线程中的 SELECT 更快,并且我已经锁定
customer_balance_update
了行,以便获取的余额0.0
和带有 INSERT 的线程将仅在第一个线程提交后完成。在这种情况下,稍后选择的is_balance_updated
标志应该为真。 - 在第二个线程中插入更快,在第一个线程中进行选择时,我跳过了锁定的行,没有选择任何内容。在这种情况下,获取的余额将是
null
,后来选择is_balance_updated
的标志应该是真的。 - 甚至在第一个线程中的选择之前执行了第二个线程中的 INSERT,因此我选择了余额与
1.0
金额。后来选择is_balance_updated
的标志应该是假的。
但是,我能够观察到第四个案例:
is_balance_updated
是假的,并在第一个线程余额中捕获0.0
。
有谁知道这怎么可能?
SELECT .. FOR NO KEY UPDATE
实际上,当我SELECT ... FROM balance
分成两个顺序查询时,我无法重现上述问题。