cqrs java demo_CQRS-Demo

Martin Fowler

我们不应该使用既能修改数据也能返回数据的方法,这样我们就有了两种类型的方法:

查询:返回数据但不修改数据,因此没有副作用

命令:修改数据但不返回数据

CQRS: Command Query Responsibility Segregation

1. CRUD

围绕关系数据库构建而成的“创建、读取、更新、删除”系统(即CRUD系统),此类系统在一些业务逻辑简单的项目中可能没有什么问题,但是随着系统逻辑变得复杂,用户增多,这种设计就会出现一些性能问题。

bd42999b169f8ea9b7e08a4ffeb5fd9f.png

2. CQRS

简单的说,CQRS 就是一个系统,从架构上把 CRUD 系统拆分为两部分:命令(Command)处理和查询(Query)处理。其中命令处理包括增、删、改。

1d4d59d38fdc98e18fb48ad6326f2d04.png

然后命令与查询两边可以用不同的架构实现,以实现CQ两端(即Command Side,简称C端;Query Side,简称Q端)的分别优化。两边所涉及到的实体对象也可以不同,从而继续演变成下面这样。

f28b5dce8bd089cbc4a6850366fdad62.png

CQRS 强调的是 Command & Query 访问的数据模型不同,分别根据 Command & Query 需求的不同特性设计数据模型。比如 Command 更强调模型的范式化、完整性约束等。适用与查询的模型更强调性能,可以不过多地受范式的约束、又更多的数据冗余。或者 Command 用关系数据库,查询用NoSQL数据库。当然 Command & Query 使用同一个物理数据库,Query 使用View也是可以的,这是与读写分离的区别。

2.1 CQRS 实现方式

2.1.1 Command & Query 共享同一个数据库

两端数据库共享,只是在上层代码上分离。这样做的好处是可以让我们的代码读写分离,更容易维护,而且不存在 Command & Query 两端的数据一致性问题,因为是共享一个数据库的。

2.1.2 Command & Query 数据库分离

两端不仅代码分离,数据库也分离,然后Q端数据由C端同步过来。同步方式有两种:同步或异步。

如果需要 CQ 两端数据的强一致性,则需要用同步;

如果能接受 CQ 两端数据的最终一致性,则可以使用异步。

C端可以采用**Event Sourcing(简称ES)**模式,所有C端的最新数据全部用 Domain Event 表达即可;而要查询显示用的数据,则从Q端的DB查询即可。

2.2 CQRS 适用于什么场景?

应用的 读模型 和 写模型 差别比较大

单一的存储模型无法同时满足高性能的读和写需求

2.3 CQRS 带来的问题

事务

保持 CQ 两端数据一致性有两种方式:同步 & 异步。

同步

在Command端,除了维护自身的 写数据库 以外,还需要维护 读数据库,并且可能需要维护读数据库中的多张表。(感觉还不如一开始的CRUD方式)

异步

数据完整性的实时性

如何保证两个数据源之间的数据保持一致?

相关框架

Axon

x. CQRS 示例模拟

模拟CQRS的运行机制,加深理解

3.1 示例架构图

143642006ff034492d8aececfc50c5f7.png

或者,看这个

426e29ed196b0bf6fe4daacbf72a9f1e.png

3.2 代码

domain

作用:“数据库”的存储

@Data

@RequiredArgsConstructor

public class User {

@NonNull

private String userId;

@NonNull

private String firstName;

@NonNull

private String lastName;

private Set contacts = new HashSet<>();

private Set

addresses = new HashSet<>();

}

@Data

@AllArgsConstructor

@NoArgsConstructor

public class Address {

private String city;

private String state;

private String postcode;

}

@Data

@AllArgsConstructor

@NoArgsConstructor

public class Contact {

private String type;

private String detail;

}

@Data

public class UserAddress {

private Map> addressByRegion = new HashMap<>();

}

@Data

public class UserContact {

private Map> contactByType = new HashMap<>();

}

command

作用:UI发起的增、删、改请求命令的对象

@Data

@AllArgsConstructor

public class CreateUserCommand {

private String userId;

private String firstName;

private String lastName;

}

@Data

@AllArgsConstructor

public class UpdateUserCommand {

private String userId;

private Set

addresses;

private Set contacts;

}

Query

作用:UI发起的查询请求命令的对象

@Data

@AllArgsConstructor

public class AddressByRegionQuery {

private String userId;

private String state;

}

@Data

@AllArgsConstructor

public class ContactByTypeQuery {

private String userId;

private String contactType;

}

Repository

作用:操作数据库

public class UserReadRepository {

private Map userAddress = new HashMap<>();

private Map userContact = new HashMap<>();

public void addUserAddress(String id, UserAddress user) {

userAddress.put(id, user);

}

public UserAddress getUserAddress(String id) {

return userAddress.get(id);

}

public void addUserContact(String id, UserContact user) {

userContact.put(id, user);

}

public UserContact getUserContact(String id) {

return userContact.get(id);

}

}

