服务器将加密后的密文返回给客户端。
客户端接收服务器返回的密文,用自己的密钥(客户端密钥)对称解密,得到服务器返回的数据。
8、说说事务隔离级别和可重复读实现原理
8.1 数据库四大隔离级别
为了解决并发事务中的脏读、不可重复读、幻读等问题,数据库叔叔设计了四种隔离级别。 它们是未提交读、已提交读、可重复读和序列化()。
四大隔离级别存在哪些并发问题?
8.2 读取视图可见性规则
Read View的可见性规则如下:
如果数据事务ID < ,则表示生成该版本的事务在生成Read View之前已经提交(因为事务ID是递增的),因此该版本可以被当前事务访问。
如果>=,则说明生成该版本的事务是在Read View生成后生成的,因此当前事务无法访问该版本。
如果=
如果m_ids包含,代表Read View产生的时间,这个事务还没有提交。 但如果数据相等,则意味着数据是自己生成的,因此是可见的。
如果m_ids包含且不等于,则该事务在Read View生成时并未提交,也不是自己产生的,所以当前事务也是不可见的;
如果m_ids不包含,则说明你的交易在Read View生成之前就已经提交了,当前交易可以看到修改的结果。
8.3 可重复读实现原理
数据库通过锁定来实现隔离级别。 比如,如果你想一个人呆着,不被别人打扰,你可以把自己锁在屋子里,在门上加一把锁! 序列化隔离级别是通过加锁来实现的。 但如果频繁加锁,性能就会下降。 于是设计数据库的大叔就想到了MVCC。
可重复读取的实现原理是MVCC多版本并发控制。 在事务范围内,两个相同的查询读取相同的记录但返回不同的数据。 这是不可重复的读取。 可重复读隔离级别是为了解决不可重复读问题。
基于MVCC查询一条记录的流程是怎样的?
获取交易本身的版本号,即交易ID
获取阅读视图
查询得到的数据,然后在Read View中对比交易版本号。
如果不满足Read View的可见性规则,则需要Undo log中的历史快照;
最终返回符合规则的数据
MVCC是通过Read View + Undo Log来实现的。 Undo Log保存历史快照,Read View可见性规则帮助确定当前版本的数据是否可见。
可重复读(RR)隔离级别如何解决不可重复读问题?
假设事务A和B存在,SQL执行流程如下:
在可重复读(RR)隔离级别下,一个事务中只会获取一次读视图,该读视图由副本共享,从而保证每个查询中的数据相同。
假设当前有一张表,插入一条初始化数据如下:
基于MVCC,我们看一下执行流程:
1、A发起一笔交易,首先获取交易ID为100
2、B打开交易,获取交易ID为101
3.事务A生成一个读视图。 读取视图对应的值如下:
然后回到版本链,开始从版本链中挑选可见记录:
从图中可以看出,最新版本的列名内容为Sun Quan,该版本的值为100。开始执行读视图可见性规则验证:
min_limit_id(100)=
creator_trx_id = trx_id =100;
可以看出,当前交易对这条记录可见=100。 于是我们找到了一条记录,名字叫孙权。
4、事务B进行修改操作,将姓名改为曹操。
将原始数据复制到undo log,然后修改数据。 在undo log中标记事务ID和前一个数据版本的地址。
5.事务B提交事务
6、事务A再次执行查询操作。 由于是RR(可重复读)隔离级别,旧的读视图副本将被重用。 Read View对应的值如下:
然后再次回到版本链,从版本链中选取可见记录:
从图中可以看出,最新版本的列名内容为曹操,该版本的值为101。开始执行读视图可见性规则验证:
min_limit_id(100)=
因为 m_ids{100,101} 包含 trx_id(101),并且 creator_trx_id (100) 不等于 trx_id(101),所以 trx_id=101 这个记录,对于当前事务是不可见的。
这时候呢,版本链 roll_pointer 跳到下一个版本,trx_id=100 这个记录,再次校验是否可见:
min_limit_id(100)=
因为 m_ids{100,101} 包含 trx_id(100),并且 creator_trx_id (100) 等于 trx_id(100),所以,trx_id=100 这个记录,对于当前事务是可见的,所以两次查询结果,都是 name=孙权 的那个记录。即在可重复读(RR)隔离级别下,复用老的 Read View 副本,解决了不可重复读的问题。
9、我们来说说索引在什么场景下会失效?
10.什么是虚拟内存
虚拟内存就是虚拟内存。 其核心思想是保证每个程序都有自己的地址空间。 地址空间被划分为多个块,每个块都有连续的地址空间。 同时,物理空间也被划分为多个区块。 块大小与虚拟地址空间的块大小一致。 操作系统会自动将虚拟地址空间映射到物理地址空间。 程序只需要关注虚拟内存,请求的也是虚拟内存,但实际并没有使用。 是物理内存。
现代操作系统使用虚拟内存,即虚拟地址代替物理地址。 使用虚拟内存有两个好处:
零拷贝实现的思想利用了虚拟内存的优势:多个虚拟内存可以指向同一个物理地址,内核空间和用户空间的虚拟地址可以映射到同一个物理地址。 这样可以减少IO数据的副本数量,如下:
11、实行排名,如高考成绩排名
排名版本的实现一般使用Redis的zset数据类型。
使用格式如下:
zadd key score member [score member ...],zrank key member
demo实现如下:
12.分布式锁实现
分布式锁是控制分布式系统中不同进程共同访问共享资源的锁的实现。 秒杀订单、抢红包等业务场景都需要用到分布式锁。
我们项目中经常使用Redis作为分布式锁。 我选取了几种Redis分布式锁的实现方法。 我们来讨论一下,看看有没有问题。
12.1 命令setnx+单独写入
if(jedis.setnx(key,lock_value) == 1){ //加锁
expire(key,100); //设置过期时间
try {
do something //业务请求
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}
如果执行setnx加锁后,设置过期时间时,进程崩溃或者需要重启维护,那么锁就会“不朽”,其他线程永远无法获得锁,所以分布式锁不能像这完成了。
12.2 setnx + value value为过期时间
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);
// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
笔者见过一些开发者通过这种方式实现分布式锁,但这种方案也存在以下缺点:
12.3 set的扩展命令(set ex px nx)(注意可能出现的问题)
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}
该解决方案可能存在问题:
12.4 set ex px nx + 验证唯一随机值然后删除
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
//判断是不是当前线程加的锁,是才释放
if (uni_request_id.equals(jedis.get(key))) {
jedis.del(key); //释放锁
}
}
}
这里判断当前线程添加和释放的锁是否是原子操作。 如果调用jedis.del()释放锁,该锁可能不再属于当前客户端,其他人添加的锁就会被释放。
一般都是用lua脚本来代替。 lua脚本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
这个方法相当不错,一般情况下,这个实现方法已经可以使用了。 但是存在一个问题,锁已经过期被释放,业务还没有完成(其实业务处理时间估计一般都不是问题)。
12.5
分布式锁可能会出现锁过期、释放等问题,导致业务未完成。 有的朋友认为把锁过期时间设置得稍微长一点就可以了。 其实我们想象一下,是否可以为获得锁的线程开启一个调度的守护线程,每隔一段时间检查一下锁是否还存在。 如果存在,则会延长锁的过期时间,防止过期后锁被提前释放。
目前的开源框架解决了这个分布式锁问题。 我们先看一下其基本原理:
一旦线程锁定成功,就会启动看门狗。 它是一个后台线程,每 10 秒检查一次。 如果线程1仍然持有锁,那么锁key的生存时间就会不断延长。 。 因此,使用它解决了过期锁释放和业务执行未完成的问题。
13. 零拷贝
零复制意味着不需要将数据从一个存储区域复制到另一个存储区域。 意思是在传统IO模型中,CPU副本数为0。是IO的优化方案:
13.1 传统IO流程
流程图如下:
从流程图可以看出,传统的IO读写过程包括4次上下文切换(4次用户态和内核态切换)和4次数据拷贝(2次CPU拷贝和2次DMA拷贝)。
13.2 mmap+write实现的零拷贝
mmap的函数原型如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap使用虚拟内存,可以将内核空间和用户空间的虚拟地址映射到同一个物理地址,从而减少数据拷贝的次数!
mmap+write实现的零拷贝流程如下:
可以发现,mmap+write实现的零拷贝中,I/O发生了4次用户空间和内核空间的上下文切换,以及3次数据拷贝。 三个数据副本包括两个DMA副本和一个CPU副本。
mmap 将读缓冲区的地址映射到用户缓冲区的地址。 内核缓冲区和应用程序缓冲区是共享的,因此保存了 CPU 副本。 并且用户进程内存是虚拟的,仅映射到内核的读缓冲区。 ,可以节省一半的内存空间。
实施零复制
是.1内核版本之后引入的系统调用函数。 API如下:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
表示两个文件描述符之间的数据传输。 它在操作系统内核中进行操作,避免了数据从内核缓冲区到用户缓冲区的复制操作,因此可以用来实现零复制。
实现的零拷贝流程如下:
实施零拷贝
用户进程发起系统调用,上下文( 1)从用户态变为内核态。
DMA 控制器将数据从硬盘复制到内核缓冲区。
CPU将读缓冲区中的数据复制到缓冲区
DMA控制器,异步地将数据从缓冲区复制到网卡,
上下文(开关 2)从内核模式切换回用户模式,并且调用返回。
可以发现,在零拷贝实现中,用户空间和内核空间发生了两次上下文切换,并且在I/O中发生了3次数据拷贝。 三个数据副本包括两个DMA副本和一个CPU副本。
我们可以将CPU副本数量减少到0吗?
有的,就是带有DMA采集复制功能!
+DMA / 实现零拷贝
Linux 2.4版本之后进行了优化升级,引入了SG-DMA技术。 事实上,在DMA复制中添加了/操作,可以直接从内核空间缓冲区读取数据到网卡。 利用这一特性实现零拷贝意味着可以多保存一份CPU拷贝。
+DMA/实现的零拷贝流程如下:
用户进程发起系统调用,上下文( 1)从用户态变为内核态。
DMA 控制器将数据从硬盘复制到内核缓冲区。
CPU将内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到缓冲区。
DMA控制器根据文件描述符信息直接将数据从内核缓冲区复制到网卡。
上下文(开关 2)从内核模式切换回用户模式,并且调用返回。
可以发现,通过+DMA/实现的零拷贝,I/O发生了2次用户空间和内核空间的上下文切换,以及2次数据拷贝。 其中两个数据副本是数据包 DMA 副本。 这是真正的零拷贝技术。 整个过程中没有数据通过CPU传输。 所有数据均通过DMA传输。
14.
它是Java中的一个关键字,是一种同步锁。 关键字可以作用于方法或代码块。
在一般采访中。 你可以这样回答:
14.1,,
如果作用于代码块,反编译可以看到两条指令: , 。 JVM使用 和 两条指令来实现同步。
如果该操作应用于方法,则反编译可以看到标记。 JVM 通过添加方法访问标识符(标志)来实现同步功能。
14.2 监控
之后怎么样了? 操作系统的()是一个概念原理及其实现。
在Java虚拟机中,()是由()实现的,其主要数据结构如下:
{
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
几个关键字段的含义如图:
14.3 Java 的工作原理
14.4 对象和关联
Mark Word用于存储对象本身的运行时数据,如哈希码()、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
重量级锁,指向互斥体的指针。 其实它是一个重量级锁,也就是说是一个对象锁。 Mark Word锁定标志为10,指针指向对象的起始地址。
15. 分布式ID生成方案有哪些? 什么是雪花算法?
分布式ID生成方案主要包括:
什么是雪花算法?
雪花算法是一种生成分布式全局唯一ID的算法,生成的ID称为ID。 该算法由推文 ID 创建并用于推文 ID。
ID 有 64 位。
雪花算法
-EOF-