CQRS Event Sourcing之简单场景落地分析

前言

CQRS全称为Command Query Responsibility Segregation,是领域驱动编程思想中的一个概念,当然也可以脱离DDD,当作读写分离去使用。

传统Rest模式中,DTO -> PO基本上是一样的,是一种面向数据库模型编程,且读和写操作的模型耦合,也不太方便将领域数据映射到页面显示。

CQRS将读和写分为Query与Command。
其中Command属于写操作,应该声明为void 或者返回id。
其中Query属于读操作,不应该存在修改状态行为,返回具体数据类型。


简单应用

首先抽象出Command和CommandHandler的概念,前者代表命令,后者代表命令处理者,Query同理。

public interface Command {
}

public interface CommandHandler> {
   /**
    * command handle
    *
    * @param command
    * @return
    */
   R handle(C command);
}

public interface Query {
}

public interface QueryHandler> {
   /**
    * query handle
    *
    * @param query
    * @return
    */
   R handle(C query);
}

基于Spring实现的话,可以使用IOC容器现成的applicationContext工厂实现Command Handler打表。

public class CommandProvider> {
    private final ApplicationContext applicationContext;
    private final Class type;

    CommandProvider(ApplicationContext applicationContext, Class type) {
        this.applicationContext = applicationContext;
        this.type = type;
    }

    public H get() {
        return applicationContext.getBean(type);
    }
}

public class QueryProvider> {
    private final ApplicationContext applicationContext;
    private final Class type;

    QueryProvider(ApplicationContext applicationContext, Class type) {
        this.applicationContext = applicationContext;
        this.type = type;
    }

    public H get() {
        return applicationContext.getBean(type);
    }
}


public class CommandHandlerRegistrar {
    private Map, CommandProvider> commandProviderMap = new HashMap<>();
    private Map, QueryProvider> queryProviderMap = new HashMap<>();

    public CommandHandlerRegistrar(ApplicationContext applicationContext) {
        String[] names = applicationContext.getBeanNamesForType(CommandHandler.class);
        for (String name : names) {
            registerCommand(applicationContext, name);
        }
        names = applicationContext.getBeanNamesForType(QueryHandler.class);
        for (String name : names) {
            registerQuery(applicationContext, name);
        }
    }

    private void registerCommand(ApplicationContext applicationContext, String name) {
        Class> handlerClass = (Class>)applicationContext.getType(name);
        Class[] generics = GenericTypeResolver.resolveTypeArguments(handlerClass, CommandHandler.class);
        Class commandType = (Class)generics[1];
        commandProviderMap.put(commandType, new CommandProvider(applicationContext, handlerClass));
    }

    private void registerQuery(ApplicationContext applicationContext, String name) {
        Class> handlerClass = (Class>)applicationContext.getType(name);
        Class[] generics = GenericTypeResolver.resolveTypeArguments(handlerClass, QueryHandler.class);
        Class queryType = (Class)generics[1];
        queryProviderMap.put(queryType, new QueryProvider(applicationContext, handlerClass));
    }

    @SuppressWarnings("unchecked")
    > CommandHandler getCmd(Class commandClass) {
        return commandProviderMap.get(commandClass).get();
    }

    @SuppressWarnings("unchecked")
    > QueryHandler getQuery(Class commandClass) {
        return queryProviderMap.get(commandClass).get();
    }
}

再抽象出EventBus

public interface EventBus {
    /**
     * command
     *
     * @param command
     * @param 
     * @param 
     * @return
     */
    > R executeCommand(C command);

    /**
     * query
     *
     * @param query
     * @param 
     * @param 
     * @return
     */
    > R executeQuery(Q query);
}

public class SpringEventBus implements EventBus {
    private final CommandHandlerRegistrar registry;

    public SpringEventBus(CommandHandlerRegistrar registry) {
        this.registry = registry;
    }

