开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

开发规范-基于充血模型的DDD开发模式的架构分层及职责

发表于 2020-12-22

基于充血模型的DDD开发模式的架构分为四层,将原三层架构中的service层分为四层架构中的service层和domain层,将原三层架构中service层的业务逻辑转移到domain层

DDD四层架构

Controller层
  1. 接口暴露
  2. 简单的参数校验
  3. 统一的异常处理
Service层
  1. 连接domain与repository
  2. 跨domain聚合
  3. 非功能性/三方交互等工作
Domain层

domain entity,value object,domain event,domain factory

  1. 模型对象初始化
  2. 具体业务逻辑实现
  3. 值对象的数据一致性
Repository层
  1. DB交互
  2. 网关服务
  3. 缓存服务
  4. 链路监控
  5. …

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

距离 Java 开发者玩转 Serverless,到底还有多

发表于 2020-12-22

头图.png

作者 | 方剑(洛夜) Spring Cloud Alibaba 开源项目负责人/创始人之一

来源|阿里巴巴云原生公众号

导读:本文摘自 Spring Cloud Alibaba 开源项目创始团队成员方剑撰写的《深入理解 Spring Cloud 与实战》一书,主要讲述了 Java 微服务框架 Spring Boot/Cloud 这个事实标准下如何应对 FaaS 场景。

Serverless & FaaS

2019 年,O’Reilly 对 1500 名 IT 专业人员的调查中,有 40% 的受访者在采用 Serverless 架构的组织中工作。2020 年 DataDog 调查显示,现在有超过 50% 的 AWS 用户正在使用 Serverless 架构的 AWS Lambda。

Serverless 正在成为主流,于是就诞生了下面这幅图,从单体应用的管理到微服务应用的管理再到函数的管理。

1.png

Serverless 到目前为止还没有一个精准定义。Martin Fowler 在个人博客上有一篇《Serverless Architectures》文章,其对 Serverless 的的定义分成了 BaaS 或 FaaS。

2.png

Baas 是全称是 Backend-as-a-Service,后端即服务,FaaS 的全称是 Function-as-a-Service,函数即服务。

今天我们来聊聊 FaaS。这是维基百科对 FaaS 的定义:

函数即服务(FaaS)是一类云计算服务,它提供了一个平台,使客户可以开发,运行和管理应用程序功能,而无需构建和维护通常与开发和启动应用程序相关的基础架构。遵循此模型构建应用程序是实现 Serverless 架构的一种方法,通常在构建微服务应用程序时使用。

对于 Python、JavaScript 这种天生支持 Lambda 的开发语言,和 FaaS 简直是完美结合。Serverless Framework 的调研报告也很好地说明了这一点。NodeJS、Python 是 FaaS 使用率前二的语言。

3.png

我们知道,因为 JVM 占用的内存比较大,所以 Java 应用的启动会有点慢,不太适合 FaaS 这个场景,这也是 Java 在使用率上偏低的原因。

另外,对 Java 开发者来说 Spring Boot/Cloud 已经成为了事实标准,依赖注入是 Spring Framework 的核心,Spring Boot/Cloud 这个事实标准应对 FaaS 这个场景,会碰撞出怎么样的火花呢?这就是今天我们要聊的 Spring Cloud Function。

Java Function

在对 Spring Cloud Function 介绍之前,我们先来看 Java 里的核心函数定义。

JDK 1.8 推出了新特性 Lambda 表达式,java.util.function 包下面提供了很多的函数。这 3 个函数尤为重要:

  1. java.util.function.Function: 需要一个参数,得到另一个结果.

1
2
3
4
csharp复制代码@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}

比如通过 Stream API 里的 map 方法可以通过 Function 把字符串从小写变成大写:

1
arduino复制代码Stream.of("a", "b", "c").map(String::toUpperCase);

这里的 map 方法需要一个 Function 参数:

1
javascript复制代码<R> Stream<R> map(Function<? super T, ? extends R> mapper);
  1. java.util.function.Consumer: 需要一个参数进行操作,无返回值。

1
2
3
4
csharp复制代码@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}

比如通过 Stream API 里的 forEach 方法遍历每个元素,做对应的业务逻辑处理:

1
2
3
4
5
6
arduino复制代码RestTemplate restTemplate = new RestTemplate();
Stream.of("200", "201", "202").forEach(code -> {
ResponseEntity<String> responseEntity =
restTemplate.getForEntity("http://httpbin.org/status/" + code, String.class);
System.out.println(responseEntity.getStatusCode());
});
  1. java.util.function.Supplier: 得到一个结果,无输入参数。

1
2
3
4
csharp复制代码@FunctionalInterface
public interface Supplier<T> {
T get();
}

比如自定义 Supplier 可以返回随机数:

1
2
3
4
5
6
7
ini复制代码Random random = new Random();

Supplier supplier100 = () -> random.nextInt(100);
Supplier supplier1000 = () -> random.nextInt(1000);

System.out.println(supplier100.get());
System.out.println(supplier1000.get());

Spring Cloud Function

Java Function 的编程模型非常简单,本质上就是这 3 个核心函数:

  • Supplier
  • Function<I, O>
  • Consumer

Spring Cloud Function 是 Spring 生态跟 Serverless(FaaS) 相关的一个项目。它出现的目的是增强 Java Function,主要体现在这几点:

  • 统一云厂商的 FaaS 编程模型: Spring Cloud Function 的口号是 “Write Once, Run Anywhere”。我们写的 Spring Cloud Function 代码可以运行在本地、各个云厂商(AWS Lambda, GCP Cloud Functions, Azure Functions)。
  • 自动类型转换: 理解过 Spring MVC 或者 Spring Cloud Stream 的同学肯定对 HttpMessageConverter 或者 MessageConverter 模型,这个转换器的作用是将 HTTP BODY(或者 Message Payload)、HTTP Query Parameter、HTTP HEADER(或者 Message Header)自动转换成对应的 POJO。有了这个特性后,我们就无需关注函数的入参和返回值,用 String 参数就可以获取原始的入参信息,用 User 这个 POJO 参数就可以将原始的入参参数自动转换成 User 对象。
  • 函数组合: 可以让多个函数之间进行组合操作。
  • 函数管理: 新增 FunctionCatalog、FunctionRegistry 接口用于 Function 的管理。管理 ApplicationContext 内的 Function,动态注册 Function 等操作。
  • Reactive 支持: Spring Cloud Function 新增比如 FluxFunction、FluxSupplier、FunctionConsumer 这种 Reactive 函数。
  • 自动跟 Spring 生态内部原有的组件进行深度集成:
+ Spring Web/Spring WebFlux: 一次 HTTP 请求是一次函数调用。
+ Spring Cloud Task: 一次任务执行是一次函数调用。
+ Spring Cloud Stream: 一次消息消费/生产/转换是一次函数调用。

4.png

这里再多介绍统一云厂商的 FaaS 编程模型,让大家对 Spring Cloud Function 更有体感。

AWS Lambda 是第一个是提供 FaaS 服务的云厂商,RequestStreamHandler 是 AWS 提供的针对 Java 开发者的接口,需要实现这个接口:

1
2
3
4
5
java复制代码public class HandlerStream implements RequestStreamHandler {
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException
{
...

Azure Functions 针对 Java 开发者提供了 @HttpTrigger 注解:

1
2
3
4
5
6
7
less复制代码public class Function {
public String echo(@HttpTrigger(name = "req",
methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS)
String req, ExecutionContext context) {
...
}
}

从这两段代码可以看出,不同的云厂商要编写不同的代码。如果要变换云厂商,这个过程会很痛苦。

另外,无论是 AWS、Azure 或者 GCP 提供的接口或注解,他们没有任何 Spring 上下文相关的初始化逻辑。如果我们是一个 Spring Boot/Cloud 应用迁移到 FaaS 平台,需要添加 Spring 上下文初始化逻辑等改动量。

Spring Cloud Function 的出现就是为了解决这些问题。

Spring Cloud Function 的使用

Spring Cloud Function & Spring Web:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码@SpringBootApplication
public class SpringCloudFunctionWebApplication {

public static void main(String[] args) {
SpringApplication.run(SpringCloudFunctionWebApplication.class, args);
}

@Bean
public Function<String, String> upperCase() {
return s -> s.toUpperCase();
}

@Bean
public Function<User, String> user() {
return user -> user.toString();
}

}

访问对应的 Endpoint:

1
2
3
4
shell复制代码$ curl -XPOST -H "Content-Type: text/plain" localhost:8080/upperCase -d hello
HELLO
$ curl -XPOST -H "Content-Type: text/plain" localhost:8080/user -d '{"name":"hello SCF"}'
User{name\u003d\u0027hello SCF\u0027}

Spring Cloud Function & Spring Cloud Stream:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码@SpringBootApplication
public class SpringCloudFunctionStreamApplication {

public static void main(String[] args) {
SpringApplication.run(SpringCloudFunctionStreamApplication.class, args);
}

@Bean
public Function<String, String> uppercase() {
return x -> x.toUpperCase();
}

@Bean
public Function<String, String> prefix() {
return x -> "prefix-" + x;
}

}

加上 function 相关的配置(针对 input-topic 上的每个消息,payload 转换大写后再加上 prefix- 前缀,再写到 output-topic 上):

1
2
3
4
5
6
ini复制代码spring.cloud.stream.bindings.input.destination=input-topic
spring.cloud.stream.bindings.input.group=scf-group

spring.cloud.stream.bindings.output.destination=output-topic

spring.cloud.stream.function.definition=uppercase|prefix

Spring Cloud Function & Spring Cloud Task:

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
typescript复制代码@SpringBootApplication
public class SpringCloudFunctionTaskApplication {

public static void main(String[] args) {
SpringApplication.run(SpringCloudFunctionTaskApplication.class, args);
}

@Bean
public Supplier<List<String>> supplier() {
return () -> Arrays.asList("200", "201", "202");
}

@Bean
public Function<List<String>, List<String>> function() {
return (list) ->
list.stream().map( item -> "prefix-" + item).collect(Collectors.toList());
}

@Bean
public Consumer<List<String>> consumer() {
return (list) -> {
list.stream().forEach(System.out::println);
};
}

}

加上 function 相关的配置(Supplier 模拟任务的输入源,Function 模拟对任务输入源的处理,Consumer 模拟处理对 Function 处理输入源后的数据):

1
2
3
ini复制代码spring.cloud.function.task.function=function
spring.cloud.function.task.supplier=supplier
spring.cloud.function.task.consumer=consumer

《深入理解 Spring Cloud 与实战》一书正式开始预售啦,这是一本深入剖析 Spring Cloud 全家桶的书籍,涉及以下内容:

  • Spring Boot 核心特性
  • Spring Cloud 服务注册/服务发现原理剖析
  • 双注册双订阅模型完成 Eureka 迁移至 Nacos 的案例
  • 负载均衡:Spring Cloud LoadBalancer 和 Netflix Ribbon
  • Dubbo Spring Cloud:Spring Cloud 与 Apache Dubbo 的融合
  • Spring Cloud 灰度发布案例
  • Spring 体系配置,动态刷新加载原理剖析
  • Spring Cloud Circuit Breaker 抽象以及 Sentinel、Hystrix、Resilience4j 熔断器对比
  • Spring 体系消息编程模型剖析
  • Spring Cloud Data Flow 完成批处理和流处理任务
  • Spring Cloud Gateway 网关剖析
  • Spring 与 Serverless 的融合

点击了解详情,更有机会赢取免费图书!

作者简介

方剑 Spring Cloud Alibaba 开源项目负责人/创始人之一。《深入理解 Spring Cloud 与实战》作者,Apache RocketMQ Committer,Alibaba Nacos Committer。曾在个人博客上编写过《SpringMVC 源码分析系列》、《SpringBoot 源码分析系列》文章,目前,关注微服务、云原生、Kubernetes。

《深入理解 Spring Cloud 与实战》作者方剑将出席1 月 9 日 Spring Cloud Alibaba 上海站,现场活动也有互动赠书活动,欢迎来现场与作者面基。点击链接参与活动报名:www.huodongxing.com/event/25765…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

5 穿过拥挤的人潮,Spring已为你制作好高级赛道 ✍前

发表于 2020-12-22

分享、成长,拒绝浅藏辄止。关注公众号【BAT的乌托邦】,回复关键字专栏有Spring技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 www.yourbatman.cn 收录。

✍前言

你好,我是YourBatman。

上篇文章 大篇幅把Spring全新一代类型转换器介绍完了,已经至少能够考个及格分。在介绍Spring众多内建的转换器里,我故意留下一个尾巴,放在本文专门撰文讲解。

为了让自己能在“拥挤的人潮中”显得不(更)一(突)样(出),A哥特意准备了这几个特殊的转换器助你破局,穿越拥挤的人潮,踏上Spring已为你制作好的高级赛道。

版本约定

  • Spring Framework:5.3.1
  • Spring Boot:2.4.0

✍正文

本文的焦点将集中在上文留下的4个类型转换器上。

  • StreamConverter:将Stream流与集合/数组之间的转换,必要时转换元素类型

这三个比较特殊,属于“最后的”“兜底类”类型转换器:

  • ObjectToObjectConverter:通用的将原对象转换为目标对象(通过工厂方法or构造器)
  • IdToEntityConverter:本文重点。给个ID自动帮你兑换成一个Entity对象
  • FallbackObjectToStringConverter:将任何对象调用toString()转化为String类型。当匹配不到任何转换器时,它用于兜底

默认转换器注册情况

Spring新一代类型转换内建了非常多的实现,这些在初始化阶段大都被默认注册进去。注册点在DefaultConversionService提供的一个static静态工具方法里:

static静态方法具有与实例无关性,我个人觉得把该static方法放在一个xxxUtils里统一管理会更好,放在具体某个组件类里反倒容易产生语义上的误导性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码DefaultConversionService:

public static void addDefaultConverters(ConverterRegistry converterRegistry) {
// 1、添加标量转换器(和数字相关)
addScalarConverters(converterRegistry);
// 2、添加处理集合的转换器
addCollectionConverters(converterRegistry);

// 3、添加对JSR310时间类型支持的转换器
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());

// 4、添加兜底转换器(上面处理不了的全交给这几个哥们处理)
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}

}

该静态方法用于注册全局的、默认的转换器们,从而让Spring有了基础的转换能力,进而完成绝大部分转换工作。为了方便记忆这个注册流程,我把它绘制成图供以你保存:

特别强调:转换器的注册顺序非常重要,这决定了通用转换器的匹配结果(谁在前,优先匹配谁)。

针对这幅图,你可能还会有疑问:

  1. JSR310转换器只看到TimeZone、ZoneId等转换,怎么没看见更为常用的LocalDate、LocalDateTime等这些类型转换呢?难道Spring默认是不支持的?
    1. 答:当然不是。 这么常见的场景Spring怎能会不支持呢?不过与其说这是类型转换,倒不如说是格式化更合适。所以会在后3篇文章格式化章节在作为重中之重讲述
  2. 一般的Converter都见名之意,但StreamConverter有何作用呢?什么场景下会生效
    1. 答:本文讲述
  3. 对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何
    1. 答:本文讲述

