校招python总结--建议全文背诵

Python基础

1、Python判断对象是否相等(== 和 is)

Python中的对象包含三个基本要素,分别是:id:用来唯一标识一个对象,可以理解为内存地址;type:标识对象的类型;value:对象的值;

== :比较两个对象的内容是否相等,即两个对象的 value 是否相等,无论 id 是否相等,默认会调用对象的 eq()方法。
is: 比较的是两个对象是不是完全相同,即他们的 id 要相等。也就是说如果 a is b 为 True,那么 a == b 也为True。

2、如何在Python中管理内存?

Python内存由Python私有堆空间管理。所有Python对象数据结构都位于私有堆空间中。程序员无法访问这个私有堆空间,解释器负责处理这个Python私有堆空间。Python内存管理器的Python堆空间的分配,核心API允许程序员使用一些工具来编写代码。Python还拥有一个内置的grabage收集器,它回收所有未使用的内存,并释放内存并使其可用到堆空间。

Python的内存管理机制可以从三个方面来讲:

(1)垃圾回收

(2)引用计数

(3)内存池机制

一、垃圾回收:

python不像C++,Java等语言一样,他们可以不用事先声明变量类型而直接对变量进行赋值。对Python语言来讲,对象的类型和内存都是在运行时确定的。这也是为什么我们称Python语言为动态类型的原因(这里我们把动态类型可以简单的归结为对变量内存地址的分配是在运行时自动判断变量类型并对变量进行赋值)。

垃圾回收

1、当内存中有不再使用的部分时,垃圾收集器就会把他们清理掉。它会去检查那些引用计数为0的对象,然后清除其在内存的空间。当然除了引用计数为0的会被清除,还有一种情况也会被垃圾收集器清掉:当两个对象相互引用时,他们本身其他的引用已经为0了。

2、垃圾回收机制还有一个循环垃圾回收器, 确保释放循环引用对象(a引用b, b引用a, 导致其引用计数永远不为0)。

二、引用计数:

Python采用了类似Windows内核对象一样的方式来对内存进行管理。每一个对象,都维护这一个对指向该对象的引用的计数。如图所示(图片来自Python核心编程)

img

x = 3.14

y = x

我们首先创建了一个对象3.14, 然后将这个浮点数对象的引用赋值给x,因为x是第一个引用,因此,这个浮点数对象的引用计数为1. 语句y = x创建了一个指向同一个对象的引用别名y,我们发现,并没有为Y创建一个新的对象,而是将Y也指向了x指向的浮点数对象,使其引用计数为2.

我们可以很容易就证明上述的观点:

img

变量a 和 变量b的id一致(我们可以将id值想象为C中变量的指针).

我们援引另一个网址的图片来说明问题:对于C语言来讲,我们创建一个变量A时就会为为该变量申请一个内存空间,并将变量值 放入该空间中,当将该变量赋给另一变量B时会为B申请一个新的内存空间,并将变量值放入到B的内存空间中,这也是为什么A和B的指针不一致的原因。如图:

img img

int A = 1 int A = 2

而Python的情况却不一样,实际上,Python的处理方式和Javascript有点类似,如图所示,变量更像是附在对象上的标签(和引用的定义类似)。当变量被绑定在一个对象上的时候,该变量的引用计数就是1,(还有另外一些情况也会导致变量引用计数的增加),系统会自动维护这些标签,并定时扫描,当某标签的引用计数变为0的时候,该对就会被回收。

img img img

a = 1 a = 2 b = a

三、内存池机制

img

Python的内存机制以金字塔型,-1,-2层主要有操作系统进行操作,

第0层是C中的malloc,free等内存分配和释放函数进行操作;

第1层和第2层是内存池,有Python的接口函数PyMem_Malloc函数实现,当对象小于256K时有该层直接分配内存;

第3层是最上层,也就是我们对Python对象的直接操作;

在 C 中如果频繁的调用 malloc 与 free 时,是会产生性能问题的.再加上频繁的分配与释放小块的内存会产生内存碎片. Python 在这里主要干的工作有:

  • 如果请求分配的内存在1~256字节之间就使用自己的内存管理系统,否则直接使用 malloc。这里还是会调用 malloc 分配内存,但每次会分配一块大小为256k的大块内存.

  • 经由内存池登记的内存到最后还是会回收到内存池,并不会调用 C 的 free 释放掉.以便下次使用。对于简单的Python对象,例如数值、字符串,元组(tuple不允许被更改)采用的是复制的方式(深拷贝?),也就是说当将另一个变量B赋值给变量A时,虽然A和B的内存空间仍然相同,但当A的值发生变化时,会重新给A分配空间,A和B的地址变得不再相同

3、python的闭包和装饰器

就是你调用了一个函数A,这个函数A返回了一个函数B给你。这个返回的函数B就叫做闭包。你在调用函数A的时候传递的参数就是自由变量。多层函数嵌套,(函数里面还有定义函数,一般是两个),往往内层函数会用到外层函数的变量,把内层函数以及外部函数的变量当成一个特殊的对象,这就是闭包。闭包比面向对象更纯净、更轻量,既有数据又有执行数据的代码;比普通函数功能更强大,不仅有代码还有数据;
一、闭包的应用场景

  1. 统计函数的被调用次数

  2. 当做装饰器使用

def make_averager():
    series = [] # series 相对averager函数而言是全局变量

    def averager(new_value):
        series.append(new_value) #此处访问了全局变量series
        total = sum(series)
        return total/len(series)

    return averager


avg = make_averager()
使用make_average 函数返回的是averager函数

当这个过程发生后,理论上讲make_averager的任务就完成了,serier变量是应该消失的,此时,如果使用avg会出现报错

但是
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12) 
11.0

说明series的变量依然存在

匿名函数、普通函数、闭包、面向对象的区别?
1). 匿名函数能够完成基本的简单功能,传递是这个函数的引用 只有功能。
2). 普通函数能够完成较为复杂的功能,传递是这个函数的引用 只有功能。
3). 闭包能够将较为复杂的功能,传递是这个闭包中的函数以及数据,占用资源比较小
4). 对象能够完成最为复杂的功能,传递是数据+功能,但占用大量空间,浪费资源。

4、Lambda表达式
calc=lambda x,y:x**y
print(calc(2,3))


sum_lambda = lambda a, b, c: a + b + c


#一般解决方案
li = [1,2,3,4,5,6,7,8,9]
for ind,val in enumerate(li):
    li[ind] = val * val
print(li)
# [1, 4, 9, 16, 25, 36, 49, 64, 81]

# 高级解决方案
li = [1,2,3,4,5,6,7,8,9]
print(list(map(lambda x:x*x,li)))
# [1, 4, 9, 16, 25, 36, 49, 64, 81]



#接受一个list并利用reduce()求积
from functools import reduce
li = [1,2,3,4,5,6,7,8,9]
print(reduce(lambda x,y:x * y,li))
# 结果=1*2*3*4*5*6*7*8*9 = 362880



# 把下面单词以首字母排序
li = ['bad', 'about', 'Zoo', 'Credit']
print(sorted(li, key = lambda x : x[0]))
# 输出['Credit', 'Zoo', 'about', 'bad']
"""
对字符串排序,是按照ASCII的大小比较的,由于'Z' < 'a',结果,大写字母Z会排在小写字母a的前面。
"""

# 假设我们用一组tuple表示学生名字和成绩:

L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
# 请用sorted()对上述列表分别按名字排序
print(sorted(L, key = lambda x : x[0]))
# 输出[('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]

# 再按成绩从高到低排序
print(sorted(L, key = lambda x : x[1], reverse=True))
# 输出[('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]


result = [li+3 for li in range(4)]
print(result)


a_list=[i*i for i in range(10)]
5、字典和数组

有序字典

  • 遍历字典,返回数据,和定义字典时数据顺序,一致。
  • 按顺序插入,遍历时,按顺序返回。
import collections
x =collections.OrderedDict() #有序字典
x['a'] = 3
x['b'] = 5
x['c'] = 8

>>> x    #输出按输入的顺序输出
OrderedDict([('a', 3), ('b', 5),('c', 8)])

6、生成器和迭代器

迭代器

迭代是Python最强大的功能之一,是访问集合元素的一种方式。

迭代器是一个可以记住遍历的位置的对象。

迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

迭代器有两个基本的方法:iter()next()

字符串,列表或元组对象都可用于创建迭代器:

>>> list=[1,2,3,4]
>>> it = iter(list)    # 创建迭代器对象
>>> print (next(it))   # 输出迭代器的下一个元素
1
>>> print (next(it))
2
>>>

 
list=[1,2,3,4]
it = iter(list)    # 创建迭代器对象
for x in it:
    print (x, end=" ")
    
import sys         # 引入 sys 模块
list=[1,2,3,4]
it = iter(list)    # 创建迭代器对象
while True:
    try:
        print (next(it))
    except StopIteration:
        sys.exit()

生成器

在 Python 中,使用了 yield 的函数被称为生成器(generator)。

跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。

调用一个生成器函数,返回的是一个迭代器对象。

以下实例使用 yield 实现斐波那契数列:

import sys
 
def fibonacci(n): # 生成器函数 - 斐波那契
    a, b, counter = 0, 1, 0
    while True:
        if (counter > n): 
            return
        yield a
        a, b = b, a + b
        counter += 1
f = fibonacci(10) # f 是一个迭代器,由生成器返回生成
 
while True:
    try:
        print (next(f), end=" ")
    except StopIteration:
        sys.exit()

类方法和静态方法的区

列表生成式

li2=[i*i for i in range(1,11)]
li=[i*i for i in range(1,11) if i%2==0]
li2=[i+j for i in "ABC" for j in "123"]
li1=[
  [1,2,3],
  [4,5,6],
  [7,8,9]
]
print([item2 for item1 in li1 for item2 in item1 if item2%2==0])

迭代生成式

>>> def square(x) :         # 计算平方数
...     return x ** 2
...
>>> map(square, [1,2,3,4,5])    # 计算列表各个元素的平方
<map object at 0x100d3d550>     # 返回迭代器
>>> list(map(square, [1,2,3,4,5]))   # 使用 list() 转换为列表
[1, 4, 9, 16, 25]
>>> list(map(lambda x: x ** 2, [1, 2, 3, 4, 5]))   # 使用 lambda 匿名函数
[1, 4, 9, 16, 25]
>>>
7、元组和列表的区别、底层实现
元组和列表有哪些区别呢?

一个可以修改(列表),一个不可以修改(元组)

列表和元组的底层实现
  • 元组和列表同属序列类型,且都可以按照特定顺序存放一组数据,数据类型不受限制,只要是 Python 支持的数据类型就可以。
元组和列表有哪些区别呢?
  • 元组和列表最大的区别就是,列表中的元素可以进行任意修改,就好比是用铅笔在纸上写的字,写错了还可以擦除重写;而元组中的元素无法修改,除非将元组整体替换掉,就好比是用圆珠笔写的字,写了就擦不掉了,除非换一张纸。可以理解为,tuple 元组是一个只读版本的 list 列表。

需要注意的是,这样的差异势必会影响两者的存储方式,我们来直接看下面的例子:

listdemo = []
listdemo.__sizeof__()
40
tupleDemo = ()
tupleDemo.__sizeof__()
24

可以看到,对于列表和元组来说,虽然它们都是空的,但元组却比列表少占用 16 个字节,这是为什么呢?

  • 事实上,就是由于列表是动态的,它需要存储指针来指向对应的元素(占用 8 个字节)。
  • 另外,由于列表中元素可变,所以需要额外存储已经分配的长度大小(占用 8 个字节)。

但是对于元组,情况就不同了,元组长度大小固定,且存储元素不可变,所以存储空间也是固定的。

既然列表这么强大,还要元组这种序列类型干什么?
  • 通过对比列表和元组存储方式的差异,我们可以引申出这样的结论,即元组要比列表更加轻量级,所以从总体上来说,元组的性能速度要优于列表。

  • 另外,Python 会在后台,对静态数据做一些资源缓存。通常来说,因为垃圾回收机制的存在,如果一些变量不被使用了,Python 就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。

  • 但是对于一些静态变量(比如元组),如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存

  • 这样的话,当下次再创建同样大小的元组时,Python 就可以不用再向操作系统发出请求去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。

下面的例子,是计算初始化一个相同元素的列表和元组分别所需的时间。可以看到,元组的初始化速度要比列表快 5 倍。

C:\Users\qinjl>python -m timeit 'x=(1,2,3,4,5,6)'
20000000 loops, best of 5: 9.97 nsec per loop
C:\Users\qinjl>python -m timeit 'x=[1,2,3,4,5,6]'
5000000 loops, best of 5: 50.1 nsec per loop
  • 当然,如果你想要增加、删减或者改变元素,那么列表显然更优。因为对于元组来说,必须得通过新建一个元组来完成。总的来说,元组确实没有列表那么多功能,但是元组依旧是很重要的序列类型之一,元组的不可替代性体现在以下这些场景中:

  • 元组作为很多内置函数和序列类型方法的返回值存在,也就是说,在使用某些函数或者方法时,它的返回值会是元组类型,因此你必须对元组进行处理。

  • 元组比列表的访问和处理速度更快,因此,当需要对指定元素进行访问,且不涉及修改元素的操作时,建议使用元组。

元组可以在映射(和集合的成员)中当做“键”使用,而列表不行。

列表和元组的底层实现
有关列表(list)和元组(tuple)的底层实现,分别从它们的源码来进行分析。

首先来分析 list 列表,它的具体结构如下所示:

typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;
    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    Py_ssize_t allocated;
} PyListObject;

list 列表实现的源码文件 listobject.h 和 listobject.c。

list 本质上是一个长度可变的连续数组:

  • 其中 ob_item 是一个指针列表,里边的每一个指针都指向列表中的元素,而 allocated 则用于存储该列表目前已被分配的空间大小。需要注意的是,allocated 和列表的实际空间大小不同,列表实际空间大小,指的是 len(list) 返回的结果,也就是上边代码中注释中的 ob_size,表示该列表总共存储了多少个元素。

  • 而在实际情况中,为了优化存储结构,避免每次增加元素都要重新分配内存,列表预分配的空间 allocated 往往会大于 ob_size。因此 allocated 和 ob_size 的关系是:allocated >= len(list) = ob_size >= 0。如果当前列表分配的空间已满(即 allocated == len(list)),则会向系统请求更大的内存空间,并把原来的元素全部拷贝过去。

接下来再分析元组,如下所示为 Python 3.7 tuple 元组的具体结构:

typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];
    /* ob_item contains space for 'ob_size' elements.
     * Items must normally not be NULL, except during construction when
     * the tuple is not yet visible outside the function that builds it.
     */
} PyTupleObject;

tuple 元组实现的源码文件 tupleobject.h 和 tupleobject.c。

  • tuple 和 list 相似,本质也是一个数组,但是空间大小固定。不同于一般数组,Python 的 tuple 做了许多优化,来提升在程序中的效率。举个例子,为了提高效率,避免频繁的调用系统函数 free 和 malloc 向操作系统申请和释放空间,tuple 源文件中定义了一个 free_list:static PyTupleObject *free_list[PyTuple_MAXSAVESIZE];

所有申请过的,小于一定大小的元组,在释放的时候会被放进这个 free_list 中以供下次使用。也就是说,如果以后需要再去创建同样的 tuple,Python 就可以直接从缓存中载入。

原文链接:https://blog.csdn.net/qq_32727095/article/details/118383426

8、python set()去重的底层原理
一、set去重简单实例
ls = [1,2,3,1,2]
print(set(ls))

img

我们知道对于一个列表最简单的去重方法就是直接调用set函数,利用集合元素的唯一性,就可以做到去重。但是,这个底层原理究竟是什么样的却一直半解。

且看下面剖析

二、重新set实现机制
class Foo:
    def __init__(self,name,count):
        self.name = name
        self.count = count
    def __hash__(self):
        print("%s调用了哈希方法"%self.name)
        return hash(id(self))
    def __eq__(self, other):
        print("%s调用了eq方法")
        if self.__dict__ == other.__dict__:
            return True
        else:return False
f1 = Foo('f1',1)
f2 = Foo('f2',2)
f3 = Foo('f3',3)
ls = [f1,f2,f3]
print(set(ls))

img

从上面可以看出,set方法就是去调用hash方法(内置的),然后根据哈希值一不一样就行去重判断,但是事实就是样吗?且看下面程序。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xfzW7Pz-1647795627089)(/Users/hello/Desktop/面试/image/截屏2022-03-19 15.10.12.png)]我看可以看出,实际上f1,f2的哈希值是相等的,但是set并没有这么简单就判断f1,f2是重复的,而是进一步通过eq方法判断这两个值是否相等,只有相等时才会认为这两个之间实际上是同一个。为了验证上面的说法,我们来看看下面的代码。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HVnteRKD-1647795627090)(/Users/hello/Desktop/面试/image/截屏2022-03-19 15.10.27.png)]可以看出去重后,只有两个元素,所以上面说法得证。

三、结论
set的去重是通过两个函数__hash__和__eq__结合实现的。
1、当两个变量的哈希值不相同时,就认为这两个变量是不同的
2、当两个变量哈希值一样时,调用__eq__方法,当返回值为True时认为这两个变量是同一个,应该去除一个。返回FALSE时,不去重
9、Python字典底层实现原理

​ 在Python中,字典是通过散列表或说哈希表实现的。也就是说,字典也是一个数组,但数组的索引是键经过哈希函数处理后得到的散列值。哈希函数的目的是使键均匀地分布在数组中,并且可以在内存中以O(1)的时间复杂度进行寻址,从而实现快速查找和修改。

​ 哈希表中哈希函数的设计困难在于将数据均匀分布在哈希表中,从而尽量减少哈希碰撞和冲突。由于不同的键可能具有相同的哈希值,即可能出现冲突,高级的哈希函数能够使冲突数目最小化。Python中并不包含这样高级的哈希函数,几个重要(用于处理字符串和整数)的哈希函数是常见的几个类型。通常情况下建立哈希表的具体过程如下:

  • 1、数据添加:把key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。

  • 2、数据查询:再次使用哈希函数将key转换为对应的数组下标,并定位到数组的位置获取value。
    哈希函数就是一个映射,因此哈希函数的设定很灵活,只要使得任何关键字由此所得的哈希函数值都落在表长允许的范围之内即可。本质上看哈希函数不可能做成一个一对一的映射关系,其本质是一个多对一的映射,这也就引出了下面一个概念–哈希冲突或者说哈希碰撞。哈希碰撞是不可避免的,但是一个好的哈希函数的设计需要尽量避免哈希碰撞。

Python2 中使用使用开放地址法解决冲突。

CPython使用伪随机探测(pseudo-random probing)的散列表(hash table)作为字典的底层数据结构。由于这个实现细节,只有可哈希的对象才能作为字典的键。字典的三个基本操作(添加元素,获取元素和删除元素)的平均事件复杂度为O(1)。

Python中所有不可变的内置类型都是可哈希的。
可变类型(如列表,字典和集合)就是不可哈希的,因此不能作为字典的键。

常见的哈希碰撞解决方法:

  • 1 开放寻址法(open addressing)

    开放寻址法中,所有的元素都存放在散列表里,当产生哈希冲突时,通过一个探测函数计算出下一个候选位置,如果下一个获选位置还是有冲突,那么不断通过探测函数往下找,直到找个一个空槽来存放待插入元素。

    开放地址的意思是除了哈希函数得出的地址可用,当出现冲突的时候其他的地址也一样可用,常见的开放地址思想的方法有线性探测再散列,二次探测再散列等,这些方法都是在第一选择被占用的情况下的解决方法。

  • 2 再哈希法

    这个方法是按顺序规定多个哈希函数,每次查询的时候按顺序调用哈希函数,调用到第一个为空的时候返回不存在,调用到此键的时候返回其值。

  • 3 链地址法

    将所有关键字哈希值相同的记录都存在同一线性链表中,这样不需要占用其他的哈希地址,相同的哈希值在一条链表上,按顺序遍历就可以找到。

  • 4 公共溢出区

    ​ 其基本思想是:所有关键字和基本表中关键字为相同哈希值的记录,不管他们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。

  • 5 装填因子α

    一般情况下,处理冲突方法相同的哈希表,其平均查找长度依赖于哈希表的装填因子。哈希表的装填因子定义为表中填入的记录数和哈希表长度的比值,也就是标志着哈希表的装满程度。直观看来,α越小,发生冲突的可能性就越小,反之越大。一般0.75比较合适,涉及数学推导。

在python中一个key-value是一个entry,

entry有三种状态

Unused: me_key == me_value == NULL

Unused是entry的初始状态,key和value都为NULL。插入元素时,Unused状态转换成Active状态。这是me_key为NULL的唯一情况。

Active: me_key != NULL and me_key != dummy 且 me_value != NULL

插入元素后,entry就成了Active状态,这是me_value唯一不为NULL的情况,删除元素时Active状态刻转换成Dummy状态。

Dummy: me_key == dummy 且 me_value == NULL

此处的dummy对象实际上一个PyStringObject对象,仅作为指示标志。Dummy状态的元素可以在插入元素的时候将它变成Active状态,但它不可能再变成Unused状态。

为什么entry有Dummy状态呢?

  • 这是因为采用开放寻址法中,遇到哈希冲突时会找到下一个合适的位置,例如某元素经过哈希计算应该插入到A处,但是此时A处有元素的,通过探测函数计算得到下一个位置B,仍然有元素,直到找到位置C为止,此时ABC构成了探测链,查找元素时如果hash值相同,那么也是顺着这条探测链不断往后找,当删除探测链中的某个元素时,比如B,如果直接把B从哈希表中移除,即变成Unused状态,那么C就不可能再找到了,因为AC之间出现了断裂的现象,正是如此才出现了第三种状态—Dummy,Dummy是一种类似的伪删除方式,保证探测链的连续性。

提示

  • 一般情况下普通的顺序表数组存储结构也可以认为是简单的哈希表,虽然没有采用哈希函数(取余),但同样可以在O(1)时间内进行查找和修改。但是这种方法存在两个问题:扩展性不强;浪费空间。

  • set集合和dict一样也是基于散列表的,只是他的表元只包含键的引用,而没有对值的引用,其他的和dict基本上是一致的,所以在此就不再多说了。并且dict要求键必须是能被哈希的不可变对象,因此普通的set无法作为dict的键,必须选择被“冻结”的不可变集合类:frozenset。顾名思义,一旦初始化,集合内数据不可修改。
    ————————————————

原文链接:https://blog.csdn.net/answer3lin/article/details/84523332

10、深拷贝浅拷贝

如何区分深拷贝与浅拷贝,简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短,如果B没变,那就是深拷贝,自食其力。
在时间上来看浅拷贝花费时间更少;空间角度:浅拷贝花费内存更少;
效率角度:浅拷贝只拷贝顶层数据,一般情况下比深拷贝效率高。
浅拷贝:只是去拷贝第一层
请添加图片描述
L改变a和b都会改变。

深拷贝:拷贝所有层(完全拷贝)
请添加图片描述

a=9999
b=a
print(a)
print(b)
输出:9999 9999      这里的赋值知识复制了引用,a、b都指向了同一个数据对象
print(id(a))
print(id(b))
打印的也相同,表明a和b指向同一个数据对象

请添加图片描述

a=9999
b=9999
print(id(a))
print(id(b))
打印的不相同,表明a和b指向不同一个数据对象
11、Python中的GIL(全局解释器锁)以及python的多线程性能。
  • 多线程

对于单核CPU,同一时刻只能有一个线程来执行,但是现在我们的电脑设备都是多核CPU,所以为了充分利用CPU的性能优势,我们会选择多线程或者多进程来开发。但是前面已经说了,多进程之间切换对于CPU的开销是巨大的,所以一般的应用程序开发会选择使用多线程来实现。

  • GIL

上面的一切看似很完美,但是很遗憾的是,python为了解决多线程之间数据的完整性和状态同步问题,python的解释器CPython中加了一把大锁GIL即全局解释器锁,它的作用在于,同一时刻只允许一个线程执行,不论计算机是几核。

所以对于CPU密集型场景,python的多线程是没有意义的。

但是对于IO密集型场景,python的多线程确实有意义的。

原因在于,IO密集型大部分情况下是处于等待状态,比如一个请求发出去了,线程就空闲了,要等待返回结果之后,才会继续执行,所以这一段时间,CPU是空闲的,可以分配给别的线程来执行任务。

12、解释装饰器概念
12.1 装饰器

简言之,python装饰器就是用于拓展原来函数功能的一种函数,这个函数的特殊之处在于它的返回值也是一个函数,使用python装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。
一般而言,我们要想拓展原来函数代码,最直接的办法就是侵入代码里面修改,例如:

import time
def func():
    print("hello")
    time.sleep(1)
    print("world")

这是我们最原始的的一个函数,然后我们试图记录下这个函数执行的总时间,那最简单的做法就是:

#原始侵入,篡改原函数
import time
def func():
    startTime = time.time()
    
    print("hello")
    time.sleep(1)
    print("world")
    endTime = time.time()
    
    msecs = (endTime - startTime)*1000
    print("time is %d ms" %msecs)

但是如果你的Boss在公司里面和你说:“小祁,这段代码是我们公司的核心代码,你不能直接去改我们的核心代码。”那该怎么办呢,我们仿照装饰器先自己试着写一下:

#避免直接侵入原函数修改,但是生效需要再次执行函数

import time

def deco(func):
    startTime = time.time()
    func()
    endTime = time.time()
    msecs = (endTime - startTime)*1000
    print("time is %d ms" %msecs)


def func():
    print("hello")
    time.sleep(1)
    print("world")

if __name__ == '__main__':
    f = func
    deco(f)#只有把func()或者f()作为参数执行,新加入功能才会生效
    print("f.__name__ is",f.__name__)#f的name就是func

这里我们定义了一个函数deco,它的参数是一个函数,然后给这个函数嵌入了计时功能。然后你可以拍着胸脯对老板说,看吧,不用动你原来的代码,我照样拓展了它的函数功能。
然后你的老板有对你说:“小祁,我们公司核心代码区域有一千万个func()函数,从func01()到func1kw(),按你的方案,想要拓展这一千万个函数功能,就是要执行一千万次deco()函数,这可不行呀,我心疼我的机器。”
好了,你终于受够你老板了,准备辞职了,然后你无意间听到了装饰器这个神器,突然发现能满足你闫博士的要求了。
我们先实现一个最简陋的装饰器,不使用任何语法糖和高级语法,看看装饰器最原始的面貌:

#既不需要侵入,也不需要函数重复执行
import time

def deco(func):
    def wrapper():
        startTime = time.time()
        func()
        endTime = time.time()
        msecs = (endTime - startTime)*1000
        print("time is %d ms" %msecs)
    return wrapper


@deco
def func():
    print("hello")
    time.sleep(1)
    print("world")

if __name__ == '__main__':
    f = func #这里f被赋值为func,执行f()就是执行func()
    f()

这里的deco函数就是最原始的装饰器,它的参数是一个函数,然后返回值也是一个函数。其中作为参数的这个函数func()就在返回函数wrapper()的内部执行。然后在函数func()前面加上@deco,func()函数就相当于被注入了计时功能,现在只要调用func(),它就已经变身为“新的功能更多”的函数了。
所以这里装饰器就像一个注入符号:有了它,拓展了原来函数的功能既不需要侵入函数内更改代码,也不需要重复执行原函数。

#带有参数的装饰器
import time

def deco(func):
    def wrapper(a,b):
        startTime = time.time()
        func(a,b)
        endTime = time.time()
        msecs = (endTime - startTime)*1000
        print("time is %d ms" %msecs)
    return wrapper


@deco
def func(a,b):
    print("hello,here is a func for add :")
    time.sleep(1)
    print("result is %d" %(a+b))

if __name__ == '__main__':
    f = func
    f(3,4)
    #func()

然后你满足了Boss的要求后,Boss又说:“小祁,我让你拓展的函数好多可是有参数的呀,有的参数还是个数不定的那种,你的装饰器搞的定不?”然后你嘿嘿一笑,深藏功与名!

然后你满足了Boss的要求后,Boss又说:“小祁,我让你拓展的函数好多可是有参数的呀,有的参数还是个数不定的那种,你的装饰器搞的定不?”然后你嘿嘿一笑,深藏功与名!

#带有不定参数的装饰器

import time

def deco(func):
    def wrapper(*args, **kwargs):
        startTime = time.time()
        func(*args, **kwargs)
        endTime = time.time()
        msecs = (endTime - startTime)*1000
        print("time is %d ms" %msecs)
    return wrapper


@deco
def func(a,b):
    print("hello,here is a func for add :")
    time.sleep(1)
    print("result is %d" %(a+b))

@deco
def func2(a,b,c):
    print("hello,here is a func for add :")
    time.sleep(1)
    print("result is %d" %(a+b+c))


if __name__ == '__main__':
    f = func
    func2(3,4,5)
    f(3,4)
    #func()



最后,你的老板说:“可以的,小祁,我这里一个函数需要加入很多功能,一个装饰器怕是搞不定,装饰器能支持多个嘛"
最后你就把这段代码丢给了他:

#多个装饰器

import time

def deco01(func):
    def wrapper(*args, **kwargs):
        print("this is deco01")
        startTime = time.time()
        func(*args, **kwargs)
        endTime = time.time()
        msecs = (endTime - startTime)*1000
        print("time is %d ms" %msecs)
        print("deco01 end here")
    return wrapper

def deco02(func):
    def wrapper(*args, **kwargs):
        print("this is deco02")
        func(*args, **kwargs)

        print("deco02 end here")
    return wrapper

@deco01
@deco02
def func(a,b):
    print("hello,here is a func for add :")
    time.sleep(1)
    print("result is %d" %(a+b))



if __name__ == '__main__':
    f = func
    f(3,4)
    #func()


'''
this is deco01
this is deco02
hello,here is a func for add :
result is 7
deco02 end here
time is 1003 ms
deco01 end here
'''

多个装饰器执行的顺序就是从最后一个装饰器开始,执行到第一个装饰器,再执行函数本身。

多个装饰器执行的顺序就是从最后一个装饰器开始,执行到第一个装饰器,再执行函数本身。

多个装饰器执行的顺序就是从最后一个装饰器开始,执行到第一个装饰器,再执行函数本身。

盗用评论里面一位童鞋的例子:

def dec1(func):  
    print("1111")  
    def one():  
        print("2222")  
        func()  
        print("3333")  
    return one  

def dec2(func):  
    print("aaaa")  
    def two():  
        print("bbbb")  
        func()  
        print("cccc")  
    return two  

@dec1  
@dec2  
def test():  
    print("test test")  

test()  

输出:

aaaa  
1111  
2222  
bbbb  
test test  
cccc  
3333

================================================

数据库

一、命令相关

1、常见命令
mysql -uroot -p #如果刚安装好MySQL,root是没有密码的*
mysql> mysql -h192.168.206.100 -uroot -p12345678; #u与root可以不加空格

mysql> drop database db_name;    # -- 删除数据库
mysql> use db_name;              #-- 选择数据库
mysql> create table tb_name (字段名 varchar(20), 字段名 char(1));   #-- 创建数据表模板
mysql> show tables;              #-- 显示数据表
mysql> desc tb_name;            # -- 显示表结构
mysql> drop table tb_name;      #-- 删除表



#创建学生表
create table Student(
     Sno char(10) primary key,
     Sname char(20) unique,
     Ssex char(2),
     Sage smallint,
     Sdept char(20)
)#第一种形式无需指定要插入数据的列名,只需提供被插入的值即可:
mysql> insert into tb_name values (value1,value2,value3,...);
#第二种形式需要指定列名及被插入的值:
mysql> insert into tb_name (column1,column2,column3,...) values (value1,value2,value3,...);


#插入数据
mysql> insert into Student values ( 20180001,张三,男,20,CS);
mysql> insert into Student (Sno,Sname,Ssex,Sage,Sdept) values ( 20180003,王五,男,18,MA);
mysql> insert into Student (Sno,Sname,Ssex,Sage,Sdept) values ( 20180004,赵六,男,20,IS);
3.指定查询结果中的列标题

通过指定列标题(也叫列别名)可使输出结果更容易被人理解。 指定列标题时,可在列名之后使用AS子句;也可使用:列别名=<表达式>的形式指定列标题。AS子句的格式为:列名或计算表达式 [AS] 列标题
模板:

select <字符型字段> as 列标题1,<字符型字段> as 列标题2, <字符型字段> as 列标题3 from bt_name;
4.查询经过计算的列(即表达式的值)

选择行:选择表中的部分行或全部行作为查询的结果。格式: select [all|distinct] [top n[percent]]<目标列表达式列表> from 表名

  • 消除查询结果中的重复行

在select语句中使用distinct关键字可以消除结果集中的重复行,
模板:select distinct <字符型字段>[,<字符型字段>,…] from tb_name;

  • 限制查询结果中的返回行数

使用top选项可限制查询结果的返回行数,即返回指定个数的记录数。其中:n是一个正整数,表示返回查询结果集的前n行;若带 percent关键字,则表示返回结果集的前n%行。

  模板:select  top n from tb_name; /*查询前 n 的数据*/
  模板:select top n percent from tb_name; /*查询前 n% tb_name的数据*/
  • 查询满足条件的行: 用where子句实现条件查询

通过where子句实现,该子句必须紧跟在From子句之后。
格式为:select [all|distinct] [top n[percent]]<目标列表达式列表> from 表名 where <条件>;
说明:在查询条件中可使用以下运算符或表达式:

 运算符                 运算符标识
 比较运算符         <=,<,=,>,>=,!=,<>,!>,!<
 范围运算符         between... and,not between... and
 列举运算符         in,not in
 模糊匹配运算符 like,not like
 空值运算符         is null,is not null
 逻辑运算符         and,or,not
  • 使用比较运算符:
模板:select * from tb_name where <字符型字段> >= n ;
  • 指定范围:
用于指定范围的关键字有两个:between...and和 not between...and。
格式为:
select * from tb_name where [not] between <表达式1> and <表达式2>;

其中:between关键字之后的是范围的下限(即低值),and关键字之后的是范围的上限(即高值)。用于查找字段值在(或不在)指定范围的行。

  • 使用列举:

使用in关键字可以指定一个值的集合,集合中列出所有可能的值,当表达式的值与集合中的任一元素个匹配时,即返回true,否则返回false。

模板:select * from tb_name where <字符型字段> [not] in(值1,值2,...,值n);
  • 使用通配符进行模糊查询:

可用like 子句进行字符串的模糊匹配查询,like子句将返回逻辑值(true或False)。
like子句的格式: select * from tb_name where <字符型字段> [not] like <匹配串>;
其含义是:查找指定字段值与匹配串相匹配的记录。匹配串中通常含有通配符%和_(下划线)。
其中: %:代表任意长度(包括0)的字符串

  • 多重条件查询:使用逻辑运算符

逻辑运算符and(与:两个条件都要满足)和or(或:满足其中一个条件即可)可用来联接多个查询条件。and的优先级高于or,但若使用括号可以改变优先级。
模板:select * from tb_name where <字符型字段> = ‘volues’ and <字符型字段> > n;

5.对查询结果排序

order by子句可用于对查询结果按照一个或多个字段的值(或表达式的值)进行升序(ASC)或降序(DESC)排列,默认为升序。
格式:order by {排序表达式[ASC|DESC]}[,…n];
其中:排序表达式既可以是单个的一个字段,也可以是由字段、函数、常量等组成的表达式,或一个正整数。
模板:

select * from tb_name order by <排序表达式> <排序方法>

使用统计函数:又称集函数,聚合函数
在对表进行检索时,经常需要对结果进行计算或统计,T-SQL提供了一些统计函数(也称集函数或聚合函数),用来增强检索功能。统计函数用于计算表中的数据,即利用这些函数对一组数据进行计算,并返回单一的值。
常用统计函数表

        函数名      功能
        AVG         求平均值
        count        求记录个数,返回int类型整数
        max          求最大值
        min           求最小值
        sum          求和
  1. SUM和AVG
    功能:求指定的数值型表达式的和或平均值。
    模板:

    select avg(<字符型字段>) as 平均数,sum(<字符型字段>) as 总数 from tb_name where <字符型字段> ='字符串';
    
  2. Max和Min
    功能:求指定表达式的最大值或最小值。
    模板:select max(<字符型字段>) as 最大值,min(<字符型字段>) as 最小值 from tb_name;

  3. count
    该函数有两种格式:count()和count([all]|[distinct] 字段名),为避免出错,查询记录个数一般使用count(),而查询某字段有几种取值用count(distinct 字段名)。
    (1).count():
    功能:统计记录总数。
    模板:select count(
    ) as 总数 from tb_name;
    (2).count([all]|[distinct] 字段名)
    功能:统计指定字段值不为空的记录个数,字段的数据类型可以是text、image、ntext、uniqueidentifier之外的任何类型。

 模板:select count(<字符型字段>) as 总数 from tb_name;
6.对查询结果分组

​ group by子句用于将查询结果表按某一列或多列值进行分组,列值相等的为一组,每组统计出一个结果。该子句常与统计函数一起使用进行分组统计。
​ 格式为:group by 分组字段[,…n][having <条件表达式>];

  1. 在使用group by子句后
    select列表中只能包含:group by子句中所指定的分组字段及统计函数。

  2. having子句的用法
    having子句必须与group by 子句配合使用,用于对分组后的结果进行筛选(筛选条件中常含有统计函数)。

  3. 分组查询时不含统计函数的条件
    通常使用where子句;含有统计函数的条件,则只能用having子句。
    模板:

    select <字符型字段>,count(*) as 列标题 from tb_name where <字符型字段>='字符串' group by <字符型字段>
    1. 修改数据(Update)

    Update 语句用于修改表中的数据。

      格式:update tb_name set 列名称 = 新值 where 列名称 = 某值;
    

    5、删除数据(Delete)

    删除单行
    格式:delete from tb_name where 列名称 = 某值;
    删除所有行
    可以在不删除表的情况下删除所有的行。这意味着表的结构、属性和索引都是完整的:
    格式:delete * from tb_name 或 delete from tb_name;

7、MySQL – 应用

