一 file-gateway/Tokenfilter

这个是定义了一个pring Cloud Gateway 应用中的全局过滤器

用于拦截请求并验证请求中的 JWT 令牌。

逐行注释下面的代码,一个token验证拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package top.quhailong.pan.file.gateway.filter;
@Component//将这个类注册成Spring bean
@RefreshScope// 允许在允许的时候刷新这个bean,通常用于动态更新配置属性
public class TokenFilter implements GlobalFilter, Ordered {
@Value("${filter-url}")
private String filterUrl;// 从nacos读取
//包含需要进行令牌验证的 URL 列表,从配置中注入






@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();//从exchange中获得当前请求的request
String uri = request.getURI().getPath();//从request中获得uri
List<String> uriList = Arrays.asList(filterUrl.split(","));//分割这个filterUrl,获取全部需要拦截的url列表
for (String filterUrl:uriList) {///如果当前这个url需要被拦截,就拦截下来,验证token,如果有token,且正确,就可以放行
if(uri.contains(filterUrl)){
return verifyToken(exchange, chain);
}
}
return chain.filter(exchange);
}


// 这里就是验证token方法
private Mono<Void> verifyToken(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerHttpRequest request;//尝试从请求的 Cookies 中获取名为 token 的令牌。
HttpCookie cookie = exchange.getRequest().getCookies().getFirst("token");//获得token,返回类型是cookie类型
String token = cookie.getValue();

//解析token(JWT)令牌,获得claim对象,这个对象是个啥。如果令牌失效就会返回错误的响应
Claims claims = JWTUtils.parseJWT(token, "nimadetou".getBytes());
//从 Claims 中提取 subject 并将其转换为 UserInfoDTO 对象。
String subject = claims.getSubject();
UserInfoDTO userinfo = JSONUtils.parseObject(subject, UserInfoDTO.class);



//将编码后的用户信息添加到请求头中,继续处理请求。
String operationInfo = URLEncoder.encode(JSONUtils.toJSONString(userinfo), StandardCharsets.UTF_8.toString());

// 把这个请求头放到request里面,然后继续向后面传递
request = exchange.getRequest().mutate().header("operationInfo", operationInfo).build();
return chain.filter(exchange.mutate().request(request).build());
} catch (Exception e) {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(RestAPIResultDTO.Error("token验证失败")).getBytes());
return response.writeWith(Flux.just(dataBuffer));
}
}

@Override
public int getOrder() {
return 0;//返回过滤器的顺序,数值越小优先级越高。这里返回 0 表示最高优先级。
}
}

二 JWT

JWT(JSON Web Token)是一种用于在各方之间作为 JSON 对象安全传输信息的紧凑且自包含的方式。JWT 的使用场景包括身份验证、信息交换等。

(1)JWT结构

包含三个部分:Header(头部)、Payload(负载)、Signature(签名)

1
Header.Payload.Signature

1. 头部

header通常包含两个部分,一个是令牌的类型(JWT)和签名算法(RSA,HMAC SH256)

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

2.Payload负载

远程调用理论

一、远程调用方客户端

1
2
3
4
5
6
7
8
9
@FeignClient(name = "some-service")// 这个指定了远程调用的服务名,一般是在服务注册中心中注册的服务吗
// 这个注释说明spring cloud feign要创建一个这个接口的代理实例,并将其用于名"some-service"发送远程服务的请求
public interface SomeServiceClient {
@GetMapping("/some-endpoint")//定义了一个get请求,目标是远程服务some-service的/some-endpoint 端点
ResponseEntity<String> getSomeData();
// 这个是feign客户端接口的方法的定义,也就是说,如果这个方法被调用了,那么就会把相应的请求转发到这个远程服务的端点上,、
// 此外还定义了这个方法的响应,也就是ResponseEntity<String>,接受响应的结果
}

二、使用这个feign客户端的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class SomeOtherServiceController {

@Autowired
private SomeServiceClient someServiceClient;

@GetMapping("/use-some-service")
public ResponseEntity<String> useSomeService() {
// 调用远程服务的端点
//这里就调用了一里面的客户端
return someServiceClient.getSomeData();
}
}

三、被远程调用的服务名

