ddd的战术篇: domain event(事件)

news/2024/7/6 6:27:06

承接上篇文章谈到的aggregate的设计策略。aggregate是用来保证数据(*注: 之前的文章都是用数据完整性这个说法,其实是想表达data consistency这个意思,查了一下翻译,感觉还是数据一致性比较贴切)一致性的一个单位,但是如果设计大的aggregate的话,容易产生并发问题和资源浪费等问题。所以在考虑数据一致性的同时。我们要尽量设计小的aggregate。
另外自然会碰到这样的问题,aggregate的内部数据一致性由aggregate来保证。那aggregate之间会不会存在数据一致性呢?尤其在尽量设计小aggregate的前提下,这个问题好像更加容易发生。

跨aggregate的数据一致性

用例子来说明问题吧。
比如有一个类似博客的网站的功能。有用户User和文章post。

class User {
  private UserId userId;
  private String email;
  private String username;
  private AccountStatus accountStatus;

  public void deactivate(){
    accountStatus = AccountStatus.DEACTIVATED;
  }
}

这里为了说明问题,我们就定义User的一个行为,注销deactive。

class Post {
  private PostId postId;
  private UserId authorId;
  private String text;
  private PostStatus postStatus;
  private Long views;

  public void view(UserId userId) {
    if(userId == authorId){
      throw new CannotAddViewToYourOwnPostException(postId);
    }
    this.likes = this.likes + 1;
  }

  public void delete(UserId userId){
    if(userId != authorId){
      throw new IllegalAccessException(postId, userId);
    }
    postStatus = PostStatus.DELETED;
  }
}

Post呢,简单起见也定义两更行为,阅览(记录阅览次数)和删除。如果你觉得物理删除是个邪恶的行为。那就定义一个状态来表示删除,例子中我们定义了一个枚举。
接下来要实现一个用例。当一个User账号删除后,与User关联的Post也必须删除。当User处在注销状态下,User的Post处在废删除状态是不合理的。这又是个数据一致性需要保障。

如何保障跨aggregate的数据一致性

先别想太多,就按普通的思路走,把对多个aggregate的操作写进一个事务不就行了?

class AuthorApplicationService {
  @Transactional
  public void deactivateAccount(Long userId){
    User user = userRepository.findOne(new UserSpecificationById(userId);
    user.deactivate();
    userRepository.save(user);

    Posts posts = postRepository.find(new PostSpecificationByUserId(user.getUserId()));
    posts.forEach(post -> {
      post.delete();
      postRepository.save(post);
    });
  }
}

用户的注销和文章的删除处在同一个事务中,当用户备注小时她的文章肯定也会都被删除。
然而,这样的方式书中居然并不提倡。原书中有这样的阐述。

Any rule that spans aggregates will not be expected to be update at all times. Through event processing, batch processing, or other update mechanisms, other dependencies can be resolved within some specific time.

如果一个规则是跨aggregate的,那我们就不能指望在所有时刻都被遵守(直译很僵硬)。我们可以理解成对多个aggregates的操作,我们不能指望他们能同事进行。而实践ddd的书中也引用了这个规矩,并将其解释成”一个事务处理中应避免更新多个aggregate”。
也有说法表示因为aggregate需要保证数据的一致性,自身就代表了一个事务的边界。因此把对多个aggregate的处理放在一个事务中是很不自然的(这是一种直觉)。
那为什么要比面在一个事务中更新多个事务呢?
本人能想到的理由有两个
1. 可能会操作过多的数据,造成程序的低效。
尤其是你要更新好多个aggregate的时候。(这个问题在上面的例子中并没有很好地展示出。硬要说也就是万一有博主写了太多文章,一删删一万篇)
2. 同时操作过多的数据容易造成并发的问题。
假如说一个博主的文章超有人气,一天到晚有人看。导致对Post的更新很频繁,结果博主想要注销时,可能造成锁等待(悲观锁)或者提交失败(乐观锁)。当然这样的博主有必要注销吗?(好啦,我知道其实我在为谈问题而制造问题。)
这写话是不是好像在哪里听到过。感觉这好像就是让我们不要设计大aggregate的理由啊。
仔细想一想,如果我们允许在一个事务中对多个aggregate进行更新操作,我们是不是在某种意义上制造了一个更大的aggregate。结果就是我们明明设计了小的aggregate,结果反而没能得到我么预期的结果。
另外很多关于ddd的文章也有这种观点。ddd中通过aggregate来保证数据的一致性,所以aggregate自然而然产生了一个事务边界。出了aggregate的事务边界,自然无法很有效地保证数据一致性。如果当你发现自己必须把对多个aggregate的操作放进一个事务,很有可能你的建模是有问题的,应该重新审视自己的模型。
如此提倡小的aggregate设计,而跨aggregate的数据一致性却有又没有有效的手段来保证,那怎么办?搞一个大的aggregate?这不是打脸吗?
不过重新审视自己的模型,这话是没错啦~我们可以考虑是不是有更好的建模方法。按例子上来说,我们真的需要把统计阅览数的行为给Post来做吗?是不是可以把它分解出Post,个其他的模型来实现。这是一个关于更深入地理解业务,改进模型的思路。
另一个思路是aggregate的设计策略问题(也算是从另一个角度对业务进行理解)。即是前面的文章提到的,如果这是一种不能妥协的数据一致性,自然须将它放进一个边界,即一个aggregate。当然为此我们愿意付出代价即aggregate可能会变得巨大而导致性能问题。然而当我们希望优先性能及用户体验,数据一致性只须最终一致(eventual consistency)就行的话,我们有另一个选择。

domain event

从当数据的一致性只需要达到最终一致的话,ddd提倡利用event的设计模式。event也是解耦的一种常规操作。ddd也借鉴了这个模式来解决数据最终一致的问题。
当我们要对两个aggregate进行操作。两个处理会进行在独立的事务中。当第一个处理完成后,抛出一个event(事件),然后监听event的组件在接受到event后开始第二个操作。
比如在上面的例子里

