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

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


  • 首页

  • 归档

  • 搜索

SpringCloud-微服务概述 为什么要使用微服务? 微

发表于 2021-11-06

好久不见,从今天起开启新的学习连载—SpringCloud。

由于篇幅过多,我会把我的学习笔记分成几大块展开

今天只讲概念,谈谈什么是微服务。

学习中用到的学习资料如下:

文章:SpringCloud极简入门

视频:Spring Cloud从入门到实战


为什么要使用微服务?

传统的 Java Web 都是采用单体架构的方式来进行开发、部署、运维的,所谓单体架构就是将 Application 的所有业务模块全部打包在一个文件中进行部署。但是随着互联网的发展、用户数量的激增、业务的极速扩展,传统的单体应用方式的缺点就逐渐显现出来了,给开发、部署、运维都带来了极大的难度,工作量会越来越大,难度越来越高。

因为在单体架构中,所有的业务模块的耦合性太高,耦合性过高的同时项目体量又很大势必会给各个技术环节带来挑战。项目越进行到后期,这种难度越大,只要有改动,整个应用都需要重新测试,部署,极大的限制了开发的灵活性,降低了开发效率。同时也带来了更大的安全隐患,如果某个模块发生故障无法正常运行就有可能导致整个项目崩溃,单体应用的架构如下图所示。

单体应用存在的问题

  • 随着业务的发展,开发变得越来越复杂。
  • 修改、新增某个功能,需要对整个系统进行测试,重新部署。
  • 一个模块出现问题,很可能导致整个系统崩溃。
  • 多团队同时对数据进行管理,容易产生安全漏洞。
  • 各模块使用同一种技术框架,很难根据具体业务需求选择更合适的框架,局限性太大。
  • 模块内容太复杂,如果员工离职,可能需要很长时间才能完成任务交接。

为了解决上述问题,微服务架构应运而生,简单来说,微服务就是将一个单体应用拆分成若干个小型的服务,协同完成系统功能的一种架构模式,在系统架构层面进行解耦合,将一个复杂问题拆分成若干个简单问题。这样的好处是对于每一个简单问题,开发、维护、部署的难度就降低了很多,可以实现自治,可自主选择最合适的技术框架,提高了项目开发的灵活性。

微服务就像一个饭店,原本一个厨师负责切菜洗锅颠勺烹饪卤煮所有的活,现在把厨房里的活分模块的包给好多个厨师专门负责,然后多个厨师互相协作维持整个厨房的工作流程。

微服务架构不仅是单纯的拆分,拆分之后的各个微服务之间还要进行通信,否则就无法协同完成需求,也就失去了拆分的意义。不同的微服务之间可以通过某种协议进行通信,相互调用、协同完成功能,并且各服务之间只需要制定统一的协议即可,至于每个微服务是用什么技术框架来实现的,统统不需要关心。这种松耦合的方式使得开发、部署都变得更加灵活,同时系统更容易扩展,降低了开发、运维的难度,单体应用拆分之后的架构如下图所示。

微服务的优点

  • 各个服务之间实现了松耦合,彼此之间不需要关注对方是用什么语言,什么技术开发的,只需要保证自己的接口可以正常访问,通过标准协议可以访问其他微服务的接口即可。
  • 各个微服务之间是独立自治的,只需要专注于做好自己的业务,开发和维护不会影响到其他的微服务,这和单体架构中”牵一发而动全身”相比是有很大优势的。
  • 微服务是一个去中心化的架构方式,相当于用零件去拼接一台机器,如果某个零件出现问题,可以随时进行替换从而保证机器的正常运转,微服务就相当于零件,整个项目就相当于零件组成的机器。

微服务的不足

  • 各个服务之间是通过远程调用的方式来完成协作任务的,如果因为某些原因导致远程调用出现问题,导致微服务不可用,就有可能产生级联反应,造成整个系统崩溃。
  • 如果某个需求需要调用多个微服务,如何来保证数据的一致性是一个比较大的问题,这就给给分布式事务处理带来了挑战。
  • 相比较于单体应用开发,微服务的学习难度会增加,对于加入团队的新员工来讲,如何快速掌握上手微服务架构,是他需要面对的问题。

即便是微服务存在这样那样的问题,也不能掩盖这种技术选型在当今互联网环境下的巨大优势,了解完微服务的基本概念、优缺点之后,我们来谈一谈微服务的设计原则。

如何来拆分服务也是一个有难度的挑战,如果拆分的服务粒度太小,导致微服务可完成的功能有限,这种情况反而会增加系统的复杂度。相反如果服务粒度太大,又没有实现足够程度的解耦合,那它本质上就还是单体应用的架构,同样是失去了拆分服务的意义。比较合理的拆分方式是由大到小,提炼出核心需求,搞清楚服务间的交互关系,先拆分成粒度相对较大的服务,然后再根据具体的业务需求逐步细化服务粒度,最终形成一套合理的微服务体系。

微服务设计原则

  • 服务粒度不能过大也不能过小,提炼核心需求,根据服务间的交互关系找到最合理的服务粒度。
  • 各个微服务的功能职责尽量单一,避免出现多个服务处理同一个需求。
  • 各个微服务之间要相互独立、自治,自主开发、自主部署、自主维护。
  • 保证数据独立性,各个微服务独立管理其业务模块下的数据,可开放出接口供其他微服务访问数据,但是其他微服务不能直接管理这些数据。
  • 使用 REST 协议完成微服务之间的协作任务,数据交互采用 JSON 格式,方便调用与整合。

综上所述,微服务的架构设计不是一次成型的,需要反复进行实践和总结,不断进行优化,最终形成一套更为合理的解决方案,微服务架构图如下所示。

微服务架构的核心组件

  • 服务治理(服务注册、服务发现)

拆分之后的微服务首先需要完成的工作就是实现服务治理,包括服务注册和服务发现。这里我们把微服务分为两类:提供服务的叫做服务提供者,调用服务的叫做服务消费者。一个服务消费者首先需要知道有哪些可供调用的微服务,以及如何来调用这些微服务。所以就需要将所有的服务提供者在注册中心完成注册,记录服务信息,如 IP 地址、端口等,然后服务消费者可以通过服务发现获取服务提供者的这些信息,从而实现调用。

  • 服务负载均衡

微服务间的负载均衡是必须要考虑的,服务消费者在调用服务提供者的接口时,可根据配置选择某种负载均衡算法,从服务提供者列表中选择具体要调用的实例,从而实现服务消费者与服务提供者之间的负载均衡。

  • 微服务网关

在一个微服务架构中会包含多个微服务实例,每个微服务实例都有其特定的网络信息,如 IP 地址、端口等,很多时候完成一个需求需要多个微服务来协同工作,那么客户端就需要频繁请求不同的 URL ,不便于统一管理,可以使用微服务网关来解决这一问题,为客户端提供统一的入口,系统内部由网关将请求映射到不同的微服务。

  • 微服务容错

前面我们提到过,各个服务之间是通过远程调用的方式来完成协作任务的,如果因为某些原因使得远程调用出现问题,导致微服务不可用,就有可能产生级联反应,造成整个系统崩溃。这个问题我们可以使用微服务的熔断器来处理,熔断器可以防止整个系统因某个微服务调用失败而产生级联反应导致系统崩溃的情况。

  • 分布式配置

每个微服务都有其对应的配置文件,在一个大型项目中管理这些配置文件也是工作量很大的一件事情,为了提高效率、便于管理,我们可以对各个微服务的配置文件进行集中统一管理,将配置文件集中保存到本地系统或者 Git 仓库,再由各个微服务读取自己的配置文件。

  • 服务监控

一个分布式系统中往往会部署很多个微服务,这些服务彼此之间会相互调用,整个过程就会较为复杂,我们在进行问题排查或者优化的时候工作量就会比较大。如果我们能准确跟踪到每一个网络请求,了解它整个运行流程,经过了哪些微服务、是否有延迟、耗费时间等,这样的话我们分析系统性能,排查解决问题就会容易很多,这就是服务监控。

Spring Cloud

微服务是一种分布式软件架构设计方式,具体的落地框架有很多,如阿里的 Dubbo、Google 的 gRPC、新浪的 Motan、Apache 的 Thrift 等,都是基于微服务实现分布式架构的技术框架,本课程我们选择的框架的是 Spring Cloud,Spring Cloud 基于 Spring Boot 使得整体的开发、配置、部署都非常方便,可快速搭建基于微服务的分布式应用,Spring Cloud 相当于微服务各组件的集大成者,下图来自 Spring 官网。

Spring Boot 和 Spring Cloud 的关系可大致理解为,Spring Boot 快速搭建基础系统,Spring Cloud 在此基础上实现分布式系统中的公共组件,如服务注册、服务发现、配置管理、熔断器、控制总线等,服务调用方式是基于 REST API,整合了各种成熟的产品和架构。

对于 Java 开发者而言,Spring 框架已成为事实上的行业标准,Spring 全家桶系列产品的优势在于功能齐全、简单好用、性能优越、文档规范,实际开发中就各方面综合因素来看,Spring Cloud 是微服务架构中非常优秀的实现方案,本课程就将为各位读者详细解读如何快速上手 Spring Cloud,Spring Cloud 的核心组件如下图所示。

服务治理的核心组成有三部分:服务提供者,服务消费者,注册中心。

在分布式系统架构中,每个微服务(服务提供者、服务消费者)在启动时,将自己的信息存储在注册中心,把这个过程称之为服务注册。服务消费者要调用服务提供者的接口,就得找到服务提供者,从注册中心查询服务提供者的网络信息,并通过此信息来调用服务提供者的接口,这个过程就是服务发现。

既然叫服务治理就不仅仅是服务注册与服务发现,同时还包括了服务的管理,即注册中心需要对记录在案的微服务进行统一管理,如何来具体实现管理呢?各个微服务与注册中心通过心跳机制完成通信,每间隔一定时间微服务就会向注册中心汇报一次,如果注册中心长时间无法与某个微服务通信,就会自动销毁该微服务。当某个微服务的网络信息发生变化时,会重新注册。同时,微服务可以通过客户端缓存将需要调用的服务地址保存起来,并通过定时任务更新的方式来保证服务的时效性,这样可以降低注册中心的压力,如果注册中心出现问题,也不会影响微服务之间的调用。

服务提供者、服务消费者、注册中心的关联

  • 首先启动注册中心。
  • 服务提供者启动时,在注册中心注册可以提供的服务。
  • 服务消费者启动时,在注册中心订阅需要调用的服务。
  • 注册中心将服务提供者的信息推送给服务调用者。
  • 服务调用者通过相关信息(IP、端口等)调用服务提供者的服务。

注册中心核心模块:

服务注册表:用来存储各个微服务的信息,注册中心提供 API 来查询和管理各个微服务。

服务注册:微服务在启动时,将自己的信息在保存在注册中心。

服务发现:查询需要调用的微服务的网络信息,如 IP 地址、端口。

服务检查:通过心跳机制与完成注册的各个微服务完成通信,如果发现某个微服务长时间无法访问,则销毁该服务。

后记

Spring Cloud 的服务治理可以使用 Consul 和 Eureka 组件,下篇文章我将分享SpringCloud框架中的 Eureka。

本文转载自: 掘金

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

百度人脸识别应用(一)

发表于 2021-11-05

「这是我参与11月更文挑战的第 3 天,活动详情查看:2021最后一次更文挑战」

1、前言

最近的项目中,有一个考试功能出现了很多替考作弊的现象,经过研究决定加入百度的人脸查找,实现一张人脸数据对应一个账号信息。

2、人脸查找API的使用

首先人脸查找,是在指定人脸集合中,找到最相似的人脸。(根据人脸特征对比) 这里的人脸集合就是一个人脸库,在一个百度云用户下可以维护多个人脸库,人脸库中可以放多张人脸数据,并且同一个人脸数据可以存在不同的人脸库中。但一个人脸库中建议最多放80w条数据,不然对比起来会很耗时。所以如果我们的人脸数据很大的情况下,就需要对所有的人脸库进行一便检索了,这是我们需要注意的一个地方。

好了,下面我们直接上代码,这里我使用的百度的Java SDK 进行开发的。