StreamConverter

用于实现集合/数组类型到Stream类型的互转,这从它支持的Set<ConvertiblePair> 集合也能看出来:

1
2
3
4
5
6
7
8
9
java复制代码@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>();
convertiblePairs.add(new ConvertiblePair(Stream.class, Collection.class));
convertiblePairs.add(new ConvertiblePair(Stream.class, Object[].class));
convertiblePairs.add(new ConvertiblePair(Collection.class, Stream.class));
convertiblePairs.add(new ConvertiblePair(Object[].class, Stream.class));
return convertiblePairs;
}

它支持的是双向的匹配规则:

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/**
* {@link StreamConverter}
*/
@Test
public void test2() {
System.out.println("----------------StreamConverter---------------");
ConditionalGenericConverter converter = new StreamConverter(new DefaultConversionService());

TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(Set.class);
TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Stream.class);
boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp);
System.out.println("是否能够转换:" + matches);

// 执行转换
Object convert = converter.convert(Collections.singleton(1), sourceTypeDesp, targetTypeDesp);
System.out.println(convert);
System.out.println(Stream.class.isAssignableFrom(convert.getClass()));
}

运行程序,输出:

1
2
3
4
java复制代码----------------StreamConverter---------------
是否能够转换:true
java.util.stream.ReferencePipeline$Head@5a01ccaa
true

关注点:底层依旧依赖DefaultConversionService完成元素与元素之间的转换。譬如本例Set -> Stream的实际步骤为:

也就是说任何集合/数组类型是先转换为中间状态的List,最终调用list.stream()转换为Stream流的;若是逆向转换先调用source.collect(Collectors.<Object>toList())把Stream转为List后,再转为具体的集合or数组类型。

说明:若source是数组类型,那底层实际使用的就是ArrayToCollectionConverter,注意举一反三

使用场景

StreamConverter它的访问权限是default,我们并不能直接使用到它。通过上面介绍可知Spring默认把它注册进了注册中心里,因此面向使用者我们直接使用转换服务接口ConversionService便可。

1
2
3
4
5
6
7
8
9
10
java复制代码@Test
public void test3() {
System.out.println("----------------StreamConverter使用场景---------------");
ConversionService conversionService = new DefaultConversionService();
Stream<Integer> result = conversionService.convert(Collections.singleton(1), Stream.class);

// 消费
result.forEach(System.out::println);
// result.forEach(System.out::println); //stream has already been operated upon or closed
}

运行程序,输出:

1
2
java复制代码----------------StreamConverter使用场景---------------
1

再次特别强调:流只能被读(消费)一次。

因为有了ConversionService提供的强大能力,我们就可以在基于Spring/Spring Boot做二次开发时使用它,提高系统的通用性和容错性。如:当方法入参是Stream类型时,你既可以传入Stream类型,也可以是Collection类型、数组类型,是不是瞬间逼格高了起来。

兜底转换器

按照添加转换器的顺序,Spring在最后添加了4个通用的转换器用于兜底,你可能平时并不关注它,但它实时就在发挥着它的作用。

ObjectToObjectConverter

将源对象转换为目标类型,非常的通用:Object -> Object:

1
2
3
4
java复制代码@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
}

虽然它支持的是Object -> Object,看似没有限制但其实是有约定条件的:

1
2
3
4
5
java复制代码@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return (sourceType.getType() != targetType.getType() &&
hasConversionMethodOrConstructor(targetType.getType(), sourceType.getType()));
}

是否能够处理的判断逻辑在于hasConversionMethodOrConstructor方法,直译为:是否有转换方法或者构造器。代码详细处理逻辑如下截图:

此部分逻辑可分为两个part来看:

  • part1:从缓存中拿到Member,直接判断Member的可用性,可用的话迅速返回
  • part2:若part1没有返回,就执行三部曲,尝试找到一个合适的Member,然后放进缓存内(若没有就返回null)

part1:快速返回流程

当不是首次进入处理时,会走快速返回流程。也就是第0步isApplicable判断逻辑,有这几个关注点:

  1. Member包括Method或者Constructor
  2. Method:若是static静态方法,要求方法的第1个入参类型必须是源类型sourceType;若不是static方法,则要求源类型sourceType必须是method.getDeclaringClass()的子类型/相同类型
  3. Constructor:要求构造器的第1个入参类型必须是源类型sourceType

创建目标对象的实例,此转换器支持两种方式:

  1. 通过工厂方法/实例方法创建实例(method.invoke(source))
  2. 通过构造器创建实例(ctor.newInstance(source))

以上case,在下面均会给出代码示例。

part2:三部曲流程

对于首次处理的转换,就会进入到详细的三部曲逻辑:通过反射尝试找到合适的Member用于创建目标实例,也就是上图的1、2、3步。

step1:determineToMethod,从sourceClass里找实例方法,对方法有如下要求:

  • 方法名必须叫 "to" + targetClass.getSimpleName(),如toPerson()
  • 方法的访问权限必须是public
  • 该方法的返回值必须是目标类型或其子类型

step2:determineFactoryMethod,找静态工厂方法,对方法有如下要求:

  • 方法名必须为valueOf(sourceClass) 或者 of(sourceClass) 或者from(sourceClass)
  • 方法的访问权限必须是public

step3:determineFactoryConstructor,找构造器,对构造器有如下要求:

  • 存在一个参数,且参数类型是sourceClass类型的构造器
  • 构造器的访问权限必须是public

特别值得注意的是:此转换器不支持Object.toString()方法将sourceType转换为java.lang.String。对于toString()支持,请使用下面介绍的更为兜底的FallbackObjectToStringConverter。

代码示例

  • 实例方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码// sourceClass
@Data
public class Customer {
private Long id;
private String address;

public Person toPerson() {
Person person = new Person();
person.setId(getId());
person.setName("YourBatman-".concat(getAddress()));
return person;
}

}

// tartgetClass
@Data
public class Person {
private Long id;
private String name;
}

书写测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Test
public void test4() {
System.out.println("----------------ObjectToObjectConverter---------------");
ConditionalGenericConverter converter = new ObjectToObjectConverter();

Customer customer = new Customer();
customer.setId(1L);
customer.setAddress("Peking");

Object convert = converter.convert(customer, TypeDescriptor.forObject(customer), TypeDescriptor.valueOf(Person.class));
System.out.println(convert);

// ConversionService方式(实际使用方式)
ConversionService conversionService = new DefaultConversionService();
Person person = conversionService.convert(customer, Person.class);
System.out.println(person);
}

运行程序,输出:

1
2
3
java复制代码----------------ObjectToObjectConverter---------------
Person(id=1, name=YourBatman-Peking)
Person(id=1, name=YourBatman-Peking)
  • 静态工厂方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码// sourceClass
@Data
public class Customer {
private Long id;
private String address;
}

// targetClass
@Data
public class Person {

private Long id;
private String name;

/**
* 方法名称可以是:valueOf、of、from
*/
public static Person valueOf(Customer customer) {
Person person = new Person();
person.setId(customer.getId());
person.setName("YourBatman-".concat(customer.getAddress()));
return person;
}
}

测试用例完全同上,再次运行输出:

1
2
3
java复制代码----------------ObjectToObjectConverter---------------
Person(id=1, name=YourBatman-Peking)
Person(id=1, name=YourBatman-Peking)

方法名可以为valueOf、of、from任意一种,这种命名方式几乎是业界不成文的规矩,所以遵守起来也会比较容易。但是:建议还是注释写好,防止别人重命名而导致转换生效。

  • 构造器

基本同静态工厂方法示例,略

使用场景

基于本转换器可以完成任意对象 -> 任意对象的转换,只需要遵循方法名/构造器默认的一切约定即可,在我们平时开发书写转换层时是非常有帮助的,借助ConversionService可以解决这一类问题。

对于Object -> Object的转换,另外一种方式是自定义Converter<S,T>,然后注册到注册中心。至于到底选哪种合适,这就看具体应用场景喽,本文只是多给你一种选择

IdToEntityConverter

Id(S) –> Entity(T)。通过调用静态查找方法将实体ID兑换为实体对象。Entity里的该查找方法需要满足如下条件find[EntityName]([IdType]):

  1. 必须是static静态方法
  2. 方法名必须为find + entityName。如Person类的话,那么方法名叫findPerson
  3. 方法参数列表必须为1个
  4. 返回值类型必须是Entity类型

说明:此方法可以不必是public,但建议用public。这样即使JVM的Security安全级别开启也能够正常访问

支持的转换Pair如下:ID和Entity都可以是任意类型,能转换就成

1
2
3
4
java复制代码@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class, Object.class));
}

判断是否能执行准换的条件是:存在符合条件的find方法,且source可以转换为ID类型(注意source能转换成id类型就成,并非目标类型哦)

1
2
3
4
5
6
java复制代码@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
Method finder = getFinder(targetType.getType());
return (finder != null
&& this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0])));
}

根据ID定位到Entity实体对象简直太太太常用了,运用好此转换器的提供的能力,或许能让你事半功倍,大大减少重复代码,写出更优雅、更简洁、更易于维护的代码。

代码示例

Entity实体:准备好符合条件的findXXX方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Data
public class Person {

private Long id;
private String name;

/**
* 根据ID定位一个Person实例
*/
public static Person findPerson(Long id) {
// 一般根据id从数据库查,本处通过new来模拟
Person person = new Person();
person.setId(id);
person.setName("YourBatman-byFindPerson");
return person;
}

}

应用IdToEntityConverter,书写示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void test() {
System.out.println("----------------IdToEntityConverter---------------");
ConditionalGenericConverter converter = new IdToEntityConverter(new DefaultConversionService());

TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(String.class);
TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Person.class);
boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp);
System.out.println("是否能够转换:" + matches);

// 执行转换
Object convert = converter.convert("1", sourceTypeDesp, targetTypeDesp);
System.out.println(convert);
}

运行程序,正常输出:

1
2
3
java复制代码----------------IdToEntityConverter---------------
是否能够转换:true
Person(id=1, name=YourBatman-byFindPerson)

示例效果为:传入字符串类型的“1”,就能返回得到一个Person实例。可以看到,我们传入的是字符串类型的的1,而方法入参id类型实际为Long类型,但因为它们能完成String -> Long转换,因此最终还是能够得到一个Entity实例的。

使用场景

这个使用场景就比较多了,需要使用到findById()的地方都可以通过它来代替掉。如:

Controller层:

1
2
3
4
5
6
7
8
9
java复制代码@GetMapping("/ids/{id}")
public Object getById(@PathVariable Person id) {
return id;
}

@GetMapping("/ids")
public Object getById(@RequestParam Person id) {
return id;
}

Tips:在Controller层这么写我并不建议,因为语义上没有对齐,势必在代码书写过程中带来一定的麻烦。

Service层:

1
2
3
4
5
6
7
8
java复制代码@Autowired
private ConversionService conversionService;

public Object findById(String id){
Person person = conversionService.convert(id, Person.class);

return person;
}

Tips:在Service层这么写,我个人觉得还是OK的。用类型转换的领域设计思想代替了自上而下的过程编程思想。

FallbackObjectToStringConverter

通过简单的调用Object#toString()方法将任何支持的类型转换为String类型,它作为底层兜底。

1
2
3
4
java复制代码@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(Object.class, String.class));
}

该转换器支持CharSequence/StringWriter等类型,以及所有ObjectToObjectConverter.hasConversionMethodOrConstructor(sourceClass, String.class)的类型。

说明:ObjectToObjectConverter不处理任何String类型的转换,原来都是交给它了

代码示例

略。

ObjectToOptionalConverter

将任意类型转换为一个Optional<T>类型,它作为最最最最最底部的兜底,稍微了解下即可。

代码示例

1
2
3
4
5
6
7
8
java复制代码@Test
public void test5() {
System.out.println("----------------ObjectToOptionalConverter---------------");
ConversionService conversionService = new DefaultConversionService();
Optional<Integer> result = conversionService.convert(Arrays.asList(2), Optional.class);

System.out.println(result);
}

运行程序,输出:

1
2
java复制代码----------------ObjectToOptionalConverter---------------
Optional[[2]]

使用场景

一个典型的应用场景:在Controller中可传可不传的参数中,我们不仅可以通过@RequestParam(required = false) Long id来做,还是可以这么写:@RequestParam Optional<Long> id。

✍总结

本文是对上文介绍Spring全新一代类型转换机制的补充,因为关注得人较少,所以才有机会突破。

针对于Spring注册转换器,需要特别注意如下几点:

  1. 注册顺序很重要。先注册,先服务(若支持的话)
  2. 默认情况下,Spring会注册大量的内建转换器,从而支持String/数字类型转换、集合类型转换,这能解决协议层面的大部分转换问题。
    1. 如Controller层,输入的是JSON字符串,可用自动被封装为数字类型、集合类型等等
    2. 如@Value注入的是String类型,但也可以用数字、集合类型接收

对于复杂的对象 -> 对象类型的转换,一般需要你自定义转换器,或者参照本文的标准写法完成转换。总之:Spring提供的ConversionService专注于类型转换服务,是一个非常非常实用的API,特别是你正在做基于Spring二次开发的情况下。

当然喽,关于ConversionService这套机制还并未详细介绍,如何使用?如何运行?如何扩展?带着这三个问题,咱们下篇见。


✔✔✔推荐阅读✔✔✔

【Spring类型转换】系列:

  • 1. 揭秘Spring类型转换 - 框架设计的基石
  • 2. Spring早期类型转换,基于PropertyEditor实现
  • 3. 搞定收工,PropertyEditor就到这
  • 4. 上新了Spring,全新一代类型转换机制

【Jackson】系列:

  • 1. 初识Jackson – 世界上最好的JSON库
  • 2. 妈呀,Jackson原来是这样写JSON的
  • 3. 懂了这些,方敢在简历上说会用Jackson写JSON
  • 4. JSON字符串是如何被解析的?JsonParser了解一下
  • 5. JsonFactory工厂而已,还蛮有料,这是我没想到的
  • 6. 二十不惑,ObjectMapper使用也不再迷惑
  • 7. Jackson用树模型处理JSON是必备技能,不信你看

【数据校验Bean Validation】系列:

  • 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知
  • 2. Bean Validation声明式校验方法的参数、返回值
  • 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸
  • 4. Validator校验器的五大核心组件,一个都不能少
  • 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类
  • 6. 自定义容器类型元素验证,类级别验证(多字段联合验证)

【新特性】系列:

  • IntelliJ IDEA 2020.3正式发布,年度最后一个版本很讲武德
  • IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效
  • IntelliJ IDEA 2020.1正式发布,你要的Almost都在这!
  • Spring Framework 5.3.0正式发布,在云原生路上继续发力
  • Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容)
  • Spring改变版本号命名规则:此举对非英语国家很友好
  • JDK15正式发布,划时代的ZGC同时宣布转正

