- 分布式开发
- 服务发现
- 配置中心
- 消息总线
- 负载均衡
- 声明式 服务调用
- 断路器
- 数据监控
- 分布式 事务
服务治理 和 服务发现 ——Eureka
注册中心
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>
配置
@SpringBootApplication
@EnableEurekaServer
public class Chapter17ServerApplication {}
# Spring项目名称
spring.application.name=server
# 服务器端口
server.port=7001
# Eureka注册服务器名称
eureka.instance.hostname=localhost
# 是否注册给服务中心:默认会自动查找 服务治理中心
eureka.client.register-with-eureka=false
# 是否检索服务:注册中心是 维护服务实例的,不需要这个功能
eureka.client.fetch-registry=false
# 治理客户端服务域:服务中心的域,提供给别的微服务注册
eureka.client.serviceUrl.defaultZone=http://localhost:7001/eureka/
服务发现
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置
//@EnableDiscoveryClient 新版本已经不需要这个注解了
#服务器端口
server.port=9001
#Spring服务名称
spring.application.name=product
#治理客户端服务域
eureka.client.serviceUrl.defaultZone=http://localhost:7001/eureka/
management.endpoints.web.exposure.include=health,info,hystrix.stream
紧急!eureka可能是错误地声称实例已经启动,而实际上它们还没有启动。续费低于阈值,因此为了安全起见,实例不会过期。
15min中 低于85%
配置多个服务治理中心节点
## 微服务名称依旧保持不变
spring.application.name=server
#server.port=7002
eureka.instance.hostname=localhost
## 将7002端口服务治理中心,注册给7001端口服务治理中心
eureka.client.serviceUrl.defaultZone=http://localhost:7001/eureka/
## 微服务名称依旧保持不变
spring.application.name=server
#server.port=7001
eureka.instance.hostname=localhost
## 将7002端口服务治理中心,注册给7001端口服务治理中心
eureka.client.serviceUrl.defaultZone=http://localhost:7002/eureka/
#治理客户端服务域
eureka.client.serviceUrl.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/
微服务之间的调用
Ribbon
- 一个 RestTemplate
- @LoadBalance 提供负载均衡算法
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
服务提供者
@RestController
public class UserController {
// 日志
private Logger log = Logger.getLogger(this.getClass());
// 服务发现客户端
@Autowired
private DiscoveryClient discoveryClient = null;
// 获取用户信息
@GetMapping("/user/{id}")
public UserPo getUserPo(@PathVariable("id") Long id) {
ServiceInstance service = discoveryClient.getInstances("USER").get(0);
log.info("【" + service.getServiceId() + "】:" + service.getHost() + ":" + service.getPort());
UserPo user = new UserPo();
user.setId(id);
int level = (int) (id % 3 + 1);
user.setLevel(level);
user.setUserName("user_name_" + id);
user.setNote("note_" + id);
return user;
}
// 新增用户,POST请求,且以请求体(body)形式传递
@PostMapping("/insert")
public Map<String, Object> insertUser(@RequestBody UserPo user) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
map.put("message", "插入用户信息【" +user.getUserName() + "】成功");
return map;
}
// 修改用户名,POST请求,其中用户编号使用请求头的形式传递
@PostMapping("/update/{userName}")
public Map<String, Object> updateUsername(
@PathVariable("userName") String userName,
@RequestHeader("id") Long id) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
map.put("message", "更新用户【" +id +"】名称【" +userName + "】成功");
return map;
}
@GetMapping("/timeout")
public String timeout() {
// 生成一个3000之内的随机数
long ms = (long)(3000L*Math.random());
try {
// 程序延迟,有一定的概率超过2000毫秒
Thread.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "熔断测试";
}
}
服务调用者
//自定义扫描包
@ComponentScan(basePackages = "com.springboot.chapter17.product")
@SpringCloudApplication
public class Chapter17ProductApplication {
// 初始化RestTemplate
@LoadBalanced // 多节点负载均衡
@Bean(name = "restTemplate")
public RestTemplate initRestTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Chapter17ProductApplication.class, args);
}
}
@RestController
@RequestMapping("/product")
public class ProductController {
// 注入RestTemplate
@Autowired
private RestTemplate restTemplate = null;
@GetMapping("/ribbon")
public UserPo testRibbon() {
UserPo user = null;
// 循环10次,然后可以看到各个用户微服务后台的日志打印
for (int i = 0; i < 10; i++) {
// 注意这里直接使用了USER这个服务ID,代表用户微服务系统
// 该ID通过属性spring.application.name来指定
user = restTemplate.getForObject("http://USER/user/" + (i + 1), UserPo.class);
}
return user;
}
}
Feign 声明式 调用
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
配置feign
@EnableFeignClients(basePackages = "com.springboot.chapter17.product")
使用feign
// 测试
@GetMapping("/feign")
public UserPo testFeign() {
UserPo user = null;
// 循环10次
for (int i = 0; i < 10; i++) {
Long id = (long) (i + 1);
user = userService.getUser(id);
}
return user;
}
@GetMapping("/feign2")
public Map<String, Object> testFeign2() {
Map<String, Object> result = null;
UserPo user = null;
for (int i = 1; i <= 10; i++) {
Long id = (long) i;
user = new UserPo();
user.setId(id);
int level = i % 3 + 1;
user.setUserName("user_name_" + id);
//user.setLevel(level);
user.setNote("note_" + i);
result = userService.addUser(user);
}
return result;
}
@GetMapping("/feign3")
public Map<String, Object> testFeign3() {
Map<String, Object> result = null;
for (int i = 0; i < 10; i++) {
Long id = (long) (i + 1);
String userName = "user_name_" + id;
result = userService.updateName(userName, id);
}
return result;
}
@FeignClient("user") //user为 service Id
public interface UserService {
// 指定通过HTTP的GET方法请求路径
@GetMapping("/user/{id}")
// 这里会采用Spring MVC的注解配置
public UserPo getUser(@PathVariable("id") Long id);
// POST方法请求用户微服务
@PostMapping("/insert")
public Map<String, Object> addUser(
// 请求体参数
@RequestBody UserPo user);
// POST方法请求用户微服务
@PostMapping("/update/{userName}")
public Map<String, Object> updateName(
// URL参数
@PathVariable("userName") String userName,
// 请求头参数
@RequestHeader("id") Long id);
// 调用用户微服务的timeout请求
@GetMapping("/timeout")
public String testTimeout();
}
// 获取用户信息
@GetMapping("/user/{id}")
public UserPo getUserPo(@PathVariable("id") Long id) {
ServiceInstance service = discoveryClient.getInstances("USER").get(0);
log.info("【" + service.getServiceId() + "】:" + service.getHost() + ":" + service.getPort());
UserPo user = new UserPo();
user.setId(id);
int level = (int) (id % 3 + 1);
user.setLevel(level);
user.setUserName("user_name_" + id);
user.setNote("note_" + id);
return user;
}
// 新增用户,POST请求,且以请求体(body)形式传递
@PostMapping("/insert")
public Map<String, Object> insertUser(@RequestBody UserPo user) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
map.put("message", "插入用户信息【" +user.getUserName() + "】成功");
return map;
}
// 修改用户名,POST请求,其中用户编号使用请求头的形式传递
@PostMapping("/update/{userName}")
public Map<String, Object> updateUsername(
@PathVariable("userName") String userName,
@RequestHeader("id") Long id) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
map.put("message", "更新用户【" +id +"】名称【" +userName + "】成功");
return map;
}
Hystrix 断路器
- 其自身 不可用之后 可能继续蔓延 到其他 与之相关的服务上,会使更多的微服务不可用
- 如果 电器 耗电大,导致电流过大,那么保险丝 就会熔断。
服务降级
-
请求其他 微服务出现超时,或者 发生故障时,就会使用自身服务其他的方法进行相应。
-
Hystrix 默认服务之间的调用 超过 2000ms (2s),会根据你配置的其他方法进行相应。
-
写了一个3秒以上的方法
@GetMapping("/timeout")
public String timeout() {
// 生成一个3000之内的随机数
long ms = (long)(3000L*Math.random());
try {
// 程序延迟,有一定的概率超过2000毫秒
Thread.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "熔断测试";
}
引入 pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
配置
// 启动断路器
@EnableCircuitBreaker
使用断路器
// Ribbon断路
@GetMapping("/circuitBreaker1")
@HystrixCommand(fallbackMethod = "error", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") })
public String circuitBreaker1() {
return restTemplate.getForObject("http://USER/timeout", String.class);
}
// Feign断路测试
@GetMapping("/circuitBreaker2")
@HystrixCommand(fallbackMethod = "error")
public String circuitBreaker2() {
return userService.testTimeout();
}
// 降级服务方法
public String error() {
return "超时出错。";
}
dashboard 启用hystrix仪表盘
引入xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
启动仪表盘
// 启用Hystrix仪表盘
@EnableHystrixDashboard
server.port=6001
spring.application.name=hystrix_dashboard
http://localhost:6001/hystrix
- Delay: 2000 ms 轮询时间
- title: 标题,如产品监控
- https://hystrix-app:port/actuator/hystrix.stream
产品微服务引入监控的依赖
-
spring-boot-starter-actuator
-
还要打开端点的暴露:
-
management.endpoints.web.exposure.include=health,info,hystrix.stream
-
-
之后再访问一下 产品微服务 地址的断路器
路由网关
- 请求路由到真实的服务器上,保护真实服务器的IP地址
- 作为负载均衡的手段
- 提供过滤器,判定请求是否有效
引入 pom
<!--引入服务发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
配置
//启动Zuul代理功能。里面包含了 启用断路器的机制
@EnableZuulProxy
# 服务端口
server.port=80
# Spring应用名称
spring.application.name=zuul
# 用户微服务映射规则
# 指定ANT风格的URL匹配
zuul.routes.user-service.path=/u/**
#指定映射的服务用户地址,这样Zuul就会将请求转发到用户微服务上了
zuul.routes.user-service.url=http://localhost:8001/
#产品微服务映射规则
zuul.routes.product-service.path=/p/**
#映射产品服务中心服务ID,Zuul会自动使用服务端负载均衡,分摊请求
zuul.routes.product-service.serviceId=product
#注册给服务治理中心
eureka.client.serviceUrl.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/
- 默认访问: http://localhost/user/timeout 即可映射到 user 项目,timeout方法
- 配置了上面:localhost/p/product/ribbon 即可访问
使用redis 做过滤器
网关:
-
检测用户登录
-
黑名单用户
-
购物验证码
-
恶意刷请求
-
模拟场景:
- 提交表单,每个表单存在一个序列号:serialNumber
- 对应 一个验证码 verificationCode
- 这两个参数一起提交给表单。reids以 序列号为key,验证码为值
- 判断用户提交的验证码 与 redis上是否一致。
引入redis和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!--不依赖Redis的异步客户端lettuce -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入Redis的客户端驱动jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000
spring.redis.port=6379
spring.redis.host=192.168.10.128
spring.redis.password=123456
spring.redis.timeout=1000
ZuulFilter
-
实现 4个抽象方法
- shouldFilter 如果为true,则执行 这个过滤器的方法
- run 核心方法
- filterType 过滤器类型
- pre 执行前
- route 处理请求,进行路由
- post 处理完成后
- error 出现错误的时候
- filterOrder 过滤器的顺序。值越小越优先。
-
Qualifier的意思是合格者,通过这个标示,表明了哪个实现类才是我们所需要的,
-
@Qualifier(“myZuulFilter”)
@Component
@Qualifier
public class MyZuulFilter extends ZuulFilter {
// 注入StringRedisTemplate
@Autowired
private StringRedisTemplate residTemplate = null;
// 是否过滤
@Override
public boolean shouldFilter() {
// 请求上下文
RequestContext ctx = RequestContext.getCurrentContext();
// 获取HttpServletRequest对象
HttpServletRequest req = ctx.getRequest();
// 取出表单序列号
String serialNumber = req.getParameter("serialNumber");
// 如果存在验证码返回为true,启用过滤器
return !StringUtils.isEmpty(serialNumber);
//这里有问题,如果没有这个表单序列号,那这个过滤器不启用,应该在下面,拦截。
}
// 过滤器逻辑方法
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest req = ctx.getRequest();
// 取出表单序列号和请求验证码
String serialNumber = req.getParameter("serialNumber");
String reqCode = req.getParameter("verificationCode");
// 从Redis中取出验证码
String verifCode = residTemplate.opsForValue().get(serialNumber);
// Redis验证码为空或者与请求不一致,拦截请求报出错误
if (verifCode == null || !verifCode.equals(reqCode)) {
// 不再转发请求
ctx.setSendZuulResponse(false);
// 设置HTTP响应码为401(未授权)
ctx.setResponseStatusCode(401);
// 设置响应类型为JSON数据集
ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8.getType());
// 设置响应体
ctx.setResponseBody("{'success': false, " + "'message':'Verification Code Error'}");
}
// 一致放过
return null;
}
// 过滤器类型为请求前
@Override
public String filterType() {
return "pre";
}
// 过滤器排序,数字越小优先级越高
@Override
public int filterOrder() {
return 0;
}
}
开发自己的注解
- 研究 @SpringCloudApplication
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
-
目前没有配置 扫描包,所以 一般都加上
-
//自定义扫描包 @ComponentScan(basePackages = "com.springboot.chapter17.product") @EnableFeignClients(basePackages = "com.springboot.chapter17.product")
-
boot 的知识补充点
可以选择 tomcat 服务器
-
还可以选择 Jetty 或 Undertow
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <!--<artifactId>spring-boot-starter-jetty</artifactId>--> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>
修改商标
- http://patorjk.com/software/taag/
- 录入文字,保存为 banner.txt
- 或者把图标保存为 banner.jpg
boot自动装配
-
spring-boot-start-web 会引入 spriing-boot-starters ——引入了 spring-boot-autocofigure包
-
RedisAutoConfiguration
//配置类 @Configuration( proxyBeanMethods = false ) //存在哪些类,ioc容器才会装配这个类 @ConditionalOnClass({RedisOperations.class}) //使用哪个类 可以通过配置文件装配 @EnableConfigurationProperties({RedisProperties.class})//可以通过文件配置 @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {"redisTemplate"} )//在缺失redisTemplate,才将方法 返回的bean装配到 Ioc容器中 public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(EnableRedisRepositories.class) @ConditionalOnBean(RedisConnectionFactory.class) //当存在这个 .repositories.* 属性后,才会启动这个类作为 配置文件 @ConditionalOnProperty(prefix = "spring.data.redis.repositories", name = "enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnMissingBean(RedisRepositoryFactoryBean.class) //加在其他其他类 到当前环境中来 @Import(RedisRepositoriesRegistrar.class) //完成RedisAutoConfiguration的装配后,才执行。还存在before 定制在哪些类之前初始化 @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisRepositoriesAutoConfiguration { }