CQRS 实战:使用 Spring Boot 构建强大的微服务

CQRS 实战:使用 Spring Boot 构建强大的微服务

在本文中,我们将清楚地看到 CQRS 设计模式如何揭开我们在构建传统微服务时遇到的一些挑战。本文将带您了解从理论到概念的实际实施的不同方面。这篇文章可能很小,很长,但花点时间阅读这篇精彩的文章,就能一口气理解 CQRS。

img

由作者 MK Pavan Kumar 创作

介绍:

CQRS代表Command Query Responsibility Segregation,是一种设计模式,它从根本上改变了我们对软件应用程序中数据模型和数据库交互的架构的看法。传统上,系统使用相同的数据模型来读取和写入数据。然而,CQRS打破了这一常规,将数据模型分为读取和写入两个部分。这种分离具有许多优势,尤其是在微服务等复杂系统环境中。

在微服务架构中,不同的服务被设计为执行特定功能并独立运行,CQRS可以发挥关键作用。通过划分读取和写入操作,CQRS允许每个微服务以最有效的方式与数据库交互。例如,可以针对查询性能优化读取模型,利用非规范化和其他技术快速高效地获取数据。另一方面,写入模型可以专注于事务完整性和业务规则执行,这对于维护数据一致性和可靠性至关重要。

典型的微服务架构:

img

由作者 MK Pavan Kumar 创作

上图所示的架构代表了微服务应用程序的典型分层结构,专门用于处理 HTTP 请求并与数据库交互。此模型在许多 Web 框架(如 Spring Boot)中很常见。以下是各层的细分:

  1. **Web 客户端:**这是应用程序面向用户的部分。它代表任何可以向服务器发送 HTTP 请求的客户端。这可能是 Web 浏览器、移动应用程序或其他服务。客户端可以从服务器请求数据或发送要处理的数据。
  2. **控制器:**控制器充当 Web 客户端和服务层之间的中间人。在 Spring Boot 应用程序中,这通常是一个 REST 控制器,它处理各种 HTTP 请求,例如 GET、POST、PUT 和 DELETE。控制器解释客户端的请求,调用必要的服务层操作,并返回适当的响应。
  3. 服务:此层包含应用程序的业务逻辑。它负责处理数据、执行业务规则和执行计算。服务层在控制器层和存储库层之间运行,确保业务逻辑与 Web 层和数据访问层分开。
  4. 存储库:在 Spring Boot 中,存储库层通常是抽象数据层的接口。它提供了一个类似集合的接口,用于访问域对象(例如数据库中的实体),通常使用 Spring Data。存储库负责持久化和从数据库检索数据,向其他层隐藏数据访问代码的细节。
  5. 数据库:这是保存数据的存储系统。它可以是任何类型的数据库,例如关系数据库(例如 MySQL、PostgreSQL)或 NoSQL 数据库(例如 MongoDB、Cassandra)。存储库层与数据库通信以执行 CRUD(创建、读取、更新、删除)操作。

现在,很明显,无论操作负载如何,写入和读取都包含在同一个微服务中,如果它们很难在单个微服务中编码,那么就很难分别处理操作强度(读取与写入)。这是将 CQRS 变为现实的真正触发因素。

让我们开始使用 Spring Boot 微服务实现 CQRS。本文中我们假设银行场景,其中我们有存款(写入服务/命令服务)和查看账户(读取服务/查询服务),我们还将为此目的将 mongodb 视为持久存储,并将 redis pub/sub 视为事件处理程序。

实施:

让我们看一下命令和查询 API 的项目脚手架,它们如下所示。

img

现在让我们深入了解我们的第一个 API,即命令 API。让我们看看我们的pom.xml依赖管理。