【程序人生】系列:

  • 蚂蚁金服上市了,我不想努力了
  • 如果程序员和产品经理都用凡尔赛文学对话……
  • 程序人生 | 春风得意马蹄疾,一日看尽长安花

还有诸如【Spring配置类】【Spring-static】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】…更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,也可加我fsx1056342982,交个朋友。

有些已完结,有些连载中。我是A哥(YourBatman),咱们下期再见

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Restful API 接口设计标准及规范

发表于 2020-12-22

RESTful概念

理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。REST指的是一组架构约束条件和原则。” 如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构。

REST本身并没有创造新的技术、组件或服务,而隐藏在RESTful背后的理念就是使用Web的现有特征和能力, 更好地使用现有Web标准中的一些准则和约束。虽然REST本身受Web技术的影响很深, 但是理论上REST架构风格并不是绑定在HTTP上,只不过目前HTTP是唯一与REST相关的实例。 所以我们这里描述的REST也是通过HTTP实现的REST。

RestfulAPI 导图

Restful导图

理解RESTful

要理解RESTful架构,需要理解Representational State Transfer这个词组到底是什么意思,它的每一个词都有些什么涵义。我们围绕资源展开讨论,从资源的定义、获取、表述、关联、状态变迁等角度,列举一些关键概念并加以解释。

  • 资源与URI
  • 统一资源接口
  • 资源的表述
  • 资源的链接
  • 状态的转移

资源与URI

URI 表示资源,资源一般对应服务器端领域模型中的实体类

  • URI 是地址也是资源
  • URI里边带上版本号、后缀来区分表述格式
  • 必备约定
    • 用名词、不用动词
    • 层级结构明确、用/来表示
    • 用?用来过滤资源

统一资源接口

标准HTTP方法包含:GET、POST、PUT、DELETE、Patch,他们的使用功能如下列表所示

方法作用列表

Get方法执行流程原理如下所示

Get方法执行流程图

Put 方法执行流程图如下所示

Put方法执行流程图

安全性和幂等性

  • 1、安全性:不会改变资源状态,可以理解为只读的;
  • 2、幂等性:执行1次和执行N次,对资源状态改变的效果是等价的。
接口 安全性 幂等性
GET √ √
POST × ×
PUT × √
DELETE × √

安全性和幂等性均不保证反复请求能拿到相同的response。以 DELETE 为例,第一次DELETE返回200表示删除成功,第二次返回404提示资源不存在,这是允许的。

资源的表述

什么是资源?什么是表述?

就本质而言,任何足够重要并被引用的事物都可以是资源。如果你的用户“想要建立指向它的超文本链接,指出或者反对关于它的断言,获取或者缓存它的表述,共另外的表述引用它的全部或者部分,给它增加注释信息,或者对它执行某些操作”,(源自《万维网的架构》),你都应该将它定义为资源。

每个资源必须拥有URL,在web上,我们使用URL来为每个资源提供一个全球唯一的地址,将一个事物赋以URL,它就会成为一个资源。

石榴可以是一个资源,但是你不可能通过互联网传输它,数据库中的一条记录可以是一个资源,并且可以通过互联网传输。

当客户端对一个资源发起一个Get请求的时候,服务器会以一种有效的方式提供一个采集了资源信息的文档作为回应。这种资源信息的文档就是一种表述,一种以机器可读的方式对资源当前的状态进行说明

资源有多重表述

一个资源可以有多种表述。比如,有的资源可以有整体概括性的表述,也可以有面面俱到的详细表述,又或者可能以JSON格式或者XML格式来表述同一个资源。

资源的连接

我们知道REST是使用标准的HTTP方法来操作资源的,但仅仅因此就理解成带CURD的Web数据库架构就太过于简单了。

这种反模式忽略了一个核心概念:”超媒体即应用状态引擎(hypermedia as the engine of application state)”。 超媒体是什么?

当你浏览Web网页时,从一个连接跳到一个页面,再从另一个连接跳到另外一个页面,就是利用了超媒体的概念:把一个个把资源链接起来.

要达到这个目的,就要求在表述格式里边加入链接来引导客户端。在《RESTful Web Services》一书中,作者把这种具有链接的特性成为连通性。下面我们具体来看一些例子。

下面展示的是github获取某个组织下的项目列表的请求,可以看到在响应头里边增加Link头告诉客户端怎么访问下一页和最后一页的记录。 而在响应体里边,用url来链接项目所有者和项目地址。

上面的例子展示了如何使用超媒体来增强资源的连通性。很多人在设计RESTful架构时,使用很多时间来寻找漂亮的URI,而忽略了超媒体。所以,应该多花一些时间来给资源的表述提供链接,而不是专注于”资源的CRUD”。

状态的转移

访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化;

互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生”状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是”表现层状态转化”;

客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;

博主GitHub地址

github.com/yuyue5945

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于NodeJS的高性能分布式游戏日志系统

发表于 2020-12-22

大纲:

  • 前言
  • 日志系统架构是怎样的
  • 游戏分析有什么内容
  • 为什么要自己架一个系统
  • FEN架构
    • 架构图
    • Fluentd
    • ElasticSearch
    • NodeJS
    • pusher
    • logger
    • analyser
    • 用户界面
  • 总结

前言

最近我司需要做一个统一的游戏日志系统,要求有一定的通用性,能应对公司所有的游戏业务。接下来分享一下这次日志系统的项目经验。

日志系统架构是怎样的

目前流行的日志系统为ELK,由Beats、Logstash、Elasticsearch、Kibana等组件共同实现,但万变不离其宗,一个基本的日志系统架构类似如下:

基本架构

游戏分析有什么内容

游戏分析,与其它服务系统不同的是,游戏内的系统可能是天马行空的,数据类型是多样的,甚至频繁变化的。我们要在变化中总结到不变的内容,例如系统经济产出,玩家物品消耗,商店购买等进行分析。所以这次的游戏日志系统要满足以下需求:

  • 记录游戏日志,并随时检索日志;
  • 分析玩家行为:玩家留存相关,玩家物品消耗,商店消耗等有一定复杂度的分析;
  • 能建立一个统一的日志系统:一次性满足未来游戏运营多样性。

为什么要自己架一个系统

虽然ELK在安装配置方面不算困难,插件众多,例如Filebeat,读log文件,过滤格式,转发,但谁来生产这些log文件,没有提及。实际上,业务具有多样性,只要有日志文件的地方,它就可以用。例如多数会使用Nginx进行日志收集。我们也需要考虑到日志生产者的问题,责权分离,需要单独一台机子进行日志采集。

游戏是一种技术与艺术结合的产品,数据庞杂,形态各异,光日志埋点也花不少功夫,但不能因此放弃治疗。好的游戏日志,还可以帮我们还原玩家玩家画像。游戏更新周期短,数据变化大,需要提供更实时参照报表,为非技术人员更好友的查询界面,才能更好的服务于游戏数据分析。ELK 在这方面,基本解决了采集和储存的问题,但实现分析方面还不能满足我们的需求。

经过一翻思索,我们可以用现有工具,粘合多个套件,所以,我们有了以下思路:

  • 日志采集器:
    利用Fluented作为日志文件采集器,生产者通过内网HTTP发送到采集器上,那每个生产者同一内网只要部署一个采集器即可,如果量特别大,可以多个,游戏的功能埋点可以统一;
  • 转发器:
    利用NodeJS进行 HTTP 转发即可,前提是能按顺序和分段读取日志文件,结合Fluented间接实现;
  • 接收器与实时分析:
    接收器可以用Koa实现,Redis进行缓存;同时用NodeJS另外一个进程分析和日志入库,分析行为,玩家画像,得出报表,这些非日志源的数据,可以放到MongoDB上,因为这些数据是修改性增长缓慢数据,占用空间不大;
  • 储存仓库:
    ElasticSearch是个很好的选择,能集群,可热增减节点,扩容,还可以全文检索,分词;
  • 用户界面:
    Kibana针对 ElasticSearch提供良好的分析,结合原有的管理后台系统,我们自己实现了一套用户界面。

FEN架构

这个框架主要使用到了Fluentd,ElasticSearch,以及NodeJS,我就称它为 FEN 架构吧,如下图。

架构图

FEN架构

上图看出,这样的日志架构和第一个图基本没什么不同,只是多了后面的分析与分批入库处理,并且大量使用了NodeJS。

注:在这里不会介绍各组件的详细的安装配置方法,网上有太多了,怎样使用好每一个组件才是关键。

先介绍我们用到的工具:

Fluentd

Fluentd是一个完全开源免费的log信息收集软件,支持超过125个系统的log信息收集。Fluentd在收集源日志方面非常方便而且高性能,通过HTTP GET就可以,这类似于Nginx的日志记录行为。它的优点是,日志文件可以高度定制化,例如我们这里每5秒生成一个文件,这样每分钟有12个文件,每个文件体积非常小。为什么要这样做?下面会介绍。Fluentd还有非常多的插件,例如直接存入MongoDB,亚马逊云等,要是熟悉Ruby,也可以自己写插件。

ElasticSearch

有人使用MongoDB进行日志收集,是非常不明智的,只有几千万条还可以,如果半个月生产10亿条日志呢?日志文件需要保存一个月甚至更长,那么集群和硬盘维护就非常重要。使用便利性也很重要,例如分词检索,在客服回溯玩家日志,分析游戏 BUG 的时候非常有用。下文的 ES 也是该组件的简称。

NodeJS

NodeJS不适合做 CPU 密集型任务,但在网络应用方面还不错,并且是我们正好熟悉的。日志系统对实时性要求并不高,延时半小时以内都是允许的,事实上,正常情况延时也就10来秒。下面的读与转发日志的Pusher,收集日志的logger,分析日志并数据落袋为安的的analyser,都是由NodeJS实现的。

下面继续介绍用 NodeJS实现的每一个部分:

转发器Pusher

(注:这是一个nodeJS编写的服务。)
上面说到,为什么Fluentd使用分割成多个小文件的方式,因为NodeJS在大文件处理方面并不友好,并且要考虑到通过网络发送到另一台机,转发速度比读慢太多了,所以必须实现续传与断点记录功能。想想,如果读几百 M 的文件,出现中断后,需要永久记录上次位置,下次再从此处读起,这就增加了程序复杂度。NodeJS虽然有readline模块,但测过发现并不如文件流那样可控,访模块用于交互界面尚可。相反,如果日志分割成多个小文件,则读的速度非常高效,并且每5秒一个文件,哪怕有上万条记录,文件也大不到哪里去,内存也不会占用太多,在断点续传与出错重试方面都能自如应对。如果游戏日志增多,可以增加节点来缓解文件过大的压力。

为什么不直接让日志生产者直接发到Koa上?因为效率与带宽。NodeJS的适合做网站,但比专业的HTTP服务器要弱太多,4核心主机面对3000QPS就吃力,更多的关于NodeJS的性能问题,可以参考网络文章。在高并发量下,带宽是个很大的问题,尤其是需要做统一服务,面对的情况是日志机器与游戏并不在同一内网中。在10万日活下,带宽超过了50M,非常吓人,带宽可是很贵的,过高的带宽费用在这里性价比太低了。

Pusher的注意点:
  • 批量转发:不要一条条日志发,采用批量发送。根据单条日志文件大小,如果是 JSON 数据,有10多个字段,那么每次请求发送50~100条发送都是没问题的,也就几十 KB;
  • 串行序顺发送:从时间小的文件,从文件关开始发,等待上一次发送请求完成再执行下一次;
  • 发送失败保存重试:如果某一次请求失败,则保存到另外一个文件目录,以时间戳作为文件名,下次重试,尽可能保证数据完整性;
  • 每100毫秒读一次文件列表,检查有没有新的日志文件。虽然是每5秒产生一次日志文件,但有可能出现效率下降导致发送速度跟不上而产生文件积压,即使是空读也是允许的,这不基本不占什么CPU。第100毫秒的间隔不要使用setInterval,应该在上一次文件发送完毕再setTimeout来执行;
  • 发送速度提供可变性,如果下面的logger效率低下,上面的100毫秒可以适当放缓一些。

日志收集器logger

这里我们使用Koa作为日志采集器。使用Koa,无论在性能还是开发效率上,都比expressJS高效。我们还用到了Redis作为缓存,而不是直接在这里做分析任务,是为了尽量提高与Pusher的对接效率,毕竟日志的生产速度是很快的,但网络传送是相对低效的。

logger的注意点:
  • 使用缓存缓存数据,如Redis;
  • 关注内存:logger与pusher是两台机子,当logger的缓存提升太快,也就是后面的分析与入库速度跟不上了,需要返回消息告知pusher放慢发送速度;
  • 安全验证:简单的方式是pusher发送时可以进行md5验证,logger验签;
  • 如果使用Redis,在Redis 4.0以下,使用list记录每条日志 ID,日志使用hash节省内存。在Redis 3.x不要使用Scan,它有BUG,就是Scan出的数量是无法确定的,就算明确指定了条数,但有可能出现一次读数万条,也有可能一次读几十条,这对后面的分析器非常不利;
  • Redis记得开启 RDB,以及maxmemory设置,前者可以在出问题时还原状态,后者可以防止出现灾难时资源暴掉,搞崩其它服务;
  • 无论是不是使用Redis,应该使用支持管道,或者批量的方法,如redisio,根据机器效率,如每次满500条就入缓存,不满就100毫秒入一次,减少缓存操作次数可以提高效率;
  • logger可以用pm2的集群模式,提高效率。

注:pm2 3.2.2的集群可能出现集群内端口冲突的吊诡问题,建议用3.0.3或最新版本

分析器analyser:

(注:这也是一个nodeJS编写的服务。)
分析器读取Redis的内容,这里就是单进程的队列操作。到这一步,日志怎么分析,就可以很自由了。

分析器analyser的注意点:

  • 单线程可以确保每个玩家的日志时间序列;
  • Redis的读取使用管道,一次读取数千条进行分析。参考值:目前每次读3000条进行处理,在4核心中低配置云主机下单线程占用仅为35%左右;
  • 日志存ES:源日志文件可以进行进一步分析或者格式优化,处理后的放ES,ES 就是为集群而生,通过加入子节点可以热扩容,硬盘便宜,所以先做3个节点的集群吧;
  • 配置好ES的索引(mapping),仔细考虑各字段类型,凡是要与搜索条件有关的,例如要查元宝大于多少的,那么元宝字段必须有索引,否则将无法根据该字段查找日志。还有,想要分词的必须使用text类型。日志一般不会进行汇总,因为我们已经统计大部分内容了,所以可以适当减少doc_value,压缩率等,否则一千万条日志半小时内就吃掉1G硬盘。这需要你好好研究 ES 的索引配置了,后面还得研究 ES 的搜索,因为它比MongoDB的复杂得多,但这很值得;
  • ES和MongoDB的入库,使用批量处理,根据机器性能和系统资源找到合适的批处理数量。参考值,4核下 ES 批量入库1000条效率300ms 左右;
  • ES 配置好内存,默认是1G JVM内存,经常不够用就会崩溃。在配置文件同目录下有个jvm option文件,可以加大JVM,建议至少分配一半以上内存;
  • ES 的写入效率:不要以为 ES 的输入速度很快,默认它是写一条更新一条索引,也就是必须等把数据更新到索引才会返回,无论使用批量处理还是单个,日志量大的时候,批处理仅100条也会超过500ms。设置durability为async,不要马上更新到索引;
  • ES使用别名索引,好处是当你需要重建索引时,可以通过另外重新指向到新的索引,因为 ES 不能修改索引,只能重建;
  • 在分析的时候,先还原玩家画像,对其它数据报表,组织好你的数据结构,数据量小、简单的可以同时放内存中进行计数,并定期条件清理,大的如玩家画像放redis中,定期更新入库。这些数据的缓存方式可以使用完整版本,简化问题,减少出现脏数据的可能;同时分析也要注意效率的问题,例如有Mongodb数据的读写,要务必配置到index,否则将引起灾难性效率下降。

