SQL 行锁和事务

Posted

技术标签:

【中文标题】SQL 行锁和事务【英文标题】:SQL row locks and transactions 【发布时间】:2019-04-20 15:29:37 【问题描述】:

我对关系数据库真的很陌生。我正在从事一个涉及财务的项目,因此我希望不会同时发生任何影响平衡的操作,并且我想使用锁来实现这一点,但是我不确定如何使用它们。我现在的愿景: 我想为每个操作创建一个单独的表,并在 users 表中创建一个 balance 字段,其值将来自所有相关表。可悲的是,我实际上永远不会更新现有记录 - 只会添加它们。我想确保在这些表中一次只为每个用户插入一条记录。例如:3 个事务同时发生,因此 3 条记录将被添加到任何相关表中。其中两条记录具有相同的用户 ID,即我的 users 表的外键,另一条具有不同的。我希望将具有相同外键的记录流水线化,并且可以随时完成另一个记录。我如何实现这一目标?有没有更好的方法来解决这个问题?

【问题讨论】:

【参考方案1】:

我希望任何影响平衡的动作不要同时发生

为什么?

我想用锁来实现这一点

为什么?

给你一个反例。假设您想避免出现负账户余额。当用户提取 500 美元时,如何在没有锁的情况下进行建模。

UPDATE accounts
   SET balance = balance - 500
 WHERE accountholderid = 42
   AND balance >= 500

这在没有任何显式锁的情况下工作,并且对于并发访问是安全的。您必须检查更新计数,如果为 0,您将透支帐户。

(我知道 mysql 仍然会获取行锁)

拥有一个分类帐仍然是有意义的,但即使在那里对我来说对锁的需求也不是很明显。

【讨论】:

有人告诉我,将余额存储为普通数字是一种不好的做法。当它可以从我记录的数据中导出时,为什么要存储它呢?我需要确保不会同时发生涉及余额 51% 以上的交易。否则,该用户的派生余额最终将变为负数。 您是否被告知了原因?为了确定帐户余额,您必须汇总的最大记录数是多少?你对“同时”的定义是什么?我给出的示例代码中的余额如何变为负数? 不,我没有得到任何理由,但这对我来说很有意义。最大记录数是无限的。同时对我来说,如果我的节点应用程序大致同时启动 2 个不同的任务,当一个任务在另一个启动之前没有影响数据库,所以两者都使用相同的派生值作为用户余额,并且每个可能都可以创建用完余额 51% 以上的新记录。 如果最大记录数是无限的,那么您需要将无限量的记录相加才能得出帐户余额。你看这是一个问题吗?同样,显示的查询避免了这个问题,也避免了三个“并发”事务的问题。 我知道并发和性能下降。然而,安全不是更重要吗?我宁愿排除查询中出现错误的可能性,该错误会永久增加某人的余额大量并让他们破产您的业务。【参考方案2】:
    为所有表格使用ENGINE=InnoDB

    使用交易:

    BEGIN;
    do all the work for a single action
    COMMIT;
    

单个操作的经典示例是从一个帐户中取出资金并将其添加到另一个帐户。删除将包括检查透支,在这种情况下,您将拥有ROLLBACK 而不是COMMIT 的代码。

您获得的锁可确保单个操作的所有内容要么完全完成,要么什么都不做。这甚至适用于系统在BEGINCOMMIT 之间崩溃的情况。

没有 begin 和 commit,但 autocommit=ON,每条语句都被 begin 和 commit 隐式包围。那就是先前答案中的UPDATE 示例是“原子的”。但是,如果从一个帐户中扣除的钱需要添加到另一个帐户,如果在UPDATE之后发生崩溃会发生什么?钱不见了。所以,你真的需要

 BEGIN;
 if not enough funds, ROLLBACK and exit
 UPDATE to take money from one account
 UPDATE to add that money to another account
 INSERT into some log or audit trail to track all transactions
 COMMIT;

在每一步后检查 -- ROLLBACK 并对任何意外错误采取规避措施。

如果两个(或更多)动作“同时”发生会怎样?

一个等待另一个。 出现死锁并对其强制执行 ROLLBACK。

但是,在任何情况下,数据都不会被弄乱。

进一步说明...在某些情况下您需要FOR UPDATE

BEGIN;
SELECT some stuff from a row FOR UPDATE;
test the stuff, such as account balance
UPDATE that same row;
COMMIT;

FOR UPDATE 对其他线程说:“请不要把手放在这一行上,我可能会更改它;请等我完成。”如果没有FOR UPDATE,另一个线程可能会潜入并耗尽您认为存在的资金的帐户。

评论你的一些想法:

对于许多用户及其帐户来说,一个表通常就足够了。它将包含每个帐户的“当前”余额。我提到了一个“日志”;那将是一个单独的表格;它将包含“历史”(而不仅仅是“当前”信息)。 FOREIGN KEYs 在本次讨论中大多无关紧要。它们有两个目的:验证另一个表是否有应该存在的行;并隐式创建 INDEX 以加快检查速度。 流水线?如果您每秒执行的“事务”不超过 100 次,那么您只需担心 BEGIN..COMMIT 逻辑。 “同时”和“同时”是误用的术语。两个用户“同时”访问数据库的可能性很小——考虑浏览器延迟、网络延迟、操作系统延迟等。再加上大多数这些步骤迫使活动进入单个文件的事实。网络强制一条消息先于另一条消息到达那里。同时,如果您的“交易”之一需要 0.01 秒,谁在乎“同时”请求是否必须等待它完成。关键是,如果需要,我所描述的将强制“等待”以避免弄乱数据。

话虽如此,仍然可能有一些“同时”——如果事务没有触及相同的行,那么从BEGINCOMMIT 所花费的几毫秒可能会重叠。考虑一下几乎同时发生的两笔交易的时间线:

BEGIN;  -- A
pull money from Alice  -- A
      BEGIN;   -- B
      pull money from Bobby  -- B
give Alice's money to Alan  -- A
      give Bobby's money to Betty  --B
COMMIT;   --A
      COMMIT;  --B

【讨论】:

以上是关于SQL 行锁和事务的主要内容,如果未能解决你的问题,请参考以下文章

mysql之innodb引擎的行锁和表锁

数据库中的行锁和表锁

MySQL锁和事务:InnoDB锁(MySQL 官方文档粗翻)

关于数据库行锁与表锁的认识

关于mysql 共享锁和排他锁 互斥问题?

锁机制