<?xml version= "1.0" encoding= "UTF-8" ?> 
< project  xmlns = "http://maven.apache.org/POM/4.0.0"  xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > 
    < modelVersion > 4.0.0 </ modelVersion > 
    < parent > 
        < groupId > org.springframework.boot </ groupId > 
        < artifactId > spring-boot-starter-parent </ artifactId > 
        < version > 3.2.2 </ version > 
        < relationPath />  <!-- 从存储库查找父级 --> 
    </ parent > 
    < groupId > org.vaslabs </ groupId > 
    < artifactId > deposit </artifactId> <version> 0.0.1 - SNAPSHOT </version> <name>存款</name> <description>存款
    </description> <properties> < java.version > 21 < /java.version > </properties> <dependency> <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring - boot - starter - data - mongodb </artifactId> </dependency> <dependency> <groupId> org.springframework .启动< / groupId > <artifactId> spring - boot - starter - web </artifactId> </dependency> <dependency> <groupId> org.springframework.boot < / groupId
    
    
    
        
    
    
        
            
            
        
        
            
            
        

        
            > 
            <artifactId> spring - boot - devtools </artifactId> <scope> runtime </scope> <optional> true </optional> </dependency> <dependency> <groupId> org.projectlombok </groupId> <artifactId> lombok </artifactId> <optional> true </optional> </dependency> <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring - boot - starter - data - redis </artifactId> </dependency> </dependencies> <build> <plugins> 
            <plugin> <groupId> org.springframework.boot </groupId> <artifactId> spring - boot - maven - plugin </artifactId> <configuration> <excludes> <exclude> <groupId> org.projectlombok </groupId> <artifactId> lombok </artifactId> </exclude> </excludes> </configuration> </plugin></插件> </构建> </项目>
            
        
        
            
            
            
        
        
            
            
        
    

    
        
            
                
                
                
                    
                        
                            
                            
                        
                    
                
            
        
    

现在让我们看看application.yaml哪些管理我们的财产。

服务器:
  端口: 9090 
spring:
  数据:
    mongodb:
      数据库: account_deposits
      主机: localhost
      端口: 27017
日志记录:
  级别:
    org:
      springframework:
        数据:
          mongodb:
            核心:
              MongoTemplate: DEBUG 
redis:
  pubsub:
    主题: deposit_event

现在我们需要创建实体来将数据保存到数据库中。

包org.vaslabs.deposit.entity;

导入lombok.Data;
导入lombok.Getter;
导入lombok.Setter;
导入org.springframework.data.annotation.Id;
导入org.springframework.data.mongodb.core.mapping.Document;

@Data 
@Document(collection = "accounts") 
@Getter 
@Setter
公共 类 Deposit { 
    @Id
    私有字符串 id;
    私有字符串 accountNumber;
    私有字符串 firstName;
    私有字符串 lastName;
    私有 双精度金额;
}

现在是时候创建writemodel可以作为我们的命令对象的了。

包org.vaslabs.deposit.writemodel ;导入lombok.Data ;导入lombok.Getter ;导入lombok.Setter ; @Data @Getter @Setter public class DepositCommand { private String accountNumber ; private String firstName ; private String lastName ; private double amount ; }








  
     
     
     
    

让我们从事件处理的角度看一下配置,服务将能够将事件传播到 redis pub/sub。

包org.vaslabs.deposit.config;

导入 o rg.springframework.beans.factory.annotation.Value ;导入o rg.springframework.context.annotation.Bean ;导入 o rg.springframework.context.annotation.Configuration ;导入 o rg.springframework.data.redis.connection.RedisConnectionFactory ;导入o rg.springframework.data.redis.core.RedisTemplate ;导入o rg.springframework.data.redis.listener.ChannelTopic ; 导入 org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer ; @Configuration public class EventHandlerConfig { @Value ( " $ { redis.pubsub.topic } " ) private String topic ; @Bean公共RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){         RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();         redisTemplate.setConnectionFactory(redisConnectionFactory);         redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());返回redisTemplate;     } @Bean公共ChannelTopic channelTopic(){返回新 ChannelTopic(topic);     } }








  

    
    

    
    



        


    
    
        

让我们创建一个存储库来处理包装为存储库的数据库逻辑。

包org.vaslabs.deposit.respositories;

