@Transactional等效于C#和DDD应用程序服务的并发性

时间:2013-11-14 09:08:05

标签: c# design-patterns domain-driven-design

我正在阅读Vaughn Vernon关于实现域驱动设计的书。我也经历了book code, C# version, from his github here

本书的Java版本有装饰器@Transactional,我相信它来自spring框架。

public class ProductBacklogItemService
{
    @Transactional
    public void assignTeamMemberToTask(
        string aTenantId,
        string aBacklogItemId,
        string aTaskId,
        string aTeamMemberId)
        {
            BacklogItem backlogItem =
                backlogItemRepository.backlogItemOfId(
                    new TenantId(aTenantId),
                    new BacklogItemId(aBacklogItemId));

            Team ofTeam =
                teamRepository.teamOfId(
                    backlogItem.tennantId(),
                    backlogItem.teamId());

            backlogItem.assignTeamMemberToTask(
                new TeamMemberId(aTeamMemberId),
                ofTeam,
                new TaskId(aTaskId));
        }
}

C#中的等效手动实现是什么?我正在考虑以下几点:

public class ProductBacklogItemService
{
    private static object lockForAssignTeamMemberToTask = new object();
    private static object lockForOtherAppService = new object();

    public voice AssignTeamMemberToTask(string aTenantId,
        string aBacklogItemId,
        string aTaskId,
        string aTeamMemberId)
        {
            lock(lockForAssignTeamMemberToTask)
            {
                // application code as before
            }
        }

        public voice OtherAppsService(string aTenantId)
        {
            lock(lockForOtherAppService)
            {
                // some other code
            }
        }
}

这给我留下了以下问题:

  1. 我们是通过应用程序服务还是通过存储库锁定的?即我们不应该做backlogItemRepository.lock()
  2. 当我们读取多个存储库作为我们的应用程序服务的一部分时,我们如何在事务期间保护存储库之间的依赖关系(其中聚合根通过标识引用其他聚合根) - 我们是否需要在存储库之间具有互连锁?
  3. 是否有任何DDD基础架构框架可以处理任何此类锁定?
  4. 修改

    使用事务时有两个有用的答案,因为我没有选择我使用内存存储库的持久层,这些是非常原始的并且我写了它们(它们没有事务支持,因为我没有知道如何添加!)。

    我将设计系统,因此我不需要同时对多个聚合根进行原子更改,但是我需要在多个存储库中一致地读取(即,如果从多个存储库引用BacklogItemId)其他聚合,那么我们需要在BacklogItemId被删除时防止竞争条件。)

    那么,我可以通过使用锁来逃脱,还是需要在我的内存存储库中添加TransactionScope支持?

4 个答案:

答案 0 :(得分:5)

TL; DR版本

您需要将代码包装在System.Transactions.TransactionScope中。关于多线程btw要小心。

完整版

因此,聚合点是定义一致性边界。这意味着任何更改都应该导致聚合的状态仍然遵循它的不变量。这不一定与交易相同。真实交易是一个贯穿各领域的实施细节,因此应该如此实施。

关于锁定的警告

不要做锁定。试着忘记你实现悲观锁定的任何想法。要构建可扩展的系统,您没有真正的选择。事实上数据需要时间来从磁盘到屏幕,这意味着你有最终的一致性,所以你应该为此构建。你无法真正保护免受竞争条件的影响,你只需要考虑它们可能发生的事实并且能够警告“失败”的用户他们的命令失败了。通常,您可以稍后开始查找这些问题(秒,分钟,小时,天,无论您的域专家告诉您SLA是什么),并告诉用户他们可以对此做些什么。

例如,想象一下,如果两名工资单职员与银行同时支付了员工的费用。他们会在以后找到书籍平衡的时候发现并采取一些补偿行动来纠正这种情况。您不希望将工资单部门缩减为一次工作的单个人,以避免这些(罕见)问题。

我的实施

我个人使用命令处理器样式,因此我的所有应用程序服务都实现为ICommandHandler<TCommand>CommandProcessor本身就是查找正确的处理程序并要求它处理命令的东西。这意味着CommandProcessor.Process(command)方法可以在System.Transactions.TransactionScope中处理整个内容。

示例:

public class CommandProcessor : ICommandProcessor
{
    public void Process(Command command)
    {
        using (var transaction = new TransactionScope())
        {
            var handler = LookupHandler(command);
            handler.Handle(command);

            transaction.Complete();
        }
    }
}

你没有采用这种方法,所以为了使你的交易成为一个跨领域的问题,你需要将它们移动到堆栈中更高的水平。这在很大程度上取决于您正在使用的技术(ASP.NET,WCF等),因此如果您添加更多细节,可能会有一个显而易见的地方放置这些东西。

答案 1 :(得分:4)

锁定不允许在这些代码路径上任何并发。

我认为您正在寻找transaction scope

答案 2 :(得分:1)

我不知道您将使用哪个持久层,但ADO.NET,Entity Framework等标准层支持TransactionScope语义:

using(var tr = new TransactionScope())
{
    doStuff();
    tr.Complete();
}

如果调用tr.Complete(),则提交事务。在任何其他情况下,它都会回滚。

通常,聚合是事务一致性的单位。如果您需要将事务分布在多个聚合中,那么您应该重新考虑您的模型。

答案 3 :(得分:-1)

lock(lockForAssignTeamMemberToTask)
{
    // application code as before
}

这会照顾同步。但是,您还需要在发生任何异常时还原更改。所以,模式将是这样的:

lock(lockForAssignTeamMemberToTask)
{
    try {
        // application code as before
    } catch (Exception e) {
        // rollback/restore previous values
    }
}