    @Override
    public > R executeCommand(C command) {
        CommandHandler commandHandler = (CommandHandler)registry.getCmd(command.getClass());
        return commandHandler.handle(command);
    }

    @Override
    public > R executeQuery(Q query) {
        QueryHandler queryHandler = (QueryHandler)registry.getQuery(query.getClass());
        return queryHandler.handle(query);
    }
}

@Configuration即完成了Command Handler注册发现。

    @Bean
    public CommandHandlerRegistrar registry(ApplicationContext applicationContext) {
        return new CommandHandlerRegistrar(applicationContext);
    }

    @Bean
    public EventBus commandBus(CommandHandlerRegistrar registry) {
        return new SpringEventBus(registry);
    }

然后在Controller层就可以直接依赖EventBus做读写处理,替换以前的service操作。

@RestController
@RequiredArgsConstructor
public class PoliciesController {
    private final EventBus bus;

    @PostMapping
    public ResponseEntity createPolicy(@RequestBody CreatePolicyCommand command) {
        return ok(bus.executeCommand(command));
    }

    @PostMapping("/confirmTermination")
    public ResponseEntity terminatePolicy(@RequestBody ConfirmTerminationCommand command) {
        return ok(bus.executeCommand(command));
    }

    @PostMapping("/confirmBuyAdditionalCover")
    public ResponseEntity buyAdditionalCover(@RequestBody ConfirmBuyAdditionalCoverCommand command) {
        return ok(bus.executeCommand(command));
    }

    @PostMapping("/find")
    public Collection find(@RequestBody FindPoliciesQuery query) {
        return bus.executeQuery(query);
    }

    @GetMapping("/details/{policyNumber}/versions")
    public ResponseEntity getPolicyVersions(@PathVariable String policyNumber) {
        return ok(bus.executeQuery(new GetPolicyVersionsListQuery(policyNumber)));
    }

    @GetMapping("/details/{policyNumber}/versions/{versionNumber}")
    public ResponseEntity getPolicyVersionDetails(@PathVariable String policyNumber, @PathVariable int versionNumber) {
        return ok(bus.executeQuery(new GetPolicyVersionDetailsQuery(policyNumber, versionNumber)));
    }

}


这里是一个Command和Query操作分发的实现雏形,有几点细节。

(1) EventBus实现有多种方式,Controller依赖抽象即可替换,本质是Scan到所有CommandHandler子类以后打一张map表,key是Command Class,value是CommandProvider工厂。这里自研注解在ImportBeanDefinitionRegistrar流程操作BeanDefinition也可以,自己用scanner跳过spring打表也可以。

(2)Bus就不区分Command和Query了,他属于dispatcher。

(3)读和写的模型分开了,写入参Command实现类,读入参Query实现类。

往下看一下Handler逻辑

@Component
@Transactional(rollbackFor = Throwable.class)
@RequiredArgsConstructor
public class CreatePolicyHandler implements CommandHandler {

    private final OfferRepository offerRepository;
    private final PolicyRepository policyRepository;
    private final EventPublisher eventPublisher;

    @Override
    public CreatePolicyResult handle(CreatePolicyCommand command) {
        Offer offer = offerRepository.withNumber(command.getOfferNumber());
        Policy policy = Policy.convertOffer(offer, UUID.randomUUID().toString(), command.getPurchaseDate(), command.getPolicyStartDate());
        policyRepository.add(policy);

        eventPublisher.publish(new PolicyEvents.PolicyCreated(this, policy));

        return new CreatePolicyResult(policy.getNumber());
    }
}

Repository和EventPublisher都属于抽象,可替换实现。

看一下领域对象和Event。

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Policy {

    @Id
    @GeneratedValue
    private UUID id;
    private String number;
    @ManyToOne(optional = false)
    private Product product;
    @OneToMany(cascade = CascadeType.ALL)
    private List versions = new ArrayList<>();
    private LocalDate purchaseDate;

    public Policy(UUID uuid, String policyNumber, Product product, LocalDate purchaseDate) {
        this.id = uuid;
        this.number = policyNumber;
        this.product = product;
        this.purchaseDate = purchaseDate;
    }

    public static Policy convertOffer(
        Offer offer,
        String policyNumber,
        LocalDate purchaseDate,
        LocalDate policyStartDate) {
        if (offer.isConverted()) { throw new BusinessException("Offer already converted"); }

        if (offer.isRejected()) { throw new BusinessException("Offer already rejected"); }

        if (offer.isExpired(purchaseDate)) { throw new BusinessException("Offer expired"); }

        if (offer.isExpired(policyStartDate)) { throw new BusinessException("Offer not valid at policy start date"); }

        Policy
            newPolicy = new Policy(
            UUID.randomUUID(),
            policyNumber,
            offer.getProduct(),
            purchaseDate
        );

        newPolicy.addFirstVersion(offer, purchaseDate, policyStartDate);
        newPolicy.confirmChanges(1);

        return newPolicy;
    }

    public void extendCoverage(LocalDate effectiveDateOfChange, CoverPrice newCover) {
        //preconditions
        if (isTerminated()) { throw new BusinessException("Cannot annex terminated policy"); }

        Optional versionAtEffectiveDate = getPolicyVersions().effectiveAtDate(effectiveDateOfChange);
        if (!versionAtEffectiveDate.isPresent()) { throw new BusinessException("No active version at given date"); }

        PolicyVersion annexVer = addNewVersionBasedOn(versionAtEffectiveDate.get(), effectiveDateOfChange);
        annexVer.addCover(newCover, effectiveDateOfChange, annexVer.getCoverPeriod().getTo());
    }

    private boolean isTerminated() {
        return versions.stream().anyMatch(v -> v.isActive() && PolicyStatus.Terminated.equals(v.getPolicyStatus()));
    }

    public void terminatePolicy(LocalDate effectiveDateOfChange) {
        if (isTerminated()) { throw new BusinessException("Policy already terminated"); }

        Optional versionAtEffectiveDateOpt = getPolicyVersions().effectiveAtDate(effectiveDateOfChange);
        if (!versionAtEffectiveDateOpt.isPresent()) { throw new BusinessException("No active version at given date"); }

        PolicyVersion versionAtEffectiveDate = versionAtEffectiveDateOpt.get();

        if (!versionAtEffectiveDate.getCoverPeriod().contains(effectiveDateOfChange)) {
            throw new BusinessException("Cannot terminate policy at given date as it is not withing cover period");
        }

        PolicyVersion termVer = addNewVersionBasedOn(versionAtEffectiveDate, effectiveDateOfChange);
        termVer.endPolicyOn(effectiveDateOfChange.minusDays(1));
    }

    public void cancelLastAnnex() {
        PolicyVersion lastActiveVer = getPolicyVersions().latestActive();
        if (lastActiveVer == null) { throw new BusinessException("There are no annexed left to cancel"); }

        lastActiveVer.cancel();
    }

    public void confirmChanges(int versionToConfirmNumber) {
        Optional versionToConfirm = getPolicyVersions().withNumber(versionToConfirmNumber);
        if (!versionToConfirm.isPresent()) { throw new BusinessException("Version not found"); }

        versionToConfirm.get().confirm();
    }

    private void addFirstVersion(Offer offer, LocalDate purchaseDate, LocalDate policyStartDate) {
        PolicyVersion
            ver = new PolicyVersion(
            UUID.randomUUID(),
            1,
            PolicyStatus.Active,
            DateRange.between(policyStartDate, policyStartDate.plus(offer.getCoverPeriod())),
            DateRange.between(policyStartDate, policyStartDate.plus(offer.getCoverPeriod())),
            offer.getCustomer().copy(),
            offer.getDriver().copy(),
            offer.getCar().copy(),
            offer.getTotalCost(),
            offer.getCovers()
        );

        versions.add(ver);
    }

    private PolicyVersion addNewVersionBasedOn(
        PolicyVersion versionAtEffectiveDate, LocalDate effectiveDateOfChange) {
        PolicyVersion
            newVersion = new PolicyVersion(
            versionAtEffectiveDate,
            getPolicyVersions().maxVersionNumber() + 1,
            effectiveDateOfChange);

        versions.add(newVersion);
        return newVersion;
    }

    public PolicyVersions getPolicyVersions() {
        return new PolicyVersions(versions);
    }

    public enum PolicyStatus {
        Active,
        Terminated
    }
}

