Back-end development practices series of three - event-driven architecture (EDA) coding practices

In the first two articles in this series, the author talked about code templates back-end projects and DDD coding practices , in this article, I will continue to share practical way how to encode landing event-driven architecture.

Simply speaking event-driven architecture (Event Driven Architecture, EDA), it is the topic appeared a few decades ago; simply speaking areas of the event, that is a lot of years are mentioned and discussed faster ripe language software . However, look at the author's observation, event-driven architecture far from imaginary universally accepted as the development team. Even engage in micro-services people know that in addition to the message as well as asynchronous HTTP synchronization mechanism, even if people know DDD engage in the field of events is one of the first-class citizens, the advantages of event-driven architecture brought no corresponding transformed into software favored by practitioners.

I try to think about the reasons, summed up the two points: the first is likely to be event-driven mode of operation of the objective world, but it is not a natural way of thinking of the people; second is event-driven architecture to bring benefits to the software at the same time, we will add additional complexity, such as the difficulty of debugging, but such is not intuitive eventual consistency.

Of course, there are in fact many software projects use a message queue, but there needs to be clear that the use of message queue does not mean that your project will certainly be an event-driven architecture, a lot of projects just because the drive technology, a small scale using certain message queue of products (such as RabbitMQ Kafka and the like) only. A huge system, if you notice the message queue is only used as a mailing, then this natural system not to mention the use of event-driven architecture.

Into the moment, the rise of micro-services, DDD reproduce, in the event-driven architecture, we need to consider the constraint modeling business, design events, DDD, the delimitation of the boundary context factors and more technical aspects, this one how do systems engineering from start to finish floor, is the need to think through and scrutiny. Again, pay attention to programming is not an easy task.

Indeed, there is good use of event-driven architecture difficulty on the practice, but its advantage is also really attractive, we expect a certain "structured" and "routine" so that event-driven architecture is simpler landing.

This paper is divided into two parts, the first part independent of the specific message queue implementation to explain the general modeling of the field events, with the second part of a real micro-service system, for example, the use RabbitMQ as a message queue, and thus sharing complete event-driven architecture practice floor.

In this paper, DDD as the basis for coding, which will involve a lot of DDD concept, such as aggregate root, resource libraries and application services, DDD unfamiliar to the reader can refer to the author of DDD coding practices article .

Herein refer to the sample code on GitHub E-Commerce-Sample project.


Modeling field events: the first part

Field events is a concept of DDD, is represented by a business valuable thing in a field that occur, fell at the technical level is a business entity object (usually it is the aggregate root) status has changed after the need to issue a field event. Although event-driven architecture "event" does not necessarily mean "field event", but this article due to the close connection DDD, so when referring to an incident, we especially "in the field events."


Create a field event

Basic knowledge about the field events, please refer to the author's use in the field of micro-services in the event the article, article links directly into coding practices.

When modeling the field events, we first need to record some general information about the event, such as a unique identification ID and creation time for this event to create a base class DomainEvent:

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

In DDD scene, field events with general state polymerization update root and produce, in addition, in the event the consumer side, we hope that all event listeners sometimes occur at a certain aggregate root, it is recommended this author for each aggregate root event objects created corresponding base class, which contains the ID of the root of the polymerization, for example, order (Order) class, create OrderEvent:

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

Then for the actual event Order unity inherited from OrderEvent, for example, to create an order of OrderCreatedEventevents:

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

Inheritance chain field events is as follows:

Field events inheritance chain

When you create a field event, you need to pay attention to two points:

  • Field event itself should be unchanging (Immutable);
  • Field events should carry contextual data associated with the event occurs, but not the root of the entire state data aggregation, for example, when you create an order may carry basic information about the order, and for the product (Product) Name updated ProductNameUpdatedEventevents, you should It contains the name of the product before and after the update:
public class ProductNameUpdatedEvent extends ProductEvent {
    private String oldName; //更新前的名称
    private String newName; // 更新后的名称
}

Published field events

There are many ways publishing field events, such as the application service can be published (ApplicationService) can also be published in the repository (Repository), the way the event table can also be introduced, published a detailed comparison of these three methods can refer to the author's use event in the field of micro-service article. I suggest using the event table mode, here to discuss it.

The usual business process will update the database and then publish the field events, a more important point here is: we need to ensure atomicity between database updates and event publishing, that either both succeed or fail. In conventional practice, the global transaction (Global Transaction / XA Transaction) is commonly used to resolve such problems. However, the efficiency is very low global transaction itself, in addition, some of the technology framework does not provide support for global transactions. Currently, a more highly regarded for introducing an event table approach is that process is as follows:

  1. While updating the business table, the field events are saved in the event table in the database, then the business table and a local event table in the same transaction, which is to ensure the atomicity, but also to ensure efficiency.

  2. Open a task in the background, it will be released in the event table event to the message queue, deleted after the event sent successfully.

But here there is another problem: In step 2, how do we ensure atomicity between the published events and delete events it? The answer is: we do not guarantee their atomicity, we need to ensure that "at least once delivered", and to ensure that consumer power and so on. At this time, the scene is substantially as follows:

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

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

Posted field events

  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个操作过程属于同一个本地事务,此时整个事件的发布和消费过程如下:

Event publishing and consumption of the whole process

在编码实践时,可以考虑与事件发布过程相同的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请求从源事件系统获取,如图:

Event Notification

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

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

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

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

Event carry the state transition

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

笔者的建议是,对于发布方来说,作为一种数据提供者的“自我修养”,事件应该包含足够多的上下文数据,而对于消费方来讲,可以根据自身的实际情况确定具体采用哪种风格。在同一个系统中,同时采用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镜像,用于在本地测试。

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

Field event example electricity supplier system

其中:

  • 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 simple way configuration

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

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

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

RabbitMQ simple way configuration

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

Configuring the sender and receiver recovery DLQ Exchange

  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
}'
  • After the increase in Inventory, will send InventoryChangedEvent, Product service received will automatically synchronize their inventory after the event, see Product again:
curl -X GET \
  http://localhost:8082/products/3c11b3f6217f478fbdb486998b9b2fee \
  -H 'cache-control: no-cache'

Returns the following:

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

You can see, Product inventory has been updated to 10.

  • So far, Product and Inventory are ready, let's orders it:
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号"
  }
}'

Returns Order ID:

{
    "id": "d764407855d74ff0b5bb75250483229f"
}
  • After creating the order, will be sent OrderCreatedEvent, Inventory service receives the event will automatically deduct the appropriate inventory:
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.

Meanwhile, Inventory sends InventoryChangedEvent, Product service receives the event will automatically update inventory Product view Product again:

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

Returns the following:

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

We can see, Product inventory reduction from 10 to 8, because of a previous order we chose 2 Product.

to sum up

Firstly independent message queue technology, referred to the event-driven architecture, and many aspects of the problem in the process of landing, an event field comprises modeling by polymerizing root temporary event and completed by the Repository is stored and then read by the background task events form the event the actual release. In the consumer side, repeated consumption problem in the case of "at least once delivered" brought about by idempotency resolved. In addition, also spoke of two kinds of common style event-driven architecture, event notifications, and event that is carried by the state transition, as well as the advantages and disadvantages between them. In the second part, to RabbitMQ for example, shared how landing in the service of a micro-system event-driven architecture.

Guess you like

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