学生-课程数据库
学生表:Student(Sno,Sname,Ssex,Sage,Sdept)
课程表:Course(Cno,Cname,Cpno,Ccredit)
学生选课表:SC(Sno,Cno,Grade)
关系的主码加下划线表示。各个表中的数据示例如图所示:

 建立一个“学生”表Student:
create table Student(
  Sno char(9) peimary key, /*列级完整性约束条件,Sno是主码*/
  Sname char(20) unique, /* Sname取唯一值*/
  Ssex char(2),
  Sage smallint,
  Sdept char(20)
);
 建立一个“课程”表Course:
create table Course(
  Sno char(4) primary key, /*列级完整性约束条件,Cname不能取空值*/
  Sname char(40) not null, /*Cpno的含义是先修课*/
  Cpno char(4)
  Ccredit smallint,
  foreign key (Cpnoo) references Course(Cno) /*表级完整性约束条件,Cpno是外码,被参照表是Course,被参照列是Cno*/
);
 建立学生选课表SC:
create table SC(
  Sno char(9),
  Cno char(4),
  Grade smallint,
  frimary key (Sno,Cno), /*主码由两个属性构成,必须作为表级完整性进行定义*/
  foreign key (Sno) references Student(Sno), /*表级完整性约束条件,Sno是外码,被参照表是Student*/
  foreign key (Cno) references Course(Cno)   /*表级完整性约束条件,Cno是外码,被参照表是Course */
);
8、SQL,考察联合语句,如何分页以及复杂语句的优化

二、MySQL索引

关于MySQL索引的好处,如果正确合理设计并且使用索引的MySQL是一辆兰博基尼的话,那么没有设计和使用索引的MySQL就是一个人力三轮车。对于没有索引的表,单表查询可能几十万数据就是瓶颈,而通常大型网站单日就可能会产生几十万甚至几百万的数据,没有索引查询会变的非常缓慢。还是以WordPress来说,其多个数据表都会对经常被查询的字段添加索引,比如wp_comments表中针对5个字段设计了BTREE(二叉树)索引。

合理的设计自己的数据库索引可以大大提高数据的检索速度,如果在大表中滥用索引反而会影响你的数据库性能,下边数据库优化有详细提到。

Alter table employees add index first_name (first_name);
2.1 索引类型
2.1.1 B树

大多数存储引擎都支持B树索引。B树通常意味着所有的值都是按顺序存储的,并且每一个叶子也到根的距离相同。B树索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取数据。下图就是一颗简单的B🌲。
请添加图片描述

2.1.2 B+树

下图为B+树的结构,B+树是B树的升级版,我们可以观察一下,B树和B+树的区别是什么?[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-trpi8UYo-1647795627090)(/Users/hello/Library/Application Support/typora-user-images/截屏2022-03-12 20.51.07.png)]

2.1.3 B+树和B树的区别是:
  • B树的节点中没有重复元素,B+树有(因为B树的节点是储存信息的)。

  • B树的中间节点会存储数据指针信息,而B+树只有叶子节点才存储

  • B+树的每个叶子节点有一个指针指向下一个节点,把所有的叶子节点串在了一起。

  • B+树最大的区别就是所有的数据都是存储在叶子节点上的,而非叶子节点中存储的都是数据索引。并且所有的叶子结点再连接成一个链表!

从下图我们可以直观的看到B树和B+树的区别:紫红色的箭头是指向被索引的数据的指针,大红色的箭头即指向下一个叶子节点的指针。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SX3Go4U8-1647795627091)(/Users/hello/Library/Application Support/typora-user-images/截屏2022-03-12 20.53.12.png)]

我们假设被索引的列是主键,现在查找主键为5的记录,模拟一下查找的过程:

B树,在倒数第二层的节点中找到5后,可以立刻拿到指针获取行数据,查找停止。

B+树,在倒数第二层的节点中找到5后,由于中间节点不存有指针信息,则继续往下查找,在叶子节点中找到5,拿到指针获取行数据,查找停止。

B+树每个父节点的元素都会出现在子节点中,是子节点的最大(或最小)元素。叶子节点存储了被索引列的所有的数据。

2.1.4 B+树比起B树有什么优点呢?
  • 由于中间节点不存指针,同样大小的磁盘页可以容纳更多的节点元素,树的高度就小。(数据量相同的情况下,B+树比B树更加“矮胖”),查找起来就更快。
  • B+树每次查找都必须到叶子节点才能获取数据,而B树不一定,B树可以在非叶子节点上获取数据。因此B+树查找的时间更稳定
  • B+树的每一个叶子节点都有指向下一个叶子节点的指针,方便范围查询和全表查询:只需要从第一个叶子节点开始顺着指针一直扫描下去即可,而B树则要对树做中序遍历

了解了B+树的结构之后,我们对一张具体的表做分析:

create table Student(
 last_name varchar(50) not null, 
 first_name varchar(50) not null, 
 birthday date not null, 
 gender int(2) not null, 
 key(last_name, first_name, birthday)
);

对于表中的每一行数据,索引中包含了name,birthday列的值。下图显示了该索引的结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4K06XdN-1647795627091)(/Users/hello/Library/Application Support/typora-user-images/截屏2022-03-12 20.58.31.png)]

索引对多个值进行排序的依据是create table语句中定义索引时列的顺序,即如果名字相同,则根据生日来排序。

B+树的结构决定了这种索引对以下类型的查询有效:

  • 全值匹配 和索引中所有的列进行匹配,例如查找姓名为Cuba Allen,生日为1960-01-01的人。
  • 匹配最左前缀 查找姓为Allen的人,即只用索引的第一列。
  • 匹配列前缀 匹配某一列的值的开头部分,例如查找所有以J开头的姓的人。
  • 匹配范围值 查找姓在Allen和Barrymore之间的人。
  • 精确匹配某一列并范围匹配另外一列 查找姓为Allen,名字是字母K开头的人。即第一列last_name全匹配,第二列first_name范围匹配。
  • 只访问索引的查询 查询只需要访问索引,无需访问数据行。这种索引叫做覆盖索引。

一些限制:

  • 如果不是按照索引的最左列开始查找,无法使用索引。例如上面例子中的索引无法用于查找某个特定生日的人,因为生日不是最左数据列。也不能查找last_name以某个字母结尾的人。

  • 不能跳过索引的列。上述索引无法用于查找last_name为Smith并且某个特定生日的人。如果不指定first_name,则mysql只能使用索引的第一列。

  • 如果查询中有某个列的范围查询,则右边所有的列都无法使用索引优化查找。例如查询WHERE last_name=’Smith’ AND first_name LIKE ‘J%’ AND birthday=‘1996-05-19’,这个查询只能使用索引的前两列。

2.2 MySQL中InnoDB的一级索引、二级索引

每个InnoDB表具有一个特殊的索引称为聚簇索引(也叫聚集索引,聚类索引,簇集索引)。如果表上定义有主键,该主键索引就是聚簇索引。如果未定义主键,MySQL取第一个唯一索引(unique)而且只含非空列(NOT NULL)作为主键,InnoDB使用它作为聚簇索引。如果没有这样的列,InnoDB就自己产生一个这样的ID值,它有六个字节,而且是隐藏的,使其作为聚簇索引。

表中的聚簇索引(clustered index )就是一级索引,除此之外,表上的其他非聚簇索引都是二级索引,又叫辅助索引(secondary indexes)。

原文链接:https://blog.csdn.net/RoxLiu/article/details/70160664

2.3 数据库Mysql-索引的最左前缀匹配原则

最左前缀匹配原则

​ 最左优先,以最左边的为起点任何连续的索引都能匹配上。同时如果范围查询(>、<、between、like)就会停止匹配。

2.3.1 例子来理解最左前缀匹配原则

前一篇文中,我们已经了解到Mysql数据库的索引的底层存储是一棵B+树,那么联合索引的底层也还是一棵B+树。只不过联合索引的键值对数量不是一个,而是多个。

假如:构建一个(a,b)的联合索引,那么它在数据库底层的索引树是下列这样的:

可以看到a的值是有顺序的,1,1,2,2,3,3,而b的值是没有顺序的1,2,1,4,1,2。所以b = 2这种查询条件没有办法利用索引,因为联合索引首先是按a排序的,b是无序的。

同时我们还可以发现在a值相等的情况下,b值又是按顺序排列的,但是这种顺序是相对的。所以最左匹配原则遇上范围查询就会停止,剩下的字段都无法使用索引。例如a = 1 and b = 2 a,b字段都可以使用索引,因为在a值确定的情况下b是相对有序的,而a>1and b=2,a字段可以匹配上索引,但b值不可以,因为a的值是一个范围,在这个范围中b是无序的。

2.3.2 最左前缀匹配原则适用场景

假如建立联合索引(a,b,c)

1 全值匹配查询时

select * from table_name where a = '1' and b = '2' and c = '3' 
select * from table_name where b = '2' and a = '1' and c = '3' 
select * from table_name where c = '3' and b = '2' and a = '1' 

用到了索引

where子句几个搜索条件顺序调换不影响查询结果,因为Mysql中有查询优化器,会自动优化查询顺序

2 匹配左边的列时

select * from table_name where a = '1' 
select * from table_name where a = '1' and b = '2'  
select * from table_name where a = '1' and b = '2' and c = '3'

都从最左边开始连续匹配,用到了索引

select * from table_name where  b = '2' 
select * from table_name where  c = '3'
select * from table_name where  b = '1' and c = '3' 

这些没有从最左边开始,最后查询没有用到索引,用的是全表扫描

select * from table_name where a = '1' and c = '3' 

如果不连续时,只用到了a列的索引,b列和c列都没有用到

3 匹配列前缀
如果列是字符型的话它的比较规则是先比较字符串的第一个字符,第一个字符小的哪个字符串就比较小,如果两个字符串第一个字符相通,那就再比较第二个字符,第二个字符比较小的那个字符串就比较小,依次类推,比较字符串。

如果a是字符类型,那么前缀匹配用的是索引,后缀和中缀只能全表扫描了

select * from table_name where a like 'As%'; //前缀都是排好序的,走索引查询
select * from table_name where  a like '%As'//全表查询
select * from table_name where  a like '%As%'//全表查询

4 匹配范围值

select * from table_name where  a > 1 and a < 3

可以对最左边的列进行范围查询

select * from table_name where  a > 1 and a < 3 and b > 1;

多个列同时进行范围查找时,只有对索引最左边的那个列进行范围查找才用到B+树索引,也就是只有a用到索引,在1<a<3的范围内b是无序的,不能用索引,找到1<a<3的记录后,只能根据条件 b > 1继续逐条过滤

  1. 精准匹配某一列并范围匹配另外一列
    如果左边的列是精确查找的,右边的列可以进行范围查找
select * from table_name where  a = 1 and b > 3;

a=1的情况下b是有序的,进行范围查找走的是联合索引

6 排序
一般情况下,我们只能把记录加载到内存中,再用一些排序算法,比如快速排序,归并排序等在内存中对这些记录进行排序,有时候查询的结果集太大不能在内存中进行排序的话,还可能暂时借助磁盘空间存放中间结果,排序操作完成后再把排好序的结果返回客户端。Mysql中把这种再内存中或磁盘上进行排序的方式统称为文件排序。文件排序非常慢,但如果order子句用到了索引列,就有可能省去文件排序的步骤

select * from table_name order by a,b,c limit 10;

因为b+树索引本身就是按照上述规则排序的,所以可以直接从索引中提取数据,然后进行回表操作取出该索引中不包含的列就好了

order by的子句后面的顺序也必须按照索引列的顺序给出,比如

select * from table_name order by b,c,a limit 10;

这种颠倒顺序的没有用到索引

select * from table_name order by a limit 10;
select * from table_name order by a,b limit 10;

这种用到部分索引

select * from table_name where a =1 order by b,c limit 10;

联合索引左边列为常量,后边的列排序可以用到索引
————————————————
原文链接:https://blog.csdn.net/Mind_programmonkey/article/details/114693532

2.4 哈希索引

哈希索引,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。如果多个列的哈希值相同,索引会以链表的方式存放多个指针记录到同一个哈希条目中。

因为索引自身只存储对应的哈希值,所以索引的结构十分紧凑,哈希索引查找的速度非常快。但是哈希索引也有它的限制:

  • 哈希索引不是按照索引顺序存储的,无法用于排序。
  • 不支持部分索引列匹配查找。
  • 不支持范围查找。

三、MySQL锁

  1. 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  2. 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
  3. 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

粒度越小并发度越高,锁冲突的机率越低,加锁速度越慢,开销越大。

3.1 Mysql中有哪些不同的表?

共有5种类型的表:
MyISAM
Heap
Merge
INNODB
ISAM
简述在MySQL数据库中MyISAMInnoDB的区别

MyISAM:
  • 不支持事务,但是每次查询都是原子的;
  • 支持表级锁,即每次操作是对整个表加锁;
  • 存储表的总行数;
  • 一个MYISAM表有三个文件:索引文件、表结构文件、数据文件

采用非聚集索引索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性

InnoDb:

支持ACID的事务,支持事务的四种隔离级别;支持行级锁及外键约束:因此可以支持写并发;不存储总行数;

事务特性ACID**:**
(1)**原子性:**即不可分割性,事务要么全部被执行,要么就全部不被执行。
(2)一致性:事务的执行使得数据库从一种正确状态转换成另一种正确状态
(3)**隔离性:**在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务。
(4) 持久性: 事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。

一个InnoDb引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;

主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。

3.2 乐观锁和悲观锁
(1)乐观锁

简单地说,就是从应用系统层面上做并发控制,去加锁。

实现乐观锁常见的方式:

版本号version。实现方式,在数据表中增加版本号字段,每次对一条数据做更新之前,先查出该条数据的版本号,每次更新数据都会对版本号进行更新。在更新时,把之前查出的版本号跟库中数据的版本号进行比对,如果相同,则说明该条数据没有被修改过,执行更新。如果比对的结果是不一致的,则说明该条数据已经被其他人修改过了,则不更新,客户端进行相应的操作提醒。

(2)悲观锁

简单地说,就是从数据库层面上做并发控制,去加锁。

悲观锁的实现方式有两种:共享锁(读锁)和排它锁(写锁)

详细说明
  • 相信很多朋友在面试的时候,都会被问到乐观锁和悲观锁的问题,如果不清楚其概念和用法的情况下,相信很多朋友都会感觉很懵逼,那么面试的结果也就不言而喻了。

那么乐观锁和悲观锁到底是个什么东西,用它能来做什么呢?

相信大家都遇到这种场景,当很多人(一两个人估计不行)同时对同一条数据做修改的时候,那么数据的最终结果是怎样的呢?
这也就是我们说的并发情况,这样会导致以下两种结果:

  • 更新错误,你修改之后的数据可能被别人覆盖了,导致你很懵逼,甚至怀疑自己开发的功能是否有问题;

  • 脏读,数据更新错误,导致读数据也是错的,查询出一些默认奇妙的数据,看到的不是你自己修改的结果。

这样的问题怎么解决呢?于是乎,锁就这样产生了,锁分为乐观锁和悲观锁,它的目的是用来解决并发控制的问题。
MyISAM引擎不支持事务,所以不考虑它有乐观锁和悲观锁概念。**MyISAM只有表锁,**锁又分为读锁和写锁。在这里我们只讨论InnoDB引擎。
需要注意的是,乐观锁和悲观锁并不是解决并发控制的唯一手段(也可以使用消息中间件kafka,MQ之类的作为缓冲等等),而且乐观锁和悲观锁并不仅限制在mysql中使用,它是一种概念,很多其他的应用,如redis,memcached等,只要存在并发情况的,都可以应用这种概念,只是方式上有些差别而已。

一、乐观锁

乐观锁,简单地说,就是从应用系统层面上做并发控制,去加锁。
实现乐观锁常见的方式:版本号version

实现方式:

​ 在数据表中增加版本号字段,每次对一条数据做更新之前,先查出该条数据的版本号,每次更新数据都会对版本号进行更新。在更新时,把之前查出的版本号跟库中数据的版本号进行比对,如果相同,则说明该条数据没有被修改过,执行更新。如果比对的结果是不一致的,则说明该条数据已经被其他人修改过了,则不更新,客户端进行相应的操作提醒。
使用版本号实现乐观锁
使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号。

//1.查询出商品信息 

select status,version from t_goods where id=#{id} 

//2.根据商品信息生成订单 

//3.修改商品status为2 

update t_goods 

set status=2,version=version+1 

where id=#{id} and version=#{version};

注意第二个事务执行update时,第一个事务已经提交了,所以第二个事务能够读取到第一个事务修改的version。

下面这种极端的情况:

我们知道MySQL数据库引擎InnoDB,事务的隔离级别是Repeatable Read,因此是不会出现脏读、不可重复读。
在这种极端情况下,第二个事务的update由于不能读取第一个事务未提交的数据(第一个事务已经对这一条数据加了排他锁,第二个事务需要等待获取锁),第二个事务获取了排他锁后,会发现version已经发生了改变从而提交失败。

二、悲观锁

悲观锁,简单地说,就是从数据库层面上做并发控制,去加锁。
悲观锁的实现方式有两种:共享锁(读锁)和排它锁(写锁)

  1. 共享锁(IS锁),实现方式是在sql后加LOCK IN SHARE MODE,比如SELECT … LOCK IN SHARE MODE,即在符合条件的rows上都加了共享锁,这样的话,其他session可以读取这些记录,也可以继续添加IS锁,但是无法修改这些记录直到你这个加锁的session执行完成(否则直接锁等待超时)。
  2. 排它锁(IX锁),实现方式是在sql后加FOR UPDATE,比如SELECT … FOR UPDATE ,即在符合条件的rows上都加了排它锁,其他session也就无法在这些记录上添加任何的S锁或X锁。如果不存在一致性非锁定读的话,那么其他session是无法读取和修改这些记录的,但是innodb有非锁定读(快照读并不需要加锁),for update之后并不会阻塞其他session的快照读取操作,除了select …lock in share mode和select … for update这种显示加锁的查询操作。

通过对比,发现for update的加锁方式无非是比lock in share mode的方式多阻塞了select…lock in share mode的查询方式,并不会阻塞快照读。

mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型。

在Java中,synchronized的思想也是悲观锁。

以排它锁为例

要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用auto commit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。set autocommit=0;

//0.开始事务 

begin;/begin work;/start transaction; (三者选一就可以) 

//1.查询出商品信息 

select status from t_goods where id=1 for update; 

//2.根据商品信息生成订单 

insert into t_orders (id,goods_id) values (null,1); 

//3.修改商品status为2 

update t_goods set status=2; 

//4.提交事务 

commit;/commit work;

上面的查询语句中,我们使用了select…for update的方式, 这样就通过开启排他锁的方式实现了悲观锁。此时在t_goods表中,id为1的 那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

三、补充:
  • 1.MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作 (UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁。
  • 2.MySQL InnoDB默认行级锁。 行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住。
  • 3.从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

————————————————

原文链接:https://blog.csdn.net/u013521882/article/details/85275987

四、 MySQL的四种事务隔离级别

Read Uncommitted(读取未提交内容)

​ 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)

Read Committed(读取提交内容)

​ 这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一 个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的 不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。

Repeatable Read(可重读)

​ 这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题**:幻读(Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再**读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB 和 Falcon 存储引擎通过多版本并发控制 (MVCC,Multiversion Concurrency Control 间隙锁)机制解决了该问题。注:其实多版本只是解决不可重复读问题,而加上间隙锁(也 就是它这里所谓的并发控制)才解决了幻读问题。

Serializable(可串行化)

​ 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

请添加图片描述

五、 数据库中的事务

​ **事务(transaction):**是作为一个单元的一组有序的数据库操作。如果组中的所有操作都成功,则认为事务成功,即使只有一个操作失败,事务也不成功。如果所有操作完成,事务则提交,其修改将作用于所有其他数据库进程。如果一个操作失败,则事务将回滚,该事务所有操作的影响都将取消。

5.1 MySQL事务提交和回滚

为了保证数据的持久性,数据库在执行SQL操作数据之前会先记录redo log和undo log
redo log是重做日志,通常是物理日志,记录的是物理数据页的修改,它用来恢复提交后的物理数据页
undo log是回滚日志,用来回滚行记录到某个版本,undo log一般是逻辑日志,根据行的数据变化进行记录
redo/undo log都是写先写到日志缓冲区,再通过缓冲区写到磁盘日志文件中进行持久化保存
undo日志还有一个用途就是用来控制数据的多版本(MVCC)
简单理解就是:

redo log是用来恢复数据的,用于保障已提交事务的持久性
undo log是用来回滚事务的,用于保障未提交事务的原子性

六、MYSQL之模糊查询

Mysql模糊查询正常情况下在数据量小的时候,速度还是可以的,但是不容易看出查询的效率,在数据量达到百万级,千万级的甚至亿级时 mysql查询的效率是很关键的,也是很重要的。

一、一般情况下 like 模糊查询的写法:前后模糊匹配

Select ‘column’ from ‘table’ where ‘field’ like ‘%keyword%’;

这个SQL语句,如果用explain解释的话,我们很容易就能发觉它是没有走索引搜索,而是对全表进行了扫描,这显然是很慢的,还有卡库的可能。

如果将上面的SQL语句改成下面的写法:

Select ‘column’ from ‘table’ where ‘field’ like ‘keyword%’;

就是把‘keyword’前面的%去掉了,这样的写法用explain解释看到,SQL语句使用了索引,这样就可以大大的提高查询的效率。

有时候,我们在做模糊查询的时候,并非要想查询的关键词都在开头,所以如果不是特别的要求,"keywork%"并不合适所有的模糊查询。

二、模糊查询高效的方法:

1、LOCATE(‘substr’,str,pos)方法

解释:返回 substr 在 str 中第一次出现的位置,如果 substr 在 str 中不存在,返回值为 0 。如果pos存在,返回 substr 在 str 第pos个位置后第一次出现的位置,如果 substr 在 str 中不存在,返回值为0。

实例:

Select companyName from company WHERE LOCATE(“百”,companyName);

备注:keyword是要搜索的内容,business为被匹配的字段,查询出所有存在keyword的数据

2、POSITION(‘substr’ IN field)方法

其实我们就可以把这个方法当做是locate()方法的别名,因为它和locate()方法的作用是一样的。

实例:

Select distinct id from member_business Where POSITION(‘keyword’ IN bussiness);

3、FIND_IN_SET(str1,str2):

返回str2中str1所在的位置索引,其中str2必须以","分割开。

Select * from interface where FIND_IN_SET(‘aaa’,category);

七、三个范式

第一范式:1NF是对属性的原子性约束,要求属性具有原子性,不可再分解;
第二范式:2NF是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;
第三范式:3NF是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。
范式化设计优缺点:
优点: 可以尽量得减少数据冗余,使得更新快,体积小
缺点: 对于查询需要多个表进行关联,减少写得效率增加读得效率,更难进行索引优化
反范式化:
优点:可以减少表得关联,可以更好得进行索引优化
缺点:数据冗余以及数据异常,数据得修改需要更多的成本

八、MySQL优化、多表查询

对查询进行优化,要尽量避免全表扫描,首先应考虑在 whereorder by 涉及的列上建立索引。应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null。

最好不要给数据库留NULL,尽可能的使用 NOT NULL填充数据库.备注、描述、评论之类的可以设置为 NULL,其他的,最好不要使用NULL。不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL也包含在内),都是占用 100个字符的空间的,如果是varchar这样的变长字段, null 不占用空间。可以在num上设置默认值0,确保表中num列没有null值,然后这样查询 select id from t where num=0。应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。

MYSQL的索引
索引是一种用于快速查询行的数据结构,就像一本书的目录就是一个索引,如果想在一本书中找到某个主题,一般会先找到对应页码。在MYSQL中,存储引擎用类似的方法使用索引,先在索引中找到对应值,然后根据匹配的索引记录找到对应的行。我们首先了解一下索引的几种类型和索引的结构。

  1. 数据库索引

(1)B+树是数据库实现索引的首选数据结构呢?——评价一个数据结构作为索引的指标就是在查找时IO操作的次数

(2)B+树非叶子节点只存储key,只有叶子结点存储value,叶子结点包含了这棵树的所有键值,每个叶子结点有一个指向相邻叶子结点的指针,这样可以降低B树的高度。

(3)B+树相比于B树的优势:(1)B+树单一节点存储更多的元素,使得查询的IO次数更少(2)B+树所有查询都要查找到叶子节点,查询效率更加稳定(3)B+树所有叶子节点形成有序链表,便于范围查询,更有利于对数据库的扫描

(4)索引字段要尽量的小:IO次数取决于b+数的高度h,h=log(m+1)N,数据量N一定的情况下,每个磁盘块数据项的数量m越大h越小,m=磁盘块的大小/数据项的大小,前者固定,后者越小越好

(5)索引的最左匹配特性(即从左往右匹配)

\2. mysql索引:

(1)普通索引index,加速查找

(2)唯一索引:主键索引primary key和唯一索引unique

(3)联合索引:前三者对(id,name)复合数据项产生索引时

(4)其他:全文索引fulltext和空间索引spatial

八、聚集索引和非聚簇索引

索引是存储了表数据的物理地址

聚集索引既存储了表数据key又存储了行值,物理地址的逻辑顺序和表存储的顺序一致!是唯一的

对于Innodb,主键毫无疑问是一个聚集索引。但是当一个表没有主键,或者没有一个索引,Innodb会如何处理呢。请看如下规则 如果一个主键被定义了,那么这个主键就是作为聚集索引 如果没有主键被定义,那么该表的第一个唯一非空索引被作为聚集索引 如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,改列的值会随着数据的插入自增。

非聚簇索引:存放了表数据的物理地址和key值,可根据key值对应的物理地址再查询具体的行值,但是物理地址存放的顺序和表存放的逻辑顺序没有强一致性!

  • 如果一个主键被定义了,那么这个主键就是作为聚集索引
  • 如果没有主键被定义,那么该表的第一个唯一非空索引被作为聚集索引
  • 如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键是一个6个字节的列,改列的值会随着数据的插入自增。

InnoDB引擎会为每张表都加一个聚集索引,而聚集索引指向的的数据又是以物理磁盘顺序来存储的,自增的主键会把数据自动向后插入,避免了插入过程中的聚集索引排序问题。如果对聚集索引进行排序,这会带来磁盘IO性能损耗是非常大的。

九、事务的隔离级别

1、为什么要设置隔离级别

在数据库操作中,在并发的情况下可能出现如下问题:

  • 更新丢失(Lost update)
    如果多个线程操作,基于同一个查询结构对表中的记录进行修改,那么后修改的记录将会覆盖前面修改的记录,前面的修改就丢失掉了,这就叫做更新丢失。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
    第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。
    这里写图片描述
    第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。
    这里写图片描述
    解决方法:对行加锁,只允许并发一个更新事务。
  • 脏读(Dirty Reads)
    脏读(Dirty Read):A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。
    这里写图片描述
    解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。
  • 不可重复读(Non-repeatable Reads)
    一个事务对同一行数据重复读取两次,但是却得到了不同的结果。事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读该数据时得到与前一次不同的值。
    这里写图片描述
    解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。
  • 幻象读
    指两次执行同一条 select 语句会出现不同的结果,第二次读会增加一数据行,并没有说这两次执行是在同一个事务中。一般情况下,幻象读应该正是我们所需要的。但有时候却不是,如果打开的游标,在对游标进行操作时,并不希望新增的记录加到游标命中的数据集中来。隔离级别为 游标稳定性 的,可以阻止幻象读。例如:目前工资为1000的员工有10人。那么事务1中读取所有工资为1000的员工,得到了10条记录;这时事务2向员工表插入了一条员工记录,工资也为1000;那么事务1再次读取所有工资为1000的员工共读取到了11条记录。
    这里写图片描述
    解决办法:如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。

正是为了解决以上情况,数据库提供了几种隔离级别。

2、事务的隔离级别

数据库事务的隔离级别有4个,由低到高依次为Read uncommitted(未授权读取、读未提交)、Read committed(授权读取、读提交)、Repeatable read(可重复读取)、Serializable(序列化),这四个级别可以逐个解决脏读、不可重复读、幻象读这几类问题。

  • Read uncommitted(未授权读取、读未提交):
    如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。这样就避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据。
  • Read committed(授权读取、读提交):
    读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
  • Repeatable read(可重复读取):
    可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,即使第二个事务对数据进行修改,第一个事务两次读到的的数据是一样的。这样就发生了在一个事务内两次读到的数据是一样的,因此称为是可重复读。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。这样避免了不可重复读取和脏读,但是有时可能出现幻象读。(读取数据的事务)这可以通过“共享读锁”和“排他写锁”实现。
  • Serializable(序列化):
    提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。
    这里写图片描述
    隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。MySQL的默认隔离级别就是Repeatable read。

十、悲观锁和乐观锁

虽然数据库的隔离级别可以解决大多数问题,但是灵活度较差,为此又提出了悲观锁和乐观锁的概念。

1、悲观锁

悲观锁,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统的数据访问层中实现了加锁机制,也无法保证外部系统不会修改数据。

  • 使用场景举例:以MySQL InnoDB为例

商品t_items表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单(此时该商品无法再次下单),那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。
如果不采用锁,那么操作方法如下:

//1.查询出商品信息
select status from  t_items where id=1;
//2.根据商品信息生成订单,并插入订单表 t_orders 
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_items set status=2;

但是上面这种场景在高并发访问的情况下很可能会出现问题。例如当第一步操作中,查询出来的商品status为1。但是当我们执行第三步Update操作的时候,有可能出现其他人先一步对商品下单把t_items中的status修改为2了,但是我们并不知道数据已经被修改了,这样就可能造成同一个商品被下单2次,使得数据不一致。所以说这种方式是不安全的。

  • 使用悲观锁来解决问题

在上面的场景中,商品信息从查询出来到修改,中间有一个处理订单的过程,使用悲观锁的原理就是,当我们在查询出t_items信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为t_items被锁定了,就不会出现有第三者来对其进行修改了。需要注意的是,要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。我们可以使用命令设置MySQL为非autocommit模式:set autocommit=0;
设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:

//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_items where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_items set status=2;
//4.提交事务
commit;/commit work;

​ 上面的begin/commit为事务的开始和结束,因为在前一步我们关闭了mysql的autocommit,所以需要手动控制事务的提交。
上面的第一步我们执行了一次查询操作:select status from t_items where id=1 for update;与普通查询不一样的是,我们使用了**select…for update的方式,这样就通过数据库实现了悲观锁**。此时在t_items表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。需要注意的是,在事务中,只有SELECT ... FOR UPDATELOCK IN SHARE MODE 操作同一个数据时才会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。拿上面的实例来说,当我执行select status from t_items where id=1 for update;后。我在另外的事务中如果再次执行select status from t_items where id=1 for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但是如果我是在第二个事务中执行select status from t_items where id=1;则能正常查询出数据,不会受第一个事务的影响。

  • Row Lock与Table Lock

使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键或者索引,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。举例如下:
1、select * from t_items where id=1 for update;
这条语句明确指定主键(id=1),并且有此数据(id=1的数据存在),则采用row lock。只锁定当前这条数据。
2、select * from t_items where id=3 for update;
这条语句明确指定主键,但是却查无此数据,此时不会产生lock(没有元数据,又去lock谁呢?)。
3、select * from t_items where name='手机' for update;
这条语句没有指定数据的主键,那么此时产生table lock,即在当前事务提交前整张数据表的所有字段将无法被查询。
4、select * from t_items where id>0 for update; 或者select * from t_items where id<>1 for update;(注:<>在SQL中表示不等于)
上述两条语句的主键都不明确,也会产生table lock。
5、select * from t_items where status=1 for update;(假设为status字段添加了索引)
这条语句明确指定了索引,并且有此数据,则产生row lock。
6、select * from t_items where status=3 for update;(假设为status字段添加了索引)
这条语句明确指定索引,但是根据索引查无此数据,也就不会产生lock。

  • 悲观锁小结
    悲观锁并不是适用于任何场景,它也有它存在的一些不足,因为悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受。所以与悲观锁相对的,我们有了乐观锁。

2、乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以只会在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户决定如何去做。实现乐观锁一般来说有以下2种方式:

    • 使用版本号
      使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
    • 使用时间戳
      乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

十一、MVCC多版本并发控制

什么是 MVCC ?

MVCC
MVCC,全称 Multi-Version Concurrency Control ,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存

MVCCMySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

什么是当前读和快照读?

在学习 MVCC 多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的当前读快照读?

  • 当前读

    像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读
    像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

说白了 MVCC 就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现

https://blog.csdn.net/SnailMann/article/details/94724197?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164734731416781685348277%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=164734731416781685348277&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-94724197.142v2pc_search_result_control_group,143v4register&utm_term=MVCC&spm=1018.2226.3001.4187

十二、意向锁

使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。

在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。

意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:

  • 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
  • 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。

通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。

各种锁的兼容关系如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OS8xEETe-1647795627092)(/Users/hello/Desktop/面试/image/截屏2022-03-20 08.53.49.png)]

意向锁的作用

③这两中类型的锁共存的问题

考虑这个例子:

事务A锁住了表中的一行,让这一行只能读,不能写。

之后,事务B申请整个表的写锁。

如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。

数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。

数据库要怎么判断这个冲突呢?

step1:判断表是否已被其他事务用表锁锁表

step2:判断表中的每一行是否已被行锁锁住。

注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。

于是就有了意向锁。

在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。

在意向锁存在的情况下,上面的判断可以改成

step1:不变

step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。

注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。

===============================================

数据结构

1、如何避免Hash碰撞

(1)开放地址法

开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入

(2)再哈希法

当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不在产生为止

(3)链地址法

将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头节点的链表的尾部

(4)建立公共溢出区

将哈希表分为基本表和溢出表两部分,发生冲突的元素都放在溢出表中

2、二叉查找树、红黑树、AVL树、平衡二叉树、B树、B+树

一、二叉查找树
1、简介

二叉查找树也称为有序二叉查找树,满足二叉查找树的一般性质,是指一棵空树具有如下性质:

  • 任意节点左子树不为空,则左子树的值均小于根节点的值.
  • 任意节点右子树不为空,则右子树的值均大于于根节点的值.
  • 任意节点的左右子树也分别是二叉查找树.
  • 没有键值相等的节点.
2、局限性及应用

一个二叉查找树是由n个节点随机构成,所以,对于某些情况,二叉查找树会退化成一个有n个节点的线性链.如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SmqBqOh8-1647795627092)(/Users/hello/Desktop/面试/image/截屏2022-03-14 21.33.11.png)]
图为一个普通的二叉查找树,大家看a图,如果我们的根节点选择是最小或者最大的数,那么二叉查找树就完全退化成了线性结构;

​ 因此,在二叉查找树的基础上,又出现了AVL树,红黑树,它们两个都是基于二叉查找树,只是在二叉查找树的基础上又对其做了限制.

二、AVL树
1、简介

​ AVL树是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右子树树高不超过1,和红黑树相比,它是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1).不管我们是执行插入还是删除操作

​ 只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,**由此我们可以知道AVL树适合用于插入删除次数比较少,但查找多的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ujt5dYpY-1647795627092)(/Users/hello/Desktop/面试/image/截屏2022-03-14 21.35.41.png)]

从上面这张图我们可以看出,任意节点的左右子树的平衡因子差值都不会大于1.

2、局限性

​ 由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树.当然,如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树.

三、红黑树
1、简介

​ 一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是red或black. 通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍.它是一种弱平衡二叉树(由于是若平衡,可以推出,相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数变少,所以对于搜索,插入,删除操作多的情况下,我们就用红黑树.

2、性质
  • 每个节点非红即黑.
  • 根节点是黑的。(根叶黑)
  • 每个叶节点(叶节点即树尾端NUL指针或NULL节点)都是黑的.
  • 如果一个节点是红的,那么它的两儿子都是黑的.(不红红)
  • 对于任意节点而言,其到叶子点树NIL指针的每条路径都包含相同数目的黑节点.(黑路同)
    这里写图片描述
    每条路径都包含相同的黑节点.
3、应用
  • 广泛用于C++的STL中,map和set都是用红黑树实现的.
  • 著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间.
  • IO多路复用epoll的实现采用红黑树组织管理sockfd,以支持快速的增删改查.
  • ngnix中,用红黑树管理timer,因为红黑树是有序的,可以很快的得到距离当前最小的定时器.
  • java中TreeMap的实现.
四、B/B+树

注意B-树就是B树,-只是一个符号.

1、简介

B/B+树是为了磁盘或其它存储设备而设计的一种平衡多路查找树(相对于二叉,B树每个内节点有多个分支),与红黑树相比,在相同的的节点的情况下,一颗B/B+树的高度远远小于红黑树的高度(在下面B/B+树的性能分析中会提到).B/B+树上操作的时间通常由存取磁盘的时间和CPU计算时间这两部分构成,而CPU的速度非常快,所以B树的操作效率取决于访问磁盘的次数,关键字总数相同的情况下B树的高度越小,磁盘I/O所花的时间越少.

2、B树的性质
  • 定义任意非叶子结点最多只有M个儿子;且M>2;
  • 根结点的儿子数为[2, M];
  • 除根结点以外的非叶子结点的儿子数为[M/2, M];
  • 每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
  • 非叶子结点的关键字个数=指向儿子的指针个数-1;
  • 非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
  • 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
  • 所有叶子结点位于同一层;
    这里写图片描述
    这里只是一个简单的B树,在实际中B树节点中关键字很多的.上面的图中比如35节点,35代表一个key(索引),而小黑块代表的是这个key所指向的内容在内存中实际的存储位置.是一个指针.
3、B+树

B+树是应文件系统所需而产生的一种B树的变形树(文件的目录一级一级索引,只有最底层的叶子节点(文件)保存数据.),非叶子节点只保存索引,不保存实际的数据,数据都保存在叶子节点中.这不就是文件系统文件的查找吗?我们就举个文件查找的例子:有3个文件夹,a,b,c, a包含b,b包含c,一个文件yang.c, a,b,c就是索引(存储在非叶子节点), a,b,c只是要找到的yang.c的key,而实际的数据yang.c存储在叶子节点上.
所有的非叶子节点都可以看成索引部分

B+树的性质(下面提到的都是和B树不相同的性质)
  • 非叶子节点的子树指针与关键字个数相同;
  • 非叶子节点的子树指针p[i],指向关键字值属于[k[i],k[i+1]]的子树.(B树是开区间,也就是说B树不允许关键字重复,B+树允许重复);
  • 为所有叶子节点增加一个链指针.
  • 所有关键字都在叶子节点出现(稠密索引). (且链表中的关键字恰好是有序的);
  • 非叶子节点相当于是叶子节点的索引(稀疏索引),叶子节点相当于是存储(关键字)数据的数据层.
  • 更适合于文件系统;
    看下图:
    这里写图片描述
    非叶子节点(比如5,28,65)只是一个key(索引),实际的数据存在叶子节点上(5,8,9)才是真正的数据或指向真实数据的指针.
4、应用

B和B+树主要用在文件系统以及数据库做索引.比如MYSQL;

5、B/B+树性能分析
  • n个节点的平衡二叉树的高度为H(即logn),而n个节点的B/B+树的高度为logt((n+1)/2)+1;
  • 若要作为内存中的查找表,B树却不一定比平衡二叉树好,尤其当m较大时更是如此.因为查找操作CPU的时间在B-树上是O(mlogtn)=O(lgn(m/lgt)),而m/lgt>1;所以m较大时O(mlogtn)比平衡二叉树的操作时间大得多. 因此在内存中使用B树必须取较小的m.(通常取最小值m=3,此时B-树中每个内部结点可以有2或3个孩子,这种3阶的B-树称为2-3树)。
6、为什么说B+tree比B树更适合实际应用中操作系统的文件索引和数据索引.
  • B±tree的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了.
  • 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
    ps:我在知乎上看到有人是这样说的,我感觉说的也挺有道理的:
    他们认为数据库索引采用B+树的主要原因是:
  • B树在提高了IO性能的同时并没有解决元素遍历时效率低下的问题,正是为了解决这个问题,B+树应用而生.
  • B+树只需要去遍历叶子节点就可以实现整棵树的遍历.而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低).
