0

背景资料

我试图了解 postgresql 如何足够好,以便我可以确保当用户同时尝试更新同一记录时我的 web 应用程序不会失败。

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

代码

下面是脚本 1 的样子:

http://pastebin.com/6BKyx1dW

这是脚本二的样子:

http://pastebin.com/cHUhCBL2

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

问题

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

  1. 执行这些脚本的服务器不够健壮,因此请求没有发送到数据库服务器......
  2. 数据库服务器正在静默中止事务。

为了消除理论 1,我将设置两个不同的服务器并从每个服务器运行一次脚本,而不是在 1 个服务器上打开 2 个命令行。如果创建的记录数量增加......我想这会告诉我当前服务器上的性能是一个问题。(我目前正在运行脚本的“服务器”只是一个美化的桌面......所以它很可能是问题所在)。

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

我在上面列出的 postgresql 文档的最后一段说 postgresql 也将无限期地等待释放冲突的锁......但我不能 100% 确定我没有在我的 sql 代码中搞砸了。

所以我的问题如下:

  1. 我在我的 sql 代码中做任何明显错误的事情吗?
  2. 我如何测试事物的 sql 方面以查看正在使用哪些锁/引擎盖下发生了什么?

编辑 1

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

4

1 回答 1

2

是的,你做错了什么。
看简单的例子。

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

简而言之:
如果一个会话锁定了该行,而另一个会话试图锁定同一行,那么第二个会话将“挂起”并等待第一个会话提交或回滚。当第一个会话提交事务时,第二个会话将重新评估 WHERE 搜索条件。如果搜索条件不匹配(因为第一个事务更改了某些列),那么第二个会话将跳过该行,并处理与 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

现在会话 2

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 个会话同时尝试从该表中获取下一行 - 每个会话都将挂起并等待上一个会话提交。
所以不要在生产代码中使用这个查询。
您真的要“从表中查找并保留下一个值”吗?为什么 ?
如果是,你必须有一些序列化设备(比如这个查询,或者,可能更容易实现,锁定整个表),但这将是一个瓶颈。

于 2013-08-03T00:44:20.713 回答