首先我们需要先在配置文件中配置AppID、APIKey等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//创建百度API
public static AipFace initBaiduV2ApiFace() {
try {
Properties properties = ResourceUtil.getResourceProperties();
String APP_ID = properties.getProperty("face.baidu.AppID").trim();
String API_KEY = properties.getProperty("face.baidu.APIKey").trim();
String SECRET_KEY = properties.getProperty("face.baidu.SecretKey").trim();
AipFace client = new AipFace(APP_ID, API_KEY, SECRET_KEY);
// 可选:设置网络连接参数
// 建立连接的超时时间(单位:毫秒)
client.setConnectionTimeoutInMillis(15000);
// 通过打开的连接传输数据的超时时间(单位:毫秒)
client.setSocketTimeoutInMillis(60000);
return client;
} catch (Exception e) {
Log4jUtil.logError.error("创建百度API失败:" + e.getMessage());
return null;
}
}

需要注意的时,这里的image是base64编码后的图片数据,需urlencode,每次只支持单张图片,编码后的图片大小不超过10M。(PS:图片的base64编码指将图片数据编码成一串字符串,使用该字符串代替图像地址。您可以首先得到图片的二进制,然后用Base64格式编码即可。需要注意的是,图片的base64编码是不包含图片头的,如data:image/jpg;base64)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
java复制代码public class FaceFindComponent {

/**
* 人脸查找
* @param certId --该用户唯一ID
* @param imageData --人脸照片的Base64值
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
public MessageModel certFaceFind(String certId, String imageData) throws IOException {
//1、查找所有的用户组
String groupIds = getGroupIds();
//2、人脸查找
BASE64Decoder decoder = new BASE64Decoder();
byte[] imageBytes = decoder.decodeBuffer(imageData);
JSONObject resObject = aipFace.identifyUser(groupIds, imageBytes, null);
//3、解析返回结果
return parseFaceFindResObject(certId, resObject);
}

/**
* 转换百度人脸识别返回结果集
* @param srcObject
* @return
*/
private MessageModel parseFaceFindResObject(String certIdDe, JSONObject srcObject) {
LOGGER.info("人脸查找返回信息为:{}", srcObject);
if (srcObject.has("error_code")) {
Object error_code = srcObject.get("error_code");
String errCode = String.valueOf(error_code);
Properties properties = ResourceUtil.getResourcePropertiesBaiDu();
messageModel.setErrorCode(messageModel.constErrorCode);
String errorMsg = String.format("人脸识别失败,返回码:(%s),错误信息:%s。请重试!", errCode, properties.getProperty("baidu."+errCode));
LOGGER.error(errorMsg);
return messageModel;
}
//查找到的人脸结果数
int resultNum = (int) srcObject.get("result_num");
if (resultNum > 0) {
//人脸库中已经存在,则不能让其他用户重复建档
JSONArray result = (JSONArray) srcObject.get("result");
for (Object item : result) {
JSONObject jsObj = (JSONObject) item;
JSONArray scores = (JSONArray) jsObj.get("scores");
String resCertId = (String) jsObj.get("user_info");
if (!resCertId.equals(certIdDe)) {
for (Object sc: scores) {
int resCompare = ((Double)sc).compareTo(80.0);
if (1 == resCompare || 0 == resCompare) {
LOGGER.info("该用户已在人脸库中建档,用户信息:{}", certIdDe);
return messageModel;
}
}
}
}
}
//可正常进行后续人脸建档操作
LOGGER.info("人脸信息唯一检测通过,可正常进行后续人脸建档操作,用户信息:{}", certIdDe);
return messageModel;
}

/**
* 获取到所有的用户组ID(用于百度人脸识别的用户组)
* @return
*/
private String getGroupIds() {
return faceGroupMapper.getFaceGroupIds();
}

}

本文转载自: 掘金

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

设计模式 -- 代理模式

发表于 2021-11-05

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」

含义: 通过代理的方式对其他对象进行访问。

顾名思义就是别人代理你去做一些事情,下面就是买咖啡的一个例子,老板想喝咖啡了,老板这么忙,当然不能自己跑腿了,所以他就吩咐自己的秘书去做这件事情,这时候老板就是被代理的类,秘书就是代理类。

代理模式按照创建时期可以分成静态代理和动态代理两种,下面这种是是静态代理,静态代理是在运行之前就创建代理类。代理对象持有被代理对象的引⽤,调⽤代理对象⽅法时也会调⽤被代理对象的⽅法。

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
java复制代码 public interface BuyCoffee{
     void buyCoffee();
 }
 ​
 //被代理类
 class Boss implements BuyCoffee{
     @Override
     public void buyCoffee(){
         System.out.println("我想喝咖啡了");
    }
 }
 ​
 //代理类
 class Secretary implements BuyCoffee{
     private BuyCoffee buyCoffee = null;
     
     public Secretary (BuyCoffee buyCoffee){
         this.buyCoffee = buyCoffee;
    }
     
     @Override
     public void buyCoffee(){
         System.out.println("在秘书室等待");
         buyCoffee.buyCoffee();
         System.out.println("买完咖啡送给老板");
    }
 }
 ​
 public class ProxyTest {
     public static void main(String[] args) {
         Boss boss = new Boss();
         boss.buyHosue();
         Secretary secretary = new Secretary(boss);
         secretary.buyHosue();
    }
 }

动态代理就不需要在运行之前就创建代理类,而是在程序运⾏时通过反射的方式来创建具体的代理类。

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
typescript复制代码 public class DynamicProxyHandler implements InvocationHandler {
 ​
     private Object object;
 ​
     public DynamicProxyHandler(final Object object) {
         this.object = object;
    }
 ​
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         System.out.println("在秘书室等待");
         Object result = method.invoke(object, args);
         System.out.println("买完咖啡送给老板");
         return result;
    }
 }
 ​
 public class DynamicProxyTest {
     public static void main(String[] args) {
         Boss boss = new Boss();
         BuyCoffee proxyBuyCoffee = (BuyCoffee) Proxy.newProxyInstance(BuyCoffee.class.getClassLoader(), new
                 Class[]{BuyCoffee.class}, new DynamicProxyHandler(buyHouse));
         proxyBuyHouse.buyHosue();
    }
 }
 ​
 /* 这里的 Proxy.newProxyInstance() 需要传入三个参数
  * ClassLoader loader 指定当前目标对象使用的类加载器
  * Class<?>[] interfaces 指定目标对象实现的接口的类型
  * InvocationHandler 指定动态处理器
  */

动态代理减少了对业务接口的依赖,降低了耦合度。

