如何在Oracle / PostgreSQL中保护正在运行的列(MAX-result锁定之类的东西)

时间:2009-05-15 07:25:33

标签: oracle postgresql concurrency transactions

我需要有关Oracle / PostgreSQL的以下情况的建议:

我有一个带有“运行计数器”的数据库表,并希望在以下两种并发事务的情况下保护它:

T1                      T2
SELECT MAX(C) FROM TABLE WHERE CODE='xx'
-- C for new : result + 1
                        SELECT MAX(C) FROM TABLE WHERE CODE='xx';
                        -- C for new : result + 1
                        INSERT INTO TABLE... 
INSERT INTO TABLE... 

因此,在这两种情况下,INSERT的列值都是从一个添加的旧结果计算出来的。

由此,db处理的一些运行计数器就可以了。但这不起作用,因为

  • 计数器值或现有行有时会更改
  • 有时我希望有多个计数器“值组”(与提到的CODE一样):使用不同的CODE值,计数器将是独立的。

对于其他一些数据库,这可以通过SERIALIZABLE隔离状态来处理,但至少对于Oracle& Postgre,可以防止幻像读取,但结果表最终会有两个具有相同计数器值的不同行。这似乎与谓词锁定有关,锁定“查询覆盖的所有可能行” - 其他一些db:s最终会锁定整个表或其他东西..

SELECT ... FOR UPDATE -statements似乎用于其他目的,甚至似乎不适用于MAX()函数。

在列上设置UNIQUE约束可能是解决方案,但还有其他一些方法可以防止这种情况发生吗?

b.r。 Touko

编辑:另外一个选项可能是手动锁定,即使它对我来说不是很好..

5 个答案:

答案 0 :(得分:6)

OraclePostgreSQL都支持所谓的序列,非常适合您的问题。您可以拥有常规的int列,但每个组定义一个序列,并执行单个查询,如

--PostgreSQL
insert into table (id, ... ) values (nextval(sequence_name_for_group_xx), ... )

--Oracle
insert into table (id, ... ) values (sequence_name_for_group_xx.nextval, ... )

序列的增量是原子的,所以你的问题就不存在了。这只是创建所需序列的问题,每组一个。

答案 1 :(得分:1)

  
      
  • 计数器值或现有行有时会更改
  •   

如果这对您的应用来说是一个问题,您应该在该列上添加一个唯一约束。这样做可以保证SERIALIZABLE隔离级别的事务在尝试使用与另一个事务相同的id时将中止。

  

另一个选项可能是手动锁定,即使它对我来说不太好......

在这种情况下手动锁定非常简单:只需在选择最大值之前对表格进行SHARE UPDATE EXCLUSIVE或更强锁定。但这会破坏并发性能。

  
      
  • 有时我希望有多个计数器“值组”(与提到的CODE一样):使用不同的CODE值,计数器将是独立的。
  •   

这引出了我对这个问题的正确解决方案:序列。设置多个序列,每个“值组”一个,您希望在自己的范围内获取ID。有关序列及其使用方法的详细信息,请参阅Section 9.15 of The Manual;看起来它们非常适合你。序列永远不会给出相同的值两次,但可能会跳过值:如果事务从序列中获取值“2”并中止,则下一个事务将获得值“3”而不是“2”。

答案 2 :(得分:1)

序列答案很常见,但可能不对。此解决方案的可行性取决于您实际需要的内容。如果你在语义上想要的是“一些保证是唯一的数字”那么这就是一个序列的用途。但是,如果您想要的是确保您的值在每个插入上只增加一个(如您所要求的那样),那么请勿使用序列!我在自己面前陷入了这个陷阱。序列不保证是连续的!他们可以跳过数字。根据您配置的优化类型,他们可以跳过很多数字。即使你配置的东西恰到好处,你也不应该跳过任何数字,这是不能保证的,也不是那些序列的用途。所以,如果你(误)使用它们,你只会遇到麻烦。

更好的解决方案是将select捆绑到插入中,如下所示:

INSERT INTO table(code, c, ...) 
VALUES ('XX', (SELECT MAX(c) + 1 AS c FROM table WHERE code = 'XX'), ...);

(我没有测试运行该查询,但我很确定它应该可以工作。如果没有,我道歉。)但是,做这样的事情反映了你想要做的事情的语义意图。但是,这是低效的,因为你必须对MAX进行扫描,我从你的样本中得到的推论是你有相对于表大小的少量代码值,所以你要做一个每个插页都有昂贵的全表扫描。那不好。此外,这甚至没有为您提供您正在寻找的ACID保证。 select不以事务方式绑定到插入。您无法“锁定”MAX()函数的结果。因此,您仍然可以运行此查询的两个事务,它们都执行子选择并获得相同的最大值,两者都添加一个,然后两者都尝试插入。这是一个小得多的窗口,但你可能在技术上仍然存在竞争条件。

最终,如果你试图增加插入,我会挑战你可能有错误的数据模型。您应该使用唯一键插入,最常见的是序列值(至少作为任何自然键的简单代理键)。这样可以安全地插入数据。然后,如果您需要计算一些东西,那么请有一个表来存储您的计数。

CREATE TABLE code_counts (
    code VARCHAR(2), --or whatever
    count NUMBER
);

如果您确实要在插入每个项目时存储代码计数,单独的计数表也允许您在事务上正确地执行此操作,如下所示:

UPDATE code_counts SET count = count + 1 WHERE code = 'XX' RETURNING count INTO :count;
INSERT INTO table(code, c, ...) VALUES ('XX', :count, ...);
COMMIT;

关键是更新会锁定计数器表并为您保留该值。然后您的插入使用该值。所有这些都是作为一次交易变更而实施的。您必须在事务中执行此操作。拥有单独的计数表可以避免执行SELECT MAX()...的全表扫描。在本质上,它的作用是重新实现序列,但它也保证了顺序,有序使用。

在不知道你的整个问题域和数据模型的情况下,很难说,但是将你的计数抽象到一个单独的表中,你不需要做一个select max来获得正确的值可能是个好的理念。当然,假设计数是你真正关心的。如果您只是在进行日志记录或要确保事物是唯一的,那么请使用序列和时间戳进行排序。

请注意,我说不要按顺序排序。基本上,从不相信序列不是唯一的。因为当您在多节点系统上进行缓存序列值时,您的应用程序甚至可能会不按顺序使用它们。

答案 3 :(得分:0)

这就是为什么你应该使用Serial数据类型,它将C的查找推迟到插入时间(使用我假设的表锁)。然后,您不会指定C,但会自动生成。如果您需要C进行某些中间计算,则需要先保存,然后读取C,最后使用派生值进行更新。

编辑:对不起,我没有看完你的整个问题。如何通过规范化解决其他问题?只需为每种特定类型(对于每个x,其中A ='x')创建第二个表,其中您有另一个自动增量。手动编辑的序列可以是同一个表中的另一列,它使用生成的序列作为基础(即如果pk = 34,则可以使用另一列mypk ='34Changed')。

答案 4 :(得分:0)

您可以使用序列作为默认值来创建顺序匹配:

首先,您必须创建序列计数器:

CREATE SEQUENCE SEQ_TABLE_1 START WITH 1 INCREMENT BY 1;

因此,您可以将其用作默认值:

CREATE TABLE T (
  COD NUMERIC(10)  DEFAULT NEXTVAL('SEQ_TABLE_1') NOT NULL,
  collumn1...
  collumn2...
);

现在您不必担心插入行的顺序:

INSERT INTO T (collumn1, collumn2) VALUES (value1, value2);

问候。