Spring?Cloud?使用?Resilience4j?實現(xiàn)服務熔斷的方法
CircuitBreaker 斷路器
服務熔斷是為了保護我們的服務,比如當某個服務出現(xiàn)問題的時候,控制打向它的流量,讓它有時間去恢復,或者限制一段時間只能有固定數(shù)量的請求打向這個服務。這些都是保護措施。我在實際工作中也確實遇到過,數(shù)據(jù)庫出現(xiàn)問題了,進而導致Web服務出現(xiàn)問題了,導致不依賴數(shù)據(jù)庫的服務也出現(xiàn)問題了,出現(xiàn)一連串問題。 這次學習《玩轉(zhuǎn) Spring 全家桶》,丁雪豐老師給了使用resilience4j的例子。 丁老師的例子是2019年的,這個框架已經(jīng)修改了些方法,所以我自己也花了些時間來理解了它的用法?,F(xiàn)將過程記錄下來。
首先POM文件引入
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>2.0.2</version> </dependency>
接著改造之前的Controller方法
@RestController @RequestMapping("/customer") @Slf4j public class BookController { @Autowired private BookService bookService; private CircuitBreaker circuitBreaker; public BookController(CircuitBreakerRegistry registry) { circuitBreaker = registry.circuitBreaker("menu"); } @GetMapping("/menu") public List<Book> readMenu() { Supplier<List<Book>> supplier = () -> bookService.getAll(); circuitBreaker.getEventPublisher() .onEvent(event -> log.info(event.toString())); try{ return circuitBreaker.executeSupplier(supplier); } catch (Exception ex) { log.error(ex.getMessage()); return Collections.emptyList(); } } }
不同的地方就是引入了CircuitBreaker, 然后使用它將我們的方法“bookService.getAll()”包起來了。
然后在配置文件中添加如下的配置
resilience4j.circuitbreaker.backends.menu.failure-rate-threshold=50 resilience4j.circuitbreaker.backends.menu.wait-duration-in-open-state=60000 resilience4j.circuitbreaker.backends.menu.sliding-window-size=5 resilience4j.circuitbreaker.backends.menu.permitted-number-of-calls-in-half-open-state=2 resilience4j.circuitbreaker.backends.menu.minimum-number-of-calls=2
稍微解釋一下這里的配置
failure-rate-threshold=50是說失敗率超過50%就熔斷,
wait-duration-in-open-state= 60000,是說熔斷后等待60S才允許再次調(diào)用。
sliding-window-size =5 可以理解為5個請求統(tǒng)計一次,
permitted-number-of-calls-in-half-open-state = 2是說進入半開的狀態(tài)的時候,還允許請求多少個。
minimum-number-of-calls=2是說最少有多少個請求才開始統(tǒng)計。 這里的參數(shù)都是我為了實驗設置的,實際情況根據(jù)需要進行調(diào)整。參數(shù)比較多,具體可以參加官方文檔
https://resilience4j.readme.io/docs/circuitbreaker
我們來看下實際的效果通過瀏覽器訪問,
首先我們現(xiàn)打開BookService,讓它有一次成功的請求,日志會輸出
CircuitBreaker 'menu' recorded a successful call.
然后我們將BookService關(guān)閉,讓它請求失敗,日志會輸出如下
CircuitBreaker 'menu' recorded an error: 'feign.RetryableException: Connection refused: no further information executing GET http://bookshop-service/book/getAll'. Elapsed time: 2050 ms
CircuitBreaker 'menu' exceeded failure rate threshold. Current failure rate: 50.0
CircuitBreaker 'menu' changed state from CLOSED to OPEN
可以看到斷路器已經(jīng)打開了,
接著我們繼續(xù)訪問會出現(xiàn),
CircuitBreaker 'menu' recorded a call which was not permitted.
這個時候請求不會打到BookService上面了。就算這個時候我們的BookService恢復正常。
等待60s后進入半Open的狀態(tài)
CircuitBreaker 'menu' changed state from OPEN to HALF_OPEN
這個時候恢復BookService正常,我們請求也會正常響應了
CircuitBreaker 'menu' recorded a successful call
多請求幾次,斷路器就從HALF_OPEN變成了CLOSED
CircuitBreaker 'menu' changed state from HALF_OPEN to CLOSED
這里給一個官方的狀態(tài)圖來說明
斷路器有三個狀態(tài): CLOSED, OPEN, HALF_OPEN。
CLOSED是最開始的狀態(tài),也就是關(guān)閉狀態(tài),流量可以正常通過,當失敗比率超過threshold后,斷路器打開, 變成OPEN 打開后流量不可以通過;等待一定的時間后,斷路器進入半開狀態(tài) HALF_OPEN, 這個時候如果失敗率低于閾值,斷路器進入CLOSED狀態(tài),如果超過閾值,斷路器繼續(xù)保證OPEN,再等待,如此往復。
斷路器現(xiàn)在還支持設置慢請求,使用起來還是比較方便。對于參數(shù)的設置如果不是很理解,可以通過單元測試的方法來加深對它的理解。這里參考https://github.com/eugenp/tutorials/blob/master/libraries-6/src/test/java/com/baeldung/resilence4j/Resilience4jUnitTest.java 上面的例子,給出來個單元測試
interface RemoteService { int process(int i); } private RemoteService service; @Test public void whenCircuitBreakerIsUsed_thenItWorksAsExpected() { service = mock(RemoteService.class); CircuitBreakerConfig config = CircuitBreakerConfig.custom() // Percentage of failures to start short-circuit .failureRateThreshold(20) .minimumNumberOfCalls(5) .build(); CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config); CircuitBreaker circuitBreaker = registry.circuitBreaker("my"); Function<Integer, Integer> decorated = CircuitBreaker.decorateFunction(circuitBreaker, service::process); when(service.process(anyInt())).thenThrow(new RuntimeException()); circuitBreaker.getEventPublisher() .onEvent(event -> { log.info(event.toString()); }); for (int i = 0; i < 10; i++) { try { decorated.apply(i); } catch (Exception ignore) { } } verify(service, times(5)).process(any(Integer.class)); }
這里設置最少請求5次,失敗率超過20%就熔斷,然后我們請求了10次,實際上只調(diào)用了Service5次。
對于其它參數(shù),你可以調(diào)整后,根據(jù)需要來驗證是否符合預期。它的日志輸出如下
CircuitBreaker 'my' recorded an error: 'java.lang.RuntimeException'. Elapsed time: 2 ms
CircuitBreaker 'my' recorded an error: 'java.lang.RuntimeException'. Elapsed time: 0 ms
CircuitBreaker 'my' recorded an error: 'java.lang.RuntimeException'. Elapsed time: 0 ms
CircuitBreaker 'my' recorded an error: 'java.lang.RuntimeException'. Elapsed time: 0 ms
CircuitBreaker 'my' recorded an error: 'java.lang.RuntimeException'. Elapsed time: 0 ms
CircuitBreaker 'my' exceeded failure rate threshold. Current failure rate: 100.0
CircuitBreaker 'my' changed state from CLOSED to OPEN
CircuitBreaker 'my' recorded a call which was not permitted.
CircuitBreaker 'my' recorded a call which was not permitted.
CircuitBreaker 'my' recorded a call which was not permitted.
CircuitBreaker 'my' recorded a call which was not permitted.
CircuitBreaker 'my' recorded a call which was not permitted.
可以看到5次過后,就開始打開斷路器,后面的call就不被允許了。
隔艙Bulkhead
Resilience4j 里面的Bulkhead可以簡單的理解為允許多少個并發(fā)訪問。我們這里還是通過單元測試的方法來演示它的功能
@Test public void whenBulkheadIsUsed_thenItWorksAsExpected() throws InterruptedException { service = mock(RemoteService.class); BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(2).build(); BulkheadRegistry registry = BulkheadRegistry.of(config); Bulkhead bulkhead = registry.bulkhead("my"); Function<Integer, Integer> decorated = Bulkhead.decorateFunction(bulkhead, service::process); try { callAndBlock(decorated); } catch(BulkheadFullException ex) { log.error("isfull"); } finally { verify(service, times(2)).process(any(Integer.class)); } } private void callAndBlock(Function<Integer, Integer> decoratedService) throws InterruptedException { when(service.process(anyInt())).thenAnswer(invocation -> { log.info("service called"); return null; }); ArrayList<Integer> numberList = new ArrayList<Integer>(); for(int i = 0;i<10;i++) { numberList.add(i); } numberList.parallelStream().forEach((i)->{ try { decoratedService.apply(i); } catch (Exception ex) { log.error("meet error " + ex.getMessage()); } catch (Throwable e) { throw new RuntimeException(e); } }); }
首先我們解讀一下callAndBlock, 它會并發(fā)的去執(zhí)行一個function. 如果我們不用隔艙,它的輸出會是這樣。
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-4] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-9] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-5] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-3] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-1] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-6] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-7] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-8] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.010+08:00 INFO 37276 --- [onPool-worker-2] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:22:52.011+08:00 INFO 37276 --- [ main] c.k.r.bookcustomer.Resilience4jUnitTest : service called
可以看到啟動了10個線程去訪問方法。加了隔艙后,隔艙限定了一次只能兩個,輸出如下
2022-12-28T15:33:48.648+08:00 ERROR 32256 --- [onPool-worker-4] c.k.r.bookcustomer.Resilience4jUnitTest : meet error Bulkhead 'my' is full and does not permit further calls
2022-12-28T15:33:48.648+08:00 ERROR 32256 --- [onPool-worker-6] c.k.r.bookcustomer.Resilience4jUnitTest : meet error Bulkhead 'my' is full and does not permit further calls
2022-12-28T15:33:48.648+08:00 ERROR 32256 --- [onPool-worker-7] c.k.r.bookcustomer.Resilience4jUnitTest : meet error Bulkhead 'my' is full and does not permit further calls
2022-12-28T15:33:48.648+08:00 ERROR 32256 --- [onPool-worker-4] c.k.r.bookcustomer.Resilience4jUnitTest : meet error Bulkhead 'my' is full and does not permit further calls
2022-12-28T15:33:48.648+08:00 ERROR 32256 --- [onPool-worker-8] c.k.r.bookcustomer.Resilience4jUnitTest : meet error Bulkhead 'my' is full and does not permit further calls
2022-12-28T15:33:48.648+08:00 ERROR 32256 --- [onPool-worker-5] c.k.r.bookcustomer.Resilience4jUnitTest : meet error Bulkhead 'my' is full and does not permit further calls
2022-12-28T15:33:48.648+08:00 ERROR 32256 --- [onPool-worker-2] c.k.r.bookcustomer.Resilience4jUnitTest : meet error Bulkhead 'my' is full and does not permit further calls
2022-12-28T15:33:48.648+08:00 ERROR 32256 --- [onPool-worker-3] c.k.r.bookcustomer.Resilience4jUnitTest : meet error Bulkhead 'my' is full and does not permit further calls
2022-12-28T15:33:48.650+08:00 INFO 32256 --- [onPool-worker-1] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:33:48.650+08:00 INFO 32256 --- [ main] c.k.r.bookcustomer.Resilience4jUnitTest : service called
可以看到只有兩次成功的訪問,其它的訪問都被block了。
限速器RateLimiter
RateLimiter的功能是限定一段時間內(nèi)允許多少次訪問,還是使用和Bulkhead一樣的例子一樣
@Test public void whenRateLimiterInUse_thenItWorksAsExpected() throws InterruptedException { service = mock(RemoteService.class); RateLimiterConfig config = RateLimiterConfig.custom() .limitRefreshPeriod(Duration.ofMillis(1000)) .limitForPeriod(4) .timeoutDuration(Duration.ofMillis(25)) .build(); RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.of(config); RateLimiter rateLimiter = rateLimiterRegistry .rateLimiter("name1"); CheckedFunction<Integer, Integer> decorated = RateLimiter .decorateCheckedFunction(rateLimiter, service::process); try { callAndBlock(decorated); } catch(Exception ex) { log.error("isfull"); } finally { verify(service, times(4)).process(any(Integer.class)); } } private void callAndBlock(CheckedFunction<Integer, Integer> decoratedService) throws InterruptedException { when(service.process(anyInt())).thenAnswer(invocation -> { log.info("service called"); return null; }); ArrayList<Integer> numberList = new ArrayList<Integer>(); for(int i = 0;i<10;i++) { numberList.add(i); } numberList.parallelStream().forEach((i)->{ try { decoratedService.apply(i); } catch (Exception ex) { log.error("meet error " + ex.getMessage()); } catch (Throwable e) { throw new RuntimeException(e); } }); }
我們這里故意設置1S中允許訪問4次,實際的運行情況也是只允許了4次。日志輸出如下
2022-12-28T15:39:52.027+08:00 INFO 35236 --- [onPool-worker-2] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:39:52.027+08:00 INFO 35236 --- [onPool-worker-5] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:39:52.027+08:00 INFO 35236 --- [ main] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:39:52.027+08:00 INFO 35236 --- [onPool-worker-7] c.k.r.bookcustomer.Resilience4jUnitTest : service called
2022-12-28T15:39:52.053+08:00 ERROR 35236 --- [onPool-worker-6] c.k.r.bookcustomer.Resilience4jUnitTest : meet error RateLimiter 'name1' does not permit further calls
2022-12-28T15:39:52.060+08:00 ERROR 35236 --- [onPool-worker-3] c.k.r.bookcustomer.Resilience4jUnitTest : meet error RateLimiter 'name1' does not permit further calls
2022-12-28T15:39:52.060+08:00 ERROR 35236 --- [onPool-worker-9] c.k.r.bookcustomer.Resilience4jUnitTest : meet error RateLimiter 'name1' does not permit further calls
2022-12-28T15:39:52.060+08:00 ERROR 35236 --- [onPool-worker-1] c.k.r.bookcustomer.Resilience4jUnitTest : meet error RateLimiter 'name1' does not permit further calls
2022-12-28T15:39:52.060+08:00 ERROR 35236 --- [onPool-worker-8] c.k.r.bookcustomer.Resilience4jUnitTest : meet error RateLimiter 'name1' does not permit further calls
2022-12-28T15:39:52.075+08:00 ERROR 35236 --- [onPool-worker-4] c.k.r.bookcustomer.Resilience4jUnitTest : meet error RateLimiter 'name1' does not permit further calls
限速器這個功能只能限制在整體性能上面,如果要限制某個用戶,只能某段時間訪問多少次,它就做不到了。
Relilience4j 里面還提供了Retry,TimeLimiter,Cache. 感覺不是很有必要的功能, Retry在spring里面有相應的功能了,沒有必要專門為了使用它而多加個包。 TimeLimiter,Cache 我感覺不是很受重視的功能,連例子文檔都懶得提供,可見意義不大。
到此這篇關(guān)于Spring Cloud 使用 Resilience4j 實現(xiàn)服務熔斷的方法的文章就介紹到這了,更多相關(guān)Spring Cloud 服務熔斷內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring boot+ redis 接口訪問頻率限制的實現(xiàn)
這篇文章主要介紹了spring boot+ redis 接口訪問頻率限制的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01Java Web程序中利用Spring框架返回JSON格式的日期
這里我們來介紹一下Java Web程序中利用Spring框架返回JSON格式的日期的方法,前提注意使用@DatetimeFormat時要引入一個類庫joda-time-版本.jar,否則會無法訪問相應路徑2016-05-05Java深入了解數(shù)據(jù)結(jié)構(gòu)之二叉搜索樹增 插 刪 創(chuàng)詳解
二叉搜索樹是以一棵二叉樹來組織的。每個節(jié)點是一個對象,包含的屬性有l(wèi)eft,right,p和key,其中,left指向該節(jié)點的左孩子,right指向該節(jié)點的右孩子,p指向該節(jié)點的父節(jié)點,key是它的值2022-01-01深入理解Spring MVC的數(shù)據(jù)轉(zhuǎn)換
這篇文章主要給大家介紹了關(guān)于Spring MVC數(shù)據(jù)轉(zhuǎn)換的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起看看吧。2017-09-09聊聊BeanUtils.copyProperties和clone()方法的區(qū)別
這篇文章主要介紹了聊聊BeanUtils.copyProperties和clone()方法的區(qū)別,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09Java 中 Date 與 Calendar 之間的編輯與轉(zhuǎn)換實例詳解
這篇文章主要介紹了Java 中 Date 與 Calendar 之間的編輯與轉(zhuǎn)換 ,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-07-07