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

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


  • 首页

  • 归档

  • 搜索

Java内置锁的核心原理(一) 引言:线程安全问题 Java

发表于 2021-11-22

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

引言:线程安全问题

什么是线程安全问题?当多个线程并发的访问一个Java对象时,无论系统如何调度这些线程,这个对象都能表现出一致的、正确的行为,那么我们就说对这个对象的操作是线程安全的。反之,对这个线程的操作不是线程安全的,发生了线程安全问题。

本文将回答如下几个问题:synchronize是如何保证线程安全的? synchronize加锁到底是怎么加的? 锁信息放在Object对象的什么位置? 如何查看锁升级的具体过程?等等。

关于synchronize的使用场景、synchronize与ReentryLock有什么区别将在下一期介绍Lock的博客中给出。

Java内置锁的核心原理

Java内置锁是一种互斥(独占)锁。当线程A占有一个对象的锁,线程B也想要尝试获取这个对象的内置锁时,线程B会等待或阻塞,直到线程A执行完成后后释放锁(抛出异常也会释放),如果线程A不释放,线程B将会永远等待下去。

Java中的每个对象都已可用作内置锁。线程进入同步代码块时会自动获取改锁,在退出代码块时会自动释放该锁。

  1. synchronized关键字

synchronize关键字的使用方式有三种:

1.修饰一个普通方法

1
2
3
java复制代码public synchronized void add1() {
val ++;
}

此时仅有一个线程可进入这个对象的这个同步方法。这时候锁是加在这个对象上的,即这个对象的this属性。

2.修饰代码块

1
2
3
4
5
java复制代码public void add3() {
synchronized (this) {
val ++;
}
}

与上面的代码稍有不同,synchronize修饰的是一段代码,因为很多情况下并不需要将整个方法全部锁住,仅锁住部分代码即可保证线程安全的执行。

还有一点不同的是我们既可以使用synchronize包裹住this对象,也可以包裹住其他的对象,实现更为灵活的锁操作。

3.修饰一个静态方法

1
2
3
java复制代码public static synchronized void add2() {
val ++;
}

Java的对象可以分为两大类,一类是Object对象,分配在JVM的堆中,另一类是Class对象,存放在方法区(Java1.8 的HotSpot实现中称为元空间),JVM中一个Class文件只会有一个Class类,而由Class实例化出的Object对象会有很多个。synchronize修饰静态方法时便是将锁加在了Class对象中。

小结:以上三种加锁的方法各异,但本质类似,都是锁住了一个Java对象从而实现一次只有一个线程可以访问同步代码块。好,了解了原理,那我们就来看看他的实现,synchronize具体是实现如何锁住一个对象的?

  1. Java对象结构与内置锁

在介绍内置锁之前,有必要先和大家介绍下Java对象的结构。

image-20211114163922614

一个Java对象可以分为三个部分:

1)对象头

对象头又包含三个字段:第一个是Mark Word,用来存储对象的GC信息,锁信息,hashcode值等。第二个是Class Pointer,存放的是类指针,虚拟机通过这个类指针这个对象是哪个类的实例。第三个是Array Length,是一个可选字段,当此对象为数组时才会存在,用于记录数组长度的数据。

2)对象体

这部分包含对象的实例变量,包含父类的属性,这部分按照4字节对齐。

3)对齐字节

也叫填充区,其作用是保证这个对象占用的内存字节数为8的整数倍,因为对象头是8位的,所以仅需保证对象体也是8的倍数即可,当对象的实例变量数据不为8的倍数时,便需要填充来保证8字节的对齐。

Mark Word、Class Pointer、Array Length等字段的长度都与JVM的位数有关。Mark Word的长度为JVM的一个Word(字)大小,也就是说32位JVM的Mark Word为32位,64位JVM的Mark Word为64位。Class Pointer(类对象指针)字段的长度也为JVM的一个Word(字)大小,即32位JVM的Mark Word为32位,64位JVM的Mark Word为64位。所以,在32位JVM虚拟机中,Mark Word和Class Pointer这两部分都是32位的;在64位JVM虚拟机中,Mark Word和ClassPointer这两部分都是64位的。

不同锁状态下32位Mark Word的结构信息

image-2021111417134723864位的Mark Word与32位的Mark Word结构相似,

不同锁状态下64位Mark Work的结构信息

image-20211114171406229

对象加内置锁及锁的升级过程

无锁 –> 偏向锁:

当一个线程进入同步代码块时,发现此对象没有线程占用,那么这个对象就使用CAS(null, threadID)null是期望的值,threadID是将要写入的值,将自己的线程ID写入对象的Mark Word中,如果写入成功,即对象中没有threadID,则对象由原来的无锁状态变为偏向锁状态,lock不变化,为01,将偏向锁的标志位biased发生变化,由0变为1。

偏向锁 –> 轻量级锁:

当一个线程进入同步代码块时,使用CAS(null, threadID)将自己的线程ID写入对象的Mark Word中,如果写入失败,则说明此时线程已经被占用,则撤销对象的偏向锁定状态,升级为轻量级锁。对象Mark Word中的lock位由01变为00;

轻量级锁 –> 重量级锁:

轻量级锁的本意是为了减少多线程进入操作系统底层的互斥锁(Mutex Lock)的概率,并不是要替代操作系统互斥锁。所以,在争用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的重量级锁。轻量级锁升级为重量级锁的条件较为复杂,我们下一讲在详细探究。

本文转载自: 掘金

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

su 和 sudo,你用对了吗?

发表于 2021-11-22

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

之前一直对 su 和 sudo 这两个命令犯迷糊,最近专门搜了这方面的资料,总算是把两者的关系以及用法搞清楚了,这篇文章来系统总结一下。

1. 准备工作

因为本篇博客中涉及到用户切换,所以我需要提前准备好几个测试用户,方便后续切换。

Linux 中新建用户的命令是 useradd ,一般系统中这个命令对应的路径都在 PATH 环境变量里,如果直接输入 useradd 不管用的话,就用绝对路径名的方式:/usr/sbin/useradd 。

useradd 新建用户命令只有 root 用户才能执行,我们先从普通用户 ubuntu 切换到 root 用户(如何切换后文会介绍):

1
2
3
4
5
ruby复制代码ubuntu@VM-0-14-ubuntu:~$ su -
Password: # 输入 root 用户登录密码
root@VM-0-14-ubuntu:~# useradd -m test_user # 带上 -m 参数
root@VM-0-14-ubuntu:~# ls /home
test_user ubuntu # 可以看到 /home 目录下面有两个用户了

因为还没有给新建的用户 test_user 设置登录密码,这就导致我们无法从普通用户 ubuntu 切换到 test_user,所以接下来,我们需要用 root 来设置 test_user 的登录密码。需要用到 passwd 命令:

1
2
3
4
5
scss复制代码root@VM-0-14-ubuntu:~# passwd test_user
Enter new UNIX password: # 输出 test_user 的密码
Retype new UNIX password:
passwd: password updated successfully
root@VM-0-14-ubuntu:~#

接着我们输入 exit 退出 root 用户到 普通用户 ubuntu:

1
2
3
ruby复制代码root@VM-0-14-ubuntu:~# exit
logout
ubuntu@VM-0-14-ubuntu:~$

可以看到,命令提示符前面已经由 root 变成 ubuntu,说明我们现在的身份是 ubuntu 用户。

2. su 命令介绍及主要用法

首先需要解释下 su 代表什么意思。

之前一直以为 su 是 super user,查阅资料之后才知道原来表示 switch user。

知道 su 是由什么缩写来的之后,那么它提供的功能就显而易见了,就是切换用户。

2.1 - 参数

su 的一般使用方法是:

1
xml复制代码su  <user_name>

或者

1
xml复制代码su - <user_name>

两种方法只差了一个字符 -,会有比较大的差异:

  • 如果加入了 - 参数,那么是一种 login-shell 的方式,意思是说切换到另一个用户 <user_name> 之后,当前的 shell 会加载 <user_name> 对应的环境变量和各种设置;
  • 如果没有加入 - 参数,那么是一种 non-login-shell 的方式,意思是说我现在切换到了 <user_name>,但是当前的 shell 还是加载切换之前的那个用户的环境变量以及各种设置。

光解释会比较抽象,我们看一个例子就比较容易理解了。

我们首先从 ubuntu 用户以 non-login-shell 的方式切换到 root 用户,比较两种用户状态下环境变量中 PWD 的值(su 命令不跟任何 <user_name> ,默认切换到 root 用户):

1
2
3
4
5
6
7
8
9
perl复制代码ubuntu@VM-0-14-ubuntu:~$ env | grep ubuntu
USER=ubuntuPWD=/home/ubuntu # 是 /home/ubuntu
HOME=/home/ubuntu
# 省略......
ubuntu@VM-0-14-ubuntu:~$ su # non-login-shell 方式
Password: # 输入 root 用户登录密码
root@VM-0-14-ubuntu:/home/ubuntu# env | grep ubuntu
PWD=/home/ubuntu # 可以发现还是 /home/ubuntu
root@VM-0-14-ubuntu:/home/ubuntu#

我们的确是切换到 root 用户了,但是 shell 环境中的变量并没有改变,还是用之前 ubuntu 用户的环境变量。

接着我们从 ubuntu 用户以 login-shell 的方式切换到 root 用户,同样比较两种用户转台下环境变量中 PWD 的值:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码ubuntu@VM-0-14-ubuntu:~$ env | grep ubuntu
USER=ubuntuPWD=/home/ubuntu # 是 /home/ubuntu
HOME=/home/ubuntu
# 省略.......
ubuntu@VM-0-14-ubuntu:~$ su - # 是 login-shell 方式
Password:
root@VM-0-14-ubuntu:~# env | grep root
USER=rootPWD=/root # 已经变成 /root 了
HOME=/root
MAIL=/var/mail/root
LOGNAME=root
root@VM-0-14-ubuntu:~#

可以看到用 login-shell 的方式切换用户的话,shell 中的环境变量也跟着改变了。

总结:具体使用哪种方式切换用户看个人需求:

  • 如果不想因为切换到另一个用户导致自己在当前用户下的设置不可用,那么用 non-login-shell 的方式;
  • 如果切换用户后,需要用到该用户的各种环境变量(不同用户的环境变量设置一般是不同的),那么使用 login-shell 的方式。

2.2 切换到指定用户

前面已经介绍了,如果 su 命令后面不跟任何,那么默认是切换到 root 用户:

1
2
3
perl复制代码ubuntu@VM-0-14-ubuntu:~$ su -
Password: # root 用户的密码
root@VM-0-14-ubuntu:/home/ubuntu#

因为我们在 1. 准备工作 部分已经新建了一个 test_user 用户,并且我们也知道 test_user 用户的登录密码(root 用户设置的),我们就能从 ubuntu 用户切换到 test_user 用户:

1
2
ruby复制代码ubuntu@VM-0-14-ubuntu:~$ su - test_user
Password: # test_user 用户的密码$

2.3 -c 参数

前面的方法中,我们都是先切换到另一个用户(root 或者 test_user),在哪个用户的状态下执行命令,最后输入 exit 返回当前 ubuntu 用户。

还有一种方式是:不需要先切换用户再执行命令,可以直接在当前用户下,以另一个用户的方式执行命令,执行结束后就返回当前用户。这就得用到 -c 参数。

另外,Linux 系列面试题和答案全部整理好了,微信搜索Java技术栈,在后台发送:面试,可以在线阅读。

具体使用方法是:

1
arduino复制代码su - -c "指令串"                                  # 以 root 的方式执行 "指令串"

我么看个例子:

1
2
3
4
5
6
7
8
9
ruby复制代码ubuntu@VM-0-14-ubuntu:~$ cat /etc/shadow
cat: /etc/shadow: Permission denied # ubuntu 用户不能直接查看 /etc/shadow 文件内容
ubuntu@VM-0-14-ubuntu:~$ su - -c "tail -n 4 /etc/shadow"
Password: # 输入 root 用户密码
ubuntu:$1$fZKcWEDI$uwZ64uFvVbwpHTbCSgim0/:18352:0:99999:7:::
ntp:*:17752:0:99999:7:::
mysql:!:18376:0:99999:7:::
test_user:$6$.ZY1lj4m$ii0x9CG8h.JHlh6zKbfBXRuolJmIDBHAd5eqhvW7lbUQXTRS//89jcuTzRilKqRkP8YbYW4VPxmTVHWRLYNGS/:18406:0:99999:7:::
ubuntu@VM-0-14-ubuntu:~$ # 执行完马上返回 ubuntu 用户而不是 root 用户

这种执行方式和后面要介绍的 sudo 很像,都是临时申请一下 root 用户的权限。但还是有差异,我们接着往后看。

3. sudo 命令介绍及主要用法

首先还是解释下 sudo 命令是什么意思。

sudo 的英文全称是 super user do,即以超级用户(root 用户)的方式执行命令。这里的 sudo 和之前 su 表示的 switch user 是不同的,这点需要注意,很容易搞混。

我们先介绍 sudo 命令能做什么事情,然后说明为何能做到这些,以及如何做到这些。

我们开始。

3.1 主要用法

我们在 Linux 中经常会碰到 Permission denied 这种情况,比如以 ubuntu 用户的身份查看 /etc/shadow 的内容。因为这个文件的内容是只有 root 用户能查看的。

那如果我们想要查看怎么办呢?这时候就可以使用 sudo :

1
2
3
4
ruby复制代码ubuntu@VM-0-14-ubuntu:~$ tail -n 3 /etc/shadow
tail: cannot open '/etc/shadow' for reading: Permission denied # 没有权限
ubuntu@VM-0-14-ubuntu:~$ sudo !! # 跟两个惊叹号
sudo tail -n 3 /etc/shadowntp:*:17752:0:99999:7:::mysql:!:18376:0:99999:7:::test_user:$6$.ZY1lj4m$ii0x9CG8h.JHlh6zKbfBXRuolJmIDBHAd5eqhvW7lbUQXTRS//89jcuTzRilKqRkP8YbYW4VPxmTVHWRLYNGS/:18406:0:99999:7:::ubuntu@VM-0-14-ubuntu:~$

实例中,我们使用了 sudo !! 这个小技巧,表示重复上面输入的命令,只不过在命令最前面加上 sudo 。

因为我已经设置了 sudo 命令不需要输入密码,所以这里 sudo !! 就能直接输出内容。如果没有设置的话,需要输入当前这个用户的密码,例如本例中,我就应该输入 ubuntu 用户的登录密码。

两次相邻的 sudo 操作,如果间隔在 5min 之内,第二次输入 sudo 不需要重新输入密码;如果超过 5min,那么再输入 sudo 时,又需要输入密码。所以一个比较省事的方法是设置 sudo 操作不需要密码。后面介绍如何设置。

sudo 除了以 root 用户的权限执行命令外,还有其它几个用法,这里做简单介绍。

切换到 root 用户:

1
复制代码sudo su -

这种方式也能以 login-shell 的方式切换到 root 用户,但是它和 su - 方法是有区别的:

  • 前者输入 sudo su - 后,需要提供当前用户的登录密码,也就是 ubuntu 用户的密码;
  • 后者输入 su - 后,需要提供 root 用户的登录密码。

还有一个命令:

1
css复制代码sudo -i

这个命令和 sudo su - 效果一致,也是切换到 root 用户,也是需要提供当前用户(ubuntu 用户)的登录密码。

我们现在切换到 test_user 用户,尝试显示 /etc/shadow 文件的内容:

1
2
3
ruby复制代码ubuntu@VM-0-14-ubuntu:~$ su - test_user
Password: # test_user 的密码
$ sudo cat /etc/shadow[sudo] password for test_user: # test_user 的密码test_user is not in the sudoers file. This incident will be reported.$

我们会看到倒数第二行中的错误提示信息,我们无法查看 /etc/shadow 的内容,这是为什么?为什么 ubuntu 可以使用 sudo 但是 test_user 不行呢?

这就涉及到 sudo 的工作原理了。

3.2 sudo 工作原理

一个用户能否使用 sudo 命令,取决于 /etc/sudoers 文件的设置。

从 3.1 节中我们已经看到,ubuntu 用户可以正常使用 sudo ,但是 test_user 用户却无法使用,这是因为 /etc/sudoers 文件里没有配置 test_user。

/etc/sudoers 也是一个文本文件,但是因其有特定的语法,我们不要直接用 vim 或者 vi 来编辑它,需要用 visudo 这个命令。输入这个命令之后就能直接编辑 /etc/sudoers 这个文件了。

需要说明的是,只有 root 用户有权限使用 visudo 命令。

我们先来看下输入 visudo 命令后显示的内容。

输入(root 用户):

1
perl复制代码root@VM-0-14-ubuntu:~# visudo

输出:

1
less复制代码# User privilege specificationroot    ALL=(ALL:ALL) ALL# Members of the admin group may gain root privileges%admin ALL=(ALL) ALL# Allow members of group sudo to execute any command%sudo   ALL=(ALL:ALL) ALL# See sudoers(5) for more information on "#include" directives:#includedir /etc/sudoers.dubuntu  ALL=(ALL:ALL) NOPASSWD: ALL

解释下每一行的格式:

  • 第一个表示用户名,如 root 、ubuntu 等;
  • 接下来等号左边的 ALL 表示允许从任何主机登录当前的用户账户;
  • 等号右边的 ALL 表示:这一行行首对一个的用户可以切换到系统中任何一个其它用户;
  • 行尾的 ALL 表示:当前行首的用户,能以 root 用户的身份下达什么命令,ALL 表示可以下达任何命令。

我们还注意到 ubuntu 对应的那一行有个 NOPASSWD 关键字,这就是表明 ubuntu 这个用户在请求 sudo 时不需要输入密码,到这里就解释了前面的问题。

同时我们注意到,这个文件里并没有 test_user 对应的行,这也就解释了为什么 test_user 无法使用 sudo 命令。

接下来,我们尝试将 test_user 添加到 /etc/sudoers 文件中,使 test_user 也能使用 sudo 命令。我们在最后一行添加:

1
sql复制代码test_user  ALL=(ALL:ALL)  ALL       # test_user 使用 sudo 需要提供 test_user 的密码

接下来我们再在 test_user 账户下执行 sudo :

1
scss复制代码ubuntu@VM-0-14-ubuntu:~$ su - test_userPassword:$ tail -n 3 /etc/shadowtail: cannot open '/etc/shadow' for reading: Permission denied$ sudo tail -n 3 /etc/shadow                   # 加上 sudontp:*:17752:0:99999:7:::mysql:!:18376:0:99999:7:::test_user:$6$.ZY1lj4m$ii0x9CG8h.JHlh6zKbfBXRuolJmIDBHAd5eqhvW7lbUQXTRS//89jcuTzRilKqRkP8YbYW4VPxmTVHWRLYNGS/:18406:0:99999:7:::$

可以看到,现在已经可以使用 sudo 了。

3.3 思考

我们已经看到了,如果一个用户在 /etc/sudoers 文件中,那么它就具有 sudo 权限,就能通过 sudo su - 或者 sudo -i 等命令切换到 root 用户了,那这时这个用户就变成 root 用户了,那这不对系统造成很大的威胁吗?

实际上的确是这样的。所以如果在编辑 /etc/sudoers 文件赋予某种用户 sudo 权限时,必须要确定该用户是可信任的,不会对系统造成恶意破坏,否则将所有 root 权限都赋予该用户将会有非常大的危险。

当然,root 用户也可以编辑 /etc/sudoers 使用户只具备一部分权限,即只能执行一小部分命令。有兴趣的读者可以参考 Reference 部分第二条,这篇文章不再赘述。

4. 二者的差异对比

我们已经看到:

  • 使用 su - ,提供 root 账户的密码,可以切换到 root 用户;
  • 使用 sudo su - ,提供当前用户的密码,也可以切换到 root 用户

两种方式的差异也显而易见:如果我们的 Linux 系统有很多用户需要使用的话,前者要求所有用户都知道 root 用户的密码,这显然是非常危险的;后者是不需要暴露 root 账户密码的,用户只需要输入自己的账户密码就可以,而且哪些用户可以切换到 root,这完全是受 root 控制的(root 通过设置 /etc/sudoers 实现的),这样系统就安全很多了。

一般都是推荐使用 sudo 方式。

References

  • www.rootusers.com/the-differe…
  • 《鸟哥的 Linux 私房菜》13.4 节:使用者身份切换
  • github.com/ustclug/Lin…
  • www.maketecheasier.com/differences…
  • stackoverflow.com/questions/3…
  • www.zhihu.com/question/51…
  • www.linuxidc.com/Linux/2017-…

本文转载自: 掘金

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

【Impala】架构原理

发表于 2021-11-22

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

一、组件

Impala 是一个分布式, 大规模并行处理(MPP)数据库引擎, 它包括多个进程。

Impala 与 Hive 类似不是数据库而是 数据分析工具

1
2
3
bash复制代码# 在 linux123 中执行

$ ps -ef | grep impala

如图:
2020-08-2610:15_1.png

  1. impalad
  • 角色名称为 Impala Daemon , 是在每个节点上运行的进程, 是 Impala 的核心组件, 进程名是 Impalad;
  • 作用: 负责读写数据文件, 接收来自 Impala-shell , JDBC , ODBC 等的查询请求, 与集群其它 Impalad 分布式并行完成查询任务, 并将查询结果返回给中心协调者。
  • 为了保证 Impalad 进程了解其它 Impalad 的健康状况, Impalad 进程会一直与 statestore 保持通信。
  • Impalad 服务由三个模块组成: Query Planner、Query Coordinator 和 Query Executor , 前两个模块组成前端, 负责接收 SQL 查询请求, 解析 SQL 并转换成执行计划, 交由后端执行。
  1. statestored
  • statestore 监控集群中 Impalad 的健康状况, 并将集群健康信息同步给 Impalad
  • statestore 进程名为 statestored
  1. catalogd
  • Impala 执行的 SQL 语句引发元数据发生变化时, catalog 服务负责把这些元数据的变化同步给其它 Impalad 进程(日志验证, 监控 statestore 进程日志)
  • catalog 服务对应进程名称是 catalogd
  • 由于一个集群需要一个 catalogd 以及一个 statestored 进程, 而且 catalogd 进程所有请求都是经过 statestored 进程发送, 所以官方建议让 statestored 进程与 catalogd 进程安排同个节点。

二、查询

查询流程如下图:
2020-08-2610:21.png

  1. Client 提交任务

Client 发送一个 SQL 查询请求到任意一个 Impalad 节点, 会返回一个 queryId 用于之后的客户端操作。

  1. 生成单机和分布式执行计划

SQL 提交到 Impalad 节点之后, Analyser 依次执行 SQL 的词法分析、语法分析、语义分析等操作;
从 MySQL 元数据库中获取元数据, 从 HDFS 的名称节点中获取数据地址, 以得到存储这个查询相关数据的所有数据节点

  • 单机执行计划: 根据上一步对 SQL 语句的分析, 由 Planner 先生成单机的执行计划, 该执行计划是有 PlanNode 组成的一棵树, 这个过程中也会执行一些 SQL 优化, 例如 Join 顺序改变、谓词下推等。
  • 分布式并行物理计划: 将单机执行计划转换成分布式并行物理执行计划, 物理执行计划由一个的 Fragment 组成, Fragment 之间有数据依赖关系, 处理过程中需要在原有的执行计划之上加入一些 ExchangeNode 和 DataStreamSink 信息等。
  • Fragment : sql 生成的分布式执行计划的一个子任务;
  • DataStreamSink : 传输当前的 Fragment 输出数据到不同的节点
  1. 任务调度和分发

Coordinator 将 Fragment (子任务) 根据数据分区信息发配到不同的 Impalad 节点上执行。Impalad 节点接收到执行 Fragment 请求交由 Executor 执行。

  1. Fragment 之间的数据依赖

每一个 Fragment 的执行输出通过 DataStreamSink 发送到下一个 Fragment , Fragment 运行过程中不不断向 coordinator 节点汇报当前运行状态。

  1. 结果汇总

查询的 SQL 通常情况下需要有一个单独的 Fragment 用于结果的汇总, 它只在 Coordinator 节点运行, 将多个节点的最终执行结果汇总, 转换成 ResultSet 信息。

  1. 获取结果

客户端调用获取 ResultSet 的接口, 读取查询结果。

(1)查询计划示例

1
2
3
4
5
6
7
sql复制代码select t1.n1, t2.n2, count(1) as c
from t1 join t2 on t1.id = t2.id
join t3 on t1.id = t3.id
where t3.n3 between ‘a’ and ‘f’
group by t1.n1, t2.n2
order by c desc
limit 100;

(2)单机执行计划

QueryPlanner 生成单机的执行计划。

如图:
2020-08-2610:30.png

分析上面的单机执行计划

  1. 先去扫描 t1 表中需要的数据, 如果数据文件存储是列式存储,可以便利的扫描到所需的列 id
  2. n1 需要与 t2 表进行 Join 操作, 扫描 t2 表与 t1 表类似获取到所需数据列 id , n2
  3. t1 与 t2 表进行关联, 关联之后再与 t3 表进行关联, 这里 Impala 会使用谓词下推扫描 t3 表只取 join 所需数据
  4. 对 group by 进行相应的 aggregation 操作, 最终是排序取出指定数量的数据返回。

(3)分布式并行计划

所谓的分布式并行化执行计划: 就是在单机执行计划基础之上结合数据分布式存储的特点, 按照任务的计算要求把单机执行计划拆分为多段子任务, 每个子任务都是可以并行执行的。

上面的单机执行计划 转为 分布式并行执行计划。

分布式并行执行计划,如图:
2020-08-2611:52.png

流程图,如下:
2020-08-2610:35.png

分布式执行计划中涉及到多表的 Join , Impala 会根据表的大小来决定 Join 的方式。

主要有两种: Hash Join 与 Broadcast Join

上面分布式执行计划中可以看出 T1,T2 表大一些, 而 T3 表小一些, 所以对于 T1 与 T2 的 Join Impala 选择使用 Hash Join
对于 T3 表选择使用 Broadcast 方式, 直接把 T3 表广播到需要 Join 的节点上。

分布式并行计划流程:

  1. T1 和 T2 使用 Hash join , 此时需要按照 id 的值分别将 T1 和 T2 分散到不同的 Impalad 进程, 但是相同的 id 会散列到相同的 Impalad 进程, 这样每一个 Join 之后是全部数据的一部分
  2. T1 与 T2 Join 之后的结果数据再与 T3 表进行 Join , 此时 T3 表采用 Broadcast 方式把自己全部数据(id 列) 广播到需要的 Impala 节点上
  3. T1 , T2 , T3 Join 之后再根据 Group by 执行本地的预聚合, 每一个节点的预聚合结果只是最终结果的一部分(不同的节点可能存在相同的 group by 的值), 需要再进行一次全局的聚合。
  4. 全局的聚合同样需要并行, 则根据聚合列列进行 Hash 分散到不同的节点执行 Merge 运算(其实仍然是一次聚合运算), 一般情况下为了较少数据的网络传输, Impala 会选择之前本地聚合节点做全局聚合工作。
  5. 通过全局聚合之后, 相同的 key 只存在于一个节点, 然后对于每一个节点进行排序和 TopN 计算, 最终将每一个全局聚合节点的结果返回给 Coordinator 进行合并、排序、limit 计算, 返回结果给用户。

本文转载自: 掘金

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

【MySQL】了解索引

发表于 2021-11-22

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

一、概述

索引是一种用于快速查询和检索数据的数据结构。

类比 索引–目录

打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。

二、优缺点

优点 缺点
大大加快数据的检索速度 创建索引和维护索引需要耗费许多时间,在对有索引的数据进行增删改时,需要动态的修改索引,降低 SQL执行效率。
通过添加唯一性索引可以确保数据库表中的每一行数据的唯一性 索引使用物理文件存储,会消耗一定空间
当查询数据量不大时, 不一定能带来速度的提升,索引查询不一定比全表查询快。

三、数据结构

MySQL索引使用的数据结构主要有 BTree索引和哈希索引。

查询单条记录的时候,可以选择哈希索引。

其他场景则使用 BTree索引。

四、索引类型

1、主键索引(Primary Key)

数据表的主键列使用的就是主键索引。

一张数据表有只能有一个主键,并且主键不能为 null,不能重复。

2、二级索引(辅助索引)

二级索引的叶子节点存储的数据是主键。就是说通过二级索引,可以定位主键的位置。

包含唯一索引、普通索引、前缀索引、全文索引等。

1-唯一索引(Unique Key)

唯一索引也是一种约束。唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。

2-普通索引(Index)

普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。

3-前缀索引(Prefix)

前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小, 因为只取前几个字符。

4-全文索引(Full Text)

全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。

五、聚集索引&非聚集索引

1-聚集索引

聚集索引即索引结构和数据一起存放的索引。

主键索引属于聚集索引。

2-非聚集索引

非聚集索引即索引结构和数据分开存放的索引。

二级索引属于非聚集索引。


Q:MyISAM为什么查询快,更新慢?

  • 一个MyISAM表在硬盘中会有表结构、数据、索引三个文件,
    • 就是说是索引和数据是分开存储的;
      • 修改 MyISAM表的数据,还需要去维护索引,索引更新慢。
  • Innodb的数据和索引是放着同一个文件中的;
    • 就是说索引和数据存储在一起;
      • 修改 Innodb表的数据时,就不需要去维护索引了。

Q:如何判断什么字段适合创建索引?

  1. 首先主键的列,MySQL是默认创建索引的,保证主键的唯一性;
  2. 然后是作为和外表进行连接的字段(一般是外键)也适合创建索引加快连接的速度;
  1. 还有就是经常需要去搜索的列上,也适合添加索引,提过搜索速度;
    1. (冷知识,如果where上有两个搜索的字段,只在一个字段加索引,索引是不会起作用的)
      1. 经常排序的字段很适合添加索引,因为索引本身就是排序好的;

Q:B树索引?

索引的存储结构是 B树(balance tree)

1、结构实例


–>

广度优先,会把倾向于把树深度(层数)压扁,减少层数,加快检索速度。


2、自动层级控制

当树不平衡时,会通过改变树的头节点来让树保持平衡。

Q:组合索引?

通过组合索引,可以了解到索引的使用细节(前缀索引)。

1
less复制代码CREATE TABLE article ADD INDEX index_title_time (title(50),time(10));

创建和文字标题和发表时间的组合索引。

1
2
3
4
5
sql复制代码-- 使用到了组合索引
SELECT * FROM article WHERE title='测试' AND time=123456789;
SELECT * FROM article WHERE title='测试';
-- 没有使用到组合索引
SELECT * FROM article WHERE time=123456789;

Q:解释SQL语句,判断是否走索引?

可以通过在 sql语句前面加 explain来判断语句是否走索引。

Q:sql查询语句的优化?

一般 sql查询语句的优化,底层逻辑是表的设计嘛,

  1. 比如说存储引擎是 innodb还是MyISAM(如果表会把查询多,而更新少的表使用 MyISAM存储引擎,当然还有是不能在 MyISAM表里开启事务,修改值,因为不支持事务,无法回滚);
  2. 还有就是表的索引设置,不能太多,如果太多会拖慢索引的查询速度,索引文件也会在硬盘里变大(当然小问题,顺带一说),一般一张表设置不超过6个吧好像;
  1. 再然后 sql查询语句的的优化就是,尽量走索引查询,加快查询速度;
    1. 比如不用 *查询,这样会走全表查询;
      1. 像 like的使用不要用左模糊查询或者全模糊查询,因为索引是前缀匹配的,左模糊或者全模糊会导致查询不走索引。
    1. 还有的总结就是编写 sql的时候注意要走索引查找,避免全表查找,在项目中一般在写了 查询sql语句后用 explain关键字来查看有没有走索引。

本文转载自: 掘金

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

ES 读写底层过程

发表于 2021-11-22

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

写入数据过程

  1. 客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node (协调节点)。
  2. coordinating node 对 document 进行路由,将请ff求转发给对应的 node(有 primary shard)。
  1. 实际的node上的primary shard处理请求,然后将数据同步到replica node。
  2. coordinating node如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端。

读取数据过程

通过doc id来查询, 很先对doc id进行hash,判断出在写入时把document分配到 了哪个shard上面去,从那个shard去查询。

  1. 客户端发送请求到任意一个node,成为coordinate node。
  2. coordinage node对doc id进行哈希路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡。
  1. 接收请求的node返回document给coordinate node。
  2. coordinate node返回document给客户端。

搜索数据过程

全文检索

  1. 客户端发送请求到一个coordinate node。
  2. 协调节点将搜索请求转发到所有的shard对应的primary shard或replica shard。
  1. query phrase:每个shard将自己的搜索结果(其实就是一些doc id)返回给协调节点,由协调节点进行输的合并、排序、分页等操作,产出最终结果。
  2. fetch phrase:接着由协调节点根据个doc id去各个节点上拉取实际实际的document数据,返回给客户端。

写数据底层原理

  1. 先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。
  2. 如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 refresh 到一个新的 segment file 中,但是此时数据不是直接进入 segment file 磁盘文件,而是先进入 os cache 。这个过程就是 refresh 。
  1. 每隔 1 秒钟,es 将 buffer 中的数据写入一个新的 segment file ,每秒钟会产生一个新的磁盘文件 segment file ,这个 segment file 中就存储最近 1 秒内 buffer 中写入的数据。
  2. 但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。
  1. 操作系统里面,磁盘文件其实都有一个东西,叫做 os cache ,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 os cache ,先进入操作系统级别的一个内存缓存中去。只要 buffer 中的数据被 refresh 操作刷入 os cache 中,这个数据就可以被搜索到了。
  2. 为什么叫 es 是准实时的?NRT ,全称 near real-time 。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 restful api 或者 java api ,手动执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 os cache 中,让数据立马就可以被搜索到。只要数据被输入 os cache 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。
  1. 重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 buffer 数据写入一个又一个新的 segment file 中去,每次 refresh 完 buffer 清空,translog 保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 commit 操作。
  2. commit 操作发生第一步,就是将 buffer 中现有数据 refresh 到 os cache 中去,清空 buffer。然后,将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file ,同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。最后清空 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。
  1. 这个 commit 操作叫做 flush 。默认 30 分钟自动执行一次 flush ,但如果 translog 过大,也会触发 flush 。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。
  2. translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 translog 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。
  1. translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会丢失 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。
  2. 实际上你在这里,如果面试官没有问你 es 丢数据的问题,你可以在这里给面试官炫一把,你说,其实 es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的数据丢失。
  1. 总结一下,数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。

删除/更新数据底层原理

  1. 如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 是否被删除了。
  2. 如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。
  1. buffer 每 refresh 一次,就会产生一个 segment file ,所以默认情况下是 1 秒钟一个 segment file ,这样下来 segment file 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,这里会写一个 commit point ,标识所有新的 segment file ,然后打开 segment file 供搜索使用,同时删除旧的 segment file 。

本文转载自: 掘金

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

IoT 的概念、发展趋势与挑战 IoT 的概念、发展趋势与挑

发表于 2021-11-22

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

目录

  • 概念
  • 智能时代的“物连接”红利,也带来巨大挑战
      1. 让“物”说话
      1. 让“物”说一种话
      1. 让“物”说有价值的话
  • 总结

IoT 的概念、发展趋势与挑战

概念

物联网(Internet of Things),简称 IoT,是新一代信息技术的重要组成部分,也是信息化时代的重要发展阶段。

智能时代,物联网是万物互联的基础链接平台。万物互联是智能化的基础,从PC时代到移动互联时代,再到智能时代是历史的发展趋势。

感知物理世界,让物变成数字信号,需要解决数字化感知、联接和商业价值闭环的问题。通过调研显示各行业物联接趋势,发现来自17个国家、11个垂直行业,总计1096家公司的调研数据显示2020年到2025年的IoT连接数将呈指数增长。

智能时代的“物连接”红利,也带来巨大挑战

1. 让“物”说话

现在,这方面的内容充满了挑战,大量现存设备未被数字化,多样化的设备、各种行业协议、各种通信网络,它们普遍要求低功耗、低成本,另外,设备安全问题突出。

2. 让“物”说一种话

统一所有“物”的语言,这方面也充满挑战,百万种数据元素,缺乏标准和规范,多样化数据格式,各厂商烟囱式发展,数据孤岛林立,数据互通困难。

3. 让“物”说有价值的话

让“物”说的话变得有价值,也充满了挑战,海量数据带来算力、存储问题,实时流、时许、离线、多维分析复制,行业应用场景的复杂性,存在商业闭环、价值变现和分配问题。

物联网正在渗透与改变着我们的生活,比如共享单车、车联网、智慧家居等。目前,共享单车在停车时进行了“电子围栏”的限制;车联网可以提前预判很多危险,有效的保护司机的人身安全。

总结

IoT与AI融合,它不是简单的AI+IoT叠加,而是应用人工智能、物联网等技术,以大数据、云计算为基础支撑,以半导体为算法载体,以网络安全技术作为实施保障,以5G为催化剂,对数据、知识和智能进行集成。

作者简介:大家好,我是 liuzhen007,是一位音视频技术爱好者,同时也是CSDN博客专家、华为云社区云享专家、签约作者,欢迎关注我分享更多音视频相关内容!

本文转载自: 掘金

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

每天一个 Linux 命令(17)—— tar 命令简介 命

发表于 2021-11-22

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

命令简介

tar 是 Linux 系统备份与恢复数据的基本工具之一。利用 tar 命令,可以把一大堆的文件和目录全部打包成一个文件,这对于备份文件或将几个文件组合成为一个文件以便于网络传输是非常有用的。

首先要弄清两个概念:打包和压缩。打包是指将一大堆文件或目录变成一个总的文件;压缩则是将一个大的文件通过一些压缩算法变成一个小文件。

为什么要区分这两个概念呢?这源于 Linux 中很多压缩程序只能针对一个文件进行压缩,这样当你想要压缩一大堆文件时,你得先将这一大堆文件先打成一个包(tar 命令),然后再用压缩程序进行压缩(gzip,bzip2 命令)。

Linux 下最常用的打包程序就是 tar 了,使用 tar 程序打出来的包我们常称为 tar 包,tar 包文件的命令通常都是以 .tar 结尾的。生成 tar 包后,就可以用其它的程序来进行压缩。

tar 具有 8 种不同的工作模式,可用于创建、替换、查询、更新、抽取、追加或比较档案文件,删除档案文件中的指定文件等。

命令格式

1
css复制代码tar[必要参数][选择参数][文件]

命令参数

必要参数

参数 解释
-A 新增压缩文件到已存在的压缩。
-B 设置区块大小。
-c 建立新的压缩文件。
-d 记录文件的差别。
-r 添加文件到已经压缩的文件。
-u 添加改变了和现有的文件到已经存在的压缩文件。
-x 从压缩的文件中提取文件。
-t 显示压缩文件的内容。
-z 支持 gzip 解压文件。
-j 支持 bzip2 解压文件。
-Z 支持 compress 解压文件。
-v 显示操作过程。
-l 文件系统边界设置。
-k 保留原有文件不覆盖。
-m 保留文件不被覆盖。
-W 确认压缩文件的正确性。

可选参数

参数 解释
-b 设置区块数目。
-C 切换到指定目录。
-f 指定压缩文件。
--help 显示帮助信息。
--version 显示版本信息。

常见解压/压缩命令

tar 文件格式

  • 解包:tar xvf FileName.tar
  • 打包:tar cvf FileName.tar DirName
  • (注:tar 是打包,不是压缩!)

.gz 文件格式

  • 解压 1:gunzip FileName.gz
  • 解压 2:gzip -d FileName.gz
  • 压缩:gzip FileName

.tar.gz 和 .tgz

  • 解压:tar zxvf FileName.tar.gz
  • 压缩:tar zcvf FileName.tar.gz DirName

.bz2 文件格式

  • 解压 1:bzip2 -d FileName.bz2
  • 解压 2:bunzip2 FileName.bz2
  • 压缩: bzip2 -z FileName

.tar.bz2 文件格式

  • 解压:tar jxvf FileName.tar.bz2
  • 压缩:tar jcvf FileName.tar.bz2 DirName

.bz 文件格式

  • 解压 1:bzip2 -d FileName.bz
  • 解压 2:bunzip2 FileName.bz
  • 压缩:未知

.tar.bz 文件格式

  • 解压:tar jxvf FileName.tar.bz
  • 压缩:未知

.Z 文件格式

  • 解压:uncompress FileName.Z
  • 压缩:compress FileName

.tar.Z 文件格式

  • 解压:tar Zxvf FileName.tar.Z
  • 压缩:tar Zcvf FileName.tar.Z DirName

.zip 文件格式

  • 解压:unzip FileName.zip
  • 压缩:zip FileName.zip DirName

.rar

  • 解压:rar x FileName.rar
  • 压缩:rar a FileName.rar DirName

参考文档

  • tar命令打包解压示例
  • 《Linux 常用命令简明手册》—— 邢国庆编著

本文转载自: 掘金

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

【Sentinel系列】 Sentinel 整合 Nacos

发表于 2021-11-22

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

Spring Cloud Sentinel 整合 Nacos 实现动态流控规则,步骤如下:

  1. 添加Nacos数据源的maven依赖·
1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.7.0</version>
</dependency>
2. 创建一个REST接口,用于测试
1
2
3
4
5
6
7
8
9
java复制代码@RestController
@RequestMapping("/sentinel")
public class SentinelController {

@GetMapping(value = "/info")
public String getName(){
return " Dynamic Rule";
}
}
  1. 在application.yml文件配置添加数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
yml复制代码spring:
application:
name: spring-cloud-sentinel
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8718
datasource:
nacos:
server-addr: 127.0.0.1:8848
data-id: ${spring.applicaton.name}-dev
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow

部分参数相关说明如下:

  • datasource:支持Nacos、Redis、Zookeeper、Apollo、file等,选择哪个类型的数据源对应配置对应的key
  • data-id:设置成${spring.application.name} ,用于区分不同应用的配置文件
  • data-type:配置项的内容格式,Spring Cloud Alibaba Sentinel 提供了JSON和XML两种内容格式,如果需要自定义内容格式,则把值设置为custom,并且converter-class指向converter类
  • rule-type:数据源找那个的规则是哪种类型,有flow、degrade、gw-flow、param-flow等。

访问http://127.0.0.1:8848/nacos 进入Nacos控制台,创建流控配置规则,具体的配置信息如下:

1
2
3
4
5
6
7
8
json复制代码{
"resource":"teaching-management",
"count":1000,
"grade":1,
"limitApp":"default",
"strategy":0,
"controlBehavior":0
}

访问http://127.0.0.1:8718 进入Sentinel Dashboard,找到对应的模块菜单下的【流控规则】,可看到在Nacos上锁配置的流控规则已经被加载了

image-20211122231443736.png

在Nacos 控制台上修改对应的流控规则后,可实时同步在Sentinel Dashboard上能查看流控规则的修改。

流控规则的动态修改有两种方式:

  1. 通过Nacos 控制台修改。
  2. 在Sentinel Dashboard上修改。

在Nacos控制台上修流控规则,可同步到Sentinel Dashboard,Nacos是一个流控规则的持久化动态修改平台,如果在Sentinel Dashboard上修改对应的流控规则应该也能同步到Nacos上,才能达到双向同步流控规则。但是Sentinel Dashboard还没能实现Sentinel Dashboard 同步流控规则到Nacos功能。

本文转载自: 掘金

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

Netty架构设计

发表于 2021-11-22

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

前言

Netty.png

Netty作为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。前面了解了一下Netty相关的基础知识,我们从Netty整体架构设计去了解一下,Netty的功能设计初衷。

我们主要从Netty功能特性、模块组件、运作过程来了解Netty的架构设计

功能特性

image.png

  • 传输服务 支持BIO和NIO
  • 容器集成 支持OSGI、JBossMC、Spring、Guice容器
  • 协议支持 HTTP、Protobuf、二进制、文本、WebSocket等一系列常见协议都支持。 还支持通过实行编码解码逻辑来实现自定义协议
  • Core核心 可扩展事件模型、通用通信API、支持零拷贝的ByteBuf缓冲对象

模块组件

这边就不介绍了后面梳理源码的时候在介绍。

工作基本原理

介绍服务端Netty的工作架构图:
image.png

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
java复制代码public static void main(String[] args) throws Exception {
//创建两个线程组bossGroup和workerGroup, 含有的子线程NioEventLoop的个数默认为cpu核数的两倍
// bossGroup只是处理连接请求 ,真正的和客户端业务处理,会交给workerGroup完成
EventLoopGroup bossGroup = new NioEventLoopGroup(10);
EventLoopGroup workerGroup = new NioEventLoopGroup(100000);
try {
//创建服务器端的启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来配置参数
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioServerSocketChannel作为服务器的通道实现
// 初始化服务器连接队列大小,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。
// 多个客户端同时来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {//创建通道初始化对象,设置初始化参数

@Override
protected void initChannel(SocketChannel ch) throws Exception {
//对workerGroup的SocketChannel设置处理器
ch.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println("netty server start。。");
//绑定一个端口并且同步, 生成了一个ChannelFuture异步对象,通过isDone()等方法可以判断异步事件的执行情况
//启动服务器(并绑定端口),bind是异步操作,sync方法是等待异步操作执行完毕
ChannelFuture cf = bootstrap.bind(9000).sync();
//给cf注册监听器,监听我们关心的事件
// 通过sync方法同步等待通道关闭处理完毕,这里会阻塞等待通道关闭完成
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}

2. 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码public static void main(String[] args) throws Exception {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动对象
//注意客户端使用的不是ServerBootstrap而是Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 使用NioSocketChannel作为客户端的通道实现
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//加入处理器
ch.pipeline().addLast(new NettyClientHandler());
}
});

System.out.println("netty client start。。");
//启动客户端去连接服务器端
ChannelFuture cf = bootstrap.connect("127.0.0.1", 9000).sync();
//对通道关闭进行监听
cf.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}

本文转载自: 掘金

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

MySQL事务隔离性与MVCC 写在前面 背景 隔离性导致的

发表于 2021-11-22

写在前面

  1. 事务缺乏隔离性会导致什么问题?
  2. 什么是事务隔离级别,为什么需要隔离级别?
  3. MVCC是什么,能解决什么问题?
  4. MVCC是怎么实现的?

背景

mysql作为一个基础服务,同一时刻一般会有多个应用服务作为客户端与之交互。每个应用服务在执行一个事务中的多条语句时,肯定不希望被其他客户端的事务影响,或影响其他客户端的事务

最简单的办法就是,所有事务排他性执行,当一个事务的所有语句执行完成(提交或回滚)后,其他事务的语句才能开始执行。但这样对性能的影响太大。实际上在大部分业务场景下,不需要这么强的隔离性,因此可以舍弃一部分隔离性来换取数据库服务的性能

隔离性导致的问题

如果不同的事务之间缺乏隔离性,可能会造成什么问题?

我们先定义什么是没有问题:事务执行结果和串行执行每个事务一样

脏写

脏写,即一个事务对另一个事务未提交的修改记录进行了修改

举个例子:两个事务A,B,当事务B对一条记录更新后,还没有提交,此时事务A更新该记录并提交。若之后事务B执行回滚,该记录变为B更新前的状态,不仅B的更新回滚了,事务A的更新也会回滚掉

这有什么问题呢?

  • 这与完全保证事务的隔离性即串行执行的结果不符,若串行执行,不管A,B谁先执行,在B回滚的情况下,该记录的最终结果一定是A修改后的结果,而不是现在B更新前的结果
  • 其次事务A明明在事务B的修改之后修改,最后结果是回到了B修改之前,好像更新丢失了一样,不符合大部分的业务预期

所幸Mysql Innodb的所有事务隔离级别中都能避免脏写

脏读

脏读,即一个事务读到了另一个事务未提交的修改

举个例子:两个事务A,B,当事务 B修改了一条数据后,还未提交,事务A读取该记录,并依据此记录进行一些操作。若之后事务B进行回滚操作,B修改的记录也就恢复原状,则称A之前读该记录的操作为脏读

这有什么问题呢?

  • 和脏写一样,若事务A,B串行执行,则一定不会出现脏读的情况,该操作和具有完全隔离性的操作表现出来的结果不一致
  • 若事务B执行到后面发现需要回滚,则该修改过记录也需要回滚。该修改后的值其实从一开始就不应该存在,事务A若依赖一个本不应该存在的数据进行一些后续的操作,可能造成一些不良后果。也就是错误的因导致错误的果

不可重复读

不可重复读,即一个事务对一条记录的读,不是每次都一样

举个例子:事务A一开始读一条记录X,结果为M,此时另一个事务B修改了记录X,将记录值M改为N并提交,事务A并未提交,再读记录X,发现X的值变为了N,也就是一个事务对一条记录的前后两次读结果不一样

这里的不可重复读只能读到已经提交的数据,避免了脏读的情况

那这样有什么问题呢?

  • 要说没有问题也是可以的,毕竟读的数据都是已经提交的,没有脏读的问题,只是在一个事务内多次查询一条记录的值结果不一样
  • 要说有问题也是有问题的,还是那句话,这和完全串行执行,也就是完全保证隔离性的执行结果不一样。若完全串行,当一个事务开始任何操作时,都不会都其他事务穿插执行,也就不会有其他事务修改任何记录,当然原事务每次读到的数据都是一样的。若用户期望的就是在一个事务中每次读出来结果都一样,那这就有问题

幻读

幻读,即一个事务根据某个条件查询一些记录,之后另一个事务插入了一些服务该条件的记录,原先的事务按照原先的条件再次查询时,会将后面插入的记录也读出来,称这一现象为幻读

虽然对之前读出过记录是可重复读的,但对新插入的记录没有这个保证

小总结

从脏写,脏读到不可重复读,幻读,每种隔离性问题的严重程度依次递减

其实每种隔离性问题你可以说他有问题,因为若比照完全隔离性,这些隔离性问题或多或少都不符合完全隔离性的定义

但也可以说没有问题,例如不可重复读:如果用户就是期望在事务中每次都读到最新且已提交的数据,那不可重复读这一特性也没什么问题

因此数据库通常支持用户自定义隔离级别,根据自己的需求进行选择,且在性能和隔离性之间达到平衡

事务的隔离级别

SQL标准中定义了四中隔离级别,每种隔离级别及其可能发生的隔离性问题如下所示:
​

隔离级别 脏读 不可重复读 幻影读
未提交读 可能发生 可能发生 可能发生
已提交读 - 可能发生 可能发生
可重复读 - - 可能发生
可序列化 - - -

​​

mysql在可重复读隔离级别下能禁止幻读发生

MVCC

未提交读隔离级别会产生除脏读以外的所有隔离性问题,其实现比较简单,每次读某条记录最新的值就行,不管产生该值的事务有没有提交

可序列化可以实现为串行执行每个事务,也比较简单

那已提交读和可重复读隔离级别是怎么实现避免脏读,和避免脏读及可重复读问题的呢?

Mysql Innodb使用MVCC (Multi Version Concurrency Control)多版本并发控制来实现,即当普通读(没有加锁读)其他事务正在写的记录时,选择读该记录的历史版本,以支持读-写操作并发执行,提升系统性能

什么是历史版本呢?

Mysql Innodb的聚集索引记录中每条记录都有两个隐藏列:

  • trx_id :每次某事务对某条聚簇索引记录进行改动(插入,更新,删除)时,都会把该事务的事务id 赋值给 trx_id 隐藏列

Innodb中每个事务都会被分配一个事务id,按从小到大递增分配,例如当前有两个事务id分别为1,2。那下次有新事务开启时其事务id就为3

  • roll_pointer :每次对某条聚簇索引记录进行改动时,不会将该记录的旧值丢弃,而是把该记录旧版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息

如下所示:该记录按时间先后顺序做了如下修改:

  1. 事务100插入了一条name为james的记录
  2. 事务101将该记录的name列从james更新为jerry
  3. 事务102将该记录的name列从jerry改为tom

image.png
为了支持MVCC的访问,这些历史版本都会保留,直到系统判断再也用不到这些历史版本为止

避免脏读

避免脏读的核心:保证每次读到记录的版本所属的事务都是已提交的

Innodb设计了一个叫做ReadView的结构,主要包含以下4个字段

  • m_ids:在进行读操作开始时,系统中活跃的事务id列表
  • min_trx_id:m_ids中最小的值
  • max_trx_id:在进行读操作开始时,系统应该分配给下一个事务的id
  • creator_trx_id:执行读操作所属事务的事务id

有了ReadView,我们就能判断在读某一条记录时,应该读该记录的哪个版本,也就是哪个版本对当前事务是可见的:

  • 若某条记录某个版本的trx_id和当前ReadView的creator_trx_id相同,说明该版本就是当前事务创建的,那一定可见
  • 若某条记录某个版本的trx_id小于当前ReadView的min_trx_id:
+ 首先该版本所属的事务id不在`m_ids`中,说明该事务是不活跃的,则要么已经提交,要么还没开始
+ 而`m_ids`中的事务id都大于该版本的`trx_id`,且事务id是递增分配,说明不可能没开始,那一定是已提交的,已提交的版本是可以读的
  • 若某条记录某个版本的trx_id大于当前ReadView的max_trx_id:
+ 说明该版本在`ReadView`生成后才出现,该版本不能被当前事务访问
  • 否则只剩下一种情况:若某条记录某个版本的trx_id在[min_trx_id,max_trx_id-1]之间,则需要看该trx_id在不在m_ids里
+ 若在,说明记录的版本是当前活跃,也就是未提交的事务创建的,既然未提交,当然是不可见
+ 若不在,说明该记录的版本是已经提交的事务生成的,已经已经提交,那就是可见

注意区间[m_ids中的最大值,max_trx_id-1]中的事务也可能是可见的,例如当前系统中有活跃事务1,2,3,4此时max_trx_id=5,事务3,4提交

此时创建ReadView,m_ids=[1,2],区间[m_ids中的最大值,max_trx_id-1]为[3,4],3,4在创建当前ReadView之前已经提交,因此可见

image.png
每次读从目标记录的最新版本开始判断,若某个版本不可见,则顺着该记录版本链往下寻找,直到找到某个可见的版本为止。若该记录的所有版本都不可见,说明在记录在当前事务创建ReadView前还不存在,则返回空即可

在每次读之前,根据系统当前所有事务的活跃或提交情况,创建一个ReadView,依据该ReadView判断目标记录的哪个版本可读,即可避免脏读,因为读到的版本一定是已经提交的,且此刻系统中所有提交的都可读。已提交读隔离级别采用该方式实现

避免不可重复读

我们再看怎么避免不可重复读:

和避免脏读的操作不一样的是,只在事务中第一次读时创建一份ReadView,该事务中后续的读操作都根据该ReadView判断需要读的记录的某个版本是否可见

第一次生成ReadView时相当于给当前系统中的事务执行情况打了快照,该事务中后续的读都基于这个快照来判断某记录的版本是否可见,当然每次读出来的结果都是一样的,可重复读隔离级别采用该方式实现

总结

  • 事务缺乏隔离性,会造成脏写,脏读,不可重复读,幻读的隔离性问题
  • 数据库支持用户设置事务隔离级别,在隔离性和性能直接权衡
  • Mysql Innodb通过MVCC机制来实现已提交读和可重复读隔离级别

本文转载自: 掘金

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

1…228229230…956

开发者博客

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