MakaL-0-

加载图片需要翻出去


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 公益404

  • 搜索

Spring Cloud Alibaba一站式方案

发表于 2021-02-18 | 分类于 history , SpringCloud | 阅读次数:

Spring Cloud Alibaba一站式方案

  • Nacos:服务发现/注册,配置中心
  • Ribbon:负载均衡
  • Feign:声明式HTTP客户端(调用远程服务)
  • Sentinel:服务容错(限流,降级,熔断)
  • Gateway:API网关(webflux编程模式)
  • Sleuth:调用链监控
  • Seata:分布式事务解决方案

Nacos

官网:https://nacos.io/zh-cn/docs/quick-start.html

  1. Nacos注册中心下载(Nacos服务器是个jar包,可以使用sh或cmd命令运行)

  2. 应用application.yml配置Nacos Server地址 spring.cloud.nacos.discovery.server-addr=xxx:xx

  3. @EnableDiscoveryClient
  4. 访问 http://192.168.186.1:8848/nacos/index.html 即可,默认账号密码都是nacos

Feign

声明式HTTP客户端,整合了Ribbon(负载均衡)和Hystrix(服务熔断)

  1. 引入OpenFeign(这个用的不是阿里的)

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 编写一个接口,告诉SpringCloud这个接口需要调用远程服务

    1
    2
    3
    4
    5
    6
    @FeignClient("gulimall-coupon") // nacos服务名
    public interface CouponFeignService {
    // 方法签名必须与远程的接口服务一致
    @RequestMapping("/coupon/coupon/member/list")
    R membercoupons();
    }
  3. 开启远程调用功能

    1
    2
    3
    @EnableFeignClients(basePackages = "com.mkl.gulimall.member.feign")
    public class GulimallMemberApplication {
    ...

Nacos配置中心

Nacos作为配置中心官方GitHub

Nacos官方文档

  1. 添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  2. 在应用的 /src/main/resources/bootstrap.properties 配置文件中配置 Nacos Config 元数据

    1
    2
    spring.application.name=nacos-config-example # 这里填的是应用名
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848

    更多配置项:

    image-20210113224848995

  3. 在Nacos中添加配置

    image-20210111224944134

    image-20210113221134391

    需要注意的是:Data ID必须是 ${prefix}-${spring.profiles.active}.${file-extension}

    • ${prefix}: 默认 spring.application.name

    • ${spring.profiles.active}:即为当前环境对应的 profile,由 spring.profiles.active 指定

    • ${file-extension}: 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension来配置(默认是properties,可以指定为yml)
    • 基于此,可以创建 {applicationName}-dev.properties, {applicationName}-st.yml, {applicationName}-uat.properties 等配置文件,然后在bootstrap.properties中指定环境即可(spring.profiles.active=dev)
    • 上述内容官方文档都有,且更详细
  4. 动态刷新:给对应Bean添加 @RefreshScope + @Value 或 @ConfigurationProperties注解

    例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 配置文件设置对应的值 ${} 表示从配置文件取
coupon.user.name=${coupon.user.name}
coupon.user.age=${coupon.user.age}

// Controller
// 演示用,不代表实际工程代码正确用法
@ConfigurationProperties(prefix = "coupon.user")
@Data
@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
@Autowired
private CouponService couponService;

private String name;
private Integer age;

@RequestMapping("/test")
public R test() {
return R.ok().put("name", name).put("age", age);
}
}

命名空间

可以给配置文件添加命名空间

image-20210113225237687

命名空间可以按照微服务维度去创建,不同微服务都有自己的命名空间,然后在对应微服务的bootstrap.properties指定spring.cloud.nacos.config.namespace=命名空间对应的UUID(在Nacos中命名空间栏可以看到)

配置分组

配置分组:group 默认为 DEFAULT_GROUP,可以在Nacos添加配置文件时指定配置分组,通过bootstrap.properties的 spring.cloud.nacos.config.group 配置,可以用于区分环境(dev,st,uat,prod等)

多配置集

配置中心中同时加载多配置集:如数据源配置,框架相关配置,微服务相关配置等,在nacos创建对应的配置文件,data-id和group对应即可(需要注意必须在指定命名空间下)

1
2
3
4
5
6
7
spring.cloud.nacos.config.ext-config[0].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[0].group=dev
spring.cloud.nacos.config.ext-config[0].refresh=false # 是否动态刷新

spring.cloud.nacos.config.ext-config[1].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=false

Gateway

网关:作为流量入口,提供包括路由转发,权限校验,限流控制等功能

gateway官网

  1. gateway需要作为一个应用启动,同样需要注册进服务中心,过程略

  2. 引入gateway

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
  3. 设置路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    spring:
    cloud:
    gateway:
    routes:
    # 当访问localhost:80/?url=baidu的时候,会路由到https://www.baidu.com
    - id: test_route
    uri: https://www.baidu.com
    predicates:
    - Query=url, baidu
    # 当访问localhost:80/?url=qq的时候,会路由到https://www.qq.com
    - id: qq_route
    uri: https://www.qq.com
    predicates:
    - Query=url, qq
  4. TODO:更多路由规则

IDEA注意事项

发表于 2020-08-19 | 分类于 history , 开发工具 | 阅读次数:

IDEA注意事项

IDEA插件

  • Alibaba Java Coding Guidelines
  • Lombok
  • Free Mybatis Plugin
  • Material Theme UI
  • Rainbow Brackets
  • Codota
  • Key Promoter X
  • CodeGlance
  • Restful Tookit

如果控制台乱码,网上的方式都不行,试试修改界面字体,如下:

image-20200819224810437

Kafka01-基础

发表于 2020-08-02 | 分类于 history , Kafka | 阅读次数:

Kafka01-基础概念

打算按着这条路来自学:

  1. 基础概念,基础架构
  2. 服务器Docker集群
  3. SpringBoot客户端连接
  4. 看极客时间的资料深入

基础概念

是什么?

最早是Linkedin公司用于日志处理的分布式消息队列。后面开源给Apache,为Apache Kafka

同时也是一个分布式流处理平台

有什么特性?

  • 传输的消息编码格式是纯二进制的字节序列
  • 发布/订阅模型:与点对点模型区分开,能有多个消息发布者和多个消息订阅者
  • 高吞吐:(单机每秒10W条消息传输)
  • 消息持久化(磁盘)
  • 分布式
  • 消费消息采用pull模式(由consumer保存offset)
  • 支持online和offline场景

应用场景?

  • 构建实时流处理管道,可靠获取系统到应用程序之间的数据

  • 构建实时流的应用程序,对数据流进行转换或反应

4个核心API

  • 应用程序使用producer API发布消息到一个或多个topic中
  • 应用程序使用consumer API来订阅一个或多个topic,并处理产生的消息
  • 应用程序使用streams API充当一个流处理器,从一个或多个topic消费输入流,并产生一个输出流到一个或多个topic,有效地将输入流转换到输出流
  • connector API允许构建或运行可重复使用的生产者或消费者,将topic链接到现有的应用程序或数据系统

不同公司的Kafka与版本区别

架构,术语,模型

  • Broker:Kafka集群中的每一台服务器
  • Topic:主题,承载消息的逻辑容器,实际使用中多用来区分具体的业务
  • Partition:分区,有序不变的消息序列,一个主题下可以有多个分区
  • Replication:副本,一个topic可以有多个partition,多个partition在多个broker,每个broker负责partition中存储的消息的读写操作。此外,每个partition都有备份,称为replication,备份有一个leader,0到多个follower。leader负责所有读写操作,follower只做备份
  • Leader:备份的领导者,对外提供服务。生产者总是向领导者副本写消息;消费者总是向领导者副本读消息
  • Follower:备份的追随者,不能与外界进行交互,只做一件事:向领导者副本发送请求,请求领导者把最新生产的消息发给它,保持与领导者的同步
  • Producer,Consumer:生产者消费者,略
  • Consumer Group:多个消费者组成的同一个组,用于同时消费多个分区,后续会介绍
  • Consumer Offset:消费者消费进度,每个消费者都有自己的位移,后续会介绍
  • Offset:分区中每条消息的位置信息,是一个单调递增且不变的值

