我有一个带有触发器的表,该触发器设置每个客户的订单号。我经常在这个表中遇到唯一的冲突错误:
SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint "order_customer_unique"
触发器定义为BEFORE INSERT ON orders FOR EACH ROW
。
有什么技术可以解决表中的并发性问题吗?
理想情况下,每个客户的号码是一个序列,并且不会丢失号码。
CREATE OR REPLACE FUNCTION "public"."trigger_set_order_customer_number"()
RETURNS "pg_catalog"."trigger" AS
$BODY$
BEGIN
NEW.sequence = (
SELECT COALESCE( MAX ( order.sequence), 0 ) + 1
FROM orders
WHERE orders.customer_type = NEW.customer_type
AND orders.customer_id = NEW.customer_id
AND orders.model_id = NEW.model_id LIMIT 1);
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
在继续之前,请考虑而不是这样做。在并发写负载下,每个分区的无间隙序列号有许多含义,并且可能成为一项代价高昂的工作。很大程度上取决于您的设置和需求的细节。看到:
- 复合键的每组行序列号
对于并发写,您拥有的触发器本质上是不安全的。冲突是预料之中的,因为每个事务都看到它自己的表快照,以及计算出来的"下一个数字"。对于多个并发插入,结果可能相同。
您可以升级到SERIALIZABLE
事务隔离。但这是昂贵的,而且您必须为序列化失败做好准备,就像您现在必须为唯一的违例做好准备一样。赶上22。
- 获取在并发事务中执行的INSERT的不同行集
同时,我们可以将order_count
放入父表中,以使一切更简单,更便宜。可以像这样:
CREATE TABLE unique_customer_account(
customer_id int -- replace with actual data types
, customer_type int
, model_id int
, order_count int NOT NULL DEFAULT 0
, PRIMARY KEY (customer_id, customer_type, model_id)
);
CREATE OR REPLACE FUNCTION public.trigger_set_order_customer_number()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
-- this locks the row and makes concurrent transactions targeting
-- the same unique_customer_account wait till this one is done
INSERT INTO unique_customer_account AS u
( customer_type, customer_id, model_id, order_count)
VALUES (NEW.customer_type, NEW.customer_id, NEW.model_id, 1)
ON CONFLICT (customer_type, customer_id, model_id) DO UPDATE -- ①
SET order_count = u.order_count + 1 -- ②
RETURNING u.order_count
INTO NEW.sequence; -- ③
IF NOT FOUND THEN
RAISE EXCEPTION 'Row in "unique_customer_account" not found!'; -- ④
END IF;
RETURN NEW;
END
$func$;
CREATE TRIGGER orders_insbef
BEFORE INSERT ON orders
FOR EACH ROW EXECUTE FUNCTION public.trigger_set_order_customer_number();
db<此处小提琴>此处小提琴>
具有并发插入的快速本地测试也像宣传的那样工作。
①UPDATE
隐式地对行执行排他锁,与SELECT FOR UPDATE
相同。
②没有COALESCE
,因为order_count
被定义为NOT NULL
。
NEW.sequence
。
④unique_customer_account
中的行必须存在。我在扳机里装了UPSERT。或者你们可以分开处理。无论哪种方式,如果多个并发事务希望同时在unique_customer_account
中输入新行,则会出现另一种(不太常见的)竞争条件,这可能导致UPSERT中没有行。可以解决。看到:
- 如何使用返回与ON冲突在PostgreSQL?
下一个问题的一个简单而干净的解决方案是在插入任何订单之前,在单独的命令中INSERT ... ON CONFLICT DO NOTHING
unique_customer_account
中的新行。可以在同一个事务中,但不能在同一个命令的CTE中。
那么您可以假设unique_customer_account
中的行存在,并在触发器中简化为普通的UPDATE
。所有的比赛条件都消失了。(除了外来的)
就像我说的,很多复杂的细节…
YES!不要使用扳机。将sequence
列定义为integer generated always as identity
。当Postgres处理赋值时,这消除了对触发器的复杂需求;你没有——事实上你不能。仅供参考,将值赋值为MAX + 1几乎可以保证创建重复项。不幸的是,这会使丢失数字,因为在某些点上会有空白。生成无间隙序列相当困难,而且几乎没有任何收益。除非有合法的,否则你最好习惯它有丢失的值,然后继续前进。