钨丝woosmart 用100行代码手写一个Hystrix

熔断与降级
离小眼睛家不远的地方 , 开了一个熟食店 。 店内有两个窗口总能排起长龙 , 一个窗口是选好的凉菜让师傅调味 , 一个窗口是买到的扒鸡让胖师傅现场脱骨 。 顾客的正常的流程 , 大致是这个样子滴:
钨丝woosmart 用100行代码手写一个Hystrix
文章图片
炎炎夏日 , 邀三五好友 , 喝杯啤酒吹吹牛皮 , 岂不美哉 。 可能大家跟小眼睛想法一致 , 小店的生意日渐火爆 。 这天 , 小眼睛选好了菜 , 付了钱 , 正准备排队让师傅调口味、脱骨 。 目测两个窗口排队时间不会少于20分钟 , 加之几个朋友轮番催促 , 果断放弃 , 拎着菜直接回家 。 于是我到流程就变成了:
钨丝woosmart 用100行代码手写一个Hystrix
文章图片
【钨丝woosmart 用100行代码手写一个Hystrix】当下游的服务(调料、脱骨)因为某种原因突然变得不可用或响应过慢(买菜3分钟排队半小时) , 上游服务为了保证自己整体服务的可用性(等不及了) , 不再继续调用目标服务 , 直接返回 , 快速释放资源 。 如果目标服务情况好转则恢复调用 。 这就叫做服务熔断 。
小眼睛因为排队时间过长 , 果断放弃后续流程 , 提供了「降低品质」的菜品 。 这叫做服务降级 。
熔断有多种方式
服务降级的方式有很多种 , 比如限流、开关、熔断 , 熔断是降级的一种 。
熔断 , 在SpringCloud中有熔断降级库Hystrix , 在分布式项目中也可以使用阿里开源的Sentinel达到熔断降级目的 。 无论是Hystrix还是Sentinel都需要引入第三方组件 , 搞明白实现原理 , 不适合简单场景下的使用 。
手写熔断器的使用
本文介绍一种适合简单应用的熔断方法 , 核心代码不超过100行 。 使用方法大致如下:
//初始化一个熔断器privateCircuitBreakerbreaker=newCircuitBreaker(0.1,10,true,''serviceDemo'');publicvoiddoSomething(){//每次调用都检查服务状态breaker.checkStatus();//如果熔断器返回true认为服务可用 , 继续执行逻辑if(breaker.isWorked()){try{service.doSomething();}catch(Exceptione){e.printStackTrace();//出现调用失败 , 记录失败次数breaker.addFailTimes();}finally{//每一次调用 , 增加调用次数breaker.addInvokeTimes();}}//服务不可用 , 执行降级逻辑}这段伪代码中 , 熔断器做了三件事儿:
检查服务状态 , 并且输出统计日志返回服务状态breaker.isWorked()记录调用次数和失败次数 , 作为熔断依据熔断器的实现
熔断器具体实现如下:
publicclassCircuitBreaker{/***记录失败次数*/privateAtomicLongfailTimes=newAtomicLong(0);/***记录调用次数*/privateAtomicLonginvokeTimes=newAtomicLong(0);/***降级阈值 , 比如0.1*请求失败次数/请求总次数的比例*/privatedoublefailedRate=0.1;/***降级最小条件 , 请求总次数大于该值*才会执行阈值判断*比如设置为10 , *当请求次数大于10次时才会执行判断*/privatedoubleminTimes;/***熔断开关 , 默认关闭*/privatebooleanenabled;/***熔断后是否发送邮件告警*/privatebooleanmail;/***熔断后是否发送短信告警*/privatebooleansms;/***熔断器名字*/privateStringname;/***保存上一次统计的时间戳 , 记录单位是分钟*/privateAtomicLongcurrentTime=newAtomicLong(System.currentTimeMillis()/60000);/***记录服务是否是不可用状态*/privateAtomicBooleanisFailed=newAtomicBoolean(false);/***服务宕掉的状态放到线程容器中*/privateThreadLocal<Boolean>fail=newThreadLocal<Boolean>();privateLoggerlog=LoggerFactory.getLogger(getClass());/***构造熔断器**@paramfailedRate熔断的阈值 , *请求失败次数/请求总次数*@paramminTimes熔断的最小条件 , *请求总次数大于该值才会根据阈值判断 , *执行降级操作*@paramenabled是否需开启熔断操作*/publicCircuitBreaker(doublefailedRate,doubleminTimes,booleanenabled,Stringname){fail.set(false);this.failedRate=failedRate;this.minTimes=minTimes;this.enabled=enabled;this.name=name;}/***判断服务是否是失败状态**@return*/publicbooleanisFailed(){returnisFailed.get();}/***增加错误次数*/publicvoidaddFailTimes(){fail.set(true);if(enabled){failTimes.incrementAndGet();}}/***增加一次调用次数*/publicvoidaddInvokeTimes(){if(enabled){invokeTimes.incrementAndGet();}}/***判断服务是否可用**@return*/publicbooleanisWorked(){if(!enabled){returntrue;}//当服务不可用时 , 牺牲掉1%的流量做探活请求if(isFailed.get()&&System.currentTimeMillis()%100==0){returntrue;}if(isFailed.get()){fail.set(true);returnfalse;}returntrue;}publicvoidcheckStatus(){if(!enabled){return;}longnewTime=System.currentTimeMillis()/60000;if((newTime>currentTime.get())&&(invokeTimes.get()>minTimes)){doublepercent=failTimes.get()*1.0/invokeTimes.get();if(percent>failedRate){if(isFailed.get()){//日志输出if(mail){//发送邮件通知}}else{//日志输出isFailed.set(true);if(sms){//发送短信通知}if(mail){//发送邮件通知}}}else{//服务恢复if(isFailed.get()){//日志输出if(sms){//发送短信通知}if(mail){//发送邮件通知}}isFailed.set(false);}if(log.isInfoEnabled()){//日志输出}currentTime.set(newTime);failTimes.set(0);invokeTimes.set(0);}}}总体思路: