postgresql - 使用事务块的脚本无法创建所有记录

时间:2013-08-01 18:52:29

标签: postgresql lua locking database-deadlocks

背景资料

我正在努力了解postgresql如何做得足够好以便我可以确保当用户同时尝试更新同一记录时我的网络应用程序不会失败。

出于测试目的,我创建了两个脚本 - 一个脚本创建/定义事务块,第二个脚本通过调用脚本1来模拟加载(并尝试创建冲突)。

代码

以下是脚本1的样子:

  

http://pastebin.com/6BKyx1dW

以下是脚本2的样子:

  

http://pastebin.com/cHUhCBL2

为了模拟数据库上的负载并测试锁定问题,我从我服务器上的两个不同的命令行窗口调用脚本2。我传入两组不同的参数,这样当我在数据库中分析结果时,我可以看到哪个会话创建了哪条记录。

问题

当我查询数据库以计算每个脚本实例创建的记录数时,我并不是每次都得到200个。 我已经从脚本的每个实例中捕获了结果,以查看是否记录了任何回滚,但没有。 所以我有两个理论。

  1. 执行这些脚本的服务器不够健壮,因此请求没有进入数据库服务器......
  2. 数据库服务器以静默方式中止事务。
  3. 为了消除理论1,我将设置两个不同的服务器并从每个服务器运行一次脚本,而不是在1台服务器上打开2个命令行。 如果创建的记录数量增加......我想这会告诉我当前服务器上的性能是个问题。 (我正在运行脚本的“服务器”只是一个美化的桌面......所以它很可能是问题所在。)

    关于理论2,我一直在努力阅读和理解http://www.postgresql.org/docs/current/static/explicit-locking.html 但由于我不是数据库专家,所以我需要花一点时间来消化所有内容。 我知道,对于MS SQL Server,如果记录被事务A锁定,事务B将无限期地等待,直到A完成。 使用SQLLite,开箱即用,事务B终止。但您可以指定重试前等待的毫秒数。

    我在上面列出的postgresql文档中的最后一段说postgresql也会无限期地等待释放冲突的锁...但我不是百分之百确定我在我的sql中没有弄乱一些东西代码。

    所以我的问题如下:

    1. 我在sql代码中做了哪些明显错误的事情?
    2. 我如何测试sql方面的东西,看看正在使用什么锁/在幕后发生了什么?
    3. 编辑1

      我从两台独立的机器再次运行脚本。和机器1成功创建了122条记录,机器2创建了183条。

1 个答案:

答案 0 :(得分:1)

是的,你做错了。
看一下简单的例子。

第1节

postgres=# select * from user_reservation_table;
 id | usedyesno | userid | uservalue
----+-----------+--------+-----------
  1 | f         |      0 |         1
  2 | f         |      0 |         2
  3 | f         |      0 |         3
  4 | f         |      0 |         4
  5 | f         |      0 |         5
(5 wierszy)