用户界面

因为我们本身有后台管理系统,所以我们很方便的把用户画像与其它分析点接了入去,在查询玩家行为时,我们搜索ES,在查询分析报表时,我们查询MongoDB中的数据。当然我们也使用了Kibana来满足可能的需求。

总结

目前该日志系统运行1个半月,由纯MongoDB到结合 ES,走了不少弯路,还好现在终于稳定下来。目前在性能方面,logger 与 analyser都在同一台机,平均 CPU 为23%左右,高峰47%左右,说明还有更大的机器压榨空间。

内存方面,在高峰期5G 以内,总体非常平稳没多大波动,其中redis内存使用为800MB以内,但机器是16G,还有很大余量保障。

NodeJS 的脚本中,logger的CPU占用更小,3条进程,每条才3%,每条内存占用不到100MB。analyser 的 CPU 与内存占用多一点,这一点可以通过脚本内的参数调整,例如内存计数的内容清理得更快,使用pm2的话设置max_memory_restart : ‘4G’ 都可以提高稳定性。

以上是我在游戏日志系统中的经验总结。

新的挑战

以上方式,是【本地文件收集】-【推送到】-【分析】-【入库与分析结果保存】,万一需要对已经入库的数据重新分析呢?例如以前有个数据运营漏了放到需求里,需要对某个时期的日志重新分析。

解决思路有2个:

  • 运营人员导出日志,重新分析。缺点是需要大量人工处理;
  • 把日志分析模块独立出来,不要在入库的时候分析,在入库后分析,就是把原来的前分析改为后分析。后分析好处是模块化,统计日志的时间点可选,还可以做到后台界面里随时统计。有点kibana的意味。

参考文献:

  • 在Node.js中读写大文件 cnodejs.org/topic/55a73…

注:本文首次发布与简书

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【理论篇】浅析分布式中的 CAP、BASE、2PC、3PC、

发表于 2020-12-22

个人公众号『码农札记』,欢迎关注,查看更多精彩文章。

序

在常见的分布式系统中,总会发生诸如机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序,还有网络分区)等情况。基于此,产生了适应各种场景的一致性
算法,解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致, 并且保证不论发生以上任何异常,都不会
破坏整个系统的一致性。

由于涉及理论较多,本文借鉴了好多博主的文章,反复认真研读,在此特别感谢。

CAP

2000年7月,加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。
2年后,麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。
之后,CAP理论正式成为分布式计算领域的公认定理。

CAP理论:
一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和
分区容错性(Partition tolerance)这三项中的两项。

概念

一致性(Consistency)

一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,
所有节点在同一时间的数据完全一致。

可用性(Availability)

可用性指“Reads and writes always succeed”,即服务一直可用,而且是正常响应时间。

分区容错性(Partition tolerance)

分区容错性指 “the system continues to operate despite arbitrary message loss or
failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外
提供满足一致性和可用性的服务。

CAP权衡

选择 说明
CA 放弃分区容错性,加强一致性和可用性,其实就是传统的单机数据库的选择
AP 放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,例如很多NoSQL系统就是如此
CP 放弃可用性,追求一致性和分区容错性,基本不会选择,网络问题会直接让整个系统不可用

通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?

对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9,
即保证P和A,舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。

对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证CA,舍弃P。貌似这几年国内银行业发生了不下10起事故,
但影响面不大,报到也不多,广大群众知道的少。还有一种是保证CP,舍弃A。例如网络故障事只读不写。

孰优孰略,没有定论,只能根据场景定夺,适合的才是最好的。

BASE理论

eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是即使无法
做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。

BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。

概念

基本可用(Basically Available)

基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

软状态( Soft State)

软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时
就是软状态的体现。mysql replication的异步复制也是一种体现。

最终一致性( Eventual Consistency)

最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

BASE与ACID

总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性
来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态
。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同
的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。

2PC

二阶段提交又称2PC(two-phase commit protocol),2pc是一个非常经典的强一致、中心化的原子提交协议。这里所说的中心化是指协议中有两
类节点:一个是中心化协调者节点(coordinator)和N个参与者节点(partcipant),事务的提交过程分成了两个阶段来进行处理。

2pc执行成功,参与者全部同意
success

2pc执行失败,参与者任意一个不同意都会失败
fail

阶段一:提交事务请求

事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。
(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)

阶段二:事务执行

如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;
参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。

注意:必须在最后阶段释放锁资源。

优缺点

优点

原理简单,实现方便

缺点

  • 单点服务: 若协调者突然崩溃则事务流程无法继续进行或者造成状态不一致
  • 无法保证一致性: 若协调者第二阶段发送提交请求时崩溃,可能部分参与者受到COMMIT请求提交了事务,而另一部分参与者未受到请求而放弃事
    务造成不一致现象。
  • 阻塞: 为了保证事务完成提交,各参与者在完成第一阶段事务执行后必须锁定相关资源直到正式提交,影响系统的吞吐量。

参与者在完成阶段一的事务执行后等待协调者的下一个请求,若协调者超时则可以自行放弃事务。
这种方案仍然有无法保证一致性的缺点,但并不会出现某些资料所述一直锁定资源,无法继续的情况。

3pc

3PC,全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求Prepare” 过程一分为二(CanCommit、PreCommit),共形成了由
CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。

基本流程

执行步骤

CanCommit

  • 1 协调者进行事务询问
    协调者向所有的参与者发送一个包含事务内容的CanCommit请求,询问是否可以执行事务提交操作,并开始等待 各参与者的响应。
  • 2 参与者向协调者反馈事务询问
    参与者在接收到来自协调者的包含了事务内容的CanCommit请求后,正常情况下,如果自身认为可以顺利执行事 务,则反馈Yes响应,
    并进入预备状态,否则反馈No响应。

PreCommit

协调者在得到所有参与者的响应之后,参与者在CanCommit反馈的是Yes,执行事务预提交:

  • 1 协调者发送预提交请求(发出preCommit请求,并进入prepared阶段)
  • 2 参与者进行事务预提交(参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。)
  • 3)各参与者向协调者反馈事务执行的结果(若参与者成功执行了事务操作,那么反馈Ack)

协调者在得到所有参与者的响应之后,参与者在CanCommit反馈的是No,中断事务:

  • 1 协调者发送中断请求:(协调者向所有参与者发出abort请求。)
  • 2 中断事务(无论是收到来自协调者的abort请求或者等待协调者请求过程中超时,参与者都会中断事务)

DoCommit

DoCommit阶段完成真正的事务提交或者完成事务回滚。

在第二阶段PreCommit阶段收到ACK确认消息,则完成事务提交:

  • 1 协调者发送提交DoCommit请求(协调者将从预提交状态转化为提交状态,并向所有的参与者发送doCommit请求)
  • 2 参与者进行事务提交(参与者接收到DoCommit请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行过程中占用的事务资源。)
  • 3 各参与者向协调者反馈事务提交的结果(若参与者成功完成事务提交,那么反馈Ack响应)
  • 4 完成事务(协调者接收到所有参与者反馈的Ack消息后,完成事务。)

在第二阶段PreCommit阶段超时中断没有收到ACK确认消息,则完成事务中断:

  • 1 协调者发送中断请求(协调者向所有的参与者节点发送abort请求)
  • 2 参与者进行事务回滚(根据记录的Undo信息来执行事务回滚,并在完成回滚之后释放整个事务执行期间占用的资源)
  • 3 各参与者向协调者反馈事务回滚的结果(参与者在完成事务回滚后,向协调者发送Ack消息。)
  • 4 中断事务(协调者接收到所有参与者反馈的Ack消息后,中断事务。)

注意:在DoCommit阶段可能出现协调者宕机、协调者与参与者出现网络故障;导致参与者接收不到协调者的DoCommit请求或Abort请求,
参与者会在请求超时后,继续进行事务提交
。

优缺点

降低了阻塞

  • 参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若等待超时,则自动 abort,降低了阻塞;
  • 参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若等待超时,则自动 commit 事务,也降低了阻塞;

解决单点故障问题

  • 参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若协调者宕机,等待超时后自动 abort,;
  • 参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若协调者宕机,等待超时后自动 commit 事务;

数据不一致问题仍然是存在的

比如第三阶段协调者发出了 abort 请求,然后有些参与者没有收到 abort,那么就会自动 commit,造成数据不一致。

paxos

Paxos算法是莱斯利·兰伯特
(英语:Leslie Lamport,LaTeX中的“La”)于1990年提出的一种基于消息传递且具有高度容
错特性的共识(consensus)算法。
需要注意的是,Paxos常被误称为“一致性算法”。但是“一致性(consistency)”和“共识
(consensus)”并不是同一个概念。Paxos是一个共识(consensus)算法。

算法流程

Paxos算法解决的问题是分布式共识性问题,即一个分布式系统中的各个进程如何就某个值(决议)通过共识达成一致。

Paxos算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。
它利用大多数 (Majority) 机制保证了2F+1的容错能力,即2F+1个节点的系统最多允许F个节点同时出现故障。

一个或多个提议进程 (Proposer) 可以发起提案 (Proposal),Paxos算法使所有提案中的某一个提案,在所有进程中达成一致。
系统中的多数派同时认可该提案,即达成了一致。最多只针对一个确定的提案达成一致。

Paxos将系统中的角色分为提议者 (Proposer),决策者 (Acceptor),和最终决策学习者 (Learner):

  • Proposer: 提出提案 (Proposal)。Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)。
  • Acceptor:参与决策,回应Proposers的提案。收到Proposal后可以接受提案,若Proposal获得多数Acceptors的接受,则称该Proposal被批准。
  • Learner:不参与决策,从Proposers/Acceptors学习最新达成一致的提案(Value)。

在具体的实现中,一个进程可能 同时充当多种角色 。比如一个进程可能 既是Proposer又是Acceptor又是Learner 。Proposer负责提出提案,
Acceptor负责对提案作出裁决(accept与否),learner负责学习提案结果。

还有一个很重要的概念叫提案(Proposal)。最终要达成一致的value就在提案里。只要Proposer发的提案被Acceptor接受(
半数以上的Acceptor同意才行),Proposer就认为该提案里的value被选定了。Acceptor告诉Learner哪个value被选定,
Learner就认为那个value被选定。只要Acceptor接受了某个提案,Acceptor就任务该提案里的value被选定了。

为了避免单点故障,会有一个Acceptor集合,Proposer想Acceptor集合发送提案,Acceptor集合中的每个成员都有可能同意该提案且每个Acceptor
只能批准一个提案,只有当一半以上的成员同意了一个提案,就认为该提案被选定了。

拜占庭问题

拜占庭将军问题:是指 拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的,
只能依靠通讯员进行传递命令,但是通讯员中存在叛徒,它们可以篡改消息,叛徒可以欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定,
如当将军们不希望进攻时促成进攻行动;或者迷惑某些将军,使他们无法做出决定。

Paxos算法的前提假设是不存在拜占庭将军问题,即: \信道是安全的(信道可靠),发出的信号不会被篡改,因为Paxos算法是基于消息传递的**。

从理论上来说,在分布式计算领域,试图在异步系统和不可靠信道上来达到一致性状态是不可能的。因此在对一致性的研究过程中,都往往假设信道是可靠的,
而事实上,大多数系统都是部署在一个局域网中,因此消息被篡改的情况很罕见;另一方面,由于硬件和网络原因而造成的消息不完整问题,只需要一套简单
的校验算法即可。因此,在实际工程中,可以假设所有的消息都是完整的,也就是没有被篡。

协议过程

Paxos在原作者的《Paxos Made Simple》中内容是比较精简的:

Phase 1