按照上述的,远程调用的服务名“远程服务 some-service:”

  • 这是注册在服务发现系统(如 Eureka)中的服务,name = "some-service" 对应它的服务名。
  • 它有一个暴露的 RESTful 端点 /some-endpoint,这个端点被 Feign 客户端所调用。
1
2
3
4
5
6
7
8
9
@RestController
public class SomeServiceController {

@GetMapping("/some-endpoint")
public ResponseEntity<String> getSomeData() {
// 服务逻辑处理
return ResponseEntity.ok("Some data from some-service");
}
}

四、总结

  • SomeServiceClient 是远程调用的客户端接口,通过 Feign 创建。
  • SomeServiceController 中的 /some-endpoint 是被远程调用的端点。
  • useSomeService 方法使用 Feign 客户端 someServiceClient 调用远程服务 some-service/some-endpoint

在调用方法具体流程,浏览器在调用/use-some-service的时候,被这个服务的消费者,也就是这个SomeOtherServiceController捕捉到,

在这个方法中,注入了

1
2
3
@Autowired
private SomeServiceClient someServiceClient;

在具体这个@GetMappping方法中,会调用这个远程调用的客户端接口的对象,使用里面的

1
return someServiceClient.getSomeData();

这个客户端方法里,会把这个方法的请求,转移发送到远程调用的服务端点那边去,

1
2
@GetMapping("/some-endpoint")//定义了一个get请求,目标是远程服务some-service的/some-endpoint 端点
ResponseEntity<String> getSomeData();

发送到服务的some-endpoint端点上去

然后远程调用的服务提供者提供服务,返回数据

消费者得到返回的响应,形成一个对象

openfeign的具体实现和理论

一、实现

1. pom.xml中添加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2.启动feign客户端,在启动类上使用@EnableFeignClients注释

1
2
3
4
5
6
7
@SpringBootApplication
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

3.定义Feign客户端接口

使用 @FeignClient 注解定义一个接口来描述远程服务的 API。注解的 name 属性指定服务的名称,这个名称应该和服务注册中心(例如 Eureka)中的服务名一致。

1
2
3
4
5
6
7
8
9
@FeignClient(name = "some-service")
public interface SomeServiceClient {

@GetMapping("/some-endpoint")
ResponseEntity<String> getSomeData();

@GetMapping("/another-endpoint")
ResponseEntity<String> getAnotherData(@RequestParam("param") String param);
}

4.使用这个feign客户端接口

就是在controller中调用这个feign客户端中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class SomeOtherServiceController {

@Autowired
private SomeServiceClient someServiceClient;

@GetMapping("/use-some-service")
public ResponseEntity<String> useSomeService() {
// 调用远程服务的端点
return someServiceClient.getSomeData();
}

@GetMapping("/use-another-service")
public ResponseEntity<String> useAnotherService(@RequestParam("param") String param) {
// 调用另一个远程服务的端点
return someServiceClient.getAnotherData(param);
}
}

5.配置feign客户端

1
2
3
4
5
6
7
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full

假设有一个名为 some-service 的微服务,它有两个端点 /some-endpoint/another-endpoint,定义如下

image-20240519175609772

二、原理

  • Feign Client@FeignClient 注解用于定义 Feign 客户端,Spring Cloud Feign 会自动创建该接口的实现。
  • 负载均衡:如果启用了 Ribbon(默认情况),Feign 客户端将通过 Ribbon 实现负载均衡,选择合适的实例进行调用。
  • Hystrix:Feign 还可以与 Hystrix 配合使用,实现熔断和回退机制。
  • 日志:通过配置 Feign 日志级别,可以记录详细的 HTTP 请求和响应。

openfeign是一个声明式的HTTP客户端,简化了REST API的调用,底层涉及了非常多的组件,包括:

  • 动态代理
  • 负载均衡
  • 拦截器
  • ……

1. 动态代理

按照之前介绍的,可以看到,声明的feign客户端其实是一个接口类,并不是一个实现类,

所以,当定义一个feign客户端接口的时候,openfeign会创建这个接口的代理对象,在调用方法的时候,代理对象会拦截方法调用,然后将这个转化为HTTP请求

a. 创建这个接口的代理对象,应该是cglib,

b.在调用方法的时候,这个代理对象会把这个方法的调用给拦截下来,