7、B+树相比B树优化之处
  • 1:稳定性优化——B树的关键字分布在整颗树中,同一个关键字只会出现一次。B+树可能会出现多次,所有的叶子结点中包含了全部关键字的信息,B树搜索有可能在非叶子结点就结束搜索,而B+树必须搜索到叶子结点,但是锁定磁盘块指针消耗可以忽略不计;

  • 2:相同层级数数据量增加,减少IO次数——B树中间节点会保存数据,B+树的中间节点不保存数据,磁盘页能容纳更多关键字和指针信息,更“矮胖”,树的层高能进一步被压缩;

  • 3:范围查找优化——B+树叶子结点本身依关键字的大小自小而大顺序排列而且允许链接(有序链表),只需遍历叶子节点链表即可,b树却需要重复地遍历树。

3、常见排序算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ujUGHPv-1647795627093)(/Users/hello/Desktop/面试/image/1730367-20190709231552258-1256281704 (1)].png)

1、冒泡排序
思路:外层循环从1到n-1,内循环从当前外层的元素的下一个位置开始,依次和外层的元素比较,出现逆序就交换,通过与相邻元素的比较和交换来把小的数交换到最前面。

1 for(int i=0;i<arr.length-1;i++){//外层循环控制排序趟数
2       for(int j=0;j<arr.length-1-i;j++){//内层循环控制每一趟排序多少次
3         if(arr[j]>arr[j+1]){
4           int temp=arr[j];
5           arr[j]=arr[j+1];
6           arr[j+1]=temp;
7         }
8       }
9     } 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QxVuy9fJ-1647795627093)(/Users/hello/Desktop/面试/image/1730367-20190709233249402-580041311.gif)]

2、选择排序
思路:冒泡排序是通过相邻的比较和交换,每次找个最小值。选择排序是:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l8gNJuYX-1647795627093)(/Users/hello/Desktop/面试/image/1730367-20190709234546406-1933756989.gif)]

 1  private static void sort(int[] array) {
 2         int n = array.length;
 3         for (int i = 0; i < n-1; i++) {
 4             int min = i;
 5             for (int j = i+1; j < n; j++) {
 6                 if (array[j] < array[min]){//寻找最小数
 7                     min = j;                      //将最小数的索引赋值
 8                  }  
 9             }
10             int temp = array[i];
11             array[i] = array[min];
12             array[min] = temp;
13 
14         }
15    1730367-20190709235607059-17861797.gif }
16                        

3、插入排序
思路:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。可以理解为玩扑克牌时的理牌;

 1 private static void sort(int[] array) {
 2         int n = array.length;
 3     /**
 4     *从第二位数字开始,每一个数字都试图跟它的前一个比较并交换,并重复;直到前一个数字不存在或者比它小或相等时停下来
 5     **/
 6         for (int i = 1; i < n; i++) {//从第二个数开始
 7             int key = array[i];
 8             int j = i -1;
 9             while (j >= 0 && array[j]>key) {
10                 array[j + 1] = array[j];     //交换
11                 j--;                                //下标向前移动
12             }
13             array[j+1] = key;
14         }
15     }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hSxamowO-1647795627093)(/Users/hello/Desktop/面试/image/1730367-20190709235607059-17861797.gif)]

4、希尔排序
思路:希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。
问题:增量的序列取法?
  关于取法,没有统一标准,但最后一步必须是1;因为不同的取法涉及时间复杂度不一样,具体了解可以参考《数据结构与算法分析》;一般以length/2为算法。(再此以gap=gap*3+1为公式)

[复制代码](javascript:void(0)😉

 1 private static void sort(int[] array) {
 2         int n = array.length;
 3         int h = 1;
 4         while (h<n/3) { //动态定义间隔序列
 5               h = 3*h +1;
 6                  }
 7          while (h >= 1) {
 8             for (int i = h; i < n; i++) {
 9                 for (int j = i; j >= h && (array[j] < array[j - h]); j -= h) {
10                     int temp = array[j];
11                     array[j] = array[j - h];
12                     array[j-h]= temp;
13                 }
14             }
15             h /=3;
16         }
17     }

[复制代码](javascript:void(0)😉

img

5、归并排序
思路:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。它使用了递归分治的思想;相当于:左半边用尽,则取右半边元素;右半边用尽,则取左半边元素;右半边的当前元素小于左半边的当前元素,则取右半边元素;右半边的当前元素大于左半边的当前元素,则取左半边的元素。
  自顶向下:

[复制代码](javascript:void(0)😉

 1  private static void mergeSort(int[] array) {
 2         int[] aux = new int[array.length];
 3         sort(array, aux, 0, array.length - 1);
 4     }
 5 
 6     private static void sort(int[] array, int[] aux, int lo, int hi) {
 7         if (hi<=lo) return;
 8         int mid = lo + (hi - lo)/2;
 9         sort(array, aux, lo, mid);
10         sort(array, aux, mid + 1, hi);
11         merge(array, aux, lo, mid, hi);
12     }
13 
14     private static void merge(int[] array, int[] aux, int lo, int mid, int hi) {
15         System.arraycopy(array,0,aux,0,array.length);
16         int i = lo, j = mid + 1;
17         for (int k = lo; k <= hi; k++) {
18             if (i>mid) array[k] = aux[j++];
19             else if (j > hi) array[k] = aux[i++];
20             else if (aux[j]<aux[i]) array[k] = aux[j++];
21             else array[k] = aux[i++];
22         }
23     }

[复制代码](javascript:void(0)😉

自底向上:

[复制代码](javascript:void(0)😉

 1 public static void sort(int[] array) {
 2         int N = a.length;
 3         int[] aux = new int[N];
 4         for (int n = 1; n < N; n = n+n) {
 5             for (int i = 0; i < N-n; i += n+n) {
 6                 int lo = i;
 7                 int m  = i+n-1;
 8                 int hi = Math.min(i+n+n-1, N-1);
 9                 merge(array, aux, lo, m, hi);
10             }
11         }
12     }
13 
14     private static void merge(int[] array, int[] aux, int lo, int mid, int hi) {
15         for (int k = lo; k <= hi; k++) {
16             aux[k] = array[k];
17         }
18         // merge back to a[]
19         int i = lo, j = mid+1;
20         for (int k = lo; k <= hi; k++) {
21             if      (i > mid)              array[k] = aux[j++];  // this copying is unneccessary
22             else if (j > hi)               array[k] = aux[i++];
23             else if (aux[j]<aux[i]) array[k] = aux[j++];
24             else                           array[k] = aux[i++];
25         }
26     }

[复制代码](javascript:void(0)😉

缺点:因为是Out-place sort,因此相比快排,需要很多额外的空间。

为什么归并排序比快速排序慢?

答:虽然渐近复杂度一样,但是归并排序的系数比快排大。

对于归并排序有什么改进?

答:就是在数组长度为k时,用插入排序,因为插入排序适合对小数组排序。在算法导论思考题2-1中介绍了。复杂度为O(nk+nlg(n/k)) ,当k=O(lgn)时,复杂度为O(nlgn)

例子:

[复制代码](javascript:void(0)😉

 1     private static int mark = 0; 
 2       /**
 3        * 归并排序
 4        */
 5       private static int[] sort(int[] array, int low, int high) {
 6         int mid = (low + high) / 2;
 7         if (low < high) {
 8           mark++;
 9           System.out.println("正在进行第" + mark + "次分隔,得到");
10           System.out.println("[" + low + "-" + mid + "] [" + (mid + 1) + "-" + high + "]");
11           // 左边数组
12           sort(array, low, mid);
13           // 右边数组
14           sort(array, mid + 1, high);
15           // 左右归并
16           merge(array, low, mid, high);
17         }
18         return array;
19       }
20      
21       /**
22        * 对数组进行归并
23        * 
24        * @param array
25        * @param low
26        * @param mid
27        * @param high
28        */
29       private static void merge(int[] array, int low, int mid, int high) {
30         System.out.println("合并:[" + low + "-" + mid + "] 和 [" + (mid + 1) + "-" + high + "]");
31         int[] temp = new int[high - low + 1];
32         int i = low;// 左指针
33         int j = mid + 1;// 右指针
34         int k = 0;
35         // 把较小的数先移到新数组中
36         while (i <= mid && j <= high) {
37           if (array[i] < array[j]) {
38             temp[k++] = array[i++];
39           } else {
40             temp[k++] = array[j++];
41           }
42         }
43         // 两个数组之一可能存在剩余的元素
44         // 把左边剩余的数移入数组
45         while (i <= mid) {
46           temp[k++] = array[i++];
47         }
48         // 把右边边剩余的数移入数组
49         while (j <= high) {
50           temp[k++] = array[j++];
51         }
52         // 把新数组中的数覆盖array数组
53         for (int m = 0; m < temp.length; m++) {
54           array[m + low] = temp[m];
55         }
56       }
57      
58       /**
59        * 归并排序
60        */
61       public static int[] sort(int[] array) {
62         return sort(array, 0, array.length - 1);
63       }
64      
65       public static void main(String[] args) {
66         int[] array = { 3, 5, 2, 6, 2 };
67         int[] sorted = sort(array);
68         System.out.println("最终结果");
69         for (int i : sorted) {
70           System.out.print(i + " ");
71         }
72       }

[复制代码](javascript:void(0)😉

6、快速排序
思路:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

[复制代码](javascript:void(0)😉

private static void sort(int[] array) {
        shuffle(array);
        sort(array, 0, array.length - 1);
    }
    private static void sort(int[] array, int lo, int hi) {
       if(hi<=lo+M) {
        Insert.sort(a,lo,hi);
        return;
        }
        int lt = lo, gt = hi;
        int v = array[lo];
        int i = lo;
        while (i <= gt) {
            if      (array[i]<v) exch(array, lt++, i++);
            else if (array[i]>v) exch(array, i, gt--);
            else              i++;
        }
        // a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi].
        sort(array, lo, lt-1);
        sort(array, gt+1, hi);
    }

    private static void exch(int[] a, int i, int j) {
        int swap = a[i];
        a[i] = a[j];
        a[j] = swap;
    }

    /**
     *打乱数组
     */
    private static void shuffle(int[] array) {
        Random random = new Random(System.currentTimeMillis());
        if (array == null) throw new NullPointerException("argument array is null");
        int n = array.length;
        for (int i = 0; i < n; i++) {
            int r = i + random.nextInt(n-i);     // between i and n-1
            int temp = array[i];
            array[i] = array[r];
            array[r] = temp;
        }
    }
    
代码例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4huLX6M4-1647795627097)(/Users/hello/Desktop/面试/image/1730367-20190710002639792-1166003167.gif)]

 1 package test;
 2 
 3 public class s {
 4       public static void main(String[] args) {
 5             int[] arr = { 5,2,4,9,7 };
 6             sort(arr, 0, arr.length - 1);
 7         }
 8         public static void sort(int arr[], int low, int high) {
 9             int l = low;
10             int h = high;
11             int k = arr[low];
12             while (l < h) {
13                 // 从后往前比较
14                 while (l < h && arr[h] >= k ){ // 如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较
15                     h--;// h=6
16                 }
17                 if (l < h) {
18                     int temp = arr[h];
19                     arr[h] = arr[l];
20                     arr[l] = temp;
21                     //进行过一次替换后,没必要将替换后的两值再次比较,所以i++直接下一位与k对比
22                     l++;
23                 }
24                 // 从前往后比较
25                 while (l < h && arr[l] <= k) { // 如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置
26                     l++;
27                 }
28                 if (l < h) {
29                     int temp = arr[h];
30                     arr[h] = arr[l];
31                     arr[l] = temp;
32                     h--;
33                 }
34                 // 此时第一次循环比较结束,关键值的位置已经确定了。左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用
35             }
36             print(arr);
37             System.out.print("l=" + (l + 1) + "h=" + (h + 1) + "k=" + k + "\n");
38             // 递归
39             if (l > low)//先判断l>low再次经行左边排序
40                 sort(arr, low, l - 1);// 左边序列。第一个索引位置到关键值索引-1
41             if (h < high)//左边依次排序执行完递归后,弹栈进行右边排序
42                 sort(arr, l + 1, high);// 右边序列。从关键值索引+1到最后一个
43         }
44         // 打印数组的方法
45         private static void print(int[] arr) {
46             System.out.print("[");
47             for (int i = 0; i < arr.length; i++) {
48                 if (i != (arr.length - 1)) {
49                     System.out.print(arr[i] + ",");
50                 } else {
51                     System.out.print(arr[i] + "]");
52                     System.out.println();
53                 }
54             }
55         }
56 }

javascript:void(0)😉

7、堆排序
思路:堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

[[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rZri1xCC-1647795627097)(/Users/hello/Desktop/面试/image/1730367-20190710003052018-245302424.gif)]

 1  public static void sort(int[] a){
 2         int N = a.length;
 3         int[] keys = new int[N+1];
 4         //注意,堆的数据结构是从1开始的,0不用
 5         for (int i = 1; i < keys.length; i++) {
 6             keys[i] = a[i-1];
 7         }
 8 //      //构造堆,使得堆是有序的
 9         for(int k = N/2;k>=1;k--) sink(keys,k,N);
10         //排序,相当于毁掉堆
11         while(N>1){
12             exch(keys,1,N--);
13             sink(keys,1,N);
14         }
15         //重新写回数组
16         for (int i = 0; i < a.length; i++) {
17             a[i] = keys[i+1];
18         }
19     }
20 
21     private static void sink(int[] a, int k, int N) {
22         // TODO Auto-generated method stub
23         while(2*k<=N){
24             int j = 2*k;
25             if (j < N && less(a[j], a[j+1])) j++;
26             if (less(a[j], a[k])) break;
27             exch(a, k, j);
28             k = j;
29         }
30     }
31 
32     private static boolean less(int k, int j) {
33         // TODO Auto-generated method stub
34         return k < j;
35     }
36 
37     private static void exch(int[] a, int i, int n) {
38         // TODO Auto-generated method stub
39         int temp = a[i];
40         a[i] = a[n];
41         a[n] = temp;
42     }

[复制代码](javascript:void(0)😉

代码例子:

[复制代码](javascript:void(0)😉

  1 package test;
  2 
  3 public class dui {
  4     /** 
  5        * 调整为小顶堆(排序后结果为从大到小)
  6        * 
  7        * @param array是待调整的堆数组 
  8        * @param s是待调整的数组元素的位置
  9        * @param length是数组的长度
 10        * 
 11        */
 12       public static void heapAdjustS(int[] array, int s, int length) {
 13         int tmp = array[s];
 14         int child = 2 * s + 1;// 左孩子结点的位置
 15         System.out.println("待调整结点为:array[" + s + "] = " + tmp);
 16         while (child < length) {
 17           // child + 1 是当前调整结点的右孩子
 18           // 如果有右孩子且小于左孩子,使用右孩子与结点进行比较,否则使用左孩子
 19           if (child + 1 < length && array[child] > array[child + 1]) {
 20             child++;
 21           }
 22           System.out.println("将与子孩子 array[" + child + "] = " + array[child] + " 进行比较");
 23           // 如果较小的子孩子比此结点小
 24           if (array[s] > array[child]) {
 25             System.out.println("子孩子比其小,交换位置");
 26             array[s] = array[child];// 把较小的子孩子向上移动,替换当前待调整结点
 27             s = child;// 待调整结点移动到较小子孩子原来的位置
 28             array[child] = tmp;
 29             child = 2 * s + 1;// 继续判断待调整结点是否需要继续调整
 30              
 31             if (child >= length) {
 32               System.out.println("没有子孩子了,调整结束");
 33             } else {
 34               System.out.println("继续与新的子孩子进行比较");
 35             }
 36             // continue;
 37           } else {
 38             System.out.println("子孩子均比其大,调整结束");
 39             break;// 当前待调整结点小于它的左右孩子,不需调整,直接退出
 40           }
 41         }
 42       }
 43        
 44       /** 
 45        * 调整为大顶堆(排序后结果为从小到大)
 46        * 
 47        * @param array是待调整的堆数组 
 48        * @param s是待调整的数组元素的位置
 49        * @param length是数组的长度
 50        * 
 51        */
 52       public static void heapAdjustB(int[] array, int s, int length) {
 53         int tmp = array[s];
 54         int child = 2 * s + 1;// 左孩子结点的位置
 55         System.out.println("待调整结点为:array[" + s + "] = " + tmp);
 56         while (child < length) {
 57           // child + 1 是当前调整结点的右孩子
 58           // 如果有右孩子且大于左孩子,使用右孩子与结点进行比较,否则使用左孩子
 59           if (child + 1 < length && array[child] < array[child + 1]) {
 60             child++;
 61           }
 62           System.out.println("将与子孩子 array[" + child + "] = " + array[child] + " 进行比较");
 63           // 如果较大的子孩子比此结点大
 64           if (array[s] < array[child]) {
 65             System.out.println("子孩子比其大,交换位置");
 66             array[s] = array[child];// 把较大的子孩子向上移动,替换当前待调整结点
 67             s = child;// 待调整结点移动到较大子孩子原来的位置
 68             array[child] = tmp;
 69             child = 2 * s + 1;// 继续判断待调整结点是否需要继续调整
 70              
 71             if (child >= length) {
 72               System.out.println("没有子孩子了,调整结束");
 73             } else {
 74               System.out.println("继续与新的子孩子进行比较");
 75             }
 76             // continue;
 77           } else {
 78             System.out.println("子孩子均比其小,调整结束");
 79             break;// 当前待调整结点大于它的左右孩子,不需调整,直接退出
 80           }
 81         }
 82       }
 83         
 84       /**
 85        * 堆排序算法
 86        * 
 87        * @param array
 88        * @param inverse true 为倒序排列,false 为正序排列
 89        */
 90       public static void heapSort(int[] array, boolean inverse) {
 91         // 初始堆
 92         // 最后一个有孩子的结点位置 i = (length - 1) / 2, 以此向上调整各结点使其符合堆
 93         System.out.println("初始堆开始");
 94         for (int i = (array.length - 1) / 2; i >= 0; i--) {
 95           if (inverse) {
 96             heapAdjustS(array, i, array.length);
 97           } else {
 98             heapAdjustB(array, i, array.length);
 99           }
100         }
101         System.out.println("初始堆结束");
102         for (int i = array.length - 1; i > 0; i--) {
103           // 交换堆顶元素H[0]和堆中最后一个元素
104           int tmp = array[i];
105           array[i] = array[0];
106           array[0] = tmp;
107           // 每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
108           if (inverse) {
109             heapAdjustS(array, 0, i);
110           } else {
111             heapAdjustB(array, 0, i);
112           }
113         }
114       }
115      
116       public static void main(String[] args) {
117         int[] array = { 49, 38, 65, 97, 76, 13, 27, 49 };
118         heapSort(array, false);
119         for (int i : array) {
120           System.out.print(i + " ");
121         }
122       }
123 
124 }

[复制代码](javascript:void(0)😉

10、基数排序
思路:基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

取得数组中的最大数,并取得位数;
  arr为原始数组,从最低位开始取每个位组成radix数组;
  对radix进行计数排序(利用计数排序适用于小范围数的特点);

[复制代码](javascript:void(0)😉

 1 private static void radixSort(int[] array,int radix, int distance) {
 2         int length = array.length;
 3         int[] temp = new int[length];
 4         int[] count = new int[radix];
 5         int divide = 1;
 6 
 7         for (int i = 0; i < distance; i++) {
 8 
 9             System.arraycopy(array, 0,temp, 0, length);
10             Arrays.fill(count, 0);
11 
12             for (int j = 0; j < length; j++) {
13                 int tempKey = (temp[j]/divide)%radix;
14                 count[tempKey]++;
15             }
16 
17             for (int j = 1; j < radix; j++) {
18                 count [j] = count[j] + count[j-1];
19             }
20             for (int j = length - 1; j >= 0; j--) {
21                 int tempKey = (temp[j]/divide)%radix;
22                 count[tempKey]--;
23                 array[count[tempKey]] = temp[j];
24             }
25             divide = divide * radix;
26         }
27     }

[复制代码](javascript:void(0)😉

代码例子:

 1 package test;
 2 
 3     /**
 4      * 基数排序
 5      * 平均O(d(n+r)),最好O(d(n+r)),最坏O(d(n+r));空间复杂度O(n+r);稳定;较复杂
 6      * d为位数,r为分配后链表的个数
 7      * 
 8      *
 9      */
10     public class ji_shu {
11         //pos=1表示个位,pos=2表示十位
12         public static int getNumInPos(int num, int pos) {
13             int tmp = 1;
14             for (int i = 0; i < pos - 1; i++) {
15                 tmp *= 10;
16             }
17             return (num / tmp) % 10;
18         }
19         //求得最大位数d
20         public static int getMaxWeishu(int[] a) {
21             int max = a[0];
22             for (int i = 0; i < a.length; i++) {
23                 if (a[i] > max)
24                         max = a[i];
25             }
26             int tmp = 1, d = 1;
27             while (true) {
28                 tmp *= 10;
29                 if (max / tmp != 0) {
30                     d++;
31                 } else
32                         break;
33             }
34             return d;
35         }
36         public static void radixSort(int[] a, int d) {
37             int[][] array = new int[10][a.length + 1];
38             for (int i = 0; i < 10; i++) {
39                 array[i][0] = 0;
40                 // array[i][0]记录第i行数据的个数
41             }
42             for (int pos = 1; pos <= d; pos++) {
43                 for (int i = 0; i < a.length; i++) {
44                     // 分配过程
45                     int row = getNumInPos(a[i], pos);
46                     int col = ++array[row][0];
47                     array[row][col] = a[i];
48                 }
49                 for (int row = 0, i = 0; row < 10; row++) {
50                     // 收集过程
51                     for (int col = 1; col <= array[row][0]; col++) {
52                         a[i++] = array[row][col];
53                     }
54                     array[row][0] = 0;
55                     // 复位,下一个pos时还需使用
56                 }
57             }
58         }
59         public static void main(String[] args) {
60             int[] a = { 49, 38, 65, 197, 76, 213, 27, 50 };
61             radixSort(a, getMaxWeishu(a));
62             for (int i : a)
63                   System.out.print(i + " ");
64         }
65     }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-di9x03mN-1647795627099)(/Users/hello/Desktop/面试/image/1730367-20190710004213327-335264969.gif)]

选择排序的不稳定例子很简单。

比如A 80 B 80 C 70 这三个卷子从小到大排序

第一步会把C和A做交换 变成C B A

第二步和第三步不需要再做交换了。所以排序完是C B A

但是稳定的排序应该是C A B

===============================================

计算机网络

1、HTTP 请求方法?

1、Opions 返回服务器针对特定资源所支持的HTML请求方法 或web服务器发送测试服务器功能(允许客户端查看服务器性能

2、Get 向特定资源发出请求(请求指定页面信息,并返回实体主体)。

3、Post 把指定资源提交数据进行处理请求(提交表单、上传文件),又可能导致新的资源的建立或原有资源的修改,一般修改数据库。

4、Put 向指定资源位置上上传其最新内容(从客户端向服务器传送的数据取代指定文档的内容

5、Head 与服务器索与get请求一致的相应,响应体不会返回,获取包含在小消息头中的原信息(与get请求类似,返回的响应中没有具体内容,用于获取报头)

6、Delete 请求服务器删除request-URL所标示的资源(请求服务器删除页面)

7、Trace 回显服务器收到的请求,用于测试和诊断

8、Connect HTTP/1.1协议中能够将连接改为管道方式的代理服务器

(代理服务器?===》)

1.1、GET 和 POST 比较
  1. 作用
    GET 用于获取资源,而 POST 用于传输实体主体。

  2. 参数
    GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。

因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 中文 会转换为 %E4%B8%AD%E6%96%87,而空格会转换为 %20。POST 参数支持标准字符集。

GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2
  1. 安全
    安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。

安全的方法除了 GET 之外还有:HEAD、OPTIONS。不安全的方法除了 POST 之外还有 PUT、DELETE。

  1. 幂等性
    幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。

所有的安全方法也都是幂等的。

在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。

GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:

GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1
GET /pageX HTTP/1.1

POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录:

POST /add_row HTTP/1.1   -> Adds a 1nd row
POST /add_row HTTP/1.1   -> Adds a 2nd row
POST /add_row HTTP/1.1   -> Adds a 3rd row

DELETE /idX/delete HTTP/1.1 是幂等的,即使不同的请求接收到的状态码不一样:

DELETE /idX/delete HTTP/1.1   -> Returns 200 if idX exists
DELETE /idX/delete HTTP/1.1   -> Returns 404 as it just got deleted
DELETE /idX/delete HTTP/1.1   -> Returns 404
  1. 可缓存
    如果要对响应进行缓存,需要满足以下条件:请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的
    响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
    响应报文的 Cache-Control 首部字段没有指定不进行缓存。

  2. XMLHttpRequest
    为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest:

XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。

在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。
而 GET 方法 Header 和 Data 会一起发送。

作者:CyC2018
链接:https://leetcode-cn.com/leetbook/read/tech-interview-cookbook/op713i/

2、Post 和 Put 的区别?

PUT方法做了什么?
PUT方法将会完全地替代目标URL下的资源,不论目标URL下是否存在资源。使用这个方法,你可以创建一个全新的资源或覆盖有一个已经存在的资源,前提是您知道确切的请求URI。使用PUT方法创建新资源的示例如下:

PUT /forums/<new_thread> HTTP/2.0
Host: yourwebsite.com

其中<new_thread>是线程的实际名称或ID号。或者,用于覆盖现有资源的PUT方法可以如下所示:

PUT /forums/<existing_thread> HTTP/2.0
Host: yourwebsite.com

简而言之,PUT方法用于创建或覆盖浏览器认识的指定URL下的资源。

POST方法做了什么?
POST方法用于发送用户生成的数据发送到web服务器。比如说,当一个用户对论坛进行了评论或者上传了头像,这时候就应该使用POST方法。如果您不知道新创建的资源应该驻留在哪里,没有确定的URL,那么也应该使用POST方法。换言之,如果创建了一个新的论坛线程,并且没有指定线程路径,那么您可以使用如下所示:

POST /forums HTTP/2.0
Host: yourwebsite.com

使用此方法,源服务器将会返回URL path,您将收到类似以下内容的响应:

HTTP/2.0 201 Created
Location: /forums/<new_thread>

总之,POST方法应该用于创建一个下级(或者说孩子)资源的标识,通过请求URI。在上面的例子中,根据源定义,请求URI 是/forums以及下级或孩子应该是 <new_thread>

何时使用?
当您知道要创建或覆盖的内容的URL时,应该使用PUT方法。
当您只知道要创建内容的对象的类别或子部分的URL,请使用POST方法。

3、几种进程间的通信方式深信服,进出之间的通信方式?

1、管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系

2、有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信

3、信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

4、消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5、信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

6、共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两配合使用,来实现进程间的同步和通信。

7、套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

3、HTTP 和HTTPS的区别?

HTTP与HTTPS介绍
超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息

​ 为了解决HTTP协议的这一缺陷,需要使用另一种协议:安全套接字层超文本传输协议HTTPS,为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL/TLS协议,SSL/TLS依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密

HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全

HTTPS协议的主要作用可以分为两种:

  • 一种是建立一个信息安全通道,来保证数据传输的安全;

  • 另一种就是确认网站的真实性。

HTTPS和HTTP的主要区别
1、https协议需要到CA申请证书,一般免费证书较少,因而需要一定费用

​ 2、http是超文本传输协议,信息是明文传输,https则是具有安全性的SSL/TLS加密传输协议。

​ 3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

​ 4、http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

3.1、说说http1.x 和http2.0的区别

(1).HTTP2使用的是二进制传送,HTTP1.X是文本(字符串)传送。

二进制传送的单位是帧和流。帧组成了流,同时流还有流ID标示

(2).HTTP2支持多路复用

因为有流ID,所以通过同一个http请求实现多个http请求传输变成了可能,可以通过流ID来标示究竟是哪个流从而定位到是哪个http请求

(3).HTTP2头部压缩

HTTP2通过gzip和compress压缩头部然后再发送,同时客户端和服务器端同时维护一张头信息表,所有字段都记录在这张表中,这样后面每次传输只需要传输表里面的索引Id就行,通过索引ID查询表头的值

(4).HTTP2支持服务器推送

HTTP2支持在未经客户端许可的情况下,主动向客户端推送内容

3.2、HTTPS的流程
1.什么是HTTP协议?

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议),位于TCP/IP模型当中的应用层。HTTP协议通过请求/响应的方式,在客户端和服务端之间进行通信。HTTP协议的信息传输完全以明文的方式,不做任何加密,相当于在网络上“裸奔”,所以容易遭受中间人的恶意截获甚至篡改(中间人攻击)。

img

2.什么是HTTPS协议?

HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。

3.对称加密和非对称加密

对称加密

对称加密采用了对称密码编码技术,它的特点是文件加密和解密都是使用相同的密钥

这种方法在密码学中叫做对称加密算法,对称加密算法使用起来简单快捷,密钥较短,且破译困难,除了数据加密标准(DES),另一个对称密钥加密系统是国际数据加密算法(IDEA),它比DES的加密性好,而且对计算机功能要求也没有那么高。

非对称加密

与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。

公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。

因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

4.HTTPS流程

1.服务端首先把自己的公钥(Key1)发给证书颁发机构,向证书颁发机构申请证书。

img

2.证书颁发机构自己也有一对公钥私钥。机构利用自己的私钥来加密Key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样经过机构的私钥加密。证书制作完成后,机构把证书发送给了服务端。

img

3.当客户端向服务端请求通信的时候,服务端不再直接返回自己的公钥(Key1),而是把自己申请的证书返回给客户端。

4.客户端收到证书以后,要做的第一件事情是验证证书的真伪。需要说明的是,各大浏览器和操作系统已经维护了所有权威证书机构的名称和公钥。所以客户端只需要知道是哪个机构颁布的证书,就可以从本地找到对应的机构公钥,解密出证书签名。

客户端对证书验证成功后,就可以放心地再次利用机构公钥,解密出服务端的公钥Key1。

客户端本地如何验证证书呢?
证书本身就已经告诉客户端怎么验证证书的真伪,也就是证书上写着如何根据证书上的方法自己生成一个证书编号,如果生成的证书编号与证书上的证书编号相同,那么证明这个证书是真实的。同时,为避免证书编号本身又被调包,所以使用第三方机构的私钥进行加密。
证书就是HTTPS中的数字证书,证书编号就是数字签名,而第三方机构就是指数字证书签发机构(CA)

img

5.客户端生成自己的对称加密密钥Key2,并且用服务端公钥Key1加密Key2,发送给服务端。

img

6.服务端用自己的私钥解开加密,得到对称加密密钥Key2。于是客户端与服务端开始用Key2进行对称加密的通信。

img

5、HTTP通过什么保证安全传输?

目前大多数网站和app的接口都是采用http协议,但是http协议很容易就可以通过抓包工具监听到内容,甚至篡改内容,为了保证数据不被别人看到和修改,可以通过以下几个方面避免:

  • 重要数据加密 比如用户名和密码,我们需要加密,这样即使被抓包监听,他们也不知道原始数据是什么。简单的md5是可以暴力破解的,所以加密方式越复杂就越安全,可以根据需要自由组合 常见的是 md5(不可逆),aes(可逆)
  • 非重要数据要签名 签名的目的是防止篡改,比如http://www.xxx.com/getnews?id=1,获取 id=1的内容,如果不签名通过 id=2,就可以获取 id=2的内容。怎样签名呢?通常使用sign,比如原链接请求的时候加一个sign参数,sign=md5(id=1),服务器接受到请求,验证sign是否等于md5(id=1),如果等于说明正常请求。这会有个弊端,假如规则被发现,那么就会被伪造,所以适当复杂一些,还是能够提高安全性的。
  • 登录态怎么做 http是无状态的,也就是服务器没法自己判断两个请求是否有联系,那么登录之后,以后的接口怎么判断是否登录呢?简单的做法在数据库中存一个token(名字随意),当用户调用登录接口成功的时候就将该字段设一个值,(比如aes(过期时间)),同时返回给前端,以后每次前端请求带上该值,服务器首先校验是否过期,其次校验是否正确,不通过就不让登录

5、Cookie 和 Session 的区别?

面试常考
  • ①Cookie可以存储在浏览器或者本地,Session只能存在服务器
  • ②session 能够存储任意的 java 对象,cookie 只能存储 String 类型的对象
  • ③Session比Cookie更具有安全性(Cookie有安全隐患,通过拦截或本地文件找得到你的cookie后可以进行攻击)
  • ④Session占用服务器性能,Session过多,增加服务器压力
  • ⑤单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie,Session是没有大小限制和服务器的内存大小有关。
一.Cookie详解
(1)Cookie是什么 ?

Cookie,有时也用其复数形式Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行Session跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息。

(2)为什么要使用Cookie?解决了什么问题 ?
  • web程序是使用HTTP协议传输的,而HTTP协议是无状态的协议,对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

cookie的出现就是为了解决这个问题。

  • 第一次登录后服务器返回一些数据(cookie)给浏览器,然后浏览器保存在本地,当该用户发送第二次请求的时候,就会自动的把上次请求存储的cookie数据自动的携带给服务器,服务器通过浏览器携带的数据就能判断当前用户是哪个了。
  • 特点:cookie存储的数据量有限,不同的浏览器有不同的存储大小,但一般不超过4KB。因此使用cookie只能存储一些小量的数据。
  • 给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理。
(3)Cookie什么时候产生 ?
  • Cookie的使用一先要看需求。因为浏览器可以禁用Cookie,同时服务端也可以不Set-Cookie。
  • 客户端向服务器端发送一个请求的时,服务端向客户端发送一个Cookie然后浏览器将Cookie保存
  • Cookie有两种保存方式,一种是浏览器会将Cookie保存在内存中,还有一种是保存在客户端的硬盘中,之后每次HTTP请求浏览器都会将Cookie发送给服务器端。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Spjzvm3g-1647795627101)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.13.14.png)]

(4)Cookie的生存周期?
  • Cookie在生成时就会被指定一个Expire值,这就是Cookie的生存周期,在这个周期内Cookie有效,超出周期Cookie就会被清除。有些页面将Cookie的生存周期设置为“0”或负值,这样在关闭浏览器时,就马上清除Cookie,不会记录用户信息,更加安全。
(5)Cookie有哪些缺陷 ?
  • ①数量受到限制。一个浏览器能创建的 Cookie 数量最多为 300 个,并且每个不能超过 4KB,每个 Web 站点能设置的
    Cookie 总数不能超过 20 个
  • ②安全性无法得到保障。通常跨站点脚本攻击往往利用网站漏洞在网站页面中植入脚本代码或网站页面引用第三方法脚本代码,均存在跨站点脚本攻击的可能,在受到跨站点脚本攻击时,脚本指令将会读取当前站点的所有Cookie 内容(已不存在 Cookie 作用域限制),然后通过某种方式将 Cookie 内容提交到指定的服务器(如:AJAX)。一旦 Cookie 落入攻击者手中,它将会重现其价值。
  • ③浏览器可以禁用Cookie,禁用Cookie后,也就无法享有Cookie带来的方便。
(6)cookie的应用场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s4jWMtwr-1647795627101)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.13.40.png)]

二.Session详解

(1)web中什么是会话 ?
  • 用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话。
(2)什么是Session ?
  • Session:在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。
(3)Session什么时候产生 ?
  • 当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。
    这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5vyrR9Vz-1647795627101)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.17.02.png)]

  • 服务器会向客户浏览器发送一个每个用户特有的会话编号sessionID,让他进入到cookie里。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jhw4hfOG-1647795627101)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.14.15.png)]

  • 服务器同时也把sessionID和对应的用户信息、用户操作记录在服务器上,这些记录就是session。再次访问时会带入会发送cookie给服务器,其中就包含sessionID。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mDSLjJhk-1647795627102)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.14.19.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M7euMcxi-1647795627102)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.14.23.png)]

  • 服务器从cookie里找到sessionID,再根据sessionID找到以前记录的用户信息就可以知道他之前操控些、访问过哪里。
