SpringCloud容錯保護Hystrix(一)

與Eureka和Ribbon一樣,Hystrix也是Netfix開源的一個框架,中文名:容錯保護系統。SpringCloudHystrix實現了斷路器、線程隔離等一系列服務保護功能。在微服務架構中,每個單元都在不同的進程中運行,進程間通過遠程調用的方式相互依賴,這樣就可能因爲網絡的原因出現調用故障和延遲,如果調用請求不斷增加,將會導致自身服務的癱瘓。爲了解決這些問題,產生了斷路器等一系列服務保護機制。斷路器詳細介紹:斷路器

簡單使用

直接使用上一篇:SpringCloud學習之Ribbon,在article-service中添加。
pom文件

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

在主類上添加@EnableCircuitBreaker@EnableHystrix註解開啓Hystrix的使用。

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class ArticleApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(ArticleApplication.class, args);
    }
}

這裏也可以使用@SpringCloudApplication註解,該註解已經包含了我們添加的三個註解,所以可以看出SpringCloud的標準應用應該包服務發現和斷路器

然後在ArticleController添加方法,並添加@HystrixCommand定義服務降級,這裏的fallbackMethod服務調用失敗後調用的方法。

    /**
     * 使用Hystrix斷路器
     * @param id
     * @return
     */
    @HystrixCommand(fallbackMethod = "fallback")
    @GetMapping("/hystrix/{id}")
    public String findUserHystrix(@PathVariable("id") Long id){
        return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id).toString();
    }

    private String fallback(Long id){
        return "Error:"+id;
    }

重啓服務,如果沒有出現故障,這裏是可以正常訪問並返回正確的數據。下面將服務接口sleep來模擬網絡延遲:

@RestController
public class UserController {
    @Autowired
    private UserRepository userRepository;
    @GetMapping("/user/{id}")
    public User findById(@PathVariable("id") Long id) throws InterruptedException {
        Thread.sleep(5000);
        return userRepository.findOne(id);
    }
}

訪問:http://localhost:30000/hystrix/3,這裏會調用回調函數返回數據。

通過上面的使用,發現一個問題:使用這種方法配置服務降級的方式,回調函數的入參和返回值必須與接口函數的一直,不然會拋出異常。

自定義Hystrix命令

上面使用註解方式配置非常簡單。在Hystrix中我們也可以通過繼承HystrixCommand來實現自定義的HystrixCommand,而且還支持同步請求和異步請求兩種方式。

創建UserCommand並繼承HystrixCommand,實現run方法:

public class UserCommand extends HystrixCommand<User> {

    private final Logger logger =  LoggerFactory.getLogger(UserCommand.class);
    private RestTemplate restTemplate;
    private Long id;

    public UserCommand(Setter setter,RestTemplate restTemplate,Long id){
        super(setter);
        this.restTemplate = restTemplate;
        this.id = id;
    }

    @Override
    protected User run() throws Exception {
        logger.info(">>>>>>>>>>>>>自定義HystrixCommand請求>>>>>>>>>>>>>>>>>>>>>>>>>>");
        return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id);
    }
}

然後添加一個接口

 @GetMapping("/command/{id}")
    public User findUserCommand(@PathVariable("id") Long id) throws ExecutionException, InterruptedException {
        com.netflix.hystrix.HystrixCommand.Setter setter = com.netflix.hystrix.HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(""));
        UserCommand userCommand = new UserCommand(setter,restTemplate,id);
        //同步調用
//        User user = userCommand.execute();
        //異步請求
        Future<User> queue = userCommand.queue();
        User user = queue.get();
        return user;
    }

Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(""))是設置自定義命令的參數。先調用withGroupKye來設置分組,然後通過asKey來設置命令名;因爲在Setter的定義中,只有withGroupKye靜態函數可以創建Setter實例,所以GroupKey是Setter必需的參數。深入介紹可以查看源碼或者看DD大佬的《SpringCloud微服務實戰》。查看@HystrixCommand註解源碼,可以看到這裏也有groupKey、commandKey等參數,這也就是說使用@HystrixCommand註解時是可以配置命令名稱、命令分組和線程池劃分等參數的。

註解實現異步請求

上面自定義命令中可以實現異步,同樣也可以直接使用註解來實現異步請求;
1. 配置HystrixCommandAspect的Bean

@Bean
public HystrixCommandAspect hystrixCommandAspect(){
    return new HystrixCommandAspect();
}
  1. 然後使用AsyncResult來執行調用
    @HystrixCommand
    @GetMapping("/async/{id}")
    public Future<User> findUserAsync(@PathVariable("id") Long id){
        return new AsyncResult<User>() {
            @Override
            public User invoke() {
                return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id);
            }
        };
    }

異常處理

異常傳播