c.然后将这个方法转化为http的请求发送出去,到远程调用的远程服务提供者暴露的服务端点处

2.核心组件

(1)Feign.Builder// 后续还需要学

这个是创建feign客户端的核心类,是负责构造feign客户端实例,可以配置各种的选项,包括编码器,解码器,日志记录器

1
2
3
4
Feign.Builder builder = Feign.builder()
.decoder(new JacksonDecoder())//将 HTTP 响应解码为响应对象
.encoder(new JacksonEncoder());//将请求对象编码为 HTTP 请求
SomeServiceClient client = builder.target(SomeServiceClient.class, "http://some-service");

spring cloud feign默认使用spring的消息转换器(jackson)来处理编解码

(2)Contract

1
Contract 负责解析 Feign 客户端接口的注解。Spring Cloud Feign 使用 SpringMvcContract 来解析 Spring MVC 的注解,如 @RequestMapping、@GetMapping 等。

(3)Client//是一个接口,负责执行HTTP请求.

就是一个HTTP的客户端,feign在builder的时候,可以选择使用Apache HttpClient 或 OkHttp\或者其他,然后利用这个客户端来实现执行HTTP的请求.

1
2
Client client = new OkHttpClient();
Feign.Builder builder = Feign.builder().client(client);

(4)Logger,用于记录请求和响应的日志

image-20240519182131701

三springcloud的组件

1. @EnableFeignClients

1
2
@EnableFeignClients
这个注解启用了 Feign 客户端的自动配置。它会扫描指定包下的所有接口,并为每个接口创建一个 Feign 客户端代理。

这个注释启用了feign的自动化配置,它会扫描指定包下面的所有的接口,为每个接口都创建一个feign的客户端代理,

2.自动配置类

spring cloud feign提供了一些自动配置类,如 FeignAutoConfigurationFeignClientsConfiguration.

这些配置类会自动的配置feign客户端所需要的各项组件.

3.load balance

这个feign是默认集成了ribbon,用于客户端的负载均衡

image-20240519183020401

4.Hystrix 整合

Feign 与 Hystrix 整合,可以实现熔断和回退机制。通过在配置中启用 Hystrix,Feign 客户端在调用失败时可以自动进行回退。

image-20240519183310894

5.扩展

image-20240519183321748

@EnableFeignClients

1.注释的声明

@EnableFeignClients 注解定义在 org.springframework.cloud.openfeign 包中。它的主要功能是通过 FeignClientsRegistrar 来注册 Feign 客户端。

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};//指定要扫描的包。
Class<?>[] basePackageClasses() default {};
Class<?>[] defaultConfiguration() default {};//指定默认的配置类。
Class<?>[] clients() default {};//指定具体的 Feign 客户端类。
}

2.FeignClientsRegistrar 的工作原理

1
FeignClientsRegistrar` 是一个 `ImportBeanDefinitionRegistrar

作用是在spring的bean的定义注册阶段注册Feign客户端的相关Bean

注册过程

a. 扫描Feign客户端接口:FeignClientsRegistrar会根据@EnableFeignClients 注释的属性,比如basePackages,扫描指定包中的所有接口

‘’这个basePackages中的接口是怎么定义的?

b.注册Feign客户端Bean:对于每一个扫描到的接口,FeignClientsRegistrar 会注册一个 FeignClientFactoryBean,它是一个工厂 Bean,负责创建 Feign 客户端的代理对象。

自动配置类

FeignAutoConfigurationFeignClientsConfiguration

1.FeignAutoConfiguration

FeignAutoConfiguration 负责配置 Feign 的核心组件,如 Feign.BuilderDecoderEncoderContract 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
@ConditionalOnClass(Feign.class)
@Import(FeignClientsConfiguration.class)
public class FeignAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public Feign.Builder feignBuilder() {
return Feign.builder();
}

@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
}

@Bean
@ConditionalOnMissingBean
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
}

@Bean
@ConditionalOnMissingBean
public Contract feignContract() {
return new SpringMvcContract();
}

@Bean
@ConditionalOnMissingBean
public Logger feignLogger() {
return new Slf4jLogger();
}
}