(4)Session的生命周期 ?
  • 根据需求设定,一般来说,半小时。举个例子,你登录一个服务器,服务器返回给你一个sessionID,登录成功之后的半小时之内没有对该服务器进行任何HTTP请求,半小时后你进行一次HTTP请求,会提示你重新登录。
    小结:Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。

三.cookie和session结合使用

web开发发展至今,cookie和session的使用已经出现了一些非常成熟的方案。在如今的市场或者企业里,一般有两种存储方式:

  • 1、存储在服务端:通过cookie存储一个session_id,然后具体的数据则是保存在session中。如果用户已经登录,则服务器会在cookie中保存一个session_id,下次再次请求的时候,会把该session_id携带上来,服务器根据session_id在session库中获取用户的session数据。就能知道该用户到底是谁,以及之前保存的一些状态信息。这种专业术语叫做server side session。
  • 2、将session数据加密,然后存储在cookie中。这种专业术语叫做client side session。flask采用的就是这种方式,但是也可以替换成其他形式。

————————————————
版权声明:本文为CSDN博主「辰兮要努力」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45393094/article/details/104747360

6、什么是中间人攻击?

中间人攻击(Man-in-the-MiddleAttack,简称“MITM攻击”)是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方 直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。中间人攻击是一个(缺乏)相互认证的攻击。大多数的加密协议都专门加入了一些特殊的认证方法以阻止中间人攻击。例如,SSL协议可以验证参与通讯的一方或双方使用的证书是否是由权威的受信任的数字证书认证机构颁发,并且能执行双向身份认证。

中间人攻击过程

1)客户端发送请求到服务端,请求被中间人截获。

2)服务器向客户端发送公钥

3)中间人截获公钥,保留在自己手上。然后自己生成一个【伪造的】公钥,发给客户端。

4)客户端收到伪造的公钥后,生成加密hash值发给服务器

5)中间人获得加密hash值,用自己的私钥解密获得真秘钥。同时生成假的加密hash值,发给服务器。

6 ) 服务器用私钥解密获得假密钥。然后加密数据传输给客户端。

7、TCP/UDP协议

TCP(传输控制协议):TCP是一种面向连接的、可靠的传输层协议;TCP协议建立在不可靠的网络层 IP 协议之上,IP协议并不能提供任何可靠性机制,TCP的可靠性完全由自己实现;TCP采用的最基本的可靠性技术是:确认与超时重传机制、流量控制机制;

  • 超时重传是TCP协议保证数据可靠性的一个重要机制,其原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止。
  • 流量控制就是让发送速率不要过快,让接收方来得及接收。利用滑动窗口机制就可以实施流量控制。

TCP三次握手(非常重要)

  • 第一次握手:客户端向服务器发送请求报文段,其中同步位SYN=1,序号SEQ=x(表明传送数据时的第一个数据字节的序号是x),等待服务器确认;
  • 第二次握手:服务器收到客户端发来的请求,如果同意建立连接,就发回一个确认报文段,该报文段中同步位SYN=1,确认号ACK=x+1,序号SEQ=y;
  • 第三次握手:客户端收到服务器的确认报文段后,还需要向服务器给出确认,向其发送确认包ACK(ack=y+1),进而完成三次握手。

通过这样的三次握手,客户端与服务端建立起可靠的双工的连接,开始传送数据。为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。

用户数据报协议(用户报文协议)UDP:UDP是一种无连接的、不可靠传输层协议;提供了有限的差错检验功能;目的是希望以最小的开销来达到网络环境中的进程通信目的;

UDP协议以其简单、传输快的优势,在越来越多场景下取代了TCP,如网页浏览、流媒体、实时游戏、物联网。

  1. 网速的提升给UDP稳定性提供可靠网络保障,UDP的丢包率低于5%,如果再使用应用层重传,能够完全确保传输的可靠性。
  2. 对比测试结果UDP性能优于TCP,为了提升浏览速度,Google基于TCP提出了SPDY协议以及HTTP/2。Google在Chrome上实验基于UDP的QUIC协议,传输速率减少到100ms以内。
  3. TCP设计过于冗余,速度难以进一步提升
    TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程以及重传策略。由于TCP内置在系统协议栈中,极难对其进行改进。
  4. UDP协议以其简单、传输快的优势,在越来越多场景下取代了TCP

TCP与UDP的不同:

  1. 是否需要建立连接。UDP在传送数据之前不需要先建立连接;TCP则提供面向连接的服务;
  2. 是否需要给出确认。对方的传输层在收到UDP报文后,不需要给出任何确认,而 TCP需要给出确认报文,要提供可靠的、面向连接的传输服务。
  3. 虽然UDP不提供可靠交付,但在某些情况下UDP是一种最有效的工作方式;【UDP取代TCP】

和IP层的联系: IP层只负责把数据送到节点,而不能区分上面的不同应用,所以TCP和UDP协议在其基础上加入了端口的信息,每个端口标识的是一个节点上的一个应用。 除了增加端口信息,UPD协议基本就没有对IP层的数据进行任何的处理了。而TCP协议还加入了更加复杂的传输控制,比如滑动的数据发送窗口,以及接收确认和重发机制,以达到数据的可靠传送。不管应用层看到的是怎样一个稳定的TCP数据流,下面传送的都是一个个的IP数据包,需要由TCP协议来进行数据重组。

TCP和UDP区别详解

https://blog.csdn.net/wyplj2015/article/details/108779017?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0.pc_relevant_default&spm=1001.2101.3001.4242.1&utm_relevant_index=3

TCP粘包

TCP滑动窗口

12、TCP是通过什么机制保障可靠性的?

从四个方面进行回答,ACK确认机制、超时重传、滑动窗口以及流量控制,深入的话要求详细讲出流量控制的机制。

ACK确认机制

超时重传

滑动窗口

https://blog.csdn.net/qq_40459977/article/details/123076728?spm=1001.2101.3001.6650.11&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-11.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-11.pc_relevant_default&utm_relevant_index=17

流量控制

TCP的拥塞控制

产生的原因

  • 注意单纯的增加网络资源无法解决问题。例如:把结点的存储空间扩大,更换更高速率的链路,提高结点处理机的运算速度,不仅不能解决问题,而且可能使网络性能更坏。原因:网络拥塞是许多因素引起的,单纯的解决一个可能会使上述情况得到一些缓解,但是会把拥塞转移到其他地方。

  • 扩大结点存储空间——>由于输出链路的容量和处理机的速度并未提高,增大排队等待时间,超时重传,浪费资源。更换更高速率的链路——>可能会缓解,,有可能造成各部分不匹配。

拥塞控制的作用

注意
拥塞控制与流量控制的区别

  • 拥塞控制是防止过多的数据注入到网络中,可以使网络中的路由器或链路不致过载,是一个全局性的过程。

  • 流量控制是点对点通信量的控制,是一个端到端的问题,主要就是抑制发送端发送数据的速率,以便接收端来得及接收。

拥塞的标志

  1. 重传计时器超时
  2. 接收到三个重复确认
拥塞控制的机制
慢开始与拥塞避免

慢开始

1.慢开始不是指cwnd的增长速度慢(指数增长),而是指TCP开始发送设置cwnd=1。

2.思路:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。这里用报文段的个数的拥塞窗口大小举例说明慢开始算法,实时拥塞窗口大小是以字节为单位的。如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hi2qDupq-1647795627102)(/Users/hello/Desktop/面试/image/截屏2022-03-17 19.20.16.png)]

3.为了防止cwnd增长过大引起网络拥塞,设置一个慢开始门限(ssthresh状态变量)
当cnwd<ssthresh,使用慢开始算法
当cnwd=ssthresh,既可使用慢开始算法,也可以使用拥塞避免算法
当cnwd>ssthresh,使用拥塞避免算法

拥塞避免(按线性规律增长)

1.拥塞避免并非完全能够避免拥塞,是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。

2.思路:让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞控制窗口加一。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法。

加法增大与乘法减小
乘法减小:无论是慢开始阶段还是拥塞避免,只要出现了网络拥塞(超时),就把慢开始门限值ssthresh减半
加法增大:执行拥塞避免算法后,拥塞窗口线性缓慢增大,防止网络过早出现拥塞

快重传与快恢复

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CcvbTozA-1647795627102)(/Users/hello/Desktop/面试/image/截屏2022-03-17 19.19.29.png)]

快重传
1.快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。

2.由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量。

快恢复(与快重传配合使用)
1.采用快恢复算法时,慢开始只在TCP连接建立时和网络出现超时时才使用。
2.当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半。但是接下去并不执行慢开始算法。
3.考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。

注意
发送方窗口的上限值=Min(接受窗口rwnd,拥塞窗口cwnd)
rwnd>cwnd 接收方的接收能力限制发送方窗口的最大值
rwnd<cwnd 网络的拥塞限制发送方窗口的最大值

面试题
TCP的拥塞控制机制是什么?请简单说说。

答:我们知道TCP通过一个定时器(timer)采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,然而重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这就导致了恶性循环,最终形成“网络风暴” —— TCP的拥塞控制机制就是用于应对这种情况。
首先需要了解一个概念,为了在发送端调节所要发送的数据量,定义了一个“拥塞窗口”(Congestion Window),在发送数据时,将拥塞窗口的大小与接收端ack的窗口大小做比较,取较小者作为发送数据量的上限。
拥塞控制主要是四个算法:

  • 1.慢启动:意思是刚刚加入网络的连接,一点一点地提速,不要一上来就把路占满。
    连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。
    每当收到一个ACK,cwnd++; 呈线性上升
    每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
    阈值ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”

  • 2.拥塞避免:当拥塞窗口 cwnd 达到一个阈值时,窗口大小不再呈指数上升,而是以线性上升,避免增长过快导致网络拥塞。
    每当收到一个ACK,cwnd = cwnd + 1/cwnd
    每当过了一个RTT,cwnd = cwnd + 1
    拥塞发生:当发生丢包进行数据包重传时,表示网络已经拥塞。分两种情况进行处理:
    等到RTO超时,重传数据包
    sshthresh = cwnd /2
    cwnd 重置为 1

  • 3.进入慢启动过程
    在收到3个duplicate ACK时就开启重传,而不用等到RTO超时
    sshthresh = cwnd = cwnd /2
    进入快速恢复算法——Fast Recovery

  • 4.快速恢复:至少收到了3个Duplicated Acks,说明网络也不那么糟糕,可以快速恢复。
    cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
    重传Duplicated ACKs指定的数据包
    如果再收到 duplicated Acks,那么cwnd = cwnd +1
    如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。

    原文链接:https://blog.csdn.net/shuxnhs/article/details/80644531

8、三次握手和四次挥手

三次握手

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yo9SBYXP-1647795627103)(/Users/hello/Desktop/面试/image/截屏2022-03-14 15.09.31.png)]

四次挥手

状态转化:A、B连接建立状态ESTABLISHED -> A终止等待1状态FIN-WAIT-1 -> B关闭等待状态2CLOSE-WAIT -> A终止等待2状态FIN-WAIT-2 -> B最后确认状态LAST-ACK -> A时间等待状态TIME-WAIT -> B、A关闭状态CLOSED

四次挥手过程
第一次挥手:A数据传输完毕需要断开连接,A的应用进程向其TCP发出连接释放报文段(FIN = 1,序号seq = u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1状态,等待B的确认。

第二次挥手:B收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),B进入CLOSE-WAIT关闭等待状态,此时的TCP处于半关闭状态,A到B的连接释放。而A收到B的确认后,进入FIN-WAIT-2状态,等待B发出的连接释放报文段。

第三次挥手:当B数据传输完毕后,B发出连接释放报文段(FIN = 1,ACK = 1,序号seq = w,确认号ack=u+1),B进入LAST-ACK(最后确认)状态,等待A 的最后确认。

第四次挥手:A收到B的连接释放报文段后,对此发出确认报文段(ACK = 1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,A才进入CLOSE状态。

  • 客户端发送报文:FIN=1,seq=a;
  • 服务器响应报文:ACK=1,seq=b,ack=a+1;
  • 服务器发送报文:FIN=1,ACK=1,seq=c,ack=a+1;
  • 客户端响应报文:ACK=1,seq=a+1,ack=c+1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8KUpEOb2-1647795627103)(/Users/hello/Desktop/面试/image/截屏2022-03-14 15.09.45.png)]

9、为什么A在TIME-WAIT状态必须等待2MSL(最大报文生存时间)的时间?

1.保证A发送的最后一个ACK报文段能够到达B,保证A、B正常进入CLOSED状态

这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认,B超时重传FIN+ACK报文段,A能2MSL时间内收到这个重传的FIN+ACK报文段,接着A重传一次确认,同时重启2MSL计数器,2MSL时间后A和B进入CLOSE状态,如果A在TIME-WAIT状态时接收到B的FIN+ACK报文段之后向B发出确认报文段,而不再确认B是否收到立即进入CLOSED状态,如若B并没有正常收到A 的确认报文段,则B无法正正常进入到CLOSED状态(因为这时候A已经关闭了)。

2.防止“已经失效的连接请求报文段”出现在本连接中

A在发送完最后一个ACK报文段并经过2MSL,会使本次连接持续时间内所有产生的报文段消失,保证在下一次新连接中不会出现旧连接遗留的请求报文段。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SWELt70p-1647795627103)(/Users/hello/Library/Application Support/typora-user-images/截屏2022-03-11 07.55.02.png)]

11、TIME_WAIT状态为什么是2MSL的时长?为什么不是等待其他时长?

MSL,Maximum Segment Lifetime,最大报文段生存时间。即任何TCP报文在网络中存在的最大时长,如果超过这个时间,这个TCP报文就会被丢弃。2MSL,即两个最大报文段生存时间。
TIME_WAIT状态为什么是2MSL的时长?因为客户端不知道服务端是否能收到ACK应答数据包,服务端如果没有收到ACK,会进行重传FIN,考虑最坏的一种情况:

第四次挥手的ACK包的最大生存时长(MSL)+服务端重传的FIN包的最大生存时长(MSL)=2MSL

13、为什么TCP握手不能是两次呢?

​ 先假设是两次吧。我们知道, TCP的连接过程中有一个超时重传算法(karn算法是比较典型的), 如果client发出SYN包后, 由于网络原因, 没有立即收到ACK/SYN包, 那么client会再次发起SYN包。如果第二次SYN包正常达到且与server端建立了TCP连接, server端维护了一个连接, 一次貌似OK, 但别忘了, 第一次那个SYN包可能就在此时达到server端了, 于是server端又要维护一个连接, 而这个连接是无效的, 可以认为是死连接。 而一个进程打开的socket是有限度的, 维护这些死连接非常耗费资源。所以, 二次握手, 服务端有较大隐患, 容易因为资源耗尽而崩溃

14、描述线程、进程以及协程的区别?

什么是协程

**协程,英文Coroutines,是一种比线程更加轻量级的存在。**正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程

15、网络IO模型有哪些?

5种网络I/O模型,阻塞、非阻塞、I/O多路复用、信号驱动IO、异步I/O。从数据从I/O设备到内核态,内核态到进程用户态分别描述这5种的区别。

16、问题: HTTP协议的请求报文和响应报文格式

要非常清楚请求报文和响应报文的组成部分,要求在写具体案例。

17、浏览器中输入域名后发生了什么,整个流程。

整体流程:

1. DNS域名解析

2. 建立TCP连接

3. 发送HTTP请求

4. 服务器处理请求

5. 返回响应结果

6. 关闭TCP连接

7. 浏览器解析HTML

8. 浏览器布局渲染

使用的协议:

DNS、TCP、IP、OSPF(IP数据包在路由器中,路由选择协议)、ARP、HTTP

详细:

1. DNS域名解析

在浏览器输入网址,其实就是要向服务器请求我们想要的页面内容,所以浏览器首先要确认的是域名所对应的服务器在哪里。

将域名解析成对应的服务器IP地址这项工作,是由DNS服务器来完成的。

客户端收到域名地址后,首先去找本地的hosts文件,检查在该文件中是否有相应的域名、IP对应关系,如果有,则向其IP地址发送 请求,如果没有,再去找DNS服务器。

2. 建立TCP连接

三次握手:请求连接(SYN数据包),确认信息(SYN/ACK数据包),握手结束(ACK数据包)

3. 发起http请求

与服务器建立了连接后,就可以向服务器发起请求了。

4. 服务器处理请求

服务器端收到请求后的由web服务器(准确说应该是http服务器)处理请求。

web服务器解析用户请求,知道了需要调度哪些资源文件,再通过相应的这些资源文件处理用户请求和参数,并调用数据库信息,最 后将结果通过web服务器返回给浏览器客户端。

5. 返回响应结果

在http里,有请求就会有响应,哪怕是错误信息。

在响应结果中都会有个一个http状态码,如200、301、404、500等。通过这个状态码可以知道服务器端的处理是否正常,并能了解 具体的错误。

6. 关闭TCP连接

为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。四次挥手。

7. 浏览器解析HTML

浏览器需要加载解析的不仅仅是HTML,还包括CSS、JS。以及还要加载图片、视频等其他媒体资源。

浏览器通过解析HTML,生成DOM树,解析CSS,生成CSS规则树,然后通过DOM树和CSS规则树生成渲染树。

渲染树与DOM树不同,渲染树中并没有head、display为none等不必显示的节点。

8. 浏览器布局渲染

根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。

HTML默认是流式布局的,CSS和js会打破这种布局,改变DOM的外观样式以及大小和位置。

18、SSL握手的过程

  1. 客户端发送请求 Client Hello 向服务端传输自己支持的加密套件、SSL版本信息和一个随机生成数Random1,
  2. 服务端发送响应 Server Hello 从客户端的套件中确认一个加密方案,然后生成一个随机数Random2,将加密方案和随机数Random2返回给客户端。
  3. 服务端再发送自己的证书和公钥给客户端,
  4. 客户端接收到证书后,验证该证书的合法性,如果验证通过会取出证书中的服务端公钥,并且生成一个随机数Random3,通过公钥加密后返回给服务端。
  5. 服务端用自己的私钥解密获得Random3,此时服务器和客户端都有了三个随机数,根据Random1+Random2+Random3,还有前面确定的加密方案生成一个密钥。
  6. 后续的传输就使用该密钥进行对称加密就可以了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qKQTkmOg-1647795627104)(/Users/hello/Desktop/面试/image/20160908134036615.png)]

19、http常见的状态码

状态码的类别:

类别	                原因短语
1XX	       Informational(信息性状态码)	             接受的请求正在处理
2XX	       Success(成功状态码)	                      请求正常处理完毕
3XX	       Redirection(重定向状态码)	               需要进行附加操作以完成请求
4XX	       Client Error(客户端错误状态码)	            服务器无法处理请求
5XX	       Server Error(服务器错误状态码)	            服务器处理请求出错
2开头 (请求成功)

表示成功处理了请求的状态代码

200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
201 (已创建) 请求成功并且服务器创建了新的资源。
202 (已接受) 服务器已接受请求,但尚未处理。
203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。
204 (无内容) 服务器成功处理了请求,但没有返回任何内容。
205 (重置内容) 服务器成功处理了请求,但没有返回任何内容。
206 (部分内容) 服务器成功处理了部分 GET 请求。

3开头 (请求被重定向)

表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。

300 (多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。
307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

4开头 (请求错误)

这些状态代码表示请求可能出错,妨碍了服务器的处理。

400 (错误请求) 服务器不理解请求的语法。
401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
403 (禁止) 服务器拒绝请求。
404 (未找到) 服务器找不到请求的网页。
405 (方法禁用) 禁用请求中指定的方法。
406 (不接受) 无法使用请求的内容特性响应请求的网页。
407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
408 (请求超时) 服务器等候请求时发生超时。
409 (冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。
410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。
411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。
412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。
413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。
415 (不支持的媒体类型) 请求的格式不受请求页面的支持。
416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。
417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求。

5开头(服务器错误)

这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

500 (服务器内部错误) 服务器遇到错误,无法完成请求。
501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。
502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
503 **(服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
504 (**网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。
505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本

面试常考:

200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
404 (未找到) 服务器找不到请求的网页。
500 (服务器内部错误) 服务器遇到错误,无法完成请求。
504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。

20、协议原理以及流程

ARP

​ 以太网设备比如网卡都有自己唯一的MAC地址,它们是以MAC地址传输以太网数据包的,但是它们却识别不了IP包中的IP地址,所以我们在以太网中进行IP通信的时候就需要一个协议来建立IP地址与MAC地址的对应关系,以使数据包能发到一个确定的地方去,这就是ARP(地址解析协议)所以它的作用为:

  • ARP协议建立了主机 IP地址 和 MAC地址 的映射关系。
    在网络通讯时,源主机的应用程序知道目的主机的IP地址和端口号,却不知道目的主机的硬件地址

  • 主机会发送广播请求ff-ff-ff-ff,其他主机不会响应,目的主机收到后会携带自己的mac地址单播回复,收到后保存在ARP缓存中

DNS

DNS协议则是用来将域名转换为IP地址(也可以将IP地址转换为相应的域名地址)。

DNS系统

  • 一个组织的系统管理机构, 维护系统内的每个主机的IP和主机名的对应关系
    如果新计算机接入网络,将这个信息注册到数据库中,用户输入域名的时候,会自动查询DNS服务器,由DNS服务器检索数据库,得到对应的IP地址。

DNS理论知识
一、DNS域名结构
1、域名的层次结构
域名系统必须要保持唯一性。
为了达到唯一性的目的,因特网在命名的时候采用了层次结构的命名方法:

  1. 每一个域名(本文只讨论英文域名)都是一个标号序列(labels),用字母(A-Z,a-z,大小写等价)、数字(0-9)和连接符(-)组成
  2. 标号序列总长度不能超过255个字符,它由点号分割成一个个的标号(label)
  3. 每个标号应该在63个字符之内,每个标号都可以看成一个层次的域名。
  4. 级别最低的域名写在左边,级别最高的域名写在右边。
    域名服务主要是基于UDP实现的,服务器的端口号为53

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Wx3VAHZ-1647795627104)(/Users/hello/Desktop/面试/image/截屏2022-03-14 15.43.33.png)]

eg :我们熟悉的,www.baidu.com

  1. com: 一级域名. 表示这是一个企业域名。同级的还有 “net”(网络提供商), “org”(⾮非盈利组织) 等。
  2. baidu: 二级域名,指公司名。
  3. www: 只是一种习惯用法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K5y7dviJ-1647795627104)(/Users/hello/Desktop/面试/image/截屏2022-03-14 15.45.54.png)]

域名解析过程

域名解析总体可分为一下过程:
(1) 输入域名后, 先查找自己主机对应的域名服务器,域名服务器先查找自己的数据库中的数据.
(2) 如果没有, 就向上级域名服务器进行查找, 依次类推
(3) 最多回溯到根域名服务器, 肯定能找到这个域名的IP地址
(4) 域名服务器自身也会进行一些缓存, 把曾经访问过的域名和对应的IP地址缓存起来, 可以加速查找过程
具体可描述如下:

  1. 主机先向本地域名服务器进行递归查询
  2. 本地域名服务器采用迭代查询,向一个根域名服务器进行查询
  3. 根域名服务器告诉本地域名服务器,下一次应该查询的顶级域名服务器的IP地址
  4. 本地域名服务器向顶级域名服务器进行查询
  5. 顶级域名服务器告诉本地域名服务器,下一步查询权限服务器的IP地址
  6. 本地域名服务器向权限服务器进行查询
  7. 权限服务器告诉本地域名服务器所查询的主机的IP地址
  8. 本地域名服务器最后把查询结果告诉主机

如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pCOXRfzu-1647795627104)(/Users/hello/Desktop/面试/image/截屏2022-03-14 15.49.25.png)]

上文我们提出了两个概念:递归查询和迭代查询
(1)递归查询:
  • 本机向本地域名服务器发出一次查询请求,就静待最终的结果。如果本地域名服务器无法解析,自己会以DNS客户机的身份向其它域名服务器查询,直到得到最终的IP地址告诉本机
(2)迭代查询:
  • 本地域名服务器向根域名服务器查询,根域名服务器告诉它下一步到哪里去查询,然后它再去查,每次它都是以客户机的身份去各个服务器查询。

通俗地说,递归就是把一件事情交给别人,如果事情没有办完,哪怕已经办了很多,都不要把结果告诉我,我要的是你的最终结果,而不是中间结果;如果你没办完,请你找别人办完。
迭代则是我交给你一件事,你能办多少就告诉我你办了多少,然后剩下的事情就由我来办。

SMTP和POP

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-twqdDNBi-1647795627115)(/Users/hello/Desktop/面试/image/截屏2022-03-14 15.53.17.png)]

ICMP

ICMP协议是一个网络层协议。
一个新搭建好的网络,往往需要先进行一个简单的测试,来验证网络是否畅通;但是IP协议并不提供可靠传输。如果丢包了,IP协议并不能通知传输层是否丢包以及丢包的原因。所以我们就需要一种协议来完成这样的功能–ICMP协议。

ICMP协议的功能主要有:

  • 确认IP包是否成功到达目标地址
  • 通知在发送过程中IP包被丢弃的原因

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DtewX8NA-1647795627115)(/Users/hello/Desktop/面试/image/截屏2022-03-14 16.05.01.png)]

常见的ICMP报文
相应请求

我们用的ping操作中就包括了相应请求(类型字段值为8)和应答(类型字段值为0)ICMP报文。
过程:
一台主机向一个节点发送一个类型字段值为8的ICMP报文,如果途中没有异常(如果没有被路由丢弃,目标不回应ICMP或者传输失败),则目标返回类型字段值为0的ICMP报文,说明这台主机存在。

目标不可达,源抑制和超时报文

这三种报文的格式是一样的。

  • (1)目标不可到达报文(类型值为3)在路由器或者主机不能传递数据时使用。
    例如:我们要连接对方一个不存在的系统端口(端口号小于1024)时,将返回类型字段值3、代码字段值为3的ICMP报文。
    常见的不可到达类型还有网络不可到达(代码字段值为0)、主机不可达到(代码字段值为1)、协议不可到达(代码字段值为2)等等。
  • (2)源抑制报文(类型字段值为4,代码字段值为0)则充当一个控制流量的角色,通知主机减少数据报流量。由于ICMP没有回复传输的报文,所以只要停止该报文,主机就会逐渐恢复传输速率。
  • (3)无连接方式网络的问题就是数据报会丢失,或者长时间在网络游荡而找不到目标,或者拥塞导致主机在规定的时间内无法重组数据报分段,这时就要触发ICMP超时报文的产生。
    超时报文(类型字段值为11)的代码域有两种取值:代码字段值为0表示传输超时,代码字段值为1表示分段重组超时。
时间戳请求

时间戳请求报文(类型值字段13)和时间戳应答报文(类型值字段14)用于测试两台主机之间数据报来回一次的传输时间。传输时,主机填充原始时间戳,接受方收到请求后填充接受时间戳后以类型值字段14的报文格式返回,发送方计算这个时间差。

ping命令的功能
  • (1)能验证网络的连通性
  • (2)会统计响应时间和TTL(IP包中的Time To Live,生存周期)

那么如何验证的呢?

  • (1)ping命令会先发送一个 ICMP Echo Request给对端
  • (2)对端接收到之后, 会返回一个ICMP Echo Reply
  • (3)若没有返回,就是超时了,会认为指定的网络地址不存在。

问题:
telnet是23端口,ssh是22端口,那么ping是什么端口?
答:ping命令是基于ICMP,是在网络层。
而端口号,是传输层的内容。所以在ICMP中根本就不关注端口号这样的信息。

traceroute
  • traceroute也是基于ICMP协议实现的。
    功能:
    打印出可执行程序主机,一直到目标主机之前经历多少路由器。
    举例如下:
IGP(内部网关协议)
  • IGP(内部网关协议)是在一个自治网络内网关(主机和路由器)间交换路由信息的协议。路由信息能用于网间协议(IP)或者其它网络协议来说明路由传送是如何进行的。IGP协议包括RIP、OSPF。
域(自治系统)

Internet网被分成多个域或多个自治系统。一个域(domain)是一组主机和使用相同路由选择协议的路由器集合,并由单一机构管理。换言之,一个域可能是由一所大学或其它机构管理的互联网。

内部网关协议

​ 内部网关协议(IGP)在一个域中选择路由。

外部网关协议

​ 外部网关协议(EGP)为两个相邻的位于各自域边界上的路由器提供一种交换消息和信息的方法。BGP是常用的外部网关协议,是不同 自治系统的路由器之间交换路由信息的协议。

内部网关协议分类
1、距离矢量路由协议RIP

​ RIP (Routing Information Protocol),为最早出现的距离向量路由协议。属于网络层,其主要应用规模较小,可靠性要求较低的网络,可以通过不断的交换信息让路由器动态的适应网络连接的变化,这些信息包括每个路由器可以到达那些网络,这些网络有多远等。
RIP协议要求网络中每一个路由器都维护从它自己到其他每一个目的网络的唯一最佳距离记录,如:

​ 通常为“跳数”,即从源端口到目的的端口所经过的路由器个数,经过一个路由器跳数+1,。特别的,从一路由器到直接连接的网络距离为1.RIP允许一条路由最多智能包含15个路由器,因此距离为16表示网络不可到达。

RIP如何交换信息?
仅仅只和相邻路由器交换信息RIP协议好消息传得快,坏消息传的慢:
当网络出现故障时,要经过较长的时间才能将此信息传送到所有的路由器,“慢收敛”。
当出现坏消息时,比如网1出现了故障:

这个时候R1是知道是无法到达的,则其到网1的距离为16,并且为直接交付。
但是R2在收到R1报文之前,即在R2并不知道R1出故障时,发送了原来的报文,1 2 R1。
于是,R1收到R2跟新报文后,误以为可以经过R2到网1,于是更新自己的路由表,1 3 R2,并且将次更新信息发送给R2.

2、链路状态路由协议

配置了链路状态路由协议的路由器可以获取所有其它路由器的信息来创建网络的“完整视图”(即拓扑结构)。并在拓扑结构中选择到达所有目的网络的最佳路径(链路状态路由协议是触发更新,就是说有变化时就更新)。
链路状态协议适用于以下情形:
(1)网络进行了分层设计,大型网络通常如此。
(2)管理员对于网络中采用的链路状态路由协议非常熟悉。
(3)网络对收敛速度的要求极高。

OSPF(开放最短路径优先协议)

“最短路径优先”是指OSPF使用Dijkstra最短路径算法,是IGP协议。

OSPF用于单一系统内决策路由。与RIP相比,OSPF是链路状态路由协议,而RIP是距离向量路由协议。链路是路由器接口的另一个说法,即OSPF也称为接口状态路由协议。

OSPF通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,每个OSPF路由器使用这些最短路径构造路由。

与大多数路由协议不同(参考BGP和RIP的工作过程),本协议不依赖于传输层协议(如TCP、UDP)提供数据传输、错误检测与恢复服务,数据包直接封装在网际协议(协议号89)内传输。

21、http1.x 和http2.0的区别

二进制分帧(Binary Framing)

HTTP2.0和HTTP1.X都是基于TCP/IP进行通信的。但是HTTP2.0通过在传输层(TCP)之上增加了二进制分帧层,简单来说就是把之前HTTP1.X的Header和body分成一个或者多个二进制帧进行传输,这样做有什么好处呢?首先HTTP1.X要识别首行、头部和body这3部分就要做协议解析,解析是基于文本,而文本的形式有多样性,解析考的场景多,而采用二进制只有0和1的组合,解析方便。而且多帧传输配合可以并发的多路复用提高性能。

多路复用(MultiPlexing)

几个前提概念

  • 流(stream):已建立连接上的双向字节流。
  • 消息:与逻辑消息对应的完整的一系列数据帧。
  • 帧(frame):HTTP2.0通信的最小单位,每个帧包含帧头部,至少也会标识出当前帧所属的流(stream id)。

多路复用也叫连接共享。多路复用实际上就是多个HTTP请求复用一个TCP链接通道,在这个通道里面传输以帧为单位的流数据,这些流数据是可以并发的而且数量不限。实际上就是一个TCP通道里面实现多个HTTP并发请求。

多路复用也有可能会导致关键请求阻塞。通过通过设置HTTP数据流的优先级,提高关键请求的优先级,优先级高会被服务器优先处理和给客户端发送响应。

这种单连接多资源的方式,减少服务端的链接压力,内存占用更少,连接吞吐量更大;而且由于 TCP 连接的减少而使网络拥塞状况得以改善,同时慢启动时间的减少,使拥塞和丢包恢复速度更快。

问题:HTTP2.0的多路复用和HTTP1.1的长链接有什么不同?
长连接:同一个域名访问同一个文件的多个请求都可以复用一个tcp连接(不用像1.0一样 每次请求都需要重新建立连接)
**依然存在的问题:**1.多个请求只能被串行处理(数据基于文本,只能按顺序传输);2.访问多个不同的文件依然会建立多个请求。
**多路复用:**同一个域名访问多个文件的请求也可以复用一个tcp连接,且多个请求可以被并行处理。
**并行实现原理:**http2.0引入二进制数据帧和流的概念(数据帧对每一个数据进行标识,可以不按顺序传输,从而实现并行)

Header压缩

HTTP2.0采用Hpack算法对请求的Header进行压缩,服务器和客户端双方各自维护一份Header表,这个表有静态表和动态表组成.静态表由一个预定义的头部字段静态列表组成。 动态表格由按先入先出顺序维护的头部字段列表组成。 动态表中的第一个和最新条目处于最低索引处,并且动态表的最旧条目处于最高索引处。

服务端推送

同SPDY一样,HTTP2.0也具有server push功能。服务器推送是在客户端之前发送数据的机制。在HTTP/2中,服务端可以对客户端的一个请求发送多个相应。

Server Push的功能前面已经提到过,http2.0能通过push的方式将客户端需要的内容预先推送过去,所以也叫“cache push”。另外有一点值得注意的是,客户端如果退出某个业务场景,出于流量或者其它因素需要取消server push,也可以通过发送RST_STREAM类型的frame来做到。

更安全的SSL

HTTP2.0使用了TLS的拓展ALPN来做协议升级,除此之外加密这块还有一个改动,HTTP2.0对tls的安全性做了近一步加强,通过黑名单机制禁用了几百种不再安全的加密算法,一些加密算法可能还在被继续使用。如果在ssl协商过程当中,客户端和server的cipher suite没有交集,直接就会导致协商失败,从而请求失败。在server端部署http2.0的时候要特别注意这一点。

重置连接表现更好

很多app客户端都有取消图片下载的功能场景,对于http1.x来说,是通过设置tcp segment里的reset flag来通知对端关闭连接的。这种方式会直接断开连接,下次再发请求就必须重新建立连接。http2.0引入RST_STREAM类型的frame,可以在不断开连接的前提下取消某个request的stream,表现更好。

链接:https://www.jianshu.com/p/82d66f4795e5

22、HTTP协议详解之URL篇

