在并发编辑环境中为子实体强制实施不变量

时间:2017-02-22 05:12:52

标签: c# domain-driven-design

鉴于子集合不能超过x个项目的不变量,域保证如何在并发/ Web环境中强制实施这种不变量?让我们看一个(经典)例子:

我们有一个Manager Employee秒。 (假设的)不变量表明经理不能超过七个直接报告(Employee s)。我们可以这样(天真地)实现这样:

public class Manager {

    // Let us assume that the employee list is mapped (somehow) from a persistence layer
    public IList<Employee> employees { get; private set; }

    public Manager(...) {
        ...
    }

    public void AddEmployee(Employee employee) {

        if (employees.Count() < 7) {
            employees.Add(employee);
        } else {
            throw new OverworkedManagerException();
        }
    }
}

直到最近,我才认为这种方法足够好。但是,似乎存在一种边缘情况,使数据库可以存储 more 而不是七名员工,从而打破了不变量。考虑这一系列事件:

  1. 人员A在UI中编辑管理员 (内存中有6名员工,数据库中有6名员工)
  2. B人在UI中编辑管理员 (内存中有6名员工,数据库中有6名员工)
  3. B人员添加员工并保存更改 (内存中7名员工,数据库中7名员工)
  4. 人员A添加员工并保存更改
    (内存中有7名员工,数据库中的8名员工
  5. 当再次从数据库中提取域对象时,Manager构造函数可能(或可能不)强化集合上的Employee计数不变量,但无论哪种方式,我们现在都存在差异我们的数据和我们的不变量所期望的。我们如何防止这种情况发生?我们如何干净利落地恢复过来?

3 个答案:

答案 0 :(得分:5)

  

考虑这一系列事件:

Person A goes to edit Manager in UI
(6 employees in memory, 6 employees in database)
Person B goes to edit Manager in UI
(6 employees in memory, 6 employees in database)
Person B adds Employee and saves changes
(7 employees in memory, 7 employees in database)
Person A adds Employee and saves changes
(7 employees in memory, 8 employees in database)

最简单的方法是将数据库写入实现为比较和交换操作。所有写入都使用聚合的陈旧副本(毕竟,我们正在查看内存中的聚合,但记录簿是磁盘上的持久副本)。关键的想法是,当我们实际执行写入时,我们还检查我们正在使用的陈旧副本仍然是记录簿中的实时副本。

(例如,在事件源系统中,您不会附加到流,而是附加到流中的特定位置 - 即,您希望尾指针位于的位置。因此在比赛中,仅一个写入会提交到尾部位置;另一个写入会因并发冲突而失败并重新开始。)

在Web环境中对此类似可能是使用eTag,并在执行写入时验证etag是否仍然有效。获胜者获得成功回复,失败者得到412 Precondition Failed

对此的改进是为您的域使用更好的模型。 Udi Dahan wrote

  

时间上的微秒差异不应对核心业务行为产生影响

具体来说,如果您的模型因为命令A和B碰巧以不同的顺序处理而最终处于不同的状态,那么您的模型可能与您的业务不匹配。

您的示例中的模拟将是两个命令都应该成功,但是两个中的第二个也应该设置一个标记,指出聚合当前不合规。当addEmployee命令和removeEmployee命令碰巧在传输层中以错误的方式排序时,这种方法可以防止出现这种情况。

  

(假设的)不变量表明经理不能有超过七个直接报告

要注意的是 - 即使在假设的例子中,数据库是否是记录簿。数据库很少在现实世界中获得否决权。如果现实世界是记录簿,你可能不应该拒绝改变。

答案 1 :(得分:2)

  

我们如何防止这种情况发生?

您在Repository实施中实施此行为:加载Aggregate时,您还会跟踪Aggregate's版本。该版本可以实现为Aggregate's Id 的唯一键约束和整数序列号。每个Aggregate都有自己的序列号(最初每个Aggregate都有序列号0)。在Repository尝试持久化之前,它会递增序列号;如果发生了并发持久化,Repository后面的数据库将抛出一个&#34;违反的唯一键约束&#34;一种例外和坚持不会发生。 然后(如果你已经将Aggregate设计为一个纯粹的非副作用对象,就像你应该在DDD中做的那样!),你可以透明地重试命令执行,重新运行所有聚合域代码,从而重新运行检查不变量。请注意,只有在违反&#34;唯一约束条件时才能重试该操作。发生基础结构异常,而不是Aggregate抛出域异常。

  

我们如何干净利落地恢复?

您可以重试命令执行,直到没有&#34;唯一约束违规&#34;被扔了。 我在这里用PHP实现了这个重试:https://github.com/xprt64/cqrs-es/blob/master/src/Gica/Cqrs/Command/CommandDispatcher/ConcurrentProofFunctionCaller.php

答案 2 :(得分:1)

这不是DDD问题,而是持久层问题。有多种方法可以看这个。

从传统的ACID /强一致性角度

您需要查看特定数据库的可用并发和隔离策略,这些策略可能反映在您的ORM功能中。其中一些将允许您检测此类冲突并抛出异常,因为人员A在步骤4保存其更改。

正如我在评论中所说,在使用工作单元模式(通过ORM或其他方式)的典型Web应用程序中,这不应该像您似乎暗示的那样经常发生。实体不会停留在UoW跟踪的内存中的所有步骤1.到4.它们在步骤3和4中重新加载。事务3和4必须同时发生问题。

较弱,无锁一致

你有几个选择。

  • 最后一次胜利,来自人员A的7名员工将从人员B中删除这些人员。这在某些商业环境中是可行的。您可以将更改保留为employees = <new list>而不是employees.Add来执行此操作。

  • 依赖于版本号,如@VoiceOfUnreason所述。

  • 最终与补偿保持一致,应用程序中的其他内容在人员A和B的交易之外检查事后的不变(employees.Count() < 7)。如果检测到违反规则,则必须采取补偿措施,例如回滚上一次操作并通知A人经理会过度工作。