查看@HystrixCommand註解源碼可以發現裏面有個ignoreExceptions參數。該參數是定義忽略指定的異常功能。如下代碼,當方法拋出NullPointerException時會將異常拋出,而不觸發降級服務。

  @HystrixCommand(fallbackMethod = "fallback",ignoreExceptions = {NullPointerException.class})
    @GetMapping("/hystrix/{id}")
    public User findUserHystrix(@PathVariable("id") Long id){
        return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id);
    }

異常獲取

  1. 傳統的繼承方式,在繼承了HystrixCommand類中重寫getFallback()方法,這裏在run方法中添加弄出一個異常
@Override
protected User getFallback() {
    Throwable e = getExecutionException();
    logger.info(">>>>>>>>>>>>>>>>>>>>>{}<<<<<<<<<<<<<<<<",e.getMessage());
    return new User(-1L,"",-1);
}

@Override
protected User run() throws Exception {
    logger.info(">>>>>>>>>>>>>自定義HystrixCommand請求>>>>>>>>>>>>>>>>>>>>>>>>>>");
    int i = 1/0;
    return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id);
}


2. 使用註解,在自定義的服務降級方法中可以使用Throwable 獲取異常信息,

@HystrixCommand(fallbackMethod = "fallback")
@GetMapping("/hystrix/{id}")
public User findUserHystrix(@PathVariable("id") Long id){
    int i = 1/0;
    return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id);
}

private User fallback(Long id,Throwable throwable){
    LoggerFactory.getLogger(ArticleController.class).info("========{}=============",throwable.getMessage());
    return new User();
}

請求緩存

在高併發的場景下,Hystrix中提供了請求緩存的功能,可以方便的開啓和使用請求緩存來優化系統,達到減輕高併發時的請求線程消耗、降低請求相應時間。

繼承方式

在繼承了HystrixCommand類中重寫getCacheKey()方法

@Override
protected String getCacheKey() {
    return String.valueOf(id);
}
public UserCommand(RestTemplate restTemplate,Long id){
    super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("userGroup")));
    this.restTemplate = restTemplate;
    this.id = id;
}

通過getCacheKey()方法返回請求的Key值,Hystrix會根據getCacheKey返回的值來區分是否是重複請求,如果cacheKey相同,那麼該依賴服務只會在第一個請求達到時被真實的調用,另一個請求則是直接從請求緩存中返回結果。

修改後的接口類,該方法第一句爲初始化HystrixRequestContext,如果不初始化該對象會報錯。這裏是在測試環境,如果在真正項目中該初始化不應該在指定方法中。

 @GetMapping("/command/{id}")
    public User findUserCommand(@PathVariable("id") Long id) throws ExecutionException, InterruptedException {
        HystrixRequestContext.initializeContext();
        UserCommand u1 = new UserCommand(restTemplate,id);
        UserCommand u2 = new UserCommand(restTemplate,id);
        UserCommand u3 = new UserCommand(restTemplate,id);
        UserCommand u4 = new UserCommand(restTemplate,id);
        User user1 = u1.execute();
        System.out.println("第一次請求"+user1);
        User user2 = u2.execute();
        System.out.println("第二次請求"+user2);
        User user3 = u3.execute();
        System.out.println("第三次請求"+user3);
        User user4 = u4.execute();
        System.out.println("第四次請求"+user4);
        return user1;
    }

註解方式

在SpringCloudHystrix中與緩存有關的三個註解:
- @CacheResult:用來標記其你去命令的結果應該被緩存,必須與@HystrixCommand註解結合使用;
- @CacheRemove:該註解用來讓請求命令的緩存失敗,失效的緩存根據定義的Key決定;
- @CacheKey:該註解用來在請求命令的參數上標記,是其作文緩存的Key值,如果沒有標註則會使用所有參數。如果同時使用了@CacheResult和 @CacheRemove註解的cacheKeyMethod方法指定緩存Key生成,那麼該註解將不會起作用。
設置請求緩存,修改ArticleService方法,

@Service
public class ArticleService {

    @Autowired
    private RestTemplate restTemplate;

    @HystrixCommand
    @CacheResult
    public User getUserById(Long id){
        return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id);
    }

}

添加接口

    @GetMapping("/cache/{id}")
    public User findUserCache(@PathVariable("id") Long id){
        HystrixRequestContext.initializeContext();
        User user1  = articleService.getUserById(id);
        System.out.println("第一次請求"+user1);
        User user2 = articleService.getUserById(id);
        System.out.println("第二次請求"+user2);
        User user3 = articleService.getUserById(id);
        System.out.println("第三次請求"+user3);
        User user4 =articleService.getUserById(id);
        System.out.println("第四次請求"+user4);
       return articleService.getUserById(id);
    }

定義緩存的Key
1. 使用@CacheKey,該註解除了可以指定方法參數作爲緩存key之外,還可以指定方法參數對象的內不屬性作爲Key

 @HystrixCommand
    @CacheResult
    public User getUserById(@CacheKey("id") Long id){
        return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id);
    }
  1. 使用@CacheResult和@CacheRemove的cacheKeyMethod屬性指定Key,如果與上面的CacheKey註解一起使用,則CacheKey將失效
