14. 亿级流量电商详情页系统实战---hystrix实现案例

1、Request Context

首先,有一个概念,叫做reqeust context,请求上下文,一般来说,在一个web应用中,hystrix

我们会在一个filter里面,对每一个请求都施加一个请求上下文,就是说,tomcat容器内,每一次请求,就是一次请求上下文

然后在这次请求上下文中,我们会去执行N多代码,调用N多依赖服务,有的依赖服务可能还会调用好几次

在一次请求上下文中,如果有多个command,参数都是一样的,调用的接口也是一样的,其实结果可以认为也是一样的

那么这个时候,我们就可以让第一次command执行,返回的结果,被缓存在内存中,然后这个请求上下文中,后续的其他对这个依赖的调用全部从内存中取用缓存结果就可以了

不用在一次请求上下文中反复多次的执行一样的command,提升整个请求的性能

HystrixCommand和HystrixObservableCommand都可以指定一个缓存key,然后hystrix会自动进行缓存,接着在同一个request context内,再次访问的时候,就会直接取用缓存

多次调用一个command,那么只会执行一次,后面都是直接取缓存

对于请求缓存(request caching),请求合并(request collapsing),请求日志(request log),等等技术,都必须自己管理HystrixReuqestContext的声明周期

在一个请求执行之前,都必须先初始化一个request context

HystrixRequestContext context = HystrixRequestContext.initializeContext();

然后在请求结束之后,需要关闭request context

context.shutdown();

一般来说,在java web来的应用中,都是通过filter过滤器来实现的

例如 查询多个商品信息

http://localhost:8081/getProductInfos?productIds=1,1,2,2

    @RequestMapping("/getProductInfos")
    @ResponseBody
    public String getProductInfos(String productIds){
        for(String productId : productIds.split(",")){
            GetProductInfoCommand command = new GetProductInfoCommand(Long.valueOf(productId));
            ProductInfo productInfo = command.execute();
            System.out.println(productInfo);
            System.out.println("是否是存缓存中获取的结果:"+command.isResponseFromCache());
        }
        return "success";
    }
/**
 * 获取商品信息
 */
public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {

    private Long productId;

    static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");

    public GetProductInfoCommand(Long productId) {       super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
             .andCommandKey(KEY)
             .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool"))
               .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
               .withCoreSize(15)
               .withQueueSizeRejectionThreshold(10))
       );


        this.productId = productId;

    }

    @Override
    protected ProductInfo run() throws Exception {
        String url = "http://localhost:8082/getProductInfo?productId=" + productId;
        String response = HttpClientUtils.sendGetRequest(url);
        System.out.println(response);
        ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class);

        System.out.println("调用接口查询 productId="+productId);
        return productInfo;
    }

}

请求日志输出