HTTP(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。

host表示合法的Internet主机域名或者IP地址;

port指定一个端口号,为空则使用缺省端口80;

abs_path指定请求资源的URI;如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出

二、HTTP协议详解之请求篇

http请求由三部分组成,分别是:请求行、消息报头、请求正文

请求方法(所有方法全为大写)有多种,各个方法的解释如下:GET 请求获取Request-URI所标识的资源POST 在Request-URI所标识的资源后附加新的数据HEAD 请求获取由Request-URI所标识的资源的响应消息报头PUT 请求服务器存储一个资源,并用Request-URI作为其标识DELETE 请求服务器删除Request-URI所标识的资源TRACE 请求服务器回送收到的请求信息,主要用于测试或诊断CONNECT 保留将来使用OPTIONS 请求查询服务器的性能,或者查询与资源相关的选项和需求

2、请求报头后述

3、请求正文

三、HTTP协议详解之响应篇(深信服考了这道题5种状态码表示的意思)

HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文

1、状态行格式如下:HTTP-Version Status-Code Reason-Phrase CRLF其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码;Reason-Phrase表示状态代码的文本描述。状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:1xx:指示信息–表示请求已接收,继续处理2xx:成功–表示请求已被成功接收、理解、接受3xx:重定向–要完成请求必须进行更进一步的操作4xx:客户端错误–请求有语法错误或请求无法实现5xx:服务器端错误–服务器未能实现合法的请求常见状态代码、状态描述、说明:200 OK //客户端请求成功400 Bad Request //客户端请求有语法错误,不能被服务器所理解401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 403 Forbidden //服务器收到请求,但是拒绝提供服务404 Not Found //请求资源不存在,eg:输入了错误的URL500 Internal Server Error //服务器发生不可预期的错误503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常eg:HTTP/1.1 200 OK (CRLF)

2、响应报头后述

3、响应正文就是服务器返回的资源的内容

套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

应用层协议分类,深信服考了这道题,不属于应用层协议的

(1)域名系统(Domain Name System,DNS):用于实现网络设备名字到IP地址映射的网络服务。

(2)文件传输协议(File Transfer Protocol,FTP):用于实现交互式文件传输功能。

(3)简单邮件传送协议(Simple Mail Transfer Protocol, SMTP):用于实现电子邮箱传送功能

(4)超文本传输协议(HyperText Transfer Protocol,HTTP):用于实现WWW服务。

(5)简单网络管理协议(simple Network Management Protocol,SNMP):用于管理与监视网络设备。

(6)远程登录协议(Telnet):用于实现远程登录功能

以上都属于应用层协议

网际层协议:IP协议、ICMP协议、ARP协议、RARP协议。

传输层协议:TCP协议、UDP协议

大题四、对于我们的网站向已注册用户提供某些专门的服务,比如网上购物、在线下载、收费浏览等等,就会要求用户在使用这些服务之前进入登录页面,输入用户名和密码,并进行验证。提出两种方法实现自动登录:深信服考了这道大题

实现自动完成登陆基本思路:当用户第一次登录网站的时候,网站向客户端发送一个包含有用户名的Cookie。当用户在之后的某个时候再次访问,浏览器就会向网站服务器回送这个Cookie,于是,我们可以从这个Cookie中读取到用户名,然后调用登录的方法,从而实现自动为用户登录。

第一种方法: 可以把SessionId(GUID)放到cookies中,但这样为了让用户下次访问我们网站时,知道这 个sessionId对应的是哪一个用户,我们还要在数据库中建张表。 表字段: 主键,UserId SessionId 时间 缺点:不能在两台机器上同时保存

第二种方法: 把UserId放cookies中 密码(加密) 相对于第一种方法优点:多台机器可以保存 缺点:不安全,密码放到了客户端。

熟悉Linux/unix操作系统基础原理,熟练使用Linux系统;

6.算法题:

3.Redis有哪些数据结构,底层实现了解过吗?(有看过博客,没有系统学习,时间久远已忘却。。)

6.读已提交和可重复读底层实现的区别?(没答好)

8.MySQL的索引?二级索引?(经典八股文)

3.MongoDB和MySQL的区别?MongoDB为什么读写快?(非关系型数据库,表结构松散,但也会带来编程难题。具体读写快肯定是设计架构上有精巧之处,具体的还没有深入系统学习过)
结论

  1. 相比较MySQL,MongoDB数据库更适合那些读作业较重的任务模型。MongoDB能充分利用机器的内存资源。如果机器的内存资源丰富的话,MongoDB的查询效率会快很多。

  2. 在带”_id”插入数据的时候,MongoDB的插入效率其实并不高。如果想充分利用MongoDB性能的话,推荐采取不带”_id”的插入方式,然后对相关字段作索引来查询。

  3. MongoDB适合那些对数据库具体数据格式不明确或者数据库数据格式经常变化的需求模型,而且对开发者十分友好。

  4. MongoDB官方就自带一个分布式文件系统,可以很方便地部署到服务器机群上。MongoDB里有一个Shard的概念,就是方便为了服务器分片使用的。每增加一台Shard,MongoDB的插入性能也会以接近倍数的方式增长,磁盘容量也很可以很方便地扩充。

  5. MongoDB还自带了对map-reduce运算框架的支持,这也很方便进行数据的统计。

MongoDB的缺陷

  1. 事务关系支持薄弱。这也是所有NoSQL数据库共同的缺陷,不过NoSQL并不是为了事务关系而设计的,具体应用还是很需求。
  2. 稳定性有些欠缺,这点从上面的测试便可以看出。
  3. MongoDB一方面在方便开发者的同时,另一方面对运维人员却提出了相当多的要求。业界并没有成熟的MongoDB运维经验,MongoDB中数据的存放格式也很随意,等等问题都对运维人员的考验。
  4. 4.Redis和MongoDB与MySQL的区别(内存数据库,避免了磁盘IO,读写快)

===============================================

操作系统

1、进程

什么是进程(Process)和线程(Thread)?有何区别?

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

进程与应用程序的区别在于应用程序作为一个静态文件存储在计算机系统的硬盘等存储空间中,而进程则是处于动态条件下由操作系统维护的系统资源管理实体。

父子进程之间的关系:

进程间的通信方式

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

  • 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

  • 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

  • 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

  • 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

2、死锁(deadlock)

  • 两个进程独占性的访问某个资源,从而等待另外一个资源的执行结果,会导致两个进程都被阻塞,并且两个进程都不会释放各自的资源,这种情况就是 死锁(deadlock)。

  • 死锁与不可抢占资源有关

  • 可抢占式资源和不可抢占资源
    可抢占资源可以从拥有它的进程中抢占而不会造成其他影响,内存就是一种可抢占性资源,任何进程都能够抢先获得内存的使用权。
    不可抢占资源指的是除非引起错误或者异常,否则进程无法抢占指定资源,这种不可抢占的资源比如光盘,在进程执行调度的过程中,其他进程是不能得到该资源的。

2.1、什么是死锁?

​ 在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。

2.2、死锁产生的四个条件

(有一个条件不成立,则不会产生死锁)

  • 互斥条件:一个资源一次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
  • 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系
2.3、死锁的处理策略—预防死锁、避免死锁、检测和解除死锁
一、死锁的处理策略——预防死锁
(一)破坏互斥条件

互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁。
如果
把只能互斥使用的资源改造为允许共享使用
,则系统不会进入死锁状态。比如: SPOOLing技术。操作系统可以采用 SPOOLing 技术把独占设备在逻辑上改造成共享设备。比如,用SPOOLing技术将打印机改造为共享设备…

SPOOLing技术:

​ **该策略的缺点:**并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。

(二)破坏不剥夺条件

​ 不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
破坏不剥夺条件:
​ ①、方案一:当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件。
​ ②、方案二:当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)
该策略的缺点:
​ ①、实现起来比较复杂。
​ ②、释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。
​ ③、反复地申请和释放资源会增加系统开销,降低系统吞吐量。
​ ④、若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿。

(三)破坏请求和保持条件
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。
    可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。
    缺点:
    有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。另外,该策略也有可能导致某些进程饥饿。
(四)破坏循环等待条件

​ 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

  • 可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。
    原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按

    该策略的缺点:
    ①、不方便增加新的设备,因为可能需要重新分配所有的编号;
    ②、进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费;
    ③、必须按规定次序申请资源,用户编程麻烦。

二、死锁的处理策略——避免死锁
(一)什么是安全序列、安全序列、不安全状态、死锁的联系
  • 所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个。
    如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。
  • 当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。【比如A 先归还了10亿,那么就有安全序列T→B → A】
  • 如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
    因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法”的核心思想。
(二)银行家算法 --避免死锁
  • 银行家算法是荷兰学者 Dijkstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁。 核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
  1. 实现步骤

    分配+回收已经分配的

以此类推,共五次循环检查即可将5个进程都加入安全序列中,最终可得一个安全序列。该算法称为安全性算法。可以很方便地用代码实现以上流程,每一轮检查都从编号较小的进程开始检查。实际做题时可以更快速的得到安全序列。

​ 安全性算法步骤:

  • ①、检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收。
  • ②、不断重复上述过程,看最终是否能让所有进程都加入安全序列。
    系统处于不安全状态未必死锁,但死锁时一定处于不安全状态。系统处于安全状态一定不会死锁。
三、死锁的处理策略——检测和解除

如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法:
①死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。
②死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。

(一)死锁的检测

​ 为了能对系统是否已发生了死锁进行检测,必须:
​ ①用某种数据结构来保存资源的请求和分配信息;
​ ②提供一种算法,利用上述信息来检测系统是否已进入死锁状态。

有向边图消去

(二)死锁的解除

一旦检测出死锁的发生,就应该立即解除死锁。
补充:并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程
解除死锁的主要方法有:

  • ①、 资源剥夺法 。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。
  • ②、 撤销进程法(或称终止进程法) 。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。
  • ③、 进程回退法 。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。

3、说说分段和分页

  • 页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提高内存的利用率;或者说,分页仅仅是由于系统管理的需要,而不是用户的需要。

  • 段是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了能更好的满足用户的需要。

页的大小固定且由系统确定,把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的,因而一个系统只能有一种大小的页面。段的长度却不固定,决定于用户所编写的程序,通常由编辑程序在对源程序进行编辑时,根据信息的性质来划分。

分页的作业地址空间是一维的,即单一的线性空间,程序员只须利用一个记忆符,即可表示一地址。分段的作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。

4、Linux中常用到的命令

  显示文件目录命令ls    如ls

  改变当前目录命令cd    如cd /home

  建立子目录mkdir      如mkdir xiong

  删除子目录命令rmdir    如rmdir /mnt/cdrom

  删除文件命令rm      如rm /ucdos.bat

  文件复制命令cp      如cp /ucdos /fox

  获取帮助信息命令man   如man ls

  显示文件的内容less    如less mwm.lx

  重定向与管道typetype readme>>direct,将文件readme的内容追加到文direct中

5、线程同步的方式有哪些?

  • 互斥量:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
  • 信号量:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  • 事件(信号):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

6、操作系统中进程调度策略有哪几种?

6.1、先来先服务和短作业(进程)优先调度算法
6.1.1、先来先服务调度算法
  1. 先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。
  2. 在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。
6.1.2、短作业(进程)优先调度算法
  1. 短作业(进程)优先调度算法SJ§F,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。
  2. 而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。
6.2、高优先权优先调度算法
6.2.1、优先权调度算法的类型

​ 为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。

1) 非抢占式优先权算法

​ 在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。

2) 抢占式优先权调度算法

​ 在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。

6.2.2、高响应比优先调度算法

​ 在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为:

由于等待时间与服务时间之和就是系统对该作业的响应时间,故该优先权又相当于响应比RP。据此,又可表示为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2EjJ6eu-1647795627116)(/Users/hello/Desktop/面试/image/截屏2022-03-14 20.33.08的副本.png)]由上式可以看出:

(1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。

(2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。

(3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

6.3、基于时间片的轮转调度算法
6.3.1、时间片轮转法
  1. 基本原理

​ 系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;

​ 然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。

6.3.2、多级反馈队列调度算法

​ 前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。

  • (1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。
  • (2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去
  • (3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。

7、操作系统——内存分配管理

7.1、逻辑地址空间与物理地址空间

物理地址空间

  • 物理地址空间是指内存中物理单元的集合,它是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址从主存中存取。

逻辑地址空间

  • 编译后,每个目标模块都是从0号单元开始编址,称为该目标模块的相对地址(或逻辑地址)。当链接程序将各个模块链接成一个完整的可执行目标程序时,链接程序顺序依次按各个模块的相对地址构成统一的从0号单元开始编址的逻辑地址空间。用户程序和程序员只需知道逻辑地址,而内存管理的具体机制则是完全透明的,它们只有系统编程人员才会涉及。不同进程可以有相同的逻辑地址,因为这些相同的逻辑地址可以映射到主存的不同位置。
7.2、内存保护

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vsAVd86-1647795627116)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.20.31.png)]

逻辑地址如果超出界地址会直接错误,再经过重定位寄存器可得到物理地址。

7.3、内存非连续分配管理方式
7.3.1 基本分页存储管理方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vXOu1Var-1647795627116)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.22.04.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B548SUvb-1647795627116)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.22.58.png)]

逻辑地址记录的是页号和页内偏移量,通过页表可以查到物理地址的块号。

快慢表

为了加快查询的速度,会引入块表的概念,但是比慢表要小,所以当块表没有查询到相应的块号的时候,会再去查询慢表。

多级页查询

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HS2I9wS6-1647795627117)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.23.23.png)]

7.3.1 基本分段存储管理方式

分段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SjTows5S-1647795627117)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.24.02.png)]

分段中的逻辑地址结构

段表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUxwqiwo-1647795627117)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.24.12.png)]

与页表类似,根据段号找到段长和基址,如上图:8kb = 8*1024 再加上偏移量就是 8292.

段页式管理方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Ij1OfgZ-1647795627118)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.25.03.png)]

相对来说比页表,段表都要复杂。

逻辑地址结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dVmWc5wl-1647795627118)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.24.50.png)]

7.4、虚拟内存

虚拟内存基于局部性原理,会在加载程序的时候会将一部分程序加载进内存,当需要另外部分的时候再去外存中加载,同时会将暂时不使用的内存换到外存去,从而腾出空间存放将要调入内存的信息。这样,系统好像为用户提供了一个比实际内存大得多的存储器,称为虚拟存储器。

虚拟内存技术的实现
虚拟内存的实现有以下三种方式:

  • 请求分页存储管理。
  • 请求分段存储管理。
  • 请求段页式存储管理。

页表机制

​ 请求分页系统的页表机制不同于基本分页系统,请求分页系统在一个作业运行之前不要求全部一次性调入内存,因此在作业的运行过程中,必然会出现要访问的页面不在内存的情况,如何发现和处理这种情况是请求分页系统必须解决的两个基本问题。为此,在请求页表项中增加了四个字段。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JuJUjJjj-1647795627118)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.25.20.png)]

加的四个字段说明如下:

  • 状态位P:用于指示该页是否已调入内存,供程序访问时参考。
  • 访问字段A:用于记录本页在一段时间内被访问的次数,或记录本页最近己有多长时间未被访问,供置换算法换出页面时参考。
  • 修改位M:标识该页在调入内存后是否被修改过。
  • 外存地址:用于指出该页在外存上的地址,通常是物理块号,供调入该页时参考。

缺页中断机构
在请求分页系统中,每当所要访问的页面不在内存时,便产生一个缺页中断,请求操作系统将所缺的页调入内存。此时应将缺页的进程阻塞(调页完成唤醒),如果内存中有空闲块,则分配一个块,将要调入的页装入该块,并修改页表中相应页表项,若此时内存中没有空闲块,则要淘汰某页(若被淘汰页在内存期间被修改过,则要将其写回外存)。

​ 缺页中断作为中断同样要经历,诸如保护CPU环境、分析中断原因、转入缺页中断处理程序、恢复CPU环境等几个步骤。但与一般的中断相比,它有以下两个明显的区别:

  • 在指令执行期间产生和处理中断信号,而非一条指令执行完后,属于内部中断。

  • 一条指令在执行期间,可能产生多次缺页中断。

地址变换机构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Cm3EvZp-1647795627118)(/Users/hello/Desktop/面试/image/截屏2022-03-17 22.25.45.png)]

7.5、页面置换算法
7.5.1、最佳置换算法(OPT)

​ 最佳算法需要知道后面需要什么页,当出现缺页的时候,会对后面进行遍历,将最后出现的页给替换掉。

最佳置换算法可以用来评价其他算法。假定系统为某进程分配了三个物理块,并考虑有以下页面号引用串:
7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1

进程运行时,先将7, 0, 1三个页面依次装入内存。进程要访问页面2时,产生缺页中断,根据最佳置换算法,选择第18次访问才需调入的页面7予以淘汰。然后,访问页面0时,因为已在内存中所以不必产生缺页中断。访问页面3时又会根据最佳置换算法将页面1淘汰……依此类推。从图中可以看出釆用最佳置换算法时的情况。

7.5.2、先进先出(FIFO)页面置换算法

按照上面的数据,再来一遍(先进先出)。

7.5.3、最近最久未使用(LRU)置换算法
7.5.4、时钟(CLOCK)置换算法

​ LRU算法的性能接近于OPT,但是实现起来比较困难,且开销大;FIFO算法实现简单,但性能差。所以操作系统的设计者尝试了很多算法,试图用比较小的开销接近LRU的性能,这类算法都是CLOCK算法的变体。

​ 简单的CLOCK算法是给每一帧关联一个附加位,称为使用位。当某一页首次装入主存时,该帧的使用位设置为1;当该页随后再被访问到时,它的使用位也被置为1。对于页替换算法,用于替换的候选帧集合看做一个循环缓冲区,并且有一个指针与之相关联。当某一页被替换时,该指针被设置成指向缓冲区中的下一帧。当需要替换一页时,操作系统扫描缓冲区,以查找使用位被置为0的一帧。每当遇到一个使用位为1的帧时,操作系统就将该位重新置为0;如果在这个过程开始时,缓冲区中所有帧的使用位均为0,则选择遇到的第一个帧替换;如果所有帧的使用位均为1,则指针在缓冲区中完整地循环一周,把所有使用位都置为0,并且停留在最初的位置上,替换该帧中的页。由于该算法循环地检查各页面的情况,故称为CLOCK算法,又称为最近未用(Not Recently Used, NRU)算法。

算法执行如下操作步骤:

  • 从指针的当前位置开始,扫描帧缓冲区。在这次扫描过程中,对使用位不做任何修改。选择遇到的第一个帧(u=0, m=0)用于替换。

  • 如果第1)步失败,则重新扫描,查找(u=0, m=1)的帧。选择遇到的第一个这样的帧用于替换。在这个扫描过程中,对每个跳过的帧,把它的使用位设置成0。

  • 如果第2)步失败,指针将回到它的最初位置,并且集合中所有帧的使用位均为0。重复第1步,并且如果有必要,重复第2步。这样将可以找到供替换的帧。
    https://blog.csdn.net/xmzyjr123/article/details/86585506

简历问题

===============================================

===============================================

Django项目描述:

项目是构建学习交流平台,用户能够分享学习资料以及学习视频,能够发布学习文章,以及交流评论功能。我主要职责是负责使用Django框架完成后端开发,完成的功能有短视频区,文本区,学习区,资源共享区, 搜索以及登录注册功能。

其中登录和注册功能,使用了redis和celery相关技术,celery

在Elasticsearch代替MYSQL模糊查询实现搜索功能

使用Redis主页面缓存提高用 户浏览速度,通过Django里面的修饰器实现

FDFS分布式文件系统储存网站资源,

通过ER图完成数据库的设计和建表,MYSQL相关

Django项目相关

1、什么是中间件?

中间件是介于request与response处理之间的一道处理过程,相对比较轻量级,并且在全局上改变django的输入与输出。中间件一般做认证或批量请求处理,django中的中间件,其实是一个类,在请求和结束后,django会根据自己的规则在合适的时机执行中间件中相应的方法。

如请求过来 执行process_request, view,process_response方法

2、Django、Tornado、Flask各自的优势

Django:Django无socket,django的目的是简便,快速开发,并遵循MVC设计,多个组件可以很方便的以“插件”形式服务于整个框架,django有许多功能强大的第三方插件。django具有很强的可扩展性。
Tornado:它是非阻塞式服务器,而且速度相当快,得力于其 非阻塞的方式和对epoll的运用,Future对象,缺点:没有session,需要自定制

Flask:是一个微型的web框架,配合SQLALchemy来使用,jinja2模板, werkzeug接口

Tornado开发的时候遇到跨域请求问题?

因为采用的是前后端分离的架构,前端是同学使用react写的,我负责后端,这就会发现前端拿不到数据,出现跨域的报错,解决方法:

  • 一种主流的跨域方案是CORS,他仅需要服务端在返回数据的时候在相应头中加入标识信息。这种方式非常简便。唯一的缺点是需要浏览器的支持,一些较老的浏览器可能不支持CORS特性。在返回响应头header中注入Access-Control-Allow-Origin,这样浏览器检测到header中的Access-Control-Allow-Origin,则就可以跨域操作了。
  • 利用Nginx的反向代理,在nginx的配置文件中配置重新定向到同一端口
3、什么是阻塞式和什么是非阻塞式?

阻塞模式与非阻塞模式

阻塞模式 程序碰到了一些耗时操作,无法继续向下走。

非阻塞模式 当程序碰到耗时操作,分发给别的线程,主线程继续执行。

同步与异步

同步和异步关注的是消息通信机制.
同步异步指的是调用者与被调用者两者之间的关系,而不是经常容易误解的多个被调用者之间的关系。

同步

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回,一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用结果.按照此定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他程序或者IO协作或者需要一定时间完成的任务。

#1. multiprocessing.Pool下的apply #发起同步调用后,
就在原地等着任务结束,根本不考虑任务是在计算还是在io阻塞,总之就是一股脑地等任务结束
#2. concurrent.futures.ProcessPoolExecutor().submit(func,).result()
#3. concurrent.futures.ThreadPoolExecutor().submit(func,).result()

简单一句话就是:调用者调用了一个功能时调用者要等着被调用者执行完毕,无论被调用者是在阻塞还是非阻塞状态,才能继续进行自己的任务

异步

异步的概念和同步相对。调用在发出之后,这个调用就直接返回了,所以没有返回结果.换句话说当一个异步功能调用发出后,调用者不能立刻得到结果。但是这个时候被调用者可以去执行下面的代码而不是一味的等待。
另外需要强调的一点:
当该异步功能完成后,被调用者可以通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

#1. multiprocessing.Pool().apply_async() #发起异步调用后,
并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,
可能是封装好的一个对象)。
#2. concurrent.futures.ProcessPoolExecutor(3).submit(func,)
#3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)

阻塞与非阻塞

阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。调用线程只有在得到结果之后才会返回。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。

1. 同步调用:apply一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,
但并未阻塞住(即便是被抢走cpu的执行权限,那也是处于就绪态);
2. 阻塞调用:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,
则当前线程就会被挂起,直到有数据为止。

非阻塞

非阻塞和阻塞的概念相对应,非阻塞调用指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。

总结

  1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活(非阻塞)状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行,当函数返回的时候通过状态、通知、事件等方式通知进程任务完成。
  2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程。
4、django怎么解决并发的

nginx+uwsgi为django提供高并发,nginx的并发能力强,在纯静态的web服务中更是突出其优越的地方,由于底层使用epoll异步IO模型进行处理。

5、什么是ORM?

ORM,即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间做一个映射,ORM优缺点:
优点:摆脱复杂的SQL操作,适应快速开发,让数据结果变得简单,数据库迁移成本更低
缺点:性能较差,不适用于大型应用,复杂的SQL操作还需要通过SQL语句实现

6、ngnix的正向代理与反向代理

答:正向代理 是一个位于客户端和原始服务器(originserver) 之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理。
反向代理正好相反,对于客户端而言它就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容原本就是它自己的一样。

Django框架实现的基本原理

1.简述django对http请求的执行流程
  • 在接收一个http请求之前,启动uWsgi服务器的WSGI协议监听端口等待来自外界的http请求。比如Django自带的开发者服务器或者uWSGI服务器,Django服务器根据WSGI协议指定相应的handler来处理http请求。这个时候服务器已处于监听状态可以接受外界的http请求。

  • Django服务器根据WSGI协议从http请求中获取参数组成字典并且传输到handler中处理,在handler中对已经符合wsgi规范标准的httpRequest请求进行分析,比如直接加载django提供的中间件,路由分配,视图函数分析,最后返回一个可以被浏览器解析的符合HTTP协议的HTTPResponse。

2.Django中session会话的运行机制是什么
  • 自django中可以将session在settings.py中进行注册或者生成中间件用于启动。设置存储模式数据库缓存混合存储和配置数据库缓存用于存储,生成django表单用于读写。
3.什么是CSRF,描述攻击原理在django中如何解决?

cross-site request forgery是跨站请求伪造。

CSRF攻击原理(只正对POST请求方式):

  1. 登入正常网站之后,你的浏览器会保存sessionid,如果你没有退出
  2. 你不小心访问了另一个恶意网站,这个网站是模拟正常网站中修改密码的网站,且隐藏那些修改信息,可能只显示一张图片或者是一个按钮,你不小心点击这个按钮,那个这个按钮的action就是去访问正常网站的修改用户的界面的url并附带有修改密码的post表单,那么你的数据就会被修改。
Django防御csrf的方式:
  • Django默认启动了csrf防护,在settins.py文件中MIDDLEWARE_CLASSES中的"django.middleware.csrf.CsrfViewMiddleware", 如果注释这行,表示关闭防御CSRF攻击,注意点: CSRF攻击只针正对post提交

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b3oXy0tl-1647795627119)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.56.43.png)]

  • 表单提交数据时加上{% csrf_token %}标签

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iwyFdYl4-1647795627119)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.56.49.png)]

Django防御原理:

开启Django中CSRF防御,且在表单中添加{% csrf_token %}, 那么在渲染模板文件时在页面生成一个名字叫做csrfmiddlewaretoken的隐藏域。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lb0QiHwD-1647795627119)(/Users/hello/Desktop/面试/image/截屏2022-03-13 13.57.15.png)]

服务器交给浏览器保存一个名字为csrftoken的cookie信息

提交表单时,俩个值都会发给服务器,服务器进行比较,如果一样,则csrf验证通过,否则失败

原文链接:https://blog.csdn.net/u010013838/article/details/102521777

4.Django的CSRF跨站请求伪造的实现机制

1.django在第一次响应来自客户端的请求时,服务器会随机产生一个token保存在session中,

服务器将token传递给cookie并且送交给前端服务器

2.客户端再次从服务器进行请求的时候,把token值加入到请求数据或者头信息中,一起传送给服务器

3.服务器检验前端请求传过来的数据token和session中的token是否一致

5.什么是跨域请求?有哪些方式?

csrf cross-site request forgery指的是一个域下的脚本文档请求另外一个域下的脚本文件资源

方式如下*

资源跳转*link a 表单提交 重定向

资源嵌入*link script img frame dom 等的标签,

样式中background ----- * url() @font-face()文件外链

脚本请求 js发起的ajax请求,dom和js对象的跨域请求操作

6.跨域请求django是如何处理的?

使用第三方工具 django-cross-headers

注册app

添加中间件

配置运行跨域请求方法

7.什么是信号量

Django包含一个信号调度程序,它有助于在框架中的其他位置发生操作时通知分离的应用程序。

简而言之信号允许一些发送者通知一组接收器已经接收到信号

8.web框架的本质是什么?

web框架是socket服务端,用户的浏览器是socket客户端

9.RESTful规范的理解

restful是一种软件架构设计风格,提供了设计原则和约束条件,主要适用于客户端服务器的交互

restful的设计规范和原则

1.restful是提倡面向资源的编程,在url资源分发器中尽量使用名词,

使用https协议,网络接口比较安全

2.根据url的不同方法进行post/get请求来进行不同资源操作 post表单操作文件传输和ajax

3.在url中添加版本号,在url中可以体现是否是api接口,添加条件去筛选匹配

4.响应式添加设置状态码 status code

5.restful是有返回值是json格式,可以返回错误信息

6.返回结果中提供帮助链接,api接口最好做到Hypermedia

10.Django中如何加载初始化数据

django在创建对象创建数据后,save保存数据之后,ORM对象关系映射将数据写入到数据库中,

实现对数据的初始化,保存数据实现对数据库的调用操作对象,查询数据库数据,

将查询集返回给视图函数,通过模板语言将数据展示在前端页面

11.Django的缓存机制类型有哪些

1.全站缓存

2.视图缓存 用户视图函数或者视图类中

3.模板缓存 缓存不会经常变换的模板页面

12.Django的内建缓存机制

django根据设置的缓存方式,浏览器第一次请求时cache会缓存单个变量或者整个网页等的内容到硬盘或者内存中,

顺便携带者header请求头,当浏览器再次发送请求时,附带f-Modified-Since请求时间到Django,

django取参数之后,将缓存数据时间进行比较如果说缓存时间较新的话,则会重新请求数据缓存起来交给响应客户端,

如果缓存cache没有过期则直接从缓存中提取数据,返回给response客户端

二 1.什么是WSGI web site gateway interface 网关接口 么是ASGI异步网关协议接口

ASGI异步网关协议接口 * 介于网络协议服务和python之间的标准接口,能够处理多种通用的协议类型包括HTTP/HTTPS/WebSocket

WSGI 网关协议接口 是基于HTTPS服务的协议模式,不支持WebSocket 而ASGI的诞生是为了解决python常用的WSGI不支持当前WEB开发中一些新的协议标准

同时,ASGI对于WSGI原有的模式的支持和WebSocket的扩展,

ASGI是WSGi的扩展。

2.Django如何实现websocket?

django实现websocket使用channels。 http请求是基于tcp请求的三次握手四次挥手 进行客户端服务器之间的连接和断开连接

channels使用https升级到了websocket 是一次链接就可以使用客户端浏览器的保证实时通信,我们完全可以使用channels来实现即时通信。

它使用的是异步跨域请求接口asgi ,通过改造django框架,是django既支持http协议也会支持websocket协议

3.列举django的核心组件

MTV模式

Models 数据库ORM对象关系映射 创建模型的对象关系映射

Templates 模板页面 对设计者友好的模板语言

Views 视图函数

缓存cache 将客户端请求过来的变量或者页面保存到硬盘或者内存中

如果说没有过缓存期,就直接使用缓存的数据。

如果说已经过期,则会重新请求数据,缓存起来然后返回给response客户端

url路由分发器

管理者界面

4.django本身就已经提供了runserver,为什么不能用来部署

1.runserver是直接运行django项目的并且进行调试,它使用django自带的wsgi server运行的,

在测试和开发中使用,并且runserver的启动方式也是单进程。

2.uWsgi是Web框架的服务器,并且带有wsgi / uwsgi / http协议

uwsgi是通信协议,uWsgi是实现wsgi / http / uwsgi的服务器,

uWsgi有超快的性能,低内存占用,多app管理等的优点,

搭配Nginx服务器就是一个生产环境,将用户请求和app完全隔离开,实现真正意义上的部署。

相比来讲支持的并发量更高,方便管理多进程,发挥多核的优势,提升性能

5.ajax请求的csrf的解决方案

1.在前端表单里面添加{%csrf_token%}

2.在发起ajax post请求时,组值json参数,将浏览器cookie中的值赋予加入json中,

键名为csrfmiddlewaretoken

6.路由优先匹配规则

1.如果说在路由中有第一条和第二条同时满足需求,那么优先匹配第一条

2.如第一条为模糊匹配,第二条为精确匹配,则优先选择第一条

7.urlpattern中的name和namespace的区别是

urlpattern是路由分发通过路由实现页面的展示和模板函数的实现

name是路由的别名

namespace是为了防止多个应用apps之间的路由urls重复使用

8.Django中总项目的urls.py中的include含义是

一个django项目中有多个apps应用,每个应用中又有不同的url请求urls.py

所以从根路由发出,将app所属的url请求,全部转发到相应的urls.py模块中

9.django2.x里面的path和django1.x里面的url的区别是

path和url是不同的两个模块,但是同样的效果都是返回响应数据页面

path是导入python的第三方模块,正则表达式需要使用另外一个函数re_path

url是支持路由的正则表达式

10.Django重定向的几种方法,状态码

页面跳转 redirect

url转换 HttpResponse

Reverse

状态码*302 303 status code

三 *** 1. 模型层 makemigrations migrate的区别

make migrations 生成迁移文件

migrate 执行迁移文件

2.Django models模型类继承的方式

1.用父类model来保存在子类模板中的重复数据信息,父类models是不单独生成

也不会单独使用的数据表格,这种情况下使用抽象基类继承 Abstract base classes 父类继承

2.从现有的model继承并让每个model都有自己的数据表,使用多重表格继承 也就是说自定义继承

3.只是在models中python级别的行为,而不涉及字段的改变。

代理model proxy适合这种场合

3.class Meta 元信息字段有哪些

1.Model类可以通过元信息类设置索引和排序信息

2.元信息是定义在model模板中的一个Meta子类

自定义表格名称 db_table

联合索引 index_together

联合唯一索引 unique_together

admin管理员平台显示的表名称 verbose_name/verbose_name_plural

排序字段ordering

抽象基类 abstract

abstract

这个属性是定义当前的模型类是不是一个抽象类。所谓抽象类是不会相应数据库表的。一般我们用它来归纳一些公共属性字段,然后继承它的子类能够继承这些字段。

比方以下的代码中Human是一个抽象类。Employee是一个继承了Human的子类,那么在执行syncdb命令时,不会生成Human表。可是会生成一个Employee表,它包括了Human中继承来的字段。以后假设再加入一个Customer模型类,它能够相同继承Human的公共属性:

class Human(models.Model):
    name=models.CharField(max_length=100)
    GENDER_CHOICE=((u'M',u'Male'),(u'F',u'Female'),)
    gender=models.CharField(max_length=2,choices=GENDER_CHOICE,null=True)
    class Meta:
        abstract=True
class Employee(Human):
    joint_date=models.DateField()
class Customer(Human):
    first_name=models.CharField(max_length=100)
    birth_day=models.DateField()

4.Django中模型类的数据关系结构 数据表数据库

一对一 OneToOneField

一对多 OneToManyField ForeignKey

多对多 ManyToManyField

外键的用法 , 什么时候适合使用外键,外键一定需要索引吗

1.程序很难保证数据的完整性,如果说服务器宕机或者程序出现异常,这个时候的外键引用就可以保证数据的完整性和一致性

2.性能要求不高,安全要求高则使用外键,保证安全的前提下

性能要求高,安全性能不高则不适用外键,因为会延迟查询速度

3.外键索引能够加快关联表查询的速度

Primary Key 和 Unique Key的区别

主键和唯一键都是唯一性约束

主键是自增的 一张表格中只能有一个主键字段

唯一键是可以设置多个字段的唯一字段

主键必须不能为空 唯一键可以为空

5.DateTime的auto_now 和 auto_now_add的区别

auto_now 记录更新时间 如果说是true,那就会强制自动更新为现在的更新时间,

也就是每次更新修改数据库的时候,会直接强制更新数据库时间

auto_now_add 记录创建时间 设置为true的时候,会设置为第一次创建表格的时间以后修改的时候不会进行更改

Django的ORM

===============================================

Celery 异步技术

好处:

​ Celery是一个功能完备即插即用的任务队列,适用异步处理问题,当发送邮件,或者文件上传等耗时操作,都可以异步执行,这样用户就不用等待很久,提高用户体验。而且处理任务比较高效:单个celery进程每分钟可以处理数百万个任务。

工作流程:

​ 任务队列中包含称作任务的工作单元,有专门的工作进程持续不断的监视任务队列,并从中获得新的任务并处理。通常使用一个叫Broker(中间人)来协调client(任务的发出者)和worker(任务的处理者)clients发出消息队列中,broker将队列中的信息派发给worker处理

它可以让任务的执行完全脱离主程序,甚至可以被分配到其他主机上运行。我们通常使用它来实现异步任务(async task)和定时任务。

使用在哪里?为什么要使用?

​ 短信验证码的发送中,把发送短信验证码的请求交给celery完成,能够提升用户体验感。因为短信的发送一般是调用第三方平台进行的,是比较耗时的,当请求增加时这样能够更加快速响应。

===============================================

职业招聘网站:

Tornado:

SQLALchemy:

尝试了基于词向量的个性化推荐:

Nginx

配置nginx.conf

前后端分离

===============================================

医疗问答系统

**项目功能:**采用图数据库结构化储存患者疾病的信息,结构化医疗数据,建立医疗知识图谱, 根据患者的医 疗诊断相关图像,在海量医疗数据背景下使用卷积神经网络对图像进行分析,检测出患者所患疾病。系统还 会采用一问一答的方式与患者交流,通过患者的描述给出相对于的治疗建议。

**主要职责:**对数据进行清洗, 完成神经网络的搭建,后端功能逻辑的实现以及问答系统前后端对接。**经验收获:**对卷积神经网络有了一定的了解,了解部分图数据库的知识,对神经网络的特征提取有了进一步了解。

===============================================

数学建模

题目是什么,要求解决什么问题?

​ 题目求解的是电路板自动焊接时温度在板上的传热问题,题目要求建立焊接区域温度随时间变化规律的传热模型,同时围绕模型求解最大传送带过炉速度

我们是如何解决的?
  • 建立焊接区域温度随时间变化规律建立非稳态 一维传热模型,根据回焊炉的工作方式研究物理机理,综合考虑各类传热方式,推导热传导方程,再根据边值条件初值条件求解偏微分方程

  • 对于方程中的未知参数,根据附件中的实测温度值估计参数。然后应用该模型,代入各温区的设定值,绘出炉温曲线,计算特定点处的温度。

  • 利用遗传算法在给定的炉温曲线的制程界限基础上,给出目标为 速度最优的约束条件,建立速度的目标优化模型。

我主要负责的是什么?

代码的板块

1、根据给定数据通过最小二乘法确定相关参数(梯度下降求权重参数)

梯度下降:

梯度是函数在某点处的一个方向,并且沿着该方向变化最快,变化率最大。沿着梯度这个方向,使得值变大的方向是梯度上升的方向,沿着使值变小的方向便是下降的方向。综上,梯度下降的方向就是在该点处使值变小最快的方向。对梯度下降求最优解理解更加深入,以及遗传算法的的单目标优化问题有了一定的了解

2、根据建立的偏微分方程画出对应的炉温曲线。

3、给出目标为 速度最优的约束条件,通过遗传算法完成带约束的单目标优化求解速度。

基因是决策变量

适应度函数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o0GZxoAy-1647795627119)(/Users/hello/Desktop/面试/image/截屏2022-03-10 15.21.59.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WNGP50ov-1647795627120)(/Users/hello/Desktop/面试/image/截屏2022-03-10 15.22.20.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uSXgRYSt-1647795627120)(/Users/hello/Desktop/面试/image/截屏2022-03-10 15.22.08.png)]

x1(速度)和x2是参数,迭代100次,(二进制)编码解码最优值改善很小(1e-6)的话,提前终止迭代。越小越好和边界条件

你觉得InnoDB和MyISAM谁的性能更好?

================================================

Redis

项目中使用redis

# 建立redis连接
redis_conn = get_redis_connection(alias='verify_codes')
# 创建用户输入的手机号标志
sms_fmt = "sms_{}".format(tel).encode('utf8')
# 从数据库里面拿出手机号,名字是一样的
real_sms = redis_conn.get(sms_fmt)  # 那边的数据也是这样写入数据库的,不是只写了手机号进数据库。
# 判断是否一致
if (not real_sms) or (sms_text != real_sms.decode('utf8')):
  raise forms.ValidationError("短信验证码错误")
class SmsCodesView(View):
    """
    1,获取参数
    2,验证参数
    3,发送信息
    4,保存短信验证码
    5,返回给前端

    POST /sms_codes/
    -检查图片验证码是否正确
    -检查是否60秒有记录
    -生成短信验证码
    -发送短信
    """
    # 1. 获取参数,
    def post(self, request):   # 表单
        json_data = request.body  # body里面包含的是什么,body就是前台给到后台的数据,ajax发过来的数据
        if not json_data:
            return to_json_data(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR])  # 报错罢了
        dict_data = json.loads(json_data.decode('utf8'))
        form = forms.CheckImgCodeForm(data=dict_data)

    # 2. 校验参数
        if form.is_valid():

            # 获取手机号
            mobile = form.cleaned_data.get('mobile')

            # 创建短信验证码内容
            sms_num = "%06d" % random.randint(0, 999999)
            # 1.将短信验证码保存到数据库
            # 确保settings.py文件中有配置redis CACHE
            # Redis原生指令参考 http://redisdoc.com/index.html
            # Redis python客户端 方法参考 http://redis-py.readthedocs.io/en/latest/#indices-and-tables

            con_redis = get_redis_connection(alias='verify_codes')

            # 创建一个在60秒内是否发送记录的标记
            sms_flag_fmt = "sms_flag_{}".format(mobile).encode('utf8')

            # 创建保存短信验证码的标记key
            sms_text_fmt = "sms_{}".format(mobile).encode('utf8')
            # 节省通讯次数
            pl = con_redis.pipeline()  # redis管道技术
            try:
                pl.setex(sms_flag_fmt, constants.SEND_SMS_CODE_INTERVAL, 1)  # 60秒的标志,1号模板
                pl.setex(sms_text_fmt, constants.IMAGE_CODE_REDIS_EXPIRES, sms_num)  # 把短信验证码保存进去了

            # 通知redis执行命令
                pl.execute()
            except Exception as e:
                logger.debug('redis 执行异常{}'.format(e))
                return to_json_data(errno=Code.UNKOWNERR, errmsg=error_map[Code.UNKOWNERR])

            # 2.发送短信 通知平台
            logger.info('SMS code:{}'.format(sms_num))

            expires = constants.SMS_CODE_REDIS_EXPIRES
            sms_tasks.send_sms_code.delay(mobile, sms_num, expires, constants.SMS_CODE_TEMP_ID)
            return to_json_data(errno=Code.OK, errmsg="短信验证码发送成功")

