SpringCloud容錯保護Hystrix(一)

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

簡單使用

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

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

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

@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服務調用失敗後調用的方法。spring

/**
     * 使用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);
    }
}

而後添加一個接口app

@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);
}

  1. 使用註解,在自定義的服務降級方法中可使用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(一)

相關文章
相關標籤/搜索