导入org.springframework.data.mongodb.repository.MongoRepository ;导入org.vaslabs.deposit.entity.Deposit;公共接口DepositRepository扩展MongoRepository < Deposit ,String > { }


    

让我们创建一个控制器来处理来自客户端的请求。

包org.vaslabs.deposit.controller;

导入org.springframework.http.MediaType;
导入 o rg.springframework.http.ResponseEntity;
导入org.springframework.web.bind.注释.PostMapping;
导入org.springframework.web.bind.注释.RequestBody;
导入org.springframework.web.bind.注释.RestController;
导入org.vaslabs.deposit.command.handler.DepositCommandHandler;
导入o rg.vaslabs.deposit.entity.Deposit;
导入o rg.vaslabs.deposit.writemodel.DepositCommand; 

@RestController
公共 类 DepositController { 

    private  final DepositCommandHandler depositCommandHandler; 

    public DepositController(DepositCommandHandler depositCommandHandler) { 
        this .depositCommandHandler = depositCommandHandler; 
    } 

    @PostMapping(value = “/deposit”,consumes = MediaType.APPLICATION_JSON_VALUE)
    公共ResponseEntity <Deposit> saveDeposit(@RequestBody DepositCommand depositCommand){
        返回ResponseEntity.ok()。body(此.depositCommandHandler.handle(depositCommand)); 
    } 

}

现在是时候编写处理程序来处理传入的命令了。

包org.vaslabs.deposit.command.handler;

导入 o rg.slf4j.Logger;
导入org.slf4j.LoggerFactory;
导入org.springframework.data.redis.core.RedisTemplate;导入org.springframework.data.redis.listener.ChannelTopic;
导入org.springframework.stereotype.Service;导入org.vaslabs.deposit.entity.Deposit;导入org.vaslabs.deposit.respositories.DepositRepository;导入 o rg.vaslabs.deposit.writemodel.DepositCommand; @Service公共类DepositCommandHandler {私有静态最终Logger LOGGER = LoggerFactory.getLogger(DepositCommandHandler.class);私有最终DepositRepository depositRepository;私有最终RedisTemplate<String, Object> redisTemplate;私有最终ChannelTopic channelTopic;公共DepositCommandHandler (DepositRepository depositRepository,                                 RedisTemplate <String,Object> redisTemplate,                                 ChannelTopic channelTopic) { this .depositRepository = depositRepository; this .redisTemplate = redisTemplate; this .channelTopic = channelTopic;     }公共Deposit句柄(DepositCommand depositCommand) { Deposit deposit = mapCommandToEntity(depositCommand);         deposit = this .depositRepository.save(deposit);         publishEvent(deposit);返回deposit;     }私有void publishEvent (Deposit deposit) {         LOGGER.info( "将事件:{} 发布到频道:{}" , deposit, channelTopic.getTopic()); Long id = redisTemplate.convertAndSend(channelTopic.getTopic(), deposit); if (id != null ) {             LOGGER.info( "事件发布到频道:{}" , channelTopic.getTopic());         }     } private Deposit mapCommandToEntity (DepositCommand depositCommand) { Deposit deposit = new Deposit ();






  

         
     
     
     

     


        
        
        


    
          


        


      

          
        




    
            
        存款.setAccountNumber(depositCommand.getAccountNumber());
        存款.setFirstName(depositCommand.getFirstName());
        存款.setLastName(depositCommand.getLastName());
        存款.setAmount(depositCommand.getAmount());
        返回存款;
    } 
}

现在,一旦您运行代码,并且一切顺利并且引导没有任何错误,则下面应该是输出。

img

命令处理程序 Api 的输出和事件传播到事件处理程序。

现在让我们深入研究第二个 API,即查询 API。让我们看看我们的pom.xml依赖管理。