public class PolicyEvents {

    @Getter
    public static class PolicyCreated extends Event {
        private Policy newPolicy;

        public PolicyCreated(Object source, Policy newPolicy) {
            super(source);
            this.newPolicy = newPolicy;
        }
    }

    @Getter
    public static class PolicyAnnexed extends Event {
        private Policy annexedPolicy;
        private PolicyVersion annexVersion;

        public PolicyAnnexed(
            Object source, Policy annexedPolicy, PolicyVersion annexVersion) {
            super(source);
            this.annexedPolicy = annexedPolicy;
            this.annexVersion = annexVersion;
        }
    }

    @Getter
    public static class PolicyTerminated extends Event {
        private Policy terminatedPolicy;
        private PolicyVersion terminatedVersion;

        public PolicyTerminated(
            Object source, Policy terminatedPolicy, PolicyVersion terminatedVersion) {
            super(source);
            this.terminatedPolicy = terminatedPolicy;
            this.terminatedVersion = terminatedVersion;
        }
    }

    @Getter
    public static class PolicyAnnexCancelled extends Event {
        private Policy policy;
        private PolicyVersion cancelledAnnexVersion;
        private PolicyVersion currentVersionAfterAnnexCancellation;

        public PolicyAnnexCancelled(Object source,
            Policy policy,
            PolicyVersion cancelledAnnexVersion,
            PolicyVersion currentVersionAfterAnnexCancellation) {
            super(source);
            this.policy = policy;
            this.cancelledAnnexVersion = cancelledAnnexVersion;
            this.currentVersionAfterAnnexCancellation = currentVersionAfterAnnexCancellation;
        }
    }
}

相应的EventHandler:


@Component
@RequiredArgsConstructor
class PolicyEventsProjectionsHandler {

    private final PolicyInfoDtoProjection policyInfoDtoProjection;
    private final PolicyVersionDtoProjection policyVersionDtoProjection;

    @EventListener
    public void handlePolicyCreated(PolicyEvents.PolicyCreated event) {
        policyInfoDtoProjection.createPolicyInfoDto(event.getNewPolicy());
        policyVersionDtoProjection.createPolicyVersionDto(event.getNewPolicy(),
            event.getNewPolicy().getPolicyVersions().withNumber(1).get());
    }

    @EventListener
    public void handlePolicyTerminated(PolicyEvents.PolicyTerminated event) {
        policyInfoDtoProjection.updatePolicyInfoDto(event.getTerminatedPolicy(), event.getTerminatedVersion());
        policyVersionDtoProjection.createPolicyVersionDto(event.getTerminatedPolicy(), event.getTerminatedVersion());
    }

    @EventListener
    public void handlePolicyAnnexed(PolicyEvents.PolicyAnnexed event) {
        policyInfoDtoProjection.updatePolicyInfoDto(event.getAnnexedPolicy(), event.getAnnexVersion());
        policyVersionDtoProjection.createPolicyVersionDto(event.getAnnexedPolicy(), event.getAnnexVersion());
    }

    @EventListener
    public void handlePolicyAnnexCancelled(PolicyEvents.PolicyAnnexCancelled event) {
        policyInfoDtoProjection.updatePolicyInfoDto(event.getPolicy(), event.getCurrentVersionAfterAnnexCancellation());
        policyVersionDtoProjection.updatePolicyVersionDto(event.getCancelledAnnexVersion());
    }
}