2.FeignClientsConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class FeignClientsConfiguration {
@Bean
public Decoder feignDecoder() {
return new ResponseEntityDecoder(new SpringDecoder(messageConverters));
}

@Bean
public Encoder feignEncoder() {
return new SpringEncoder(messageConverters);
}

@Bean
public Contract feignContract() {
return new SpringMvcContract();
}

@Bean
public Logger feignLogger() {
return new Slf4jLogger();
}
}

3.Feign客户端的创建

当Feign的客户端创建后,

  1. Spring会使用FeignClientFacatoryBean来创建代理对象.
  2. 把接口的方法调用,转发到feign生成的HTTP请求

FeignClientFactoryBean 是一个工厂 Bean,用于创建 Feign 客户端代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean {
@Override
public Object getObject() throws Exception {
return feignContext.getInstance(this.name, Feign.Builder.class)
.target(this.targetType, this.url);
}

@Override
public void afterPropertiesSet() throws Exception {
// 初始化配置
}
}

项目AOP 幂等性

注意:是通过自定义注释实现的,在需要实现幂等性的方法上添加注释

一、准备

  1. 自定义注解 @Idempotent,用于标识需要幂等性控制的方法。

image-20240519161422839

  1. 定义AOP切面类
  • 使用 @Aspect 定义切面,拦截使用 @Idempotent 注解的方法,确保幂等性。

image-20240519161544023

二、调用

1.在需要幂等性的接口方法上使用这个注释

image-20240519162009066

2.在应用启动的时候,spring会自动的扫描并注册aop切面类’**IdempotentAopHandler**

  • 当调用标记了 @Idempotent 注解的方法时,AOP 切面会拦截调用,检查 Redis 中是否存在锁,如果不存在则设置锁并执行方法,执行完毕后释放锁。

3.获取唯一的标识

  • @Idempotent 注解中获取 uniqueIdentification,结合当前用户的 Token 生成幂等性锁标识。

4.尝试获得锁

    • 调用 redisUtil.setIfAbsentEx 方法尝试在 Redis 中设置一个带有过期时间的键,如果设置成功,则表示获取到锁。

5.执行方法

  • 获取锁成功后,执行目标方法。
  • 如果方法执行过程中抛出异常,记录日志并抛出自定义异常。

6.释放锁

  • 方法执行完毕后,无论是否成功,都会在 finally 块中删除 Redis 锁,确保锁被释放。

7.异常处理

  • 如果未能获取到锁,抛出自定义异常,表示该方法在指定时间内不能重复执行。

通过这种方式,可以确保方法在指定的时间窗口内只会执行一次,防止重复提交或重复调用,保证接口的幂等性。

三、代码