最后说说动态代理的优缺点,优点是 符合开闭原则,有较好的扩展性,缺点是 当有太多需求时候,需要太多的接口,不利于管理接口;同时接口中的内容一旦发生改变,那么代理类也得进行相应的修改,非常不方便。

本文转载自: 掘金

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

ELK 部署与使用

发表于 2021-11-05

ELK 介绍

ELK 是一套可用来收集、分析、展示日志信息的工具,主要由三部分组成:

  • Logstash:Logstash 是服务器端数据处理管道,能够同时从多个来源采集数据,转换数据。(现在 ELK 增加了新的工具家族 Beats,其中包括 FileBeat 来读取日志文件并发送,更加简便轻量)
  • Elasticsearch: 用于存储和分析日志。
  • kibana: 可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化。

部署

使用 Docker 部署,参考 github.com/deviantony/…

  1. Clone 项目
  2. 修改模式密码,我是全局搜索的 changeme 并替换为自己的密码,官网也有一套使用非特权账户的方案 github.com/deviantony/…
  3. 修改 elasticsearch.yml 配置中的 xpack.license.self_generated.type 为 basic,不使用收费功能
  4. 修改 kibana.yml 中的 server.publicBaseUrl 为要配置的域名:log.frhello.com:5601
  5. 构建启动:docker-compose up

登录 kibana

如果没有修改密码使用默认账户 elastic 加默认密码登录,默认地址 localhost:5601

日志写入

使用文件采集的方式写入日志,参考:www.elastic.co/guide/en/be…

中文参考:cloud.tencent.com/developer/a…

filebeat

filebeat 是一个轻量级的文件日志记录器,可以将本机的日志文件发送到指定的 logstash 或 elastic

  • filebeat docker hub 镜像:hub.docker.com/r/elastic/f…
  • filebeat docker 使用说明:www.elastic.co/guide/en/be…
  • filebeat 配置:www.elastic.co/guide/en/be…
  • filebeat 配置:www.elastic.co/guide/en/be…
  • filebeat 配置中通过 keystore 存储敏感数据:www.elastic.co/guide/en/be…

各语言写入日志

直接使用 Elastic Common Schema (ECS)

直接使用 Elastic 的 json 日志格式,无需再次转换:www.elastic.co/guide/en/ec…

github.com/elastic/ecs…

需要注意在 filebeat.yaml 配置中添加解码 json 的相关配置

1
2
3
4
yaml复制代码json.keys_under_root: true
json.overwrite_keys: true
json.add_error_key: true
json.expand_keys: true

C#

Nlog 日志组件有一个直接写入 elastic 的 target:github.com/markmcdowel…

Serilog 和 NLog ESC 日志:www.elastic.co/guide/en/ec…

ESC 文档:www.elastic.co/guide/en/ec…

参考

官方文档:
www.elastic.co/guide/cn/ki…

原文地址:frhello.com/elk-部署与使用/

本文转载自: 掘金

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

大数据权限认证解决方案Kerberos

发表于 2021-11-05

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

前言

最近一段时间和数仓同学接触的比较多,算是一个知识点的扫盲把,kerberos认证是大数据权限认证的一种解决方案。
那么什么是kerberos呢?

Kerberos简介

Kerberos是一种计算机网络授权协议,用来在非安全网络中,对个人通信以安全的手段进行身份认证,该词为麻省理工学院为这个协议开发的一套计算机软件,软件设计采用CS架构,并且能够进行互相认证,客户端和服务器都可对对方进行身份验证,可以用于防止窃听,防止重放攻击,保护数据完整性等场合,是一种应用对称密钥体进行密钥管理的系统

Kerberos基本概念

1.KDC : 密钥分发中心,负责管理发放票据,记录授权

2.Realm: Kerberos管理领域的标识

3.principal:当每添加一个用户或服务的时候都需要向kdc添加一条principal,principal的形式为:主名称/实例名称@领域名

4.主名称: 主名称可以是用户名或服务名,表示是用于提供各种网络服务(如hdfs,yarn,hive)的主体

Kerberos 认证原理

image.png

基于kerberos安全认证部署案例

1. Kerberos部署

1.1 安装

kerberos主节点安装服务

yum install krb5-server -y

1.2 配置文件

kdc服务器三个配置文件

1
2
3
4
5
6
bash复制代码# 集群上所有节点都有这个文件而且内容同步 
/etc/krb5.conf
# 主服务器上的kdc配置
/var/kerberos/krb5kdc/kdc.conf
# 能够不直接访问 KDC 控制台而从 Kerberos 数据库添加和删除主体,需要添加配置
/var/kerberos/krb5kdc/kadm5.acl
  1. 修改/etc/krb5.conf
  2. 修改/var/kerberos/krb5kdc/kdc.conf
  3. 创建/var/kerberos/krb5kdc/kadm5.acl

1.4 创建Kerberos数据库

1
2
bash复制代码# 保存路径为/var/kerberos/krb5kdc 如果需要重建数据库,将该目录下的principal相关的文件删除即可 
kdb5_util create -r EXAMPLE.COM -s

-r指定配置的realm领域名

出现 Loading random data 的时候另开个终端执行点消耗CPU的命令如 cat /dev/vda > /dev/urandom 可以加快随机数采集

该命令会在 /var/kerberos/krb5kdc/ 目录下创建 principal 数据库

1.5 启动服务

1
2
3
4
bash复制代码chkconfig --level 35 krb5kdc on
chkconfig --level 35 kadmin on
systemctl start krb5kdc
systemctl start kadmin

image.png

1.6 创建Kerberos管理员

1
2
bash复制代码# 需要设置两次密码,当前设置为root 
kadmin.local -q "addprinc admin/admin"

principal的名字的第二部分是admin,那么该principal就拥有administrative privileges

这个账号将会被CDH用来生成其他用户/服务的principal

1.7 测试kerberos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码# 列出Kerberos中的所有认证用户,即principals 
kadmin.local -q "list_principals"
# 添加认证用户,需要输入密码
kadmin.local -q "addprinc dy"
# 使用该用户登录,获取身份认证,需要输入密码
kinit dy
# 查看当前用户的认证信息
ticket klist
# 更新ticket
kinit -R
# 销毁当前的ticket
kdestroy
# 删除认证用户
kadmin.local -q "delprinc dy"

本文转载自: 掘金

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

JVM-Java 对象模型

发表于 2021-11-05

这是我参与11月更文挑战的第 5 天,活动详情查看:2021最后一次更文挑战

Java对象保存在堆内存中。在内存中,一个Java对象包含三部分:对象头、实例数据和对齐填充。其中对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志。

对象头

对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

对markword的设计方式上,非常像网络协议报文头:将mark word划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义。

在JVM的内存结构中,对象保存在堆内存中,而我们在对对象进行操作时,其实操作的是对象的引用。那么对象本身在JVM中的结构是什么样的呢?本文的所有分析均基于 HotSpot 虚拟机。

Oop-Klass 模型

HotSpot是基于 c++ 实现,而c++是一门面向对象的语言,本身是具备面向对象基本特征的,所以Java中的对象表示,最简单的做法是为每个Java类生成一个c++类与之对应。但HotSpot JVM并没有这么做,而是设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。

为什么 HotSpot 要设计一套oop-klass model呢?答案是:HotSopt JVM的设计者不想让每个对象中都含有一个vtable(虚函数表); 这个解释似乎可以说得通。众所周知,C++和Java都是面向对象的语言,面向对象语言有一个很重要的特性就是多态。关于多态的实现,C++和Java有着本质的区别。

多态是面向对象的最主要的特性之一,是一种方法的动态绑定,实现运行时的类型决定对象的行为。多态的表现形式是父类指针或引用指向子类对象,在这个指针上调用的方法使用子类的实现版本。多态是IOC、模板模式实现的关键。

在 C++ 中通过虚函数表的方式实现多态,每个包含虚函数的类都具有一个虚函数表(virtual table),在这个类对象的地址空间的最靠前的位置存有指向虚函数表的指针。在虚函数表中,按照声明顺序依次排列所有的虚函数。由于C++在运行时并不维护类型信息,所以在编译时直接在子类的虚函数表中将被子类重写的方法替换掉。

在 Java 中,在运行时会维持类型信息以及类的继承体系。每一个类会在方法区中对应一个数据结构用于存放类的信息,可以通过Class对象访问这个数据结构。其中,类型信息具有superclass属性指示了其超类,以及这个类对应的方法表(其中只包含这个类定义的方法,不包括从超类继承来的)。而每一个在堆上创建的对象,都具有一个指向方法区类型信息数据结构的指针,通过这个指针可以确定对象的类型。

oop-klass 结构

关于 opp-klass 模型的整体定义,在 HotSpot 的 源码 中可以找到。oops 模块可以分成两个相对独立的部分:OOP 框架和 Klass 框架。在 oopsHierarchy.hpp 里定义了 oop 和 klass 各自的体系。

oop 体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arduino复制代码//定义了oops共同基类
typedef class oopDesc* oop;
//表示一个Java类型实例
typedef class instanceOopDesc* instanceOop;
//表示一个Java方法
typedef class methodOopDesc* methodOop;
//表示一个Java方法中的不变信息
typedef class constMethodOopDesc* constMethodOop;
//记录性能信息的数据结构
typedef class methodDataOopDesc* methodDataOop;
//定义了数组OOPS的抽象基类
typedef class arrayOopDesc* arrayOop;
//表示持有一个OOPS数组
typedef class objArrayOopDesc* objArrayOop;
//表示容纳基本类型的数组
typedef class typeArrayOopDesc* typeArrayOop;
//表示在Class文件中描述的常量池
typedef class constantPoolOopDesc* constantPoolOop;
//常量池告诉缓存
typedef class constantPoolCacheOopDesc* constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class klassOopDesc* klassOop;
//表示对象头
typedef class markOopDesc* markOop;

上面列出的是整个Oops模块的组成结构,其中包含多个子模块。每一个子模块对应一个类型,每一个类型的OOP都代表一个在JVM内部使用的特定对象的类型。

从上面的代码中可以看到,有一个变量opp的类型是oppDesc ,OOPS类的共同基类型为oopDesc。

在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。 在HotSpot中,根据JVM内部使用的对象业务类型,具有多种oopDesc的子类。除了oppDesc类型外,opp体系中还有很多instanceOopDesc、arrayOopDesc 等类型的实例,他们都是oopDesc的子类。

这些 OOPS 在 JVM 内部有着不同的用途,例如 , instanceOopDesc 表示类实例, arrayOopDesc 表示数组。 也就是说,当我们使用new创建一个 Java 对象实例的时候,JVM 会创建一个instanceOopDesc对象来表示这个 Java 对象。同理,当我们使用new创建一个 Java 数组实例的时候,JVM 会创建一个arrayOopDesc 对象来表示这个数组对象。


在 HotSpot 中,oopDesc 类定义在 oop.hpp 中,instanceOopDesc 定义在 instanceOop.hpp 中,arrayOopDesc 定义在 arrayOop.hpp 中。

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
arduino复制代码class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;

private:
// field addresses in oop
void* field_base(int offset) const;

jbyte* byte_field_addr(int offset) const;
jchar* char_field_addr(int offset) const;
jboolean* bool_field_addr(int offset) const;
jint* int_field_addr(int offset) const;
jshort* short_field_addr(int offset) const;
jlong* long_field_addr(int offset) const;
jfloat* float_field_addr(int offset) const;
jdouble* double_field_addr(int offset) const;
address* address_field_addr(int offset) const;
}


class instanceOopDesc : public oopDesc {
}

class arrayOopDesc : public oopDesc {
}

通过上面的源码可以看到,instanceOopDesc实际上就是继承了oopDesc,并没有增加其他的数据结构,也就是说instanceOopDesc中主要包含以下几部分数据:markOop _mark和union _metadata 以及一些不同类型的 field。

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。在虚拟机内部,一个Java对象对应一个instanceOopDesc的对象。其中对象头包含了两部分内容:_mark和_metadata,而实例数据则保存在oopDesc中定义的各种field中。

_mark

文章开头我们就说过,之所以我们要写这篇文章,是因为对象头中有和锁相关的运行时数据,这些运行时数据是synchronized以及其他类型的锁实现的重要基础,而关于锁标记、GC分代等信息均保存在_mark中。因为本文主要介绍的oop-klass模型,在这里暂时不对对象头做展开,下一篇文章介绍。

_metadata

前面介绍到的_metadata是一个共用体,其中_klass是普通指针,_compressed_klass是压缩类指针。在深入介绍之前,就要来到oop-Klass中的另外一个主角klass了。