<?xml version= "1.0" encoding= "UTF-8" ?> 
< project  xmlns = "http://maven.apache.org/POM/4.0.0"  xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > 
    < modelVersion > 4.0.0 </ modelVersion > 
    < parent > 
        < groupId > org.springframework.boot </ groupId > 
        < artifactId > spring-boot-starter-parent </ artifactId > 
        < version > 3.2.2 </ version > 
        < relationPath />  <!-- 从存储库查找父级 --> 
    </ parent > 
    < groupId > org.vaslabs </ groupId > 
    < artifactId > view_account </artifactId> <version> 0.0.1 - SNAPSHOT </version> <name> view_account </name> <description> view_account </description> <properties> < java.version > 21 < /java.version > </properties> <dependency> <dependency> <groupId> org.springframework.boot
     </groupId> <artifactId> spring - boot - starter - data - mongodb </artifactId> </dependency> <dependency> <groupId> org.springframework .启动</ groupId > < artifactId > spring-boot-starter-web </ artifactId > </ dependency > < dependency > < groupId > org.springframework.boot </
    
    
    
        
    
    
        
            
            
        
        
            
            
        

        
            groupId > 
            <artifactId> spring - boot - devtools </artifactId> <scope>运行时</scope> <optional> true </optional> </dependency> <dependency> <groupId> org.projectlombok </groupId> <artifactId> lombok </artifactId> <optional> true </optional> </dependency> <dependency>
             <groupId> org.springframework.boot </groupId> <artifactId> spring - boot - starter - data - redis </artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId> org.springframework.boot </groupId> <artifactId> spring - boot - maven - plugin </artifactId> <configuration> <excludes> <exclude> <groupId> org.projectlombok </groupId> <artifactId> lombok </artifactId> </exclude> </excludes> </configuration></插件> </插件> </构建> </项目>
            
        
        
            
            
            
        
        
            
            
        
    

    
        
            
                
                
                
                    
                        
                            
                            
                        
                    
                
            
        
    

现在我们来看看application.yaml不同的物业管理。

服务器:
  端口: 9091 
spring:
  数据:
    mongodb:
      数据库: account_views
      主机: localhost
      端口: 27017
日志记录:
  级别:
    org:
      springframework:
        数据:
          mongodb:
            核心:
              MongoTemplate: DEBUG 
redis:
  pubsub:
    主题: deposit_event

现在读取的模型和实体与实体保持相同,writemodel但没有规定它们应该相同,事实上,这是 CQRS 模式的优势,它们可以不同。让我们看看我们的Event configuration

包org.vaslabs.view_account.config;

导入 o rg.springframework.beans.factory.annotation.Autowired ;导入 o rg.springframework.beans.factory.annotation.Value ;导入 o rg.springframework.context.annotation.Bean ;导入org.springframework.context.annotation.Configuration ;导入o rg.springframework.data.redis.connection.RedisConnectionFactory ;导入o rg.springframework.data.redis.listener.ChannelTopic ;导入o rg.springframework.data.redis.listener.RedisMessageListenerContainer ;导入o rg.springframework.data.redis.listener.adapter.MessageListenerAdapter ;导入o rg.vaslabs.view_account.event.handler.RedisEventListener ; @Configuration public class EventHandlerConfig { @Value ( " ${redis.pubsub.topic} " ) private String topic ;私有最终RedisEventListener redisEventListener;公共EventHandlerConfig(RedisEventListener redisEventListener) { this .redisEventListener = redisEventListener;     } @Bean公共ChannelTopic channelTopic() {返回新的 ChannelTopic(topic);     } @Bean公共MessageListenerAdapter messageListenerAdapter() {返回新的 MessageListenerAdapter(redisEventListener,“listen” );     } @Bean公共RedisMessageListenerContainer redisMessageListenerContainer(ChannelTopic channelTopic,                                                                       RedisConnectionFactory redisConnectionFactory) {         RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();         redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);         redisMessageListenerContainer.addMessageListener(messageListenerAdapter(), channelTopic);返回redisMessageListenerContainer;     } }










  


    
    

     

    
        


    
    
        


    
    
        


    
    




        

