防止在仅插入的 postgres 中双重花费 db/sqlalchemy

Preventing double spend in insert-only postgres db/sqlalchemy

我想在一组仅插入表(带有 postgres 后端)中跟踪金融交易(以应用程序内货币表示)。我们如何定义它的一个最小示例如下:

class Balance(db.Model, CreatedTimestampMixin):
    """Balances table"""

    __tablename__ = "balances"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
    balance = db.Column(db.Integer, default=0)

    # one-to-one each balance change has a transaction
    transaction = db.relationship(
        'Transaction', uselist=False, backref=db.backref('balance'))

    def __repr__(self):
        return f"<Balance {self.id}>"


class Transaction(db.Model, CreatedTimestampMixin):
    """ Transactions that cause balance changes """

    __tablename__ = "transactions"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    balance_id = db.Column(db.Integer, db.ForeignKey(
        "balances.id"), nullable=False)
    amount = db.Column(db.Integer, nullable=False)

    def __repr__(self):
        return f"<Transaction {self.id}>"

在这张图中,交易发生时,采取了以下步骤:

  1. 获取给定用户的最后余额
  2. 在余额中新建一行,余额为上次余额 + 金额(金额可以为负)
  3. 使用新余额和金额的外键在交易中创建一个新行。

但是,如果两个不同的交易请求同时出现,这可能会造成双花(提取旧余额,然后输入两个交易)。我也会确保旧余额 + 金额 >=0,但如果我允许双花,那么我可以允许低于 0。

如何避免双重 spend/ensure 交易被一个接一个地处理?我有点理解解决方案可以通过创建某种锁或强制执行 parent/child 排序,但不确定实现它的正确方法是什么....

做到这一点的规范方法是使用

序列化余额的读取
SELECT ... FOR NO KEY UPDATE

这会在数据库事务完成之前锁定该行并阻止其他此类语句。

请注意,如果您希望该方法有效,则必须保持数据库事务简短。另一种方法是使用“乐观锁定”。