@Component
@Transactional(rollbackFor = Throwable.class)
@RequiredArgsConstructor
public class PolicyInfoDtoProjection {

    private final PolicyInfoDtoRepository policyInfoDtoRepository;

    public void createPolicyInfoDto(Policy policy) {
        PolicyVersion policyVersion = policy.getPolicyVersions().withNumber(1).get();
        PolicyInfoDto policyInfo = buildPolicyInfoDto(policy, policyVersion);
        policyInfoDtoRepository.save(policyInfo);
    }

    public void updatePolicyInfoDto(Policy policy, PolicyVersion currentVersion) {
        PolicyInfoDto policyInfo = buildPolicyInfoDto(policy, currentVersion);
        policyInfoDtoRepository.update(policyInfo);
    }

    private PolicyInfoDto buildPolicyInfoDto(Policy policy, PolicyVersion policyVersion) {
        return new PolicyInfoDto(
            policy.getId(),
            policy.getNumber(),
            policyVersion.getCoverPeriod().getFrom(),
            policyVersion.getCoverPeriod().getTo(),
            policyVersion.getCar().getPlaceNumberWithMake(),
            policyVersion.getPolicyHolder().getFullName(),
            policyVersion.getTotalPremium().getAmount()
        );
    }
}

public interface PolicyInfoDtoRepository extends CrudRepository {

    /**
     * update
     *
     * @param policy
     */
    @Modifying
    @Query("UPDATE policy_info_dto " +
        "SET " +
        "cover_from = :policy.coverFrom, " +
        "cover_to = :policy.coverTo, " +
        "vehicle = :policy.vehicle, " +
        "policy_holder = :policy.policyHolder, " +
        "total_premium = :policy.totalPremium " +
        "WHERE " +
        "policy_id = :policy.policyId")
    void update(@Param("policy") PolicyInfoDto policy);

    /**
     * find one
     *
     * @param policyId
     * @return
     */
    @Query("SELECT * FROM policy_info_dto p WHERE p.policy_id = :policyId")
    Optional findByPolicyId(@Param("policyId") UUID policyId);

}

再看一下Query:

@Component
@RequiredArgsConstructor
public class GetPolicyVersionDetailsHandler implements QueryHandler {

    private final PolicyVersionDtoFinder policyVersionDtoFinder;

    @Override
    public PolicyVersionDto handle(GetPolicyVersionDetailsQuery query) {
        return policyVersionDtoFinder.findByPolicyNumberAndVersionNumber(query.getPolicyNumber(), query.getVersionNumber());
    }
}
@Component
@RequiredArgsConstructor
public class PolicyVersionDtoFinder {

    private final PolicyVersionDtoRepository repository;

    public PolicyVersionsListDto findVersionsByPolicyNumber(String policyNumber) {
        return new PolicyVersionsListDto(policyNumber, repository.findVersionsByPolicyNumber(policyNumber));
    }

    public PolicyVersionDto findByPolicyNumberAndVersionNumber(String policyNumber, int versionNumber) {
        PolicyVersionDto dto = repository.findByPolicyNumberAndVersionNumber(policyNumber, versionNumber);
        List coversInVersion = repository.getCoversInVersion(dto.getId());
        dto.setCovers(coversInVersion);

        return dto;
    }
}

public interface PolicyVersionDtoRepository extends CrudRepository {

    /**
     * update
     *
     * @param versionStatus
     * @param policyVersionId
     */
    @Modifying
    @Query("UPDATE policy_version_dto " +
        "SET " +
        "version_status = :versionStatus " +
        "WHERE " +
        "policy_version_id = :policyVersionId")
    void update(@Param("versionStatus") String versionStatus, @Param("policyVersionId") String policyVersionId);

