由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
Spring Cloud Hystrix在Hystrix(由Netflix开发的开源软件)的基础上进行封装,提供了服务熔断,服务降级,线程隔离等功能,通过这些功能可以提供服务的容错率。
Hystrix组件,实现了断路器模式。较底层的服务如果出现故障,会导致连锁故障。当对特定的服务的调用的不可用达到一个阀值(Hystric 是5秒20次) 断路器将会被打开。断路打开后,可用避免连锁故障,fallback方法可以直接返回一个固定值。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
@EnableEurekaServer
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
server:
port: 8080
spring:
application:
name: Eureka-Server
security:
user:
name: admin
password: admin123
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
serviceUrl:
defaultZone: http://admin:admin123@localhost:8080/eureka/
Security安全配置,放行
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 方案一
http.csrf().disable(); // 关闭csrf
http.authorizeRequests().anyRequest().authenticated().and().httpBasic(); // 开启认证
// 方案二
// http.csrf().ignoringAntMatchers("/eureka/**");
// super.configure(http);
}
}
server:
port: ${PORT:8081}
spring:
application:
name: Server-Produce
eureka:
instance:
hostname: localhost
client:
register-with-eureka: true
fetch-registry: true
serviceUrl:
defaultZone: http://admin:admin123@localhost:8080/eureka/
@RestController
public class TestController {
@GetMapping("/test")
public String hello() {
return "hello world";
}
}
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
server:
port: 8083
spring:
application:
name: Server-Consumer
eureka:
client:
serviceUrl:
defaultZone: http://admin:admin123@localhost:8080/eureka/
@SpringCloudApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Service
public class TestService {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private RestTemplate restTemplate;
@HystrixCommand
public String test() {
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
public String testError() {
return "服务暂时不可用,请稍后再试";
}
}
@RestController
public class TestController {
@Autowired
private TestService testService;
@RequestMapping("test/{id}")
public String test(){
return testService.test();
}
}
添加@EnableHystrix或者@EnableCircuitBreaker注解。这两个注解是等价的
@EnableDiscoveryClient
@SpringBootApplication
@EnableCircuitBreaker
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
@EnableHystrix源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableCircuitBreaker
public @interface EnableHystrix {
}
@EnableCircuitBreaker、@EnableDiscoveryClient和@SpringBootApplication三个注解的组合可以使用@SpringCloudApplication来代替
@SpringCloudApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
@EnableHystrix注解源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
@HystrixCommand注解还包含许多别的属性功能
如果方法抛出异常,使用@HystrixCommand注解指定服务降级方法
@Service
public class TestService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "testError")
public String test() {
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
public String testError() {
return "服务暂时不可用,请稍后再试";
}
}
使用@HystrixCommand注解标注的方法,除了HystrixBadRequestException异常外,其他异常都会触发服务降级。如果想指定某个异常不触发服务降级,可以使用@HystrixCommand注解的ignoreExceptions属性进行忽略。
@Service
public class TestService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "testError",ignoreExceptions = {ArrayIndexOutOfBoundsException.class})
public String test() {
String[] strArray=new String[]{"1","2"};
System.out.println(strArray[2]);
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
public String testError() {
return "服务暂时不可用,请稍后再试";
}
}
java.lang.ArrayIndexOutOfBoundsException: 2
at com.example.demo.service.TestService.test(TestService.java:21) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_201]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_201]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_201]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_201]
对于方法抛出的异常信息,可以在服务降级的方法中使用Throwable对象获取
@Service
public class TestService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "testError")
public String test(){
String[] strArray=new String[]{"1","2"};
try {
System.out.println(strArray[2]);
} catch (Exception e) {
throw new ArrayIndexOutOfBoundsException("数组越界异常");
}
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
public String testError(Throwable e) {
System.out.println(e.toString());
return "服务暂时不可用,请稍后再试";
}
}
java.lang.ArrayIndexOutOfBoundsException: 数组越界异常
指定@HystrixCommand注解的commandKey、groupKey以及threadPoolKey属性可以设置命令名称、分组以及线程池划分
@Service
public class TestService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "testError", commandKey = "test", groupKey = "testGroup", threadPoolKey = "testThread")
public String test() {
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
public String testError(Throwable e) {
return "服务暂时不可用,请稍后再试";
}
}
通过设置命令组,Hystrix会根据组来组织和统计命令的告警、仪表盘等信息。默认情况下,Hystrix命令通过组名来划分线程池,即组名相同的命令放到同一个线程池里,如果通过threadPoolKey设置了线程池名称,则按照线程池名称划分。
INFO 23288 --- [ix-testThread-2] cn.ybzy.demo.service.TestService : 执行test()....
INFO 23288 --- [rix-testGroup-1] cn.ybzy.demo.service.TestService : 执行test()....
多次调用同一个方法
@RestController
public class TestController {
@Autowired
private TestService testService;
@RequestMapping("test")
public String test() {
for (int i = 0; i < 5; i++) {
testService.test();
}
return testService.test();
}
}
INFO 23288 --- [ix-testThread-9] cn.ybzy.demo.service.TestService : 执行test()....
INFO 23288 --- [x-testThread-10] cn.ybzy.demo.service.TestService : 执行test()....
INFO 23288 --- [x-testThread-10] cn.ybzy.demo.service.TestService : 执行test()....
INFO 23288 --- [x-testThread-10] cn.ybzy.demo.service.TestService : 执行test()....
INFO 23288 --- [x-testThread-10] cn.ybzy.demo.service.TestService : 执行test()....
INFO 23288 --- [x-testThread-10] cn.ybzy.demo.service.TestService : 执行test()....
开启缓存可以让方法只被调用一次,其余调用直接从缓存里获取。
使用@CacheResult注解即可在Hystrix中开启缓存
Hystrix会将返回的对象进行缓存,缓存的key默认为方法的所有参数,这里只有一个id参数,所以缓存的key为用户id。
@Service
public class TestService {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private RestTemplate restTemplate;
@CacheResult
@HystrixCommand(fallbackMethod = "testError", commandKey = "test", groupKey = "testGroup", threadPoolKey = "testThread")
public String test(Long id) {
log.info("执行test({})....",id);
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
public String testError(Long id,Throwable e) {
log.info("请求参数ID值:{}",id);
return "服务暂时不可用,请稍后再试";
}
}
请求缓存不可用。需要初始化 HystrixRequestContext
java.lang.IllegalStateException: Request caching is not available. Maybe you need to initialize the HystrixRequestContext?
at com.netflix.hystrix.HystrixRequestCache.get(HystrixRequestCache.java:104) ~[hystrix-core-1.5.18.jar:1.5.18]
at com.netflix.hystrix.AbstractCommand$7.call(AbstractCommand.java:478) ~[hystrix-core-1.5.18.jar:1.5.18]
at com.netflix.hystrix.AbstractCommand$7.call(AbstractCommand.java:454) ~[hystrix-core-1.5.18.jar:1.5.18]
at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46) ~[rxjava-1.3.8.jar:1.3.8]
at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35) ~[rxjava-1.3.8.jar:1.3.8]
@Component
@WebFilter(filterName = "hystrixRequestContextServletFilter", urlPatterns = "/*", asyncSupported = true)
public class HystrixRequestContextServletFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
filterChain.doFilter(servletRequest, servletResponse);
context.close();
}
@Override
public void destroy() {
}
}
INFO 22740 --- [ix-testThread-1] cn.ybzy.demo.service.TestService : 执行test(1)....
断点发现每次请求都需要为HystrixRequestContext进行初始化,在同一次请求中的第一次调用后会对结果进行缓存,缓存的生命周期在当前请求之内!
设定key值
可以明确的指定缓存的key值
指定key的值有两种方式
通过@CacheKey注解指定
@CacheResult
@HystrixCommand(fallbackMethod = "testError", commandKey = "test", groupKey = "testGroup", threadPoolKey = "testThread")
public String test(@CacheKey("id") Long id) {
log.info("执行test({})....",id);
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
指定参数对象内部属性为key值
@CacheResult
@HystrixCommand(fallbackMethod = "testError", commandKey = "test", groupKey = "testGroup", threadPoolKey = "testThread")
public String test(@CacheKey("id") User user) {
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
通过方法来指定,方法的返回值必须是String类型
public String getCacheKey(Long id) {
return String.valueOf(id);
}
@CacheResult(cacheKeyMethod = "getCacheKey")
@HystrixCommand(fallbackMethod = "testError", commandKey = "test", groupKey = "testGroup", threadPoolKey = "testThread")
public String test(Long id) {
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
@CacheRemove的commandKey属性和getUser里定义的一致
@CacheResult
@HystrixCommand(fallbackMethod = "testError", commandKey = "updateUser", groupKey = "testGroup", threadPoolKey = "testThread")
public String test(@CacheKey("id") Long id) {
return restTemplate.getForObject("http://Server-Produce/test/", String.class);
}
@CacheRemove(commandKey = "updateUser")
@HystrixCommand
public void updateUser(@CacheKey("id") User user) {
this.restTemplate.put("http://Server-Produce/updateUser/", user);
}
请求合并就是将多个单个请求合并成一个请求,去调用服务提供者,从而降低服务提供者负载的,一种应对高并发的解决办法。
Hystrix中提供了一个@HystrixCollapser注解,该注解可以将处于一个很短的时间段(默认10 毫秒)内对同一依赖服务的多个请求进行整合并以批量方式发起请求。
服务提供者
@GetMapping("testRequestMerge/{id}")
public User get(@PathVariable long id) {
return new User(Long.valueOf(id), "user" + id, "123456");
}
@GetMapping("testRequestMerge")
public List<User> testRequestMerge(String ids) {
log.info("批量获取用户信息");
List<User> list = new ArrayList<>();
for (String id : ids.split(",")) {
list.add(new User(Long.valueOf(id), "user" + id, "123456"));
}
return list;
}
服务消费者
@HystrixCollapser(batchMethod = "findUserBatch", scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,collapserProperties = {
@HystrixProperty(name = "maxRequestsInBatch",value = "5"),
@HystrixProperty(name = "timerDelayInMilliseconds", value = "100")
})
public Future<User> findUser(Long id) {
log.info("获取单个用户信息");
return new AsyncResult<User>() {
@Override
public User invoke() {
return restTemplate.getForObject("http://Server-Produce/testRequestMerge/{id}", User.class, id);
}
};
}
@HystrixCommand
public List<User> findUserBatch(List<Long> ids) {
log.info("批量获取用户信息,ids: " + ids);
User[] users = restTemplate.getForObject("http://Server-Produce/testRequestMerge?ids={1}", User[].class, StringUtils.join(ids, ","));
return Arrays.asList(users);
}
batchMethod属性指定批量处理的方法
timerDelayInMilliseconds: 设置多长时间之内算一次批处理,默认为10ms
maxRequestsInBatch 设置批量处理的最大请求数量,默认值为Integer.MAX_VALUE
测试方法
@GetMapping("testRequestMerge")
public void testRequerstMerge() throws ExecutionException, InterruptedException {
Future<User> f1 = testService.findUser(1L);
Future<User> f2 = testService.findUser(2L);
Future<User> f3 = testService.findUser(3L);
f1.get();
f2.get();
f3.get();
Thread.sleep(200);
Future<User> f4 = testService.findUser(4L);
f4.get();
}
测试方法中对findUser方法进行了4次的调用,最后一次调用(f4)之前先让线程等待200毫秒(大于timerDelayInMilliseconds中定义的100毫秒),所以预期是前三次调用会被合并,而最后一次调用不会被合并进去。
INFO 23312 --- [-TestService-10] com.example.demo.service.TestService : 批量获取用户信息,ids: [1, 3, 2]
INFO 23312 --- [-TestService-10] com.example.demo.service.TestService : 批量获取用户信息,ids: [4]
控制台并没有打印出findUser方法中的获取单个用户信息的日志,实际上findUser方法并不会被调用,所以上面的代码可以简化为:
@HystrixCollapser(batchMethod = "findUserBatch", scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,collapserProperties = {
@HystrixProperty(name = "maxRequestsInBatch",value = "5"),
@HystrixProperty(name = "timerDelayInMilliseconds", value = "100")
})
public Future<User> findUser(Long id) {
return null;
}
@HystrixCommand
public List<User> findUserBatch(List<Long> ids) {
log.info("批量获取用户信息,ids: " + ids);
User[] users = restTemplate.getForObject("http://Server-Produce/testRequestMerge?ids={1}", User[].class, StringUtils.join(ids, ","));
return Arrays.asList(users);
}
在方法上加上@HystrixCommand注解。该注解对该方法创建了熔断器的功能,并指定了fallbackMethod熔断方法,熔断方法直接返回了一个字符串。
@Service
public class HelloService {
@Autowired
private RestTemplate restTemplate;
//服务id
String serviceId = "eureka-client1";
@HystrixCommand(fallbackMethod = "error")
public String test(String name) {
String forObject = restTemplate.getForObject("http://" + serviceId + "/test", String.class);
System.out.println(forObject);
return forObject ;
}
public String error(String name) {
return "调用:"+serviceId +"服务失败!";
}
}
Feign是自带断路器的,需要在配置文件中配置打开
feign:
hystrix:
enabled: true
在接口的注解中加上fallback的指定类
@FeignClient(value = "service-hi",fallback=TestControllerError .class)
@Component
@FeignClient(value = "eureka-client1") //指定远程调用的服务名
public interface TestController {
//远程调用test接口
@GetMapping("/test")//用GetMapping标识远程调用的http的方法类型
public String test();
}
TestControllerError需要实现TestController接口,并注入到Ioc容器中
public class TestControllerError implements TestController{
public String test() {
return "sorry";
}
}
Hystrix提供了Hystrix Dashboard来实时监控Hystrix的运行情况,通过Hystrix Dashboard反馈的实时信息,可以帮助快速发现系统中存在的问题,从而及时地采取应对措施。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
spring:
application:
name: Hystrix-Dashboard
server:
port: 9000
在入口类上加入注解@EnableHystrixDashboard来启用Hystrix Dashboard的功能。
@SpringBootApplication
@EnableHystrixDashboard
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}