我正在编写一个简单的消息传递程序,其中有一个消息表,用户可以声明该表并由该用户对他们进行处理。没有预定哪个用户将声明给定消息,因此我想要一个查询来选择我拥有的所有可用消息中的第一个,然后再将该消息标记为接受,这也是我拥有的。问题是,我不希望两个用户同时使用它来声明相同的消息,所以我想连续运行这两个语句,而不必返回程序来找出接下来要运行的内容语句之间。我相信我可以通过用分号分隔它们来运行两个连续的语句,但我想将第一个查询中返回的数据用作第二个查询的一部分。变量将是完美的,但据我所知,它们在 SQL 中不存在。
5 回答
这就是 BEGIN TRAN 和 COMMIT TRAN 的用途。将要保护的语句放在事务中。
有什么方法可以保留查询之间的状态吗?
不,SQL 不是过程语言。您可以将两个查询重写为单个查询(并非总是可能,即使可能,通常也不值得),或者使用过程语言将它们粘合在一起。许多 SQL 服务器为此提供了一种内置语言(“存储过程”),或者您可以在应用程序中执行此操作。
问题是,我不希望两个用户同时使用它来声明相同的消息
使用锁。我不知道您使用的是什么 SQL 服务器,但SELECT ... FOR UPDATE
如果它可用的话,听起来就像您想要的那样。
正如 le dorfier 所说,交易是一个不错的选择,但也有替代方案:
您可以先进行更新,即使用用户 ID 或类似名称标记消息。您没有提到您使用的是哪种 sql 风格,但在 mysql 中,我认为它看起来像这样:
UPDATE message
SET user_id = ...
WHERE user_id = 0 -- Ensures no two users gets the same message
LIMIT 1
在 ms sql 中,它类似于以下内容:
WITH q AS (
SELECT TOP 1
FROM message m
WHERE user_id = 0
)
UPDATE q
SET user_id = 1
/乙
您也许可以使用临时表。
SQL 本身没有变量,但(几乎?)所有 RDBMS SQL 扩展都有。但是,我不确定仅凭这一点如何解决您的问题。
如前所述,事务将起到作用 - 有效地将您的 2 个不相关的语句组合在一起。但是,默认事务级别将不起作用。(大多数?)RDBMS 服务器的默认事务级别是 READ COMMITTED。这不会阻止用户 2 读取用户 1 读取的同一行。为此,您需要使用 REPEATABLE READ 或 SERIALIZABLE。
这是一个经典的并发问题。通常,处理它的两种方法是悲观锁定或乐观检查。REPEATABLE READ 事务将是悲观的(无论是否需要都会产生锁定费用),而检查@@ROWCOUNT 是乐观的(假设它会工作,但在@@ROWCOUNT = 0 时做一些明智的事情)。
通常,我们使用乐观(锁定是昂贵的),并且使用时间戳或读取的字段组合来确保我们正在更改我们认为的数据。因此,我的建议是包含一个 rowversion 或 timestamp 字段,并将其传递回您的 UPDATE 语句。然后,检查@@ROWCOUNT 以查看您是否更新了任何记录。如果没有,请返回并选择另一条消息。在伪代码中:
int messageId, byte[] rowVersion = DB.Select(
"SELECT TOP 1
MessageId, RowVersion
FROM Messages
WHERE
User IS NULL";
int rowsAffected = DB.Update(
"UPDATE Messages SET
User = @myUserId
WHERE
MessageId = @messageId
AND RowVersion = @rowVersion",
myUserId, messageId, rowVersion
);
if (rowsAffected = 0)
throw new ConcurrencyException("The message was taken by someone else");
根据您的特定语句,您可能只需在 UPDATE 语句中重复“UserId IS NULL”WHERE 子句即可。这类似于 Brimstedt 的解决方案 - 但您仍然必须检查 @@ROWCOUNT以查看行是否实际更新。