在代码里是在AOP切面编程中,使用了redis的分布式锁来实现最终一个结果,就是redis的操作的幂等性,这个redis只是我的猜测,保证在并发情况下只执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package top.quhailong.pan.framework.redis.core.idempotent.aop;
@Order(1)// 设置切面的执行顺序,值越小优先级越高
@Aspect//表明这个类是一个切面类
@Slf4j//引入日志记录功能
class IdempotentAopHandler {
@Autowired
private RedisUtil redisUtil;
/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
@annotation(top.quhailong.pan.framework.redis.core.idempotent.annotation.Idempotent) 标识将匹配,即,将会在所有标注了这个注释的方法拦截,并调用aop
*/
@Pointcut("@annotation(top.quhailong.pan.framework.redis.core.idempotent.annotation.Idempotent)")
public void IdempotentPointCut() {
}
// 通过定义切入点,环绕通知(@Around 注解标注的方法)可以使用该切入点来指定在哪些方法执行前后进行额外的处理。
/**
* 环绕通知,接口幂等
* @author: quhailong
* @date: 2020/7/26
* round定义了一个环绕通知,围绕切点定义的方法的前后
*/









// 这里需要处理幂等性控制逻辑
// 通过这个注释,知道要在哪里拦截,那么就通过反射得到被拦截的方法和它的注释信息,
//MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();

//Method method = signature.getMethod();
// 通过这个方式获得方法(被拦截的方法)
@Around("IdempotentPointCut()")
public Object addIdempotent(ProceedingJoinPoint proceedingJoinPoint) {
Object result = null;


//获取方法签名。
// MethodSignature 和 Method:通过反射获取被拦截的方法和其注解信息。
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
// 获取切入点所在的方法,获得方法对象,签名里包含了
Method method = signature.getMethod();


// 获取操作 dempotent 注解:获取注解实例,并读取 uniqueIdentification 属性,用于生成唯一的幂等性锁标识。
// 注解实例是 Java 中表示注解的对象。通过反射机制,可以在运行时获取这些注解实例,并读取注解中的属性值。以下是如何定义和使用自定义注解的示例:
Idempotent idempotent = method.getAnnotation(Idempotent.class);
String uniqueIdentification = idempotent.uniqueIdentification();
//uniqueIdentification 是 @Idempotent 注解中的一个属性,通过注解实例可以访问该属性的值。



// idempotentLock:构建 Redis 锁的键,使用唯一标识符和当前用户的 Token。
String idempotentLock = String.format(RedisConstants.IDEMPOTENT_LOCK, uniqueIdentification, TokenUtil.getToken());

// redisUtil.setIfAbsentEx( 尝试在redis中设置一个带有过期时间的键,如果设置成功,表示获得了锁
if (redisUtil.setIfAbsentEx(idempotentLock, UUID.randomUUID().toString(), 120, TimeUnit.SECONDS)) {
try {
result = proceedingJoinPoint.proceed();
// proceedingJoinPoint.proceed():执行目标方法。
// 这里就是分布式的关键,需要得到锁,不然就不给执行
} catch (Throwable e) {
log.error("业务处理出现异常", e);
throw new CustomException(ResultCodeEnum.SERVICE_EXCEPTION.getCode(), ResultCodeEnum.SERVICE_EXCEPTION.getDesc());
} finally {
redisUtil.delete(idempotentLock);
// 删除redis的锁
}
} else {
throw new CustomException(ResultCodeEnum.IDEMPOTENT_LOCK_ERROR.getCode(), ResultCodeEnum.IDEMPOTENT_LOCK_ERROR.getMessage());
}
return result;
}
}

四、一些细节

方法签名

方法签名是编程中描述一个方法的特征的方式,它标识方法的名称以及方法的参数的类型顺序,在java中,方法签名不包括方法的返回类型和访问修饰符,

image-20240519164704389

image-20240519164717586

作用

AOP框架中,就是可以通过方法签名来识别和拦截方法的调用

Postman测试

登录接口

添加课程接口调试

如何处理课程添加接口对于登录接口的依赖

需要登录成功,就要获得对应的token,并将这个信息伴随着添加 课程请求发送给系统

image-20240512155423448

登录接口测试

接口用例设计思路

image-20240513124926240

redis实战

image-20240411193128461

添加redis缓存

image-20240412102006771

image-20240412102223465

缓存更新策略–缓存和数据库的一致性

image-20240412103529937

image-20240412103954487

0.3一致性有问题:当缓存已经执行了上百次,但是还没有和数据库保持一致性,这就会导致缓存和数据库的不一致性

01 需要我们自己编码

好的

image-20240412105724009

坏了

更新的太慢了,

image-20240412110151242

image-20240412111423835

image-20240412113042390

image-20240412113449619

返回空值会设置ttl

image-20240412113513294

image-20240412114143006

image-20240412115029875

image-20240412121157556

image-20240412121644372

image-20240412121705685

image-20240412122228072

image-20240412123933989

redis是单线程还是多线程

  1. 在4之前是部分或者完全不支持多线程的,
  2. 版本4严格意义上也不是单线程,而是负责处理客户端请求的线程是单线程,但是开始加了一点多线程的东西(异步删除)
  3. 2020年5月版本的6和2022年的7,告别了大家印象中的单线程,用一种全新的多线程来解决问题

image-20240402094354324


Redis是单线程?

主要是指redis的网络IO和键值对读写是由一个线程来完成的,redis在处理客户端请求的时候包括获取(socket读), 解析,执行,内容返回(socket)写等都是由一个顺序串行的主线程处理,这就是所谓的单线程,这也是redis对外提供键值存储服务的主要流程.

image-20240402100232629

但是Redis的其他功能,比如持久话RDB\AOF\异步删除,集群数据同步等待,其实是由额外的线程执行的

