本文翻译自国外论坛 medium,原文地址:https://salithachathuranga94.medium.com/micro-service-patterns-circuit-breaker-with-spring-boot-253e4a829f94
微服务是目前业界使用的最重要的实现方面。通过使用微服务架构,开发人员可以消除他们以前在单体应用程序中遇到的许多问题。展望未来,人们开始在微服务中搜索和采用各种模式。大多数时候,新模式的产生是为了解决另一个模式中出现的常见问题。就这样,随着时间的推移,大量的模式进入了实践。您可以从这里获得完整的摘要: https://microservices.io/patterns/microservices.html
考虑到它们的范围,这些微服务模式进一步分为几类。在所有这些模式中,许多开发人员都使用了一些非常重要和流行的模式。断路器是其中之一,有助于以适当的方式管理下游服务故障。让我们了解这种模式的作用。💪
您可能已经听说过我们在电子产品中发现的断路器。它的主要目的是什么?简单地说,在意想不到的情况下切断电流。与此相同,这种微服务模式也因其具有相同的性质而得名。
这种模式在服务之间进行通信时出现。让我们来看一个简单的场景。假设我们有两个服务:服务 A 和 B。服务 A 正在调用服务 B(API 调用)以获取所需的一些信息。当服务 A 调用服务 B 时,如果服务 B 由于某些基础设施中断而关闭,会发生什么?服务 A 没有得到结果,它将因抛出异常而挂起。然后另一个请求来了,它也面临同样的情况。就像这个请求线程将被阻塞/挂起,直到服务 B 出现!结果,网络资源将被耗尽,性能低下,用户体验差。级联故障也可能因此发生。
在这种情况下,我们可以使用这种断路器模式来解决问题。它为我们提供了一种在不打扰最终用户或应用程序资源的情况下处理这种情况的方法。
基本上,它的行为与电路断路器相同。当应用程序的远程服务调用失败次数超过给定阈值时,断路器将在特定时间段内跳闸。在此超时到期后,断路器允许有限数量的请求通过它。如果这些请求成功,则断路器将关闭并恢复正常操作。否则,如果它们失败了,超时时间将重新开始,然后像以前一样做剩下的事情。
以下是一些常见概念讲解帮助大家来理解断路器。😎
断路器模式中讨论了 3 个主要状态。他们是:
让我们简要了解一下状态……
当正在交互的两个服务都启动并运行时,断路器默认关闭。断路器会持续统计远程 API 调用的次数。
一旦远程 API 调用失败百分比超过给定阈值,断路器就会将其状态更改为 OPEN 状态。调用微服务会立即失败,返回异常。也就是说,流量中断了。
在 OPEN 状态停留给定的超时时间后,断路器自动将其状态变为 HALF OPEN 状态。在这种状态下,只允许有限数量的远程 API 调用通过。如果失败调用计数大于此有限数量,则断路器再次变为 OPEN 状态,流量继续中断。否则关闭断路器,流量恢复正常。
为了实际演示该模式,我将使用 Spring Boot 框架来创建微服务。并用 Resilience4j 库实现断路器。
Resilience4j 是一个轻量级、易于使用的容错库,其灵感来自于 Netflix Hystrix。它提供各种功能如下:
在本文中,我们将基于 Spring Boot 项目来使用第一个功能。😎
我将使用名为 loan-service(贷款) 和 rate-service(利率) 的两个服务来实现一个简单的服务间通信场景。
带有 H2 内存中 DB、JPA、Hibernate、Actuator、Resilience4j 的 Spring Boot
贷款服务可以获取保存在数据库中的贷款,每个贷款对象都有贷款类型。根据贷款类型,有单独的利率百分比。因此,利率服务的名称包含那些利率对象的详细信息。
由于费率服务是独立的,我将首先实现费率服务的基本功能。
使用 POM 文件下方提供的依赖项创建一个新的 Spring Boot 项目。我将其命名为费率服务。https://github.com/SalithaUCSC/spring-boot-circuit-breaker/blob/main/rate-service/pom.xml
@RestController @RequestMapping("api") public class RateController { @Autowired private RateService rateService; @GetMapping(path = "/rates/{type}") public ResponseEntity<Rate> getRateByType(@PathVariable("type") String type) { return ResponseEntity.ok().body(rateService.getRateByType(type)); } }
@Service public class RateService { @Autowired private RateRepository repository; public Rate getRateByType(String type) { return repository.findByType(type).orElseThrow(() -> new RuntimeException("Rate Not Found: " + type)); } }
@Repository public interface RateRepository extends JpaRepository<Rate, Integer> { Optional<Rate> findByType(String type); }
@Builder @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Entity @Table(name = "rates") public class Rate { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Integer id; String type; @Column(name = "rate") Double rateValue; }
server: port: 9000 spring: application: name: rate-service datasource: url: jdbc:h2:mem:cb-rate-db username: root password: 123 driverClassName: org.h2.Driver jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop h2: console: enabled: true
@SpringBootApplication public class RateServiceApplication { @Autowired private RateRepository rateRepository; public static void main(String[] args) { SpringApplication.run(RateServiceApplication.class, args); } @PostConstruct public void setupData() { rateRepository.saveAll(Arrays.asList( Rate.builder().id(1).typ并检查我们需要的 APIe("PERSONAL").rateValue(10.0).build(), Rate.builder().id(2).type("HOUSING").rateValue(8.0).build() )); } }
现在我们可以启动 rate-service 并检查我们需要的 API。转到 http://localhost:9000/api/rates/PERSONAL 并查看结果。你应该得到这个回应。
{"id": 1,"type": "PERSONAL","rateValue": 10}
现在我需要实施贷款服务。 loan-service 内部需要断路器,因为它正在调用 rate-service。因此,需要 Resilience4j 库。我需要检查断路器的状态。为此,我需要在贷款服务中启用 Actuator。
使用 POM 文件下方提供的依赖项创建一个新的 Spring Boot 项目。我将其命名为贷款服务。
https://github.com/SalithaUCSC/spring-boot-circuit-breaker/blob/main/loan-service/pom.xml
让我们为贷款服务添加基本功能。
@RestController @RequestMapping("api") public class LoanController { @Autowired private LoanService loanService; @GetMapping(path = "/loans") public ResponseEntity<List<Loan>> getLoansByType(@RequestParam("type") String type) { return ResponseEntity.ok().body(loanService.getAllLoansByType(type.toUpperCase())); } }
public interface LoanRepository extends JpaRepository<Loan, Integer> { List<Loan> findByType(String type); }
@Data @AllArgsConstructor @NoArgsConstructor public class InterestRate { Integer id; String type; Double rateValue; }
@Builder @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Entity @Table(name = "loans") public class Loan { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Integer id; String type; Double amount; Double interest; }
@SpringBootApplication public class LoanServiceApplication { @Autowired private LoanRepository loanRepository; public static void main(String[] args) { SpringApplication.run(LoanServiceApplication.class, args); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } @PostConstruct public void setupData() { loanRepository.saveAll(Arrays.asList( Loan.builder().id(1).type("PERSONAL").amount(200000.0).interest(0.0).build(), Loan.builder().id(2).type("HOUSING").amount(6000000.0).interest(0.0).build(), Loan.builder().id(3).type("PERSONAL").amount(100000.0).interest(0.0).build() )); } }
这是我们执行远程调用的最重要的地方。我们需要在利率服务中使用 RestTemplate: http://localhost:9000/api/rates/{type} 调用此 API 以获取贷款类型的百分比。然后我们计算利息金额为贷款金额*(利率/100)并更新贷款利息金额。
@Service public class LoanService { @Autowired private LoanRepository loanRepository; @Autowired private RestTemplate restTemplate; private static final String SERVICE_NAME = "loan-service"; private static final String RATE_SERVICE_URL = "http://localhost:9000/api/rates/"; public List<Loan> getAllLoansByType(String type) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<InterestRate> entity = new HttpEntity<>(null, headers); ResponseEntity<InterestRate> response = restTemplate.exchange( (RATE_SERVICE_URL + type), HttpMethod.GET, entity, InterestRate.class ); InterestRate rate = response.getBody(); List<Loan> loanList = new ArrayList<>(); if (rate != null) { loanList = loanRepository.findByType(type); for (Loan loan : loanList) { loan.setInterest(loan.getAmount() * (rate.getRateValue() / 100)); } } return loanList; } }
server: port: 8000 spring: application: name: loan-service datasource: url: jdbc:h2:mem:cb-loan-db username: root password: 123 driverClassName: org.h2.Driver jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop h2: console: enabled: true management: endpoint: health: show-details: always endpoints: web: exposure: include: health health: circuitbreakers: enabled: true
我们需要在执行器暴漏断路器详细信息(暴漏端点)。稍后我们将在此处添加断路器配置。目前,不需要。
现在我们可以启动 rate-service 并检查我们需要的 API。转到 http://localhost:8000/api/loans?type=personal 并查看结果。你应该得到这个回应。
[ {"id": 1,"type": "PERSONAL","amount": 200000,"interest": 20000}, {"id": 3,"type": "PERSONAL","amount": 100000,"interest": 10000} ]
现在我们必须用注释来丰富我们的 Loan 服务方法。它被称为 “@CircuitBreaker”。在这里,SERVICE_NAME 被视为“贷款服务”。然后我们必须提供一个 fallbackMethod。这样做的目的是当下游服务(速率服务)无法响应时默认调用它。
@CircuitBreaker(name = SERVICE_NAME, fallbackMethod = "getDefaultLoans") public List<Loan> getAllLoansByType(String type) { ............... }
我已经设置了当费率服务没有响应时默认返回空列表的方法。
您可以设置此方法以显示错误消息,而不发送空消息。你可以返回这样的东西 — “Rate service is not responding.请求失败!”。发送空数组或一组默认数据不是理想的方式。因为这会给用户带来困惑。但是您必须确保这两种方法都返回相同类型的数据。在我的例子中:两种方法都返回列表!
public List<Loan> getDefaultLoans(Exception e) { return new ArrayList<>(); }
让我们添加 Resilience4j 断路器配置。将此添加到贷款服务中的 application.yml。
resilience4j: circuitbreaker: instances: loan-service: registerHealthIndicator: true failureRateThreshold: 50 minimumNumberOfCalls: 5 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s permittedNumberOfCallsInHalfOpenState: 3 slidingWindowSize: 10 slidingWindowType: COUNT_BASED
启动这两个服务。现在转到贷款服务,输入监控端点 URL:http://localhost:8000/actuator/health。断路器的详细信息会在响应中突出显示。
{ "status": "UP", "components": { "circuitBreakers": { "status": "UP", "details": { "loan-service": { "status": "UP", "details": { "failureRate": "-1.0%", "failureRateThreshold": "50.0%", "slowCallRate": "-1.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 1, "slowCalls": 0, "slowFailedCalls": 0, "failedCalls": 0, "notPermittedCalls": 0, "state": "CLOSED" } } } }, ...................................... } }
我们必须遵循一些有序的步骤才能准确地看到变化。在每一步中,我们都必须查看监控端点,并通过更改其状态查看断路器的行为方式。开始!💪
{ "loan-service": { "status": "UP", "details": { "failureRate": "-1.0%", "failureRateThreshold": "50.0%", "slowCallRate": "-1.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 2, "slowCalls": 0, "slowFailedCalls": 0, "failedCalls": 0, "notPermittedCalls": 0, "state": "CLOSED" } } }
{ "loan-service": { "status": "CIRCUIT_OPEN", "details": { "failureRate": "60.0%", "failureRateThreshold": "50.0%", "slowCallRate": "0.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 5, "slowCalls": 0, "slowFailedCalls": 0, "failedCalls": 3, "notPermittedCalls": 0, "state": "OPEN" } } }
{ "loan-service": { "status": "CIRCUIT_HALF_OPEN", "details": { "failureRate": "-1.0%", "failureRateThreshold": "50.0%", "slowCallRate": "-1.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 0, "slowCalls": 0, "slowFailedCalls": 0, "failedCalls": 0, "notPermittedCalls": 0, "state": "HALF_OPEN" } } }
由于 rate-service 仍然关闭,只需再次尝试 loan-service API 3次!http://localhost:8000/api/loans?type=personal 发生了什么事? 3次调用全部失败!那么 failureRate 就是 100%。我们的断路器将再次打开。
{ "loan-service": { "status": "CIRCUIT_OPEN", "details": { "failureRate": "100.0%", "failureRateThreshold": "50.0%", "slowCallRate": "0.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 3, "slowCalls": 0, "slowFailedCalls": 0, "failedCalls": 3, "notPermittedCalls": 0, "state": "OPEN" } } }
{ "loan-service": { "status": "UP", "details": { "failureRate": "-1.0%", "failureRateThreshold": "50.0%", "slowCallRate": "-1.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 0, "slowCalls": 0, "slowFailedCalls": 0, "failedCalls": 0, "notPermittedCalls": 0, "state": "CLOSED" } } }
自此,我们完成了断路器的测试工作。是不是很神奇?😎它正在按预期工作!
完整的源代码可以在这个 GitHub 存储库中找到:https://github.com/SalithaUCSC/spring-boot-circuit-breaker
最后感谢大家阅读,希望这篇文章能为你提供价值。公众号【waynblog】分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力😘。