    /**
     * find one
     *
     * @param policyNumber
     * @param versionNumber
     * @return
     */
    @Query(value = "SELECT " +
        "id, policy_version_id, policy_id, " +
        "policy_number, version_number, " +
        "product_code, " +
        "version_status, policy_status, " +
        "policy_holder, insured, car, " +
        "cover_from, cover_to, version_from, version_to, " +
        "total_premium_amount " +
        "FROM policy_version_dto " +
        "WHERE " +
        "policy_number = :policyNumber " +
        "AND version_number = :versionNumber",
        rowMapperClass = PolicyVersionDto.PolicyVersionDtoRowMapper.class)
    PolicyVersionDto findByPolicyNumberAndVersionNumber(
        @Param("policyNumber") String policyNumber,
        @Param("versionNumber") int versionNumber);

    /**
     * find one
     *
     * @param policyVersionDtoId
     * @return
     */
    @Query("SELECT * " +
        "FROM policy_version_cover_dto " +
        "WHERE " +
        "policy_version_dto = :policyVersionDtoId")
    List getCoversInVersion(@Param("policyVersionDtoId") Long policyVersionDtoId);

    /**
     * find one
     *
     * @param policyNumber
     * @return
     */
    @Query(value = "SELECT " +
        "version_number, " +
        "version_from, " +
        "version_to, " +
        "version_status " +
        "FROM policy_version_dto " +
        "WHERE " +
        "policy_number = :policyNumber",
        rowMapperClass = PolicyVersionsListDto.PolicyVersionInfoDtoRowMapper.class)
    List findVersionsByPolicyNumber(
        @Param("policyNumber") String policyNumber);
}

@Getter
@AllArgsConstructor
public class PolicyVersionsListDto {
    private String policyNumber;
    private List versionsInfo;

    @Getter
    @AllArgsConstructor
    public static class PolicyVersionInfoDto {
        private int number;
        private LocalDate versionFrom;
        private LocalDate versionTo;
        private String versionStatus;
    }

    static class PolicyVersionInfoDtoRowMapper implements RowMapper {

        @Override
        public PolicyVersionInfoDto mapRow(ResultSet rs, int i) throws SQLException {
            return new PolicyVersionInfoDto(
                rs.getInt("version_number"),
                rs.getDate("version_from").toLocalDate(),
                rs.getDate("version_to").toLocalDate(),
                rs.getString("version_status")
            );
        }
    }
}


代码分层

最终大体结构如下


image.png

image.png

commands存放CommandHandlers
queries存放QueryHandlers
CommandHandlers触发的Event由eventhandlers包下消费。
domain存放领域对象。

按照DDD分层的话,任何外部端口属于六边形洋葱架构,统一放在infrastructure层适配即可,本例介绍最简单的CQRS实践,就不讨论application、domain、infrastructure、interfaces那种DI分层了。


Event-Sourcing拓展

完整的Event-Sourcing的话,还需要很多细节,回溯需要Event持久化,类似于redis没有重写过的aof文件,可以将Event链路复现,方便分析数据过程,管理版本。

还有数据一致性的问题,需要引入最终一致性和柔性事务,常见的有业务上使用MQ补偿,或者Saga,像Axon Framework等现成的CQRS框架。

如果说接入Event持久化的话,并不复杂,还是Handler那个地方,Transaction注解已经包住了publish前中期的代码,publish event之前落库即可,复杂的是event可视化治理投入。

Saga现在也有现成的框架可以接。

性能方面拓展可以在Command落库以后,binlog同步es、redis、mongodb等,查询端走es,走es这个finder实现也可以随时替换成mongodb等。甚至在封装一层分布式内存缓存,击穿则读es reset。

适合Event Sourcing的场景

  • 系统没有大量的CRUD,复杂业务的团队。

  • 有DDD经验或者具备DDD素养的团队。

  • 关注业务数据产生过程,关注业务流程运维,关注报表等情况的团队。

  • 版本管理、版本回退等需求。

你可能感兴趣的:(CQRS Event Sourcing之简单场景落地分析)