postgres=# \set user 1
postgres=#
postgres=# begin;
BEGIN
postgres=# UPDATE user_reservation_table
postgres-# SET UsedYesNo = true, userid=:user
postgres-# WHERE uservalue IN(
postgres(#    SELECT uservalue FROM user_reservation_table
postgres(#    WHERE UsedYesNo=false Order By id ASC Limit 1)
postgres-# RETURNING uservalue;
 uservalue
-----------
         1
(1 wiersz)


UPDATE 1
postgres=#


会话2 - 同时,但仅在10毫秒后

postgres=# \set user 2
postgres=# begin;
BEGIN
postgres=# UPDATE user_reservation_table
postgres-# SET UsedYesNo = true, userid=:user
postgres-# WHERE uservalue IN(
postgres(#    SELECT uservalue FROM user_reservation_table
postgres(#    WHERE UsedYesNo=false Order By id ASC Limit 1)
postgres-# RETURNING uservalue;

会话2挂起.......正在等待什么......

回到第1节

postgres=# commit;
COMMIT
postgres=#



再次第2节

 uservalue
-----------
         1
(1 wiersz)


UPDATE 1
postgres=# commit;
COMMIT
postgres=#

第2节不再等待,并完成交易。

什么是最终结果?:

postgres=# select * from user_reservation_table order by id;
 id | usedyesno | userid | uservalue
----+-----------+--------+-----------
  1 | t         |      2 |         1
  2 | f         |      0 |         2
  3 | f         |      0 |         3
  4 | f         |      0 |         4
  5 | f         |      0 |         5
(5 wierszy)

两个用户使用相同的值1,但只有用户2在表

中注册





======================编辑=========================== =======

在这种情况下,我们可以使用SELECT .. FOR UPDATE并利用postgre在Read Committed Isolation Level模式下重新评估查询的方式,
请参阅文档:http://www.postgresql.org/docs/9.2/static/transaction-iso.html

  

UPDATE,DELETE,SELECT FOR UPDATE和SELECT FOR SHARE命令   在搜索目标行方面表现与SELECT相同:它们   将只查找从命令start开始提交的目标行   时间。但是,这样的目标行可能已经更新(或   当它被另一个并发事务删除或锁定时   找到。在这种情况下,可能的更新程序将等待第一个   更新事务以提交或回滚(如果它仍在   进展)。如果第一个更新程序回滚,那么它的效果是   否定,第二个更新程序可以继续更新   最初找到了一排。如果第一个更新程序提交,则第二个   如果第一个更新程序删除它,updater将忽略该行,否则   它将尝试将其操作应用于更新版本   行。命令的搜索条件(WHERE子句)是   重新评估以查看该行的更新版本是否仍然匹配   搜索条件。如果是,则第二个更新程序继续执行   使用更新版本的行进行操作。在SELECT的情况下   FOR UPDATE和SELECT FOR SHARE,这意味着它是更新版本   锁定并返回给客户端的行。

简而言之:
如果一个会话锁定了该行,而另一个会话正在尝试锁定同一行,则第二个会话将“挂起”并等待第一个会话提交或回滚。 当第一个会话提交事务时,第二个会话将重新评估WHERE搜索条件。 如果搜索条件不匹配(因为第一个transacion改变了某些列),那么第二个会话将跳过该行, 并将处理与WHERE条件匹配的下一行。

注意:此行为在可重复读取隔离级别中有所不同。 在这种情况下,第二个会话将抛出错误:由于并发更新而无法序列化访问,您必须在整个事务中重试。

我们的查询可能如下:

select id from user_reservation_table
where usedyesno = false
order by id
limit 1
for update ;

然后:

  Update .... where id = (id returned by SELECT ... FOR UPDATE)



就个人而言,我更喜欢使用普通的旧控制台客户端测试锁定风格(psql用于postgree,mysql或SQLPlus用于oracle)

所以让我们在psql中测试我们的查询:

session1 #select * from user_reservation_table order by id;
 id | usedyesno | userid | uservalue
----+-----------+--------+-----------
  1 | t         |      2 |         1
  2 | f         |      0 |         2
  3 | f         |      0 |         3
  4 | f         |      0 |         4
  5 | f         |      0 |         5
(5 wierszy)


session1 #begin;
BEGIN
session1 #select id from user_reservation_table
postgres-# where usedyesno = false
postgres-# order by id
postgres-# limit 1
postgres-# for update ;
 id
----
  2
(1 wiersz)


session1 #update user_reservation_table set usedyesno = true
postgres-# where id = 2;
UPDATE 1
session1 #

会话1已锁定并更新了行id = 2

现在session2

session2 #begin;
BEGIN
session2 #select id from user_reservation_table
postgres-# where usedyesno = false
postgres-# order by id
postgres-# limit 1
postgres-# for update ;

会话2在尝试锁定行id = 2时挂起

好的,让我们提交会话1

session1 #commit;
COMMIT
session1 #

看看会话2中会发生什么:

postgres-# for update ;
 id
----
  3
(1 wiersz)

Bingo - 会话2跳过行id = 2并选择(并锁定)行id = 3


所以我们的最终查询可能是:

update user_reservation_table
set usedyesno = true
where id = (
   select id from user_reservation_table
   where usedyesno = false
   order by id
   limit 1
   for update
) RETURNING uservalue;

一些保留 - 此示例仅用于您的测试目的,其目的是帮助了解锁定在postgre中的工作方式。
实际上,此查询将序列化对表的访问,并且不可扩展,并且可能在多用户环境中执行不良(慢)。 想象一下,10个会话同时尝试从该表中获取下一行 - 每个会话都会挂起,并等待上一个会话提交 因此,请勿在生产代码中使用此查询 你真的想“从表中找到并保留下一个价值”吗?为什么? 如果是,您必须有一些序列化设备(如此查询,或者,可能更容易实现,锁定整个表), 但这将是一个瓶颈。