【问题标题】:How to conditional update SQL tables?如何有条件地更新 SQL 表?
【发布时间】:2021-06-14 10:51:18
【问题描述】:

我有两个这样的数据库表:

CREATE TABLE IF NOT EXISTS accounts (
  id        INT PRIMARY KEY NOT NULL,
  balance   INT NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS transactions (
  from      INT FOREIGN KEY REFERENCES accounts(id),
  to        INT FOREIGN KEY REFERENCES accounts(id),
  amount    INT NOT NULL
)

在我的应用程序逻辑中,每当有新的交易请求进来时,我想先检查 from 账户是否有足够的余额,然后我想从 from 账户中减去金额,将金额添加到 to 账户,然后添加事务表的新行。

但是,我想以原子方式完成所有这些工作,原因有两个:

  • 防止竞争条件。如果有两个请求将 5 美元从账户 1 转移到账户 2,而账户 1 总共只有 5 美元,我想防止余额发生双重支出。因此,余额充足的检查和余额的后续更新必须以原子方式进行。
  • 防止应用程序崩溃时出现不一致的状态。

是否可以在单个 SQL 语句中执行此操作?假设我们使用的是 Postgres。

我知道例如我可以通过UPDATE accounts SET balance = balance - 6 WHERE id = 1 and balance > 6; 来检查帐户是否有足够的余额并同时更新余额。

我也听说过一个叫select ... for update 的东西,但我不确定这是否能帮到我。

另外,查询的结果应该表明交易是否成功,如果不成功,我可以向客户端显示一些错误消息?

【问题讨论】:

  • 假设我们正在使用 PostGres。 如果是这样,那么标记 [postgresql] 并取消标记 [mysql]。
  • @kmoser 不是真的,这只是展示了如何使用事务。这如何解决我的问题?在添加到帐户之前,我仍然需要通过某种方式检查余额是否足够
  • @shmth Transactions 允许您将多个 SQL 语句视为原子操作。这不是你想做的吗?
  • @kmoser 但我也想防止问题中提到的竞争条件。事务对此没有帮助,因为两个事务可以交错。

标签: sql postgresql


【解决方案1】:

解决方案不是将所有内容都塞进一条 SQL 语句中。除了复杂之外,它不一定能保护您免受竞争条件的影响。

使用数据库事务。为避免异常,请使用SELECT ... FOR UPDATE 锁定您读取的所有内容以防止并发修改,或者使用REPEATABLE READ 隔离级别并在出现序列化错误时重复事务。

为了保护自己免受死锁,请确保始终先锁定具有较低id 的帐户。

这是一个伪代码示例:

EXEC SQL START TRANSACTION;

if (id1 < id2) {
    EXEC SQL SELECT balance INTO :bal1 FROM accounts
         WHERE id = :id1 FOR UPDATE;
    EXEC SQL SELECT balance INTO :bal2 FROM accounts
         WHERE id = :id2 FOR UPDATE;
} else {
    EXEC SQL SELECT balance INTO :bal2 FROM accounts
         WHERE id = :id2 FOR UPDATE;
    EXEC SQL SELECT balance INTO :bal1 FROM accounts
         WHERE id = :id1 FOR UPDATE;
}

if (bal1 < amount) {
    EXEC SQL ROLLBACK;
    throw_error('too little money on account');
}

EXEC SQL UPDATE accounts SET balance = :bal1 - :amount WHERE id = :id1;
EXEC SQL UPDATE accounts SET balance = :bal1 + :amount WHERE id = :id2;
EXEC SQL INSERT INTO transaction ("from", "to", amount)
    VALUES (:id1, :id2, :amount);

EXEC SQL COMMIT;

【讨论】:

  • 谢谢,有几个后续问题: 1. 在继续更新表格之前,如何先检查第一个用户的余额是否足够? 2. sql查询是否返回一些状态,以便我知道操作是否真正成功?
  • 1.运行SELECT。 2.如果一个语句失败,总是会报错。
  • 你能提供一个SQL语句的例子吗?我仍然不明白我该怎么做 1. 在伪代码中,我想要类似:``` select * from accounts where id = i;如果 balance > 6: 更新账户 set balance = balance - 6 where id = 1 update accounts set balance = balance + 6 where id = 2 insert into transactions 1, 2, 6 ```
  • 另一种选择是不检查代码中的足够余额。而是在列上放置一个检查约束。所以:alter table accounts 添加约束 bal_ge_0 check (balance >= 0);
猜你喜欢
  • 2018-06-27
  • 1970-01-01
  • 1970-01-01
  • 2021-09-12
  • 2022-01-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多