세 가지의 백엔드 개발 사례 시리즈 - 이벤트 기반 아키텍처 (EDA) 코딩 관행

이 시리즈의 처음 두 기사에서, 저자는 이야기 코드 템플릿 프로젝트 백엔드DDD 코딩 관행 ,이 문서에서, 내가 착륙 이벤트 기반 아키텍처를 인코딩하는 방법을 실용적인 방법을 공유 할 것입니다.

이벤트의 단순히 말하기 영역, 즉 많은 세월이되어이며, 간단하게 이벤트 기반 아키텍처 (이벤트 기반 아키텍처, EDA)를 말하고, 그것은 주제 전에 몇 십 년이 등장 언급 논의 빠르게 잘 익은 언어 소프트웨어를 . 그러나, 지금까지 상상에서 보편적으로 개발 팀으로 인정 저자의 관찰, 이벤트 기반 아키텍처 봐. 심지어 사람들이 메시지에 대한 추가뿐만 아니라 사람들이 DDD 이벤트의 분야에 종사 알고있는 경우에도 비동기 HTTP 동기화 메커니즘에 일류 시민의 하나라고 알고 마이크로 서비스에 참여, 이벤트 기반 아키텍처의 장점은 더는 소프트웨어로 변환 대응 가져 없습니다 실무자에 의해 선호.

나는 두 점을 요약하는 이유에 대해 생각하려고 : 첫 번째는 객관적인 세계의 작동 이벤트 중심의 모드 될 가능성이 있지만, 사람들의 생각의 자연적인 방법이 아니다, 두 번째는 소프트웨어에 혜택을 가져 오는 이벤트 기반 아키텍처 동시에, 우리는 디버깅의 어려움으로 추가 복잡성을 추가하지만, 이러한 직관적 최종 일관성이 아니다.

물론, 많은 소프트웨어 프로젝트는 메시지 큐를 사용 사실이 있지만, 메시지 큐를 사용하는 프로젝트가 확실히 이벤트 기반 아키텍처가 될 것이라는 점을 의미하지 않는다는 것을 분명히있을 필요가 프로젝트의 많은 단지 때문에 드라이브 기술, 만 (예 RabbitMQ 카프카 등) 제품의 특정 메시지 큐를 사용하여 소규모. 당신이 메시지 큐는 메일 링으로 사용주의 경우 거대한 시스템은,이 자연 시스템은 이벤트 중심 아키텍처의 사용을 언급 할 필요가 없을 것입니다.

순간에, 마이크로 서비스의 상승, DDD는,이 이벤트 중심 아키텍처에서, 우리는 제약 모델링 비즈니스, 디자인 행사, DDD, 경계 상황 요인과 기술적 측면의 한계를 고려할 필요가 재현 어떻게 바닥을 처음부터 끝까지 시스템 엔지니어링을 수행을 통해 감시 생각 할 필요가있다. 다시 말하지만, 프로그래밍에 관심을 지불하는 것은 쉬운 일이 아니다.

사실, 연습에 이벤트 기반 아키텍처의 어려움을 잘 활용이있다, 그러나 그것의 장점은 해당 이벤트 기반 아키텍처는 간단 착륙 그래서, 우리는 특정 "구조"와 "일상을"기대도 정말 매력적이다.

이 문서는 두 부분으로 분할되고, 특정 메시지 큐 구현의 제 1 부분 독립적으로, 예를 들면, 실제 마이크로 서비스 시스템의 제 2 부분과, 상기 사용 메시지 큐 등 RabbitMQ하고, 따라서이 필드 이벤트의 일반적인 모델을 설명 완벽한 이벤트 기반 아키텍처 연습 바닥을 공유.

본 논문에서는, DDD 등의 저자를 참조 할 수 있습니다 독자에게 DDD 익숙하지 않은 집계 루트, 자원 라이브러리와 애플리케이션 서비스 등 DDD 개념을 많이 포함 할 것이다 코딩의 기초 등 DDD 코딩 관행 문서를 .

