-1

我希望在 API 中混合表锁和事务。我选择了 MySQL 5.7 作为存储引擎,尽管我并不特别致力于任何特定的 RDBMS。在深入研究之前,我有兴趣了解我当前的设计是否适合 MySQL。

在我的设计中,我有三个表:

  • expenditure( id, amount, title)
  • donation( id, amount, title)
  • contribution( id, amount, expenditure_id, donation_id)

如您所见,这是两个表之间的简单多:多,contribution作为桥接表。我已经设置了约束,以便强制执行这种关系。

该应用程序基本上是一个慈善机构的会计程序 - 管理员创建捐赠条目(来自捐赠者的钱)和支出(用于慈善目的的钱)。管理员有时会通过在桥接表中创建条目来将捐款分配给支出。大额捐款可以用于几笔支出,大笔支出会消耗几笔捐款。

我感兴趣的是如何在 MySQL 中创建这些桥接条目。我想我会有一个像这样的伪代码的函数:

function addContributions(int expenditureId, array contributions)

因此,假设我的支出为 5000,并且我有两笔捐款(id 1 和 2),金额分别为 3000 和 2500。我想要 3000(所有第一次捐款)和 2000(部分第二次捐款)的捐款。贡献行如下所示:

[
  {expenditure_id: 1, donation_id: 1, amount: 3000},
  {expenditure_id: 1, donation_id: 2, amount: 2000}
]

我有一些相关的观察:

  • 我有一些验证要做,例如确保捐款没有附加到其他地方
  • 我希望能够在出现错误的情况下回滚
  • 我希望我的验证在编写这些记录期间保持真实,即防止在检查数据库和编写记录之间出现竞争条件
  • 我认为这意味着我想在中央表上写一个表锁

这是我的代码粗略草图(节点,但语言无关紧要):

export default class ContributionWriter {

  addContributions(expenditureId, contributions) {

    this.startLock();
    try {
      this.startTransaction();
      this.addContributionsWork();
      this.commit();
    }
    catch (error) {
      this.rollback();
    }
    finally {
      this.endLock();
    }
  }

  addContributionsWork() {
    // Initial validation
    this.detectInvalidContributions();
    this.detectTooManyContributions();
    this.detectOverCommittedDonations();

    // Write contribs
    this.writeContributions();
  }

  /**
   * Write the contributions, validations passed OK
   */
  writeContributions() {
    // DELETE FROM contribution WHERE id_expenditure = x
    // Loop for all contributions:
    //   INSERT INTO contribution ...
  }

  /**
   * Prevent any writes to the table(s) during the lock
   */
  startLock() {
    // LOCK TABLES contribution
  }

  /**
   * Releases the locked table(s)
   */
  endLock() {
    // UNLOCK TABLES
  }

  /**
   * Runs a `SET autocommit = 0` to start the transaction
   */
  startTransaction() {

  }

  /**
   * Runs a ROLLBACK op to cancel any pending changes
   */
  rollback() {

  }

  commit() {

  }

}

该手册给出了这个建议

将 LOCK TABLES 和 UNLOCK TABLES 与事务表(例如 InnoDB 表)一起使用的正确方法是使用 SET autocommit = 0(不是 START TRANSACTION)开始事务,然后是 LOCK TABLES,并且在提交事务之前不要调用 UNLOCK TABLES明确的

但是它也说(在同一页面上):

LOCK TABLES 不是事务安全的,并且在尝试锁定表之前隐式提交任何活动事务

所以,我认为这些东西是不相容的。我想使用事务以便在出现问题的情况下能够回滚,这样数据库就不会处于不一致的状态。我还想使用写锁,以便我的验证在表被解锁之前保持正确,否则可能会发生竞争条件,从而超支捐赠(由于另一个用户分配了相同的捐赠之一)。

但是如果LOCK TABLES提交一个开放的事务,那么我会不会有这种情况(时间向下流动)?

User 1                  | User 2
------------------------|------------------------
lock tables (write)     |
start tx                |
validation checks       |
insert A                |
                        | lock tables (write)
                        | start tx
                        | validation checks
insert B                |
error                   |
rollback                |
unlock tables           |

我想知道这里发生了什么,lock tables因为用户 2 对用户 1 的事务进行了隐式提交,rollback用户 1 没有进行完全回滚,因为已经发出了一些提交。

或者用户 2 的锁表是否基于另一个会话具有表锁而强制等待,并且它暂停操作直到用户 1 释放锁?

4

1 回答 1

2

表锁并不是真正为并发控制而设计的,因为这些锁将除一个用户之外的所有用户都锁定在表外。您可以在一次性情况下使用它们,例如特殊维护任务,以确保没有其他进程干扰您的工作。

我会在支出和捐款表中添加一个未结金额字段,以指示尚未匹配的金额(是的,这是一个非规范化字段)。

我会将插入/更新/删除触发器添加到桥表中,当您创建/修改/删除匹配条目时,触发器将通过从字段值中减去/添加匹配值来更新未结金额字段。由于更新操作需要对正在更新的记录进行排他锁,触发器将有效地将对未完成金额字段的修改排队。触发器可以强制该值不能低于 0 并在低于 0 时引发错误。

于 2020-05-12T00:55:40.283 回答