在并发编辑环境中为儿童实体执行不变性



给定一个不变的子收集不能超过x项目数量,域 cuseplion 如何在并发/网络环境中执行这样的不变性?让我们看一个(经典)示例:

我们有一个带有Employee s的Manager。(假设的)不变性指出,经理不能超过七个直接报告(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();
        }
    }
}

直到最近,我还认为这种方法足够好。但是,看来有一个边缘案例使数据库可以比七个员工更多地存储,从而破坏了不变性。考虑这一系列事件:

  1. a的人去UI中的编辑经理
    (内存中的6名员工,数据库中的6名员工)
  2. b向UI中的编辑经理
    (内存中的6名员工,数据库中的6名员工)
  3. b添加员工并节省更改
    (内存中的7名员工,数据库中的7名员工)
  4. 人A增加了员工并保存更改
    (内存中的7名员工,数据库中的8名员工

当域对象再次从数据库中拉出时,Manager构造函数可能会(或可能)加强在集合上不变的Employee计数,但是无论哪种方式,我们现在的数据和不变性之间的期望。我们如何防止这种情况发生?我们如何干净地恢复?

考虑此系列事件:

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的先决条件。

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

微秒的时间差异不应与核心业务行为有所不同

特别是,如果您的模型仅仅是因为命令A和B恰好按不同的顺序处理,则您的模型可能与您的业务不太匹配。

您示例中的模拟是两个命令都应该成功,但是两者中的第二个也应设置一个标志,以指出当前汇总是不合规性的。当AddeMployee命令和emoveremployee命令碰巧在运输层中订购错误的方式时,这种方法可以防止白痴。

(假设的)不变状态,经理不能拥有七个以上的直接报告

即使在假设的示例中,要保持警惕的事情是数据库是否是记录书。数据库很少在现实世界中获得否决权。如果现实世界是记录书,那么您可能不应该拒绝更改。

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

您在Repository实现中实现此行为:加载Aggregate时,还可以跟踪Aggregate's版本。该版本可以作为Aggregate's ID 的唯一密钥约束实现。整数序列编号。每个Aggregate都有其自己的序列编号(最初每个Aggregate都有序列编号0)。在Repository试图持久之前,它会增加序列编号。如果同时存在持续存在,则Repository背后的数据库将抛出"唯一的密钥约束"的例外,并且不会发生持久性。然后(如果您像在DDD中那样将汇总设计为纯,非侧面效应对象!),您可以透明地重试命令执行,重新运行所有聚合的域代码,从而重新检查不变性。请注意,只有在发生"唯一约束"基础架构异常时才必须重新操作,而不是Aggregate抛出域异常。

我们如何干净地恢复?

您可以重试命令执行,直到没有抛出"唯一的约束违规"为止。div class =" ans">

这不是DDD问题,而是持续层问题。有多种方法可以看一下。

从传统的酸/强一致性角度来看

您需要查看特定数据库的可用并发和隔离策略,可能反映在您的ORM功能中。他们中的一些人将允许您检测到此类冲突并引发例外,因为人A在步骤4中保存了他们的更改。

正如我在评论中所说的那样,在使用工作模式单位的典型Web应用程序(通过ORM或其他方式)中,这应该不会像您似乎所暗示的那样频繁。实体不会一直沿步骤1。到4,它们在步骤3中重新加载。

弱,无锁一致性

您在这里有一些选择。

  • 最后一赢,那里的7名员工A将删除来自人的人。这在某些商业环境中可以可行。您可以通过将更改持续为employees = <new list>而不是employees.Add

  • 来做到这一点。
  • 依靠版本号,如所描述的@voiceofunreason。

  • 最终与补偿的一致性,在申请中,其他内容在事实之后检查了不变的(employees.Count() < 7)。如果检测到违反该规则的情况,必须采取赔偿诉讼,例如回滚最后的操作并通知人员A会过度劳累。

最新更新