여기 GitHub의에 샘플 코드를 참조 전자 상거래 - 샘플 프로젝트.


필드 이벤트를 모델링 : 첫 번째 부분

필드 이벤트, 기술적 인 수준에서 하락이 발생 필드에서 비즈니스 가치있는 일에 의해 DDD의 개념 표현된다 (일반적으로는 집계 루트가) 상태가 변경 비즈니스 엔티티 객체입니다 필요 후 필드 이벤트를 발행합니다. 이벤트 기반 아키텍처 "이벤트가"반드시 인해 밀접한 관계 DDD에 "필드 이벤트"하지만,이 기사를 의미하지 않지만, 그래서 사건을 언급 할 때, 우리는 특히 "현장 이벤트입니다."


필드 이벤트 만들기

필드 이벤트에 대한 기본 지식, 저자의 참조하시기 바랍니다 경우에 마이크로 서비스 분야에서 사용 코딩 관행에 직접, 문서 링크를 문서를.

필드 이벤트를 모델링 할 때, 우리는 먼저 이러한 기본 클래스를 만들려면이 이벤트에 대한 고유 식별 ID 및 생성 시간으로 사건에 대한 일반적인 정보를 기록해야합니다 DomainEvent:

public abstract class DomainEvent {
    private final String _id;
    private final DomainEventType _type;
    private final Instant _createdAt;
}

DDD 장면에서 일반 상태 중합 업데이트 루트 필드 이벤트가 추가로, 이벤트 소비자 측면에서, 우리는 모든 이벤트 리스너가 때때로 특정 집계 루트에서 발생 희망, 생산, 각 집계 루트이 작가를 추천합니다 이벤트 객체 중합 루트의 ID가 포함되어 해당 기본 클래스를 생성, 예를 들면, 주문 (오더) 클래스 생성 OrderEvent:

public abstract class OrderEvent extends DomainEvent {
    private final String orderId;
}

그 후로부터 상속 실제 이벤트 주문 단결 OrderEvent예를 들어,의 순서로 작성하는 OrderCreatedEvent이벤트를 :

public class OrderCreatedEvent extends OrderEvent {
    private final BigDecimal price;
    private final Address address;
    private final List<OrderItem> items;
    private final Instant createdAt;
}

다음과 같이 상속 체인 필드 이벤트는 다음과 같습니다

필드 이벤트 상속 체인

이 필드 이벤트를 만들 때, 당신은 두 가지 사항에주의해야합니다

  • 필드 이벤트 자체가 변하지 않는 (불변)이어야한다;
  • 필드 이벤트는 이벤트가 발생와 관련된 문맥 데이터를 전달해야하지만, 주 전체 데이터 집합의 루트 당신이 순서에 대한 기본 정보를 전달하고, 제품 (상품) 이름 업데이트에 대한 수 있습니다 주문 생성하지 않을 때, 예를 들어, ProductNameUpdatedEvent이벤트를, 당신은해야 이 전 업데이트 후 제품의 이름을 포함합니다 :
public class ProductNameUpdatedEvent extends ProductEvent {
    private String oldName; //更新前的名称
    private String newName; // 更新后的名称
}

게시 필드 이벤트

이러한 또한 리포지토리 (저장소)에 게시 할 수 있습니다 (ApplicationService)을 게시 할 수 있습니다 응용 프로그램 서비스, 이벤트 테이블도 도입 할 수있는 방법 등 다양한 방법으로 게시 필드 이벤트가있다, 저자의 참조 할 수 있습니다 이러한 세 가지 방법의 자세한 비교를 발표 마이크로 서비스 분야에서 이벤트를 사용하여 기사. 나는 그것을 논의하기 위해 여기에, 이벤트 테이블 모드를 사용하는 것이 좋습니다.

