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

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


  • 首页

  • 归档

  • 搜索

经验:在MySQL数据库中,这4种方式可以避免重复的插入数据

发表于 2020-09-10

最常见的方式就是为字段设置主键或唯一索引,当插入重复数据时,抛出错误,程序终止,但这会给后续处理带来麻烦,因此需要对插入语句做特殊处理,尽量避开或忽略异常,下面我简单介绍一下,感兴趣的朋友可以尝试一下:

这里为了方便演示,我新建了一个user测试表,主要有id,username,sex,address这4个字段,其中主键为id(自增),同时对username字段设置了唯一索引:

01 insert ignore into

即插入数据时,如果数据存在,则忽略此次插入,前提条件是插入的数据字段设置了主键或唯一索引,测试SQL语句如下,当插入本条数据时,MySQL数据库会首先检索已有数据(也就是idx_username索引),如果存在,则忽略本次插入,如果不存在,则正常插入数据:

02 on duplicate key update

即插入数据时,如果数据存在,则执行更新操作,前提条件同上,也是插入的数据字段设置了主键或唯一索引,测试SQL语句如下,当插入本条记录时,MySQL数据库会首先检索已有数据(idx_username索引),如果存在,则执行update更新操作,如果不存在,则直接插入:

03 replace into

即插入数据时,如果数据存在,则删除再插入,前提条件同上,插入的数据字段需要设置主键或唯一索引,测试SQL语句如下,当插入本条记录时,MySQL数据库会首先检索已有数据(idx_username索引),如果存在,则先删除旧数据,然后再插入,如果不存在,则直接插入:

04 insert if not exists

即insert into … select … where not exist … ,这种方式适合于插入的数据字段没有设置主键或唯一索引,当插入一条数据时,首先判断MySQL数据库中是否存在这条数据,如果不存在,则正常插入,如果存在,则忽略:

目前,就分享这4种MySQL处理重复数据的方式吧,前3种方式适合字段设置了主键或唯一索引,最后一种方式则没有此限制,只要你熟悉一下使用过程,很快就能掌握的,网上也有相关资料和教程,介绍的非常详细,感兴趣的话,可以搜一下。

本文转载自: 掘金

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

《一起学sentinel》二、初探sentinel的Slot

发表于 2020-09-10

slot概述

在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
  • StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
  • FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
  • AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
  • DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

下面是关系结构图

ProcessorSlot子类及实现类.png


solt的基本逻辑及代码演示

每个Slot执行完业务逻辑处理后,会调用fireEntry()方法,该方法将会触发下一个节点的entry方法,下一个节点又会调用他的fireEntry,以此类推直到最后一个Slot,由此就形成了sentinel的责任链。

  • 工作流概述:

slot工作流.png


下面我会根据slot 的基本实现processorSlot讲一下slot 的基本结构及用法


  • 先看看顶层接口ProcessorSlot
1
2
3
4
5
6
java复制代码public interface ProcessorSlot<T> {
void entry(....); //开始入口
void fireEntry(....);//finish意味着结束
void exit(....);//退出插槽
void fireExit(....);//退出插槽结束
}

​ 这个接口有4个方法,entry,fireEntry,exit,fireExit

  • ProcessorSlot 的抽象实现 AbstractLinkedProcessorSlot
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
java复制代码public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {

private AbstractLinkedProcessorSlot<?> next = null;

@Override
public void fireEntry(... ) throws Throwable {
//当业务执行完毕后,如果还有下一个slot
if (next != null) {
next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}

@SuppressWarnings("unchecked")
//指向下一个slot的entry,每一个slot根据自己的职责不同,有自己的实现
void transformEntry(... ) throws Throwable {
T t = (T)o;
entry(context, resourceWrapper, t, count, prioritized, args);
}

@Override
public void fireExit(... ) {
//当一个slot的exit执行完毕后,如果还有下一个未关闭slot
if (next != null) {
//指向下一个slot的exit
next.exit(context, resourceWrapper, count, args);
}
}

public AbstractLinkedProcessorSlot<?> getNext() {
return next;
}

public void setNext(AbstractLinkedProcessorSlot<?> next) {
this.next = next;
}

}
+ **DefaultProcessorSlotChain实现了上述的chain(setNext和getNext)**



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
java复制代码public class DefaultProcessorSlotChain extends ProcessorSlotChain {
//直接实现了AbstractLinkedProcessorSlot的实例并作为first,可以理解为当前slot
AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {

@Override
public void entry(... )
throws Throwable {
super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
}

@Override
public void exit(... ) {
super.fireExit(context, resourceWrapper, count, args);
}

};
//默认的end(可以理解为当前的后一个slot)
AbstractLinkedProcessorSlot<?> end = first;

@Override
public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
protocolProcessor.setNext(first.getNext());
first.setNext(protocolProcessor);
//如果当前为最后一个
if (end == first) {
end = protocolProcessor;
}
}

@Override
public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
//将后一个slot放进当前slot的next
end.setNext(protocolProcessor);
//将end指向后一个slot
end = protocolProcessor;
}
}
  • AbstractLinkedProcessorSlot 的实例 DemoSlot :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class DemoSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

//开始入口
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
//finish意味着结束
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
//退出插槽
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
//退出插槽结束
fireExit(context, resourceWrapper, count, args);
}
}

到这里我们看完了Slot的基本执行过程,总结一下:

  • 1.初始化first和end的slot,
  • 2.开始执行entry
  • 3.开始执行fireEntry并查询是否下一个slot,如果有则执行第2步
  • 4.开始执行exit
  • 5.开始执行fireExit并查询是否有下一个slot,如果有则执行第4步
  • 6.结束

我们使用Slot方式进行处理时,需要实现一个类似tomcat 的lifeCycle,但是差异是tomcat的lifeCycle是一个使用异步事件的方式执行容器内逻辑,而sentinel 使用的是一种子父依赖关系的链式调用,强调了顺序性执行。

默认的各个插槽之间的顺序是固定的,因为有的插槽需要依赖其他的插槽计算出来的结果才能进行工作。

下面我们看看是如何保证顺序的

–

SLOT的加载

1.定义顺序

sentinel在每个实例化的slot上面备注了顺序的参数,如

1
2
java复制代码@SpiOrder(-10000)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {

这是一个自定义的注解,保存的内容主要就是上面的(-10000)作为顺序权重

2.SPI加载

默认的chain会调用sentinel的类加载工具SpiLoader的loadPrototypeInstanceListSorted(ProcessorSlot.class);

这个方法会将所有实现了ProcessorSlot的类,用SPI的方式加载

slot-spi-class.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@SpiOrder(-10000)
public class NodeSelectorSlot
@SpiOrder(-9000)
public class ClusterBuilderSlot
@SpiOrder(-8000)
public class LogSlot
@SpiOrder(-7000)
public class StatisticSlot
@SpiOrder(-5000)
public class SystemSlot
@SpiOrder(-6000)
public class AuthoritySlot
@SpiOrder(-2000)
public class FlowSlot
@SpiOrder(-1000)
public class DegradeSlot

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
java复制代码 public static <T> List<T> loadPrototypeInstanceListSorted(Class<T> clazz) {
//这里就是第二步的加载
ServiceLoader<T> serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz);

List<SpiOrderWrapper<T>> orderWrappers = new ArrayList<>();
//循环遍历加载
for (T spi : serviceLoader) {
//查询对应类的顺序(第一步)
int order = SpiOrderResolver.resolveOrder(spi);
//将顺序和类插入List(手动有序数组)
SpiOrderResolver.insertSorted(orderWrappers, spi, order);
}
}



//排序方法很简答
private static <T> void insertSorted(List<SpiOrderWrapper<T>> list, T spi, int order) {
int idx = 0;
for (; idx < list.size(); idx++) {
//循环遍历定长的list,一次比对大小
if (list.get(idx).getOrder() > order) {
break;如果发现当前索引大于
}
}
list.add(idx, new SpiOrderWrapper<>(order, spi));
}

本文转载自: 掘金

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

从一知半解到揭晓Java高级语法—泛型

发表于 2020-09-10

前言

泛型是Java基础知识的重点,虽然我们在初学Java的时候,都学过泛型,觉得自己掌握对于Java泛型的使用(全是错觉),往后的日子,当我们深入去阅读一些框架源码,你就发现了,自己会的只是简单的使用,却看不懂别人的泛型代码是怎么写的,还可以这样,没错,别人写出来的代码那叫艺术,而我……

探讨

Java语言为什么存在着泛型,而像一些动态语言Python,JavaScipt却没有泛型的概念?

原因是,像Java,C#这样的静态编译型的语言,它们在传递参数的时候,参数的类型,必须是明确的,看一个例子,简单编写一个存放int类型的栈—StackInt,代码如下:

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
java复制代码public class StackInt {

private int maxSize;
private int[] items;
private int top;

public StackInt(int maxSize){
this.maxSize = maxSize;
this.items = new int[maxSize];
this.top = -1;
}

public boolean isFull(){
return this.top == this.maxSize-1;
}

public boolean isNull(){
return this.top <= -1;
}

public boolean push(int value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
}

public int pop(){
if(this.isNull()){
throw new RuntimeException("当前栈中无数据");
}
int value = this.items[top];
--top;
return value;
}
}

在这里使用构造函数初始化一个StackInt对象时,可以传入String字符串吗?很明显是不行的,我们要求的是int类型,传入字符串String类型,这样在语法检查阶段时会报错的,像Java这样的静态编译型的语言,参数的类型要求是明确的

在这里插入图片描述

泛型解决了什么问题?

参数不安全:引入泛型,能够在编译阶段找出代码的问题,而不是在运行阶段

泛型要求在声明时指定实际数据类型,Java 编译器在编译时会对泛型代码做强类型检查,并在代码违反类型安全时发出告警。早发现,早治理,把隐患扼杀于摇篮,在编译时发现并修复错误所付出的代价远比在运行时小。

避免类型转换:

未使用泛型:

1
2
3
java复制代码List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); //需要在取出Value的时候进行强制转换

使用泛型:

1
2
3
java复制代码List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); //不需要强制转换

重复编码::通过使用泛型,可以实现通用编码,可以处理不同类型的集合,并且类型安全且易于阅读。像上面的StackInt类,我们不能针对每个类型去编写对应类型的栈,那样太麻烦了,而泛型的出现就很好的解决了这点

扩展

在上面的StackInt类有一些不好的地方,那就是太具体了,不够抽象,不够抽象,那么它的复用性也是不高的,例如,在另外的场景下,我需要的是往栈里存String类型的字符串,或者是其他类型,那么StackInt类就做不到了,那么有什么方法能够做到呢?再写一个StackString类,不可能,那样不得累死。那就只有引入基类Object了,我们改进一下代码:

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
java复制代码public class StackObject {

private int maxSize;
private Object[] items;
private int top;

public StackObject(int maxSize){
this.maxSize = maxSize;
this.items = new Object[maxSize];
this.top = -1;
}

public boolean isFull(){
return this.top == this.maxSize-1;
}

public boolean isNull(){
return this.top <= -1;
}

public boolean push(Object value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
}

public Object pop(){
if(this.isNull()){
throw new RuntimeException("当前栈中无数据");
}
Object value = this.items[top];
--top;
return value;
}
}

使用StackObject可以存储任意类型的数据,那么这样做,又有什么优点和缺点呢?

优点:StackObject类变得相对抽象了,我们可以往里面存储任何类型的数据,这样就避免了写一些重复代码
在这里插入图片描述

缺点:

1、用Object表示的对象是比较抽象的,它失去了类型的特点,那么我们在做一些运算的时候,可能会频繁的拆箱装箱的过程

在这里插入图片描述

看上面的例图,我们理解的认为存放了两个数值,12345和54321,将两个进行相加,这是很常见的操作,但是报错了,编译器给我们的提示是,+操作运算不能用于两个Object类型,那么只能对其进行类型转换,这也是我们上面说到的泛型能解决的问题,我们需要这样做,int sum = (int)val1 + (int)val2;,同时在涉及拆箱装箱时,是有一定性能的损耗的,关于拆箱装箱在这里不作描述,可以参考我写过的随笔—— 深入理解Java之装箱与拆箱

2、对于我们push进去的值,我们在取出的时候,容易忘记类型转换,或者不记得它的类型,类型转换错误,这在后面的一些业务可能埋下祸根,例如下面这个场景:直到运行时错误才暴露出来,这是不安全的,也是违反软件开发原则的,应该尽早的在编译阶段就发现问题,解决问题

在这里插入图片描述

3、使用Object太过于模糊了,没有具体类型的意义

最好不要用到Object,因为Object是一切类型的基类,也就是说他把一些类型的特点给抹除了,比如上面存的数字,对于数字来说,加法运算就是它的一个特点,但是用了Object,它就失去了这一特点,失去类型特有的行为

引入泛型

什么是泛型?

泛型:是被参数化的类或接口,是对类型的约定

泛型类

1
java复制代码class name<T1, T2, ..., Tn> { /* ... */ }

一般将泛型中的类名称为原型,而将 <> 指定的参数称为类型参数,<> 相当于类型的约定,T就是类型,相当于一个占位符,由我们在调用时指定

使用泛型改进一下上面StackObject类,但是,数组和泛型不能很好地结合。你不能实例化具有参数化类型的数组,例如下面的代码是不合格的:

1
2
3
4
5
java复制代码public StackT(int maxSize){
this.maxSize = maxSize;
this.items = new T[maxSize];
this.top = -1;
}

在这里插入图片描述

Java 中不允许直接创建泛型数组,这是因为相比于C++,C#的语法,Java泛型其实是伪泛型,这点在后面会说到,但是,可以通过创建一个类型擦除的数组,然后转型的方式来创建泛型数组。

1
2
3
4
5
6
7
8
9
java复制代码private int maxSize;
private T[] items;
private int top;

public StackT(int maxSize){
this.maxSize = maxSize;
this.items = (T[]) new Object[maxSize];
this.top = -1;
}

实际上,真的需要存储泛型,还是使用容器更合适,回到原来的代码上,需要知道的是,泛型类型不能是基本类型的,需要是包装类

在这里插入图片描述

上面说到了Java 中不允许直接创建泛型数组,事实上,Java中的泛型我们是很难通new的方式去实例化对象,不仅仅是实例化对象,甚至是获取T的真实类型也是很难的,当然通过反射的机制还是可以获取到的,Java获取真实类型的方式有 3 种,分别是:

1、类名.class

2、对象.getClass

3、class.forName(“全限定类名”)

但是,在这里,1和2的方式都是做不到的,虽然我们在外边明确的传入了Integer类型,new StackT<Integer>(3);但是在StackT

类,使用T.class还是获取不到真实类型的,第 2 种方式的话,并没有传入对象,前面也说到是没有办法new方式实例化的,而通过反射机制是可以做到的,这里不作演示,需要了解的话可以参考 —— Java如何获得泛型类的真实类型、 Java通过反射获取泛型的类型

在这里插入图片描述

但是在C#中的泛型以及C++的模板,这是很容易做到的,所以说Java的泛型是伪泛型,Java并不是做不到像C#一样,而是为了迁就老的JDK语法所作出的妥协,至于上面为什么做不到这样,这就要说到泛型的类型擦除了。

再说类型擦除之前,先说一下泛型接口,和泛型方法吧

泛型接口

接口也可以声明泛型,泛型接口语法形式:

1
2
3
java复制代码public interface Content<T> {
T text();
}

泛型接口有两种实现方式:

  • 实现接口的子类明确声明泛型类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class ContentImpl implements Content<Integer> {
private int text;

public ContentImpl(int text) {
this.text = text;
}

public static void main(String[] args) {
ContentImpl one = new ContentImpl(10);
System.out.print(one.text());
}
}
// Output:
// 10
  • 实现接口的子类不明确声明泛型类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class ContentImpl<T> implements Content<T> {
private T text;

public ContentImpl(T text) {
this.text = text;
}

@Override
public T text() { return text; }

public static void main(String[] args) {
ContentImpl<String> two = new ContentImpl<>("ABC");
System.out.print(two.text());
}
}
// Output:
// ABC

泛型方法

泛型方法是引入其自己的类型参数的方法。泛型方法可以是普通方法、静态方法以及构造方法。

泛型方法语法形式如下:

1
java复制代码public <T> T func(T obj) {}

是否拥有泛型方法,与其所在的类是否是泛型没有关系。

泛型方法的语法包括一个类型参数列表,在尖括号内,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际类型参数的占位符。

使用泛型方法的时候,通常不必指明类型参数,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。类型推断只对赋值操作有效,其他时候并不起作用。如果将一个泛型方法调用的结果作为参数,传递给另一个方法,这时编译器并不会执行推断。编译器会认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class GenericsMethod {
public static <T> void printClass(T obj) {
System.out.println(obj.getClass().toString());
}

public static void main(String[] args) {
printClass("abc");
printClass(10);
}
}
// Output:
// class java.lang.String
// class java.lang.Integer

泛型方法中也可以使用可变参数列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class GenericVarargsMethod {
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<T>();
Collections.addAll(result, args);
return result;
}

public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
}
}
// Output:
// [A]
// [A, B, C]

类型擦除

事实上,Java的运行大致可以分为两个阶段,编译阶段,运行阶段

那么对于Java泛型来说,当编译阶段过后,泛型 T 是已经被擦除了,所以在运行阶段,它已经丢失了 T 的具体信息,而我们去实例化一个对象的时候,比如T c = new T();,它的发生时机是在运行阶段,而在运行阶段,你要new T(),就需要知道 T 的具体类型,实际上这时候 T是被替换成Integer了,而JVM是不知道T的类型的,所以是没有办法实例化的。

那么,类型擦除做了什么呢?它做了以下工作:

  • 把泛型中的所有类型参数替换为 Object,如果指定类型边界,则使用类型边界来替换。因此,生成的字节码仅包含普通的类,接口和方法。
  • 擦除出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get() ;List<String> 就变成了 List。如有必要,插入类型转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。类型擦除确保不为参数化类型创建新类;因此,泛型不会产生运行时开销。

让我们来看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码import java.util.*;

public class ErasedTypeEquivalence {

public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}

}
/* Output:
true
*/

ArrayList<String> 和 ArrayList<Integer> 应该是不同的类型。不同的类型会有不同的行为。例如,如果尝试向 ArrayList<String> 中放入一个 Integer,所得到的行为(失败)和 向 ArrayList<Integer> 中放入一个 Integer 所得到的行为(成功)完全不同。但是结果输出的是true,这意味着使用泛型时,任何具体的类型信息都被擦除了,ArrayList<Object> 和 ArrayList<Integer> 在运行时,JVM 将它们视为同一类型class java.util.ArrayList

再用一个例子来对于该谜题的补充:

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
java复制代码import java.util.*;

class Frob {}
class Fnorkle {}
class Quark<Q> {}

class Particle<POSITION, MOMENTUM> {}

public class LostInformation {

public static void main(String[] args) {

List<Frob> list = new ArrayList<>();
Map<Frob, Fnorkle> map = new HashMap<>();
Quark<Fnorkle> quark = new Quark<>();
Particle<Long, Double> p = new Particle<>();

System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
}

}
/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/

根据 JDK 文档,Class.getTypeParameters() “返回一个 TypeVariable 对象数组,表示泛型声明中声明的类型参数…” 这暗示你可以发现这些参数类型。但是正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。

残酷的现实是:在泛型代码内部,无法获取任何有关泛型参数类型的信息。

以上两个例子皆出《Java 编程思想》第五版 —— On Java 8中的例子,本文借助该例子,试图讲清楚Java泛型是使用类型擦除这里机制实现的,能力不足,有错误的地方,还请指正。关于On Java 8一书,已在github上开源,并有热心的伙伴将之翻译成中文,现在给出阅读地址,On Java 8

擦除的问题

擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。

考虑如下的代码段:

1
2
3
java复制代码class Foo<T> {
T var;
}

看上去当你创建一个 Foo 实例时:

1
java复制代码Foo<Cat> f = new Foo<>();

class Foo 中的代码应该知道现在工作于 Cat 之上。泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换,就像在 C++ 中一样。但是事实并非如此,当你在编写这个类的代码时,必须提醒自己:“不,这只是一个 Object“。

继承问题

泛型时基于类型擦除实现的,所以,泛型类型无法向上转型。

向上转型是指用子类实例去初始化父类,这是面向对象中多态的重要表现。

在这里插入图片描述

Integer 继承了 Object;ArrayList 继承了 List;但是 List<Interger> 却并非继承了 List<Object>。

这是因为,泛型类并没有自己独有的 Class 类对象。比如:并不存在 List<Object>.class 或是 List<Interger>.class,Java 编译器会将二者都视为 List.class。

如何解决上面所产生的问题:

其实并不一定要通过new的方式去实例化,我们可以通过显式的传入源类,一个Class<T> clazz的对象来补偿擦除,例如instanceof 操作,在程序中尝试使用 instanceof 将会失败。类型标签可以使用动态 isInstance() ,这样改进代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class Improve<T> {

//错误方法
public boolean f(Object arg) {
// error: illegal generic type for instanceof
if (arg instanceof T) {
return true;
}
return false;
}
//改进方法
Class<T> clazz;

public Improve(Class<T> clazz) {
this.clazz = clazz;
}

public boolean f(Object arg) {
return kind.isInstance(arg);
}
}

实例化:

试图在 new T() 是行不通的,部分原因是由于擦除,部分原因是编译器无法验证 T 是否具有默认(无参)构造函数。

Java 中的解决方案是传入一个工厂对象,并使用该对象创建新实例。方便的工厂对象只是 Class 对象,因此,如果使用类型标记,则可以使用 newInstance() 创建该类型的新对象:

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
java复制代码class Improve<T> {
Class<T> kind;

Improve(Class<T> kind) {
this.kind = kind;
}

public T get(){
try {
return kind.newInstance();
} catch (InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

class Employee {
@Override
public String toString() {
return "Employee";
}
}

public class InstantiateGenericType {
public static void main(String[] args) {
Improve<Employee> fe = new Improve<>(Employee.class);
System.out.println(fe.get());
}
}
/* Output:
Employee
*/

通过这样改进代码,可以实现创建对象的实例,但是要注意的是,newInstance();方法调用无参构造函数的,如果传入的类型,没有无参构造的话,是会抛出InstantiationException异常的。

泛型数组:

泛型数组这部分,我们在上面说到可以通过创建一个类型擦除的数组,然后转型的方式来创建泛型数组,这次我们可以通过显式的传入源类的方式来编写StackT类,解决创建泛型数组的问题,代码如下:

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
java复制代码public class StackT<T> {

private int maxSize;
private T[] items;
private int top;

public StackT(int maxSize, Class<T> clazz){
this.maxSize = maxSize;
this.items = this.createArray(clazz);
this.top = -1;
}

public boolean isFull(){
return this.top == this.maxSize-1;
}

public boolean isNull(){
return this.top <= -1;
}

public boolean push(T value){
if(this.isFull()){
return false;
}
this.items[++this.top] = value;
return true;
}

public T pop(){
if(this.isNull()){
throw new RuntimeException("当前栈中无数据");
}
T value = this.items[top];
--top;
return value;
}

private T[] createArray(Class<T> clazz){
T[] array =(T[])Array.newInstance(clazz, this.maxSize);
return array;
}

}

边界

有时您可能希望限制可在参数化类型中用作类型参数的类型。类型边界可以对泛型的类型参数设置限制条件。例如,对数字进行操作的方法可能只想接受 Number 或其子类的实例。

要声明有界类型参数,请列出类型参数的名称,然后是 extends 关键字,后跟其限制类或接口。

类型边界的语法形式如下:

1
text复制代码<T extends XXX>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class GenericsExtendsDemo01 {
static <T extends Comparable<T>> T max(T x, T y, T z) {
T max = x; // 假设x是初始最大值
if (y.compareTo(max) > 0) {
max = y; //y 更大
}
if (z.compareTo(max) > 0) {
max = z; // 现在 z 更大
}
return max; // 返回最大对象
}

public static void main(String[] args) {
System.out.println(max(3, 4, 5));
System.out.println(max(6.6, 8.8, 7.7));
System.out.println(max("pear", "apple", "orange"));
}
}
// Output:
// 5
// 8.8
// pear

示例说明:

上面的示例声明了一个泛型方法,类型参数 T extends Comparable<T> 表明传入方法中的类型必须实现了 Comparable 接口。

类型边界可以设置多个,语法形式如下:

1
text复制代码<T extends B1 & B2 & B3>

注意:extends 关键字后面的第一个类型参数可以是类或接口,其他类型参数只能是接口。

通配符

通配符是Java泛型中的一个非常重要的知识点。很多时候,我们其实不是很理解通配符?和泛型类型T区别,容易混淆在一起,其实还是很好理解的,?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :

1
2
3
4
java复制代码// 可以
T t = operate();
// 不可以
? car = operate();

但是这个并不是我们混淆的原因,虽然?和 T 都表示不确定的类型,T 通常用于泛型类和泛型方法的定义,?通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。用代码解释一下,回到文章最初说的栈类StackT,我们以这个为基础来解释,上面的观点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Why {
public static void main(String[] args) {

StackT<Integer> stackT = new StackT<>(3, Integer.class);
stackT.push(8);
StackT<String> stackT1 = new StackT<>(3, String.class);
stackT1.push("7");
test(stackT1);

}
public static void test(StackT stackT){
System.out.println(stackT.pop());
}
}
// Output: 8

以我们编写的StackT类,进行测试,编写一个test方法,传入参数类型StackT,上面的程序正常输出字符串”7” ,这没有什么问题,问题在这里失去了泛型的限定,传进去的实参StackT1,是被我们限定为StackT<String> ,但是我们通过编译器可以看到stackT.pop()出来的对象,并没有String类型的特有方法,也就是说,它其实是Object类

在这里插入图片描述

那么我们就需要修改test方法的形参,改为:

1
2
3
java复制代码public static void test(StackT<String> stackT){
System.out.println(stackT.pop());
}

这样子就回到了我们问题的本质来了,将形参修改为StackT<String>,这起到了泛型的限定作用,但是会出现这样的问题,如果我们需要向该方法传入StackT<Integer>类型的对象 stackT是,因为方法形参限定了StackT<String>,,这时候就报错了

在这里插入图片描述

这个时候就是通配符?起作用了,将方法形参改为StackT<?>就可以了,这也就确定了我们刚刚的结论,?通配符通常是用于泛型传参,而不是泛型类的定义。

1
2
3
java复制代码public static void test(StackT<?> stackT){
System.out.println(stackT.pop());
}

但是这种用法我们通常也不会去用,因为它还是失去了类型的特点,即当无界泛型通配符作为形参时,作为调用方,并不限定传递的实际参数类型。但是,在方法内部,泛型类的参数和返回值为泛型的方法,不能使用!

在这里插入图片描述

这里,StackT.push就不能用了,因为我并不知道?传的是Integer还是String ,还是其他类型,所以是会报错的。

但是我们有时候是有这样的需求的,我们在接收泛型栈StackT作为形参的时候,我想表达一种约束的关系,但是又不像StackT<String>一样,约束的比较死板,而Java是面向对象的语言,那么就会有继承的机制,我想要的约束关系是我能接收的泛型栈的类型都是Number类的派生类,即不会像?无界通配符一样失去类的特征,又不会像StackT<String>约束的很死,这就引出了上界通配符的概念。

上界通配符

可以使用**上界通配符**来缩小类型参数的类型范围。

它的语法形式为:<? extends Number>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class Why {
public static void main(String[] args) {

StackT<Integer> stackT = new StackT<>(3, Integer.class);
stackT.push(8);
StackT<String> stackT1 = new StackT<>(3, String.class);
stackT1.push("7");
StackT<Double> stackT2 = new StackT<>(3, Double.class);

//通过
test(stackT);
test(stackT2);
//error
test(stackT1);

}

public static void test(StackT<? extends Number> stackT){

System.out.println(stackT.pop());
}
}

这样就实现了一类类型的限定,但是需求变更了,我现在希望的约束关系是我能接收的泛型栈的类型都是Number类的父类,或者父类的父类,那么有上界,自然就有下界

下界通配符

**下界通配符**将未知类型限制为该类型的特定类型或超类类型。

注意:上界通配符和下界通配符不能同时使用。

它的语法形式为:<? super Number>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class Why {
public static void main(String[] args) {

StackT<Number> stackT1 = new StackT<>(3, Number.class);
stackT1.push(8);
StackT<Double> stackT2 = new StackT<>(3, Double.class);
StackT<Object> stackT3 = new StackT<>(3, Object.class);
//通过
test(stackT1);
test(stackT3);
//error
test(stackT2);

}

public static void test(StackT<? super Number> stackT){

System.out.println(stackT.pop());
}
}

这样子的话,就确保了我们的test方法只接收Number类型以上的方法。泛型的各种高级语法可能在写业务代码的时候可以规避,但是如果你要去写一些框架的时候,由于你不知道框架的使用者的使用场景,那么掌握泛型的高级语法就很有用了。

通配符和向上转型

前面,我们提到:泛型不能向上转型。但是,我们可以通过使用通配符来向上转型。

1
2
3
4
5
6
7
8
9
java复制代码public class GenericsWildcardDemo {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Error

List<? extends Integer> intList2 = new ArrayList<>();
List<? extends Number> numList2 = intList2; // OK
}
}

通配符边界问题,关于一些更加深入的解惑可以参考整理的转载的文章——Java泛型解惑之上下通配符

泛型约束

  • 泛型类型的类型参数不能是值类型
1
java复制代码Pair<int, char> p = new Pair<>(8, 'a');  // 编译错误
  • 不能创建类型参数的实例
1
2
3
4
java复制代码public static <E> void append(List<E> list) {
E elem = new E(); // 编译错误
list.add(elem);
}
  • 不能声明类型为类型参数的静态成员
1
2
3
4
5
java复制代码public class MobileDevice<T> {
private static T os; // error

// ...
}
  • 类型参数不能使用类型转换或 instanceof
1
2
3
4
5
6
7
java复制代码public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 编译错误
// ...
}
}
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 编译错误
  • 不能创建类型参数的数组
1
java复制代码List<Integer>[] arrayOfLists = new List<Integer>[2];  // 编译错误
  • 不能创建、catch 或 throw 参数化类型对象
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // 编译错误

// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 编译错误
public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
  • 仅仅是泛型类相同,而类型参数不同的方法不能重载
1
2
3
4
java复制代码public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { } // 编译错误
}

实践总结

泛型命名

泛型一些约定俗成的命名:

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

使用泛型的建议

  • 消除类型检查警告
  • List 优先于数组
  • 优先考虑使用泛型来提高代码通用性
  • 优先考虑泛型方法来限定泛型的范围
  • 利用有限制通配符来提升 API 的灵活性
  • 优先考虑类型安全的异构容器

参考资料:

深入理解 Java 泛型

On Java 8

Java泛型解惑之 extends T>和 super T>上下界限

7月的直播课——Java 高级语法—泛型

本文转载自: 掘金

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

【每日鲜蘑】Quarkus很简单的嵌入了swagger🔥

发表于 2020-09-09

Eclipse MicroProfile社区还是很不错的,为Java的微服务提供了很多很好的规范。而Quarkus在MicroProfile规范中进行构建的,天然支持MicroProfile规范的组件。

  • REST Client: 客户端调用,类似于Feign
  • Metrics 监控服务,类似于Actuator
  • OpenAPI 接口服务,类似于Springfox Swagger
  • OpenTracing 链路追踪,类似于Spring Cloud Sleuth
  • Config 配置中心,类似于Spring Cloud Config

引入 Swagger

1
2
3
4
xml复制代码<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>

访问地址

1
bash复制代码http://127.0.0.1:8080/swagger-ui

常用的注解

  • @Operation: 描述针对某个特定路径的操作,或者通常是一个 HTTP 方法。(HTTP方法)
  • @APIResponse: 描述来自一个 API 操作的一次响应。(HTTP响应)
  • @RequestBody: 描述一个请求主体。(HTTP请求)
  • @Content: 提供一种特定媒体类型的模式和示例。
  • @Schema: 允许定义输入和输出数据类型。(Model)
  • @Server: 一个用于多个服务器定义的容器。
  • @ServerVariable: 表示一个用于服务器 URL 模板置换的服务器变量。
  • @OpenAPIDefinition: 一个 OpenAPI 定义的综合元数据

与Springfox注解的对应关系

  • @ApiParam -> @Parameter
  • @ApiOperation -> @Operation
  • @Api -> @Tag
  • @ApiImplicitParams -> @Parameters
  • @ApiImplicitParam -> @Parameter
  • @ApiIgnore -> @Parameter(hidden = true) or @Operation(hidden = true) or @Hidden
  • @ApiModel -> @Schema
  • @ApiModelProperty -> @Schema

总结

OpenAPI 使用 JAX-RS 2.0 应用程序生成一个有效的 OpenAPI 文档。它处理所有 JAX-RS 注解(包括 @Path 和 @Consumes/@Produces 注解),以及用作 JAX-RS 操作的输入或输出的 POJO。

本文转载自: 掘金

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

一文搞懂高频面试题之限流算法,从算法原理到实现,再到对比分析

发表于 2020-09-09

限流是指在系统面临高并发、大流量请求的情况下,限制新的流量对系统的访问,从而保证系统服务的安全性。常用的限流算法有计数器固定窗口算法、滑动窗口算法、漏斗算法和令牌桶算法,下面将对这几种算法进行分别介绍,并给出具体的实现。本文目录如下,略长,读者可以全文阅读,同样也可以只看感兴趣的部分。

计数器固定窗口算法

原理

计数器固定窗口算法是最基础也是最简单的一种限流算法。原理就是对一段固定时间窗口内的请求进行计数,如果请求数超过了阈值,则舍弃该请求;如果没有达到设定的阈值,则接受该请求,且计数加1。当时间窗口结束时,重置计数器为0。

计数器固定窗口算法原理图

代码实现及测试

实现起来也比较简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
81
82
java复制代码package project.limiter;

import java.util.concurrent.atomic.AtomicInteger;

/**
* Project: AllForJava
* Title:
* Description:
* Date: 2020-09-07 15:56
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: exzlc@139.com
* @version 1.0
**/

public class CounterLimiter {

private int windowSize; //窗口大小,毫秒为单位
private int limit;//窗口内限流大小
private AtomicInteger count;//当前窗口的计数器

private CounterLimiter(){}

public CounterLimiter(int windowSize,int limit){
this.limit = limit;
this.windowSize = windowSize;
count = new AtomicInteger(0);

//开启一个线程,达到窗口结束时清空count
new Thread(new Runnable() {
@Override
public void run() {
while(true){
count.set(0);
try {
Thread.sleep(windowSize);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}

//请求到达后先调用本方法,若返回true,则请求通过,否则限流
public boolean tryAcquire(){
int newCount = count.addAndGet(1);
if(newCount > limit){
return false;
}else{
return true;
}
}

//测试
public static void main(String[] args) throws InterruptedException {
//每秒20个请求
CounterLimiter counterLimiter = new CounterLimiter(1000,20);
int count = 0;
//模拟50次请求,看多少能通过
for(int i = 0;i < 50;i ++){
if(counterLimiter.tryAcquire()){
count ++;
}
}
System.out.println("第一拨50次请求中通过:" + count + ",限流:" + (50 - count));
//过一秒再请求
Thread.sleep(1000);
//模拟50次请求,看多少能通过
count = 0;
for(int i = 0;i < 50;i ++){
if(counterLimiter.tryAcquire()){
count ++;
}
}
System.out.println("第二拨50次请求中通过:" + count + ",限流:" + (50 - count));
}

}

测试结果如下:

计数器固定窗口算法测试结果

可以看到50个请求只有20个通过了,30个被限流,达到了预期的限流效果。

特点分析

优点:实现简单,容易理解。

缺点:流量曲线可能不够平滑,有“突刺现象”,如下图所示。这样会有两个问题:

计数器固定窗口算法限流曲线

  1. 一段时间内(不超过时间窗口)系统服务不可用。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第1ms来了100个请求,然后第2ms-999ms的请求就都会被拒绝,这段时间用户会感觉系统服务不可用。
  2. 窗口切换时可能会产生两倍于阈值流量的请求。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第999ms来了100个请求,窗口前期没有请求,所以这100个请求都会通过。再恰好,下一个窗口的第1ms有来了100个请求,也全部通过了,那也就是在2ms之内通过了200个请求,而我们设定的阈值是100,通过的请求达到了阈值的两倍。

计数器固定窗口限流算法产生两倍于阈值流量的请求

计数器滑动窗口算法

原理

计数器滑动窗口算法是计数器固定窗口算法的改进,解决了固定窗口切换时可能会产生两倍于阈值流量请求的缺点。

滑动窗口算法在固定窗口的基础上,将一个计时窗口分成了若干个小窗口,然后每个小窗口维护一个独立的计数器。当请求的时间大于当前窗口的最大时间时,则将计时窗口向前平移一个小窗口。平移时,将第一个小窗口的数据丢弃,然后将第二个小窗口设置为第一个小窗口,同时在最后面新增一个小窗口,将新的请求放在新增的小窗口中。同时要保证整个窗口中所有小窗口的请求数目之后不能超过设定的阈值。

计数器滑动窗口算法原理图

从图中不难看出,滑动窗口算法就是固定窗口的升级版。将计时窗口划分成一个小窗口,滑动窗口算法就退化成了固定窗口算法。而滑动窗口算法其实就是对请求数进行了更细粒度的限流,窗口划分的越多,则限流越精准。

代码实现及测试

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
java复制代码package project.limiter;

/**
* Project: AllForJava
* Title:
* Description:
* Date: 2020-09-07 18:38
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: exzlc@139.com
* @version 1.0
**/

public class CounterSildeWindowLimiter {

private int windowSize; //窗口大小,毫秒为单位
private int limit;//窗口内限流大小
private int splitNum;//切分小窗口的数目大小
private int[] counters;//每个小窗口的计数数组
private int index;//当前小窗口计数器的索引
private long startTime;//窗口开始时间

private CounterSildeWindowLimiter(){}

public CounterSildeWindowLimiter(int windowSize, int limit, int splitNum){
this.limit = limit;
this.windowSize = windowSize;
this.splitNum = splitNum;
counters = new int[splitNum];
index = 0;
startTime = System.currentTimeMillis();
}

//请求到达后先调用本方法,若返回true,则请求通过,否则限流
public synchronized boolean tryAcquire(){
long curTime = System.currentTimeMillis();
long windowsNum = Math.max(curTime - windowSize - startTime,0) / (windowSize / splitNum);//计算滑动小窗口的数量
slideWindow(windowsNum);//滑动窗口
int count = 0;
for(int i = 0;i < splitNum;i ++){
count += counters[i];
}
if(count >= limit){
return false;
}else{
counters[index] ++;
return true;
}
}

private synchronized void slideWindow(long windowsNum){
if(windowsNum == 0)
return;
long slideNum = Math.min(windowsNum,splitNum);
for(int i = 0;i < slideNum;i ++){
index = (index + 1) % splitNum;
counters[index] = 0;
}
startTime = startTime + windowsNum * (windowSize / splitNum);//更新滑动窗口时间
}

//测试
public static void main(String[] args) throws InterruptedException {
//每秒20个请求
int limit = 20;
CounterSildeWindowLimiter counterSildeWindowLimiter = new CounterSildeWindowLimiter(1000,limit,10);
int count = 0;

Thread.sleep(3000);
//计数器滑动窗口算法模拟100组间隔30ms的50次请求
System.out.println("计数器滑动窗口算法测试开始");
System.out.println("开始模拟100组间隔150ms的50次请求");
int faliCount = 0;
for(int j = 0;j < 100;j ++){
count = 0;
for(int i = 0;i < 50;i ++){
if(counterSildeWindowLimiter.tryAcquire()){
count ++;
}
}
Thread.sleep(150);
//模拟50次请求,看多少能通过
for(int i = 0;i < 50;i ++){
if(counterSildeWindowLimiter.tryAcquire()){
count ++;
}
}
if(count > limit){
System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit);
faliCount ++;
}
Thread.sleep((int)(Math.random() * 100));
}
System.out.println("计数器滑动窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数:" + faliCount);
System.out.println("===========================================================================================");


//计数器固定窗口算法模拟100组间隔30ms的50次请求
System.out.println("计数器固定窗口算法测试开始");
//模拟100组间隔30ms的50次请求
CounterLimiter counterLimiter = new CounterLimiter(1000,limit);
System.out.println("开始模拟100组间隔150ms的50次请求");
faliCount = 0;
for(int j = 0;j < 100;j ++){
count = 0;
for(int i = 0;i < 50;i ++){
if(counterLimiter.tryAcquire()){
count ++;
}
}
Thread.sleep(150);
//模拟50次请求,看多少能通过
for(int i = 0;i < 50;i ++){
if(counterLimiter.tryAcquire()){
count ++;
}
}
if(count > limit){
System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit);
faliCount ++;
}
Thread.sleep((int)(Math.random() * 100));
}
System.out.println("计数器滑动窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数:" + faliCount);
}
}

测试时,取滑动窗口大小为1000/10=100ms,然后模拟100组间隔150ms的50次请求,计数器滑动窗口算法与计数器固定窗口算法进行对别,可以看到如下结果:

计数器滑动窗口算法测试结果

固定窗口算法在窗口切换时产生了两倍于阈值流量请求的问题,而滑动窗口算法避免了这个问题。

特点分析

  1. 避免了计数器固定窗口算法固定窗口切换时可能会产生两倍于阈值流量请求的问题;
  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
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
java复制代码package project.limiter;

import java.util.Date;
import java.util.LinkedList;

/**
* Project: AllForJava
* Title:
* Description:
* Date: 2020-09-08 16:45
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: exzlc@139.com
* @version 1.0
**/
public class LeakyBucketLimiter {

private int capaticy;//漏斗容量
private int rate;//漏斗速率
private int left;//剩余容量
private LinkedList<Request> requestList;

private LeakyBucketLimiter() {}

public LeakyBucketLimiter(int capaticy, int rate) {
this.capaticy = capaticy;
this.rate = rate;
this.left = capaticy;
requestList = new LinkedList<>();

//开启一个定时线程,以固定的速率将漏斗中的请求流出,进行处理
new Thread(new Runnable() {
@Override
public void run() {
while(true){
if(!requestList.isEmpty()){
Request request = requestList.removeFirst();
handleRequest(request);
}
try {
Thread.sleep(1000 / rate); //睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}

/**
* 处理请求
* @param request
*/
private void handleRequest(Request request){
request.setHandleTime(new Date());
System.out.println(request.getCode() + "号请求被处理,请求发起时间:"
+ request.getLaunchTime() + ",请求处理时间:" + request.getHandleTime() + ",处理耗时:"
+ (request.getHandleTime().getTime() - request.getLaunchTime().getTime()) + "ms");
}

public synchronized boolean tryAcquire(Request request){
if(left <= 0){
return false;
}else{
left --;
requestList.addLast(request);
return true;
}
}


/**
* 请求类,属性包含编号字符串、请求达到时间和请求处理时间
*/
static class Request{
private int code;
private Date launchTime;
private Date handleTime;

private Request() { }

public Request(int code,Date launchTime) {
this.launchTime = launchTime;
this.code = code;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public Date getLaunchTime() {
return launchTime;
}

public void setLaunchTime(Date launchTime) {
this.launchTime = launchTime;
}

public Date getHandleTime() {
return handleTime;
}

public void setHandleTime(Date handleTime) {
this.handleTime = handleTime;
}
}

public static void main(String[] args) {
LeakyBucketLimiter leakyBucketLimiter = new LeakyBucketLimiter(5,2);
for(int i = 1;i <= 10;i ++){
Request request = new Request(i,new Date());
if(leakyBucketLimiter.tryAcquire(request)){
System.out.println(i + "号请求被接受");
}else{
System.out.println(i + "号请求被拒绝");
}
}
}
}

测试时,取漏斗限流算法的容量是5,漏斗速率为2个/秒,然后模拟了连续的10个请求,编号从1-10,结果如下:

漏斗算法测试结果

可以看到1-5号请求被接受,而6-10号请求被拒绝,说明此时漏斗已经溢出了,符合我们的预期。

我们再关注下被接受的这5个请求的处理情况,可以看到这5个请求虽然被接受了,但是处理是一个一个被处理的(不一定是顺序的,取决于具体实现),大约每500ms处理一个。这就体现了漏斗算法的特点了,即虽然请求流量是瞬时产生的,但是请求以固定速率流出被处理。因为我们设定的漏斗速率为2个/秒,所以每500ms漏斗会漏出一个请求然后进行处理。

特点分析

  1. 漏桶的漏出速率是固定的,可以起到整流的作用。即虽然请求的流量可能具有随机性,忽大忽小,但是经过漏斗算法之后,变成了有固定速率的稳定流量,从而对下游的系统起到保护作用。
  2. 不能解决流量突发的问题。还是拿刚刚测试的例子,我们设定的漏斗速率是2个/秒,然后突然来了10个请求,受限于漏斗的容量,只有5个请求被接受,另外5个被拒绝。你可能会说,漏斗速率是2个/秒,然后瞬间接受了5个请求,这不就解决了流量突发的问题吗?不,这5个请求只是被接受了,但是没有马上被处理,处理的速度仍然是我们设定的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
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
java复制代码package project.limiter;

import java.util.Date;

/**
* Project: AllForJava
* Title:
* Description:
* Date: 2020-09-08 19:22
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: exzlc@139.com
* @version 1.0
**/
public class TokenBucketLimiter {

private int capaticy;//令牌桶容量
private int rate;//令牌产生速率
private int tokenAmount;//令牌数量

public TokenBucketLimiter(int capaticy, int rate) {
this.capaticy = capaticy;
this.rate = rate;
tokenAmount = capaticy;
new Thread(new Runnable() {
@Override
public void run() {
//以恒定速率放令牌
while (true){
synchronized (this){
tokenAmount ++;
if(tokenAmount > capaticy){
tokenAmount = capaticy;
}
}
try {
Thread.sleep(1000 / rate);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}

public synchronized boolean tryAcquire(Request request){
if(tokenAmount > 0){
tokenAmount --;
handleRequest(request);
return true;
}else{
return false;
}

}

/**
* 处理请求
* @param request
*/
private void handleRequest(Request request){
request.setHandleTime(new Date());
System.out.println(request.getCode() + "号请求被处理,请求发起时间:"
+ request.getLaunchTime() + ",请求处理时间:" + request.getHandleTime() + ",处理耗时:"
+ (request.getHandleTime().getTime() - request.getLaunchTime().getTime()) + "ms");
}

/**
* 请求类,属性只包含一个名字字符串
*/
static class Request{
private int code;
private Date launchTime;
private Date handleTime;

private Request() { }

public Request(int code,Date launchTime) {
this.launchTime = launchTime;
this.code = code;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public Date getLaunchTime() {
return launchTime;
}

public void setLaunchTime(Date launchTime) {
this.launchTime = launchTime;
}

public Date getHandleTime() {
return handleTime;
}

public void setHandleTime(Date handleTime) {
this.handleTime = handleTime;
}
}


public static void main(String[] args) throws InterruptedException {
TokenBucketLimiter tokenBucketLimiter = new TokenBucketLimiter(5,2);
for(int i = 1;i <= 10;i ++){
Request request = new Request(i,new Date());
if(tokenBucketLimiter.tryAcquire(request)){
System.out.println(i + "号请求被接受");
}else{
System.out.println(i + "号请求被拒绝");
}
}
}
}

测试时,为了与漏斗限流算法进行对别,同样取令牌桶算法的容量是5,产生令牌的速度为2个/秒,然后模拟了连续的10个请求,编号从1-10,结果如下:

令牌桶算法测试结果

可以看到,对于10个请求,令牌桶算法和漏斗算法一样,都是接受了5个请求,拒绝了5个请求。与漏斗算法不同的是,令牌桶算法马上处理了这5个请求,处理速度可以认为是5个/秒,超过了我们设定的2个/秒的速率,即允许一定程度的流量突发。这一点也是和漏斗算法的主要区别,可以认真体会一下。

特点分析

令牌桶算法是对漏桶算法的一种改进,除了能够在限制调用的平均速率的同时还允许一定程度的流量突发。

小结

我们对上述四种限流算法进行一下简单的总结。

计数器固定窗口算法实现简单,容易理解。和漏斗算法相比,新来的请求也能够被马上处理到。但是流量曲线可能不够平滑,有“突刺现象”,在窗口切换时可能会产生两倍于阈值流量的请求。而计数器滑动窗口算法作为计数器固定窗口算法的一种改进,有效解决了窗口切换时可能会产生两倍于阈值流量请求的问题。

漏斗算法能够对流量起到整流的作用,让随机不稳定的流量以固定的速率流出,但是不能解决流量突发的问题。令牌桶算法作为漏斗算法的一种改进,除了能够起到平滑流量的作用,还允许一定程度的流量突发。

以上四种限流算法都有自身的特点,具体使用时还是要结合自身的场景进行选取,没有最好的算法,只有最合适的算法。比如令牌桶算法一般用于保护自身的系统,对调用者进行限流,保护自身的系统不被突发的流量打垮。如果自身的系统实际的处理能力强于配置的流量限制时,可以允许一定程度的流量突发,使得实际的处理速率高于配置的速率,充分利用系统资源。而漏斗算法一般用于保护第三方的系统,比如自身的系统需要调用第三方的接口,为了保护第三方的系统不被自身的调用打垮,便可以通过漏斗算法进行限流,保证自身的流量平稳的打到第三方的接口上。

算法是死的,而算法中的思想精髓才是值得我们学习的。实际的场景中完全可以灵活运用,还是那句话,没有最好的算法,只有最合适的算法。

觉得文章有用的话,点赞+关注呗,好让更多的人看到这篇文章,也激励博主写出更多的好文章。
更多关于校招面试、算法、数据结构和计算机基础知识的内容,欢迎扫码关注我的原创公众号「超悦编程」。

超悦编程

更多推荐阅读

为什么有红黑树?什么是红黑树?看完这篇你就明白了

《深入浅出话数据结构》系列之什么是B树、B+树?为什么二叉查找树不行?

都2020年了,听说你还不会归并排序?手把手教你手写归并排序算法

为什么会有多线程?什么是线程安全?如何保证线程安全?

《一文说透数据结构》系列之什么是堆?看这一篇就够了

本文转载自: 掘金

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

架构师的初级技能,选组件!(2020更新版)

发表于 2020-09-09

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

2020年新版,对部分组件的描述进行了更新。19年文章参见 这里 。如果你在做选型方面的工作,或者想了解一些现在正在流行的技术,那么这篇文章正好适合你。

本篇内容涵盖14个方面,涉及上百个框架和工具。会有你喜欢的,大概也会有你所讨厌的家伙。这是我平常工作中打交道最多的工具,大小公司都适用。如果你有更好的,欢迎留言补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
css复制代码一、消息队列
二、缓存
三、分库分表
四、数据同步
五、通讯
六、微服务
七、分布式工具
八、监控系统
九、调度
十、入口工具
十一、OLT(A)P
十二、CI/CD
十三、问题排查
十四、本地工具

一、消息队列

  • √ 推荐:(1) 吞吐量优先选择kafka
  • (2) 稳定性优先选择RocketMQ
  • (3) 物联网:VerneMQ

一个大型的分布式系统,通常都会异步化,走消息总线。 消息队列作为最主要的基础组件,在整个体系架构中,有着及其重要的作用。异步通常意味着编程模型的改变,时效性会降低。

kafka是目前最常用的消息队列,尤其是在大数据方面,有着极高的吞吐量。而rocketmq和rabbitmq,都是电信级别的消息队列,在业务上用的比较多。相比较而言,ActiveMQ使用的最少,属于较老一代的消息框架。

pulsar是为了解决一些kafka上的问题而诞生的消息系统,比较年轻,工具链有限。有些激进的团队经过试用,反响不错,但实际使用并不多。

mqtt具体来说是一种协议,主要用在物联网方面,能够双向通信,属于消息队列范畴,推荐使用vernemq。

相关文章:

「整体」分布式消息系统,设计要点。画龙画虎难画骨

「Kafka」Kafka基础知识索引

「Kafka」 360度测试:KAFKA会丢数据么?其高可用是否满足需求?

「Kafka」 使用多线程增加kafka消费能力

「AMQ」ActiveMQ架构设计与最佳实践,需要一万字

「MQ」开源一个kafka增强:okmq-1.0.0

二、缓存

  • √ 推荐:(1) 堆内缓存使用默认的caffeine
  • (2) 分布式缓存采用redis的cluster集群模式,但要注意使用限制

数据缓存是减少数据库压力的有效途径,有单机java内缓存,和分布式缓存之分。

对于单机来说,guava的LoadingCache和ehcache都是些熟面孔,不过SpringBoot选择了caffeine作为它的默认堆内缓存,这是因为caffeine的速度比较快的原因。

对于分布式缓存来说,优先选择的就是redis,别犹豫。由于redis是单线程的(6.0支持多线程,但默认不开启),并不适合高耗时操作。所以对于一些数据量比较大的缓存,比如图片、视频等,使用老牌的memcached效果会好的多。

JetCache是一个基于Java的缓存系统封装,提供统一的api和注解来简化缓存的使用。类似SpringCache,支持本地缓存和分布式缓存,也是简化开发的利器。

相关文章:

「Redis」这可能是最中肯的Redis规范了

「Redis」与亲生的Redis Cluster,来一次亲密接触

「Redis」redis的zset有多牛?请把耳朵递过来

「Redis」好慌,Redis这么多集群方案,要用哪种?

「协议」架构秘笈:移花接木。使用mysql模拟redis

「Redis」Redis都要老了,你还在用什么古董客户端?

「堆内」新一代缓存Caffeine,速度确实比Guava的Cache快

三、分库分表

  • √ 推荐:shardingsphere中的sharding-jdbc


分库分表,几乎每一个上点规模的公司,都会有自己的方案。目前,推荐使用驱动层的sharding-jdbc(已经进入apache),或者代理层的mycat。如果你没有额外的运维团队,又不想花钱买其他机器,那么就选前者。

如果分库分表涉及的项目不多,spring的动态数据源是一个非常好的选择。它直接编码在代码里,直观但不易扩展。

如果只需要读写分离 ,那么mysql官方驱动里的replication协议,是更加轻量级的选择。

上面的分库分表组件,都是大浪淘沙,最终的优胜品。这些组件不同于其他组件选型,方案一旦确定,几乎无法回退,所以要慎之又慎。

分库分表是小case,准备分库分表的阶段,才是重点:也就是数据同步。

相关文章:

「分库分表」“分库分表” ?选型和流程要慎重,否则会失控

「数据同步」希望一个数据同步,包治百病

「分库分表」分库分表“实践”大全

「HA」”MySQL官方驱动“主从分离的神秘面纱

「Sharding」 现实中的路由规则,可能比你想象中复杂的多

「Sharding」 非规范SQL的sharding-jdbc实践

四、数据同步

  • √ 推荐:canal


国内使用mysql的公司居多,但postgresql凭借其优异的性能,使用率逐渐攀升。

不管什么数据库,实时数据同步工具,都是把自己模拟成一个从库,进行数据拉取和解析。 具体来说,mysql是通过binlog进行同步;postgresql使用wal日志进行同步。

对mysql来说,canal是国内用的最多的方案;类似的databus也是比较好用的工具。

现在,canal、maxwell等工具,都支持将要同步的数据写入到mq中,进行后续处理,方便了很多。

对于ETL(抽取、清洗、转换)来说,基本上都是source、task、sink路线,与前面的功能对应。gobblin、datax、logstash、sqoop等,都是这样的工具。

它们的主要工作,就是怎么方便的定义配置文件,编写各种各样的数据源适配接口等。这些ETL工具,也可以作为数据同步(尤其是全量同步)的工具,通常是根据ID,或者最后更新时间 等,进行处理。

binlog是实时增量工具,ETL工具做辅助。通常一个数据同步功能,需要多个组件的参与,他们共同组成一个整体。

相关文章:

「云库」MySQL痿了,放不下这么多数据!

「数据同步」由 Canal 组件分析集成中间件架构的一般过程

「云库」记一次操蛋的方案降级(云上冷热分离的坎坷之路)

五、通讯

  • √ 推荐:http+json,方便调试。高性能要求可选二进制协议


Java 中,netty已经成为当之无愧的网络开发框架,包括其上的socketio(不要再和我提mina了)。对于http协议,有common-httpclient,以及更加轻量级的工具okhttp来支持。

对于一个rpc来说,要约定一个通讯方式和序列化方式。json是最常用的序列化方式,但是传输和解析成本大,xml等文本协议与其类似,都有很多冗余的信息;avro和kryo是二进制的序列化工具,没有这些缺点,但调试不便。

rpc是远程过程调用的意思 ,其中,thrift、dubbo、gRPC默认都是二进制序列化方式的socket通讯框架;feign、hessian都是onhttp的远程调用框架。

对了,gRPC的序列化工具是protobuf,一个压缩比很高的二进制序列化工具。

通常,服务的响应时间主要耗费在业务逻辑以及数据库上,通讯层耗时在其中的占比很小。可以根据自己公司的研发水平和业务规模来选择。

相关文章:

「网络开发」使用Netty,我们到底在开发些什么?

「WS」WebSocket协议 8 问

六、微服务

  • √ 推荐:(1) 注册中心:consul
  • (2)网关:nginx+Gateway
  • (3)配置中心:Apollo
  • (4)调用链:Skywalking
  • (5)熔断:resilience4j

我们不止一次说到微服务,这一次我们从围绕它的一堆支持框架,来窥探一下这个体系。是的,这里依然是在说spring cloud。

默认的注册中心eureka不再维护,consul已经成为首选,它使用raft协议开发开箱即用。nacos、zookeeper等,都可以作为备选方案。其中nacos带有后台,比较适合国人使用习惯。

熔断组件,官方的hystrix也已经不维护了。推荐使用resilience4j,最近阿里的sentinel也表现强劲。

对于调用链来说,由于OpenTracing的兴起,有了很多新的面孔。推荐使用jaeger或者skywalking。spring cloud集成的sleuth+zipkin功能稍弱,甚至不如传统侵入式的cat。

配置中心是管理多环境配置文件的利器,尤其在你不想重启服务器的情况下进行配置更新。目前,开源中做的最好的要数apollo,并提供了对spring boot的支持。disconf使用也较为广泛。相对来说,spring cloud config功能就局限了些,用的很少。


网关方面,使用最多的就是nginx,在nginx之上,有基于lua脚本的openrestry。由于openresty的使用非常繁杂,所以有了kong这种封装级别更高的网关。

对于spring cloud来说,zuul系列推荐使用zuul2,zuul1是多线程阻塞的,有硬伤。spring-cloud-gateway是spring cloud亲生的,Spring Cloud 大力支持,基于 Spring5.0 的新特性 WebFlux 进行开发。底层网络通信框架采用的是 Netty,吞吐量高。

相关文档:

「整体」这次要是讲不明白Spring Cloud核心组件,那我就白编这故事了

「整体」微服务不是全部,只是特定领域的子集

「SCG」万字Spring Cloud Gateway2.0,面向未来的技术,了解一下?

「Trace」2w字长文,让你瞬间拥有「调用链」开发经验

「熔断」轻拢慢捻,微服务熔断大总管

七、分布式工具


大家都知道分布式系统zookeeper能用在很多场景,与其类似的还有基于raft协议的etcd和consul。

由于它们能够保证极高的一致性,所以用作协调工具是再好不过了。用途集中在:配置中心、分布式锁、命名服务、分布式协调、master选举等场所。

对于分布式事务方面,则有阿里的fescar工具进行支持。但如非特别的必要,还是使用柔性事务,追寻最终一致性,比较好。

八、监控系统

  • √ 推荐:prometheus + grafana + telegraf
  • 日志收集:大量ELKB,小量loki

监控系统组件种类繁多,目前,最流行的大概就是上面四类。

zabbix在主机数量不多的情况下,是非常好的选择。

prometheus来势凶猛,大有一统天下的架势。它也可以使用更加漂亮的grafana进行前端展示。

influxdata的influxdb和telegraf组件,都比较好用,主要是功能很全。

使用es存储的elkb工具链,也是一个较好的选择。我所知道的很多公司,都在用。

相关文档:

「整体」这么多监控组件,总有一款适合你

「日志」elkb实践经验,再赠送一套复杂的配置文件

「日志」日志收集的“DNA”

「日志」实践一把Loki,体验掌上起舞的轻盈

「日志」你的野花,朕的kibana

「日志」一般人不敢动系列之—基于logback的日志“规范”和“脱敏”

「监控」 昔日教人类用火的prometheus,如今在努力报警

「APM」 2w字长文,让你瞬间拥有「调用链」开发经验

「APM」 这一轮,skywalking胜出

「底层」 冷门instrument包,功能d炸天

「底层」你的也是我的。3例ko多线程,局部变量透传

九、调度

  • √ 推荐:xxl-job


大家可能都用过cron表达式。这个表达式,最初就是来自linux的crontab工具。

quartz是java中比较古老的调度方案,分布式调度采用数据库锁的方式,管理界面需要自行开发。

elastic-job-cloud应用比较广泛,但系统运维复杂,学习成本较高。相对来说,xxl-job就更加轻量级一些。中国人开发的系统,后台都比较漂亮。

十、入口工具

  • √ 推荐:lvs

为了统一用户的访问路口,一般会使用一些入口工具进行支持。

其中,haproxy、lvs、keepalived等,使用非常广泛。

服务器一般采用稳定性较好的centos,并配备ansible工具进行支持,那叫一个爽。

十一、OLT(A)P

  • √ 推荐:ES
  • 现在的企业,数据量都非常大,数据仓库是必须的。*

搜索方面,solr和elasticsearch比较流行,它们都是基于lucene的。solr比较成熟,稳定性更好一些,但实时搜索方面不如es。

列式存储方面,基于Hadoop 的hbase,使用最是广泛;基于LSM的leveldb写入性能优越,但目前主要是作为嵌入式引擎使用多一些。

tidb是国产新贵,兼容mysql协议,公司通过培训向外输出dba,未来可期。

时序数据库方面,opentsdb用在超大型监控系统多一些。druid和kudu,在处理多维度数据实时聚合方面,更胜一筹。

cassandra在刚出现时火了一段时间,虽然有facebook弃用的新闻,但生态已经形成,常年霸占数据库引擎前15名。

十二、CI/CD

).jpg)

为了支持持续集成和虚拟化,除了耳熟能详的docker,我们还有其他工具。

jenkins是打包发布的首选,毕竟这么多年了,一直是老大哥。当然,写Idea的那家公司,还出了一个叫TeamCity的工具,操作界面非常流畅。

solor不得不说是一个神器,用了它之后,小伙伴们的代码一片飘红,我都快被吐沫星子给淹没了。

对于公司内部来说,一般使用gitlab搭建git服务器。其实,它里面的gitlab CI,也是非常好用的。

Harbor,在 docker registry 基础上扩展了权限控制,审计,镜像同步,管理界面等治理 能力,推荐使用。

调度方面,k8sGoogle 开源,社区的强力推动,有大量的落地方案。Rancher对k8s进行了功能的拓展,实现了和k8s集群交互的一些便捷工具,包括执行命令行,管理多个 k8s集群,查看k8s集群节点的运行状态等,推荐集成。

相关文章:

「持续集成」发布系统有那么难么?

「流程」技术评审,你拿什么来吐槽?

「流程」研发里那只看不见的手,勒的很疼

「规范」外来规范水土不服?手把手教你怎么扩展阿里规范idea插件

「工具」有了MinIO,你还会用FastDFS么?

十三、问题排查


java经常发生内存溢出问题。使用jmap导出堆栈后,我一般使用mat进行深入分析。

如果在线上实时分析,有arthas和perf两款工具。

当然,有大批量的linux工具进行支持。

相关文章:

《Linux上,最常用的一批命令解析(10年精选)》

最常用的一套“Vim“技巧

最常用的一套“Sed“技巧

最常用的一套“AWK“技巧

十四、本地工具


本地使用的jar包和工具,那就多了去了。下面仅仅提一下最最常用的几个。

数据库连接池方面,国内使用druid最多。目前,有号称速度最快的hikari数据库连接池,以及老掉牙的dbcp和c3p0。

json方面,国内使用fastjson最多,三天两头冒出个漏洞;国外则使用jackson多一些。它们的api都类似,jackson特性多一些,但fastjson更加容易使用。鉴于fastjson频繁出现安全问题,现在已经掀起了一股去fastjson的浪潮。

工具包方面,虽然有各种commons包,guava首选。

End

今天是2020年9月08日。

这种文章,每一年我都会整理一次。有些新面孔,也有些被我个人t出局。架构选型,除了你本身对某项技术比较熟悉,用起来更放心。更多的是需要进行大量调研、对比,直到掌握。

技术日新月异,新瓶装旧酒,名词一箩筐,程序员很辛苦。唯有那背后的基础原理,大道至简的思想,经久不衰。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。

本文转载自: 掘金

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

《dubbo系列》-rpc、dubbo基础知识 前言 什么是

发表于 2020-09-09

点赞再看,养成习惯,微信搜一搜【三太子敖丙】关注这个喜欢写情怀的程序员。

本文 GitHub github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

前言

接下来一段时间敖丙将带大家开启紧张刺激的 Dubbo 之旅!是的要开始写 Dubbo 系列的文章了,之前我已经写过一篇架构演进的文章,也说明了微服务的普及化以及重要性,服务化场景下随之而来的就是服务之间的通信问题,那服务间的通信脑海中想到的就是 RPC,说到 RPC 就离不开咱们的 Dubbo。

这篇文章敖丙先带着大家来总览全局,一般而言熟悉一个框架你要先知道这玩意是做什么的,能解决什么痛点,核心的模块是什么,大致运转流程是怎样的。

你要一来就扎入细节之中无法自拔,一波 DFS 直接被劝退的可能性高达99.99%,所以本暖男敖丙将带大家先过一遍 Dubbo 的简介、总体分层、核心组件以及大致调用流程。

不仅如此我还会带着大家过一遍如果要让你设计一个 RPC 框架你看看都需要什么功能?这波操作之后你会发现嘿嘿 Dubbo 怎么设计的和我想的一样呢?真是英雄所见略同啊!

而且我还会写一个简单版 RPC 框架实现,让大家明白 RPC 到底是如何工作的。

如果看了这篇文章你要还是不知道 Dubbo 是啥,我可以要劝退了。

我们先来谈一谈什么叫 RPC ,我发现有很多同学不太了解这个概念,还有人把 RPC 和 HTTP 来进行对比。所以咱们先来说说什么是 RPC。

什么是 RPC

RPC,Remote Procedure Call 即远程过程调用,远程过程调用其实对标的是本地过程调用,本地过程调用你熟悉吧?

想想那青葱岁月,你在大学赶着期末大作业,正在攻克图书管理系统,你奋笔疾书疯狂地敲击键盘,实现了图书借阅、图书归还等等模块,你实现的一个个方法之间的调用就叫本地过程调用。

你要是和我说你实现图书馆里系统已经用了服务化,搞了远程调用了,我只能和你说你有点东西。

简单的说本机上内部的方法调用都可以称为本地过程调用,而远程过程调用实际上就指的是你本地调用了远程机子上的某个方法,这就是远程过程调用。

所以说 RPC 对标的是本地过程调用,至于 RPC 要如何调用远程的方法可以走 HTTP、也可以是基于 TCP 自定义协议。

所以说你讨论 RPC 和 HTTP 就不是一个层级的东西。

而 RPC 框架就是要实现像那小助手一样的东西,目的就是让我们使用远程调用像本地调用一样简单方便,并且解决一些远程调用会发生的一些问题,使用户用的无感知、舒心、放心、顺心,它好我也好,快乐没烦恼。

如何设计一个 RPC 框架

在明确了什么是 RPC,以及 RPC 框架的目的之后,咱们想想如果让你做一款 RPC 框架你该如何设计?

服务消费者

我们先从消费者方(也就是调用方)来看需要些什么,首先消费者面向接口编程,所以需要得知有哪些接口可以调用,可以通过公用 jar 包的方式来维护接口。

现在知道有哪些接口可以调用了,但是只有接口啊,具体的实现怎么来?这事必须框架给处理了!所以还需要来个代理类,让消费者只管调,啥事都别管了,我代理帮你搞定。

对了,还需要告诉代理,你调用的是哪个方法,并且参数的值是什么。

虽说代理帮你搞定但是代理也需要知道它到底要调哪个机子上的远程方法,所以需要有个注册中心,这样调用方从注册中心可以知晓可以调用哪些服务提供方,一般而言提供方不止一个,毕竟只有一个挂了那不就没了。

所以提供方一般都是集群部署,那调用方需要通过负载均衡来选择一个调用,可以通过某些策略例如同机房优先调用啊啥的。

当然还需要有容错机制,毕竟这是远程调用,网络是不可靠的,所以可能需要重试什么的。

还要和服务提供方约定一个协议,例如我们就用 HTTP 来通信就好啦,也就是大家要讲一样的话,不然可能听不懂了。

当然序列化必不可少,毕竟我们本地的结构是“立体”的,需要序列化之后才能传输,因此还需要约定序列化格式。

并且这过程中间可能还需要掺入一些 Filter,来作一波统一的处理,例如调用计数啊等等。

这些都是框架需要做的,让消费者像在调用本地方法一样,无感知。

服务提供者

服务提供者肯定要实现对应的接口这是毋庸置疑的。

然后需要把自己的接口暴露出去,向注册中心注册自己,暴露自己所能提供的服务。

然后有消费者请求过来需要处理,提供者需要用和消费者协商好的协议来处理这个请求,然后做反序列化。

序列化完的请求应该扔到线程池里面做处理,某个线程接受到这个请求之后找到对应的实现调用,然后再将结果原路返回。

注册中心

上面其实我们都提到了注册中心,这东西就相当于一个平台,大家在上面暴露自己的服务,也在上面得知自己能调用哪些服务。

当然还能做配置中心,将配置集中化处理,动态变更通知订阅者。

监控运维

面对众多的服务,精细化的监控和方便的运维必不可少。

这点很多开发者在开发的时候察觉不到,到你真正上线开始运行维护的时候,如果没有良好的监控措施,快速的运维手段,到时候就是睁眼瞎!手足无措,等着挨批把!

那种痛苦不要问我为什么知道,我就是知道!

小结一下

让我们小结一下,大致上一个 RPC 框架需要做的就是约定要通信协议,序列化的格式、一些容错机制、负载均衡策略、监控运维和一个注册中心!

简单实现一个 RPC 框架

没错就是简单的实现,上面我们在思考如何设计一个 RPC 框架的时候想了很多,那算是生产环境使用级别的功能需求了,我们这是 Demo,目的是突出 RPC框架重点功能 - 实现远程调用。

所以啥七七八八的都没,并且我用伪代码来展示,其实也就是删除了一些保护性和约束性的代码,因为看起来太多了不太直观,需要一堆 try-catch 啥的,因此我删减了一些,直击重点。

Let’s Do It!

首先我们定义一个接口和一个简单实现。

1
2
3
4
5
6
7
8
9
java复制代码public interface AobingService {  
String hello(String name);
}

public class AobingServiceImpl implements AobingService {
public String hello(String name) {
return "Yo man Hello,I am" + name;
}
}

然后我们再来实现服务提供者暴露服务的功能。

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
java复制代码public class AobingRpcFramework { 
public static void export(Object service, int port) throws Exception {
ServerSocket server = new ServerSocket(port);
while(true) {
Socket socket = server.accept();
new Thread(new Runnable() {
//反序列化
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
String methodName = input.read(); //读取方法名
Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); //参数类型
Object[] arguments = (Object[]) input.readObject(); //参数
Method method = service.getClass().getMethod(methodName, parameterTypes); //找到方法
Object result = method.invoke(service, arguments); //调用方法
// 返回结果
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);
}).start();
}
}
public static <T> T refer (Class<T> interfaceClass, String host, int port) throws Exception {
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
Socket socket = new Socket(host, port); //指定 provider 的 ip 和端口
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.write(method.getName()); //传方法名
output.writeObject(method.getParameterTypes()); //传参数类型
output.writeObject(arguments); //传参数值
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
Object result = input.readObject(); //读取结果
return result;
}
});
}
}

好了,这个 RPC 框架就这样好了,是不是很简单?就是调用者传递了方法名、参数类型和参数值,提供者接收到这样参数之后调用对于的方法返回结果就好了!这就是远程过程调用。

我们来看看如何使用

1
2
3
4
5
6
7
java复制代码        //服务提供者只需要暴露出接口
AobingService service = new AobingServiceImpl ();
AobingRpcFramework.export(service, 2333);

//服务调用者只需要设置依赖
AobingService service = AobingRpcFramework.refer(AobingService.class, "127.0.0.1", 2333);
service.hello();

看起来好像好不错哟,不过这很是简陋,用作 demo 有助理解还是极好的!

接下来就来看看 Dubbo 吧!上正菜!

Dubbo 简介

Dubbo 是阿里巴巴 2011年开源的一个基于 Java 的 RPC 框架,中间沉寂了一段时间,不过其他一些企业还在用 Dubbo 并自己做了扩展,比如当当网的 Dubbox,还有网易考拉的 Dubbok。

但是在 2017 年阿里巴巴又重启了对 Dubbo 维护。在 2017 年荣获了开源中国 2017 最受欢迎的中国开源软件 Top 3。

在 2018 年和 Dubbox 进行了合并,并且进入 Apache 孵化器,在 2019 年毕业正式成为 Apache 顶级项目。

目前 Dubbo 社区主力维护的是 2.6.x 和 2.7.x 两大版本,2.6.x 版本主要是 bug 修复和少量功能增强为准,是稳定版本。

而 2.7.x 是主要开发版本,更新和新增新的 feature 和优化,并且 2.7.5 版本的发布被 Dubbo 认为是里程碑式的版本发布,之后我们再做分析。

它实现了面向接口的代理 RPC 调用,并且可以配合 ZooKeeper 等组件实现服务注册和发现功能,并且拥有负载均衡、容错机制等。

Dubbo 总体架构

我们先来看下官网的一张图。

本丙再暖心的给上图内每个节点的角色说明一下。

节点 角色说明
Consumer 需要调用远程服务的服务消费方
Registry 注册中心
Provider 服务提供方
Container 服务运行的容器
Monitor 监控中心

我再来大致说一下整体的流程,首先服务提供者 Provider 启动然后向注册中心注册自己所能提供的服务。

服务消费者 Consumer 启动向注册中心订阅自己所需的服务。然后注册中心将提供者元信息通知给 Consumer, 之后 Consumer 因为已经从注册中心获取提供者的地址,因此可以通过负载均衡选择一个 Provider 直接调用 。

之后服务提供方元数据变更的话注册中心会把变更推送给服务消费者。

服务提供者和消费者都会在内存中记录着调用的次数和时间,然后定时的发送统计数据到监控中心。

一些注意点

首先注册中心和监控中心是可选的,你可以不要监控,也不要注册中心,直接在配置文件里面写然后提供方和消费方直连。

然后注册中心、提供方和消费方之间都是长连接,和监控方不是长连接,并且消费方是直接调用提供方,不经过注册中心。

就算注册中心和监控中心宕机了也不会影响到已经正常运行的提供者和消费者,因为消费者有本地缓存提供者的信息。

Dubbo 分层架构

总的而言 Dubbo 分为三层,如果每一层再细分下去,一共有十层。别怕也就十层,本丙带大家过一遍,大家先有个大致的印象,之后的文章丙会带着大家再深入。

大的三层分别为 Business(业务层)、RPC 层、Remoting,并且还分为 API 层和 SPI 层。

分为大三层其实就是和我们知道的网络分层一样的意思,只有层次分明,职责边界清晰才能更好的扩展。

而分 API 层和 SPI 层这是 Dubbo 成功的一点,采用微内核设计+SPI扩展,使得有特殊需求的接入方可以自定义扩展,做定制的二次开发。

接下来咱们再来看看每一层都是干嘛的。

  • Service,业务层,就是咱们开发的业务逻辑层。
  • Config,配置层,主要围绕 ServiceConfig 和 ReferenceConfig,初始化配置信息。
  • Proxy,代理层,服务提供者还是消费者都会生成一个代理类,使得服务接口透明化,代理层做远程调用和返回结果。
  • Register,注册层,封装了服务注册和发现。
  • Cluster,路由和集群容错层,负责选取具体调用的节点,处理特殊的调用要求和负责远程调用失败的容错措施。
  • Monitor,监控层,负责监控统计调用时间和次数。
  • Portocol,远程调用层,主要是封装 RPC 调用,主要负责管理 Invoker,Invoker代表一个抽象封装了的执行体,之后再做详解。
  • Exchange,信息交换层,用来封装请求响应模型,同步转异步。
  • Transport,网络传输层,抽象了网络传输的统一接口,这样用户想用 Netty 就用 Netty,想用 Mina 就用 Mina。
  • Serialize,序列化层,将数据序列化成二进制流,当然也做反序列化。

SPI

我再稍微提一下 SPI(Service Provider Interface),是 JDK 内置的一个服务发现机制,它使得接口和具体实现完全解耦。我们只声明接口,具体的实现类在配置中选择。

具体的就是你定义了一个接口,然后在META-INF/services目录下放置一个与接口同名的文本文件,文件的内容为接口的实现类,多个实现类用换行符分隔。

这样就通过配置来决定具体用哪个实现!

而 Dubbo SPI 还做了一些改进,篇幅有限留在之后再谈。

Dubbo 调用过程

上面我已经介绍了每个层到底是干嘛的,我们现在再来串起来走一遍调用的过程,加深你对 Dubbo 的理解,让知识点串起来,由点及面来一波连连看。

我们先从服务提供者开始,看看它是如何工作的。

服务暴露过程

首先 Provider 启动,通过 Proxy 组件根据具体的协议
Protocol 将需要暴露出去的接口封装成 Invoker,Invoker 是 Dubbo 一个很核心的组件,代表一个可执行体。

然后再通过 Exporter 包装一下,这是为了在注册中心暴露自己套的一层,然后将 Exporter 通过 Registry 注册到注册中心。 这就是整体服务暴露过程。

消费过程

接着我们来看消费者调用流程(把服务者暴露的过程也在图里展示出来了,这个图其实算一个挺完整的流程图了)。

首先消费者启动会向注册中心拉取服务提供者的元信息,然后调用流程也是从 Proxy 开始,毕竟都需要代理才能无感知。

Proxy 持有一个 Invoker 对象,调用 invoke 之后需要通过 Cluster 先从 Directory 获取所有可调用的远程服务的 Invoker 列表,如果配置了某些路由规则,比如某个接口只能调用某个节点的那就再过滤一遍 Invoker 列表。

剩下的 Invoker 再通过 LoadBalance 做负载均衡选取一个。然后再经过 Filter 做一些统计什么的,再通过 Client 做数据传输,比如用 Netty 来传输。

传输需要经过 Codec 接口做协议构造,再序列化。最终发往对应的服务提供者。

服务提供者接收到之后也会进行 Codec 协议处理,然后反序列化后将请求扔到线程池处理。某个线程会根据请求找到对应的 Exporter ,而找到 Exporter 其实就是找到了 Invoker,但是还会有一层层 Filter,经过一层层过滤链之后最终调用实现类然后原路返回结果。

完成整个调用过程!

总结

这次敖丙带着大家先了解了下什么是 RPC,然后规划了一波 RPC 框架需要哪些组件,然后再用代码实现了一个简单的 RPC 框架。

然后带着大家了解了下 Dubbo 的发展历史、总体架构、分层设计架构以及每个组件是干嘛的,再带着大伙走了一遍整体调用过程。

我真的是太暖了啊!

dubbo近期我会安排几个章节继续展开,最后会出一个面试版本的dubbo,我们拭目以待吧。

我是敖丙,你知道的越多,你不知道的越多,我们下期见!

人才们的 【三连】 就是敖丙创作的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言!


文章持续更新,可以微信搜一搜「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

本文转载自: 掘金

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

未读消息(小红点),前端与 RabbitMQ实时消息推送实践

发表于 2020-09-08

前几天粉丝群里有个小伙伴问过:web 页面的未读消息(小红点)怎么实现比较简单,刚好本周手头有类似的开发任务,索性就整理出来供小伙伴们参考,没准哪天就能用得上呢。

之前在 《springboot + rabbitmq 做智能家居》 中说过可以用 rabbitmq 的 MQTT 协议做智能家居的指令推送,里边还提到过能用 MQTT 协议做 web 的消息推送,而未读消息(小红点)功能刚好应用到实时消息推送了。

MQTT 协议就不再赘述了,没接触过的同学翻翻前边的文章温习一下吧,今天还是主要以实践为主!

web 端实时消息推送,常用的实现方式比较多,但万变不离其宗,底层基本上还是依赖于 websocket,MQTT 协议也不例外。

RabbitMQ 搭建

RabbitMQ 的基础搭建就不详细说了,自行百度一步一步搞问题不大,这里主要说一下两个比较重要的配置。

1、开启 mqtt 协议

默认情况下RabbitMQ 是不开启MQTT 协议的,所以需要我们手动的开启相关的插件,而RabbitMQ 的MQTT 协议分为两种。

第一种 rabbitmq_mqtt 提供与后端服务交互使用,对应端口1883。

1
javascript复制代码rabbitmq-plugins enable rabbitmq_mqtt

第二种 rabbitmq_web_mqtt 提供与前端交互使用,对应端口15675。

1
javascript复制代码rabbitmq-plugins enable rabbitmq_web_mqtt

在 RabbitMQ 管理后台看到如下的显示,就表示MQTT 协议开启成功,到这中间件环境就搭建完毕了。

协议对应端口号

使用MQTT 协议默认的交换机 Exchange 为 amp.topic,而我们订阅的主题会在 Queues 注册一个客户端队列,路由 Routing key 就是我们设置的主题。

交换机信息

服务端消息发送

web 端实时消息推送一般都是单向的推送,前端接收服务端推送的消息显示即可,所以就只实现消息发送即可。

1、mqtt 客户端依赖包

引入 spring-integration-mqtt、org.eclipse.paho.client.mqttv3 两个工具包实现

1
2
3
4
5
6
7
8
9
10
javascript复制代码<!--mqtt依赖包-->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>

2、消息发送者

消息的发送比较简单,主要是应用到 @ServiceActivator 注解,需要注意messageHandler.setAsync属性,如果设置成 false,关闭异步模式发送消息时可能会阻塞。

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
javascript复制代码@Configuration
public class IotMqttProducerConfig {

@Autowired
private MqttConfig mqttConfig;

@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setServerURIs(mqttConfig.getServers());
return factory;
}

@Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}

@Bean
@ServiceActivator(inputChannel = "iotMqttInputChannel")
public MessageHandler mqttOutbound() {
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(mqttConfig.getServerClientId(), mqttClientFactory());
messageHandler.setAsync(false);
messageHandler.setDefaultTopic(mqttConfig.getDefaultTopic());
return messageHandler;
}
}

MQTT 对外提供发送消息的 API 时,需要使用 @MessagingGateway 注解,去提供一个消息网关代理,参数 defaultRequestChannel 指定发送消息绑定的channel。

可以实现三种API接口,payload 为发送的消息,topic 发送消息的主题,qos 消息质量。

1
2
3
4
5
6
7
8
9
10
javascript复制代码@MessagingGateway(defaultRequestChannel = "iotMqttInputChannel")
public interface IotMqttGateway {

// 向默认的 topic 发送消息
void sendMessage2Mqtt(String payload);
// 向指定的 topic 发送消息
void sendMessage2Mqtt(String payload,@Header(MqttHeaders.TOPIC) String topic);
// 向指定的 topic 发送消息,并指定服务质量参数
void sendMessage2Mqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
}

前端消息订阅

前端使用与服务端对应的工具 paho-mqtt mqttws31.js实现,实现方式与传统的 websocket 方式差不多,核心方法 client = new Paho.MQTT.Client 和 各种监听事件,代码比较简洁。

注意:要保证前后端 clientId的全局唯一性,我这里就简单用随机数解决了

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
javascript复制代码<script type="text/javascript">
// mqtt协议rabbitmq服务
var brokerIp = location.hostname;
// mqtt协议端口号
var port = 15675;
// 接受推送消息的主题
var topic = "push_message_topic";

// mqtt连接
client = new Paho.MQTT.Client(brokerIp, port, "/ws", "clientId_" + parseInt(Math.random() * 100, 10));

var options = {
timeout: 3, //超时时间
keepAliveInterval: 30,//心跳时间
onSuccess: function () {
console.log(("连接成功~"));
client.subscribe(topic, {qos: 1});
},
onFailure: function (message) {
console.log(("连接失败~" + message.errorMessage));
}
};
// 考虑到https的情况
if (location.protocol == "https:") {
options.useSSL = true;
}
client.connect(options);
console.log(("已经连接到" + brokerIp + ":" + port));

// 连接断开事件
client.onConnectionLost = function (responseObject) {
console.log("失去连接 - " + responseObject.errorMessage);
};

// 接收消息事件
client.onMessageArrived = function (message) {
console.log("接受主题: " + message.destinationName + "的消息: " + message.payloadString);
$("#arrivedDiv").append("<br/>"+message.payloadString);
var count = $("#count").text();
count = Number(count) + 1;
$("#count").text(count);
};

// 推送给指定主题
function sendMessage() {
var a = $("#message").val();
if (client.isConnected()) {
var message = new Paho.MQTT.Message(a);
message.destinationName = topic;
client.send(message);
}
}
</script>

测试

前后端的代码并不多,接下来我们测试一下,弄了个页面看看效果。

首先用 postman 模拟后端发送消息

1
javascript复制代码http://127.0.0.1:8080/fun/sendMessage?message=我是程序员内点事&topic=push_message_topic

模拟发送消息

再看一下前端订阅消息的效果,看到消息被实时推送到了前端,这里只做了未读消息数量统计,一般还会做未读消息详情列表。

实时消息推送动图

总结

未读消息是一个十分常见的功能,不管是 web端还是移动端系统都是必备的模块,MQTT 协议只是其中的一种实现方式,还是有必要掌握一种方法。具体用什么工具实现还是要看具体的业务场景和学习成本,像我用RabbitMQ 做还考虑到一些运维成本在里边。

本文完整代码地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-mqtt-messagepush


原创不易,燃烧秀发输出内容,如果有一丢丢收获,点个赞鼓励一下吧!

整理了几百本各类技术电子书,送给小伙伴们。关注公号回复【666】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就加入我们吧!

本文转载自: 掘金

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

java 整合FastDFS文件服务

发表于 2020-09-08

java整合fdfs文件服务器

1. 服务器fastdfs搭建

参考地址,亲测有效,Centos7.6版本

2. java代码部分

2.1 引入相关依赖包

1
2
3
4
5
复制代码    <dependency>
      <groupId>com.github.tobato</groupId>
      <artifactId>fastdfs-client</artifactId>
      <version>1.26.5</version>
    </dependency>

2.2 FDFS 配置类

1
2
3
4
5
复制代码@Configuration
@Import(FdfsClientConfig.class) // 导入FastDFS-Client组件
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) // 解决jmx重复注册bean的问题
public class FdfsConfiguration {
}

2.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
复制代码# 分布式文件系统fastdfs配置
fdfs:
 # socket连接超时时长
 soTimeout: 1500
 # 连接tracker服务器超时时长
 connectTimeout: 600
 # nginx 访问的地址和端口
 reqHost: 114.55.164.189
 reqPort: 80
 pool:
   # 从池中借出的对象的最大数目
   max-total: 153
   # 获取连接时的最大等待毫秒数100
   max-wait-millis: 102
 # 缩略图生成参数,可选
 thumbImage:
   width: 150
   height: 150
 # 跟踪服务器tracker_server请求地址,支持多个,这里只有一个,如果有多个在下方加- x.x.x.x:port
 trackerList:
   - 114.55.164.189:22122
 #
 # 存储服务器storage_server访问地址
 web-server-url: http://114.55.164.189:80/

2.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
复制代码@Data
@ConfigurationProperties(prefix = "fdfs",ignoreInvalidFields = true)
@Slf4j
public class FastdfsProperties implements InitializingBean {


    private String reqHost;

    private String reqPort;

    private String webServerUrl;

    private String soTimeout;

    @Override
    public void afterPropertiesSet() throws Exception {
        log.debug("fdfs.reqHost is {},fdfs.reqPort is {},fdfs.webServerUrl is {}",getReqHost(),getReqPort(),getWebServerUrl());
        if (StringUtils.isBlank(getReqHost())) {
            throw new IllegalStateException("Property \"fdfs.reqHost\" cannot  be blank");
        }
        if (StringUtils.isBlank(getReqPort())) {
            throw new IllegalStateException("Property \"fdfs.reqPort\" cannot  be blank");
        }
        if (StringUtils.isBlank(getWebServerUrl())) {
            throw new IllegalStateException("Property \"fdfs.webServerUrl\" cannot  be blank");
        }
        if (StringUtils.isBlank(getSoTimeout())) {
            throw new IllegalStateException("Property \"fdfs.soTimeout\" cannot  be blank");
        }

    }
}

「注意启动类需要添加自动加载配置类注解」
@EnableConfigurationProperties({ApplicationProperties.class,FastdfsProperties.class})

2.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
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
复制代码@Component
public class FastDFSClient {

    private  Logger log = LoggerFactory.getLogger(FastDFSClient.class);

    @Autowired
    FastdfsProperties fastdfsProperties;


    @Autowired
    private  FastFileStorageClient storageClient;

    @Autowired
    private  ThumbImageConfig imageConfig;   //创建缩略图的

    @Autowired
    private  TrackerClient trackerClient;


    /**
     * 简单文件上传
     * @param file
     * @return
     */
    public  String upload (MultipartFile file){
        String fileName  = file.getOriginalFilename();
        try {
            InputStream inputStream= file.getInputStream();

            long size = file.getSize();

            StorePath path = storageClient.uploadFile(inputStream,size,fileName.substring(fileName.lastIndexOf(".")+ 1), null);

            return getResAccessUrl(path);
        } catch (IOException e) {
            e.printStackTrace();
            log.error(e.toString());
            return null;
        }
    }

    /**
     * 删除指定文件
     * @param url
     */
    public  void delFile (String url){
        storageClient.deleteFile(url);
    }


    /**
     * 文件下载
     * @param fileUrl
     * @return
     * @throws IOException
     */
    public byte[] downloadFile(String fileUrl) throws IOException {
        fileUrl = fileUrl.replaceAll(fastdfsProperties.getWebServerUrl(),"");
        String group = fileUrl.substring(0, fileUrl.indexOf("/"));
        String path = fileUrl.substring(fileUrl.indexOf("/") + 1);
        DownloadByteArray downloadByteArray = new DownloadByteArray();
        byte[] bytes = storageClient.downloadFile(group,path, downloadByteArray);
        return bytes;
    }
    /**
     * 获取文件路径
     * @param path
     * @return
     */
    private  String getResAccessUrl(StorePath path) {
        String fileUrl = "http://" +  fastdfsProperties.getReqHost() + ":" +  fastdfsProperties.getReqPort() +  "/" + path.getFullPath();
        return fileUrl;
    }
}

2.6 测试

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
复制代码@RestController
@RequestMapping("/file")
@Api(tags = "文件管理")
public class UploadController {

    @Autowired
    private FastDFSClient fastDFSClient;


    @ApiOperation(value = "文件上传")
    @RequestMapping(value = "/upload" ,method = RequestMethod.POST)
    public Result upload(
            @RequestParam("file")  MultipartFile file
    ){

        if(file == null || file.isEmpty()){
            return new Result(RetCode.PARAM_ERROR.getCode(),RetCode.PARAM_ERROR.getMsg(),null);
        }
        String path = fastDFSClient.upload(file);
        return new Result(RetCode.SUCCESS.getCode(),RetCode.SUCCESS.getMsg(),path);

    }

    @ApiOperation(value = "删除文件")
    @RequestMapping(value = "/del" ,method = RequestMethod.GET)
    @ApiImplicitParams({
            @ApiImplicitParam(value = "文件全路径" ,name = "url" ,required = true,dataType = "String",paramType = "query")
    })
    public Result delFile(String url){
        if(url == null || url.equals("")){
            return new Result(RetCode.PARAM_ERROR.getCode(),RetCode.PARAM_ERROR.getMsg(),null);
        }
        try {
            fastDFSClient.delFile(url);
            return new Result(RetCode.SUCCESS.getCode(),RetCode.SUCCESS.getMsg(),null);
        } catch (Exception e){
            return new Result(RetCode.PARAM_VALUE_ERROR.getCode(),RetCode.PARAM_VALUE_ERROR.getMsg(),null);
        }


    }

    @ApiOperation(value = "文件下载")
    @GetMapping("/downLoad")
    public ResponseEntity<byte []> downLoadFile(
            @ApiParam(value = "文件路径",required = true) @RequestParam(value = "url" ,required = true) String url
    ) {
        try {
            byte[] bytes = fastDFSClient.downloadFile(url);

            String fileName =url.substring(url.lastIndexOf("/" +1));
            fileName = new String(fileName.getBytes("utf-8"),"ISO_8859_1");

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            httpHeaders.setContentDispositionFormData("attachment", fileName);

            return new ResponseEntity<byte[]>(bytes,httpHeaders,HttpStatus.OK);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

}



2.6 文件下载(返回的是字节码数组,可以直接在前端下载文件)

前端文件下载参考

本文转载自: 掘金

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

不要再重复造轮子了,这款开源工具类库贼好使!

发表于 2020-09-08

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

Hutool是一个小而全的Java工具类库,它帮助我们简化每一行代码,避免重复造轮子。如果你有需要用到某些工具类的时候,不妨在Hutool里面找找。本文总结了平时常用的16个工具类,希望对大家有所帮助!

安装

Hutool的安装非常简单,Maven项目中只需在pom.xml添加以下依赖即可。

1
2
3
4
5
xml复制代码<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.0</version>
</dependency>

常用工具类

使用一个工具方法代替一段复杂代码,避免复制粘贴代码,可以极大的提高我们的开发效率,下面介绍下我常用的工具方法!

Convert

类型转换工具类,用于各种类型数据的转换。平时我们转换类型经常会面临类型转换失败的问题,要写try catch代码,有了它,就不用写了!

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//转换为字符串
int a = 1;
String aStr = Convert.toStr(a);
//转换为指定类型数组
String[] b = {"1", "2", "3", "4"};
Integer[] bArr = Convert.toIntArray(b);
//转换为日期对象
String dateStr = "2017-05-06";
Date date = Convert.toDate(dateStr);
//转换为列表
String[] strArr = {"a", "b", "c", "d"};
List<String> strList = Convert.toList(String.class, strArr);

DateUtil

日期时间工具类,定义了一些常用的日期时间操作方法。JDK自带的Date和Calendar对象真心不好用,有了它操作日期时间就简单多了!

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
java复制代码//Date、long、Calendar之间的相互转换
//当前时间
Date date = DateUtil.date();
//Calendar转Date
date = DateUtil.date(Calendar.getInstance());
//时间戳转Date
date = DateUtil.date(System.currentTimeMillis());
//自动识别格式转换
String dateStr = "2017-03-01";
date = DateUtil.parse(dateStr);
//自定义格式化转换
date = DateUtil.parse(dateStr, "yyyy-MM-dd");
//格式化输出日期
String format = DateUtil.format(date, "yyyy-MM-dd");
//获得年的部分
int year = DateUtil.year(date);
//获得月份,从0开始计数
int month = DateUtil.month(date);
//获取某天的开始、结束时间
Date beginOfDay = DateUtil.beginOfDay(date);
Date endOfDay = DateUtil.endOfDay(date);
//计算偏移后的日期时间
Date newDate = DateUtil.offset(date, DateField.DAY_OF_MONTH, 2);
//计算日期时间之间的偏移量
long betweenDay = DateUtil.between(date, newDate, DateUnit.DAY);

JSONUtil

JSON解析工具类,可用于对象与JSON之间的互相转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码PmsBrand brand = new PmsBrand();
brand.setId(1L);
brand.setName("小米");
brand.setShowStatus(1);
//对象转化为JSON字符串
String jsonStr = JSONUtil.parse(brand).toString();
LOGGER.info("jsonUtil parse:{}", jsonStr);
//JSON字符串转化为对象
PmsBrand brandBean = JSONUtil.toBean(jsonStr, PmsBrand.class);
LOGGER.info("jsonUtil toBean:{}", brandBean);
List<PmsBrand> brandList = new ArrayList<>();
brandList.add(brand);
String jsonListStr = JSONUtil.parse(brandList).toString();
//JSON字符串转化为列表
brandList = JSONUtil.toList(new JSONArray(jsonListStr), PmsBrand.class);
LOGGER.info("jsonUtil toList:{}", brandList);

StrUtil

字符串工具类,定义了一些常用的字符串操作方法。StrUtil比StringUtil名称更短,用起来也更方便!

1
2
3
4
5
6
7
8
9
10
11
java复制代码//判断是否为空字符串
String str = "test";
StrUtil.isEmpty(str);
StrUtil.isNotEmpty(str);
//去除字符串的前后缀
StrUtil.removeSuffix("a.jpg", ".jpg");
StrUtil.removePrefix("a.jpg", "a.");
//格式化字符串
String template = "这只是个占位符:{}";
String str2 = StrUtil.format(template, "我是占位符");
LOGGER.info("/strUtil format:{}", str2);

ClassPathResource

ClassPath单一资源访问类,可以获取classPath下的文件,在Tomcat等容器下,classPath一般是WEB-INF/classes。

1
2
3
4
5
java复制代码//获取定义在src/main/resources文件夹中的配置文件
ClassPathResource resource = new ClassPathResource("generator.properties");
Properties properties = new Properties();
properties.load(resource.getStream());
LOGGER.info("/classPath:{}", properties);

ReflectUtil

Java反射工具类,可用于反射获取类的方法及创建对象。

1
2
3
4
5
6
7
8
java复制代码//获取某个类的所有方法
Method[] methods = ReflectUtil.getMethods(PmsBrand.class);
//获取某个类的指定方法
Method method = ReflectUtil.getMethod(PmsBrand.class, "getId");
//使用反射来创建对象
PmsBrand pmsBrand = ReflectUtil.newInstance(PmsBrand.class);
//反射执行对象的方法
ReflectUtil.invoke(pmsBrand, "setId", 1);

NumberUtil

数字处理工具类,可用于各种类型数字的加减乘除操作及类型判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码double n1 = 1.234;
double n2 = 1.234;
double result;
//对float、double、BigDecimal做加减乘除操作
result = NumberUtil.add(n1, n2);
result = NumberUtil.sub(n1, n2);
result = NumberUtil.mul(n1, n2);
result = NumberUtil.div(n1, n2);
//保留两位小数
BigDecimal roundNum = NumberUtil.round(n1, 2);
String n3 = "1.234";
//判断是否为数字、整数、浮点数
NumberUtil.isNumber(n3);
NumberUtil.isInteger(n3);
NumberUtil.isDouble(n3);

BeanUtil

JavaBean工具类,可用于Map与JavaBean对象的互相转换以及对象属性的拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码PmsBrand brand = new PmsBrand();
brand.setId(1L);
brand.setName("小米");
brand.setShowStatus(0);
//Bean转Map
Map<String, Object> map = BeanUtil.beanToMap(brand);
LOGGER.info("beanUtil bean to map:{}", map);
//Map转Bean
PmsBrand mapBrand = BeanUtil.mapToBean(map, PmsBrand.class, false);
LOGGER.info("beanUtil map to bean:{}", mapBrand);
//Bean属性拷贝
PmsBrand copyBrand = new PmsBrand();
BeanUtil.copyProperties(brand, copyBrand);
LOGGER.info("beanUtil copy properties:{}", copyBrand);

CollUtil

集合操作的工具类,定义了一些常用的集合操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//数组转换为列表
String[] array = new String[]{"a", "b", "c", "d", "e"};
List<String> list = CollUtil.newArrayList(array);
//join:数组转字符串时添加连接符号
String joinStr = CollUtil.join(list, ",");
LOGGER.info("collUtil join:{}", joinStr);
//将以连接符号分隔的字符串再转换为列表
List<String> splitList = StrUtil.split(joinStr, ',');
LOGGER.info("collUtil split:{}", splitList);
//创建新的Map、Set、List
HashMap<Object, Object> newMap = CollUtil.newHashMap();
HashSet<Object> newHashSet = CollUtil.newHashSet();
ArrayList<Object> newList = CollUtil.newArrayList();
//判断列表是否为空
CollUtil.isEmpty(list);

MapUtil

Map操作工具类,可用于创建Map对象及判断Map是否为空。

1
2
3
4
5
6
7
8
9
java复制代码//将多个键值对加入到Map中
Map<Object, Object> map = MapUtil.of(new String[][]{
{"key1", "value1"},
{"key2", "value2"},
{"key3", "value3"}
});
//判断Map是否为空
MapUtil.isEmpty(map);
MapUtil.isNotEmpty(map);

AnnotationUtil

注解工具类,可用于获取注解与注解中指定的值。

1
2
3
4
5
6
7
8
java复制代码//获取指定类、方法、字段、构造器上的注解列表
Annotation[] annotationList = AnnotationUtil.getAnnotations(HutoolController.class, false);
LOGGER.info("annotationUtil annotations:{}", annotationList);
//获取指定类型注解
Api api = AnnotationUtil.getAnnotation(HutoolController.class, Api.class);
LOGGER.info("annotationUtil api value:{}", api.description());
//获取指定类型注解的值
Object annotationValue = AnnotationUtil.getAnnotationValue(HutoolController.class, RequestMapping.class);

SecureUtil

加密解密工具类,可用于MD5加密。

1
2
3
4
java复制代码//MD5加密
String str = "123456";
String md5Str = SecureUtil.md5(str);
LOGGER.info("secureUtil md5:{}", md5Str);

CaptchaUtil

验证码工具类,可用于生成图形验证码。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//生成验证码图片
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
try {
request.getSession().setAttribute("CAPTCHA_KEY", lineCaptcha.getCode());
response.setContentType("image/png");//告诉浏览器输出内容为图片
response.setHeader("Pragma", "No-cache");//禁止浏览器缓存
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expire", 0);
lineCaptcha.write(response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}

Validator

字段验证器,可以对不同格式的字符串进行验证,比如邮箱、手机号、IP等格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码//判断是否为邮箱地址
boolean result = Validator.isEmail("macro@qq.com");
LOGGER.info("Validator isEmail:{}", result);
//判断是否为手机号码
result = Validator.isMobile("18911111111");
LOGGER.info("Validator isMobile:{}", result);
//判断是否为IPV4地址
result = Validator.isIpv4("192.168.3.101");
LOGGER.info("Validator isIpv4:{}", result);
//判断是否为汉字
result = Validator.isChinese("你好");
LOGGER.info("Validator isChinese:{}", result);
//判断是否为身份证号码(18位中国)
result = Validator.isCitizenId("123456");
LOGGER.info("Validator isCitizenId:{}", result);
//判断是否为URL
result = Validator.isUrl("http://www.baidu.com");
LOGGER.info("Validator isUrl:{}", result);
//判断是否为生日
result = Validator.isBirthday("2020-02-01");
LOGGER.info("Validator isBirthday:{}", result);

DigestUtil

摘要算法工具类,支持MD5、SHA-256、Bcrypt等算法。

1
2
3
4
5
6
7
8
9
10
11
java复制代码String password = "123456";
//计算MD5摘要值,并转为16进制字符串
String result = DigestUtil.md5Hex(password);
LOGGER.info("DigestUtil md5Hex:{}", result);
//计算SHA-256摘要值,并转为16进制字符串
result = DigestUtil.sha256Hex(password);
LOGGER.info("DigestUtil sha256Hex:{}", result);
//生成Bcrypt加密后的密文,并校验
String hashPwd = DigestUtil.bcrypt(password);
boolean check = DigestUtil.bcryptCheck(password,hashPwd);
LOGGER.info("DigestUtil bcryptCheck:{}", check);

HttpUtil

Http请求工具类,可以发起GET/POST等请求。

1
2
java复制代码String response = HttpUtil.get("http://localhost:8080/hutool/covert");
LOGGER.info("HttpUtil get:{}", response);

其他工具类

Hutool中的工具类还有很多,可以参考:www.hutool.cn/

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

1…781782783…956

开发者博客

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