Java >> Java tutorial >  >> Tag >> Spring

Sådan bruges afbryderen i Spring Boot Application

I dette indlæg vil jeg vise, hvordan vi kan bruge Circuit Breaker-mønsteret i en Spring Boot Application. Når jeg siger Circuit Breaker mønster, er det et arkitektonisk mønster. Netflix havde udgivet et bibliotek Hysterix til håndtering af afbrydere. Som en del af dette indlæg vil jeg vise, hvordan vi kan bruge et strømafbrydermønster ved hjælp af resilence4j  bibliotek i en Spring Boot Application.

I andre nyheder har jeg for nylig udgivet min bog Simplifying Spring Security. Hvis du er interesseret i at lære om Spring Security, kan du købe det her.

Billede fra Pixabay – Af Jürgen Diermaier

Hvad er Circuit Breaker?

Konceptet Circuit Breaker kommer fra Electrical Engineering. I de fleste elnet er strømafbrydere afbrydere, der beskytter netværket mod skader forårsaget af overbelastning af strøm eller kortslutninger.

På samme måde stopper en strømafbryder i software opkaldet til en fjerntjeneste, hvis vi ved, at opkaldet til den fjerntjeneste enten vil mislykkes eller timeout. Fordelen ved dette er at spare ressourcer og være proaktiv i vores fejlfinding af fjernprocedurekaldene.

Afbryderen træffer beslutningen om at stoppe opkaldet baseret på den tidligere historik for opkaldene. Men der er alternative måder, hvordan den kan håndtere opkaldene. Normalt vil den holde styr på tidligere opkald. Antag, at 4 ud af 5 opkald er mislykket eller timeout, så vil det næste opkald mislykkes. Dette hjælper med at være mere proaktiv i håndteringen af ​​fejlene med den opkaldende tjeneste, og den opkaldstjeneste kan håndtere svaret på en anden måde, hvilket giver brugerne mulighed for at opleve applikationen anderledes end en fejlside.

En anden måde en afbryder kan handle på er, hvis opkald til fjernservice mislykkes i en bestemt tidsperiode. En afbryder åbnes og tillader ikke det næste opkald, før fjernbetjeningen forbedres ved fejl.

Resilience4J Library

Vi har vores kode, som vi kalder fjernservice. Afbrydermodulet fra resilience4j biblioteket vil have et lambda-udtryk for et opkald til fjerntjeneste ELLER en supplier for at hente værdier fra fjernservicekaldet. Jeg vil vise dette som en del af eksemplet. Strømafbryderen dekorerer dette fjernservicekald på en sådan måde, at det kan holde styr på svar og skiftetilstande.

Forskellige konfigurationer af Resilience4j Library

For at forstå kredsløbsafbryderkonceptet vil vi se på forskellige konfigurationer, som dette bibliotek tilbyder.

slidingWindowType() – Denne konfiguration hjælper grundlæggende med at træffe en beslutning om, hvordan afbryderen skal fungere. Der er to typer COUNT_BASED og TIME_BASED . COUNT_BASED strømafbryderens skydevindue vil tage højde for antallet af opkald til fjernservice, mens TIME_BASED strømafbryderens skydevindue vil tage hensyn til opkald til fjernservice i en bestemt tidsperiode.

failureRateThreshold() – Dette konfigurerer fejlfrekvenstærsklen i procent. Hvis x procentdel af opkald mislykkes, åbnes afbryderen.

slidingWindowSize() – Denne indstilling hjælper med at bestemme antallet af opkald, der skal tages i betragtning, når en strømafbryder lukkes.

slowCallRateThreshold() – Dette konfigurerer tærsklen for langsom opkaldshastighed i procent. Hvis x procentdel af opkald er langsomme, åbnes afbryderen.

slowCallDurationThreshold – Tidsvarighedstærskel for, hvilke opkald der betragtes som langsomme.

minimumNumberOfCalls() – Et minimum antal opkald påkrævet, før hvilket afbryder kan beregne fejlprocenten.