redis命令工作线程是单线程的,但是对于整个Redis来说,是多线程的

为什么在3.0之前要采用单线程?

单线程但是快的原因

  • 基于内存操作,Redis的所有数据都存储在内存中,因此所有的运算级别都是内存级别的,他的性能比较高

  • 数据结构简单:redis的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是O(1),因此性能是比较高的

  • 多路复用和非阻塞I/O:redis使用I/O多礼服用的功能来监听多个socket连接客户端,这样就可以使用一个线程来连接多个请求,减少线程切换带来的开销,同时也避免了i/o阻塞的操作

  • 避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的小号,而且单线程不会导致死锁的问题.

采用单线程的原因

Redis是基于内存操作的,因此他的瓶颈可能是机器的内存或者是网络的带宽而并非CPU,既然CPU不是瓶颈,自然就使用单线程的解决方案了,何况使用多线程会比较的麻烦.(但是从redi开始就开始支持多线程了,比如后台删除,备份等功能)

  • 使用单线程redis的开发和维护更加的简单,因为单线程模型方便开发和调试
  • 即使使用单线程模型也并发的解决多客户端的请求,主要使用的是IO多路复用和非组设IO
  • 对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽并非CPU

为什么要加入多线程?

正常情况下使用del指令可以很快的删除数据,但是当被删除的key是一个非常大的对象的时候,例如包含了成千上万个元素的hash集合时,那么del指令就会导致redis主线程卡顿

由于redis是单线程的, del bigkey…

等待了很久这个线程才会释放,类似加了一个synchronized的锁,

1
unlink key

image-20240402103828121

Redis6/7 开始全面多线程

随着网络硬件的性能提升,redis的性能瓶颈有时候会出现在网络的IO处理上,也就是单个主线程处理网络请求的速度跟不上底层网络硬件的速度.

为了解决这个问题

采用多个IO线程来处理网络的请求,提高网络请求处理的并行度

image-20240402105237690

image-20240402105910048

阶段一:服务端和客户端建立socket连接,并分配处理线程

首先,主线程负责接受建立连接请求,当由客户端请求和实例建立socket连接时,主线程会创建和客户端的连接,并把socket放入全局等待队列中,然后主线程通过轮询的方法把socket连接分配给io线程

阶段二:IO线程读取并解析请求

主线程一旦把socket分配给IO线程,就会进入阻塞状态,等待IO线程完成客户端请求读取和解析,因为由多个IO线程在并行处理,所以这个过程很快就可以完成.

阶段三:主线程执行请求操作

等待IO线程解析完请求,主线程还是会以单线程的方式执行这些命令操作

阶段四:IO线程回写Socket和主线程清空全局队列

当主线程执行完请求操作之后,会把需要返回的结果写入缓冲区,然后主线程会阻塞等待IO线程,把这些结果回写到socket中,并返回给客户端,和IO线程读取和解析请求一样,IO线程会 写socket,也是有多个线程在并发执行,所以回写socket的速度也是很快的,等到IO线程会谢socket完毕,主线程会清空队列,等待数据库的后续请求.

Unix网络编程中的五种IO模型

Blocking IO 阻塞IO

NoneBlocking IO 非阻塞IO

IO multiplexingIO多路复用

一种同步的IO模型,实现一个线程监视多个文件句柄,**一旦某个文件句柄就绪,**就能够通知到对应的应用程序进行相应的读写操作,没有文件句柄就绪的时候,就会阻塞应用程序,从而释放CPU资源.

I/O:

网络I/O,尤其在操作系统层面指数据在内核态与用户态之间的读写关系

多路

多个客户端连接(连接就是套接字描述符,即socket或者channel)

复用

复用一个或者几个线程

IO多路复用

也就是说一个或者一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端的连接,无需创造或者维护过多的进程/线程

一个服务端进程可以同时处理多个socket描述符

实现IO多路复用的模型有三种,可以分为==select->poll->epoll==三个阶段来描述

signal driven IO 信号驱动IO

asynchronous IO 异步IO

file descriptor

文件描述符 file descriptor 是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念.文件描述符在形式上是一个非负整数.实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表.

当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符,在程序设计中,文件描述符这一概念往往只适用于Unix,Linux这样的操作系统.

