我正在使用python(不是真正相关的)和Postgresql(如果相关的9.2)实现一个简单的基于Web的RSS阅读器。数据库模式如下(基于RSS格式):
CREATE TABLE feed_channel
(
id SERIAL PRIMARY KEY,
name TEXT,
link TEXT NOT NULL,
title TEXT
);
CREATE TABLE feed_content
(
id SERIAL PRIMARY KEY,
channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE,
guid TEXT UNIQUE NOT NULL,
title TEXT,
link TEXT,
description TEXT,
pubdate TIMESTAMP
);
当我创建新频道(并查询更新的Feed信息)时,我请求Feed,将其数据插入feed_channel表,选择新插入的ID - 或现有以避免重复 - 然后将Feed数据添加到feed_content表。典型的情况是:
这是标准的“插入,如果尚未存在,但返回相关ID”问题。为了解决这个问题,我实现了以下存储过程:
CREATE OR REPLACE FUNCTION channel_insert(
p_link feed_channel.link%TYPE,
p_title feed_channel.title%TYPE
) RETURNS feed_channel.id%TYPE AS $$
DECLARE
v_id feed_channel.id%TYPE;
BEGIN
SELECT id
INTO v_id
FROM feed_channel
WHERE link=p_link AND title=p_title
LIMIT 1;
IF v_id IS NULL THEN
INSERT INTO feed_channel(name,link,title)
VALUES (DEFAULT,p_link,p_title)
RETURNING id INTO v_id;
END IF;
RETURN v_id;
END;
$$ LANGUAGE plpgsql;
然后将其称为“select channel_insert(link,title);”从我的应用程序插入,如果尚未存在,然后返回相关行的ID,无论是插入还是刚找到(上面的列表中的第2步)。
这很棒!
但是,我最近开始想知道如果使用相同的参数同时执行此过程两次会发生什么。让我们假设如下:
这会成为PostgreSQL中潜在的竞争条件吗?解决此问题以避免此类情况的最佳方法是什么?是否有可能以原子方式制作整个存储过程,即它只能同时执行一次?
我尝试过的一个选项是将字段设为Unique,然后尝试先插入,如果是异常,则选择现有的...但是,这样可行,但SERIAL字段会为每次尝试增加,留下很多空隙顺序。我不知道从长远来看这可能是个问题(可能不是),但有点烦人。也许这是首选解决方案?
感谢您的任何反馈。这种级别的PostgreSQL魔法超出了我的范围,所以任何反馈都会受到赞赏。
答案 0 :(得分:4)
这会成为PostgreSQL的潜在竞争条件吗?
是的,事实上它将出现在任何数据库引擎中。
解决此问题以避免此类情况的最佳方法是什么?
这是一个加载的问题,需要多个用户对数据库使用情况的深入了解。但是,我会给你一些选择。简而言之,您唯一的选择是在此过程中LOCK
表,但 锁定该表将取决于 如何 全天使用数据库。
让我们从基本LOCK
开始:
LOCK TABLE feed_channel
这将使用ACCESS EXCLUSIVE
锁定选项锁定表格。
与所有模式的锁冲突(ACCESS SHARE,ROW SHARE,ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE和ACCESS EXCLUSIVE)。 此模式可确保持有者是以任何方式访问该表的唯一交易。
现在,这是最严格的锁定,并且肯定会解决竞争条件,但可能不是您想要的。这是你必须要决定的事情。所以,虽然 clear 你将不得不LOCK
表,但它不是 clear 怎么样。
你还有什么决定?
LOCK
the table?研究该链接的锁定选项以做出决定。LOCK
表吗?或者换句话说,你想要LOCK
在该功能的 顶部 (我认为你根据可能的竞争条件做),或者做您只想在LOCK
?INSERT
醇>
是否有可能以原子方式创建整个存储过程,即它只能同时执行一次?
不,代码可以由连接到数据库的任何人执行。
我希望这有助于指导你。
答案 1 :(得分:4)
这里有一场不可避免的“比赛”,因为两场比赛都无法“看到”每个人的不受约束的行。在冲突中,会话只能回滚(可能是保存点)并重试。这通常意味着:引用另一个新插入的行,而不是创建私有副本。
这里有一个数据建模问题:feed_channel似乎有很多候选键,feed_content中的级联规则可以对很多feed_content的行进行保护(我认为content-> channel是1 :: M关系;多个内容行可以引用相同的通道)
最后,feed_channel表至少需要自然键{link,title}。这就是插入/不存在的全部内容。 (以及此功能的全部目的)
我清理了一下这个功能。不需要IF构造,执行 INSERT WHERE NOT EXISTS first 也可以,甚至更好。
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
CREATE TABLE feed_channel
( id SERIAL PRIMARY KEY
, name TEXT
, link TEXT NOT NULL
, title TEXT NOT NULL -- part of PK :: must be not nullable
, CONSTRAINT feed_channel_nat UNIQUE (link,title) -- the natural key
);
CREATE TABLE feed_content
( id SERIAL PRIMARY KEY
, channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE
, guid TEXT UNIQUE NOT NULL -- yet another primary key
, title TEXT --
, link TEXT -- title && link appear to be yet another candidate key
, description TEXT
, pubdate TIMESTAMP
);
-- NOTE: omitted original function channel_insert() for brevity
CREATE OR REPLACE FUNCTION channel_insert_wp(
p_link feed_channel.link%TYPE,
p_title feed_channel.title%TYPE
) RETURNS feed_channel.id%TYPE AS $body$
DECLARE
v_id feed_channel.id%TYPE;
BEGIN
INSERT INTO feed_channel(link,title)
SELECT p_link,p_title
WHERE NOT EXISTS ( SELECT *
FROM feed_channel nx
WHERE nx.link= p_link
AND nx.title= p_title
)
;
SELECT id INTO v_id
FROM feed_channel ex
WHERE ex.link= p_link
AND ex.title= p_title
;
RETURN v_id;
END;
$body$ LANGUAGE plpgsql;
SELECT channel_insert('Bogus_link', 'Bogus_title');
SELECT channel_insert_wp('Bogus_link2', 'Bogus_title2');
SELECT * FROM feed_channel;
结果:
DROP SCHEMA
CREATE SCHEMA
SET
NOTICE: CREATE TABLE will create implicit sequence "feed_channel_id_seq" for serial column "feed_channel.id"
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "feed_channel_pkey" for table "feed_channel"
NOTICE: CREATE TABLE / UNIQUE will create implicit index "feed_channel_nat" for table "feed_channel"
CREATE TABLE
NOTICE: CREATE TABLE will create implicit sequence "feed_content_id_seq" for serial column "feed_content.id"
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "feed_content_pkey" for table "feed_content"
NOTICE: CREATE TABLE / UNIQUE will create implicit index "feed_content_guid_key" for table "feed_content"
CREATE TABLE
NOTICE: type reference feed_channel.link%TYPE converted to text
NOTICE: type reference feed_channel.title%TYPE converted to text
NOTICE: type reference feed_channel.id%TYPE converted to integer
CREATE FUNCTION
NOTICE: type reference feed_channel.link%TYPE converted to text
NOTICE: type reference feed_channel.title%TYPE converted to text
NOTICE: type reference feed_channel.id%TYPE converted to integer
CREATE FUNCTION
channel_insert
----------------
1
(1 row)
channel_insert_wp
-------------------
2
(1 row)
id | name | link | title
----+------+-------------+--------------
1 | | Bogus_link | Bogus_title
2 | | Bogus_link2 | Bogus_title2
(2 rows)
答案 2 :(得分:3)
您最重要的问题是serial
没有成为feed_channel
表的主键。如果(link, title)
可以是(link)
,则主键应为title
或null
。然后,任何尝试插入现有的Feed都会引发主键错误。
v_id
为null
时, BTW title
将为null
:
WHERE link=p_link AND title=p_title