37.CQRS模式:给你的系统装上“读写分离引擎“

当你的系统像早晚高峰的北京三环一样拥堵时,是时候拆掉读写混合的"单行道",打造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()
        );
    }
}
CreateUserCommand
+String username
+String email
UserCommandHandler
+handle(CreateUserCommand)
UserQueryHandler
+handle(FindUserQuery) : UserView
UserView
+Long id
+String username
+String displayName
FindUserQuery

三、高阶玩法:事件溯源(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() // 初始显示名
        );
    }
}
命令处理器 事件存储 投影处理器 查询模型 保存UserCreatedEvent 发布事件 更新用户视图表 更新完成 命令处理器 事件存储 投影处理器 查询模型

四、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)
用户注册1200950
商品详情页查询18005500
订单状态更新8001200
用户订单历史查询3004200

架构师笔记:读多写少的系统(如商品详情、订单查询)收益最大,写操作由于要维护双模型略有损耗

六、CQRS最佳拍档

6.1 读写库同步方案

CDC日志
物化视图
聚合查询
主库
消息队列
读库消费
读库
Elasticsearch
Redis缓存

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五大陷阱

  1. 双写不一致黑洞
// 错误示例:未使用事务
@Transactional
public void createUser(CreateUserCommand command) {
    // 写入主库
    userRepository.save(user);
    
    // 更新读库
    userViewRepository.update(new UserView(...));
}
  1. 事件风暴
命令处理
事件1
投影处理1
事件2
投影处理2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhysunny

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值