일반적인 비즈니스 프로세스가 데이터베이스를 업데이트 한 다음 필드 이벤트를 공개합니다, 여기에 더 중요한 점은 우리가 성공하거나 실패하거나 둘 것을, 데이터베이스 업데이트 및 이벤트 퍼블리싱 사이의 자성을 확인해야합니다. 기존 실제로, 글로벌 트랜잭션 (글로벌 트랜잭션 / XA 트랜잭션)은 일반적으로 이러한 문제를 해결하는 데 사용됩니다. 그러나, 효율이 매우 낮은 글로벌 트랜잭션 자체가,뿐만 아니라, 기술 프레임 워크의 일부는 글로벌 트랜잭션에 대한 지원을 제공하지 않습니다. 현재,보다 고도로 이벤트 테이블 방식을 도입 간주 다음과 같이 처리가 있다는 것이다 :

  1. 비즈니스 테이블을 업데이트하는 동안, 현장 이벤트는, 데이터베이스의 이벤트 테이블에 다음 비즈니스 테이블과 자성을 보장하는 것입니다 동일한 트랜잭션에서 로컬 이벤트 테이블을 저장뿐만 아니라 효율성을 보장하기 위해.

  2. 백그라운드에서 작업을 열고, 그것은 이벤트가 성공적으로 전송 후 삭제, 메시지 큐에 이벤트 테이블 이벤트에서 발표 될 예정이다.

2 단계에서, 우리가 어떻게 이벤트를 게시 이벤트 사이에 자성을 보장하고 삭제합니까 :하지만 여기에 또 다른 문제가있다? 대답은 우리가 자신의 원 자성을 보장하지 않습니다, 우리는 "적어도 한 번 전달"되도록해야하고, 그래서 그 소비 전력을 보장 할 수 있습니다. 다음과 같이이 때, 장면은 실질적으로 :

  • 代码中先发布事件,成功后再从事件表中删除事件;
  • 发布消息成功,事件删除也成功,皆大欢喜;
  • 如果消息发布不成功,那么代码中不会执行事件删除逻辑,就像事情没有发生一样,一致性得到保证;
  • 如果消息发布成功,但是事件删除失败,那么在第二次任务执行时,会重新发布消息,导致消息的重复发送。然而,由于我们要求了消费方的幂等性,也即消费方多次消费同一条消息是ok的,整个过程的一致性也得到了保证。

发布领域事件的整个流程如下:

게시 필드 이벤트

  1. 接受用户请求;
  2. 处理用户请求;
  3. 写入业务表;
  4. 写入事件表,事件表和业务表的更新在同一个本地数据库事务中;
  5. 事务完成后,即时触发事件的发送(比如可以通过Spring AOP的方式完成,也可以定时扫描事件表,还可以借助诸如MySQL的binlog之类的机制);
  6. 后台任务读取事件表;
  7. 后台任务发送事件到消息队列;
  8. 发送成功后删除事件。

更多有关事件表的介绍,请参考Chris Richardson"Transaction Outbox模式"Udi Dahan"在不使用分布式事务条件下如何处理消息可靠性"的视频。

在事件表场景下,一种常见的做法是将领域事件保存到聚合根中,然后在Repository保存聚合根的时候,将事件保存到事件表中。这种方式对于所有的Repository/聚合根都采用的方式处理,因此可以创建对应的抽象基类。

创建所有聚合根的基类DomainEventAwareAggregate如下:

public abstract class DomainEventAwareAggregate {
    @JsonIgnore
    private final List<DomainEvent> events = newArrayList();

    protected void raiseEvent(DomainEvent event) {
        this.events.add(event);
    }

    void clearEvents() {
        this.events.clear();
    }

    List<DomainEvent> getEvents() {
        return Collections.unmodifiableList(events);
    }
}

这里的raiseEvent()方法用于在具体的聚合根对象中产生领域事件,然后在Repository中获取到事件,与聚合根对象一起完成持久化,创建DomainEventAwareRepository基类如下:

public abstract class DomainEventAwareRepository<AR extends DomainEventAwareAggregate> {
    @Autowired
    private DomainEventDao eventDao;

    public void save(AR aggregate) {
        eventDao.insert(aggregate.getEvents());
        aggregate.clearEvents();
        doSave(aggregate);
    }

    protected abstract void doSave(AR aggregate);
}

具体的聚合根在实现业务逻辑之后调用raiseEvent()方法生成事件,以“更改Order收货地址”业务过程为例:

public class Order extends DomainEventAwareAggregate {

    //......
    
    public void changeAddressDetail(String detail) {
        if (this.status == PAID) {
            throw new OrderCannotBeModifiedException(this.id);
        }

        this.address = this.address.changeDetailTo(detail);
        raiseEvent(new OrderAddressChangedEvent(getId().toString(), detail, address.getDetail()));
    }
    
    //......
}

在保存Order的时候,只需要处理Order自身的持久化即可,事件的持久化已经在DomainEventAwareRepository基类中完成:

@Component
public class OrderRepository extends DomainEventAwareRepository<Order> {

    //......

    @Override
    protected void doSave(Order order) {
        String sql = "INSERT INTO ORDERS (ID, JSON_CONTENT) VALUES (:id, :json) " +
                "ON DUPLICATE KEY UPDATE JSON_CONTENT=:json;";
        Map<String, String> paramMap = of("id", order.getId().toString(), "json", objectMapper.writeValueAsString(order));
        jdbcTemplate.update(sql, paramMap);
    }

    //......

}

当业务操作的事务完成之后,需要通知消息发送设施即时发布事件到消息队列。发布过程最好做成异步的后台操作,这样不会影响业务处理的正常返回,也不会影响业务处理的效率。在Spring Boot项目中,可以考虑采用AOP的方式,在HTTP的POST/PUT/PATCH/DELETE方法完成之后统一发布事件:

   @Aspect
@Component
public class DomainEventPublishAspect {

    //......
    @After("@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PatchMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.DeleteMapping) ||")
    public void publishEvents(JoinPoint joinPoint) {
        logger.info("Trigger domain event publish process.");
        taskExecutor.execute(() -> publisher.publish());
    }
    //......
}

以上,我们使用了TaskExecutor在后台开启新的线程完成事件发布,实际的发布由RabbitDomainEventPublisher完成:

@Component
public class DomainEventPublisher {
  
    // ......
    public void publish() {
        Instant now = Instant.now();
        LockConfiguration configuration = new LockConfiguration("domain-event-publisher", now.plusSeconds(10), now.plusSeconds(1));
        distributedLockExecutor.execute(this::doPublish, configuration);
    }

    //......
}

这里,我们使用了分发布锁来处理并发发送的情况,doPublish()方法将调用实际的消息队列(比如RabbitMQ/Kafka等)API完成消息发送。更多的代码细节,请参考本文的示例代码


消费领域事件

在事件消费时,除了完成基本的消费逻辑外,我们需要重点关注以下两点:

  1. 消费方的幂等性
  2. 消费方有可能进一步产生事件

对于“消费方的幂等性”,在上文中我们讲到事件的发送机制保证的是“至少一次投递”,为了能够正确地处理重复消息,要求消费方是幂等的,即多次消费事件与单次消费该事件的效果相同。为此,在消费方创建一个事件记录表,用于记录已经消费过的事件,在处理事件时,首先检查该事件是否已经被消费过,如果是则不做任何消费处理。

对于第2点,我们依然沿用前文讲到的事件表的方式。事实上,无论是处理HTTP请求,还是作为消息的消费方,对于聚合根来讲都是无感知的,领域事件由聚合根产生进而由Repository持久化,这些过程都与具体的业务操作源头无关。