ignoreException() – Denne indstilling giver dig mulighed for at konfigurere en undtagelse, som en kredsløbsafbryder kan ignorere og ikke tæller med i succes eller fiasko for et opkald fra fjernservice.

waitDurationInOpenState() – Varighed, hvori afbryderen skal forblive i åben tilstand, før den går over i halvåben tilstand. Standardværdien er 60 sekunder.

Tællebaseret strømafbryder

Mens du bruger resilience4j bibliotek, kan man altid bruge de standardkonfigurationer, som afbryderen tilbyder. Standardkonfigurationer er baseret på den ANTALBASEREDE glidende vinduestype.

Så hvordan opretter vi en strømafbryder til den ANTALBASEREDE skydevindue?


      CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(10)
                .slowCallRateThreshold(65.0f)
                .slowCallDurationThreshold(Duration.ofSeconds(3))
                .build();

        CircuitBreakerRegistry circuitBreakerRegistry =
                CircuitBreakerRegistry.of(circuitBreakerConfig);

        CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("BooksSearchServiceBasedOnCount");

I ovenstående eksempel opretter vi en afbryderkonfiguration, der inkluderer et glidende vindue af typen COUNT_BASED . Denne afbryder vil registrere resultatet af 10 opkald for at skifte afbryderen til closed stat. Hvis 65 procent af opkaldene er langsomme og langsomme af en varighed på mere end 3 sekunder, åbnes afbryderen.

CircuitBreakerRegistry er en fabrik til at skabe en afbryder.

Tidsbaseret afbryder

Nu på Time-Based afbryder.


       CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
                .minimumNumberOfCalls(3)
                .slidingWindowSize(10)
                .failureRateThreshold(70.0f)
                .build();

        CircuitBreakerRegistry circuitBreakerRegistry =
                CircuitBreakerRegistry.of(circuitBreakerConfig);

        CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("BookSearchServiceBasedOnTime");

I ovenstående eksempel opretter vi en afbryderkonfiguration, der inkluderer et glidende vindue af typen TIME_BASED . Circuit breaker vil registrere svigt af opkald efter minimum 3 opkald. Hvis 70 procent af opkald mislykkes, åbnes afbryderen.

Eksempel på Circuit Breaker i Spring Boot Application

Vi har dækket de nødvendige begreber om afbryderen. Nu vil jeg vise, at vi kan bruge en afbryder i en Spring Boot-applikation.

På den ene side har vi en REST-applikation BooksApplication der grundlæggende gemmer detaljer om biblioteksbøger. På den anden side har vi en applikation Circuitbreakerdemo der kalder REST-applikationen ved hjælp af RestTemplate . Vi vil dekorere vores REST-opkald gennem afbryderen.

BooksApplication gemmer information om bøger i en MySQL-databasetabel librarybooks . REST-controlleren til denne applikation har GET og POST metoder.


package com.betterjavacode.books.controllers;

import com.betterjavacode.books.daos.BookDao;
import com.betterjavacode.books.models.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@CrossOrigin("https://localhost:8443")
@RestController
@RequestMapping("/v1/library")
public class BookController
{
    @Autowired
    BookDao bookDao;