klass 体系

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
kotlin复制代码//klassOop的一部分,用来描述语言层的类型
class Klass;
//在虚拟机层面描述一个Java类
class instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class instanceRefKlass;
//表示methodOop的Klass
class methodKlass;
//表示constMethodOop的Klass
class constMethodKlass;
//表示methodDataOop的Klass
class methodDataKlass;
//最为klass链的端点,klassKlass的Klass就是它自身
class klassKlass;
//表示instanceKlass的Klass
class instanceKlassKlass;
//表示arrayKlass的Klass
class arrayKlassKlass;
//表示objArrayKlass的Klass
class objArrayKlassKlass;
//表示typeArrayKlass的Klass
class typeArrayKlassKlass;
//表示array类型的抽象基类
class arrayKlass;
//表示objArrayOop的Klass
class objArrayKlass;
//表示typeArrayOop的Klass
class typeArrayKlass;
//表示constantPoolOop的Klass
class constantPoolKlass;
//表示constantPoolCacheOop的Klass
class constantPoolCacheKlass;

和oopDesc是其他 oop 类型的父类一样,Klass 类是其他 klass 类型的父类。

Klass 向 JVM 提供两个功能:

  • 实现语言层面的 Java 类(在 Klass 基类中已经实现)
  • 实现 Java 对象的分发功能(由 Klass 的子类提供虚函数实现)

文章开头的时候说过:之所以设计oop-klass模型,是因为HotSopt JVM的设计者不想让每个对象中都含有一个虚函数表。

HotSopt JVM的设计者把对象一拆为二,分为klass和oop,其中oop的职能主要在于表示对象的实例数据,所以其中不含有任何虚函数。而klass为了实现虚函数多态,所以提供了虚函数表。所以,关于Java的多态,其实也有虚函数的影子在。

_metadata是一个共用体,其中_klass是普通指针,_compressed_klass是压缩类指针。这两个指针都指向instanceKlass对象,它用来描述对象的具体类型。

instanceKlass

JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个instanceKlass对象,用来在JVM层表示Java类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码//类拥有的方法列表
objArrayOop _methods;
//描述方法顺序
typeArrayOop _method_ordering;
//实现的接口
objArrayOop _local_interfaces;
//继承的接口
objArrayOop _transitive_interfaces;
//域
typeArrayOop _fields;
//常量
constantPoolOop _constants;
//类加载器
oop _class_loader;
//protected域
oop _protection_domain;
....

可以看到,一个类该具有的东西,这里面基本都包含了。

在JVM中,对象在内存中的基本存在形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。klassKlass作为oop的klass链的端点。关于对象和数组的klass链大致如下图:

在这种设计下,JVM 对内存的分配和回收,都可以采用统一的方式来管理。oop-klass-klass Klass 关系如图:

内存存储

关于一个Java对象,他的存储是怎样的? 对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。

方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 所谓加载的类信息,其实不就是给每一个被加载的类都创建了一个 instantKlass对象么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Model
{
public static int a = 1;
public int b;

public Model(int b) {
this.b = b;
}
}

public static void main(String[] args) {
int c = 10;
Model modelA = new Model(2);
Model modelB = new Model(3);
}

存储结构如下:

从上图中可以看到,在方法区的instantKlass中有一个int a=1的数据存储。在堆内存中的两个对象的oop中,分别维护着int b=3,int b=2的实例数据。和oopDesc一样,instantKlass也维护着一些fields,用来保存类中定义的类数据,比如int a=1。

总结

每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了两部分信息,对象头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息。元数据其实维护的是指针,指向的是对象所属的类的instanceKlass。

本文整合自 openjdk 源码及网络相关知识,引用中已备注参考来源,如有侵权,请联系删除。感谢参考文章作者对于技术社区的知识传播。

引用

  • www.voidcn.com/article/p-p…
  • www.jianshu.com/p/b6cb4c694…
  • blog.csdn.net/linxdcn/

本文转载自: 掘金

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

【分布式事务系列】Saga模式实现方式

发表于 2021-11-05

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

Saga的实现方式

在一个电商平台的下单场景中,会涉及到订单的创建、商品库存的扣减、钱包支付、积分赠送等操作,整体的时序图如下:
image.png

电商平台下单的流程是一个典型的长事务场景,根据Saga模式的定义,先将长事务拆分成多个本地短事务,每个服务的本地事务按照执行顺序逐一提交,一旦其中一个服务的事务出现异常,则采用补偿方式逐一撤回,这个过程的实现涉及到Saga的协调模式,它有两种常用的协调模式。

  1. 事件编排式:把Saga的决策和执行顺序逻辑分布在Saga的每一个参与者中,它们通过交换事件的方式进行沟通。

在基于事件的编排模式中,第一个服务执行完一个本地事务后,发送一个时间,这个事件会被一个或多个服务监听,监听到时间的服务再执行本地事务并发布新事件,伺候一直延续这种事件触发模式,直到业务流程中最后一个服务的本地事务执行结束,相当于整个分布式长事务执行完成。

流程复杂,但是比较常见的解决方案,具体步骤如下:

  • 订单服务创建新的订单,把订单状态设置为待支付,并发布一个order_create_event事件
  • 库存服务监听到order_create_event事件后,执行本地库存冻结方法,如果执行成功,则发布一个order_prepared_event事件
  • 支付服务监听order_prepared_event事件后,执行账户扣款方法,并发布pay_order_event事件。
  • 积分服务监听pay_order_event事件,增加账户积分,并更新订单状态为成功。

如果上述某一个步骤执行失败,会发送一个失败的时间,每个服务需要监听失败情况根据实际需求进行逐一回滚。

本文转载自: 掘金

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

学习一下Java中Character类的使用

发表于 2021-11-05

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

写在前面

今天我们来学习一下Java中的Character类的相关使用,主要是其中的一些方法的使用,因为最近用的比较频繁,特此整理一下,分享给大家,如果大家有什么更好的知识点,可以在评论区中分享一下哈。

Character类主要是针对单个字符进行一些判断、处理和操作的一个类,其中的一些方法很是实用。

Character类中的一些方法

如果你对Character类有点陌生,那你一定会对其中的一些方法有一些熟悉的感觉,一起来学习一下吧。


方法:isLetter

描述:判断当前字符是否是一个字母。

具体使用:

1
2
3
4
java复制代码char a = 'a';
char b = '1';
System.out.println(Character.isLetter(a));
System.out.println(Character.isLetter(b));

运行结果:

1
2
java复制代码true
false

方法:isDigit

描述:判断当前字符是否是一个数字。

具体使用:

1
2
3
4
java复制代码char a = 'a';
char b = '1';
System.out.println(Character.isDigit(a));
System.out.println(Character.isDigit(b));

运行结果:

1
2
java复制代码false
true

方法:isWhitespace

描述:判断当前字符是否是一个空白字符。

具体使用:

1
2
3
4
5
6
java复制代码char a = 'a';
char b = '1';
char c = ' ';
System.out.println(Character.isWhitespace(a));
System.out.println(Character.isWhitespace(b));
System.out.println(Character.isWhitespace(c));

运行结果:

1
2
3
java复制代码false
false
true

方法:isUpperCase

描述:判断当前字符是否是一个大写字母,在这里就算是数字也一样能判断,只不过只能返回false了。

具体使用:

1
2
3
4
5
6
ini复制代码char a = 'a';
char b = '1';
char c = 'A';
System.out.println(Character.isUpperCase(a));
System.out.println(Character.isUpperCase(b));
System.out.println(Character.isUpperCase(c));

运行结果:

1
2
3
java复制代码false
false
true

方法:isLowerCase

描述:判断当前字符是否是一个小写字母,和判断大写字母的方法相同,在这里就算是数字也一样能判断,结果依然。

具体使用:

1
2
3
4
5
6
ini复制代码char a = 'a';
char b = '1';
char c = 'A';
System.out.println(Character.isLowerCase(a));
System.out.println(Character.isLowerCase(b));
System.out.println(Character.isLowerCase(c));

运行结果:

1
2
3
java复制代码true
false
false

方法:toUpperCase

描述:将所传入的字符转成大写,当然,这里如果是数字的话,是不会有任何变动的,可以在下面的代码示例中查看。

具体使用:

1
2
3
4
5
6
java复制代码char a = 'a';
char b = '1';
char c = 'A';
System.out.println(Character.toUpperCase(a));
System.out.println(Character.toUpperCase(b));
System.out.println(Character.toUpperCase(c));

运行结果:

1
2
3
java复制代码A
1
A

方法:toLowerCase

描述:将所传入的字符转成小写,和转大写方法类似。

具体使用:示例代码可以参考转大写toUpperCase方法。

运行结果:同上

本文转载自: 掘金

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

Java 基础知识记录(一)

发表于 2021-11-05

对象与类

  1. 对象变量并没有实际包含一个对象,它只是引用一个对象,在Java中,任何对象变量的值都是对存储在另外一个地方的某个对象的引用。new操作符的返回值也是一个引用。
  2. 可以将实例字段定义为final。这样的字段必须在构造对象时初始化。也就是说,必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。
1
2
3
4
java复制代码private final StringBuilder evaluations = new StringBuilder();
public void giveGoldStar(){
   evaluations.append(LocalDate.now() + “: Gold start!”);
}

final关键字只是表示存储在evaluations变量中的对象引用不会再指示另一个不同的StringBuilder对象。 不过这个对象可以更改。
2. 对于类中非静态的实例字段,每个对象都有自己的一个副本,但是对于静态字段,类的所有实例将共享同一个静态字段,静态字段属于类,不属于任何单个的对象。
3. 在程序中,可以用Math.PI来访问这个常量。

1
2
3
java复制代码public class Math{
   public static final double PI = 3.141592654;
}

如果省略关键字static,PI就变成了Math类的一个实例字段。也就是说,需要通过Math类的一个对象来访问PI,并且每一个Math对象都有它自己的一个PI副本,浪费资源。
4. 静态方法是不在对象上执行的方法。 静态方法不能访问非静态的实例字段,因为它不能在对象上执行操作。但是,静态方法可以访问静态字段。可以使用对象调用静态方法,但是不建议。

在下面两种情况下可以使用静态方法:

  • 方法不需要访问对象状态,因为它需要的所有参数都通过显示参数提供(例如:Math.pow)。
    • 方法只需要访问类的静态字段。
  1. Java中对方法参数能做什么和不能做什么:
* 方法不能修改基本数据类型的参数(即数值型或布尔型)。
* 方法可以改变对象参数的状态。
* 方法不能让一个对象参数引用一个新的对象。
  1. 有一种import语句允许导入静态方法和静态字段,而不只是类。例如:
1
java复制代码import static java.lang.System.*;

就可以使用System类的静态方法和静态字段,而不必加类名前缀:

1
java复制代码out.println(“Hello World!”);

继承

  1. 在设计类的时候,应该将最一般的方法放在超类中,而将更特殊的方法放在字类中。
  2. 在Java中,子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换。
  3. 不允许扩展的类被称为final类,如果在定义类的时候使用了final修饰符就表明这个类是final类,如果类中的方法被声明为final类型,子类就不能覆盖这个方法,如果将一个类声明为final,只有其中的方法自动地成为final,而不包括字段。
  4. 将一个值存入变量时,编译器将检查你是否承诺过多。如果将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量时,就承诺过多了。必须进行强制类型转换,这样才能通过运行时的检查。
  5. 综上:
* > 只能在继承层次内进行强制类型转换。
* > 在将超类强制转换成子类之前,应该使用instanceof进行检查。
* > 一般情况下,最好尽量少用强制类型转换和instanceof运算符。
  1. 为了提高程序的清晰度,包含;一个或多个抽象方法的类本身必须被声明为抽象的。即使不含抽象方法,也可以将类声明为抽象类。
  2. 抽象类不能实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。
  3. 由于每个值分别包装在对象中,所以ArrayList 的效率远远低于int[] 数组。因此,只有当程序员操作的方便性比执行效率更重要的时候,才会考虑对较小的集合使用这种构造。
  4. ==运算符可以应用于包装器对象,不过检测的是对象是否有相同的内存位置。不过如果值是介于-128和127之间的两个包装器对象比较结果就会成功。
  5. 枚举类:
1
arduino复制代码public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }

实际上,这个声明定义的类型是一个类,它刚好有4个实例,不可能构造新的对象。因此在比较两个枚举类型的值时,并不需要调用equals,直接使用 “==” 就可以了。
10. 继承的设计技巧:

* 将公共操作和字段放在超类中。
* 不要使用受保护的字段。
* 使用继承实现“is-a”关系。
* 除非所有继承的方法都有意义,否则不要使用继承。
* 在覆盖方法时,不要改变预期的行为。
* 使用多态,而不要使用类型信息。
* 不要滥用反射。

本文转载自: 掘金

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

Paxos协议和流程详解!细说分布式系统中的去中心化的分布式

发表于 2021-11-05

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

基本概念

  • Paxos协议是少数在工程实践中证实的强一致性,高可用的去中心化分布式协议
  • Paxos协议的流程较为复杂,但是基本思想类似投票过程
  • Paxos协议中有一组完全对等的参与节点 :acceptor
    • 这组节点各自就某一件事做出决议,如果某个决议获得了超过半数节点的同意则生效
  • Paxos协议中只要有超过一半的节点正常,就可以工作,就能很好对抗宕机,网络分化等异常情况

角色

  • Proposer: 提案者
    • Proposer可以有多个
    • Proposor提出议案value:
      • value: 指在工程实践中的任何操作:
        • 比如修改某个变量为某个值
        • 设置当前primary为某个节点等等
      • Paxos协议中统一将这些操作抽象为value
    • 不同的Proposer可以提出不同甚至矛盾的value:
      • 比如某个Proposer提议“将变量X设置为1”
      • 另一个Proposer提议“将变量X设置为2”
    • 但是对于同一轮Paxos过程,最多只有一个value被批准
  • Acceptor: 批准者
    • Acceptor可以有多个
    • Proposer提出的value必须获得超过半数的Acceptor批准后才能通过
    • Acceptor之间完全对等独立
  • Learner: 学习者
    • Learner学习被批准的value:
      • 就是通过读取各个Proposer对value的选择结果
      • 如果某个value被超过半数Acceptor通过,那么Learner就学习到了这个value
  • 类似Quorum机制,某个value需要获得N2+1\frac{N}{2}+12N​+1的Acceptor批准,学习者Learner需要至少读取N2+1\frac{N}{2}+12N​+1个Acceptor, 至多读取N个Acceptor的结果后,才能学习到一个通过的value
  • 这三类角色只是逻辑上的划分,工程实践中一个节点可以同时充当这三类角色

流程

  • Paxos协议一轮一轮进行,每轮都有一个编号.每轮Paxos协议可能会批准一个value. 如果某一轮Paxos协议批准了某个value, 那么以后各轮Paxos协议只能批准这个value
  • 各轮协议流程组成一个Paxos协议实例: 一次Paxos协议实例只能批准一个value. 这也是Paxos协议强一致性的重要体现
  • 每轮Paxos协议分为两个阶段:
    • 准备阶段
    • 批准阶段
    • 这两个阶段Proposer和Acceptor有各自的处理流程

Proposer处理流程

  • 向所有的Acceptor发送消息 “Prepare(b)”, 这里的b是Paxos的轮数,每轮递增
  • 如果收到任何一个Acceptor发送的消息 “Reject(B)”, 对于这个Proposer而言,本轮Paxos失败,将轮数b设置为B+1后重新进行上一个步骤
    • 在批准阶段,会根据收到的Acceptor的消息作出不同选择
  • 如果接收到的Acceptor的 “Promise(b, v_i)” 消息达到N2+1\frac{N}{2}+12N​+1个 . N为Acceptor总数,除法取整 ,v-i表示Acceptor最近一次在i轮批准过的value
    • 如果收到的 “Promise(b, v)消息” 中 ,v都为空 ,Promise会选择一个value作为v, 向所有Acceptor广播消息Accept(b, v)
    • 否则,在所有接收的 “Promise(b, v_i)消息” 中,选择i最大的value作为v, 向所有Acceptor广播消息 “Accept(b, v)”
  • 如果接收到Nack(B), 将轮数b设置为B+1后重新进行第一步步骤

Acceptor流程

  • 接收某个Proposer的消息Prepare(b). 参数B是该Acceptor收到的最大Paxos轮数编号 ,V是Acceptor批准的value, 可以为空
    • 如果b > B, 回复Promise(b, V_B), 设置B = b. 表示不再接受编号小于b提案
    • 否则,回复 “Reject(B)消息”
  • 接受Accept(b, v):
    • 如果b < B, 回复Nack(B), 表示Proposer有一个更大编号的提案被这个Acceptor接收了
    • 否则设置V=v. 表示这个Acceptor批准的Value是v. 广播 “Accepted消息”

Paxos协议示例

  • 有5个Acceptor,1个Proposer, 不存在任何网络,宕机异常
  • 着重考察各个Acceptor上变量B和变量V的变化以及Proposer上变量B的变化
  • 初始状态:
    在这里插入图片描述
  • Proposer向所有Acceptor发送“Prepare(1)”. 所有Acceptor正确处理,并回复Promise(1, NULL) :
    在这里插入图片描述
  • Proposer收到5个Promise(1,NULL),满足多余半数的Promise的value为空,此时发送Accept(1, v1),其中v1是Proposer选择的value :
    在这里插入图片描述
  • 此时,v1被超过半数的Acceptor批准,v1即是本次Paxos协议实例批准的value. 如果Learner学习value,学习到的只能是v1
  • Paxos协议的核心 : 批准的value无法改变. 这也是整个协议正确性的基础
    • 在同一个Paxos实例中, 批准的value是无法改变的,即使后续Proposer以更高的序号发起Paxos协议也无法改变value
  • Paxos协议是被人为设计出来的,设计过程也是协议的推导过程:
    • Paxos协议利用了Quorum机制,选择的W=R=N2+1W=R=\frac{N}{2}+1W=R=2N​+1
    • 协议就是Proposer更新Acceptor的过程,一旦某个Proposer成功更新了超过半数的Acceptor, 那么就更新成功
    • Learner按照Quorum机制去读取Acceptor, 一旦某个value在超过半数的Proposer上被成功读取,则说明这是一个被批准的value
    • 协议通过引入轮次,使得高轮次的提议抢占低轮次的提议来避免死锁
  • 协议设计的关键点:
    • 如何满足 “在一次Paxos算法实例过程中只批准一个value” 这一约束条件

本文转载自: 掘金

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

1…410411412…956

开发者博客

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