(a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.

(b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request with a promise not to accept any more proposals numbered less than n and with the highest-numbered pro-posal (if any) that it has accepted.

Phase 2

(a) If the proposer receives a response to its prepare requests (numbered n) from a majority of acceptors, then it sends an accept request to each of those acceptors for a proposal numbered n with a value v , where v is the value of the highest-numbered proposal among the responses, or is any value if the responses reported no proposals.

(b) If an acceptor receives an accept request for a proposal numbered n, it accepts the proposal unless it has already responded to a prepare request having a number greater than n.

阶段 1. (a) 一个 proposer 选择一个提议编号 *n*,然后将提议编号 *n* 放入 *prepare* request 中并发送给 acceptors 中的多数派。

在我们的例子中,proposer 会将 prepare request 发送给了所有的 acceptors,但是出于性能优化的考虑,即然它只需要大多数的 acceptors 同意,那么他可以只给 acceptors 中的一个多数派发送 prepare requests。具体发送给哪些 acceptors 由 proposer 自己决定。

(b) 如果一个 accptor 接受了一个 *prepare* request,其中的数字 *n* 大于这个 acceptor 已经回复过的所有 *prepare* request,那么他会回复这个 request,同时承诺不接受比 n 小的提议编号,而且会返回它已经接受过的提议中编号最大的那个提议(如果有的话)。

在我们上面的某张图中,(TODO,具体哪张图?),E 只返回了 OK,因为它之前没有接受过提议,但是 ACD 除了来返回 OK 之外,还返回了之前接受过的编号最大的提议,也就是 Alice

阶段 2. (a) 如果这个 proposer 收到了来自多数 acceptors 对 *prepare* requests (编号 n) 的回复,那么他会对这些 acceptors 分别发送一个 *accept* request,其中包含提议编号 *n* 和一个值 *v*。*v* 是响应中的提议中编号最大的那个提议的值,如果响应中没有提议,那 *v* 可以是任何值。

举例

此例子 来源 ocavue.com/paxos.html#… 『划重点,强烈吐槽掘金显示图片有bug,不支持svg向量图片,请点击公众号链接 mp.weixin.qq.com/s/D_nYSERTf… 查阅文章感谢』

想象一个用来卖票的分布式系统,就像 12306 那样。这个系统一共有五台机器,分布在不同的地点。为了讲解的方便,我们设定这个系统只卖一张票。五台机器有各自的数据库,用来储存买票者的名字。如果机器 A 认为把票卖给了我,而机器 B 认为把票卖给了你,那就糟糕了。于是我们要分布式系统的一致性,具体到这个例子就是保证不同机器中买票者的名字是同一个。

此时机器 D 收到了来自 Alice 的买票请求。于是 D 首先进入 Prepare 阶段:,

D 向其他 4 台机器发送了一条 提议(这里用蓝色的箭头表示提议):

其中 P-1D 表示:

  1. 这是一个提议(P for Prepare)
  2. 提议的 ID 是 1D

每个提议都需要一个递增的全局唯一 ID,最简单的方法就是当前时间加上当前机器的名字。这个 ID 会贯穿整个 Paxos 流程。值得注意的是,在提议阶段,D 并没有把购买者的名字 Alice 告诉其他机器。

其他机器收到这个提议后,他们发现之前并没有收到过提议,于是同意了这份提议。具体来说是做了下面几件事:

  1. 将 P-1D 记录下来
  2. 承诺以后不再接受 ID < "1D" 的提议
  3. 向 D 回复 OK,这里用红色的箭头表示对提议的回复

D 收到其他机器的回复后,发现加上自己的同意,发现已经有超过半数的机器同意将票卖给 Alice(事实上,所有五台机器都同意这点)。即然多数派已经同意了这份提议,那么 D 就认为认为这个提议已经被通过了,于是进入了 Commit 阶段。

在 Commit 阶段,D 向所有机器发出了一个决议(这里用绿色的箭头表示表示决议):

其中 A-1D-Alice 表示

  • 这是一个决议(A for Accept)
  • 决议的 ID 是 1D
  • 决议内容:将票卖给 Alice

其他四台机器到了这个决议后,就会把决议的内容记录下来,并返回给 D。D 最后把买票成功的消息发给 Alice,用图表示就是这样,这里用紫色的箭头表示对决议的回复:

此时,所有五台机器都认为票卖给了 Alice,一致性得到了保证。

上面的情况是所有机器和网络都能正常运行的理想情况,可使现实中总是不理想的,而分布式系统的最大价值之一就是能应对部分节点的故障,所以下面我们来模拟一下节点故障的情况。

# 节点故障的例子

我们假设有两台机器 B 和 E 发生了故障,那么重复一下上面的步骤,看看会发生什么。

第一步,D 接受到了 Alice 的请求,于是向其他机器发送提议:

第二步,除了发生故障的 B 和 E,其他机器都回复了提议:

第三步,D 认为提议得到了多数派(A、C、D)的认可,于是向大家发送决议

第四步,除了故障机外,其他机器都回复了决议。最后 D 向 Alice 发送购票成功的消息。

到目前为止,一切都 OK。由于只有少数的机器发生了故障,依然有一个多数派(3 台机器 > 5 台机器 / 2)存在,所以系统的运行没有受到影响。但是,如果我们这时候修好了 B 和 D,就会发现一个问题:B 和 D 就像刚苏醒的植物人,他们还以为这张票没卖出去呢!Paxos 算法如何解决这种情况呢?让我们继续这个例子:

↑ B 和 E 恢复工作了,但是他们此时没有 P-1D 和 C-1D-Alice 的信息

假如这时候 Bob 来买票了,他向机器 B 发出了买票请求,于是 B 向其他机器发送了一个提议。

对于这个提议,E 的回复是 OK,但是 A、C 和 D 的回复是 "1B" < "1D", Fail。因为在之前,A、C、D 已经承诺过了,他们不会接受 id < "1D" 的提议请求。

于是 B 只能放弃 1B 提议,而是紧接着又提出一个 ID 为 2B 提议,并将提议发送给其他机器:

然后机器其他机器都同意了这个提议,不过 A、C 和 D 在同意提议的同时,还返回了这样一条信息:「我已经把票卖给 Alice 了,你不可能通知我把票买给其他人」:

B 收到了包括自己在内的五分针对 2B 提议的同意,所以 B 可以进入下一步,也就是发表决议了。但是知道了票已经被 Alice 拿走后,
由于卖掉的票是不能再拿回来的,所以 B 被迫修改决议的内容,也就是发表一个将票卖给 Alice 的决议,毕竟:

其他机器收到了这个决议后,也把这个决议写在自己的数据库中,并且将结果返回给 B。B 再通知 Bob 票已经被卖掉了:

在这个例子中,我们的分布式系统非常好地处理了节点故障的情况,最后达到的效果如下:

  1. 同一张票没有被卖给两个人。虽然 Bob 请求的机器 B 一开始并不知道票已经被 Alice 拿走了,但是最终 Bob 还是知道了。这就是最终一致性。
  2. 最终所有机器上都储存了相同的信息,也就是 P-2B 和 C-2B-Alice。

# 更加现实的情况

同时处理多个 paxos 实例:

在现实中,一个卖票系统不可能像上面的例子中只卖一张票。我们只要把每一张票当成一个独立的 paxos 算法流程,比如说在每个请求和响应中添加上票的唯一标示,
就能从逻辑上同时处理多张票的售卖。

*TODO添加图示

票的转台

我们做的另一个假设是一张票只会被卖出一次,但是在现实中由于可以退票,一张票可以在退票后卖个另一位顾客。在 paxos 中,我们需要引入状态机的概念:
简单来说就有一个 状态 经过一个 操作 后变成了另一个状态。

一张票被卖掉之后,它的状态由 可买 变成了 不可买,退票后其状态又重新变成了 可买。

火车票的售卖 和 银行账号的余额 都可以表示为这样的逻辑:

只要知道初始状态和所有的操作,根据状态机的逻辑就能算出当前的状态。我们可以为每一个状态制定一个 Paxos 算法实例,所有机器上使用 Paxos
算法同步了状态 1, 2, 3, …. 这样就能在所有机器上都保存相同的记录。

Paxos的死锁情况

img

“活锁”的根本原因在于两个proposer交替提案,避免“活锁”的方式为,如果一个proposer通过accpter返回的消息知道此时有更高编号的提案被提出时,
该proposer静默一段时间,而不是马上提出更高的方案,静默期长短为一个提案从提出到被接受的大概时间长度即可,静默期过后,proposer重新提案。
系统中之所以要有主proposer的原因在于,如果每次数据更改都用paxos,那实在是太慢了,还是通过主节点下发请求这样来的快,因为省去了不必要的paxos时间。
所以选择主proposer用paxos算法,因为选主的频率要比更改数据频率低太多。但是主proposer挂了咋整,整个集群就一直处于不可用状态,所以一般都用租约的
方式,如果proposer挂了,则租约会过期,其它proposer就可以再重新选主,如果不挂,则主proposer自己续租。

raft

Raft是由Stanford提出的一种更易理解的一致性算法,意在取代目前广为使用但难以理解的Paxos算法。目前,在各种主流语言中都有了一些开源实现。

原理

raft

在Raft中,每个结点会处于下面三种状态中的一种:

  • follower:所有结点都以follower的状态开始。如果没收到leader消息则会变成candidate状态
  • candidate:会向其他结点“拉选票”,如果得到大部分的票则成为leader。这个过程就叫做Leader选举(Leader Election)
  • leader: 所有对系统的修改都会先经过leader,每个修改都会写一条日志(log entry),leader收到修改请求后的过程如下,
1. 复制日志到所有follower结点(replicate entry)
2. 大部分结点响应时才提交日志
3. 通知所有follower结点日志已提交
4. 所有follower也提交日志
5. 现在整个系统处于一致的状态
这个过程叫做`日志复制(Log Replication)`。

Leader Election

当follower在选举超时时间(election timeout)内未收到leader的心跳消息(append entries),则变成candidate状态。
为了避免选举冲突,这个超时时间是一个150~300ms之间的随机数。

成为candidate的结点发起新的选举期(election term)去“拉选票”:

  1. 重置自己的计时器
  2. 投自己一票
  3. 发送 Request Vote消息

如果接收结点在新term内没有投过票那它就会投给此candidate,并重置它自己的选举超时时间。candidate拉到大部分选票就会成为leader,
并定时发送心跳——Append Entries消息,去重置各个follower的计时器。当前Term会继续直到某个follower接收不到心跳并成为candidate。

如果不巧两个结点同时成为candidate都去“拉票”怎么办?

这时会发生Splite Vote情况。两个结点可能都拉到了同样多的选票,难分胜负,
选举失败,本term没有leader。之后又有计时器超时的follower会变成candidate,将term加一并开始新一轮的投票。

Log Replication

当发生改变时,leader会复制日志给follower结点,这也是通过Append Entries心跳消息完成的。前面已经列举了Log Replication的过程,这里就不重复了。

脑裂问题: 指在一个高可用(HA)系统中,当联系着的两个节点断开联系时,本来为一个整体的系统,分裂为两个独立节点,这时两个节点开始争抢共享资源,
结果会导致系统混乱,数据损坏。

Raft能够正确地处理网络分区(“脑裂”)问题。假设A~E五个结点,B是leader。如果发生“脑裂”,A、B成为一个子分区,C、D、E成为一个子分区。
此时C、D、E会发生选举,选出C作为新term的leader。这样我们在两个子分区内就有了不同term的两个leader。这时如果有客户端写A时,
因为B无法复制日志到大部分follower所以日志处于uncommitted未提交状态。而同时另一个客户端对C的写操作却能够正确完成,因为C是新的leader,
它只知道D和E。

当网络通信恢复,B能够发送心跳给C、D、E了,却发现“改朝换代”了,因为C的term值更大,所以B自动降格为follower。
然后A和B都回滚未提交的日志,并从新leader那里复制最新的日志。

举例论证

强烈推荐动画版Raft讲解。
此处不在本文赘述。

ZAB协议

ZAB(Zookeeper Atomic Broadcast) 协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的一致性协议。基于该协议,ZooKeeper 实现了一种
主从模式的系统架构来保持集群中各个副本之间的数据一致性。

ZAB协议只允许有一个主进程(即leader)接收客户端事务请求并进行处理。当leader收到事务请求后,将请求事务转化成事务proposal,
由于leader会为每个follower创建一个队列,将该事务proposal放入响应队列,保证事务的顺序性。之后会在队列中顺序向其它节点广播该提案,
follower收到后会将其以事务的形式写入到本地日志中,并且向leader发送Ack信息确认,当有一半以上的follower返回Ack信息时,
leader会提交该提案并且向其它节点发送commit信息。

相关知识参考了 segmentfault.com/a/119000003… 文章。

概念

三种角色

Leader :负责整个Zookeeper 集群工作机制中的核心,主要工作有以下两个:

  • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
  • 集群内部各服务器的调度者

Follower :它是 Leader 的追随者,其主要工作如下:

  • 处理客户端的非实物请求,转发事务请求给 Leader 服务器
  • 参与事务请求 Proposal 的投票
  • 参与 Leader 选举投票

Observer :是 zookeeper 自 3.3.0 开始引入的一个角色,它不参与事务请求 Proposal 的投票,也不参与 Leader 选举投票,只提供非事务的服务(查询),通常在不影响集群事务处理能力的前提下提升集群的非事务处理能力。

三种状态

在 ZAB 协议中定义:通过自身的状态来区分自己的角色的,在运行期间各个进程可能出现以下三种状态之一:

  • LOOKING:处在这个状态时,会进入 Leader 选举状态
  • FOLLOWER:Follower 服务器和 Leader 服务器保持同步时的状态
  • LEADING:Leader 服务器作为主进程领导者的状态

在组成 ZAB 协议的所有进程启动的时候,初始化状态都是 LOOKING 状态,此时进程组中不存在 Leader,选举之后才有,在进行选举成功后,就进入消息广播模式(后文介绍),此时 Zookeeper 集群中的角色状态就不再是 LOOKING 状态。

ZXID

zookeeper 消息有严格的因果关系,因此必须将每一个事务请求按照先后顺序来进行排序与处理。那 Zookeeper 是如何保持请求处理的顺序的呢?其中非常关键的点就是 ZXID。

那 ZXID 究竟是怎么发挥作用的呢?

Leader 服务器在接收到事务请求后,会为每个事务请求生成对应的 Proposal 来进行广播,并且在广播事务 Proposal 之前,Leader 服务器会首先为这个事务 Proposal 分配一个全局单调递增的唯一 ID ,我们称之为事务 ID(即 ZXID)。

ZXID 的设计也很有特点,是一个全局有序的 64 位的数字,可以分为两个部分:

  • 高 32 位是: epoch(纪元),代表着周期,每当选举产生一个新的 Leader 服务器时就会取出其本地日志中最大事务的 ZXID ,解析出 epoch(纪元)值操作加 1作为新的 epoch ,并将低 32 位置零。
  • 低 32 位是: counter(计数器),它是一个简单的单调递增的计数器,针对客户端的每个事务请求都会进行加 1 操作;

这里低 32 位 counter(计数器)单调递增还好理解,高 32 位 epoch(纪元)每次选举加 1 也许有些同学就有疑问了,为什么 epoch(纪元)每次选需要举加 1 ,它在整个 ZAB 协议中有什么作用?

我们知道每当选举产生一个新的 Leader 服务器时生成一个新的 epoch(纪元)值,而在前文我们知道,服务运行过程中触发选举 Leader 的条件是:Leader 服务器的出现网络中断、奔溃退出、重启等异常情况,或者当集群中半数的服务器与该 Leader 服务器无法通信时。

这说明整个 Zookeeper 集群此时处于一个异常的情况下,而在发生异常前,消息广播进行到哪一步骤我们根本不知道,集群中的其他 Follower 节点从这种崩溃恢复状态重新选举出 Leader 后,如果老 Leader 又恢复了连接进入集群。此时老 Leader 的 epoch 肯定会小于新 Leader 的 epoch,这时就将老 Leader 变成 Follower,对新的 Leader 进行数据同步。即便这时老 Leader 对其他的 Follower 节点发送了请求,Follower 节点也会比较 ZXID 的值,因为高 32 位加 1 了, Follower 的 epoch(纪元)大于老 Leader 的 epoch(纪元),所以 Follower 会忽略这个请求。

俩种模式

ZAB 协议的包括两种模式:崩溃恢复、消息广播。

在进入奔溃恢复模式时 Zookeeper 集群会进行 Leader 选举,一般有两种情况会发生选举:

  • 当服务器启动时期会进行 Leader 选举。
  • 当服务器运行期 Leader 服务器的出现网络中断、奔溃退出、重启等异常情况,或者当集群中半数的服务器与该 Leader 服务器无法通信时,进入崩溃恢复模式,开始 Leader 选举。

选举出 Leader 服务器后,会进入消息广播模式,开始接收处理客户端的请求。

消息广播模式

img

  • Leader 服务器接收到请求后在进行广播事务 Proposal 之前会为这个事务分配一个 ZXID,再进行广播。
  • Leader 服务器会为每个 Follower 服务器都各自分配一个单独的队列,然后将需要广播的事务 Proposal 依次放入这些队列中去,并根据 FIFO 策略进行消息的发送。
  • 每个Follower 服务器在接收到后都会将其以事务日志的形式写入到本地磁盘中,并且在成功写入后返回 Leader 服务器一个 ACK 响应。
  • 当有超过半数的服务器 ACK 响应后,Leader 就会广播一个 Commit 消息给所有的 Follower 服务器,Follower 接收到后就完成对事务的提交操作。

整个过程类似一个二阶段提交的过程,但却有所不同,ZAB 协议简化了二阶段提交模型,在超过半数的 Follower 服务器已经反馈 ACK 之后就开始提交事务 Prososal 了,无需等待所有服务器响应。

崩溃恢复模式

上文中提到过崩溃恢复是在网络中断、奔溃退出、重启等异常情况下触发,流程如下

img

小结

在高并发分布式场景下,为了保证数据的一致性,诞生了各种各样的理论,都有着各自的应用场景,并且是个逐步发展,百家齐放的过程。cap,base理论反映了
当下分布式存在的各种场景,2pc,3pc,paxos,raft,zab都是常见的一致性协议,期望本文让你有个初步的认识,要想深入理解还要靠 自己多多实践验证。

本文是一篇理论总结篇,后续文章会实例举证哪些组件使用了这些理论,敬请期待。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【进阶之路】线程池拓展与CompletionService操

发表于 2020-12-22

大家好,我是练习java两年半时间的南橘,小伙伴可以一起互相交流经验哦。

一、扩展ThreadPoolExecutor

1、扩展方法介绍

ThreadPoolExecutor是可以扩展的,它内部提供了几个可以在子类中改写的方法(红框内)。JDK内的注解上说,这些方法可以用以添加日志,计时、监视或进行统计信息的收集。是不是感觉很熟悉?有没有一种spring aop中 @Around @Before @After三个注解的既视感?

我们来对比一下

ThreadPoolExecutor spring aop
beforeExecute()(线程执行之前调用) @Before(在所拦截的方法执行之前执行 )
afterExecute() (线程执行之后调用) @After (在所拦截的方法执行之后执行)
terminated() (线程池退出时候调用)
@Around(可以同时在所拦截的方法前后执行)

其实他们的效果是一样的,只是一个在线程池里,一个在拦截器中。

对于ThreadPoolExecutor中的这些方法,有这样的一些特点:

  • 1、无论任务时从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用(但是如果任务在完成后带有一个Error,那么就不会调用afterExecute)
  • 2、同时,如果beforeExecute抛出一个RuntimeExecption,那么任务将不会被执行,连带afterExecute也不会被调用了。
  • 3、在线程池完成关闭操作时会调用terminated,类似于try-catch中的finally操作一样。terminated可以用来释放Executor在其生命周期里分配的各种资源,此外也可以用来执行发送通知、记录日志亦或是收集finalize统计信息等操作。

2、扩展方法实现

我们先构建一个自定义的线程池,它通过扩展方法来添加日志记录和统计信息的收集。为了测量任务的运行时间,beforeExecute必须记录开始时间并把它保存到一个afterExecute可以访问的地方,于是用ThreadLocal来存储变量,用afterExecute来读取,并通过terminated来输出平均任务和日志消息。

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
java复制代码public class WeedThreadPool extends ThreadPoolExecutor {
private final ThreadLocal<Long> startTime =new ThreadLocal<>();
private final Logger log =Logger.getLogger("WeedThreadPool");
//统计执行次数
private final AtomicLong numTasks =new AtomicLong();
//统计总执行时间
private final AtomicLong totalTime =new AtomicLong();
/**
* 这里是实现线程池的构造方法,我随便选了一个,大家可以根据自己的需求找到合适的构造方法
*/
public WeedThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
//线程执行之前调用
protected void beforeExecute(Thread t,Runnable r){
super.beforeExecute(t,r);
System.out.println(String.format("Thread %s:start %s",t,r));
//因为currentTimeMillis返回的是ms,而众所周知ms是很难产生差异的,所以换成了nanoTime用ns来展示
startTime.set(System.nanoTime());
}
//线程执行之后调用
protected void afterExecute(Runnable r,Throwable t){
try {
Long endTime =System.nanoTime();
Long taskTime =endTime-startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
System.out.println(String.format("Thread %s:end %s, time=%dns",Thread.currentThread(),r,taskTime));
}finally {
super.afterExecute(r,t);
}
}
//线程池退出时候调用
protected void terminated(){
try{
System.out.println(String.format("Terminated: avg time =%dns, ",totalTime.get()/numTasks.get()));
}finally {
super.terminated();
}
}

}

测试案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class WeedThreadTest {
BlockingQueue<Runnable> taskQueue;
final static WeedThreadPool weedThreadPool =new WeedThreadPool(3,10,1, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(100));
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++) {
weedThreadPool.execute(WeedThreadTest::run);
}
Thread.sleep(2000L);
weedThreadPool.shutdown();
}

private static void run() {
System.out.println("thread id is: " + Thread.currentThread().getId());
try {
Thread.sleep(1024L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

3、使用场景

用到这些方法的地方其实和用到Spring AOP中一些场景比较相似,主要在记录跟踪、优化等方面可以使用,如日志记录和统计信息的收集、测量任务的运行时间,以及一些任务完成之后发送通知、邮件、信息之类的。

二、CompletionService操作异步任务

1、异步方法的原理

如果我们意外收获了一大批待执行的任务(举个例子,比如去调用各大旅游软件的出行机票信息),为了提高任务的执行效率,我们可以使用线程池submit异步计算任务,通过调用Future接口实现类的get方法获取结果。

虽然使用了线程池会提高执行效率,但是调用Future接口实现类的get方法是阻塞的,也就是和当前这个Future关联的任务全部执行完成的时候,get方法才返回结果,如果当前任务没有执行完成,而有其它Future关联的任务已经完成了,就会白白浪费很多等待的时间。

所以,有没有这样一个方法,遍历的时候谁先执行完成就先获取哪个结果?

没错,我们的ExecutorCompletionService就可以实现这样的效果,它的内部有一个先进先出的阻塞队列,用于保存已经执行完成的Future,通过调用它的take方法或poll方法可以获取到一个已经执行完成的Future,进而通过调用Future接口实现类的get方法获取最终的结果。

逻辑图如下:

ExecutorCompletionService实现了CompletionService接口,在CompletionService接口中定义了如下这些方法:

  • Future submit(Callable task):提交一个Callable类型任务,并返回该任务执行结果关联的Future;
  • Future submit(Runnable task,V result):提交一个Runnable类型任务,并返回该任务执行结果关联的Future;
  • Future take():从内部阻塞队列中获取并移除第一个执行完成的任务,阻塞,直到有任务完成;
  • Future poll():从内部阻塞队列中获取并移除第一个执行完成的任务,获取不到则返回null,不阻塞;
  • Future poll(long timeout, TimeUnit unit):从内部阻塞队列中获取并移除第一个执行完成的任务,阻塞时间为timeout,获取不到则返回null;

2、异步方法的实现

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
ini复制代码public class WeedExecutorServiceDemo {
/**
* 继续用之前建好的线程池,只是调整一下池大小
*/
BlockingQueue<Runnable> taskQueue;
final static WeedThreadPool weedThreadPool = new WeedThreadPool(1, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
public static Random r = new Random();

public static void main(String[] args) throws InterruptedException, ExecutionException {
CompletionService<Integer> cs = new ExecutorCompletionService<Integer>(weedThreadPool);
for (int i = 0; i < 3; i++) {
cs.submit(() -> {
//获取计算任务
int init = 0;
for (int j = 0; j < 100; j++) {
init += r.nextInt();
}
Thread.sleep(1000L);
return Integer.valueOf(init);
});
}
weedThreadPool.shutdown();
/**
* 通过take方法获取,阻塞,直到有任务完成
*/
for (int i = 0; i < 3; i++) {
Future<Integer> future = cs.take();
if (future != null) {
System.out.println(future.get());
}
}
}
}

调用结果如下

我们也可以通过poll方法来获取。

1
2
3
4
5
6
7
csharp复制代码 		 /**
* 通过poll方法获取
*/
for (int i = 0; i < 3; i++) {
System.out.println(cs.poll(1200L,TimeUnit.MILLISECONDS).get());

}

结果自然是一样的


如果把阻塞时间改小一些,目前的代码就会出问题

1
2
3
4
5
6
7
csharp复制代码		/**
* 通过poll方法获取
*/
for (int i = 0; i < 3; i++) {
System.out.println(cs.poll(800L,TimeUnit.MILLISECONDS).get());

}

同样的,poll方法也可以用来打断超时执行的业务,比如在poll超时的情况下,直接调用线程池的shutdownNow(),残暴地关闭整个线程池。

1
2
3
4
5
6
7
ini复制代码	for (int i = 0; i < 3; i++) {
Future<Integer> poll = cs.poll(800L, TimeUnit.MILLISECONDS);
if (poll==null){
System.out.println("执行结束");
weedThreadPool.shutdownNow();
}
}

3、使用场景

选择怎么样的方法来异步执行任务,什么样的方式来接收任务,也是需要根据实际情况来考虑的。

  • 1.、需要批量提交异步任务的时候建议你使用 CompletionService。CompletionService 将线程池 Executor 和阻塞队列 BlockingQueue 的功能融合在了一起,能够让批量异步任务的管理更简单。
  • 2、让异步任务的执行结果有序化。先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待。
  • 3、线程池隔离。CompletionService支持创建知己的线程池,这种隔离性能避免几个特别耗时的任务拖垮整个应用的风险。

有需要的同学可以加我的公众号,以后的最新的文章第一时间都在里面,需要之前文章的思维导图也可以给我留言

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

图解 IP 基础知识!

发表于 2020-12-22

我把自己以往的文章汇总成为了 Github ,欢迎各位大佬 star
github.com/crisxuan/be…

IP 协议

路由器对分组进行转发后,就会把数据包传到网络上,数据包最终是要传递到客户端或者服务器上的,那么数据包怎么知道要发往哪里呢?起到关键作用的就是 IP 协议。

IP 主要分为三个部分,分别是 IP 寻址、路由和分包组包。下面我们主要围绕这三点进行阐述。

IP 地址

既然一个数据包要在网络上传输,那么肯定需要知道这个数据包到底发往哪里,也就是说需要一个目标地址信息,IP 地址就是连接网络中的所有主机进行通信的目标地址,因此,在网络上的每个主机都需要有自己的 IP 地址。

在 IP 数据报发送的链路中,有可能链路非常长,比如说由中国发往美国的一个数据报,由于网络抖动等一些意外因素可能会导致数据报丢失,这时我们在这条链路中会放入一些 中转站,一方面能够确保数据报是否丢失,另一方面能够控制数据报的转发,这个中转站就是我们前面聊过的路由器,这个转发过程就是 路由控制。

路由控制(Routing) 是指将分组数据发送到最终目标地址的功能,即时网络复杂多变,也能够通过路由控制到达目标地址。因此,一个数据报能否到达目标主机,关键就在于路由器的控制。

这里有一个名词,就是 跳,因为在一条链路中可能会布满很多路由器,路由器和路由器之间的数据报传送就是跳,比如你和隔壁老王通信,中间就可能会经过路由器 A-> 路由器 B -> 路由器 C 。

那么一跳的范围有多大呢?

一跳是指从源 MAC 地址到目标 MAC 地址之间传输帧的区间,这里引出一个新的名词,MAC 地址是啥?

MAC 地址指的就是计算机的物理地址(Physical Address),它是用来确认网络设备位置的地址。在 OSI 网络模型中,网络层负责 IP 地址的定位,而数据链路层负责 MAC 地址的定位。MAC 地址用于在网络中唯一标示一个网卡,一台设备若有一或多个网卡,则每个网卡都需要并会有一个唯一的 MAC 地址,也就是说 MAC 地址和网卡是紧密联系在一起的。

路由器的每一跳都需要询问当前中转的路由器,下一跳应该跳到哪里,从而跳转到目标地址。而不是数据报刚开始发送后,网络中所有的通路都会显示出来,这种多次跳转也叫做多跳路由。

IP 地址定义

现如今有两个版本的 IP 地址,IPv4 和 IPv6,我们首先探讨一下现如今还在广泛使用的 IPv4 地址,后面再考虑 IPv6 。

IPv4 由 32 位正整数来表示,在计算机内部会转化为二进制来处理,但是二进制不符合人类阅读的习惯,所以我们根据易读性的原则把 32 位的 IP 地址以 8 位为一组,分成四组,每组之间以 . 进行分割,再将每组转换为十进制数。如下图所示

那么上面这个 32 位的 IP 地址就会被转换为十进制的 156.197.1.1。

除此之外,从图中我们还可以得到如下信息

每个这样 8 位位一组的数字,自然是非负数,其取值范围是 [0,255]。

IP 地址的总个数有 2^32 次幂个,这个数值算下来是 4294967296 ,大概能允许 43 亿台设备连接到网络。实际上真的如此吗?

实际上 IP 不会以主机的个数来配置的,而是根据设备上的 网卡(NIC) 进行配置,每一块网卡都会设置一个或者多个 IP 地址,而且通常一台路由器会有至少两块网卡,所以可以设置两个以上的 IP 地址,所以主机的数量远远达不到 43 亿。

IP 地址构造和分类

IP 地址由 网络标识 和 主机标识 两部分组成,网络标识代表着网络地址,主机标识代表着主机地址。网络标识在数据链路的每个段配置不同的值。网络标识必须保证相互连接的每个段的地址都不重复。而相同段内相连的主机必须有相同的网络地址。IP 地址的 主机标识 则不允许在同一网段内重复出现。

举个例子来说:比如说我在石家庄(好像不用比如昂),我所在的小区的某一栋楼就相当于是网络标识,某一栋楼的第几户就相当于是我的主机标识,当然如果你有整栋楼的话,那就当我没说。你可以通过xx省xx市xx区xx路xx小区xx栋来定位我的网络标识,这一栋的第几户就相当于是我的主机标识

IP 地址分为四类,分别是 A类、B类、C类、D类、E类,它会根据 IP 地址中的第 1 位到第 4 位的比特对网络标识和主机标识进行分类。

  • A 类:(1.0.0.0 - 126.0.0.0)(默认子网掩码:255.0.0.0 或 0xFF000000)第一个字节为网络号,后三个字节为主机号。该类 IP 地址的最前面为 0 ,所以地址的网络号取值于 1~126 之间。一般用于大型网络。
  • B 类:(128.0.0.0 - 191.255.0.0)(默认子网掩码:255.255.0.0 或 0xFFFF0000)前两个字节为网络号,后两个字节为主机号。该类 IP 地址的最前面为 10 ,所以地址的网络号取值于 128~191 之间。一般用于中等规模网络。
  • C 类:(192.0.0.0 - 223.255.255.0)(子网掩码:255.255.255.0 或 0xFFFFFF00)前三个字节为网络号,最后一个字节为主机号。该类 IP 地址的最前面为 110 ,所以地址的网络号取值于 192~223 之间。一般用于小型网络。
  • D 类:是多播地址。该类 IP 地址的最前面为 1110 ,所以地址的网络号取值于 224~239 之间。一般用于多路广播用户。
  • E 类:是保留地址。该类 IP 地址的最前面为 1111 ,所以地址的网络号取值于 240~255 之间。

为了方便理解,我画了一张 IP 地址分类图,如下所示

根据不同的 IP 范围,有下面不同的地总空间分类

子网掩码

子网掩码(subnet mask) 又叫做网络掩码,它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的网络。子网掩码是一个 32位 地址,用于屏蔽 IP 地址的一部分以区别网络标识和主机标识。

一个 IP 地址只要确定了其分类,也就确定了它的网络标识和主机标识,由此,各个分类所表示的网络标识范围如下

用 1 表示 IP 网络地址的比特范围,0 表示 IP 主机地址的范围。将他们用十进制表示,那么这三类的表示如下

保留地址

在IPv4 的几类地址中,有几个保留的地址空间不能在互联网上使用。这些地址用于特殊目的,不能在局域网外部路由。

IP 协议版本

目前,全球 Internet 中共存有两个IP版本:IP 版本 4(IPv4)和 IP 版本6(IPv6)。 IP 地址由二进制值组成,可驱动 Internet 上所有数据的路由。 IPv4 地址的长度为 32 位,而 IPv6 地址的长度为 128 位。

Internet IP 资源由 Internet 分配号码机构(IANA)分配给区域 Internet 注册表(RIR),例如 APNIC,该机构负责根 DNS ,IP 寻址和其他 Internet 协议资源。

下面我们就一起认识一下 IP 协议中非常重要的两个版本 IPv4 和 IPv6。

IPv4

IPv4 的全称是 Internet Protocol version 4,是 Internet 协议的第四版。IPv4 是一种无连接的协议,这个协议会尽最大努力交付数据包,也就是说它不能保证任何数据包能到达目的地,也不能保证所有的数据包都会按照正确的顺序到达目标主机,这些都是由上层比如传输控制协议控制的。也就是说,单从 IP 看来,这是一个不可靠的协议。

前面我们讲过网络层分组被称为 数据报,所以我们接下来的叙述也会围绕着数据报展开。

IPv4 的数据报格式如下

IPv4 数据报中的关键字及其解释

  • 版本字段(Version)占用 4 bit,通信双方使用的版本必须一致,对于 IPv4 版本来说,字段值是 4。
  • 首部长度(Internet Header Length) 占用 4 bit,首部长度说明首部有多少 32 位(4 字节)。由于 IPv4 首部可能包含不确定的选项,因此这个字段被用来确定数据的偏移量。大多数 IP 不包含这个选项,所以一般首部长度设置为 5, 数据报为 20 字节 。
  • 服务类型(Differential Services Codepoint,DSCP) 占用 6 bit,以便使用不同的 IP 数据报,比如一些低时延、高吞吐量和可靠性的数据报。服务类型如下表所示

  • 拥塞通告(Explicit Congestion Notification,ECN) 占用 2 bit,它允许在不丢弃报文的同时通知对方网络拥塞的发生。ECN 是一种可选的功能,仅当两端都支持并希望使用,且底层网络支持时才被使用。 最开始 DSCP 和 ECN 统称为 TOS,也就是区分服务,但是后来被细化为了 DSCP 和 ECN。
  • 数据报长度(Total Length) 占用 16 bit,这 16 位是包括在数据在内的总长度,理论上数据报的总长度为 2 的 16 次幂 - 1,最大长度是 65535 字节,但是实际上数据报很少有超过 1500 字节的。IP 规定所有主机都必须支持最小 576 字节的报文,但大多数现代主机支持更大的报文。当下层的数据链路协议的最大传输单元(MTU)字段的值小于 IP 报文长度时,报文就必须被分片。
  • 标识符(Identification) 占用 16 bit,这个字段用来标识所有的分片,因为分片不一定会按序到达,所以到达目标主机的所有分片会进行重组,每产生一个数据报,计数器加1,并赋值给此字段。
  • 标志(Flags) 占用 3 bit,标志用于控制和识别分片,这 3 位分别是
+ 0 位:保留,必须为0;
+ 1 位:`禁止分片(Don’t Fragment,DF)`,当 DF = 0 时才允许分片;
+ 2 位:`更多分片(More Fragment,MF)`,MF = 1 代表后面还有分片,MF = 0 代表已经是最后一个分片。如果 DF 标志被设置为 1 ,但是路由要求必须进行分片,那么这条数据报回丢弃
  • 分片偏移(Fragment Offset) 占用 13 位,它指明了每个分片相对于原始报文开头的偏移量,以 8 字节作单位。
  • 存活时间(Time To Live,TTL) 占用 8 位,存活时间避免报文在互联网中迷失,比如陷入路由环路。存活时间以秒为单位,但小于一秒的时间均向上取整到一秒。在现实中,这实际上成了一个跳数计数器:报文经过的每个路由器都将此字段减 1,当此字段等于 0 时,报文不再向下一跳传送并被丢弃,这个字段最大值是 255。
  • 协议(Protocol) 占用 8 位,这个字段定义了报文数据区使用的协议。协议内容可以在 www.iana.org/assignments… 官网上获取。
  • 首部校验和(Header Checksum) 占用 16 位,首部校验和会对字段进行纠错检查,在每一跳中,路由器都要重新计算出的首部检验和并与此字段进行比对,如果不一致,此报文将会被丢弃。
  • 源地址(Source address) 占用 32 位,它是 IPv4 地址的构成条件,源地址指的是数据报的发送方
  • 目的地址(Destination address)占用 32 位,它是 IPv4 地址的构成条件,目标地址指的是数据报的接收方
  • 选项(Options) 是附加字段,选项字段占用 1 - 40 个字节不等,一般会跟在目的地址之后。如果首部长度 > 5,就应该考虑选项字段。
  • 数据 不是首部的一部分,因此并不被包含在首部检验和中。

在 IP 发送的过程中,每个数据报的大小是不同的,每个链路层协议能承载的网络层分组也不一样,有的协议能够承载大数据报,有的却只能承载很小的数据报,不同的链路层能够承载的数据报大小如下。

IPv4 分片

一个链路层帧能承载的最大数据量叫做最大传输单元(Maximum Transmission Unit, MTU),每个 IP 数据报封装在链路层帧中从一台路由器传到下一台路由器。因为每个链路层所支持的最大 MTU 不一样,当数据报的大小超过 MTU 后,会在链路层进行分片,每个数据报会在链路层单独封装,每个较小的片都被称为 片(fragement)。

每个片在到达目的地后会进行重组,准确的来说是在运输层之前会进行重组,TCP 和 UDP 都会希望发送完整的、未分片的报文,出于性能的原因,分片重组不会在路由器中进行,而是会在目标主机中进行重组。

当目标主机收到从发送端发送过来的数据报后,它需要确定这些数据报中的分片是否是由源数据报分片传递过来的,如果是的话,还需要确定何时收到了分片中的最后一片,并且这些片会如何拼接一起成为数据报。

针对这些潜在的问题,IPv4 设计者将 标识、标志和片偏移放在 IP 数据报首部中。当生成一个数据报时,发送主机会为该数据报设置源和目的地址的同时贴上标识号。发送主机通常将它发送的每个数据报的标识 + 1。当某路由器需要对一个数据报分片时,形成的每个数据报具有初始数据报的源地址、目标地址和标识号。当目的地从同一发送主机收到一系列数据报时,它能够检查数据报的标识号以确定哪些数据是由源数据报发送过来的。由于 IP 是一种不可靠的服务,分片可能会在网路中丢失,鉴于这种情况,通常会把分片的最后一个比特设置为 0 ,其他分片设置为 1,同时使用偏移字段指定分片应该在数据报的哪个位置。

IPv4 寻址

IPv4 支持三种不同类型的寻址模式,分别是

  • 单播寻址模式:在这种模式下,数据只发送到一个目的地的主机。

  • 广播寻址模式:在此模式下,数据包将被寻址到网段中的所有主机。这里客户端发送一个数据包,由所有服务器接收:

  • 组播寻址模式:此模式是前两种模式的混合,即发送的数据包既不指向单个主机也不指定段上的所有主机

IPv6

随着断系统接入的越来越多,IPv4 已经无法满足分配了,所以,IPv6 应运而生,IPv6 就是为了解决 IPv4 的地址耗尽问题而被标准化的网际协议。IPv4 的地址长度为 4 个 8 字节,即 32 比特, 而 IPv6 的地址长度是原来的四倍,也就是 128 比特,一般写成 8 个 16 位字节。

从 IPv4 切换到 IPv6 及其耗时,需要将网络中所有的主机和路由器的 IP 地址进行设置,在互联网不断普及的今天,替换所有的 IP 是一个工作量及其庞大的任务。我们后面会说。

我们先来看一下 IPv6 的地址是怎样的

  • 版本与 IPv4 一样,版本号由 4 bit 构成,IPv6 版本号的值为 6。
  • 流量类型(Traffic Class) 占用 8 bit,它就相当于 IPv4 中的服务类型(Type Of Service)。
  • 流标签(Flow Label) 占用 20 bit,这 20 比特用于标识一条数据报的流,能够对一条流中的某些数据报给出优先权,或者它能够用来对来自某些应用的数据报给出更高的优先权,只有流标签、源地址和目标地址一致时,才会被认为是一个流。
  • 有效载荷长度(Payload Length) 占用 16 bit,这 16 比特值作为一个无符号整数,它给出了在 IPv6 数据报中跟在鼎昌 40 字节数据报首部后面的字节数量。
  • 下一个首部(Next Header) 占用 8 bit,它用于标识数据报中的内容需要交付给哪个协议,是 TCP 协议还是 UDP 协议。
  • 跳限制(Hop Limit) 占用 8 bit,这个字段与 IPv4 的 TTL 意思相同。数据每经过一次路由就会减 1,减到 0 则会丢弃数据。
  • 源地址(Source Address) 占用 128 bit (8 个 16 位 ),表示发送端的 IP 地址。
  • 目标地址(Destination Address) 占用 128 bit (8 个 16 位 ),表示接收端 IP 地址。

可以看到,相较于 IPv4 ,IPv6 取消了下面几个字段

  • 标识符、标志和比特偏移:IPv6 不允许在中间路由器上进行分片和重新组装。这种操作只能在端系统上进行,IPv6 将这个功能放在端系统中,加快了网络中的转发速度。
  • 首部校验和:因为在运输层和数据链路执行了报文段完整性校验工作,IP 设计者大概觉得在网络层中有首部校验和比较多余,所以去掉了。IP 更多专注的是快速处理分组数据。
  • 选项字段:选项字段不再是标准 IP 首部的一部分了,但是它并没有消失,而是可能出现在 IPv6 的扩展首部,也就是下一个首部中。

IPv6 扩展首部

IPv6 首部长度固定,无法将选项字段加入其中,取而代之的是 IPv6 使用了扩展首部

扩展首部通常介于 IPv6 首部与 TCP/UDP 首部之间,在 IPv4 中可选长度固定位 40 字节,在 IPv6 中没有这样的限制。IPv6 的扩展首部可以是任意长度。扩展首部中还可以包含扩展首部协议和下一个扩展字段。

IPv6 首部中没有标识和标志字段,对 IP 进行分片时,需要使用到扩展首部。

具体的扩展首部表如下所示

下面我们来看一下 IPv6 都有哪些特点

IPv6 特点

IPv6 的特点在 IPv4 中得以实现,但是即便实现了 IPv4 的操作系统,也未必实现了 IPv4 的所有功能。而 IPv6 却将这些功能大众化了,也就表明这些功能在 IPv6 已经进行了实现,这些功能主要有

  • 地址空间变得更大:这是 IPv6 最主要的一个特点,即支持更大的地址空间。
  • 精简报文结构: IPv6 要比 IPv4 精简很多,IPv4 的报文长度不固定,而且有一个不断变化的选项字段;IPv6 报文段固定,并且将选项字段,分片的字段移到了 IPv6 扩展头中,这就极大的精简了 IPv6 的报文结构。
  • 实现了自动配置:IPv6 支持其主机设备的状态和无状态自动配置模式。这样,没有 DHCP 服务器不会停止跨段通信。
  • 层次化的网络结构: IPv6 不再像 IPv4 一样按照 A、B、C等分类来划分地址,而是通过 IANA -> RIR -> ISP 这样的顺序来分配的。IANA 是国际互联网号码分配机构,RIR 是区域互连网注册管理机构,ISP 是一些运营商(例如电信、移动、联通)。
  • IPSec:IPv6 的扩展报头中有一个认证报头、封装安全净载报头,这两个报头是 IPsec 定义的。通过这两个报头网络层自己就可以实现端到端的安全,而无需像 IPv4 协议一样需要其他协议的帮助。
  • 支持任播:IPv6 引入了一种新的寻址方式,称为任播寻址。

IPv6 地址

我们知道,IPv6 地址长度为 128 位,他所能表示的范围是 2 ^ 128 次幂,这个数字非常庞大,几乎涵盖了你能想到的所有主机和路由器,那么 IPv6 该如何表示呢?

一般我们将 128 比特的 IP 地址以每 16 比特为一组,并用 : 号进行分隔,如果出现连续的 0 时还可以将 0 省略,并用 :: 两个冒号隔开,记住,一个 IP 地址只允许出现一次两个连续的冒号。

下面是一些 IPv6 地址的示例

  • 二进制数表示

  • 用十六进制数表示

  • 出现两个冒号的情况

如上图所示,A120 和 4CD 中间的 0 被 :: 所取代了。

另外,我自己肝了六本 PDF,微信搜索 程序员cxuan 关注公众号后,在后台回复 cxuan ,领取全部 PDF,这些 PDF 如下

六本 PDF 链接

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis核心数据结构与高性能原理

发表于 2020-12-22

点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章。

五种常用数据结构

String 结构

字符串常用操作

1
2
3
4
5
6
7
java复制代码SET key value 	//存入字符串键值对
MSET key value [key value ...] //批量存储字符串键值对
SETNX key value //存入一个不存在的字符串键值对
GET key //获取一个字符串键值
MGET key [key ...] //批量获取字符串键值
DEL key [key ...] //删除一个键
EXPIRE key seconds //设置一个键的过期时间(秒)

原子加减

1
2
3
4
java复制代码INCR key //将key中储存的数字值加1
DECR key //将key中储存的数字值减1
INCRBY key increment //将key所储存的值加上increment
DECRBY key decrement //将key所储存的值减去decrement

String 应用场景

  • 单值缓存
  • 对象缓存
  • 分布式锁
  • 计数器
  • Web集群Session共享
  • 分布式系统全局序列号
  1. 单值缓存
1
2
java复制代码SET key value
Get key
  1. 对象缓存
1
2
3
java复制代码SET user:1 value(json格式数据)
MSET user:1:name yijiaoqian user:1:balance 1888
MGET user:1:name user:1:balance
  1. 分布式锁
1
2
3
4
5
6
java复制代码SETNX product:10001  true //返回1代表获取锁成功
SETNX product:10001 true //返回0代表获取锁失败
...执行业务操作...
DEL product:10001 //执行完业务释放锁

SET product:10001 true ex 10 nx //防止程序意外终止导致死锁
  1. 计数器
1
2
java复制代码INCR article:readcount:{文章id}  	
GET article:readcount:{文章id}
  1. Web集群Session共享

Spring session + redis 实现sessio共享

  1. 分布式系统全局序列号
1
java复制代码INCRBY orderId 1000	//redis批量生成序列号提升性能

Hash 结构

Hash常用操作

1
2
3
4
5
6
7
8
9
10
java复制代码HSET key field value 	//存储一个哈希表key的键值
HSETNX key field value //存储一个不存在的哈希表key的键值
HMSET key field value [field value ...] //在一个哈希表key中存储多个键值对
HGET key field //获取哈希表key对应的field键值
HMGET key field [field ...] //批量获取哈希表key中多个field键值
HDEL key field [field ...] //删除哈希表key中的field键值
HLEN key //返回哈希表key中field的数量
HGETALL key //返回哈希表key中所有的键值

HINCRBY key field increment //为哈希表key中field键的值加上增量increment

Hash应用场景

  • 对象存储
1
2
3
java复制代码HMSET user {userId}:name  yijiaoqian {userId}:balance  1888
HMSET user 1:name yijiaoqian 1:balance 1888
HMGET user 1:name 1:balance
  • 电商购物车
  1. 以用户id为key
  2. 商品id为field
  3. 商品数量为value


购物车操作:

  1. 添加商品:hset cart:1001 10088 1
  2. 增加数量:hincrby cart:1001 10088 1
  3. 商品总数:hlen cart:1001
  4. 删除商品:hdel cart:1001 10088
  5. 获取购物车所有商品:hgetall cart:1001

Hash结构优缺点

优点:

  1. 同类数据归类整合储存,方便数据管理
  2. 相比string操作消耗内存与cpu更小
  3. 相比string储存更节省空间

缺点:

  1. 过期功能不能使用在field上,只能用在key上
  2. Redis集群架构下不适合大规模使用

List 结构

List常用操作

1
2
3
4
5
6
7
8
java复制代码LPUSH key value [value ...] //将一个或多个值value插入到key列表的表头(最左边)
RPUSH key value [value ...] //将一个或多个值value插入到key列表的表尾(最右边)
LPOP key //移除并返回key列表的头元素
RPOP key //移除并返回key列表的尾元素
LRANGE key start stop //返回列表key中指定区间内的元素,区间以偏移量start和stop指定

BLPOP key [key ...] timeout //从key列表表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key ...] timeout //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待

List应用场景

  • 常用数据结构:
  1. Stack(栈) = LPUSH + LPOP (FILO)
  2. Queue(队列)= LPUSH + RPOP (FIFO)
  3. Blocking MQ(阻塞队列)= LPUSH + BRPOP

  • 微博和微信公号消息流

1
2
3
4
5
6
7
java复制代码一角钱关注了雷军、马云等大V
1)雷布斯发微博,消息ID为10018
LPUSH msg:{一角钱-ID} 10018
2)马云发微博,消息ID为10086
LPUSH msg:{一角钱-ID} 10086
3)查看最新微博消息
LRANGE msg:{一角钱-ID} 0 4

Set 结构

Set常用操作

1
2
3
4
5
6
7
java复制代码SADD key member [member ...]	//往集合key中存入元素,元素存在则忽略,若key不存在则新建
SREM key member [member ...] //从集合key中删除元素
SMEMBERS key //获取集合key中所有元素
SCARD key //获取集合key的元素个数
SISMEMBER key member //判断member元素是否存在于集合key中
SRANDMEMBER key [count] //从集合key中选出count个元素,元素不从key中删除
SPOP key [count] //从集合key中选出count个元素,元素从key中删除

Set运算操作

1
2
3
4
5
6
java复制代码SINTER key [key ...] //交集运算
SINTERSTORE destination key [key ..] //将交集结果存入新集合destination中
SUNION key [key ..] //并集运算
SUNIONSTORE destination key [key ...] //将并集结果存入新集合destination中
SDIFF key [key ...] //差集运算
SDIFFSTORE destination key [key ...] //将差集结果存入新集合destination中

Set应用场景

  • 微信抽奖小程序

1
2
3
shell复制代码1.点击参与抽奖加入集合: SADD key {userID} 
2.查看参与抽奖所有用户:SMEMBERS key
3.抽取count名中奖者:SRANDMEMBER key [count]/ SPOP key [count]
  • 微信微博点赞、收藏、标签

1
2
3
4
5
shell复制代码1.点赞: SADD  like:{消息ID}  {用户ID} 
2.取消点赞: SREM like:{消息ID} {用户ID}
3.检查用户是否点过赞: SISMEMBER like:{消息ID} {用户ID}
4.获取点赞的用户列表: SMEMBERS like:{消息ID}
5.获取点赞用户数: SCARD like:{消息ID}
  • 集合操作

1
2
3
java复制代码SINTER set1 set2 set3 -> { c } 	// 交集
SUNION set1 set2 set3 -> { a,b,c,d,e } // 并集
SDIFF set1 set2 set3 -> { a } // 差集
  • 集合操作实现微博微信关注模型

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码1) 张三关注的人: 
zhangsanSet-> {lisi, wangwu}
2) 一角钱关注的人:
yijiaoqianSet--> {zhangsan, zhaoliu, lisi, wangwu}
3) 李四关注的人:
lisiSet-> {zhangsan, yijiaoqian, zhaoliu, wangwu, xunyu)
4) 我和一角钱共同关注:
SINTER zhangsanSet yijiaoqianSet--> {lisi, wangwu}
5) 我关注的人也关注他(一角钱):
SISMEMBER lisiSet yijiaoqian
SISMEMBER wangwuSet yijiaoqian
6) 我可能认识的人:
SDIFF yijiaoqianSet zhangsanSet->(zhangsan, zhaoliu}
  • 集合操作实现电商商品筛选