@HystrixCommand
    @CacheResult(cacheKeyMethod = "getCacheKey")
    public User getUserById(Long id){
        return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id);
    }

    private Long getCacheKey(Long id){
        return id;
    }

緩存清理

上面說通過繼承和註解方式都可以將請求保存到緩存,但是當我們更新了數據庫的數據,緩存的數據已經是過期數據,這時候再次請求,數據已經失效。所以我們需要更新緩存。在Hystrix中繼承和註解都可以實現清除緩存。
1. 使用繼承方式:前面介紹使用繼承是繼承HystrixCommand,然後再run方法中觸發請求操作,所以這裏創建兩個類進程HystrixCommand,一個實現查詢,一個實現更新。

public class GetUserCommand  extends HystrixCommand<User> {
    private static final Logger logger = LoggerFactory.getLogger(GetUserCommand.class);

    private static final HystrixCommandKey GETTER_KEY = HystrixCommandKey.Factory.asKey("CommandKey");
    private RestTemplate restTemplate;
    private Long id;

    public GetUserCommand(RestTemplate restTemplate, Long id) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("userGroup")));
        this.restTemplate = restTemplate;
        this.id = id;
    }
    @Override
    protected User run() throws Exception {
        logger.info(">>>>>>>>>>>>>查詢操作>>>>>>>>>>>>>>>>>>>>>>>>>>");
        return restTemplate.getForObject("http://USER-SERVICE/user/{1}", User.class, id);
    }
    @Override
    protected String getCacheKey() {
        //根據id保存緩存
        return String.valueOf(id);
    }
    /**
     * 根據id清理緩存
     * @param id
     */
    public static void flushCache(Long id){
        logger.info(" >>>>>>>>>>>>>GETTER_KEY:{}>>>>>>>>>>>>>>>>",GETTER_KEY);
        HystrixRequestCache.getInstance(GETTER_KEY,
                HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(id));
    }
}
public class PostUserCommand extends HystrixCommand<User> {
    private final Logger logger =  LoggerFactory.getLogger(UserCommand.class);
    private RestTemplate restTemplate;
    private User user;

    public PostUserCommand(RestTemplate restTemplate,User user){
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("userGroup")));
        this.restTemplate = restTemplate;
        this.user = user;
    }
    @Override
    protected User run() throws Exception {
        logger.info(">>>>>>>>>>>>>更新操作>>>>>>>>>>>>>>>>>>>>>>>>>>");
        User user1 = restTemplate.postForEntity("http://USER-SERVICE/u/update", user, User.class).getBody();
        //刷新緩存,清理失效的緩存
        GetUserCommand.flushCache(user1.getId());
        return user1;
    }
}

添加接口:

 @GetMapping("/getcommand/{id}")
    public User testGetCommand(@PathVariable("id") Long id){
        GetUserCommand u1 = new GetUserCommand(restTemplate,id);
        GetUserCommand u2 = new GetUserCommand(restTemplate,id);
        GetUserCommand u3 = new GetUserCommand(restTemplate,id);
        GetUserCommand u4 = new GetUserCommand(restTemplate,id);
        User user1 = u1.execute();
        System.out.println("第一次請求"+user1);
        User user2 = u2.execute();
        System.out.println("第二次請求"+user2);
        User user3 = u3.execute();
        System.out.println("第三次請求"+user3);
        User user4 = u4.execute();
        System.out.println("第四次請求"+user4);
        return user1;
    }

    @PostMapping("/postcommand")
    public User testPostCommand(User user){
        HystrixRequestContext.initializeContext();
        PostUserCommand u1 = new PostUserCommand(restTemplate,user);
        User execute = u1.execute();
        return execute;
    }

在上面GetUserCommand方法中添加flushCache的靜態方法,該方法通過HystrixRequestCache.getInstance(GETTER_KEY, HystrixConcurrencyStrategyDefault.getInstance());方法從默認的Hystrix併發策略中根據GETTER_KEY獲取到該命令的請求緩存對象HystrixRequestCache,然後再調用clear方法清理key爲id的緩存。
2. 使用註解方式:上面提到了@CacheRemove註解是使緩存失效

@CacheRemove(commandKey = "getUserById")
public User update(@CacheKey("id")User user){
    return  restTemplate.postForEntity("http://USER-SERVICE/u/update", user, User.class).getBody();
}

@CacheRemove的commandKey屬性是必須指定的,它用來指明需要使用請求緩存的請求命令,只有通過該屬性的配置,Hystrix才能找到正確的請求命令緩存位置。

使用請求緩存的時候需要注意的是,必須先使用 HystrixRequestContext.initializeContext();,該方法的調用可以放到攔截器中執行,這裏因爲是測試,所以直接在接口中調用。


作爲SpringCloud學習筆記,有很多地方不好。望指出!!!

源碼地址:https://gitee.com/wqh3520/spring-cloud-1-9/tree/master/

原文地址:SpringCloud容錯保護Hystrix(一)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章