调用接口查询 productId=1
ProductInfo(id=1, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:03, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
{"cityId":1,"color":"红色,白色,黑色","id":1,"modifiedTime":"2022-08-31 10:10:03","name":"iphone7","pictureList":"a.jpg,b.jpg","price":5588,"service":"售后服务","shopId":1,"size":"S","specification":"iphone7的规格"}

调用接口查询 productId=1
ProductInfo(id=1, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:03, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
{"cityId":1,"color":"红色,白色,黑色","id":2,"modifiedTime":"2022-08-31 10:10:03","name":"iphone7","pictureList":"a.jpg,b.jpg","price":5588,"service":"售后服务","shopId":1,"size":"S","specification":"iphone7的规格"}

调用接口查询 productId=2
ProductInfo(id=2, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:03, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
{"cityId":1,"color":"红色,白色,黑色","id":2,"modifiedTime":"2022-08-31 10:10:03","name":"iphone7","pictureList":"a.jpg,b.jpg","price":5588,"service":"售后服务","shopId":1,"size":"S","specification":"iphone7的规格"}

调用接口查询 productId=2
ProductInfo(id=2, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:03, cityId=1, cityName=null)
是否是存缓存中获取的结果:false

我们希望相同的商品id请求,不在执行查询操作,而是直接走requestContext缓存

1.新建filter

/**
 * 请求上下文过滤器
 */
public class HystrixRequestContextServletFilter implements Filter {

    @Override
    public void init(FilterConfig config) {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        try{
            chain.doFilter(request, response);
        }finally {
            context.shutdown();
        }
    }

    @Override
    public void destroy() {

    }
}

2.添加到容器中

    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean registration = new FilterRegistrationBean(new HystrixRequestContextServletFilter());
        registration.addUrlPatterns("/*");
        return registration;
    }

3.在GetProductInfoCommand中,新增cacheKey

  /*hystrix 每次都会将返回的数据,保存到request context中,key 就是getCahceKey()*/
    @Override
    protected String getCacheKey() {
        return "product_info_" + productId;
    }

只要没有cacheKey,就不会走requstContext上下文,从中获取缓存
此时再访问,日志为


调用接口查询 productId=1
ProductInfo(id=1, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:28, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
ProductInfo(id=1, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:28, cityId=1, cityName=null)
是否是存缓存中获取的结果:true
{"cityId":1,"color":"红色,白色,黑色","id":2,"modifiedTime":"2022-08-31 10:10:28","name":"iphone7","pictureList":"a.jpg,b.jpg","price":5588,"service":"售后服务","shopId":1,"size":"S","specification":"iphone7的规格"}

调用接口查询 productId=2
ProductInfo(id=2, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:28, cityId=1, cityName=null)
是否是存缓存中获取的结果:false
ProductInfo(id=2, name=iphone7, price=5588.0, pictureList=a.jpg,b.jpg, specification=iphone7的规格, service=售后服务, size=S, color=红色,白色,黑色, shopId=1, modifiedTime=2022-08-31 10:10:28, cityId=1, cityName=null)
是否是存缓存中获取的结果:true

2、fallback降级机制

hystrix调用各种接口,或者访问外部依赖,mysql,redis,zookeeper,kafka,等等,如果出现了任何异常的情况

情况1: error 比如说报错了,访问mysql报错,redis报错,zookeeper报错,kafka报错,

情况2 :reject 对每个外部依赖,无论是服务接口,中间件,资源隔离,对外部依赖只能用一定量的资源去访问,线程池/信号量,如果资源池已满

情况3 : timeout 访问外部依赖的时候,访问时间过长,可能就会导致超时,报一个TimeoutException异常,

上述三种情况,都是我们说的异常情况,对外部依赖的东西访问的时候出现了异常,发送异常事件到短路器中去进行统计

情况4:circuit breaker 如果短路器发现异常事件的占比达到了一定的比例,直接开启短路,circuit breaker

上述四种情况,都会去调用fallback降级机制

fallback,降级机制,你之前都是必须去调用外部的依赖接口,或者从mysql中去查询数据的,但是为了避免说可能外部依赖会有故障

比如,你可以再内存中维护一个ehcache,作为一个纯内存的基于LRU自动清理的缓存,数据也可以放入缓存内

如果说外部依赖有异常,fallback这里,直接尝试从ehcache中获取数据

比如说,本来你是从mysql,redis,或者其他任何地方去获取数据的,获取调用其他服务的接口的,结果人家故障了,人家挂了,fallback,可以返回一个默认值

两种最经典的降级机制:纯内存数据,默认值

    @RequestMapping("/getProductInfo")
    @ResponseBody
    public String getProductInfo(Long productId){
        HystrixCommand<ProductInfo> hystrixCommand = new GetProductInfoCommand(productId);

        ProductInfo productInfo = hystrixCommand.execute();


        Long cityId = productInfo.getCityId();

        GetCityNameCommand getCityNameCommand = new GetCityNameCommand(cityId);
        String cityName = getCityNameCommand.execute();
        productInfo.setCityName(cityName);

        Long brandId = productInfo.getBrandId();
        GetBrandNameCommand getBrandNameCommand = new GetBrandNameCommand(brandId);
        //例如获取品牌名称是报错,降级处理
        String brandName = getBrandNameCommand.execute();
        productInfo.setBrandName(brandName);
        System.out.println(productInfo);
        return "success";
    }
/**
 * 品牌缓存
 */
public class BrandCache {

    private static Map<Long/*品牌id*/,String/*品牌名称*/> brandMap = new HashMap<>();

    static {
        brandMap.put(1l, "华为");
    }

    public static String getBrandName(Long brandId){
        return brandMap.get(brandId);
    }
}
/**
 * 获取品牌名称的command
 */
public class GetBrandNameCommand extends HystrixCommand<String> {

    private Long brandId;

    public GetBrandNameCommand(Long brandId){
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("BrandInfoService"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("GetBrandNameCommand"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetBrandInfoPool"))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        .withCoreSize(15)
                        .withQueueSizeRejectionThreshold(10))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        /*同时调用fallback的并发线程不能超过15个,默认是10,超过线程将被reject掉*/
                        .withFallbackIsolationSemaphoreMaxConcurrentRequests(15))
        );
        this.brandId = brandId;
    }

    @Override
    protected String run() throws Exception {
       throw new Exception();
    }

    @Override
    protected String getFallback() {
        System.out.println("降级----从获取获取");
        return BrandCache.getBrandName(brandId);
    }


}

HystrixObservableCommand,是实现resumeWithFallback方法

fallback.isolation.semaphore.maxConcurrentRequests

这个参数设置了HystrixCommand.getFallback()最大允许的并发请求数量,默认值是10,也是通过semaphore信号量的机制去限流

如果超出了这个最大值,那么直接被reject

3、短路器

1、如果经过短路器的流量超过了一定的阈值,HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()

举个例子,可能看起来是这样子的,要求在10s内,经过短路器的流量必须达到20个;在10s内,经过短路器的流量才10个,那么根本不会去判断要不要短路

2、如果断路器统计到的异常调用的占比超过了一定的阈值,HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()

如果达到了上面的要求,比如说在10s内,经过短路器的流量(只要执行一个command,这个请求就一定会经过短路器),达到了30个;同时其中异常的访问数量,占到了一定的比例,比如说60%的请求都是异常(报错,timeout,reject),会开启短路

3、然后断路器从close状态转换到open状态

4、断路器打开的时候,所有经过该断路器的请求全部被短路,不调用后端服务,直接走fallback降级

5、经过了一段时间之后,HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(),会half-open,让一条请求经过短路器,看能不能正常调用。如果调用成功了,那么就自动恢复,转到close状态

短路器,会自动恢复的,half-open,半开状态

默认 在滑动窗口的10s内,有超过20个请求,且有超过50%的请求异常,超时,被拒绝,则开启短路器,断路器开启以后,就只能调用fallback进行降级,且并发降低调用默认是10个线程; 超过5s后,短路器变为half-open状态,让部分请求绕过短路器进行业务访问,如果成功,则关闭短路器

6、circuit breaker短路器的配置

(1)circuitBreaker.enabled

控制短路器是否允许工作,包括跟踪依赖服务调用的健康状况,以及对异常情况过多时是否允许触发短路,默认是true

HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(boolean value)

(2)circuitBreaker.requestVolumeThreshold

设置一个rolling window,滑动窗口中,最少要有多少个请求时,才触发开启短路

举例来说,如果设置为20(默认值),那么在一个10秒的滑动窗口内,如果只有19个请求,即使这19个请求都是异常的,也是不会触发开启短路器的

HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(int value)

(3)circuitBreaker.sleepWindowInMilliseconds

设置在短路之后,需要在多长时间内直接reject请求,然后在这段时间之后,再重新导holf-open状态,尝试允许请求通过以及自动恢复,默认值是5000毫秒

HystrixCommandProperties.Setter()
.withCircuitBreakerSleepWindowInMilliseconds(int value)

(4)circuitBreaker.errorThresholdPercentage

设置异常请求量的百分比,当异常请求达到这个百分比时,就触发打开短路器,默认是50,也就是50%

HystrixCommandProperties.Setter()
.withCircuitBreakerErrorThresholdPercentage(int value)

(5)circuitBreaker.forceOpen

如果设置为true的话,直接强迫打开短路器,相当于是手动短路了,手动降级,默认false

HystrixCommandProperties.Setter()
.withCircuitBreakerForceOpen(boolean value)

(6)circuitBreaker.forceClosed

如果设置为ture的话,直接强迫关闭短路器,相当于是手动停止短路了,手动升级,默认false

HystrixCommandProperties.Setter()
.withCircuitBreakerForceClosed(boolean value)

  public GetProductInfoCommand(Long productId) {
       super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
             .andCommandKey(KEY)
             .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool"))
               .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
               .withCoreSize(15)
               .withQueueSizeRejectionThreshold(10))
             .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                     /*10s内经过的流量*/
                .withCircuitBreakerRequestVolumeThreshold(30)
                     /*默认50%,设置40%的错误,超时,被拒绝,则开启短路器*/
                .withCircuitBreakerErrorThresholdPercentage(40)
                     /*默认5s,这里设置3s尝试恢复*/
                .withCircuitBreakerSleepWindowInMilliseconds(3000))
       );


        this.productId = productId;

    }

