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

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


  • 首页

  • 归档

  • 搜索

老面试官问我:LRU 和 Innodb Buffer Poo

发表于 2021-09-14

你好,我是yes。

这 LRU 和 Innodb Buffer Pool 之间有关系吗?

确实有。

其实我之前的文章写到过这个,就在今年的三月份,不过是写 Kafka 的冷热分区时顺带提了一下 Innodb Buffer Pool。

今天咱们再来仔细盘一盘它们两者之间的联系,还是挺有启发的。

Buffer Pool

Buffer Pool 翻译过来就是缓冲池,缓冲什么呢?缓冲的就是数据库的数据。

数据库的数据都是要落盘存储的,但硬盘的访问速度又过于缓慢,所以需要把硬盘内部的数据加载到内存中,这样数据库直接从内存读取数据,减少磁盘的 I/O,速度就快了。

其实借助操作系统的 page cache 就能透明的实现这个功能,但是这不便于数据库自身对数据的管理,因为操作系统上还有很多其他程序也会使用 page cache。

所以 MySQL 就自己划了个池子来管理数据,即 Buffer Pool。

LRU

但是 Buffer Pool 是有限的,因为内存是有限的,一般而言内存不会比硬盘大吧?

所以想要把硬盘上的所有数据都加载到内存中是不实际的。当请求的数据不在内存的时候,不得不去硬盘拿,而这种时候查询速度就会变慢,用户在使用上的直接反应是:为什么这个破网站这么卡?

因此,我们要尽可能的避免这种情况的发生,也就是提高缓存的命中率。

如何提高呢?

当内存存储的数据满了的时候,把用户经常访问的数据保留着,淘汰一些不经常被访问的数据,腾出位置存放新访问的数据。

这样不就能提高缓存命中率了?

冷热数据就是有这样的特性,类比微博热搜,越热的数据,访问量就越大,越冷门的数据越没有人访问。

根据这个特性,LRU 就很合适,Least Recently Used,最近最少使用。根据这个算法就能选择最近最少使用的数据淘汰之~

第五层

如果就回答到上面那个程度,还不够,满足不了面试官对你的期望。

我们来想一下普通的 LRU 实现在 buffer 管理这个场景会有什么问题。

下图为先后访问数据 6 和数据 3 之后的情况。

可以看到,被访问的数据会被移到的头部,如果内存不足,会淘汰尾部的数据。

这种实现放在 Buffer Pool 中会有什么问题?

首先你需要了解一个原理:局部性原理。

  • 时间局部性:如果一个数据现在被访问了,在近期可能还会被多次访问。
  • 空间局部性:如果一个数据被访问了,那么存储在它附近的数据,很有可能立马被访问。

在硬件、操作系统、应用程序有很多都根据局部性原理做了对应的实现,像磁盘就有预读功能来减少磁盘I/O。

对应到 Buffer Pool 中也实现了预读的功能。当顺序访问数据页面到达一定的数量或者一个 extent(页面管理的逻辑分区)中有很多页面被加载的时候,Innodb 都会预读页面加载到 Buffer Pool 中。

预读是好事,如果用朴素的 LRU 来实现数据的淘汰就有点问题。

因为预读的数据也会被移动到头部,这样头部原本的热数据就会更靠后了,面临着被淘汰的危机,如果预读的数据有用那没事,如果没用的话,这波就是好心做了坏事。

所以怎么办?

冷热分区,又称老年代和新生代(有 JVM 那味儿了)

Innodb 将缓冲池分为了新生代和老年代。默认头部的 63% 为新生代,尾部 37% 为老年代。

当第一次从磁盘加载数据到 Buffer Pool 时,会将数据放置在老年代的头部,而不是新生代的头部,这样即使有预读功能也不会把前面的热数据给顶一下。

然后下次访问这个数据的时候,会把数据从老年代移动带新生代的头部。

好像已经很完美了?我们再来看另一种情况全表扫描。

全表扫描是我们在日常开发中需避免的一种查询,但是有时候就是有需求会全表扫描,或者不经意的错误使用导致全表扫描。

这时候会有很多冷数据被加载到 Buffer Pool 中,被放在老年代,紧接着肯定又会对全表扫描到的数据进行一波处理,那这样这些数据再次被访问,就会被放到新生代的头部,这样就会大量淘汰热区的数据。

一次全表扫描,就替换了很多热数据,降低了缓存的命中率,这波有点伤。

所以怎么办?

加个时间判断。

因为全表扫描的数据,大部分紧接着就会被访问,然后之后就没用了,于是 Innodb 设置了一个时间窗口,默认是1s。

即在老年代数据被再次访问的时间与之前被访问的时间间隔超过1s,才会晋升到新生代,否则还是在老年代,这样就不会污染新生代的热数据。

这波有点秀吧。

所以 Innodb 针对数据库数据访问的特性,基于分区和时间窗口两个实现改进了 LRU 淘汰缓存页的机制,提高了缓存的命中率,提升了查询效率。

所以,面试官如果问你* LRU 与 Innodb Buffer Pool 之间有什么联系吗?*就用我上面这句话回答即可。

​紧接着等他深入询问,再把上面的缘由解释给他听,这波就OK了。

如果面试官问:还有吗?

那下面这个回答可以用上:有。

如果按照普通的 LRU 实现,新生代页面的访问会频繁把数据移动到头部,这个移动是有开销的,而且在很大程度上没有必要,你想想都是热数据自个儿在那移动来移动去的,是不是又是白给?

所以新生代又可以被分个区,新生代前面四分之一的数据访问不会被移动到头部,后面四分之三的数据范围才会被移动到头部(这个内容参考自《从根儿上理解MySQL》,我没看过源码不清楚 Innodb是否真的是这样实现)。

这波回答可以说差不多在第五层了。

不过关于 LRU 变型实现还有很多,比如和 CDN 相关的 TLRU,和 CPU cache 相关的 PLRU 等等,有兴趣的同学自行查询,这里就不做展开了。

最后

好了,至此想必你已经能说清 LRU 与 Innodb Buffer Pool 两者的关系啦。

上面说的冷热分区的比例是可以调整的,参数是:innodb_old_blocks_pct。

还有我上面都是用数据作为单位来移动头部和尾部,这只是为了便于理解。其实 Buffer Pool 是基于页来管理数据的,等我下篇再来好好盘盘 Buffer Pool 吧。

我是yes,从一点点到亿点点我们下篇见。

本文转载自: 掘金

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

Rust 技巧篇 | 用 `#【doc】` 属性宏改善你的文

发表于 2021-09-14

编辑: 张汉东

说明: 本文是在原文基础上的梳理,也引入了其他内容。


属性宏的新特性介绍

从 Rust 1.54 开始,属性宏增加了类函数宏的支持。

类函数的宏可以是基于macro_rules!的声明宏,也可以是像macro!(...)那样被调用的过程宏。这对于文档注释相当有好处。如果你的项目的 README 是一个很好的文档注释,你可以使用include_str!来直接纳入其内容。以前,各种变通方法允许类似的功能,但从1.54开始,这就更符合人体工程学了。

1
rust复制代码#![doc = include_str!("README.md")]

如果你看过一些 Rust 开源项目,你应该在 lib.rs 中看到过一大堆文档注释吧?这些注释太长,导致真正的代码被挤到到最下面。有了这个功能,就可以解决这类问题了。

1
2
3
4
5
6
7
8
rust复制代码macro_rules! make_function {
($name:ident, $value:expr) => {
// 这里使用 concat! 和 stringify! 构建文档注释
#[doc = concat!("The `", stringify!($name), "` example.")]
///
/// # Example
///
///
#[doc = concat!(
    "assert_eq!(", module_path!(), "::", stringify!($name), "(), ",
    stringify!($value), ");")
]
/// 
1
2
3
4
5
6
7
        pub fn $name() -> i32 {
$value
}
};
}

make_function! {func_name, 123}

也可以像这样,在属性中嵌入宏调用来构建文档注释。可以对比下展开后的代码:

1
2
3
4
5
rust复制代码///The `func_name` example.
///
/// # Example
///
///

///assert_eq!(doc_attr::func_name(), 123);
///

1
2
3
pub fn func_name() -> i32 {
123
}

这样的话,文档也可以复用了。当然你也可以扩展出其他用法。

其他用法

在 国外社区朋友的这篇文章中,他列举了一些应用场合。

用文档测试扩展测试能力

Rust 的文档测试相当灵活,假如你写了一些函数或者宏,你想确保它在输入某个值的时候不能编译。使用单元测试比较麻烦,但是用文档测试就很方便了。

1
2
3
rust复制代码/// ```compile_fail
#[doc = include_str!("compile_fail.rs")]
///

mod doc_test {}

1
2
3
4
5

你可以把相关测试放到 `complile_fail.rs` 中,然后使用 文档注释将其包括进来,这样在 cargo 执行测试的时候就可以进行测试了。而且对于 Rust 代码整体增加了可读性和可维护性。同样,你也可以检查 panic 等。


我们也不希望这种注释出现在最终用户的文档中,或者是编译文件中,所以需要使用 `cfg(doctest)` 来将其隐藏:

rust复制代码#[cfg(doctest)]
///

1
2
#[doc = include_str!("compile_fail.rs")]
///

mod doc_test {}



**本文转载自:** [掘金](https://juejin.cn/post/7007616186229719047)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

论文导读 Rudra 查找 Rust 生态系统中的内

发表于 2021-09-14

作者:张汉东


引子

美国佐治亚理工学院的系统软件安全实验室开源了Rudra ,用于分析和报告 Unsafe Rust 代码中潜在的内存安全和漏洞,为此他们也将在 2021 年第 28 届 ACM 操作系统原则研讨会论文集上发表相关论文,该论文目前在 Rudra 源码仓库中提供下载。

说明:本篇文章不是论文的翻译,而是本人对该论文的梳理和总结。

概要

Rust 语言关注内存安全和性能,Rust 目前已经在传统的系统软件中得到了广泛的应用,如操作系统、嵌入式系统、网络框架、浏览器等,在这些领域,安全和性能都是不可或缺的。

Rust 内存安全的思想是在编译时验证内存的所有权,具体而言是验证内存分配对象的访问和生存期。Rust 编译器对值的共享和独占引用通过借用检查提供两个保证:

  1. 引用的生存期不能长于其拥有者变量的生存期。为了避免 use-after-free (UAF) 。
  2. 共享和独占引用不能同时存在,排除了并发读写同一个值的风险。

不幸的是,这些安全规则太过限制。在某些需要调用底层硬件系统,或需要获得更好性能时,需要暂时绕过安全规则。这些需求无法被 Safe Rust 解决,但是对于系统开发却是必不可少的,所以 Unsafe Rust 被引入。Unsafe Rust 意味着,编译器的安全检查职责被暂时委托给了程序员。

Unsafe Rust代码的健全性(soundness )对于整个程序的内存安全是至关重要的,因为大多数系统软件,如操作系统或标准库,都离不开它。

有些人可能比较天真地以为,Unsafe Rust 只要在审查源码的时候就可以排除它的风险。然而,问题的关键在于,健全性的推理是非常微妙的,且很容易出错,原因有三:

  1. 健全性的错误会顺道破坏Rust的安全边界,这意味着所有的外部代码,包括标准库都应该是健全的。
  2. Safe 和 Unsafe 的代码是相互依赖的。
  3. 编译器插入的所有不可见的代码路径都需要由程序员正确推理。

为了让 Rust 有一个健全性的基础,已经有了很多研究型项目,比如形式化类型系统和操作语义,验证其正确性,并且建立模型用于检查。这些都是非常重要的,但还不够实用,因为它没有覆盖到整个生态系统。另外还有一些动态方法,比如 Miri 和 Fuzz 模糊测试,但是这些方法不太容易被大规模使用,因为它需要大量的计算资源。

当前,Rust 语言正在变得流行,Unsafe Rust 的包也逐渐变多。因此,设计一个实用的检测内存安全的算法就很重要了。

这篇论文介绍了三种重要的Bug模式,并介绍了 Unsafe 代码,以及提供 Rudra 这样的工具。该论文作者的工作一共有三个贡献:

  1. 确定了三种 Unsafe Rust 中的 Bug 模式,并且设计了两种新的算法可以发现它们。
  2. 使用 Rudra 在Rust 生态系统中发现263个新的内存安全漏洞。这代表了自2016年以来RustSec中所有bug的41.4%。
  3. 开源。Rudra 是开源的,我们计划 贡献其核心算法到官方的Rust linter中。

Rudra

Rudra 用于分析和报告Unsafe Rust 代码中潜在的内存安全漏洞。 由于Unsafe 代码中的错误威胁到 Rust 安全保证的基础,Rudra 的主要重点是将我们的分析扩展到 Rust 包注册仓库(比如 crates.io)中托管的所有程序和库。Rudra 可以在 6.5 小时内扫描整个注册仓库(43k 包)并识别出 263 个以前未知的内存安全漏洞,提交 98 个 RustSec 公告和 74 个 CVE,占自 2016 年以来报告给 RustSec 的所有漏洞的 41.4%。

image.png

Rudra 发现的新漏洞很微妙,它们存在于Rust 专家的库中:两个在 std 库中,一个在官方 futures 库中,一个在 Rust 编译器 rustc 中。 Rudra 已经开源, 并计划将其算法集成到官方 Rust linter 中。

Rudra, 这个名称来自于 梵文,译为鲁特罗(或楼陀罗),印度神话中司风暴、狩猎、死亡和自然界之神。他在暴怒时会滥伤人畜;他又擅长以草药来给人治病。其名意为“狂吼”或“咆哮”(可能是飓风或暴风雨)。

Rudra 和 Miri 的区别 :

Rudra 是静态分析,无需执行即可分析源码。Miri 是解释器,需要执行代码。

两者可以结合使用。

关于 Unsafe Rust

因为 unsafe 关键字的存在,引出了一个有趣的 API 设计领域: 如何交流 API 的安全性。

通常有两种方法:

  1. 内部 Unsafe API 直接暴露给 API 用户,但是使用 unsafe 关键字来声明该 API 是不安全的,也需要添加安全边界的注释。
  2. 对 API 进行安全封装(安全抽象),即在内部使用断言来保证在越过安全边界时可以Panic,从而避免 UB 的产生。

第二种方法,即将 Unsafe 因素隐藏在安全 API 之下的安全抽象,已经成为 Rust 社区的一种约定俗成。

Safe 和 Unsafe 的分离,可以让我们区分出谁为安全漏洞负责。Safe Rust 意味着,无论如何都不可能导致未定义行为。换句话说,Safe API 的职责是,确保任何有效的输入不会破坏内部封装的 Unsafe 代码的行为预期。

这与C或C++形成了鲜明的对比,在C或C++中,用户的责任是正确遵守 API 的预期用法。

比如,在 libc 中的printf(),当它调用一个错误的指针而导致段错误的时候,没有人会指责它。然而这个问题却导致了一系列的内存安全问题:格式字符串漏洞(format-string vulnerability)。还记得前段时间 苹果手机因为加入一个经过特别构造名字的Wifi就变砖的漏洞否?

而在 Rust 中,println!() 就不应该也不可能导致一个段错误。此外,如果一个输入确实导致了段错误,那么它会被认为是 API 开发者的错误。

Rust 中内存安全Bug 的定义

在 Rust 中有两类 Unsafe 定义: Unsafe 函数 和 Unsafe 特质(trait)。

Unsafe 函数希望调用者在调用该函数时,可以确保其安全性。

Unsafe 特质则希望实现该 trait 的时候提供额外的语义保证。比如标准库里的 pub unsafe trait TrustedLen: Iterator { },该 trait 要求必须检查 Iterator::size_hint() 的上界,才能保证 TrustedLen 所表达的“可信的长度”语义。

该论文对 内存安全 Bug 提供了一个清晰的一致性的定义,而非 Rust 操作语义:

定义 1: 类型(Type)和值(Value)是以常规方式定义的。类型是值的集合。

定义2: 对于 类型 T, safe-value(T) 被定义为可以安全创建的值。例如 Rust 里的字符串是内部表示为字节的数组,但它在通过 安全 API 创建的时候只能包含 UTF-8 编码的值。

定义3:函数 F 是接收类型为 arg(F)的值,并返回一个类型为 ret(F) 的值。对于多个参数,我们将其看作元组。

定义4: 如果 在 safe-value(arg(F))集合中存在v (记为:∃𝑣 ∈ safe-value(𝑎𝑟𝑔(𝐹)) ),使得当调用 F(v)时触发违反内存安全的行为,或者返回一个不属于 safe-value(𝑟𝑒𝑡(𝐹)) 集合中的返回值𝑣𝑟𝑒𝑡 时(记为:𝑣𝑟𝑒𝑡 ∉ safe-value(𝑟𝑒𝑡(𝐹))),则 函数 F 有内存安全缺陷。

定义5: 对于一个泛型函数Λ,pred(Λ)被定义为满足Λ的类型谓词(指trait 限定)的类型集合。给定一个类型𝑇∈pred(Λ),resolve(Λ,𝑇)将泛型函数实例化为具体函数𝐹。

定义6: 如果一个泛型函数Λ可以被实例化为一个具有内存安全缺陷的函数,即,∃𝑇 ∈ pred(Λ),使得𝐹=resolve(Λ,𝑇)具有内存安全缺陷,则该泛型函数具有内存安全缺陷。

定义7:如果一个类型的Send实现不能跨越线程边界传输,那么该类型就有内存安全问题。

定义8: 如果一个类型的Sync实现不能通过别名指针(aliased pointer)并发地访问该类型,那么它就有内存安全问题。即,定义了一个非线程安全的方法,该方法接收&self。

Unsafe Rust 中三类重要 Bug 模式

论文通过对已知漏洞进行定性分析,总结出 Unsafe Rust 中三类重要的 Bug 模式:

  1. Panic Safety (恐慌安全): 由恐慌导致的内存安全 Bug。
  2. Higher-order Safety Invariant(高阶安全不变性 ):由高阶类型没有给定安全保证而引发的 Bug。
  3. Propagating Send/Sync in Generic Types(泛型中Send/Sync传播):由泛型内部类型不正确的手工Send/Sync实现引起泛型 Send/Sync 约束不正确而引发的 Bug。

Panic Safety

这与其他编程语言(如C++)中的异常安全的概念类似。Rust 中类似其他编程语言中异常(Exception)的概念叫 恐慌(Panic)。恐慌一般在程序达到不可恢复的状态才用,当然在 Rust 中也可以对一些实现了 UnwindSafe trait 的类型捕获恐慌。

当 Panic 发生时,会引发栈回退(stack unwind),调用栈分配对象的析构函数,并将控制流转移给恐慌处理程序中。所以,当恐慌发生的时候,当前存活变量的析构函数将会被调用,从而导致一些内存安全问题,比如释放已经释放过的内存。

但是想要正确的推理在 Unsafe 代码中的恐慌安全,是非常困难且易于出错的。通常, 封装的Unsafe 代码可能会暂时绕过所有权检查,而且,安全封装的 API 在内部unsafe 代码的值返回之前,会根据安全边界条件确保它不会违反安全规则。但是,假如封装的Unsafe 代码发生了恐慌,则其外部安全检查可能不会执行。这很可能导致类似 C/C++ 中 未初始化(Uninitialized )或双重释放(Double Free)的内存不安全问题。

论文对此给出定义:

如果一个函数𝐹 Drop一个类型为𝑇的值𝑣,使得𝑣在Unwind 过程中 𝑣 ∉ safe-value(𝑇),并导致违反内存安全,则说明该函数存在恐慌性安全漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
rust复制代码// 标准库 `String::retain()` 曝出的 CVE-2020-36317 Panic safety bug

pub fn retain<F>(&mut self, mut f: F)
where
F: FnMut(char) -> bool
{
let len = self.len();
let mut del_bytes = 0;
let mut idx = 0;

unsafe { self.vec.set_len(0); } // + 修复bug 的代码
while idx < len {
let ch = unsafe {
self.get_unchecked(idx..len).chars().next().unwrap()
};
let ch_len = ch.len_utf8();

// self is left in an inconsistent state if f() panics
// 此处如果 f() 发生了恐慌,self 的长度就会不一致
if !f(ch) {
del_bytes += ch_len;
} else if del_bytes > 0 {
unsafe {
ptr::copy(self.vec.as_ptr().add(idx),
self.vec.as_mut_ptr().add(idx - del_bytes),
ch_len);
}
}
idx += ch_len; // point idx to the next char
}
unsafe { self.vec.set_len(len - del_bytes); } // + 修复bug 的代码 ,如果 while 里发生panic,则将返回长度设置为 0
}

fn main(){
// PoC: creates a non-utf-8 string in the unwinding path
// 此处传入一个 非 UTF-8 编码字符串引发恐慌
"0è0".to_string().retain(|_| {
match the_number_of_invocation() {
1 => false,
2 => true,
_ => panic!(),
}
});
}

Higher-order Safety Invariant

一个函数应该安全地执行所有安全的输入,包括参数数据类型、泛型类型参数以及外部传入的闭包。

换句话说,一个安全的函数不应该提供比 Rust 编译器提供的安全不变式更多的东西。所谓 安全不变式就是指 Rust 里的安全函数,在任何有效输入的情况下,都不应该发生任何未定义行为。

例如,Rust 里的 sort 函数,不应该触发任何未定义行为,哪怕用户提供的比较器不遵循全序关系,也不会发生段错误。但是 Cpp 中的排序函数,当用户提供一个不兼容当前的比较器的情况下,就会发生段错误。

Rust 为 高阶类型提供的唯一安全不变式是 类型签名的正确性。然而常见的错误是,对调用者提供的函数在以下方面产生了不正确的假设:

  1. 逻辑一致性:比如,sort函数遵循全序关系。
  2. 纯洁性:对相同的输入总是返回相同的输出。
  3. 语义约束:只针对参数,因为它可能包含未初始化字节。

对于 Unsafe 代码,必须自己检查这些属性,或者指定正确的约束(例如,用Unafe 的特质)让调用者义务检查这些属性。

在 Rust 类型系统下,执行高阶类型的安全不变式是很困难的。比如,将一个未初始化的缓冲区传给一个调用者提供的 Read 实现。

不幸的是,许多Rust程序员为调用者提供的函数提供一个未初始化的缓冲区来优化性能,而没有意识到其固有的不健全性。由于其普遍性和微妙性,Rust标准库现在明确指出,用一个未初始化的缓冲区调用read() 本身就是不健全的行为。

论文对此给出定义:

高阶不变性bug是指函数中的内存安全bug,它是由假设保证高阶不变性引起的,而 Rust 的类型系统对调用者提供的代码没有保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
rust复制代码1 // CVE-2020-36323: a higher-order invariant bug in join()
2 fn join_generic_copy<B, T, S>(slice: &[S], sep: &[T]) -> Vec<T>
3 where T: Copy, B: AsRef<[T]> + ?Sized, S: Borrow<B>
4 {
5 let mut iter = slice.iter();
6
7 // `slice`is converted for the first time
8 // during the buffer size calculation.
9 let len = ...; // `slice` 在这里第一次被转换
10 let mut result = Vec::with_capacity(len);
11 ...
12 unsafe {
13 let pos = result.len();
14 let target = result.get_unchecked_mut(pos..len);
15
16 // `slice`is converted for the second time in macro
17 // while copying the rest of the components.
18 spezialize_for_lengths!(sep, target, iter; // `slice` 第二次被转换
19 0, 1, 2, 3, 4);
20
21 // Indicate that the vector is initialized
22 result.set_len(len);
23 }
24 result
25 }
26
27 // PoC: a benign join() can trigger a memory safety issue
28 impl Borrow<str> for InconsistentBorrow {
29 fn borrow(&self) -> &str {
30 if self.is_first_time() {
31 "123456"
32 } else {
33 "0"
34 }
35 }
36 }
37
38 let arr: [InconsistentBorrow; 3] = Default::default();
39 arr.join("-");

该代码是为 Borrow<str>实现 join 方法内部调用的一个函数 join_generic_copy的展示。 在 join_generic_copy 内部,会对 slice 进行两次转换,而在 spezialize_for_lengths! 宏内部,调用了.borrow()方法,如果第二次转换和第一次不一样,而会返回一个未初始化字节的字符串。

这里, Borrow<B> 是高阶类型,它内部 borrow 的一致性其实并没有保证,可能会返回不同的slice,如果不做处理,很可能会暴露出未初始化的字节给调用者。

Propagating Send/Sync in Generic Types

当涉及泛型时候, Send/Sync 的规则会变得很复杂,如图:

image.png
通常 Send/Sync 会由编译器自动实现,但是当开发者涉及 Unsafe 时,可能需要手动实现这俩 trait。手动实现 Send/Sync 想要正确很困难。一个不懂 Send/Sync 如何手动实现的开发者,很容易在代码中引入 Bug。

论文对此给出定义:

如果泛型在实现Send/Sync类型时,如果它对内部类型上指定了不正确的Send/Sync约束,那么泛型的Send/Sync约束就会变得不正确。这就是 泛型中 Send/Sync 传播引发的不安全 Bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
rust复制代码1 // CVE-2020-35905: incorrect uses of Send/Sync on Rust's futures
2 pub struct MappedMutexGuard<'a, T: ?Sized, U: ?Sized> {
3 mutex: &'a Mutex<T>,
4 value: *mut U,
5 _marker: PhantomData<&'a mut U>, // + 修复代码
6 }
7
8 impl<'a, T: ?Sized> MutexGuard<'a, T> {
9 pub fn map<U: ?Sized, F>(this: Self, f: F)
10 -> MappedMutexGuard<'a, T, U>
11 where F: FnOnce(&mut T) -> &mut U {
12 let mutex = this.mutex;
13 let value = f(unsafe { &mut *this.mutex.value.get() });
14 mem::forget(this);
15 // MappedMutexGuard { mutex, value }
16 MappedMutexGuard { mutex, value, _marker: PhantomData } // + 修复代码
17 }
18 }
19
20 // unsafe impl<T: ?Sized + Send, U: ?Sized> Send
21 unsafe impl<T: ?Sized + Send, U: ?Sized + Send> Send // + 修复代码
22 for MappedMutexGuard<'_, T, U> {}
23 //unsafe impl<T: ?Sized + Sync, U: ?Sized> Sync
24 unsafe impl<T: ?Sized + Sync, U: ?Sized + Sync> Sync // + 修复代码
25 for MappedMutexGuard<'_, T, U> {}
26
27 // PoC: this safe Rust code allows race on reference counter
28 * MutexGuard::map(guard, |_| Box::leak(Box::new(Rc::new(true))));

Rust futures 库中发现的问题,错误的手工 Send/Sync实现 破坏了线程安全保证。

受影响的版本中,MappedMutexGuard的Send/Sync实现只考虑了T上的差异,而MappedMutexGuard则取消了对U的引用。

当MutexGuard::map()中使用的闭包返回与T无关的U时,这可能导致安全Rust代码中的数据竞争。

这个问题通过修正Send/Sync的实现,以及在MappedMutexGuard类型中添加一个PhantomData<&'a mut U>标记来告诉编译器,这个防护也是在U之上。

Rudra 的设计

整体设计图如下:

image.png

Rudra 通过 HIR 来获取 crate 的代码结构(包括 trait定义、函数签名、Unsafe 块等),通过 MIR 来获取代码语义(数据流、控制流图、调用依赖等)。为什么不使用 LLVM IR 呢?因为在这个层面上 Rust 的抽象已经消失了。

然后通过内部的 Unsafe Dataflow Checker (UD) 来检查 Panic Safety Bug 和 Higher-order Invariant Bug,通过 Send/Sync Variance Checker(SV)来检查 Send/Sync Variance Bug。最终将结果按优先级汇总输出报告。

Unsafe Dataflow Checker (UD) 和 Send/Sync Variance Checker(SV) 对应两套算法,具体可参加论文和代码。

关于安全性相关英文术语解释

英文中关于安全性有多个单词,比如 Security和Safety,但是中文只有“安全性”这一个词。所以这里需要说明一下:

  1. Security,通常指信息安全、网络安全之类。
  2. Safety,通常指功能性安全。

通常,会因为功能性的漏洞,而造成信息安全问题。

小结

该论文的最后一章,还包含了很多数据来证明 Rudra 的效果,以及 Rudra 和 Fuzz 测试、Miri 和其他 Rust 静态分析工具的比较等结果。

image.png
上图是论文作者们使用 Rudra 对 Rust 实现的几个操作系统进行检查的结果,详细内容参加论文。

这篇论文非常值得一看,对于我们真正理解 Rust 的安全理念有所帮助。该论文也为 Rust 语言的安全状况提供了新的视角,也提供了一个静态检查工具,值得我们关注。

本文转载自: 掘金

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

【MyBatis】几种批量插入效率的比较

发表于 2021-09-14

批处理数据主要有三种方式:

  1. 反复执行单条插入语句
  2. foreach 拼接 sql
  3. 批处理

一、前期准备

基于Spring Boot + Mysql,同时为了省略get/set,使用了lombok,详见pom.xml。

1.1 表结构

id 使用数据库自增。

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码DROP TABLE IF EXISTS `user_info_batch`;
CREATE TABLE `user_info_batch` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_name` varchar(100) NOT NULL COMMENT '账户名称',
`pass_word` varchar(100) NOT NULL COMMENT '登录密码',
`nick_name` varchar(30) NOT NULL COMMENT '昵称',
`mobile` varchar(30) NOT NULL COMMENT '手机号',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱地址',
`gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_update` timestamp NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT 'Mybatis Batch';

1.2 项目配置文件

细心的你可能已经发现,数据库url 后面跟了一段 rewriteBatchedStatements=true,有什么用呢?先不急,后面会介绍。

1
2
3
4
5
6
7
8
9
10
11
xml复制代码# 数据库配置
spring:
datasource:
url: jdbc:mysql://47.111.118.152:3306/mybatis?rewriteBatchedStatements=true
username: mybatis
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.van.mybatis.batch.entity

1.3 实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码@Data
@Accessors(chain = true)
public class UserInfoBatchDO implements Serializable {
private Long id;

private String userName;

private String passWord;

private String nickName;

private String mobile;

private String email;

private LocalDateTime gmtCreate;

private LocalDateTime gmtUpdate;
}

1.4 UserInfoBatchMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public interface UserInfoBatchMapper {

/** 单条插入
* @param info
* @return
*/
int insert(UserInfoBatchDO info);

/**
* foreach 插入
* @param list
* @return
*/
int batchInsert(List<UserInfoBatchDO> list);
}

1.5 UserInfoBatchMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.van.mybatis.batch.mapper.UserInfoBatchMapper">

<insert id="insert" parameterType="cn.van.mybatis.batch.entity.UserInfoBatchDO">
insert into user_info_batch (user_name, pass_word, nick_name, mobile, email, gmt_create, gmt_update)
values (#{userName,jdbcType=VARCHAR}, #{passWord,jdbcType=VARCHAR},#{nickName,jdbcType=VARCHAR}, #{mobile,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}, #{gmtCreate,jdbcType=TIMESTAMP}, #{gmtUpdate,jdbcType=TIMESTAMP})
</insert>

<insert id="batchInsert">
insert into user_info_batch (user_name, pass_word, nick_name, mobile, email, gmt_create, gmt_update)
values
<foreach collection="list" item="item" separator=",">
(#{item.userName,jdbcType=VARCHAR}, #{item.passWord,jdbcType=VARCHAR}, #{item.nickName,jdbcType=VARCHAR}, #{item.mobile,jdbcType=VARCHAR}, #{item.email,jdbcType=VARCHAR}, #{item.gmtCreate,jdbcType=TIMESTAMP}, #{item.gmtUpdate,jdbcType=TIMESTAMP})
</foreach>
</insert>
</mapper>

1.6 预备数据

为了方便测试,抽离了几个变量,并进行提前加载。

1
2
3
4
5
6
ini复制代码    private List<UserInfoBatchDO> list = new ArrayList<>();
private List<UserInfoBatchDO> lessList = new ArrayList<>();
private List<UserInfoBatchDO> lageList = new ArrayList<>();
private List<UserInfoBatchDO> warmList = new ArrayList<>();
// 计数工具
private StopWatch sw = new StopWatch();
  • 为了方便组装数据,抽出了一个公共方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码    private List<UserInfoBatchDO> assemblyData(int count){
List<UserInfoBatchDO> list = new ArrayList<>();
UserInfoBatchDO userInfoDO;
for (int i = 0;i < count;i++){
userInfoDO = new UserInfoBatchDO()
.setUserName("Van")
.setNickName("风尘博客")
.setMobile("17098705205")
.setPassWord("password")
.setGmtUpdate(LocalDateTime.now());
list.add(userInfoDO);
}
return list;
}
  • 预热数据
1
2
3
4
5
6
7
ini复制代码    @Before
public void assemblyData() {
list = assemblyData(200000);
lessList = assemblyData(2000);
lageList = assemblyData(1000000);
warmList = assemblyData(5);
}

二、反复执行单条插入语句

可能‘懒’的程序员会这么做,很简单,直接在原先单条insert语句上嵌套一个for循环。

2.1 对应 mapper 接口

1
arduino复制代码int insert(UserInfoBatchDO info);

2.2 测试方法

因为这种方法太慢,所以数据降低到 2000 条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码@Test
public void insert() {
log.info("【程序热身】");
for (UserInfoBatchDO userInfoBatchDO : warmList) {
userInfoBatchMapper.insert(userInfoBatchDO);
}
log.info("【热身结束】");
sw.start("反复执行单条插入语句");
// 这里插入 20w 条太慢了,所以我只插入了 2000 条
for (UserInfoBatchDO userInfoBatchDO : lessList) {
userInfoBatchMapper.insert(userInfoBatchDO);
}
sw.stop();
log.info("all cost info:{}",sw.prettyPrint());
}

2.3 执行时间

  • 第一次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
59887 100% 反复执行单条插入语句
  • 第二次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
64853 100% 反复执行单条插入语句
  • 第三次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
58235 100% 反复执行单条插入语句

该方式插入2000 条数据,执行三次的平均时间:60991 ms。

三、foreach 拼接SQL

3.1 对应mapper 接口

1
java复制代码int batchInsert(List<UserInfoBatchDO> list);

3.2 测试方法

该方式和下一种方式都采用20w条数据测试。

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码@Test
public void batchInsert() {
log.info("【程序热身】");
for (UserInfoBatchDO userInfoBatchDO : warmList) {
userInfoBatchMapper.insert(userInfoBatchDO);
}
log.info("【热身结束】");
sw.start("foreach 拼接 sql");
userInfoBatchMapper.batchInsert(list);
sw.stop();
log.info("all cost info:{}",sw.prettyPrint());
}

3.3 执行时间

  • 第一次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
18835 100% foreach 拼接 sql
  • 第二次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
17895 100% foreach 拼接 sql
  • 第三次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
19827 100% foreach 拼接 sql

该方式插入20w 条数据,执行三次的平均时间:18852 ms。

四、批处理

该方式 mapper 和xml 复用了 2.1。

4.1 rewriteBatchedStatements 参数

我在测试一开始,发现改成 Mybatis Batch提交的方法都不起作用,实际上在插入的时候仍然是一条条记录的插,而且速度远不如原来 foreach 拼接SQL的方法,这是非常不科学的。

后来才发现要批量执行的话,连接URL字符串中需要新增一个参数:rewriteBatchedStatements=true

  • rewriteBatchedStatements参数介绍

MySql的JDBC连接的url中要加rewriteBatchedStatements参数,并保证5.1.13以上版本的驱动,才能实现高性能的批量插入。MySql JDBC驱动在默认情况下会无视executeBatch()语句,把我们期望批量执行的一组sql语句拆散,一条一条地发给MySql数据库,批量插入实际上是单条插入,直接造成较低的性能。只有把rewriteBatchedStatements参数置为true, 驱动才会帮你批量执行SQL。这个选项对INSERT/UPDATE/DELETE都有效。

4.2 批处理准备

  • 手动注入 SqlSessionFactory
1
2
java复制代码    @Resource
private SqlSessionFactory sqlSessionFactory;
  • 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码@Test
public void processInsert() {
log.info("【程序热身】");
for (UserInfoBatchDO userInfoBatchDO : warmList) {
userInfoBatchMapper.insert(userInfoBatchDO);
}
log.info("【热身结束】");
sw.start("批处理执行 插入");
// 打开批处理
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserInfoBatchMapper mapper = session.getMapper(UserInfoBatchMapper.class);
for (int i = 0,length = list.size(); i < length; i++) {
mapper.insert(list.get(i));
//每20000条提交一次防止内存溢出
if(i%20000==19999){
session.commit();
session.clearCache();
}
}
session.commit();
session.clearCache();
sw.stop();
log.info("all cost info:{}",sw.prettyPrint());
}

4.3 执行时间

  • 第一次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
09346 100% 批处理执行 插入
  • 第二次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
08890 100% 批处理执行 插入
  • 第三次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
09042 100% 批处理执行 插入

该方式插入20w 条数据,执行三次的平均时间:9092 ms。

4.4 如果数据更大

当我把数据扩大到 100w 时,foreach 拼接 sql 的方式已经无法完成插入了,所以我只能测试批处理的插入时间。

测试时,仅需将 【4.2】测试代码中的 list 切成 lageList 测试即可。

  • 第一次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
32419 100% 批处理执行 插入
  • 第二次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
31935 100% 批处理执行 插入
  • 第三次
1
2
3
4
xml复制代码-----------------------------------------
ms % Task name
-----------------------------------------
33048 100% 批处理执行 插入

该方式插入100w 条数据,执行三次的平均时间:32467 ms。

五、总结

批量插入方式 数据量 执行三次的平均时间
循环插入单条数据 2000 60991 ms
foreach 拼接sql 20w 18852 ms
批处理 20w 9092 ms
批处理 100w 32467 ms
  1. 循环插入单条数据虽然效率极低,但是代码量极少,数据量较小时可以使用,但是数据量较大禁止使用,效率太低了;
  2. foreach 拼接sql的方式,使用时有大段的xml和sql语句要写,很容易出错,虽然效率尚可,但是真正应对大量数据的时候,依旧无法使用,所以不推荐使用;
  3. 批处理执行是有大数据量插入时推荐的做法,使用起来也比较方便。

【本文示例代码】

1
复制代码

本文转载自: 掘金

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

刚出炉热腾腾的定时任务可视化管理系统

发表于 2021-09-14

定时任务也算是我们日常开发中比较常见的需求了,市面上也有很多成熟的框架:

  • quartz
  • elastic-job
  • xxl-job
  • …

不过小伙伴们知道,其实我们 Spring 框架中也提供了相应的定时任务,这个定时任务通过 @EnableScheduling 注解开启,松哥之前也写过文章和大家分享这个注解的基本用法:

  • Spring Boot 中实现定时任务的两种方式!

不过之前的定时任务都是固定的,提前写死的,没法动态配置,前两天有小伙伴问松哥如何实现定时任务的动态配置?

这个东西要是基于 xxl-job 之类的框架来做其实是比较容易的,不过用 Spring 自带的 @EnableScheduling 注解其实也能实现,而且并不难,松哥基于此火急火燎的写了一个,今天先和大家聊聊大致用法,后面抽空再写一篇文章介绍实现原理。

项目已开源,项目地址:

  • github.com/lenve/sched…

食用方式

  1. 克隆项目:git clone https://github.com/lenve/scheduling.git。
  2. 本地数据库创建一个名为 scheduling 的库。
  3. 修改配置文件 src/main/resources/application.yaml,主要修改数据库连接的用户名和地址。
  4. 启动项目。
  5. 浏览器访问 http://localhost:8080,可以看到如下页面:

表示启动成功。

功能介绍

  1. 项目启动时,会自动从数据库中加载状态为 1 的定时任务并开始执行,1 表示处于开启状态的定时任务,0 表示处于禁用状态的定时任务。
  2. 点击页面上的添加作业按钮,可以添加一个新的定时任务,新任务的 Bean 名称、方法名称以及方法参数如果和已有的记录相同,则认为是重复作业,重复作业会添加失败。

添加作业的页面如下:

这里涉及到几个参数,含义如下:

  • Bean 名称:这是项目中注入 Spring 的 Bean 名称,测试代码中以 org/javaboy/scheduling02/service/SchedulingTaskDemo.java 为例。
  • 方法名称:参数 1 中 bean 里边的方法名称。
  • 方法参数:参数 2 中方法的参数。
  • Cron 表达式:定时任务的 Cron 表达式。
  • 作业状态:开启和禁用两种。开启的话,添加完成后这个定时任务就会开始执行,禁用的话,就单纯只是将记录添加到数据库中。

作业添加成功提示如下:

作业添加失败提示如下:

  1. 点击作业编辑,可以修改作业的各项数据:

修改后会立马生效。

  1. 点击作业删除,可以删除一个现有的作业。假如删除的作业正在执行,则先停止该作业,然后删除。
  2. 点击列表中的 switch 按钮也可以切换作业的状态。

技术栈

  • SpringBoot
  • Jpa
  • MySQL
  • Spring Job
  • Vue

其他

这是一个学习的 Demo,并非完整项目,后面松哥会出一篇文章和大家分享具体的实现思路。

好啦,先说这么多。

感兴趣的小伙伴赶紧去体验一把吧:

  • github.com/lenve/sched…
  • gitee.com/lenve/sched…

本文转载自: 掘金

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

求你了,别再说数据库锁的只是索引了!!! Gap Lock

发表于 2021-09-14

GitHub 21.5k Star 的Java工程师成神之路,不来了解一下吗!

GitHub 21.5k Star 的Java工程师成神之路,真的不来了解一下吗!

在MySQL数据库中,为了解决并发问题,引入了很多的锁机制,很多时候,数据库的锁是在有数据库操作的过程中自动添加的。

所以,这就导致很多程序员经常会忽略数据库的锁机制的真正的原理。比如,我经常在面试中会问候选人,你知道MySQL Innodb的锁,到底锁的是什么吗?

关于这个问题的回答,我听到过很多种,但是很少有人可以把他回答的很完美。因为想要回答好这个问题,需要对数据库的隔离级别、索引等都有一定的了解才行。

MySQL Innodb的锁的相关介绍,在MySQL的官方文档(dev.mysql.com/doc/refman/… )中有一定的介绍,本文的介绍也是基于这篇官方文档的。

Record Lock

**Record Lock,翻译成记录锁,是加在索引记录上的锁。**例如,SELECT c1 FROM t WHERE c1 = 10 For UPDATE;会对c1=10这条记录加锁,为了防止任何其他事务插入、更新或删除c1值为10的行。

需要特别注意的是,记录锁锁定的是索引记录。即使表没有定义索引,InnoDB也会创建一个隐藏的聚集索引,并使用这个索引来锁定记录。

Gap Lock

Gap Lock,翻译成间隙锁,他指的是在索引记录之间的间隙上的锁,或者在第一个索引记录之前或最后一个索引记录之后的间隙上的锁。

那么,这里所谓的Gap(间隙)又怎么理解呢?

Gap指的是InnoDB的索引数据结构中可以插入新值的位置。

当你用语句SELECT…FOR UPDATE锁定一组行时。InnoDB可以创建锁,应用于索引中的实际值以及他们之间的间隙。例如,如果选择所有大于10的值进行更新,间隙锁将阻止另一个事务插入大于10的新值。

既然是锁,那么就可能会影响到数据库的并发性,所以,间隙锁只有在Repeatable Reads这种隔离级别中才会起作用。

在Repeatable Reads这种隔离下,对于锁定的读操作(select … for update 、 lock in share mode)、update操作、delete操作时,会进行如下的加锁:

  • 对于具有唯一搜索条件的唯一索引,InnoDB只锁定找到的索引记录,而不会锁定间隙。
  • 对于其他搜索条件,InnoDB锁定扫描的索引范围,使用gap lock或next-key lock来阻塞其他事务插入范围覆盖的间隙。

也就是说,对于SELECT FOR UPDATE、LOCK IN SHARE MODE、UPDATE和DELETE等语句处理时,除了对唯一索引的唯一搜索外都会获取gap锁或next-key锁,即锁住其扫描的范围。

Next-Key Lock

Next-Key锁是索引记录上的记录锁和索引记录之前间隙上的间隙锁的组合。

假设一个索引包含值10、11、13和20。此索引可能的next-key锁包括以下区间:

1
2
3
4
5
复制代码(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, ∞ ]

对于最后一个间隙,∞不是一个真正的索引记录,因此,实际上,这个next-key锁只锁定最大索引值之后的间隙。

所以,Next-Key 的锁的范围都是左开右闭的。

Next-Key Lock和Gap Lock一样,只有在InnoDB的RR隔离级别中才会生效。

Repeatable Reads能解决幻读

很多人看过网上的关于数据库事务级别的介绍,会认为MySQL中Repeatable Reads能解决不可重复读的问题,但是不能解决幻读,只有Serializable才能解决。但其实,这种想法是不对的。

因为MySQL跟标准RR不一样,标准的Repeatable Reads确实存在幻读问题,但InnoDB中的Repeatable Reads是通过next-key lock解决了RR的幻读问题的。

因为我们知道,因为有了next-key lock,所以在需要加行锁的时候,会同时在索引的间隙中加锁,这就使得其他事务无法在这些间隙中插入记录,这就解决了幻读的问题。

关于这个问题,引起过广泛的讨论,可以参考:github.com/Yhzhtk/note… ,这里有很多大神发表过自己的看法。

MySQL的加锁原则

前面介绍过了Record Lock、Gap Lock和Next-Key Lock,但是并没有说明加锁规则。关于加锁规则,我是看了丁奇大佬的《MySQL实战45讲》中的文章之后理解的,他总结的加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”:

原则 1:加锁的基本单位是 next-key lock。是一个前开后闭区间。 原则 2:查找过程中访问到的对象才会加锁。 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

假如,数据库表中当前有以下记录:

当我们执行update t set d=d+1 where id = 7的时候,由于表 t 中没有 id=7 的记录,所以:

  • 根据原则 1,加锁单位是 next-key lock,session A 加锁范围就是 (5,10];
  • 根据优化 2,这是一个等值查询 (id=7),而 id=10 不满足查询条件,next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)。

当我们执行select * from t where id>=10 and id<11 for update的时候:

  • 根据原则 1,加锁单位是 next-key lock,会给 (5,10]加上 next-key lock,范围查找就往后继续找,找到 id=15 这一行停下来
  • 根据优化 1,主键 id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。
  • 根据原则 2,访问到的都要加锁,因此需要加 next-key lock(10,15]。因此最终加的是行锁 id=10 和 next-key lock(10,15]。

当我们执行select * from t where id>10 and id<=15 for update的时候: * 根据原则 1,加锁单位是 next-key lock,会给 (10,15]加上 next-key lock,并且因为 id 是唯一键,所以循环判断到 id=15 这一行就应该停止了。 * 但是,InnoDB 会往前扫描到第一个不满足条件的行为止,也就是 id=20。而且由于这是个范围扫描,因此索引 id 上的 (15,20]这个 next-key lock 也会被锁上。

假如,数据库表中当前有以下记录:

当我们执行select id from t where c=5 lock in share mode的时候:

  • 根据原则 1,加锁单位是 next-key lock,因此会给 (0,5]加上 next-key lock。要注意 c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的,需要向右遍历,查到 c=10 才放弃。
  • 根据原则 2,访问到的都要加锁,因此要给 (5,10]加 next-key lock。
  • 根据优化 2:等值判断,向右遍历,最后一个值不满足 c=5 这个等值条件,因此退化成间隙锁 (5,10)。
  • 根据原则 2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁。

当我们执行select * from t where c>=10 and c<11 for update的时候:

  • 根据原则 1,加锁单位是 next-key lock,会给 (5,10]加上 next-key lock,范围查找就往后继续找,找到 id=15 这一行停下来
  • 根据原则 2,访问到的都要加锁,因此需要加 next-key lock(10,15]。
  • 由于索引 c 是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终 sesion A 加的锁是,索引 c 上的 (5,10] 和 (10,15] 这两个 next-key lock。

总结

以上,我们介绍了InnoDB中的锁机制,一共有三种锁,分别是Record Lock、Gap Lock和Next-Key Lock。

Record Lock表示记录锁,锁的是索引记录。 Gap Lock是间隙锁,说的是索引记录之间的间隙。 Next-Key Lock是Record Lock和Gap Lock的组合,同时锁索引记录和间隙。他的范围是左开右闭的。

InnoDB的RR级别中,加锁的基本单位是 next-key lock,只要扫描到的数据都会加锁。唯一索引上的范围查询会访问到不满足条件的第一个值为止。

同时,为了提升性能和并发度,也有两个优化点:

  • 索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
  • 索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。

关于锁的介绍,就是这么多了,但是其实,RR的隔离级别引入的这些锁,虽然一定程度上可解决很多如幻读这样的问题,但是也会带来一些副作用,比如并发度降低、容易导致死锁等。

后面我们再来单独介绍一下为什么RR作为InnoDB的默认级别,却”不受待见”,很多大厂都会把数据库默认级别修改为RC。

关于作者:Hollis,一个对Coding有着独特追求的人,阿里巴巴技术专家,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者。

关注公众号【Hollis】,后台回复”成神导图”可以咯领取Java工程师进阶思维导图。

本文转载自: 掘金

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

为什么人家的开源项目文档如此炫酷?原来用的是这款神器!

发表于 2021-09-14

之前有很多朋友问我,我的开源项目文档网站是用什么搭建的。其实是用Docsify搭建的,具体可以参考如何写出优雅的开源项目文档 。Docsify用来搭建成体系的文档网站基本够用了,但有时候我们既有成体系的文章、又有碎片化的文章,如果把文章都挂上去,看起来未免有些凌乱,这时候我们可能需要搭建一个类似知识库的网站了。最近发现使用VuePress可以搭建一个功能强大的文档网站,推荐给大家!

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

VuePress简介

VuePress是Vue驱动的静态网站生成器。对比我们的Docsify动态生成网站,对SEO更加友好。

使用VuePress具有如下优点:

  • 使用Markdown来写文章,程序员写起来顺手,配置网站非常简洁。
  • 我们可以在Markdown中使用Vue组件,如果你熟悉Vue的话会非常方便。
  • 打包网站时会为每个页面预渲染生成静态的HTML,性能好,也有利于SEO。

Vdoing主题

一般我们使用VuePress搭建网站的时候,都会选择一个主题。这里选择的是vuepress-theme-vdoing,一款简洁高效的知识管理&博客主题,用来搭建文档网站绰绰有余。

学了技术老忘怎么破?用Vdoing搭建一个知识库试试!它能帮助我们更好地管理知识,并能够快速地把遗忘的知识点找回来。

使用Vdoing主题具有如下优点:

  • 知识管理:由于该主题具有目录、分类、标签等功能,可以方便地整合结构化或碎片化的内容。
  • 简洁高效:以 Markdown 为中心的项目结构,内置自动化工具,以更少的配置完成更多的事。
  • 沉浸式阅读:专为阅读设计的UI,配合多种颜色模式、可关闭的侧边栏和导航栏,带给你一种沉浸式阅读体验。

效果演示

我们先来看下成品效果,有三种不同模式可供选择,是不是够炫酷!

搭建

通过Vdoing搭建网站非常简单,就算你对Vue不熟悉也没关系。

  • 首先我们需要去Vdoing的官网下载项目,下载地址:github.com/xugaoyi/vue…

  • 下载完成后导入IDEA中,由于是Vue项目,导入成功后需要使用如下命令来安装依赖,之后以dev模式运行;
1
2
3
4
bash复制代码# 安装
npm install
# 运行
npm run dev
  • 运行成功后,随便找个文章进去体验下,界面还是挺不错的,访问地址:http://localhost:8080/

  • 还支持主题切换,比如切换到深色模式。

配置

由于Vdoing本身就是个完整的网站了,里面有很多我们不需要的文章和配置,所以我们得替换这些文章并自定义这些配置。

  • 我们先来看下我自定义后的首页效果,这个主题还是非常简洁的,看起来挺舒服;

  • 项目文件基本都在docs目录下,我们来看下这些文件的作用;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
lua复制代码docs
│ index.md -- 首页配置
├─.vuepress -- 用于存放全局的配置、组件、静态资源等
│ │ config.js -- 配置文件的入口文件
│ │ enhanceApp.js -- 客户端应用的增强
│ ├─config
│ │ head.js -- 注入到页面<head>中的配置
│ │ htmlModules.js -- 插入自定义html模块
│ │ nav.js -- 顶部导航栏配置
│ │ plugins.js -- 插件配置
│ │ themeConfig.js -- 主题配置
│ ├─public -- 静态资源目录
│ │ └─img -- 用于存放图片
│ ├─styles
│ │ palette.styl -- 主题演示配置
│ └─<结构化目录>
├─@pages --自动生成的文件夹
│ archivesPage.md -- 归档页
│ categoriesPage.md -- 分类页
│ tagsPage.md -- 标签页
├─images -- 可以用来存放自己的图片
└─_posts -- 专门存放碎片化博客文章的文件夹,不会自动生成目录
  • 大家都用过SpringBoot,有约定优于配置的说法,Vdoing也有这种说法,如果我们想要根据目录结构自动生成文章目录的话,需要给目录和文件添加序号,比如下面的目录;

  • 在此目录结构下,一级目录被称为专栏,二级目录为专栏内容,专栏之间是相互独立的,上面的目录结构将生成如下结构的侧边栏,同时也会生成右侧的大纲栏;

  • 如果你还想给专栏添加个目录页的话,可以在00.目录页文件夹中添加02.mall学习教程.md作为目录,目录页内容如下,permalink可以指定目录页的永久路径;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
markdown复制代码---
pageComponent:
name: Catalogue
data:
key: 02.mall学习教程
imgUrl: /img/ui.png
description: mall学习教程,架构、业务、技术要点全方位解析。
title: mall学习教程
date: 2020-03-11 21:50:54
permalink: /mall-learning
sidebar: false
article: false
comment: false
editLink: false
---
  • 接下来通过如下地址就可以访问目录页了:http://localhost:8080/mall-learning/

  • 当然你也可以修改导航栏配置nav.js,这样会访问起来会方便许多;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码module.exports = [
{ text: '首页', link: '/' },
{
text: 'mall学习教程',
link: '/mall-learning/',
items: [
{ text: '序章', link: '/pages/72bed2/' },
{ text: '架构篇', link: '/pages/c68875/' },
{ text: '业务篇', link: '/pages/c981c1/' },
{ text: '技术要点篇', link: '/pages/fab7d9/' },
{ text: '部署篇', link: '/pages/db2d1e/' },
],
}
]
  • 添加成功后导航栏显示效果如下,点击导航栏即可跳转到该目录;

  • 其实你还可以在首页index.md中通过添加feature来实现快速访问,这里我们创建了三个feature;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
markdown复制代码---
home: true
# heroImage: /img/web.png
heroText: macrozheng's blog
tagline: Java后端技术博客,积跬步以至千里,致敬每个爱学习的你。

features: # 可选的
- title: mall学习教程
details: mall学习教程,架构、业务、技术要点全方位解析。
link: /mall-learning/
imgUrl: /img/ui.png
- title: SpringCloud学习教程
details: 一套涵盖大部分核心组件使用的Spring Cloud教程,包括Spring Cloud Alibaba及分布式事务Seata。
link: /springcloud-learning/
imgUrl: /img/other.png
- title: K8S学习教程
details: 实实在在的K8S实战教程,专为Java方向人群打造!
link: /springcloud-learning/ # 可选
imgUrl: /img/web.png # 可选
---
  • 首页显示效果如下;

  • 每次我们创建文章的Markdown文件时,会自动生成front matter,比如下面这个格式;
1
2
3
4
5
6
7
8
9
10
11
markdown复制代码---
title: mall整合SpringBoot+MyBatis搭建基本骨架
date: 2021-08-19 16:30:11
permalink: /pages/c68875/
categories:
- mall学习教程
- 架构篇
tags:
- SpringBoot
- MyBatis
---
  • 下面分别介绍下这些属性的作用:
    • title:文章标题,默认为文件名称;
    • date:文章日期,默认为文件创建日期;
    • permalink:文件访问永久链接,可以自行修改;
    • categories:文章的分类,根据目录会自动生成;
    • tags:文章标签,方便碎片化文章的查找。
  • 如果你仔细看下文章列表的话,可以发现有的文章会显示摘要,而有的不会显示,我们可以通过<!-- more -->注释来控制摘要的显示,该注释之前内容均会作为摘要来显示;

  • 文章列表是否显示摘要对比如下;

  • 如果你想修改文章作者信息、侧边栏是否折叠、社交信息、页面底部版权信息等信息的话,可以修改主题配置文件themeConfig.js;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
js复制代码// 主题配置
module.exports = {
nav,
sidebarDepth: 2, // 侧边栏显示深度,默认1,最大2(显示到h3标题)
logo: '/img/avatar.png', // 导航栏logo
repo: 'macrozheng', // 导航栏右侧生成Github链接
searchMaxSuggestions: 10, // 搜索结果显示最大数
lastUpdated: '上次更新', // 开启更新时间,并配置前缀文字 string | boolean (取值为git提交时间)
docsDir: 'docs', // 编辑的文件夹
editLinks: false, // 启用编辑
editLinkText: '编辑',

sidebar: { mode: 'structuring', collapsable: false}, // 侧边栏 'structuring' | { mode: 'structuring', collapsable: Boolean} | 'auto' | 自定义 温馨提示:目录页数据依赖于结构化的侧边栏数据,如果你不设置为'structuring',将无法使用目录页

author: {
// 文章默认的作者信息,可在md文件中单独配置此信息 String | {name: String, link: String}
name: 'macrozheng', // 必需
link: 'https://github.com/macrozheng', // 可选的
},
blogger: {
// 博主信息,显示在首页侧边栏
avatar: '/img/avatar.png',
name: 'macrozheng',
slogan: '这家伙很懒,什么都没写...',
},
social: {
// 社交图标,显示于博主信息栏和页脚栏
// iconfontCssFile: '//at.alicdn.com/t/font_1678482_u4nrnp8xp6g.css', // 可选,阿里图标库在线css文件地址,对于主题没有的图标可自由添加
icons: [
{
iconClass: 'icon-github',
title: 'GitHub',
link: 'https://github.com/macrozheng',
},
{
iconClass: 'icon-gitee',
title: 'Gitee',
link: 'https://gitee.com/macrozheng',
},
{
iconClass: 'icon-juejin',
title: '掘金',
link: 'https://juejin.cn/user/958429871749192',
}
],
},
footer: {
// 页脚信息
createYear: 2019, // 博客创建年份
copyrightInfo:
'marcozheng | <a href="https://github.com/xugaoyi/vuepress-theme-vdoing/blob/master/LICENSE" target="_blank">MIT License</a>', // 博客版权信息,支持a标签
},
htmlModules // 插入html模块
}
  • Vdoing添加了很多插件,有些你用不上,可以通过修改plugins.js来禁用,比如禁用下百度统计插件,第二个参数改为false就行了;
1
2
3
4
5
6
7
8
9
10
js复制代码// 插件配置
module.exports = [
[
'vuepress-plugin-baidu-tongji', // 百度统计
false, //禁用
{
hm: 'xxx',
},
],
]
  • Vdoing浅色主题默认代码块主题也是浅色主题,我们可以通过修改palette.styl文件来改成深色主题;
1
2
3
4
5
6
7
8
9
10
sass复制代码 // 浅色模式
.theme-mode-light
// 代码块浅色主题
//--codeBg: #f6f6f6
//--codeColor: #525252
//codeThemeLight()
// 代码块深色主题
--codeBg: #252526
--codeColor: #fff
codeThemeDark()
  • 我们放在docs一级目录下的带序号的专栏默认会生成目录,如果我们有些碎片化的文章不想生成结构化目录的话,可以放在_posts目录下;

  • Linux命令这篇文章就没有生成结构化目录,只是使用文章中的二级标题生成了个目录。

部署

VuePress生成网站也非常简单,一个命令完成打包,然后放置到Nginx的html目录下即可。

  • 在命令行使用npm run build命令可以将项目打包成静态文件,输出文件目录为docs/.vuepress/dist;

  • 接下来把dist目录下的所有文件复制到Nginx的html目录下即可完成部署,部署后显示内容如下。

总结

使用VuePress+Vdoing来搭建文档网站不仅炫酷而且功能强大!对比Docsify的动态生成文档,VuePress生成静态页面性能更好,同时对SEO也更友好。 如果你只想搭建简单的单项目文档的话,Docsify基本上也够用了。如果你想搭建多项目文档,或者博客网站的话,还是推荐你使用VuePress的。

参考资料

  • vuepress-theme-vdoing主题官网:doc.xugaoyi.com/
  • VuePress官网:vuepress.vuejs.org/zh/

项目地址

github.com/xugaoyi/vue…

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

本文转载自: 掘金

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

源码解读Dubbo分层设计思想 一、Dubbo分层整体设计概

发表于 2021-09-14

一、Dubbo分层整体设计概述

我们先从下图开始简单介绍Dubbo分层设计概念:

(引用自Duboo开发指南-框架设计文档)

如图描述Dubbo实现的RPC整体分10层:service、config、proxy、registry、cluster、monitor、protocol、exchange、transport、serialize。

service:使用方定义的接口和实现类;

config:负责解析Dubbo定义的配置,比如注解和xml配置,各种参数;

proxy:主要负责生成消费者和提供者的代理对象,加载框架功能,比如提供者过滤器链,扩展点;

registry:负责注册服务的定义和实现类的装载;

cluster:只有消费者有这么一层,负责包装多个服务提供者成一个‘大提供者’,加载负载均衡、路有等扩展点;

monitor:定义监控服务,加载监控实现提供者;

protocol:封装RPC调用接口,管理调用实体的生命周期;

exchange:封装请求响应模式,同步转异步;

transport:抽象传输层模型,兼容netty、mina、grizzly等通讯框架;

serialize:抽象序列化模型,兼容多种序列化框架,包括:fastjson、fst、hessian2、kryo、kryo2、protobuf等,通过序列化支持跨语言的方式,支持跨语言的rpc调用;

Dubbo这么分层的目的在于实现层与层之间的解耦,每一层都定义了接口规范,也可以根据不同的业务需求定制、加载不同的实现,具有极高的扩展性。

1.1. RPC调用过程

接下来结合上图简单描述一次完整的rpc调用过程:

从Dubbo分层的角度看,详细时序图如下,蓝色部分是服务消费端,浅绿色部分是服务提供端,时序图从消费端一次Dubbo方法调用开始,到服务端本地方法执行结束。

从Dubbo核心领域对象的角度看,我们引用Dubbo官方文档说明,如下图所示。Dubbo核心领域对象是Invoker,消费端代理对象是proxy,包装了Invoker的调用;服务端代理对象是一个Invoker,他通过exporter包装,当服务端接收到调用请求后,通过exporter找到Invoker,Invoker去实际执行用户的业务逻辑。

(引用自Dubbo官方文档)

1.2 Dubbo服务的注册和发现流程

下图出自开发指南-框架设计-引用服务时序,主要流程是:从注册中心订阅服务提供者,然后启动tcp服务连接远端提供者,将多个服务提供者合并成一个Invoker,用这个Invoker创建代理对象。

下图出自开发指南-框架设计-暴露服务时序,主要流程是:创建本地服务的代理Invoker,启动tcp服务暴露服务,然后将服务注册到注册中心。

接下来我们结合Dubbo服务的注册和发现,从配置层开始解释每一层的作用和原理。

示例服务接口定义如下:

1
2
3
4
5
6
7
java复制代码public interface CouponServiceViewFacade {

/**
* 查询单张优惠券
*/
CouponViewDTO query(String code);
}

二、配置层

2.1. 做什么

配置层提供配置处理工具类,在容器启动的时候,通过ServiceConfig.export实例化服务提供者,ReferenceConfig.get实例化服务消费者对象。

Dubbo应用使用spring容器启动时,Dubbo服务提供者配置处理器通过ServiceConfig.export启动Dubbo远程服务暴露本地服务。Dubbo服务消费者配置处理器通过ReferenceConfig.get实例化一个代理对象,并通过注册中心服务发现,连接远端服务提供者。

Dubbo配置可以使用注解和xml两种形式,本文采用注解的形式进行说明。

2.2. 怎么做

2.2.1 服务消费端的解析

Spring容器启动过程中,填充bean属性时,对含有Dubbo引用注解的属性使用org.apache.dubbo.config.spring.beans.factory.annotation.ReferenceAnnotationBeanPostProcessor进行初始化。如下是ReferenceAnnotationBeanPostProcessor的构造方法,Dubbo服务消费者注解处理器处理以下三个注解:DubboReference.class、Reference.class、com.alibaba.dubbo.config.annotation.Reference.class修饰的类。

ReferenceAnnotationBeanPostProcessor类定义:

1
2
3
4
5
6
7
java复制代码public class ReferenceAnnotationBeanPostProcessor extends AbstractAnnotationBeanPostProcessor implements
ApplicationContextAware {

public ReferenceAnnotationBeanPostProcessor() {
super(DubboReference.class, Reference.class, com.alibaba.dubbo.config.annotation.Reference.class);
}
}

Dubbo服务发现到这一层,Dubbo即将开始构建服务消费者的代理对象,CouponServiceViewFacade接口的代理实现类。

2.2.2 服务提供端的解析

Spring容器启动的时候,加载注解@org.apache.dubbo.config.spring.context.annotation.DubboComponentScan指定范围的类,并初始化;初始化使用dubbo实现的扩展点org.apache.dubbo.config.spring.beans.factory.annotation.ServiceClassPostProcessor。

ServiceClassPostProcessor处理的注解类有DubboService.class,Service.class,com.alibaba.dubbo.config.annotation.Service.class。

如下是ServiceClassPostProcessor类定义:

1
2
3
4
5
6
7
8
java复制代码public class ServiceClassPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware,
ResourceLoaderAware, BeanClassLoaderAware {

private final static List<Class<? extends Annotation>> serviceAnnotationTypes = asList(
DubboService.class,Service.class,com.alibaba.dubbo.config.annotation.Service.class
);
。。。
}

等待Spring容器ContextRefreshedEvent事件,启动Dubbo应用服务监听端口,暴露本地服务。

Dubbo服务注册到这一层,Dubbo即将开始构建服务提供者的代理对象,CouponServiceViewFacade实现类的反射代理类。

三、 代理层

3.1 做什么

为服务消费者生成代理实现实例,为服务提供者生成反射代理实例。

CouponServiceViewFacade的代理实现实例,消费端在调用query方法的时候,实际上是调用代理实现实例的query方法,通过他调用远程服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.apache.dubbo.common.bytecode;

public class proxy1 implements DC, Destroyable, CouponServiceViewFacade, EchoService {
public static Method[] methods;
private InvocationHandler handler;

public proxy1(InvocationHandler var1) {
this.handler = var1;
}

public proxy1() {
}

public CouponViewDTO query(String var1) {
Object[] var2 = new Object[]{var1};
Object var3 = this.handler.invoke(this, methods[0], var2);
return (CouponViewDTO)var3;
}
}

CouponServiceViewFacade的反射代理实例,服务端接收到请求后,通过该实例的Invoke方法最终执行本地方法query。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码/**
* InvokerWrapper
*/
public class AbstractProxyInvoker<CouponServiceViewFacade> implements Invoker<CouponServiceViewFacade> {
// 。。。

public AbstractProxyInvoker(CouponServiceViewFacade proxy, Class<CouponServiceViewFacade> type, URL url) {
//。。。
this.proxy = proxy;
this.type = type;
this.url = url;
}

@Override
public Result invoke(Invocation invocation) throws RpcException {
//。。。
Object value = doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
//。。。
}

protected Object doInvoke(CouponServiceViewFacade proxy, String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Throwable{
//。。。
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}

}

3.2 怎么做

Dubbo代理工厂接口定义如下,定义了服务提供者和服务消费者的代理对象工厂方法。服务提供者代理对象和服务消费者代理对象都是通过工厂方法创建,工厂实现类可以通过SPI自定义扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@SPI("javassist")
public interface ProxyFactory {

// 生成服务消费者代理对象
@Adaptive({PROXY_KEY})
<T> T getProxy(Invoker<T> invoker) throws RpcException;

// 生成服务消费者代理对象
@Adaptive({PROXY_KEY})
<T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException;


// 生成服务提供者代理对象
@Adaptive({PROXY_KEY})
<T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) throws RpcException;

}

3.2.1 服务消费者

3.2.1.1 创建服务消费者代理类

默认采用Javaassist代理工厂实现,Proxy.getProxy(interfaces)创建代理工厂类,newInstance创建具体代理对象。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class JavassistProxyFactory extends AbstractProxyFactory {

@Override
@SuppressWarnings("unchecked")
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
}

。。。

}

3.2.1.2 服务消费者代理

Dubbo为每个服务消费者生成两个代理类:代理工厂类,接口代理类。

CouponServiceViewFacade代理工厂类:

1
2
3
4
5
6
7
8
java复制代码public class Proxy1 extends Proxy implements DC {
public Proxy1() {
}

public Object newInstance(InvocationHandler var1) {
return new proxy1(var1);
}
}

最终生成的CouponServiceViewFacade的代理对象如下,其中handler的实现类是InvokerInvocationHandler,this.handler.invoke方法发起Dubbo调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.apache.dubbo.common.bytecode;

public class proxy1 implements DC, Destroyable, CouponServiceViewFacade, EchoService {
public static Method[] methods;
private InvocationHandler handler;

public proxy1(InvocationHandler var1) {
this.handler = var1;
}

public proxy1() {
}

public CouponViewDTO query(String var1) {
Object[] var2 = new Object[]{var1};
Object var3 = this.handler.invoke(this, methods[0], var2);
return (CouponViewDTO)var3;
}
}

3.2.2 服务提供者

3.2.2.1 创建服务提供者代理类

默认Javaassist代理工厂实现,使用Wrapper包装本地服务提供者。proxy是实际的服务提供者实例,即CouponServiceViewFacade的本地实现类,type是接口类定义,URL是injvm协议URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class JavassistProxyFactory extends AbstractProxyFactory {

。。。

@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// 代理包装类,包装了本地的服务提供者
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
// 代理类入口
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}

}

3.2.2.2 Wrapper包装类

Dubbo为每个服务提供者的本地实现生成一个Wrapper代理类,抽象Wrapper类定义如下:

1
2
3
4
5
java复制代码public abstract class Wrapper {
。。。

abstract public Object invokeMethod(Object instance, String mn, Class<?>[] types, Object[] args) throws NoSuchMethodException, InvocationTargetException;
}

具体Wrapper代理类使用字节码技术动态生成,本地服务CouponServiceViewFacade的代理包装类举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.apache.dubbo.common.bytecode;

import com.xxx.CouponServiceViewFacade;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import org.apache.dubbo.common.bytecode.ClassGenerator.DC;

public class Wrapper25 extends Wrapper implements DC {
。。。

public Wrapper25() {
}

public Object invokeMethod(Object var1, String var2, Class[] var3, Object[] var4) throws InvocationTargetException {
CouponServiceViewFacade var5;
try {
var5 = (CouponServiceViewFacade)var1;
} catch (Throwable var8) {
throw new IllegalArgumentException(var8);
}

try {
if ("query".equals(var2) && var3.length == 1) {
return var5.query((String)var4[0]);
}
} catch (Throwable var9) {
throw new InvocationTargetException(var9);
}

throw new NoSuchMethodException("Not found method \"" + var2 + "\" in class com.xxx.CouponServiceViewFacade.");
}


。。。

}

在服务初始化流程中,服务消费者代理对象生成后初始化就完成了,服务消费端的初始化顺序:ReferenceConfig.get->从注册中心订阅服务->启动客户端->创建DubboInvoker->构建ClusterInvoker→创建服务代理对象;

而服务提供端的初始化才刚开始,服务提供端的初始化顺序:ServiceConfig.export->创建AbstractProxyInvoker,通过Injvm协议关联本地服务->启动服务端→注册服务到注册中心。

接下来我们讲注册层。

四、注册层

4.1 做什么

封装服务地址的注册与发现,以服务 URL 为配置中心。服务提供者本地服务启动成功后,监听Dubbo端口成功后,通过注册协议发布到注册中心;服务消费者通过注册协议订阅服务,启动本地应用连接远程服务。

注册协议URL举例:

zookeeper://xxx/org.apache.dubbo.registry.RegistryService?application=xxx&…

4.2 怎么做

注册服务工厂接口定义如下,注册服务实现通过SPI扩展,默认是zk作为注册中心。

1
2
3
4
5
6
7
java复制代码@SPI("dubbo")
public interface RegistryFactory {

@Adaptive({"protocol"})
Registry getRegistry(URL url);

}

注册服务接口定义;

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


void register(URL url);


void unregister(URL url);


void subscribe(URL url, NotifyListener listener);


void unsubscribe(URL url, NotifyListener listener);


List<URL> lookup(URL url);

}

五、集群层

5.1 做什么

服务消费方从注册中心订阅服务提供者后,将多个提供者包装成一个提供者,并且封装路由及负载均衡策略;并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance;

服务提供端不存在集群层。

5.2 怎么做

5.2.1 Cluster

集群领域主要负责将多个服务提供者包装成一个ClusterInvoker,注入路由处理器链和负载均衡策略。主要策略有:failover、failfast、failsafe、failback、forking、available、mergeable、broadcast、zone-aware。

集群接口定义如下,只有一个方法:从服务目录中的多个服务提供者构建一个ClusterInvoker。

作用是对上层-代理层屏蔽集群层的逻辑;代理层调用服务方法只需执行Invoker.invoke,然后通过ClusterInvoker内部的路由策略和负载均衡策略计算具体执行哪个远端服务提供者。

1
2
3
4
5
6
7
8
9
java复制代码@SPI(Cluster.DEFAULT)
public interface Cluster {
String DEFAULT = FailoverCluster.NAME;

@Adaptive
<T> Invoker<T> join(Directory<T> directory) throws RpcException;

。。。
}

ClusterInvoker执行逻辑,先路由策略过滤,然后负载均衡策略选择最终的远端服务提供者。示例代理如下:

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

。。。
@Override
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();

// binding attachments into invocation.
Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
}

// 集群invoker执行时,先使用路由链过滤服务提供者
List<Invoker<T>> invokers = list(invocation);
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}
。。。

}

5.2.2 Directory

服务目录接口定义如下,Dubbo方法接口调用时,将方法信息包装成invocation,通过Directory.list过滤可执行的远端服务。

通过org.apache.dubbo.registry.integration.RegistryDirectory桥接注册中心,监听注册中心的路由配置修改、服务治理等事件。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public interface Directory<T> extends Node {


Class<T> getInterface();

List<Invoker<T>> list(Invocation invocation) throws RpcException;

List<Invoker<T>> getAllInvokers();

URL getConsumerUrl();

}

5.2.3 Router

从已知的所有服务提供者中根据路由规则刷选服务提供者。

服务订阅的时候初始化路由处理器链,调用远程服务的时候先使用路由链过滤服务提供者,再通过负载均衡选择具体的服务节点。

路由处理器链工具类,提供路由筛选服务,监听更新服务提供者。

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

。。。

public List<Invoker<T>> route(URL url, Invocation invocation) {
List<Invoker<T>> finalInvokers = invokers;
for (Router router : routers) {
finalInvokers = router.route(finalInvokers, url, invocation);
}
return finalInvokers;
}

/**
* Notify router chain of the initial addresses from registry at the first time.
* Notify whenever addresses in registry change.
*/
public void setInvokers(List<Invoker<T>> invokers) {
//路由链监听更新服务提供者
this.invokers = (invokers == null ? Collections.emptyList() : invokers);
routers.forEach(router -> router.notify(this.invokers));
}

}

订阅服务的时候,将路由链注入到RegistryDirectory中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class RegistryProtocol implements Protocol {
。。。

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
。。。
// 服务目录初始化路由链
directory.buildRouterChain(subscribeUrl);
directory.subscribe(toSubscribeUrl(subscribeUrl));
。。。
return registryInvokerWrapper;
}

。。。

}

5.2.4 LoadBalance

根据不同的负载均衡策略从可使用的远端服务实例中选择一个,负责均衡接口定义如下:

1
2
3
4
5
6
7
java复制代码@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

六、监控层

6.1 做什么

监控RPC调用次数和调用时间,以Statistics为中心,扩展接口为 MonitorFactory, Monitor, MonitorService。

6.2 怎么做

监控工厂接口定义,通过SPI方式进行扩展;

1
2
3
4
5
6
7
8
9
10
11
java复制代码@SPI("dubbo")
public interface MonitorFactory {


@Adaptive("protocol")
Monitor getMonitor(URL url);

}

@Adaptive("protocol")
Monitor getMonitor(URL url);

监控服务接口定义如下,定义了一些默认的监控维度和指标项;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码public interface MonitorService {

// 监控维度

String APPLICATION = "application";

String INTERFACE = "interface";

String METHOD = "method";

String GROUP = "group";

String VERSION = "version";

String CONSUMER = "consumer";

String PROVIDER = "provider";

String TIMESTAMP = "timestamp";

//监控指标项

String SUCCESS = "success";

String FAILURE = "failure";

String INPUT = INPUT_KEY;

String OUTPUT = OUTPUT_KEY;

String ELAPSED = "elapsed";

String CONCURRENT = "concurrent";

String MAX_INPUT = "max.input";

String MAX_OUTPUT = "max.output";

String MAX_ELAPSED = "max.elapsed";

String MAX_CONCURRENT = "max.concurrent";

void collect(URL statistics);

List<URL> lookup(URL query);

}

6.2.1 MonitorFilter

通过过滤器的方式收集服务的调用次数和调用时间,默认实现:

org.apache.dubbo.monitor.dubbo.DubboMonitor。

七、协议层

7.1 做什么

封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter。

接下来介绍Dubbo RPC过程中的常用概念:

1)Invocation是请求会话领域模型,每次请求有相应的Invocation实例,负责包装dubbo方法信息为请求参数;

2)Result是请求结果领域模型,每次请求都有相应的Result实例,负责包装dubbo方法响应;

3)Invoker是实体域,代表一个可执行实体,有本地、远程、集群三类;

4)Exporter服务提供者Invoker管理实体;

5)Protocol是服务域,管理Invoker的生命周期,提供服务的暴露和引用入口;

服务初始化流程中,从这一层开始进行远程服务的暴露和连接引用。

对于CouponServiceViewFacade服务来说,服务提供端会监听Dubbo端口启动tcp服务;服务消费端通过注册中心发现服务提供者信息,启动tcp服务连接远端提供者。

7.2 怎么做

协议接口定义如下,统一抽象了不同协议的服务暴露和引用模型,比如InjvmProtocol只需将Exporter,Invoker关联本地实现。DubboProtocol暴露服务的时候,需要监控本地端口启动服务;引用服务的时候,需要连接远端服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@SPI("dubbo")
public interface Protocol {


int getDefaultPort();


@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;


@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;


void destroy();


default List<ProtocolServer> getServers() {
return Collections.emptyList();
}

}

Invoker接口定义

Invocation是RPC调用的会话对象,负责包装请求参数;Result是RPC调用的结果对象,负责包装RPC调用的结果对象,包括异常类信息;

1
2
3
4
5
6
7
8
9
java复制代码public interface Invoker<T> extends Node {


Class<T> getInterface();


Result invoke(Invocation invocation) throws RpcException;

}

7.2.1 服务的暴露和引用

服务暴露的时候,开启RPC服务端;引用服务的时候,开启RPC客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码public class DubboProtocol extends AbstractProtocol {

。。。

@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
。。。
// 开启rpc服务端
openServer(url);
optimizeSerialization(url);

return exporter;
}

@Override
public <T> Invoker<T> protocolBindingRefer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url);

// 创建dubbo invoker,开启rpc客户端
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker);

return invoker;
}
。。。

}

7.2.2 服务端响应请求

接收响应请求;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {

@Override
public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
。。。
Invocation inv = (Invocation) message;
Invoker<?> invoker = getInvoker(channel, inv);

RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
//调用本地服务
Result result = invoker.invoke(inv);
return result.thenApply(Function.identity());
}

。。。
};

7.2.3 客户端发送请求

调用远程服务;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public class DubboInvoker<T> extends AbstractInvoker<T> {

。。。

@Override
protected Result doInvoke(final Invocation invocation) throws Throwable {
。。。
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
int timeout = calculateTimeout(invocation, methodName);
if (isOneway) {
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else {
ExecutorService executor = getCallbackExecutor(getUrl(), inv);
CompletableFuture<AppResponse> appResponseFuture =
currentClient.request(inv, timeout, executor).thenApply(obj -> (AppResponse) obj);
FutureContext.getContext().setCompatibleFuture(appResponseFuture);
AsyncRpcResult result = new AsyncRpcResult(appResponseFuture, inv);
result.setExecutor(executor);
return result;
}

}

}

八、交换层

8.1 做什么

封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer。

使用request包装Invocation作为完整的请求对象,使用response包装result作为完整的响应对象;Request、Response相比Invocation、Result添加了Dubbo的协议头。

8.2 怎么做

交换器对象接口定义,定义了远程服务的绑定和连接,使用SPI方式进行扩展;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@SPI(HeaderExchanger.NAME)
public interface Exchanger {


@Adaptive({Constants.EXCHANGER_KEY})
ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException;


@Adaptive({Constants.EXCHANGER_KEY})
ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException;

}

@Adaptive({Constants.EXCHANGER_KEY})
ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException;


@Adaptive({Constants.EXCHANGER_KEY})
ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException;

交换层模型类图:

8.2.1 服务提供者

服务提供端接收到请求后,本地执行,发送响应结果;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码public class HeaderExchangeHandler implements ChannelHandlerDelegate {


。。。


void handleRequest(final ExchangeChannel channel, Request req) throws RemotingException {
//封装响应
Response res = new Response(req.getId(), req.getVersion());
。。。
Object msg = req.getData();
try {
CompletionStage<Object> future = handler.reply(channel, msg);
future.whenComplete((appResult, t) -> {
try {
if (t == null) {
res.setStatus(Response.OK);
res.setResult(appResult);
} else {
res.setStatus(Response.SERVICE_ERROR);
res.setErrorMessage(StringUtils.toString(t));
}
channel.send(res);
} catch (RemotingException e) {
logger.warn("Send result to consumer failed, channel is " + channel + ", msg is " + e);
}
});
} catch (Throwable e) {
res.setStatus(Response.SERVICE_ERROR);
res.setErrorMessage(StringUtils.toString(e));
channel.send(res);
}
}
。。。
}

8.2.2 服务消费者

服务消费端发起请求的封装,方法执行成功后,返回一个future;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码final class HeaderExchangeChannel implements ExchangeChannel {

。。。

//封装请求实体
@Override
public CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException {
。。。


// create request.
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
//RpcInvocation
req.setData(request);
DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout, executor);
try {
channel.send(req);
} catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}
。。。

}

九、传输层

9.1 做什么

抽象传输层模型,兼容netty、mina、grizzly等通讯框架。

9.2 怎么做

传输器接口定义如下,它与交换器Exchanger接口定义相似,区别在于Exchanger是围绕Dubbo的Request和Response封装的操作门面接口,而Transporter更加的底层,Exchanger用于隔离Dubbo协议层和通讯层。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@SPI("netty")
public interface Transporter {


@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException;


@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
Client connect(URL url, ChannelHandler handler) throws RemotingException;

}

自定义传输层模型

通过SPI的方式,动态选择具体的传输框架,默认是netty;

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

。。。

public static RemotingServer bind(URL url, ChannelHandler... handlers) throws RemotingException {
。。。

return getTransporter().bind(url, handler);
}


public static Client connect(URL url, ChannelHandler... handlers) throws RemotingException {
。。。
return getTransporter().connect(url, handler);
}

public static Transporter getTransporter() {
return ExtensionLoader.getExtensionLoader(Transporter.class).getAdaptiveExtension();
}

}

netty框架的channel适配如下,采用装饰模式,使用netty框架的channel作为Dubbo自定义的channel做实现;

1
2
3
4
5
6
7
8
9
10
11
java复制代码final class NettyChannel extends AbstractChannel {

private NettyChannel(Channel channel, URL url, ChannelHandler handler) {
super(url, handler);
if (channel == null) {
throw new IllegalArgumentException("netty channel == null;");
}
this.channel = channel;
}

}

十、序列化

10.1 做什么

抽象序列化模型,兼容多种序列化框架,包括:fastjson、fst、hessian2、kryo、kryo2、protobuf等,通过序列化支持跨语言的方式,支持跨语言的RPC调用。

10.2 怎么做

定义Serialization扩展点,默认hessian2,支持跨语言。Serialization接口实际是一个工厂接口,通过SPI扩展;实际序列化和反序列化工作由ObjectOutput,ObjectInput完成,通过装饰模式让hessian2完成实际工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@SPI("hessian2")
public interface Serialization {


byte getContentTypeId();


String getContentType();


@Adaptive
ObjectOutput serialize(URL url, OutputStream output) throws IOException;


@Adaptive
ObjectInput deserialize(URL url, InputStream input) throws IOException;

}

10.2.1 通讯协议设计

下图出自开发指南-实现细节-远程通讯细节,描述Dubbo协议头设计;

  • 0-15bit表示Dubbo协议魔法数字,值:0xdabb;
  • 16bit请求响应标记,Request - 1; Response - 0;
  • 17bit请求模式标记,只有请求消息才会有,1表示需要服务端返回响应;
  • 18bit是事件消息标记,1表示该消息是事件消息,比如心跳消息;
  • 19-23bit是序列化类型标记,hessian序列化id是2,fastjson是6,详见org.apache.dubbo.common.serialize.Constants;
  • 24-31bit表示状态,只有响应消息才有用;
  • 32-64bit是RPC请求ID;
  • 96-128bit是会话数据长度;
  • 128是消息体字节序列;

十一、总结

Dubbo将RPC整个过程分成核心的代理层、注册层、集群层、协议层、传输层等,层与层之间的职责边界明确;核心层都通过接口定义,不依赖具体实现,这些接口串联起来形成了Dubbo的骨架;这个骨架也可以看作是Dubbo的内核,内核使用SPI 机制加载插件(扩展点),达到高度可扩展。

vivo互联网服务器团队-Wang Genfu

本文转载自: 掘金

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

密码学系列之 海绵函数sponge function 简介

发表于 2021-09-14

简介

海绵函数sponge function是密码学中使用的一种函数,它接收一定长度的输入,然后输出一定长度的输出,中间包含了有限个内部状态。

因为海绵函数的强大功能,所以可以用来建模和实现许多密码原语,包括密码散列,消息身份验证码,生成掩码,流密码,伪随机数生成器等。

本文将会讲解海绵函数的结构。

海绵函数的结构

我们先看一个海绵函数的结构图:

这个函数被分成了两部分,左边部分叫做吸收部分,右边部分叫做输出部分,一吸一出,像是海绵一样,所以叫做海绵函数。

P表示的是输入的字符串,Z表示的时候输出字符串。

一个海绵函数由三部分组成,分别是state, 函数f和填充函数pad。

state就是上图的r+c部分,r被称为Bitrate, c被称为Capacity。

P被分成n份,每一份都会跟Bitrate进行异或操作,如果P的长度不是Bitrate的整数倍,那么需要使用Pad函数进行填充。

每一轮,Bitrate跟P进行异或操作的结果作为最新的Bitrate, 然后生成新的state,然后这个state又被f(state)来替换。

其中函数 f 是 从n个{0,1} 到n个{0,1}的映射。

就这样一轮一轮进行下去,直到所有的P都参与了运算。

输出部分是将最终生成的state进行f运算,每次运算都取Bitrate部分作为输出,从而得到最终的输出。

海绵函数的应用

因为海绵函数的优秀的特性,所以被用在很多方面。比如SHA-3的实现算法Keccak就是使用的海绵函数。

通过替换f和多轮置换,海绵函数可以生成非常安全的密码算法,所以得到了广泛的使用。

本文已收录于 www.flydean.com/36-sponge-f…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

雪花算法,什么情况下发生 ID 冲突?

发表于 2021-09-14

分布式系统中,有一些需要使用全局唯一 ID 的场景,这种时候为了防止 ID 冲突可以使用 36 位的 UUID,但是 UUID 有一些缺点,首先他相对比较长,另外 UUID 一般是无序的

有些时候我们希望能使用一种简单些的 ID,并且希望 ID 能够按照时间有序生成

公众号:龙台的技术笔记

GitHub:acmenlt

什么是雪花算法

Snowflake 中文的意思是雪花,所以常被称为雪花算法,是 Twitter 开源的分布式 ID 生成算法

Twitter 雪花算法生成后是一个 64bit 的 long 型的数值,组成部分引入了时间戳,基本保持了自增

SnowFlake 算法的优点:

  1. 高性能高可用:生成时不依赖于数据库,完全在内存中生成
  2. 高吞吐:每秒钟能生成数百万的自增 ID
  3. ID 自增:存入数据库中,索引效率高

SnowFlake 算法的缺点:

依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成 ID 冲突或者重复

雪花算法组成

snowflake 结构如下图所示:

包含四个组成部分

不使用:1bit,最高位是符号位,0 表示正,1 表示负,固定为 0

时间戳:41bit,毫秒级的时间戳(41 位的长度可以使用 69 年)

标识位:5bit 数据中心 ID,5bit 工作机器 ID,两个标识位组合起来最多可以支持部署 1024 个节点

序列号:12bit 递增序列号,表示节点毫秒内生成重复,通过序列号表示唯一,12bit 每毫秒可产生 4096 个 ID

通过序列号 1 毫秒可以产生 4096 个不重复 ID,则 1 秒可以生成 4096 * 1000 = 409w ID

默认的雪花算法是 64 bit,具体的长度可以自行配置。如果希望运行更久,增加时间戳的位数;如果需要支持更多节点部署,增加标识位长度;如果并发很高,增加序列号位数

总结:雪花算法并不是一成不变的,可以根据系统内具体场景进行定制

雪花算法适用场景

因为雪花算法有序自增,保障了 MySQL 中 B+ Tree 索引结构插入高性能

所以,日常业务使用中,雪花算法更多是被应用在数据库的主键 ID 和业务关联主键

雪花算法生成 ID 重复问题

假设:一个订单微服务,通过雪花算法生成 ID,共部署三个节点,标识位一致

此时有 200 并发,均匀散布三个节点,三个节点同一毫秒同一序列号下生成 ID,那么就会产生重复 ID

通过上述假设场景,可以知道雪花算法生成 ID 冲突存在一定的前提条件

  1. 服务通过集群的方式部署,其中部分机器标识位一致
  2. 业务存在一定的并发量,没有并发量无法触发重复问题
  3. 生成 ID 的时机:同一毫秒下的序列号一致

标识位如何定义

如果能保证标识位不重复,那么雪花 ID 也不会重复

通过上面的案例,知道了 ID 重复的必要条件。如果要避免服务内产生重复的 ID,那么就需要从标识位上动文章

我们先看看开源框架中使用雪花算法,如何定义标识位

Mybatis-Plus v3.4.2 雪花算法实现类 Sequence,提供了两种构造方法:无参构造,自动生成 dataCenterId 和 workerId;有参构造,创建 Sequence 时明确指定标识位

Hutool v5.7.9 参照了 Mybatis-Plus dataCenterId 和 workerId 生成方案,提供了默认实现

一起看下 Sequence 的创建默认无参构造,如何生成 dataCenterId 和 workerId

1
2
3
4
5
6
7
8
9
10
11
ini复制代码public static long getDataCenterId(long maxDatacenterId) {
   long id = 1L;
   final byte[] mac = NetUtil.getLocalHardwareAddress();
   if (null != mac) {
       id = ((0x000000FF & (long) mac[mac.length - 2])
               | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
       id = id % (maxDatacenterId + 1);
  }
​
   return id;
}

入参 maxDatacenterId 是一个固定值,代表数据中心 ID 最大值,默认值 31

为什么最大值要是 31?因为 5bit 的二进制最大是 11111,对应十进制数值 31

获取 dataCenterId 时存在两种情况,一种是网络接口为空,默认取 1L;另一种不为空,通过 Mac 地址获取 dataCenterId

可以得知,dataCenterId 的取值与 Mac 地址有关

接下来再看看 workerId

1
2
3
4
5
6
7
8
9
10
java复制代码public static long getWorkerId(long datacenterId, long maxWorkerId) {
   final StringBuilder mpid = new StringBuilder();
   mpid.append(datacenterId);
   try {
       mpid.append(RuntimeUtil.getPid());
  } catch (UtilException igonre) {
       //ignore
  }
   return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}

入参 maxWorkderId 也是一个固定值,代表工作机器 ID 最大值,默认值 31;datacenterId 取自上述的 getDatacenterId 方法

name 变量值为 PID@IP,所以 name 需要根据 @ 分割并获取下标 0,得到 PID

通过 MAC + PID 的 hashcode 获取16个低位,进行运算,最终得到 workerId

分配标识位

Mybatis-Plus 标识位的获取依赖 Mac 地址和进程 PID,虽然能做到尽量不重复,但仍有小几率

标识位如何定义才能不重复?有两种方案:预分配和动态分配

预分配

应用上线前,统计当前服务的节点数,人工去申请标识位

这种方案,没有代码开发量,在服务节点固定或者项目少可以使用,但是解决不了服务节点动态扩容性问题

动态分配

通过将标识位存放在 Redis、Zookeeper、MySQL 等中间件,在服务启动的时候去请求标识位,请求后标识位更新为下一个可用的

通过存放标识位,延伸出一个问题:雪花算法的 ID 是 服务内唯一还是全局唯一

以 Redis 举例,如果要做服务内唯一,存放标识位的 Redis 节点使用自己项目内的就可以;如果是全局唯一,所有使用雪花算法的应用,要用同一个 Redis 节点

两者的区别仅是 不同的服务间是否公用 Redis。如果没有全局唯一的需求,最好使 ID 服务内唯一,因为这样可以避免单点问题

服务的节点数超过 1024,则需要做额外的扩展;可以扩展 10 bit 标识位,或者选择开源分布式 ID 框架

动态分配实现方案

Redis 存储一个 Hash 结构 Key,包含两个键值对:dataCenterId 和 workerId

在应用启动时,通过 Lua 脚本去 Redis 获取标识位。dataCenterId 和 workerId 的获取与自增在 Lua 脚本中完成,调用返回后就是可用的标示位

具体 Lua 脚本逻辑如下:

  1. 第一个服务节点在获取时,Redis 可能是没有 snowflake_work_id_key 这个 Hash 的,应该先判断 Hash 是否存在,不存在初始化 Hash,dataCenterId、workerId 初始化为 0
  2. 如果 Hash 已存在,判断 dataCenterId、workerId 是否等于最大值 31,满足条件初始化 dataCenterId、workerId 设置为 0 返回
  3. dataCenterId 和 workerId 的排列组合一共是 1024,在进行分配时,先分配 workerId
  4. 判断 workerId 是否 != 31,条件成立对 workerId 自增,并返回;如果 workerId = 31,自增 dataCenterId 并将 workerId 设置为 0

dataCenterId、workerId 是一直向下推进的,总体形成一个环状。通过 Lua 脚本的原子性,保证 1024 节点下的雪花算法生成不重复。如果标识位等于 1024,则从头开始继续循环推进

开源分布式 ID 框架

Leaf 和 Uid 都有实现雪花算法,Leaf 额外提供了号段模式生成 ID

美团 Leaf:https://github.com/Meituan-Dianping/Leaf

百度 Uid:https://github.com/baidu/uid-generator

雪花算法可以满足大部分场景,如无必要,不建议引入开源方案增加系统复杂度

回顾总结

文章通过图文并茂的方式帮助读者梳理了一遍什么是雪花算法,以及如何解决雪花算法生成 ID 冲突的问题

关于雪环算法生成 ID 冲突问题,文中给了一种方案:分配标示位;通过分配雪花算法的组成标识位,来达到默认 1024 节点下 ID 生成唯一

可以去看 Hutool 或者 Mybatis-Plus 雪花算法的具体实现,帮助大家更好的理解

雪花算法不是万能的,并不能适用于所有场景。如果 ID 要求全局唯一并且服务节点超出 1024 节点,可以选择修改算法本身的组成,即扩展标识位,或者选择开源方案:LEAF、UID

创作不易,文章看完有帮助,点关注支持一下,祝好

本文转载自: 掘金

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

1…530531532…956

开发者博客

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