生产者向topic中的partition leader发送消息

1个topic可以有多个partition,partition同时有1个leader和多个follower

consumer可以有consumer group消费topic中的每个partition

broker中有不同topic,不同partition,不同replication

zookeeper来管理集群

物理

Docker环境Kafka集群部署

SpringBoot整合Kafka

Swagger2

发表于 2020-08-02 | 分类于 history , SpringBoot | 阅读次数:

Swagger2

Swagger2官方文档

knife4j仓库

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- knife4j官方Gitee:https://gitee.com/xiaoym/knife4j -->
<!-- 使用knife4j来美化界面,knife4j同样支持微服务,可以去官方文档查看如何配置 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.4</version>
</dependency>

配置类:

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
@Configuration
@EnableSwagger2
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class Swagger2Configuration implements WebMvcConfigurer {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
// 只需要到第2级路径
.apis(RequestHandlerSelectors.basePackage("com.mkl"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("DEMO的API接口文档")
.description("Demo各种接口的信息")
// 服务网站
.termsOfServiceUrl("cn.bing.com")
.contact(new Contact("mkl", "xxblog.com", "[email protected]"))
.version("1.0")
.build();
}
}

输入: http://localhost:8080/doc.html 即可访问knife4j的API界面

API列表

  • @Api 用于请求的类上,表示对类的说明,也代表这个类是swagger2的资源

    • tags:说明该类的作用,参数是数组,可以填写多个
    • description:描述
  • @ApiImplicitParams:用在请求的方法上,包含多个 @ApiImplicitParam

  • @ApiImplicitParam:用于请求的方法上,表示单独的请求参数

    • name:参数名
    • value:参数说明
    • dataType:参数数据类型
    • paramType:表示参数放在哪
      • header:请求参数的获取:@RequestHeader
      • query:请求参数的获取:@RequestParam
      • path:请求参数的获取:@PathVariable
      • body:请求参数的获取:@RequestBody
      • form:form表单
    • defaultValue:参数的默认值
    • required:参数是否必需
  • @ApiOperation:用于方法,表示一个http请求访问该方法的操作
    • value:方法的用途和作用
    • notes:方法的注意事项和备注
    • tags:说明该方法的作用,参数是个数组,可以填多个,用于在左边导航栏显示
    • response:返回的类型(是一个class)
    • 还有很多其他的可以去看源码
  • @ApiParam:用于方法,参数,字段说明,表示对参数的要求和说明
    • name:参数名
    • value:参数简要说明
    • defaultValue:参数默认值
    • required:参数是否必需
  • @ApiModel:用于响应实体类上,说明实体作用
    • description:描述实体作用
  • @ApiModelProperty:用于实体的属性上
    • value:描述参数的意义
    • name:参数的变量名
    • required:参数是否必选
    • notes:提示
    • dataType:数据类型
  • @ApiResponses:用于请求的方法上,根据响应码不同表示不同响应
    • 包含多个 @ApiResponse
  • @ApiResponse:用在请求的方法上,表示不同的响应
    • code:响应码(int),如:404,200,可自定义
    • message:响应码对应的响应信息
  • @ApiIgnore:用于类或方法上,不被显示在页面上
  • @Profile({"dev", "test"}):用于配置类,表示只对开发和测试环境有用

最佳实践参考文章

https://www.jianshu.com/p/6cac7bd21c8f

https://blog.csdn.net/qq_28767795/article/details/81880404

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
@RestController
@Api(description = "12员工Controller")
public class EmployeeController {

@PostMapping("/emp")
@ApiOperation(value = "保存员工信息", notes = "保存员工信息aaaaaa", response = Object.class)
@ApiImplicitParams({
@ApiImplicitParam(name = "employee",
value = "保存的员工信息",
dataType = "com.mkl.demo.pojo.Employee",
paramType = "body",
required = true),
@ApiImplicitParam(name = "result",
value = "参数校验错误信息",
dataType = "org.springframework.validation.BindingResult")
})
@ApiResponses({
@ApiResponse(code = HttpServletResponse.SC_OK, message = "保存成功"),
@ApiResponse(code = HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message = "服务器错误"),
@ApiResponse(code = HttpServletResponse.SC_UNAUTHORIZED, message = "未授权")
})
public Object postEmp(@Validated @RequestBody Employee employee,
BindingResult result) {
Map<String ,Object> errorMap = new HashMap<>();
if (result.hasErrors()) {
result.getFieldErrors().forEach(p -> {
errorMap.put(p.getField(), p.getDefaultMessage());
});
return errorMap;
}
// 数据校验无误,去后台保存employee数据
return true;
}
}
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
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@ApiModel(value = "员工对象", description = "员工对象")
public class Employee {

@Valid
@ApiModelProperty(value = "员工账号", name = "account", required = true, notes = "测试一下这个提示是什么样的")
@NotNull(message = "账号不能为空")
Account account;

@ApiModelProperty(value = "员工ID", name = "id", required = true, notes = "测试一下这个提示是什么样的")
@NotNull(message = "ID不能为null")
public Integer id;

@Length(min = 1, max = 5, message = "名字长度不合法")
@NotBlank(message = "姓名不能为空")
@ApiModelProperty(value = "员工姓名", name = "name", required = true)
public String name;

@AssertFalse(message = "只招女员工")
@ApiModelProperty(value = "员工性别", name = "gender")
public Boolean gender;

@Range(min = 18, max = 65, message = "不在合法年龄内")
@ApiModelProperty(value = "员工年龄", name = "age")
public Integer age;

@Email(message = "不是合法的email格式")
@ApiModelProperty(value = "员工邮箱", name = "mail")
public String mail;

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@ApiModelProperty(value = "生日", name = "birth")
public String birth;

@Mobile
@ApiModelProperty(value = "电话号码", name = "phone")
public String phone;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "账号", description = "员工的账号信息")
public class Account {

@NotNull(message = "用户名不能为空")
@ApiModelProperty(value = "用户名", name = "username")
public String username;

@Size(min = 6, max = 15,message = "密码长度必须在 6 ~ 15 字符之间!")
@Pattern(regexp = "^[a-zA-Z0-9|_]+$", message = "密码必须由字母、数字、下划线组成!")
@ApiModelProperty(value = "密码", name = "password")
public String password;
}

JSR-303

发表于 2020-08-02 | 分类于 history , SpringBoot | 阅读次数:

JSR-303

在前端发送过来请求的时候,对前端发送的数据进行校验是必须要做的事情。JSR 303 - Bean Validation为JavaBean验证定义了相应的元数据类型和API,我们可以使用Bean Validation或者是自己定义的constraint来确保数据模型的正确性。Bean Validation是一个运行时的数据验证框架,验证之后错误信息会被马上返回

JSR 303 - Bean Validation规范: http://jcp.org/en/jsr/detail?id=303

Hibernate Validator是Bean Validation的参考实现,它提供了JSR 303规范中所有内置constraint的实现,除此之外还有一些附加的constraint

JSR-303中Bean Validation中的constraint如下

参考自 https://developer.ibm.com/zh/articles/j-lo-jsr303/

Hibernate Validator 附加的 constraint 如下:

Spring Boot与JSR-303

Spring Boot支持JSR-303验证规范,Hibernate Validator就是Spring Boot用于实现JSR-303的框架

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

hibernate-validator就集成在上面的依赖中

编写测试用例:

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
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Employee {

// @Valid使Account内部的校验也有效
@Valid
@NotNull(message = "账号不能为空")
Account account;

@NotNull(message = "ID不能为null")
public Integer id;

@Length(min = 1, max = 5, message = "名字长度不合法")
@NotBlank(message = "姓名不能为空")
public String name;

@AssertFalse(message = "只招女员工")
public Boolean gender;

@Range(min = 18, max = 65, message = "不在合法年龄内")
public Integer age;

@Email(message = "不是合法的email格式")
public String mail;

@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
public String birth;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {

@NotNull(message = "用户名不能为空")
public String username;

@Size(min = 6, max = 15,message = "密码长度必须在 6 ~ 15 字符之间!")
@Pattern(regexp = "^[a-zA-Z0-9|_]+$", message = "密码必须由字母、数字、下划线组成!")
public String password;
}

Controller编写:

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

@PostMapping("/emp")
public Object postDefaultEmp(@Validated @RequestBody Employee employee,
BindingResult result) {
Map<String ,Object> errorMap = new HashMap<>();
if (result.hasErrors()) {
result.getFieldErrors().forEach(p -> {
errorMap.put(p.getField(), p.getDefaultMessage());
});
return errorMap;
}
// 数据校验无误,去后台保存employee数据,略
return true;
}
}

测试结果:

自定义注解的实现

有的时候官方提供的注解不够用,我们可以实现自定义注解,需要编写对应的注解类以及其相应的constraint validator

第一步,编写自定义注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class) // 对应的验证实现类
public @interface Mobile {

//默认提示
String message() default "手机号码格式错误!";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}