1
2
3
4
5
6
7
8
java复制代码SADD brand:huawei  P40
SADD brand:xiaomi mi-10
SADD brand:iPhone iphone12
SADD os:android P40 mi-10
SADD cpu:brand:intel P40 mi-10
SADD ram:8G P40 mi-10 iphone12

SINTER os:android cpu:brand:intel ram:8G > {P40,mi-10}

ZSet 有序集合结构

ZSet常用操作

1
2
3
4
5
6
7
shell复制代码ZADD key score member [[score member]…]	//往有序集合key中加入带分值元素
ZREM key member [member …] //从有序集合key中删除元素
ZSCORE key member //返回有序集合key中元素member的分值
ZINCRBY key increment member //为有序集合key中元素member的分值加上increment
ZCARD key //返回有序集合key中元素个数
ZRANGE key start stop [WITHSCORES] //正序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]//倒序获取有序集合key从start下标到stop下标的元素

ZSet集合操作

1
2
shell复制代码ZUNIONSTORE destkey numkeys key [key ...] 	//并集计算
ZINTERSTORE destkey numkeys key [key …] //交集计算

ZSet应用场景

  • ZSet集合操作实现排行榜

)

1
2
3
4
5
6
7
8
9
shell复制代码1. 点击新闻:
ZINCRBY hotNews:20201221 1 完善低龄未成年人犯罪规定
2. 展示当日排行前十:
ZREVRANGE hotNews:20201221 0 9 WITHSCORES
3. 七日搜索榜单计算:
ZUNIONSTORE hotNews:20201215-20201221 7
hotNews:20201215 hotNews:20201216... hotNews:20201221
4. 展示七日排行前十:
ZREVRANGE hotNews:20201215-20201221 0 9 WITHSCORES