当socket连接的时候,返回一个fd

epoll

image-20240402170350161

image-20240402170557188

客户端请求服务端时,实际就是在服务端的socket文件中写入客户端对应的文件描述符,如果与多个客户端同时请求服务端,为每次请求分配一个线程,类似于每次来都new一个 如此就会比较耗费服务端的资源

因此我们只使用一个线程来监听多个文件描述符,这就是IO多路复用

image-20240402171459395

Redis6-7

image-20240402171733957

redis7将所有的数据都放在内存里,内存的响应时长大概是100纳秒,对于小数据包,redis服务器可以处理8W到10W的QPS,这也是redis处理的季羡林,对于80%的公司来说,单线程的redis已经足够使用了.

多线程默认是关闭的,如果要使用多线程功能,需要在redis.conf中完成两个设置,

image-20240402172207532

image-20240402172348792

BIGKEY

image-20240402172659202

为什么不能使用key * 这个指令

key * 这个指令有致命的弊端,这个指令没有offset,limit参数,是要一次性吐出所有满足条件的key,由于redis是单线程的,其所有操作都是原子的,而keys算法是遍历算法,复杂度是O(n),如果实例中有千万级别以上的key,这个指令i聚会导致redis服务卡顿,所有读写redis的其他的指令都会被延后甚至会超时报错,可能会导致雪崩甚至是数据库宕机

生产上限制keys */fulshdb/flushall等危险命令以防止误删误用?

image-20240402174418542 image-20240402174533926

不用keys 怎么迭代呢

scan命令,类似于mysql limit 但是不完全相同

用于迭代数据库当中的数据库的健

image-20240402175152842 image-20240402175345171

阿里云的开发规范

image-20240402202143721

Bigkey定义

==String是value,最大512MB但是≥10kb就是bigkey了==

==list\hash\set与zset,个数超过5000就是bigkey==

image-20240402202430812

按照上述的来看,可存放的大小要大得多,为什么只能存储5000个?

BigKey的危害

内存不均,集群迁移困难

超时删除,大key删除作梗

网络流量阻塞

BigKey怎么产生的

image-20240402202751172

redis-cli-bigkeys

memory usage 命令 计算所占的每个字节数

怎么删除

image-20240402203649521

非字符串的bigkey不要使用del删除,

渐进式删除就是慢慢删,一点点的把这个field给减小了.最后再删掉

hash 使用hscan 每次获取少量的field-value ,再使用hdel删除每个field

hscan与hdel

image-20240402205831413

Redis集群

image-20240401213935236

集群:由于数据量过大,单个master复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展每个复制集只负责存储整个数据集的一部分,这就是redis的集群,其作用是提供在多个redis节点间共享数据的程序集

考集群的算法和架构拓扑安装

image-20240401214301548

==Redis集群是一个提供在多个Redis节点间共享数据的程序集==

==Redis集群可以支持多个Master==

干什么

redis集群支持多个Master,每个Master又可以挂载多个slave

  • 读写分离
  • 支持数据的高可用
  • 支持海量数据的读写存储操作

由于cluster自带的故障转移机制,内置了高可用的支持,无需再去使用哨兵的功能

客户端与redis的节点连接,不再需要连接集群中的所有节点,只需要任意连接集群中的一个可用节点即可

槽位slot负责分配到各个物理服务节点,又对应的集群来负责维护节点,插槽和数据之间的关系.

集群算法 分片 槽位slot

槽位slot

集群的密钥空间被分为16384个槽位,有效设置了16384个主节点的集群大小上限(但是建议,最大的节点大小约为1000个节点)

redis有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定防止在哪个槽,集群的每个节点负责一部分hash槽.

image-20240401220220448

分片

1. 定义:使用redis集群时我们会将存储的数据分散到多台redis机器中,这称为分片.

集群中的每个redis实例都被认为是整个数据的一个分片

2.如何找到给定key的一个分片

为了找到给定key的分片,我们对key进行CRC16(KEY)算法处理并通过对总分片数量取模.然后,使用确定性哈希函数,这意味着给定的key将多次始终映射到同一个分片,我们可以推断将来读取特定key的位置

优势

image-20240401221138492

slot槽位映射,一般业界有三种解决方案

