【问题标题】:Handling concurrency in PostgreSQL transactions处理 PostgreSQL 事务中的并发性
【发布时间】:2021-09-15 03:15:24
【问题描述】:

我不熟悉 Postgres 并发和锁模型,所以我认为可能有一个解决方案可以解决我遇到的问题,而无需在代码中使用显式锁。 我正在集成一项支付网关服务。每次付款时,我都会收到通知并将总和添加到用户的余额中。 现在问题来了,我收到 2 条具有相同有效负载的消息的可能性很小,但理论上的机会。即单笔交易的两次付款通知。问题是避免在用户余额中添加双倍金额。

代码如下:

        var deposit = await _repository.GetDepositByGatewayDepositId(ipn.DepositId);
        var oldStatus = default(DepositStatus);
        if (deposit == null)
        {
            deposit = _converter.ToDeposit(model, status, ipn.Id, user.Id);
        }
        else
        {
            oldStatus = deposit.Status;
            deposit.IpnId = ipn.Id;
            deposit.Updated = DateTime.UtcNow;
            deposit.Status = status;
        }
        await _repository.SaveAsync(deposit);

        if (deposit.Status == DepositStatus.Completed && oldStatus != DepositStatus.Completed)
        {
            await _repository.AddBalance(user.Id, ipn.Amounti);
        }

仅当先前的付款状态(oldStatus 变量)不是CompletedPending)时,我才更新用户的余额,但是,我从数据库中获取 oldStatus,并且如果有重复的请求并且第二笔交易是也在进行中,我最终会遇到一个竞争条件。

所以我的问题是,有没有一种有效的方法可以在不使用代码锁的情况下解决问题?也许告诉 Postgres 使用行级锁来阻止其他事务的读取,直到第一个事务完成?

更新: 感谢大家的回复。我想我会为这个主题添加更多细节。当 trans 状态发生变化时,我可能会收到同一笔交易的多条消息(即相同的 ID)。好的部分是消息是数字签名的,所以我已经在签名字段上添加了唯一约束。但是支付网关声明任何状态 >= 100 都是成功状态。所以理论上,如果他们给我发送状态 100,然后是 101,理论上这可能会导致竞争状况和财务后果。同样,机会可能非常低,但我宁愿安全而不是抱歉。

根据回复,最优雅的解决方案似乎是乐观锁定。但是,对于这种特殊情况,我认为分布式锁定可能是更好的选择,因为它似乎更简单。仅为一位特定客户获取锁定,因此客户A 的锁定不会导致客户B 的等待锁定。

谢谢

【问题讨论】:

  • 为了避免锁定,您可以使用“乐观锁定”(带有溢出桶和随机重试时间)。它的性能要高得多。或者...您可以序列化交易。或者,您可以在应用程序中使用队列。或者您可以使用显式锁。不过,对于大批量,我仍然更喜欢第一种选择。

标签: .net postgresql concurrency


【解决方案1】:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 会将您的事务置于可序列化模式,这基本上意味着没有事务会干扰您的编辑:只要它们也是可序列化模式。但这可能会带来很大的性能成本,如果(以及何时)失败,您必须准备好重试您的事务。或者 Postgres 有咨询锁。这两个功能都有一点必读。 Impaler 的评论提到了这个和其他选项。但也许有办法避免这种情况......

这些通知确实应该有 ID,就像您描述它们的方式一样。 IpnId = ipn.Id; 脱颖而出。那些是独一无二的吗?您可以对(payment_id, status) 的组合设置一个独特的约束,这样两笔交易就不可能竞争并且两者(或两者都不)认为他们是第一个标记付款完成的。即使是默认的、不可序列化的隔离级别也能保证这一点,因此您可以避免处理可序列化模式。

一个可能更优雅的解决方案是进一步规范您的架构:不要为存款和余额设置单独的表。仅在需要时跟踪存款,并从中获取用户的余额(以及可能的其他表格,如“取款”等)。那么您不必担心确保这两个表彼此一致。就我个人而言,我很少在自己的服务代码中使用多语句写入事务,因为我不想处理这些警告,而且我发现这个政策强加给我的更规范化的模式更容易使用大多数方式。

【讨论】:

  • 嗨 sudo,感谢您的回复。 SERIALIZABLE 级别是否会导致全表锁定或行级别一级?
  • @Davita 如果你小心点,它只会锁定行。您必须确保不会发生 seq 扫描,因为它们会锁定整个表,并且可能还有其他情况也会锁定它。当我说“锁”时,这些都是乐观锁。他们可能不会阻止其他事务写入。如果这样做,您的可序列化 xact 将失败。
  • 忘了说,如果一个不可序列化的 xact 编辑行,可序列化的 xact 不会实现。影响相同数据的所有 xact 都必须是可序列化的。见stackoverflow.com/questions/23805128/…。详细信息,官方文档最好:postgresql.org/docs/9.5/transaction-iso.html
猜你喜欢
  • 1970-01-01
  • 2014-11-21
  • 1970-01-01
  • 1970-01-01
  • 2012-01-04
  • 2015-06-30
  • 1970-01-01
  • 2013-04-02
  • 2018-02-11
相关资源
最近更新 更多