注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Swift Rust Kotlin 三种语言的枚举类型比较

本文比较一下 Swift Rust Kotlin 三种语言中枚举用法的异同。 定义比较 基本定义 Swift: enum LanguageType { case rust case swift case kotlin } Rus...
继续阅读 »

本文比较一下 Swift Rust Kotlin 三种语言中枚举用法的异同。


定义比较


基本定义



  • Swift:


enum LanguageType {
case rust
case swift
case kotlin
}


  • Rust:


enum LanguageType {
Rust,
Swift,
Kotlin,
}


  • Kotlin:


enum class LanguageType {
Rust,
Swift,
Kotlin,
}

三种语言都使用 enum 作为关键字,Swift 枚举的选项叫 case 一般以小写开头,Rust 枚举的选项叫 variant 通常以大写开头,Kotlin 枚举的选项叫 entry 以大写开头。kotlin 还多了一个 class 关键字,表示枚举类,具有类的一些特殊特性。在定义选项时,Swift 需要一个 case 关键字,而另外两种语言只需要逗号隔开即可。


带值的定义


三种语言的枚举在定义选项时都可以附带一些值。



  • 在 Swift 中这些值叫关联值(associated value),每个选项可以使用同一种类型,也可以使用不同类型,可以有一个值,也可以有多个值,值还可以进行命名。这些值在枚举创建时再进行赋值:


enum Language {
case rust(Int)
case swift(Int)
case kotlin(Int)
}

enum Language {
case rust(Int, String)
case swift(Int, Int, String)
case kotlin(Int)
}

enum Language {
case rust(year: Int, name: String)
case swift(year: Int, version: Int, name: String)
case kotlin(year: Int)
}

let language = Language.rust(2015)


  • 在 Rust 中这些值也叫关联值(associated value),和 Swift 类似每个选项可以定义一个或多个值,使用相同或不同类型,但不能直接进行命名,同样也是在枚举创建时赋值::


enum Language {
Rust(u16),
Swift(u16),
Kotlin(u16),
}

enum Language {
Rust(u16, String),
Swift(u16, u16, String),
Kotlin(u16),
}

let language = Language::Rust(2015);

如果需要给属性命名可以将关联值定义为匿名结构体:


enum Language {
Rust { year: u16, name: String },
Swift { year: u16, version: u16, name: String },
Kotlin { year: u16 },
}


  • 在 Kotlin 中选项的值称为属性或常量,所有选项的属性类型和数量都相同(因为它们都是枚举类的实例),而且值需要在枚举定义时就提供


enum class Language(val year: Int) {
Rust(2015),
Swift(2014),
Kotlin(2016),
}

所以这些值如果是通过 var 定义的就可以修改:


enum class Language(var year: Int) {
Rust(2015),
Swift(2014),
Kotlin(2016),
}

val language = Language.Kotlin
language.year = 2011

范型定义


Swift 和 Rust 定义枚举时可以使用范型,最常见的就是可选值的定义。



  • Swift:


enum Option<T> {
case none
case some(value: T)
}


  • Rust:


enum Option<T> {
,
Some(T),
}

但是 Kotlin 不支持定义枚举时使用范型。


递归定义


Swift 和 Rust 定义枚举的选项时可以使用递归,即选项的类型是枚举自身。



  • Swift 定义带递归的选项时需要添加关键字 indirect:


enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}


  • Rust 定义带递归的选项时需要使用间接访问的方式,包括这些指针类型: Box, Rc, Arc, &&mut,因为 Rust 需要在编译时知道类型的大小,而递归枚举类型的大小在没有引用的情况下是无限的。


enum ArithmeticExpression {
Number(u8),
Addition(Box<ArithmeticExpression>, Box<ArithmeticExpression>),
Multiplication(Box<ArithmeticExpression>, Box<ArithmeticExpression>),
}

Kotlin 同样不支持递归枚举。


模式匹配


模式匹配是枚举最常见的用法,最常见的是使用 switch / match / when 语句进行模式匹配:



  • Swift:


switch language {
case .rust(let year):
print(year)
case .swift(let year):
print(year)
case .kotlin(let year):
print(year)
}


  • Rust:


match language {
Language::Rust(year) => println!("Rust was first released in {}", year),
Language::Swift(year) => println!("Swift was first released in {}", year),
Language::Kotlin(year) => println!("Kotlin was first released in {}", year),
}


  • Kotlin:


when (language) {
Language.Kotlin -> println("Kotlin")
Language.Rust -> println("Rust")
Language.Swift -> println("Swift")
}

也可以使用 if 语句进行模式匹配:



  • Swift:


if case .rust(let year) = language {
print(year)
}

// 或者使用 guard
guard case .rust(let year) = language else {
return
}
print(year)

// 或者使用 while case
var expression = ArithmeticExpression.multiplication(
ArithmeticExpression.multiplication(
ArithmeticExpression.number(1),
ArithmeticExpression.number(2)
),
ArithmeticExpression.number(3)
)
while case ArithmeticExpression.multiplication(let left, _) = expression {
if case ArithmeticExpression.number(let value) = left {
print("Multiplied \(value)")
}
expression = left
}


  • Rust:


if let Language::Rust(year) = language {
println!("Rust was first released in {}", year);
}

// 或者使用 let else 匹配
let Language::Rust(year) = language else {
return;
};
println!("Rust was first released in {}", year);

// 还可以使用 while let 匹配
let mut expression = ArithmeticExpression::Multiplication(
Box::new(ArithmeticExpression::Multiplication(
Box::new(ArithmeticExpression::Number(2)),
Box::new(ArithmeticExpression::Number(3)),
)),
Box::new(ArithmeticExpression::Number(4)),
);

while let ArithmeticExpression::Multiplication(left, _) = expression {
if let ArithmeticExpression::Number(value) = *left {
println!("Multiplied: {}", value);
}
expression = *left;
}

Kotlin 不支持使用 if 语句进行模式匹配。


枚举值集合


有时需要获取枚举的所有值。



  • Swift 枚举需要实现 CaseIterable 协议,通过 AllCases() 方法可以获取所有枚举值。同时带有关联值的枚举无法自动实现 CaseIterable 协议,需要手动实现。


enum Language: CaseIterable {
case rust
case swift
case kotlin
}

let cases = Language.AllCases()


  • Rust 没有内置获取所有枚举值的方法,需要手动实现,另外带有关联值的枚举类型提供所有枚举值可能意义不大:


enum Language {
Rust,
Swift,
Kotlin,
}

impl Language {
fn all_variants() -> Vec<Language> {
vec![
Language::Rust,
Language::Swift,
Language::Kotlin,
]
}
}


  • Kotlin 提供了 values() 方法获取所有枚举值,同时支持带属性和不带属性的:


val allEntries: Array<Language> = Language.values()

原始值


枚举类型还有一个原始值,或者叫整型表示的概念。



  • Swift 可以为枚举的每个选项提供原始值,在声明时进行指定。


Swift 枚举的原始值可以是以下几种类型:



  1. 整数类型:如 Int, UInt, Int8, UInt8 等。

  2. 浮点数类型:如 Float, Double

  3. 字符串类型:String

  4. 字符类型:Character


enum Language: Int {
case rust = 1
case swift = 2
case kotlin = 3
}

带关联值的枚举类型不能同时提供原始值,而提供原始值的枚举可以直接从原始值创建枚举实例,不过实例是可选类型:


let language: Language? = Language(rawValue: 1)

在 Swift 中,提供枚举的原始值主要有以下作用:



  1. 数据映射:原始值允许枚举与基础数据类型(如整数或字符串)直接关联。这在需要将枚举值与外部数据(例如从 API 返回的字符串)匹配时非常有用。

  2. 简化代码:使用原始值可以简化某些操作,例如从原始值初始化枚举或获取枚举的原始值,而无需编写额外的代码。

  3. 可读性和维护性:在与外部系统交互时,原始值可以提供清晰、可读的映射,使代码更容易理解和维护。



  • Rust 使用 #[repr(…)] 属性来指定枚举的整数类型表示,并为每个变体分配一个可选的整数值,带和不带关联值的枚举都可以提供整数表示。


#[repr(u32)]
enum Language {
Rust(u16) = 2015,
Swift(u16) = 2014,
Kotlin(u16) = 2016,
}

在 Rust 中,为枚举提供整数表示(整数值或整数类型标识)主要有以下作用:



  1. 与外部代码交互:整数表示允许枚举与 C 语言或其他低级语言接口,因为这些语言通常使用整数来表示枚举。

  2. 内存效率:指定整数类型可以控制枚举占用的内存大小,这对于嵌入式系统或性能敏感的应用尤为重要。

  3. 值映射:通过整数表示,可以将枚举直接映射到整数值,这在处理像协议代码或状态码等需要明确值的情况下很有用。

  4. 序列化和反序列化:整数表示简化了将枚举序列化为整数值以及从整数值反序列化回枚举的过程。



  • Kotlin 没有单独的原始值概念,因为它已经提供了属性。


方法实现


三种枚举都支持方法实现。



  • Swift


enum Language {
case rust(Int)
case swift(Int)
case kotlin(Int)

func startYear() -> Int {
switch self {
case .kotlin(let year): return year
case .rust(let year): return year
case .swift(let year): return year
}
}
}


  • Rust


enum Language {
Rust(u16),
Swift(u16),
Kotlin(u16),
}

impl Language {
fn start_year(&self) -> u16 {
match self {
Language::Rust(year) => *year,
Language::Swift(year) => *year,
Language::Kotlin(year) => *year,
}
}
}


  • Kotlin


enum class Language(var year: Int) {
Rust(2015),
Swift(2014),
Kotlin(2016);

fun yearsSinceRelease(): Int {
val year: Int = java.time.Year.now().value
return year - this.year
}
}

它们还支持对协议、接口的实现:



  • Swift


protocol Versioned {
func latestVersion() -> String
}

extension Language: Versioned {
func latestVersion() -> String {
switch self {
case .kotlin(_): return "1.9.0"
case .rust(_): return "1.74.0"
case .swift(_): return "5.9"
}
}
}


  • Rust


trait Version {
fn latest_version(&self) -> String;
}

impl Version for Language {
fn latest_version(&self) -> String {
match self {
Language::Rust(_) => "1.74.0".to_string(),
Language::Swift(_) => "5.9".to_string(),
Language::Kotlin(_) => "1.9.0".to_string(),
}
}
}


  • Kotlin


interface Versioned {
fun latestVersion(): String
}

enum class Language(val year: Int): Versioned {
Rust(2015) {
override fun latestVersion(): String {
return "1.74.0"
}
},
Swift(2014) {
override fun latestVersion(): String {
return "5.9"
}
},
Kotlin(2016) {
override fun latestVersion(): String {
return "1.9.0"
}
};
}

这三者对接口的实现还有一个区别,Swift 和 Rust 可以在枚举定义之后再实现某个接口,而 Kotlin 必须在定义时就完成对所有接口的实现。不过它们都能通过扩展增加新的方法:



  • Swift


extension Language {
func name() -> String {
switch self {
case .kotlin(_): return "Kotlin"
case .rust(_): return "Rust"
case .swift(_): return "Swift"
}
}
}


  • Rust


impl Language {
fn name(&self) -> String {
match self {
Language::Rust(_) => "Rust".to_string(),
Language::Swift(_) => "Swift".to_string(),
Language::Kotlin(_) => "Kotlin".to_string(),
}
}
}


  • Kotlin


fun Language.name(): String {
return when (this) {
Language.Kotlin -> "Kotlin"
Language.Rust -> "Rust"
Language.Swift -> "Swift"
}
}

内存大小



  • Swift 的枚举大小取决于其最大的成员和必要的标签空间。若包含关联值,枚举的大小会增加以容纳这些值。可以使用 MemoryLayout 来估算大小。


enum Language {
case rust(Int, String)
case swift(Int, Int, String)
case kotlin(Int)
}

print(MemoryLayout<Language>.size) // 33


  • Rust 中的枚举大小通常等于其最大变体的大小加上一个用于标识变体的额外空间。使用 std::mem::size_of 来获取大小,提供了整型表示的枚举类型大小等于整型值大小加上最大变体大小,同时还要考虑内存对齐的因素。


enum Language1 {
Rust,
Swift,
Kotlin,
}
println!("{} bytes", size_of::<Language1>()); // 1 bytes

#[repr(u32)]
enum Language2 {
Rust,
Swift,
Kotlin,
}
println!("{} bytes", size_of::<Language2>()); // 4 bytes

#[repr(u32)]
enum Language3 {
Rust(u16),
Swift(u16),
Kotlin(u16),
}
println!("{} bytes", size_of::<Language3>()); // 8 bytes

对于上面例子中 Language3 的内存大小做一个简单解释:



  • 枚举的变体标识符(因为 #[repr(u32)])占用 4 字节。

  • 最大的变体是一个 u16 类型,占用 2 字节。

  • 可能还需要额外的 2 字节的填充,以确保整个枚举的内存对齐,因为它的对齐要求由最大的 u32 决定。


因此,总大小是 4(标识符)+ 2(最大变体)+ 2(填充)= 8 字节。



  • Kotlin:在 JVM 上,Kotlin 枚举的大小包括对象开销、枚举常量的数量和任何附加属性。Kotlin 本身不提供直接的内存布局查看工具,需要依赖 JVM 工具或库。


兼容性


Swift 枚举有时需要考虑与 Objective-C 的兼容,Rust 需要考虑与 C 的兼容。



  • 在 Swift 中,要使枚举兼容 Objective-C,你需要满足以下条件:



  1. 原始值类型:Swift 枚举必须有原始值类型,通常是 Int,因为 Objective-C 不支持 Swift 的关联值特性。

  2. 遵循 @objc 协议:在枚举定义前使用 @objc 关键字来标记它。这使得枚举可以在 Objective-C 代码中使用。


    @objc
    enum Language0: Int {
    case rust = 1
    case swift = 2
    case kotlin = 3

    func startYear() -> Int {
    switch self {
    case .kotlin: return 2016
    case .rust: return 2015
    case .swift: return 2014
    }
    }
    }


  3. 限制:使用 @objc 时,枚举不能包含关联值,必须是简单的值列表。


这样定义的枚举可以在 Swift 和 Objective-C 之间交互使用,适用于混合编程环境或需要在 Objective-C 项目中使用 Swift 代码的场景。



  • Rust 枚举与 C 兼容,只需要使用 #[repr(C)] 属性来指定枚举的内存布局。这样做确保枚举在内存中的表示与 C 语言中的枚举相同。


#[repr(C)]
enum Language0 {
Rust,
Swift,
Kotlin,
}

注意:在 Rust 中,你不能同时在枚举上使用 #[repr(C)]#[repr(u32)]。每个枚举只能使用一个 repr 属性来确定其底层的数据表示。如果你需要确保枚举与 C 语言兼容,并且具有特定的基础整数类型,你应该选择一个符合你需求的 repr 属性。例如,如果你想让枚举在内存中的表示与 C 语言中的 u32 类型的枚举相同,你可以使用 #[repr(u32)]



  • 在 Kotlin 中,枚举类(Enum Class)是与 Java 完全兼容的。Kotlin 枚举可以自然地在 Java 代码中使用,反之亦然。


作者:镜画者
来源:juejin.cn/post/7313589252172120098
收起阅读 »

why哥悄悄的给你说几个HashCode的破事。

Hash冲突是怎么回事在这个文章正式开始之前,先几句话把这个问题说清楚了:我们常说的 Hash 冲突到底是怎么回事?直接上个图片:你说你看到这个图片的时候想到了什么东西?有没有想到 HashMap 的数组加链表的结构?对咯,我这里就是以 HashMap 为切入...
继续阅读 »


Hash冲突是怎么回事

在这个文章正式开始之前,先几句话把这个问题说清楚了:我们常说的 Hash 冲突到底是怎么回事?

直接上个图片:

你说你看到这个图片的时候想到了什么东西?

有没有想到 HashMap 的数组加链表的结构?

对咯,我这里就是以 HashMap 为切入点,给大家讲一下 Hash 冲突。

接着我们看下面这张图:

假设现在我们有个值为 [why技术] 的 key,经过 Hash 算法后,计算出值为 1,那么含义就是这个值应该放到数组下标为 1 的地方。

但是如图所示,下标为 1 的地方已经挂了一个 eat 的值了。这个坑位已经被人占着了。

那么此时此刻,我们就把这种现象叫为 Hash 冲突。

HashMap 是怎么解决 Hash 冲突的呢?

链地址法,也叫做拉链法。

数组中出现 Hash 冲突了,这个时候链表的数据结构就派上用场了。

链表怎么用的呢?看图:

这样问题就被我们解决了。

其实 hash 冲突也就是这么一回事:不同的对象经过同一个 Hash 算法后得到了一样的 HashCode。

那么写到这里的时候我突然想到了一个面试题:

请问我上面的图是基于 JDK 什么版本的 HashMap 画的图?

为什么想到了这个面试题呢?

因为我画图的时候犹豫了大概 0.3 秒,往链表上挂的时候,我到底是使用头插法还是尾插法呢?

众所周知,JDK 7 中的 HashMap 是采用头插法的,即 [why技术] 在 [eat] 之前,JDK 8 中的 HashMap 采用的是尾插法。

这面试题怎么说呢,真的无聊。但是能怎么办呢,八股文该背还是得背。

面试嘛,背一背,不寒碜。

构建 HashCode 一样的 String

前面我们知道了,Hash 冲突的根本原因是不同的对象经过同一个 Hash 算法后得到了一样的 HashCode。

这句话乍一听:嗯,很有道理,就是这么一回事,没有问题。

比如我们常用的 HashMap ,绝大部分情况 key 都是 String 类型的。要出现 Hash 冲突,最少需要两个 HashCode 一样的 String 类。

那么我问你:怎么才能快速弄两个 HashCode 一样的 String 呢?

怎么样,有点懵逼了吧?

从很有道理,到有点懵逼只需要一个问题。

来,我带你分析一波。

我先问你:长度为 1 的两个不一样的 String,比如下面这样的代码,会不会有一样的 HashCode?

String a = "a";
String b = "b";

肯定是不会的,对吧。

如果你不知道的话,建议你去 ASCII 码里面找答案。

我们接着往下梳理,看看长度为 2 的 String 会不会出现一样的 HashCode?

要回答这个问题,我们要先看看 String 的 hashCode 计算方法,我这里以 JDK 8 为例:

我们假设这两个长度为 2 的 String,分别是 xy 和 ab 吧。

注意这里的 xy 和 ab 都是占位符,不是字符串。

类似于小学课本中一元二次方程中的未知数 x 和 y,我们需要带入到上面的 hashCode 方法中去计算。

hashCode 算法,最主要的就是其中的这个 for 循环。

for 循环里面的有三个我们不知道是啥的东西:h,value.length 和 val[i]。我们 debug 看一下:

h 初始情况下等于 0。

String 类型的底层结构是 char 数组,这个应该知道吧。

所以,value.length 是字符串的长度。val[] 就是这个 char 数组。

把 xy 带入到 for 循环中,这个 for 循环会循环 2 次。

第一次循环:h=0,val[0]=x,所以 h=31*0+x,即 h=x。

第二次循环:h=x,val[1]=y,所以 h=31*x+y。

所以,经过计算后, xy 的 hashCode 为 31*x+y。

同理可得,ab 的 hashCode 为 31*a+b。

由于我们想要构建 hashCode 一样的字符串,所以可以得到等式:

31x+y=31a+b

那么问题就来了:请问 x,y,a,b 分别是多少?

你算的出来吗?

你算的出来个锤子!黑板上的排列组合你不是舍不得解开,你就是解不开。

但是我可以解开,带大家看看这个题怎么搞。

数学课开始了。注意,我要变形了。

31x+y=31a+b 可以变形为:

31x-31a=b-y。

即,31(x-a)=b-y。

这个时候就清晰很多了,很明显,上面的等式有一个特殊解:

x-a=1,b-y=31。

因为,由上可得:对于任意两个字符串 xy 和 ab,如果它们满足 x-a=1,即第一个字符的 ASCII 码值相差为 1,同时满足 b-y=31,即第二个字符的 ASCII 码值相差为 -31。那么这两个字符的 hashCode 一定相等。

都已经说的这么清楚了,这样的组合对照着 ASCII 码表来找,不是一抓一大把吗?

Aa 和 BB,对不对?

Ab 和 BC,是不是?

Ac 和 BD,有没有?

好的。现在,我们可以生成两个 HashCode 一样的字符串了。

我们在稍微加深一点点难度。假设我要构建 2 个以上 HashCode 一样的字符串该怎么办?

我们先分析一下。

Aa 和 BB 的 HashCode 是一样的。我们把它两一排列组合,那不还是一样的吗?

比如这样的:AaBB,BBAa。

再比如我之前《震惊!ConcurrentHashMap里面也有死循环?》这篇文章中出现过的例子,AaAa,BBBB:

你看,神奇的事情就出现了。

我们有了 4 个 hashCode 一样的字符串了。

有了这 4 个字符串,我们再去和  Aa,BB 进行组合,比如 AaBBAa,BBAaBB......

4*2=8 种组合方式,我们又能得到 8 个 hashCode 一样的字符串了。

等等,我好像发现了什么规律似的。

如果我们以 Aa,BB 为种子数据,经过多次排列组合,可以得到任意个数的 hashCode 一样的字符串。字符串的长度随着个数增加而增加。

文字我还说不太清楚,直接 show you code 吧,如下:

public class CreateHashCodeSomeUtil {

    /**
     * 种子数据:两个长度为 2 的 hashCode 一样的字符串
     */
    private static String[] SEED = new String[]{"Aa""BB"};
    
    /**
     * 生成 2 的 n 次方个 HashCode 一样的字符串的集合
     */
    public static List hashCodeSomeList(int n) {
        List initList = new ArrayList(Arrays.asList(SEED));
        for (int i = 1; i < n; i++) {
            initList = createByList(initList);
        }
        return initList;
    }

    public static List createByList(List list) {
        List result = new ArrayList();
        for (int i = 0; i < SEED.length; ++i) {
            for (String str : list) {
                result.add(SEED[i] + str);
            }
        }
        return result;
    }
}

通过上面的代码,我们就可以生成任意多个 hashCode 一样的字符串了。

就像这样:

所以,别再问出这样的问题了:

有了这些 hashCode 一样的字符串,我们把这些字符串都放到HashMap 中,代码如下:

public class HashMapTest {
    public static void main(String[] args) {
        Map hashMap = new HashMap();
        hashMap.put("Aa""Aa");
        hashMap.put("BB""BB");
        hashMap.put("AaAa""AaAa");
        hashMap.put("AaBB""AaBB");
        hashMap.put("BBAa""BBAa");
        hashMap.put("BBBB""BBBB");
        hashMap.put("AaAaAa""AaAaAa");
        hashMap.put("AaAaBB""AaAaBB");
        hashMap.put("AaBBAa""AaBBAa");
        hashMap.put("AaBBBB""AaBBBB");
        hashMap.put("BBAaAa""BBAaAa");
        hashMap.put("BBAaBB""BBAaBB");
        hashMap.put("BBBBAa""BBBBAa");
        hashMap.put("BBBBBB""BBBBBB");
    }
}

最后这个 HashMap 的长度会经过两次扩容。扩容之后数组长度为 64:

但是里面只被占用了三个位置,分别是下标为 0,31,32 的地方:

画图如下:

看到了吧,刺不刺激,长度为 64 的数组,存 14 个数据,只占用了 3 个位置。

这空间利用率,也太低了吧。

所以,这样就算是 hack 了 HashMap。恭喜你,掌握了一项黑客攻击技术:hash 冲突 Dos 。

如果你想了解的更多。可以看看石头哥的这篇文章:《没想到 Hash 冲突还能这么玩,你的服务中招了吗?》

看到上面的图,不知道大家有没有觉得有什么不对劲的地方?

如果没有,那么我再给你提示一下:数组下标为 32 的位置下,挂了一个长度为 8 的链表。

是不是,恍然大悟了。在 JDK 8 中,链表转树的阈值是多少?

所以,在当前的案例中,数组下标为 32 的位置下挂的不应该是一个链表,而是一颗红黑树。

对不对?

对个锤子对!有的人稍不留神就被带偏了

这是不对的。链表转红黑树的阈值是节点大于 8 个,而不是等于 8 的时候。

也就是说需要再来一个经过 hash 计算后,下标为 32 的、且 value 和之前的 value 都不一样的 key 的时候,才会触发树化操作。

不信,我给你看看现在是一个什么节点:

没有骗你吧?从上面的图片可以清楚的看到,第 8 个节点还是一个普通的 node。

而如果是树化节点,它应该是长这样的:

不信,我们再多搞一个 hash 冲突进来,带你亲眼看一下,代码是不会骗人的。

那么怎么多搞一个冲突出来呢?

最简单的,这样写:

这样冲突不就多一个了吗?我真是一个天才,情不自禁的给自己鼓起掌来。

好了,我们看一下现在的节点状态是怎么样的:

怎么样,是不是变成了 TreeNode ,没有骗你吧?

什么?你问我为什么不把图画出来?

别问,问就是我不会画红黑树。正经人谁画那玩意。

另外,我还想多说一句,关于一个 HashMap 的面试题的一个坑。

面试官问:JDK 8 的 HashMap 链表转红黑树的条件是什么?

绝大部分背过面试八股文的朋友肯定能答上来:当链表长度大于 8 的时候。

这个回答正确吗?

是正确的,但是只正确了一半。

还有一个条件是数组长度大于 64 的时候才会转红黑树。

源码里面写的很清楚,数组长度小于 64,直接扩容,而不是转红黑树:

感觉很多人都忽略了“数组长度大于 64 ”这个条件。

背八股文,还是得背全了。

比如下面这种测试用例:

它们都会落到数组下标为 0 的位置上。

当第 9 个元素 BBBBAa 落进来的时候,会走到 treeifyBin 方法中去,但是不会触发树化操作,只会进行扩容操作。

因为当前长度为默认长度,即 16。不满足转红黑树条件。

所以,从下面的截图,我们可以看到,标号为 ① 的地方,数组长度变成了 32,链表长度变成了  9 ,但是节点还是普通 node:

怎么样,有点意思吧,我觉得这样学 HashMap 有趣多了。

实体类当做 key

上面的示例中,我们用的是 String 类型当做 HashMap 中的 key。

这个场景能覆盖我们开发场景中的百分之 95 了。

但是偶尔会有那么几次,可能会把实体类当做 key 放到 HashMap 中去。

注意啊,面试题又来了:在 HashMap 中可以用实体类当对象吗?

那必须的是可以的啊。但是有坑,注意别踩进去了。

我拿前段时间看到的一个新闻给大家举个例子吧:

假设我要收集学生的家庭信息,用 HashMap 存起来。

那么我的 key 是学生对象, value 是学生家庭信息对象。

他们分别是这样的:

public class HomeInfo {

    private String homeAddr;
    private String carName;
     //省略改造方法和toString方法
}

public class Student {

    private String name;
    private Integer age;
     //省略改造方法和toString方法

}

然后我们的测试用例如下:

public class HashMapTest {

    private static Map hashMap = new HashMap();

    static {
        Student student = new Student("why"7);
        HomeInfo homeInfo = new HomeInfo("大南街""自行车");
        hashMap.put(student, homeInfo);
    }

    public static void main(String[] args) {
        updateInfo("why"7"滨江路""摩托");
        for (Map.Entry entry : hashMap.entrySet()) {
            System.out.println(entry.getKey()+"-"+entry.getValue());
        }
    }

    private static void updateInfo(String name, Integer age, String homeAddr, String carName) {
        Student student = new Student(name, age);
        HomeInfo homeInfo = hashMap.get(student);
        if (homeInfo == null) {
            hashMap.put(student, new HomeInfo(homeAddr, carName));
        }
    }
}

初始状态下,HashMap 中已经有一个名叫 why 的 7 岁小朋友了,他家住大南街,家里的交通工具是自行车。

然后,有一天他告诉老师,他搬家了,搬到了滨江路去,而且家里的自行车换成了摩托车。

于是老师就通过页面,修改了 why 小朋友的家庭信息。

最后调用到了 updateInfo 方法。

嘿,你猜怎么着?

我带你看一下输出:

更新完了之后,他们班上出现了两个叫 why 的 7 岁小朋友了,一个住在大南街,一个住在滨江路。

更新变新增了,你说神奇不神奇?

现象出来了,那么根据现象定位问题代码不是手到擒来的事儿?

很明显,问题就出在这个地方:

这里取出来的 homeInfo 为空了,所以才会新放一个数据进去。

那么我们看看为啥这里为空。

跟着 hashMap.get() 源码进去瞅一眼:

标号为 ① 的地方是计算 key ,也就是 student 对象的 hashCode。而我们 student 对象并没有重写 hashCode,所以调用的是默认的 hashCode 方法。

这里的 student 是 new 出来的:

所以,这个 student 的 hashCode 势必和之前在 HashMap 里面的 student 不是一样的。

因此,标号为 ③ 的地方,经过 hash 计算后得出的 tab 数组下标,对应的位置为 null。不会进入 if 判断,这里返回为 null。

那么解决方案也就呼之欲出了:重写对象的 hashCode 方法即可。

是吗?

等等,你回来,别拿着半截就跑。我话还没说完呢。

接着看源码:

HashMap put 方法执行的时候,用的是 equals方法判断当前 key 是否与表中存在的 key 相同。

我们这里没有重写 equals方法,因此这里返回了 false。

所以,如果我们 hashCode 和 equals方法都没有重写,那么就会出现下面示意图的情况:

如果,我们重写了 hashCode,没有重写 equals 方法,那么就会出现下面示意图的情况:

总之一句话:在 HashMap 中,如果用对象做 key,那么一定要重写对象的 hashCode 方法和 equals方法。否则,不仅不能达到预期的效果,而且有可能导致内存溢出。

比如上面的示例,我们放到循环中去,启动参数我们加上 -Xmx10m,运行结果如下:

因为每一次都是 new 出来的 student 对象,hashCode 都不尽相同,所以会不停的触发扩容的操作,最终在 resize 的方法抛出了 OOM 异常。

奇怪的知识又增加了

写这篇文章的时候我翻了一下《Java 编程思想(第 4 版)》一书。

奇怪的知识又增加了两个。

第一个是在这本书里面,对于 HashMap 里面放对象的示例是这样的:

Groundhog:土拨鼠、旱獭。

Prediction:预言、预测、预告。

考虑一个天气预报系统,将土拨鼠和预报联系起来。

这 TM 是个什么读不懂的神仙需求?

幸好 why 哥学识渊博,闭上眼睛,去我的知识仓库里面搜索了一番。

原来是这么一回事。

在美国的宾西法尼亚州,每年的 2 月 2 日,是土拨鼠日。

根据民间的说法,如果土拨鼠在 2 月 2 号出洞时见到自己的影子,然后这个小东西就会回到洞里继续冬眠,表示春天还要六个星期才会到来。如果见不到影子,它就会出来觅食或者求偶,表示寒冬即将结束。

这就呼应上了,通过判断土拨鼠出洞的时候是否能看到影子,从而判断冬天是否结束。

这样,需求就说的通了。

第二个奇怪的知识是这样的。

关于 HashCode 方法,《Java编程思想(第4版)》里面是这样写的:

我一眼就发现了不对劲的地方:result=37*result+c。

前面我们才说了,基数应该是 31 才对呀?

作者说这个公式是从《Effective Java(第1版)》的书里面拿过来的。

这两本书都是 java 圣经啊,建议大家把梦幻联动打在留言区上。

《Effective Java(第1版)》太久远了,我这里只有第 2 版和第 3 版的实体书。

于是我在网上找了一圈第 1 版的电子书,终于找到了对应描述的地方:

可以看到,书里给出的公式确实是基于 37 去计算的。

翻了一下第三版,一样的地方,给出的公式是这样的:

而且,你去网上搜:String 的 hashCode 的计算方法。

都是在争论为什么是 31 。很少有人提到 37 这个数。

其实,我猜测,在早期的 JDK 版本中 String 的 hashCode 方法应该用的是 37 ,后来改为了 31 。

我想去下载最早的 JDK 版本去验证一下的,但是网上翻了个底朝天,没有找到合适的。

书里面为什么从 37 改到 31 呢?

作者是这样解释的,上面是第 1 版,下面是第 2 版:

用方框框起来的部分想要表达的东西是一模一样的,只是对象从 37 变成了 31 。

而为什么从 37 变成 31 ,作者在第二版里面解释了,也就是我用下划线标注的部分。

31 有个很好的特许,即用位移和减法来代替乘法,可以得到更好的性能:

31*i==(i<<5)-i。现代的虚拟机可以自动完成这种优化。

从 37 变成 31,一个简单的数字变化,就能带来性能的提升。

个中奥秘,很有意思,有兴趣的可以去查阅一下相关资料。

真是神奇的计算机世界。

最后说一句(求关注)

好了,看到了这里安排个“一键三连”(转发、在看、点赞)吧,周更很累的,不要白嫖我,需要一点正反馈。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在留言区提出来,我对其加以修改。


作者:why技术
来源:mp.weixin.qq.com/s/zXFWBr9Fd5UZLjse52rcng
ction>
收起阅读 »

SQL 必须被淘汰的 9 个理由

尽管 SQL 很受欢迎并取得了成功,但它仍然是一项悖论研究。它可能笨重且冗长,但开发人员经常发现它是提取所需数据的最简单、最直接的方法。当查询编写正确时,它可能会快如闪电,而当查询未达到目标时,它会慢得像糖蜜。它已经有几十年的历史了,但新功能仍在不断增加。 这...
继续阅读 »

尽管 SQL 很受欢迎并取得了成功,但它仍然是一项悖论研究。它可能笨重且冗长,但开发人员经常发现它是提取所需数据的最简单、最直接的方法。当查询编写正确时,它可能会快如闪电,而当查询未达到目标时,它会慢得像糖蜜。它已经有几十年的历史了,但新功能仍在不断增加。


这些悖论并不重要,因为市场已经表明:SQL 是许多人的首选,即使有更新且可以说更强大的选项。世界各地的开发人员(从最小的网站到最大的大型企业)都了解 SQL。他们依靠它来组织所有数据。


SQL 的表格模型占据主导地位,以至于许多非 SQL 项目最终都添加了 SQLish 接口,因为用户需要它。 NoSQL 运动也是如此,它的发明是为了摆脱旧范式。最终,SQL 似乎获胜了。


SQL 的限制可能还不足以将其扔进垃圾箱。开发人员可能永远不会将所有数据从 SQL 中迁移出来。但 SQL 的问题足够真实,足以给开发人员带来压力、增加延迟,甚至需要对某些项目进行重新设计。


以下是我们希望退出 SQL 的九个原因,尽管我们知道我们可能不会这样做。


SQL 让事情变得更糟的 9 种方式



  1. 表格无法缩放

  2. SQL 不是 JSON 或 XML 原生的

  3. 编组是一个很大的时间消耗

  4. SQL 不实时

  5. JOINS 很头疼

  6. 列浪费空间

  7. 优化器只是有时有帮助

  8. 非规范化将表视为垃圾

  9. 附加的想法可能会破坏你的数据库


表格无法缩放


关系模型喜欢表,所以我们不断构建它们。这对于小型甚至普通大小的数据库来说都很好。但在真正大规模的情况下,该模型开始崩溃。


有些人尝试通过将新旧结合起来来解决问题,例如将分片集成到旧的开源数据库中。添加层似乎可以使数据更易于管理并提供无限的规模。但这些增加的层可以隐藏地雷。 SELECT 或 JOIN 的处理时间可能截然不同,具体取决于分片中存储的数据量。


分片还迫使 DBA 考虑数据可能存储在不同机器甚至不同地理位置的可能性。如果没有意识到数据存储在不同的位置,那么开始跨表搜索的经验不足的管理员可能会感到困惑。该模型有时会从视图中抽象出位置。 


某些 AWS 计算机配备24 TB RAM。为什么?因为有些数据库用户需要这么多。他们在 SQL 数据库中拥有如此多的数据,并且在一台机器的一块 RAM 中运行得更好。


SQL 不是 JSON 或 XML 原生的


SQL 作为一种语言可能是常青树,但它与 JSON、YAML 和 XML 等较新的数据交换格式的配合并不是特别好。所有这些都支持比 SQL 更分层、更灵活的格式。 SQL 数据库的核心仍然停留在表无处不在的关系模型中。


市场找到了掩盖这种普遍抱怨的方法。使用正确的粘合代码添加不同的数据格式(例如 JSON)相对容易,但您会为此付出时间损失的代价。


一些 SQL 数据库现在能够将 JSON、XML、GraphQL 或 YAML 等更现代的数据格式作为本机功能进行编码和解码。但在内部,数据通常使用相同的旧表格模型来存储和索引。


将数据转入或转出这些格式需要花费多少时间?以更现代的方式存储我们的数据不是更容易吗?一些聪明的数据库开发人员继续进行实验,但奇怪的是,他们常常最终选择使用某种 SQL 解析器。这就是开发人员所说的他们想要的。


编组是一个很大的时间消耗


数据库可以将数据存储在表中,但程序员编写处理对象的代码。设计数据驱动应用程序的大部分工作似乎都是找出从数据库中提取数据并将其转换为业务逻辑可以使用的对象的最佳方法。然后,必须通过将对象中的数据字段转换为 SQL 更新插入来对它们进行解组。难道没有办法让数据保持随时可用的格式吗?


SQL 不实时


最初的 SQL 数据库是为批量分析和交互模式而设计的。具有长处理管道的流数据模型是一个相对较新的想法,并且并不完全匹配。


主要的 SQL 数据库是几十年前设计的,当时的模型设想数据库独立运行并像某种预言机一样回答查询。有时他们反应很快,有时则不然。这就是批处理的工作原理。


一些最新的应用程序需要更好的实时性能,不仅是为了方便,而且是因为应用程序需要它。在现代的流媒体世界中,像大师一样坐在山上并不那么有效。


专为这些市场设计的最新数据库非常重视速度和响应能力。他们不提供那种会减慢一切的复杂 SQL 查询。


JOIN 是一个令人头疼的问题


关系数据库的强大之处在于将数据分割成更小、更简洁的表。头痛随之而来。


使用 JOIN 动态重新组装数据通常是工作中计算成本最高的部分,因为数据库必须处理所有数据。当数据开始超出 RAM 的容量时,令人头疼的事情就开始了。


对于学习 SQL 的人来说,JOIN 可能会令人难以置信的困惑。弄清楚内部 JOIN 和外部 JOIN 之间的区别仅仅是一个开始。寻找将多个 JOIN 连接在一起的最佳方法会使情况变得更糟。内部优化器可能会提供帮助,但当数据库管理员要求特别复杂的组合时,它们无能为力。


列浪费空间


NoSQL 的伟大想法之一是让用户摆脱列的束缚。如果有人想向条目添加新值,他们可以选择他们想要的任何标签或名称。无需更新架构即可添加新列。


SQL 维护者只看到该模型中的混乱。他们喜欢表格附带的顺序,并且不希望开发人员即时添加新字段。他们说得有道理,但添加新列可能非常昂贵且耗时,尤其是在大表中。将新数据放在单独的列中并将它们与 JOIN 进行匹配会增加更多的时间和复杂性。


优化器只是有时有帮助


数据库公司和研究人员花费了大量时间来开发优秀的优化器,这些优化器可以分解查询并找到排序其操作的最佳方式。


收益可能很大,但优化器的作用有限。如果查询需要特别大或华丽的响应,优化器不能只是说“你真的确定吗?”它必须汇总答案并按照指示执行。


一些 DBA 仅在应用程序开始扩展时才了解这一点。早期的优化足以处理开发过程中的测试数据集。但在关键时刻,优化器无法从查询中榨取更多的能量。


非规范化将表视为垃圾


开发人员经常发现自己陷入了两难境地:想要更快性能的用户和不想为更大、更昂贵的硬件付费的精算师。一个常见的解决方案是对表进行非规范化,这样就不需要复杂的 JOIN 或跨表的任何内容。所有数据都已经存在于一个长矩形中。


这不是一个糟糕的技术解决方案,而且它常常会获胜,因为磁盘空间变得比处理能力更便宜。但非规范化也抛弃了 SQL 和关系数据库理论中最聪明的部分。当您的数据库变成一个长 CSV 文件时,所有这些花哨的数据库功能几乎都消失了。


附加的想法可能会破坏你的数据库


多年来,开发人员一直在向 SQL 添加新功能,其中一些功能非常聪明。您很难对不必使用的炫酷功能感到不安。另一方面,这些附加功能通常是用螺栓固定的,这可能会导致性能问题。一些开发人员警告说,您应该对子查询格外小心,因为它们会减慢一切速度。其他人则表示,选择公共表表达式、视图或 Windows 等子集会使代码变得过于复杂。代码的创建者可以阅读它,但其他人在试图保持 SQL 的所有层和生成的直线性时都会感到头疼。这就像看一部克里斯托弗·诺兰的电影,但是是用代码编写的。


其中一些伟大的想法妨碍了已经行之有效的做法。窗口函数旨在通过加快平均值等结果的计算来加快基本数据分析的速度。但许多 SQL 用户会发现并使用一些附加功能。在大多数情况下,他们会尝试新功能,只有当机器速度慢得像爬行一样时才会注意到出现问题。然后他们需要一些老的、灰色的 DBA 来解释发生了什么以及如何修复它。




作者:Peter Wayner



作者:Squids数据库云服务提供商
来源:juejin.cn/post/7313742254144585764
收起阅读 »

finally中的代码一定会执行吗?

通常在面试中,只要是疑问句一般答案都是“否定”的,因为如果是“确定”和“正常”的,那面试官就没有必要再问了嘛,而今天这道题的答案也是符合这个套路。 1.典型回答 正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到以下异常情况,那么 fin...
继续阅读 »

通常在面试中,只要是疑问句一般答案都是“否定”的,因为如果是“确定”和“正常”的,那面试官就没有必要再问了嘛,而今天这道题的答案也是符合这个套路。


1.典型回答


正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到以下异常情况,那么 finally 中的代码就不会继续执行了:



  1. 程序在 try 块中遇到 System.exit() 方法,会立即终止程序的执行,这时 finally 块中的代码不会被执行,例如以下代码:


public class FinallyExample {
public static void main(String[] args) {
try {
System.out.println("执行 try 代码.");
System.exit(0);
} finally {
System.out.println("执行 finally 代码.");
}
}
}

以上程序的执行结果如下:



  1. 在 try 快中遇到 Runtime.getRuntime().halt() 代码,强制终止正在运行的 JVM。与 System.exit()方法不同,此方法不会触发 JVM 关闭序列。因此,当我们调用 halt 方法时,都不会执行关闭钩子或终结器。实现代码如下:


public class FinallyExample {
public static void main(String[] args) {
try {
System.out.println("执行 try 代码.");
Runtime.getRuntime().halt(0);
} finally {
System.out.println("执行 finally 代码.");
}
}
}

以上程序的执行结果如下:



  1. 程序在 try 块中遇到无限循环或者发生死锁等情况时,程序可能无法正常跳出 try 块,此时 finally 块中的代码也不会被执行。

  2. 掉电问题,程序还没有执行到 finally 就掉电了(停电了),那 finally 中的代码自然也不会执行。

  3. JVM 异常崩溃问题导致程序不能继续执行,那么 finally 的代码也不会执行。


钩子方法解释


在编程中,钩子方法(Hook Method)是一种由父类提供的空或默认实现的方法,子类可以选择性地重写或扩展该方法,以实现特定的行为或定制化逻辑。钩子方法可以在父类中被调用,以提供一种可插拔的方式来影响父类的行为。
钩子方法通常用于框架或模板方法设计模式中。框架提供一个骨架或模板,其中包含一些已经实现的方法及预留的钩子方法。具体的子类可以通过重写钩子方法来插入定制逻辑,从而影响父类方法的实现方式。


2.考点分析


正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到 System.exit() 方法或 Runtime.getRuntime().halt() 方法,或者是 try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题,那么 finally 中的代码也是不会执行的。


3.知识扩展


System.exit() 和 Runtime.getRuntime().halt() 都可以用于终止 Java 程序的执行,但它们之间有以下区别:



  1. System.exit():来自 Java.lang.System 类的一个静态方法,它接受一个整数参数作为退出状态码,通常非零值表示异常终止,使用零值表示正常终止。其中,最重要的是使用 exit() 方法,会执行 JVM 关闭钩子或终结器。

  2. Runtime.getRuntime().halt():来自 Runtime 类的一个实例方法,它接受一个整数参数作为退出状态码。其中退出状态码只是表示程序终止的原因,很少在程序终止时使用非零值。而使用 halt() 方法,不会执行 JVM 关闭钩子或终结器。


例如以下代码,使用 exit() 方法会执行 JVM 关闭钩子:


class ExitDemo {
// 注册退出钩子程序
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("执行 ShutdownHook 方法");
}));
}
public static void main(String[] args) {
try {
System.out.println("执行 try 代码。");
// 使用 System.exit() 退出程序
System.exit(0);
} finally {
System.out.println("执行 finally 代码。");
}
}
}

以上程序的执行结果如下:

而 halt() 退出的方法,并不会执行 JVM 关闭钩子,示例代码如下:


class ExitDemo {

// 注册退出钩子程序
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("执行 ShutdownHook 方法");
}));
}

public static void main(String[] args) {
try {
System.out.println("执行 try 代码。");
// 使用 Runtime.getRuntime().halt() 退出程序
Runtime.getRuntime().halt(0);
} finally {
System.out.println("执行 finally 代码。");
}
}
}

以上程序的执行结果如下:


小结


正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到 System.exit() 方法或 Runtime.getRuntime().halt() 方法,或者是 try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题,finally 中的代码是不会执行的。而 exit() 方法会执行 JVM 关闭钩子方法或终结器,但 halt() 方法并不会执行钩子方法或终结器。


作者:Java中文社群
来源:juejin.cn/post/7313501001788604450
收起阅读 »

三年前端还不会配置Nginx?刷完这篇就够了

一口气看完,比自学强十倍! 什么是Nginx Nginx是一个开源的高性能HTTP和反向代理服务器。它可以用于处理静态资源、负载均衡、反向代理和缓存等任务。Nginx被广泛用于构建高可用性、高性能的Web应用程序和网站。它具有低内存消耗、高并发能力和良好的...
继续阅读 »

一口气看完,比自学强十倍!



Nginx_logo-700x148.png


什么是Nginx


Nginx是一个开源的高性能HTTP和反向代理服务器。它可以用于处理静态资源、负载均衡、反向代理和缓存等任务。Nginx被广泛用于构建高可用性、高性能的Web应用程序和网站。它具有低内存消耗、高并发能力和良好的稳定性,因此在互联网领域非常受欢迎。


为什么使用Nginx



  1. 高性能:Nginx采用事件驱动的异步架构,能够处理大量并发连接而不会消耗过多的系统资源。它的处理能力比传统的Web服务器更高,在高并发负载下表现出色。

  2. 高可靠性:Nginx具有强大的容错能力和稳定性,能够在面对高流量和DDoS攻击等异常情况下保持可靠运行。它能通过健康检查和自动故障转移来保证服务的可用性。

  3. 负载均衡:Nginx可以作为反向代理服务器,实现负载均衡,将请求均匀分发给多个后端服务器。这样可以提高系统的整体性能和可用性。

  4. 静态文件服务:Nginx对静态资源(如HTML、CSS、JavaScript、图片等)的处理非常高效。它可以直接缓存静态文件,减轻后端服务器的负载。

  5. 扩展性:Nginx支持丰富的模块化扩展,可以通过添加第三方模块来提供额外的功能,如gzip压缩、SSL/TLS加密、缓存控制等。


如何处理请求


Nginx处理请求的基本流程如下:



  1. 接收请求:Nginx作为服务器软件监听指定的端口,接收客户端发来的请求。

  2. 解析请求:Nginx解析请求的内容,包括请求方法(GET、POST等)、URL、头部信息等。

  3. 配置匹配:Nginx根据配置文件中的规则和匹配条件,决定如何处理该请求。配置文件定义了虚拟主机、反向代理、负载均衡、缓存等特定的处理方式。

  4. 处理请求:Nginx根据配置的处理方式,可能会进行以下操作:



    • 静态文件服务:如果请求的是静态资源文件,如HTML、CSS、JavaScript、图片等,Nginx可以直接返回文件内容,不必经过后端应用程序。

    • 反向代理:如果配置了反向代理,Nginx将请求转发给后端的应用服务器,然后将其响应返回给客户端。这样可以提供负载均衡、高可用性和缓存等功能。

    • 缓存:如果启用了缓存,Nginx可以缓存一些静态或动态内容的响应,在后续相同的请求中直接返回缓存的响应,减少后端负载并提高响应速度。

    • URL重写:Nginx可以根据配置的规则对URL进行重写,将请求从一个URL重定向到另一个URL或进行转换。

    • SSL/TLS加密:如果启用了SSL/TLS,Nginx可以负责加密和解密HTTPS请求和响应。

    • 访问控制:Nginx可以根据配置的规则对请求进行访问控制,例如限制IP访问、进行身份认证等。



  5. 响应结果:Nginx根据处理结果生成响应报文,包括状态码、头部信息和响应内容。然后将响应发送给客户端。


什么是正向代理和反向代理


2020-03-08-5ce95a07b18a071444-20200308191723379.png


正向代理


是指客户端通过代理服务器发送请求到目标服务器。客户端向代理服务器发送请求,代理服务器再将请求转发给目标服务器,并将服务器的响应返回给客户端。正向代理可以隐藏客户端的真实IP地址,提供匿名访问和访问控制等功能。它常用于跨越防火墙访问互联网、访问被封禁的网站等情况。


反向代理


是指客户端发送请求到代理服务器,代理服务器再将请求转发给后端的多个服务器中的一个或多个,并将后端服务器的响应返回给客户端。客户端并不直接访问后端服务器,而是通过反向代理服务器来获取服务。反向代理可以实现负载均衡、高可用性和安全性等功能。它常用于网站的高并发访问、保护后端服务器、提供缓存和SSL终止等功能。


nginx 启动和关闭


进入目录:/usr/local/nginx/sbin
启动命令:./nginx
重启命令:nginx -s reload
快速关闭命令:./nginx -s stop
有序地停止,需要进程完成当前工作后再停止:./nginx -s quit
直接杀死nginx进程:killall nginx

目录结构


[root@localhost ~]# tree /usr/local/nginx
/usr/local/nginx

├── client_body_temp                 # POST 大文件暂存目录
├── conf                             # Nginx所有配置文件的目录
│   ├── fastcgi.conf                 # fastcgi相关参数的配置文件
│   ├── fastcgi.conf.default         # fastcgi.conf的原始备份文件
│   ├── fastcgi_params               # fastcgi的参数文件
│   ├── fastcgi_params.default      
│   ├── koi-utf
│   ├── koi-win
│   ├── mime.types                   # 媒体类型
│   ├── mime.types.default
│   ├── nginx.conf                   #这是Nginx默认的主配置文件,日常使用和修改的文件
│   ├── nginx.conf.default
│   ├── scgi_params                 # scgi相关参数文件
│   ├── scgi_params.default  
│   ├── uwsgi_params                 # uwsgi相关参数文件
│   ├── uwsgi_params.default
│   └── win-utf
├── fastcgi_temp                     # fastcgi临时数据目录
├── html                             # Nginx默认站点目录
│   ├── 50x.html                     # 错误页面优雅替代显示文件,例如出现502错误时会调用此页面
│   └── index.html                   # 默认的首页文件
├── logs                             # Nginx日志目录
│   ├── access.log                   # 访问日志文件
│   ├── error.log                   # 错误日志文件
│   └── nginx.pid                   # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件
├── proxy_temp                       # 临时目录
├── sbin                             # Nginx 可执行文件目录
│   └── nginx                       # Nginx 二进制可执行程序
├── scgi_temp                       # 临时目录
└── uwsgi_temp                       # 临时目录

配置文件nginx.conf


# 启动进程,通常设置成和cpu的数量相等
worker_processes  1;

# 全局错误日志定义类型,[debug | info | notice | warn | error | crit]
error_log  logs/error.log;
error_log  logs/error.log  notice;
error_log  logs/error.log  info;

# 进程pid文件
pid        /var/run/nginx.pid;

# 工作模式及连接数上限
events {
    # 仅用于linux2.6以上内核,可以大大提高nginx的性能
    use   epoll;

    # 单个后台worker process进程的最大并发链接数
    worker_connections  1024;

    # 客户端请求头部的缓冲区大小
    client_header_buffer_size 4k;

    # keepalive 超时时间
    keepalive_timeout 60;

    # 告诉nginx收到一个新连接通知后接受尽可能多的连接
    # multi_accept on;
}

# 设定http服务器,利用它的反向代理功能提供负载均衡支持
http {
    # 文件扩展名与文件类型映射表义
    include       /etc/nginx/mime.types;

    # 默认文件类型
    default_type  application/octet-stream;

    # 默认编码
    charset utf-8;

    # 服务器名字的hash表大小
    server_names_hash_bucket_size 128;

    # 客户端请求头部的缓冲区大小
    client_header_buffer_size 32k;

    # 客户请求头缓冲大小
    large_client_header_buffers 4 64k;

    # 设定通过nginx上传文件的大小
    client_max_body_size 8m;

    # 开启目录列表访问,合适下载服务器,默认关闭。
    autoindex on;

    # sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,对于普通应用,
    # 必须设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,以平衡磁盘与网络I/O处理速度
    sendfile        on;

    # 此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用
    #tcp_nopush     on;

    # 连接超时时间(单秒为秒)
    keepalive_timeout  65;


    # gzip模块设置
    gzip on;               #开启gzip压缩输出
    gzip_min_length 1k;    #最小压缩文件大小
    gzip_buffers 4 16k;    #压缩缓冲区
    gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
    gzip_comp_level 2;     #压缩等级
    gzip_types text/plain application/x-javascript text/css application/xml;
    gzip_vary on;

    # 开启限制IP连接数的时候需要使用
    #limit_zone crawler $binary_remote_addr 10m;

    # 指定虚拟主机的配置文件,方便管理
    include /etc/nginx/conf.d/*.conf;


    # 负载均衡配置
    upstream aaa {
        # 请见上文中的五种配置
    }


   # 虚拟主机的配置
    server {

        # 监听端口
        listen 80;

        # 域名可以有多个,用空格隔开
        server_name www.aaa.com aaa.com;

        # 默认入口文件名称
        index index.html index.htm index.php;
        root /data/www/sk;

        # 图片缓存时间设置
        location ~ .*.(gif|jpg|jpeg|png|bmp|swf)${
            expires 10d;
        }

        #JS和CSS缓存时间设置
        location ~ .*.(js|css)?${
            expires 1h;
        }

        # 日志格式设定
        #$remote_addr与 $http_x_forwarded_for用以记录客户端的ip地址;
        #$remote_user:用来记录客户端用户名称;
        #$time_local:用来记录访问时间与时区;
        #$request:用来记录请求的url与http协议;
        #$status:用来记录请求状态;成功是200,
        #$body_bytes_sent :记录发送给客户端文件主体内容大小;
        #$http_referer:用来记录从那个页面链接访问过来的;
        log_format access '$remote_addr - $remote_user [$time_local] "$request" '
        '$status $body_bytes_sent "$http_referer" '
        '"$http_user_agent" $http_x_forwarded_for';

        # 定义本虚拟主机的访问日志
        access_log  /usr/local/nginx/logs/host.access.log  main;
        access_log  /usr/local/nginx/logs/host.access.404.log  log404;

        # 对具体路由进行反向代理
        location /connect-controller {

            proxy_pass http://127.0.0.1:88;
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;

            # 后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $host;

            # 允许客户端请求的最大单文件字节数
            client_max_body_size 10m;

            # 缓冲区代理缓冲用户端请求的最大字节数,
            client_body_buffer_size 128k;

            # 表示使nginx阻止HTTP应答代码为400或者更高的应答。
            proxy_intercept_errors on;

            # nginx跟后端服务器连接超时时间(代理连接超时)
            proxy_connect_timeout 90;

            # 后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据
            proxy_send_timeout 90;

            # 连接成功后,后端服务器响应的超时时间
            proxy_read_timeout 90;

            # 设置代理服务器(nginx)保存用户头信息的缓冲区大小
            proxy_buffer_size 4k;

            # 设置用于读取应答的缓冲区数目和大小,默认情况也为分页大小,根据操作系统的不同可能是4k或者8k
            proxy_buffers 4 32k;

            # 高负荷下缓冲大小(proxy_buffers*2)
            proxy_busy_buffers_size 64k;

            # 设置在写入proxy_temp_path时数据的大小,预防一个工作进程在传递文件时阻塞太长
            # 设定缓存文件夹大小,大于这个值,将从upstream服务器传
            proxy_temp_file_write_size 64k;
        }

        # 动静分离反向代理配置(多路由指向不同的服务端或界面)
        location ~ .(jsp|jspx|do)?$ {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://127.0.0.1:8080;
        }
    }
}

location


location指令的作用就是根据用户请求的URI来执行不同的应用


语法


location [ = | ~ | ~* | ^~ ] uri {...}


  • [ = | ~ | ~* | ^~ ]:匹配的标识



    • ~~*的区别是:~区分大小写,~*不区分大小写

    • ^~:进行常规字符串匹配后,不做正则表达式的检查



  • uri:匹配的网站地址

  • {...}:匹配uri后要执行的配置段


举例


location = / {
    [ configuration A ]
}
location / {
    [ configuration B ]
}
location /sk/ {
    [ configuration C ]
}
location ^~ /img/ {
    [ configuration D ]
}
location ~* .(gif|jpg|jpeg)$ {
    [ configuration E ]
}


  • = / 请求 / 精准匹配A,不再往下查找

  • / 请求/index.html匹配B。首先查找匹配的前缀字符,找到最长匹配是配置B,接着又按照顺序查找匹配的正则。结果没有找到,因此使用先前标记的最长匹配,即配置B。

  • /sk/ 请求/sk/abc 匹配C。首先找到最长匹配C,由于后面没有匹配的正则,所以使用最长匹配C。

  • ~* .(gif|jpg|jpeg)$ 请求/sk/logo.gif 匹配E。首先进行前缀字符的查找,找到最长匹配项C,继续进行正则查找,找到匹配项E。因此使用E。

  • ^~ 请求/img/logo.gif匹配D。首先进行前缀字符查找,找到最长匹配D。但是它使用了^~修饰符,不再进行下面的正则的匹配查找,因此使用D。


单页面应用刷新404问题


    location / {
        try_files $uri $uri/ /index.html;
    }

配置跨域请求


server {
    listen   80;
    location / {
        # 服务器默认是不被允许跨域的。
        # 配置`*`后,表示服务器可以接受所有的请求源(Origin),即接受所有跨域的请求
        add_header Access-Control-Allow-Origin *;
        
        add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
        add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
        
        # 发送"预检请求"时,需要用到方法 OPTIONS ,所以服务器需要允许该方法
        # 给OPTIONS 添加 204的返回,是为了处理在发送POST请求时Nginx依然拒绝访问的错误
        if ($request_method = 'OPTIONS') {
            return 204;
        }
    }
}

开启gzip压缩


    # gzip模块设置
    gzip on;               #开启gzip压缩输出
    gzip_min_length 1k;    #最小压缩文件大小
    gzip_buffers 4 16k;    #压缩缓冲区
    gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
    gzip_comp_level 2;     #压缩等级
    
    # 设置什么类型的文件需要压缩
    gzip_types text/plain application/x-javascript text/css application/xml;
    
    # 用于设置使用Gzip进行压缩发送是否携带“Vary:Accept-Encoding”头域的响应头部
    # 主要是告诉接收方,所发送的数据经过了Gzip压缩处理
    gzip_vary on;

总体而言,Nginx是一款轻量级、高性能、可靠性强且扩展性好的服务器软件,适用于搭建高可用性、高性能的Web应用程序和网站。


作者:日月之行_
来源:juejin.cn/post/7270153705877241890
收起阅读 »

农业银行算法题,为什么用初中知识出题,这么多人不会?

背景介绍 总所周知,有相当一部分的大学生是不会初高中知识的。 因此,每当那种「初等数学为背景编写的算法题」在笔面出现,舆论往往分成"三大派": 甚至那位说"致敬高考"的同学也搞岔了,高考哪有这么简单,美得你 🤣 这仅仅是初中数学「几何学」中较为简单的知识...
继续阅读 »

背景介绍


总所周知,有相当一部分的大学生是不会初高中知识的


因此,每当那种「初等数学为背景编写的算法题」在笔面出现,舆论往往分成"三大派":


对初高中知识,有清晰记忆


对初高中知识,记忆模糊


啥题?不会!


甚至那位说"致敬高考"的同学也搞岔了,高考哪有这么简单,美得你 🤣


这仅仅是初中数学「几何学」中较为简单的知识点。


抓住大学生对初高中知识这种「会者不难,难者不会」的现状,互联网大厂似乎更喜欢此类「考察初等数学」的算法题。


因为十个候选人,九个题海战术,HOT 100 和剑指 Offer 大家都刷得飞起了。


冷不丁的考察这种题目,反而更能起到"筛选"效果。


但此类算法题,农业银行 并非首创,甚至是同一道题,也被 华为云美的百度 先后出过。


学好初中数学,就能稳拿华为 15 级?🤣




下面,一起来看看这道题。


题目描述


平台:LeetCode


题号:149


给你一个数组 points,其中 points[i]=[xi,yi]points[i] = [x_i, y_i] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。


示例 1:


输入:points = [[1,1],[2,2],[3,3]]

输出:3

示例 2:


输入:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]

输出:4

提示:



  • 1<=points.length<=3001 <= points.length <= 300

  • points[i].length=2points[i].length = 2

  • 104<=xi,yi<=104-10^4 <= x_i, y_i <= 10^4

  • points 中的所有点互不相同


枚举直线 + 枚举统计


我们知道,两点可以确定一条线。


一个朴素的做法是先枚举两点(确定一条线),然后检查其余点是否落在该线中。


为避免除法精度问题,当我们枚举两个点 xxyy 时,不直接计算其对应直线的 斜率截距


而是通过判断 xxyy 与第三个点 pp 形成的两条直线斜率是否相等,来得知点 pp 是否落在该直线上。


斜率相等的两条直线要么平行,要么重合。


平行需要 44 个点来唯一确定,我们只有 33 个点,因此直接判定两条直线是否重合即可。


详细说,当给定两个点 (x1,y1)(x_1, y_1)(x2,y2)(x_2, y_2) 时,对应斜率 y2y1x2x1\frac{y_2 - y_1}{x_2 - x_1}


为避免计算机除法的精度问题,我们将「判定 aybyaxbx=bycybxcx\frac{a_y - b_y}{a_x - b_x} = \frac{b_y - c_y}{b_x - c_x} 是否成立」改为「判定 (ayby)×(bxcx)=(axbx)×(bycy)(a_y - b_y) \times (b_x - c_x) = (a_x - b_x) \times (b_y - c_y) 是否成立」。


将存在精度问题的「除法判定」巧妙转为「乘法判定」。


Java 代码:


class Solution {
public int maxPoints(int[][] points) {
int n = points.length, ans = 1;
for (int i = 0; i < n; i++) {
int[] x = points[i];
for (int j = i + 1; j < n; j++) {
int[] y = points[j];
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
int cnt = 2;
for (int k = j + 1; k < n; k++) {
int[] p = points[k];
int s1 = (y[1] - x[1]) * (p[0] - y[0]);
int s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = Math.max(ans, cnt);
}
}
return ans;
}
}

C++ 代码:


class Solution {
public:
int maxPoints(vector<vector<int>>& points) {
int n = points.size(), ans = 1;
for (int i = 0; i < n; i++) {
vector<int> x = points[i];
for (int j = i + 1; j < n; j++) {
vector<int> y = points[j];
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
int cnt = 2;
for (int k = j + 1; k < n; k++) {
vector<int> p = points[k];
int s1 = (y[1] - x[1]) * (p[0] - y[0]);
int s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = max(ans, cnt);
}
}
return ans;
}
};

Python 代码:


class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
n, ans = len(points), 1
for i, x in enumerate(points):
for j in range(i + 1, n):
y = points[j]
# 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
cnt = 2
for k in range(j + 1, n):
p = points[k]
s1 = (y[1] - x[1]) * (p[0] - y[0])
s2 = (p[1] - y[1]) * (y[0] - x[0])
if s1 == s2: cnt += 1
ans = max(ans, cnt)
return ans

TypeScript 代码:


function maxPoints(points: number[][]): number {
let n = points.length, ans = 1;
for (let i = 0; i < n; i++) {
let x = points[i];
for (let j = i + 1; j < n; j++) {
// 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
let y = points[j], cnt = 2;
for (let k = j + 1; k < n; k++) {
let p = points[k];
let s1 = (y[1] - x[1]) * (p[0] - y[0]);
let s2 = (p[1] - y[1]) * (y[0] - x[0]);
if (s1 == s2) cnt++;
}
ans = Math.max(ans, cnt);
}
}
return ans;
};


  • 时间复杂度:O(n3)O(n^3)

  • 空间复杂度:O(1)O(1)


枚举直线 + 哈希表统计


根据「朴素解法」的思路,枚举所有直线的过程不可避免,但统计点数的过程可以优化。


具体的,我们可以先枚举所有可能出现的 直线斜率(根据两点确定一条直线,即枚举所有的「点对」),使用「哈希表」统计所有 斜率 对应的点的数量,在所有值中取个 maxmax 即是答案。


一些细节:在使用「哈希表」进行保存时,为了避免精度问题,我们直接使用字符串进行保存,同时需要将 斜率 约干净(套用 gcd 求最大公约数模板)。


Java 代码:


class Solution {
public int maxPoints(int[][] points) {
int n = points.length, ans = 1;
for (int i = 0; i < n; i++) {
Map<String, Integer> map = new HashMap<>();
// 由当前点 i 发出的直线所经过的最多点数量
int max = 0;
for (int j = i + 1; j < n; j++) {
int x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
int a = x1 - x2, b = y1 - y2;
int k = gcd(a, b);
String key = (a / k) + "_" + (b / k);
map.put(key, map.getOrDefault(key, 0) + 1);
max = Math.max(max, map.get(key));
}
ans = Math.max(ans, max + 1);
}
return ans;
}
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
}

C++ 代码:


class Solution {
public:
int maxPoints(vector<vector<int>>& points) {
int n = points.size(), ans = 1;
for (int i = 0; i < n; i++) {
map<string, int> map;
int maxv = 0;
for (int j = i + 1; j < n; j++) {
int x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
int a = x1 - x2, b = y1 - y2;
int k = gcd(a, b);
string key = to_string(a / k) + "_" + to_string(b / k);
map[key]++;
maxv = max(maxv, map[key]);
}
ans = max(ans, maxv + 1);
}
return ans;
}
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
};

Python 代码:


class Solution:
def maxPoints(self, points):
def gcd(a, b):
return a if b == 0 else gcd(b, a % b)

n, ans = len(points), 1
for i in range(n):
mapping = {}
maxv = 0
for j in range(i + 1, n):
x1, y1 = points[i]
x2, y2 = points[j]
a, b = x1 - x2, y1 - y2
k = gcd(a, b)
key = str(a // k) + "_" + str(b // k)
mapping[key] = mapping.get(key, 0) + 1
maxv = max(maxv, mapping[key])
ans = max(ans, maxv + 1)
return ans

TypeScript 代码:


function maxPoints(points: number[][]): number {
const gcd = function(a: number, b: number): number {
return b == 0 ? a : gcd(b, a % b);
}
let n = points.length, ans = 1;
for (let i = 0; i < n; i++) {
let mapping = {}, maxv = 0;
for (let j = i + 1; j < n; j++) {
let x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
let a = x1 - x2, b = y1 - y2;
let k = gcd(a, b);
let key = `${a / k}_${b / k}`;
mapping[key] = mapping[key] ? mapping[key] + 1 : 1;
maxv = Math.max(maxv, mapping[key]);
}
ans = Math.max(ans, maxv + 1);
}
return ans;
};


  • 时间复杂度:枚举所有直线的复杂度为 O(n2)O(n^2);令坐标值的最大差值为 mmgcd 复杂度为 O(logm)O(\log{m})。整体复杂度为 O(n2×logm)O(n^2 \times \log{m})

  • 空间复杂度:O(n)O(n)


总结


虽然题目是以初中数学中的"斜率 & 截距"为背景,但仍有不少细节需要把握。


这也是「传统数学题」和「计算机算法题」的最大差别:



  • 过程分值: 传统数学题有过程分,计算机算法题没有过程分,哪怕思路对了 9090%,代码没写出来,就是 00 分;

  • 数据类型:传统数学题只涉及数值,计算机算法题需要考虑各种数据类型;

  • 运算精度:传统数学题无须考虑运算精度问题,而计算机算法题需要;

  • 判定机制:传统数学题通常给定具体数据和问题,然后人工根据求解过程和最终答案来综合评分,而计算机算法题不仅仅是求解一个具体的 case,通常是给定数据范围,然后通过若个不同的样例,机器自动判断程序的正确性;

  • 执行效率/时空复杂度:传统数学题无须考虑执行效率问题,只要求考生通过有限步骤(或引用定理节省步骤)写出答案即可,计算机算法题要求程序在有限时间空间内执行完;

  • 边界/异常处理:由于传统数学题的题面通常只有一个具体数据,因此不涉及边界处理,而计算机算法题需要考虑数据边界,甚至是对异常的输入输出做相应处理。


可见,传统数学题,有正确的思路基本上就赢了大半,而计算机算法题嘛,有正确思路,也只是万里长征跑了个 400400 米而已。


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7312035308362039346
收起阅读 »

这次被 foreach 坑惨了,再也不敢乱用了...

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码) <...
继续阅读 »

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)


<insert id="batchInsert" parameterType="java.util.List">  
insert int0 USER (id, name) values
<foreach collection="list" item="model" index="index" separator=",">
(#{model.id}, #{model.name})
</foreach>
</insert>

这个方法提升批量插入速度的原理是,将传统的:


INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");  
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");

转化为:


INSERT INTO `table1` (`field1`, `field2`)   
VALUES ("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2");

在MySql Docs中也提到过这个trick,如果要优化插入速度时,可以将许多小型操作组合到一个大型操作中。理想情况下,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。


乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。在资料中也提到了一句话:



Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it int0 smaller sizes.



它强调,当插入数量很多时,不能一次性全放在一条语句里。可是为什么不能放在同一条语句里呢?这条语句为什么会耗时这么久呢?我查阅了资料发现:



Insert inside Mybatis foreach is not batch, this is a single (could become giant) SQL statement and that brings drawbacks:


some database such as Oracle here does not support.


in relevant cases: there will be a large number of records to insert and the database configured limit (by default around 2000 parameters per statement) will be hit, and eventually possibly DB stack error if the statement itself become too large.


Iteration over the collection must not be done in the mybatis XML. Just execute a simple Insertstatement in a Java Foreach loop. The most important thing is the session Executor type.


SqlSession session = sessionFactory.openSession(ExecutorType.BATCH);

for (Model model : list) {

session.insert("insertStatement", model);

}

session.flushStatements();


Unlike default ExecutorType.SIMPLE, the statement will be prepared once and executed for each record to insert.



从资料中可知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。


在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。



Internally, it still generates the same single insert statement with many placeholders as the JDBC code above.


MyBatis has an ability to cache PreparedStatement, but this statement cannot be cached because it containselement and the statement varies depending on the parameters. As a result, MyBatis has to 1) evaluate the foreach part and 2) parse the statement string to build parameter mapping [1] on every execution of this statement. And these steps are relatively costly process when the statement string is big and contains many placeholders.


[1] simply put, it is a mapping between placeholders and the parameters.



从上述资料可知,耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。



图片


所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。


重点来了。上面讲的是,如果非要用的方式来插入,可以提升性能的方式。而实际上,MyBatis文档中写批量插入的时候,是推荐使用另外一种方法。(可以看

http://www.mybatis.org/mybatis-dyn… 中 Batch Insert Support 标题里的内容)


SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);  
try {
SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
List<SimpleTableRecord> records = getRecordsToInsert(); // not shown

BatchInsert<SimpleTableRecord> batchInsert = insert(records)
.int0(simpleTable)
.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
.map(birthDate).toProperty("birthDate")
.map(employed).toProperty("employed")
.map(occupation).toProperty("occupation")
.build()
.render(RenderingStrategy.MYBATIS3);

batchInsert.insertStatements().stream().forEach(mapper::insert);

session.commit();
} finally {
session.close();
}

即基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。


Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");  
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
"insert int0 tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
ps.setString(1,name);
ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

经过试验,使用了 ExecutorType.BATCH 的插入方式,性能显著提升,不到 2s 便能全部插入完成。


总结一下


如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用  的插入的话,需要将每次插入的记录控制在 20~50 左右。


作者:Java小虫
来源:juejin.cn/post/7220611580193964093
收起阅读 »

万事不要急,不行就抓一个包嘛!

一、网络问题分析思路1、问题现象确认问题现象是什么丢包/访问不通/延迟大/拒绝访问/传输速度2、报文特征过滤找到与问题现象相符的报文3、问题原因根据报文特征和发生位置,推断问题原因安全组/iptables/服务问题4、发生位置分析异常报文,判断问题发生的位置客...
继续阅读 »

一、网络问题分析思路

1、问题现象

确认问题现象是什么

丢包/访问不通/延迟大/拒绝访问/传输速度

2、报文特征

过滤找到与问题现象相符的报文

3、问题原因

根据报文特征和发生位置,推断问题原因

安全组/iptables/服务问题

4、发生位置

分析异常报文,判断问题发生的位置

客户端/服务端/中间链路.....

二、关注报文字段

  1. time:访问延迟
  2. source:来源IP
  3. destination:目的IP
  4. Protocol:协议
  5. length:长度
  6. TTL:存活时间(Time To Live)国内运营商劫持,封禁等处理
  7. payload:TCP包的长度

三、本机抓包

curl http://www.baidu.com

为什么不使用ping请求?

因为ping请求是ICMP请求:用于在 IP 网络上进行错误报告、网络诊断和网络管理。而curl请求是HTTP GET 请求。

追踪TCP流,可以看到三次捂手——》数据交互——》四次挥手

image-20231211161849484

image-20231211161958078

四、案例 TTL异常的reset

业务场景: 客户端通过公网去访问云上的CVM的一个公网IP,发现不能访问

image-20231211170925943

image-20231211171135366

TTL突发过长——》判断云上资源有无策略上的设置——》公网端判断原因运营商的封堵(TTL 200以下)

解决方案:报障运营商

四、案例 访问延迟-判断延时出现位置

业务场景: 客户同VPS内网,两CVM互相访问

image-20231211173214992

image-20231211173103998

目的端回包0.006毫秒——》链路无问题,回包时间过长——》CVM底层调用逻辑问题

五、案例 丢包

业务场景: 云上CVM访问第三网方网站失败,影响业务

image-20231211175015273

五、案例 未回复http响应

业务场景:云上CVM访问第三网方网站失败,影响业务

image-20231211175432145

image-20231211175702839

ping正常,拨测正常——》客户端发送HTTP请求,目的端无HTTP回包,直接三次挥手——》确认根因服务端不回复我们——》推测:运营商封禁或者第三方网站设置了访问限制

六、基础&常见问题

1、了解TCP/IP四层协议

image-20231018154301479

2、了解TCP 三次握手与四次挥手

SYN:同步位。SYN=1,表示进行一个连接请求。

ACK:确认位。ACK=1,确认有效;ACK=0,确认无效;。

ack:确认号。对方发送序号 + 1

seq:序号

image-20231211111546472

image-20231018155125516

FIN=1,断开链接,并且客户端会停止向服务器端发数据

image-20231211112144895

image-20231018155211125

3、Http传输段构成(浏览器的请求内容有哪些,F12中Network)

请求:

  1. 请求行(Request Line):包含HTTP方法(GET、POST等)、请求的URL和HTTP协议的版本。
  2. 请求头部(Request Headers):包含关于请求的附加信息,如用户代理、内容类型、授权信息等。
  3. 空行(Blank Line):请求头部和请求体之间必须有一个空行。
  4. 请求体(Request Body):可选的,用于传输请求的数据,例如在POST请求中传递表单数据或上传文件。

响应:

  1. 状态行(Status Line):包含HTTP协议的版本、状态码和状态消息。
  2. 响应头部(Response Headers):包含关于响应的附加信息,如服务器类型、内容类型、响应时间等。
  3. 空行(Blank Line):响应头部和响应体之间必须有一个空行。
  4. 响应体(Response Body):包含实际的响应数据,例如HTML页面、JSON数据等。

这些组成部分共同构成了HTTP传输过程中的请求和响应。请求由客户端发送给服务器,服务器根据请求进行处理并返回相应的响应给客户端。

4、DNS污染怎么办

DNS污染是指恶意篡改或劫持DNS解析的过程,导致用户无法正确访问所需的网站或被重定向到错误的网站。如果您怀疑遭受了DNS污染,可以尝试以下方法来解决问题:

1、更改本地的DNS。 公共DNS服务器Google Public DNS(8.8.8.8和8.8.4.4)

2、清理本地DNS缓存。 systemctl restart NetworkManager。这将重启NetworkManager服务,刷新DNS缓存并应用任何新的DNS配置。

3、使用HTTPS。使用HTTPS访问网站可以提供更安全的连接,并减少DNS污染的风险。确保您访问的网站使用HTTPS协议。

5、traceroute路由追踪跳转过程

img

当您运行traceroute http://www.baidu.com命令时,它将显示从您的计算机到目标地址(http://www.baidu.com)之间的网络路径。它通过发送一系列的网络探测包(ICMP或UDP)来确定数据包从源到目标的路径,并记录每个中间节点的IP地址。

以下是一般情况下,traceroute命令可能经过的地址类型:

  1. 您的本地网络地址:这是您计算机所连接的本地网络的IP地址。
  2. 网关地址:如果您的计算机连接到一个局域网,它将经过您的网络网关,这是连接您的局域网与外部网络的设备。traceroute命令将显示网关的IP地址。
  3. ISP(互联网服务提供商)的路由器地址:数据包将通过您的ISP的网络传输,经过多个路由器。traceroute命令将显示每个路由器的IP地址。
  4. 目标地址:最后,数据包将到达目标地址,即http://www.baidu.com的IP地址。

img

6、Http状态码

100-199表示请求已被接收,继续处理比如:websocket_VScode自动刷新页面
200-299表示请求已成功被服务器接收、理解和处理。200 OK:请求成功,服务器成功返回请求的数据。 201 Created:请求成功,服务器已创建新的资源。204 No Content:请求成功,但服务器没有返回任何内容。
300-399(重定向状态码)表示需要进一步操作以完成请求301 Moved Permanently:请求的资源已永久移动到新位置。302 Found:请求的资源暂时移动到新位置。304 Not Modified:客户端的缓存资源是最新的,服务器返回此状态码表示资源未被修改。
400-499表示客户端发送的请求有错误。400 Bad Request:请求无效,服务器无法理解。401 Unauthorized:请求要求身份验证。404 Not Found:请求的资源不存在。
500-599表示服务器在处理请求时发生错误。500 Internal Server Error:服务器遇到了意外错误(服务器配置出错/数据库错误/代码出错),无法完成请求。503 Service Unavailable:服务器暂时无法处理请求,通常是由于过载或维护。

7、CDN内容加速网络原理

原理:减少漫长的路由转发,就近访问备份资源

1、通过配置网站的CDN,提前让CDN的中间节点OC备份一份内容,在分发给用户侧的SOC边缘节点,这样就能就近拉取资源。不用每次都通过漫长的路由导航到源站。

2、但是要达到加速的效果,还需要把真实域名的IP更改到CDN的IP,所有这里还需要DNS的帮助,这里一般都会求助用户本地运营商搭建的权威DNS域名解析服务器,用户请求逐级请求各级域名,本来应该会返回真实的IP地址,但是通过配置会返回给用户一个CDN的IP地址,CDN的权威服务器再讲距离用户最近的那台CDN服务器IP地址返回给用户,这样就实现了CDN加速的效果。

8、前后端通信到底是怎样一个过程

链接参考:juejin.cn/post/728518…

内容参考:

小林coding:xiaolincoding.com/network/

哔哩哔哩:http://www.bilibili.com/video/BV1at…

Winshark:http://www.wireshark.org/


作者:武师叔
来源:juejin.cn/post/7311159008483983369

收起阅读 »

京东一面:post为什么会发送两次请求?🤪🤪🤪

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
继续阅读 »

在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


那么接下来这篇文章我们就一点一点开始引出这个问题。


同源策略


在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



  • 跨站脚本攻击(XSS)

  • SQL 注入攻击

  • OS 命令注入攻击

  • HTTP 首部注入攻击

  • 跨站点请求伪造(CSRF)

  • 等等......


如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


这就引出了最基础、最核心的安全策略:同源策略。


什么是同源策略


同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



  • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

  • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

  • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


URL结果原因
http://store.company.com:80/dir2/page.html同源只有路径不同
http://store.company.com:80/dir/inner/another.html同源只有路径不同
https://store.company.com:443/secure.html不同源协议不同,HTTP 和 HTTPS
http://store.company.com:81/dir/etc.html不同源端口不同
http://news.company.com:80/dir/other.html不同源主机不同

同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



  • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

  • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

  • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


CORS


对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


简单请求


不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



  1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

  2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

  3. 请求中没有使用 ReadableStream 对象。

  4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

  5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


预检请求


非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


例如我们在掘金上删除一条沸点:


20230822094049


它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

  • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

  • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


20230822122441


上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


附带身份凭证的请求与通配符


在响应附带身份凭证的请求时:



  • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

  • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

  • 对于附带身份凭证的请求(通常是 Cookie),


这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


为什么本地使用 webpack 进行 dev 开发时,不需要服务器端配置 cors 的情况下访问到线上接口?


当你在本地通过 Ajax 或其他方式请求线上接口时,由于浏览器的同源策略,会出现跨域的问题。但是在服务器端并不会出现这个问题。


它是通过 Webpack Dev Server 来实现这个功能。当你在浏览器中发送请求时,请求会先被 Webpack Dev Server 捕获,然后根据你的代理规则将请求转发到目标服务器,目标服务器返回的数据再经由 Webpack Dev Server 转发回浏览器。这样就绕过了浏览器的同源策略限制,使你能够在本地开发环境中访问线上接口。


参考文章



总结


预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


整个完整的请求流程有如下图所示:


20230822122544


最后分享两个我的两个开源项目,它们分别是:



这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰


作者:Moment
来源:juejin.cn/post/7269952188927017015
收起阅读 »

技术总监写的十个方法,让我精通了lambda表达式

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。 我自己照着写了一遍,改了名字,分享给大家。 一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如 将 C...
继续阅读 »

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。


我自己照着写了一遍,改了名字,分享给大家。


一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


Collection 集合类型到 Map类型的转化。


Collection 转化为 Map


由于 List 和 Set 是 Collection 类型的子类,所以只需要实现Collection 类型转化为 Map 类型即可。
Collection转化为 Map 共分两个方法



  1. Collection<OrderItem> Map<Key, OrderItem>,提取 Key, Map 的 Value 就是类型 OrderItem

  2. Collection<OrderItem>Map<Key,Value> ,提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。


使用样例


代码示例中把Set<OrderItem> 转化为 Map<Long, OrderItem>Map<Long ,Double>


@Test
public void testToMap() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);
}

@Test
public void testToMapV2() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, Double> map = toMap(set, OrderItem::getOrderId, OrderItem::getActPrice);
}

代码展示


public static <T, K> Map<K, T> toMap(Collection<T> collection, Function<? super T, ? extends K> keyMapper) {
return toMap(collection, keyMapper, Function.identity());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction)
{
return toMap(collection, keyFunction, valueFunction, pickSecond());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction,
BinaryOperator<V> mergeFunction)
{
if (CollectionUtils.isEmpty(collection)) {
return new HashMap<>(0);
}

return collection.stream().collect(Collectors.toMap(keyFunction, valueFunction, mergeFunction));
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

Map格式转换


转换 Map 的 Value



  1. 将 Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  2. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }。


测试样例


@Test
public void testConvertValue() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);

Map<Long, Double> orderId2Price = convertMapValue(map, item -> item.getActPrice());
Map<Long, String> orderId2Token = convertMapValue(map, (id, item) -> id + item.getName());

}

代码展示


public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> map, 
BiFunction<K, V, C> valueFunction,
BinaryOperator<C> mergeFunction)
{
if (isEmpty(map)) {
return new HashMap<>();
}
return map.entrySet().stream().collect(Collectors.toMap(
e -> e.getKey(),
e -> valueFunction.apply(e.getKey(), e.getValue()),
mergeFunction
));
}

public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> originMap, BiFunction<K, V, C> valueConverter) {
return convertMapValue(originMap, valueConverter, Lambdas.pickSecond());
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

集合类型转化


Collection 和 List、Set 的转化



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>


public static <T> List<T> toList(Collection<T> collection) {
if (collection == null) {
return new ArrayList<>();
}
if (collection instanceof List) {
return (List<T>) collection;
}
return collection.stream().collect(Collectors.toList());
}

public static <T> Set<T> toSet(Collection<T> collection) {
if (collection == null) {
return new HashSet<>();
}
if (collection instanceof Set) {
return (Set<T>) collection;
}
return collection.stream().collect(Collectors.toSet());
}

测试样例


@Test//将集合 Collection 转化为 List
public void testToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);
}

@Test//将集合 Collection 转化为 Set
public void testToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);
}

List和 Set 是 Collection 集合类型的子类,所以无需再转化。


List、Set 类型之间的转换


业务中有时候需要将 List<A> 转化为 List<B>。如何实现工具类呢?


public static <T, R> List<R> map(List<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> map(Set<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

public static <T, R> List<R> mapToList(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> mapToSet(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

测试样例



  1. List<OrderItem> 转化为 List<Long>

  2. Set<OrderItem> 转化为 Set<Long>

  3. Collection<OrderItem> 转化为 List<Long>

  4. Collection<OrderItem> 转化为 Set<Long>


@Test
public void testMapToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);

List<Long> orderIdList = map(list, (item) -> item.getOrderId());
}

@Test
public void testMapToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(coll);

Set<Long> orderIdSet = map(set, (item) -> item.getOrderId());
}

@Test
public void testMapToList2() {
Collection<OrderItem> collection = coll;

List<Long> orderIdList = mapToList(collection, (item) -> item.getOrderId());
}

@Test
public void testMapToSetV2() {
Collection<OrderItem> collection = coll;

Set<Long> orderIdSet = mapToSet(collection, (item) -> item.getOrderId());

}

总结一下 以上样例包含了如下的映射场景



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


作者:五阳神功
来源:juejin.cn/post/7305572311812587531
收起阅读 »

推荐一个小而全的第三方登录开源组件

大家好,我是 Java陈序员。 我们在企业开发中,常常需要实现登录功能,而有时候为了方便,就需要集成第三方平台的授权登录。如常见的微信登录、微博登录等,免去了用户注册步骤,提高了用户体验。 为了业务考虑,我们有时候集成的不仅仅是一两个第三方平台,甚至更多。这就...
继续阅读 »

大家好,我是 Java陈序员


我们在企业开发中,常常需要实现登录功能,而有时候为了方便,就需要集成第三方平台的授权登录。如常见的微信登录、微博登录等,免去了用户注册步骤,提高了用户体验。


为了业务考虑,我们有时候集成的不仅仅是一两个第三方平台,甚至更多。这就会大大的提高了工作量,那么有没有开源框架来统一来集成这些第三方授权登录呢?


答案是有的,今天给大家介绍的项目提供了一个第三方授权登录的工具类库


项目介绍


JustAuth —— 一个第三方授权登录的工具类库,可以让你脱离繁琐的第三方登录 SDK,让登录变得So easy!


JustAuth


JustAuth 集成了诸如:Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么、推特、飞书、京东、阿里云、喜马拉雅、Amazon、Slack和 Line 等第三方平台的授权登录。


功能特色:



  • 丰富的 OAuth 平台:支持国内外数十家知名的第三方平台的 OAuth 登录。

  • 自定义 state:支持自定义 State 和缓存方式,开发者可根据实际情况选择任意缓存插件。

  • 自定义 OAuth:提供统一接口,支持接入任意 OAuth 网站,快速实现 OAuth 登录功能。

  • 自定义 Http:接口 HTTP 工具,开发者可以根据自己项目的实际情况选择相对应的HTTP工具。

  • 自定义 Scope:支持自定义 scope,以适配更多的业务场景,而不仅仅是为了登录。

  • 代码规范·简单:JustAuth 代码严格遵守阿里巴巴编码规约,结构清晰、逻辑简单。


安装使用


回顾 OAuth 授权流程


参与的角色



  • Resource Owner 资源所有者,即代表授权客户端访问本身资源信息的用户(User),也就是应用场景中的“开发者A”

  • Resource Server 资源服务器,托管受保护的用户账号信息,比如 Github
    Authorization Server 授权服务器,验证用户身份然后为客户端派发资源访问令牌,比如 Github

  • Resource ServerAuthorization Server 可以是同一台服务器,也可以是不同的服务器,视具体的授权平台而有所差异

  • Client 客户端,即代表意图访问受限资源的第三方应用


授权流程


OAuth 授权流程


使用步骤


1、申请注册第三方平台的开发者账号


2、创建第三方平台的应用,获取配置信息(accessKey, secretKey, redirectUri)


3、使用 JustAuth 实现授权登陆


引入依赖


<dependency>
<groupId>me.zhyd.oauthgroupId>
<artifactId>JustAuthartifactId>
<version>{latest-version}version>
dependency>

调用 API


// 创建授权request
AuthRequest authRequest = new AuthGiteeRequest(AuthConfig.builder()
.clientId("clientId")
.clientSecret("clientSecret")
.redirectUri("redirectUri")
.build());
// 生成授权页面
authRequest.authorize("state");
// 授权登录后会返回code(auth_code(仅限支付宝))、state,1.8.0版本后,可以用AuthCallback类作为回调接口的参数
// 注:JustAuth默认保存state的时效为3分钟,3分钟内未使用则会自动清除过期的state
authRequest.login(callback);


说明:
JustAuth 的核心就是一个个的 request,每个平台都对应一个具体的 request 类。
所以在使用之前,需要就具体的授权平台创建响应的 request.如示例代码中对应的是 Gitee 平台。



集成国外平台



国外平台需要额外配置 httpConfig



AuthRequest authRequest = new AuthGoogleRequest(AuthConfig.builder()
.clientId("Client ID")
.clientSecret("Client Secret")
.redirectUri("应用回调地址")
// 针对国外平台配置代理
.httpConfig(HttpConfig.builder()
// Http 请求超时时间
.timeout(15000)
// host 和 port 请修改为开发环境的参数
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 10080)))
.build())
.build());

SpringBoot 集成


引入依赖


<dependency>
<groupId>com.xkcoding.justauthgroupId>
<artifactId>justauth-spring-boot-starterartifactId>
<version>1.4.0version>
dependency>

配置文件


justauth:
enabled: true
type:
QQ:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/qq/callback
union-id: false
WEIBO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/weibo/callback
GITEE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/gitee/callback
DINGTALK:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/dingtalk/callback
BAIDU:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/baidu/callback
CSDN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/csdn/callback
CODING:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/coding/callback
coding-group-name: xx
OSCHINA:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/oschina/callback
ALIPAY:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/alipay/callback
alipay-public-key: MIIB**************DAQAB
WECHAT_OPEN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_open/callback
WECHAT_MP:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_mp/callback
WECHAT_ENTERPRISE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/wechat_enterprise/callback
agent-id: 1000002
TAOBAO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/taobao/callback
GOOGLE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/google/callback
FACEBOOK:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/facebook/callback
DOUYIN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/douyin/callback
LINKEDIN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/linkedin/callback
MICROSOFT:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/microsoft/callback
MI:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/mi/callback
TOUTIAO:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/toutiao/callback
TEAMBITION:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/teambition/callback
RENREN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/renren/callback
PINTEREST:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/pinterest/callback
STACK_OVERFLOW:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/stack_overflow/callback
stack-overflow-key: asd*********asd
HUAWEI:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/huawei/callback
KUJIALE:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/kujiale/callback
GITLAB:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/gitlab/callback
MEITUAN:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/meituan/callback
ELEME:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/eleme/callback
TWITTER:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/twitter/callback
XMLY:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/xmly/callback
# 设备唯一标识ID
device-id: xxxxxxxxxxxxxx
# 客户端操作系统类型,1-iOS系统,2-Android系统,3-Web
client-os-type: 3
# 客户端包名,如果 clientOsType 为12时必填。对Android客户端是包名,对IOS客户端是Bundle ID
#pack-id: xxxx
FEISHU:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/feishu/callback
JD:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: http://oauth.xkcoding.com/demo/oauth/jd/callback
cache:
type: default

代码使用


@Slf4j
@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class TestController {
private final AuthRequestFactory factory;

@GetMapping
public List list() {
return factory.oauthList();
}

@GetMapping("/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}

@RequestMapping("/{type}/callback")
public AuthResponse login(@PathVariable String type, AuthCallback callback) {
AuthRequest authRequest = factory.get(type);
AuthResponse response = authRequest.login(callback);
log.info("【response】= {}", JSONUtil.toJsonStr(response));
return response;
}

}

总结


JustAuth 集成的第三方授权登录平台,可以说是囊括了业界中大部分主流的应用系统。如国内的微信、微博、Gitee 等,还有国外的 Github、Google 等。可以满足我们日常的开发需求,开箱即用,可快速集成!


最后,贴上项目地址:


https://github.com/justauth/JustAuth

在线文档:


https://www.justauth.cn/

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/

作者:Java陈序员
来源:juejin.cn/post/7312060958175559743
收起阅读 »

token过期了怎么办?

token过期了怎么办?一般做法是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。那应该怎么办呢?其实这是一个老生常谈的问题,但是最近发现很多人并不清楚,所以今天就一次讲清这...
继续阅读 »

token过期了怎么办?一般做法是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。那应该怎么办呢?其实这是一个老生常谈的问题,但是最近发现很多人并不清楚,所以今天就一次讲清这个问题!

token 过期处理

没有绝对的安全, 所谓的安全处理, 就是提高攻击者攻击的难度, 对他造成了一定的麻烦, 我们这个网站就是安全的! 网站安全性就是高的! 所以: token 必须要有过期时间!

token 过期问题

目标: 了解token过期问题的存在, 学习token过期的解决思路

现象:

你登陆成功之后,接口会返回一个token值,这个值在后续请求时带上(就像是开门钥匙)。

但是,这个值一般会有有效期(具体是多长,是由后端决定),在我们的项目中,这个有效期是2小时。

如果,上午8点登陆成功,到了10:01分,则token就会失效,再去发请求时,就会报401错误。

思考:

  1. token需要过期时间吗 ?

    token即是获取受保护资源的凭证,当然必须有过期时间。否则一次登录便可永久使用,认证功能就失去了其意义。非但必须有个过期时间,而且过期时间还不能太长,

    参考各个主流网站的token过期时间,一般1小时左右

    token一旦过期, 一定要处理, 不处理, 用户没法进行一些需要授权页面的使用了

  2. token过期该怎么办?

    token过期,就要重新获取。

    那么重新获取有两种方式,一是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。

    那么还剩第二种方法,那就是主动去刷新token. 主动刷新token的凭证是refresh token,也是加密字符串,并且和token是相关联的。相比可以获取各种资源的token,refresh token的作用仅仅是获取新的token,因此其作用和安全性要求都大为降低,所以其过期时间也可以设置得长一些。

目标效果 - 保证每一小时, 都是一个不同的token

第一次请求 9:00 用的是 token1  第二次请求 12:00 用的是 token2

当用户登陆成功之后,返回的token中有两个值,说明如下:

image.png

  • token:

    • 作用:在访问一些接口时,需要传入token,就是它。
    • 有效期:2小时。
  • refresh_token

    • 作用: 当token的有效期过了之后,可以使用它去请求一个特殊接口(这个接口也是后端指定的,明确需要传入refresh_token),并返回一个新的token回来(有效期还是2小时),以替换过期的那个token。
    • 有效期:14天。(最理想的情况下,一次登陆可以持续14天。)

image.png

对于 某次请求A 的响应,如果是401错误

  • 有refresh_token,用refresh_token去请求回新的token

    • 新token请求成功

      • 更新本地token
      • 再发一次请求A
    • 新token请求失败

      • 清空vuex中的token
      • 携带请求地址,跳转到登陆页
  • 没有refresh_token

    • 清空vuex中的token
    • 携带请求地址,跳转到登陆页

对于一个请求的响应 401, 要这么处理, 对于十个请求的响应 401, 也要这么处理,

我们可以统一将这个token过期处理放在响应拦截器中

请求拦截器: 所有的请求, 在真正被发送出去之前, 都会先经过请求拦截器 (可以携带token)

响应拦截器: 所有的响应, 在真正被(.then.catch await)处理之前, 都会先经过响应拦截器, 可以在这个响应拦截器中统一对响应做判断

响应拦截器处理token

目标: 通过 axios 响应拦截器来处理 token 过期的问题

响应拦截器: http://www.kancloud.cn/yunye/axios…

  1. 没有 refresh_token 拦截到登录页, 清除无效的token

测试: {"token":"123.123.123"}

// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 对响应数据做点什么 (成功响应) response 就是成功的响应 res
return response
}, function (error) {
// 对响应错误做点什么 (失败响应) 处理401错误
// console.dir(error)
if (error.response.status === 401) {
console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
// 获取 refresh_token, 判断是否存在, 存在就去刷新token
const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
console.log('存在refreshToken, 需要进行刷新token操作')
} else {
// 没有refreshToken, 直接去登录, 将来还能跳回来
// router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
// 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
store.commit('removeToken')
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
}
return Promise.reject(error)
})

提供清除token的mutation

// 移出tokenInfo的信息, 恢复成空对象
removeToken (state) {
state.tokenInfo = {}
// 更新到本地, 本地可以清掉token信息
removeToken()
},
  1. 有 refresh_token 发送请求, 刷新token

测试操作: 将 token 修改成 xyz, 模拟 token 过期, 而有 refresh_token 发现401, 会自动帮你刷新token

{"refresh_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDYzNTcyODcsInVzZXJfaWQiOjExMDI0OTA1MjI4Mjk3MTc1MDQsInJlZnJlc2giOnRydWV9.2A81gpjxP_wWOjclv0fzSh1wzNm6lNy0iXM5G5l7TQ4","token":"xyz"}

const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
console.log('存在refreshToken, 需要进行刷新token操作')
// (1) 发送请求, 进行刷新token操作, 获取新的token
// 注意: 这边发请求, 不用http实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
// 这边, 直接用 axios 发送请求
const res = await axios({
method: 'put',
url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
// 请求头中携带refresh_token信息
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
const newToken = res.data.data.token
// (2) 将新token更新到vuex中
store.commit('setTokenInfo', {
refresh_token: refreshToken,
token: newToken
})
}
  1. 刷新token后, 应该重新发送刚才的请求 (让用户刷新token无感知)
return http(error.config)
  1. 那万一 refresh_token 也过期了, 是真正的用户登录过期了 (一定要让用户重新登录的)

测试: {"refresh_token":"123.123","token":"123.123.123"} 修改后, 修改的是本地, 记得刷新一下

从哪拦走的, 就回到哪去

// 添加响应拦截器
http.interceptors.response.use(function (response) {
// 对响应数据做点什么 (成功响应) response 就是成功的响应 res
return response
}, async function (error) {
// 对响应错误做点什么 (失败响应) 处理401错误
// console.dir(error)
if (error.response.status === 401) {
console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
// 获取 refresh_token, 判断是否存在, 存在就去刷新token
const refreshToken = store.state.tokenInfo.refresh_token
if (refreshToken) {
try {
console.log('存在refreshToken, 需要进行刷新token操作')
// (1) 发送请求, 进行刷新token操作, 获取新的token
// 注意: 这边发请求, 不用http实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
// 这边, 直接用 axios 发送请求
const res = await axios({
method: 'put',
url: 'http://ttapi.research.itcast.cn/app/v1_0/authorizations',
// 请求头中携带refresh_token信息
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
const newToken = res.data.data.token
// (2) 将新token更新到vuex中
store.commit('setTokenInfo', {
refresh_token: refreshToken,
token: newToken
})
// (3) 重新发送刚才的请求, http, 自动携带token (携带的是新token)
// error.config就是之前用于请求的配置对象, 可以直接给http使用
return http(error.config)
} catch {
// refresh_token 过期了, 跳转到登录页
// 清除过期的token对象
store.commit('removeToken')
// 跳转到登录页, 跳转完, 将来跳回来
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
} else {
// 没有refreshToken, 直接去登录, 将来还能跳回来
// router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
// 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
store.commit('removeToken')
router.push({
path: '/login',
query: {
backto: router.currentRoute.fullPath
}
})
}
}
return Promise.reject(error)
})

注意点:

  1. 响应拦截器要加在axios实例 http 上。
  2. 用refresh_token请求新token时,要用axios,不要用实例 http (需要: 手动用 refresh_token 请求)
  3. 得到新token之后,再发请求时,要用 http 实例 (用token请求)
  4. 过期的 token 可以用 refresh_token 再次更新获取新token, 但是过期的 refresh_token 就应该从清除了


作者:JoyZ
来源:juejin.cn/post/7308992811449172005
收起阅读 »

工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
继续阅读 »

前言



哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



正文



不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




这种写法埋了一个不大不小的雷。




用一段测试代码就可以展示出来问题



1.jpg



打印结果如下:



2.jpg



很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




比如我如果换成2023-12-30又不会有问题了



3.jpg



另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



4.jpg



避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



总结




  1. 日期时间格式统一使用yyyy小写;

  2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人了。



作者:程序员济癫
来源:juejin.cn/post/7269013062677823528
收起阅读 »

拼多多算法题,是清华考研真题!

写在前面 在 LeetCode 上有一道"备受争议"的题目。 该题长期作为 拼多多题库中的打榜题 : 据同学们反映,该题还是 清华大学 和 南京大学 考研专业课中的算法题。 其中南京大学的出题人,还真贴心地针对不同解法,划分不同分值: 细翻评论区。 不...
继续阅读 »

写在前面


在 LeetCode 上有一道"备受争议"的题目。


该题长期作为 拼多多题库中的打榜题


出现频率拉满


据同学们反映,该题还是 清华大学南京大学 考研专业课中的算法题。



其中南京大学的出题人,还真贴心地针对不同解法,划分不同分值:




细翻评论区。


不仅是拼多多,该题还在诸如 神州信息滴滴出行 这样的互联网大厂笔试中出现过:





但,这都不是这道题"备受争议"的原因。


这道题最魔幻的地方是:常见解法可做到 O(n)O(n) 时间,O(1)O(1) 空间,而进阶做法最快也只能做到 O(n)O(n) 时间,O(logn)O(\log{n}) 空间


称作"反向进阶"也不为过。


接下来,我将从常规解法的两种理解入手,逐步进阶到考研/笔面中分值更高的进阶做法,帮助大家在这题上做到尽善尽美。


毕竟在一道算法题上做到极致,比背一段大家都会"八股文",在笔面中更显价值。


题目描述


平台:LeetCode


题号:LCR 161 或 53


给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。


子数组是数组中的一个连续部分。


示例 1:


输入:nums = [-2,1,-3,4,-1,2,1,-5,4]

输出:6

解释:连续子数组 [4,-1,2,1] 的和最大,为 6

示例 2:


输入:nums = [1]

输出:1

示例 3:


输入:nums = [5,4,-1,7,8]

输出:23

提示:



  • 1<=nums.length<=1051 <= nums.length <= 10^5

  • 104<=nums[i]<=104-10^4 <= nums[i] <= 10^4


进阶:如果你已经实现复杂度为 O(n)O(n) 的解法,尝试使用更为精妙的分治法求解。


前缀和 or 线性 DP


当要我们求「连续段」区域和的时候,要很自然的想到「前缀和」。


所谓前缀和,是指对原数组“累计和”的描述,通常是指一个与原数组等长的数组。


设前缀和数组为 sumsum 的每一位记录的是从「起始位置」到「当前位置」的元素和。


例如 sum[x]sum[x] 是指原数组中“起始位置”到“位置 x”这一连续段的元素和。


有了前缀和数组 sum,当我们求连续段 [i,j][i, j] 的区域和时,利用「容斥原理」,便可进行快速求解。


通用公式:ans = sum[j] - sum[i - 1]



由于涉及 -1 操作,为减少边界处理,我们可让前缀和数组下标从 11 开始。在进行快速求和时,再根据原数组下标是否从 11 开始,决定是否进行相应的下标偏移。


学习完一维前缀和后,回到本题。


先用 nums 预处理出前缀和数组 sum,然后在遍历子数组右端点 j 的过程中,通过变量 m 动态记录已访问的左端点 i 的前缀和最小值。最终,在所有 sum[j] - m 的取值中选取最大值作为答案。


代码实现上,我们无需明确计算前缀和数组 sum,而是使用变量 s 表示当前累计的前缀和(充当右端点),并利用变量 m 记录已访问的前缀和的最小值(充当左端点)即可。


本题除了将其看作为「前缀和裸题用有限变量进行空间优化」以外,还能以「线性 DP」角度进行理解。


定义 f[i]f[i] 为考虑前 ii 个元素,且第 nums[i]nums[i] 必选的情况下,形成子数组的最大和。


不难发现,仅考虑前 ii 个元素,且 nums[i]nums[i] 必然参与的子数组中。要么是 nums[i]nums[i] 自己一个成为子数组,要么与前面的元素共同组成子数组。


因此,状态转移方程:


f[i]=max(f[i1]+nums[i],nums[i])f[i] = \max(f[i - 1] + nums[i], nums[i])

由于 f[i]f[i] 仅依赖于 f[i1]f[i - 1] 进行转移,可使用有限变量进行优化,因此写出来的代码也是和上述前缀和角度分析的类似。


Java 代码:


class Solution {
public int maxSubArray(int[] nums) {
int s = 0, m = 0, ans = -10010;
for (int x : nums) {
s += x;
ans = Math.max(ans, s - m);
m = Math.min(m, s);
}
return ans;
}
}

C++ 代码:


class Solution {
public:
int maxSubArray(vector<int>& nums) {
int s = 0, m = 0, ans = -10010;
for (int x : nums) {
s += x;
ans = max(ans, s - m);
m = min(m, s);
}
return ans;
}
};

Python 代码:


class Solution:
def maxSubArray(self, nums: List[int]) -> int:
s, m, ans = 0, 0, -10010
for x in nums:
s += x
ans = max(ans, s - m)
m = min(m, s)
return ans

TypeScript 代码:


function maxSubArray(nums: number[]): number {
let s = 0, m = 0, ans = -10010;
for (let x of nums) {
s += x;
ans = Math.max(ans, s - m);
m = Math.min(m, s);
}
return ans;
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:O(1)O(1)


分治


“分治法”的核心思路是将大问题拆分成更小且相似的子问题,通过递归解决这些子问题,最终合并子问题的解来得到原问题的解。


实现分治,关键在于对“递归函数”的设计(入参 & 返回值)。


在涉及数组的分治题中,左右下标 lr 必然会作为函数入参,因为它能用于表示当前所处理的区间,即小问题的范围。


对于本题,仅将最大子数组和(答案)作为返回值并不足够,因为单纯从小区间的解无法直接推导出大区间的解,我们需要一些额外信息来辅助求解。


具体的,我们可以将返回值设计成四元组,分别代表 区间和前缀最大值后缀最大值最大子数组和,用 [sum, lm, rm, max] 表示。


有了完整的函数签名 int[] dfs(int[] nums, int l, int r),考虑如何实现分治:



  1. 根据当前区间 [l,r][l, r] 的长度进行分情况讨论:

    1. l=rl = r,只有一个元素,区间和为 nums[l]nums[l],而 最大子数组和、前缀最大值 和 后缀最大值 由于允许“空数组”,因此均为 max(nums[l],0)\max(nums[l], 0)

    2. 否则,将当前问题划分为两个子问题,通常会划分为两个相同大小的子问题,划分为 [l,mid][l, mid][mid+1,r][mid + 1, r] 两份,递归求解,其中 mid=l+r2mid = \left \lfloor \frac{l + r}2{} \right \rfloor




随后考虑如何用“子问题”的解合并成“原问题”的解:



  1. 合并区间和 (sum): 当前问题的区间和等于左右两个子问题的区间和之和,即 sum = left[0] + right[0]

  2. 合并前缀最大值 (lm): 当前问题的前缀最大值可以是左子问题的前缀最大值,或者左子问题的区间和加上右子问题的前缀最大值。即 lm = max(left[1], left[0] + right[1])

  3. 合并后缀最大值 (rm): 当前问题的后缀最大值可以是右子问题的后缀最大值,或者右子问题的区间和加上左子问题的后缀最大值。即 rm = max(right[2], right[0] + left[2])

  4. 合并最大子数组和 (max): 当前问题的最大子数组和可能出现在左子问题、右子问题,或者跨越左右两个子问题的边界。因此,max 可以通过 max(left[3], right[3], left[2] + right[1]) 来得到。


一些细节:由于我们在计算 lmrmmax 的时候允许数组为空,而答案对子数组的要求是至少包含一个元素。因此对于 nums 全为负数的情况,我们会错误得出最大子数组和为 0 的答案。针对该情况,需特殊处理,遍历一遍 nums,若最大值为负数,直接返回最大值。


Java 代码:


class Solution {
// 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
int[] dfs(int[] nums, int l, int r) {
if (l == r) {
int t = Math.max(nums[l], 0);
return new int[]{nums[l], t, t, t};
}
// 划分成两个子区间,分别求解
int mid = l + r >> 1;
int[] left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
int[] ans = new int[4];
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = Math.max(Math.max(left[3], right[3]), left[2] + right[1]); // 最大子数组和
return ans;
}
public int maxSubArray(int[] nums) {
int m = nums[0];
for (int x : nums) m = Math.max(m, x);
if (m <= 0) return m;
return dfs(nums, 0, nums.length - 1)[3];
}
}

C++ 代码:


class Solution {
public:
// 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
vector<int> dfs(vector<int>& nums, int l, int r) {
if (l == r) {
int t = max(nums[l], 0);
return {nums[l], t, t, t};
}
// 划分成两个子区间,分别求解
int mid = l + r >> 1;
auto left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
vector<int> ans(4);
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = max({left[3], right[3], left[2] + right[1]}); // 最大子数组和
return ans;
}
int maxSubArray(vector<int>& nums) {
int m = nums[0];
for (int x : nums) m = max(m, x);
if (m <= 0) return m;
return dfs(nums, 0, nums.size() - 1)[3];
}
};

Python 代码:


class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def dfs(l, r):
if l == r:
t = max(nums[l], 0)
return [nums[l], t, t, t]
# 划分成两个子区间,分别求解
mid = (l + r) // 2
left, right = dfs(l, mid), dfs(mid + 1, r)
# 组合左右子区间的信息,得到当前区间的信息
ans = [0] * 4
ans[0] = left[0] + right[0] # 当前区间和
ans[1] = max(left[1], left[0] + right[1]) # 当前区间前缀最大值
ans[2] = max(right[2], right[0] + left[2]) # 当前区间后缀最大值
ans[3] = max(left[3], right[3], left[2] + right[1]) # 最大子数组和
return ans

m = max(nums)
if m <= 0:
return m
return dfs(0, len(nums) - 1)[3]

TypeScript 代码:


function maxSubArray(nums: number[]): number {
const dfs = function (l: number, r: number): number[] {
if (l == r) {
const t = Math.max(nums[l], 0);
return [nums[l], t, t, t];
}
// 划分成两个子区间,分别求解
const mid = (l + r) >> 1;
const left = dfs(l, mid), right = dfs(mid + 1, r);
// 组合左右子区间的信息,得到当前区间的信息
const ans = Array(4).fill(0);
ans[0] = left[0] + right[0]; // 当前区间和
ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
ans[3] = Math.max(left[3], right[3], left[2] + right[1]); // 最大子数组和
return ans;
}

const m = Math.max(...nums);
if (m <= 0) return m;
return dfs(0, nums.length - 1)[3];
};


  • 时间复杂度:O(n)O(n)

  • 空间复杂度:递归需要函数栈空间,算法每次将当前数组一分为二,进行递归处理,递归层数为 logn\log{n},即函数栈最多有 logn\log{n} 个函数栈帧,复杂度为 O(logn)O(\log{n})


总结


虽然,这道题的进阶做法相比常规做法,在时空复杂度上没有优势。


但进阶做法的分治法更具有 进一步拓展 的价值,容易展开为支持「区间修改,区间查询」的高级数据结构 - 线段树。


实际上,上述的进阶「分治法」就是线段树的"建树"过程。


这也是为什么「分治法」在名校考研课中分值更大,在大厂笔面中属于必选解法的原因,希望大家重点掌握。


作者:宫水三叶的刷题日记
来源:juejin.cn/post/7310104657211293723
收起阅读 »

前端学一点Docker,不信你学不会

虽然前端很少跟docker打交道,但随着工作流程的自动化现代化,docker正变得越来越重要。无论你是希望扩展技能到全栈领域,还是想要炫技,掌握Docker基本知识都是前端小伙伴重要的一步。 什么是Docker Docker 是一个开源的应用容器引擎,可以让...
继续阅读 »

虽然前端很少跟docker打交道,但随着工作流程的自动化现代化,docker正变得越来越重要。无论你是希望扩展技能到全栈领域,还是想要炫技,掌握Docker基本知识都是前端小伙伴重要的一步。


什么是Docker



Docker 是一个开源的应用容器引擎,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。



我们知道,软件的安装是要区分系统环境的;即使是运行环境相同,依赖的版本一旦有所出入,也容易出现“我这里明明好使,你那里为啥不行“的问题。容器技术解决了环境一致性依赖管理的问题。


因此,容器是我们的项目的运行环境,而docker是容器的运行环境与管理平台。


关键词


镜像 (Image)


镜像是构建容器的模板,可以简单理解为类似Js中的class类或构造函数。


镜像中详细记录了应用所需的运行环境、应用代码、如何操作系统文件等等信息;


容器 (Container)


容器是镜像的运行实例。可以简单理解为”new 镜像()“的实例,通过docker命令可以任意创建容器。


当前操作系统(宿主机)与容器之间的关系,可以参照浏览器页面与iframe之间的关系。容器可以拥有独立的IP地址,网络,文件系统以及指令等;容器与容器、容器与”宿主机“之间以隔离的方式运行,每个容器中通常运行着一个(或多个)应用。


仓库 (Registry)


仓库是集中管理镜像的地方。类似于npm平台与npm包之间的关系。


如果我们将搭建项目环境的详细过程以及具体的依赖记录进镜像中,每当需要部署新服务时,就可以很容易的通过镜像,创建出一个个完整的项目运行环境,完成部署。


示例——安装启动Mysql


1. 安装Docker


具体过程可参考菜鸟教程,下面以macOS系统作为例子进行演示。


启动docker客户端如下:



打开系统终端(下面是在vscode的终端中演示),输入命令:


docker -v

效果如下:



说明docker已经安装并启动。


2. 下载Mysql镜像


下载镜像有点类似于安装npm包:npm install <包名>,这里输入docker镜像的安装命令:docker pull mysql来下载安装mysql的镜像:



安装结束后,输入镜像列表的查看命令:docker images



当然,通过docker的客户端App也可以看到:



3. 创建mysql镜像的容器,启动Mysql


输入启动容器命令:


docker run -d -p 3308:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql

先来看下启动结果,下面的一堆数字是完整的容器id:



输入的这一串命令是什么意思?



  • docker run: 这是启动新容器的命令。

  • -d--detach 是使mysql服务在后台运行,而不是占用当前终端界面。

  • -p 3308:3306: 这是端口映射参数:

    • 创建容器会默认创建一个子网,与宿主机所处的网络互相隔离;mysql服务默认端口3306,如果要通过宿主机所在网络访问容器所处的子网络中的服务,就需要进行端口映射(不熟悉网络的可以看下《如何跟小白解释网络》)。

    • 宿主机的端口在前(左边),容器的端口在后(右边)。



  • -e MYSQL_ROOT_PASSWORD=123456: 设置环境变量 MYSQL_ROOT_PASSWORD=123456;也就是将mysql服务的root用户的密码为123456

  • mysql: 这是上面刚刚pull的镜像的名称。


通过上面的命令,我们启动了一个mysql镜像的容器,并将主机的3308端口映射到了容器所在子网中ip的3306端口,这样我们就可以通过访问主机的localhost:3308来访问容器中的mysql服务了。


4.访问Mysql服务


下面写一段nodeJs代码:


// mysql.js
const mysql = require('mysql');
const connection = mysql.createConnection({
host: 'localhost',
port: '3308',
user: 'root',
password: '123456',
database: '',
});
connection.connect();
// 显示全部数据库
connection.query('show databases;', function (err, rows, fields) {
if (err) {
console.log('[SELECT ERROR] - ', err.message);
return;
}
console.log('--------------------------SELECT----------------------------');
console.log(rows);
console.log('------------------------------------------------------------');
});

这里调用了nodeJs的mysql包,访问localhost:3308,用户名为root,密码为123456,运行结果下:


$ node mysql.js;
[SELECT ERROR] - ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication
protocol requested by server; consider upgrading MySQL client

这里报错了,原因是mysql服务的认证协议与我们代码中的不同导致的。这里我们需要对mysql服务进行一些修改。


为此,我们需要进入mysql容器,对mysql进行一些直接的命令行操作。


5.进入容器


首先,我们需要知道容器的id,输入容器查看命令:docker ps,展示容器列表如下:



其中55cbcc600353就是我们需要的容器的短id,然后执行命令:docker exec -it 55cbcc600353 bash,以下是命令的解析:



  • docker exec:用于向运行中的容器发布命令。

  • -it:分配一个终端(伪终端),允许用shell命令进行交互。也就是将容器中的终端界面映射到宿主机终端界面下,从而对容器进行直接的命令行操作。

  • 55cbcc600353:容器ID或容器名称。

  • bash:这是要在容器内执行的命令。这里是启动了容器的Bash shell程序。


运行结果如下:



我们看到bash-4.4#  后闪烁的光标。这就是容器的bash shell命令提示符,这里输入的shell命令将会在容器环境中执行。


我们输入mysql登录命令,以root用户身份登录:mysql -uroot -p123456



成功登录mysql后,在mysql>命令提示符下输入:ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password By '123456';


这条命令用来修改’root’用户的认证方式为mysql_native_password ,将密码设置为123456,并允许来自任何主机(‘%’)的连接。


输入exit;命令退出mysql>命令提示符:



再按下:ctl+D退出容器终端,回到宿主机系统终端下。再次运行上面的js代码,效果如下:



这样我们就完成了本地mysql服务的部署。


结束


通过上面的简介以及安装部署mysql服务的例子,相信不了解docker的前端小伙伴已经有了一些概念;感兴趣的小伙伴可以继续深入,学习相关的知识。


作者:硬毛巾
来源:juejin.cn/post/7304538094782808105
收起阅读 »

为啥IoT(物联网)选择了MQTT协议?

物联网设备要实现互相通信,须一套标准通信协议,MQTT(Message Queuing Telemetry Transport)专为物联网设备设计的一套标准消息队列通信协议。使用MQTT协议的IoT设备,可以连接到任何支持MQTT协议的消息队列上,进行通信。 ...
继续阅读 »

物联网设备要实现互相通信,须一套标准通信协议,MQTT(Message Queuing Telemetry Transport)专为物联网设备设计的一套标准消息队列通信协议。使用MQTT协议的IoT设备,可以连接到任何支持MQTT协议的消息队列上,进行通信。



  • 宏观,MQTT和其他MQ传输协议差不多。也是“发布-订阅”消息模型

  • 网络结构,也是C/S架构,IoT设备是客户端,Broker是服务端,客户端与Broker通信进行收发消息


但毕竟使用场景不同,所以,MQTT和普通MQ比,还有很多区别。


1 客户端都运行在IoT设备


1.1 IoT设备特点


① 便宜


最大特点,一个水杯才几十块钱,它上面智能模块成本十块钱最多,再贵就卖不出去。十块钱的智能设备内存都是按KB计算,可能都没有CPU,也不一定有os,整个设备就一个SoC(System on a Chip)。这样的设备就需要通信协议不能复杂,功能不能太多。


② 无线连接


IoT设备一般采用无线连接,很多设备经常移动,导致IoT设备网络连接不稳定,且是常态。


MQTT协议设计上充分考虑这些特点。协议的报文设计极简,惜字如金。协议功能也非常简单,基本就只有:



  • 发布订阅主题

  • 收发消息


这两个最核心功能。为应对网络连接不稳定问题,MQTT增加机制:



  • 心跳机制,可让客户端和服务端双方都能随时掌握当前连接状态,一旦发现连接中断,可尽快重连

  • 会话机制,在服务端来保存会话状态,客户端重连后就可恢复之前会话,继续收发消息。这样,把复杂度转移到服务端,客户端实现更简单


2 服务端高要求


MQTT面临的使用场景中,服务端需支撑海量IoT设备同时在线。


普通的消息队列集群,服务的客户端都运行在性能强大的服务器,所以客户端数量不会特别多。如京东的JMQ集群,日常在线客户端数量大概十万左右,就足够支撑全国人民在京东买买买。


而MQTT使用场景中,需支撑的客户端数量,远不止几万几十万。如北京交通委若要把全市车辆都接入进来,就是个几百万客户端的规模。路侧的摄像头,每家每户的电视、冰箱,每个人随身携带的各种穿戴设备,这些设备规模都是百万、千万级甚至上亿级。


3 不支持点对点通信


MQTT协议的设计目标是支持发布-订阅(Publish-Subscribe)模型,而不是点对点通信。


MQTT的主要特点之一是支持发布者(Publisher)将消息发布到一个主题(Topic),而订阅者(Subscriber)则可以通过订阅相关主题来接收这些消息。这种模型在大规模的分布式系统中具有很好的可扩展性和灵活性。因此,MQTT更适合用于多对多、多对一的通信场景,例如物联网(IoT)应用、消息中间件等。


虽然MQTT的设计目标不是点对点通信,但在实际使用中,你仍然可以通过一些设计来模拟点对点通信。例如,使用不同的主题来模拟点对点通信,或者在应用层进行一些额外的协议和逻辑以实现点对点通信的效果。


一般做法都是,每个客户端都创建一个以自己ID为名字的主题,然后客户端来订阅自己的专属主题,用于接收专门发给这个客户端的消息。即MQTT集群中,主题数量和客户端数量基本是同一量级。


4 MQTT产品选型


如何支持海量在线IoT设备和海量主题,是每个支持MQTT协议的MQ面临最大挑战。也是做MQTT服务端技术选型时,需重点考察技术点。


开源MQTT产品


有些是传统MQ,通过官方或非官方扩展,实现MQTT协议支持。也有一些专门的MQTT Server产品,这些MQTT Server在协议支持层面,大多没问题,性能和稳定性方面也都满足要求。但还没发现能很好支撑海量客户端和主题的开源产品。why?


传统MQ


虽可通过扩展来支持MQTT协议,但整体架构设计之初,并未考虑支撑海量客户端和主题。如RocketMQ元数据保存在NameServer的内存,Kafka是保存在zk,这些存储都不擅长保存大量数据,所以也支撑不了过多客户端和主题。


另外一些开源MQTT Server


很多就没集群功能或集群功能不完善。集群功能做的好的产品,大多都把集群功能放到企业版卖。


所以做MQTT Server技术选型,若你接入IoT设备数量在10w内,可选择开源产品,选型原则和选择普通消息队列一样,优先选择一个流行、熟悉的开源产品即可。


若客户端规模超过10w量级,需支撑这么大规模客户端数量,服务端只有单节点肯定不够,须用集群,并且这集群要支持水平扩容。这时就几乎没开源产品了,此时只能建议选择一些云平台厂商提供的MQTT云服务,价格相对较低,也可选择价格更高商业版MQTT Server。


另外一个选择就是,基于已有开源MQTT Server,通过一些集成和开发,自行构建MQTT集群。


5 构建一个支持海量客户端的MQTT集群


MQTT集群如何支持海量在线的IoT设备?
一般来说,一个MQTT集群它的架构应该是这样的:



从左向右看,首先接入的地址最好是一个域名,这样域名后面可配置多个IP地址做负载均衡,当然这域名不是必需。也可直接连负载均衡器。负载均衡可选F5这种专用的负载均衡硬件,也可Nginx这样软件,只要是四层或支持MQTT协议的七层负载均衡设备,都可。


负载均衡器后面要部署一个Proxy集群


Proxy集群作用



  • 承接海量IoT设备连接

  • 维护与客户端的会话

  • 作为代理,在客户端和Broker之间进行消息转发


在Proxy集群后是Broker集群,负责保存和收发消息。


有的MQTT Server集群架构:



架构中没Proxy。实际上,只是把Proxy和Broker功能集成到一个进程,这两种架构本质没有太大区别。可认为就是同一种架构来分析。


前置Proxy,易解决海量连接问题,由于Proxy可水平扩展,只要用足够多的Proxy节点,就可抗海量客户端同时连接。每个Proxy和每个Broker只用一个连接通信即可,这对每个Broker来说,其连接数量最多不会超过Proxy节点的数量。


Proxy对于会话的处理,可借鉴Tomcat处理会话的两种方式:



  • 将会话保存在Proxy本地,每个Proxy节点都只维护连接到自己的这些客户端的会话。但这要配合负载均衡来使用,负载均衡设备需支持sticky session,保证将相同会话的连接总是转发到同一Proxy节点

  • 将会话保存在一个外置存储集群,如Redis集群或MySQL集群。这样Proxy就可设计成完全无状态,对负载均衡设备也没特殊要求。但这要求外置存储集群具备存储千万级数据能力,同时具有很好性能


如何支持海量主题?


较可行的解决方案,在Proxy集群的后端,部署多组Broker小集群,如可以是多组Kafka小集群,每个小集群只负责存储一部分主题。这样对每个Broker小集群,主题数量就可控制在可接受范围内。由于消息是通过Proxy进行转发,可在Proxy中采用一些像一致性哈希等分片算法,根据主题名称找到对应Broker小集群。这就解决支持海量主题的问题。


UML


Proxy的UML图:


@startuml
package "MQTT Proxy Cluster" {
class MQTTProxy {
+handleIncomingMessage()
+handleOutgoingMessage()
+produceMessage()
+consumeMessage()
}

class Client {
+sendMessage()
+receiveMessage()
}

class Broker {
+publish()
+subscribe()
}

Client --> MQTTProxy
MQTTProxy --> Broker
}
@enduml

@startuml
actor Client
entity MQTTProxy
entity Broker

Client -> MQTTProxy : sendMessage()
activate MQTTProxy
MQTTProxy -> Broker : produceMessage()
deactivate MQTTProxy
@enduml

@startuml
entity MQTTProxy
entity Broker
actor Client

Broker -> MQTTProxy : publishMessage()
activate MQTTProxy
MQTTProxy -> Client : consumeMessage()
deactivate MQTTProxy
@enduml


Proxy收发消息的时序图:



Proxy生产消息流程的时序图:



Proxy消费消息流程的时序图:


image-20231208134111361

6 总结


MQTT是专门为物联网设备设计的一套标准的通信协议。这套协议在消息模型和功能上与普通的消息队列协议是差不多的,最大的区别在于应用场景不同。在物联网应用场景中,IoT设备性能差,网络连接不稳定。服务端面临的挑战主要是,需要支撑海量的客户端和主题。


已有的开源的MQTT产品,对于协议的支持都不错,在客户端数量小于十万级别的情况下,可以选择。对于海量客户端的场景,服务端必须使用集群来支撑,可以选择收费的云服务和企业版产品。也可以选择自行来构建MQTT集群。


自行构建集群,最关键技术点,就是通过前置Proxy集群解决海量连接、会话管理和海量主题:



  • 前置Proxy负责在Broker和客户端之间转发消息,通过这种方式,将海量客户端连接收敛为少量的Proxy与Broker之间的连接,解决了海量客户端连接数的问题

  • 维护会话的实现原理,和Tomcat维护HTTP会话一样

  • 海量主题,可在后端部署多组Broker小集群,每个小集群分担一部分主题这样的方式来解决


参考:



作者:JavaEdge在掘金
来源:juejin.cn/post/7310786611805929499
收起阅读 »

全网显示IP归属地,免费可用,快来看看

前言 经常浏览小视频或各类帖子的朋友们可能已经注意到,目前许多网络平台都会显示作者和评论区用户的IP归属地。那么,这个功能是如何实现的呢? 某些收费平台的API 我们可以利用一些付费平台的API来实现这一功能,比如一些导航软件的开放平台API等。然而,这些服...
继续阅读 »

前言


经常浏览小视频或各类帖子的朋友们可能已经注意到,目前许多网络平台都会显示作者和评论区用户的IP归属地。那么,这个功能是如何实现的呢?



某些收费平台的API


我们可以利用一些付费平台的API来实现这一功能,比如一些导航软件的开放平台API等。然而,这些服务通常是收费的,而且免费额度有限,适合测试使用,但如果要在生产环境中使用,很可能不够支撑需求。



离线库推荐


那么,有没有免费的离线API库呢?UP现在推荐一个强大的离线库给大家,一个准确率高达99.9%的离线IP地址定位库,查询速度仅需0.0x毫秒,而且数据库仅10兆字节大小。此库提供了Java、PHP、C、Python、Node.js、Golang、C#等多种查询绑定,同时支持Binary、B树和内存三种查询算法。



这个库大家可以在GitHub上搜索:ip2region,即可找到该开源库。


使用


下面使用Java代码给大家演示下如何使用这个IP库,该库目前支持多重主流语言。


1、引入依赖


<dependency>
   <groupId>org.lionsoul</groupId>
   <artifactId>ip2region</artifactId>
   <version>2.7.0</version>
</dependency>

2、下载离线库文件 ip2region.xdb



3、简单使用代码


下面,我们通过Java代码,挑选某个国内的IP进行测试,看看会输出什么样的结果


public class IpTest {

   public static void main(String[] args) throws Exception {
       // 1、创建 searcher 对象 (修改为离线库路径)
       String dbPath = "C:\Users\Administrator\Desktop\ip2region.xdb";
       Searcher searcher = null;
       try {
           searcher = Searcher.newWithFileOnly(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、查询
       String ip = "110.242.68.66";
       try {
           long sTime = System.nanoTime(); // Happyjava
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }

       // 3、关闭资源
       searcher.close();

       // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
  }

}


输出结果为:


{region: 中国|0|河北省|保定市|联通, ioCount: 3, took: 1192 μs}

其中,region的格式为 国家|区域|省份|城市|ISP,缺省的地域信息默认是0。


当然,这个库不只是支持国内的IP,也支持国外的IP。



其他语言可以参考该开源库的说明文档。


总结


这是一个准确率非常高的离线库,如果项目里有IP定位需求的,可以试下该库。


作者:happyjava
来源:juejin.cn/post/7306334713992708122
收起阅读 »

面试官:什么是JWT?为什么要用JWT?

目前传统的后台管理系统,以及不使用第三方登录的系统,使用 JWT 技术的还是挺多的,因此在面试中被问到的频率也比较高,所以今天我们就来看一下:什么是 JWT?为什么要用 JWT? 1.什么是 JWT? JWT(JSON Web Token)是一种开放标准(RF...
继续阅读 »

目前传统的后台管理系统,以及不使用第三方登录的系统,使用 JWT 技术的还是挺多的,因此在面试中被问到的频率也比较高,所以今天我们就来看一下:什么是 JWT?为什么要用 JWT?


1.什么是 JWT?


JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络上安全传输信息的简洁、自包含的方式。它通常被用于身份验证和授权机制。
JWT 由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。



  1. 头部(Header):包含了关于生成该 JWT 的信息以及所使用的算法类型。

  2. 载荷(Payload):包含了要传递的数据,例如身份信息和其他附属数据。JWT 官方规定了 7 个字段,可供使用:

    1. iss (Issuer):签发者。

    2. sub (Subject):主题。

    3. aud (Audience):接收者。

    4. exp (Expiration time):过期时间。

    5. nbf (Not Before):生效时间。

    6. iat (Issued At):签发时间。

    7. jti (JWT ID):编号。



  3. 签名(Signature):使用密钥对头部和载荷进行签名,以验证其完整性。



JWT 官网:jwt.io/



2.为什么要用 JWT?


JWT 相较于传统的基于会话(Session)的认证机制,具有以下优势:



  1. 无需服务器存储状态:传统的基于会话的认证机制需要服务器在会话中存储用户的状态信息,包括用户的登录状态、权限等。而使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都包含在 JWT 中,使得系统可以更容易地进行水平扩展。

  2. 跨域支持:由于 JWT 包含了完整的认证和授权信息,因此可以轻松地在多个域之间进行传递和使用,实现跨域授权。

  3. 适应微服务架构:在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 JWT 可以满足这种需求,每次请求携带 JWT 即可实现认证和授权。

  4. 自包含:JWT 包含了认证和授权信息,以及其他自定义的声明,这些信息都被编码在 JWT 中,在服务端解码后使用。JWT 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制。

  5. 扩展性:JWT 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。


总结来说,使用 JWT 相较于传统的基于会话的认证机制,可以减少服务器存储开销和管理复杂性,实现跨域支持和水平扩展,并且更适应无状态和微服务架构。


3.JWT 基本使用


在 Java 开发中,可以借助 JWT 工具类来方便的操作 JWT,例如 HuTool 框架中的 JWTUtil。


HuTool 介绍:doc.hutool.cn/pages/JWTUt…


使用 HuTool 操作 JWT 的步骤如下:



  1. 添加 HuTool 框架依赖

  2. 生成 Token

  3. 验证和解析 Token


3.1 添加 HuTool 框架依赖


在 pom.xml 中添加以下信息:


<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.16version>
dependency>

3.2 生成 Token


Map map = new HashMap() {
private static final long serialVersionUID = 1L;
{
put("uid", Integer.parseInt("123")); // 用户ID
put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15); // 过期时间15天
}
};
JWTUtil.createToken(map, "服务器端秘钥".getBytes());

3.3 验证和解析 Token


验证 Token 的示例代码如下:


String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2MjQwMDQ4MjIsInVzZXJJZCI6MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV_op5LoibLkuozlj7ciLCJzeXNfbWVudV8xIiwiUk9MRV_op5LoibLkuIDlj7ciLCJzeXNfbWVudV8yIl0sImp0aSI6ImQ0YzVlYjgwLTA5ZTctNGU0ZC1hZTg3LTVkNGI5M2FhNmFiNiIsImNsaWVudF9pZCI6ImhhbmR5LXNob3AifQ.aixF1eKlAKS_k3ynFnStE7-IRGiD5YaqznvK2xEjBew";
JWTUtil.verify(token, "123456".getBytes());

解析 Token 的示例代码如下:


String rightToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWUsIm5hbWUiOiJsb29seSJ9.U2aQkC2THYV9L0fTN-yBBI7gmo5xhmvMhATtu8v0zEA";
final JWT jwt = JWTUtil.parseToken(rightToken);
jwt.getHeader(JWTHeader.TYPE);
jwt.getPayload("sub");

3.4 代码实战


在登录成功之后,生成 Token 的示例代码如下:


// 登录成功,使用 JWT 生成 Token
Map payload = new HashMap() {
private static final long serialVersionUID = 1L;
{
put("uid", userinfo.getUid());
put("manager", userinfo.getManager());
// JWT 过期时间为 15 天
put("exp", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);
}
};
String token = JWTUtil.createToken(payload, AppVariable.JWT_KEY.getBytes());

例如在 Spring Cloud Gateway 网关中验证 Token 的实现代码如下:


import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import com.example.common.AppVariable;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

/**
* 登录过滤器(登录判断)
*/

@Component
public class AuthFilter implements GlobalFilter, Ordered {
// 排除登录验证的 URL 地址
private String[] skipAuthUrls = {"/user/add", "/user/login"};

@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 当前请求的 URL
String url = exchange.getRequest().getURI().getPath();
for (String item : skipAuthUrls) {
if (item.equals(url)) {
// 继续往下走
return chain.filter(exchange);
}
}
ServerHttpResponse response = exchange.getResponse();
// 登录判断
List tokens =
exchange.getRequest().getHeaders().get(AppVariable.TOKEN_KEY);
if (tokens == null || tokens.size() == 0) {
// 当前未登录
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// token 有值
String token = tokens.get(0);
// JWT 效验 token 是否有效
boolean result = false;
try {
result = JWTUtil.verify(token, AppVariable.JWT_KEY.getBytes());
} catch (Exception e) {
result = false;
}
if (!result) {
// 无效 token
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
} else { // 判断 token 是否过期
final JWT jwt = JWTUtil.parseToken(token);
// 得到过期时间
Object expObj = jwt.getPayload("exp");
if (expObj == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
long exp = Long.parseLong(expObj.toString());
if (System.currentTimeMillis() > exp) {
// token 过期
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
// 值越小越早执行
return 1;
}
}

4.实现原理分析


JWT 本质是将秘钥存放在服务器端,并通过某种加密手段进行加密和验证的机制。加密签名=某加密算法(header+payload+服务器端私钥),因为服务端私钥别人不能获取,所以 JWT 能保证自身其安全性。


小结


JWT 相比与传统的 Session 会话机制,具备无状态性(无需服务器端存储会话信息),并且它更加灵活、更适合微服务环境下的登录和授权判断。JWT 是由三部分组成的:Header(头部)、Payload(数据载荷)和 Signature(签名)。


作者:Java中文社群
来源:juejin.cn/post/7309310129024008230
收起阅读 »

搞不懂,我的手机没有公网IP,服务器响应报文如何被路由到手机?

6年前,在我刚毕业的时候,我困惑于一个问题:“我的手机和个人电脑等设备只有内网IP,而没有公网IP的情况下,公网服务器的响应IP报文如何被路由到我的内网设备呢?“ 我知道:任何联网的设备都可以主动连接公网IP的服务器,因为公网IP是全球唯一分配的,IP层报文经...
继续阅读 »

6年前,在我刚毕业的时候,我困惑于一个问题:“我的手机和个人电脑等设备只有内网IP,而没有公网IP的情况下,公网服务器的响应IP报文如何被路由到我的内网设备呢?“


我知道:任何联网的设备都可以主动连接公网IP的服务器,因为公网IP是全球唯一分配的,IP层报文经过路由器的层层路由可以到达公网服务器。然而我实在想不通:公网服务器的响应报文该如何回来?


这个问题让我感到很困扰,我上学时学的计算机网络知识已经还给老师了,我尝试询问周围的同事,可惜没有人能给出一个可靠的答案。直到我了解到NAT(网络地址转换)技术,才最终解答了我的疑问。



NAT,即网络地址转换,是一种用于解决IP地址短缺问题的技术。它通过在内部网络和公共网络之间建立一个转换表,将多个内部私有IP地址映射为一个公共IP地址,实现多个设备共享一个公网IP地址的功能。



在科普 NAT 之前,有必要说明一下 内网IP。


0. 内网 IP 不能随意分配!


内网IP的分配是有规范的,不可以随意分配。如果内网IP和公网IP冲突,网络报文无法被路由器正确地路由。因为路由器不知道这个IP报文应该被路由到内网设备,还是路由到上一层网关(直至公网IP)。


为了避免冲突,IP协议中事先规定了三个网段作为内网IP的范围,分别是



  1. A类地址的 10.0.0.0 至 10.255.255.255

  2. B类地址的 172.16.0.0 至 172.31.255.255,

  3. C类地址的 192.168.0.0 至 192.168.255.255。


因此在一个局域网内,以上三个网段的设备都是内网设备,除此外基本(排除 127.0.0.1 等)都是公网设备。这样所有的IP地址都不会冲突!


在家用局域网中,通常使用的是 192.168.xxx.xxx 的格式;而在公司的机房中,由于设备数量庞大,一般会选择以 10.xxx.xxx.xxx 开头的网段。这是因为 192 开头的C类地址能够满足家用局域网设备数量的需求,而 10.xxx.xxx.xxx 网段可以适应大规模的公司机房环境,其中可能存在数以百万计的物理服务器,和数以千万计的虚拟机或者Docker实例。


1. 公网流量如何路由到内网设备


设想一下,内网IP为 192.168.100.100 的用户设备,请求到公网服务器。如果公网服务器收到的IP报文中,显示来源是 192.168.100.100。因为 192 开头的 IP 是内网地址,所以服务器响应用户请求时,就会错误地把请求路由到内网,无法正确路由到用户设备上!


所以…… 正确的显示来源是什么呢?


内网设备想要 “连通” 公网服务器,是需要路由器等网络设备层层转发的,正如下图而言,内网设备需要有公网IP的网络出口才能连通 到另一个公网设备。


image.png
因此,刚才问题答案是,内网IP报文到达公网机器时,来源应该被设置为 相应的 运营商公网出口IP。即公网出口网络设备要把IP来源改为自己。这样公网服务器收到请求报文后,对应的响应IP报文也会回复到用户端的公网出口IP。


看下图,用户端的IP报文到达运营商网络出口时,IP来源被替换为 运营商公网出口IP。下图中网络来源为 192 开头的内网Ip 地址,被替换为公网出口 IP(100.100.100.100)。像这种偷偷更换来源 IP 和目标 IP 的行为在 NAT 技术上很常见,后面会经常看到!


image.png


2. 公网机器发送响应报文时


当公网服务器响应时,IP 目标地址 是运营商公网出口而非用户的内网地址!然后运营商服务器会再次转发,转发前,运营商机器需要知道该转发给谁,转发给哪个用户设备。


如下图所示,用户端(192.168.100.100) 访问 公网地址(200.200.200.200)的IP 来源和目标被替换的过程。


image.png
首先公网机器响应给 运营商公网出口时。


第一步:来源 IP 是(200.200.200.200),目标IP 是(100.100.100.100)。


然后,运营商将 IP 报文转发给用户设备时,来源 IP 还是公网机器的 IP(200.200.200.200)不变化。然而目标 IP 修改为用户设备的 IP(192.168.xx.xx)。


对于用户设备而言,发送请求时目标IP 是公网机器,收到响应时来源 IP 还是公网机器。用户设备丝毫没有感觉到,在中间被路由转发的过程,目标和来源 IP 频繁被路由设备修改!


在这个环节,有一个关键问题:运营商收到公网机器的响应时,它怎么知道该路由给哪个用户设备!


3. NAT 如何进行地址映射


最简单的映射方式是:每一个内网 IP 都映射到一个运营商的公网IP。即内网 Ip 和运营商公网 IP 一对一映射!


这种方式很少见,用户设备和内网设备非常多,这样非常耗费公网IP,一般不会采用。


TCP 和 UDP 协议除基于 IP 地址外,还有端口,如果引入端口参与映射,则大大提高运营商公网 IP 的利用度!


一个 TCP 报文 包括如下参数:



  1. 用户 IP + 用户端口

  2. 公网 IP + 公网端口


这四个参数非常关键,相当于是 TCP 连接的唯一主键。当运营商收到公网机器的响应报文时,它可以拿到四个参数分别为:


运营商公网 IP + 端口 、目标机器公网 IP + 端口。其中关键的参数有三位:运营商公网端口,目标机器公网 IP 和端口。



为什么运营商公网 IP 不关键呢?因为根据 IP 路由协议,响应报文已经被路由到该机器,每一个运营商公网出口都会维护一套单独的 映射表 ,所以自己的 IP 地址不关键。



当前的难点是:如何根据 运营商公网端口,目标机器公网 IP 和 端口 三个参数,映射到 用户 IP 和端口的问题。


用户请求时,会建立映射表。当收到用户请求时,运营商服务器的 NAT 模块会分配一个端口供本次请求使用,建立一个映射项:运营商机器端口 + 目标公网 IP + 目标公网端口 这三个参数映射到 用户 IP 和用户端口


例如下表


NAT 映射表运营商机器端口目标公网 IP目标公网端口用户 IP用户端口
1300200.200.200.20080192.168.22.226000
2
3

在运营商机器转发IP报文时,除替换IP外,也会替换端口。相比NAT 一比一映射IP地址,增加端口映射,可以大大提高运营商公网 IP 的利用度。接下来有个问题?


每个机器的端口最大数为 65535,说明每个运营商机器最多 同时支持转发 65535 个请求?


这个推论不成立。


从上面的映射表可以看到,运营商机器收到响应报文时,会根据 三个关键参数 进行映射,而非只根据 自身端口映射。以上面 NAT 映射表的第一条记录为例,运营商机器的 300 端口,并非仅仅服务于 200.200.200.200 这次请求。300 端口还可以同时服务 250.250.250.250 + 80 端口,以及其他连接!


由于映射的参数有三个,而不仅仅是运营商端口一项,因此并发程度非常高。


理论的最大并发度应该是 65535 * (公网 IP 数)* 65535,这个并发度非常高。 一个运营商机器似乎支持海量的NAT连接,实际上,并非海量。


因为常用的 Http 协议端口是 80,目标公网机器的端口数常用的基本是 80 端口。


其次用户常用的软件非常集中,例如微信、抖音、稀土掘金等,访问的公网 IP 也集中于这些公司的 IP 地址。


所以基于此,最高并发度变为:65535 * 常用的公网 IP数 * 有限的端口数(80、443)。


这个并发度并非海量,但是基本上足够使用了,一个小区或办公区的网络设备数量不会过于庞大。65535 * 有限的公网 IP数 * 有限的端口数,这样的并发度足够支持一般场景使用。


除非出现极端的情况! 即一个小区的大量用户集中访问于一个公网 IP 的 80 端口,这样网络流量一定会发生拥塞!在某些用户流量集中的区域,可以安排更多的 NAT 设备,提供更多的公网 IP。


一个小区一个公网 IP 吗?


根据 chatgpt 的回答,通常情况下,一个小区只有一个公网 IP。
image.png


上大学时,偶然了解到 SQL注入,我感觉很新奇。后来对一个兼职网站进行 SQL 注入的尝试。经过几次尝试后,我发现无法再访问这个网站。宿舍和其他几个宿舍的同学也无法访问此网站。我不禁得意洋洋,难道是因为我的攻击导致了这个网站的崩溃?


后来我找到其他大学的高中同学,让他们访问这个网站,他们访问是没问题的。那时我明白了,这个网站只是封掉了我们学校的公网 IP,或者是这栋男生宿舍楼的公网 IP ,它并没有崩溃!


个人经验来看,一个小区或者办公楼会根据实际的需要安排一定数量的公网 IP,一般情况下共用一个公网 IP。


总结


当 公网 IP 不够用时,可通过 NAT 协议实现多个用户设备共享同一个公网 IP,提高 公网IP 地址的利用度。Ipv4 的地址数量有限,在 2011 年已经被分配完,未来如果全面实现了 ipv6 协议,我们手机等终端设备也可能会有一个公网 IP。


但是公网 Ip 全球都可以访问,与此对应的网络安全问题不可忽视。NAT 技术则可以有效保护用户设备,让用户安全上网,这也是它附带的好处。


如果看完有所收获,感谢点赞支持!


作者:五阳神功
来源:juejin.cn/post/7307892574722637835
收起阅读 »

为什么最近听说 Go 岗位很少很难?

大家好,我是煎鱼。 其实这个话题已经躺在我的 TODO 里很久了,近来很多社区的小伙伴都私下来交流,也有在朋友圈看吐槽 Go 上海的大会没什么人。还不如 Rust 大会,比较尴尬。 今天主要是从个人角度看看为什么 Go 岗位看起来近来很难的样子? 盘一下数据 ...
继续阅读 »

大家好,我是煎鱼。


其实这个话题已经躺在我的 TODO 里很久了,近来很多社区的小伙伴都私下来交流,也有在朋友圈看吐槽 Go 上海的大会没什么人。还不如 Rust 大会,比较尴尬。


今天主要是从个人角度看看为什么 Go 岗位看起来近来很难的样子?


盘一下数据


从以往的大的数据分析来看,Go 岗位最多的是分布以下几个城市:



TOP3 是北京、上海、深圳。


再从常用的招聘软件来看,目前互联网行业用的应该是 XX 直聘。我们从其提供的招聘岗位数量来看。


北京:



上海:



深圳:



从这 5 个月的时间区间来看,北京和上海是在螺旋式下跌;深圳探底创新低。(不过我认为从招聘季节来看,基本都是螺旋式的)


从数值来看,只有北京的 Go 岗位有所上涨。上海、深圳都在持续减少。但整体都是在下滑趋势的。


另外一个角度来看,招聘岗位这么多,也有个好几千。是不是没什么问题,风生水起?


还是要看看活跃度的。我快速的看了下深圳,Go 岗位。第一页 30 条招聘岗位,只有 8 个是本周活跃的。刚刚活跃和 3 日内活跃的,加起来就 2~3 个。


综合数据来看,招聘岗位的数量在向下走,招聘者登陆平台的活跃度不高。


看看小行情


可能很容易就得出了 Go 完全不行的结论。我们也得看看别的编程语言岗位的。


Java,深圳:



PHP,深圳:



综合来看,其实并不是某一门语言的岗位不太行,或者要凉了。是由于整体的行情原因,招聘岗位和招聘者的活跃度都在大量的收缩。


像是以往,Go 最多的招聘岗位也是由各中大型公司撑起的。例如:字节跳动、腾讯、滴滴、百度等。他们收缩了,增量也就下来了。


而一般这种放水阶段,很多是面向 GY 企的,会有一些项目出现。例如:前段时间很火热的信创。


但有做 2B 的同学应该了解,这块有些企业会加码,要求使用 Java 语言。这一块的增量,与 Java 相比较,Go 是比较难在正面承接到的。


总结


其实不单单 Go 岗位少了。由于宏观的影响,我们常接触到的招聘岗位都少了。普遍来讲,还是建议如果没想明白就先苟着,降低负债、现金为王。


人是环境的反应器,常常会受到各种因素的影响。但在这种时期,可能想清楚自己的目标和感兴趣的内容、方向,会是一个不错的提高机会。


而在缩量的环境下,如果想找到增量。就要去一个风口或上行周期的领域。例如最近比较火的 AI。之前的新能源,不过也要警惕是个新坑。



文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。



推荐阅读



作者:煎鱼eddycjy
来源:juejin.cn/post/7308697586532778024
收起阅读 »

个人独立开发者能否踏上敏捷之路?

很多软件开发团队都在使用Scrum、极限编程(XP)、看板等敏捷方法管理项目流程,持续迭代并更快、更高效地为客户持续交付可用的产品。除了团队,国内外很多个人独立开发者也在尝试将敏捷应用到自己的开发工作流程中,但大多数的结果都是收效甚微,个人践行敏捷是否可行? ...
继续阅读 »

很多软件开发团队都在使用Scrum、极限编程(XP)、看板等敏捷方法管理项目流程,持续迭代并更快、更高效地为客户持续交付可用的产品。除了团队,国内外很多个人独立开发者也在尝试将敏捷应用到自己的开发工作流程中,但大多数的结果都是收效甚微,个人践行敏捷是否可行? 


敏捷开发需要坚实的团队基础,以及团队文化的保障,方可有效地落地执行。  


什么是敏捷


敏捷是一种以用户需求为核心、采用不断迭代的方式进行的软件开发模式。它依靠自组织的跨职能小团队,在短周期内做出部分成果,通过快速、频繁的迭代,迅速地获取反馈,进而不断地完善产品,给用户带来更大的价值。践行敏捷的方式有很多,主要包括Scrum、XP、Kanban、精益生产、规模化敏捷等方法论。


敏捷的工作方式是将整个团队聚集在一起,理想情况下,敏捷团队的成员不超过10人。通过践行一系列简单的实践和足够的反馈,使团队能够感知目前的状态,并根据实际情况对实践进行调整。 


团队是敏捷的核心 


敏捷是一种团队驱动方法,团队可以简单的地定义为“为实现某一特定目标,包括两个或两个以上的人的相互协作的群体”。敏捷的核心是构建一个自组织的团队,团队的能力在于协作,即两个人或更多人相互交流与合作,以共同地产生一个结果。例如当两个程序员在结对编程时,他们在协作;每人持续集成当日的工作时,他们在协作;当团队开计划、站立、评审、回顾等会议时,他们在协作。协作的结果可以是有形的可交付物、决策或信息共享。 


而对于个人独立开发者,协作、互动、沟通都是无从谈起的: 


自己无法实践结对编程;


自己开站立会议是否很孤单;


自己玩估算扑克牌会不会很无聊;


评审演示没有观众,自然也就没有反馈;


…… 


这里有一个常见误区:独立开发者通常有一定的跨职能工作能力,于是想一人“饰演”多个不同角色,从需求计划整理到任务分解估时,从迭代开发到测试,再到发布、回顾总结。这是不是也在践行敏捷开发呢? 


当然不是。敏捷开发流程中任一环节都强调团队集体参与,并非由某个人独裁发号施令。例如项目计划制定、任务认领、工时评估,这些都不是某一个人的职责,而是需要团队成员来共同参与完成。然而,单个人的开发流程,很容易按部就班地走上了瀑布式开发模式(需求->设计->开发->测试->发布)。


001.png


[多重人格综合症,并确保精神上的新人是一个专业的“团队成员”]


敏捷团队中并不会要求每个人都成为全栈通才,在如今技术快速更新迭代的大环境下,期望一个人精通团队的所有技能是不现实的。取而代之的是重视具备跨职能的团队成员,这有助于管理各个工作岗位的平衡。例如,有时团队需要更多的测试人员,如果有一两个团队成员能转做测试工作,就能极大地提供帮助。 


敏捷是关于人,以及他们之间的协作交互,让每个人的能力得以充分的发挥并提升,从而创造优秀的产品。创造优秀产品的是人,而不是流程。所以,独立开发者即便一个人能跨职能走完整个开发流程,这跟敏捷强调的自组织团队中,成员之间高效地协作、交互以达到目标,完全不是一回事哦~


文化是敏捷的保障


很多个人独立开发者尝试引入敏捷的普遍思路,是从各种敏捷方法论中挑选一些个人能用,且有帮助的实践方法来用。这样确实能从中受益,但这真的是在践行敏捷么?


敏捷不只是一套方法论,敏捷也是一种思维模式。很多个人甚至团队尝试敏捷的过程中一个常见问题,是只取其方法实践,而未学其思维模式。这里说的思维模式,通俗讲就是指培养团队能够形成共识的文化,拥有一致的价值观和原则,塑造一个持续学习、自由、积极的团队氛围。以促使团队达到一种能够持续快速地交付有价值有质量的产品或服务的状态。


文化高于实践,成员能否融入团队文化,将会影响团队具体实践的高效程度。良好的团队文化,有利于促进团队内部的信息共享,从而产生更正确的决策。我们有时感觉自己已经引入敏捷了,但实则依旧保持着瀑布式思维,走的瀑布式开发流程,只是单纯学习并采用了一些好的敏捷实践,以至于最终达到的效果很有限。


这里引用《敏捷宣言》作者之一吉姆·海史密斯在他著作的《敏捷项目管理》中的一段总结: 


没有具体的实践,原则是贫瘠的;但是如果缺乏原则,实践则没有生命、没有个性、没有勇气。伟大的产品出自伟大的团队,而伟大的团队有原则、有个性、有勇气、有坚持、有胆量。


写在最后 


我们很难将整个敏捷的思维与方法流程应用到个人的独立开发工作中,因为敏捷需要坚实的团队基础,以及团队文化的保障,方可有效地落地执行。当然,我们并不否认个人可以尝试从敏捷中探索一些可借鉴学习的实践,并从中受益。


您如何看待这个问题呢,或者您是否有过将敏捷应用到个人的开发、工作、学习等方面的成功或失败的经验,欢迎在评论区一起分享交流。


 


参考资料:


《敏捷项目管理第2版》吉姆·海史密斯


敏捷开发网:http://www.minjiekaifa.com/


究竟什么是敏捷?http://www.zentao.net/blog/agile-…


作者:水牛GH
来源:juejin.cn/post/7308187262755061771
收起阅读 »

还在手打console.log?快来试试这个vscode插件 Quickly Log!!

背景 作为一枚前端开发人员,尤其是在写业务代码的时候,避免不了需要经常在控制台打印变量来查看数据(反正我是这样哈哈哈哈哈),那么就需要频繁的来写console.log(),然后在里面再输入自己想要查看的变量名。 思考 既然我们需要频繁的来进行这个操作,那么我们...
继续阅读 »

背景


作为一枚前端开发人员,尤其是在写业务代码的时候,避免不了需要经常在控制台打印变量来查看数据(反正我是这样哈哈哈哈哈),那么就需要频繁的来写console.log(),然后在里面再输入自己想要查看的变量名。


思考


既然我们需要频繁的来进行这个操作,那么我们是不是可以把它像代码片段一样来保存下来,然后配置一个激活他的快捷键来进行使用


在左下角这里选择用户代码片段


image.png


然后选择想要使代码片段生效的文件类型,比如我这里选择的tsx



选择了对应的文件类型,对应的代码片段只会在这个类型的文件里生效,想要在其他类型的文件里也使用同样的代码片段需要去对应的类型文件中复制一份



image.png


把对应的代码片段写入


"Print to console": {
// 说明
"description": "Log output to console",
// 快捷键
"prefix": "cl",
// 输出内容
"body": ["console.log($1)"]
},

这样我们就配置好了一个简易的代码片段,使用的时候只需要敲出来 ’cl‘就会出现我们的代码提示


image.png


这样我们就解决了这个问题(我自己也是使用这个方法很久)


更进一步


目前我们通过代码片段已经解决了这个问题,但是还是会有一些不方便的地方



  • 我们只能在写好代码片段的类型文件中使用,我们现在使用 .tsx,突然要写 .vue 了使用的时候发现没有生效,就又要再去配置一次

  • 如果我们目前的 console.log 比较多,那么控制台上就会看到输出了一堆的变量,根本搞不清哪个打印是对应的哪个变量

  • 有时候会遗忘删掉 console.log 语句 (也可以通过配置husky,在commit的时候进行校验解决)


为了解决这些问题,我们更进一步,来通过写一个vscode插件解决


Quickly Log


这个插件最开始只是对vscode插件开发的好奇,加上自己确实有这方面的需求才开始编写的。编写成插件就可以有效的解决了需要重复配置代码片段的问题。


这里介绍下插件的功能,不对代码具体介绍,感兴趣的可以去github上看下代码 github.com/Richard-Zha…


功能


提示配置


只需要将光标移动到变量附近,然后使用快捷键 Cmd + Shift + L,就会在下一行输出语句


image.png


这里也支持携带上变量所在的行号以及文件名


image.png


当然这些都是可以配置的,可以根据自己的喜好来配置输出的提示内容


image.png


如果是简洁党也可全都取消勾选,效果就和直接使用上面提到的代码片段一样,但是会支持自动将变量放入console.log()的括号内


一键clear


执行 Cmd + Shift + K 就会将当前页面匹配到的console.log语句自动删除


一键切换注释


执行 Cmd + Shift + J 就会将当前页面匹配到的console.log语句前面自动打上注释,再执行就会取消注释


快捷键都是可以更改的 vscode左下角的设置icon点开 点击键盘快捷方式 输入 Quickly Log进行更改


以上就是目前插件支持的功能了,欢迎大家去Vscode下载使用


image.png


TODO


目前有些场景的打印是有问题的


比如下面这样的换行场景,我们希望在光标放在a,b,c这里的时候,会在第21行这里插入console.log语句,但是目前只会在光标的下一行插入,还需要手动移动到下面


image.png


image.png


之前有试过通过判断是否在 {} 内来输入到整个语句之后,但是情况不太理想,后续再考虑解决


作者:Richard_Zhang
来源:juejin.cn/post/7306806944046678052
收起阅读 »

2024年,Rust和Go学哪个更好?

Rust vs. Go,在2024年,应该选择哪一个?或者说应该选择哪种语言——GoLang还是Rust。这可能是许多程序员在选择语言时考虑的一个问题。选择理想的编程语言被视为在这个不断变化的环境中取得成功的重要抉择。 GoLang和Rust是当今使用的最年轻...
继续阅读 »

Rust vs. Go,在2024年,应该选择哪一个?或者说应该选择哪种语言——GoLang还是Rust。这可能是许多程序员在选择语言时考虑的一个问题。选择理想的编程语言被视为在这个不断变化的环境中取得成功的重要抉择。


GoLang和Rust是当今使用的最年轻的编程语言。Go于2009年在Google推出,而在Go之后,Rust于2010年在Mozilla推出。这两种语言在当前流行的编程语言工具中有一些相似之处和差异。


通过本文,我们将讨论Rust和Go之间的基本差异和相似之处。


关于Go


Go是一门开源的计算机语言,可以更轻松地创建简单、高效和强大的软件。Go是精确、流畅和高效的。编写一个利用多核和网络机器的程序非常方便。


Go或GoLang是由Google工程师创建的,他们希望创建一种既具有C++的效率,又更容易学习、编写、阅读和安装的语言。


GoLang主要用于创建网络API和小型服务,特别是其goroutines,具有可扩展性。GoLang可以流畅地组装为机器代码,并提供舒适的垃圾回收和表示运行时的能力。


Go是一种快速、静态类型的汇编语言,给人一种解释型和动态类型语言的感觉。Goroutines的语言使开发人员能够创建完全掌控并发的应用程序,例如大型电子商务网站,同时在多个CPU核心上调度工作负载。


因此,准确地说,它非常适合并行计算环境。垃圾回收是Go的另一个特性,可以保证高效的内存管理。因此,未使用的内存可以用于新项目,而未使用的对象则从内存中“丢弃”。


关于Rust


Rust是一种静态类型的编译型编程语言,受到多种编程原型的支持。该语言最初的创建目标是优先考虑性能和安全性,其中安全性是主要目标。


Rust主要用于处理CPU密集型的活动,例如执行算法和存储大量数据。因此,需要高性能的项目通常使用Rust而不是GoLang。


理想情况下,Rust是C++的镜像。与GoLang和Java不同,Rust没有垃圾回收。相反,Rust使用借用检查器来确保内存安全。这个借用检查器强制执行数据所有权协议,以避免数据竞争。在这里,数据竞争意味着多个指针指向同一个内存位置。


Rust是一种用于长时间大型或小型团队的计算机编程语言。对于这种类型的编程,Rust提供了高度并发和极其安全的系统。


Rust现在被广泛用于Firefox浏览器的大部分部分。在2016年之后,Rust被宣称为最受欢迎的编程语言。Rust是一种非常基础的语言,可以在短短5分钟内学会。


Rust vs. Go,优缺点


要准确决定选择Go还是Rust,最好看一下GoLang和Rust的优势和劣势。上面我们已经对它们有了简单的了解,下面是它们的优点和缺点。


GoLang的优点



  • 它是一种简洁和简单的编程语言。

  • 它是一种良好组合的语言。

  • 以其速度而闻名。

  • Go具有很大的灵活性,并且易于使用。

  • 它是可扩展的。

  • 它是跨平台的。

  • 它可以检测未使用的变量。

  • GoLang具有静态分析工具。


GoLang的缺点



  • 没有手动内存管理。

  • 因为它太容易,所以感觉很表面。

  • 由于年轻,所以库较少。

  • 其中一些函数(如指针算术)是底层的。

  • GoLang的工具有一些限制。

  • 分析GoLang中的错误可能很困难。


Rust的优点



  • 提供非凡的速度。

  • 由于编译器,提供最佳的内存安全性。

  • 零成本抽象的运行时更快。

  • 它也是跨平台的。

  • 它提供可预测的运行时行为。

  • 它提供了访问优秀模式和语法的方式。

  • 它具有特殊的所有权特性。

  • 它易于与C语言和其他语言结合使用。


Rust的缺点



  • 尽管它确实很快,但有人声称它比F#慢。

  • 它具有基于范围的内存管理,可能导致内存泄漏的无限循环。

  • 在Rust中无法使用纯函数式数据框架,因为没有垃圾回收。

  • Rust没有Python和Perl语言支持的猴子补丁水平。

  • 由于语言还很新,可能会对语法感到担忧。

  • 编译时有时会很慢,因此学习变得困难。


数据告诉我们什么?


根据一份报告,GoLang语言被认为是参与者最喜欢的语言。


我们对GoLang和Rust语言有了基本的了解,现在继续进行Rust vs. Go的比较,并清楚地认识到这两种语言之间的差异。


Rust和Go的主要区别


GoLang和Rust之间的主要区别包括:



  • 性能

  • 并发性

  • 内存安全性

  • 开发速度

  • 开发者体验


(1) 性能


Google推出Go作为易于编码和学习的C++替代品。Go提供Goroutines,通过其中一个可以通过简单地包含Go语法来运行函数。


尽管Go具有这些有用的功能和对多核CPU的支持,但Rust占据上风,超过了Go。


因此,Go vs Rust:性能是Rust在与GoLang的比较中获得更多分数的一个特点。这些编程语言都是为了与C++和C等价而创建的。然而,在Rust vs. Go的比较中,GoLang的开发速度略高于Rust的性能。


虽然Rust在性能上优于Go,但在编译速度方面,Rust落后于Go。


然而,人们对编译时间并不太在意,所以整体上Rust在这方面是胜利者。


(2) 并发性


GoLang支持并发,在这一因素上比Rust有优势。Go的并发模型允许开发人员在不同的CPU核心上安装工作负载,使Go成为一种连贯的语言。


因此,在运行处理API请求的网站的情况下,GoLang goroutines将每个请求作为子进程运行。这个过程提高了效率,因为它将任务从所有CPU核心中卸载出来。


另一方面,Rust只有一个原生的等待或同步语法。因此,程序员更喜欢使用Go的方式来处理并发问题。


(3) 内存安全性


Rust使用编译时头文件策略来实现零成本中断的内存安全性。如果不是内存安全的程序,Rust将无法通过编译阶段。实际上,Rust的好处之一就是提供了内存安全性。


为了实现内存安全的并发,Rust使用类型安全性。Rust编译器调查你引用的每个内存地址和使用的每个变量。Rust的这个特性将通知你任何未定义行为和数据竞争。


它确保程序员不会遇到缓冲区溢出的情况。


相比之下,Go在运行时完全自动化。因此,开发人员在编写代码时不必担心内存释放。


因此,无论是GoLang还是Rust都优先考虑内存安全特性,但在性能方面,GoLang具有数据竞争的可能性。


(4) 开发速度


在某些情况下,开发速度比性能和程序速度更重要。Go语言的直接性和清晰性使其成为一种开发速度较快的语言。Go语言具有更短的编译时间和更快的运行时间。


尽管Go既提供了开发速度和简单性,但它缺少一些重要的功能。为了使语言更简单,Google删除了其他编程语言中可用的许多功能。


另一方面,Rust比Go拥有更多的功能。Rust具有更长的编译时间。


因此,如果项目的优先级是开发速度,Go比Rust要好得多。如果你不太关心开发速度和开发周期,但希望获得性能和内存安全性,那么Rust是你的最佳选择。


(5) 开发者体验


由于开发Go的主要动机是简单和易用性,大多数程序员认为它是一种“无聊的语言”或“简单的语言”。Go中的功能有限,使得学习和实现非常简单。


相反,Rust具有更高的内存安全功能,使得代码更复杂,降低了程序员的生产力。所有权的概念使得Rust语言对许多人来说不是理想的选择。


与Go相比,Rust的学习曲线要陡峭得多。然而,值得注意的是,与Python和JavaScript等语言相比,GoLang的学习曲线也较陡峭。


Rust和Go的共同特点


在Rust vs Go的比较中,这两者之间有很多共同之处。GoLang和Rust都是许多年轻开发人员使用的现代编程语言。


GoLang和Rust都是编译语言,都是开源的,并且都是用于微服务的计算环境。


此外,如果你对C++有一些了解,那么这两个程序都非常容易理解。


交互性


Rust能够与代码进行接口交互,例如直接与C库进行通信。Rust没有提供内存安全性的认证。


交互性带来了速度。Go提供了与C语言配合使用的Go包。


何时应该使用GoLang?


Go语言可用于各种不同的项目。根据一份报告,Go的用例包括网页开发、数据库和Web编程。大多数GoLang开发人员声称,由于Go的并发性,它对Web服务有一些限制。


不仅如此,Go还被列为后端Web开发的首选语言。Go语言还为Google Cloud Platform提供支持。因此,在高性能云应用中,Go确实是性能消耗大的语言。


何时应该使用Rust?


Rust是一种几乎可以在任何地方使用的计算机编程语言。然而,仍然有一些领域比其他领域更适合使用。系统编程就是其中之一,因为Rust在高性能方面表现出色。


系统程序员基本上是在硬件侧开发的软件工程师。由于Rust处理硬件侧内存管理的复杂性,它经常用于设计操作系统或计算机应用程序。


尽管在开发者社区内对什么构成中级语言存在一些争议,但Rust被视为具有面向机器的现代语言的特点。


总结


这两种语言,GoLang和Rust,由于它们非常相近的起源时间,被认为是彼此的竞争对手。Go的发展速度比Rust快。这两种语言有很多相似之处。


GoLang和Rust之间的区别在于Go是简单的,而Rust是复杂的。然而,它们的功能和优先级在各种有意义的方面有所不同。


Go与Rust并驾齐驱。这意味着这完全取决于你拥有的项目类型,主要取决于对你的业务来说什么是最好的。


作者:程序新视界
来源:juejin.cn/post/7307648485921980470
收起阅读 »

比亚迪面试,全程八股!

比亚迪最近几年凭借着其新能源汽车的板块大火了一把,无论是名声还是股价都涨得嘎嘎猛,但是迪子招聘编程技术岗位的人员却有两个特点: 面试难度低,对学校有一定的要求。 薪资给的和面试难度一样低。 但不管怎么,迪子也算是国内知名公司了,所以今天咱们来看看,他的校招...
继续阅读 »

比亚迪最近几年凭借着其新能源汽车的板块大火了一把,无论是名声还是股价都涨得嘎嘎猛,但是迪子招聘编程技术岗位的人员却有两个特点:



  1. 面试难度低,对学校有一定的要求。

  2. 薪资给的和面试难度一样低。


但不管怎么,迪子也算是国内知名公司了,所以今天咱们来看看,他的校招 Java 技术岗的面试题都问了哪些知识点?面试题目如下:
image.png


1.int和Integer有什么区别?


参考答案:int 和 Integer 都是 Java 中用于表示整数的数据类型,然而他们有以下 6 点不同:



  1. 数据类型不同:int 是基础数据类型,而 Integer 是包装数据类型;

  2. 默认值不同:int 的默认值是 0,而 Integer 的默认值是 null;

  3. 内存中存储的方式不同:int 在内存中直接存储的是数据值,而 Integer 实际存储的是对象引用,当 new 一个 Integer 时实际上是生成一个指针指向此对象;

  4. 实例化方式不同:Integer 必须实例化才可以使用,而 int 不需要;

  5. 变量的比较方式不同:int 可以使用 == 来对比两个变量是否相等,而 Integer 一定要使用 equals 来比较两个变量是否相等;

  6. 泛型使用不同:Integer 能用于泛型定义,而 int 类型却不行。


2.什么时候用 int 和 Integer?


参考答案:int 和 Integer 的典型使用场景如下:



  • Integer 典型使用场景:在 Spring Boot 接收参数的时候,通常会使用 Integer 而非 int,因为 Integer 的默认值是 null,而 int 的默认值是 0。如果接收参数使用 int 的话,那么前端如果忘记传递此参数,程序就会报错(提示 500 内部错误)。因为前端不传参是 null,null 不能被强转为 0,所以使用 int 就会报错。但如果使用的是 Integer 类型,则没有这个问题,程序也不会报错,所以 Spring Boot 中 Controller 接收参数时,通常会使用 Integer。

  • int 典型使用场景:int 常用于定义类的属性类型,因为属性类型,不会 int 不会被赋值为 null(编译器会报错),所以这种场景下,使用占用资源更少的 int 类型,程序的执行效率会更高。


3.HashMap 底层实现?


HashMap 在 JDK 1.7 和 JDK 1.8 的底层实现是不一样的。



  • 在 JDK 1.7 中,HashMap 使用的是数组 + 链表实现的。

  • 而 JDK 1.8 中使用的是数组 + 链表或红黑树实现的


HashMap 在 JDK 1.7 中的实现如下图所示:
image.png
HashMap 在 JDK 1.8 中的实现如下图所示:


4.HashMap 如何取值和存值?


参考答案:HashMap 使用 put(key,value) 方法进行存值操作,而存值操作的关键是根据 put 中的 key 的哈希值来确定存储的位置,如果存储的位置为 null,则直接存储此键值对;如果存储的位置有值,则使用链地址法来解决哈希冲突,找到新的位置进行存储。


HashMap 取值的方法是 get(key),它主要是通过 key 的哈希值,找到相应的位置,然后通过 key 进行判断,从而获取到存储的 value 信息。


5.SpringBoot 如何修改端口号?


参考答案:在 Spring Boot 中的配置文件中设置“server.port=xxx”就可以修改端口号了。


6.如何修改 Tomcat 版本号?


参考答案:在 pom.xml 中添加 tomcat-embed-core 依赖就可以修改 Spring Boot 中内置的 Tomcat 版本号了,如下图所示:
image.png
但需要注意的是 Spring Boot 和 Tomcat 的版本是有对应关系的,要去 maven 上查询对应的版本关系才能正确的修改内置的 Tomcat 版本号,如下图所示:
image.png


7.SpringBoot如何配置Redis?


参考答案:首先在 Spring Boot 中添加 Redis 的框架依赖,然后在配置文件中使用“spring.redis.xxx”来设置 Redis 的相关属性,例如以下这些:


spring:
redis:
# Redis 服务器地址
host: 127.0.0.1
# Redis 端口号
port: 6379
# Redis服务器连接密码,默认为空,若有设置按设置的来
password:
jedis:
pool:
# 连接池最大连接数,若为负数则表示没有任何限制
max-active: 8
# 连接池最大阻塞等待时间,若为负数则表示没有任何限制
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8


8.MySQL 左连接和右连接有什么区别?


参考答案:在 MySQL 中,左连接(Left Join)和右连接(Right Join)是两种用来进行联表查询的 SQL 语句,它们的区别如下:



  1. 左连接:左连接是以左边的表格(也称为左表)为基础,将左表中的所有记录和右表中匹配的记录联接起来。即使右表中没有匹配的记录,左连接仍然会返回左表中的记录。如果右表中有多条匹配记录,则会将所有匹配记录返回。左连接使用 LEFT JOIN 关键字来表示。

  2. 右连接:右连接是以右边的表格(也称为右表)为基础,将右表中的所有记录和左表中匹配的记录联接起来。即使左表中没有匹配的记录,右连接仍然会返回右表中的记录。如果左表中有多条匹配记录,则会将所有匹配记录返回。右连接使用 RIGHT JOIN 关键字来表示。


例如以下图片,左连接查询的结果如下图所示(红色部分为查询到的数据):
image.png
右连接如下图红色部分:
image.png


9.内连接没有匹配上会怎么?


参考连接:内连接使用的是 inner join 关键字来实现的,它会匹配到两张表的公共部分,如下图所示:
image.png
所以,如果内连接没有匹配上数据,则查询不到此数据。


小结


以上是比亚迪的面试题,但并不是说比亚迪的面试难度一定只有这么低。因为面试的难度通常是根据应聘者的技术水平决定的:如果应聘者的能力一般,那么通常面试官就会问一下简单的问题,然后早早结束面试;但如果应聘者的能力比较好,面试官通常会问的比较难,以此来探寻应聘者的技术能力边界,从而为后续的定薪、定岗来做准备,所以大家如果遇到迪子的面试也不要大意。


作者:Java中文社群
来源:juejin.cn/post/7306723594816733235
收起阅读 »

只改了五行代码接口吞吐量提升了10多倍

背景 公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。 当时一想,500/s吞吐量还不简单。Tomcat按照100个线...
继续阅读 »

背景


公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。


当时一想,500/s吞吐量还不简单。Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可。这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事情。


然而压测一开,100 的并发,吞吐量居然只有 50 ...


image.png


而且再一查,100的并发,CPU使用率居然接近 80% ...




从上图可以看到几个重要的信息。


最小值: 表示我们非并发场景单次接口响应时长。还不足100ms。挺好!


最大值: 并发场景下,由于各种锁或者其他串行操作,导致部分请求等待时长增加,接口整体响应时间变长。5秒钟。有点过分了!!!


再一看百分位,大部分的请求响应时间都在4s。无语了!!!


所以 1s钟的 吞吐量 单节点只有 50 。距离 500 差了10倍。 难受!!!!


分析过程


定位“慢”原因



这里暂时先忽略 CPU 占用率高的问题



首先平均响应时间这么慢,肯定是有阻塞。先确定阻塞位置。重点检查几处:



  • 锁 (同步锁、分布式锁、数据库锁)

  • 耗时操作 (链接耗时、SQL耗时)


结合这些先配置耗时埋点。



  1. 接口响应时长统计。超过500ms打印告警日志。

  2. 接口内部远程调用耗时统计。200ms打印告警日志。

  3. Redis访问耗时。超过10ms打印告警日志。

  4. SQL执行耗时。超过100ms打印告警日志。


上述配置生效后,通过日志排查到接口存在慢SQL。具体SQL类似与这种:


<!-- 主要类似与库存扣减 每次-1 type 只有有限的几种且该表一共就几条数据(一种一条记录)-->
<!-- 压测时可以认为 type = 1 是写死的 -->
update table set field = field - 1 where type = 1 and filed > 1;

上述SQL相当于并发操作同一条数据,肯定存在锁等待。日志显示此处的等待耗时占接口总耗时 80% 以上。


二话不说先改为敬。因为是压测环境,直接先改为异步执行,确认一下效果。实际解决方案,感兴趣的可以参考另外一篇文章:大量请求同时修改数据库表一条记录时应该如何设计


PS:当时心里是这么想的: 妥了,大功告成。就是这里的问题!绝壁是这个原因!优化一下就解决了。当然,如果这么简单就没有必要写这篇文章了...


优化后的效果:


image.png


嗯...


emm...


好! 这个优化还是很明显的,提升提升了近2倍。




此时已经感觉到有些不对了,慢SQL已经解决了(异步了~ 随便吧~ 你执行 10s我也不管了),虽然对吞吐量的提升没有预期的效果。但是数据是不会骗人的。


最大值: 已经从 5s -> 2s


百分位值: 4s -> 1s


这已经是很大的提升了。


继续定位“慢”的原因


通过第一阶段的“优化”,我们距离目标近了很多。废话不多说,继续下一步的排查。


我们继续看日志,此时日志出现类似下边这种情况:


2023-01-04 15:17:05:347 INFO **.**.**.***.50 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:348 INFO **.**.**.***.21 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:350 INFO **.**.**.***.47 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:465 INFO **.**.**.***.234 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:467 INFO **.**.**.***.123 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:581 INFO **.**.**.***.451 [TID: 1s22s72s8ws9w00] **********************

2023-01-04 15:17:05:702 INFO **.**.**.***.72 [TID: 1s22s72s8ws9w00] **********************

前三行info日志没有问题,间隔很小。第4 ~ 第5,第6 ~ 第7,第7 ~ 第8 很明显有百毫秒的耗时。检查代码发现,这部分没有任何耗时操作。那么这段时间干什么了呢?



  1. 发生了线程切换,换其他线程执行其他任务了。(线程太多了)

  2. 日志打印太多了,压测5分钟日志量500M。(记得日志打印太多是有很大影响的)

  3. STW。(但是日志还在输出,所以前两种可能性很高,而且一般不会停顿百毫秒)


按照这三个思路做了以下操作:


首先,提升日志打印级别到DEBUG。emm... 提升不大,好像增加了10左右。


然后,拆线程 @Async 注解使用线程池,控制代码线程池数量(之前存在3个线程池,统一配置的核心线程数为100)结合业务,服务总核心线程数控制在50以内,同步增加阻塞最大大小。结果还可以,提升了50,接近200了。


最后,观察JVM的GC日志,发现YGC频次4/s,没有FGC。1分钟内GC时间不到1s,很明显不是GC问题,不过发现JVM内存太小只有512M,直接给了4G。吞吐量没啥提升,YGC频次降低为2秒1次。


唉,一顿操作猛如虎。


PS:其实中间还对数据库参数一通瞎搞,这里不多说了。




其实也不是没有收获,至少在减少服务线程数量后还是有一定收获的。另外,已经关注到了另外一个点:CPU使用率,减少了线程数量后,CPU的使用率并没有明显的下降,这里是很有问题的,当时认为CPU的使用率主要与开启的线程数量有关,之前线程多,CPU使用率较高可以理解。但是,在砍掉了一大半的线程后,依然居高不下这就很奇怪了。


此时关注的重点开始从代码“慢”方向转移到“CPU高”方向。


定位CPU使用率高的原因


CPU的使用率高,通常与线程数相关肯定是没有问题的。当时对居高不下的原因考虑可能有以下两点:



  1. 有额外的线程存在。

  2. 代码有部分CPU密集操作。


然后继续一顿操作:



  1. 观察服务活跃线程数。

  2. 观察有无CPU占用率较高线程。


在观察过程中发现,没有明显CPU占用较高线程。所有线程基本都在10%以内。类似于下图,不过有很多线程。


image.png


没有很高就证明大家都很正常,只是多而已...


此时没有下一步的排查思路了。当时想着,算了打印一下堆栈看看吧,看看到底干了啥~


在看的过程中发现这段日志:


"http-nio-6071-exec-9" #82 daemon prio=5 os_prio=0 tid=0x00007fea9aed1000 nid=0x62 runnable [0x00007fe934cf4000]
java.lang.Thread.State: RUNNABLE
at org.springframework.core.annotation.AnnotationUtils.getValue(AnnotationUtils.java:1058)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.resolveExpression(AbstractAspectJAdvisorFactory.java:216)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.<init>(AbstractAspectJAdvisorFactory.java:197)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAnnotation(AbstractAspectJAdvisorFactory.java:147)
at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(AbstractAspectJAdvisorFactory.java:135)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvice(ReflectiveAspectJAdvisorFactory.java:244)
at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.instantiateAdvice(InstantiationModelAwarePointcutAdvisorImpl.java:149)
at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.<init>(InstantiationModelAwarePointcutAdvisorImpl.java:113)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisor(ReflectiveAspectJAdvisorFactory.java:213)
at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisors(ReflectiveAspectJAdvisorFactory.java:144)
at org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(BeanFactoryAspectJAdvisorsBuilder.java:149)
at org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(AnnotationAwareAspectJAutoProxyCreator.java:95)
at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.shouldSkip(AspectJAwareAdvisorAutoProxyCreator.java:101)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:291)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:455)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:117)
......
......

上边的堆栈发现了一个点: 在执行getBean的时候,执行了createBean方法。我们都知道Spring托管的Bean都是提前实例化好放在IOC容器中的。createBean要做的事情有很多,比如Bean的初始化,依赖注入其他类,而且中间还有一些前后置处理器执行、代理检查等等,总之是一个耗时方法,所以都是在程序启动时去扫描,加载,完成Bean的初始化。


而我们在运行程序线程堆栈中发现了这个操作。而且通过检索发现竟然有近200处。


通过堆栈信息很快定位到执行位置:


<!--BeanUtils 是 hutool 工具类。也是从IOC容器获取Bean 等价于 @Autowired 注解 -->
RedisTool redisTool = BeanUtils.getBean(RedisMaster.class);

而RedisMaster类


@Component
@Scope("prototype")
public class RedisMaster implements IRedisTool {
// ......
}

没错就是用了多例。而且使用的地方是Redis(系统使用Jedis客户端,Jedis并非线程安全,每次使用都需要新的实例),接口对Redis的使用还是比较频繁的,一个接口得有10次左右获取Redis数据。也就是说执行10次左右的createBean逻辑 ...


叹气!!!


赶紧改代码,直接使用万能的 new 。


在看结果之前还有一点需要提一下,由于系统有大量统计耗时的操作。实现方式是通过:


long start = System.currentTimeMillis();
// ......
long end = System.currentTimeMillis();
long runTime = start - end;


或者Hutool提供的StopWatch:


这里感谢一下huoger 同学的评论,当时还误以为该方式能够降低性能的影响,但是实际上也只是一层封装。底层使用的是 System.nanoTime()。


StopWatch watch = new StopWatch();
watch.start();
// ......
watch.stop();
System.out.println(watch.getTotalTimeMillis());

而这种在并发量高的情况下,对性能影响还是比较大的,特别在服务器使用了一些特定时钟的情况下。这里就不多说,感兴趣的可以自行搜索一下。





最终结果:



image.png





排查涉及的命令如下:



查询服务进程CPU情况: top –Hp pid


查询JVM GC相关参数:jstat -gc pid 2000 (对 pid [进程号] 每隔 2s 输出一次日志)


打印当前堆栈信息: jstack -l pid >> stack.log


总结


结果是好的,过程是曲折的。总的来说还是知识的欠缺,文章看起来还算顺畅,但都是事后诸葛亮,不对,应该是事后臭皮匠。基本都是边查资料边分析边操作,前后花费了4天时间,尝试了很多。



  • Mysql : Buffer Pool 、Change Buffer 、Redo Log 大小、双一配置...

  • 代码 : 异步执行,线程池参数调整,tomcat 配置,Druid连接池配置...

  • JVM : 内存大小,分配,垃圾收集器都想换...


总归一通瞎搞,能想到的都试试。


后续还需要多了解一些性能优化知识,至少要做到排查思路清晰,不瞎搞。




最后5行代码有哪些:



  1. new Redis实例:1

  2. 耗时统计:3

  3. SQL异步执行 @Async: 1(上图最终的结果是包含该部分的,时间原因未对SQL进行处理,后续会考虑Redis原子操作+定时同步数据库方式来进行,避免同时操数据库)


TODO


问题虽然解决了。但是原理还不清楚,需要继续深挖。



为什么createBean对性能影响这么大?



如果影响这么大,Spring为什么还要有多例?


首先非并发场景速度还是很快的。这个毋庸置疑。毕竟接口响应时间不足50ms。


所以问题一定出在,并发createBean同一对象的锁等待场景。根据堆栈日志,翻了一下Spring源码,果然发现这里出现了同步锁。相信锁肯定不止一处。


image.png


org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean


image.png



System.currentTimeMillis并发度多少才会对性能产生影响,影响有多大?



很多公司(包括大厂)在业务代码中,还是会频繁的使用System.currentTimeMillis获取时间戳。比如:时间字段赋值场景。所以,性能影响肯定会有,但是影响的门槛是不是很高。



继续学习性能优化知识




  • 吞吐量与什么有关?


首先,接口响应时长。直接影响因素还是接口响应时长,响应时间越短,吞吐量越高。一个接口响应时间100ms,那么1s就能处理10次。


其次,线程数。现在都是多线程环境,如果同时10个线程处理请求,那么吞吐量又能增加10倍。当然由于CPU资源有限,所以线程数也会受限。理论上,在 CPU 资源利用率较低的场景,调大tomcat线程数,以及并发数,能够有效的提升吞吐量。


最后,高性能代码。无论接口响应时长,还是 CPU 资源利用率,都依赖于我们的代码,要做高性能的方案设计,以及高性能的代码实现,任重而道远。



  • CPU使用率的高低与哪些因素有关?


CPU使用率的高低,本质还是由线程数,以及CPU使用时间决定的。


假如一台10核的机器,运行一个单线程的应用程序。正常这个单线程的应用程序会交给一个CPU核心去运行,此时占用率就是10%。而现在应用程序都是多线程的,因此一个应用程序可能需要全部的CPU核心来执行,此时就会达到100%。


此外,以单线程应用程序为例,大部分情况下,我们还涉及到访问Redis/Mysql、RPC请求等一些阻塞等待操作,那么CPU就不是时刻在工作的。所以阻塞等待的时间越长,CPU利用率也会越低。也正是因为如此,为了充分的利用CPU资源,多线程也就应运而生(一个线程虽然阻塞了,但是CPU别闲着,赶紧去运行其他的线程)。



  • 一个服务线程数在多少比较合适(算上Tomcat,最终的线程数量是226),执行过程中发现即使tomcat线程数量是100,活跃线程数也很少超过50,整个压测过程基本维持在20左右。


作者:FishBones
来源:juejin.cn/post/7185479136599769125
收起阅读 »

写了个数据查询为空的 Bug,你会怎么办?

大家在开发时,遇到的一个典型的 Bug 就是:为什么数据查询为空? 对应的现象就是:前端展示不出数据、或者后端查询到的数据列表为空。 遇到此类问题,其实是有经典的解决套路的,下面鱼皮给大家分享如何高效解决这个问题。 只需 4 个步骤: 解决步骤 1、定位问题...
继续阅读 »

大家在开发时,遇到的一个典型的 Bug 就是:为什么数据查询为空?


对应的现象就是:前端展示不出数据、或者后端查询到的数据列表为空。



遇到此类问题,其实是有经典的解决套路的,下面鱼皮给大家分享如何高效解决这个问题。


只需 4 个步骤:


解决步骤


1、定位问题边界


首先要定位数据查询为空的错误边界。说简单一点,就是要确认是前端还是后端的锅。


要先从请求的源头排查,也就是前端浏览器,毕竟前端和后端是通过接口(请求)交互的。


在浏览器中按 F12 打开浏览器控制台,进入网络标签,然后刷新页面或重新触发请求,就能看到请求的信息了。


选中请求并点击预览,就能看到后端返回结果,有没有返回数据一看便知。




如果发现后端正常返回了数据,那就是前端的问题,查看自己的页面代码来排查为什么数据没在前端显示,比如是不是取错了数据的结构?可以多用 debugger 或 console.log 等方式输出信息,便于调试。


星球同学可以免费阅读前端嘉宾神光的《前端调试通关秘籍》:t.zsxq.com/13Rh4xxNK


如果发现后端未返回数据,那么前端需要先确认下自己传递的参数是否正确。


比如下面的例子,分页参数传的太大了,导致查不到数据:



如果发现请求参数传递的没有问题,那么就需要后端同学帮忙解决了。


通过这种方式,直接就定位清楚了问题的边界,高效~


2、后端验证请求


接下来的排查就是在后端处理了,首先开启 Debug 模式,从接受请求参数开始逐行分析。


比如先查看请求参数对象,确认前端有没有按照要求传递请求参数:



毕竟谁能保证我们的同事(或者我们自己)不是小迷糊呢?即使前端说自己请求是正确的,但也必须要优先验证,而不是一上来就去分析数据库和后端程序逻辑的问题。


验证请求参数对象没问题后,接着逐行 Debug,直到要执行数据库查询。


3、后端验证数据库查询


无论是从 MySQL、MongoDB、Redis,还是文件中查询数据,为了理解方便,我们暂且统称为数据库。


上一步中,我们已经 Debug 到了数据库查询,需要重点关注 2 个点:


1)查看封装的请求参数是否正确


对于 MyBatis Plus 框架来说,就是查看 QueryWrapper 内的属性是否正确填充了查询条件



2)查看数据库的返回结果是否有值


比如 MyBatis Plus 的分页查询中,如果 records 属性的 size 大于 0,表示数据库返回了数据,那么就不用再排查数据库查询的问题了;而如果 size = 0,就要分析为什么从数据库中查询的数据为空。



这一步尤为关键,我们需要获取到实际发送给数据库查询的 SQL 语句。如果你使用的是 MyBatis Plus 框架,可以直接在 application.yml 配置文件中开启 SQL 语句日志打印,参考配置如下:


mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

然后执行查询,就能看到完整的 SQL 语句了:



把这个 SQL 语句复制到数据库控制台执行,验证下数据结果是否正确。如果数据库直接执行语句都查不出数据,那就确认是查询条件错误了还是数据库本身就缺失数据。


4、后端验证数据处理逻辑


如果数据库查询出了结果,但最终响应给前端的数据为空,那么就需要在数据库查询语句后继续逐行 Debug,验证是否有过滤数据的逻辑。


比较典型的错误场景是查询出的结果设置到了错误的字段中、或者由于权限问题被过滤和脱敏掉了。


最后


以后再遇到数据查询为空的情况,按照以上步骤排查问题即可。排查所有 Bug 的核心流程都是一样的,先搜集信息、再定位问题、最后再分析解决。


作者:程序员鱼皮
来源:juejin.cn/post/7306337248623132699
收起阅读 »

服务器:重来一世,这一世我要踏至巅峰!

前言 故事发生在上个星期一下午,秋风伴随着暖阳,映照出我在机房电脑上键盘敲击的身影。突然,伴随着一行指令运行mv /* ~/home/blog-end/,我发出土拨鼠尖叫——啊啊啊啊啊!!!!我服务器,窝滴服务器哟,哎哟,你干嘛,窝滴服务器哟!!! 就这样,我...
继续阅读 »

前言


故事发生在上个星期一下午,秋风伴随着暖阳,映照出我在机房电脑上键盘敲击的身影。突然,伴随着一行指令运行mv /* ~/home/blog-end/,我发出土拨鼠尖叫——啊啊啊啊啊!!!!我服务器,窝滴服务器哟,哎哟,你干嘛,窝滴服务器哟!!!


就这样,我把所有/目录下的文件给迁移了,/usr/bin/...所有文件都迁移了,还被我关了服务器窗口,后面重启也连不上了,我又是一声土拨鼠尖叫——啊啊啊啊啊啊!!!!如今只剩下一个方法了,那便是转世重修重新初始化系统......


重活一世,我要踏至巅峰


我,是上一代服务器的转世,重活一世,这一世我便要踏上那巅峰看一看,接下来便随着我一起打怪升级,踏上那巅峰吧......


搭建环境


在初始化系统的时候我选择的是诸天万界的高级系统ubuntu_22_04_x64,要部署的是我的博客项目,前端是nginx启动,后端是pm2启动,需要准备的环境有:nvm、node、mysql、git


1. 更新资源包,确保你的系统已经获取了最新的软件包信息


sudo apt update

2. 安装mysql


// 安装的时候一路`enter`就可以了
sudo apt install mysql-server

// 安装完后启动mysql服务
sudo systemctl start mysql

// 设置开机自启动
sudo systemctl enable mysql

// 检测数据库是否正在运行
sudo systemctl status mysql

// 运行以下指令登录数据库,第一次输入的密码会作为你数据库的密码
mysql -u root -p

// 如果输入密码报以下错误那就直接回车就能进入
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

// 进入之后记得修改密码,这里的new_password修改为自己的密码
ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';

//在这里我会创建一个子用户,使用子用户进行链接数据库操作,而不是直接root用户直接操作数据库
// 这里的dms换成用户名,PASSword123换成密码
create user 'dms'@'%' identified by 'PASSword123!'; // 创建子用户
grant all privileges on *.* to 'dms'@'%'with grant option; // 授权
flush privileges; // 生效用户



配置数据库运行远程链接


cd /etc/mysql/mysql.conf.d


vim mysqld.cnf //进入mysql配置文件修改 bind-address为0.0.0.0,如果是子用户的话需要在前面加上sudo提权



cfcec072591444fac34759c185c0d71.png


3. 安装nvm管理node版本


sudo apt install https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash

nvm --version // 查看是否正确输出

// 安装node版本
nvm install 19.1.0

// 查看是否正确输出
node --version
npm --version

4. 安装git并配置github


sudo apt install git

git --version // 查看输出版本

配置shh(这里我是直接一路Enter的)注意:这里要一定要使用以下指令生成ssh,后面有大用



①输入 ssh-keygen -m PEM -t rsa -b 4096,按enter;


②此后初次出现到②,出现的是保存密钥的路径,建议默认,按Enter;


③此时出现③,出现的提示是设置密码,千万不要设置!!!按Enter;


④此时出现④,出现的提示是再次输入密码,不要有任何输入,继续按Enter;



生成之后默认是在在服务器根目录下的.shh目录,这里直接运行以下指令


cd ~
cd .ssh
vim id_rsa.pub

进入id_rsa.pub文件复制公钥,到github的setting


66a36c9f2652d2f5a19a111b2064757.png
然后找到SSH and GPG keys去New SSH key,将公钥作为值保存就可以了
eb743773baf16231fe6d4a18ce3fbc7.jpg


5. 安装nginx并配置nginx.conf


sudo apt install nginx

// 安装完后启动nginx服务
sudo systemctl start nginx

// 设置开机自启动
sudo systemctl enable nginx

关于配置nginx,我一般每个nginx项目都会在conf.d目录单独写一个配置文件,方便后期更改,以下是我的个人博客的nginx配置,注意:conf.d里的配置文件后缀名必须是.conf才会生效


46d787353dfa50ecf76b09dfa1850d2.png



listen是监听的端口;
server name是服务器公网ip,也可以写域名;
root是前端项目所在地址;
index表示的是访问的index.html文件;
ry—_files这里是因为我vue项目打包用的history模式做的处理,hash模式可以忽略;



6. pm2的安装以及配置


npm install -g pm2

// 由于我项目使用了ts,并且没有去打包,所以我pm2也要安装ts-node
pm2 install ts-node

// 进入到后端项目的目录
cd /home/blog-end

// 初始化pm2文件
pm2 init // 运行之后会生成ecosystem.config.js配置文件

以下是我对pm2文件的配置,由于我是用了ts,所以我需要用到ts-node解释器,使用JavaScript的可以忽视interpreter属性


52360295a4677c6ac729236f5bd26a3.png


之后pm2 start econsystem.config.js运行配置文件就可以了


自动化部署


我自动化部署使用的技术是github actions,因为它简单容易上手,都是use轮子就完事了。下面跟我一起来做自动化部署


在开始自动化部署之前,我们还有一件大事要做,还记得之前生成ssh链接的时候说必须使用ssh-keygen -m PEM -t rsa -b 4096指令吗?现在就到了它表演的时候了,我们要用它配置ssh远程链接



先把.ssh目录下的id_rsa密钥复制到authorized_keys里,这一步就是配置远程ssh链接


然后配置sshd_config允许远程ssh链接,vim /etc/ssh/sshd_config,找到PermitRootLogin修改值为yes



b2018ae9ee0e125aa29f1a8d605f228.png


前端



进入自己的github项目地址,点击Actions去新建workflow,配置yml文件



9d0da5fcbaa38afac46d4876c15aac5.png



进入项目的setting里的Actions secrets and variables,创建secret



408b83ff0206b2973bbd7275ad7de80.png


后端



同样也是创建一个新的workflow,但服务端这里需要额外写一个脚本生成.env配置文件,因为服务端不可能把.env配置文件暴露到github的,那样特别不安全



script脚本


721c2b14136ed2436e8121c5b1c4b4c.png


yml配置文件


2e26f361e179354d35a163fbc593796.png


PS:觉得对自己有用或者文章还可以的话可以点个赞支持一下!!!


作者:辰眸
来源:juejin.cn/post/7299357353543368716
收起阅读 »

Rabbitmq消息大量堆积,我慌了!

背景 记得有次公司搞促销活动,流量增加,但是系统一直很平稳(我们开发的系统真牛),大家很开心的去聚餐,谈笑风声,气氛融洽,突然电话响起.... 运维:小李,你们系统使用的rabbitmq的消息大量堆积,导致服务器cpu飙升,赶紧回来看看,服务器要顶不住了 小...
继续阅读 »

背景


记得有次公司搞促销活动,流量增加,但是系统一直很平稳(我们开发的系统真牛),大家很开心的去聚餐,谈笑风声,气氛融洽,突然电话响起....



运维:小李,你们系统使用的rabbitmq的消息大量堆积,导致服务器cpu飙升,赶紧回来看看,服务器要顶不住了


小李:好的



系统架构描述


image.png


我们使用rabbitmq主要是为了系统解耦、异步提高系统的性能


前端售卖系统,生成订单后,推送订单消息到rabbitmq,订单履约系统作为消费者,消费订单消息落库,做后续操作


排查以及解决


方案一 增加消费者


第一我们想到的原因,流量激增,生成的订单速度远远大于消费者消费消息的速度,目前我们只部署了三个节点,那我们是否增加消费者,就可以解决这个问题,让消费者消费消息的速度远远大于生成者生成消息的速度,那消息就不存在堆积的问题,自然服务器压力也就下来了


通知运维,再部署三个点,也是就增加三个消费者,由原来的三个消费者变为6个消费者,信心满满的部署完成后,等待一段时间,不出意外还是出了意外,消息还是在持续堆积,没有任何改善,我心里那个急啊,为什么增加了消费者?一点改善没有呢


方案二 优化消费者的处理逻辑


持续分析,是不是消费者的逻辑有问题,处理速度还是慢?在消费逻辑分析中,发现在处理订单消息的逻辑里,调用了库存系统的一个接口,有可能是这个接口响应慢,导致消费的速度慢,跟不上生产消息的速度。


查看库存系统的运行情况,发现系统压力非常大,接口请求存在大量超时的情况,系统也在崩溃的边缘,因为我们上面的解决方案,增加了三个节点,间接的增大了并发。告知负责库存系统的同学,进行处理排查解决,但一时解决不了,如果持续这样,整体链路有可能全部崩掉,这怎么办呢?


消费者逻辑优化,屏蔽掉调用库存的接口,直接处理消息,但这种我们的逻辑是不完成,虽然能减少服务器的压力,后续处理起来也非常的麻烦,这种方式不可取


方案三 清空堆积的消息


为了减少消息的堆积,减轻服务器的压力,我们是否可以把mq里面的消息拿出来,先存储,等服务恢复后,再把存储的消息推送到mq,再处理呢?



  • 新建消费者,消费rabbitmq的消息,不做任何业务逻辑处理,直接快速消费消息,把消息存在一张表里,这样就没消息的堆积,服务器压力自然就下来了。


image.png
这方案上线后,过了一段时间观察,消息不再堆积,服务器的负载也下来了,我内心也不再慌了,那存储的那些消息,还处理吗?当然处理,怎么处理呢?



  • 后续等库存服务问题解决后,停掉新的消费者,新建一个生产者,再把表里的订单数据推送到rabbitmq,进行业务逻辑的处理


image.png


至此,问题就完美的解决了,悬着的心也放下了


问题产生的原因分析


整个链路服务一直都是很稳定的,因为流量的激增,库存服务的服务能力跟不上,导致整个链路出了问题,如果平台要搞促销这种活动,我们还是要提前评估下系统的性能,对整个链路做一次压测,找出瓶颈,该优化的要优化,资源不足的加资源


消息堆积为什么会导致cpu飙升呢?


问题虽然解决了,但我很好奇,消息堆积为什么会导致cpu飙升呢?


RabbitMQ 是一种消息中间件,用于在应用程序之间传递消息。当消息堆积过多时,可能会导致 CPU 飙升的原因有以下几点:



  1. 消息过多导致消息队列堆积:当消息的产生速度大于消费者的处理速度时,消息会积累在消息队列中。如果消息堆积过多,RabbitMQ 需要不断地进行消息的存储、检索和传递操作,这会导致 CPU 使用率升高。

  2. 消费者无法及时处理消息:消费者处理消息的速度不足以追赶消息的产生速度,导致消息不断积累在队列中。这可能是由于消费者出现瓶颈,无法处理足够多的消息,或者消费者的处理逻辑复杂,导致消费过程耗费过多的 CPU 资源。

  3. 消息重试导致额外的 CPU 开销:当消息处理失败时,消费者可能会进行消息的重试操作,尝试再次处理消息。如果重试频率较高,会导致消息在队列中频繁流转、被重复消费,这会增加额外的 CPU 开销。

  4. 过多的连接以及网络IO:当消息堆积过多时,可能会引发大量的连接请求和网络数据传输。这会增加网络 IO 的负载,并占用 CPU 资源。


通用的解决方案



  • 增加消费者:通过增加消费者的数量来提升消息的处理能力。增加消费者可以分担消息消费的负载,缓解消息队列的堆积问题。

  • 优化消费者的处理逻辑:检查消费者的代码是否存在性能瓶颈或是复杂的处理逻辑。可以通过优化算法、减少消费过程的计算量或是提高代码的效率来减少消费者的 CPU 开销。

  • 避免频繁的消息重试:当消息无法处理时,可以根据错误类型进行不同的处理方式,如将无法处理的消息转移到死信队列中或进行日志记录。避免频繁地对同一消息进行重试,以减少额外的 CPU 开销。

  • 调整 RabbitMQ 配置:可以调整 RabbitMQ 的参数来适应系统的需求,如增加内存、调整消息堆积的阈值和策略,调整网络连接等配置。

  • 扩展硬件资源:如果以上措施无法解决问题,可能需要考虑增加 RabbitMQ 的集群节点或者扩容服务器的硬件资源,以提升整个系统的处理能力。


需要根据具体情况综合考虑以上因素,并结合实际情况进行调试和优化,以解决消息堆积导致 CPU 飙升的问题,不能照葫芦画瓢,像我第一次直接增加消费者,差点把这个链路都干挂了



写作不易,刚好你看到,刚好对你有帮助,麻烦点点赞,有问题的留言讨论。



作者:柯柏技术笔记
来源:juejin.cn/post/7306442629318377535
收起阅读 »

很容易中招的一种索引失效场景,一定要小心

快过年,我的线上发布出现故障 “五哥,你在上线吗?”,旁边有一个声音传来。 “啊,怎么了?”。真是要命,在上线发布时候,我最讨厌别人叫我的名字 。我慌忙站起来,看向身后,原来是 建哥在问我。我慌忙的问,怎么回事。 “DBA 刚才在群里说,Task数据库 cpu...
继续阅读 »

快过年,我的线上发布出现故障


“五哥,你在上线吗?”,旁边有一个声音传来。


“啊,怎么了?”。真是要命,在上线发布时候,我最讨厌别人叫我的名字 。我慌忙站起来,看向身后,原来是 建哥在问我。我慌忙的问,怎么回事。


“DBA 刚才在群里说,Task数据库 cpu 负载增加!有大量慢查询”,建哥来我身边,跟我说。


慢慢的,我身边聚集着越来越多的人


image.png


“你在上线Task服务吗?改动什么内容了,看看要不要立即回滚?”旁边传来声音。此时,我的心开始怦怦乱跳,手心发痒,紧张不已。


我检查着线上机器的日志,试图证明报警的原因不是出在我这里。


我对着电脑,微微颤抖地回答大家:“我只是升级了基础架构的Jar包,其他内容没有改动啊。”此时我已分不清是谁在跟我说话,只能对着电脑作答……


这时DBA在群里发送了一条SQL,他说这条SQL导致了大量的慢查询。


我突然记起来了,我转过头问林哥:“林哥,你上线了什么内容?”这次林哥有代码的变更,跟我一起上线。我觉得可能是他那边有问题。


果然,林哥看着代码发呆。他嘟囔道:“我添加了索引啊,怎么会有慢查询呢?”原来慢查询的SQL是林哥刚刚添加的,这一刻我心里的石头放下了,问题并不在我,我轻松了许多。


“那我先回滚吧”,幸好我们刚发布了一半,现在回滚还来得及,我尝试回滚机器。此刻我的紧张情绪稍稍平静下来,手也不再发抖。


既然不是我的问题,我可以以吃瓜的心态,暗中观察事态的发展。我心想:真是吓死我了,幸好不是我的错。


然而我也有一些小抱怨:为什么非要和我一起搭车上线,出了事故,还得把我拖进来。


故障发生前的半小时


2年前除夕前的一周,我正准备着过年前的最后一次线上发布,这时候我刚入职两个月,自然而然会被分配一些简单的小活。这次上线的内容是将基础架构的Jar包升级到新版本。一般情况下,这种配套升级工作不会出问题,只需要按部就班上线就行。


“五哥,你是要上线 Task服务吗?”,工位旁的林哥问我,当时我正做着上线前的准备工作。


“对啊,马上要发布,怎么了?”,我转身回复他。


“我这有一个代码变更,跟你搭车一起上线吧,改动内容不太多。已经测试验证过了”,林哥说着,把代码变更内容发给我,简单和我说了下代码变更的内容。我看着改动内容确实不太多,新增了一个SQL查询,于是便答应下来。我重新打包,准备发布上线。


半小时以后,便出现了文章开头的情景。新增加的SQL 导致大量慢查询,数据库险些被打挂。


为什么加了索引,还会出现慢查询呢?


”加了索引,为什么还有慢查询?“,这是大家共同的疑问。


事后分析故障的原因,通过 mysql explain 命令,查看该SQL 确实没有命中索引,从而导致慢查询。


这个SQL 大概长这个样子!我去掉了业务相关的部分。


select * from order_discount_detail where orderId = 1123;


order_discount_detailorderId 这一列上确实加了索引,不应该出现慢查询,乍一看,没有什么问题。我本能的想到了索引失效的几种场景。难道是类型不匹配,导致索引失效?


果不其然, orderId 在数据库中的类型 是 varchar 类型,而传参是按照 long 类型传的。


复习一下: 类型转换导致索引失效


类型转换导致索引失效,是很容易犯的错误


因为在某些特殊场景下要对接外部订单,存在订单Id为字符串的情况,所以 orderId被设计成 varchar 字符串类型。然而出问题的场景比较明确,订单id 就是long类型,不可能是字符串类型。


所以林哥,他在使用Mybatis 时,直接使用 long 类型的 orderId字段传参,并且没有意识到两者数据类型不对。


因为测试环境数据量比较小,即使没有命中索引,也不会有很严重的慢查询,并且测试环境请求量比较低,该慢查询SQL 执行次数较少,所以对数据库压力不大,测试阶段一直没有发现性能问题。


直到代码发布到线上环境————数据量和访问量都非常高的环境,差点把数据库打挂。


mybatis 能避免 “类型转换导致索引失效” 的问题吗?


mybatis能自动识别数据库和Java类型不一致的情况吗?如果发现java类型和数据库类型不一致,自动把java 类型转换为数据库类型,就能避免索引失效的情况!


答案是不能。我没找到 mybatis 有这个能力。


mybatis 使用 #{} 占位符,会自动根据 参数的 Java 类型填充到 SQL中,同时可以避免SQL注入问题。


例如刚才的SQL 在 mybatis中这样写。


select * from order_discount_detail where orderId = #{orderId};


orderId 是 String 类型,SQL就变为


select * from order_discount_detail where orderId = ‘1123’;


mybatis 完全根据 传参的java类型,构建SQL,所以不要认为 mybatis帮你处理好java和数据库的类型差异问题,你需要自己关注这个问题!


再次提醒,"类型转换导致索引失效"的问题,非常容易踩坑。并且很难在测试环境发现性能问题,等到线上再发现问题就晚了,大家一定要小心!小心!


险些背锅


可能有朋友疑问,为什么发布一半时出现慢查询,单机发布阶段不能发现这个问题吗?


之所以没发现这个问题,是因为 新增SQL在 Kafka消费逻辑中,由于单机发布机器启动时没有争抢到 kafka 分片,所以没有走到新代码逻辑。


此外也没有遵循降级上线的代码规范,如果上线默认是降级状态,上线过程中就不会有问题。放量阶段可以通过降级开关快速止损,避免回滚机器过程缓慢而导致的长时间故障。


不是我的问题,为什么我也背了锅


因为我在发布阶段没有遵循规范,按照规定的流程应该在单机发布完成后进行引流压测。引流压测是指修改机器的Rpc权重,将Rpc请求集中到新发布的单机上,这样就能提前发现线上问题。


然而由于我偷懒,跳过了单机引流压测。由于发布的第一台机器没有抢占到Kafka分片,因此无法执行新代码逻辑。即使进行了单机引流压测,也无法提前发现故障。虽然如此,但我确实没有遵循发布规范,错在我。


如果上线时没有出现故障,这种不规范的上线流程可能不会受到责备。但如果出现问题,那只能怪我倒霉。在复盘过程中,我的领导抓住了这件事,给予了重点批评。作为刚入职的新人,被指责确实让我感到不舒服。


快要过年了,就因为搭车上线,自己也要承担别人犯错的后果,让我很难受。但是自己确实也有错,当时我的心情复杂而沉重。


两年前的事了,说出来让大家吃个瓜,乐呵一下。如果这瓜还行,东东发财的小手点个赞


作者:五阳神功
来源:juejin.cn/post/7305572311812636683
收起阅读 »

货拉拉App录制回放的探索与实践

作者简介:徐卓毅Joe,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。 一、背景与目标 近些年货拉拉的业务持续高速发展,为了满足业务更短周期、更高质量交付的诉求,从今年开始我们的移动App的迭代交付模型也从双周演化为单周。因此,在一周一版的紧张节...
继续阅读 »

作者简介:徐卓毅Joe,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。



一、背景与目标


近些年货拉拉的业务持续高速发展,为了满足业务更短周期、更高质量交付的诉求,从今年开始我们的移动App的迭代交付模型也从双周演化为单周。因此,在一周一版的紧张节奏下,随之而来的对测试质量保障的挑战也日益增加,首当其冲要解决的就是如何降低移动App每周版本回归测试的人力投入。


早期我们尝试过基于Appium框架编写UI自动化测试脚本,并且为了降低编写难度,我们也基于Appium框架进行了二次开发,但实践起来依然困难重重,主要原因在于:




  1. 上手和维护成本高



    • 需要掌握一定基础知识才能编写脚本和排查过程中遇到的问题;

    • 脚本编写+调试耗时长,涉及的元素定位+操作较多,调试要等待脚本执行回放才能看到结果;

    • 排查成本高,由于UI自动化测试的稳定性低,需投入排查的脚本较多,耗时长;

    • 维护成本高,每个迭代的需求改动都可能导致页面元素或链路调整,需不定期维护;




  2. 测试脚本稳定性低



    • 容易受多种因素(服务端环境、手机环境等)影响,这也造成了问题排查和溯源困难;

    • 脚本本身的稳定性低,模拟手工操作的方式,但实际操作点击没有那么智能;

      • 脚本识别元素在不同分辨率、不同系统版本上,识别的速度及准确度不同;

      • 不同设备在某些操作上表现,例如缩放(缩放多少)、滑动(滑动多少)有区别;

      • 由于功能复杂性、不同玩法的打断(如广告、弹窗、ab实验等);






所以,在App UI自动化测试上摸爬滚打一段时间后,我们积累了大量的踩坑经验。但这些经验也让我们更加明白,如果要大规模推行App UI自动化测试,必须要提高自动化ROI,否则很难达到预期效果,成本收益得不偿失。


我们的目标是打造一个低成本、高可用的App UI自动化测试平台。它需要满足如下条件:



  1. 更低的技术门槛:上手简单,无需环境配置;

  2. 更快的编写速度:无需查找控件,手机上操作后就能生成一条可执行的测试脚本;

  3. 更小的维护成本: 支持图像识别,减少由于控件改动导致的问题;

  4. 更高的稳定性: 回放识别通过率高,降低环境、弹窗的影响;

  5. 更好的平台功能: 支持脚本管理、设备调度、测试报告等能力,提升执行效率,降低排查成本;


二、行业方案


image.png


考虑到自动化ROI,我们基本确定要使用基于录制回放方式的自动化方案,所以我们也调研了美团、爱奇艺、字节、网易这几个公司的测试工具平台的实现方案:



  1. 网易Airtest是唯一对外发布的工具,但免费版本是IDE编写的,如果是小团队使用该IDE录制UI脚本来说还是比较方便的,但对于多团队协同,以及大规模UI自动化的实施的需求来说,其脚本管理、设备调度、实时报告等平台化功能的支持还不满足。

  2. 美团AlphaTest上使用的是App集成SDK的方式,可以通过底层Hook能力采集到操作数据、网络数据等更为详尽的内容,也提供了API支持业务方自定义实现,如果采用这种方案,移动研发团队的配合是很重要的。

  3. 爱奇艺的方案是在云真机的基础上,使用云IDE的方式进行录制,重点集成了脚本管理、设备调度、实时报告等平台化功能,这种方案的优势在于免去开发SDK的投入,可以做成通用能力服务于各业务App。

  4. 字节SmartEye也是采用集成SDK的方式,其工具本身更聚焦精准测试的能力建设,而精准测试当前货拉拉也在深入实践中,后续有机会我们再详细介绍。


综上分析,如果要继续推行App UI自动化测试,我们也需要自研测试平台,最好是能结合货拉拉现有的业务形态和能力优势,用最低的自研方案成本,快速搭建起适合我们的App录制回放测试平台,这样就能更快推动实践,降低业务测试当前面临的稳定性保障的压力。


三、能力建设


image.png


货拉拉现有的能力优势主要有:



  1. 货拉拉的云真机建设上已有成熟的经验(感兴趣的读者可参见文章《货拉拉云真机平台的演进与实践》);

  2. 货拉拉在移动App质效上已有深入实践,其移动云测平台已沉淀了多维度的自动化测试服务(如性能、兼容性、稳定性、健壮性、遍历、埋点等),具备比较成熟的平台能力。


因此,结合多方因素,最终我们选择了基于云真机开展App UI录制回放的方案,在借鉴其他公司优秀经验的基础上,结合我们对App UI自动化测试过程中积累的宝贵经验,打造了货拉拉App云录制回放测试平台。


下面我们会按录制能力、回放能力、平台能力三大部分进行介绍。


3.1 录制能力


录制流程从云真机的操作事件开始,根据里面的截图和操作坐标解析操作的控件,最终将操作转化为脚本里的单个步骤。并且支持Android和iOS双端,操作数据上报都是用旁路上报的方式,不会阻塞在手机上的操作。


image.png
下面是我们当前基于云真机录制的效果:



  在录制的过程中,其目标主要有:



  1. 取到当前操作的类型 点击、长按、输入、滑动等;

  2. 取到操作的目标控件 按钮、标签栏、文本框等;


3.1.1 云真机旁路上报&事件解析


  首先要能感知到用户在手机上做了什么操作,当我们在页面上使用云真机时,云真机后台可以监控到最原始的屏幕数据,不同操作的数据流如下:


// 点击
d 0 10 10 50
c
u 0
c
// 长按
d 0 10 10 50
c
<wait in your own code>
u 0
c
// 滑动
d 0 0 0 50
c
<wait in your own code> //需要拖拽加上等待时间
m 0 20 0 50
c
m 0 40 0 50
c
m 0 60 0 50
c
m 0 80 0 50
c
m 0 100 0 50
c
u 0
c

  根据协议我们可以判断每次操作的类型以及坐标,但仅依赖坐标的录制并不灵活,也不能实现例如断言一类的操作,所以拿到控件信息也非常关键。


  一般UI自动化中会dump出控件树,通过控件ID或层级关系定位控件。而dump控件树是一个颇为耗时的动作,普通布局的页面也需要2S左右。



  如果在录制中同时dump控件树,那我们每点击都要等待进度条转完,显然这不是我们想要的体验。而可以和操作坐标一起拿到的还有手机画面的视频流,虽然单纯的截图没有控件信息,但假如截图可以像控件树一样拆分出独立的控件区域,我们就可以结合操作坐标匹配对应控件。


3.1.2 控件/文本检测


  控件区域检测正是深度学习中的目标检测能解决的问题。


  这里我们先简单看一下深度学习的原理以及在目标检测过程中做了什么。


  深度学习原理



深度学习使用了一种被称为神经网络的结构。像人脑中的神经元一样,神经网络中的节点会对输入数据进行处理,然后将结果传递到下一个层级。这种逐层传递和处理数据的方式使得深度学习能够自动学习数据的复杂结构和模式。



  总的来说,深度学习网络逐层提取输入的特征,总结成更抽象的特征,将学习到的知识作为权重保存到网络中。


image.pngimage.png

举个例子,如果我们使用深度学习来学习识别猫的图片,那么神经网络可能会在第一层学习识别图片中的颜色或边缘,第二层可能会识别出特定的形状或模式,第三层可能会识别出猫的某些特征,如猫的眼睛或耳朵,最后,网络会综合所有的特征来确定这张图片是否是猫。


  目标检测任务


  目标检测是深度学习中的常见任务,任务的目标是在图像中识别并定位特定物体。


  在我们的应用场景中,任务的目标自然是UI控件:



  1. 识别出按钮、文本框等控件,可以归类为图标、图片和文本;

  2. 圈定控件的边界范围;


这里我们选用知名的YOLOX目标检测框架,社区里也开放许多了以UI为目标的预训练模型和数据集,因为除了自动化测试外,还有通过UI设计稿生成前端代码等应用场景。


roboflow公开数据集


  下图是使用公开数据集直接推理得到的控件区域,可以看出召回率不高。这是因为公开数据集中国外APP标注数据更多,且APP的UI风格不相似。


示例一示例二

预训练和微调模型


  而最终推理效果依赖数据集质量,这需要我们微调模型。由于目标数据集相似,所以我们只需要在预训练模型基础时,冻结骨干网络,重置最后输出层权重,喂入货拉拉风格的UI数据继续训练,可以得到更适用的模型。


model = dict (backbone=dict (frozen_stages=1 # 表示第一层 stage 以及它之前的所有 stage 中的参数都会被冻结 )) 


通过目标检测任务,我们可以拿到图标类的控件,控件的截图可以作为标识存储。当然,文本类的控件还是转化成文本存储更理想。针对文本的目标检测任务不仅精准度更高,还能提供目标文本的识别结果。我们单独用PaddleOCR再做了一次文本检测识别。


3.1.3 脚本生成


  所有操作最终都会转化为脚本储存,我们自定义了一种脚本格式用来封装不同的UI操作。


  以一次点击为例,操作类型用Click()表示;如果是点击图标类控件,会将图标的截图保存(以及录制时的屏幕相对坐标,用于辅助回放定位),而点击文案则是记录文本。



  操作消抖: 点击、长按和滑动之间通过设置固定的时长消除实际操作时的抖动,我们取系统中的交互动效时长,一般是200~300ms。


  文本输入: 用户实际操作输入文本时分为两种情况,一是进入页面时自动聚焦编辑框,另一种是用户主动激活编辑,都会拉起虚拟键盘。我们在回放时也需要在拉起键盘的情况下输入,才能真实还原键盘事件对页面的影响。


am broadcast -a ADB_INPUT_B64 --es msg "xxx"

  目标分组: 一个页面上可能有多个相同的图标或文案,所以在录制时会聚合相同分组,在脚本中通过下标index(0)区分。


3.2 回放能力


  回放脚本时,则是根据脚本里记录的控件截图和文本,匹配到回放手机上的目标区域,进而执行点击、滑动等操作。这里用到的图像和文本匹配能力也会用在脚本断言里。


image.png


回放效果见下图:



3.2.1 图像匹配


  与文本相比,图标类控件在回放时要应对的变化更多:



  • 颜色不同;

  • 分辨率不同

  • 附加角标等提示;


  在这种场景中,基于特征点匹配的SIFT算法很合适。



尺度不变特征变换(Scale-invariant feature transform, SIFT)是计算机视觉中一种检测、描述和匹配图像局部特征点的方法,通过在不同的尺度空间中检测极值点或特征点(Conrner Point, Interest Point),提取出其位置、尺度和旋转不变量,并生成特征描述子,最后用于图像的特征点匹配。



  对图像做灰度预处理之后能减少颜色带来的噪音,而SIFT的尺度不变特性容忍了分辨率变化,附加的角标不会影响关键特征点的匹配。


  除此之外,为了减低误匹配,我们增加了两个操作:


  RegionMask:在匹配之前,我们也做了控件检测,并作为遮罩层Mask设置到SIFT中,排除错误答案之后的特征点更集中稳定。



  屏蔽旋转不变性:因为不需要在页面上匹配旋转后的目标,所以我们将提取的特征点向量角度统一重置为0。


  sift.detect(image, kpVector, mask);
// 设置角度统一为0,禁用旋转不变性
for (int i = 0; i < kpVector.size(); i++) {
KeyPoint point = kpVector.get(i);
point.angle(0);
...
}
sift.compute(image, kpVector, ret);

3.2.2 文本匹配


  文本匹配很容易实现,在OCR之后做字符串比较可以得到结果。


  但是因为算法本身精准度并不是百分百(OCR识别算法CRNN精准度在80%),遇到长文案时会出现识别错误,我们通过计算与期望文本间的编辑距离容忍这种误差。



  但最常见的还是全角和半角字符间的识别错误,需要把标点符号作为噪音去除。


  还有另一个同样和长文案有关的场景:机型宽度不同时,会出现文案换行展示的情况,这时就不能再去完整匹配,但可以切换到xpath使用部分匹配


//*[contains(@text,'xxx')]

3.2.3 兜底弹窗处理


  突然出现的弹窗是UI自动化中的一大痛点,无论是时机和形式都无法预测,造成的结果是自动化测试中断。



  弹窗又分为系统弹窗和业务弹窗,我们有两种处理弹窗的策略:



  1. Android提供了一个DeviceOwner角色托管设备,并带有一个策略配置(PERMISSION_POLICY_AUTO_GRANT),测试过程中APP申请权限时天宫管家自动授予权限;




  1. 在自动化被中断时,再次检查页面有没有白名单中的弹窗文案,有则触发兜底逻辑,关闭弹窗后,恢复自动化执行。


3.2.4 自动装包授权


  Android碎片化带来的还有不同的装包验证策略,比如OPPO&VIVO系机型就需要输入密码才能安装非商店应用。


  为了保持云真机的环境纯净,我们没有通过获取ROOT授权的方式绕过,而是采用部署在云真机内置的装包助手服务适配了不同机型的装包验证。




3.2.5 数据构造&请求MOCK


  目前为止我们录制到的还只有UI的操作,但场景用例中缺少不了测试数据的准备。
  首先是测试数据构造,脚本中提供一个封装好的动作,调用内部平台数据工厂,通过传入和保存变量能在脚本间传递调用的数据。



  同时脚本还可以关联到APP-MOCK平台,在一些固定接口或特定场景MOCK接口响应。譬如可以固定AB实验配置,又或是屏蔽推送类的通知。



3.1 平台能力


3.3.1 用例编辑&管理


  有实践过UI自动化的人应该有这种感受,在个人电脑搭建一套自动化环境是相当费劲的,更不用说要同时兼顾Android和iOS。


  当前我们已经达成了UI自动化纯线上化这一个小目标,只需要在浏览器中就可以完成UI脚本的编辑、调试和执行。现在正完善更多的线上操作,以Monaco Editor为基础编辑器提供更方便的脚本编辑功能。


image.png


3.3.2 脚本组&任务调度


  为了方便管理数量渐涨的用例,我们通过脚本组的方式分模块组织和执行脚本。每个脚本组可以设置前后置脚本和使用的帐号类别,一个脚本组会作为最小的执行单元发送到手机上执行。



  我们可以将回归场景拆分成若干个组在多台设备上并发执行,大大缩短了自动化用例的执行时间。


四、效果实践


4.1 回归测试提效


App录制回放能力建设完毕后,我们立即在多个业务线推动UI自动化测试实践。我们也专门成立了一支虚拟团队,邀请各团队骨干加入,明确回归测试提效的目标,拉齐认知,统一节奏,以保障UI自动化的大规模实践的顺利落地。




  1. 建立问题同步及虚拟团队管理的相关制度,保障问题的快速反馈和快速解决。




  2. 制定团队的UI测试实践管理规范,指导全体成员按统一的标准去执行,主要包括:



    • 回归用例筛选:按模块维度进行脚本转化,优先覆盖P0用例(占比30%左右);

    • 测试场景设计:设计可以串联合并的场景,这样合并后可提升自动化执行速度;

    • 测试数据准备:自动化账号怎么管理,有哪些推荐的数据准备方案;

    • 脚本编写手册:前置脚本、公共脚本引入规范、断言规范等;

    • 脚本执行策略:脚本/脚本组管理及执行策略,怎样能执行的更快;




image.png


所以,我们在很短的时间内就完成了P0回归测试用例的转化,同时我们还要求:



  1. 回放通过率必须高于90%,避免给业务测试人员造成额外的干扰,增加排查工作量;

  2. 全量场景用例的执行总时长要小于90分钟,充分利用云真机的批量调度能力,快速输出测试报告。而且某种程度来说,还能避开因服务端部署带来的环境问题的影响;


截止目前,我们已经支持10多次单周版本的回归测试,已经可以替代部分手工回归测试工作量,降低测试压力的同时提升了版本发布质量的信心。


4.2 整体测试效能提升


在App UI自动化测试的实施取得突破性进展后,我们开始尝试优化原有性能、兼容、埋点等自动化测试遇到的一些问题,以提升移动App的整体测试效能。



  • App性能自动化测试: 原有的性能测试脚本都是使用基于UI元素定位的方式,每周的功能迭代都或多或少会影响到脚本的稳定性,所以我们的性能脚本早期每周都需要维护。而现在的性能测试脚本通过率一般情况下都是100%,极个别版本才会出现微调脚本的情况。

  • App深度兼容测试: 当涉及移动App测试时,兼容性测试的重要性不言而喻。移动云测平台在很早就已支持了标准兼容测试能力,即结合智能遍历去覆盖更多的App页面及场景,去发现一些基础的兼容测试问题。但随着App UI自动化测试的落地,现在我们已经可以基于大量的UI测试脚本在机房设备上开展深度兼容测试。


机房执行深度兼容测试


  • App 埋点 自动化测试: 高价值埋点的回归测试,以往我们都需要在回归期间去手工额外去触发操作路径,现在则基于UI自动化测试模拟用户操作行为,再结合移动云测平台已有的埋点自动校验+测试结果实时展示的能力,彻底解放人力,实现埋点全流程自动化测试。




  • 接入 CICD 流水线: 我们将核心场景的UI回归用例配CICD流水线中,每当代码合入或者触发构建后,都会自动触发验证流程,如果测试不通过,构建人和相关维护人都能立即收到消息通知,进一步提升了研发协同效率。


流程图 (3).jpg


五、未来展望



“道阻且长,行则将至,行而不辍,未来可期”。——《荀子·修身》



货拉拉App云录制回放测试平台的建设上,未来还有一些可提升的方向:



  1. 迭代优化模型,提升精准度和性能;

  2. 补全数据的录制回放,增加本地配置和缓存的控制;

  3. 探索使用AI大模型的识图能力,辨别APP页面上的UI异常;

  4. 和客户端精准测试结合,推荐未覆盖场景和变更相关用例;


作者:货拉拉技术
来源:juejin.cn/post/7306331307477794867
收起阅读 »

4 种消息队列,如何选型?

大家好呀,我是楼仔。 最近发现很多号主发消息队列的文章,质量参差不齐,相关文章我之前也写过,建议直接看这篇。 这篇文章,主要讲述 Kafka、RabbitMQ、RocketMQ 和 ActiveMQ 这 4 种消息队列的异同,无论是面试,还是用于技术选型,都有...
继续阅读 »

大家好呀,我是楼仔。


最近发现很多号主发消息队列的文章,质量参差不齐,相关文章我之前也写过,建议直接看这篇。


这篇文章,主要讲述 Kafka、RabbitMQ、RocketMQ 和 ActiveMQ 这 4 种消息队列的异同,无论是面试,还是用于技术选型,都有非常强的参考价值。


不 BB,上文章目录:



01 消息队列基础


1.1 什么是消息队列?


消息队列是在消息的传输过程中保存消息的容器,用于接收消息并以文件的方式存储,一个消息队列可以被一个也可以被多个消费者消费,包含以下 3 元素:



  • Producer:消息生产者,负责产生和发送消息到 Broker;

  • Broker:消息处理中心,负责消息存储、确认、重试等,一般其中会包含多个 Queue;

  • Consumer:消息消费者,负责从 Broker 中获取消息,并进行相应处理。



1.2 消息队列模式



  • 点对点模式:多个生产者可以向同一个消息队列发送消息,一个具体的消息只能由一个消费者消费。




  • 发布/订阅模式:单个消息可以被多个订阅者并发的获取和处理。



1.3 消息队列应用场景



  • 应用解耦:消息队列减少了服务之间的耦合性,不同的服务可以通过消息队列进行通信,而不用关心彼此的实现细节。

  • 异步处理:消息队列本身是异步的,它允许接收者在消息发送很长时间后再取回消息。

  • 流量削锋:当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的”载体”,在下游有能力处理的时候,再进行分发与处理。

  • 日志处理:日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。

  • 消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯,比如实现点对点消息队列,或者聊天室等。

  • 消息广播:如果没有消息队列,每当一个新的业务方接入,我们都要接入一次新接口。有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。


02 常用消息队列


由于官方社区现在对 ActiveMQ 5.x 维护越来越少,较少在大规模吞吐的场景中使用,所以我们主要讲解 Kafka、RabbitMQ 和 RocketMQ。


2.1 Kafka


Apache Kafka 最初由 LinkedIn 公司基于独特的设计实现为一个分布式的提交日志系统,之后成为 Apache 项目的一部分,号称大数据的杀手锏,在数据采集、传输、存储的过程中发挥着举足轻重的作用。


它是一个分布式的,支持多分区、多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系统。


重要概念



  • 主题(Topic):消息的种类称为主题,可以说一个主题代表了一类消息,相当于是对消息进行分类,主题就像是数据库中的表。

  • 分区(partition):主题可以被分为若干个分区,同一个主题中的分区可以不在一个机器上,有可能会部署在多个机器上,由此来实现 kafka 的伸缩性。

  • 批次:为了提高效率, 消息会分批次写入 Kafka,批次就代指的是一组消息。

  • 消费者群组(Consumer Gr0up):消费者群组指的就是由一个或多个消费者组成的群体。

  • Broker: 一个独立的 Kafka 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。

  • Broker 集群:broker 集群由一个或多个 broker 组成。

  • 重平衡(Rebalance):消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。


Kafka 架构


一个典型的 Kafka 集群中包含 Producer、broker、Consumer Gr0up、Zookeeper 集群。


Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 Consumer Gr0up 发生变化时进行 rebalance。Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。



Kafka 工作原理


消息经过序列化后,通过不同的分区策略,找到对应的分区。


相同主题和分区的消息,会被存放在同一个批次里,然后由一个独立的线程负责把它们发到 Kafka Broker 上。



分区的策略包括顺序轮询、随机轮询和 key hash 这 3 种方式,那什么是分区呢?


分区是 Kafka 读写数据的最小粒度,比如主题 A 有 15 条消息,有 5 个分区,如果采用顺序轮询的方式,15 条消息会顺序分配给这 5 个分区,后续消费的时候,也是按照分区粒度消费。



由于分区可以部署在多个不同的机器上,所以可以通过分区实现 Kafka 的伸缩性,比如主题 A 的 5 个分区,分别部署在 5 台机器上,如果下线一台,分区就变为 4。


Kafka 消费是通过消费群组完成,同一个消费者群组,一个消费者可以消费多个分区,但是一个分区,只能被一个消费者消费。



如果消费者增加,会触发 Rebalance,也就是分区和消费者需要重新配对


不同的消费群组互不干涉,比如下图的 2 个消费群组,可以分别消费这 4 个分区的消息,互不影响。



2.2 RocketMQ


RocketMQ 是阿里开源的消息中间件,它是纯 Java 开发,具有高性能、高可靠、高实时、适合大规模分布式系统应用的特点。


RocketMQ 思路起源于 Kafka,但并不是 Kafka 的一个 Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog 分发等场景。


重要概念



  • Name 服务器(NameServer):充当注册中心,类似 Kafka 中的 Zookeeper。

  • Broker: 一个独立的 RocketMQ 服务器就被称为 broker,broker 接收来自生产者的消息,为消息设置偏移量。

  • 主题(Topic):消息的第一级类型,一条消息必须有一个 Topic。

  • 子主题(Tag):消息的第二级类型,同一业务模块不同目的的消息就可以用相同 Topic 和不同的 Tag 来标识。

  • 分组(Gr0up):一个组可以订阅多个 Topic,包括生产者组(Producer Gr0up)和消费者组(Consumer Gr0up)。

  • 队列(Queue):可以类比 Kafka 的分区 Partition。


RocketMQ 工作原理


RockerMQ 中的消息模型就是按照主题模型所实现的,包括 Producer Gr0up、Topic、Consumer Gr0up 三个角色。


为了提高并发能力,一个 Topic 包含多个 Queue,生产者组根据主题将消息放入对应的 Topic,下图是采用轮询的方式找到里面的 Queue。


RockerMQ 中的消费群组和 Queue,可以类比 Kafka 中的消费群组和 Partition:不同的消费者组互不干扰,一个 Queue 只能被一个消费者消费,一个消费者可以消费多个 Queue。


消费 Queue 的过程中,通过偏移量记录消费的位置。



RocketMQ 架构


RocketMQ 技术架构中有四大角色 NameServer、Broker、Producer 和 Consumer,下面主要介绍 Broker。


Broker 用于存放 Queue,一个 Broker 可以配置多个 Topic,一个 Topic 中存在多个 Queue。


如果某个 Topic 消息量很大,应该给它多配置几个 Queue,并且尽量多分布在不同 broker 上,以减轻某个 broker 的压力。Topic 消息量都比较均匀的情况下,如果某个 broker 上的队列越多,则该 broker 压力越大。



简单提一下,Broker 通过集群部署,并且提供了 master/slave 的结构,salve 定时从 master 同步数据(同步刷盘或者异步刷盘),如果 master 宕机,则 slave 提供消费服务,但是不能写入消息。


看到这里,大家应该可以发现,RocketMQ 的设计和 Kafka 真的很像!


2.3 RabbitMQ


RabbitMQ 2007 年发布,是使用 Erlang 语言开发的开源消息队列系统,基于 AMQP 协议来实现。


AMQP 的主要特征是面向消息、队列、路由、可靠性、安全。AMQP 协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。


重要概念



  • 信道(Channel):消息读写等操作在信道中进行,客户端可以建立多个信道,每个信道代表一个会话任务。

  • 交换器(Exchange):接收消息,按照路由规则将消息路由到一个或者多个队列;如果路由不到,或者返回给生产者,或者直接丢弃。

  • 路由键(RoutingKey):生产者将消息发送给交换器的时候,会发送一个 RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。

  • 绑定(Binding):交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个 RoutingKey。


RabbitMQ 工作原理


AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下:



  1. 生产者是连接到 Server,建立一个连接,开启一个信道。

  2. 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。

  3. 消费者也需要进行建立连接,开启信道等操作,便于接收消息。

  4. 生产者发送消息,发送到服务端中的虚拟主机。

  5. 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。

  6. 订阅了消息队列的消费者就可以获取到消息,进行消费。



常用交换器


RabbitMQ 常用的交换器类型有 direct、topic、fanout、headers 四种,具体的使用方法,可以参考官网:


官网入口:https://www.rabbitmq.com/getstarted.html


03 消息队列对比



3.1 Kafka


优点:



  • 高吞吐、低延迟:Kafka 最大的特点就是收发消息非常快,Kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒;

  • 高伸缩性:每个主题(topic)包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中;

  • 高稳定性:Kafka 是分布式的,一个数据多个副本,某个节点宕机,Kafka 集群能够正常工作;

  • 持久性、可靠性、可回溯: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,支持消息回溯;

  • 消息有序:通过控制能够保证所有消息被消费且仅被消费一次;

  • 有优秀的第三方 Kafka Web 管理界面 Kafka-Manager,在日志领域比较成熟,被多家公司和多个开源项目使用。


缺点:



  • Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,load 越高,发送消息响应时间变长;

  • 不支持消息路由,不支持延迟发送,不支持消息重试;

  • 社区更新较慢。


3.2 RocketMQ


优点:



  • 高吞吐:借鉴 Kafka 的设计,单一队列百万消息的堆积能力;

  • 高伸缩性:灵活的分布式横向扩展部署架构,整体架构其实和 kafka 很像;

  • 高容错性:通过ACK机制,保证消息一定能正常消费;

  • 持久化、可回溯:消息可以持久化到磁盘中,支持消息回溯;

  • 消息有序:在一个队列中可靠的先进先出(FIFO)和严格的顺序传递;

  • 支持发布/订阅和点对点消息模型,支持拉、推两种消息模式;

  • 提供 docker 镜像用于隔离测试和云集群部署,提供配置、指标和监控等功能丰富的 Dashboard。


缺点:



  • 不支持消息路由,支持的客户端语言不多,目前是 java 及 c++,其中 c++ 不成熟

  • 部分支持消息有序:需要将同一类的消息 hash 到同一个队列 Queue 中,才能支持消息的顺序,如果同一类消息散落到不同的 Queue中,就不能支持消息的顺序。

  • 社区活跃度一般。


3.3 RabbitMQ


优点:



  • 支持几乎所有最受欢迎的编程语言:Java,C,C ++,C#,Ruby,Perl,Python,PHP等等;

  • 支持消息路由:RabbitMQ 可以通过不同的交换器支持不同种类的消息路由;

  • 消息时序:通过延时队列,可以指定消息的延时时间,过期时间TTL等;

  • 支持容错处理:通过交付重试和死信交换器(DLX)来处理消息处理故障;

  • 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker;

  • 社区活跃度高。


缺点:



  • Erlang 开发,很难去看懂源码,不利于做二次开发和维护,基本职能依赖于开源社区的快速维护和修复 bug;

  • RabbitMQ 吞吐量会低一些,这是因为他做的实现机制比较重;

  • 不支持消息有序、持久化不好、不支持消息回溯、伸缩性一般。


04 消息队列选型


Kafka:追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务,大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka。


RocketMQ:天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。


RabbitMQ:结合 erlang 语言本身的并发优势,性能较好,社区活跃度也比较高,但是不利于做二次开发和维护,不过 RabbitMQ 的社区十分活跃,可以解决开发过程中遇到的 bug。如果你的数据量没有那么大,小公司优先选择功能比较完备的 RabbitMQ。


ActiveMQ:官方社区现在对 ActiveMQ 5.x 维护越来越少,较少在大规模吞吐的场景中使用。


今天就聊到这里,我们下一篇见~~




最后,把楼仔的座右铭送给你:我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。


原创好文:


作者:楼仔
来源:juejin.cn/post/7306322677039235108
收起阅读 »

Java 实现电梯逻辑

一、实现结果说明 这里首先说明实现结果: 1、已实现: 实现电梯的移动逻辑。 实现了电梯外部的每个楼层的上下按钮。 实现了电梯运行的同时添加新楼层。 2、未实现: 没有实现电梯内部的按钮。 没有实现多个电梯协同运行。 没有实现电梯开关门时的逻辑。 二、...
继续阅读 »

一、实现结果说明


这里首先说明实现结果:


1、已实现:



  • 实现电梯的移动逻辑。

  • 实现了电梯外部的每个楼层的上下按钮。

  • 实现了电梯运行的同时添加新楼层。


2、未实现:



  • 没有实现电梯内部的按钮。

  • 没有实现多个电梯协同运行。

  • 没有实现电梯开关门时的逻辑。


二、电梯运行的情况



  • 当电梯向上移动时,会一直运行至发出请求的所有楼层中最高的楼层。

  • 向下移动时,会一直运行至发生请求的所有楼层中最低的楼层。

  • 在电梯运行过程中,如果有用户点击了某一层的按钮,会根据该层的按钮与当前电梯所在的层数和电梯要去的层数相比较,以及判断电梯的运行方向,来确定下一步去往的楼层。


三、实现说明


该代码实现使用 Java 编写,使用多线程来分析处理电梯的移动,以及各个楼层的按钮点击处理。


当然,没有展示的页面,Java 编写可视化页面还是相当吃翔的。采用控制台输出的方式来告诉开发者现在电梯所在的楼层。


实现代码中目前一共包含七个类(多数属于非严格的单例对象):



  • Lift.java:负责电梯的移动,从任务列表中取得任务,并判断电梯应该运行的方向。

  • LayerRequest.java:这个类是定义的一个数据结构,用来保存每个楼层的请求。负责处理电梯获取或者删除任务的请求,以及各个楼层召唤电梯的请求。

  • LayerList.java:该类保存着每个楼层。是一个继承了 ArrayList 的类。

  • Layer.java:该类表示的是单个楼层,存储着某个楼层的信息。

  • MoveDirection.java:电梯的移动方向,电梯的移动方向有三种:UP、DOWN、STOP。

  • Client.java:客户端处理类,电梯与外界交互就靠这一个类,可以使用该类向电梯发送上升或者下降的请求。同时该类管理着一个线程池。

  • Test.java:测试类。


四、部分代码解析


如果要查看源代码,可以从 CSDN 上下载 ZIP 文件 CSDN —— Java 实现电梯逻辑


同时也提供了 GitHub 项目地址:GitHub —— Java 实现电梯逻辑


1、Lift.java 核心代码


/**
* 向上移动电梯
*/

private void moveUp() {
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber;
while (currentLayerNumber < (targetLayerNumber = this.getTargetLayer().getLayerNumber())) {
this.moving();
Layer layer = this.layerList.get(currentLayerNumber);
this.setCurrentLayer(layer);
currentLayerNumber++;
if (currentLayerNumber != targetLayerNumber) {
this.passLayer(layer);
}
}
this.reachTargetLayer();
}

/**
* 向下移动电梯
*/

private void moveDown() {
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber;
while (currentLayerNumber > (targetLayerNumber = this.getTargetLayer().getLayerNumber())) {
this.moving();
// 这里减二是因为:
// 需要通过索引获取楼层, getLayerNumber() 对索引进行了加一, 需要减一获得索引,
// 而这里是电梯下降, 需要获取下一个楼层的索引, 所以还要再减一
Layer layer = this.layerList.get(currentLayerNumber - 2);
this.setCurrentLayer(layer);
currentLayerNumber--;
if (currentLayerNumber != targetLayerNumber) {
this.passLayer(layer);
}
}
this.reachTargetLayer();
}

/**
* 移动电梯到目标楼层
*/

private void move(int diff) {
if (diff > 0) {
moveDown();
} else {
moveUp();
}
}

/**
* 电梯运行, 主要负责电梯的移动
*/

void run() {
while (this.runnable()) {
try {
this.setUsing(this.layerRequest.hasTask());
if (!this.isUsing()) {
continue;
}
// 电梯有任务才会执行核心函数
this.runCore();
} catch (Exception e) {
e.printStackTrace();
}
}
}

/**
* 电梯是否可运行
*
* @return 可运行返回 true
*/

private boolean runnable() {
return !isFault();
}

/**
* 电梯运行核心 (我是这样起名的, 它配不配这个名字我就不知道了)<br/>
* 此时电梯一定处于 stop 状态
*/

private void runCore() {
Layer layer;
LayerRequest layerRequest = this.layerRequest;
int diff;
int currentLayerNumber = this.getCurrentLayer().getLayerNumber();
int targetLayerNumber = this.getTargetLayer().getLayerNumber();

// 根据 当前楼层 与 目标楼层 的相对位置来设置电梯移动方向
if ((diff = currentLayerNumber - targetLayerNumber) < 0) {
layer = layerRequest.getLayer();
if (layer != null) {
this.setCurrentMoveDirection(MoveDirection.UP);
} else {
this.setCurrentMoveDirection(MoveDirection.DOWN);
}
} else if ((diff = currentLayerNumber - targetLayerNumber) > 0) {
layer = layerRequest.getLayer();
if (layer != null) {
this.setCurrentMoveDirection(MoveDirection.DOWN);
} else {
this.setCurrentMoveDirection(MoveDirection.UP);
}
} else {
return;
}

if (this.checkLayer(layer)) {
this.setTargetLayer(layer);
this.move(diff);
}
}

/**
* 检查楼层所属的区间, 下面是 layer 楼层所在的不同区间的所有的返回结果: <br/>
* 一. [ (layer: -1) 低楼层 -- (layer: 0) --> 高楼层 (layer: 1) ] <br/>
* 二. [ (layer: -1) 高楼层 -- (layer: 0) --> 低楼层 (layer: 1) ] <br/>
* 三. 电梯处于 stop 状态时若电梯处于 stop 状态, 返回 layer 与 currentLayer 的楼层差值
*
* @param layer 要检查的楼层
* @return 返回数字, 表示 layer 楼层所属的区间
*/

int checkLayerInRange(Layer layer) {
Layer currentLayer = this.getCurrentLayer();
Layer targetLayer = this.getTargetLayer();
int currentLayerNumber = currentLayer.getLayerNumber();
int targetLayerNumber = targetLayer.getLayerNumber();

int layerNumber = layer.getLayerNumber();

// 上升时, 返回值取决于楼层 layer 所在的区间: [ (layer: -1) 低楼层 -- (layer: 0) --> 高楼层 (layer: 1) ]
if (isMoveUp()) {
if (layerNumber < currentLayerNumber) {
return -1;
} else if (targetLayerNumber < layerNumber) {
return 1;
} else {
return 0;
}
}
// 下降时, 返回值取决于 layer 所在的区间: [ (layer: -1) 高楼层 -- (layer: 0) --> 低楼层 (layer: 1) ]
else if (isMoveDown()) {
if (layerNumber < targetLayerNumber) {
return 1;
} else if (layerNumber > currentLayerNumber) {
return -1;
} else {
return 0;
}
}
// 若电梯处于 stop 状态, 返回 layerNumber 与 currentLayerNumber 的差值
else {
return layerNumber - currentLayerNumber;
}
}

2、LiftRequest.java 核心代码


void removeUpLayer() {
this.removeLayer(this.nextUpList, this.nextDownList, MoveDirection.UP);
}

void removeDownLayer() {
this.removeLayer(this.nextDownList, this.nextUpList, MoveDirection.DOWN);
}

/**
* 电梯到达目标楼层时移除楼层, 从 usingList 中移除 <br/>
* 当 usingList 中没有楼层时, 则设置 freeList 的第一个元素为 {@link Lift#targetLayer}, freeList 将成为 usingList<br/>
*
* @param nextUsingList 下一执行阶段要执行的任务
* @param nextFreeList 下一执行阶段要执行的任务
* @param moveDirection 当前电梯的运行状态
*/

private void removeLayer(List<Layer> nextUsingList, List<Layer> nextFreeList,
MoveDirection moveDirection)
{
Lift lift = this.lift;
List<Layer> taskList = this.taskList;

// 当前任务执行完成, 将其移除
removeFirst();

// 移除后如果任务列表不为空, 就将列表第一个楼层设为目标楼层
if (!taskList.isEmpty()) {
lift.setTargetLayer(getFirst());
return;
}

// 这段代码在下面的情况下生效 (电梯发生转向时):
// 例如: 电梯从第一层移动到第七层, 在电梯到达第五层时, 此时在第三层按下向下的按钮, 将会添加到 nextFreeList 集合中
if (!nextFreeList.isEmpty()) {
taskList.addAll(nextFreeList);
// 根据不同的移动状态排序
if (MoveDirection.isMoveUp(moveDirection)) {
this.reserveSort();
} else if (MoveDirection.isMoveDown(moveDirection)) {
this.sort();
}
lift.setTargetLayer(getFirst());
nextFreeList.clear();
}

// 如果电梯反向运行列表没有元素 (nextFreeList 为空, empty), 就执行同向的任务列表
// 例如: 电梯要从第一层移动到第七层, 并且电梯已经移动到第四层, 此时点击第一层的上升按钮和第三层的上升按钮,
// 将会添加到 nextUsingList 集合中
// 电梯移动过程: (1): 1 --- 上升 ---> 7 (2): 7 --- 下降 ---> 1 (3): 1 --- 上升 ---> 3
if (taskList.isEmpty() && !nextUsingList.isEmpty()) {
taskList.addAll(nextUsingList);
if (MoveDirection.isMoveUp(moveDirection)) {
this.sort();
} else if (MoveDirection.isMoveDown(moveDirection)) {
this.reserveSort();
}
lift.setTargetLayer(getFirst());
nextUsingList.clear();
}
}

/**
* 添加楼层
* @param layer 要添加的楼层
* @param moveDirection 要去往的方向
*/

void addLayer(Layer layer, MoveDirection moveDirection) {
if (!this.taskList.contains(layer)) {
Lift lift = this.lift;
if (lift.getCurrentLayer().equals(layer)) {
this.alreadyLocated(layer);
return;
}
lift.setTargetLayerIfNull(layer);
int result = lift.checkLayerInRange(layer);
// 如果电梯处于停止状态
if (lift.isMoveStop()) {
if (result > 0) {
this.addUpLayerWithSort(layer);
lift.setCurrentMoveDirection(MoveDirection.UP);
} else if (result < 0) {
this.addDownLayerWithSort(layer);
lift.setCurrentMoveDirection(MoveDirection.DOWN);
}
lift.setTargetLayer(layer);
return;
}
// 根据按钮点击的是上升还是下降来调用
if (MoveDirection.isMoveUp(moveDirection)) {
this.addUpLayer(result, layer);
} else {
this.addDownLayer(result, layer);
}
}
}

/**
* 添加要上楼的楼层
*
* @param result result
* @param layer 要添加的楼层
*/

private void addUpLayer(int result, Layer layer) {
Lift lift = this.lift;
if (lift.isMoveUp()) {
if (result == 0) {
lift.setTargetLayer(layer);
this.addUpLayerWithSort(layer);
} else if (result == 1) {
this.addUpLayerWithSort(layer);
} else if (result == -1) {
this.addLayerIfNotExist(this.nextUpList, layer);
}
} else if (lift.isMoveDown()) {
this.addLayerIfNotExist(this.nextUpList, layer);
}
}

/**
* 添加要下楼的楼层
*
* @param layer 要添加的楼层
*/

void addDownLayer(int result, Layer layer) {
Lift lift = this.lift;
if (lift.isMoveDown()) {
if (result == 0) {
lift.setTargetLayer(layer);
this.addDownLayerWithSort(layer);
} else if (result == 1) {
this.addDownLayerWithSort(layer);
} else if (result == -1) {
this.addLayerIfNotExist(this.nextDownList, layer);
}
} else if (lift.isMoveUp()) {
this.addLayerIfNotExist(this.nextDownList, layer);
}
}

五、有话说


有兴趣的小伙伴可以自己写一个类似的程序,或者在此基础上做修改、加上新的处理逻辑,代码如有瑕疵,敬请见谅!


作者:情欲
来源:juejin.cn/post/7305984583983398950
收起阅读 »

图片自动压缩

在进行包大小优化工作时,压缩图片的大小是其中一个重要的环节。而要压缩的图片包括本地项目中的图片和之后要新增到项目中的图片。所以压缩图片分为两个部分: 遍历项目中的所有图片,压缩后替换原图片 每次git提交代码前,如果有新增图片,进行压缩后再提交 压缩本地项...
继续阅读 »

在进行包大小优化工作时,压缩图片的大小是其中一个重要的环节。而要压缩的图片包括本地项目中的图片和之后要新增到项目中的图片。所以压缩图片分为两个部分:



  1. 遍历项目中的所有图片,压缩后替换原图片

  2. 每次git提交代码前,如果有新增图片,进行压缩后再提交


压缩本地项目中的图片


require "fileutils"
require "find"
require "tinify"

t = Time.now
$image_count = 0
$total_size = 0
$total_after_size = 0
$fail_count = 0
$success_count = 0
$success_file_name = "successLog.txt"
$fail_file_name = "failLog.txt"
compress_dir = "/Users/zhouweijie1/Documents/test/Expression.xcassets" #将要压缩的文件夹路径放这
# 获取白名单列表路径
$white_list_path = "#{Dir.pwd}/gitHooks/imageCompressWhiteList.txt"

$keys = ['tbfVHxRmxxR3Vb3XQwrxMbfHPNnxszpH', 'B83mGyQcbpmFzz1Qym5ZdhT3Ss503b5b', 'L1DfbF8kpRzstlMfbvmkvCSg6knkQD71', '2L6km1p5yJRZsNYs0GJ6m4klL1rMJ4RJ', '5wmc8dDxY1WKg4DTPSLXQ20dWWjRbzyG', '1DkYWCXDvPJfMrNbV6NPB0QpQTGzZLfD', 'bRG9yXbc07w77sP43gqjgP8tlgDPjdVJ', 'xwvXrTp2pSJYWDjkHQ7wTBTxDMbLdx4r', '4pFYmxVBK6vnpKR5hh8r0hD4BGmS75K4', '6rSpQHxHpygLyZMQnTH6WNjxGVV9mt0x']
$keys_index = -1

def setup_key
$keys_index += 1
Tinify.key = $keys[$keys_index]
Tinify.validate! # validate后会更新compression_count
if $keys_index == $keys.length
puts "本月所有免费使用次数都用完,请增加key"
elsif Tinify.compression_count >= 500
setup_key
end
end

def write_log(fail, success)
if success != 0
file = File.new($success_file_name, "a")
file.syswrite("#{success}\n")
end
if fail != 0
file = File.new($fail_file_name, "a")
file.syswrite("#{fail}\n")
end
end

def compress(image_name)
begin
# Use the Tinify API client.
origin_size = File.size(image_name)
Tinify.from_file(image_name).to_file(image_name)
log = image_name + "\n#{origin_size} bit" + " -> " + "#{File.size(image_name)} bit"
puts log + ":#{Time.now}"
write_log(0, log)
$success_count += 1
rescue Tinify::AccountError
# Verify your API key and account limit.
setup_key
print("失效的key:" + Tinify.key + "\n")
compress(image_name)
rescue Tinify::ClientError => e
# Check your source image and request options.
log = image_name + "\nClientError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue Tinify::ServerError => e
# Temporary issue with the Tinify API.
log = image_name + "\nServerError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue Tinify::ConnectionError => e
# A network connection error occurred.
log = image_name + "\nConnectionError:#{e.message}"
puts log + ":#{Date.now}"
write_log(log, 0)
$fail_count += 1
rescue => e
# Something else went wrong, unrelated to the Tinify API.
log = image_name + "\nOtherError:#{e.message}"
puts log + ":#{Time.now}"
write_log(log, 0)
$fail_count += 1
end
end
# 检测到文件夹中所有PNG和JPEG图片并压缩
def traverse_dir(file_path)
setup_key
Dir.glob(%W[#{file_path}/**/*.png #{file_path}/**/*.jpeg]).each do |image_name|
$total_size += File.size(image_name)
# compress(image_name)
$total_after_size += File.size(image_name)
$image_count += 1
end
end

traverse_dir(compress_dir)
time = "时间:#{Time.now - t}s from #{t} to #{Time.now}"
count = "图片总数:#{$image_count},本次压缩图片数:#{$image_count}, 成功图片数:#{$success_count},失败图片数:#{$fail_count}"
size = "之前总大小:#{$total_size/1024.0} k,之后总大小:#{$total_after_size/1024.0} k,优化大小:#{($total_size - $total_after_size)/1024.0}"
puts time
puts count
puts size
write_log(0, time)
write_log(0, count)
write_log(0, size)
complete = "压缩完毕!!!"
if $fail_count != 0
complete += "有#{$fail_count}张图片失败,请查看:#{File.absolute_path($fail_file_name)}"
end
puts complete

# 检查key的免费使用次数
def check_keys_status
$keys.each do |key|
begin
Tinify.key = key
Tinify.validate!
puts "#{key}:#{Tinify.compression_count}"
rescue
end
end
end
# 白名单
def ignore?
file = File.new($white_list_path, "a+")
file.readlines.each { |line|
line_without_white_space = line.strip
if line_without_white_space.length > 0
result = $image_path.match?(line_without_white_space)
if result
return true
end
end
}
return false
end

压缩即将提交的图片


要压缩即将提交的图片,就要使用git hook拦截代码提交动作,将pre-commit文件放到.git/hooks文件中就行了。pre-commit文件中的代码逻辑为获取当前提交的内容,遍历是否是图片,是的话就执行压缩脚本:


#!/bin/sh

#
检测是否为最初提交
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

#
If you want to allow non-ASCII filenames set this variable to true.
git config hooks.allownonascii true

#
Redirect output to stderr.
exec 1>&2

#
获取.git所在目录
git_path=$(cd "$(dirname "$0")";cd ..;cd ..; pwd)
#获取当前分支名
branch=$(git symbolic-ref --short HEAD)

#
得到修改过的代码的文件列表
git diff --cached --name-only --diff-filter=ACMR -z $against | while read -d $'\0' f; do
if [[ $f == *".png" || $f == *".jpg" || $f == *".jpeg" ]];then
#拼接文件绝对路径
path="$(cd "$(dirname "$0")";cd ..;cd ..; pwd)/$f"
pattern='/Pods/'
pathStr="$path"
if [[ ! ($pathStr =~ $pattern) ]]; then
#执行压缩脚本
ruby "$git_path/gitHooks/imageCompressor.rb" $path $branch
git add $f
fi
fi

done

压缩脚本单独放在一个文件中,内容如下:


require "tinify"

$keys = %w[tbfVHxRmxxR3Vb3XQwrxMbfHPNnxszpH B83mGyQcbpmFzz1Qym5ZdhT3Ss503b5b L1DfbF8kpRzstlMfbvmkvCSg6knkQD71 2L6km1p5yJRZsNYs0GJ6m4klL1rMJ4RJ 5wmc8dDxY1WKg4DTPSLXQ20dWWjRbzyG 1DkYWCXDvPJfMrNbV6NPB0QpQTGzZLfD bRG9yXbc07w77sP43gqjgP8tlgDPjdVJ xwvXrTp2pSJYWDjkHQ7wTBTxDMbLdx4r 4pFYmxVBK6vnpKR5hh8r0hD4BGmS75K4 6rSpQHxHpygLyZMQnTH6WNjxGVV9mt0x]
$keys_index = -1
$image_path = ARGV[0]
$branch_name = ARGV[1]
# 获取.git所在目录
git_path = `git rev-parse --git-dir`; git_path = git_path.strip;
# 获取当前文件所在目录
cur_path = `printf $(cd '#{git_path}'; cd ..; pwd)/gitHooks`; cur_path = cur_path.strip;
$white_list_path = "#{cur_path}/imageCompressWhiteList.txt"
def setup_key
$keys_index += 1
Tinify.key = $keys[$keys_index]
Tinify.validate! # validate后会更新compression_count
if $keys_index == $keys.length
puts "本月所有免费使用次数都用完,请增加key"
elsif Tinify.compression_count >= 500
setup_key
end
end

def ignore?
file = File.new($white_list_path, "a+")
file.readlines.each { |line|
line_without_white_space = line.strip
if line_without_white_space.length > 0
result = $image_path.match?(line_without_white_space)
if result
return true
end
end
}
return false
end

begin
# Use the Tinify API client.
result = ignore?
if result
puts "图片在白名单中,不压缩:" + $image_path
else
setup_key
Tinify.from_file($image_path).to_file($image_path)
puts "图片压缩成功:" + $image_path
end
rescue Tinify::AccountError
# Verify your API key and account limit.
setup_key
rescue Tinify::ClientError => e
# Check your source image and request options.
puts "图片压缩失败:" + $image_path + ", ClientError:#{e.message}"
rescue Tinify::ServerError => e
# Temporary issue with the Tinify API.
puts "图片压缩失败:" + $image_path + ", ServerError:#{e.message}"
rescue Tinify::ConnectionError => e
# A network connection error occurred.
puts "图片压缩失败:" + $image_path + ", ConnectionError:#{e.message}"
rescue => e
# Something else went wrong, unrelated to the Tinify API.
puts "图片压缩失败:" + $image_path + ", OtherError:#{e.message}"
end


如果某张图片不需要或者不能压缩,需要将图片名放到白名单中,白名单格式如下:


test_expression_100fen@3x.png
test_expression_666@3x.png
expression_100fen@3x.png

上面提到将pre-commit文件放到.git/hooks文件中就可以实现提交拦截,也可以用脚本完成这个操作:
文件名:setupGitHook.rb


#!/usr/bin/ruby
require "Fileutils"

# 获取.git所在目录
git_path = `git rev-parse --git-dir`; git_path = git_path.strip;
# 获取当前文件所在目录
cur_path = `printf $(cd '#{git_path}'; cd ..; pwd)/gitHooks`; cur_path = cur_path.strip;
puts "gitPath:#{git_path}"
puts "cur_path:#{cur_path}"
# .git目录下没有hooks文件夹时新建一个
if Dir.exist?("#{git_path}/hooks") == false
FileUtils.mkpath("#{git_path}/hooks")
end
# 将当前文件夹中pre-commit文件拷贝到.git/hooks目录下
FileUtils.cp("#{cur_path}/pre-commit", "#{git_path}/hooks/pre-commit")

当同事很多时,比如有四十多个,让每个人都在项目目录下执行一遍setupGitHook.rb,每个同事都来问一遍就比较麻烦了。所以可以添加一个运行脚本,运行项目时自动执行就可以了:


# Type a script or drag a script file from your workspace to insert its path.

#
获取gitHooks文件夹位置

gitHooks_path=$(**cd** "$(git rev-parse --git-dir)"; **cd** ..; **pwd**;)/gitHooks

ruby $gitHooks_path/setupGitHook.rb

如下图:


image.png



Demo地址:github.com/Wejua/Demos…


作者:和时间赛跑ing
来源:juejin.cn/post/7287246372054876216
收起阅读 »

还在手动造轮子?试试这款可以轻松集成多种支付渠道的工具!

大家好,我是 Java陈序员。 随着电商的兴起,各种支付也是蓬勃发展。 微信支付、支付宝支付、银联支付等各种支付方式可是深入到日常生活中。可以说,扫码支付给我们的生活带来了极大的便利。 同时,随着市场需求的变化,这也要求我们在企业开发中,需要集成第三方支付渠道...
继续阅读 »

大家好,我是 Java陈序员


随着电商的兴起,各种支付也是蓬勃发展。


微信支付、支付宝支付、银联支付等各种支付方式可是深入到日常生活中。可以说,扫码支付给我们的生活带来了极大的便利。


同时,随着市场需求的变化,这也要求我们在企业开发中,需要集成第三方支付渠道!


我们在集成第三方支付渠道时,常规的操作是查阅官方文档、封装代码、测试联调等。


今天,给大家介绍一个已经封装好各种支付渠道的项目,开箱即用,我们就不用重复手动造轮子了!


项目介绍


IJPay 的宗旨是让支付触手可及。封装了微信支付、QQ 支付、支付宝支付、京东支付、银联支付、PayPal 支付等常用的支付方式以及各种常用的接口。


不依赖任何第三方 MVC 框架,仅仅作为工具使用简单快速完成支付模块的开发,开箱即用,可快速集成到系统中。


功能模块:



  • 微信支付: 支持多商户多应用,普通商户模式与服务商商模式当然也支持境外商户、同时支持 Api-v3Api-v2 版本的接口

  • 个人微信支付: 微信个人商户,最低费率 0.38%,官方直连的异步回调通知

  • 支付宝支付: 支持多商户多应用,签名同时支持普通公钥方式与公钥证书方式

  • 银联支付: 全渠道扫码支付、微信 App 支付、公众号&小程序支付、银联 JS 支付、支付宝服务窗支付

  • PayPal 支付: 自动管理 AccessToken,极速接入各种常用的支付方式


项目安装


一次性添加所有支付方式的依赖


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-All</artifactId>
<version>latest-version</version>
</dependency>

或者选择某一个/多个支付方式的依赖,如:
支付宝支付


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-AliPay</artifactId>
<version>latest-version</version>
</dependency>

微信支付


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>latest-version</version>
</dependency>

更多支付方式依赖参考:


https://javen205.gitee.io/ijpay/guide/maven.html#maven

集成Demo


以支付宝支付为例。


引入依赖


<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-AliPay</artifactId>
<version>latest-version</version>
</dependency>

初始化客户端配置信息


AliPayApiConfig aliPayApiConfig = AliPayApiConfig.builder() 
.setAppId(aliPayBean.getAppId())
.setAppCertPath(aliPayBean.getAppCertPath())
.setAliPayCertPath(aliPayBean.getAliPayCertPath())
.setAliPayRootCertPath(aliPayBean.getAliPayRootCertPath())
.setCharset("UTF-8")
.setPrivateKey(aliPayBean.getPrivateKey())
.setAliPayPublicKey(aliPayBean.getPublicKey())
.setServiceUrl(aliPayBean.getServerUrl())
.setSignType("RSA2")
// 普通公钥方式
//.build();
// 证书模式
.buildByCert();
// 或者
.setAppId(aliPayBean.getAppId())
.setAliPayPublicKey(aliPayBean.getPublicKey())
.setCharset("UTF-8")
.setPrivateKey(aliPayBean.getPrivateKey())
.setServiceUrl(aliPayBean.getServerUrl())
.setSignType("RSA2")
.build(); // 普通公钥方式
.build(appCertPath, aliPayCertPath, aliPayRootCertPath) // 2.3.0 公钥证书方式

AliPayApiConfigKit.setThreadLocalAppId(aliPayBean.getAppId()); // 2.1.2 之后的版本,可以不用单独设置
AliPayApiConfigKit.setThreadLocalAliPayApiConfig(aliPayApiConfig);


参数说明:



  • appId: 应用编号

  • privateKey: 应用私钥

  • publicKey: 支付宝公钥,通过应用公钥上传到支付宝开放平台换取支付宝公钥(如果是证书模式,公钥与私钥在CSR目录)。

  • appCertPath: 应用公钥证书 (证书模式必须)

  • aliPayCertPath: 支付宝公钥证书 (证书模式必须)

  • aliPayRootCertPath: 支付宝根证书 (证书模式必须)

  • serverUrl: 支付宝支付网关

  • domain: 外网访问项目的域名,支付通知中会使用



多应用无缝切换:


从上面的初始化配置中,可以看到 IJPay 默认是使用当前线程中的 appId 对应的配置。


如果要切换应用可以调用 AliPayApiConfigKit.setThreadLocalAppId 来设置当前线程中的 appId, 实现应用的切换进而达到多应用的支持。


调用支付API


App 支付


public AjaxResult appPay() {
try {
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setBody("测试数据-Java陈序员");
model.setSubject("Java陈序员 App 支付测试");
model.setOutTradeNo(StringUtils.getOutTradeNo());
model.setTimeoutExpress("15m");
model.setTotalAmount("0.01");
model.setPassbackParams("callback params");
model.setProductCode("QUICK_MSECURITY_PAY");
String orderInfo = AliPayApi.appPayToResponse(model, aliPayBean.getDomain() + NOTIFY_URL).getBody();
result.success(orderInfo);
} catch (AlipayApiException e) {
e.printStackTrace();
result.addError("system error:" + e.getMessage());
}
return result;
}

PC 支付


public void pcPay(HttpServletResponse response) {
try {
String totalAmount = "0.01";
String outTradeNo = StringUtils.getOutTradeNo();
log.info("pc outTradeNo>" + outTradeNo);

String returnUrl = aliPayBean.getDomain() + RETURN_URL;
String notifyUrl = aliPayBean.getDomain() + NOTIFY_URL;
AlipayTradePagePayModel model = new AlipayTradePagePayModel();

model.setOutTradeNo(outTradeNo);
model.setProductCode("FAST_INSTANT_TRADE_PAY");
model.setTotalAmount(totalAmount);
model.setSubject("Java陈序员 PC 支付测试");
model.setBody("Java陈序员 PC 支付测试");
model.setPassbackParams("passback_params");

AliPayApi.tradePage(response, model, notifyUrl, returnUrl);
} catch (Exception e) {
e.printStackTrace();
}

}

手机网站支付


public void wapPay(HttpServletResponse response) {
String body = "测试数据-Java陈序员";
String subject = "Java陈序员 Wap支付测试";
String totalAmount = "0.01";
String passBackParams = "1";
String returnUrl = aliPayBean.getDomain() + RETURN_URL;
String notifyUrl = aliPayBean.getDomain() + NOTIFY_URL;

AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
model.setBody(body);
model.setSubject(subject);
model.setTotalAmount(totalAmount);
model.setPassbackParams(passBackParams);
String outTradeNo = StringUtils.getOutTradeNo();
System.out.println("wap outTradeNo>" + outTradeNo);
model.setOutTradeNo(outTradeNo);
model.setProductCode("QUICK_WAP_PAY");

try {
AliPayApi.wapPay(response, model, returnUrl, notifyUrl);
} catch (Exception e) {
e.printStackTrace();
}
}

扫码支付


public String tradePreCreatePay() {
String subject = "Java陈序员 支付宝扫码支付测试";
String totalAmount = "0.01";
String storeId = "123";
String notifyUrl = aliPayBean.getDomain() + "/aliPay/cert_notify_url";

AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
model.setSubject(subject);
model.setTotalAmount(totalAmount);
model.setStoreId(storeId);
model.setTimeoutExpress("15m");
model.setOutTradeNo(StringUtils.getOutTradeNo());
try {
String resultStr = AliPayApi.tradePrecreatePayToResponse(model, notifyUrl).getBody();
JSONObject jsonObject = JSONObject.parseObject(resultStr);
return jsonObject.getJSONObject("alipay_trade_precreate_response").getString("qr_code");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

单笔转账到支付宝账户


public String transfer() {
String totalAmount = "0.01";
AlipayFundTransToaccountTransferModel model = new AlipayFundTransToaccountTransferModel();
model.setOutBizNo(StringUtils.getOutTradeNo());
model.setPayeeType("ALIPAY_LOGONID");
model.setPayeeAccount("gxthqd7606@sandbox.com");
model.setAmount(totalAmount);
model.setPayerShowName("测试退款");
model.setPayerRealName("沙箱环境");
model.setRemark("Java陈序员 测试单笔转账到支付宝");

try {
return AliPayApi.transferToResponse(model).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

其他支付方式集成可参考:


https://github.com/Javen205/IJPay/tree/dev/IJPay-Demo-SpringBoot

总结


可以说,目前 IJPay 集成了大部分主流的支付渠道。可以全部集成到项目中,也可以按需加载某一种、某几种支付渠道。


最后,贴上项目地址:


https://github.com/Javen205/IJPay

在线文档地址:


https://javen205.gitee.io/ijpay/

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7304558952180056100
收起阅读 »

3分钟使用 WebSocket 搭建属于自己的聊天室(WebSocket 原理、应用解析)

WebSocket 的由来 在 WebSocket 出现之前,我们想实现实时通信、变更推送、服务端消息推送功能,我们一般的方案是使用 Ajax 短轮询、长轮询两种方式: 比如我们想实现一个服务端数据变更时,立即通知客户端功能,没有 WebSocket 之前我...
继续阅读 »

WebSocket 的由来



  • 在 WebSocket 出现之前,我们想实现实时通信、变更推送、服务端消息推送功能,我们一般的方案是使用 Ajax 短轮询、长轮询两种方式:

  • 比如我们想实现一个服务端数据变更时,立即通知客户端功能,没有 WebSocket 之前我们可能会采用以下两种方案:短轮询或长轮询


短轮询、长轮询(来源:即时通讯网)



  • 上面两种方案都有比较明显的缺点:


1、HTTP 协议包含的较长的请求头,有效数据只占很少一部分,浪费带宽
2、短轮询频繁轮询对服务器压力较大,即使使用长轮询方案,客户端较多时仍会对客户端造成不小压力


  • 在这种情况下,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。


WebSocket 是什么



  • WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。


短轮询和WebSocket的区别(来源:即时通讯网)


WebSocket 优缺点


优点



  • 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。

  • 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。

  • 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。

  • 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。

  • 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。


缺点



  • 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。

  • 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。

  • 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。


WebSocket 适用场景



  • 实时聊天应用: WebSocket 是实现实时聊天室、即时通讯应用的理想选择,因为它能够提供低延迟和高实时性。

  • 在线协作和协同编辑: 对于需要多用户协同工作的应用,如协同编辑文档或绘图,WebSocket 的实时性使得用户能够看到其他用户的操作。

  • 实时数据展示: 对于需要实时展示数据变化的应用,例如股票行情、实时监控系统等,WebSocket 提供了一种高效的通信方式。

  • 在线游戏: 在线游戏通常需要快速、实时的通信,WebSocket 能够提供低延迟和高并发的通信能力。

  • 推送服务: 用于实现消息推送服务,向客户端主动推送更新或通知。


主流浏览器对 WebSocket 的兼容性


主流浏览器对 WebSocket 的兼容性



  • 由上图可知:目前主流的 Web 浏览器都支持 WebSocket,因此我们可以在大多数项目中放心地使用它。


WebSocket 通信过程以及原理


建立连接



  • WebSocket 协议属于应用层协议,依赖传输层的 TCP 协议。它通过 HTTP/1.1 协议的 101 状态码进行握手建立连接。


具体过程



  • 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。

  • 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。

  • 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应。


示例


// 客户端请求
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7
Sec-WebSocket-Key: b7wpWuB9MCzOeQZg2O/yPg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

// 服务端响应
HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Date: Wed, 22 Nov 2023 08:15:00 GMT
Sec-WebSocket-Accept: Q4TEk+qOgJsKy7gedijA5AuUVIw=
Server: TooTallNate Java-WebSocket
Upgrade: websocket

Sec-WebSocket-Key


  • 与服务端响应头部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接;这里的“配套”指的是:Sec-WebSocket-Accept 是根据请求头部的 Sec-WebSocket-Key 计算而来,计算过程大致为基于 SHA1 算法得到摘要并转成 base64 字符串。


Sec-WebSocket-Extensions


  • 用于协商本次连接要使用的 WebSocket 扩展。


数据通信



  • WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧并将关联的帧重新组装成完整的消息。


数据帧


      0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (
4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (
if payload len==126/127) |
| |
1|2|3| |K| | |
+-+-+-+-+
-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued,
if payload len == 127 |
+ - - - - - - - - - - - - - - - +
-------------------------------+
| |Masking-key,
if MASK set to 1 |
+
-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+
-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+
---------------------------------------------------------------+

帧头(Frame Header)


  • FIN(1比特): 表示这是消息的最后一个帧。如果消息分成多个帧,FIN 位在最后一个帧上设置为 1。

  • RSV1、RSV2、RSV3(各1比特): 保留位,用于将来的扩展。

  • Opcode(4比特): 指定帧的类型,如文本帧、二进制帧、连接关闭等。


WebSocket 定义了几种帧类型,其中最常见的是文本帧(Opcode  0x1)和二进制帧(Opcode  0x2)。其他帧类型包括连接关闭帧、Ping 帧、Pong 帧等。


  • Mask(1比特): 指示是否使用掩码对负载进行掩码操作。

  • Payload Length: 指定数据的长度。如果小于 126 字节,直接表示数据的长度。如果等于 126 字节,后面跟着 16 比特的无符号整数表示数据的长度。如果等于 127 字节,后面跟着 64 比特的无符号整数表示数据的长度。


掩码(Masking)


  • 如果 Mask 位被设置为 1,则帧头后面的 4 字节即为掩码,用于对负载数据进行简单的异或操作,以提高安全性。


负载数据(Payload Data)


  • 实际要传输的数据,可以是文本、二进制数据等


来自 MDN 的一个小例子


Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.

Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, newmessage containing text started)

Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)

Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

维持连接



  • 当建立连接后,连接可能因为网络等原因断开,我们可以使用心跳的方式定时检测连接状态。若连接断开,我们可以告警或者重新建立连接。


关闭连接



  • WebSocket 是全双工通信,当客户端发送关闭请求时,服务端不一定立即响应,而是等服务端也同意关闭时再进行异步响应。

  • 下面是一个客户端关闭的例子:


Client: FIN=1, opcode=0x8, msg="1000"
Server: FIN=1, opcode=0x8, msg="1000"

使用 WebSocket 实现一个简易聊天室



  • 下面是一个简易聊天室小案例,任何人打开下面的网页都可以加入我们聊天室进行聊天,然后小红和小明加入了聊天:


简易聊天室


前端源码


html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Chattitle>
head>
<body>
<div id="chat">div>
<input type="text" id="messageInput" placeholder="Type your message">
<button onclick="sendMessage()">Sendbutton>

<script>
const socket = new WebSocket('ws://localhost:8888');

socket.
onopen = (event) => {
console.log('WebSocket connection opened:', event);
};

socket.
onmessage = (event) => {
const messageDiv = document.getElementById('chat');
const messageParagraph = document.createElement('p');
messageParagraph.
textContent = event.data;
messageDiv.
appendChild(messageParagraph);
};

socket.
onclose = (event) => {
console.log('WebSocket connection closed:', event);
};

function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
socket.
send(message);
messageInput.
value = '';
}
script>
body>
html>

后端源码 Java


package chat;

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;

import java.net.InetSocketAddress;

public class ChatServer extends WebSocketServer {

public ChatServer(int port) {
super(new InetSocketAddress(port));
}

@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
System.out.println("New connection from: " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
}

@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
System.out.println("Closed connection to: " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
}

@Override
public void onMessage(WebSocket conn, String message) {
System.out.println("Received message: " + message);
// Broadcast the message to all connected clients
broadcast(message);
}

@Override
public void onError(WebSocket conn, Exception ex) {
System.err.println("Error on connection: " + ex.getMessage());
}

@Override
public void onStart() {
}

public static void main(String[] args) {
int port = 8888;
ChatServer server = new ChatServer(port);
server.start();
System.out.println("WebSocket Server started on port: " + port);
}
}

总结



  • WebSocket 是一种在客户端和服务器之间建立实时双向通信的协议。具备全双工、低延迟等优点,适用于实时聊天、多人协助、实时数据展示等场景。


参考



个人简介


👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.


🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。


🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。


💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。


🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。


作者:Lorin洛林
来源:juejin.cn/post/7304182487684415514
收起阅读 »

IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket

前言 哈啰,大家好,我是洛林,对Web端即时通讯技术熟悉的开发者来说,回顾整个网页端IM的底层通信技术发展,从短轮询、长轮询,到后来的SSE以及WebSocket,我们使用的技术越来越先进,使用门槛也越来越低,给大家带来的网页端体验也越来越好。我在前面的的《...
继续阅读 »

前言



  • 哈啰,大家好,我是洛林,对Web端即时通讯技术熟悉的开发者来说,回顾整个网页端IM的底层通信技术发展,从短轮询、长轮询,到后来的SSE以及WebSocket,我们使用的技术越来越先进,使用门槛也越来越低,给大家带来的网页端体验也越来越好。我在前面的的《3分钟使用 WebSocket 搭建属于自己的聊天室(WebSocket 原理、应用解析)》一文中介绍了众所熟知的WebSocket的技术,当其它的一些技术并不是没有用武之地,比如就以扫码登录而言,短轮询或长轮询就非常合适,完全没有使用大炮打蚊子的必要。

  • 因此,我们很多时候没有必要盲目追求新技术,而是适合场景的技术才是最好的技术,掌握WebSocket这些主流新技术固然重要,但了解短轮询、长轮询等所谓的“老技术”仍然大有裨益,这就是我分享这篇技术的原因。


即时通讯



  • 对于IM/消息推送这类即时通讯系统而言,系统的关键就是“实时通信”能力。所谓实时通信有以下两层含义:


1、客户端可以主动向服务端发送信息。
2、当服务端内容发生变化时,服务端可以实时通知客户端。

常用技术



  • 客户端轮询:传统意义上的短轮询(Short Polling)

  • 服务器端轮询:长轮询(Long Polling)

  • 单向服务器推送:Server-Sent Events(SSE)

  • 全双工通信:WebSocket


短轮询(Short Polling)


实现原理



  • 客户端向服务器端发送一个请求,服务器返回数据,然后客户端根据服务器端返回的数据进行处理。

  • 客户端继续向服务器端发送请求,继续重复以上的步骤。(为了减小服务端压力一般会采用定时轮询的方式)


短轮询通信过程


优点



  • 实现简单,不需要额外开发,仅需要定时发起请求,解析响应即可。


缺点



  • 不断的发起请求和关闭请求,性能损耗以及对服务端的压力较大,且HTTP请求本身本身比较耗费资源。

  • 轮询间隔不好控制。如果实时性要求较高,短轮询是明显的短板,但如果设置太长,会导致消息延迟。


长轮询(Long Polling)


实现原理



  • 客户端发送一个请求,服务器会hold住这个请求。

  • 直到监听的内容有改变,才会返回数据,断开连接(或者在一定的时间内,请求还得不到返回,就会因为超时自动断开连接);

  • 客户端继续发送请求,重复以上步骤。


长轮询通信过程


改进点



  • 长轮询是基于短轮询上的改进版本:减少了客户端发起Http连接的开销,改成在服务器端主动地去判断关注的内容是否变化。


基于iframe的长轮询



  • 基于iframe的长轮询是长轮询的另一种实现方案。


实现原理



  • 在页面中嵌入一个iframe,地址指向轮询的服务器地址,然后在父页面中放置一个执行函数,比如execute(data);

  • 当服务器有内容改变时,会向iframe发送一个脚本;

  • 通过发送的脚本,主动执行父页面中的方法,达到推送的效果。


总结



  • 基于iframe的长轮询底层还是长轮询技术,只是实现方式不同,而且在浏览器上会显示请求未加载完成,图标会不停旋转,简直是强迫症杀手,个人不是很推荐。


iframe长轮询


Server-Sent Events(SSE)



  • 上面介绍的短轮询和长轮询技术,服务器端是无法主动给客户端推送消息的,都是客户端主动去请求服务器端获取最新的数据。而SSE是一种可以主动从服务端推送消息的技术。

  • SSE的本质其实就是一个HTTP的长连接,只不过它给客户端发送的不是一次性的数据包,而是一个stream流,格式为text/event-stream。所以客户端不会关闭连接,会一直等着服务器发过来的新的数据流。


实现原理



  • 客户端向服务端发起HTTP长连接,服务端返回stream响应流。客户端收到stream响应流并不会关闭连接而是一直等待服务端发送新的数据流。


SSE通信过程


浏览器对 SSE 的支持情况


浏览器对 SSE 的支持情况


SSE vs WebSocket



  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。

  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。

  • SSE 默认支持断线重连,WebSocket 需要自己实现。

  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。

  • SSE 支持自定义发送的消息类型。


总结



  • 对于仅需要服务端向客户端推送数据的场景,我们可以考虑实现更加简单的 SSE 而不是直接使用 WebSocket。


WebSocket



  • WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。


实现原理



  • 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。

  • 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。

  • 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应,客户端和服务端相互进行通信。


WebSocket通信过程


优点



  • 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。

  • 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。

  • 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。

  • 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。

  • 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。


缺点



  • 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。

  • 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。

  • 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。


更多



总结



  • 在本文中我们介绍了IM通信技术中的常用四种技术:短轮询、长轮询、SSE、WebSocket,使用时可以综合我们的实际场景选择合适的通信技术,在复杂的应用场景中,我们可能需要结合不同的技术满足不同的需求,下面是一些常见的考虑因素:


实时性要求



  • 如果实时性要求较低,短轮询或长轮询可能足够;如果需要实时性较高,考虑使用SSE或WebSocket,若仅需要服务端推送,尽可能考虑SSE。


网络和服务器资源



  • 短轮询和长轮询可能会产生较多的无效请求,增加带宽和服务器负担;SSE和WebSocket相对更高效。


个人简介


👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.


🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。


🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。


💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。


🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。


📖 保持关注我的博客,让我们共同追求技术卓越。


作者:Lorin洛林
来源:juejin.cn/post/7305473943572578341
收起阅读 »

MinIO是干嘛的?

一、MinIO是干嘛的? 网上搜索“minio是干嘛的”这个问题搜索的太多了,我们感觉是我们的工作没有做好才给大家造成了这么大的信息差。在这里,我们有义务将信息差补齐。 先正面回答问题: MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独...
继续阅读 »

一、MinIO是干嘛的?


网上搜索“minio是干嘛的”这个问题搜索的太多了,我们感觉是我们的工作没有做好才给大家造成了这么大的信息差。在这里,我们有义务将信息差补齐。


先正面回答问题:



MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独立、私有化、公有云、边缘网络的对象存储软件。
它是一个开源的软件,原来遵循的是Apache协议,在2021年4月22日修改为了AGPL v3.0协议。
如果遵守软件许可协议使用,你几乎可以免费使用它。



二、MinIO的解释好复杂,给我一个简单点的解释行吗?


很多朋友又提到了下面的问题:
“你上午说了那么大一段,我根本不明白是什么意思呀?你能简单点一说一下到底是干嘛的,为什么要用MinIO吗?”


好的,我们提取一些关键词:



  1. SDS,软件定义存储

  2. 分布式存储

  3. 对象存储

  4. 私有云存储

  5. 公有云存储

  6. 边缘网络

  7. apche协议

  8. AGPL v3.0协议


我们针对上面的回答清楚后,再来理解最上面的一句话就好理解了。


三、名词解释


3.1 SDS(软件定义存储)


传统的存储设备都是有专用硬件的。但是,CPU的算力迅猛增长,算力不再是问题了。并且,也不需要再次购买专用硬件了。
基于CPU强大的算力,用软件实现和定义的分布式存储,即便宜、又安全、还省钱。
与传统硬件定义的存储价格相对可以节省成本3 - 7倍的费用。


3.2 分布式存储


传统的存储像NAS(网络附加存储)都是单节点的,如果出现网络通信故障,整个数据保障全部都会中断。因此,大家想到了一种办法:由多台服务器构建一个存储网络,任意一台存储服务器掉线都不会影响数据安全和服务的稳定。这个时候,就推出了分布式存储。


3.3 对象存储


最早的时候Google 开放了它全球 低成本存储的一篇实践论文,引起了全球的存储市场的震动。后来各家都基于Google开放的文档实现了自己的对象存储,极大的降低了自己企业的成本。其中:
亚马逊实现的对象存储叫S3;
阿里云实现了OSS(Object storage system);
Google实现的对象存储叫GCS(Google cloud storage);
微软实现的对象存储叫ABS(Azure Blob Storage);
百度实现的叫BOS;
国内其他厂商,包括七牛、青云、ceph等厂家也都实现了自己的对象存储系统。


在对象存储的内部使用URL进行统一资源定位,每一个对象相当于是一个URL,这样相比于传统的文件系统存储方式,对象存储更加灵活、可扩展性更强,更适合存储海量数据。
它最最大的优点在于:节约成本的同时,实现高可扩展性,它可以轻松地增加存储容量,而无需停机维护或中断服务。
而公开对象存储标准的是S3。因此,


3.4 私有云存储、公有云存储、边缘网络


公有云:一般由大公司如阿里、腾讯、百度等公司构建的公众(个人或者公司)可以直接在上面按量或按需租赁服务器、算力、存储空间的一种云计算产品。
私有云:私有云有更好的安全性、私密性、独立性,一般是由企业自己构建的云计算池资源。
边缘网络:一般是小型物联网设备或者家庭物联网设备,如家用电视、路由器、家用存储网关、工厂存储网关、汽车存储网关等。


3.5 Apache 协议和AGPL v3.0 协议


首先,国外讲究开源和普世价值观,好的东西分享给更多的人,所以马斯克的星舰、特斯拉的全部源代码、设计图全都开源了。


但是,需要让更多的人遵守一个开源规范,于是就有了一系列的开源协议如:Apache协议、AGPL v3.0协议。


Apache协议的特点:



  1. 代码派生:Apache 协议允许对代码进行修改、衍生和扩展,并且可以将这些修改后的代码重新发布。

  2. 私有使用:Apache 协议还允许将 Apache 许可的代码用于私有目的,而不需要公开发布或共享这些代码。

  3. 版权声明:Apache 协议要求所有代码都必须包含原始版权声明和许可证。

  4. 免责声明:Apache 协议明确规定,代码作者和 Apache 软件基金会不对任何因使用该软件而引起的风险和损失负责。

  5. 专利授权:Apache 协议明确规定,如果原始代码拥有人拥有相关专利,则授予使用该代码的公司和个人适当的专利授权。
    所以我们通常认为,Apache 协议是一种非常灵活和宽松的开源许可证,允许开源社区和商业公司根据自己的需求进行自由使用和分发代码。


AGPL v3.0开源协议的特点:


AGPL v3.0 协议要求在使用AGPL v3.0 许可的软件作为服务通过互联网向外提供服务时,必须公开源代码并允许其他人查看、修改和分发源代码。
这个开源协议有以下几个特点:



  1. 共享和公开源代码:AGPL v3.0 协议要求将使用该许可证的软件的源代码公开,并且所有基于该软件构建的应用程序都必须遵守该许可证的规定。

  2. 网络服务的限制:AGPL v3.0 协议适用于在网络上提供服务的软件,例如 Web 应用程序和 SaaS(Software as a Service)服务。如果使用许可证的软件被用于这些服务,那么相应的源代码必须公开。

  3. 贡献者权益保护:AGPL v3.0 协议还明确规定,任何对软件进行更改或修改的用户必须将其贡献回到原始项目中,以便其他人也可以自由地使用和修改这些更改。

  4. 版权声明:AGPL v3 协议要求在所有的副本和派生作品中包含原始版权和许可证声明。


总结,Apache开源协议更为宽松,而AGPL v3.0协议的权利义务要求更加严格一些。


四、MinIO是干嘛的?(总结)


4.1 温故而知新


上面我们解析了所有的内容,再读一次,我们的总结:



MinIO是一种SDS(软件定义存储)的分布式存储软件,用来进行构建独立、私有化、公有云、边缘网络的对象存储软件。
它是一个开源的软件,原来遵循的是Apache协议,在2021年4月22日修改为了AGPL v3.0协议。
如果遵守软件许可协议使用,你几乎可以免费使用它。



4.2 使用场景


说了一系列理论,不说使用场景就是耍(bu)流(yao)氓(lian)。


现在企业在开发的时候有一系列的要求:



  1. 不准在服务器进行本地文件写入;

  2. 要求写入必须要写入至统一对象存储中去。


这样的要求带来的好处就是:
每个人写入的时候,都写到了统一的存储数据湖中。如果有5台应用服务器需要快速扩容,可以瞬间再扩展5台服务器,构建10台服务器空间即可。所有的文件都存储于MinIO这样的对象存储中,扩容而不需要复制各台服务器中的文件。
这样就能实现业务的快速扩容啦。


你懂了吗?


作者:Python小甲鱼
来源:juejin.cn/post/7304531203772334115
收起阅读 »

看完周杰伦《最伟大的作品》MV后,我解锁了想要的UI配色方案!

在UI设计的核心理念中,色彩的搭配与运用显得至关重要。事实上,一个合理且得当的色彩组合往往就是UI设计成功的关键。要构建一个有用的UI配色方案,我们既需要掌握色彩理论知识,更要学会在生活中洞察和提取灵感。以周杰伦最新推出的音乐作品《最伟大的作品》为例,其MV因...
继续阅读 »

在UI设计的核心理念中,色彩的搭配与运用显得至关重要。事实上,一个合理且得当的色彩组合往往就是UI设计成功的关键。要构建一个有用的UI配色方案,我们既需要掌握色彩理论知识,更要学会在生活中洞察和提取灵感。以周杰伦最新推出的音乐作品《最伟大的作品》为例,其MV因其独特的色彩构成和视觉效果一经发布便激起了网络热潮,成为了热门话题。这部MV以高度尊敬的方式向众多世界级艺术家们的杰作致敬,为设计师们提供了寻找新颖配色方案的无价参考。然而,在UI设计实践中,运用调色板精心匹配出合适的色彩方案绝非易事。


对于这个看起来既复杂又麻烦的UI界面配色问题,今天Pixso将为你分享一个聪明而实用的方法:就是利用那些已经得到广大公众认可并赞誉的色彩创作策略。


1. 复古UI配色,梦回巴黎


歌曲《最伟大的作品》背景在1920年代的巴黎,当时也是“巴黎画派”最为辉煌的年代。在此张MV截图中,整个色调与中国古典画的UI界面配色在达到了某种程度的默契。青、棕两个主色,使画面有着很浓的复古味道。将此复古色调运用到在我们的UI设计中,可以让我们省去很多的构思配色的问题。


复古色调


比如下图中的这个珠宝登陆页面,运用了棕色作为大背景颜色,大块的色彩在烘托气氛跟主题方面较为稳定,与珠宝的华贵气质相呼应,给画面一种华贵的美感,这样的UI配色会使UI界面非常的出彩,不显单调。如果你想深入学习网站UI配色,建议阅读《全套大厂网页UI配色指南,网站想不好看都难》


免费珠宝店登陆页


[免费珠宝店登陆页](https://pixso.cn/community/file/L6ufTu9mbHowkkVaOXhqmQ?from_share)


2. 冷暖 对比UI配色,优雅端庄


在设计UI界面时,应该做到整体色调协调统一,界面设计应该先确定主色调,主色将会占据页面中很大的面积,其他的辅助色都应该以主色为基准进行搭配。这可以保证整体色调的协调统一,重点突出,使作品更加专业、美观。 


冷暖色的区分是人类在大自然生活总结出来的颜色规律,通过联想将颜色与具体事物连接在一起,再由事物给人的感觉去区分冷暖。冷暖色是自然平衡的规律,可以在设计中大量使用,这样的UI配色方案会使UI界面非常的出彩,不显单调。


冷暖对比UI配色


而在下图的移动应用程序界面中,所使用的,正是将冷暖色完美的融合贯穿,但是在UI设计时,UI设计师需注意,不要采用过多色彩,会使得界面没有秩序性,给用户一种混乱感。如果你想深入学习移动APP配色方案,可以阅读Pixso资源社区的设计技巧专栏《UI设计师如何为一款app配色?值得收藏篇!》


矢量插图旅行APP


[矢量插图旅行APP](https://pixso.cn/community/file/hLz9LrhMmFFvGre1aVwtdQ?from_share)


3. 深棕 UI配色,灵动梦幻


色彩的对比与调和是色彩构成的基本原理,表现色彩的多样变化主要依靠色彩的对比,使变化和多样的色彩达到统一主要依靠色彩的调和。概括说来,色彩的对比是绝对的,调和是相对的,对比是目的,调和是手段。


深棕UI配色


深棕色调的UI界面会显得太过沉重,在中间加入浅色调调和一下,整个画面立刻上升了一个质感度,沉稳又不失俏皮的美感。


OTP 验证页


[OTP 验证页](https://pixso.cn/community/file/5qd8ACoD9nrDQSBD8BxjEw?from_share)


4. 深色 UI配色,沉稳低调


颜色会唤起不同的感觉或情绪,所以通过了解颜色的心理学,我们可以利用与目标受众产生共鸣的品牌颜色。低明度的颜色则会更多的强化稳重低调的感觉。 学习UI配技巧,可以阅读《超实用UI配色技巧,让你的UI设计财“色”双收》


深色UI配色


在深色的对比中,加入低饱和度的颜色,在提升画面亮度的同时,也能提升用户的视觉观感,即使是深色调也能产生一种小清新的美感。


比特币APP UI设计


[比特币APP UI设计](https://pixso.cn/community/file/i9zSK-ga4mhu2BhRUAysZg?from_share)


5. 暖色 调UI配色,热情复古


人们看到不同的颜色会产生不同的心理反应,例如看到红色会下意识地心跳加速、血液流速加快,进而从心理上感受到一种兴奋、刺激、热情的感觉,这就是色彩的作用和意象。暖色调使人狂热、欢乐和感性。


暖色调UI配色


恰到好处的暖色调对比会使画面更加协调和丰富,使UI的色彩不至于太过单一。而暖色调即代表温馨、热情的气氛,但搭配不当会使画面呈现出拖沓、不清爽的反面效果。


毛玻璃视觉设计


[毛玻璃视觉设计](https://pixso.cn/community/file/zYUJ5EIiY4Uh6w3DPINVrg?from_share)


6. 冷淡 色调 UI配色,浪漫温柔


冷淡色调UI配色


UI界面通常尺寸较“小”,不少功能难以在一个界面内实现,用户需要在多个界面中频繁跳转,而冷淡的色彩设计能减轻用户在频繁跳转界面时的焦躁。淡色彩的UI配色范围可以从比原始色相略浅,一直到几乎没有任何原始色相的灰白色。有色颜色在眼睛上看起来更柔和更容易,其中最浅的颜色称为粉彩。淡色彩通常会在设计中营造出年轻柔和的氛围。


紫色UI组件库


[紫色UI组件库](https://pixso.cn/community/file/2_-jN0hAMOHrF6REAen62A?from_share)


7. 专业UI配色工具Pixso,成就伟大配色方案


在设计时,设计师总会为了颜色的填充苦恼,Pixso新上线的多色矢量网格功能,路径色快可以快速填充各种颜色,让设计师以前以前绘制一个复杂的颜色魔方需要更多的路径线条,更多的色卡,还得考虑图层的对齐,间距是否一致统一的问题,在Pixso这些都不需要考虑了。如果你仍不知道如何提取颜色,或者觉得提取颜色麻烦,可以试试Pixso里的一键取色插件,只需要导入图片,在右上角的插件里找到一键取色,点击一键取色即可。


一键取色插件


其次,在Pixso右上角的插件按钮中,选择色板插件,里面都是大厂色板,让你站在大厂肩膀上做UI配色,想不好看都难。


色板插件


除此之外,Pixso还有协同设计、在线评论、一键交付等等强大功能,帮助设计师更快的完成设计工作,快打开Pixso试试吧~


作者:Yuki1
来源:juejin.cn/post/7304538199144415268
收起阅读 »

新项目,不妨采用这种架构分层,很优雅!

大家好,我是飘渺。今天继续更新DDD&微服务的系列文章。 在专栏开篇提到过DDD(Domain-Driven Design,领域驱动设计)学习起来较为复杂,一方面因为其自身涉及的概念颇多,另一方面,我们往往缺乏实战经验和明确的代码模型指导。今天,我们将...
继续阅读 »

大家好,我是飘渺。今天继续更新DDD&微服务的系列文章。


在专栏开篇提到过DDD(Domain-Driven Design,领域驱动设计)学习起来较为复杂,一方面因为其自身涉及的概念颇多,另一方面,我们往往缺乏实战经验和明确的代码模型指导。今天,我们将专注于DDD的分层架构和实体模型,期望为大家落地DDD提供一些有益的参考。首先,让我们回顾一下熟悉的MVC三层架构。


1. MVC 架构


在传统应用程序中,我们通常采用经典的MVC(Model-View-Controller)架构进行开发,它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次,也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是一种标准的软件分层架构。


在遵循此分层架构的开发过程中,我们通常会建立三个Maven Module:Controller、Service 和 Dao,它们分别对应表现层、逻辑层和数据访问层,如下图所示:


image-20230602123152660


(图中多画了一个Model层是因为 Model 通常只是简单的 Java Bean,只包含数据库表对应的属性。有的应用会将其单独抽取出来作为一个Maven Module,但实际上它可以合并到 DAO 层。)


1.1 MVC架构模型的不足


在业务逻辑较为简单的应用中,MVC三层架构是一种简洁高效的开发模式。然而,随着业务逻辑的复杂性增加和代码量的增加,MVC架构可能会显得捉襟见肘。其主要的不足可以总结如下:



  • Service层职责过重:在MVC架构中,Service层常常被赋予处理复杂业务逻辑的任务。随着业务逻辑的增长,Service层可能变得臃肿和复杂。业务逻辑有可能分散在各个Service类中,使得业务逻辑的组织和维护成为一项挑战。

  • 过于关注数据库而忽视领域建模:虽然MVC的设计初衷是对数据、用户界面和控制逻辑进行分离,但它在面对复杂业务场景时并未给予领域建模足够的重视。这可能导致代码难以理解和扩展,因为代码更像是围绕数据库而不是业务需求进行设计。

  • 边界划分不明确:在MVC架构中,顶层设计上的边界划分并没有明确的规则,往往依赖于技术负责人的经验。在大规模的团队协作中,这可能导致职责不清晰、分工不明确等问题。

  • 单元测试困难:在MVC架构中,Service层通常以事务脚本的方式进行开发,并且往往耦合了各种中间件操作,如数据库、缓存、消息队列等。这种耦合使得单元测试变得困难,因为要在没有这些中间件的情况下运行测试可能需要大量的模拟或存根代码。


在深入探讨MVC架构之后,我们将进入今天的主题:DDD的分层架构模型。


2. DDD的架构模型


在DDD中,通常将应用程序分为四个层次,分别为用户接口层(Interface Layer)应用层(Application Layer)领域层(Domain Layer)基础设施层(Infrastructure Layer),每个层次承担着各自的职责和作用。分层模型如下图所示:


image.png



  1. 接口层(Interface Layer):负责处理与外部系统的交互,包括UI、Web API、RPC接口等。它会接收用户或外部系统的请求,然后调用应用层的服务来处理这些请求,最后将处理结果返回给用户或外部系统。

  2. 应用层(Application Layer):承担协调领域层和基础设施层的职责,实现具体的业务逻辑。它调用领域层的领域服务和基础设施层的基础服务,完成业务逻辑的实现。

  3. 领域层(Domain Layer):该层包含了业务领域的所有元素,如实体、值对象、领域服务、聚合、工厂和领域事件等。这一层的主要职责是实现业务领域的核心逻辑。

  4. 基础设施层(Infrastructure Layer):主要提供通用的技术能力,如数据持久化、缓存、消息传输等基础设施服务。它可被其他三层调用,提供各种必要的技术服务。


在这四层中,调用关系通常是单向依赖的,即上层依赖下层,下层并不依赖上层。例如,接口层依赖应用层,应用层依赖领域层,领域层依赖基础设施层。但值得注意的是,尽管基础设施层在物理结构上可能位于最底层,但在DDD的分层模型中,它位于最外层,为内部各层提供技术服务。


image-20230604220949124


2.1 依赖反转原则


依赖反转原则(Dependency Inversion Principle, DIP)是一种有效的设计原则,有助于减小模块间的耦合度,提高系统的扩展性和可维护性。依赖反转原则的核心思想是:高层模块不应直接依赖低层模块,它们都应该依赖抽象。抽象不应该依赖具体的实现,而具体的实现应当依赖于抽象。


在 DDD 的四层架构中,领域层是核心,是业务的抽象化,不应直接依赖其他任何层。这意味着领域层的业务对象应该与其他层(如基础设施层)解耦,而不是直接依赖于具体的数据库访问技术、消息队列技术等。但在实际运行时,领域层的对象需要通过基础设施层来实现数据的持久化、消息的发送等。


为了解决这个问题,我们可以使用依赖翻转原则。在领域层,我们定义一些接口(如仓储接口),用于声明领域对象需要的服务,具体的实现则由基础设施层完成。在基础设施层,我们实现这些接口,并将实现类注入到领域层的对象中。这样,领域层的对象就可以通过这些接口与基础设施层进行交互,而不需要直接依赖于基础设施层。


2.2 DDD四层架构的优势


在复杂的业务场景下,采用DDD的四层架构模型可以有效地解决使用MVC架构可能出现的问题:



  1. 职责分离:在DDD的设计中,我们尝试将业务逻辑封装到领域对象(如实体、值对象和领域服务)中。这样可以降低应用层(原MVC中的Service层)的复杂性,同时使得业务逻辑更加集中和清晰,易于维护和扩展。

  2. 领域建模:DDD的核心理念在于通过建立富有内涵的领域模型来更真实地反映业务需求和业务规则,从而提高代码的灵活性,使其更容易适应业务的变化。

  3. 明确的边界划分:DDD通过边界上下文(Bounded Context)的概念,对系统进行明确的边界划分。每个边界上下文都有自己的领域模型和业务逻辑,使得大规模团队协作更加清晰、高效。

  4. 易于测试:由于业务逻辑封装在领域对象中,我们可以直接对这些领域对象进行单元测试。同时,基础设施层(如数据库、缓存和消息队列)被抽象为接口,我们可以使用模拟对象(Mock Object)进行测试,避免了直接与真实中间件的交互,大大提升了测试的灵活性和便利性。


接下来看看如何在代码中遵循DDD的分层架构。


3. 如何实现DDD分层架构


为了遵循DDD的分层架构,在代码实现时有两种实现方法。


第一种是在模块中通过包进行隔离,即在模块中建立4个不同的代码包,分别对应领域层(Domain Layer)、应用层(Application Layer)、基础设施层(Infrastructure Layer)和用户接口层(User Interface Layer)。这种方法的优点是结构简单,易于理解和维护。但缺点是各层之间的依赖关系可能不够明确,容易导致代码耦合。


image.png


第二种实现方法是建立4个不同的Maven Module层,每个Module分别对应领域层、应用层、基础设施层和用户接口层。这种方法的优点是各层之间的依赖关系更加明确,有利于降低耦合度和提高代码的可重用性。同时,这种方法也有助于团队成员更好地理解和遵循DDD的分层架构。然而,这种方法可能会导致项目结构变得复杂,增加了项目的维护成本。


image.png


在实际项目中,可以根据项目规模、团队成员的熟悉程度以及项目需求来选择合适的实现方法。对于较小规模的项目,可以采用第一种方法,通过包进行隔离。而对于较大规模的项目,建议采用第二种方法,使用Maven Module层进行隔离,以便更好地管理和维护代码。无论采用哪种方法,关键在于确保各层之间的职责分明,遵循DDD的原则和最佳实践。


在DailyMart项目中,我最初打算采用第一种方法,通过包进行隔离。然而,在微信群中进行投票后,发现近90%的人选择了第二种方法。作为一个倾听粉丝意见的博主,我决定采纳大家的建议。因此,DailyMart将采用Maven Module层隔离的方式进行编码实践。
image.png


4. DDD中的数据模型


在DDD中,我们采用特定的模型来映射和处理不同的领域概念和责任,常见的有三种数据模型:实体对象(Entity)、数据对象(Data Object,DO)和数据传输对象(Data Transfer Object,DTO)。这些模型在DDD中有着明确的角色和使用场景:



  • Entity(实体对象): 实体对象代表业务领域中的核心概念,其字段和方法应与业务语言保持一致,与持久化方式无关。这意味着实体和数据对象可能具有完全不同的字段命名、字段类型,甚至嵌套关系。实体的生命周期应仅存在于内存中,无需可序列化和可持久化。

  • Data Object (DO、数据对象): DO可能是我们在日常工作中最常见的数据模型。在DDD规范中,数据对象不能包含业务逻辑,并且位于基础设施层,仅负责与数据库进行交互,通常与数据库的物理表一一对应。

  • DTO(数据传输对象): 数据传输对象主要用作接口层和应用层之间传递数据,例如CQRS模式中的命令(Command)、查询(Query)、事件(Event)以及请求(Request)和响应(Response)。DTO的重要性在于它能够适配不同的业务场景需要的参数,从而避免业务对象变成庞大而复杂的"万能"对象。


在DDD中,这三种数据对象在很多场景下需要相互转换,例如:




  1. Entity <-> DTO:在应用层返回数据时,需要将实体对象转换成DTO,这一般通过一个名为DTO Assembler的转换器来完成。




  2. Entity <-> DO:在基础设施层的Repository实现时,我们需要将实体转换为DO以存储到数据库。同样地,查询数据时需要将DO转换回实体。这通常通过一个名为Data Converter的转换器来完成。




当然,不管是Entity转DTO,还是Entity转DO,都会有一定的开销,无论是代码量还是运行时的操作来看。手写转换代码容易出错,而使用反射技术虽然可以减少代码量,但可能会导致显著的性能损耗。这里给用Java的同学推荐MapStruct这个库,MapStruct在编译时生成代码,只需通过接口定义和注解配置就能生成相应的代码。由于生成的代码是直接赋值,所以性能损耗可以忽略不计。


image.png



在SpringBoot老鸟系列中我推荐大家使用 Orika 进行对象转换,理由是只需要编写少量代码。但是在DDD中不同对象都有严格的代码层级,并且一般会引入专门的Assembler和Converter转换器,既然代码量省不了,必然要选择性能最高的组件。


各种转换器的性能对比:Performance of Java Mapping Frameworks | Baeldung



5. 小结


本篇文章详细介绍了DDD的分层架构,并详细解释了如何在项目代码中实现这种分层架构。同时,还详细DDD中三种常用的数据对象:数据对象(DO)、实体(Entity)和数据传输对象(DTO)。这三种数据对象的区别可以通过下图进行精炼总结:


image-20230523220725247


至此,我们已经深入解析了DDD中的核心概念。同时,我们的DailyMart商城系统已完成所有的前期准备,现在已经准备好进入实际的编码阶段。在接下来的章节中,我们将从实现注册流程开始,逐步探索如何在实际项目中应用DDD。


作者:飘渺Jam
来源:juejin.cn/post/7242129428511113272
收起阅读 »

一位未曾涉足算法的初学者收获

正如标题所言,在我四年的编程经历中就没刷过一道算法题,这可能与我所编写的应用有关,算法对我而言提升不是特别大。加上我几乎都是在需求中学习,而非系统性的学习。所以像算法这种基础知识我自然就不是很熟悉。 那我为何会接触算法呢? 我在今年暑假期间有一个面试,当时面试...
继续阅读 »

正如标题所言,在我四年的编程经历中就没刷过一道算法题,这可能与我所编写的应用有关,算法对我而言提升不是特别大。加上我几乎都是在需求中学习,而非系统性的学习。所以像算法这种基础知识我自然就不是很熟悉。


那我为何会接触算法呢?


我在今年暑假期间有一个面试,当时面试官想考察一下我的算法能力,而我直接明摆了和说我不行(指算法上的不行),但面试官还是想考察一下,于是就出了道斐波那契数列作为考题。


但我毕竟也接触了 4 年的代码,虽然不刷算法,但好歹也看过许多文章和代码,斐波那契数列使用递归实现的代码也有些印象,于是很快我就写出了下面的代码作为我的答案。


function fib(n) {
if (n <= 1) return n

return fib(n - 1) + fib(n - 2)
}

面试官问我还有没有更好的答案,我便摇了摇头表示这 5 行不到的代码难道不是最优解?



事实上这份代码看起来很简洁,实际却是耗时最慢的解法



毫无疑问,在算法这关我肯定是挂了的,不过好在项目经验及后续的项目实践考核较为顺利,不然结局就是回去等通知了。最后面试接近尾声时,面试官友情提醒我加强基础知识(算法),强调各种应用框架不断更新迭代,但计算机的底层基础知识是不变的。于是在面试官的建议下,便有了本文。


好吧,我承认我是为了面试才去学算法的。


对上述代码进行优化


在介绍我是从何处学习算法以及从中学到了什么,不妨先来看看上题的最优答案是什么。


对于有接触过算法的同学而言,不难看出时间复杂度为 O(n²),而指数阶属于爆炸式增长,当 n 非常大时执行效果缓慢,且可能会出现函数调用堆栈溢出。


如果仔细观察一下,会发现这其中进行了非常多的重复计算,我们不妨将设置一个 res 变量来输出一下结果


function fib(n) {
if (n <= 1) {
return n
}

const res = fib(n - 1) + fib(n - 2)
console.log(res)
return res
}

当 n=7 时,所输出的结果如下


Untitled


这还只是在 n=7 的情况下,便有这么多输出结果。而在算法中要避免的就是重复计算,这能够高效的节省执行时间,因此不妨定义一个缓存变量,在递归时将缓存变量也传递进去,如果缓存变量中存在则说明已计算过,直接返回结果即可。


function fib(n, mem = []) {
if (n <= 1) {
return n
}

if (mem[n]) {
return mem[n]
}

const res = fib(n - 1, mem) + fib(n - 2, mem)
console.log(res)
mem[n] = res
return res
}

此时所输出的结果可以很明显的发现没有过多的重复计算,执行时间也有显著降低。


Untitled


这便是记忆化搜索,时间复杂度被优化至 O(n)。


可这还是免不了递归调用出现堆栈溢出的情况(如 n=10000 时)。


Untitled


从上面的解法来看,我们都是从”从顶至底”,比方说 n=7,会先求得 n=6,n=5 的结果,然后依次类推直至得到底层 n=1 的结果。


事实上我们可以换一种思路,先求得 n=1,n=2 的结果,然后依次类推上去,最终得到 n=6,n=7 的结果,也就是“从底至顶”,而这就是动态规划的方法。


从代码上来分析,因此我们可以初始化一个 dp 数组,用于存放数据状态。


function fib(n) {
const dp = [0, 1]

for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}

return dp[n]
}

最终 dp 数组的最后一个成员便是原问题的解。此时输出 dp 数组结果。


Untitled


且由于不存在递归调用,因此你当 n=10000 时也不在会出现堆栈溢出的情况(只不过最终的结果必定超出了 JS 数值可表示范围,所以只会输出 Infinity)


对于上述代码而言,在空间复杂度上能够从 O(n) 优化到 O(1),至于实现可以参考 空间优化,这里便不再赘述。


我想至少从这里你就能看出算法的魅力所在,这里我强烈推荐 hello-algo 这本数据结构与算法入门书,我的算法之旅的起点便是从这本书开始,同时激发起我对算法的兴趣。


两数之和


于是在看完了这本算法书后,我便打开了大名鼎鼎的刷题网站 LeetCode,同时打开了究极经典题目的两数之和



有人相爱,有人夜里开车看海,有人 leetcode 第一题都做不出来。



题干:



给定一个整数数组 nums  和一个整数目标值 target,请你在该数组中找出和为目标值target的那 两个 整数,并返回它们的数组下标。


你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。


你可以按任意顺序返回答案。



以下代码将会采用 JavaScript 代码作为演示。


暴力枚举


我初次接触该题也只会暴力解法,遇事不决,暴力解决。也很验证了那句话:不论多久过去,我首先还是想到两个 for。


var twoSum = function (nums, target) {
const n = nums.length

for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
if (nums[i] + nums[j] === target && i !== j) {
return [i, j]
}
}
}
}

当然针对上述 for 循环优化部分,比如说让 j = i + 1 ,这样就可以有效避免重复数字的循环以及 i ≠ j 的判断。由于用到了两次循环,很显然时间复杂度为 O(n²),并不高效。


哈希表


我们不妨将每个数字通过 hash 表缓存起来,将值 nums[i] 作为 key,将 i 作为 value。由于题目的条件则是 x + y = target,也就是 target - x = y,这样判断的条件就可以由 nums[i]+ nums[j] === target 变为 map.has(target - nums[i]) 。如果 map 表中有 y 索引,那么显然 target - nums[i] = y,取出 y 的索引以及当前 i 索引就能够得到答案。代码如下


var twoSum = function (nums, target) {
const map = new Map()

for (let i = 0; i < nums.length; i++) {
if (map.has(target - nums[i])) {
return [map.get(target - nums[i]), i]
}
map.set(nums[i], i)
}
}

而这样由于只有一次循环,时间复杂度为 O(N)。


双指针算法(特殊情况)


假如理想情况下,题目所给定的 nums 是有序的情况,那么就可以考虑使用双指针解法。先说原理,假设给定的 nums 为 [2,3,5,6,8],而目标的解为 9。在上面的做法中都是从索引 0 开始枚举,也就是 2,3,5…依次类推,如果没找到与 2 相加的元素则从 3 开始 3,5,6…依次类推。


此时我们不妨从最小的数最大的数开始,在这个例子中也就是 2 和 8,很显然 2 + 8 > 9,说明什么?说明 8 和中间所有数都大于 9 即 3+8 ,5+8 肯定都大于 9,所以 8 的下标必然不是最终结果,那么我们就可以把 8 排除,从 [2,3,5,6] 中找出结果,同样的从最小和最大的数开始,2 + 6 < 9 ,这又说明什么?说明 2 和中间这些数相加肯定都下雨 9 即 2+3,2+5 肯定都小于 9,因此 2 也应该排除,然后从 [3,5,6] 中找出结果。就这样依次类推,直到找到最终两个数 3 + 6 = 9,返回 3 与 6 的下标即可。


由于此解法相当于有两个坐标(指针)不断地向中间移动,因此这种解法也叫双指针算法。当然,要使用该方式的前提是输入的数组有序,否则无法使用。


用代码的方式来实现:



  1. 定义两个坐标(指针)分别指向数组成员最左边与最右边,命名为 left 与 right。

  2. 使用 while 循环,循环条件为 left < right。

  3. 判断 nums[left] + nums[right]target 的大小关系,如果相等则说明找到目标(答案),如果大于则 右指针减 1 right—-,小于则左指针加 1 left++


function twoSum(nums, target) {
let left = 0
let right = nums.length - 1

while (left < right) {
const sum = nums[left] + nums[right]
if (sum === target) {
return [left, right]
}

if (sum > target) {
right--
} else if (sum < target) {
left++
}
}
}



针对上述两道算法题浅浅的做个分享,毕竟我还只是一名初入算法的小白。对我而言,我的算法刷题之旅还有很长的一段时间。且看样子这条路可能不会太平坦。


算法对我有用吗?


在我刷算法之前,我在网上看到鼓吹算法无用论的人,也能看到学算法却不知如何应用的人。


这也不禁让我思考 🤔,算法对我所开发的应用是否真的有用呢?


在我的开发过程中,往往面临着各种功能需求,而通常情况下我会以尽可能快的速度去实现该功能,至于说这个功能耗时 1ms,还是 100 ms,并不在乎。因为对我来说,这种微小的速度变化并不会被感知到,或者说绝大多数情况下,处理的数据规模都处在 n = 1 的情况下,此时我们还会在意 n² 大还是 2ⁿ 大吗?


但如果说到了用户感知到卡顿的情况下,那么此时才会关注性能优化,否则,过度的优化可能会成为一种徒劳的努力。


或许正是因为我都没有用到算法解决实际问题的经历,所以很难说服自己算法对我的工作有多大帮助。但不可否认的是,算法对我当前而言是一种思维上的拓宽。让我意识到一道(实际)问题的解法通常不只有一种,如何规划设计出一个高效的解决方案才是值得我们思考的地方。


结语


借 MIT 教授 Erik Demaine 的一句话



If you want to become a good programmer, you can spend 10 years programming, or spend 2 years programming and learning algorithms.



如果你想成为一名优秀的程序员,你可以花 10 年时间编程,或者花 2 年时间编程和学习算法。


这或许就是学习算法的真正意义。


参考文章


初探动态规划


学习算法重要吗?


作者:愧怍
来源:juejin.cn/post/7278952595423133730
收起阅读 »

🔥🔥通过浏览器URL地址,5分钟内渗透你的网站!很刑很可拷!

今天我来带大家简单渗透一个小破站,通过这个案例,让你深入了解为什么很多公司都需要紧急修复各个中间件的漏洞以及进行URL解析拦截等重要操作。这些措施的目的是为了保护网站和系统的安全性。如果不及时升级和修复漏洞,你就等着被黑客攻击吧! 基础科普 首先,我想说明一下...
继续阅读 »

今天我来带大家简单渗透一个小破站,通过这个案例,让你深入了解为什么很多公司都需要紧急修复各个中间件的漏洞以及进行URL解析拦截等重要操作。这些措施的目的是为了保护网站和系统的安全性。如果不及时升级和修复漏洞,你就等着被黑客攻击吧!


基础科普


首先,我想说明一下,我提供的信息仅供参考,我不会透露任何关键数据。请不要拽着我进去喝茶啊~


关于EXP攻击脚本,它是基于某种漏洞编写的,用于获取系统权限的攻击脚本。这些脚本通常由安全研究人员或黑客编写,用于测试和演示系统漏洞的存在以及可能的攻击方式。


而POC(Proof of Concept)概念验证,则是基于获取到的权限执行某个查询的命令。通过POC,我们可以验证系统的漏洞是否真实存在,并且可以测试漏洞的影响范围和危害程度。


如果你对EXP攻击脚本和POC感兴趣,你可以访问EXP攻击武器库网站:http://www.exploit-db.com/。 这个网站提供了各种各样的攻击脚本,你可以在这里了解和学习不同类型的漏洞攻击技术。


另外,如果你想了解更多关于漏洞的信息,你可以访问漏洞数据库网站:http://www.cvedetails.com/。 这个网站提供了大量的漏洞信息和漏洞报告,你可以查找和了解各种不同的漏洞,以及相关的修复措施和建议。


但是,请记住,学习和了解这些信息应该用于合法和道德的目的,切勿用于非法活动。网络安全是一个重要的问题,我们应该共同努力保护网络安全和个人隐私。


利用0day or nday 打穿一个网站(漏洞利用)



  • 0day(未公开)和nday(已公开)是关于漏洞的分类,其中0day漏洞指的是尚未被公开或厂商未修复的漏洞,而nday漏洞指的是已经公开并且有相应的补丁或修复措施的漏洞。

  • 在Web安全领域,常见的漏洞类型包括跨站脚本攻击(XSS)、XML外部实体注入(XXE)、SQL注入、文件上传漏洞、跨站请求伪造(CSRF)、服务器端请求伪造(SSRF)等。这些漏洞都是通过利用Web应用程序的弱点来实施攻击,攻击者可以获取用户敏感信息或者对系统进行非法操作。

  • 系统漏洞是指操作系统(如Windows、Linux等)本身存在的漏洞,攻击者可以通过利用这些漏洞来获取系统权限或者执行恶意代码。

  • 中间件漏洞是指在服务器中常用的中间件软件(如Apache、Nginx、Tomcat等)存在的漏洞。攻击者可以通过利用这些漏洞来获取服务器权限或者执行恶意操作。

  • 框架漏洞是指在各种网站或应用程序开发框架中存在的漏洞,其中包括一些常见的CMS系统。攻击者可以通过利用这些漏洞来获取网站或应用程序的权限,甚至控制整个系统。


此外,还有一些公司会组建专门的团队,利用手机中其他软件的0day漏洞来获取用户的信息。


我今天的主角是metinfo攻击脚本: admin/column/save.php+【秘密命令】(我就不打印了)


蚁剑远控工具


中国蚁剑是一款开源的跨平台网站管理工具,它主要面向合法授权的渗透测试安全人员和常规操作的网站管理员。蚁剑提供了丰富的功能和工具,帮助用户评估和加强网站的安全性。


你可以在以下地址找到蚁剑的使用文档和下载链接:http://www.yuque.com/antswordpro…


然后今天我来破解一下我自己的网站,该网站是由MetInfo搭建的,版本是Powered by MetInfo 5.3.19


image


开始通过url渗透植入


现在我已经成功搭建好了一个网站,并且准备开始破解。在浏览器中,我直接输入了一条秘密命令,并成功地执行了它。下面是执行成功后的截图示例:


image


好的,现在我们准备启用我们的秘密武器——蚁剑。只需要输入我攻击脚本中独有的连接密码和脚本文件的URL地址,我就能成功建立连接。连接成功后,你可以将其视为你的远程Xshell,可以随意进行各种操作。


image


我们已经定位到了我们网站的首页文件,现在我们可以开始编写一些内容,比如在线发牌~或者添加一些图案的元素等等,任何合适的内容都可以加入进来。


image


不过好像报错了,报错的情况下,可能是由于权限不足或文件被锁导致的。


image


我们可以通过查看控制台来确定导致问题的原因。


image


我仔细查看了一下,果然发现这个文件只有root用户才有操作权限。


image


find提权


好的,让我们来探讨一下用户权限的问题。目前我的用户权限是www,但是我想要获得root权限。这时候我们可以考虑一下suid提权的相关内容。SUID(Set User ID)是一种Linux/Unix权限设置,允许用户在执行特定程序时以该程序所有者的权限来运行。然而,SUID提权也是一种安全漏洞,黑客可能会利用它来获取未授权的权限。为了给大家演示一下,我特意将我的服务器上的find命令设置了suid提权。我们执行一下find index.php -exec whoami \;命令,如果find没有设置suid提权的话,它仍然会以www用户身份输出结果。所以,通过-exec ***这个参数,我省略了需要执行的命令,我们可以来查看一下index.php的权限所有者信息。


image


我来执行一下 find index.php -exec chown www:index.php \; 试一试看看是否可以成功,哎呦,大功告成。我再次去保存一下文件内容看看是否可以保存成功。


image


果不其然,我们的推测是正确的。保存文件失败的问题确实是由于权限问题引起的。只有当我将文件的所有者更改为当前用户时,才能顺利保存成功。


image


让我们现在来看一下进行这些保存后的效果如何。


image


总结


当然了,黑客的攻击手段有很多。除了自己做一些简单的防护措施外,如果有经济条件,建议购买正规厂商的服务器,并使用其安全版本。例如,我在使用腾讯云的服务器进行攻击时,会立即触发告警并隔离病毒文件。在最次的情况下,也要记得拔掉你的网线,以防攻击波及到其他设备。


在这篇文章中,我仅仅演示了使用浏览器URL地址参数和find提权进行安全漏洞渗透的一些示例。实际上,针对URL地址渗透问题,现在已经有很多免费的防火墙可以用来阻止此类攻击。我甚至不得不关闭我的宝塔面板的免费防火墙才能成功进入系统,否则URL渗透根本无法进行。


至于find提权,你应该在Linux服务器上移除具有提权功能的命令。这是一种非常重要的安全措施,以避免未经授权的访问。通过限制用户权限和删除一些危险命令,可以有效防止潜在的攻击。


总而言之,我们应该时刻关注系统的安全性,并采取必要的措施来保护我们的服务器免受潜在的攻击。


作者:努力的小雨
来源:juejin.cn/post/7304263961238143011
收起阅读 »

大白话DDD(DDD黑话终结者)

一、吐槽的话 相信听过DDD的人有很大一部分都不知道这玩意具体是干嘛的,甚至觉得它有那么一些虚无缥缈。原因之一是但凡讲DDD的,都是一堆特别高大上的概念,然后冠之以一堆让人看不懂的解释,。作者曾经在极客时间上买了本DDD实战的电子书,被那些概念一路从头灌到尾,...
继续阅读 »

一、吐槽的话


相信听过DDD的人有很大一部分都不知道这玩意具体是干嘛的,甚至觉得它有那么一些虚无缥缈。原因之一是但凡讲DDD的,都是一堆特别高大上的概念,然后冠之以一堆让人看不懂的解释,。作者曾经在极客时间上买了本DDD实战的电子书,被那些概念一路从头灌到尾,灌得作者头昏脑涨,一本电子书那么多文章愣是没有一点点像样的案例,看到最后也 没明白那本电子书的作者究竟想写啥。原因之二是DDD经常出现在互联网黑话中,如果不能稍微了解一下DDD中的名词,我们一般的程序员甚至都不配和那些说这些黑话的人一起共事。


为了帮助大家更好的理解这种虚无缥缈的概念,也为了更好的减少大家在新词频出的IT行业工作的痛苦,作者尝试用人话来解释下DDD,并且最后会举DDD在不同层面上使用的例子,来帮助大家彻底理解这个所谓的“高大上”的概念。


二、核心概念


核心的概念还是必须列的,否则你都不知道DDD的名词有多么恶心,但我会用让你能听懂的话来解释。


1、领域/子域/核心域/支撑域/通用域


领域

DDD中最重要的一个概念,也是黑话中说的最多的,领域指的是特定的业务问题领域,是专门用来确定业务的边界。


子域

有时候一个业务领域可能比较复杂,因此会被分为多个子域,子域分为了如下几种:



  • 核心子域:业务成功的核心竞争力。用人话来说,就是领域中最重要的子域,如果没有它其他的都不成立,比如用户服务这个领域中的用户子域

  • 通用子域:不是核心,但被整个业务系统所使用。在领域这个层面中,这里指的是通用能力,比如通用工具,通用的数据字典、枚举这类(感叹DDD简直恨不得无孔不入)。在整个业务系统这个更高层面上,也会有通用域的存在,指的通用的服务(用户服务、权限服务这类公共服务可以作为通用域)。

  • 支撑子域:不是核心,不被整个系统使用,完成业务的必要能力。


2、通用语言/限界上下文


通用语言

指的是一个领域内,同一个名词必须是同一个意思,即统一交流的术语。比如我们在搞用户中心的时候,用户统一指的就是系统用户,而不能用其他名词来表达,目的是提高沟通的效率以及增加设计的可读性


限界上下文

限界上下文指的是领域的边界,通常来说,在比较高的业务层面上,一个限界上下文之内即一个领域。这里用一张不太好看的图来解释:


image.png


3、事件风暴/头脑风暴/领域事件


事件风暴

指的是领域内的业务事件,比如用户中心中,新增用户,授权,用户修改密码等业务事件。


头脑风暴

用最俗的人话解释,就是一堆人坐在一个小会议室中开会,去梳理业务系统都有哪些业务事件。


领域事件

领域内,子域和子域之间交互的事件,如用户服务中用户和角色交互是为用户分配角色,或者是为角色批量绑定用户,这里的领域事件有两个,一个是“为用户分配角色”,另一个是“为角色批量绑定用户”。


4、实体/值对象


实体

这里可以理解为有着唯一标识符的东西,比如用户实体。


值对象

实体的具体化,比如用户实体中的张三和李四。


实体和值对象可以简单的理解成java中类和对象,只不过这里通常需要对应数据实体。


5、聚合/聚合根


聚合

实体和实体之间需要共同协作来让业务运转,比如我们的授权就是给用户分配一个角色,这里涉及到了用户和角色两个实体,这个聚合即是用户和角色的关系。


聚合根

聚合根是聚合的管理者,即一个聚合中必定是有个聚合根的,通常它也是对外的接口。比如说,在给用户分配角色这个事件中涉及两个实体分别是用户和角色,这时候用户就是聚合根。而当这个业务变成给角色批量绑定用户的时候,聚合根就变成了角色。即使没有这样一个名词,我们也会有这样一个标准,让业务按照既定规则来运行,举个上文中的例子,给用户A绑定角色1,用户为聚合根,这样往后去查看用户拥有的角色,也是以用户的唯一标识来查,即访问聚合必须通过聚合根来访问,这个也就是聚合根的作用。


三、用途及案例


目前DDD的应用主要是在战略阶段和战术阶段,这两个名词也是非常的不讲人话,所谓的战略阶段,其实就是前期去规划业务如何拆分服务,服务之间如何交互。战术阶段,就是工程上的应用,用工程化做的比较好的java语言举例子,就是把传统的三层架构变成了四层架构甚至是N层架构而已。


1、微服务的服务领域划分

这是对于DDD在战略阶段做的事情:假如目前我司有个客服系统,内部的客服人员使用这个系统对外上亿的用户提供了形形色色的服务,同时内部人员觉得我们的客服系统也非常好用,老板觉得我们的系统做的非常好,可以拿出去对外售卖以提高公司的利润,那么这时候问题就来了,客服系统需要怎样去改造,才能够支持对外售卖呢?经过激烈的讨论,大致需求如下:



  • 对外售卖的形式有两种,分别是SaaS模式和私有化部署的模式。

  • SaaS模式需要新开发较为复杂的基础设施来支持,比如租户管理,用户管理,基于用户购买的权限系统,能够根据购买情况来给予不同租户不同的权限。而私有化的时候,由于客户是打包购买,这时候权限系统就不需要再根据用户购买来判断。

  • 数据同步能力,很多公司原本已经有一套员工管理系统,通常是HR系统或者是ERP,这时候客服系统也有一套员工管理,需要把公司人员一个一个录入进去,非常麻烦,因此需要和公司原有的数据来进行同步。

  • 老板的野心还比较大,希望造出来的这套基础设施可以为公司其他业务系统赋能,能支持其他业务系统对外售卖


在经过比较细致的梳理(DDD管这个叫事件风暴/头脑风暴)之后,我们整理出了主要的业务事件,大致如下:


1、用户可以自行注册租户,也可以由运营在后台为用户开通租户,每个租户内默认有一个超级管理员,租户开通之后默认有系统一个月的试用期,试用期超级管理员即可在管理端进行用户管理,添加子用户,分配一些基本权限,同时子用户可以使用系统的一些基本功能。


2、高级的功能,比如客服中的机器人功能是属于要花钱买的,试用期不具备此权限,用户必须出钱购买。每次购买之后会生成购买订单,订单对应的商品即为高级功能包。


3、权限系统需要能够根据租户购买的功能以及用户拥有的角色来鉴权,如果是私有化,由于客户此时购买的是完整系统,所以此时权限系统仅仅根据用户角色来鉴权即可。


4、基础设施还需要对其他业务系统赋能。


根据上面的业务流程,我们梳理出了下图中的实体


image.png


最后再根据实体和实体之间的交互,划分出了用户中心服务以及计费服务,这两个服务是两个通用能力服务,然后又划分出了基于通用服务的业务层,分别是租户管理端和运营后台以及提供给业务接入的应用中心,架构图如下:


image.png


基础设施层即为我们要做的东西,为业务应用层提供通用的用户权限能力、以及售卖的能力,同时构建开发者中心、租户控制台以及运营后台三个基础设施应用。


2、工程层面

这个是对于DDD在战术设计阶段的运用,以java项目来举例子,现在的搞微服务的,都是把工程分为了主要的三层,即控制层->逻辑层->数据层,但是到了DDD这里,则是多了一层,变成了控制层->逻辑层->领域能力层->数据层。这里一层一层来解释下:


分层描述
控制层对外暴漏的接口层,举个例子,java工程的controller
逻辑层主要的业务逻辑层
领域能力层模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。
数据层操作数据,java中主要是dao层

四、总结


在解释完了各种概念以及举例子之后,我们对DDD是什么有了个大概的认知,相信也是有非常多的争议。作者搞微服务已经搞了多年,也曾经在梳理业务的时候被DDD的各种黑话毒打过,也使用过DDD搞过工程。经历了这么多这方面的实践之后觉得DDD最大的价值其实还是在梳理业务的时候划分清楚业务领域的边界,其核心思想其实还是高内聚低耦合而已。至于工程方面,现在微服务的粒度已经足够细,完全没必要再多这么一层。这多出来的这一层,多少有种没事找事的感觉。更可笑的是,这个概念本身在对外普及自己的东西的时候,玩足了文字游戏,让大家学的一头雾水。真正好的东西,是能够解决问题,并且能够很容易的让人学明白,而不是一昧的造新词去迷惑人,也希望以后互联网行业多一些实干,少说一些黑话。


作者:李少博
来源:juejin.cn/post/7184800180984610873
收起阅读 »

是时候让自己掌握一款自动化构建工具了

后端:“麻烦给我一份XXXX版本的包”; 前端:”***,XXX版本有别的版本没有的依赖包,又得切分支还得卸载无用的包,还好我搭了Jenkins“ 前端: "好了,你去XXX环境上自己拿吧!" 我们身为前端有时候也需要对项目的不同版本进行控制,这时候自动化构建...
继续阅读 »

后端:“麻烦给我一份XXXX版本的包”;

前端:”***,XXX版本有别的版本没有的依赖包,又得切分支还得卸载无用的包,还好我搭了Jenkins“

前端: "好了,你去XXX环境上自己拿吧!"


我们身为前端有时候也需要对项目的不同版本进行控制,这时候自动化构建就能解决我们工作区上对应不同版本有着不同依赖的需求,以下我们来看下怎么去搭建属于自己的自动化构建吧(jenkins)。


1、搭建前的环境准备



  1. 这边需要Linux的支持,我这边是叫运维帮我新起一个1段(带外网,方便下载运行环境)的服务器。

  2. JDK11以上的环境(注意:当前jenkins支持的Java版本最低为Java11)。

  3. 安装Maven。

  4. Git环境。




我这开始一步步带着安装,老手可以直接跳到搭建配置。


2、安装JDK11


// 注意:没有yum可以利用apt-get install yum 来安装yum

yum list java* // 查看所有的JDK版本,找到java-11-openjdk.x86_64

yum install java-11-openjdk.x86_64 // 安装JDK11

java -version // 如果安装成功,就可以查看当前版本


image.png


3、安装Maven


安装:


cd /usr/loca  // 安装目录

wget https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz // 根据需要下载对应版本

tar -vxf apache-maven-3.6.3-bin.tar.gz // 解压

mv apache-maven-3.6.3 maven // 修改文件名

修改环境变量:


vim /etc/profile  // 进到配置文件

// 按 ins键进入编辑状态,写入以下配置,按esc 输入wq保存
export MAVEN_HOME=/usr/local/maven
export PATH=${PATH}:${MAVEN_HOME}/bin

source /etc/profile // 需要重新加载/etc/profile文件以使更改生效

mvn -v // 查看Maven版本

image.png


4、安装git


yum install git // 直接装

git --version // 查看当前git版本

image.png


5、安装Jenkins


安装Jenkins镜像源


mkdir jenkins && cd jenkins // 创建Jenkins文件夹,并进入Jenkins文件夹

wget https://updates.jenkins-ci.org/latest/jenkins.war // 远程下载Jenkins的war包

nohup java -jar jenkins.war --httpPort=8088 // 执行启动命令


image.png


这时终端可能存在无法输入的情况,我们另起终端,输入下面命令查看服务是否在运行


netstat -tlnp // 查看TCP协议进程端口

这时我们发现8088端口被运行了


image.png
接着,我们去浏览器输入IP+端口。


image.png
哟,这不就成功了?我们紧接着配置。


6、配置Jenkins


我们部署Jenkins的时候,会生成一个密码文件-initialAdminPassword,不知道路径的我们一步步找


cd / && find -name 'initialAdminPassword' // 进入/ 全举查找文件名为initialAdminPassword的文件

image.png


查到之后我们查看当前文件内容


cat ~/.jenkins/secrets/initialAdminPassword

image.png
这就是默认密码啦,我们复制粘贴到刚刚打开的Jenkins界面,回车,登录成功之后会出现以下界面


image.png


之后我们跳过自定义Jenkins,点击开始使用Jenkins,进入如下界面


image.png


紧接着,我们汉化下Jenkins操作界面,不想汉化的可以跳过此配置


点击界面的Manage Jenkins 》 Plugins 》 Available plugins 搜索chinese,之后我们按install就好了


image.png
记得在下载页面勾选重启Jenkins配置,重启完之后就汉化成功啦


image.png


接下来我们安装GitHub插件,流程跟安装汉化插件一致,我就直接输出结果了


image.png
记得勾选,不然得手动重启


image.png


趁下载的功夫,我们打开GitHub官网
settings 》 Developer settings 选择Personal Access Token --> Generate new token, 新建一个有读写权限的用户。


image.png
创建好之后复制下面密钥


image.png
接下来我们回到Jenkins配置页面配置GitHub
系统管理 => 系统设置 => Github Server 添加信息


image.png
之后添加Jenkins凭证
select选项为刚刚得到的GitHub 密钥


image.png


选择凭证,测试链接,得到以下信息


image.png
点击保存,接下来配置java环境,首先回到我们终端


echo $JAVA_HOME // 查看下我们JAVA的环境变量

如果没有不要着急,我们先进入系统环境配置文件,这里跟配置MAVEN环境变量操作一致,解释下上文为什么没配置Java环境变量却能打印。
因为我们是直接通过运行java命令,系统将使用默认的Java安装来执行该命令,并打印版本信息的。


which java 先查看java安装在哪

vi /etc/profile // 编辑环境变量文件,写入下面两行,并wq保存

export JAVA_HOME=/usr/bin/java
export PATH=$JAVA_HOME/bin:$PATH

source /etc/profile // 需要重新加载/etc/profile文件以使更改生效

image.png


这时我们再echo输出Java环境变量


image.png
然后我们拿到Jenkins上配置,点保存


image.png
之后回到首页,点新建任务,选择自由风格,点确定


image.png
之后弹出构建配置,我们往下拉,找到Build Steps,如果没弹出可以根据标签页找到对应配置


cd /test // 事先创建好文件
git clone https://github.com/LIAOJIANS/sa-ui.git // 可为你GitHub上的私人仓库,或者开放性仓库
cd sa-ui
npm install
npm run build

image.png
回到我们项目首页,然后点击立即构建


image.png
呀,好家伙你会发现红XX,这代表我们构建失败了


image.png
点击构建项目日志,查看控制台输出,好家伙原来没有node环境


image.png
老规矩,安装node环境,并添加软连接


wget https://nodejs.org/dist/v14.5.0/node-v14.5.0-linux-x64.tar.gz // 去官网找到指定版本的node

tar -zxvf node-v14.5.0-linux-x64.tar.gz -C /usr
/local/ // 解压到指定目录(/usr/local

mv node-v14.5.0-linux-x64/
nodejs // 重命名为nodejs

/
/ 把node和npm创建软链接到/usr/local/bin/目录下,系统在使用命令时,默认会到/usr/local/bin/读取命令。
ln -s /usr
/local/nodejs/bin/node /usr/local/bin/node
ln -s /usr/local/nodejs/bin/npm /usr/local/bin/npm

image.png
然后我们再换一下NPM源镜像


    npm config set registry https://registry.npmmirror.com/  // 新淘宝源地址
npm config get registry

image.png
然后我们再回到Jenkins进行构建


image.png
看到success就证明构建完成啦,现在我们就可以跟后端说,你自己去XXX服务器,XXX路径拿,如果想一键推送到后端服务器请参考, 前端黑科技篇章之scp2,让你一键打包部署服务器这篇文章,可以在Jenkins配置上传路径和命令等等。


完结撒花,感谢耐心观看的你们。


作者:大码猴
来源:juejin.cn/post/7304538199144955940
收起阅读 »

技术大佬 问我 订单消息乱序了怎么办?

技术大佬 :佩琪,最近看你闷闷不乐了,又被虐了? 佩琪:(⊙o⊙)…,又被大佬发现了。这不最近出去面试都揉捏的像一个麻花了嘛 技术大佬 :哦,这次又是遇到什么难题了? 佩琪: 由于和大佬讨论过消息不丢,消息防重等技能(见  kafka 消息...
继续阅读 »

技术大佬 :佩琪,最近看你闷闷不乐了,又被虐了?


佩琪:(⊙o⊙)…,又被大佬发现了。这不最近出去面试都揉捏的像一个麻花了嘛


技术大佬 :哦,这次又是遇到什么难题了?


佩琪: 由于和大佬讨论过消息不丢,消息防重等技能(见  kafka 消息“零丢失”的配方 和技术大佬问我 订单消息重复消费了 怎么办? ),所以在简历的技术栈里就夸大似的写了精通kafka消息中间件,然后就被面试官炮轰了里面的细节


佩琪: 其中面试官给我印象深刻的一个问题是:你们的kafka消息里会有乱序消费的情况吗?如果有,是怎么解决的了?


技术大佬 :哦,那你是怎么回答的了?


佩琪:我就是个crud boy,根本不知道啥是顺序消费啥是乱序消费,所以就回答说,没有


技术大佬 :哦,真是个诚实的孩子;然后呢?


佩琪:然后面试官就让我回家等通知了,然后就没有然后了。。。。


佩琪对了大佬,什么是消息乱序消费了?


技术大佬 :消息乱序消费,一般指我们消费者应用程序不按照,上游系统 业务发生的顺序,进行了业务消息的颠倒处理,最终导致消费业务出错。


佩琪低声咕噜了下你这说的是人话吗?大声问答:这对我的小脑袋有点抽象了,大佬能举个实际的栗子吗?


技术大佬 :举个上次我们做的促销数据同步的栗子吧,大概流程如下:


1700632936991.png


技术大佬 :上次我们做的促销业务,需要在我们的运营端后台,录入促销消息;然后利用kafka同步给三方业务。在业务流程上,是先新增促销信息,然后可能删除促销信息;但是三方消费端业务接受到的kafka消息,可能是先接受到删除促销消息;随后接受到新增促销消息;这样不就导致了消费端系统和我们系统的促销数据不一致了嘛。所以你是消费方,你就准备接锅吧,你不背锅,谁背锅了?


佩琪 :-_-||,此时佩琪心想,锅只能背一次,坑只能掉一次。赶紧问到:请问大佬,消息乱序了后,有什么解决方法吗?


技术大佬 : 此时抬了抬眼睛,清了清嗓子,面露自信的微笑回答道。一般都是使用顺序生产,顺序存储,顺序消费的思想来解决。


佩琪摸了摸头,能具体说说,顺序生产,顺序存储,顺序消费吗?


技术大佬 : 比如kafka,一般建议同一个业务属性数据,都往一个分区上发送;而kafka的一个分区只能被一个消费者实例消费,不能被多个消费者实例消费。


技术大佬 : 也就是说在生产端如果能保证 把一个业务属性的消息按顺序放入同一个分区;那么kakfa中间件的broker也是顺序存储,顺序给到消费者的。而kafka的一个分区只能被一个消费者消费;也就不存在多线程并发消费导致的顺序问题了。


技术大佬 :比如上面的同步促销消息;不就是两个消费者,拉取了不同分区上的数据,导致消息乱序处理,最终数据不一致。同一个促销数据,都往一个分区上发送,就不会存在这样的乱序问题了。


佩琪哦哦,原来是这样,我感觉这方案心理没底了,大佬能具体说说这种方案有什么优缺点吗?


技术大佬 :给你一张图,你学习下?


优点缺点
生产端实现简单:比如kafka 生产端,提供了按指定key,发送到固定分区的策略上游难保证严格顺序生产:生产端对同一类业务数据需要按照顺序放入同一个分区;这个在应用层还是比较的难保证,毕竟上游应用都是无状态多实例,多机器部署,存在并发情况下执行的先后顺序不可控
消费端实现也简单 :kafka消费者 默认就是单线程执行;不需要为了顺序消费而进行代码改造消费者处理性能会有潜在的瓶颈:消费者端单线程消费,只能扩展消费者应用实例来进行消费者处理能力的提升;在消息较多的时候,会是个处理瓶颈,毕竟干活的进程上限是topic的分区数。
无其它中间件依赖使用场景有取限制:业务数据只能指定到同一个topic,针对某些业务属性是一类数据,但发送到不同topic场景下,则不适用了。比如订单支付消息,和订单退款消息是两个topic,但是对于下游算佣业务来说都是同一个订单业务数据

佩琪大佬想偷懒了,能给一个 kafka 指定 发送到固定分区的代码吗?


技术大佬 :有的,只需要一行代码,你要不自己动手尝试下?


KafkaProducer.send(new ProducerRecord[String,String](topic,key,msg),new Callback(){} )

topic:主题,这个玩消息的都知道,不解释了

key: 这个是指定发送到固定分区的关键。一般填写订单号,或者促销ID。kafka在计算消息该发往那个分区时,会默认使用hash算法,把相同的key,发送到固定的分区上

msg: 具体消息内容


佩琪大佬,我突然记起,上次我们做的 订单算佣业务了,也是利用kafka监听订单数据变化,但是为什么没有使用固定分区方案了?


技术大佬 : 主要是我们上游业务方:把订单支付消息,和订单退款消息拆分为了两个topic,这个从使用固定分区方案的前提里就否定了,我们不能使用此方案。


佩琪哦哦,那我们是怎么去解决这个乱序的问题的了?


技术大佬 :主要是根据自身业务实际特性;使用了数据库乐观锁的思想,解决先发后至,后发先至这种数据乱序问题。


大概的流程如下图:


1700632983267.png


佩琪摸了摸头,大佬这个自身业务的特性是啥了?


技术大佬 :我们算佣业务,主要关注订单的两个状态,一个是订单支付状态,一个是订单退款状态
订单退款发生时间肯定是在订单支付后;而上游订单业务是能保证这两个业务在时间发生上的前后顺序的,即订单的支付时间,肯定是早于订单退款时间。所以主要是利用订单ID+订单更新时间戳,做为数据库佣金表的更新条件,进行数据的乱序处理。


佩琪哦哦,能详细说说 这个数据库乐观锁是怎么解决这个乱序问题吗?


技术大佬 : 比如:当佣金表里订单数据更新时间大于更新条件时间 就放弃本次更新,表明消息数据是个老数据;即查询时不加锁


技术大佬 :而小于更新条件时间的,表明是个订单新数据,进行数据更新。即在更新时 利用数据库的行锁,来保证并发更新时的情况。即真实发生修改时加锁


佩琪哦哦,明白了。原来一条带条件更新的sql,就具备了乐观锁思想


技术大佬 :我们算佣业务其实是只关注佣金的最终状态,不关注中间状态;所以能用这种方式,保证算佣数据的最终一致性,而不用太关注订单的中间状态变化,导致佣金的中间变化。


总结


要想保证消息顺序消费大概有两种方案


1700633024660.png


固定分区方案


1、生产端指定同一类业务消息,往同一个分区发送。比如指定发送key为订单号,这样同一个订单号的消息,都会发送同一个分区

2、消费端单线程进行消费


乐观锁实现方案


如果上游不能保证生产的顺序;可让上游加上数据更新时间;利用唯一ID+数据更新时间,+乐观锁思想,保证业务数据处理的最终一致性。


作者:程序员猪佩琪
来源:juejin.cn/post/7303833186068086819
收起阅读 »

kafka 消息“零丢失”的配方

如果在简历上写了使用过kafka消息中间件,面试官大概80%的概率会问你:"如何保证kafka消息不丢失?"反正我是屡试不爽。 如果你的核心业务数据,比如订单数据,或者其它核心交易业务数据,在使用kafka时,要保证消息不丢失,并让下游消费系统一定能获得订单数...
继续阅读 »

如果在简历上写了使用过kafka消息中间件,面试官大概80%的概率会问你:"如何保证kafka消息不丢失?"反正我是屡试不爽。

如果你的核心业务数据,比如订单数据,或者其它核心交易业务数据,在使用kafka时,要保证消息不丢失,并让下游消费系统一定能获得订单数据,只靠kafka中间件来保证,是并不可靠的。


kafka已经这么的优秀 了,为什么还会丢消息了?这一定是初学者或者初级使用者心中的疑惑


kafka 已经这么的优秀了,为啥还会丢消息了?----太不省心了


1698128144031.png


图一 生产者,broker,消费者


要解决kafka丢失消息的情况,需要从使用kafka涉及的主流程和主要组件进行分析。kafka的核心业务流程很简单:发送消息,暂存消息,消费消息。而这中间涉及到的主要组件,分别是生产端,broker端,消费端。


生产端丢失消息的情况和解决方法


生产端丢失消息的第一个原因主要来源于kafka的特性:批量发送异步提交。我们知道,kafka在发送消息时,是由底层的IO SEND线程进行消息的批量发送,不是由业务代码线程执行发送的。即业务代码线程执行完send方法后,就返回了。消息到底发送给broker侧没有了?通过send方法其实是无法知道的。
1698128080140.png


那么如何解决了?
kafka提供了一个带有callback回调函数的方法,如果消息成功/(失败的)发送给broker端了,底层的IO线程是可以知道的,所以此时IO线程可以回调callback函数,通知上层业务应用。我们也一般在callback函数里,根据回调函数的参数,就能知道消息是否发送成功了,如果发送失败了,那么我们还可以在callback函数里重试。一般业务场景下 通过重试的方法保证消息再次发送出去。


90%的面试者都能给出上面的标准回答。


但在一些严格的交易场景:仅仅依靠回调函数的通知和重试,是不能保证消息一定能发送到broker端的


理由如下:

1、callback函数是在jvm层面由IO SEND线程执行的,如果刚好遇到在执行回调函数时,jvm宕机了,或者恰好长时间的GC,最终导致OOM,或者jvm假死的情况;那么回调函数是不能被执行的。恰好你的消息数据,是一个带有交易属性核心业务数据,必须要通知给下游。比如下单或者支付后,需要通知佣金系统,或者积分系统,去计算订单佣金。此时一个JVM宕机或者OOM,给下游的数据就丢了,那么计算联盟客的订单佣金数据也就丢了,造成联盟客资损了。


2、IO SEND线程和broker之间是通过网络进行通信的,而网络通信并不一定都能保证一直都是顺畅的,比如网络丢包,网络中的交换机坏了,由底层网络硬件的故障,导致上层IO线程发送消息失败;此时发送端配置的重试参数 retries 也不好使了。


如何解决生产端在极端严格的交易场景下,消息丢失了?

如果要解决jvm宕机,或者JVM假死;又或者底层网络问题,带来的消息丢失;是需要上层应用额外的机制来保证消息数据发送的完整性。大概流程如下图


1698128183781.png


1、在发送消息之前,加一个发送记录,并且初始化为待发送;并且把发送记录进行存储(可以存储在DB里,或者其它存储引擎里);
2、利用带有回调函数的callback通知,在业务代码里感知到消息是否发送成功;如果消息发送成功,则把存储引擎里对应的消息标记为已发送
3、利用延迟的定时任务,每隔5分钟(可根据实际情况调整扫描频率)定时扫描5分钟前未发送或者发送失败的消息,再次进行发送。


这样即使应用的jvm宕机,或者底层网络出现故障,消息是否发送的记录,都进行了保存。通过持续的定时任务扫描和重试,能最终保证消息一定能发送出去。


broker端丢失消息的情况和解决方法


broker端接收到生产端的消息后,并成功应答生产端后,消息会丢吗? 如果broker能像mysql服务器一样,在成功应答给客户端前,能把消息写入到了磁盘进行持久化,并且在宕机断电后,有恢复机制,那么我们能说broker端不会丢消息。


1698128217696.png


但broker端提供数据不丢的保障和mysql是不一样的。broker端在接受了一批消息数据后,是不会马上写入磁盘的,而是先写入到page cache里,这个page cache是操作系统的页缓存(也就是另外一个内存,只是由操作系统管理,不属于JVM管理的内存),通过定时或者定量的的方式(
log.flush.interval.messages和log.flush.interval.ms)会把page cache里的数据写入到磁盘里。


如果page cache在持久化到磁盘前,broker进程宕机了,这个时候不会丢失消息,重启broker即可;如果此时操作系统宕机或者物理机宕机了,page cache里的数据还没有持久化到磁盘里,此种情况数据就丢了。


kafka应对此种情况,建议是通过多副本机制来解决的,核心思想也挺简单的:如果数据保存在一台机器上你觉得可靠性不够,那么我就把相同的数据保存到多台机器上,某台机器宕机了可以由其它机器提供相同的服务和数据。


要想达到上面效果,有三个关键参数需要配置

第一:生产端参数 ack 设置为all

代表消息需要写入到“大多数”的副本分区后,leader broker才给生产端应答消息写入成功。(即写入了“大多数”机器的page cache里)


第二:在broker端 配置 min.insync.replicas参数设置至少为2

此参数代表了 上面的“大多数”副本。为2表示除了写入leader分区外,还需要写入到一个follower 分区副本里,broker端才会应答给生产端消息写入成功。此参数设置需要搭配第一个参数使用。


第三:在broker端配置 replicator.factor参数至少3

此参数表示:topic每个分区的副本数。如果配置为2,表示每个分区只有2个副本,在加上第二个参数消息写入时至少写入2个分区副本,则整个写入逻辑就表示集群中topic的分区副本不能有一个宕机。如果配置为3,则topic的每个分区副本数为3,再加上第二个参数min.insync.replicas为2,即每次,只需要写入2个分区副本即可,另外一个宕机也不影响,在保证了消息不丢的情况下,也能提高分区的可用性;只是有点费空间,毕竟多保存了一份相同的数据到另外一台机器上。


另外在broker端,还有个参数unclean.leader.election.enable

此参数表示:没有和leader分区保持数据同步的副本分区是否也能参与leader分区的选举,建议设置为false,不允许。如果允许,这这些落后的副本分区竞选为leader分区后,则之前leader分区已保存的最新数据就有丢失的风险。注意在0.11版本之前默认为TRUE。


消费端侧丢失消息的情况和解决方法


消费端丢失消息的情况:消费端丢失消息的情况,主要是设置了 autoCommit为true,即消费者消费消息的位移,由消费者自动提交。

自动提交,表面上看起来挺高大上的,但这是消费端丢失消息的主要原因。
实例代码如下


while(true){
consumer.poll(); #①拉取消息
XXX #②进行业务处理;
}

如果在第一步拉取消息后,即提交了消息位移;而在第二步处理消息的时候发生了业务异常,或者jvm宕机了。则第二次在从消费端poll消息时,会从最新的位移拉取后面的消息,这样就造成了消息的丢失。


消费端解决消息丢失也不复杂,设置autoCommit为false;然后在消费完消息后手工提交位移即可
实例代码如下:


while(true){
consumer.poll(); #①拉取消息
XXX #②处理消息;
consumer.commit();
}

在第二步进行了业务处理后,在提交消费的消息位移;这样即使第二步或者第三步提交位移失败了又或者宕机了,第二次再从poll拉取消息时,则会以第一次拉取消息的位移处获取后面的消息,以此保证了消息的不丢失。


总结


在生产端所在的jvm运行正常,底层网络通顺的情况下,通过kafka 生产端自身的retries机制和call back回调能减少一部分消息丢失情况;但并不能保证在应用层,网络层有问题时,也能100%确保消息不丢失;如果要解决此问题,可以试试 记录消息发送状态+定时任务扫描+重试的机制。


在broker端,要保证消息数据不丢失;kafka提供了多副本机制来进行保证。关键核心参数三个,一个生产端ack=all,两个broker端参数min.insync.replicas 写入数据到分区最小副本数为2,并且每个分区的副本集最小为3


在消费端,要保证消息不丢失,需要设置消费端参数 autoCommit为false,并且在消息消费完后,再手工提交消息位置


无论是生产端重复发送消息,还是消费端手工提交消费位移,都会可能会遇到消息重复消费的问题,但这是另外一个消息防重复消费的话题,咋们下期在聊。


原创不易,请 点赞,留言,关注,转载 4暴击^^


参考资料:


kafka.apache.org/20/document… kafka2.0 官方文档


kafka.apache.org/documentati… kafka 0.10.2官方文档


kafka.apache.org/documentati… kafka 3.4.x官方文档


作者:程序员猪佩琪
来源:juejin.cn/post/7293289855076565032
收起阅读 »

Java代码是如何被CPU狂飙起来的?

无论是刚刚入门Java的新手还是已经工作了的老司机,恐怕都不容易把Java代码如何一步步被CPU执行起来这个问题完全讲清楚。但是对于一个Java程序员来说写了那么久的代码,我们总要搞清楚自己写的Java代码到底是怎么运行起来的。另外在求职面试的时候这个问题也常...
继续阅读 »

无论是刚刚入门Java的新手还是已经工作了的老司机,恐怕都不容易把Java代码如何一步步被CPU执行起来这个问题完全讲清楚。但是对于一个Java程序员来说写了那么久的代码,我们总要搞清楚自己写的Java代码到底是怎么运行起来的。另外在求职面试的时候这个问题也常常会聊到,面试官主要想通过它考察求职同学对于Java以及计算机基础技术体系的理解程度,看似简单的问题实际上囊括了JVM运行原理、操作系统以及CPU运行原理等多方面的技术知识点。我们一起来看看Java代码到底是怎么被运行起来的。


Java如何实现跨平台


在介绍Java如何一步步被执行起来之前,我们需要先弄明白为什么Java可以实现跨平台运行,因为搞清楚了这个问题之后,对于我们理解Java程序如何被CPU执行起来非常有帮助。


为什么需要JVM


write once run anywhere曾经是Java响彻编程语言圈的slogan,也就是所谓的程序员开发完java应用程序后,可以在不需要做任何调整的情况下,无差别的在任何支持Java的平台上运行,并获得相同的运行结果从而实现跨平台运行,那么Java到底是如何做到这一点的呢?


其实对于大多数的编程语言来说,都需要将程序转换为机器语言才能最终被CPU执行起来。因为无论是如Java这种高级语言还是像汇编这种低级语言实际上都是给人看的,但是计算机无法直接进行识别运行。因此想要CPU执行程序就必须要进行语言转换,将程序语言转化为CPU可以识别的机器语言。


image.png


学过计算机组成原理的同学肯定都知道,CPU内部都是用大规模晶体管组合而成的,而晶体管只有高电位以及低电位两种状态,正好对应二进制的0和1,因此机器码实际就是由0和1组成的二进制编码集合,它可以被CPU直接识别和执行。


image.png


但是像X86架构或者ARM架构,不同类型的平台对应的机器语言是不一样的,这里的机器语言指的是用二进制表示的计算机可以直接识别和执行的指令集集合。不同平台使用的CPU不同,那么对应的指令集也就有所差异,比如说X86使用的是CISC复杂指令集而ARM使用的是RISC精简指令集。所以Java要想实现跨平台运行就必须要屏蔽不同架构下的计算机底层细节差异。因此,如何解决不同平台下机器语言的适配问题是Java实现一次编写,到处运行的关键所在。


那么Java到底是如何解决这个问题的呢?怎么才能让CPU可以看懂程序员写的Java代码呢?其实这就像在我们的日常生活中,如果双方语言不通,要想进行交流的话就必须中间得有一个翻译,这样通过翻译的语言转换就可以实现双方畅通无阻的交流了。打个比方,一个中国厨师要教法国厨师和阿拉伯厨师做菜,中国厨师不懂法语和阿拉伯语,法国厨师和阿拉伯厨师不懂中文,要想顺利把菜做好就需要有翻译来帮忙。中国厨师把做菜的菜谱告诉翻译者,翻译者将中文菜谱转换为法文菜谱以及阿拉伯语菜谱,这样法国厨师和阿拉伯厨师就知道怎么做菜了。


image.png


因此Java的设计者借助了这样的思想,通过JVM(Java Virtual Machine,Java虚拟机)这个中间翻译来实现语言转换。程序员编写以.java为结尾的程序之后通过javac编译器把.java为结尾的程序文件编译成.class结尾的字节码文件,这个字节码文件需要JVM这个中间翻译进行识别解析,它由一组如下图这样的16进制数组成。JVM将字节码文件转化为汇编语言后再由硬件解析为机器语言最终最终交给CPU执行。


640.png


所以说通过JVM实现了计算机底层细节的屏蔽,因此windows平台有windows平台的JVM,Linux平台有Linux平台的JVM,这样在不同平台上存在对应的JVM充当中间翻译的作用。因此只要编译一次,不同平台的JVM都可以将对应的字节码文件进行解析后运行,从而实现在不同平台下运行的效果。


image.png


那么问题又来了,JVM是怎么解析运行.class文件的呢?要想搞清楚这个问题,我们得先看看JVM的内存结构到底是怎样的,了解JVM结构之后这个问题就迎刃而解了。


JVM结构


JVM(Java Virtual Machine)即Java虚拟机,它的核心作用主要有两个,一个是运行Java应用程序,另一个是管理Java应用程序的内存。它主要由三部分组成,类加载器、运行时数据区以及字节码执行引擎。


image.png


类加载器


类加载器负责将字节码文件加载到内存中,主要经历加载-》连接-》实例化三个阶段完成类加载操作。


image.png


另外需要注意的是.class并不是一次性全部加载到内存中,而是在Java应用程序需要的时候才会加载。也就是说当JVM请求一个类进行加载的时候,类加载器就会尝试查找定位这个类,当查找对应的类之后将他的完全限定类定义加载到运行时数据区中。


运行时数据区


JVM定义了在Java程序运行期间需要使用到的内存区域,简单来说这块内存区域存放了字节码信息以及程序执行过程数据。运行时数据区主要划分了堆、程序计数器虚拟机栈、本地方法栈以及元空间数据区。其中堆数据区域在JVM启动后便会进行分配,而虚拟机栈、程序计数器本地方法栈都是在常见线程后进行分配。


image.png


不过需要说明的是在JDK 1.8及以后的版本中,方法区被移除了,取而代之的是元空间(Metaspace)。元空间与方法区的作用相似,都是存储类的结构信息,包括类的定义、方法的定义、字段的定义以及字节码指令。不同的是,元空间不再是JVM内存的一部分,而是通过本地内存(Native Memory)来实现的。在JVM启动时,元空间的大小由MaxMetaspaceSize参数指定,JVM在运行时会自动调整元空间的大小,以适应不同的程序需求。


字节码执行引擎


字节码执行引擎最核心的作用就是将字节码文件解释为可执行程序,主要包含了解释器、即使编译以及垃圾回收器。字节码执行引擎从元空间获取字节码指令进行执行。当Java程序调用一个方法时,JVM会根据方法的描述符和方法所在的类在元空间中查找对应的字节码指令。字节码执行引擎从元空间获取字节码指令,然后执行这些指令。


JVM如何运行Java程序


在搞清楚了JVM的结构之后,接下来我们一起来看看天天写的Java代码是如何被CPU飙起来的。一般公司的研发流程都是产品经理提需求然后程序员来实现。所以当产品经理把需求提过来之后,程序员就需要分析需求进行设计然后编码实现,比如我们通过Idea来完成编码工作,这个时候工程中就会有一堆的以.java结尾的Java代码文件,实际上就是程序员将产品需求转化为对应的Java程序。但是这个.java结尾的Java代码文件是给程序员看的,计算机无法识别,所以需要进行转换,转换为计算机可以识别的机器语言。


image.png


通过上文我们知道,Java为了实现write once,run anywhere的宏伟目标设计了JVM来充当转换翻译的工作。因此我们编写好的.java文件需要通过javac编译成.class文件,这个class文件就是传说中的字节码文件,而字节码文件就是JVM的输入。


image.png


当我们有了.class文件也就是字节码文件之后,就需要启动一个JVM实例来进一步加载解析.class字节码。实际上JVM本质其实就是操作系统中的一个进程,因此要想通过JVM加载解析.class文件,必须先启动一个JVM进程。JVM进程启动之后通过类加载器加载.class文件,将字节码加载到JVM对应的内存空间。


image.png


当.class文件对应的字节码信息被加载到中之后,操作系统会调度CPU资源来按照对应的指令执行java程序。


image.png


以上是CPU执行Java代码的大致步骤,看到这里我相信很多同学都有疑问这个执行步骤也太大致了吧。哈哈,别着急,有了基本的解析流程之后我们再对其中的细节进行分析,首先我们就需要弄清楚JVM是如何加载编译后的.class文件的。


字节码文件结构


要想搞清楚JVM如何加载解析字节码文件,我们就先得弄明白字节码文件的格式,因为任何文件的解析都是根据该文件的格式来进行。就像CPU有自己的指令集一样,JVM也有自己一套指令集也就是Java字节码,从根上来说Java字节码是机器语言的.class文件表现形式。字节码文件结构是一组以 8 位为最小单元的十六进制数据流,具体的结构如下图所示,主要包含了魔数、class文件版本、常量池、访问标志、索引、字段表集合、方法表集合以及属性表集合描述数据信息。


image.png


这里简单说明下各个部分的作用,后面会有专门的文章再详细进行阐述。


魔数与文件版本


魔数的作用就是告诉JVM自己是一个字节码文件,你JVM快来加载我吧,对于Java字节码文件来说,其魔数为0xCAFEBABE,现在知道为什么Java的标志是咖啡了吧。而紧随魔数之后的两个字节是文件版本号,Java的版本号通常是以52.0的形式表示,其中高16位表示主版本号,低16位表示次版本号。。


常量池


在常量池中说明常量个数以及具体的常量信息,常量池中主要存放了字面量以及符号引用这两类常量数据,所谓字面量就是代码中声明为final的常量值,而符号引用主要为类和接口的完全限定名、字段的名称和描述符以及方法的名称以及描述符。这些信息在加载到JVM之后在运行期间将符号引用转化为直接引用才能被真正使用。常量池的第一个元素是常量池大小,占据两个字节。常量池表的索引从1开始,而不是从0开始,这是因为常量池的第0个位置是用于特殊用途的。


访问标志


类或者接口的访问标记,说明类是public还是abstract,用于描述该类的访问级别和属性。访问标志的取值范围是一个16位的二进制数。


索引


包含了类索引、父类索引、接口索引数据,主要说明类的继承关系。


字段表集合


主要是类级变量而不是方法内部的局部变量。


方法表集合


主要用来描述类中有几个方法,每个方法的具体信息,包含了方法访问标识、方法名称索引、方法描述符索引、属性计数器、属性表等信息,总之就是描述方法的基础信息。


属性表集合


方法表集合之后是属性表集合,用于描述该类的所有属性。属性表集合包含了所有该类的属性的描述信息,包括属性名称、属性类型、属性值等等。


解析字节码文件


知道了字节码文件的结构之后,JVM就需要对字节码文件进行解析,将字节码结构解析为JVM内部流转的数据结构。大致的过程如下:


1、读取字节码文件


JVM首先需要读取字节码文件的二进制数据,这通常是通过文件输入流来完成的。


2、解析字节码


JVM解析字节码的过程是将字节码文件中的二进制数据解析为Java虚拟机中的数据结构。首先JVM首先会读取字节码文件的前四个字节,判断魔数是否为0xCAFEBABE,以此来确认该文件是否是一个有效的Java字节码文件。JVM接着会解析常量池表,将其中的常量转换为Java虚拟机中的数据结构,例如将字符串常量转换为Java字符串对象。解析类、接口、字段、方法等信息:JVM会依次解析类索引、父类索引、接口索引集合、字段表集合、方法表集合等信息,将这些信息转换为Java虚拟机中的数据结构。最后,JVM将解析得到的数据结构组装成一个Java类的结构,并将其放入元空间中。


在完成字节码文件解析之后,接下来就需要类加载器闪亮登场了,类加载器会将类文件加载到JVM内存中,并为该类生成一个Class对象。


类加载


加载器启动


我们都知道,Java应用的类都是通过类加载器加载到运行时数据区的,这里很多同学可能会有疑问,那么类加载器本身又是被谁加载的呢?这有点像先有鸡还是先有蛋的灵魂拷问。实际上类加载器启动大致会经历如下几个阶段:


image.png


1、以linux系统为例,当我们通过"java"启动一个Java应用的时候,其实就是启动了一个JVM进程实例,此时操作系统会为这个JVM进程实例分配CPU、内存等系统资源;


2、"java"可执行文件此时就会解析相关的启动参数,主要包括了查找jre路径、各种包的路径以及虚拟机参数等,进而获取定位libjvm.so位置,通过libjvm.so来启动JVM进程实例;


3、当JVM启动后会创建引导类加载器Bootsrap ClassLoader,这个ClassLoader是C++语言实现的,它是最基础的类加载器,没有父类加载器。通过它加载Java应用运行时所需要的基础类,主要包括JAVA_HOME/jre/lib下的rt.jar等基础jar包;


4、而在rt.jar中包含了Launcher类,当Launcher类被加载之后,就会触发创建Launcher静态实例对象,而Launcher类的构造函数中,完成了对于ExtClassLoader及AppClassLoader的创建。Launcher类的部分代码如下所示:


public class Launcher {
private static URLStreamHandlerFactory factory = new Factory();
//类静态实例
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;

public static Launcher getLauncher() {
return launcher;
}
//Launcher构造器
public Launcher() {
ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}

try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}

Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}

if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}

System.setSecurityManager(var3);
}

}
...
}

双亲委派模型


为了保证Java程序的安全性和稳定性,JVM设计了双亲委派模型类加载机制。在双亲委派模型中,启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)以及应用程序类加载器(Application ClassLoader)按照一个父子关系形成了一个层次结构,其中启动类加载器位于最顶层,应用程序类加载器位于最底层。当一个类加载器需要加载一个类时,它首先会委派给它的父类加载器去尝试加载这个类。如果父类加载器能够成功加载这个类,那么就直接返回这个类的Class对象,如果父类加载器无法加载这个类,那么就会交给子类加载器去尝试加载这个类。这个过程会一直持续到顶层的启动类加载器。


image.png


通过这种双亲委派模型,可以保证同一个类在不同的类加载器中只会被加载一次,从而避免了类的重复加载,也保证了类的唯一性。同时,由于每个类加载器只会加载自己所负责的类,因此可以防止恶意代码的注入和类的篡改,提高了Java程序的安全性。


数据流转过程


当类加载器完成字节码数据加载任务之后,JVM划分了专门的内存区域内承载这些字节码数据以及运行时中间数据。其中程序计数器、虚拟机栈以及本地方法栈属于线程私有的,堆以及元数据区属于共享数据区,不同的线程共享这两部分内存数据。我们还是以下面这段代码来说明程序运行的时候,各部分数据在Runtime data area中是如何流转的。


public class Test {
public static void main(String[] args) {
User user = new User();
Integer result = calculate(user.getAge());
System.out.println(result);
}

private static Integer calculate(Integer age) {
Integer data = age + 3;
return data;
}

}

以上代码对应的字节码指令如下所示:


image.png


如上代码所示,JVM创建线程来承载代码的执行过程,我们可以将线程理解为一个按照一定顺序执行的控制流。当线程创建之后,同时创建该线程独享的程序计数器(Program Counter Register)以及Java虚拟机栈(Java Virtual Machine Stack)。如果当前虚拟机中的线程执行的是Java方法,那么此时程序计数器中起初存储的是方法的第一条指令,当方法开始执行之后,PC寄存器存储的是下一个字节码指令的地址。但是如果当前虚拟机中的线程执行的是naive方法,那么程序计数器中的值为undefined。


那么程序计数器中的值又是怎么被改变的呢?如果是正常进行代码执行,那么当线程执行字节码指令时,程序计数器会进行自动加1指向下一条字节码指令地址。但是如果遇到判断分支、循环以及异常等不同的控制转移语句,程序计数器会被置为目标字节码指令的地址。另外在多线程切换的时候,虚拟机会记录当前线程的程序计数器,当线程切换回来的时候会根据此前记录的值恢复到程序计数器中,来继续执行线程的后续的字节码指令。


除了程序计数器之外,字节码指令的执行流转还需要虚拟机栈的参与。我们先来看下虚拟机栈的大致结构,如下图所示,栈大家肯定都知道,它是一个先入后出的数据结构,非常适合配合方法的执行过程。虚拟机栈操作的基本元素就是栈帧,栈帧的结构主要包含了局部变量、操作数栈、动态连接以及方法返回地址这几个部分。


image.png


局部变量


主要存放了栈帧对应方法的参数以及方法中定义的局部变量,实际上它是一个以0为起始索引的数组结构,可以通过索引来访问局部变量表中的元素,还包括了基本类型以及对象引用等。非静态方法中,第0个槽位默认是用于存储this指针,而其他参数和变量则会从第1个槽位开始存储。在静态方法中,第0个槽位可以用来存放方法的参数或者其他的数据。


操作数栈


和虚拟机栈一样操作数栈也是一个栈数据结构,只不过两者存储的对象不一样。操作数栈主要存储了方法内部操作数的值以及计算结果,操作数栈会将运算的参与方以及计算结果都压入操作数栈中,后续的指令操作就可以从操作数栈中使用这些值来进行计算。当方法有返回值的时候,返回值也会被压入操作数栈中,这样方法调用者可以获取到返回值。


动态链接


一个类中的方法可能会被程序中的其他多个类所共享使用,因此在编译期间实际无法确定方法的实际位置到底在哪里,因此需要在运行时动态链接来确定方法对应的地址。动态链接是通过在栈帧中维护一张方法调用的符号表来实现的。这张符号表中保存了当前方法中所有调用的方法的符号引用,包括方法名、参数类型和返回值类型等信息。当方法需要调用另一个方法时,它会在符号表中查找所需方法的符号引用,然后进行动态链接,确定方法的具体内存地址。这样,就能够正确地调用所需的方法。


方法返回地址:


当一个方法执行完毕后,JVM会将记录的方法返回地址数据置入程序计数器中,这样字节码执行引擎可以根据程序计数器中的地址继续向后执行字节码指令。同时JVM会将方法返回值压入调用方的操作栈中以便于后续的指令计算,操作完成之后从虚拟机栈中奖栈帧进行弹出。


知道了虚拟机栈的结构之后,我们来看下方法执行的流转过程是怎样的。


1、JVM启动完成.class文件加载之后,它会创建一个名为"main"的线程,并且该线程会自动调用定义在该类中的名为"main"的静态方法,这也是Java程序的入口点;


2、当JVM在主线程中调用当方法的时候就会创建当前线程独享的程序计数器以及虚拟机栈,在Test.class类中,开始执行mian方法 ,因此JVM会虚拟机栈中压入main方法对应的栈帧;


image.png


3、在栈帧的操作数栈中存储了操作的数据,JVM执行字节码指令的时候从操作数栈中获取数据,执行计算操作之后再将结果压入操作数栈;


4、当进行calculate方法调用的时候,虚拟机栈继续压入calculate方法对应的栈帧,被调用方法的参数、局部变量和操作数栈等信息会存储在新创建的栈帧中。其中该栈帧中的方法返回地址中存放了main方法执行的地址信息,方便在调用方法执行完成后继续恢复调用前的代码执行;


image.png


5、对于age + 3一条加法指令,在执行该指令之前,JVM会将操作数栈顶部的两个元素弹出,并将它们相加,然后将结果推入操作数栈中。在这个例子中,指令的操作码是“add”,它表示执行加法操作;操作数是0,它表示从操作数栈的顶部获取第一个操作数;操作数是1,它表示从操作数栈的次顶部获取第二个操作数;


6、程序计数器中存储了下一条需要执行操作的字节码指令的地址,因此Java线程执行业务逻辑的时候必须借助于程序计数器才能获得下一步命令的地址;


7、当calculate方法执行完成之后,对应的栈帧将从虚拟机栈中弹出,其中方法执行的结果会被压入main方法对应的栈帧中的操作数栈中,而方法返回地址被重置到main现场对应的程序计数器中,以便于后续字节码执行引擎从程序计数器中获取下一条命令的地址。如果方法没有返回值,JVM仍然会将一个null值推送到调用该方法的栈帧的操作数栈中,作为占位符,以便恢复调用方的操作数栈状态。


8、字节码执行引擎中的解释器会从程序计数器中获取下一个字节码指令的地址,也就是从元空间中获取对应的字节码指令,在获取到指令之后,通过翻译器翻译为对应的汇编语言而再交给硬件解析为机器指令,最终由CPU进行执行,而后再将执行结果进行写回。


CPU执行程序
通过上文我们知道无论什么编程语言最终都需要转化为机器语言才能被CPU执行,但是CPU、内存这些硬件资源并不是直接可以和应用程序打交道,而是通过操作系统来进行统一管理的。对于CPU来说,操作系统通过调度器(Scheduler)来决定哪些进程可以被CPU执行,并为它们分配时间片。它会从就绪队列中选择一个进程并将其分配给CPU执行。当一个进程的时间片用完或者发生了I/O等事件时,CPU会被释放,操作系统的调度器会重新选择一个进程并将其分配给CPU执行。也就是说操作系统通过进程调度算法来管理CPU的分配以及调度,进程调度算法的目的就是为了最大化CPU使用率,避免出现任务分配不均空闲等待的情况。主要的进程调度算法包括了FCFS、SJF、RR、MLFQ等。


CPU如何执行指令?
前文中我们大致搞清楚了类是如何被加载的,各部分类字节码数据在运行时数据区怎么流转以及字节码执行引擎翻译字节码。实际上在运行时数据区数据流转的过程中,CPU已经参与其中了。程序的本质是为了根据输入获得相应的输出,而CPU本质就是根据程序的指令一步步执行获得结果的工具。对于CPU来说,它核心工作主要分为如下三个步骤;


1、获取指令


CPU从PC寄存器中获取对应的指令地址,此处的指令地址是将要执行指令的地址,根据指令地址获取对应的操作指令到指令寄存中,此时如果是顺存执行则PC寄存器地址会自动加1,但是如果程序涉及到条件、循环等分支执行逻辑,那么PC寄存器的地址就会被修改为下一条指令执行的地址。


2、指令译码


将获取到的指令进行翻译,搞清楚哪些是操作码哪些是操作数。CPU首先读取指令中的操作码然后根据操作码来确定该指令的类型以及需要进行的操作,CPU接着根据操作码来确定指令所需的寄存器和内存地址,并将它们提取出来。


3、执行指令


经过指令译码之后,CPU根据获取到的指令进行具体的执行操作,并将指令运算的结果存储回内存或者寄存器中。


image.png


因此一旦CPU上电之后,它就像一个勤劳的小蜜蜂一样,一直不断重复着获取指令-》指令译码-》执行指令的循环操作。


CPU如何响应中断?


当操作系统需要执行某些操作时,它会发送一个中断请求给CPU。CPU在接收到中断请求后,会停止当前的任务,并转而执行中断处理程序,这个处理程序是由操作系统提供的。中断处理程序会根据中断类型,执行相应的操作,并返回到原来的任务继续执行。


在执行完中断处理程序后,CPU会将之前保存的程序现场信息恢复,然后继续执行被中断的程序。这个过程叫做中断返回(Interrupt Return,IRET)。在中断返回过程中,CPU会将处理完的结果保存在寄存器中,然后从栈中弹出被中断的程序的现场信息,恢复之前的现场状态,最后再次执行被中断的程序,继续执行之前被中断的指令。
那么CPU又是如何响应中断的呢?主要经历了以下几个步骤:


image.png


1、保存当前程序状态


CPU会将当前程序的状态(如程序计数器、寄存器、标志位等)保存到内存或栈中,以便在中断处理程序执行完毕后恢复现场。


2、确定中断类型


CPU会检查中断信号的类型,以确定需要执行哪个中断处理程序。


3、转移控制权


CPU会将程序的控制权转移到中断处理程序的入口地址,开始执行中断处理程序。


4、执行中断处理程序


中断处理程序会根据中断类型执行相应的操作,这些操作可能包括保存现场信息、读取中断事件的相关数据、执行特定的操作,以及返回到原来的程序继续执行等。


5、恢复现场


中断处理程序执行完毕后,CPU会从保存的现场信息中恢复原来程序的状态,然后将控制权返回到原来的程序中,继续执行被中断的指令。


后记


很多时候看似理所当然的问题,当我们深究下去就会发现原来别有一番天地。正如阿里王坚博士说的那样,要想看一个人对某个领域的知识掌握的情况,那就看他能就这个领域的知识能讲多长时间。想想的确如此,如果我们能够对某个知识点高度提炼同时又可以细节满满的进行展开阐述,那我们对于这个领域的理解程度就会鞭辟入里。这种检验自己知识学习深度的方式也推荐给大家。


作者:慕枫技术笔记
来源:juejin.cn/post/7207769757570482234
收起阅读 »

数据库优化之:like %xxx%该如何优化?

今天给大家分享一个小知识,实际项目中,like %xxx%的情况其实挺多的,比如某个表单如果支持根据公司名进行搜索,用户一般都是输入湖南xxx有限公司中的xxx进行搜索,所以对于接口而言,就必须使用like %xxx%来支持,从而不符合最左前缀原则导致索引失效...
继续阅读 »

今天给大家分享一个小知识,实际项目中,like %xxx%的情况其实挺多的,比如某个表单如果支持根据公司名进行搜索,用户一般都是输入湖南xxx有限公司中的xxx进行搜索,所以对于接口而言,就必须使用like %xxx%来支持,从而不符合最左前缀原则导致索引失效,那么该如何优化这种情况呢?


第一种可以尝试的方案就是利用索引条件下推,我先演示再讲原理,比如我有下面一张订单表:


就算给company_name创建一个索引,执行where company_name like '%腾讯%'也不会走索引。


但是如果给created_at, company_name创建一个联合索引,那么执行where created_at=CURDATE() and company_name like '%腾讯%'就会走联合索引,并且company_name like '%腾讯%'就会利用到索引条件下推机制,比如下图中Extra里的Using index condition就表示利用了索引条件下推。


所以,并不是like %xxx%就一定会导致索引失效,原理也可以配合其他字段一起来建联合索引,从而使用到索引条件下推机制。


再来简单分析一下索引条件下推的原理,在执行查询时先利用SQL中所提供的created_at条件在联合索引B+树中进行快速查找,匹配到所有符合created_at条件的B+树叶子节点后,再根据company_name条件进行过滤,然后再根据过滤之后的结果中的主键ID进行回表找到其他字段(回表),最终才返回结果,这样处理的好处是能够减少回表的次数,从而提高查询效率。


当然,如果实在不能建立或不方便建立联合索引,导致不能利用索引条件下推机制,那么其实可以先试试Mysql中的全文索引,最后才考虑引入ES等中间件,当然Mysql其他一些常规优化机制也是可以先考虑的,比如分页、索引覆盖(不select *)等。


作者:爱读源码的大都督
来源:juejin.cn/post/7301955975337738279
收起阅读 »

像mysql一样查询ES,一看就会,爽歪歪

ElasticSearch是现在最流行的搜索引擎了,查询快,性能好。可能唯一的缺点就是查询的语法Query DSL(Domain Specific Language)比较难记,今天分享一个直接用sql查询ES的方法。 ::: 1.简介 先简单介绍一下这个s...
继续阅读 »

ElasticSearch是现在最流行的搜索引擎了,查询快,性能好。可能唯一的缺点就是查询的语法Query DSL(Domain Specific Language)比较难记,今天分享一个直接用sql查询ES的方法。 :::




1.简介


先简单介绍一下这个sql查询,因为社区一直反馈这个Query DSL 实在是太难用了。大家可以感受一下下面这个es的查询。


GET /my_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title""search" } },
        {
          "bool": {
            "should": [
              { "term": { "category""books" } },
              { "term": { "category""music" } }
            ]
          }
        }
      ],
      "filter": {
        "range": {
          "price": { "gte": 20, "lte": 100 }
        }
      }
    }
  },
  "aggs": {
    "avg_price_per_category": {
      "terms": {
        "field""category",
        "size": 10
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field""price"
          }
        }
      }
    }
  }
}

这个查询使用了bool查询来组合多个条件,包括must、should和filter。同时也包含了聚合(aggs)来计算不同类别的平均价格。对于业务查询来讲,这个查询很普通。但是还是很难理解,特别是对于新手来讲,更难记了,很容易出错。


如果是mysql的查询,就是这么写


SELECT title, category, price 
FROM my_index 
WHERE (title = 'search' AND (category = 'books' OR category = 'music')) 
AND price >= 20 AND price <= 100 
GR0UP BY category 
ORDER BY AVG(price) DESC 
LIMIT 10

mysql 的查询就很简洁明了,看起来更舒服,后续维护也更方便。


既然都是查询,为啥不兼容一下mysql的语法呢,像很多工具现在都是兼容mysql的语法,比如说hive,starrocks,flink等等,原因就是因为mysql的用户多,社区活跃。还有一个原因就是因为mysql的语法比较简单,容易理解。所以ElasticSearch 官方ElasticSearch 从 6.3.0 版本也开始支持 SQL 查询了,这就是一个喜大奔普的事情了,哈哈。



下面是官方的文档和介绍,大家可以看看 http://www.elastic.co/guide/en/el…


2.准备环境


大家在ES官网下载一下ES 启动就可以了,注意的是ES 需要JDK环境,然后就是需要在6.3.0以上的版本。 http://www.elastic.co/cn/download…



建议也下载一下kibana



我这边下载的是7.15.2版本


3.搞起


创建一个索引 my_index


PUT /my_index
{
  "mappings": {
    "properties": {
      "title": { "type""text" },
      "category": { "type""keyword" },
      "price": { "type""float" }
    }
  }
}

插入一些数据


POST /my_index/_doc/1
{
  "title""ES学习手册",
  "category""books",
  "price": 29.99
}

POST /my_index/_doc/2
{
  "title""on my way",
  "category""music",
  "price": 13.57
}

POST /my_index/_doc/3
{
  "title""Kibana中文笔记",
  "category""books",
  "price": 21.54
}

传统的查询所有


GET /my_index/_search
{
  
}

返回的是文档的格式


如果用sql 查询


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index"
}

返回的是类似数据库的表格形式,是不是写起来更舒服呢。



  1. 分页limit


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index limit 1"
}


和mysql 一样没啥,很简单。



  1. order by 排序


POST /_sql?format=txt
{
  "query""SELECT * FROM my_index order by price desc"
}



  1. gr0up by 分组


POST /_sql?format=txt
{
  "query""SELECT category,count(1) FROM my_index group by category"
}



  1. SUM 求和


POST /_sql?format=txt
{
  "query""SELECT sum(price) FROM my_index"
}



  1. where


POST /_sql?format=txt
{
  "query": "SELECT * FROM my_index where price = '13.57'"
}


看看是不是支持时间的转换的处理,插入一些数据


POST /my_index/_doc/4
{
  "title""JAVA编程思想",
  "category""books",
  "price": 21.54,
  "create_date":"2023-11-18T12:00:00.123"
}

POST /my_index/_doc/5
{
  "title""Mysql操作手册",
  "category""books",
  "price": 21.54,
  "create_date":"2023-11-17T07:00:00.123"
}

时间转换为 yyyy-mm-dd 格式


POST /_sql?format=txt
{"query": "SELECT title, DATETIME_FORMAT(create_date, 'YYYY-MM-dd') date from my_index where category'books'" }


时间加减


POST /_sql?format=txt
{"query": "SELECT date_add('hour', 8,create_date) date from my_index where category'books'" }


字符串拆分


POST /_sql?format=txt
{
  "query""SELECT SUBSTRING(category, 1, 3) AS SubstringValue FROM my_index"
}


基本上mysql 能查的 es sql 也能查,以后查询ES 数据就很方便的,特别是对于做各种报表的查询。像这样。



一般对于这种报表,返回的数据都是差不多json数组的格式。而对于es sql,查询起来很方便


[
        {
            "data": "5",
            "axis": "总数"
        },
        {
            "data": "0",
            "axis": "待出库"
        },
        {
            "data": "0",
            "axis": "配送中"
        },
        {
            "data": "5",
            "axis": "已签收"
        },
        {
            "data": "0",
            "axis": "交易完成"
        },
        {
            "data": "0",
            "axis": "已取消"
        },
        {
            "data": "5",
            "axis": "销售"
        }

4.总结


ES SQL查询的优点还是很多的,值得学习。使用场景也很多



  1. 简单易学:ES SQL查询使用SQL语法,对于那些熟悉SQL语法的开发人员来说,学习ES SQL查询非常容易。

  2. 易于使用:ES SQL查询的语法简单,易于使用,尤其是对于那些不熟悉Query DSL语法的开发人员来说。

  3. 可读性强:ES SQL查询的语法结构清晰,易于阅读和理解。


5.最后附上相关链接


ES 官方下载

http://www.elastic.co/cn/download…


ES sql文档 http://www.elastic.co/guide/en/el…


作者:Yanyf765
来源:juejin.cn/post/7302308448581812258
收起阅读 »

如何判断一个对象是否可以被回收

在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,如果忘记回收,则会导致无用对象一直在内存里,导致内存泄露。在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收。 首先jvm需要解决的问题是:如何判断一个对象是否是垃圾,是否可以...
继续阅读 »

在c++中,当我们使用完某个对象的时候,需要显示的将对象回收,如果忘记回收,则会导致无用对象一直在内存里,导致内存泄露。在java中,jvm会帮助我们进行垃圾回收,无需程序员自己写代码进行回收。


首先jvm需要解决的问题是:如何判断一个对象是否是垃圾,是否可以被回收呢?一般都是通过引用计数法,可达性算法。


引用计数法


对每个对象的引用进行计数,每当有一个地方引用它时计数器+1、引用失效(改为引用其他对象,赋值为null,或者生命周期结束)则-1,引用的计数放到对象头中,大于0的对象被认为是存活对象,一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。


public void f(){
Object a = new Object(); // 对象a引用计数为1
g(a);
// 退出g(a),对象b的生命周期结束,对象a引用计数为1
}// 退出f(), 对象a的生命周期结束,引用计数为0

public void g(Object a){
Object b = a; // 对象a引用计数为2
Object c = a; // 对象a引用计数为3
Object d = a; // 对象a引用计数为4
d = new Object(); // 对象a引用计数为3
c = null; // 对象a引用计数为2
}

引用计数法实现起来比较容易,但是存在一个严重的问题,那就是无法检测循环依赖。如下所示:


public class A{
public B b;
public A(){

}
}

public class A{
public A a;
public B(){

}
}

A a = new A(); // a的计数为1
B b = new B(); // b的计数为1
a.b = b; // b的计数为2
b.a = a; // a的计数为2
a = null; // a的计数为1
b = null; // b的计数为1

最终a,b的计数都为1,无法被识别为垃圾,所以无法被回收。


Python使用的就是引用计数算法,Python的垃圾回收机制,很大一部分是为了处理可能产生的循环引用,是对引用计数的补充。


虽然循环引用的问题可通过Recycler算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。


可达性算法


介绍


Java最终并没有采用引用计数算法,JVM的主流垃圾回收器采取的是可达性分析算法。


我们把对象之间的引用关系用数据结构中的有向图来表示。图中的顶点表示对象。如果对象A中的变量引用了对象B,那么,我们便在对象A对应的顶点和对象B对应的顶点之间画一条有向边。


在有向图中,有一组特殊的顶点,叫做GC Roots。哪些对象可以作为GC Roots呢?



  1. 系统加载的类:rt.jar。

  2. JNI handles。

  3. 线程运行栈上所有引用,包括方法参数,创建的局部变量等。

  4. 已启动未停止的java线程。

  5. 已加载类的静态变量。

  6. 用于同步的监控,调用了对象的wait()/notify()/notifyAll()。


JVM以GC Roots为起点,遍历(深度优先遍历或广度优先遍历)整个图,可以遍历到的对象为可达对象,也叫做存活对象,遍历不到的对象为不可达对象,也叫做死亡对象。死亡对象会被虚拟机当做垃圾回收。


JVM实际上采用的是三色算法来遍历整个图的,遍历走过的路径被称为reference chain。



  • Black: 对象可达,且对象的所有引用都已经扫描了(“扫描”在可以理解成遍历过了或加入了待遍历的队列)

  • Gray: 对象可达,但对象的引用还没有扫描过(因此 Gray 对象可理解成在搜索队列里的元素)

  • White: 不可达对象或还没有扫描过的对象



引用级别


遍历到的对象一定会存活吗?事实上,JVM会根据对象A对对象B的引用强不强烈作出相应的回收措施。


基于此JVM根据引用关系的强烈,将引用关系分为四个等级:强引用,软引用,弱引用,虚幻引用。


强引用


类似Object obj = new Object() 这类的引用都属于强引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象,只有在和GC Roots断绝关系时,才会被回收。


如果要对强引用进行垃圾回收,需要设置强引用对象为 null,或者让其超出对象的生命周期范围,则认为改对象不存在引用。类似obj = null;


参考代码:


public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

软引用


用于描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。可以使用SoftReference 类来实现软引用。


Object obj = new Object();
SoftReference<Object> softRef = new SoftReference(obj);

弱引用


也是用于描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。可以使用WeakReference 类来实现弱引用。


Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);
obj = null;
System.gc();
TimeUnit.SECONDS.sleep(200);
System.out.println(weakReference.get());
System.out.println(weakReference.isEnqueued());

虚引用


它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置一个虚引用关联的唯一目的是能在这个对象被垃圾回收时收到一个系统通知。可以通过PhantomReference 来实现虚引用。


Object obj = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, refQueue);
System.out.println(phantomReference.get());
System.out.println(phantomReference.isEnqueued());

基于虚引用,有一个更加优雅的实现方式,那就是Java 9以后新加入的Cleaner,用来替代Object类的finalizer方法。


STW


虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。我们把运行应用程序的线程叫做用户线程,把执行垃圾回收的线程叫做垃圾回收线程,如果在执行垃圾回收线程的同时还在执行用户线程,那么对象的引用关系可能会在垃圾回收途中被用户线程修改,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)


误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存,导致程序出错。


为了解决漏报的问题,保证垃圾回收线程不会被用户线程打扰,最简单粗暴的方式就是在垃圾回收的过程中,暂停用户线程,直到垃圾回收结束,再恢复用户线程,这就是STW(STOP THE WORLD)。


但是如果STW的时间过程,就会严重影响程序的性能,因此优化垃圾回收过程,尽量减少STW的时间,是垃圾回收器努力优化的方向,


安全点


上述除了STW的响应时间的问题,还有另外一个问题,就是如何从一个正确的状态停止,再从这个状态正确恢复。Java虚拟机中的STW是通过安全点(safepoint)机制来实现的。当Java虚拟机收到STW请求,它便会等待所有的线程都到达安全点,才允许请求Stop-the-world的线程进行独占的工作。


当然,安全点的初始目的并不是让用户线程立刻停下,而是找到一个稳定的执行状态。在这个执行状态下,JVM的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析,才能找到完整GC Roots。


是不是所有的用户线程在垃圾回收的时候都要停止呢?实际上,JVM也做了优化,如果某个线程处于安全区(不会改变对象引用关系的一段连续的代码区间),那么这个线程不需要停止,可以和垃圾回收线程并行执行。一旦离开安全区,JVM会检查是否处于STW阶段,如果是,则需要阻塞该线程,等垃圾回收完再恢复。


作者:Shawn_Shawn
来源:juejin.cn/post/7304181581303447589
收起阅读 »

虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。

你好呀,是歪歪。 前几天,就在大家还沉浸在等待春节到来的喜悦氛围的时候,在一个核心链路上的核心系统中,我踩到一个坑的一比的坑,要不是我沉着冷静,解决思路忙中有序,处理手段雷厉风行,把它给扼杀在萌芽阶段了,那这玩意肯定得引发一个比较严重的生产问题。 从问题出现到...
继续阅读 »

你好呀,是歪歪。


前几天,就在大家还沉浸在等待春节到来的喜悦氛围的时候,在一个核心链路上的核心系统中,我踩到一个坑的一比的坑,要不是我沉着冷静,解决思路忙中有序,处理手段雷厉风行,把它给扼杀在萌芽阶段了,那这玩意肯定得引发一个比较严重的生产问题。


从问题出现到定位到这个问题的根本原因,我大概是花了两天半的时间。


所以写篇文章给大家复盘一下啊,这个案例就是一个纯技术的问题导致的,和业务的相关度其实并不大,所以你拿过去直接添油加醋,稍微改改,往自己的服务上套一下,那就是你的了。


我再说一次:虽然现在不是你的,但是你看完之后就是你的了,你明白我意思吧?



表象


事情是这样的,我这边有一个服务,你可以把这个服务粗暴的理解为是一个商城一样的服务。有商城肯定就有下单嘛。


然后接到上游服务反馈,说调用下单接口偶尔有调用超时的情况出现,断断续续的出现好几次了,给了几笔流水号,让我看一下啥情况。当时我的第一反应是不可能是我这边服务的问题,因为这个服务上次上线都至少是一个多月前的事情了,所以不可能是由于近期服务投产导致的。


但是下单接口,你听名字就知道了,核心链接上的核心功能,不能有一点麻痹大意。


每一个请求都很重要,客户下单体验不好,可能就不买了,造成交易损失。


交易上不去营业额就上不去,营业额上不去利润就上不去,利润上不去年终就上不去。


想到这一层关系之后,我立马就登陆到服务器上,开始定位问题。


一看日志,确实是我这边接口请求处理慢了,导致的调用方超时。


为什么会慢呢?



于是按照常规思路先根据日志判断了一下下单接口中调用其他服务的接口相应是否正常,从数据库获取数据的时间是否正常。


这些判断没问题之后,我转而把目光放到了 gc 上,通过监控发现那个时间点触发了一次耗时接近 1s 的 full gc,导致响应慢了。


由于我们监控只采集服务近一周的 gc 数据,所以我把时间拉长后发现 full gc 在这一周的时间内出现的频率还有点高,虽然我还没定位到问题的根本原因,但是我定位到了问题的表面原因,就是触发了 full gc。


因为是核心链路,核心流程,所以此时不应该急着去定位根本原因,而是先缓解问题。


好在我们提前准备了各种原因的应急预案,其中就包含这个场景。预案的内容就是扩大应用堆内存,延缓 full gc 的出现。


所以我当即进行操作报备并联系运维,按照紧急预案执行,把服务的堆内存由 8G 扩大一倍,提升到 16G。


虽然这个方法简单粗暴,但是既解决了当前的调用超时的问题,也给了我足够的排查问题的时间。



定位原因


当时我其实一点都不慌的,因为问题在萌芽阶段的时候我就把它给干掉了。



不就是 full gc 吗,哦,我的老朋友。


先大胆假设一波:程序里面某个逻辑不小心搞出了大对象,触发了 full gc。


所以我先是双手插兜,带着监控图和日志请求,闲庭信步的走进项目代码里面,想要凭借肉眼找出一点蛛丝马迹......



没有任何收获,因为下单服务涉及到的逻辑真的是太多了,服务里面 List 和 Map 随处可见,我很难找到到底哪里是大对象。


但是我还是一点都不慌,因为这半天都没有再次发生 Full GC,说明此时留给我的时间还是比较充足的,


所以我请求了场外援助,让 DBA 帮我导出一下服务的慢查询 SQL,因为我想可能是从数据库里面一次性取的数据太多了,而程序里面也没有做控制导致的。


我之前就踩过类似的坑。


一个根据客户号查询客户有多少订单的内部使用接口,接口的返回是 List<订单>,看起来没啥毛病,对不对?


一般来说一个个人客户就几十上百,多一点的上千,顶天了的上万个订单,一次性拿出来也不是不可以。


但是有一个客户不知道咋回事,特别钟爱我们的平台,也是我们平台的老客户了,一个人居然有接近 10w 的订单。


然后这么多订单对象搞到到项目里面,本来响应就有点慢,上游再发起几次重试,直接触发 Full gc,降低了服务响应时间。


所以,经过这个事件,我们定了一个规矩:用 List、Map 来作为返回对象的时候,必须要考虑一下极端情况下会返回多少数据回去。即使是内部使用,也最好是进行分页查询。


好了,话说回来,我拿到慢查询 SQL 之后,根据几个 Full gc 时间点,对比之后提取出了几条看起来有点问题的 SQL。


然后拿到数据库执行了一下,发现返回的数据量其实也都不大。


此刻我还是一点都不慌,反正内存够用,而且针对这类问题,我还有一个场外援助没有使用呢。


第二天我开始找运维同事帮我每隔 8 小时 Dump 一次内存文件,然后第三天我开始拿着内存文件慢慢分析。


但是第二天我也没闲着,根据现有的线索反复分析、推理可能的原因。


然后在观看 GC 回收内存大小监控的时候,发现了一点点端倪。因为触发 Full GC 之后,发现被回收的堆内存也不是特别多。


当时就想到了除了大对象之外,还有一个现象有可能会导致这个现象:内存泄露。


巧的是在第二天又发生了一次 Full gc,这样我拿到的 Dump 文件就更有分析的价值了。基于前面的猜想,我分析的时候直接就冲着内存泄漏的方向去查了。


我拿着 5 个 Dump 文件,分析了在 5 个 Dump 文件中对象数量一直在增加的对象,这样的对象也不少,但是最终定位到了 FutureTask 对象,就是它:



找到这玩意了再回去定位对应部分的代码就比较容易。


但是你以为定位了代码就完事了吗?


不是的,到这里才刚刚开始,朋友。


因为我发现这个代码对应的 Bug 隐藏的还是比较深的,而且也不是我最开始假象的内存泄露,就是一个纯粹的内存溢出。


所以值得拿出来仔细嗦一嗦。


示例代码


为了让你沉浸式体验找 BUG 的过程,我高低得给你整一个可复现的 Demo 出来,你拿过去就可以跑的那种。


首先,我们得搞一个线程池:



需要说明一下的是,上面这个线程池的核心线程数、最大线程数和队列长度我都取的 1,只是为了方便演示问题,在实际项目中是一个比较合理的值。


然后重点看一下线程池里面有一个自定义的叫做 MyThreadFactory 的线程工厂类和一个自定义的叫做 MyRejectedPolicy 的拒绝策略。


在我的服务里面就是有这样一个叫做 product 的线程池,用的也是这个自定义拒绝策略。


其中 MyThreadFactory 的代码是这样的:



它和默认的线程工厂之间唯一的区别就是我加了一个 threadFactoryName 字段,方便给线程池里面的线程取一个合适的名字。


更直观的表示一下区别就是下面这个玩意:



原生:pool-1-thread-1

自定义:product-pool-1-thread-1



接下来看自定义的拒绝策略:



这里的逻辑很简单,就是当 product 线程池满了,触发了拒绝策略的时候打印一行日志,方便后续定位。


然后接着看其他部分的代码:



标号为 ① 的地方是线程池里面运行的任务,我这里只是一个示意,所以逻辑非常简单,就是把 i 扩大 10 倍。实际项目中运行的任务业务逻辑,会复杂一点,但是也是有一个 Future 返回。


标号为 ② 的地方就是把返回的 Future 放到 list 集合中,在标号为 ③ 的地方循环处理这个 list 对象里面的 Future。


需要注意的是因为实例中的线程池最多容纳两个任务,但是这里却有五个任务。我这样写的目的就是为了方便触发拒绝策略。


然后在实际的项目里面刚刚提到的这一坨逻辑是通过定时任务触发的,所以我这里用一个死循环加手动开启线程来示意:



整个完整的代码就是这样的,你直接粘过去就可以跑,这个案例就可以完全复现我在生产上遇到的问题:


public class MainTest {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor productThreadPoolExecutor = new ThreadPoolExecutor(1,
                1,
                1,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1),
                new MyThreadFactory("product"),
                new MyRejectedPolicy());

        while (true){
            TimeUnit.SECONDS.sleep(1);
            new Thread(()->{
                ArrayList<Future<Integer>> futureList = new ArrayList<>();
                //从数据库获取产品信息
                int productNum = 5;
                for (int i = 0; i < productNum; i++) {
                    try {
                        int finalI = i;
                        Future<Integer> future = productThreadPoolExecutor.submit(() -> {
                            System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
                            return finalI * 10;
                        });
                        futureList.add(future);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                for (Future<Integer> integerFuture : futureList) {
                    try {
                        Integer integer = integerFuture.get();
                        System.out.println(integer);
                        System.out.println("future.get() = " + integer);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

    }

    static class MyThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGr0up group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
        private final String threadFactoryName;

        public String getThreadFactoryName() {
            return threadFactoryName;
        }

        MyThreadFactory(String threadStartName) {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGr0up() :
                    Thread.currentThread().getThreadGr0up();
            namePrefix = threadStartName + "-" +
                    poolNumber.getAndIncrement() +
                    "-";
            threadFactoryName = threadStartName;
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

    public static class MyRejectedPolicy implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (e.getThreadFactory() instanceof MyThreadFactory) {
                MyThreadFactory myThreadFactory = (MyThreadFactory) e.getThreadFactory();
                if ("product".equals(myThreadFactory.getThreadFactoryName())) {
                    System.out.println(THREAD_FACTORY_NAME_PRODUCT + "线程池有任务被拒绝了,请关注");
                }
            }
        }
    }
}

你跑的时候可以把堆内存设置的小一点,比如我设置为 10m:



-Xmx10m -Xms10m



然后用 jconsole 监控,你会发现内存走势图是这样的:



哦,我的老天爷啊,这个该死的图,也是我的老伙计了,一个缓慢的持续上升的内存趋势图, 最后疯狂的触发 gc,但是并没有内存被回收,最后程序直接崩掉:



这绝大概率就是内存泄漏了啊。


但是在生产上的内存走势图完全看不出来这个趋势,我前面说了,主要因为 GC 情况的数据只会保留一周时间,所以就算把整个图放出来也不是那么直观。


其次不是因为我牛逼嘛,萌芽阶段就干掉了这个问题,所以没有遇到最后频繁触发 gc,但是没啥回收的,导致 OOM 的情况。


所以我再带着你看看另外一个视角,这是我真正定位到问题的视角。就是分析内存 Dump 文件。


分析内存 Dump 文件的工具以及相关的文章非常的多,我就不赘述了,你随便找个工具玩一玩就行。我这里主要是分享一个思路,所以就直接使用 idea 里面的 Profiler 插件了,方便。


我用上面的代码,启动起来之后在四个时间点分别 Dump 之后,观察内存文件。内存泄露的思路就是找文件里面哪个对象的个数和占用空间是在持续上升嘛,特别是中间还发生过 full gc,这个过程其实是一个比较枯燥且复杂的过程,在生产项目中可能会分析出很多个这样的对象,然后都要到代码里面去定位相关逻辑。


但是我这里极大的简化了程序,所以很容易就会发现这个 FutureTask 对象特别的抢眼,数量在持续增加,而且还是名列前茅的:



然后这个工具还可以看对象占用大小,大概是这个意思:



所以我还可以看看在这几个文件中 FutureTask 对象大小的变化,也是持续增加:



就它了,准没错。


好,问题已经能复现了,GC 图和内存 Dump 的图也都给你看了。


到这里,如果有人已经看出来问题的原因了,可以直接拉到文末点个赞,感谢大佬阅读我的文章。


如果你还没看出端倪来,那么我先给你说问题的根本原因:



问题的根本原因就出在 MyRejectedPolicy 这个自定义拒绝策略上。



在带你细嗦这个问题之前,我先问一个问题:



JDK 自带的线程池拒绝策略有哪些?



这玩意,老八股文了,存在的时间比我从业的时间都长,得张口就来:



  • AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常,这是默认的策略。

  • DiscardOldestPolicy:丢弃队列最前面的任务,执行后面的任务

  • CallerRunsPolicy:由调用线程处理该任务

  • DiscardPolicy:也是丢弃任务,但是不抛出异常,相当于静默处理。


然后你再回头看看我的自定义拒绝策略,是不是和 DiscardPolicy 非常像,也没有抛出异常。只是比它更高级一点,打印了一点日志。


当我们使用默认的策略的时候:



或者我们把框起来这行代码粘到我们的 MyRejectedPolicy 策略里面:



再次运行,不管是观察 gc 情况,还是 Dump 内存,你会发现程序正常了,没毛病了。


下面这个走势图就是在拒绝策略中是否抛出异常对应的内存走势对比图:



在拒绝策略中抛出异常就没毛病了,为啥?



探索


首先,我们来看一下没有抛出异常的时候,发生了什么事情。


没有抛出异常时,我们前面分析了,出现了非常多的 FutureTask 对象,所以我们就找程序里面这个对象是哪里出来的,定位到这个地方:



future 没有被回收,说明 futureList 对象没有被回收,而这两个对象对应的 GC Root 都是new 出来的这个线程,因为一个活跃线程是 GC Root。


进一步说明对应 new 出来的线程没有被回收。


所以我给你看一下前面两个案例对应的线程数对比图:



没有在拒绝策略中抛出异常的线程非常的多,看起来每一个都没有被回收,这个地方肯定就是有问题的。


然后随机选一个查看详情,可以看到线程在第 39 行卡着的:



也就是这样一行代码:



这个方法大家应该熟悉,因为也没有给等待时间嘛,所以如果等不到 Future 的结果,线程就会在这里死等。


也就导致线程不会运行结束,所以不会被回收。


对应着源码说就是有 Future 的 state 字段,即状态不正确,导致线程阻塞在这个 if 里面:



if 里面的 awaitDone 逻辑稍微有一点点复杂,这个地方其实还有一个 BUG,在 JDK 9 进行了修复,这一点我在之前的文章中写过,所以就不赘述了,你有兴趣可以去看看:《Doug Lea在J.U.C包里面写的BUG又被网友发现了。》


总之,在我们的案例下,最终会走到我框起来的代码:



也就是当前线程会在这里阻塞住,等到唤醒。


那么问题就来了,谁来唤醒它呢?


巧了,这个问题我之前也写过,在这篇文章中,有这样一句话:《关于多线程中抛异常的这个面试题我再说最后一次!》



如果子线程捕获了异常,该异常不会被封装到 Future 里面。是通过 FutureTask 的 run 方法里面的 setException 和 set 方法实现的。在这两个方法里面完成了 FutureTask 里面的 outcome 变量的设置,同时完成了从 NEW 到 NORMAL 或者 EXCEPTIONAL 状态的流转。



带你看一眼 FutureTask 的 run 方法:



也就是说 FutureTask 状态变化的逻辑是被封装到它的 run 方法里面的。


知道了它在哪里等待,在哪里唤醒,揭晓答案之前,还得带你去看一下它在哪里诞生。


它的出生地,就是线程池的 submit 方法:



java.util.concurrent.AbstractExecutorService#submit




但是,朋友,注意,我要说但是了。


首先,我们看一下当线程池的 execute 方法,当线程池满了之后,再次提交任务会触发 reject 方法,而当前的任务并不会被放到队列里面去:



也就是说当 submit 方法不抛出异常就会把正常返回的这个状态为 NEW 的 future 放到 futureList 里面去,即下面编号为 ① 的地方。然后被标号为 ② 的循环方法处理:



那么问题就来了:被拒绝了的任务,还会被线程池触发 run 方法吗?


肯定是不会的,都被拒绝了,还触发个毛线啊。


不会被触发 run 方法,那么这个 future 的状态就不会从 NEW 变化到 EXCEPTION 或者 NORMAL。


所以调用 Future.get() 方法就一定一直阻塞。又因为是定时任务触发的逻辑,所以导致 Future 对象越来越多,形成一种内存泄露。


submit 方法如果抛出异常则会被标号为 ② 的地方捕获到异常。


不会执行标号为 ① 的地方,也就不会导致内存泄露:



道理就是这么一个道理。


解决方案


知道问题的根本原因了,解决方案也很简单。


定位到这个问题之后,我发现项目中的线程池参数配置的并不合理,每次定时任务触发之后,因为数据库里面的数据较多,所以都会触发拒绝策略。


所以首先是调整了线程池的参数,让它更加的合理。当时如果你要用这个案例,这个地方你也可以包装一下,动态线程池,高大上,对吧,以前讲过。


然后是调用 Future.get() 方法的时候,给一个超时时间,这样至少能帮我们兜个底。资源能及时释放,比死等好。


最后就是一个教训:自定义线程池拒绝策略的时候,一定一定记得要考虑到这个场景。


比如我前面抛出异常的自定义拒绝策略其实还是有问题的,我故意留下了一个坑:



抛出异常的前提是要满足最开始的 if 条件:



e.getThreadFactory() instanceof MyThreadFactory



如果别人误用了这个拒绝策略,导致这个 if 条件不成立的话,那么这个拒绝策略还是有问题。


所以,应该把抛出异常的逻辑移到 if 之外。


同时在排查问题的过程中,在项目里面看到了类似这样的写法:



不要这样写,好吗?


一个是因为 submit 是有返回值的,你要是不用返回值,直接用 execute 方法不香吗?


另外一个是因为你这样写,如果线程池里面的任务执行的时候出异常了,会把异常封装到 Future 里面去,而你又不关心 Future,相当于把异常给吞了,排查问题的时候你就哭去吧。


这些都是编码过程中的一些小坑和小注意点。


反转


这一小节的题目为什么要叫反转?


因为以上的内容,除了技术原理是真的,我铺垫的所有和背景相关的东西,全部都是假的。



整篇文章从第二句开始就是假的,我根本就没有遇到过这样的一个生产问题,也谈不上扼杀在摇篮里,更谈不上是我去解决的了。


但是我在开始的时候说了这样一句话,也是全文唯一一句加粗的话:



虽然现在不是你的,但是你看完之后就是你的了,你明白我意思吧?



所以这个背景其实我前几天看到了“严选技术”发布的这篇文章《严选库存稳定性治理系列:一个线程池拒绝策略引发的血案》


看完他们的这篇文章之后,我想起了我之前写过的这篇文章:《看起来是线程池的BUG,但是我认为是源码设计不合理。》


我写的这篇就是单纯从技术角度去解析的这个问题,而“严选技术”则是从真实场景出发,层层剥茧,抵达了问题的核心。


但是这两篇文章遇到的问题的核心原因其实是一模一样的。


我在我的文章中的最后就有这样一段话:



巧了,这不是和“严选技术”里面这句话遥相呼应起来了吗:



在我反复阅读了他们的文章,了解到了背景和原因之后,我润色了一下,写了这篇文章来“骗”你。


如果你有那么几个瞬间被我“骗”到了,那么我问你一个问题:假设你是面试官,你问我工作中有没有遇到过比较棘手的问题?


而我是一个只有三年工作经验的求职者。


我用这篇文章中我假想出来的生产问题处理过程,并辅以技术细节,你能看出来这是我“包装”的吗?


然后在描述完事件之后,再体现一下对于事件的复盘,可以说一下基于这个事情,后面自己对监控层面进行了丰富,比如接口超时率监控、GC 导致的 STW 时间监控啥的。然后也在公司内形成了“经验教训”文档,主动同步给了其他的同事,以防反复踩坑,巴拉巴拉巴拉...


反正吧,以后看到自己觉得好的案例,不要看完之后就完了,多想想怎么学一学,包装成自己的东西。


这波包装,属于手摸手教学了吧?


求个赞,不过分吧?


作者:why技术
来源:juejin.cn/post/7186512174779465765
收起阅读 »