redis集群分片之hash取余分区算法

image-20240401221729042

一致性哈希

image-20240401223213985

分布式缓存数据变更与映射问题,某个机器宕机了,分母的数量就改变了,自然取余就不可以了

能干啥

  • 提出了一致性hash解决方案
  • 目的是当服务器个数发生变动时,尽量减少影响客户端到服务器的映射关系

步骤

算法构建一致性哈希环

image-20240401223306047

服务器IP节点映射

image-20240401223600375

key落到服务器的落键规则

优点

image-20240401223814591

image-20240401223833557

哈希槽分区

image-20240401224030139

一定要-c 这个指令是值用集群的形式来运行 ,

哨兵sentinel

吹哨人巡查监控后台master主机是否故障,如果故障了根据投票数自动将某一个从库换成新主库;

投票数写多少:?

作用:无人值守运维

监控redis运行状态,包括master和slave

当master down 机,能自动将slave切换成新master

能干什么

主从监控

监控主从redis库运行是否正常

消息通知

哨兵可以将故障转移的结果发送给客户端

故障转移

如果master异常,则会进行主从切换,将其中一个slave作为新的Master

配置中心

客户端通过连接哨兵来获得当前redis服务的主节点

image-20240401190333439

实战

重点参数项说明

1
sentinel monitor <master-name> <ip> <redis-port> <quorum>

设置要监控的master服务器

quorum表示最少有几个哨兵 认可客观下线,同意故障迁移的法定票数

image-20240401201726993

quorum这个参数是进行客观下线的一个依据,意思是至少有quorum个sentinel认为这个master有故障,才会对这个master进行下线以及故障转移.因为有的时候,某个sentinel节点可能因为自身网络的原因,无法连接master,而此时master并没有出现故障,所以这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,就保证了公平性和高可用.

1
sentinel auth-pass <master-name> <password> 

master 设置了密码,连接master服务的密码

 image-20240401202852489 image-20240401203501054

运行流程和选举原理

当一个主从配置中的master失效之后,sentinel可以选举出一个新的master用于自动接替原master的工作,主从配置中的其他redis服务器自动指向新的master同步数据.一般建议sentinel采用奇数台

运行流程,故障切换

image-20240401205758253

三个哨兵监控一主二从,正常运行中……

SDown主观下线(Subjectively Down)

  • SDOWN(主观不可用)是单个sentinel自己主观上检测到的关于master的状态,从sentinel的角度来看,如果发送了ping心跳之后,在一定的时间里面没有收到合法的回复,就达到了SDOWN的条件.
  • sentinel配置文件中的down-after-milliseconds设置了判断主观下线的时间长度
image-20240401210114847

ODOWN客观下线(Objectively down)

ODOWN需要一定数量的sentinel,多个哨兵达成一致意见才能认为一个master客观上已经宕了

image-20240401210306067

选举出领导者哨兵(哨兵中选出兵王)leader

当主节点被判断客观下线以后,哥哥哨兵节点会协商,先选举出一个**==领导者哨兵节点==** 并由该领导者节点,也即被选举出的leader进行failover(故障迁移)

哨兵leader如何选举的 Raft算法

基本思路:先到先得 ,

image-20240406194705477

在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,就会同意A成为领导者

image-20240401211541666

由leader开始推动故障切换流程并选择一个新的master

某一个slave被选中成为一个新的master,规则如下:

由于存在网络抖动,不见得每个slave都会有效的得到master的信息,replication 谁复制的多就选谁

image-20240401212047065 image-20240401212224451

执行slaveof no one命令让选出来的从节点成为新的主节点,并通过slaveof命令让其他节点成为其从节点

Sentinel leader会对选举出的新master执行slaveof no one 操作,将其提升为master节点

sentinel leader向其他slave发送命令,让剩余的slave成为新的master节点的slave

哨兵使用建议

  • 哨兵系欸但的数量应该为多个,哨兵本身应该集群,保证高可用
  • 哨兵节点的数量应该是奇数
  • 各个哨兵节点的配置应该是一致的.最好硬件一样
  • 如果哨兵节点部署在Docker等容器里面,尤其是注意端口的正确映射
  • 哨兵集群+主从复制,并不能保证数据的零丢失
0%