public class UserWriteRepository {

private Map store = new HashMap<>();

public void addUser(String id, User user) {

store.put(id, user);

}

public User getUser(String id) {

return store.get(id);

}

}

Aggregate

作用:处理增、删、改的请求命令

public class UserAggregate {

private UserWriteRepository writeRepository;

public UserAggregate(UserWriteRepository repository) {

this.writeRepository = repository;

}

public User handleCreateUserCommand(CreateUserCommand command) {

User user = new User(command.getUserId(), command.getFirstName(), command.getLastName());

writeRepository.addUser(user.getUserId(), user);

return user;

}

public User handleUpdateUserCommand(UpdateUserCommand command) {

User user = writeRepository.getUser(command.getUserId());

user.setAddresses(command.getAddresses());

user.setContacts(command.getContacts());

writeRepository.addUser(user.getUserId(), user);

return user;

}

}

Projection

作用:处理查询的请求命令

public class UserProjection {

private UserReadRepository readRepository;

public UserProjection(UserReadRepository readRepository) {

this.readRepository = readRepository;

}

public Set handle(ContactByTypeQuery query) {

UserContact userContact = readRepository.getUserContact(query.getUserId());

return userContact.getContactByType()

.get(query.getContactType());

}

public Set

handle(AddressByRegionQuery query) {

UserAddress userAddress = readRepository.getUserAddress(query.getUserId());

return userAddress.getAddressByRegion()

.get(query.getState());

}

}

Projector

作用:同步 写数据库 至 读数据库

public class UserProjection {

private UserReadRepository readRepository;

public UserProjection(UserReadRepository readRepository) {

this.readRepository = readRepository;

}

public Set handle(ContactByTypeQuery query) {

UserContact userContact = readRepository.getUserContact(query.getUserId());

return userContact.getContactByType()

.get(query.getContactType());

}

public Set

handle(AddressByRegionQuery query) {

UserAddress userAddress = readRepository.getUserAddress(query.getUserId());

return userAddress.getAddressByRegion()

.get(query.getState());

}

}

测试

@SpringBootTest

class DemoApplicationTests {

private UserWriteRepository writeRepository = new UserWriteRepository();

private UserReadRepository readRepository = new UserReadRepository();

private UserProjector projector = new UserProjector(readRepository);

private UserAggregate userAggregate = new UserAggregate(writeRepository);

private UserProjection userProjection = new UserProjection(readRepository);

@Test

public void givenCQRSApplication_whenCommandRun_thenQueryShouldReturnResult() throws Exception {

// 1. 初始化userId

String userId = UUID.randomUUID().toString();

// 2. 模拟创建用户对应的命令

CreateUserCommand createUserCommand = new CreateUserCommand(userId, "Tom", "Sawyer");

// 3. 聚合器处理创建用户命令,往“写数据库”写入数据。对应的实体类是:User.java

User user = userAggregate.handleCreateUserCommand(createUserCommand);

// 4. 将实体类User.java,以另一种方式存入“读数据库”中。对应的实体类是:UserContact.java & UserAddress.java

projector.project(user);

// 5. 模拟修改用户对应的命令:添加用户的地址 & 联系方式

UpdateUserCommand updateUserCommand = new UpdateUserCommand(user.getUserId(),

Stream.of(new Address("New York", "NY", "10001"), new Address("Los Angeles", "CA", "90001")).collect(Collectors.toSet()),

Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("EMAIL", "tom.sawyer@rediff.com")).collect(Collectors.toSet()));

// 6. 聚合器处理修改用户命令,往“写数据库”修改 & 写数据。对应的实体类是:User.java

user = userAggregate.handleUpdateUserCommand(updateUserCommand);

// 7. 将实体类User.java,以另一种方式存入“读数据库”中。对应的实体类是:UserContact.java & UserAddress.java

projector.project(user);

// 8. 模拟修改用户对应的命令:添加用户的地址 & 联系方式

updateUserCommand = new UpdateUserCommand(userId,

Stream.of(new Address("New York", "NY", "10001"), new Address("Housten", "TX", "77001")).collect(Collectors.toSet()),

Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("PHONE", "700-000-0001")).collect(Collectors.toSet()));

// 9. 聚合器处理修改用户命令,往“写数据库”修改 & 写数据。对应的实体类是:User.java

user = userAggregate.handleUpdateUserCommand(updateUserCommand);

// 10. 将实体类User.java,以另一种方式存入“读数据库”中。对应的实体类是:UserContact.java & UserAddress.java

projector.project(user);

// 11. 发起查询命令

ContactByTypeQuery contactByTypeQuery = new ContactByTypeQuery(userId, "EMAIL");

assertEquals(Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com")).collect(Collectors.toSet()),

userProjection.handle(contactByTypeQuery));

AddressByRegionQuery addressByRegionQuery = new AddressByRegionQuery(userId, "NY");

assertEquals(Stream.of(new Address("New York", "NY", "10001")).collect(Collectors.toSet()),

userProjection.handle(addressByRegionQuery));

}

}

代码来源

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值