Redis的单线程和高性能

Redis是单线程吗?

Redis的单线程主要是指 Redis 的网络IO和键值对读写是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程。但是Redis的其他功能,比如持久化、异步删除、集群数据同步等,其实由额外的线程执行的。

Redis 单线程为什么还能这么快?

因为它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免来多线程的切换性能损耗问题,正因为Redis是单线程,所以要小心使用Redis 指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。

Redis 单线程如何处理那么多的并发客户端连接?

Redis 的IO多路复用:redis利用epoll实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。

1
2
3
4
java复制代码# 查看redis支持的最大连接数,在redis.conf文件中可修改,# maxclients 10000
127.0.0.1:6379> CONFIG GET maxclients
    ##1) "maxclients"
    ##2) "10000"

其他高级命令

keys:全量遍历键

用来列出所有满足特定正则字符串规则的key,当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
java复制代码127.0.0.1:6379> set codehole1 a
OK
127.0.0.1:6379> set codehole2 b
OK
127.0.0.1:6379> set codehole3 c
OK
127.0.0.1:6379> set code1hole a
OK
127.0.0.1:6379> set code2hole b
OK
127.0.0.1:6379> set code3hole c
OK
127.0.0.1:6379> keys *
1) "codehole1"
2) "codehole3"
3) "codehole2"
4) "code3hole"
5) "code1hole"
6) "code2hole"
127.0.0.1:6379> keys codehole*
1) "codehole1"
2) "codehole3"
3) "codehole2"
127.0.0.1:6379> keys code*hole
1) "code3hole"
2) "code1hole"
3) "code2hole"