让我们看一下EventListener上述配置。此事件侦听器的功能是一旦从 redis pub/sub 接收到事件,就将其持久保存在读取数据库中。

包org.vaslabs.view_account.event.handler;

导入com.fasterxml.jackson.core.JsonProcessingException;
导入com.fasterxml.jackson.databind.ObjectMapper;
导入org.slf4j.Logger;
导入org.slf4j.LoggerFactory;
导入org.springframework.beans.factory.annotation.Autowired;
导入org.springframework.stereotype.Service;导入org.vaslabs.view_account.entity.Deposit;
导入org.vaslabs.view_account.respositories.DepositRepository;导入java.io.IOException; @Service公共类RedisEventListener { private static final Logger LOGGER = LoggerFactory.getLogger(RedisEventListener.class); private ObjectMapper objectMapper = new ObjectMapper (); private final DepositRepository depositRepository; public RedisEventListener (DepositRepository depositRepository) { this .depositRepository = depositRepository;     } public void listen (String message) throws JsonProcessingException { try {             LOGGER.info( "收到新事件:{}" , message); Deposit depositEvent = objectMapper.readValue(message, Deposit.class);             LOGGER.info( "已解析事件:{}" , depositEvent); this .depositRepository.save(depositEvent);         } catch (IOException e) {             LOGGER.error( "解析消息时出错" );         }     } }




  
         
         

     

     
        


       
        

              

            

让我们看一下哪些ViewAccountRepository将与实际数据库交互以实现不同的查询。

包org.vaslabs.view_account.respositories;

导入o rg.springframework.data.mongodb.repository.MongoRepository ;
导入org.springframework.data.mongodb.repository.Query ;导入 o rg.vaslabs.view_account.entity.Deposit;公共接口ViewAccountRepository扩展MongoRepository < Deposit,String > { @Query(“{'accountNumber':?0}”)     Deposit findByAccountNumber(String accountNumber); }


    
    

让我们看看哪个ViewAccountController将处理传入的请求。

包org.vaslabs.view_account.controller;

导入 o rg.springframework.http.ResponseEntity;
导入 o rg.springframework.web.bind.注释.GetMapping;
导入 o rg.springframework.web.bind.注释.PathVariable;
导入org.springframework.web.bind .注释.RestController;
导入org.vaslabs.view_account.entity.Deposit;
导入 o rg.vaslabs.view_account.query.handler.DepositQueryHandler;

导入java.util.List; 

@RestController
公共 类 ViewAccountController {

    私有 最终DepositQueryHandler depositQueryHandler;

    公共ViewAccountController(DepositQueryHandler depositQueryHandler) { 
        this .depositQueryHandler = depositQueryHandler; 
    } 

    @GetMapping(value = "/deposit/{accountNumber}" ) 
    public ResponseEntity<Deposit> findbyAccountNumber( @PathVariable( "accountNumber" ) String accountNumber){ 
        return ResponseEntity.ok().body( this .depositQueryHandler.handleByAccountNumber(accountNumber)); 
    } 

    @GetMapping(value = "/deposit" ) 
    public ResponseEntity<List<Deposit>> findAll(){ 
        return ResponseEntity.ok().body( this .depositQueryHandler.handleAll()); 
    } 

}

现在我们来看看哪个ViewAccountQueryHandler将处理来自用户的查询/项目。

包org.vaslabs.view_account.query.handler;

导入org.slf4j.Logger;
导入org.slf4j.LoggerFactory;
导入 o rg.springframework.stereotype.Service;
导入 o rg.vaslabs.view_account.entity.Deposit;
导入org.vaslabs.view_account.respositories.ViewAccountRepository;

导入java.util.List; 