    User user = userRepository.findOne(new UserSpecificationById(userId);
    user.deactivate();
    userRepository.save(user);

当用户注销后,我们可以抛出一个UserDeactivatedEvent的事件,监听 UserDeactivatedEvent的组件会在接收到事件后进行文章删除的操作。

    Posts posts = postRepository.find();
    posts.forEach(post -> {
      post.delete();
      postRepository.save(post);
    });

如此一来,我们便可以把处理分拆成两个事务,保证了性能和用户体验,同时也能达成数据的一致性。
是不是听起来很简单?但其实操作成面上并不简单。当A,B两个处理处在同一个事务中,A,B要么同时成功,要么同时失败。而通过event模式来做的话,A成功的话并不代表B一定会成功。那么B不成功时该怎么办?这会有多种策略
- 不断尝试进行B处理,直到B成功为止。
- B失败后抛出事件,对A处理进行回滚或者其他挽救处理
- 结合上边的两种方式
无疑为了实现最终一致性,我们引入了很多复杂度,需要考虑的状况会更多。
另外我们还需要一个事件订阅和分发的系统来帮我们实现这个功能。烦人的是如何实现事件订阅和分发的事情并不是ddd所关注的,ddd对实现没有任何的指向与意见。所以怎么解决就全得靠自己啦。。。
一篇文章码不了太多字,先介绍一下Spring提供的一个轻量级方案吧。
Spring Data 1.13后有下面一个方便实现aggregate的抽象类。

public class AbstractAggregateRoot {

    /**
     * All domain events currently captured by the aggregate.
     */
    @Getter(onMethod = @__(@DomainEvents)) //
    private transient final List<Object> domainEvents = new ArrayList<Object>();

    /**
     * Registers the given event object for publication on a call to a Spring Data repository's save method.
     * 
     * @param event must not be {@literal null}.
     * @return
     */
    protected <T> T registerEvent(T event) {
        Assert.notNull(event, "Domain event must not be null!");
        this.domainEvents.add(event);
        return event;
    }

    /**
     * Clears all domain events currently held. Usually invoked by the infrastructure in place in Spring Data
     * repositories.
     */
    @AfterDomainEventPublication
    public void clearDomainEvents() {
        this.domainEvents.clear();
    }
}

其中有一个registerEvent()的方法,可供调用,它是用来创建时间的。注意当调用这个方法是,事件只是被创建,而没有被发布。

class User {
  private UserId userId;
  private String email;
  private String username;
  private AccountStatus accountStatus;

  public void deactivate(){
    accountStatus = AccountStatus.DEACTIVATED;
    registerEvent(UserDeactivatedEvent(this.userId));  // 创建用户注销的事件
  }
}

在用户注销的方法里,调用刚才提到的regsiterEvent()来创建事件。
而Spring data会在Repository.save()被调用的时候把实现发布出去。

class AuthorApplicationService {
  @Transactional
  public void deactivateAccount(Long userId){
    User user = userRepository.findOne(new UserSpecificationById(userId);
    user.deactivate();
    userRepository.save(user);  // UserDeactivatedEvent会被发布
  }
}

然后订阅事件部分的实现会是下面这样。

@Service
public class PostService {
  @Autowired
  private IPostRepository postRepository;

  @Async
  @TransactionalEventListener
  public void handleUserDeactivatedEvent(UserDeactivatedEvent event) {
    Posts posts = postRepository.find(new PostSpecificationByUserId(event.getUserId()));
    posts.forEach(post -> {
      post.delete();
      postRepository.save(post);
    });
  }
}

严格意义上说删除文章这个实现还是在一个事务中操作了多个aggregate的。因为只要作者有两篇以上的文章,删除时我们就等于在一个事务中操作了多个Post实例。所以还要把每个删除放进一个事务。

@Service
public class PostService {
  @Autowired
  private IPostRepository postRepository;
  @Autowired
  private PostTransactionService postTransactionService;

  @Async
  @TransactionalEventListener
  public void handleUserDeactivatedEvent(UserDeactivatedEvent event) {
    Posts posts = postRepository.find(new PostSpecificationByUserId(event.getUserId()));
    posts.forEach(post -> {
      postTransactionService.delete(post);
    });
  }
}

public class PostTransactionService {
  @Autowired
  private IPostRepository postRepository;

  @Transactional
  public void delete(Post post){
      post.delete();
      postRepository.save(post);
  }
}

总结一下spring data对domain event的实现如下
1. aggregate root来创建事件
2. repository.save()是发布事件
3. 用@TransactionalEventListener来声明对时间的处理
因为repository.save()是触发事件的地方,所以看起来有点像数据库的trigger功能。

关于最终一致性是否可行

从理想情况下而言,我们当然是希望数据在任何时刻都保持一致性。但很遗憾,很多时候因为很多的限制,我们必须做一些选择题来权衡。
最终数据的一致性意味着数据的状态会有延迟。那延迟会不会被接受。
其实数据的一致性是源自于业务的需求,而延迟能否被接受也不取决程序员。我们完全可以和domain expert(精通业务的人)来商讨,从业务角度来说多少的延迟是可以接受的。最近看到一篇关于ddd的文章说在计算机到来之前,人类用纸来管理业务的时代,延迟是家常便饭的事情,所以没必要视延迟为一种罪恶。。。

总结

ddd中由aggregate来保证数据一致性。当走出aggregate的边界后,我们能通过domain event来实现数据的最终一致性。
domain event具体如何实现又是一个能展开的话题,之后的文章还会进行论述。


http://www.niftyadmin.cn/n/4428418.html

相关文章

jvm栈大小设置

1、栈内存大小设置栈内存为线程私有的空间&#xff0c;每个线程都会创建私有的栈内存。栈空间内存设置过大&#xff0c;创建线程数量较多时会出现栈内存溢出StackOverflowError。同时&#xff0c;栈内存也决定方法调用的深度&#xff0c;栈内存过小则会导致方法调用的深度较小&…

unable to connect to :5555

有可能批处理文件用的adb和eclipse的adb不兼容。把你的批处理文件用的adb换成eclipse的adb就可以了&#xff1a; 运行结果&#xff1a; 转载于:https://www.cnblogs.com/johnsonwei/p/5965643.html

固执的程序员学习函数式编程的收获 之 一

最近因为写node js&#xff0c;开始有机会接触js的函数式写法。关于函数式语言&#xff0c;其实久闻其名&#xff0c;但只是大概了解过一些概念罢了。刚开始听到这个概念觉得不会就是面向过程编程的改良版吧?&#xff08;自己还是太无知了…&#xff09; 由于自己的编程语言主…

固执的程序员学习函数式编程的收获 之 二 说说monad

之前说了函数式编程的收获。比如说函数可以当作变量&#xff0c;然后尽量避免写副作用的程序。 之后可以说遇到了一个超级难理解的东西–monad。 一切要从和小田君的对话说起 当我在写java时&#xff0c;大概是下面的一段代码 List.map( item -> item.getName()); List.…

MAYA影视动漫高级模型制作全解析出_完整版PDF电子书下载 带索引书签目录高清版...

MAYA影视动漫高级模型制作全解析_页数384_出版日期2016.04_完整版PDF电子书下载 带索引书签目录高清版_13936277 下载链接 http://pan.baidu.com/s/1skA4FZf 【作 者】CGWANG动漫教育著【形态项】 384【出版项】 北京&#xff1a;人民邮电出版社 , 2016.04【ISBN号】7-115-41…

自己写deque

//deque /* what is a deque? In Chinese, its called "双端队列". Its different from a queue. Its elements can be added to or removed from either the front(head) or back(tail) ,called a head-tail linked list.输入限制deque An input-restricted deque …

ddd的战术篇: CQRS

之前的文章介绍了ddd在战术层面的要素&#xff0c;包括entity&#xff0c;value object&#xff0c;aggregate和一些设计模式如repository。在ddd中&#xff0c;repository几乎成为了等同于entity一样的基本要素。 关于aggregate与repository的回顾 aggregate是entity和value…

领域驱动设计(domain driven design)战略篇之一 战略 Bounded Context

之前的文章主要从战术层面的角度介绍了ddd。在岛国也被称为轻量级ddd。它提供了一些概念如aggregate, entity, domain event和一些设计模式如repository, specification来帮助我们建模和设计。各种战术还有能够扩展的地方&#xff0c;有机会还会再写下去。不过从这篇文章开始会…