第二部,编写对应的constraint validator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MobileValidator implements ConstraintValidator<Mobile, String> {

private String mobileReg = "^1(3|4|5|7|8)\\d{9}$";

private Pattern mobilePattern = Pattern.compile(mobileReg);

@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
// 为空时,不进行验证
if (StringUtils.isEmpty(value)) {
return true;
}
// 返回匹配结果
return mobilePattern.matcher(value).matches();
}

@Override
public void initialize(Mobile constraintAnnotation) {

}
}

第三步,为对应的类添加 @Mobile 注解,此注解此时就生效了

Redis09-缓存设计

发表于 2020-07-28 | 分类于 history , Redis | 阅读次数:

缓存设计

缓存更新策略

缓存中数据是有生命周期的,需要在指定时间后被删除或更新,这样可以保证缓存空间在一个可控的范围,下面是三种缓存更新策略

  • LRU/LFU/FIFO算法:使用 maxmemory-policy 指定,用于缓存使用量超过预设最大值的时候,一致性是最差的,但是维护成本是最低的
  • 超时剔除:设置 expire 来进行超时剔除,这个方法在一定时间窗口内存在一致性问题(缓存数据和真实数据不一致)
  • 主动更新:应用方对数据的一致性要求高,需要在真实数据更新后,立即刷新缓存到数据,如利用消息系统或其他方式通知缓存更新

缓存粒度控制

很多时候只需要缓存部分数据即可,比如用户表,只缓存关键的列,这样对空间,网络流量,序列化CPU开销等资源的消耗都能降低。但是缓存部分数据通用性会比较低,代码维护也较为复杂(如果出现要缓存新字段就需要修改业务代码,再刷新缓存数据)

穿透优化

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常处于容错的考虑,如果从存储层查不到数据则不写入缓存层

缓存穿透导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义,可能导致后端存储负载加大,在后端存储不具备高并发性的情况下可能导致后端存储宕机。程序中可以分别统计总调用数,缓存层命中数,存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透问题

造成缓存穿透有两种可能的原因:业务代码或数据有问题;恶意攻击,爬虫等造成大量空命中

