事件来源和并发、矛盾的事件创建



我很难弄清楚这一点。也许你能帮我。

问题说明:

想象一下,有一个记录账户财务交易的系统(就像钱包服务)。交易存储在数据库中,每个交易表示给定金额余额的increasedecrease

在应用程序代码方面,当用户想要购买时,将从数据库中提取其账户的所有交易,并计算当前余额。根据结果,客户是否有足够的资金用于购买(余额永远不会低于零)。

交易示例:

ID   userId amount currency, otherData
Transaction(12345, 54321, 180,  USD,    ...)
Transaction(12346, 54321, -50,  USD,    ...)
Transaction(12347, 54321, 20,   USD,    ...)

以上3项意味着用户的余额为150美元。

并发访问:

现在,假设有2个或更多这样的应用程序实例。想象一下,用户有100美元的余额,同时购买了两件价值100美元的物品。对此类购买的请求将发送到两个不同的实例,这两个实例都从DB中读取所有事务并将其缩减为currentBalance。在两个副本中,同时余额等于100美元。这两项服务都允许购买并添加新的事务Transaction(12345, 54321, -100, USD, ...),这将使余额减少100。

如果数据库中插入了两个相互矛盾的事务,则余额不正确:-100 USD

问题:

我该如何处理这种情况

我知道通常使用optimisticpessimistic并发控制。因此,以下是我对两者的怀疑:

乐观并发

这是关于保留资源的版本,并在实际更新之前对其进行比较,就像CAS操作一样。由于事务是事件的一种形式——不可变的实体——我无法掌握哪个版本的资源。我不更新任何内容。我只在余额中插入新的更改,它必须与所有其他现有交易保持一致。

悲观并发

这是关于锁定表/页/行以进行修改,以防它们在系统中更频繁地发生。是的,好吧…我认为,为每次插入阻塞一个表/页面是不可能的(可伸缩性和高负载问题)。锁定行-好吧,我要锁定哪些行?同样,我不会修改DB状态中的任何内容。

开放的想法

我的感觉是,这类问题必须在应用程序代码级别上解决。我现在想到了一些模糊的想法:

  1. 分布式缓存;给定用户的锁定";,以便一次只能处理一笔交易(购买、存款、取款、退款等)
  2. 每个事务都有一个字段,例如previousTransactionId-指向最后提交的事务的指针,以及该字段上的某种唯一索引(正好一个事务可以指向过去的正好一个交易,第一个具有null值的事务)。这样,我在尝试插入重复项时会出现违反约束的错误
  3. 使用排队系统进行异步处理,每个用户都有一个主题:正好有一个实例逐个处理给定用户的事务。尝试得很好,但不幸的是,我需要与购买同步才能回复第三方系统

需要注意的一点是,通常每个实体都有一个与每个事件相关的偏移量(一个单调增加的数字,例如Account|12345|6789可能是账号#1345的第6789个事件)。因此,假设您存储事件的数据库支持它,您可以通过记住重建该实体的状态时看到的最高偏移量来获得乐观的并发控制,并将事件的插入条件为没有偏移量大于6789的帐户#1345的事件。

有一些数据存储支持";围栏":只允许一个实例将事件发布到特定流,这是乐观并发控制的另一种方式。

有一些方法可以将悲观并发控制转移到应用程序/框架/工具包代码中。Akka/Akka.Net(免责声明:我受雇于Lightbend,该公司为这两个项目中的一个维护和销售商业支持)具有集群分片功能,允许应用程序的多个实例在它们之间协调实体的所有权。例如,实例A可能具有帐户12345,而实例B可能具有帐户23456。如果实例B接收到对帐户12345的请求,则它(极大地简化)有效地将请求转发到实例a,这强制执行一次只处理对帐户12345的请求。在某种程度上,这种方法可以被认为是1(注意:这种分布式缓存不仅提供并发控制,而且实际上还缓存了应用程序状态(例如,帐户余额和任何其他有助于决定是否可以接受事务的数据))和3(即使它向外部世界提供了同步的API)的组合。

此外,通常可以设计事件,使它们形成一个无冲突的复制数据类型(CRDT),只要保证它们可以协调,就可以有效地允许在事件日志中使用分叉。人们可能会斜视,也许会将允许透支的银行账户(其中对账允许负余额并收取大量费用)视为CRDT的一个例子。

我应该如何处理这种情况?

您描述的问题的通用术语是设置验证。如果某个属性必须为作为一个整体的集合保留,那么您需要有某种形式的锁来防止冲突的写入。

乐观/悲观只是两种不同的锁定实现。

如果您有并发写入,通常的一般机制是第一个写入程序获胜。比赛的失败者遵循";同时修改";分支,然后重试(再次重新计算以确保所需的属性仍然有效)或中止。

在您描述的情况下,如果您的插入代码负责确认用户余额不是负数,那么该代码需要能够锁定用户的整个交易历史。

现在:注意前一段中的if,因为它真的很重要。在您的域中,您需要了解的一件事是您的系统是否是交易的权威。

如果你的系统是权威,那么保持不变量是合理的,因为你的系统可以说";不,那不是允许的交易";,其他人都必须接受。

如果你的系统不是权威机构,你会从";在其他地方";,那么你的系统就没有否决权,不应该因为余额不平衡就试图跳过交易。

所以我们可能需要一个像";透支";在我们的系统中,而不是试图绝对地声明平衡总是满足一些不变量。

从根本上讲,与我们可以与单个权威机构一起使用的更简单的模型相比,具有许多并行工作的权威机构的协作/竞争领域需要对属性和约束有不同的理解。


在实现方面,通常的方法是集合具有可以作为一个整体锁定的数据表示。一种常见的方法是保留对集合的仅追加更改列表(有时称为具有集合的历史或"事件流")。

在关系数据库中,我看到的一种成功的方法是实现一个存储过程,该过程接受必要的参数,然后获取适当的锁(即对关系数据存储应用"tell,don't ask");它允许您将应用程序代码与数据存储的详细信息隔离开来。

最新更新