    @GetMapping("/books")
    public ResponseEntity<List> getAllBooks(@RequestParam(required = false) String bookTitle)
    {
        try
        {
            List listOfBooks = new ArrayList<>();
            if(bookTitle == null || bookTitle.isEmpty())
            {
                bookDao.findAll().forEach(listOfBooks::add);
            }
            else
            {
                bookDao.findByTitleContaining(bookTitle).forEach(listOfBooks::add);
            }

            if(listOfBooks.isEmpty())
            {
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

            return new ResponseEntity<>(listOfBooks, HttpStatus.OK);
        }
        catch (Exception e)
        {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @GetMapping("/books/{id}")
    public ResponseEntity getBookById(@PathVariable("id") long id)
    {
        try
        {
            Optional bookOptional = bookDao.findById(id);

            return new ResponseEntity<>(bookOptional.get(), HttpStatus.OK);
        }
        catch (Exception e)
        {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @PostMapping("/books")
    public ResponseEntity addABookToLibrary(@RequestBody Book book)
    {
        try
        {
            Book createdBook = bookDao.save(new Book(book.getTitle(), book.getAuthor(),
                    book.getIsbn()));
            return new ResponseEntity<>(createdBook, HttpStatus.CREATED);
        }
        catch (Exception e)
        {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @PutMapping("/books/{id}")
    public ResponseEntity updateABook(@PathVariable("id") long id, @RequestBody Book book)
    {
        Optional bookOptional = bookDao.findById(id);

        if(bookOptional.isPresent())
        {
            Book updatedBook = bookOptional.get();
            updatedBook.setTitle(book.getTitle());
            updatedBook.setAuthor(book.getAuthor());
            updatedBook.setIsbn(book.getIsbn());
            return new ResponseEntity<>(bookDao.save(updatedBook), HttpStatus.OK);
        }
        else
        {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @DeleteMapping("/books/{id}")
    public ResponseEntity deleteABook(@PathVariable("id") long id)
    {
        try
        {
            bookDao.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        }
        catch (Exception e)
        {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

På den anden side, vores applikation Circuitbreakerdemo har en controller med thymeleaf-skabelon, så en bruger kan få adgang til applikationen i en browser.

Til demoformålet har jeg defineret CircuitBreaker i en separat bean, som jeg vil bruge i min serviceklasse.


    @Bean
    public CircuitBreaker countCircuitBreaker()
    {
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(10)
                .slowCallRateThreshold(65.0f)
                .slowCallDurationThreshold(Duration.ofSeconds(3))
                .build();

        CircuitBreakerRegistry circuitBreakerRegistry =
                CircuitBreakerRegistry.of(circuitBreakerConfig);

        CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("BooksSearchServiceBasedOnCount");

        return cb;
    }

    @Bean
    public CircuitBreaker timeCircuitBreaker()
    {
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
                .minimumNumberOfCalls(3)
                .slidingWindowSize(10)
                .failureRateThreshold(70.0f)
                .build();

        CircuitBreakerRegistry circuitBreakerRegistry =
                CircuitBreakerRegistry.of(circuitBreakerConfig);

        CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("BookSearchServiceBasedOnTime");
        return cb;
    }

Jeg har defineret to bønner, en til den tællebaserede afbryder og en anden til tidsbaseret.

BookStoreService vil indeholde en kaldende BooksApplication og vise bøger, der er tilgængelige. Denne service vil se ud som nedenfor:


@Controller
public class BookStoreService
{

    private static final Logger LOGGER = LoggerFactory.getLogger(BookStoreService.class);

    @Autowired
    public BookManager bookManager;

    @Autowired
    private CircuitBreaker countCircuitBreaker;

    @RequestMapping(value = "/home", method= RequestMethod.GET)
    public String home(HttpServletRequest request, Model model)
    {
        return "home";
    }

    @RequestMapping(value = "/books", method=RequestMethod.GET)
    public String books(HttpServletRequest request, Model model)
    {
        Supplier<List> booksSupplier =
                countCircuitBreaker.decorateSupplier(() -> bookManager.getAllBooksFromLibrary());

        LOGGER.info("Going to start calling the REST service with Circuit Breaker");
        List books = null;
        for(int i = 0; i < 15; i++)
        {
            try
            {
                LOGGER.info("Retrieving books from returned supplier");
                books = booksSupplier.get();
            }
            catch(Exception e)
            {
                LOGGER.error("Could not retrieve books from supplier", e);
            }
        }
        model.addAttribute("books", books);

        return "books";
    }
}

Så når brugeren klikker på bogsiden, henter vi bøger fra vores BooksApplication REST Service.

Jeg har automatisk koblet bønnen til countCircuitBreaker . Til demo-formål – jeg vil ringe til REST-tjenesten 15 gange i en løkke for at få alle bøgerne. På denne måde kan jeg simulere afbrydelse på min REST-serviceside.

Vores strømafbryder dekorerer en leverandør, der foretager REST-kald til fjernservice, og leverandøren gemmer resultatet af vores fjernservicekald.

I denne demo kalder vi vores REST-service på en sekventiel måde, men fjernservicekald kan også ske parallelt. Afbryderen vil stadig holde styr på resultaterne uanset sekventielle eller parallelle opkald.

Demo

Lad os se på, hvordan afbryderen vil fungere i en live demo nu. Min REST-tjeneste kører på port 8443 og min Circuitbreakerdemo programmet kører på port 8743.

Til at begynde med starter jeg begge applikationer og åbner startsiden for Circuitbreakerdemo Ansøgning. Hjemmesiden indeholder linket til at se alle bøgerne fra butikken.

For nu at simulere nogle fejl har jeg tilføjet følgende kode i mit RestTemplate-kald, der stort set sover i 3 sekunder, før resultatet af REST-opkaldet returneres.


    public List getAllBooksFromLibrary ()
    {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);

        ResponseEntity<List> responseEntity;
        long startTime = System.currentTimeMillis();
        LOGGER.info("Start time = {}", startTime);
        try
        {
            responseEntity= restTemplate.exchange(buildUrl(),
                    HttpMethod.GET, null, new ParameterizedTypeReference<List>()
                    {});
            if(responseEntity != null && responseEntity.hasBody())
            {
                Thread.sleep(3000);
                LOGGER.info("Total time to retrieve results = {}",
                        System.currentTimeMillis() - startTime);
                return responseEntity.getBody();
            }
        }
        catch (URISyntaxException | InterruptedException e)
        {
            LOGGER.error("URI has a wrong syntax", e);
        }

        LOGGER.info("No result found, returning an empty list");
        return new ArrayList<>();
    }

Kort sagt, min afbryderkreds vil kalde tjenesten nok gange til at passere tærsklen på 65 procent af langsomme opkald, der varer mere end 3 sekunder. Når jeg har klikket på linket til here , jeg vil modtage resultatet, men min afbryder vil være åben og tillader ikke fremtidige opkald, før den er i enten half-open stat eller closed tilstand.

Du vil bemærke, at vi begyndte at få en undtagelse CallNotPermittedException når afbryderen var i OPEN stat. Også afbryderen blev åbnet, da de 10 opkald blev udført. Dette skyldes, at vores skydevinduesstørrelse er 10.

En anden måde, jeg kan simulere fejlen ved at lukke min REST-tjeneste eller databasetjeneste. På den måde kan REST-opkald tage længere tid end nødvendigt.

Lad os nu skifte COUNT_BASED afbryder til TIME_BASED afbryder. I TIME_BASED afbryder, slukker vi vores REST-tjeneste efter et sekund, og derefter klikker vi på here link fra hjemmesiden. Hvis 70 procent af opkaldene inden for de sidste 10 sekunder mislykkes, åbner vores strømafbryder.

Da REST Service er lukket, vil vi se følgende fejl i Circuitbreakdemo ansøgning

Vi vil se antallet af fejl, før afbryderen vil være i OPEN tilstand.

En konfiguration kan vi altid tilføje, hvor længe vi ønsker at holde afbryderen i åben tilstand. Til demoen har jeg tilføjet, at afbryderen vil være i åben tilstand i 10 sekunder.

Hvordan håndteres OPEN-afbrydere?

Et spørgsmål melder sig, hvordan håndterer du OPEN afbrydere? Heldigvis resilience4j tilbyder en reservekonfiguration med Decorators nytte. I de fleste tilfælde kan du altid konfigurere dette for at få resultatet fra tidligere vellykkede resultater, så brugerne stadig kan arbejde med applikationen.

Konklusion

I dette indlæg har jeg dækket, hvordan man bruger en afbryder i en Spring Boot-applikation. Koden til denne demo er tilgængelig her.

I denne demo har jeg ikke dækket, hvordan man overvåger disse afbryderhændelser som resilience4j biblioteket tillader lagring af disse hændelser med målinger, som man kan overvåge med et overvågningssystem.

Hvis du kunne lide dette indlæg, kan du overveje at abonnere på min blog her.

Referencer

  1. Resilience4J Library – Resilience4J
  2. Circuit Breaker med Resilience4j – Circuit Breaker

Java tag