scan:渐进式遍历键

1
java复制代码SCAN cursor [MATCH pattern] [COUNT count]

scan 参数提供了三个参数:

  • 第一个参数 cursor 整数值(hash桶的索引值)
  • 第二个是 key 的正则模式
  • 第三个是一次遍历的key的数量(参考值,底层遍历的数量不一定),并不少符合条件的结果数量。

第一次遍历时,cursor 值为0,然后将返回结果中的第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为0时结束。

注意:但是scan并非完美无暇,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。

info:查看redis服务运行信息

分为 9 大块,每个块都有非常多的参数:

  • Server 服务器运行的环境参数
  • Clients 客户端相关信息
  • Memory 服务器运行内存的统计数据
  • Persistence 持久化信息
  • Stats 通用统计数据
  • Replication 主从复制相关信息
  • CPU CPU使用情况
  • Cluster 集群信息
  • KeySpace 键值对统计数量信息


核心属性说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码connected_clients:2                  # 正在连接的客户端数量

instantaneous_ops_per_sec:789 # 每秒执行多少次指令

used_memory:929864 # Redis分配的内存总量(byte),包含redis进程内部的开销和数据占用的内存
used_memory_human:908.07K # Redis分配的内存总量(Kb,human会展示出单位)
used_memory_rss_human:2.28M # 向操作系统申请的内存大小(Mb)(这个值一般是大于used_memory的,因为Redis的内存分配策略会产生内存碎片)
used_memory_peak:929864 # redis的内存消耗峰值(byte)
used_memory_peak_human:908.07K # redis的内存消耗峰值(KB)

maxmemory:0 # 配置中设置的最大可使用内存值(byte),默认0,不限制
maxmemory_human:0B # 配置中设置的最大可使用内存值
maxmemory_policy:noeviction # 当达到maxmemory时的淘汰策略

文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

公司大佬说我不懂String,一个Stringintern

发表于 2020-12-21

String是我们日常开发中经常使用的一个类,关于它的使用相信大家都不会陌生,今天就说说其中的一个方法String.intern();其实它的底层并不简单,一起来看看吧。

在这里插入图片描述

另外本人整理了20年面试题大全,包含spring、并发、数据库、Redis、分布式、dubbo、JVM、微服务等方面总结,需要的话下面链接自行领取:腾讯文档

First Blood

先看下面的代码:

1
2
3
java复制代码String s = new String("1");
String s1 = s.intern();
System.out.println(s == s1);
1
2
java复制代码打印结果为:
false

对于new String(“1”),会生成两个对象,一个是String类型对象,它将存储在Java Heap中,另一个是字符串常量对象”1”,它将存储在字符串常量池中。

s.intern()方法首先会去字符串常量池中查找是否存在字符串常量对象”1”,如果存在则返回该对象的地址,如果不存在则在字符串常量池中生成为一个”1”字符串常量对象,并返回该对象的地址。

如下图:

在这里插入图片描述
变量s指向的是Stirng类型对象,变量s1指向的是”1”字符串常量对象,所以s == s1结果为false。

Double kill

在上面的基础上我们再定义一个s2如下:

1
2
3
4
5
java复制代码String s = new String("1");
String s1 = s.intern();
String s2 = "1";
System.out.println(s == s1);
System.out.println(s1 == s2); // true

s1 == s2为true,表示变量s2是直接指向的字符串常量,如下图

在这里插入图片描述

Triple kill

在上面的基础上我们再定义一个t如下:

1
2
3
4
5
6
7
8
java复制代码String s = new String("1");
String t = new String("1");
String s1 = s.intern();
String s2 = "1";
System.out.println(s == s1);
System.out.println(s1 == s2);
System.out.println(s == t); // false
System.out.println(s.intern() == t.intern()); // true

s == t为false,这个很明显,变量s和变量t指向的是不同的两个String类型的对象。
s.intern() == t.intern()为true,因为intern方法返回的是字符串常量池中的同一个”1”对象,所以为true。

在这里插入图片描述

Quadra Kill

在上面的基础上我们再定义一个x和s3如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码String s = new String("1");
String t = new String("1");
String x = new String("1") + new String("1");
String s1 = s.intern();
String s2 = "1";
String s3 = "11";
System.out.println(s == s1);
System.out.println(s1 == s2);
System.out.println(s == t);
System.out.println(s.intern() == t.intern());
System.out.println(x == s3); // fasle
System.out.println(x.intern() == s3.intern()); // true

变量x为两个String类型的对象相加,因为x != s3,所以x肯定不是指向的字符串常量,实际上x就是一个String类型的对象,调用x.intern()方法将返回”11”对应的字符串常量,所以x.intern() == s3.intern()为true。

Rampage

将上面的代码简化并添加几个变量如下:

1
2
3
4
5
6
7
8
java复制代码String x = new String("1") + new String("1");
String x1 = new String("1") + "1";
String x2 = "1" + "1";
String s3 = "11";

System.out.println(x == s3); // false
System.out.println(x1 == s3); // false
System.out.println(x2 == s3); // true

x == s3为false表示x指向String类型对象,s3指向字符串常量;
x1 == s3为false表示x1指向String类型对象,s3指向字符串常量;
x2 == s3为true表示x2指向字符串常量,s3指向字符串常量;

所以我们可以看到new String("1") + "1"返回的String类型的对象。

总结

现在我们知道intern方法就是将字符串保存到常量池中,在保存字符串到常量池的过程中会先查看常量池中是否已经存在相等的字符串,如果存在则直接使用该字符串。

所以我们在写业务代码的时候,应该尽量使用字符串常量池中的字符串,比如使用String s = "1";比使用new String("1");更节省内存。我们也可以使用String s = 一个String类型的对象.intern();方法来间接的使用字符串常量,这种做法通常用在你接收到一个String类型的对象而又想节省内存的情况下,当然你完全可以String s = 一个String类型的对象;但是这么用可能会因为变量s的引用而影响String类型对象的垃圾回收。所以我们可以使用intern方法进行优化,但是需要注意的是intern能节省内存,但是会影响运行速度,因为该方法需要去常量池中查询是否存在某字符串。

点个小赞,好运不断,来个关注,青春常驻

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…750751752…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%