Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
----来自百度
服务注册中心 Eureka,Zookeeper,Consul
服务调用 openFeign
负载均衡 Ribbon
服务降级/服务熔断 Hystrix
服务网关 GetWay,Zuul
配置中心 Config
消息总线 Bus
消息驱动 Stream
链路跟踪 Sleuth
UTF-8
1.8
1.8
4.12
1.16.18
1.2.17
8.0.18
1.1.16
1.3.0
org.apache.maven.plugins
maven-project-info-reports-plugin
3.0.0
org.springframework.boot
spring-boot-dependencies
2.2.2.RELEASE
pom
import
org.springframework.cloud
spring-cloud-dependencies
Hoxton.SR1
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.1.0.RELEASE
pom
import
mysql
mysql-connector-java
${mysql.version}
runtime
com.alibaba
druid
${druid.version}
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.spring.boot.version}
junit
junit
${junit.version}
log4j
log4j
${log4j.version}
org.springframework.boot
spring-boot-maven-plugin
true
true
POM依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.1.0version>
dependency>
dependencies>
Payment实体类和commonResult实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
private Long id;
private String serial;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommenResult<T> {
private Integer code;
private String message;
private T data;
public CommenResult(Integer code,String message)
{
this(code,message,null);
}
}
1.建module 2.改POM 3.写YML 4.主启动 5.业务类
POM依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>CR553groupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
application.yml配置文件
server:
port: 8001
spring:
application:
name: cloud-payment-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采样率介于0到1之间,1表示全部采集
probability: 1
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: com.mysql.jdbc.Driver # mysql驱动包
url: jdbc:mysql://127.0.0.1:3306/cloud2020?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
username: root
password: root
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true.单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#defaultZone: http://localhost:7001/eureka
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka
instance:
instance-id: payment8001
prefer-ip-address: true #访问路径可以显示ip地址
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.CR553.springcloud.entities # 所有Entity别名类所在包
主启动
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}
}
Dao层
@Mapper//使用mybatis建议使用@Mapper注解
public interface PaymentDao {
int create(Payment payment);
Payment getPaymentById(@Param("id") Long id);
}
Mapper.xml
<mapper namespace="com.CR553.springcloud.dao.PaymentDao">
<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
insert into payment(serial) values(#{serial});
insert>
<resultMap id="BaseResultMap" type="com.CR553.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="BIGINT">id>
<id column="serial" property="serial" jdbcType="VARCHAR">id>
resultMap>
<select id="getPaymentById" resultMap="BaseResultMap" parameterType="Long">
select * from payment where id=#{id};
select>
mapper>
service层
public interface PaymentService {
int create(Payment payment);
Payment getPaymentById(@Param("id") Long id);
}
@Service
public class PaymentServiceImpl implements PaymentService {
@Resource
private PaymentDao paymentDao;
@Override
public int create(Payment payment) {
return paymentDao.create(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}
}
controller层
@Controller
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@Resource
private DiscoveryClient discoveryClient;
@RequestMapping(value = "/payment/create")
@ResponseBody
public CommenResult create(@RequestBody Payment payment) {
int result = paymentService.create(payment);
log.info("******插入结果" + result);
if (result > 0) {
return new CommenResult(200, "插入数据库成功,端口为:" + serverPort, result);
} else {
return new CommenResult(444, "插入数据库失败", null);
}
}
@GetMapping(value = "/payment/get/{id}")
@ResponseBody
public CommenResult getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);
log.info("******插入结果" + payment);
if (payment != null) {
return new CommenResult(200, "查询成功,端口为:" + serverPort, payment);
} else {
return new CommenResult(444, "没有改记录,查询id:" + id, null);
}
}
@GetMapping("/payment/discovery")
@ResponseBody
public Object discovery() {
List<String> services = discoveryClient.getServices();
for (String service : services) {
log.info("******* service:" + service);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return discoveryClient;
}
//测试手写轮询算法
@GetMapping(value = "/payment/lb")
@ResponseBody
public String getPaymentLB()
{
return serverPort;
}
@GetMapping(value = "/payment/feign/timeout")
@ResponseBody
public String PaymentFeignTimeout()
{
try{
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e)
{
e.printStackTrace();
}
return serverPort;
}
@GetMapping("/payment/zipkin")
@ResponseBody
public String paymentZipkin()
{
return "hi,it`s paymentzipkin server fall back,welcome to CR553";
}
}
POM依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>CR553groupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
application.yml配置类
在这之前,为了本机地址不混淆,我配置了host文件
路径为 C:\Windows\System32\drivers\etc
######SpringCloud2020 ###############
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
127.0.0.1 eureka7003.com
127.0.0.1 config3344.com
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eurake服务端的实例名称
client:
#false 表示不向注册中心注册自己
register-with-eureka: false
#false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
fetch-registry: false
service-url:
#设置与eureka Server交互的地址查询服务和注册服务都需要依赖这个地方
#defaultZone: http://eureka7002.com:7002/eureka/ #集群搭建在这里添加其他Eureka server
defaultZone: http://eureka7001.com:7001/eureka/ #单机
主启动
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class,args);
}
}
测试
先启动Eureka7001
再启动Payment8001
浏览器访问 http://eureka7001.com:7001/
测试结果如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q4KerV2c-1596214459469)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595831106478.png)]
基于Netflix Ribbon实现的一套客户端 负载均衡的工具
主要功能是提供客户端的软件负载均衡算法和服务调用。ribbon客户端组件提供一系列万三的配置项如连接超时,重试等。就是在配置文件中列出Load Banlancer后面的所有机器,Ribbon会自动的帮你基于某种规则去连接这些机器.
一句话,ribbon是负载均衡+restTemplate调用
新建Config类
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //这里表示开启了Ribbon负载均衡,默认为轮询
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
接着在controller类里面调用即可
@Resource
private RestTemplate restTemplate;
public static final String PAMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
@GetMapping(value = "/consumer/payment/create")
@ResponseBody
public CommenResult<Payment> create(Payment payment)
{
return restTemplate.postForObject(PAMENT_URL+"/payment/create"
,payment,CommenResult.class);
}
此时微服务端controller层
@RequestMapping(value = "/payment/create")
@ResponseBody
public CommenResult create(@RequestBody Payment payment) {
int result = paymentService.create(payment);
log.info("******插入结果" + result);
if (result > 0) {
return new CommenResult(200, "插入数据库成功,端口为:" + serverPort, result);
} else {
return new CommenResult(444, "插入数据库失败", null);
}
}
测试,启动eureka7001,payment8001,order80即可
@Configuration
public class MySelfRule {
@Bean
public IRule myRule()
{
return new RandomRule();//定义为随机
}
}
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name="CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
注意:
这个自定义类不能放在@CommponentScan所扫描的当钱包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户的所共享,达不到特殊化定制的目的了.
改造步骤
1.去掉@LoadBalanced注解
2.LoadBalancer接口
public interface LoadBalancer {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
3.新建MyLB类
@Component
public class MyLB implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement()
{
int current;
int next;
do{
current=atomicInteger.get();
next=current >= 2147483647? 0: current+1;
}while(!this.atomicInteger.compareAndSet(current,next));
System.out.println("********** next:"+next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
4.修改OrderController
//测试手写轮询
@Resource
private LoadBalancer loadBalancer;
@GetMapping(value = "/consumer/payment/lb")
@ResponseBody
public String getPaymentLB()
{
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT- SERVICE");
if(instances==null ||instances.size()==0)
{
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
// log.info(uri.toString());
return restTemplate.getForObject(uri.toString()+"/payment/lb",String.class);
}
5.依次打开eureka7001,payment8001,order80测试 http://localhost/consumer/payment/lb
新建项目
POM依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
YML配置文件
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka
#设置feign客户端超时时间
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
logging:
level:
com.CR553.springcloud.service.PaymentService: debug
主启动
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class,args);
}
}
service接口
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentService {
@GetMapping(value = "/payment/get/{id}")
@ResponseBody
public CommenResult<Payment> getPaymentById(@PathVariable("id") Long id);
@GetMapping(value = "/payment/feign/timeout")
@ResponseBody
public String PaymentFeignTimeout();
}
config配置类
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
controller配置类
@Controller
public class OrderFeignController {
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/payment/get/{id}")
@ResponseBody
public CommenResult<Payment> getPaymentById(@PathVariable("id") Long id)
{
return paymentService.getPaymentById(id);
}
@GetMapping(value = "/consumer/payment/feign/timeout")
@ResponseBody
public String PaymentFeignTimeout()
{
return paymentService.PaymentFeignTimeout();
}
}
主启动类
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class,args);
}
}
NONE: 默认,不显示任何日志
BASIC:仅记录请求方法,URL,响应状态码及执行时间
HEADERS:除了BASIC中定义的信息之外,还有请求和响应头信息
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
新建工程cloud-provider-hystrix-payment8001
POM依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
application.yml配置类
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-register: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
service
@Service
public class PaymentService {
public String paymentInfo_OK(Integer id)
{
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK, id: "+id+"\t";
}
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds"
,value = "5000")
})
public String paymentInfo_TimeOut(Integer id) {
int timeNumber = 3;
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e)
{
e.printStackTrace();
}
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_TimeOut, id: "+id+"\t"+"耗时(秒):"+timeNumber;
}
public String paymentInfo_TimeOutHandler(Integer id)
{
return "线程池: "+Thread.currentThread().getName()+" 服务超时或者运行错误 , id: "+id+"\t";
}
//=========服务熔断
@HystrixCommand(fallbackMethod ="paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name="circuitBreaker.enabled",value = "true"),
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "10"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value = "10000"),
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value = "60"),
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
if(id<0)
{
throw new RuntimeException("******id 不能为负数");
}
String serialNumber= IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: "+serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id)
{
return "id 不能为负数,请稍后再试 id: "+id;
}
}
controller
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
@ResponseBody
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result=paymentService.paymentInfo_OK(id);
log.info("************result: "+result);
return result;
}
@GetMapping("/payment/hystrix/timeOut/{id}")
@ResponseBody
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
String result=paymentService.paymentInfo_TimeOut(id);
log.info("************result: "+result);
return result;
}
//======服务熔断
@GetMapping("/payment/circuit/{id}")
@ResponseBody
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
String result=paymentService.paymentCircuitBreaker(id);
return result;
}
}
主启动
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
@Bean
public ServletRegistrationBean getServlet()
{
HystrixMetricsStreamServlet streamServlet=new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
测试 打开eureka7001,打开cloud-provider-hystrix-payment8001
在浏览器输入url
目前问题
解决方法,在客户端的controller类里面添加一下注释和方法
定义一个通用的服务兜底方法,其他特殊问题特殊处理
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public String payment_Global_FallbackMethod()
{
return "Global 异常处理信息,请稍后再试!";
}
新建cloud-consumer-feign-hystrix-order80
POM依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
application.yml配置文件
server:
port: 80
eureka:
client:
register-with-eureka: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
feign:
hystrix:
enabled: true
service
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
@ResponseBody
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeOut/{id}")
@ResponseBody
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "------PaymentFallbackService fall back-paymentInfo_OK!";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "------PaymentFallbackService fall back-paymentInfo_TimeOut!";
}
}
controller
@Controller
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class PaymentHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
@ResponseBody
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeOut/{id}")
@ResponseBody
//当设置特定兜底方法的时候用该注解
/* @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})*/
@HystrixCommand//当用全局兜底方法的时候使用该注解
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentInfo_TimeOutHandler(@PathVariable("id") Integer id)
{
return "我是消费者80,对方支付系统繁忙.请10秒钟后再试或者自己运行出错请检查自己!";
}
public String payment_Global_FallbackMethod()
{
return "Global 异常处理信息,请稍后再试!";
}
}
主启动类
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
测试
打开eureka7001,hystrix-payment8001,hystrix-order80
当客户端突然挂了,解决方法如下,新建一个PaymentHystrixService接口
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "------PaymentFallbackService fall back-paymentInfo_OK!";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "------PaymentFallbackService fall back-paymentInfo_TimeOut!";
}
}
新建cloud-consumer-hystrix-dashboard9001
POM依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
application.yml
server:
port: 9001
主启动
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashBoardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashBoardMain9001.class,args);
}
}
测试
输入http://localhost:9001/hystrix
进入一下页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l1YnsdXl-1596214459472)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1596012464385.png)]
多次点击http://localhost:8001/payment/hystrix/timeOut/11
查看页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5Qbzovi-1596214459473)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1596012416947.png)]
搭建cloud-gateway-gateway9527
POM依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
application.yml配置文件
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://CLOUD-PAYMENT-SERVICE #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://CLOUD-PAYMENT-SERVICE #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
- Cookie=username,CR553
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetchRegistry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
主启动类
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class,args);
}
}
filter过滤类
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("**********come in MyLogGateWayFilter: "+new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname == null)
{
log.info("**********用户名为null,非法用户");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
测试
启动eureka7001,启动payment8001和8002,再启动9527
在cmd里面
curl http://localhost:9527/payment/lb
curl http://localhost:9527/payment/lb --cookie "username=CR553"
curl http://localhost:9527/payment/lb?uname=aa
curl http://localhost:9527/payment/lb?uname=aa --cookie "username=CR553"
注意日志文件
POM依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>com.jcraftgroupId>
<artifactId>jschartifactId>
<version>0.1.50version>
dependency>
application.yml配置类
server:
port: 3344
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
uri: https://github.com/CR553/springcloud-config.git
####搜索目录
search-path:
- springcloud-config
#### 读取分支
label: master
#rabbit相关配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
#rabbitmq相关配置,暴露bus刷新配置的端点
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
主启动
@EnableConfigServer
@SpringBootApplication
public class MainAppConfigCenter3344 {
public static void main(String[] args) {
SpringApplication.run(MainAppConfigCenter3344.class,args);
}
}
测试
客户端
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>com.jcraftgroupId>
<artifactId>jschartifactId>
<version>0.1.50version>
dependency>
bootstrap.yml
server:
port: 3355
spring:
application:
name: config-client
cloud:
config:
label: master
name: config
profile: prod
uri: http://localhost:3344
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
#暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
主启动
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class,args);
}
}
controller
@Controller
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
@ResponseBody
public String getConfigInfo()
{
return configInfo;
}
}
测试
在github上创建好仓库和对应的文件
打开eureka7001,center3344,client3355,client3366
访问
http://localhost:3355/configInfo
http://localhost:3366/configInfo
修改文件,不要重启系统,手动cmd中:
curl -X POST "http://localhost:3355/actuator/refresh"
再看变化
问题:这是直接对3355发送修改更新的,3366此时并未修改,这意味着我们得手动发送n个请求,不符合我们的预期
在微服务架构中,通常回事会使用轻量级的消息代理来构建一个公用的消息主题,并让系统中所有微服务实例都连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以称他为消息总线.在总线上的各个实例都可以方便地传播一些需要让其他连接在该主题上的实例都知道的消息
ConfigClient实例都监听MQ中同一个topic.当一个服务刷新数据的时候,他会把这个信息放入到topic中,这样其他监听同一个topic的服务就能得到通知,然后去更新自身的配置.
Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统连接起来的框架,它融合了java的事件处理机制和消息中间件的功能.Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改,事件推送,也可以当作微服务间的通信通道
安装Erlang
安装RabbitMQ
进入RabbitMQ的sbin目录,输入
rabbitmq-plugins enable rabbitmq_management
访问地址查看是否安装成功
http://localhost:15672
输入账号密码并登录: guest guest
在pom文件加上起步依赖spring-cloud-starter-bus-amqp,完整的配置文件如下:
org.springframework.cloud
spring-cloud-starter-bus-amqp
org.springframework.cloud
spring-cloud-starter-config
com.jcraft
jsch
0.1.50
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
CR553
cloud-api-commons
${project.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
在配置文件bootstrap.yml中加上RabbitMq的配置,包括RabbitMq的地址、端口,用户名、密码。并需要加上spring.cloud.bus的三个配置,具体如下:
server:
port: 3366
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
spring:
application:
name: config-client
cloud:
config:
label: master
name: config
profile: prod
uri: http://localhost:3344
bus: #这里的配置没配也可以实现更新功能,具体作用以后再研究
enabled: true
trace:
enabled: true
#开启消息总线时添加的
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
controller上面加上@RefreshScope注解
@Controller
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
@ResponseBody
public String getConfigInfo()
{
return configInfo;
}
}
依次启动eureka-server、confg-server,启动两个config-client,端口为:3355、3366。
访问http://localhost:3355/configInfo 或者http://localhost:3366/configInfo浏览器显示:
version=1(举例子)
这时候去github上面将version改为2,之后发送post请求(本次用的是cmd,因为咱的浏览器不支持post)
curl -X POST "http://localhost:3344/actuator/bus-refresh"
会发现config-client会重新读取配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WPdgMit2-1596214459477)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1596116101778.png)]
这时再访问http://localhost:3355/configInfo 或者http://localhost:3366/configInfo浏览器显示:
version =2
另外,/actuator/bus-refresh接口可以指定服务,即使用"destination"参数,比如 “/actuator/bus-refresh?destination=customers:**” 即刷新服务名为customers的所有服务。
新建cloud-stream-rabbitmq-provider8801模块
POM依赖,新增spring-cloud-starter-stream-rabbit
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>CR553groupId>
<artifactId>cloud-api-commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
application.yml配置文件,配置Stream的binders,mq的环境等等
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #表示定义的名称,用于binding集合
type: rabbit #消息组件类型
environment: #设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: #服务的整合处理
output: #这个名字是一个通道的名称
destination: studyExchange #表示要使用的Exchange名称定义
content-type: application/json #设置消息类型
binder: defaultRabbit #设置要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #设置心跳的时间间隔
lease-expiration-duration-in-seconds: 5
instance-id: send-8801.com #在信息列表时显示主机名称
prefer-ip-address: true #访问的路径变为IP地址
service接口
public interface IMessageProvider {
public String send();
}
service实现类,这里注入MessageChannel
@EnableBinding(Source.class)
public class IMessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output;
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("********** serial: "+serial);
return null;
}
}
controller
@Controller
public class SendMessageController {
@Resource
private IMessageProvider iMessageProvider;
@GetMapping("/sendMessage")
@ResponseBody
public String sendMessage()
{
return iMessageProvider.send();
}
}
主启动类
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
测试,依次启动7001,rabbitMQ服务器,启动8801,访问http://localhost:8801/sendMessage 测试8801的send方法调用正常
进入RabbitMQ的sbin目录,输入
rabbitmq-plugins enable rabbitmq_management
访问地址查看是否安装成功
http://localhost:15672
输入账号密码并登录:
guest guest
新建模块cloud-stream-rabbitmq-consumer8802/8803
POM依赖,和服务端一样增加spring-cloud-starter-stream-rabbit
org.springframework.cloud
spring-cloud-starter-stream-rabbit
8002的application.yml配置文件
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #表示定义的名称,用于binding集合
type: rabbit #消息组件类型
environment: #设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: #服务的整合处理
input: #这个名字是一个通道的名称
destination: studyExchange #表示要使用的Exchange名称定义
content-type: application/json #设置消息类型
binder: defaultRabbit #设置要绑定的消息服务的具体设置
group: CR553A
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #设置心跳的时间间隔
lease-expiration-duration-in-seconds: 5
instance-id: receive-8802.com #在信息列表时显示主机名称
prefer-ip-address: true #访问的路径变为IP地址
controller,@Component注解不要忘记,新增注解@EnableBinding(Sink.class), @StreamListener(Sink.INPUT)
@Component
@EnableBinding(Sink.class)
public class ReveiceMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String>message)
{
System.out.println("消费者1号,--->接收到的消息: "+message.getPayload()+"\t port:" +serverPort);
}
}
测试,启动eureka7001,rabbitmq服务器,依次启动streamServer,streamclient,访问http://localhost:8801/sendMessage访问一次,8802打印内容,访问两次8803才会打印内容.这是因为我们给两个客户端分到了一个组里面,处于竞争关系,一次只有一个能获取信息.分组后还有一个功能,先关闭两个客户端8802,8803,更改8802的配置文件,注释掉
#group: CR553A
多次访问http://localhost:8801/sendMessage,先启动8802客户端,观察日志,没有打印,再启动8803客户端,观察日志,有信息出现.
改造cloud-provider-payment8001
POM依赖,新增spring-cloud-starter-zipkin
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
application.yml配置文件,在spring下面配置zipkin的url,采样率
spring:
application:
name: cloud-payment-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
#采样率介于0到1之间,1表示全部采集
probability: 1
controller,常规添加方法
@GetMapping("/payment/zipkin")
@ResponseBody
public String paymentZipkin()
{
return "hi,it`s paymentzipkin server fall back,welcome to CR553";
}
改造cloud-consumer-order80
POM,application同服务端,controller如下
//+++++++++++zipkin + sleuth
@GetMapping("/consumer/payment/zipkin")
@ResponseBody
public String paymentZipkin()
{
return restTemplate.getForObject("http://localhost:8001/payment/zipkin/",String.class);
}
先启动zipkin,安装目录cmd直接java -jar 执行即可,依次启动eureka7001,payment8001,order80,访问localhost:9411可以看到下面页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DrbjuFcZ-1596214459479)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1596169170726.png)]
Nacos下载
Nacos依赖于Java环境,所以必须安装Java环境。然后从官网下载Nacos的解压包,安装稳定版的,下载地址:https://github.com/alibaba/nacos/releases
本次案例下载的版本为0.09 ,下载完成后,解压,在解压后的文件的/bin目录下,windows系统点击startup.cmd就可以启动nacos。linux或mac执行以下命令启动nacos。
sh startup.sh -m standalone
启动时会在控制台,打印相关的日志。nacos的启动端口为8848,在启动时要保证端口不被占用。
启动成功,在浏览器上访问:http://localhost:8848/nacos,会跳转到登陆界面,默认的登陆用户名为nacos,密码也为nacos。
新建cloudalibaba-provider-payment9001
POM依赖,添加spring-cloud-starter-alibaba-nacos-discovery
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
application.yml配置,配置nacos地址
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置nacaos地址
management:
endpoints:
web:
exposure:
include: '*'
controller
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverport;
@GetMapping("/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id)
{
return "nacos registry,serverPort: "+serverport+"\t id: "+id;
}
}
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class,args);
}
}
新建cloudalibaba-consumer-nacos-order83
POM依赖,同上
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
application.yml,配置nacos-user-service
server:
port: 83
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
application:
name: nacos-order-consumer
#消费者将要去访问的微服务名称
service-url:
nacos-user-service: http://nacos-payment-provider
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain83 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain83.class,args);
}
}
controller
@RestController
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id)
{
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}
测试,启动nacos服务器,payment9001,再启动order83.访问http://localhost:83/consumer/payment/nacos/11
返回 nacos registry,serverPort: 9001 id: 11
新建cloud-alibaba-config-nacos-clent3377
POM依赖,新增spring-cloud-starter-alibaba-nacos-config
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
application.yml配置
spring:
profiles:
active: dev #表示开发环境
#active: test #表示测试环境
#active: info #表示测试环境
bootstrap.yml配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yaml #指定yaml格式的配置
group: DEV_GROUP
namespace: 6608bb77-fa9b-42e3-8bac-e7b9063622f5
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
#nacos-config-client-dev.yaml
启动类
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClienMain3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClienMain3377.class,args);
}
}
controller,注意@RefreshScope
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo()
{
return configInfo;
}
}
nacos 配置文件的起名规范
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
#nacos-config-client-dev.yaml
测试
启动 nacos server启动pament9001,order83,config3377,输入http://localhost:3377/config/info将配置文件中info的version更改,再次访问http://localhost:3377/config/info查看结果改变了.
问题
实际开发中,通常一个系统会准备dev开发环境,test测试环境,prod生产环境,如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境,测试环境,预发环境,正式环境,那么怎么对这些为服务配置进行管理呢?
默认情况
Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT
Nacos默认的命名空间是public,Namespace主要用来实现隔离,比方说三个环境,开发,测试,生产.
Group默认是DEFAULT_GROUP,GROUP可以把不同的微服务分到同一个分组里面去
Service就是微服务;一个Service可以包含多个Cluster(集群),Cluster是对指定微服务的一个虚拟划分,比方说为了容灾,将service微服务分别部署在了两个机房.
新建
POM依赖
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
application.yml配置文件配置sentinel
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
#Nacos服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直到找到未被占用的端口
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
including: '*'
controller层FlowLimitController,一般方法,主要测试正常返回,有延时返回,有报错返回,这里只用testA和testB就行
@RestController
public class FlowLimitController {
@GetMapping("/testA")
@ResponseBody
public String testA()
{
/* try{
TimeUnit.MILLISECONDS.sleep(1000);
}
catch (InterruptedException e){
e.printStackTrace();
}*/
return "_____testA";
}
@GetMapping("/testB")
@ResponseBody
public String testB()
{
System.out.println("testBBBBBBBBBBBBBBBBBB");
return "________testB";
}
@GetMapping("/testD")
@ResponseBody
public String testD()
{
/* try{
TimeUnit.MILLISECONDS.sleep(1000);
}
catch (InterruptedException e){
e.printStackTrace();
}*/
int a=10/0;
return "_____testD";
}
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotkey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2)
{
return "***************testHotKey";
}
public String deal_testHotkey(String p1, String p2, BlockException exception)
{
return "***************兜底";
}
}
RateLimitController
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "handleException")
public CommenResult byResource()
{
return new CommenResult(200,"按资源名称限流测试OK",new Payment(2020L,"serial001"));
}
public CommenResult handleException(BlockException exception)
{
return new CommenResult(444,exception.getClass().getCanonicalName()+"\t 服务不可用");
}
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommenResult byUrl()
{
return new CommenResult(200,"按url限流测试OK",new Payment(2020L,"serial002"));
}
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommenResult customerBlockHandler()
{
return new CommenResult(200,"按客户自定义",new Payment(2020L,"serial003"));
}
}
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class,args);
}
}
测试,启动nacos8848,启动sentinel8080,启动微服务8401.进入localhost:8080,登录,用户名和密码为sentinel
多次访问
localhost:8401/testA
localhost:8401/testB
刷新:8080,就可以查看到调用记录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iMaceVoY-1596214459481)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595393355616.png)]
资源名:唯一名称,默认请求路径
针对来源:sentinel可以针对调用者进行限流,填写微服务名,默认default
阈值类型/单机阈值:
QPS(每秒钟的请求数量): 当调用该api的QPS达到阈值的时候进行限流
线程数:当调用该api的线程数达到阈值的时候,进行限流
流控模式:
直接:api达到限流条件时,直接限流
关联:当关联的资源达到阈值时,就限流自己
链路:只记录指定链路上的流量,
流控效果:
流控模式
直接,QPS偏向外部,而线程数偏向内部
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eb9VfbUi-1596214459482)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595393806705.png)]
当访问次数超过阈值立刻进行流量控制,问题,如何自定义fallback兜底?
关联,当关联的资源达到阈值时,就限流自己,当与A关联的资源B达到阈值后,就限流A自己.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kfrDbe5G-1596214459483)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595406033587.png)]
流控效果
直接,预热
Warm Up方式,即预热冷启动方式,当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮.通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热时间,避免冷系统被压垮.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-khUKIszg-1596214459484)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595406622542.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yVyjxgHG-1596214459485)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595406756922.png)]
排队等待
匀速排队让请求以匀速的速度通过,阈值类型必须设置成QPS,否则无效.
设置含义:/testA每秒钟1次请求,超过的话就排队等待,等待的超时时间为20000毫秒
匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以匀速的速度通过,对应的是漏桶算法.这种方式主要用于处理间隔性突发的流量,例如消息队列,想想一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ml7gSq8q-1596214459486)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595407176015.png)]
平均响应时间超出阈值且在时间窗口内通过的请求>=5,两个条件同时满足后触发降级
窗口期过后关闭断路器,RT最大4900
QPS>=5且异常比例超过阈值时,触发降级;时间窗口结束后,关闭降级
异常数(分钟级)
异常数超过阈值,厨房降级,时间窗口结束后,关闭降级
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或者异常比例升高),对这个资源的调用进行限制会让请求快速失败,避免影响到其它的资源而导致级联错误.当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都会自动熔断,Sentinel的断路器是没有半开状态的
RT
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VimC93D7-1596214459487)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595408261991.png)]
@GetMapping("/testD")
@ResponseBody
public String testD()
{
try{
TimeUnit.MILLISECONDS.sleep(1000);
}
catch (InterruptedException e){
e.printStackTrace();
}
return "_____testD";
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-apN8W1K8-1596214459488)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595408285468.png)]
效果: 压测时手动访问访问不了,停止压测后立马恢复
异常比例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Xla2GXG-1596214459490)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595409197147.png)]
@GetMapping("/testD")
@ResponseBody
public String testD()
{
/* try{
TimeUnit.MILLISECONDS.sleep(1000);
}
catch (InterruptedException e){
e.printStackTrace();
}*/
int a=10/0;
return "_____testD";
}
效果,单独访问一次,必然调用一次报错一次;开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了,断路器开启(保险丝跳闸),微服务不可用了,不再报错error而是服务降级了.
异常数
当资源近1分钟的异常数且超过阈值之后会进行熔断,注意由于统计时间窗口是分钟级别的,若timewindow小于60s,则结束熔断状态后仍可能在进入熔断状态.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Q868SAm-1596214459491)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595409508065.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DOMiOGxR-1596214459492)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595427508698.png)]
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotkey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2)
{
return "***************testHotKey";
}
public String deal_testHotkey(String p1, String p2, BlockException exception)
{
return "***************兜底";
}
访问
http://localhost:8401/testHotKey?p2=2
http://localhost:8401/testHotKey?p1=2&&p2=4
http://localhost:8401/testHotKey?p1=2
参数例外项
设置例外的限流目标
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vytd6Lq-1596214459493)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595428208510.png)]
@SentinelResource处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理.而RuntimeException .int age=10/0,这个是java运行时报出的运行时异常RunTimeException.
总结@SentinelResource主管配置出错,运行出错走异常
各项参数配置说明
系统保护规则是从应用级别的入口流量进行控制,从单台机器的load,cpu使用率,平均RT,入口QPS,和并发线程数等几个维度监控应用指标,让系统尽可能泡在最大吞吐量的同时保证系统整体的稳定性.
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效,入口流量指的是进入应用的流量.
系统规则支持一下的模式:
Load自适应(仅对Linux/Unix-like机器生效):系统的load1作为启发指标,进行自适应系统保护.当系统load超过设定的启发值,且系统当前的并发线程数超过估值的系统容量时才会触发系统保护(BBR阶段).系统容量由系统的maxQPS*minRt估算得出,设定参考值一般是cpu cores * 2.5.
Cpu usage:当系统cpu使用率超过阈值即触发系统保护(0-1),比较灵敏.
平均RT: 当单台机器上所有入口流量的平均RT达到阈值即触发系统保护.
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护
入口QPS:当单台机器所有入口流量QPS达到阈值即触发系统保护
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJ4W7Sd7-1596214459494)(C:\Users\CR553\AppData\Roaming\Typora\typora-user-images\1595470103412.png)]
若超过阈值则所有微服务失效
按资源名称,URL地址依次测试出现的问题:
客户自定义限流处理逻辑
创建CustomerBlockHandler类用于自定义限流处理逻辑,再定义限流处理类
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommenResult customerBlockHandler()
{
return new CommenResult(200,"按客户自定义",new Payment(2020L,"serial003"));
}
public class CustomerBlockHandler {
public static CommenResult handlerException(BlockException exception)
{
return new CommenResult(4444,"按客户自定义,global handlerException-----1");
}
public static CommenResult handlerException2(BlockException exception)
{
return new CommenResult(4444,"按客户自定义,global handlerException-----2");
}
}
下载地址:
https://github.com/seata/seata/releases
修改conf目录下的file.conf配置文件,修改的主要内容为自定义事务组名称+事务日志存储模式为db+数据库连接信息
数据库建库seata
在seata库里建表
测试,先启动Nacos端口8848,再启动seata-server
service {
#vgroup->rgroup
vgroup_mapping.my_test_tx_group = "fsp_tx_group"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
## transaction log store
store {
## store mode: file、db
mode = "db"
## file store
file {
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC"
user = "root"
password = "root"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
建库SQL
-- the table to store GlobalSession data
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
);
-- the table to store BranchSession data
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT ,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256) ,
`lock_key` VARCHAR(128) ,
`branch_type` VARCHAR(8) ,
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
);
-- the table to store lock data
DROP TABLE IF EXISTS `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
Seata事务实例–模拟网上支付
业务逻辑: 当用户下单时,会在订单服务汇总创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,在通过
新建
数据库搭建,建表语句
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
###
CREATE TABLE t_order(
`id` BIGINT(11)NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT'用户id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`count`INT(11) DEFAULT NULL COMMENT'数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT'金额',
`status` INT(1) DEFAULT NULL COMMENT'订单状态: 0:创建中; 1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
###
CREATE TABLE t_storage(
`id` BIGINT(11)NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT'产品id',
`total`INT(11) DEFAULT NULL COMMENT'总库存',
`used`INT(11) DEFAULT NULL COMMENT'已用库存',
`residue` INT(11) DEFAULT NULL COMMENT'剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
###
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT'用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT'总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT'已用余额',
`residue` DECIMAL(10,0) DEFAULT'0' COMMENT'剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
回滚日志表
新建项目
seata-order-service2001
seata-storage-service2002
seata-account-service2003
POM依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
io.seata
seata-all
io.seata
seata-all
0.9.0
org.springframework.cloud
spring-cloud-starter-openfeign
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-jdbc
org.mybatis.spring.boot
mybatis-spring-boot-starter
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
CR553
cloud-api-commons
${project.version}
YML
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3366/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapper-locations: classpath:mapper/*.xml
domain
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code,String message)
{
this(code,message,null);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal moeny;
private Integer status; //订单状态
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long userId;
private BigDecimal total;
private BigDecimal used;
private BigDecimal residue;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Long id;
private Long productId;
private Integer total;//总库存
private Integer used; //已用库存
private Integer residue;//剩余库存
}
config
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource()
{
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource)
{
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception
{
SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
@Configuration
@MapperScan({"com.CR553.springcloud.dao"})
public class MyBatisConfig {
}
controller
@RestController
public class StorageController {
@Resource
private StorageService storageService;
@RequestMapping("/storage/decrease")
@ResponseBody
public CommenResult decrease(Long productId,Integer count)
{
storageService.decrease(productId,count);
return new CommenResult(200,"扣减库存成功");
}
}
@RestController
public class AccountController {
@Resource
private AccountService accountService;
@RequestMapping("/account/decrease")
@ResponseBody
public CommenResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money)
{
accountService.decrease(userId,money);
return new CommenResult(200,"扣减账户余额成功!");
}
}
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
@ResponseBody
public CommenResult create(Order order)
{
orderService.create(order);
return new CommenResult(200,"订单创建成功");
}
}
测试
先设置数据库中库存和账户的值
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
账户支付延时与否为变量
esult(Integer code,String message)
{
this(code,message,null);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal moeny;
private Integer status; //订单状态
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long userId;
private BigDecimal total;
private BigDecimal used;
private BigDecimal residue;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Long id;
private Long productId;
private Integer total;//总库存
private Integer used; //已用库存
private Integer residue;//剩余库存
}
config
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource()
{
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource)
{
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception
{
SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
@Configuration
@MapperScan({“com.CR553.springcloud.dao”})
public class MyBatisConfig {
}
controller
@RestController
public class StorageController {
@Resource
private StorageService storageService;
@RequestMapping("/storage/decrease")
@ResponseBody
public CommenResult decrease(Long productId,Integer count)
{
storageService.decrease(productId,count);
return new CommenResult(200,"扣减库存成功");
}
}
@RestController
public class AccountController {
@Resource
private AccountService accountService;
@RequestMapping("/account/decrease")
@ResponseBody
public CommenResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money)
{
accountService.decrease(userId,money);
return new CommenResult(200,"扣减账户余额成功!");
}
}
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
@ResponseBody
public CommenResult create(Order order)
{
orderService.create(order);
return new CommenResult(200,"订单创建成功");
}
}
测试
先设置数据库中库存和账户的值
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
账户支付延时与否为变量
全局事务添加