综上,在消费领域事件的过程中,程序需要更新业务表、事件记录表以及事件发送表,这3个操作过程属于同一个本地事务,此时整个事件的发布和消费过程如下:

이벤트 게시 및 전체 프로세스의 소비

在编码实践时,可以考虑与事件发布过程相同的AOP方式完成对事件的记录,以Spring和RabbitMQ为例,可以将@RabbitListener通过AOP代理起来:

@Aspect
@Component
public class DomainEventRecordingConsumerAspect {

    //......
    @Around("@annotation(org.springframework.amqp.rabbit.annotation.RabbitHandler) || " +
            "@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener)")
    public Object recordEvents(ProceedingJoinPoint joinPoint) throws Throwable {
        return domainEventRecordingConsumer.recordAndConsume(joinPoint);
    }
    //......
}

然后在代理过程中通过DomainEventRecordingConsumer完成事件的记录:

@Component
public class DomainEventRecordingConsumer {

    //......
    @Transactional
    public Object recordAndConsume(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        Optional<Object> optionalEvent = Arrays.stream(args)
                .filter(o -> o instanceof DomainEvent)
                .findFirst();

        if (optionalEvent.isPresent()) {
            DomainEvent event = (DomainEvent) optionalEvent.get();
            try {
                dao.recordEvent(event);
            } catch (DuplicateKeyException dke) {
                logger.warn("Duplicated {} skipped.", event);
                return null;
            }

            return joinPoint.proceed();
        }
        return joinPoint.proceed();
    }
    //......
}

这里的DomainEventRecordingConsumer通过直接向事件记录表中插入事件的方式来判断消息是否重复,如果发生重复主键异常DuplicateKeyException,即表示该事件已经在记录表中存在了,因此直接return null;而不再执行业务过程。

需要特别注意的一点是,这里的封装方法recordAndConsume()需要打上@Transactional注解,这样才能保证对事件的记录和业务处理在同一个事务中完成。

此外,由于消费完毕后也需要即时发送事件,因此需要在发布事件的AOP配置DomainEventPublishAspect中加入@RabbitListener

@Aspect
@Component
public class DomainEventPublishAspect {

    //......
    @After("@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PatchMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.DeleteMapping) ||" +
            "@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener) ||")
    public void publishEvents(JoinPoint joinPoint) {
        logger.info("Trigger domain event publish process.");
        taskExecutor.execute(() -> publisher.publish());
    }
    //......
}

事件驱动架构的2种风格

事件驱动架构存在多种风格,本文就其中的2种主要风格展开讨论,它们是:

  1. 事件通知
  2. 事件携带状态转移(Event-Carried State Transfer)

在“事件通知”风格中,事件只是作为一种信号传递到消费方,消费方需要的数据需要额外API请求从源事件系统获取,如图:

이벤트 알림

在上图的事件通知风格中,对事件的处理流程如下:

  1. 发布方发布事件
  2. 消费方接收事件并处理
  3. 消费方调用发布方的API以获取事件相关数据
  4. 消费方更新自身状态

这种风格的好处是,事件可以设计得非常简单,通常只需要携带聚合根的ID即可,由此进一步降低了事件驱动系统中的耦合度。然而,消费方需要的数据依然需要额外的API调用从发布方获取,这又从另一个角度增加了系统之间的耦合性。此外,如果源系统宕机,消费方也无法完成后续操作,因此可用性会受到影响。

在“事件携带状态转移”中,消费方所需要的数据直接从事件中获取,因此不需要额外的API请求:

이벤트는 상태 전이를 수행

这种风格的好处在于,即便发布方系统不可用,消费方依然可以完成对事件的处理。