解决缓存穿透有如下方法:

  • 缓存空对象,当存储层不命中,仍把空对象保留到缓存层,之后再访问这个数据,就会从缓存中获取(比如 SELECT * FROM table WHERE id = 999 (不存在的ID),获取到空值后, set user:id:999 NULL ,下次再有应用要获取ID为999的对象,就直接从缓存层中获取

  • 布隆过滤器拦截:在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。如一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户ID不存在,就不会访问存储层,在一定程度保护了存储层

布隆过滤器适用于数据命中不高,数据相对固定,实时性低(通常是数据集较大)的应用场景,代码维护较为复杂但缓存空间占用少

关于布隆过滤器:

https://en.wikipedia.org/wiki/Bloom_filter

https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter

无底洞优化

键值数据库分布式情况下,通常采用哈希函数将key映射到各个节点上,造成key的分布和业务无关,由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的节点上,批量操作就要从不同节点上获取,涉及的网络时间就更多

分布式条件下,一个mget操作需要访问多个Redis节点,如果节点数量越多,需要网络时间就越多。无底洞就是说投入越多不一定产出越多

优化方法:

  • 命令本身的优化,比如优化SQL语句
  • 降低接入成本,如客户端使用长连接,连接池,NIO等
  • 减少网络通信次数
    • 串行命令:逐次执行n个get命令,时间为n次网络时间+n次命令时间
    • 串行IO:Smart客户端会维护槽和节点对应关系,根据这个关系对属于同一个节点的key进行归档,得到每个节点的key字列表,再对每个子结点执行mget或Pipeline操作,操作时间为node次网络时间+n次命令时间
    • 并行IO:将上一个方案改为多线程执行,时间为max_slow(node网络时间)+n次命令时间
    • hash_tag:使用hash_tag将同一业务下多个key强制分配到一个节点上,操作时间为1次网络时间+n次命令时间

雪崩优化

缓存层承载大量请求,有效保护了存储层,但是如果缓存层由于某些原因不能提供服务,所有请求都会来到存储层,存储层的调用量会暴增,造成存储层也级联宕机的情况。缓存雪崩指的就是缓存层宕掉后,流量会像雪崩一样冲向后端存储

解决方法:

  • 保证缓存层服务高可用性:如使用Redis Sentinel和Redis Cluster
  • 依赖隔离组件为后端限流并降级:实际项目中,对重要资源(Redis,MySQL,HBase等)都进行隔离,让每种资源都单独运行在自己的线程池中,即使个别资源出现问题,对其他服务没有影响。SpringCloud的Hystrix可以实现隔离和降级熔断(熔断就是当服务不可用,就暂停对该服务的调用;降级是某些负荷比较高的服务,暂时舍弃非核心接口和数据请求,直接返回一个提前准备好的fallback错误处理信息,保证整个系统稳定性可用性)

  • 提前演练:项目上线前提前演练缓存层宕机后,应用及后端负载的情况和可能出现的问题,在此基础上做预案设定

热点key重建优化

缓存+过期时间可以加速数据读写也可以保证数据的定期更新,但是它不适用以下情况:

  • 当前key是热点key(比如热门的娱乐新闻),并发量非常大
  • 重建缓存不能在短时间完成,可能是一个复杂计算,如复杂SQL,多次IO,多个依赖等(比如访问某个key,发现miss,就去存储层执行复杂SQL)

缓存失效瞬间,大量线程来重建缓存,造成后端负载加大

解决方法:

  • 互斥锁:只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据。可以使用setnx实现互斥锁

  • 永远不过期:包含两层意思
    • 缓存层面看,没有设置过期时间,不会出现热点key过期后产生的问题,
    • 功能层面看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间,使用单独线程去构建缓存

Redis08-集群

发表于 2020-07-26 | 分类于 history , Redis | 阅读次数:

集群

数据分布

数据分布理论

分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集化分到多个节点上,每个节点负责整体数据的一个子集

常见的数据分区规则有哈希分区和顺序分区两种

  • 哈希分区:离散度好,数据分布业务无关,无法顺序访问,代表产品:Redis Cluster
  • 顺序分区:离散度易倾斜,数据分布业务相关,可顺序访问,代表产品:HBase

下面是常见的几种哈希分区

节点取余分区

使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式:hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。这种方案存在一个问题:当节点数量变化时,如扩容或者收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。扩容时通常翻倍扩容(这样可以只迁移约50%的数据),避免数据映射全部被打乱导致全量迁移的情况

一致性哈希分区

可查看参考文章:https://zhuanlan.zhihu.com/p/78285304

一致性哈希分区(Distributed Hash Table)实现思路是为系统每个节点分配一个token,范围一般在0 ~ 2^32,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值(取余是对2^32取余),然后顺时针找到第一个大于等于该哈希值的token节点,如下图所示

一致性哈希环增加节点和删除节点只会影响哈希环中相邻的节点,对其他节点无影响,但它存在如下问题:

  • 加减节点导致部分数据无法命中,需要手动处理或忽略这部分数据,因此一致性哈希常用于缓存
  • 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案(通过虚拟槽分区解决)

虚拟槽分区利用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,这个整数定义为槽。这个整数集合的范围一般远大于节点数,比如Redis Cluster槽范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽,如下图所示

Redis数据分区

Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0 ~ 16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据

Redis虚拟槽分区特点:

  • 解耦了数据和节点之间的关系,简化了节点扩容和收缩难度
  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据
  • 支持节点,槽,键之间的映射查询,用于数据路由,在线伸缩等场景

集群功能限制

Redis集群相对单机在功能上存在一些限制:

  • key批量操作支持有限。如mset,mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset,mget等操作可能存在于多个节点因此不被支持
  • key事务操作支持有限。同理只支持多key在同一个节点上的事务操作
  • key作为数据分区的最小粒度。因此不能将一个大的键值对象如hash,list等映射到不同的节点
  • 不支持多数据库空间。集群模式下只能使用一个数据库空间,即db 0
  • 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构

搭建集群

三个步骤:准备节点,节点握手,分配槽

准备节点

通过脚本设置集群挂载目录,并启动容器

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
# 删除网络
docker network rm redis-cluster-net
# 创建docker redis集群网络
docker network create redis-cluster-net
# 获取其网关Gateway,这个网关写在下面的gateway变量
# docker network inspect redis-cluster-net | grep Gateway

# 主目录
dir_redis_cluster='/root/redis-conf/cluster/'
# docker redis集群网关
gateway=$(docker network inspect redis-cluster-net | grep -Po 'Gateway[" :]+\K[^"]+')
# 节点地址号 从2开始
idx=1

# 逐个创建各节点目录和配置文件
for port in `seq 6379 6384`; do
rm -rf ${dir_redis_cluster}/${port}
# 创建存放redis数据路径
mkdir -p ${dir_redis_cluster}/${port}/data;
# 通过模板个性化各个节点的配置文件
idx=$(($idx+1));
port=${port} ip=`echo ${gateway} | sed "s/1$/$idx/g"` \
envsubst < ${dir_redis_cluster}/redis-cluster.tmpl \
> ${dir_redis_cluster}/${port}/redis-${port}.conf
done

# 启动Redis容器
for port in `seq 6379 6384`; do
docker stop redis-${port}
docker rm redis-${port}
docker run --name redis-${port} --net redis-cluster-net -d --privileged=true \
-p ${port}:${port} -p 1${port}:1${port} \
-v ${dir_redis_cluster}/${port}/data:/data \
-v ${dir_redis_cluster}/${port}/redis-${port}.conf:/usr/local/etc/redis/redis.conf redis:5.0.2 \
redis-server /usr/local/etc/redis/redis.conf
done

配置文件模板 redis-cluster.tmpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 基本配置
## 开放端口
port ${port}
## 不作为守护进程
daemonize no
## 启用aof持久化模式
appendonly yes

# 集群配置
## 开启集群配置
cluster-enabled yes
## 存放集群节点的配置文件 系统自动建立
cluster-config-file nodes-${port}.conf
## 节点连接超时时间
cluster-node-timeout 50000
## 实际为各节点网卡分配ip
cluster-announce-ip ${ip}
## 节点映射端口
cluster-announce-port ${port}
## 节点总线端口
cluster-announce-bus-port 1${port}
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage yes

启动后,可以在容器的 /data 下执行 cat nodes-{port} 查看集群配置,也可以在 redis-cli 中通过 cluster nodes 查看(目前只能查看自己,因为节点还没有进行握手)

redis-cli –cluster命令自动创建集群

在Redis 5.0中已经不再支持redis-trib.rb,而是使用redis-cli来创建集群,使用该命令可以管理集群,创建主从节点,自动分配槽等

1
2
3
docker exec -it redis-6379 redis-cli --cluster create 172.18.0.2:6379 172.18.0.3:6380 172.18.0.4:6381 172.18.0.5:6382 172.18.0.6:6383 172.18.0.7:6384 --cluster-replicas 1
# --cluster-replicas是从节点个数
# redis-cli --cluster help 可以查看其他命令

手动创建集群

节点握手

节点握手指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程,由客户端发起命令 cluster meet {ip} {port}

1
2
3
4
5
cluster meet 172.18.0.3 6380
cluster meet 172.18.0.4 6381
cluster meet 172.18.0.5 6382
cluster meet 172.18.0.6 6383
cluster meet 172.18.0.7 6384

节点握手 meet 是一个异步命令,执行后会立刻返回,过程如下:

  1. 6379节点本地创建6380节点信息对象,发送meet消息
  2. 6380节点接受到meet消息,保存6379对象信息并回复pong消息
  3. 之后6379和6380定期发送ping/pong进行正常的节点通信

这里的meet/ping/pong都是Gossip协议通信的载体

我们只需要在集群内任意节点上执行 cluster meet 命令加入新节点,握手状态通过消息在集群内传播,这样其他节点会自动发现新节点并发起握手流程,最后执行 cluster nodes 命令确认6个节点都彼此感知并组成集群

节点握手后集群还不能正常工作,此时集群处于下线状态( cluster info 命令查看),需要分配所有16384个槽集群才进入在线状态

分配槽

1
2
3
4
5
6
7
# 将16384个槽分配到3个主节点去, 每个节点平均分的5461个槽
# 6379 0~5460
docker exec -it redis-6379 redis-cli -p 6379 cluster addslots {0..5460}
# 6380 5461~10920
docker exec -it redis-6380 redis-cli -p 6380 cluster addslots {5461..10920}
# 6381 10920~16383
docker exec -it redis-6381 redis-cli -p 6381 cluster addslots {10921..16383}

作为一个完整的集群,每个负责处理槽的节点应该都具有从节点,保证它出现故障时可以自动完成故障转移。集群模式下,Redis节点角色分为主节点和从节点。首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。使用 cluster replicate {nodeId} 让一个主节点称为从节点,命令在从节点执行,nodeId是主节点ID,通过 cluster nodes 获取节点ID信息

1
2
3
4
5
6
7
# cluster nodes获取节点ID信息
# 设置6382节点为6379节点的从节点
docker exec -it redis-6382 redis-cli -p 6382 cluster replicate 8b5af8174dcfe7704942573510cccef75e90419b
# 设置6383节点为6380节点的从节点
docker exec -it redis-6383 redis-cli -p 6383 cluster replicate 1a36ebef09a2ec389de47c35e279804a7f8d9a51
# 设置6384节点为6381节点的从节点
docker exec -it redis-6384 redis-cli -p 6384 cluster replicate 7f1ff266ab807b2450e16437d7ab5f3fb33576d2

此时通过 cluster nodes 可以查看集群的具体信息,包括主从节点的信息

节点通信

分布式存储中需要提供维护节点元数据信息的机制,元数据指:节点负责哪些数据,是否出现故障等状态信息。

常见的元数据维护方式:集中式和P2P方式。Redis集群使用P2P的Gossip(流言)协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群的完整的信息,流程如下:

  1. 集群中每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000,(因此Docker端口映射需要对通信端口号也做映射)
  2. 每个节点在固定周期内通过特定规则选择几个节点发送ping消息
  3. 接收到ping消息的节点用pong消息作为响应

不断的ping/pong消息在一段时间后能让整个集群知道全部节点的最新状态,包括节点故障,新节点加入,主从角色变化,槽信息变更等事件的发生

Gossip消息

常见的Gossip消息有ping,pong,meet和fail。

  • meet消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping,pong消息交换
  • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其他节点的状态数据
  • pong消息:当接收到ping,meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群自身状态的更新
  • fail消息:当节点判定集群内另一个节点下线后,会向集群内广播一个fail消息,其他节点接收到fail消息后把对应节点更新为下线状态

节点选择

可以根据带宽资源修改 cluster_node_timeout 参数来对系统负载进行调整

集群伸缩

这里时间原因我偷懒了,以后有这方面的需求,再来补充相关知识

使用 redis-cli --cluster help 命令查看 redis-cli --cluster 支持的命令(Redis 5.0之后)

Redis的集群伸缩可以抽象为槽和对应数据在不同节点之间灵活移动

扩容集群

三步:1.准备新节点;2.加入集群;3.迁移槽和数据

  1. 准备新节点略
  2. 加入集群
    • 手动:cluster meet ip port ,节点加入后还没有分配槽,后续可以为其迁移槽和数据实现扩容或者作为从节点使用
    • 自动:redis-cli --cluster add-node new_host:new_port existing_host:existing_port {--cluster-slave} {--cluster-master-id <arg>} ,后面参数是作为从节点并指定其主节点ID
  3. 迁移槽:如果上一步是手动迁移,则这一步需要配置,《Redis开发与运维》 P295-301

收缩集群

如果存在负责的槽,首先迁移槽,当下线节点不再负责槽或者本身是从节点,就可以通知集群内其他节点忘记下线节点,当所有节点忘记该节点后它就可以正常关闭

请求路由

请求重定向

集群模式下,Redis接收任何键相关命令时会首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。这个过程称为MOVED重定向

运行客户端时,使用 redis-cli -c 参数可以在执行命令时自动重定向到指定节点执行

1
2
3
4
5
6
7
8
[email protected]:/data# redis-cli
127.0.0.1:6379> set a aha
(error) MOVED 15495 172.18.0.4:6381
127.0.0.1:6379> exit
[email protected]:/data# redis-cli -c
127.0.0.1:6379> set a aha
-> Redirected to slot [15495] located at 172.18.0.4:6381
OK

cluster keyslot key :获取key的slot值

节点判断键命令是执行还是MOVED重定向,借助槽和节点映射的数组实现。根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端叫Dummy(傀儡)客户端。这种实现代码简单,对客户端协议影响小,只需要根据重定向信息再次发送请求即可。弊端就是每次执行键命令前要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销

Smart客户端

Smart客户端通过内部维护 slot -> node 的映射关系,本地就可以实现键到节点的查找,从而保证IO效率最大化,MOVED重定向用于负责协助Smart客户端更新 slot -> node 的映射关系

客户端的选择查看 https://redis.io/clients

JedisCluster

JedisCluster是Java的Redis Cluster Smart客户端,其过程,实现原理暂时略过(有需要再补充,看书或者源码)

故障转移

故障发现

集群通过ping/pong消息实现节点通信,通信内容包括主从状态,节点故障等。与Redis Sentinel一样,节点故障也分为主观下线和客观下线

主观下线:

客观下线:

​ 每次ping/pong消息的消息体都会携带1/10的其他节点状态数据,包括故障节点的数据。当半数以上持有槽的主节点都标记某个节点是主管下线时,触发客观下线流程。每个节点接受到其他节点下线状态(pfail状态),都会触发客观下线,计算有效下线报告数量,如果大于槽节点总数的一半,就更新为客观下线,并向集群广播下线节点的fail消息

故障恢复

故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点选出一个替换它,流程:

  1. 资格检查:每个从节点检查最后与主节点断线时间,如果从节点与主节点断线时间超过 cluster-node-time * cluster-slave-validity-factor ,则当前从节点不具备故障转移资格。参数 cluster-slave-validity-factor 用于从节点的有效因子,默认为10
  2. 准备选举时间:采用延迟触发机制,复制偏移量越大,说明从节点延迟越低,应该选择延迟低的从节点提前触发选举流程,每个从节点获取自己的复制偏移量,然后与其他从节点复制偏移量比较,进行排名,排名最前的优先发起选举(同样会延迟,时间为 failover_auth_time)
  3. 发起选举:当从节点定时任务检测到到达故障选举时间 failover_auth_time 后,发起选举流程如下:
    1. 更新配置纪元(只增不减,当前主节点的版本,所有主节点配置纪元都不相等,从节点会复制主节点配置纪元,整个集群维护一个全局的配置纪元记录集群内所有主节点配置纪元的最大版本)
    2. 广播选举消息 FAILOVER_AUTH_REQUEST
  4. 选举投票:只有持有槽的主节点才会处理故障选举消息 FAILOVER_AUTH_REQUEST,每个持有槽的节点在一个配置纪元内都有唯一一张选票,当接到第一个请求投票的从节点消息时回复 FAILOVER_AUTH_ACK 消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略
    • 在开始投票之后的 cluster-node-timeout * 2 时间内没有获取足够的投票(N/2+1),则本次选举作废。从节点对配置纪元自增并发起下一轮投票直到成功
  5. 替换主节点:
    1. 当前从节点取消复制变为主节点
    2. 执行 clusterDelSlot 操作撤销故障主节点负责的槽, clusterAddSlot 把这些槽委派给自己
    3. 向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息

集群运维

  • 保证集群完整性:分配完16384个槽
  • 带宽消耗:集群Gossip消息通信本身会消耗带宽,官方建议集群最大规模在1000以内
  • Pub/Sub广播问题:频繁应用广播功能时避免在大量节点的集群内使用,否则严重消耗集群内网络带宽,建议使用Sentinel结构
  • 集群倾斜:注意节点槽分配平均,不同槽对应键数量分布问题,集合对象包含大量元素,内存相关配置不一致等问题
  • 集群读写分离:集群模式下从节点不接受任何读写请求,只负责复制主节点做主节点的备份,发送过来的键命令会重定向到负责槽的主节点。当需要使用从节点分担主节点读压力时,使用 readonly 命令打开客户端连接只读状态, slave-read-only 配置在集群下是无效的, readonly 是连接级别生效的,每次新建连接都要执行开启。 readwrite 命令关闭连接只读状态
    • 数据迁移:单机Redis数据想要迁移到集群环境,Redis 5.0 cluster命令提供 import host:port --cluster-from <arg> --cluster-copy --cluster-replace 命令进行迁移

问题与总结

redis通信出错,执行 meet 显示 disconnected

解决方案1:创建容器时使用 --net=host 使用host网络

--net=host 告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络。此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权限。容器进程可以跟主机其它 root 进程一样可以打开低范围的端口,可以访问本地网络服务比如 D-bus,还可以让容器做一些影响整个主机系统的事情,比如重启主机。因此使用这个选项的时候要非常小心。如果进一步的使用 --privileged=true,容器会被允许直接配置主机的网络堆栈。

解决方案2:使用bridge桥接网络,为redis.conf添加 cluster-announce-ip/port/bus-port 等参数

创建集群网络 docker network create redis-cluster-net ,通过 docker network inspect redis-cluster-net | grep Gateway 查找到网关ip 递增修改cluster-announce-ip参数 –redis-cluster-net 是自己创建的bridge网络模式 docker network create redis-cluster-net

Redis07-哨兵

发表于 2020-07-14 | 分类于 history , Redis | 阅读次数:

哨兵

Redis主从复制下,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时需要通知应用方更新主节点地址,这种应用场景这种故障处理方式一般是难以接受的。Redis 2.8开始提供了Redis Sentinel架构来解决这个问题

Redis Sentinel的高可用性

当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移,并通知应用方,从而实现高可用

Redis Sentinel与Redis主从复制模式只多了若干个Sentinel节点,所以Redis Sentinel没有针对Redis节点做特殊处理

整个故障转移有如下4个步骤:

  1. 主节点出现故障,此时从节点与主节点失去连接,主从复制失败
  2. 多个Sentinel节点对主节点的故障达成一致,选举出某个Sentinel节点作为领导者负责故障转移
  3. 如下图,Sentinel领导者执行了故障转移,它选出 slave-1 来作为新master节点,然后让其它从节点 slaveof slave-1(new-master),然后通知客户端之后,待原主节点恢复正常后,让原主节点 slaveof slave-1 即可,整个过程都是自动化完成

新的拓扑结构图如下

Redis Sentinel具有以下几个功能:

  • 监控:Sentinel节点会定期检测Redis数据节点,其余Sentinel节点是否可达
  • 通知:将故障转移的结果通知给应用方
  • 主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系
  • 配置提供者:在Redis Sentinel结构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息

Sentinel节点本身是独立的Redis节点,只是它不存储数据,只支持部分命令

Docker安装和部署

Sentinel中Redis数据节点没有任何特殊配置,略(需要注意从节点添加 slaveof 配置即可)

在Docker中部署一个Redis主节点,一个Redis从节点和一个Redis Sentinel:

(Redis 5.0后把slave改名为replica,两者其实是一个东西)

部署主节点

配置主节点:(主节点配置文件设置 protected-mode no允许外部客户端接入,配置密码requirepass,配置开启AOF,注释bind参数,或添加需要bind的IP地址,设置logfile)

1
2
3
4
5
6
7
port 6379
daemonize no
logfile "master-log.log"
protected-mode no
# bind 127.0.0.1
appendonly yes
requirepass asd123

启动主节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run -p 6379:6379 \
--name redis-master \
-v /root/redis-conf/redis-master/redis-master.conf:/usr/local/etc/redis/redis-master.conf \
-v /root/redis-conf/data/master-data:/data \
--privileged=true \
--restart=always \
-itd redis:5.0.2 redis-server /usr/local/etc/redis/redis-master.conf

# --privileged=true用于centOS 7,安全Selinux禁止了一些安全权限
# -p 端口映射 --name 指定容器名称
# -v /root/redis-conf/redis-master.conf:/usr/local/etc/redis/redis-master.conf 配置文件映射
# -v /root/data:/data 持久化文件映射和日志文件映射
# redis-server /usr/local/etc/redis/redis-master.conf 以容器内部的配置文件启动Redis

在主节点客户端输入 role 可以查看主从实例所属的角色,如果是主节点,会返回其所有从节点的信息

主节点输入 info replication 可以查看相关信息

部署从节点

部署从节点,可以写 slaveof, 在5.0后也可以写成 replicaof (5.0之后的版本同样支持 slaveof)

配置从节点:开启 replicaof,注释bind,设置 masterauth,设置 requirepass,修改端口,设置 protected-mode,

1
2
3
4
5
6
7
port 6380
# 配置两个从节点端口,6380,6381
# bind 127.0.0.1
logfile "slave-log.log" # 多个从节点修改这个名称,避免覆盖,或者启动中指定
replicaof 192.168.186.128 6379 # 你的主机地址,在docker中不能为127.0.0.1
masterauth asd123
requirepass asd123

启动从节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
docker run -p 6380:6380 \
--name redis-slave-01 \
-v /root/redis-conf/redis6380/redis-slave.conf:/usr/local/etc/redis/redis-slave.conf \
-v /root/redis-conf/data/6380:/data \
--privileged=true \
--restart=always \
-itd redis:5.0.2 redis-server /usr/local/etc/redis/redis-slave.conf

docker run -p 6381:6381 \
--name redis-slave-02 \
-v /root/redis-conf/redis6381/redis-slave.conf:/usr/local/etc/redis/redis-slave.conf \
-v /root/redis-conf/data/6381:/data \
--privileged=true \
--restart=always \
-itd redis:5.0.2 redis-server /usr/local/etc/redis/redis-slave.conf

# --privileged=true用于centOS 7,安全Selinux禁止了一些安全权限
# -p 端口映射 --name 指定容器名称
# -v /root/redis-conf/redis6381/redis-slave.conf:/usr/local/etc/redis/redis-slave.conf 配置文件映射
# -v /root/redis-conf/data/6381:/data 持久化文件映射
# redis-server /usr/local/etc/redis/redis-master.conf 以容器内部的配置文件启动Redis

如果需要启动多个从节点,修改 -p new-port:6379 和 --name new-slave-name 启动即可

如果需要部署多个从节点,需要把data文件夹和conf文件分开放置

从节点输入 info replication 或 role 可以查看相关信息

部署Sentinel节点

1
2
3
4
5
6
7
8
9
10
11
# Sentinel有自己专属的配置文件
port 26379
# 配置三个不同端口,26379.26380,26381
logfile "sentinel-log.log"
dir /data
sentinel monitor mymaster 192.168.186.128 6379 1
# 1代表主节点失败至少需要1个Sentinel节点同意,mymaster是主节点别名
sentinel auth-pass mymaster asd123
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

启动Sentinel节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
docker run --name redis-sentinel-01 \
-p 26379:26379 \
-v /root/redis-conf/redis26379/redis-sentinel.conf:/usr/local/etc/redis/redis-sentinel.conf \
-v /root/redis-conf/data/26379:/data \
--privileged=true \
-itd redis:5.0.2 redis-sentinel /usr/local/etc/redis/redis-sentinel.conf

docker run --name redis-sentinel-02 \
-p 26380:26380 \
-v /root/redis-conf/redis26380/redis-sentinel.conf:/usr/local/etc/redis/redis-sentinel.conf \
-v /root/redis-conf/data/26380:/data \
--privileged=true \
-itd redis:5.0.2 redis-sentinel /usr/local/etc/redis/redis-sentinel.conf

docker run --name redis-sentinel-03 \
-p 26381:26381 \
-v /root/redis-conf/redis26381/redis-sentinel.conf:/usr/local/etc/redis/redis-sentinel.conf \
-v /root/redis-conf/data/26381:/data \
--privileged=true \
-itd redis:5.0.2 redis-sentinel /usr/local/etc/redis/redis-sentinel.conf

如果需要部署多个Sentinel文件夹,需要把data文件夹和conf文件分开放置

info sentinel 可以查看哨兵的信息

问题:部署在同一个物理机上,哨兵之间不能互相检测,目前还没找到原因所在(2020-07-09)

(哨兵不应该部署在同一个物理机上,这样不算是真正的高可用)

在所有节点启动后,Sentinel节点配置文件会发生变化,会去掉一些默认配置,添加了一些从节点,Sentinel节点的配置信息

测试

docker stop redis-master 关闭主节点,此时会发现某个从节点变成了 master,再次启动原来的主节点,查看会发现它变成了 slave 节点

配置讲解

  • sentinel monitor <master-name> <ip> <port> <quorum> :监控目标主节点,quorum代表Sentinel节点集群要判断主节点最终不可达所需要的票数(建议值为Sentinel节点数除以2再加1),同时它也与Sentinel节点的领导者选举有关系
  • sentinel down-after-milliseconds <master-name> <times> :单位毫秒,每个Sentinel节点定时发送ping命令判断数据节点和其余Sentinel节点是否可达,如果回复超过该时间,则判断不可达
  • sentinel parallel-syncs <master-name> <nums> :当Sentinel节点集合对主节点故障达成一致时,Sentinel领导者节点会做故障转移操作,选出新的节点,原来的从节点会向新主节点发起复制操作,parallel-syncs就是用于限制在一次故障转移后,每次向新主节点发起复制操作的从节点个数
  • sentinel failover-timeout <master-name> <times> :故障转移超时时间,故障转移过程(包括从选出合适从节点到等待原主节点恢复后命令它去复制新主节点)超过该时间则判断为超时
  • sentinel auth-pass <master-name> <password> :此密码与对应主节点密码一致
  • sentinel notification-script <master-name> <script-path> :在故障转移期间,如果出现警告级别的Sentinel事件发生(如-sdown:客观下线,-odown:主观下线),会触发对应路径的脚本,并向脚本发送相应的事件参数,可以利用此参数把错误信息发送到邮件或短信
  • sentinel client-reconfig-script <master-name> <script-path> :故障转移结束后,触发对应路径的脚本,并向脚本发送故障转移结果的相关参数(参数为:<master-name> <role> <from-ip> <from-port> <to-ip> <tp-port> 分别为主节点名,Sentinel节点的角色(leader和observer),原主节点地址信息,新主节点地址信息)
  • Sentinel可以监控多个主节点,配置不同的主节点名称即可
  • Sentinel节点可以动态配置参数,使用命令 sentinel set <param> <value> ,支持如下参数:

Sentinel 的 set 命令只对当前Sentinel节点有效,会立刻刷新到配置文件(数据节点执行 config rewrite 才会刷新)

API

  • sentinel masters :查看所有被监控的主节点状态以及相关的统计信息,可以添加参数主节点名称查看对应主节点的信息

  • sentinel slaves <master name> :查看从节点状态的统计信息

  • sentinel sentinels <master name> :查看sentinel节点集合(不包含当前)

  • sentinel get-master-addr-by-name <master name> :返回指定 <master name> 主节点的IP地址和端口

  • sentinel reset <pattern> :当前sentinel节点对符合样式的主节点的配置进行重置,包含清除主节点相关状态(如故障转移),重新发现从节点和Sentinel节点

  • sentinel failover <master name> :对指定名称的主节点进行强制故障转移,其他Sentinel节点在转移完成后更新自身配置

  • sentinel ckquorum <master name> :检测当前可达Sentinel节点总数是否达到 quorum 的个数

  • sentinel flushconfig :将Sentinel节点的配置强制刷到磁盘上

  • sentinel remove <master name> :取消当前Sentinel节点对指定主节点的监控

  • sentinel monitor <master name> <ip> <port> <quorum> :添加对主节点的监控

  • sentinel set <master name> :动态修改Sentinel节点配置选项,略

  • sentinel is-master-down-by-addr :Sentinel节点间用来交换对主节点是否下线的判断,根据参数不同还可以作为Sentinel领导者选举的通信方式

实现原理

三个定时监控任务

  • 每10秒每个Sentinel节点会向主节点发送info命令获取最新的拓扑结构
    • 向主节点发送info获取从节点的信息
    • 当有新的从节点加入时可以立刻感知
    • 节点不可达或者故障转移时,通过info命令实时更新节点拓扑信息
  • 每隔2秒,每个Sentinel节点会向Redis数据节点的 __sentinel__:hello 频道发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息。每个Sentinel节点都会订阅该频道,用于了解其他Sentinel节点以及它们对主节点的判断
    • 该频道用于发现新的Sentinel节点,以及Sentinel节点之间交换主节点状态,作为后面客观下线以及领导者选举的依据
  • 每隔1秒,每个Sentinel节点向主节点,从节点和其余Sentinel节点发送ping命令做一次心跳检测,来确认这些节点是否可达

主观下线和客观下线

  • 主观下线:第三个定时任务每秒向其余节点发送ping命令,如果这些节点超过 down-after-milliseconds 没有进行回复,Sentinel节点会对该节点做失败判定,这个行为叫做主观下线
  • 客观下线:当Sentinel主观下线的节点是主节点时,该Sentinel节点通过 sentinel is-master-down-by-addr 命令向其他Sentinel节点询问对主节点的判断,当超过 quorum 个数,Sentinel节点认为该主节点确实有问题,会做出客观下线的决定
  • sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid> :current_epoch:当前配置纪元,runid:若为*,则用于Sentinel判定主节点下线,若为当前Sentinel节点的runid,则用于发送给目标Sentinel节点希望其同意自己成为领导者
    • 返回结果包含3个参数:
    • down_state:目标Sentinel节点对于主节点下线判断,1为下线,0为在线
    • leader_runid:当leader_runid等于*,代表返回结果用来做主节点是否不可达,若为具体runid,代表目标节点同意runid成为领导者
    • leader_epoch:领导者纪元

领导者Sentinel节点选举

  1. 每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观下线的时候,会向其他Sentinel节点发送 sentinel is-master-down-by-addr 命令,请求将自己设置为领导者
  2. 收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的请求成为领导者命令,则同意该请求,否则拒绝
  3. 如果该Sentinel节点发现自己的票数已经大于等于max(quorum, num(sentinel)/2 + 1),则它成为领导者
  4. 如果此过程没有选出一个领导者,则进入下一次选举

故障转移

在从节点列表选出一个节点作为新的主节点,选择方法:

  1. 过滤:主观下线,断线,5秒内没有回复过Sentinel节点ping响应,与主节点失联超过 down-after-milliseconds*10 秒
  2. 选择slave-priority最高的从节点列表,存在则返回,不存在则继续
  3. 选择复制偏移量最大的从节点(复制得最完整),如果存在则返回,不存在则继续
  4. 选择runid最小的从节点

后记

  1. 单机部署多个从节点,每个从节点必须使用不同的端口以及端口映射,否则Sentinel节点无法识别,会出现奇怪的错误
  2. Sentinel节点无法启动,查看docker logs日志显示不能打开Sentinel节点log file, Can't open the log file: Permission denied ,则在本地 /root/data/sentinel-data/26379 下新建一个 sentinel-log.log 文件,并修改其权限

Redis06-内存

发表于 2020-07-03 | 分类于 history , Redis | 阅读次数:

内存

info memory 查看内存情况

自身内存指Redis进程自身内存,对象内存指用户数据,包括键和值的内存,缓冲内存指客户端缓冲,复制积压缓冲区和AOF缓冲区的内存。内存碎片跟操作系统内存分配有关(Redis使用jemalloc)

内存管理

设置内存上限

Redis使用 maxmemory 参数来限制最大可用内存,当超出上限时使用LRU等删除策略释放空间

内存回收策略

  • 删除过期键对象:分为两种情况删除
    • 惰性删除:当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,才执行删除操作兵返回空。这种策略出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除
    • 定时任务删除:默认每秒运行10次(通过配置hz控制),流程如下图,每次随机检查20个键,当发现过期时删除对应键,如果超过检查数25%的键过期,循环执行直到不足25%或运行超时为止,慢模式下超时时间为25毫秒,若超时,则触发内部事件以快模式运行,快模式超时时间1毫秒,且2秒内只能运行一次

当内存超出maxmemory会触发溢出策略,由参数 maxmemory-policy 参数控制,Redis支持6种策略

  • noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(OOM),Redis此时只响应读操作
  • volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止,如果没有可删除的键对象,回退到noeviction策略
  • allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时,直到腾出足够空间
  • allkeys-random:随机删除所有键,直到腾出足够空间
  • volatile-random:随机删除过期键,直到腾出足够空间
  • volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据,如果没有,则回退到noeviction

内存优化

Redis存储的所有值对象在内部定义为redisObject结构体,如下:

共享对象池

Redis内部维护 [0-9999] 的整数对象池,用于节约内存,set foo 100 set bar 100 之后,它们指向整数共享对象池,节省了内存开销

注意,设置了 maxmemory 并启用LRU相关淘汰策略后,Redis会禁止使用共享对象池(lru计时时钟不准确)。ziplist编码的值对象,同样不能使用共享对象池(与内部结构有关)

字符串优化

Redis内部字符串结构如下图所示

O(1)时间复杂度的获取,可用于保存字节数组,内部实现预分配机制(需要注意预分配带来的内存浪费,在append数据追加时关注内存情况),惰性删除机制(字符串缩减后空间不释放,作为预分配空间保留)

编码优化

底层编码在第一章有说明,内部编码转换只能由小内存编码向大内存编码转换,不可逆(因为大内存向小内存转换消耗CPU,得不偿失)

ziplist

  • zlbytes记录整个压缩列表长度(int-32类型)
  • zltail记录距离尾节点的偏移量(int-32类型)
  • zllen:记录压缩链表节点数量(int-16)
  • prev_entry_bytes_length:记录前一个节点所占空间
  • encoding:标示当前节点编码和长度,前两位表示编码类型:字符串/整数,其余位表示数据长度
  • zlend:记录列表结尾,1字节长

intset

intset对写入的整数进行排序,O(log(n))时间复杂度实现查找和去重,encoding表示整数类型,有3种:int-16,int-32和int-64

控制键数量

Redis存储大量键的时候,可以考虑使用hash结构降低键数量,注意只能使用ziplist底层结构的hash类型来实现内存的节省

Redis05-复制

发表于 2020-07-02 | 分类于 history , Redis | 阅读次数:

复制

在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求,Redis也提供了复制功能,实现了相同数据的多个Redis副本

配置

建立复制

参与复制的Redis实例分为主节点(master)和从节点(slave)。默认情况下,Redis都是主节点,每个从节点只能有一个主节点,而主节点可以同时具有多个从节点,复制的数据流是单向的,只能由主节点复制到从节点

配置复制的方式有以下三种:

  • 在配置文件中加入slaveof {masterHost} {masterPort}随Redis启动生效
  • 在redis-server启动命令中加入–slaveof {masterHost} {masterPort}生效
  • 直接使用命令:slaveof {masterHost} {masterPort}生效

使用 info replication 查看复制相关状态

断开复制

在从节点执行 slaveof no one 来断开与主节点复制关系,断开不会抛弃原有数据,只是无法再获取主节点上的数据变化,可以使用 slaveof 来切换主节点,切换新主节点会删除原有主节点的所有数据

安全性

主节点可以通过 requirepass 参数来进行密码验证,所有客户端访问必须使用 auth 命令实行校验,从节点需要配置 masterauth 参数来与主节点密码(requirepass的值)保持一致,这样从节点才可以正确连接到主节点并发起复制操作

只读

默认情况下,从节点使用 slave-read-only=yes 配置为只读模式。由于复制只能主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致。因此建议线上不要修改从节点的只读模式

传输延迟

复制时的网络延迟也是需要考虑到问题,Redis提供了 repl-disable-tcp-nodelay 参数来控制是否关闭TCP_NODELAY,默认为no

  • 当关闭时,主节点产生的命令数据无论大小都会及时发送给从节点,这样主从之间延迟会变小,但会增加网络带宽的消耗。适用于主从之间网络环境良好的场景,如同机架或同机房部署
  • 当开启时,主节点会合并较小的TCP数据包从而节省带宽。默认发送时间间隔取决于Linux内核,一般默认为40毫秒。这种配置节省了带宽但是增大了主从之间的延迟

拓扑

Redis的复制拓扑结构可以支持单层或多层复制关系,分为一主一从结构,一主多从结构和树状主从结构

  • 一主一从结构:用于主节点出现宕机时从节点提供故障转移支持。当应用写命令并发量较高且需要持久化时,可以只在从节点上开启AOF。(需要注意主节点如果没有开启AOF,要脱机的话需要避免自动重启操作,否则从节点因为复制主节点会导致数据也清空,安全的做法是从节点上执行 slave no one 之后再重启主节点
  • 一主多从结构:多个从节点来实现读写分离
  • 树状主从结构:从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下复制,可以有效降低主节点负载和需要传送给从节点的数据量

原理

复制过程

发送ping命令主要检测主从之间网络套接字是否可用,以及主节点当前是否可接受处理命令

第一次同步数据集时,主节点会把所有的数据发送给从节点,后续主节点会持续把写命令发送给从节点,保持主从数据一致性

数据同步

Redis在2.8版本开始使用psync命令完成主从数据同步,支持全量复制和部分复制,当主从复制过程由于某些原因造成数据丢失,可以使用部分复制把丢失的数据发送给从节点

部分复制有如下3个组件支持:

  • 复制偏移量:主从节点各自维护,从节点每秒上传自身偏移给主节点,通过对比可以判断主从数据的一致, info replication 的 master_repl_offset 查看
  • 复制积压缓冲区(repl-backlog-buffer):保存在主节点上的一个固定长度的队列,默认是1MB,主节点响应写命令时会把命令发送给从节点和复制积压缓冲区
  • 主节点运行ID: Redis节点运行后动态分配一个40位的16进制字符串作为运行ID, info server 查看当前节点的ID(用ip+port识别主节点不合适,主节点替换RDB/AOF文件重新运行,从节点再基于偏移复制数据是不安全的)

psync

psync {runId} {offset} 从节点发送psync给主节点,runId是主节点的运行ID,没有默认为?,offset是偏移量,第一次复制则为-1

  • 如果回复 +FULLRESYNC {runId} {offset},从节点触发全量复制
  • 回复 +CONTINUE,从节点触发部分复制
  • 回复 +ERR,主节点版本低于2.8,无法识别psync,从节点发送旧版的sync触发全量复制

全量复制

部分复制

心跳

主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,client list 的flags查看。主节点默认每隔10秒发送ping命令,判断从节点的存活性和连接状态,通过参数 repl-ping-slave-period 控制发送频率。从节点主线程每隔1秒发送 replconf ack {offset} 命令,用于实时监测主从节点网络状态,上报自身复制偏移量和保证从节点的数量和延迟性功能。主节点根据 replconf 命令判断从节点超时时间,体现在 info replication 的lag属性,如果超过 repl-timeout (默认60秒),则判断从节点下线并断开连接,如果此后从节点重新恢复,心跳检测会继续进行

异步复制

主节点把写命令发送给从节点的过程是异步完成的,主节点把写命令发送给客户端后就返回了,并不等待从节点复制完成

12…14
MakaLoo

MakaLoo

Beautifully struggle every day

137 日志
45 分类
263 标签
GitHub 知乎 E-Mail
© 2021 MakaLoo
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4