线程池机制的优点如下:

(1)任何一个依赖服务都可以被隔离在自己的线程池内,即使自己的线程池资源填满了,也不会影响任何其他的服务调用
(2)服务可以随时引入一个新的依赖服务,因为即使这个新的依赖服务有问题,也不会影响其他任何服务的调用
(3)当一个故障的依赖服务重新变好的时候,可以通过清理掉线程池,瞬间恢复该服务的调用,而如果是tomcat线程池被占满,再恢复就很麻烦
(4)如果一个client调用库配置有问题,线程池的健康状况随时会报告,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机
(5)如果一个服务本身发生了修改,需要重新调整配置,此时线程池的健康状况也可以随时发现,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机
(6)基于线程池的异步本质,可以在同步的调用之上,构建一层异步调用层

简单来说,最大的好处,就是资源隔离,确保说,任何一个依赖服务故障,不会拖垮当前的这个服务

线程池机制的缺点:

(1)线程池机制最大的缺点就是增加了cpu的开销
除了tomcat本身的调用线程之外,还有hystrix自己管理的线程池
(2)每个command的执行都依托一个独立的线程,会进行排队,调度,还有上下文切换
(3)Hystrix官方自己做了一个多线程异步带来的额外开销,通过对比多线程异步调用+同步调用得出,Netflix API每天通过hystrix执行10亿次调用,每个服务实例有40个以上的线程池,每个线程池有10个左右的线程
(4)最后发现说,用hystrix的额外开销,就是给请求带来了3ms左右的延时,最多延时在10ms以内,相比于可用性和稳定性的提升,这是可以接受的