SETEX key seconds value

含义:

       将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。

       如果 key 已经存在, SETEX 命令将覆写旧值。

返回值:

       设置成功时返回 OK 。

       当 seconds 参数不合法时,返回一个错误。

原文链接:https://blog.csdn.net/iteye_7682/article/details/82680515

pineline相关简介

Redis 使用的是客户端-服务器(CS)模型和请求/响应协议的 TCP 服务器。这意味着通常情况下一个请求会遵循以下步骤:客户端向服务端发送一个查询请求,并监听 Socket 返回,通常是以阻塞模式,等待服务端响应。服务端处理命令,并将结果返回给客户端。
  Redis 客户端与 Redis 服务器之间使用 TCP 协议进行连接,一个客户端可以通过一个 socket 连接发起多个请求命令。每个请求命令发出后 client 通常会阻塞并等待 redis 服务器处理,redis 处理完请求命令后会将结果通过响应报文返回给 client,因此当执行多条命令的时候都需要等待上一条命令执行完毕才能执行。比如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P96CAsxU-1647795627120)(/Users/hello/Desktop/面试/image/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcxMjExMDkwNTUwNzM5.png)]

其执行过程如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i5csHdRa-1647795627120)(/Users/hello/Desktop/面试/image/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcxMjExMDkwNjA0MjY1.png)]

由于通信会有网络延迟,假如 client 和 server 之间的包传输时间需要0.125秒。那么上面的三个命令6个报文至少需要0.75秒才能完成。这样即使 redis 每秒能处理100个命令,而我们的 client 也只能一秒钟发出四个命令。这显然没有充分利用 redis 的处理能力

而管道(pipeline)可以一次性发送多条命令并在执行完后一次性将结果返回,pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间,而且 Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。 Pipeline 的默认的同步的个数为53个,也就是说 arges 中累加到53条数据时会把数据提交。其过程如下图所示:client 可以将三个命令放到一个 tcp 报文一起发送,server 则可以将三条命令的处理结果放到一个 tcp 报文返回。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TlSK7jRR-1647795627121)(/Users/hello/Desktop/面试/image/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcxMjExMDkwNjI1MTk2.png)]

需要注意到是用 pipeline 方式打包命令发送,redis 必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。具体多少合适需要根据具体情况测试。

(二)比较普通模式与 PipeLine 模式
  测试环境:
  Windows:Eclipse + jedis2.9.0 + jdk 1.7
  Ubuntu:部署在虚拟机上的服务器 Redis 3.0.7

/*
 * 测试普通模式与 PipeLine 模式的效率: 
 * 测试方法:向 redis 中插入 10000 组数据
 */
public static void testPipeLineAndNormal(Jedis jedis)
		throws InterruptedException {
	Logger logger = Logger.getLogger("javasoft");
	long start = System.currentTimeMillis();
	for (int i = 0; i < 10000; i++) {
		jedis.set(String.valueOf(i), String.valueOf(i));
	}
	long end = System.currentTimeMillis();
	logger.info("the jedis total time is:" + (end - start));

	Pipeline pipe = jedis.pipelined(); // 先创建一个 pipeline 的链接对象
	long start_pipe = System.currentTimeMillis();
	for (int i = 0; i < 10000; i++) {
		pipe.set(String.valueOf(i), String.valueOf(i));
	}
	pipe.sync(); // 获取所有的 response
	long end_pipe = System.currentTimeMillis();
	logger.info("the pipe total time is:" + (end_pipe - start_pipe));
	
	BlockingQueue<String> logQueue = new LinkedBlockingQueue<String>();
	long begin = System.currentTimeMillis();
	for (int i = 0; i < 10000; i++) {
		logQueue.put("i=" + i);
	}
	long stop = System.currentTimeMillis();
	logger.info("the BlockingQueue total time is:" + (stop - begin));
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CPLm4hWM-1647795627121)(/Users/hello/Desktop/面试/image/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcxMjExMDkxMzU4OTkx.png)]

从上述代码以及结果中可以明显的看到 PipeLine 在 “批量处理” 时的优势。

(三)适用场景
  有些系统可能对可靠性要求很高,每次操作都需要立马知道这次操作是否成功,是否数据已经写进 redis 了,那这种场景就不适合。

还有的系统,可能是批量的将数据写入 redis,允许一定比例的写入失败,那么这种场景就可以使用了,比如10000条一下进入 redis,可能失败了2条无所谓,后期有补偿机制就行了,比如短信群发这种场景,如果一下群发10000条,按照第一种模式去实现,那这个请求过来,要很久才能给客户端响应,这个延迟就太长了,如果客户端请求设置了超时时间5秒,那肯定就抛出异常了,而且本身群发短信要求实时性也没那么高,这时候用 pipeline 最好了。
原文链接:https://blog.csdn.net/u011489043/article/details/78769428

缓存有哪些类型?

缓存是高并发场景下提高热点数据访问性能的一个有效手段,在开发项目时会经常使用到。

缓存的类型分为:本地缓存分布式缓存多级缓存

本地缓存:

本地缓存就是在进程的内存中进行缓存,比如我们的 JVM 堆中,可以用 LRUMap 来实现,也可以使用 Ehcache 这样的工具来实现。

本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。

分布式缓存:

分布式缓存可以很好得解决这个问题。

分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。

多级缓存:

为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。

淘汰策略

不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。

一般的剔除策略有 FIFO 淘汰最早数据、LRU 剔除最近最少使用、和 LFU 剔除最近使用频率最低的数据几种策略。

先简单说一下 Redis 的特点。

与 MC 不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:

  • 一个是因为采用了非阻塞的异步事件处理机制;

  • 另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。

Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。相比 MC,Redis 还有一个非常大的优势,就是除了 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。

**非阻塞的异步:**异步I/O应该是应用程序发起异步调用,而不需要进行轮询,进而处理下一个任务,只需在I/O完成后通过信号或是回调将数据传递给应用程序即可。

功能

来看 Redis 提供的功能有哪些吧!

基础类型:
String:

String 类型是 Redis 中最常使用的类型,内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。

这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。

但是真实的开发环境中,很多仔可能会把很多比较复杂的结构也统一转成String去存储使用,比如有的仔他就喜欢把对象或者List转换为JSONString进行存储,拿出来再反序列话啥的。

我在这里就不讨论这样做的对错了,但是我还是希望大家能在最合适的场景使用最合适的数据结构,对象找不到最合适的但是类型可以选最合适的嘛,之后别人接手你的代码一看这么规范,诶这小伙子有点东西呀,看到你啥都是用的String,垃圾!

String的实际应用场景比较广泛的有:

  • 缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。

  • 计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。

  • 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成。大大提高效率

Hash:

​ 这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。

​ 但是这个的场景其实还是多少单一了一些,因为现在很多对象都是比较复杂的,比如你的商品对象可能里面就包含了很多属性,其中也有对象。我自己使用的场景用得不是那么多。

List:

List 是有序列表,这个还是可以玩儿出很多花样的。

比如可以通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。

​ 比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。

比如可以搞个简单的消息队列,从 List 头怼进去,从 List 屁股那里弄出来。

List本身就是我们在开发过程中比较常用的数据结构了,热点数据更不用说了。

**消息队列:**Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。

Set:

Set 是无序集合,会自动去重的那种。

​ 直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。

​ 可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁?对吧。

反正这些场景比较多,因为对比很快,操作也简单,两个查询一个Set搞定。

Sorted Set:

Sorted set 是排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。

​ 有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。

排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。

​ 用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。微博热搜榜,就是有个后面的热度值,前面就是名称

Redis有序集合Zset的底层数据结构:跳跃表(跳表,skip list)

1 为什么引入跳跃表
我们知道红黑树是一种存在于内存中,可以保证在最坏的情况下,对红黑树进行例如search,insert,以及delete等基本的动态集合操作的时间复杂度为O(lg n)。

但是显而易见,红黑树实现起来比较复杂,尤其是对红黑树进行insert和delete操作。并且在红黑树中进行范围查询时需要对红黑树进行中序遍历,这也是比较复杂的操作。

那有没有一种能确保对动态集合search,insert以及delete等操作的时间复杂度在O(lg n)的前提下,实现比较简单,还能比较方便的进行范围查询的数据结构呢?

答案是肯定的,就是我们今天要总结的数据结构——跳跃表(skip list)。

2 引入的过程

例子:假设我们在内存中有一个长度达到10万以上的一个已经排好序的链表结构。我们要往这个链表结构中插入一个元素。我们是怎么进行插入的呢?

来看下图所示的这个列表(为了使链表结构简单,图中只画出了8个元素:1,4,5,7,8,9,12,15):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-whsHTRa7-1647795627121)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.00.png)]

上图所示链表中,各元素按照升序排列,现在要在该链表中插入元素10,首先要确定元素10应该插入的位置,如下图所示。

由于是链表结构因此无法使用二分查找算法,只能和原链表中的结点逐一比较大小来确定位置。这一步的时间复杂度是O(N)。

插入的过程到时很容易,直接改变结点指针的目标,时间复杂度是O(1)。

因此总体的时间复杂度是O(N)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iBJe9MQC-1647795627121)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.13.png)]

这对于拥有上十万的集合来说,这种办法显然太慢了。那有什么办法可以让search,insert以及delete操作性能更高一点呢?

search,insert以及delete操作其实归根结底就是search太慢的问题。所以只要search操作变快insert和delete操作也会变快。

让我们来回想一下MySQL索引。

所谓的索引就是把数据库表中的一些特定信息提取出来,缩小查询操作时的搜索范围,来提升查询性能。

那我们是不是可以借鉴数据库索引的思想,提取出链表中的部分关键结点。

还以上面的例子,那么我们可以取出所有值为奇数的结点作为关键结点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KEgeJFw-1647795627122)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.18.png)]

此时如果要插入一个值为10的新节点,不再需要和原结点1,4,5,7,8,9,12逐一进行比较,只需要比较关键结点1,5,7,9,15即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMJNoA6Y-1647795627122)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.23.png)]

确定了新结点在关键结点中的位置(9和15之间),就可以回到原链表,迅速定位到对应的位置插入(同样是9和15之间)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zpFLkZm5-1647795627122)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.28.png)]

节点数目少,优化效果不是很明显,如果是十万个结点,比较次数就整整减少了一半!也就是说虽然增加了50%的额外的空间,但是性能提高了一倍。

不过我们可以进一步思考。既然已经提取出了一层关键结点作为索引,那我们为何不能从索引中进一步提取,提出一层索引的索引?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WwdzgRRK-1647795627122)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.33.png)]

有了2级索引之后,新的结点可以先和2级索引比较,确定大体范围之后在和1级索引进行比较,最后再回到原链表,找到并插入对应位置。

当结点很多的时候,比较次数就会减少到原来的四分之一!当节点足够多的时候,我们可以不止提出两层索引,还可以向更高层次提取,保证每一层是上一层结点数的一半。

提取的极限就是同一层只有两个结点的时候,因为一个结点没有比较的意义。这样的多层链表结构就是所谓的跳跃表。

3 跳跃表的基本概念
跳跃表是将链表改造支持二分法查找的数据结构 。

如果是一个单链表的话,他查找数据的时间复杂度为O(n),于是给单链表添加一级索引每两个节点提取一个节点到上一级,我们把诌出来的哪一级叫做索引或者索引层,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dKjG9y9H-1647795627123)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.37.png)]

当你查找12的时候,你只需要遍历6次就可以得到结果值 ,

先去第一层索引查到,遍历到9的时候发现下一个节点是15那我们就知道此时12就在这两个节点之间,所以我们进行down进入下一层

继续遍历这个时候我们只需要遍历两个节点就可以找到了,所以我们遍历12在建立上层索引的情况下是只需要遍历7次,但是单链表便利需要7次,那我们在继续添加及层索引如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hEGEKrDK-1647795627123)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.41.png)]

当有64个节点的链表的时候,则会创建多少层索引,通过计算会有5层,那么每一层的索引个数有 (n为总的索引树,k为创建的索引层数(不包括原始链表数据结构)),

最高的层的索引层的长度为2,那我们计算出 层级为 ,

如果我们每一层遍历M个元素那么我们的时间复杂度为 ,

我们的是两个元素结合为一个节点那么每一层最多遍历3个元素,那么我们时间复杂度为 那么时间复杂度为

现在就是在原有的得单链表上创建了多层索引而达到二分法查找,达到很高。

那么现在这样岂不是浪费了很多的内存(空间换用时间)。

有一个问题需要注意:

当大量的新节点通过逐层比较,最终插入到原链表之后,上层的索引结点会渐渐变得不够用。

这时候需要从新结点中选取一部分提到上一层。

可是究竟应该提拔谁呢?

这可能是随机选取(也叫抛硬币,50%的可能性会被提拔,50%的可能性不会被提拔)的,也可能是根据某些规则确定性的选取,其中随机选取更加常见(因为跳跃表的元素删除和添加是不可预测的,很难用一种有效的算法来保证跳跃表的索引分布始终均匀,随机选取虽然不能保证索引绝对均匀分布,却可以大体上趋于均匀)。

下面以随机选取为例进行说明,比如给定一个长度是7的有序链表,结点值一次是1,2,3,5,6,7,8。

那么我们可以取出所有值为奇数的结点作为关键结点。假如值为9的新节点插入原链表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iq8vxkAV-1647795627123)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.50.png)]

4 跳跃表的更新
4.1 跳跃表插入节点
具体看上面的分析,这里就不再一一赘述。

跳跃表插入节点的流程有以下几步:

新结点和各层索引结点逐一比较,确定原链表的插入位置,时间复杂度是O(logN)。

把索引插入到原链表,时间复杂度是O(1)。

利用抛硬币的随机方式,决定新结点是否提升到上一级索引。结果为正则提升,并且继续抛硬币,结果为负则停止,时间复杂度是O(logN)。

总体上,跳跃表插入操作的时间复杂度是O(logN),而这种数据结构所占空间是2N。

4.2 跳跃表删除节点
跳跃表的删除操作比较简单,只要在索引层找到要删除的结点,然后顺藤摸瓜,删除每一层的相同结点即可。

这里还以一个长度是7的有序链表为例,结点值一次是1,2,3,5,6,7,8。取出所有值为奇数的结点作为关键结点。

如果某一层索引在删除后只剩下一个结点,那么整个一层就可以干掉了。例如要删除结点的值是5:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gzrKvpJQ-1647795627123)(/Users/hello/Desktop/面试/image/截屏2022-03-20 14.25.59.png)]

我们来总结一下跳跃表删除结点的操作步骤:

自上而下,查找第一次出现结点的索引,并逐层找到每一层对应的结点(因为每层索引都是由上层索引晋升的),时间复杂度是O(logN)。

删除每一层查找到的结点,如果该层只剩下一个结点,删除整个一层,时间复杂度是O(logN)。

总体上,跳跃表删除操作的时间复杂度是O(logN)。

从上面的总结可以看出,相对于红黑树来说,由于跳跃表维持结构平衡的成本比较低,完全依靠随机。而红黑树在多次插入和删除后,需要rebalance来重新调整结构平衡。

原文链接:https://blog.csdn.net/weixin_37079656/article/details/116955419

RDB和AOF的区别

持久化
Redis 提供了 RDB 和 AOF 两种持久化方式

RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;

RDB 把整个 Redis 的数据保存在单一文件中,比较适合用来做灾备,但缺点是快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可用。

AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作;

AOF 对日志文件的写入操作使用的追加模式,有灵活的同步策略,支持每秒同步、每次修改同步和不同步,缺点就是相同规模的数据集,AOF 要大于 RDB,AOF 在运行效率上往往会慢于 RDB。

AOF中如果一段时间内有很多命令,有的命令冗余了,这样都存储下来会占据很多空间,你知道他是怎么解决这个问题的吗?

Q1:什么是 Redis 持久化?

持久化就是把内存中的数据保存到硬盘中,使数据可以持久化保存。Redis 持久化有两种实现方式:RDB 和 AOF。

Q2:为啥需要 Redis 持久化?

Redis 是内存数据库,宕机后数据会消失。
Redis 重启后快速恢复数据,要提供持久化机制。

好了,知道了这两个问题后,我们就来看看 Redis 是如何将数据存储到硬盘里面,使得数据在 Redis 重启之后仍然存在的。

一、RDB

将 Redis 某一时刻的内存数据保存到硬盘的文件当中,默认保存的文件名为 dump.rdb,而在 Redis 服务器启动时,会重新加载 dump.rdb 文件的数据到内存中。

1.1 开启 RDB 持久化方式
save 命令

127.0.0.1:6379> save
OK

​ save 命令是一个同步操作。当客户端向服务器发送 save 命令请求进行持久化时,服务器会阻塞 save 命令之后的其他客户端的请求,直到数据同步完成。

​ 如果数据量太大,同步数据会执行很久,这期间 Redis 服务器也无法接收其他请求,导致不可用。

线上的 Redis 环境不建议使用此命令

bgsave 命令

127.0.0.1:6379> bgsave 
Background saving started

​ 与 save 命令不同,bgsave 命令是一个异步操作。当客户端发出 bgsave 命令时,Redis 服务器主进程会 fork 一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求,子进程会在数据保存到 rdb 文件后退出。

这里我们来思考一个问题,既然 Redis 要处理客户端的请求又要同时持久化,那持久化的同时,内存数据结构还在改变,比如一个 hash 字典正在持久化,结果一个请求过来把它给删掉了,可是还没有持久化完呢,这会不会导致持久化的数据不一致啊?

​ Redis 使用的操作系统的多进程 **COW(Copy On Write)**机制来实现快照持久化,也就是我们这里的 RDB 持久化方式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fMYUolPW-1647795627124)(/Users/hello/Desktop/面试/image/截屏2022-03-18 15.33.10.png)]

​ 如上图,Redis 会使用操作系统的 COW 机制来进行数据段页面的分离,数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改,这时子进程相应的页面是没有变化的,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为啥 RDB 持久化为啥叫快照持久化的原因。

服务器配置定期自动触发

在 redis.conf 中配置: save 多少秒内数据变了多少

save "" # 不使用RDB存储 不能主从

save 900 1 # 表示15分钟(900秒钟)内至少1个键被更改则进行快照。

save 300 10 # 表示5分钟(300秒)内至少10个键被更改则进行快照。 

save 60 10000 # 表示1分钟内至少10000个键被更改则进行快照。

通过配置文件触发持久化的方式与 bgsave 命令类似,达到触发条件时会 fork 一个子进程进行数据保存。

线上的 Redis 环境不建议使用此方式。因为设置触发的时间太短,则容易频繁写入 rdb 文件,影响服务器性能,时间设置太长则会造成数据丢失。

1.2 RDB 执行流程(原理)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ga6VS1Fz-1647795627124)(/Users/hello/Desktop/面试/image/截屏2022-03-18 15.36.30.png)]

  • Redis 父进程首先判断:当前是否在执行 save、bgsave 或 bgrewriteaof(aof 文件重写命令)的子进程,如果在执行则 bgsave 命令直接返回。(不能同时执行的原因是出于性能方面考虑,并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作,对性能会产生影响。)

  • 父进程执行 fork(调用 OS 函数复制主进程)操作创建子进程,这个过程中父进程是非阻塞的,Redis 能执行来自客户端的其它命令。

  • 子进程创建 RDB 文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。 (RDB 始终完整)

  • 子进程发送信号给父进程表示完成,父进程更新统计信息。

  • 父进程 fork 子进程后,继续工作。
    2.3 RDB 文件结构

1.2.1 REDIS

RDB 文件的开头部分是 REDIS 部分,这个部分长度为 5 个字节,保存着 “REDIS” 五个字符。通过这五个字符,程序在载入文件时,可以快速检查该文件是否是 RDB 文件。

1.2.2 db_version

db_version 长度为 4 个字节,值是一个字符串表示的整数,这个整数记录的是 RDB 文件的版本号(不是 Redis 版本号),比如 “0006” 就代表 RDB 文件的版本为第六版。

1.2.3 databases

一个 RDB 文件的 databases 部分可以保存任意多个非空数据库。

例如,如果服务器的 0 号数据库和 3 号数据库非空,那么服务器将创建如下图的 RDB 文件,图中的 database 0 代表 0 号数据库所有键值对数据,而 database 3 代表 3 号数据库所有键值对数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bFcxqlnJ-1647795627124)(/Users/hello/Desktop/面试/image/截屏2022-03-18 15.48.59.png)]

每个非空数据库在 RDB 文件中都可以保存为 SELECTDB、db_number、key_value_pairs 三个部分。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ORLz5exp-1647795627124)(/Users/hello/Desktop/面试/image/截屏2022-03-18 15.50.43.png)]

SELECTDB 常量的长度为 1 个字节,当读入程序遇到这个值的时候,它知道接下来要读入的是一个数据库号码。

db_number 保存着一个数据库号码,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中。

key_value_pairs 保存着数据库中所有键值对数据,如果键值对带有过期时间的话,那么键值对的过期时间也会保存在内。

1.2.4 EOF

结束标志

1.2.5 check_sum

校验和,就是看文件是否损坏,或者是否被修改。

最后再来看一下一个完整的 RDB 文件,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oDKIN2jO-1647795627124)(/Users/hello/Desktop/面试/image/截屏2022-03-18 15.48.59.png)]

二、AOF

AOF(Append Only File)持久化方式会记录客户端对服务器的每一次写操作命令,并将这些写操作以 Redis 协议追加保存到以后缀为 aof 文件末尾,在 Redis 服务器重启时,会加载并运行 aof 文件的命令,以达到恢复数据的目的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fYSZH7qB-1647795627125)(/Users/hello/Desktop/面试/image/截屏2022-03-18 15.48.49.png)]

AOF 会记录过程,RDB 只管结果。

2.1 开启 AOF 持久化方式

Redis 默认不开启 AOF 持久化方式,我们可以在 redis.conf 配置文件中开启并进行更加详细的配置。

appendonly no  # 默认不开启,需要开启的话要改成yes。
appendfilename "appendonly.aof" # aof文件名
dir ./ # AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。 

appendfsync always # 写入策略

appendfsync everysec # 写入策略

appendfsync no     # 写入策略

no-appendfsync-on-rewrite no # 默认不重写aof文件
2.2 AOF 执行原理

AOF 文件中存储的是 redis 的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:

  • 命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。

  • 缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。

  • 文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话, fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。

3.2.1 命令传播

当一个 Redis 客户端需要执行命令时, 它通过网络连接, 将协议文本发送给 Redis 服务器。服务器在接到客户端的请求之后, 它会根据协议文本的内容, 选择适当的命令函数, 并将各个参数从字符串文本转换为 Redis 字符串对象(StringObject)。每当命令函数成功执行之后,命令参数都会被传播到 AOF 程序。

3.2.2 缓存追加

当命令被传播到 AOF 程序之后,程序会根据命令以及命令的参数,将命令从字符串对象转换回原来的协议文本。协议文本生成之后,它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。

redisServer 结构维持着 Redis 服务器的状态,aof_buf 域则保存着所有等待写入到 AOF 文件的协议文本(RESP)。

3.2.3 文件写入和保存

每当服务器常规任务函数被执行、或者事件处理器被执行时,aof.c/flushAppendOnlyFile 函数都会被调用,这个函数执行以下两个工作:

  • WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
  • SAVE:根据条件,调用 fsync 或 fdatasync 函数,将

AOF 文件保存到磁盘中。
为了提高文件写入效率,在现代操作系统中,当用户调用 write 函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。

这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。因此系统同时提供了 fsync、fdatasync 等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。

3.2.4 AOF 保存模式

AOF 缓存区的同步文件策略由参数 appendfsync 控制,各个值的含义如下:

3.2.4.1 AOF_FSYNC_NO

在这种模式下, 每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行, 但 SAVE 会被略过。

在这种模式下, SAVE 只会在以下任意一种情况中被执行:

Redis 被关闭
AOF 功能被关闭
系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。

3.2.4.2 AOF_FSYNC_EVERYSEC

在这种模式中,SAVE 原则上每隔一秒钟就会执行一次,因为 SAVE 操作是由后台子进程(fork)调用的,所以它不会引起服务器主进程阻塞。

3.2.4.3 AOF_FSYNC_ALWAYS

在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。

另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。

对于三种 AOF 保存模式, 它们对服务器主进程的阻塞情况如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6lm6QGib-1647795627125)(/Users/hello/Desktop/面试/image/截屏2022-03-18 15.46.08.png)]

三、AOF 重写

3.1 AOF 文件重写的原理

前面了解完 RDB 和 AOF 两种持久化方式的原理后,我们再来来看我那粉丝读者的阿里面试题:Redis 中的 AOF 文件太大了怎么办?

这就要用到 AOF 的重写机制了。因为 AOF 持久化是通过保存被执行的写命令来记录数据库状态的,随着 AOF 文件内容越来越多,文件的体积也越来越大。如果不对 AOF 文件加以管控的话,可能会对 Redis 服务器产生影响。

举个例子,如果客户端执行了以下命令:

rpush list 1 2 // [1,2]
rpush list 3 // [1,2,3]
rpush list 4 5 6 // [1,2,3,4,5,6]
lpop list 1 // [2,3,4,5,6]
lpop list 2 // [3,4,5,6]
rpush list 7 // [3,4,5,6,7]

那么光记录 list 这个状态,AOF 文件就需要保存六条命令。实际线上的应用,写命令肯定比这频繁而且体积更大,更何况线上要记录很多个 key 的状态。

为了解决 AOF 文件体积膨胀的问题,Redis 提供了文件重写(rewrite)功能。通过该功能,Redis 服务器可以创建一个新的 AOF 文件来代替旧的 AOF 文件,重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 所谓的“重写”其实是一个有歧义的词语,实际上,AOF 重写并不需要对原有旧的 AOF 文件进行任何写入和读取,新旧两个 AOF 文件所保存的数据库状态相同,但新的 AOF 文件不会包含任何浪费空间的冗余命令,所以新的 AOF 文件体积通常比旧的 AOF 文件体积小得多。

从上面可以看到,为了记录 list 这个状态,AOF 文件就需要保存六条命令。如果服务器想要用尽量少的命令来记录 list 键的状态,那么最简单高效的办法不是去读取和分析现有 AOF 文件的内容,而是直接从数据库中读取键 list 的值,然后用 rpush list 3 4 5 6 7 命令来代替保存在 AOF 文件中的六条命令,这样就可以将保存在 list 键所需的命令从六条减到一条了。

除了上面的集合键以外,其它所有类型的键都可以用同样的方法去减少 AOF 文件中的命令数量。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是 AOF 重写功能的实现原理。

3.2 AOF 后台重写

上面的 AOF 重写功能是通过 aof_rewrite 函数来实现的,但这个函数会进行大量的写操作,所以调用这个线程将被长时间阻塞,因为 Redis 服务器使用单线程来处理命令请求,所以如果服务器直接调用 aof_rewrite 函数的话,那么重写 AOF 文件期间,服务器将无法处理客户端发来的命令请求。

Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到后台子进程里执行, 这样处理的最大好处是:

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
  • 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。

不过,使用子进程也有一个问题需要解决:因为子进程在进行 AOF 重写期间,主进程还需要继续处理 命令,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的 AOF 文件中的数 据不一致。

为了解决这个问题,Redis 增加了一个 AOF 重写缓存,这个缓存在 fork 出子进程之后开始启用,Redis 主进程在接到新的写命令之后,除了会将这个写命令的协议内容追加到现有的 AOF 文件之外,还会追加到这个缓存中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IDFvyM4b-1647795627125)(/Users/hello/Desktop/面试/image/截屏2022-03-18 15.42.58.png)]

4.3 重写过程分析

Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生 停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到 新 AOF 文件,并开始对新 AOF 文件进行追加操作。

当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:

  • 处理命令请求
  • 将写命令追加到现有的 AOF 文件中
  • 将写命令追加到 AOF 重写缓存中

这样一来可以保证

  • 现有的 AOF 功能会继续执行,即使在 AOF 重写期间发生停机,也不会有任何数据丢失。
  • 所有对数据库进行修改的命令都会被记录到 AOF 重写缓存中。
  • 当子进程完成 AOF 重写之后,它会向父进程发送一个完成信号,父进程在接到完成信号之后,会调用 一个信号处理函数,并完成以下工作:
    • 将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。
    • 对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。

这个信号处理函数执行完毕之后,主进程就可以继续像往常一样接受命令请求了。在整个 AOF 后台重 写过程中,只有最后的写入缓存和改名操作会造成主进程阻塞,在其他时候,AOF 后台重写都不会对主进程造成阻塞,这将 AOF 重写对性能造成的影响降到了最低。

四、RDB 与 AOF 的对比
4.1 RDB

优点:

  • 与 AOF 方式相比,通过 rdb 文件恢复数据比较快。
    RDB 是二进制压缩文件,占用空间小,便于传输(传给 slaver)、数据备份等。
    通过 RDB 进行数据备份,由于主进程 fork 子进程来进行持久化,所以对 Redis 服务器性能影响较小。

缺点:

  • 如果服务器宕机的话,采用 RDB 的方式会造成某个时段内数据的丢失,比如我们设置 5 分钟达到 1000 次写入就同步一次,那么如果还没达到触发条件服务器就死机了,那么这个时间段的数据会丢失。
    使用 save 命令会造成服务器阻塞,直到数据同步完成才能接收后续请求。
    使用 bgsave 命令在 fork 子进程时,如果数据量太大,fork 的过程也会发生阻塞,另外,fork 子进程会耗费内存。
4.2 AOF

优点:

  • AOF 设置为每秒保存一次,则最多丢 2 秒的数据。数据丢失相对 RDB 来说更少。
    AOF 写入文件时,对过期的 key 会追加一条 del 命令,当执行 AOF 重写时,会忽略过期 key 和 del 命令。

缺点:

  • 生成的日志文件太大,即使通过 AOF 重写,文件体积仍然很大。
    恢复数据的速度比 RDB 慢。

https://blog.csdn.net/riemann_/article/details/117447615

缓存穿透、击穿、雪崩

Redis是我们日常在工作中使用非常多的缓存解决手段,使用缓存,能够提升我们应用程序的性能,同时极大程度的降低数据库的压力。但如果使用不当,同样会造成许多问题,其中三大经典问题就包括了缓存穿透、缓存击穿和缓存雪崩。是不是听上去一脸懵逼?没关系,看完这篇就明白了。

缓存穿透

缓存穿透是指用户在查找一个数据时查找了一个根本不存在的数据。按照缓存设计流程,首先查询redis缓存,发现并没有这条数据,于是直接查询数据库,发现也没有,于是本次查询结果以失败告终。

当存在大量的这种请求或恶意使用不存在的数据进行访问攻击时,大量的请求将直接访问数据库,造成数据库压力甚至可能直接瘫痪。以电商商城为例,以商品id进行商品查询,这时如果使用一个不存在的id进行攻击,每次的攻击都将访问在数据库上。

来看一下应对方案:

1、缓存空对象

修改数据库写回缓存逻辑,对于缓存中不存在,数据库中也不存在的数据,我们仍然将其缓存起来,并且设置一个缓存过期时间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FgqyTT9v-1647795627125)(/Users/hello/Desktop/面试/image/截屏2022-03-19 12.30.11.png)]

如上图所示,查询数据库失败时,仍以查询的key值缓存一个空对象(key,null)。但是这么做仍然存在不少问题:

a、这时在缓存中查找这个key值时,会返回一个null的空对象。需要注意的是这个空对象可能并不是客户端需要的,所以需要对结果为空进行处理后,再返回给客户端
b、占用redis中大量内存。因为空对象能够被缓存,redis会使用大量的内存来存储这些值为空的key
c、如果在写缓存后数据库中存入的这个key的数据,由于缓存没有过期,取到的仍为空值,所以可能出现短暂的数据不一致问题

2、布隆过滤器

布隆过滤器是一个二进制向量,或者说二进制的数组,或者说是位(bit)数组。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iCC1FyP1-1647795627126)(/Users/hello/Desktop/面试/image/截屏2022-03-19 12.30.02.png)]

因为是二进制的向量,它的每一位只能存放0或者1。当需要向布隆过滤器中添加一个数据映射时,添加的并不是原始的数据,而是使用多个不同的哈希函数生成多个哈希值,并将每个生成哈希值指向的下标位置置为1。所以,别再说从布隆过滤器中取数据啦,我们根本就没有存原始数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8AGqntAJ-1647795627126)(/Users/hello/Desktop/面试/image/截屏2022-03-19 12.29.57.png)]

例如"Hydra"的三个哈希函数生成的下标分别为1,3,6,那么将这三位置为1,其他数据以此类推。那么这样的数据结构能够起到什么效果呢?我们可以根据这个位向量,来判断数据是否存在。

具体流程:

a、计算数据的多个哈希值;

b、判断这些bit是否为1,全部为1,则数据可能存在;

c、若其中一个或多个bit不为1,则判断数据不存在。

需要注意,布隆过滤器是存在误判的,因为随着数据存储量的增加,被置为1的bit数量也会增加,因此,有可能在查询一个并不存在的数据时,碰巧所有bit都已经被其他数据置为了1,也就是发生了哈希碰撞。因此,布隆过滤器只能做到判断数据是否可能存在,不能做到百分百的确定。

Google的guava包为我们提供了单机版的布隆过滤器实现,来看一下具体使用

首先引入maven依赖:

复制代码

<dependency>

    <groupId>com.google.guava</groupId>
    
    <artifactId>guava</artifactId>
    
    <version>27.1-jre</version>

</dependency>

向布隆过滤器中模拟传入1000000条数据,给定误判率,再使用不存在的数据进行判断:

复制代码

public class BloomTest {

    public static void test(int dataSize,double errorRate){
    
        BloomFilter<Integer> bloomFilter=
    
                BloomFilter.create(Funnels.integerFunnel(), dataSize, errorRate);
    
        for(int i = 0; i< dataSize; i++){
    
            bloomFilter.put(i);
    
        }
    
        int errorCount=0;
    
        for(int i = dataSize; i<2* dataSize; i++){
    
            if(bloomFilter.mightContain(i)){
    
                errorCount++;
    
            }
    
        }
    
        System.out.println("Total error count: "+errorCount);
    
    }
    
    public static void main(String[] args) {
    
        BloomTest.test(1000000,0.01);
    
        BloomTest.test(1000000,0.001);
    
    }

}

测试结果:

复制代码

Total error count: 10314

Total error count: 994

可以看出,在给定误判率为0.01时误判了10314次,在误判率为0.001时误判了994次,大体符合我们的期望。

但是因为guava的布隆过滤器是运行在的jvm内存中,所以仅支持单体应用,并不支持微服务分布式。那么有没有支持分布式的布隆过滤器呢,这时Redis站了出来,自己造成的问题自己来解决!

Redis的**BitMap(位图)**支持了对位的操作,通过一个bit位来表示某个元素对应的值或者状态。

复制代码

//对key所存储的字符串值,设置或清除指定偏移量上的位(bit)

setbit key offset value

//对key所存储的字符串值,获取指定偏移量上的位(bit)

getbit key offset

既然布隆过滤器是对位进行赋值,我们就可以使用BitMap提供的setbit和getbit命令非常简单的对其进行实现,并且setbit操作可以实现自动数组扩容,所以不用担心在使用过程中数组位数不够的情况。

复制代码

public class RedisBloomTest {
private static int dataSize = 1000;

private static double errorRate = 0.01;

//bit数组长度

private static long numBits;

//hash函数数量

private static int numHashFunctions;

public static void main(String[] args) {

    numBits = optimalNumOfBits(dataSize, errorRate);

    numHashFunctions = optimalNumOfHashFunctions(dataSize, numBits);

    System.out.println("Bits length: "+numBits);

    System.out.println("Hash nums: "+numHashFunctions);

    Jedis jedis = new Jedis("127.0.0.1", 6379);

    for (int i = 0; i <= 1000; i++) {

        long[] indexs = getIndexs(String.valueOf(i));

        for (long index : indexs) {

            jedis.setbit("bloom", index, true);

        }

    }

    num:

    for (int i = 1000; i < 1100; i++) {

        long[] indexs = getIndexs(String.valueOf(i));

        for (long index : indexs) {

            Boolean isContain = jedis.getbit("bloom", index);

            if (!isContain) {

                System.out.println(i + "不存在");

                continue  num;

            }

        }

        System.out.println(i + "可能存在");

    }

}

//根据key获取bitmap下标

private static long[] getIndexs(String key) {

    long hash1 = hash(key);

    long hash2 = hash1 >>> 16;

    long[] result = new long[numHashFunctions];

    for (int i = 0; i < numHashFunctions; i++) {

        long combinedHash = hash1 + i * hash2;

        if (combinedHash < 0) {

            combinedHash = ~combinedHash;

        }

        result[i] = combinedHash % numBits;

    }

    return result;

}

private static long hash(String key) {

    Charset charset = Charset.forName("UTF-8");

    return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();

}

//计算hash函数个数

private static int optimalNumOfHashFunctions(long n, long m) {

    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));

}

//计算bit数组长度

  private static long optimalNumOfBits(long n, double p) {

      if (p == 0) {

          p = Double.MIN_VALUE;

      }

      return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));

  }
}

基于BitMap实现分布式布隆过滤器的过程中,哈希函数的数量以及位数组的长度都是动态计算的。可以说,给定的容错率越低,哈希函数的个数则越多,数组长度越长,使用的redis内存开销越大。

guava中布隆过滤器的数组最大长度是由int值的上限决定的,大概为21亿,而redis的位数组为512MB,也就是2^32位,所以最大长度能够达到42亿,容量为guava的两倍。

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据,由于出现大量的并发请求,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

造成这种情况大致有两种情况:

  • 第一次查询数据时,没有进行缓存预热,数据并没有加入缓存当中。缓存由于到达过期时间导致失效。

解决思路:

  • 当缓存不命中时,在查询数据库前使用redis分布式锁,使用查询的key值作为锁条件;
  • 获取锁的线程在查询数据库前,再查询一次缓存。这样做是因为高并发请求获取锁的时候造成排队,但第一次进来的线程在查询完数据库后会写入缓存,之后再获得锁的线程直接查询缓存就可以获得数据;
  • 读取完数据后释放分布式锁。
    代码思路:
public String queryData(String key) throws Exception {

    String data;
    
    data = queryDataFromRedis(key);// 查询缓存数据
    
    if (data == null) {
    
        if(redisLock.tryLock()){//获取分布式锁
    
            data = queryDataFromRedis(key); // 再次查询缓存
    
            if (data == null) {
    
                data = queryDataFromDB(key); // 查询数据库
    
                writeDataToRedis(data); // 将查询到的数据写入缓存
    
            }
    
            redisLock.unlock();//释放分布式锁
    
        }
    
    }
    
    return data;

}


具体分布式锁的实现可以使用redis中强大的setnx命令:

复制代码

/*

* 加锁

* key-;value-* nxxx-nx(只在key不存在时才可以set)|xx(只在key存在的时候set)

* expx--ex代表秒,px代表毫秒;time-过期时间,单位是expx所代表的单位。

* */

jedis.set(key, value, nxxx, expx, time);

//解锁

jedis.del(key);

通过在加锁的同时设置过期时间,还可以防止线程挂掉仍然占用锁的情况。

缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,引发的大部分缓存突然同时不可用,而查询数据量巨大,引起数据库压力过大甚至宕机的情况。
需要注意缓存击穿和缓存雪崩的不同之处缓存击穿指的是大量的并发请求去查询同一条数据;而缓存雪崩是大量缓存同时过期,导致很多查询请求都查不到缓存数据从而查数据库。

解决方案:

  • 错开缓存的过期时间,可通过设置缓存数据的过期时间为默认值基础上加上一个随机值,防止同一时间大量数据过期现象发生。
  • 搭建高可用的redis集群,避免出现缓存服务器宕机引起的雪崩问题。
  • 参照hystrix,进行熔断降级。

原文链接:https://blog.csdn.net/Six_9XXX/article/details/121620906

Redis是单进程单线程的?

Redis是单进程单线程的,Redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。

Redis为什么是单线程的?

多线程处理会涉及到锁,而且多线程处理会涉及到线程切换而消耗CPU。减少上下文切换时间,因为CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存或者网络带宽。单线程无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来解决。

redis底层锁的实现

===============================================

Elasticsearch

​ 在生产环境实践经验,最佳的情况下,是仅仅在 ES中就存少量的数据,就是你要用来搜索的那些索引,如果内存留给 filesystem cache 的是 100G,那么你就将索引数据控制在 100G 以内,这样的话,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内。

结合Hbase优化:Hbase 的特点是适用于海量数据的在线存储,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id ,然后根据 doc id 到 hbase 里去查询每个 doc id 对应的完整的数据,给查出来,再返回给前端。

问题:

你好兄弟,兄弟你好在模糊查询中可能就会有不同的情况

8.1 倒排索引
8.1.1 elasticsearch 的倒排索引是什么

面试官:想了解你对基础概念的认知。
解答:通俗解释一下就可以。
传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gCB79EN4-1647795627126)(/Users/hello/Library/Application Support/typora-user-images/截屏2022-03-12 10.13.28.png)]

单词词典是以B+树的形式存在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gjDt9k6T-1647795627126)(/Users/hello/Library/Application Support/typora-user-images/截屏2022-03-12 10.14.16.png)]

优缺点

查询的时候由于可以一次得到查询关键字所对应的所有文档,所以查询效率高于正排索引

由于每个字或对应的文档数量都在动态变化,所以倒排表的简历和维护都比较复杂,因为要更新B+树

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYwbLvOp-1647795627127)(/Users/hello/Desktop/面试/image/截屏2022-03-20 15.42.52.png)]

数据预热:

举个例子,拿微博来说,你可以把一些大 V,平时看的人很多的数据,你自己提前后台搞个系统,每隔一会儿,自己的后台系统去搜索一下热数据,刷到 filesystem cache 里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。

或者是电商,你可以将平时查看最多的一些商品,比如说 iphone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 filesystem cache 里去。

对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能一定会好很多。

冷热分离

es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。

8.3、Elasticsearch查询流程
1.查询阶段

在初始化查询阶段(query phase),查询被向索引中的每个分片副本(原本或副本)广播。每个分片在本地执行搜索并且建立了匹配document的优先队列(priority queue)。

优先队列

一个优先队列(priority queue is)只是一个存有前n个(top-n)匹配document的有序列表。这个优先队列的大小由分页参数from和size决定。例如,下面这个例子中的搜索请求要求优先队列要能够容纳100个document。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oqU4Wyaj-1647795627127)(/Users/hello/Desktop/面试/image/截屏2022-03-15 19.13.38.png)]

查询阶段包含以下三步:

  • 客户端发送一个 search(搜索) 请求给 Node 3 , Node 3 创建了一个长度为 from+size 的空优先级队列。
  • Node 3 转发这个 搜索请求到索引中每个分片的原本或副本。每个分片在本地执行这个查询并且结果将结果到一个大小为 from+size 的有序本 地优先队列里去。
  • 每个分片返回document的ID和它优先队列里的所有document的排序值给协调节点 Node 3 。
    Node 3 把 这些值合并到自己的优先队列里产生全局排序结果。
    当一个搜索请求被发送到一个节点Node,这个节点就变成了协调节点。这个节点的工作是向所有相关的分片广播搜索请求并
    且把它们的响应整合成一个全局的有序结果集。这个结果集会被返回给客户端。 第一步是向索引里的每个节点的分片副本广播请求。就像document的GET 请求一样,搜索请求可以被每个分片的原本或任意
    副本处理。这就是更多的副本(当结合更多的硬件时)如何提高搜索的吞吐量的方法。对于后续请求,协调节点会轮询所有 的分片副本以分摊负载。每一个分片在本地执行查询和建立一个长度为 from+size 的有序优先队列——这个长度意味着它自己的结果数量就足够满 全局的请求要求。分片返回一个轻量级的结果列表给协调节点。只包含documentID值和排序需要用到的值,例如 _score 。

协调节点将这些分片级的结果合并到自己的有序优先队列里。这个就代表了最终的全局有序结果集。到这里,查询阶段结束。

2.取回阶段

查询阶段辨别出那些满足搜索请求的document,但我们仍然需要取回那些document本身。这就是取回阶段的工作,如图分布式搜索的取回阶段所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MqOOrwkm-1647795627127)(/Users/hello/Desktop/面试/image/截屏2022-03-15 19.13.27.png)]

分发阶段由以下步骤构成:

  • 协调节点辨别出哪个document需要取回,并且向相关分片发出 GET 请求。
  • 每个分片加载document并且根据需要丰富(enrich)它们,然后再将document返回协调节点。
  • 一旦所有的document都被取回,协调节点会将结果返回给客户端。 协调节点先决定哪些document是实际(actually)需要取回的。例如,我们指定查询 { “from”: 90, “size”: 10
    } ,那么前90 条将会被丢弃,只有之后的10条会需要取回。这些document可能来自与原始查询请求相关的某个、某些或者全部分片。
    协调节点为每个持有相关document的分片建立多点get请求然后发送请求到处理查询阶段的分片副本。 分片加载document主体—— _source field。如果需要,还会根据元数据丰富结果和高亮搜索片断。一旦协调节点收到所有结 果,会将它们汇集到单一的回答响应里,这个响应将会返回给客户端。
    原文链接:https://blog.csdn.net/xiaozhangnomoney/article/details/123083615
总流程

es 最强大的是做全文检索,就是比如你有三条数据:

你根据 java 关键词来搜索,将包含 java 的 document 给搜索出来。es 就会给你返回:java 真好玩儿啊,java 好难学啊。

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

写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。

===============================================

FDFS

FastDFS 分布式文件系统

为什么我们需要它?

众所周知,在微服务架构中,从网关进来的请求会通过Ribbon进行负载均衡,可能造成你每次请求都有可能是不同的服务器处理的,因为,为了提高系统的吞吐量,某些服务被集群化,在这种情况下,当用户需要进行文件存储的时候,如果说把文件存储在当前处理请求的服务器中,那么下次当你想要获得这个文件的时候可能就获取不到了,因为你的这次请求可能交由另一个服务器处理了。为了解决在分布式系统中文的件存储这一问题,FastDFS应运而生

FastDFS是什么?

这是一款开源的分布式文件系统,负责对文件进行存储,主要功能包括:文件存储、文件同步、文件访问等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。

FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等、并且注重高可用、高性能,使用FastDFS可以很容易的搭建一套高性能的文件服务器集群提供上传、下载文件等服务

FastDFS的结构图:

请添加图片描述
FastDFS服务端有两个角色**:跟踪器(tracker)和存储节点(storage)。在Storage集群中,每一个Volume也称作一个组(group)**

FastDFS是怎么存储文件的?

存储过程

请添加图片描述
Tracker主要负责对请求进行调度,起到负载均衡的作用,类似于微服务中的注册中心(有心跳机制等等),它有每一个存储点的信息,在收到客户端发来的存储文件的请求时,会通过负载均衡算法来选择某一个Storage来存储该文件。

为什么是都是集群?

​ 之前提到过高可用、负载均衡等名词,都是通过跟踪器(tracker)的集群化来保证的。当某一个Tracker宕机后,其他的Tracker可以继续对存储请求进行处理,这就保证了高可用。在决定文件要存到哪一个Storage的时候会使用随机或轮询等负载均衡的算法,来保证每个Storage所存储的数据比较均匀,之前也提到过冗余备份、线性扩容,是通过组(group)来保证的。

想想为什么会出现组这个概念呢?

​ 因为,当我们进行文件存储的时候,不是说把文件存储到某个Storage后就万事大吉了

万一某台机器故障了怎么办?

​ 那里面的数据可能就都要丢失了,这是一件非常严重的情况,为了解决这种情况,FastDFS中可以采用多个Storage来存储相同的文件,这样做的目的是进行数据备份,即冗余备份,解决了某个Storage出现故障时文件丢失的问题。这些存储相同文件的Storage就属于同一个组(group)。还有一种情况,**当存储的文件逐渐增多时,如何进行扩容呢?**根据FastDFS的结构,我们可以通过增加组(group)的方式来扩容。

AWS架构图

在这里插入图片描述

CDN:什么是CDN?

CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。

  1. 基本原理
    CDN的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。

3.基本思路
基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。

4 . 服务模式

内容分发网络(CDN)是一种新型网络构建方式,它是为能在传统的IP网发布宽带丰富媒体而特别优化的网络覆盖层;而从广义的角度,CDN代表了一种基于质量与秩序的网络服务模式。

简单地说,内容分发网络(CDN)是一个经策略性部署的整体系统,包括分布式存储、负载均衡、网络请求的重定向和内容管理4个要件,而内容管理和全局的网络流量管理(Traffic Management)是CDN的核心所在。通过用户就近性和服务器负载的判断,CDN确保内容以一种极为高效的方式为用户的请求提供服务。

总的来说,内容服务基于缓存服务器,也称作代理缓存(Surrogate),它位于网络的边缘,距用户仅有"一跳"(Single Hop)之遥。同时,代理缓存是内容提供商源服务器(通常位于CDN服务提供商的数据中心)的一个透明镜像。这样的架构使得CDN服务提供商能够代表他们客户,即内容供应商,向最终用户提供尽可能好的体验,而这些用户是不能容忍请求响应时间有任何延迟的。

ELB:

RDS:

EC2:

===============================================

高可用
来看 Redis 的高可用。Redis 支持主从同步,提供 Cluster 集群部署模式,通过 Sentine l哨兵来监控 Redis 主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从 slaveof 到新主。

选主的策略简单来说有三个:

slave 的 priority 设置的越低,优先级越高;
同等情况下,slave 复制的数据越多优先级越高;
相同的条件下 runid 越小越容易被选中。
在 Redis 集群中,sentinel 也会进行多实例部署,sentinel 之间通过 Raft 协议来保证自身的高可用。

Redis Cluster 使用分片机制,在内部分为 16384 个 slot 插槽,分布在所有 master 节点上,每个 master 节点负责一部分 slot。数据操作时按 key 做 CRC16 来计算在哪个 slot,由哪个 master 进行处理。数据的冗余是通过 slave 节点来保障。

哨兵
哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。

为啥必须要三个实例呢?我们先看看两个哨兵会咋样。

master宕机了 s1和s2两个哨兵只要有一个认为你宕机了就切换了,并且会选举出一个哨兵去执行故障,但是这个时候也需要大多数哨兵都是运行的。

那这样有啥问题呢?M1宕机了,S1没挂那其实是OK的,但是整个机器都挂了呢?哨兵就只剩下S2个裸屌了,没有哨兵去允许故障转移了,虽然另外一个机器上还有R1,但是故障转移就是不执行。

经典的哨兵集群是这样的:

M1所在的机器挂了,哨兵还有两个,两个人一看他不是挂了嘛,那我们就选举一个出来执行故障转移不就好了。

暖男我,小的总结下哨兵组件的主要功能:

集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
主从
提到这个,就跟我前面提到的数据持久化的RDB和AOF有着比密切的关系了。

我先说下为啥要用主从这样的架构模式,前面提到了单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的,那你一台机器又读又写,这谁顶得住啊,不当人啊!但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容。

你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。

我发出来之后来自CSDN的网友:Jian_Shen_Zer 问了个问题:

主从同步的时候,新的slaver进来的时候用RDB,那之后的数据呢?有新的数据进入master怎么同步到slaver啊

敖丙答:笨,AOF嘛,增量的就像MySQL的Binlog一样,把日志增量同步给从服务就好了

key 失效机制
Redis 的 key 可以设置过期时间,过期后 Redis 采用主动和被动结合的失效机制,一个是和 MC 一样在访问时触发被动删除,另一种是定期的主动删除。

定期+惰性+内存淘汰

缓存常见问题
缓存更新方式
这是决定在使用缓存时就该考虑的问题。

缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。更新的方式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。

当数据源不是 DB 而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。

这种场景下,可以选择失效更新,key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。

但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用。改进的办法是异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。这样就避免了失效瞬间的空窗期。另外还有一种纯异步更新方式,定时对数据进行分批更新。实际使用时可以根据业务场景选择更新方式。

数据不一致
第二个问题是数据不一致的问题,可以说只要使用缓存,就要考虑如何面对这个问题。缓存不一致产生的原因一般是主动更新失败,例如更新 DB 后,更新 Redis 因为网络原因请求超时;或者是异步更新失败导致。

解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。

缓存穿透
缓存穿透。产生这个问题的原因可能是外部的恶意攻击,例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有大量请求穿透缓存访问到 DB。

解决的办法如下。

对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据。
使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。
缓存击穿
缓存击穿,就是某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。

解决这个问题有如下办法。

可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。
缓存雪崩
缓存雪崩,产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。

解决方法:

使用快速失败的熔断策略,减少 DB 瞬间压力;
使用主从模式和集群模式来尽量保证缓存服务的高可用。
实际场景中,这两种方法会结合使用。

考点
面试的时候问你缓存,主要是考察缓存特性的理解,对 MC、Redis 的特点和使用方式的掌握。

要知道缓存的使用场景,不同类型缓存的使用方式,例如:

  • 对 DB 热点数据进行缓存减少 DB 压力;对依赖的服务进行缓存,提高并发性能;

  • 单纯 K-V 缓存的场景可以使用 MC,而需要缓存 list、set 等特殊数据格式,可以使用 Redis;

  • 需要缓存一个用户最近播放视频的列表可以使用 Redis 的 list 来保存、需要计算排行榜数据时,可以使用 Redis 的 zset 结构来保存。

要了解 MC 和 Redis 的常用命令,例如原子增减、对不同数据结构进行操作的命令等。

了解 MC 和 Redis 在内存中的存储结构,这对评估使用容量会很有帮助。

了解 MC 和 Redis 的数据失效方式和剔除策略,比如主动触发的定期剔除和被动触发延期剔除

要理解 Redis 的持久化、主从同步与 Cluster 部署的原理,比如 RDB 和 AOF 的实现方式与区别。

要知道缓存穿透、击穿、雪崩分别的异同点以及解决方案。

不管你有没有电商经验我觉得你都应该知道秒杀的具体实现,以及细节点。

………

欢迎去GitHub补充

加分项
如果想要在面试中获得更好的表现,还应了解下面这些加分项。

是要结合实际应用场景来介绍缓存的使用。例如调用后端服务接口获取信息时,可以使用本地+远程的多级缓存;对于动态排行榜类的场景可以考虑通过 Redis 的 Sorted set 来实现等等。

最好你有过分布式缓存设计和使用经验,例如项目中在什么场景使用过 Redis,使用了什么数据结构,解决哪类的问题;使用 MC 时根据预估值大小调整 McSlab 分配参数等等。

最好可以了解缓存使用中可能产生的问题。比如 Redis 是单线程处理请求,应尽量避免耗时较高的单个请求任务,防止相互影响;Redis 服务应避免和其他 CPU 密集型的进程部署在同一机器;或者禁用 Swap 内存交换,防止 Redis 的缓存数据交换到硬盘上,影响性能。再比如前面提到的 MC 钙化问题等等。

常见问题

1、10亿url找频率topK?

这个问题还是建立最小堆比较好一些,

以上就是面试时简单提到的内容,下面整理一下这方面的问题:

top K问题
在大规模数据处理中,经常会遇到的一类问题:在海量数据中找出出现频率最好的前k个数,或者从海量数据中找出最大的前k个数,这类问题通常被称为top K问题。例如,在搜索引擎中,统计搜索最热门的10个查询词;在歌曲库中统计下载最高的前10首歌等。
针对top K类问题,通常比较好的方案是分治+Trie树/hash+小顶堆(就是上面提到的最小堆),即先将数据集按照Hash方法分解成多个小数据集,然后使用Trie树活着Hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后在所有top K中求出最终的top K。

有1亿个浮点数,如果找出期中最大的10000个?
最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法的时间复杂度一般为O(nlogn),如快速排序。但是在32位的机器上,每个float类型占4个字节,1亿个浮点数就要占用400MB的存储空间,对于一些可用内存小于400M的计算机而言,很显然是不能一次将全部数据读入内存进行排序的。其实即使内存能够满足要求(我机器内存都是8GB),该方法也并不高效,因为题目的目的是寻找出最大的10000个数即可,而排序却是将所有的元素都排序了,做了很多的无用功。

**第二种方法为局部淘汰法,**该方法与排序方法类似,用一个容器保存前10000个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的10000个数还小,那么容器内这个10000个数就是最大10000个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这1亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即10000。

**第三种方法是分治法,**将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的10010000个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^64=4MB,一共需要101次这样的比较。

**第四种方法是Hash法。**如果这1亿个书里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的10000个数。

**第五种方法采用最小堆。**首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。

原文链接:https://blog.csdn.net/zyq522376829/article/details/47686867

2、UWSGI
2.1、WSGI

WSGI是一种WEB服务器网关接口。 是一个Web服务器(如nginx)与应用服务器(如uWSGI)通信的一种规范(协议)。

在生产环境中使用WSGI作为python web的服务器。Python Web服务器网关接口,是Python应用程序或框架和Web服务器之间的一种接口,被广泛接受。WSGI没有官方的实现, 因为WSGI更像一个协议,只要遵照这些协议,WSGI应用(Application)都可以在任何服务器(Server)上运行。

img

作用

WSGI有两方:“服务器”或“网关”一方,以及“应用程序”或“应用框架”一方。服务方调用应用方,提供环境信息,以及一个回调函数(提供给应用程序用来将消息头传递给服务器方),并接收Web内容作为返回值。

所谓的 WSGI中间件同时实现了API的两方,因此可以在WSGI服务和WSGI应用之间起调解作用:从WSGI服务器的角度来说,中间件扮演应用程序,而从应用程序的角度来说,中间件扮演服务器。“中间件”组件可以执行以下功能:

  • 重写环境变量后,根据目标URL,将请求消息路由到不同的应用对象。
  • 允许在一个进程中同时运行多个应用程序或应用框架。
  • 负载均衡和远程处理,通过在网络上转发请求和响应消息。
  • 进行内容后处理,例如应用XSLT样式表。
2.2、uWSGI

uWSGI实现了WSGI的所有接口,是一个快速、自我修复、开发人员和系统管理员友好的服务器。uWSGI代码完全用C编写,效率高、性能稳定。

uwsgi是一种线路协议而不是通信协议,在此常用于在uWSGI服务器与其他网络服务器的数据通信。uwsgi协议是一个uWSGI服务器自有的协议,它用于定义传输信息的类型。

uWSGI是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。Nginx中HttpUwsgiModule的作用是与uWSGI服务器进行交换。

要注意 WSGI / uwsgi / uWSGI 这三个概念的区分。

  • WSGI看过前面小节的同学很清楚了,是一种通信协议。
  • uwsgi同WSGI一样是一种通信协议。
  • 而uWSGI是实现了uwsgi和WSGI两种协议的Web服务器。

uwsgi协议是一个uWSGI服务器自有的协议,它用于定义传输信息的类型(type of information),每一个uwsgi packet前4byte为传输信息类型描述,它与WSGI相比是两样东西。

为什么有了uWSGI为什么还需要nginx?因为nginx具备优秀的静态内容处理能力,然后将动态内容转发给uWSGI服务器,这样可以达到很好的客户端响应。

2.3、作用

Django 是一个 Web 框架,框架的作用在于处理 request 和 reponse,其他的不是框架所关心的内容。所以怎么部署 Django 不是 Django 所需要关心的。

Django 所提供的是一个开发服务器,这个开发服务器,没有经过安全测试,而且使用的是 Python 自带的 simple HTTPServer 创建的,在安全性和效率上都是不行的

而uWSGI 是一个全功能的 HTTP 服务器,他要做的就是把 HTTP 协议转化成语言支持的网络协议。比如把 HTTP 协议转化成 WSGI 协议,让 Python 可以直接使用。
uwsgi 是一种 uWSGI 的内部协议,使用二进制方式和其他应用程序进行通信。
https://blog.csdn.net/mnszmlcd/article/details/78819237

最大子树和

最大回文字符串?

===============================================

讲一下进程和进程之间有哪些关联?

https://blog.csdn.net/qq_34021920/article/details/79953789?ops_request_misc=&request_id=&biz_id=102&utm_term=%E8%AE%B2%E4%B8%80%E4%B8%8B%E8%BF%9B%E7%A8%8B%E5%92%8C%E8%BF%9B%E7%A8%8B%E4%B9%8B%E9%97%B4%E6%9C%89%E5%93%AA%E4%BA%9B%E5%85%B3%E8%81%94%EF%BC%9F&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-8-79953789.142v2pc_search_result_control_group,143v4register&spm=1018.2226.3001.4187

我聊了进程通信的多种方式,讲了父子进程的关系。

最大回文字符串

4、

数据库

中间件

网络协议

  1. B+,B, AVL,红黑树区别,为什么B+,如果用B会咋样;

  2. 死锁3连:你觉得什么是死锁 -》怎么找到死锁 -》 怎么避免死锁,就差让我写个死锁了。

  3. uwsgi。

数据库:

redis的一些数据结构

redis怎么解决哈希冲突的

redis的集群了解么 讲一下

mysql为什么用B+树

数据结构:

红黑树说一下

红黑树的节点删除具体怎么实现

mysql为什么不使用红黑、二叉、AVL呢?

五大约束

为什么不能是两次握手?

TCP的粘包问题

6.算法题:

b. 最长非重复子串-- 秒给双指针+哈希表的思路?

public int lengthOfLongestSubstring(String s) {
        int left=0,right=0;
        int winlen=0;

        Set<Character> winset=new HashSet<>();
        while(left<s.length()&&right<s.length()){
            if(winset.contains(s.charAt(right))){
                winset.remove(s.charAt(left));
                left++;
            }else{
                winset.add(s.charAt(right));
                right++;
                int aa=right-left;
                winlen=Math.max(aa,winlen);
            }
            
        }
        return winlen;
    }

6.读已提交和可重复读底层实现的区别?(没答好)

8.MySQL的索引?二级索引?(经典八股文)

3.MongoDB和MySQL的区别?MongoDB为什么读写快?

===============================================

设计模式

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

0、接口

定义:一种特殊的类,声明了若干方法,要求继承该接口的类必须实现这种方法,作用:限制继承接口的类的方法的名称及调用方式,隐藏了类的内部实现请添加图片描述

1、单例模式

​ 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在。在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。
定义: 保证一个类只有一个实例,并提供一个访问它的全局访问点
适用场景:当一个类只能有一个实例,而客户可以从一个众所周知的访问点访问它时
优点
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
比如,某个服务器程序的配置信息存放在一个文件中,客户端通过一个 AppConfig 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig 对象的实例,这就导致系统中存在多个 AppConfig 的实例对象,而这样会严重浪费内存资源,类似 AppConfig 这样的类,我们希望在程序运行期间只存在一个实例对象。

方法一:使用模块

实现方法:将需要实现的单例功能放到一个.py 文件中

实现原理:Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。

mysingleton.py
class Singleton(object):
    def foo(self):
        pass
singleton = Singleton()

将上面的代码保存在文件 mysingleton.py 中,要使用时,直接在其他文件中导入此文件中的对象,这个对象即是单例模式的对象

from a import singleton
方法二、装饰器实现
def Singleton(cls):
    _instance = {}
    def _singleton(*args, **kargs):
        if cls not in _instance:#判断该实例是否存在,存在就直接返回,不存在就创建        
            _instance[cls] = cls(*args, **kargs)
        return _instance[cls]
    return _singleton
 
@Singleton
class A(object):
    a = 1
    def __init__(self, x=0):
        self.x = x

# a1 = A(2)
a2 = A(3)
# 输出:{<class '__main__.A'>: <__main__.A object at 0x7fb9af751af0>}
方法三、基于__new__方法

我们知道,当我们实例化一个对象时,是先执行了类的__new__方法(我们没写时,默认调用object.new),实例化对象;然后再执行类的__init__方法,对这个对象进行初始化,所有我们可以基于这个,实现单例模式

个人最初常用的是重写__new__方法的方式,但是用重写类中的__new__方法,在多次创建对象时,尽管返回的都是同一个对象,但是每次执行创建对象语句时,内部的__init__方法都会被自动调用,而在某些应用场景,可能存在初始化方法只能允许运行一次的需求,这时这种实现单例的方式并不可取

class Download(object):
    instance = None

    def __init__(self):
        print("__init__")
    
    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = super().__new__(cls)
        return cls.instance


object1 = Download()
object2 = Download()
print(object1)
print(object2)

运行结果,可以看到初始化方法多次执行了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kA1lBLja-1647795627128)(/Users/hello/Desktop/面试/image/截屏2022-03-18 19.43.38.png)]

原文链接:https://blog.csdn.net/weixin_43783714/article/details/103352436

2、工厂模式

概念

定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。属于创建型模式,它提供了一种创建对象的最佳方式。目标是当直接创建对象(在Python中是通过__init__()函数实现的)不太方便时,提供更好的方式。

在工厂设计模式中,客户端①可以请求一个对象,而无需知道这个对象来自哪里;也就是,使用哪个类来生成这个对象。工厂背后的思想是简化对象的创建。与客户端自己基于类实例化直接创建对象相比,基于一个中心化函数来实现,更易于追踪创建了哪些对象。

通过将创建对象的代码和使用对象的代码解耦,工厂能够降低应用维护的复杂度。

工厂方法创建对象时,我们并没有与某个特定类耦合/绑定到一起,而只是通过调用某个函数来提供关于我们想要什么的部分信息。这意味着修改这个函数比较容易,不需要同时修改使用这个函数的代码。

工厂通常有两种形式:

第一种是工厂方法(Factory Method),它是一个方法(或以Python术语来说,是一个函数),对不同的输入参数返回不同的对象;

第二种是抽象工厂,它是一组用于创建一系列相关事物对象的工厂方法。

在工厂方法(简单工厂)模式中,我们执行单个函数,传入一个参数(提供信息表明我们想要什么),但并不要求知道任何关于对象如何实现以及对象来自哪里的细节。

一个例子:

我们有一个基类Person ,包涵获取名字,性别的方法 。有两个子类male 和female,可以打招呼。还有一个工厂类。
工厂类有一个方法名getPerson有两个输入参数,名字和性别。用户使用工厂类,通过调用getPerson方法。

实现一个工厂方法,通过输入物料,然后产出不同的产品类。在程序运行期间,用户传递性别给工厂,工厂创建一个与性别有关的对象。因此工厂类在运行期,决定了哪个对象应该被创建。

class Person:
    def __init__(self):
        self.name = None
        self.gender = None

    def getName(self):
        return self.name
     
    def getGender(self):
        return self.gender

class Male(Person):
    def __init__(self, name):
        print "Hello Mr." + name

class Female(Person):
    def __init__(self, name):
        print "Hello Miss." + name

class Factory:
    def getPerson(self, name, gender):
        if gender == ‘M':
            return Male(name)
        if gender == 'F':
            return Female(name)
if __name__ == '__main__':
    factory = Factory()
    person = factory.getPerson("Chetan", "M")

3、建造者模式

​ 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式将所有细节都交由子类实现。需求,画人物,要求画一个人的头,左手,右手,左脚,右脚和身体,画一个瘦子,一个胖子

不使用设计模式:

if __name__=='__name__':
    print '画左手'
    print '画右手'
    print '画左脚'
    print '画右脚'
    print '画胖身体'

    print '画左手'
    print '画右手'
    print '画左脚'
    print '画右脚'
    print '画瘦身体'

​ 这样写的缺点每画一个人,都要依次得画他的六个部位,这些部位有一些事可以重用的,所以调用起来会比较繁琐,而且客户调用的时候可能会忘记画其中的一个部位,所以容易出错。

建造一个抽象的类Builder,声明画六个部位的方法,每画一种人,就新建一个继承Builder的类,这样新建的类就必须要实现Builder的所有方法,这里主要运用了抽象方法的特性,父类定义了几个抽象的方法,子类必须要实现这些方法,否则就报错,这里解决了会漏画一个部位的问题。建造一个指挥者类Director,输入一个Builder类,定义一个draw的方法,把画这六个部位的方法调用都放在里面,这样调用起来就不会繁琐了。

Python本身不提供抽象类和接口机制,要想实现抽象类,可以借助abc模块。abc是Abstract Base Class的缩写。
被@abstractmethod装饰为抽象方法后,该方法不能被实例化;除非子类实现了基类的抽象方法,所以能实例化。

#encoding=utf-8
from abc import ABCMeta, abstractmethod
class Builder():
    __metaclass__ = ABCMeta

    @abstractmethod
    def draw_left_arm(self):
        pass
     
    @abstractmethod
    def draw_right_arm(self):
        pass
     
    @abstractmethod
    def draw_left_foot(self):
        pass
     
    @abstractmethod
    def draw_right_foot(self):
        pass
     
    @abstractmethod
    def draw_head(self):
        pass
     
    @abstractmethod
    def draw_body(self):
        pass

class Thin(Builder):#继承抽象类,必须实现其中定义的方法
    def draw_left_arm(self):
        print '画左手'

    def draw_right_arm(self):
        print '画右手'
     
    def draw_left_foot(self):
        print '画左脚'
     
    def draw_right_foot(self):
        print '画右脚'
     
    def draw_head(self):
        print '画头'
     
    def draw_body(self):
        print '画瘦身体'



class Fat(Builder):
    def draw_left_arm(self):
        print '画左手'

    def draw_right_arm(self):
        print '画右手'
     
    def draw_left_foot(self):
        print '画左脚'
     
    def draw_right_foot(self):
        print '画右脚'
     
    def draw_head(self):
        print '画头'
     
    def draw_body(self):
        print '画胖身体'

class Director():
    def __init__(self, person):
        self.person=person

    def draw(self):
        self.person.draw_left_arm()
        self.person.draw_right_arm()
        self.person.draw_left_foot()
        self.person.draw_right_foot()
        self.person.draw_head()
        self.person.draw_body()

if __name__=='__main__':
    thin=Thin()
    fat=Fat()
    director_thin=Director(thin)
    director_thin.draw()
    director_fat=Director(fat)
    director_fat.draw()

建造者模式用于将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

===============================================

3、简述一个前端请求的处理流程,在uwsgi/nginx/django之间的处理流程

首先客户端请求服务资源,Nginx作为直接对外的服务接口,接收到客户端发送过来的http请求,会解包、分析,如果是静态文件请求就根据nginx配置的静态文件目录,返回请求的资源,如果是动态的请求,nginx就通过配置文件,将请求传递给uWSGI;uWSGI 将接收到的包进行处理,并转发给wsgi,wsgi根据请求调用django工程的某个文件或函数,处理完后django将返回值交给wsgi,wsgi将返回值进行打包,转发给uWSGI,uWSGI接收后转发给nginx,nginx最终将返回值返回给客户端(如浏览器)。

5、celery队列

Celery队列简介:

Celery 是一个 基于python开发的分布式异步消息任务队列,通过它可以轻松的实现任务的异步处理, 如果你的业务场景中需要用到异步任务,就可以考虑使用celery.

使用场景:

1.你想对100台机器执行一条批量命令,可能会花很长时间 ,但你不想让你的程序等着结果返回,而是给你返回 一个任务ID,你过一段时间只需要拿着这个任务id就可以拿到任务执行结果, 在任务执行ing进行时,你可以继续做其它的事情。

2.你想做一个定时任务,比如每天检测一下你们所有客户的资料,如果发现今天 是客户的生日,就给他发个短信祝福

Celery原理:

Celery 在执行任务时需要通过一个消息中间件来接收和发送任务消息,以及存储任务结果, 一般使用rabbitMQ or Redis 或者是数据库来存放消息的中间结果

Celery优点:

简单:一单熟悉了celery的工作流程后,配置和使用还是比较简单的
高可用:当任务执行失败或执行过程中发生连接中断,celery 会自动尝试重新执行任务
快速:一个单进程的celery每分钟可处理上百万个任务
灵活: 几乎celery的各个组件都可以被扩展及自定制
在这里插入图片描述
在这里插入图片描述

6、modelfirst dbfirst区别?
7、线程/进程/协程区别

8、Tornado框架

9、向量化–one-hot编码/数据分箱

10、栈、堆

11、你知道的排序算法

12、MySQL优化、多表查询

13、Linux下找文件

14、闭包

15、Django模型类继承

16、时间更新模型类

17、Settings里面设置东西

18、ajax请求的CSRF解决方法

19、机器数据分析/建模有什么感悟?

20、爬虫原理

30、redis为什么快?除了他是内存型数据库外,还有什么原因

31、python2和python3的区别?

32、你觉得python2的项目如果迁移到python3,困难会在哪里?

33、Elasticsearch
34、Redis~实现持久化的三种方式:快照方式(RDB)、文件追加方式(AOF)、混合持久化方式
何为持久化
所谓持久化就是将数据用内存保存到磁盘的工程, 其目的就是防止数据的丢失
因为内存中的数据 在服务器重启之后就会丢失,⽽磁盘的数据则不会,因此为了系统的稳定起⻅,我们需要将数据进⾏持久 化。同时持久化功能⼜是 Redis 和 Memcached 最主要的区别之⼀,因为 Redis ⽀持持久化⽽ Memcached 不⽀持

快照方式(RDB, Redis Database)
将某一时刻的内存中的数据以二进制的方式压缩后写入到磁盘中
对于一个Redis服务器来说,它的所有非空数据库以及数据库中的所有键值对就是当前数据库的状态。所以只需要将数据库的状态保存在硬盘当中,即使服务器停机或者断电,只要硬盘中存储的状态还在,就可以通过它来还原数据库原来的状态。
为了保证文件的安全以及容量更小,RDB持续化所生成的RDB文件是一个经过压缩的二进制文件,通过这个文件就可以还原数据库的状态。 好比mysql中的undo log
SAVA与BGSAVA
RDB持久化根据执行持久化的对象不同又分为SAVA和BGSAVA两种方式

SAVA即让Redis服务进程来执行持久化,所以直到RDB持久化结束之前,Redis服务进程会一直处于阻塞状态,无法处理任何命令。

BGSAVE则会通过fork()来创建一个子进程,然后让子进程来接管RDB持久化,而父进程继续处理命令请求

由于SAVA的会导致主进程的阻塞,所以使用时基本不会考虑,所以通常我们都会默认使用BGSAVA来进行,下面我指的也都是BGSAVA

优点
RDB的内容为二进制形式, 占用的内存小, 更适合做为备份文件
RDB对灾难恢复很有用, 因为其的小巧紧凑, 可以很快的传输到远程的服务器上进行Redis服务恢复
可以很大程度提高Redis的运行速度, 因为每次持久化时, 主进程都会fork一个子进程来进行数据持久化到磁盘, 这样就减少了主进程的任务负担, 从而提高了主进程的效率
与AOF格式的文件相比, RDB文件重启更快
RDB可以提高Redis的运行速度,因为使用BGSAVA持久化时会fork出子进程进行持久化的I/O操作. == 主进程不会受到干扰==。
缺点
RDB只能保存某个时间段间隔的数据(也就是快照与快照之间有时间 间隔), 如果在一个间隔中途Redis服务器突然挂了, 就有丢失数据的风险
上面说到RDB进行持久化操作的时候会创建一个子进程来完成, 但是如果某时间段数据集非常大, fork()很多的子进程很耗时, 并且如果CPU处理能力不行的话, 可能会导致Redis停止位客户端服务一段时间(因为Redis操作很重要一个特性就是其内存是原子操作)
文件追加方式(AOF, Append Only File)
记录所有操作的命令, 并以文本的形式追加到文件中

AOF持久化其实就是保存Redis服务器所执行的命令来保存数据库的状态,将命令追加到AOF文件的末尾(Append Only File),AOF的核心其实就是将所有执行过的命令重新执行,来恢复状态, 好比mysql中的redo log

当服务器启动时,就会读取AOF文件中的所有命令,将其在服务器上重新执行一次,来恢复服务器的状态。

随着服务器存储的数据越来越多,此时AOF保存的命令也越来越多,文件的体积就会变得非常大,这样就可能导致对Redis服务器以及宿主机造成影响,并且随着文件的增大,使用AOF来进行数据还原需要的时间也就更多。

所以AOF引入了重写的机制,即只保存能够获取最终结果的命令, 重写的流程很简单,就是去直接读取当前数据库中的键值状态,然后构造出对应的命令来进行保存
例如插入1,2,3,4,5 然后删除4,5,记录是就直接进行一次记录lpush 1 2 3,就可以直接省去了中间的操作。(实现原理是重写缓冲区)

并且和RDB的BGSAVA一样,为了不阻塞主进程,所有的重写操作都会通过创建子进程来进行,并且由于子进程创建时会通过写时拷贝机制带有服务器数据的副本,所以也不需要对数据进行加锁就可以保证安全,提高了效率

但是这时又引入了一个问题,如果父进程接受了新的命令,这些命令可能就会对数据库的状态进行修改,这样就会导致重写后的AOF文件所保存的状态和当前的数据库状态不一致。

为了解决这个问题,服务器新增了一个AOF重写缓冲区,将两个AOF的过程给分割开
服务器流程

执行客户端发送来的新命令
将执行后的写命令追加到AOF缓冲区中
将执行后的写命令追加到AOF重写缓冲区中
优点
AOF 保存的数据更加完整, 数据不易丢失, AOF 提供了三种保存策略:每次操作保存、每秒钟保存⼀次、跟随 系统的持久化策略保存,其中每秒保存⼀次,从数据的安全性和性能两⽅⾯考虑是⼀个不错的选择,也 是 AOF 默认的策略,即使发⽣了意外情况,最多只会丢失 1s 钟的数据
AOF使用的是命令追加的写入方式, 所以不会出现文件损坏的问题, 即使意外损坏也较易恢复
AOF 持久化⽂件,⾮常容易理解和解析,它是把所有 Redis 键值操作命令,以⽂件的⽅式存⼊了磁 盘。即使不⼩⼼使⽤ flushall 命令删除了所有键值信息,只要使⽤ AOF ⽂件,删除最后的 flushall 命令,重启 Redis 即可恢复之前误删的数据.
缺点
对比RDB保存相同的数据 使用AOF资源消耗更大
Redis 负载较高的时候, 使用RDB更好
RDB 使⽤快照的形式来持久化整个 Redis 数据,⽽ AOF 只是将每次执⾏的命令追加到 AOF ⽂件 中,因此从理论上说,RDB ⽐ AOF 更健壮。
混合持久化方式
混合顾名思义就是结合了RDB和AOF的优点, 在写入的时候, 先把数据以RDB的形式写入文件的开头, 再将后续的操作命令以AOF的格式保存到文件中去, 这样就保证Redis重启时的速度, 又能减轻数据丢失的风险
也就是mysql中的redo log和undo log一起使用, 但是这里是将俩个文件合并为一个文件, 文件开头记录压缩后的二进制数据, 尾巴记录操作命令
优点:
结合RDB的特点和AOF的特点, 开头以RDB的格式, 可以快启动, 同时结合AOF的特点, 可以减少数据丢失的风险
缺点:
结合俩者的特性, 导致AOF文件的可读性下降
兼容性差, 如果开启混合持久化AOF文件, 不能使用Redis4.0之前的版本
可以在 redis-cli 命令⾏中执⾏ config set aof-use-rdb-preamble yes 来开启混合持久化,当 开启混合持久化时 Redis 就以混合持久化⽅式来作为持久化策略;当没有开启混合持久化的情况下,使⽤ config set appendonly yes 来开启 AOF 持久化的策略,当 AOF 和混合持久化都没开启的情况 下默认会是 RDB 持久化的⽅式。
其优点级: 混合持久化 > 文件追加持久化 > 快照持久化

知识储备
python 后端工程师每天做什么?
网站后台业务逻辑
为网站提供API
为产品、运营提供后台网站工具,比如后台运营系统。
知识储备-上:
面试流程、技巧
通过不断的面试加深自己的面试经验

python语法基础、性能剖析优化
算法与数据结构、内置算法、排序……

编程范式,各种模式(单例模式……)

操作系统,Linux命令、线程进程,操作系统内存管理、python垃圾回收机制

WSGI,Web安全

当退出Python时,是否释放全部内存?
那些具有对象循环引用或者全局命名空间引用的变量,在 Python 退出是往往不会被释放
另外不会释放 C 库保留的部分内容。

range和xrange的区别?
首先得说明一下,只有在python2中才有xrange和range,python3中没有xrange,并且python3中的range和python2中的range有 本质的区别。所以这儿说的range和xrange的区别是只针对python2的。

  1. 不同点

range

在py2中,range得到的是一个列表,即

x = range(0, 5)
print(type(x)) # 打印x的类型,结果是list
print(x) # 结果是[0,1,2,3,4]
xrange

xrange得到的是一个生成器对象, 即

x = xrange(0, 5)
print(type(x)) # 输出类型,结果为一个生成对象
print(x) # 输出x, 结果为xrange(0,5)
那么,python3中为什么没有了range了呢(额,这个怎么描述呢,是有range,但是这个range其实是py2中的xrange,而不是range),因为使用生成器可以节约内存。比如现在有个代码是for i in range(0, 10000),如果还是使用py2中的range的话,那你就会得到一个0到9999的一个列表,这个将会占用你很大的空间,但是使用生成器的话,就会节省很大的资源。

  1. 共同点
    它们的使用都是一样的,比如都可以用for循环遍历所有的值

34、解释 Python 中的可变类型和不可变类型?
1.Python中的可变类型有list, dict;不可变类型有string,number, tuple.
2.当进行修改操作时,可变类型传递的是内存中的地址,也就是说,直接修改内存中的值,并没有开辟新的内存。
3.不可变类型被改变时,并没有改变原内存地址中的值,而是开辟一块新的内存,将原地址中的值复制过去,对这块新开辟的内存中的值进行操作。

一面 90多分钟

HTTP通过什么保证安全传输?

说一下应用层中使用UDP协议的应用?

聚集索引?
说下怎么实现高并发(服务端,软件层面)?
  • mysql和mongodb的区别,什么时候用mysql,什么时候用mongo(因为简历上有写mongodb)

  • redis持久化有哪几种,主要用来存什么数据,redis崩了怎么办(持久化没答上来,说这是运维的工作 🤣,崩了说的哨兵,顺带说了下集群)

  • 用户态和内核态的区别

  • 线程和进程的区别

  • TCP头部有哪些字段

  • 回车输入网址返回网址页面,中间经过了哪些层?

  • GC语言和不GC的区别
    内存回收机制?

  • C语言,不知道函数的参数和和返回值,怎么动态调用函数
    反汇编看函数的调用栈。(不懂)

(进程和线程死掉会不会影响其它进程线程)

  1. 线程共享的方式
  2. 物理地址和虚拟地址
  3. 当输入一个URL之后发生了什么
  4. 每一个网址都会查浏览器中的缓存么
  5. 三次握手中的参数是什么,socket
  6. 为什么要有TIME_WAIT

计算机网络
HTTP和HTTPS的工作方式/区别
HTTPS证书有什么作用/如何进行合法性校验
HTTPS的加密方式(对称加密/非对称加密)
TCP流量控制/坚持计时器
CSRF攻击
操作系统

32位操作系统和64位操作系统的区别(内存角度)
数据库(MySQL)

数据库的索引是怎么实现的
B+树的特点
为什么索引使用B+树
数据结构
数据量很大,判断某个值是否存在,选什么数据结构

布隆过滤器

TCP怎么判断它丢包了?

序列化/反序列化

10亿找Top10000,需要考虑分片,如果用最小堆需要考虑高效合并

前两天面试3面学长问我的这个问题(想说TEG的3个面试学长都是好和蔼,希望能完成最后一面,各方面原因造成我无比想去鹅场的心已经按捺不住了),这个问题还是建立最小堆比较好一些。

    先拿10000个数建堆,然后一次添加剩余元素,如果大于堆顶的数(10000中最小的),将这个数替换堆顶,并调整结构使之仍然是一个最小堆,这样,遍历完后,堆中的10000个数就是所需的最大的10000个。建堆时间复杂度是O(mlogm),算法的时间复杂度为O(nmlogm)(n为10亿,m为10000)。

    优化的方法:可以把所有10亿个数据分组存放,比如分别放在1000个文件中。这样处理就可以分别在每个文件的10^6个数据中找出最大的10000个数,合并到一起在再找出最终的结果。

    以上就是面试时简单提到的内容,下面整理一下这方面的问题:

top K问题
在大规模数据处理中,经常会遇到的一类问题:在海量数据中找出出现频率最好的前k个数,或者从海量数据中找出最大的前k个数,这类问题通常被称为top K问题。例如,在搜索引擎中,统计搜索最热门的10个查询词;在歌曲库中统计下载最高的前10首歌等。
针对top K类问题,通常比较好的方案是分治+Trie树/hash+小顶堆(就是上面提到的最小堆),即先将数据集按照Hash方法分解成多个小数据集,然后使用Trie树活着Hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后在所有top K中求出最终的top K。

eg:有1亿个浮点数,如果找出期中最大的10000个?
最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法的时间复杂度一般为O(nlogn),如快速排序。但是在32位的机器上,每个float类型占4个字节,1亿个浮点数就要占用400MB的存储空间,对于一些可用内存小于400M的计算机而言,很显然是不能一次将全部数据读入内存进行排序的。其实即使内存能够满足要求(我机器内存都是8GB),该方法也并不高效,因为题目的目的是寻找出最大的10000个数即可,而排序却是将所有的元素都排序了,做了很多的无用功。

    第二种方法为局部淘汰法,该方法与排序方法类似,用一个容器保存前10000个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的10000个数还小,那么容器内这个10000个数就是最大10000个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这1亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即10000。

    第三种方法是分治法,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100*10000个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^6*4=4MB,一共需要101次这样的比较。

    第四种方法是Hash法。如果这1亿个书里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的10000个数。

    第五种方法采用最小堆。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。

实际运行:
实际上,最优的解决方案应该是最符合实际设计需求的方案,在时间应用中,可能有足够大的内存,那么直接将数据扔到内存中一次性处理即可,也可能机器有多个核,这样可以采用多线程处理整个数据集。

   下面针对不容的应用场景,分析了适合相应应用场景的解决方案。

(1)单机+单核+足够大内存
如果需要查找10亿个查询次(每个占8B)中出现频率最高的10个,考虑到每个查询词占8B,则10亿个查询次所需的内存大约是10^9 * 8B=8GB内存。如果有这么大内存,直接在内存中对查询次进行排序,顺序遍历找出10个出现频率最大的即可。这种方法简单快速,使用。然后,也可以先用HashMap求出每个词出现的频率,然后求出频率最大的10个词。

(2)单机+多核+足够大内存
这时可以直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑同(1)类似,最后一个线程将结果归并。

    该方法存在一个瓶颈会明显影响效率,即数据倾斜。每个线程的处理速度可能不同,快的线程需要等待慢的线程,最终的处理速度取决于慢的线程。而针对此问题,解决的方法是,将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,知道所有数据处理完毕,最后由一个线程进行归并。

(3)单机+单核+受限内存
这种情况下,需要将原数据文件切割成一个一个小文件,如次啊用hash(x)%M,将原文件中的数据切割成M小文件,如果小文件仍大于内存大小,继续采用Hash的方法对数据文件进行分割,知道每个小文件小于内存大小,这样每个文件可放到内存中处理。采用(1)的方法依次处理每个小文件。

(4)多机+受限内存
这种情况,为了合理利用多台机器的资源,可将数据分发到多台机器上,每台机器采用(3)中的策略解决本地的数据。可采用hash+socket方法进行数据分发。

    从实际应用的角度考虑,(1)(2)(3)(4)方案并不可行,因为在大规模数据处理环境下,作业效率并不是首要考虑的问题,算法的扩展性和容错性才是首要考虑的。算法应该具有良好的扩展性,以便数据量进一步加大(随着业务的发展,数据量加大是必然的)时,在不修改算法框架的前提下,可达到近似的线性比;算法应该具有容错性,即当前某个文件处理失败后,能自动将其交给另外一个线程继续处理,而不是从头开始处理。

    top K问题很适合采用MapReduce框架解决,用户只需编写一个Map函数和两个Reduce 函数,然后提交到Hadoop(采用Mapchain和Reducechain)上即可解决该问题。具体而言,就是首先根据数据值或者把数据hash(MD5)后的值按照范围划分到不同的机器上,最好可以让数据划分后一次读入内存,这样不同的机器负责处理不同的数值范围,实际上就是Map。得到结果后,各个机器只需拿出各自出现次数最多的前N个数据,然后汇总,选出所有的数据中出现次数最多的前N个数据,这实际上就是Reduce过程。对于Map函数,采用Hash算法,将Hash值相同的数据交给同一个Reduce task;对于第一个Reduce函数,采用HashMap统计出每个词出现的频率,对于第二个Reduce 函数,统计所有Reduce task,输出数据中的top K即可。

    直接将数据均分到不同的机器上进行处理是无法得到正确的结果的。因为一个数据可能被均分到不同的机器上,而另一个则可能完全聚集到一个机器上,同时还可能存在具有相同数目的数据。

以下是一些经常被提及的该类问题。
(1)有10000000个记录,这些查询串的重复度比较高,如果除去重复后,不超过3000000个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。请统计最热门的10个查询串,要求使用的内存不能超过1GB。

(2)有10个文件,每个文件1GB,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。按照query的频度排序。

(3)有一个1GB大小的文件,里面的每一行是一个词,词的大小不超过16个字节,内存限制大小是1MB。返回频数最高的100个词。

(4)提取某日访问网站次数最多的那个IP。

(5)10亿个整数找出重复次数最多的100个整数。

(6)搜索的输入信息是一个字符串,统计300万条输入信息中最热门的前10条,每次输入的一个字符串为不超过255B,内存使用只有1GB。

(7)有1000万个身份证号以及他们对应的数据,身份证号可能重复,找出出现次数最多的身份证号。

重复问题
在海量数据中查找出重复出现的元素或者去除重复出现的元素也是常考的问题。针对此类问题,一般可以通过位图法实现。例如,已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。

    本题最好的解决方法是通过使用位图法来实现。8位整数可以表示的最大十进制数值为99999999。如果每个数字对应于位图中一个bit位,那么存储8位整数大约需要99MB。因为1B=8bit,所以99Mbit折合成内存为99/8=12.375MB的内存,即可以只用12.375MB的内存表示所有的8位数电话号码的内容。

原文链接:https://blog.csdn.net/zyq522376829/article/details/47686867

  1. 事务隔离级别,MVCC
  2. B+树的时间复杂度?二叉树的时间复杂度?

https://blog.csdn.net/wufeifan_learner/article/details/109724836?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164734451916780261999738%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164734451916780261999738&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-1-109724836.142v2pc_search_result_control_group,143v4register&utm_term=B%2B%E6%A0%91%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%EF%BC%9F%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6%EF%BC%9F&spm=1018.2226.3001.4187

继承多态重载

c++实现多态的方法
数据库ACID
脏读和幻读
mysql隔离级别
索引的作用
为什么用b+树
建索引建在哪些列
聚类索引和非聚类索引
数据库的范式
sql注入
线程和进程的区别
进程间通信
管道有哪些
线程池
软连接和硬连接
了解vfs吗
TCP如何保证可靠性
gobackn和选择重传
ARP协议
tcp包头部包含什么
http头部包含什么
C和C++区别
多态和虚函数
virtual关键字和虚函数表
构造函数和析构函数是不是虚函数
网桥、路由器、交换机分别属于哪一层,它们的功能
tcp和udp的区别和应用场景
如何改进udp使其变得可靠
http状态码
http加密方式
非对称加密

三次握手四次挥手
tcp拥塞控制
如何改进udp使它拥有拥塞控制的功能
数据库ACID
隔离级别
脏读幻读

数据库索引 index(b,c),查询 where b=1 and a=1和where c=1 and b=1时是否会走索引

https://blog.csdn.net/GMB1233/article/details/122020791?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164734598216780255213855%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164734598216780255213855&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-1-122020791.142v2pc_search_result_control_group,143v4register&utm_term=%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B4%A2%E5%BC%95+index%28b%2Cc%29%EF%BC%8C%E6%9F%A5%E8%AF%A2+where+b%3D1+and+a%3D1%E5%92%8Cwhere+c%3D1+and+b%3D1%E6%97%B6%E6%98%AF%E5%90%A6%E4%BC%9A%E8%B5%B0%E7%B4%A2%E5%BC%95&spm=1018.2226.3001.4187

算法题:K个一组链表反转

介绍一下项目

项目中有用到什么设计模式,介绍你熟悉的设计模式

进程通信有哪几种方式,线程通信有哪几种方式

浏览器输入一个URL过程

MySQL事务ACID

事务隔离级别、幻读

谈谈你了解的微服务

流量并发太大有什么优化方法

谈谈消息队列

如何保证Web应用设计的安全性(想多少说多少)

场景题:设计一个评论系统(数据库、缓存最重要),评论可以curd,查询可以按照页面ID查询,和用户ID查询(可以简单说说可以详细说说,自由发挥)

字节跳动----飞书一面(过了)
自我介绍

项目提问(7分钟)

当数据库查询的数据量比较大的场景怎么处理(只答出了索引优化)

说说单例模式

(八股文)

自我介绍

(同学内推的)你是xxx的同学啊,你技术应该没啥问题,那我就不问技术了

面试官对部门工作进行介绍

你的工作期望

想在实习中学到什么

实习时间

字节–飞书三面3.18
2* 021.3.18三面18分钟

兄弟部门交叉面,视频面试

自我介绍

包括设计实现的逻辑,

各种情况的解决方案等等,

如果升级为分布式存在的一些问题,

其他实现方式的对比

点开我简历上面的项目地址看

问域名,服务器,https配置等等

然后聊天

字节–飞书四面3.23
2021.3.23

hr面

自我介绍

为什么选择后端

未来职业规划

经过前面的面试,你对我们部门业务的了解

你对飞书这个产品的了解

什么时候开始实习、可以实习到什么时候

有其他公司的面试吗

如果给你发offer,你还会考虑其他公司吗
寒假投了字节跳动广告部的后端开发实习生,简历通过筛选后HR打电话通知我,让我第二天下午2点进行电话面试。

1点半我就进入了面试房间,1点50的时候面试官也进来了,然后我们开始了面试。

第零部分:自我介绍。

第一部分问的网络:

刚学了计算机网络,这一部分答上来了。我答的TCP能保证可靠交付而UDP不能,TCP和UDP各面向什么。

2.TCP的连接过程?

三次握手四次挥手,这一部分也答上来了。

3.网络的分层?

面试官问的7层(OSI),我学的五层(谢希仁),我就回答的五层。

第二部分问的操作系统:

1.了解进程和线程吗?谈一谈。

我只了解进程,不了解线程。就谈了谈进程的定义、三种状态、并发控制。

2.开始问为什么要并发控制?

我举了转账的例子。

第三部分问的编程语言:

1.Java和C++相比有什么特点?

我答的Java没有指针,不需要程序员花费太多精力管理内存。

2.C++为什么比Java运行得快?

我就说的Java既然不需要程序员花费精力管理内存Java核心就得替程序员管理内存,所以运行得慢。。。

第四部分问的数据库:

1.MySQL与其他主流数据库相比有什么特点?

虽然用过,但也只是用过,不会就说的不会。但是后来查了查特点中有几点与我想象的差不多,就是方便,安装没那么麻烦。

2.了解索引吗?知道实现原理吗?

我知道有索引查的快,在插入和更新的时候也需要耗费资源来维护索引。但我不知道底层的实现原理(后来查了是B+树)。

第五部分问的算法:

1.说一说各种排序算法原理,及其时间复杂度。

我答的选择排序、冒泡排序、插入排序、二分插入排序、快速排序、归并排序。我不仅说出了原理,还说出了每种排序算法的稳定性。

问了我自己写的一个文件名批量管理工具还有一个支付宝抢车票工具的实现以及细节
TCP建连与断连过程
(我用实际生活的例子描述了建连跟断连的过程,不过面试官好像不是很满意)
C/S两端ping一个来回200ms,用的是短连请求,从C/S两端开始发起请求(包括建连)一直到收到response,拿到响应之后这中间的时间大概要多久,假设网络是稳定的
(这个我不太会,回答的也很尴尬)
如果TCP断连改成3次(类似建连)挥手,有什么问题?为什么不改成这样?
(我说因为其中一方可能会再发送数据包,所以需要服务端发送两次挥手给客户端,确保没有后续的数据需要传送)
HTTP常见的请求方法有哪些?分别讲一下它们的特征。
(常见的有get和post,不常见的有delete,head,trace;讲了一下get和post的区别,其它不常见的方式我只知道概念,就没继续讲特点了)

说一下数据库的常用索引。
(数据库不太熟,只会简单的,面试官就没再问了)
Linux终端运行某个程序,输入CTRL+C,背后发生了什么?
(这个题经常看到,就是没怎么研究过,我说会杀掉某个进程,调用线程的XX方法,突然觉得不对劲,自己给自己挖坑,面试官果然问了我进程跟线程的区别)
那你说一下进程和线程的区别?
(没复习。。忘得差不多了,我又打了个比方,人是一个进程,人执行一个动作是一个线程的行为。。面试官让我不要打比方,太宽泛了)
在并发编程中,做数据同步的方式有哪些?
(这个我确实没经验,瞎说了一通synchronized)

动态代理了解吗?

讲一下HTTPS安全机制原理
(这个是根据我的简历提问的,回答了那些比较麻烦的加密以及流程,不过好像把自己绕进去了)
HashMap的底层原理
(底层由一个装着链表的数组实现,链表包括hash值、索引下标、元素值等,初始值为8,当碰撞时hash值超过8时,链表变成一棵红黑树)
考你一个算法吧,如何判断一棵二叉树是否对称?
(虽然面试官引导我了,最后我还是误解了他的意思,把题目想的太复杂了,想了大概20分钟,面试官说时间也差不多了,就回去了)介绍项目
项目中遇到的困难
TCP三次握手四次挥手
OSI七层模型
应用层是干什么的
应用层协议有哪些
数据库三大范式
数据库的查询
索引越多越好吗
单例,实现(只答了饿汉和懒汉)

抽象类和接口的区别,应用

手撕快排
写一个接口,用postman调用,返回传入的两个参数的和

二面(12.25):
二面忘记录音了,全靠回忆,之后想起来再补吧
上来就是一道Hard 440. 字典序的第K小数字(没做过,给不出最优解,写了通用的快速选择)
上一面哪里答的不好
聊项目,分布式事务
泛型
线程池参数,分别是什么作用
可重入锁(结合了AQS谈原理)
可重入锁的单位,或者说以什么为单位(懵,扯了好久)
一个订单表有三种查询(1)按日期查询订单(2)按用户查询订单(3)查询用户一段时间内的订单 SQL怎么写?怎么建索引?答曰:id-date;date
追问:那date-id;id可不可以?为什么?哪个好?

三面(12.29):
介绍项目
怎么防止超卖
答曰:数据库锁
面试官:来两千并发就没了啊
我:玩具玩具
面试官:你还知道是玩具啊
(顿时充满了快活的空气)
怎么保证api的安全
答曰:简单的token,然后数据库token表
cookie和session的区别
对redis了解吗
我:不太了解
面试官:不太应该啊
面试官:那了解SpringSecurity吗
我:也不太了解
面试官:拿了个玩具就来面试?
(顿时充满了快活的空气 * 2)
ConcurrentHashMap和HashMap的区别
Java的GC
面试官:你背的累不累啊
(顿时充满了快活的空气 * 3)
我:???
反问
之后连着HR面,三次快活的空气就真的离谱,真的搞心态。那天晚上直接失眠,菜的睡不着。

2.TCP UDP应用场景

3.滑动窗口

4.拥塞控制算法

5.HTTP状态码说几个 301 304 502这些都是啥?

6.服务器想要给一批ip开白名单,一批开黑名单,怎么做?

7.Mysql如何保证隔离性,read view里面有什么?

9.redis 基本数据结构了解哪些?string数据结构是什么样?sort set数据结构?

10.sort set为什么使用跳表不用红黑树?

11.如何用redis设计一个延迟队列?

12.redis 和 mysql如何保持数据一致性?

13.C++ 指针引用有什么区别?

14.C++函数调用过程,函数返回值是如何返回的?

15.vector扩容机制?基本类型 扩容跟自定义数据类型扩容机制一样吗?

16.几种I/O模型说一下,select、poll、epoll有啥区别?redis用的是什么?

计网(熟悉)

  • 那看来你了解的不错,现在主流的是https,你来说说https是什么
    • HTTP+SSL(TLS)
  • 聊一下SSL握手的过程吧(唯一败笔,没答好,在面试官引导下答出来了,感觉不太满意)
    1. 客户端发送请求 Client Hello 向服务端传输自己支持的加密套件、SSL版本信息和一个随机生成数Random1,
    2. 服务端发送响应 Server Hello 从客户端的套件中确认一个加密方案,然后生成一个随机数Random2,将加密方案和随机数Random2返回给客户端,
    3. 服务端再发送自己的证书和公钥给客户端,
    4. 客户端接收到证书后,验证该证书的合法性,如果验证通过会取出证书中的服务端公钥,并且生成一个随机数Random3,通过公钥加密后返回给服务端,
    5. 服务端用自己的私钥解密获得Random3,此时服务器和客户端都有了三个随机数,根据Random1+Random2+Random3,还有前面确定的加密方案生成一个密钥,
    6. 后续的传输就使用该密钥进行对称加密就可以了。

面试总结

类的操作、多线程实现、时间等内置函数等使用

1.sql慢查询(优化),如果没有索引怎么办?加了索引也比较慢怎么办?
2.redis高并发
3.redis高可用
4.redis主从是怎么做的

6.什么是线程安全?为什么会出现线程不安全?代码哪里会导致线程安全?

  1. java的一些锁
    8.java的内存结构,堆具体怎么分的
    9.设计模式?哪些框架有设计模式?每个模式是什么样的?
    10.乐观锁和悲观锁,具体实现
    11.事务ACID特性,隔离级别,隔离级别对应问题对其描述
    12.url解析过程
    13.七层协议
    14.三次握手过程?为什么不能两次握手

作者:唐
链接:https://leetcode-cn.com/circle/discuss/LAFhwT/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:yellowSea123
链接:https://www.nowcoder.com/discuss/867661?type=post&order=recall&pos=&page=1&ncTraceId=&channel=-1&source_id=search_post_nctrack&gio_id=8C2AB6D847B936BC19A5D88FF8799AE0-1647693383344
来源:牛客网

2.只要有两个数据结构,就可以完成所有的数据结构?为什么?

3.hashmap在jdk里的底层实现?为什么数组容量超过64,链表就转换成红黑树了?

4.concurrentHashMap怎么保证线程安全?

5.不用锁就保证线程安全的方式?

6.多线程编程有没有实战过?常用的工具和类库有了解吗?线程池和锁?

7.线程池的参数,给一个场景,怎么设计线程池参数?

8.怎么样是CPU密集型的任务?

9.tcp为什么可靠?

10.三次握手,能不能两次握手?

11.序列号的别的作用?

12.文件系统的了解?

13.读文件,操作系统的处理流程?

14.pagecache和文件读取,怎么映射?

15.对pagecache了解,怎么工作的?

16.ACID在MySQL里的底层怎么实现?

17.binlog是实现持久性的必须部件吗

18.redolog的两阶段提交?

19.redo log可不可以直接写数据页?

项目
20.为什么技术选型redis

21.缓存更新,雪崩怎么处理?

22.工厂方法模式不好的地方?

作者:仁珏。
链接:https://www.nowcoder.com/discuss/866151?type=post&order=recall&pos=&page=1&ncTraceId=&channel=-1&source_id=search_post_nctrack&gio_id=8C2AB6D847B936BC19A5D88FF8799AE0-1647693383344
来源:牛客网

\1. MySQL 的 MVCC

\2. 如何判断一个元素不在所给元素集合中 – 如果是少量数据,那么考虑使用 hash 表,如果是大量数据,那么可以考虑使用布隆过滤器

\3. 问实习项目

\4. SQL 优化

\5. TCP 的滑动窗口

\6. 算法题树的子结构

二面:

这个真的记不太清了

算法题组合

三面:

\1. Redis 的跳表如何实现

\2. 深拷贝和浅拷贝

\3. 策略模式、监听者模式(这两个是楼主实习项目写的)、工厂模式

其他的记不清了。。。

\4. 场景题:设计一个视频上传系统需要考虑什么(这个楼主答得有点不好,但是面试官一步步循循善诱),最后问了一下如何加快视频的下载速度(加大文件存储系统所在机器的配置,加大带宽、内存等,使用 CDN 进行加速)

\5. 算法题打印螺旋矩阵

3.15 三面,3.17 收到 offer call ,面试官和 HR 小姐姐人都很好,字节比较看重算法和基础,而且对于一些答不上来的场景也会加以引导,面试体验非常好。

作者:止增笑耳哈哈哈
链接:https://www.nowcoder.com/discuss/853357?type=post&order=recall&pos=&page=1&ncTraceId=&channel=-1&source_id=search_post_nctrack&gio_id=8C2AB6D847B936BC19A5D88FF8799AE0-1647693383344
来源:牛客网

  1. 主键索引和唯一索引的区别

  2. 说一下索引结构

  3. 回表

  4. 慢查询

  5. 分库分表的原因

  6. 如何认定你学到的东西都是正确的

  7. 问Java中代理的好处

    让我举个例子,我说Service和ServiceImpl。Spring中的 AOP

    又问我代理如何实现的:让我写实现的代码流程是啥。我也不知我这写的对不对

    复制代码

    class myProxy ``implements InvocationHandler{`` ``<a href=``"/profile/992988" data-card-uid=``"992988" class``=``"js-nc-card" target=``"_blank" from-niu=``"default"``>``@Override`` ``invoke( ``class``,method,args ){...}``}``Proxy.getProxyInstance( , , ){.....}</a>

    这个问题聊了得有 45 min,我讲的口干舌燥(其实我也摸不准代理到底哪里好)

    面试官最后:我还是不理解代理哪里好,我十年前写Java的时候不理解,我写了十年GO 还是不理解

    我差点绷不住了

  8. 没有算法

  9. 反问:

    • 日常实习可以转正吗?——可以
    • 要不要学MQ? ——不需要刻意去学
    • 技术栈?——GO语言天下无敌

面试官感觉是个大佬,说话十分和蔼…

研究方向中有没有需要用到编码来解决问题的

谈谈GC

编码:

1、写一个单例模式

2、写一个快排(衍生:如何查找一亿个数的中位数)

第三面是hr面。结果后面通知加面,所以又来了一次技术面

飞书四面 ,问了hr,说是leader

  1. 算法题 比较版本号,做过
  2. 设计模式 (spring里面的设计模式说了一遍)
  3. 负载均衡算法 (加权随机,加权随机轮训,一致性hash)
  4. redis如何实现一个限流 (说了zset )
  5. http 有哪些字段 (Connection, Authorization,Acceptkeepalive ,说了个cookie ,错了 )
  6. http keepalive 字段,从那个版本开始有的(长连接,好像是1.1)
  7. java动态代理原理
  8. 反问,部门做啥的,顺便问了句为什么这么快,以为凉了,面试官说前面几轮技术面已经问过了,就不多问了。

面试官人还是挺好

35 min

求offer!!!

3.acid分别怎么实现的 具体解释隔离性
4.jvm内存布局 栈内存溢出和outofmerroy的区别
5.约瑟夫环 优化版本 时间复杂度O(n)

作者:Sawyer_Ren
链接:https://www.nowcoder.com/discuss/558536?type=post&order=recall&pos=&page=1&ncTraceId=&channel=-1&source_id=search_post_nctrack&gio_id=8C2AB6D847B936BC19A5D88FF8799AE0-1647693819743
来源:牛客网

  1. 3层的b+树,用日期作为索引,查询需要几次i/o?

  2. b树索引了解吗?

  3. redis有哪些数据结构?

  4. redis出了基本数据结构还有哪些数据结构?

  5. 向zset中插入一个元素的时间复杂度是多少?

  6. 你还用过哪些非关系型数据库?

  7. 浮点数在计算机中怎么表示?

  8. 一个正数的原码和补码一样吗?负数呢?负数是怎么算补码的?

  9. 二进制怎么转十进制?

  10. 说一说接口和抽象类的区别,接口可以替代抽象类吗?抽象类可以替代接口吗?为什么?

浮点数在计算机中怎么表示?

  1. 算法题二叉树两个节点的公共祖先

  2. 如果要你设计数据库的表,你会考虑哪些?

  3. 列的数据类型怎么优化?能举具体的例子吗?大的数据类型有什么坏处?

  4. char和varchar有什么区别?varchar的最大长度是多少?什么时候用char?

  5. b+树索引一般多少层?为什么?

  6. 除了b+树还有什么索引?hash索引在innodb中有用到吗?怎么用的?怎么存储的?

  7. http缓存有哪些?cookie和session的原理说一下。除了cookie和session还有什么?

  8. tcp拥塞控制怎么做的?

  9. 算法题:数海岛,leetcode 200

  10. 算法题:用rand3实现rand7

redis的缓存穿透 击穿 雪崩 是什么以及如何去解决

reids分布式锁如何实现 需要考虑哪些问题

1.循环依赖问题

然后面试官看我不会就换了一道,换成最小共公字符串。

2.最小公共字符串

  • Zset里面跳表是什么

  • 既然讲了上面这个数据结构那讲讲Mysql里面为什么用B+树

  • B+树和二叉树区别?**那能不能用哈希呢?**不行,hash不能区间查询

  • 讲讲数据库索引?

  • **什么是事务?**讲讲数据库的隔离级别,分别怎么解决可能出现的问题?

  • 既然你说了MVCC,那简单讲讲MVCC?

  • 既然用了MVCC版本查看,为什么还会出现幻读? 因为之前博客看MVCC能解决幻读,所以晕了没答上来,回答了下串行化、间隙锁可以解决幻读

  • 那聊聊操作系统,说说进程和线程的区别。老八股了

  • 进程线程都怎么通信。八股拿下

  • 线程的状态。老八股文

  • 那聊聊计算机网络,输入URL到看到网页老八股文

  • 四次握手。答到一半短路了,没答好。GG

1.自我介绍

4.处理器适配器返回类型有哪些

5.转发和重定向的区别

8.请求拦截的时候一般会是怎么做的

9.垃圾回收机制谈一谈(哪些是需要回收)

10.循环引用的类是怎么回收的

12.对于http接口常见的状态码有哪些

13.post的常见返回码

14.http的请求格式是什么样的

15.请求头有哪些

16.session和cookie的区别

17.cookie是怎么被种到浏览器里的

18.cookie包含哪几项内容(格式)

19.cookie有一个字段叫做http only(好像是这么拼来着(大概发这个音)有去了解过吗

20.禁用cookie的话后怎么处理, 在url中添加cookie id是怎么样的传输过程

21.输入网址到浏览器渲染页面中间经历了哪些步骤

22.MySQL视图是什么?

23.视图可以修改吗(影响的字段怎么处理, 视图是被加工过的视图, 怎么处理?)

24.事务是什么?

25.你在做项目的时候哪些场景是用到了事务

26.怎么对数据库进行调优?

27.做一道笔试题吧(进制转换, 输入参数 输入原有的进制 输出转换后的进制)

28.对你写的的算法进行编写测试用例进行测试

3:假设现在有一个情景,一些客户端疯狂的访问你的服务器,

然后你现在要限制他们的访问,比如说一分钟只准访问100次,怎么实现这个功能,伪代码实现

4:说说cookie和session

5:说说HTTP(这个我当时说了好多,连请求报文,响应报文的格式啥的都说了,但是面试官还是不满意,然后我又强行瞎BB了好几分钟…)

6:HTTP2了解吗

7:说说HTTP缓存

8:数据库的特性

9:如何实现数据库的原子性,可以用伪代码实现吗

10:Linux线程与进程的区别?你觉得最主要的区别是什么

11:键盘敲一个A,发生了什么

12:说说redis

二面(2018/03/27,16:30~17:20)

1:自我介绍

2:已知一个函数rand3() 可以等概率随机产生1,2,3,请实现函数rand7(),可以等概率随机产生1~7

3:当你在搜索框输入h的时候会出现一些h开头的单词,然后再输入一个a(ha),会出现ha开头的很多单词,现在给你一个词典,让你实现这个功能,当用户动态的输入字母时,跳出以此字符串为前缀的所有单词,要求时间复杂度最优

4:在给账号输入密码时,当一个用户连续输错5次就会提醒用户休息1分钟,现在给你一堆数据,每个数据包括用户的ID,时间戳,输入密码的正确/错误,用什么样的数据结构存储并处理这些数据来实现这个功能,说说具体怎么实现

5:写两个简单的代码题吧,求一下二叉树的深度

6:普通二叉树,找一下两个节点的LCA

7:机器学习了解吗?智能AI?

就记得这些了,面试官问我数学怎么样,我居然说了还行,然后…最大似然估计?中心极限定理?偏导?梯度?一脸懵逼…

三面(2018/03/28,15:30~17:10)

最大似然和中心极限

1:输出k对括号的全部正确匹配方案,如k=2,输出()(),(())

2:将一些柱子整齐的摆在一行(立着),高度存在数组height[]中,height[i]表示第i个柱子高为height[i],然后往凹下去的地方倒水,问一共能蓄多少单位水,比如[5,1,3,4,5,1,3],答案是7 2=9

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值