@Service
公共 类 ViewAccountQueryHandler {

    私有 静态 最终 Logger  LOGGER  = LoggerFactory.getLogger(ViewAccountQueryHandler.class);

    私有 最终ViewAccountRepository viewAccountRepository;

    公共 ViewAccountQueryHandler (ViewAccountRepository viewAccountRepository) { 
        this .viewAccountRepository = viewAccountRepository; 
    } 

    public Deposit handleByAccountNumber (String accountNumber) { 
        LOGGER.info( "查询数据库之前,accountNumber:{}" , accountNumber); 
        return  this .viewAccountRepository.findByAccountNumber(accountNumber); 
    } 

    public List<Deposit> handleAll () { 
        return  this .viewAccountRepository.findAll(); 
    } 
}

就是这样,如果一切顺利,您应该会看到读取数据库中的数据,因为它正在异步地被事件填充。观察下面的输出,它清楚地表明我们仅通过提供一些 readall 查询就获取了 API,并且它还接收了事件并成功解析并将其保存在读取存储中。

img

qery api 的输出显示事件接收和查询处理。

使用 CQRS 的优点和缺点:

命令查询职责分离 (CQRS) 模式也不例外,它有自己的优点和缺点。它是软件设计中处理数据操作的独特方法,具有多种优点。首先,它通过分离读写操作来提高性能,允许每个操作针对其特定用例进行优化。在具有多种读写模式的复杂系统中,这可以显著提高性能。对于读取,数据可以以视图模型的形式进行非规范化,这些视图模型针对用户界面进行了优化,从而减少了如果使用单个数据模型进行读取和写入则可能需要的昂贵连接的数量。对于写入,系统可以专注于数据的事务完整性和一致性。

尽管 CQRS 有诸多优点,但也有缺点。引入 CQRS 后,系统的复杂性会显著增加。开发人员必须管理和同步两个独立的模型,这会使代码库和架构变得复杂。它需要对领域有透彻的了解,并且对于简单的 CRUD 应用程序来说通常是过度的,因为开销无法弥补其好处。

CQRS 的另一个挑战是数据一致性。由于读写数据的模型是分开的,因此存在读取过时的风险——读取模型尚未更新以反映最新的写入。在需要高一致性的系统中尤其如此。开发人员需要仔细设计系统以处理最终一致性,并确保应用程序可以容忍一定程度的数据过时。

结论:

存中…(img-V4QPM7jW-1722244040072)]

qery api 的输出显示事件接收和查询处理。

使用 CQRS 的优点和缺点:

命令查询职责分离 (CQRS) 模式也不例外,它有自己的优点和缺点。它是软件设计中处理数据操作的独特方法,具有多种优点。首先,它通过分离读写操作来提高性能,允许每个操作针对其特定用例进行优化。在具有多种读写模式的复杂系统中,这可以显著提高性能。对于读取,数据可以以视图模型的形式进行非规范化,这些视图模型针对用户界面进行了优化,从而减少了如果使用单个数据模型进行读取和写入则可能需要的昂贵连接的数量。对于写入,系统可以专注于数据的事务完整性和一致性。

尽管 CQRS 有诸多优点,但也有缺点。引入 CQRS 后,系统的复杂性会显著增加。开发人员必须管理和同步两个独立的模型,这会使代码库和架构变得复杂。它需要对领域有透彻的了解,并且对于简单的 CRUD 应用程序来说通常是过度的,因为开销无法弥补其好处。

CQRS 的另一个挑战是数据一致性。由于读写数据的模型是分开的,因此存在读取过时的风险——读取模型尚未更新以反映最新的写入。在需要高一致性的系统中尤其如此。开发人员需要仔细设计系统以处理最终一致性,并确保应用程序可以容忍一定程度的数据过时。

结论:

命令查询职责分离 (CQRS) 模式在优化和扩展复杂软件系统方面具有明显优势,尤其是在微服务架构中。它通过分离读取和写入操作来实现有针对性的性能改进。但是,采用它会带来额外的复杂性和数据一致性方面的挑战。当业务逻辑和数据模型的复杂性证明开销合理时,应考虑使用 CQRS,而不应将其作为简单应用程序的默认方法。明智地实施 CQRS 可以产生强大且可维护的系统,这些系统能够很好地适应不断变化的需求。
博客原文:专业人工智能社区

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值