我们可以用hystrix semaphore技术来实现对某个依赖服务的并发访问量的限制,而不是通过线程池/队列的大小来限制流量

sempahore技术可以用来限流和削峰,但是不能用来对调研延迟的服务进行timeout和隔离

execution.isolation.strategy,设置为SEMAPHORE,那么hystrix就会用semaphore机制来替代线程池机制,来对依赖服务的访问进行限流

如果通过semaphore调用的时候,底层的网络调用延迟很严重,那么是无法timeout的,只能一直block住


/**
 * 获取商品信息
 */
public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {

    private Long productId;

    static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand");

    /**
     * withCoreSize:设置你的线程池的大小
     * withMaxQueueSize:设置的是你的等待队列,缓冲队列的大小
     * withQueueSizeRejectionThreshold:如果withMaxQueueSize<withQueueSizeRejectionThreshold,
     * 那么取的是withMaxQueueSize,反之,取得是withQueueSizeRejectionThreshold
     * @param productId
     */
    public GetProductInfoCommand(Long productId) {
//        super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoGroup"));
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService"))
                .andCommandKey(KEY)
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GetProductInfoPool"))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        /*线程池数量*/
                        .withCoreSize(10)
                        /*等待队列长度*/
                        .withMaxQueueSize(12)

                        .withQueueSizeRejectionThreshold(8))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        /*10s内经过的流量*/
                        .withCircuitBreakerRequestVolumeThreshold(30)
                        /*默认50%,设置40%的错误,超时,被拒绝,则开启短路器*/
                        .withCircuitBreakerErrorThresholdPercentage(40)
                        /*默认5s,这里设置3s尝试恢复*/
                        .withCircuitBreakerSleepWindowInMilliseconds(3000)
                        /*执行超时时间,设置的时间事宜大一些,否则如果请求在缓存队列中等待的时间长了,也会超时*/
                        .withExecutionTimeoutInMilliseconds(10000)
                        /*最多30个并发执行fallback*/
                        .withFallbackIsolationSemaphoreMaxConcurrentRequests(30))
        );


        this.productId = productId;

    }

    @Override
    protected ProductInfo run() throws Exception {
        if(productId == -1){
            throw new RuntimeException();
        }

        if(productId == -2){
            Thread.sleep(2000);
        }
        String url = "http://localhost:8082/getProductInfo?productId=" + productId;
        String response = HttpClientUtils.sendGetRequest(url);
        System.out.println(response);
        ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class);

        System.out.println("调用接口查询 productId="+productId);
        return productInfo;
    }
    public static void flushCache(Long productId){
        HystrixRequestCache.getInstance(KEY,
                HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf("product_info_" + productId));

    }

    @Override
    protected ProductInfo getFallback() {
        System.out.println("走降级了");
       ProductInfo productInfo = new ProductInfo();
       productInfo.setName("降级商品");
       return productInfo;
    }
}

public class RejectTest {
    public static void main(String[] args) {

        for(int i=0;i< 25;i++){
            new TestThread(i).start();
        }

    }


    private static class TestThread extends Thread{

        private int index;

        public TestThread(int index){
            this.index = index;
        }

        @Override
        public void run() {
            String response = HttpClientUtils.sendGetRequest("http://localhost:8081/getProductInfo?productId=-2");
            System.out.println("第"+index+"次 请求,返回: "+response);

        }
    }
}
  @RequestMapping("/getProductInfo")
    @ResponseBody
    public String getProductInfo(Long productId){
        HystrixCommand<ProductInfo> hystrixCommand = new GetProductInfoCommand(productId);

        ProductInfo productInfo = hystrixCommand.execute();


        Long cityId = productInfo.getCityId();

        GetCityNameCommand getCityNameCommand = new GetCityNameCommand(cityId);
        String cityName = getCityNameCommand.execute();
        productInfo.setCityName(cityName);

        return JSON.toJSONString(productInfo);
    }

结果发现有10+8个线程请求到了数据,而7个线程降级处理了

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值