目录
当你的系统像早晚高峰的北京三环一样拥堵时,是时候拆掉读写混合的"单行道",打造CQRS的"双向十车道"了!
一、从"读写混用"到"职责分离"的进化史
传统CRUD的痛点现场:
// 典型混合式Service
public class UserService {
public void updateUser(User user) { /* 写操作 */ }
public User getUser(Long id) { /* 读操作 */ }
}
CQRS三原色:
- 🚧 命令(Command):改变系统状态的操作(如
CreateUserCommand
) - 🔍 查询(Query):获取系统状态的操作(如
FindUserQuery
) - 📊 双模型:读写采用不同数据模型和存储结构
二、基础实现:手写CQRS架构
2.1 命令侧实现
// 命令对象
public record CreateUserCommand(String username, String email) {}
// 命令处理器
public class UserCommandHandler {
public void handle(CreateUserCommand command) {
User user = new User(command.username(), command.email());
// 写入主数据库
jdbcTemplate.update("INSERT INTO users (...) VALUES (?,?)",
user.getUsername(), user.getEmail());
// 发布领域事件
eventPublisher.publish(new UserCreatedEvent(user.getId()));
}
}
2.2 查询侧实现
// 查询专用DTO
public record UserView(Long id, String username, String displayName) {}
// 查询处理器
public class UserQueryHandler {
public UserView handle(FindUserQuery query) {
// 从读库查询
return jdbcTemplate.queryForObject(
"SELECT id, username, display_name FROM user_view WHERE id = ?",
(rs, rowNum) -> new UserView(
rs.getLong("id"),
rs.getString("username"),
rs.getString("display_name")
),
query.userId()
);
}
}
三、高阶玩法:事件溯源(Event Sourcing)
3.1 事件存储实现
// 领域事件基类
public abstract class DomainEvent {
private final UUID eventId = UUID.randomUUID();
private final Instant occurredAt = Instant.now();
}
public class UserCreatedEvent extends DomainEvent {
private final Long userId;
private final String username;
// 构造函数、getters省略
}
// 事件存储仓库
public class EventStore {
private final List<DomainEvent> events = new CopyOnWriteArrayList<>();
public void save(DomainEvent event) {
events.add(event);
}
public List<DomainEvent> getEventsForAggregate(Long aggregateId) {
return events.stream()
.filter(e -> e.getAggregateId().equals(aggregateId))
.collect(Collectors.toList());
}
}
3.2 投影处理器
public class UserProjector {
private final JdbcTemplate jdbcTemplate;
@EventListener
public void handle(UserCreatedEvent event) {
// 更新查询专用视图
jdbcTemplate.update("""
INSERT INTO user_view (id, username, display_name)
VALUES (?, ?, ?)
""",
event.getUserId(),
event.getUsername(),
event.getUsername() // 初始显示名
);
}
}
四、Spring实现:用框架加速
4.1 命令总线配置
@Configuration
@EnableCqrs
public class CqrsConfig {
@Bean
public CommandBus commandBus() {
return new SimpleCommandBus();
}
@Bean
public QueryGateway queryGateway() {
return new SimpleQueryGateway();
}
}
// 命令处理注解
@Component
public class UserCommandComponent {
@CommandHandler
public void handle(CreateUserCommand command) {
// 处理命令逻辑
}
}
4.2 查询网关调用
@RestController
public class UserController {
private final QueryGateway queryGateway;
@GetMapping("/users/{id}")
public UserView getUser(@PathVariable Long id) {
return queryGateway.query(
new FindUserQuery(id),
UserView.class
).join();
}
}
五、性能对决:CQRS vs 传统模式
电商系统对比测试:
场景 | 传统模式 (QPS) | CQRS模式 (QPS) |
---|---|---|
用户注册 | 1200 | 950 |
商品详情页查询 | 1800 | 5500 |
订单状态更新 | 800 | 1200 |
用户订单历史查询 | 300 | 4200 |
架构师笔记:读多写少的系统(如商品详情、订单查询)收益最大,写操作由于要维护双模型略有损耗
六、CQRS最佳拍档
6.1 读写库同步方案
6.2 缓存策略
public class CachedUserQueryHandler {
private final Cache<Long, UserView> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
public UserView handle(FindUserQuery query) {
try {
return cache.get(query.userId(), () -> {
// 缓存未命中时查询数据库
return database.query(...);
});
} catch (ExecutionException e) {
throw new RuntimeException("Cache load failed", e);
}
}
}
七、避坑指南:CQRS五大陷阱
- 双写不一致黑洞
// 错误示例:未使用事务
@Transactional
public void createUser(CreateUserCommand command) {
// 写入主库
userRepository.save(user);
// 更新读库
userViewRepository.update(new UserView(...));
}
- 事件风暴