笔者的建议是,对于发布方来说,作为一种数据提供者的“自我修养”,事件应该包含足够多的上下文数据,而对于消费方来讲,可以根据自身的实际情况确定具体采用哪种风格。在同一个系统中,同时采用2种风格是可以接受的。比如,对于基于事件的CQRS而言,可以采用“事件通知”,此时的事件只是一个“触发器”,一个聚合下的所有事件所触发的结果是一样的,即都是告知消费方需要从源系统中同步数据,因此此时的消费方可以对聚合下的所有事件一并处理,而不用为每一种事件单独开发处理逻辑。

事实上,事件驱动还存在第3种风格,即事件溯源,本文不对此展开讨论。更多有关事件驱动架构不同风格的介绍,请参考Martin Fowler的“事件风格”文章


第二部分:基于RabbitMQ的示例项目

在本部分中,我将以一个简单的电商平台微服务系统为例,采用RabbitMQ作为消息机制讲解事件驱动架构落地的全过程。

该电商系统包含3个微服务,分别是:

  • 订单(Order)服务:用于用户下单
  • 产品(Product)服务:用于管理/展示产品信息
  • 库存(Inventory)服务:用于管理产品对应的库存

整个系统包含以下代码库:

代码库 用途 地址
order-backend Order服务 https://github.com/e-commerce-sample/order-backend
product-backend Product服务 https://github.com/e-commerce-sample/product-backend
inventory-backend Inventory服务 https://github.com/e-commerce-sample/inventory-backend
common 共享依赖包 https://github.com/e-commerce-sample/common
devops 基础设施 https://github.com/e-commerce-sample/devops

其中,common代码库包含了所有服务所共享的代码和配置,包括所有服务中的所有事件(请注意,这种做法只是笔者为了编码上的便利,并不是一种好的实践,一种更好的实践是各个服务各自管理自身产生的事件),以及RabbitMQ的通用配置(即每个服务都采用相同的方式配置RabbitMQ设施),同时也包含了异常处理和分布式锁等配置。devops库中包含了RabbitMQ的Docker镜像,用于在本地测试。

整个系统中涉及到的领域事件如下:

필드 이벤트 예를 들어 전기 공급 시스템

其中:

  • Order服务自己消费了自己产生的所有OrderEvent用于CQRS同步读写模型;
  • Inventory服务消费了Order服务的OrderCreatedEvent事件,用于在下单之后即时扣减库存;
  • Inventory服务消费了Product服务的ProductCreatedEventProductNameChangedEvent事件,用于同步产品信息;
  • Product服务消费了Inventory服务的InventoryChangedEvent用于更新产品库存。

配置RabbitMQ

阅读本小节需要熟悉RabbitMQ中的基本概念,建议不熟悉RabbitMQ的读者事先参考RabbitMQ入门文章

这里介绍2种RabbitMQ的配置方式,一种简单的,一种稍微复杂的。两种配置过程中会反复使用到以下概念,读者可以先行熟悉:

概念 类型 解释 命名 示例
发送方Exchange Exchange 用于接收某个微服务中所有消息的Exchange,一个服务只有一个发送方Exchange xxx-publish-x order-publish-x
发送方DLX Exchange 用于接收发送方无法路由的消息 xxx-publish-dlx order-publish-dlx
发送方DLQ Queue 用于存放发送方DLX的消息 xxx-publish-dlq order-publish-dlq
接收方Queue Queue 用于接收发送方Exchange的消息,一个服务只有一个接收方Queue用于接收所有外部消息 xxx-receive-q product-receive-q
接收方DLX Exchange 死信Exchange,用于接收消费失败的消息 xxx-receive-dlx product-receive-dlx
接收方DLQ Queue 死信队列,用于存放接收方DLX的消息 xxx-receive-dlq product-receive-dlq
接收方恢复Exchange Exchange 用于接收从接收方DLQ中手动恢复的消息,接收方Queue应该绑定到接收方恢复Exchange xxx-receive-recover-x product-receive-recover-x

在简单配置方式下,消息流向图如下:

RabbitMQ 간단한 방법 구성

  1. 发送方发布事件到发送方Exchange
  2. 消息到达消费方的接收方Queue
  3. 消费成功处理消息,更新本地数据库
  4. 如果消息处理失败,消息被放入接收方DLX
  5. 消息到达死信队列接收方DLQ
  6. 对死信消息做手工处理(比如作日志记录等)

对于发送方而言,事件驱动架构提倡的是“发送后不管”机制,即发送方只需要保证事件成功发送即可,而不用关心是谁消费了该事件。因此在配置发送方的RabbitMQ时,可以简单到只配置一个发送方Exchange即可,该Exchange用于接收某个微服务中所有类型的事件。在消费方,首先配置一个接收方Queue用于接收来自所有发送方Exchange的所有类型的事件,除此之外对于消费失败的事件,需要发送到接收方DLX,进而发送到接收方DLQ中,对于接收方DLQ的事件,采用手动处理的形式恢复消费。

在简单方式下的RabbitMQ配置如下:

RabbitMQ 간단한 방법 구성

在第2种配置方式稍微复杂一点,其建立在第1种基础之上,增加了发送方的死信机制以及消费方用于恢复消费的Exchange,此时的消息流向如下:

보낸 사람과받는 사람의 복구 DLQ 교환 구성

  1. 发送方发布事件
  2. 事件发布失败时被放入死信Exchange发送方DLX
  3. 消息到达死信队列发送方DLQ
  4. 对于发送方DLQ中的消息进行人工处理,重新发送
  5. 如果事件发布正常,则会到达接收方Queue
  6. 正常处理事件,更新本地数据库
  7. 事件处理失败时,发到接收方DLX,进而路由到接收方DLQ
  8. 手工处理死信消息,将其发到接收方恢复Exchange,进而重新发到接收方Queue

此时的RabbitMQ配置如下:

在以上2种方式中,我们都启用了RabbitMQ的“发送方确认”和“消费方确认”,另外,发送方确认也可以通过RabbitMQ的事务(不是分布式事务)替代,不过效率更低。更多关于RabbitMQ的知识,可以参考笔者的Spring AMQP学习笔记RabbitMQ最佳实践


系统演示

  • 启动RabbitMQ,切换到ecommerce-sample/devops/local/rabbitmq目录,运行:
./start-rabbitmq.sh
  • 启动Order服务:切换到ecommerce-sample/order-backend项目,运行:
./run.sh //监听8080端口,调试5005端口
  • 启动Product服务:切换到ecommerce-sample/product-backend项目,运行:
./run.sh //监听8082端口,调试5006端口
  • 启动Inventory服务:切换到ecommerce-sample/inventory-backend项目,运行:
./run.sh //监听8083端口,调试5007端口
  • 创建Product:
curl -X POST \
  http://localhost:8082/products \
  -H 'Content-Type: application/json' \
  -H 'cache-control: no-cache' \
  -d '{
    "name":"好吃的苹果",
    "description":"原生态的苹果",
    "price": 10.0
}'

此时返回Product ID:

{"id":"3c11b3f6217f478fbdb486998b9b2fee"}
  • 查看Product:
curl -X GET \
  http://localhost:8082/products/3c11b3f6217f478fbdb486998b9b2fee \
  -H 'cache-control: no-cache'

返回如下:

{
    "id": {
        "id": "3c11b3f6217f478fbdb486998b9b2fee"
    },
    "name": "好吃的苹果",
    "price": 10,
    "createdAt": 1564361781956,
    "inventory": 0,
    "description": "原生态的苹果"
}

可以看到,新创建的Product的库存(inventory)默认为0。

  • 创建Product时,会创建ProductCreatedEvent,Inventory服务接收到该事件后会自动创建对应的Inventory,日志如下:
2019-07-29 08:56:22.276 -- INFO  [taskExecutor-1] c.e.i.i.InventoryEventHandler : Created inventory[5e3298520019442b8a6d97724ab57d53] for product[3c11b3f6217f478fbdb486998b9b2fee].
  • 增加Inventory为10:
curl -X POST \
  http://localhost:8083/inventories/5e3298520019442b8a6d97724ab57d53/increase \
  -H 'Content-Type: application/json' \
  -H 'cache-control: no-cache' \
  -d '{
    "increaseNumber":10
}'
  • 재고의 증가 후, InventoryChangedEvent는, 제품의 서비스가 자동으로 이벤트 이후 자신의 인벤토리를 동기화합니다받은 보내 제품을 다시 볼 수 있습니다 :
curl -X GET \
  http://localhost:8082/products/3c11b3f6217f478fbdb486998b9b2fee \
  -H 'cache-control: no-cache'

다음 반환 :

{
    "id": {
        "id": "3c11b3f6217f478fbdb486998b9b2fee"
    },
    "name": "好吃的苹果",
    "price": 10,
    "createdAt": 1564361781956,
    "inventory": 10,
    "description": "原生态的苹果"
}

당신은 제품 재고는 10으로 업데이트되었습니다 볼 수 있습니다.

  • 지금까지, 제품 및 재고 주문을하자, 준비 :
curl -X POST \
  http://localhost:8080/orders \
  -H 'Content-Type: application/json' \
  -H 'cache-control: no-cache' \
  -d '{
  "items": [
    {
      "productId": "3c11b3f6217f478fbdb486998b9b2fee",
      "count": 2,
      "itemPrice": 10
    }
  ],
  "address": {
    "province": "四川",
    "city": "成都",
    "detail": "天府软件园1号"
  }
}'

주문 ID를 반환합니다 :

{
    "id": "d764407855d74ff0b5bb75250483229f"
}
  • 순서를 생성 한 후, 재고 서비스가 자동으로 적절한 재고를 공제 할 이벤트를 수신, OrderCreatedEvent를 전송됩니다 :
2019-07-29 09:11:31.202 -- INFO  [taskExecutor-1] c.e.i.i.InventoryEventHandler : Inventory[5e3298520019442b8a6d97724ab57d53] decreased to 8 due to order[d764407855d74ff0b5bb75250483229f] creation.

한편, 재고가 InventoryChangedEvent가 전송, 제품 서비스는 이벤트가 자동으로 다시 재고 제품보기 제품을 업데이트 수신 :

curl -X GET \
  http://localhost:8082/products/3c11b3f6217f478fbdb486998b9b2fee \
  -H 'cache-control: no-cache'

다음 반환 :

{
    "id": {
        "id": "3c11b3f6217f478fbdb486998b9b2fee"
    },
    "name": "好吃的苹果",
    "price": 10,
    "createdAt": 1564361781956,
    "inventory": 8,
    "description": "原生态的苹果"
}

때문에 우리는 우리가이 제품을 선택 이전 주문의 10에서 8로, 제품 재고 감소를 볼 수 있습니다.

개요

이벤트 중심 아키텍처라고 먼저 독립 메시지 큐 기술 및 방문 과정에서 문제의 여러 측면, 이벤트 필드는 루트 일시적인 이벤트를 중합하여 모델링을 포함하고, 저장소에 의해 완성 된 백그라운드 작업으로 저장하고 읽어 이벤트는 이벤트를 실제 출시를 형성한다. 소비자 측에서 "적어도 한 번 전달"의 경우 소비 문제를 반복 해결 멱등에 의해 초래. 또한,도 상태 전이뿐만 아니라, 그들 사이의 장점과 단점에 의해 수행되는 일반적인 스타일의 이벤트 중심 아키텍처, 이벤트 알림 및 이벤트 두 종류의 이야기. 두 번째 부분에서는, RabbitMQ에 예를 들어, 마이크로 시스템 이벤트 기반 아키텍처의 서비스 방법 착륙을 공유했습니다.

추천

출처www.cnblogs.com/davenkin/p/eda-coding-pratices.html