• python后端面试笔记,祝愿秋招拿到满意的offer。


    前言:秋招就要开始啦!!!能不能找到一个满意的职位就看它啦!
    这个我准备了很长时间,是一些对我自己来说觉得薄弱,掌握不牢的一些知识点的总结,一些自己掌握的还行的知识点就没有记录下来,所以可能不是很全,但是内容也算相当丰富啦!!!希望对看到的小伙伴有所帮助!!!
    同步更新于个人博客系统:python后端面试笔记,祝愿秋招拿到满意的offer。

    文章目录

    1.python

    1.1多进程和多线程

    进程是计算机资源分配的最小单元。

    线程是CPU调度的最小单元。

    一个进程中可以有多个线程,同一个进程中的线程可以共享此进程的所有资源。

    进程与进程之间是相互隔离的。

    1.1.1启动一个新的进程的方法

    import multiprocessing
    multiprocessing.set_start_method("spawn")
    
    • 1
    • 2
    方法子进程和主进程之间资源的关系操作系统
    fork会在子进程中拷贝几乎所有主进程的资源unix支持文件对象/线程锁等传参
    spawnrun参数传必备资源unix,win不支持文件对象/线程锁等传参
    forkserverrun参数传必备资源部分unix不支持文件对象/线程等传参

    1.1.2定义自己的进程类

    import multiprocessing
    
    #要想定义自己的进程类,只用继承自multiprocessing.Process,并重写run()方法即可
    class MyProcess(multiprocessing.Process):
        
        #重写run()方法
        def run(self) -> None:
            print("hello world")
    
    if __name__ == '__main__':
        my_process = MyProcess()
        my_process.start()
    
        #获取cpu的核数
        print(multiprocessing.cpu_count())
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1.1.3Pipe实现进程间的通信

    import multiprocessing
    from time import sleep
    
    def func(child_conn):
        sleep(1)
        child_conn.send('hello world')
        child_conn.send('hello world2')
        info = child_conn.recv()  # 阻塞两秒钟
        print(info)
    
    
    if __name__ == '__main__':
        parent_conn, child_conn = multiprocessing.Pipe()  # recv之前必须有一个send,否则一直处于阻塞状态
    
        p = multiprocessing.Process(target=func, args=(child_conn,))
        p.start()
        
        # p.join()
        
        info = parent_conn.recv()  # 阻塞一秒钟
        print(info)  # hello world
        sleep(2)
        parent_conn.send("bye bye")
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    1.1.4GIL锁(仅CPython里面有)

    GIL是全局解释器锁,是CPython解释器特有的一个玩意,其作用就是:让一个进程中同一个时刻只能有一个线程可以被CPU调度。

    常见的程序开发中,计算操作需要使用CPU多核优势,IO操作不需要使用CPU多核优势,所以:

    • 计算密集型用多进程,例如:大量的数据计算
    • IO密集型用多线程,例如:文件读写,网络传输

    1.1.5join()方法

    join()的作用是让当前线程执行完成后再继续向下执行,此时程序处于阻塞状态。

    1.1.6线程守护

    th.setDaemon(True) #守护线程 
    
    • 1

    线程守护,主线程执行完毕后,子线程也自动关闭
    非守护线程:主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)

    1.1.7线程安全

    一个进程中可以有多个线程,且线程共享进程中所有的资源。多个线程去同时操作一个“东西”时,可能会存在数据混乱的情况。

    python中线程安全的数据结构:

    • 列表
    • 队列
    • 字典

    在线程中如果想要自己手动加锁,一般有两种:Lock和RLock。(threading.Lock,threading.RLock)

    • Lock不支持嵌套加锁
    • RLock支持嵌套加锁

    1.1.8死锁

    由于竞争资源或者由于彼此通信而造成的一种阻塞现象。

    1.1.9线程池

    python3中才正式提供线程池。

    线程不是开的越多越好,开多了可能反而导致系统的性能降低。

    from concurrent.futures import ThreadPoolExecutor
    from time import sleep
    
    
    #任务
    def task(num):
        sleep(1)
        print(f'我的编号是:{num}')
    
    #创建一个线程池,最多维护10个线程
    pool=ThreadPoolExecutor(10)
    for i in range(1000):
        #在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行;
        #执行完毕后再将线程还给线程池;
        #如果没有空闲线程,则等待
        pool.submit(task,i)
    
    
    pool.shutdown(True) #等待所有线程执行完成后才往下走(类似于线程中的join()方法)
    print('END')
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    1.1.10给线程池中的每一个线程添加额外的任务

    from concurrent.futures import ThreadPoolExecutor
    from time import sleep
    
    
    # 任务
    def task(num):
        sleep(1)
        print(f'我的编号是:{num}')
        return f'{num}'
    
    
    # 某一个任务完成后需要执行的任务 
    # 函数的参数response,调用response.result()方法可以获得该线程的执行的函数的返回值,若无返回值,则返回None
    def done(response):
        '''
        这样写的好处是,可以将任务更加原子化。
        例如:在发送请求下载一张图片时,可以将发送请求获得返回值封装成一个任务
        将数据写入文件封装成一个任务。
        '''
        print(response.result())
        print(type(response.result()))
    
    
    # 创建一个线程池,最多维护10个线程
    pool = ThreadPoolExecutor(10)
    for i in range(1000):
        # 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行;
        # 执行完毕后再将线程还给线程池;
        # 如果没有空闲线程,则等待
        future = pool.submit(task, i)
        future.add_done_callback(done) #########
    
    pool.shutdown(True)  # 等待所有线程执行完成后才往下走(类似于线程中的join()方法)
    print('END')
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    1.1.11给进程池中的每一个进程添加额外的任务

    from concurrent.futures import ProcessPoolExecutor
    from multiprocessing import cpu_count
    
    # 子进程
    def return_str(num):
        return f'hello world{num}'
    
    
    # 子进程结束后的回调函数
    def print_str(response):
        print(response.result())
    
    
    if __name__ == '__main__':
    
        p_pool = ProcessPoolExecutor(cpu_count())  # 创建和自己电脑cpu核数相同进程数的进程池
        
        for i in range(100):
            # 给进程池添加任务
            fur = p_pool.submit(return_str, i)
            fur.add_done_callback(print_str)  # 添加回调函数
        
        # 让主线程等待子线程执行完再往下走
        p_pool.shutdown()
        
        print("END")
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    1.1.12协程(不会因为类似于IO的操作而阻塞)

    协程也可以被称为微线程,是一种用户态内的上下文切换技术,在开发中结合遇到IO自动切换,就可以通过一个线程实现并发操作。

    个人理解:在同一个线程中,有很多任务,为了提高执行效率,可以让这些任务同时执行(宏观上并发,微观上是串行),但是在某一个具体的任务中会遇到IO操作(或其他可能引起阻塞的操作),所以可能会有短暂的阻塞,此时就可以跳到另一个任务去执行,这样就保证了CPU一直处于被调度的状态,高效地利用了资源。

    2.单例模式

    通常情况下,每次实例化一个新对象时,都会为对象开辟新的空间,这样在一定程度上拉低了程序的效率。而单例模式就是用来解决这个问题的,在单例模式中,同一个类下所有的对象都共用同一个地址。

    python实现单例模式:

    class Singleton:
        instance = None
    
        def __new__(cls, *args, **kwargs):
            # 在创建一个新的对象时,首先判断有没有已经存在的对象
            # 如果没有存在的对象就创建一个新的对象
            # 这样就保证了所有的对象使用的都是同一个地址
            if not cls.instance:
                cls.instance = object.__new__(cls)
        
            return cls.instance
    
    
    if __name__ == '__main__':
        obj1 = Singleton()
        obj2 = Singleton()
    
        print(obj1)
        print(obj1)
        print(obj1 == obj2)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3.闭包

    • 闭包指延伸了作用域的函数,其中包含函数定义体中引用,同时包含不在定义体中定义的非全局变量。
      
      • 1
    • 关键是它能访问定义体之外定义的非全局变量!!!
      
      • 1
    # 闭包指延伸了作用域的函数,其中包含函数定义体中引用,同时包含不在定义体中定义的非全局变量。
    # 关键是它能访问定义体之外定义的非全局变量!!!
    # 利用闭包实现avg(num):计算不断增加系列值的均值
    def make_avager():
        history = []
    
        def avg(num):
            history.append(num)
            return sum(history) / len(history)
    
        return avg
    
    if __name__ == '__main__':
        avg = make_avager()
        print(avg(1))
        print(avg(2))
        print(avg(3))
        print(avg(4))
        print(avg(5))
    
        #avg.__closure__中的各个元素对应于avg.__code__.co_freevars中的一个名称
        print(avg.__code__.co_freevars)
        print(avg.__closure__) #对应于history
        print(avg.__closure__[0].cell_contents) #history的值
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iNuBEm3s-1660988023945)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1647003515639.png)]

    4.修饰器

    概念:函数修饰器用于在源码中“标记”函数,以某种方式增强函数的行为。

    • 装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后将其返回,或者将其替换成另外一个函数或可调用对象。
    • 装饰器的两大特性
      • 能把被装饰的函数替换成其他的函数
      • 装饰器在加载模块时立即被执行(即在使用import导入模块时)

    装饰器的典型行为:把被装饰的函数替换成新的函数,二者接受相同的函数,并且返回被装饰的函数本该返回的值,同时还会做一些额外的操作。(在不修改源代码的前提下增加新的功能。)

    import time
    
    # 一个简单的装饰器,输出函数的运行时间
    def clock(func):
        @wraps(func)  # wraps装饰器会把__name__,__doc__等属性,从func(被装饰的函数)复制给clocked(实际运行的函数)
        def clocked(*args, **kwargs):
            t0 = time.perf_counter()
            result = func(*args, **kwargs)  # 通过*args获取func的参数
            elapsed = time.perf_counter() - t0
            name = func.__name__
            arg_str = ','.join(str(arg) for arg in args)
            print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
            return result
    
        return clocked
    
    @clock
    def factorial(n):
        return 1 if n < 2 else n * factorial(n - 1)
    
    
    if __name__ == '__main__':
        print(factorial(100))
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    5.python慢的原因

    python慢的原因主要有两个,首先我们知道python是强类型动态解释性语言,慢就慢在动态和解释!

    • 因为它是动态类型语言,所以在执行的时候需要额外检查变量的类型
    • 与编译性语言不一样的是,编译性语言是首先把所有的程序编译成二进制文件,然后计算机可以一次性执行,而解释性语言需要解释一行执行一行

    6.hash冲突怎么解决

    什么是hash冲突?

    由于hash表的大小是有限的,而要存储的数据总是无限的,因此对于任何hash函数,都会出现两个不同的元素映射到同一个位置上,这种情况就叫做hash冲突。

    解决方法:开放寻址法,拉链法

    开放寻址法

    如果hash函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。

    • 线性探查:如果位置i被占用,则探查i+1,i+2…
    • 二次探查:如果位置i被占用,则探查i+1²,i-1²,i+2²,i-2²,i+3²,i-3²…
    • 二度hash,有n个hash函数,当使用第1个hash函数h1发生冲突时,则尝试使用h2,h3

    拉链法

    hash表每一个位置都连接一个链表,当冲突发生时,冲突的元素将被追加到该位置链表的最后。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uI3PydvY-1660988023946)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1658214053698.png)]

    7.深拷贝和浅拷贝

    对于列表,元组等序列,不管是深拷贝还是浅拷贝,都会创建新的对象,但是,对于浅拷贝,序列里的元素是对原序列元素的引用(地址拷贝),对于深拷贝,序列里的元素是创建的新的对象(值拷贝)。

    特例:共享字符串,“热门数字”等等,为了节省空间,一些”常见“的变量会驻留内存,在一次程序执行的过程中,不管是深拷贝还是浅拷贝,它们的地址都不会改变。默认做浅拷贝(如下):

    img

    8.Django

    8.1如何理解restful

    RESTful API是指符合REST风格的web接口。是一种设计风格,而不是标准。

    • post——新增资源
    • delete——删除资源
    • put——更新资源
    • get——获取资源

    8.2csrf_token的作用原理

    在请求表单时,Django会同时传一个字段给前端,同时将这个字段保存在服务端,前端提交表单给服务端时,服务端会首先验证这个字段是否和服务端的一致,如果是一致的,才会继续后面的业务逻辑。

    8.3middleware中间件中各个接口的调用方法

    1. process_request(self,request):一般情况下,我们在这里做一些校验,比如用户登录或者HTTP中是否有认证头之类的验证。这个方法可以有两种返回值:HttpResponse和None,如果返回HttpResponse,后面只会执行process_response;如果是None,则会继续执行其他方法。
    2. **process_view(self,request,func,*args,kwargs):这个方法是在process_request后执行的,其中func就是我们要执行的view方法,在这里可以统计view的执行时间,返回逻辑同process_request。
    3. process_template_response(self,request,response):在执行了上面两个方法后,如果使用了模板的response(通过return render方式返回的response),就会来到这里。
    4. process_response(self,response):当所有流程都执行完毕后,就来到了这里,与process_template_response不一样的是,process_response是面向所有response的。
    5. process_exception(self,request,exception):上面的所有方法都是按顺序执行的,这个方法只有在发生异常时才会执行;但需要注意的是,如果在process_view中手动调用了func就不会执行这个了。

    9.python中的垃圾回收机制

    ###引用计数法

    在cpython解释器中,提供的是引用计数法来进行垃圾回收的。其实,python中的每一个对象都对应于c中的一个结构体,在创建python对象前,cpython解释器会生成一个循环的双向链表,然后依次将这些python对象加入双向链表中,而这些python对象的结构体中,有一个公共的属性ob_refcnt,就是保存对当前对象的引用数量,初始值为1。

    回收规则

    • 增加一次对当前对象的引用,其对应的ob_refcnt值就加一
    • 减少一次对当前对象的引用,其对应的ob_refcnt值就减一,例:使用del
    • 当ob_refcnt的值变为0时,就将当前的对象进行回收

    引用计数存在的问题

    循环引用

    a = [1, 2, 3]  # 创建对象a,a的引用次数为1
    b = [4, 5, 6]  # 创建对象b,b的引用次数为1
    
    a.append(b)  # 将b添加到a中,对b进行了一次引用,b的引用次数加一后为2
    b.append(a)  # 将a添加到b中,第a进行了一次引用,a的引用次数加一后为2
    
    del a  # 删除a变量,a的引用次数减一为1,不为0,不会将a回收,其任然驻留在内存,但此时已无法使用a
    del b
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到,如果仅仅使用引用计数进行垃圾回收,在碰到循环引用时,一些经删除后的变量已无法使用,但是它们任然没有被回收,一直驻留在内存,造成内存泄漏,大量的内存泄漏必然导致程序最后无法执行。

    为了弥补引用计数的不足,出现了标记清除分代回收来弥补它的不足。

    标记清除

    目的:为了解决引用计数法无法解决循环引用导致的内存泄漏的问题而出现的。

    实现:在python底层维护一个链表,用来保存那些存在循环引用的对象(list,tuple,set,dict)。一定条件下触发扫描条件,会扫描这个链表,发现如果某个对象是循环引用,就将其对应的引用计数次数减一,引用计数为0后就将其清除。

    分代回收

    将可能存在循环引用的对象维护成三个链表:

    • 0代:0代中对象个数达到700个扫描一次
    • 1代:0代扫描10次,则1代扫描一次
    • 2代:1代扫描10次,则2代扫描一次

    2.数据库

    2.1mysql

    2.1.1MySQL存储引擎

    存储引擎其实就是如何存储数据,如何为存储的数据建立索引和如何更新,查询数据等技术的实现方法。因为在关系数据库中数据的存储是以表的形式存储的,所以存储引擎也可以称为表类型(即存储和操作此表的类型)。在oracle和sql server等数据库中只有一种存储引擎,所有的数据存储管理机制都是一样的。而MySQL数据库提供了多种存储引擎。用户可以根据不同的需求为数据表选择不同的引擎,用户也可以根据自己的需求编写自己的存储引擎。

    MySQL支持的存储引擎如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eKmOdLSp-1660988023947)(C:/Users/LLL03/Desktop/python/8.面试/imgs/1646552555967.png)]

    innodb和mysiam的区别

    角度Innodbmysiam
    事务支持事务,且是事务安全的,每一条sql语句都默认封装成事务不支持
    支持行级锁和表锁仅支持表锁
    索引不支持全文索引(5.7已支持)支持全文索引
    适用场景大型安全的应用效率快,跨平台支持,适用于小型应用
    外键支持不支持

    如何选择:

    • 是否需要支持事务,如果是可以选择innodb,否则可以考虑myisam
    • 如果表中绝大多数都只是读查询,可以考虑myisam;如果读写都很频繁,选择innodb
    • 系统崩溃后,myisam较难恢复
    • MySQL5.5之后默认是innodb,之前是myisam,如果真的不知道用什么,那就用默认的,不会太差

    2.1.2MySQL优化(索引优化)

    2.1.2.1概念

    MySQL官方对索引的定义:索引是帮助MySQL高效获取数据的数据结构

    当数据量大的时候,索引的数据量也很大,所以索引不可能全部放到内存中,因此索引一般以文件的形式存储到硬盘上

    2.1.2.2索引算法种类
    • B-tree索引
    • B±tree索引
    • Hash索引
    • full-text索引
    • R-tree索引
    2.1.2.3索引的优势
    • 类似于书目录,提高了查找效率
    • 通过索引对数据进行排序,降低了数据排序成本
    2.1.2.4索引劣势
    • 实际上索引也是一张表,该表保存了主键和索引字段,并指向实体表的记录,所以索引也是要占空间的。
    • 虽然索引大大提高了查询速度,同时却降低了更新表的速度。因为更新表时,MySQL不仅要更新数据,还需要同步更新索引表
    2.1.2.5索引分类
    • 普通索引:MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值。
    • 单值索引:即一个索引只包含单个列,一个表可以有多个单值索引
    • 复合索引:即一个索引包含多个列
    • 唯一索引:索引列的值必须唯一,但允许为空
    • 全文索引:只能在文本类型CHAR,VARCHAR,TEXT类型字段上创建全文索引。字段长度比较大时,如果创建普通索引,在进行like模糊查询时效率比较低,这时可以创建全文索引。 MyISAM和InnoDB中都可以使用全文索引。
    //创建索引
    create [unique] index indexName on tableName(columnName(length));
    //注意: 如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定length
    
    //修改索引
    alter tableName add [unique] index [indexName] on (columnName(length));
    
    //删除索引
    drop index [indexName] on tableName;
    
    //查看索引
    show index from tableName;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    2.1.2.6哪些情况适合建立索引
    • 主键自动建立唯一索引
    • 频繁作为查询条件的字段应该建立索引
    • 查询中与其他表关联的字段,外键关系建立索引
    • 查询中排序的字段建立索引,将大大提高排序效率
    • 查询中统计或者分组字段
    2.1.2.7哪些情况不适合建立索引
    • 表记录少
    • 经常需要更新的表
    • 如果某个数据列包含许多重复的内容,为它建立索引没有太大的实际效果

    2.1.3索引优化

    2.1.3.1全部用到索引
    • 建立的复合索引包含了几个字段,查询的时候最好能全部都用上,而且严格按照索引顺序,这样查询效率是最高的。
    2.1.3.2最左前缀法则
    • 如果建立的是复合索引,索引的顺序要按照建立时的顺序,即从左到右
      • 假设索引建立的顺序为a->b->c,无效索引如下:
      • a->c:a有效,c无效
      • b->c:b,c都无效
      • c:c无效
    2.1.3.3不要对索引做以下处理
    • 计算,如:+,-,*,/,!=,<>,is null,is not null
    • 函数:如:sum(),round()
    • 手动/自动类型转换,如:id=“1”,本来是数字,给写成字符串了
    2.1.3.4索引不要放在范围查询的右边
    • 比如复合索引:a->b->c,当where a=“” and b>10 and c=“”;这时候只能用到a和b,c用不到索引,因为在范围之后的索引都失效
    2.1.3.5减少select *的使用
    • 最好select查询字段和where中使用的索引字段一致
    2.1.3.6like优化
    • 失效情况

      • like “%张三%”
      • like “%张三”
    • 解决方案

      • 让like字段是查询字段,即 select name from table where name like “%张三%”;

      • 使用like “张三%”,即select name,age from table where name like “张三%”;

    2.1.3.7order by优化

    当查询语句中使用order by进行排序时,如果没有使用索引进行排序,会出现filesort文件内排序,这种情况在数据量大或者并发高的时候,会有性能问题,需要优化

    • filesort出现情况举例

      • order by字段不是索引字段
      • order by字段是索引字段,但是select中没有使用覆盖索引,如select * from table order by age asc;
      • order by中同时存在asc升序排序和desc降序排序
      • order by多个字段排序时,不是按照索引顺序进行的,即不是按照最左前缀法则
    • 解决方案

      • 使用主键索引排序
      • 按照最左前缀法则
      • 不在数据库中排序,在代码层面排序
    2.1.3.8B树和B+树的区别

    B树和B+树的主要区别在于非叶节点是否存储数据,B树在非叶节点会存储数据,而B+树的所有数据都存在叶节点中;同时,在B+树中,叶节点之间使用双向的指针相连,构成了一个双向的有序链表。

    Mysql存储引擎由B树升级到B+树的主要原因是:优化范围查询的性能,在B+树中,当查找到范围内的第一个值后,直接通过叶子节点之间的指针查找后面满足条件的值即可,而B树只能对范围内的每一个值都进行查询。

    2.1.4ACID特性

    • 原子性(Atomicity)
    • 一致性(Consistency)
    • 隔离性(Isolation)
    • 持久性(Durability)
    原子性(Atomicity)

    原子性是指事务是一个不可分割的单位,是最小的操作单元,在一个事务中的所有sql语句,要么全部执行成功,要么全部执行失败,在执行过程中,只要有一条sql语句执行失败,就会执行回滚操作,将数据恢复到执行当前事务之前的状态。能保证原子性的一个重要东西就是undo log,这是一个日志文件,在执行sql语句时,会同时将执行记录写入这个日志文件,如果事务提交失败,就会根据这个undo log日志,执行相反的操作(回滚),比如:如果执行了一个insert操作,就会执行delete操作,如果执行了delete操作就会执行insert操作,如果执行了update操作,就会执行相反的update操作,将值恢复到之前的值。

    一致性(Consistency)

    事务执行之后,数据库的完整性约束没有被破坏,事务执行前后都是一个合法的数据状态。完整性体现在比如数据库的主键要唯一,字段类型大小要符合要求,外键的约束要符合要求。一致性是事务追求的最终目标。原子性、持久性、隔离性都是为了保证数据库最终的一致性。如果另外三个特性无法保证,那么一致性肯定也保证不了 。

    隔离性(Isolation)

    **与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。**隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

    • 写-写操作:锁
    • 写-读操作:mvcc
    • 脏读,不可重复读,幻读

    事务的隔离级别

    SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:

    • 脏读:当前事务A可以读到其他事务未提交的数据。
    • img
    • 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样。
    • img
    • 幻读:在事务A中按照某个条件先后查询两次数据库,两次查询结果的条数不同。
    • img
    • 不可重复读和幻读可以通俗的理解为:前者在两次查询中数据变了;后者在两次查询中,数据的条数变了。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fBHsIhKD-1660988023949)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1174710-20190128201034603-681355962.png)]

    持久性(Durability)

    持久性是指事务一旦提交,它对数据库的改变是永久性的。

    通过redo log实现的。

    • redo log存在的背景:MySQL的数据是存在磁盘中的,每次读取或者写入数据的io操作效率就会很低,所以innodb提供了一个缓存buffer,用来存取部分数据页的数据,所以每次在读取数据的时候就先到缓冲区获取数据,如果获取不到再查询数据库,之后再放到buffer;当向数据库写入数据时,也是先写入buffer中,然后定期将buffer中的数据更新到磁盘;那么这个时候就出现了一个问题,虽然读写效率提升了,但是它也增加了数据丢失的风险,如果buffer中的数据还没来得及写入磁盘,这个时候MySQL突然宕机了,那么bufer中的数据就会丢失,进而造成数据的丢失,数据的丢失就造成了事务的持久性无法保证。
    • 引入redo log之后的流程:在将数据写入buffer之前,先写入redo log,那么在MySQL宕机之后就可以根据redo log来恢复了。redo log是预写式日志,会将所有的修改写写入日志,再写到buffer中,这样就保证了所有的数据都不会丢失。

    redo log什么时候写入磁盘?

    redo log在同步到磁盘之前,它是在缓存区中,它的更新机制是通过语句innodb_flush_log_at_trx_commit命令来控制,有三个可控选项:

    • 0:表示提交事务时,并不将缓冲区的redo log写入磁盘,而是等待主线程每秒刷新
    • 1:在事务提交时将缓冲区的redo log同步写入磁盘,保证一定会写入成功。
    • 2:在事务提交时将缓冲区的redo log异步写入磁盘,即不一定能保证写入成功,只是有这个动作。

    2.2.1缓存

    a.缓存雪崩

    缓存雪崩是指缓存同一时间大面积失效,所以,后面的请求都会落到数据库上面,造成数据库短时间承受大量的请求而崩掉。
    解决方案:

    • 缓存数据的过期时间设置为随机,防止同一时间大量数据过期现象发生
    • 互斥锁(在大量请求请求同一数据时,如果缓存中的数据过期,开启一把互斥锁,让后面的请求都处于等待状态,让第一个请求去访问数据库,在第一个请求拿到数据,更新到缓存中后,打开互斥锁,让后面的请求都直接拿缓存中的数据,这样就避免了同一时间大量的数据库请求)

    b.缓存穿透

    缓存穿透是指缓存和数据库中都没有数据,导致所有请求都落在数据库上,造成数据库短时间内承受大量请求而崩掉。

    解决方案:

    • 接口层增加校验,比如用户校验,id校验,比如id<0直接拦截
    • 鉴于这种情况一般是恶意攻击,在缓存中拿不到数据,在数据库中也拿不到数据时,可以将缓存中的key-value设置为key-null,并将缓存时间设置短些,这样就可以避免同一时间的反复攻击

    c.缓存击穿

    缓存击穿是指缓存中没有数据,但数据库中有(一般是缓存过期造成),这时由于并发用户特别多,同时读缓存没有读到数据,又同时去读数据库,引起数据库压力瞬时增大。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同的数据都过期了,很多数据都查不到从而查数据库。

    解决方案:

    • 设置热点数据永不过期(同时也应定时更新这些热点数据)
    • 互斥锁(同缓存雪崩)

    2.2.2redis

    rehash的过程

    redis的字典,底层有两个数组,以及一个重要的字段rehashidx,用来控制rehash,默认值是-1,如果其值不为-1,就说明正在rehash这个过程中;默认情况下,数组1保存着元素,数组2为NULL,当元素个数和数组长度一样时,就会发生扩容,此时会为数组2开辟2倍数组1大小的空间,同时rehashidx由默认值变为0(rehashidx!=-1标志着在rehash状态!!!),之后对字典的crud操作,都会进行一次单步的rehash操作,比如:在字典中新增一个元素时,会首先迁移数组1中的一个元素到数组2中,之后再进行新增操作,并且新增的元素会直接添加到数组2中,当数组1中的元素个数为0时,rehash结束,此时rehashidx的值变为-1,标志着rehash结束。

    redis中默认的数据库有16个,默认使用dbid=0的数据库。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nrTIn7p5-1660988023949)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1648280203367.png)]

    //可以使用select index进行数据库的切换
    select 5  //使用dbid=5的数据库
    
    //设置值
    set key val
    //批量设置
    mset k1 v1 k2 v2 k3 v3 ...
    
    //设置值的同时设置过期时间
    setex key seconds val
    
    //先判断key是否存在,不存在才能设置(适合用于分布式)
    setnx key val 
    //批量setnx操作
    msetnx k1 v1 k2 v2 k3 v3 ... //需要注意的是,这个是原子性的操作,只要其中有一个不能设置成功,所有的都不会成功,即要么一起成功,要么一起失败!
    
    //取值
    get key
    //批量获取
    mget k1 k2 k3 ...
    
    //先get再set
    getset key val
    
    //设置对象(设置1号user的属性和值的两种方式,mset效率会更高) 
    set user_1 {k1:v1,k2:v2,k3:v3...}
    mset user_1_k1 v1 user_1_k2 v2 user_1_k3 v3 ...
    
    //获取当前数据库的大小
    dbsize
    
    //清空当前数据库
    flushdb
    
    //清空所有数据库
    flushall 
    
    //判断某个键是否存在
    exists key
    
    //移除某个kv
    move key db
    
    //为某个键kv设置过期时间
    expire key seconds
    
    //查看某一个kv还有多长时间过期
    ttl key 
    
    //查看v的类型
    type key
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    List

    • 本质是一个链表!
    //添加一个或多个元素到列表中(从左边添加)
    lpush key val1 val2 val3 ...
    
    //从右边添加
    rpush key val1 val2 val3 ...
    
    //范围取值
    lrange key start stop
    
    //索引取值(从下标0开始)
    lindex key index
    
    //移除值,并返回值(从左边开始移除)
    lpop key
    
    //从右边开始移除
    rpop key
    
    //获取列表的长度
    llen key 
    
    //移除指定数量的某个值(count指定数量,val指定值,从列表的左边开始寻找符合要求的值)
    lrem key count val
    
    //截断,移除指定索引范围内的元素
    ltrim key start stop
    
    //更新值(执行该命令的前提条件:key必须存在,index没有超出范围)
    lset key index val
    
    //插入值(在值pivot的前面或者后面插入val,如果找不到pivot会失败,从列表的左边开始找起,找到第一个为止)
    linsert key before|after pivot val
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    Set

    //给集合添加成员
    sadd key member1 member2 ...
    
    //查看集合中的所有成员
    smembers key
    
    //查看集合中成员的个数
    scard key
    
    //查看某个成员是否在集合中
    sismember key member
    
    //随机获取指定数量的成员(默认获取一个)
    srandmember key [count]
    
    //随机移除指定数量的成员(默认是一个)
    spop key [count]
    
    
    //组合命令
    //将一个集合中的值移动到另一个集合中
    smove source destination member
    
    
    //差集
    sdiff key1 key2 ...
    
    //交集
    sinter key1 key2 ...
    
    //并集
    sunion key1 key2 ...
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    Hash

    • 类似于key-map的形式
    //给某一个hash表添加k-v
    hset key field val
    
    //同时设置多个值
    hmset key field1 val1 field2 val2 ...
    
    //获取值
    hget|hmget key field [field2...]
    
    //获取所有值
    hgetall key
    
    //删除值
    hdel key field1 field2...
    
    //获取hash表的长度
    hlen key
    
    //判断某一个字段是否在hash表中
    hexists key field
    
    //获取hash表所有的键
    hkeys key
    
    //获取hash表所有的值
    hvals key
    
    //让hash中某个字段的值加一[减一] increment指定增减数量
    hincreby key field increment
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    zset

    • 有序集合,在设置成员的同时,为成员设置了一个score,之后可以根据这个score对成员进行排序

    例:利用工资对成员进行排序

    //添加成员
    zadd key score1 member1 score2 member2...
    
    //按照score进行升序排列(min,max指定score的区间)
    zrangebyscore key min max [withscores]
    
    //降序排列(start和stop指定起始位置)
    zrevrange key start stop [withscores]
    
    //移除成员
    zrem key member
    
    //获取集合的大小
    zcard key
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CYMbYQtk-1660988023949)(C:\Users\LLL03\Desktop\python\8.面试\imgs\1648956791979.png)]

    redis是单线程的!为什么它还很快?

    redis为什么是单线程?

    官方解释CPU并不是您使用Redis的瓶颈,因为通常Redis要么受内存限制,要么受网络限制。例如,使用在一般Linux系统上运行的流水线Redis每秒可以发送一百万个请求,因此,如果您的应用程序主要使用O(N)或O(log(N))命令,则几乎不会使用过多的CPU 。但是,为了最大程度地利用CPU,您可以在同一服务器上启动多个Redis实例,并将它们视为不同的服务器。在某个时候,单个实例可能还不够,因此,如果您要使用多个CPU,则可以开始考虑更早地分片的某种方法。但是,在Redis 4.0中,我们开始使Redis具有更多线程。目前,这仅限于在后台删除对象,以及阻止通过Redis模块实现的命令。对于将来的版本,计划是使Redis越来越线程化。
    既然redis的瓶颈不是cpu,那么在单线程可以实现的情况下,自然就使用单线程了。

    tips:

    • 但是,我们使用单线程的方式是无法发挥多核CPU 性能,不过我们可以通过在单机开多个Redis 实例来完善!

    • 这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis Server运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行 。

    快的原因!
    1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
    2. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作;
    3. 使用多路I/O复用模型( 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。 ),非阻塞IO;
    4. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

    3.数据结构

    3.1常见的排序算法比较表

    排序平均情况最好情况最坏情况稳定与否空间复杂度
    冒泡排序O(N2)O(N)O(N2)稳定1
    选择排序O(N2)O(N2)O(N2)不稳定1
    插入排序O(N2)O(N)O(N2)稳定1
    快速排序O(NlogN)O(NlogN)O(N2)不稳定O(logN)
    归并排序O(NlogN)O(NlogN)O(NlogN)稳定O(N)
    堆排序O(NlogN)O(NlogN)O(NlogN)不稳定1

    4.操作系统

    4.1.页面置换算法

    4.1.1OPT( OPTimal最佳置换算法)

    最佳置换算法,其所选择淘汰的页面是以后永不使用的,或者在最长时间内不会被访问的页面。

    最佳置换算法是一种理想状态,因为无法预知,所以该算法是无法实现的,采用最佳置换算法可保证获得最低的缺页率。

    实用价值:给别的置换算法作参考对比。

    4.1.2FIFO(First Input first Output先进先出页面置换算法)

    FIFO算法是最早出现的置换算法。该算法总是淘汰最新进入内存的页面,即选择在内存中驻留时间最长的页面进行淘汰。

    4.1.3LRU(Least Recnetly Used最近最久未使用算法)

    如果一个数据在最近一段时间没有被访问,那么可以认为它在将来被访问的可能性很小,因此,当空间满时,最久没有被访问的就会最新被淘汰。

    个人理解:LRU算法是对FIFO算法的一种优化,在FIFO中先进的一定是最久未使用的,但是在LRU中,如果后面进入的页面是命中最先进入的页面时,该页面就会被更新,从而不是“最久未使用的”。

    4.1.4LFU(Least Frequently Used最近最少访问算法)

    如果一个数据在最近一段时间很少被访问,那么可以认为它在将来被访问的可能性很小,因此,当空间满时,访问频率最小的数据最先被淘汰。

    4.2进程间的通信

    共享内存

    在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行读写操作实现进程之间的信息交换。在对共享空间进行读写操作时,需要使用同步互斥工具,对共享空间的读写进行控制。

    消息传递

    在消息传递系统中,进程间的数据交换以格式化的消息为单位。若通信的进程间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发消息和接受消息两个原语进行数据交换。这种方式隐藏了通信实现的细节,使通信过程对用户透明,简化了通信程序的设计,是当前应用最广泛的进程间通信机制。

    • 直接消息传递
    • 间接消息传递

    管道通信

    管道通信是消息传递的一种特殊方式。所谓“管道”,是指通过用于连接一个读进程和一个写进程以实现它们之间的通信的一个共享文件,又名pipe文件。

    4.2虚拟内存

    把应用程序在逻辑上的地址映射到真实的物理内存上,这种逻辑上的内存就是虚拟内存。

    基于局部性原理,在程序装入时,仅需要将程序当前要运行的少数页面或段先装入内存,而将其余部分暂留在外存,便可启动程序执行。在程序装入的过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后执行程序。另一方面,操作系统将内存中暂不使用的内存搬到外存上,从而腾出空间放将要调入内存的信息。这样,系统好像为用户提供了一个比实际内存容量大很多的存储器,称为虚拟存储器。

    5.计算机网络

    5.1TCP/IP五层网络模型

    网络协议是分层的,每一层都有各自的作用和职责,我们常见的网络协议有五层,从上到下依次是:

    • 应用层:为用户提供功能

    • 传输层:应用层的数据包会传给传输层,传输层是为应用层提供网络支持的,传输层有两个传输协议:TCP和UDP。

    • 网络层:真正负责传输功能的一层,常用的协议是IP协议

    • 数据链路层:为⽹络层提供链路级别传输的服务

    • 物理层:把数据包转换成电信号,让其可以在物理介质中传输

    5.2TCP和UDP的区别

    不管是TCP还是UDP,它们都属于传输层的协议。它们之前的主要区别如下:

    TCP(Transmission Control Protocol),传输控制协议UDP(User Datagram Protocol),用户数据报协议
    连接性面向连接无连接
    可靠性可靠传输,不丢包不可靠传输,可能会丢包
    首部占用空间
    传输速率
    资源消耗
    应用场景浏览器,文件传输,邮件发送音视屏通话,直播
    应用层协议HTTP,HTTPS,FTP,SMTP,DNSDNS
    服务对象TCP是一对一的两点服务,即一条连接只有两个端点UDP支持一对一,一对多,多对多

    TCP是面向连接的,虽然资源消耗大,但是可靠传输,只支持一对一的两点服务;UDP是无连接的,虽然是不可靠传输,但是资源消耗小,传输效率高,支持一对多,多对多的服务。

    5.3.1 TCP的三次握手

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cfyAgPTL-1660988023950)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220819161554539.png)]

    TCP三次握手就是建立一个TCP连接

    TCP的连接主要解决以下三个问题:

    • 是TCP双方能够确认对方的存在

    • 协商一些参数(如果最大窗口值,是否使用窗口扩大选项等等)

    • 使TCP双方能够对运输实体资源(如缓存大小,连接表中的数据等)进行分配。

      TCP建立连接的过程:

    1. 服务端创建传输控制块,进入监听状态:首先,TCP服务器进程块会创建传输控制块,用来存储TCP连接中一些重要的信息(例如TCP连接表,指向发送和接受缓存的指针,指向重传队列的指针,当前的发送和接受序号等等),此时,TCP服务进程就进入监听状态,等待客户端的连接请求。
    2. 客户端创建传输控制块
    3. 客户端向服务端发送TCP连接请求报文段,并进入同步已发送状态,TCP连接请求报文段首部中的同步位SYN被设置为1(SYN被设置为1的报文不能携带数据,但要消耗掉一个序号),序号字段seq被设置了一个初始值x,作为TCP客户进程所选择的初始序号。
    4. 如果服务端同意建立连接,在接收到连接请求后,则向客户端发送确认报文段,并进入同步已接收状态,该报文端首部中的同步位SYN和确认位ACK都设置为1(表明这是一个TCP连接请求确认报文段),序号字段seq被设置了一个初始值y,作为服务端进程所选择的初始序号,确认字段ack的值被设置成了x+1,这个是对客户端初始序号的确认。
    5. 客户端收到服务端的确认报文段后,还要想向务端发送一个普通的TCP确认报文段,并进入连接已建立状态,该报文段首部中的ACK=1,序号字段seq=x+1,确认字段ack=y+1(对服务端序号的确认)
    6. 服务端收到客户端的第二个报文段后,进入连接已建立状态,此时,TCP双方就都进入了连接已建立状态,此时就可以基于已经建立的连接进行可靠的数据传输了!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1qBFlIyP-1660988023950)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220819151844153.png)]

    5.3.2TCP的四次挥手

    数据传输结束后,TCP双方都可以释放连接。

    假设客户端的应用进程通知其主动关闭TCP连接,则四次挥手过程如下:

    1. 客户端发送连接释放报文,并进入终止等待1状态。该报文段中的终止位FIN(终止位FIN=1的报文不能携带数据,但要消耗一个序号)和确认位ACK的值都为1(表明这是一个TCP连接释放报文段),同时也对之前手收到的报文段进行确认ack=v,序号seq=u(它等于客户端已传送过的最后一个报文的序号值加一)。
    2. 服务端收到客户端的连接关闭报文后,进入关闭等待状态。并返回一个确认报文段,该报文段首部的确认位ACK=1,seq=v,ack=u+1,此时TCP服务器要通知高层应用进程:TCP客户进程要断开与自己的TCP连接,此时,从客户端到服务端这个方向的连接就释放了(此时,数据只能从服务端发往客户端,而不能从客户端发往服务端)。
    3. 客户端收到服务端的确认报文后,客户端进入终止等待2状态,此时客户端若没有数据再要发送,等待一段时间后,服务端就向客户端发送连接释放报文段,进入最后确认状态。该报文段首部中的终止位FIN和确认位ACK都为1,同时也对之前收到的报文段进行确认ack=u+1,seq=w。
    4. 客户端收到服务端的连接释放报文后,进入时间等待状态。同时想服务端发送确认报文段,该报文段首部中的确认位ACK=1,seq=u+1,ack=w+1,TCP收到该报文后,就进入关闭状态,而客户端还要经过2MSL(Maximum Segment Lifetime:最长报文段寿命)后才能进入关闭状态。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q2RxzZiU-1660988023951)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220819161431995.png)]

    5.4 TCP的拥塞控制

    什么是拥塞

    在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏。这种情况就叫做拥塞。

    拥塞控制原则

    发送方维护一个叫拥塞窗口cwnd的状态变量,其值取决于网络的拥塞程度,并且动态变化。拥塞窗口cwnd的维护原则:只要网络没有出现拥塞,拥塞窗口就再增大一些;但只要网络出现拥塞,拥塞窗口就减小一些。

    发送方将拥塞窗口作为发送窗口swnd,即swnd=cwnd,同时会维护一个慢启动门限ssthresh状态变量:

    • 当cwnd
    • 当cwnd>ssthresh时,停止使用慢开始算法而改用拥塞避免算法
    • 当cwnd=ssthresh时,即可以使用慢开始算法也可以使用拥塞避免算法

    判断出现网络拥塞的依据:没有按时收到应当达到的确认报文(即发生超时重传

    超时重传

    假设在某次传输过程中,部分报文段丢失,发送方一直接收不到确认,发送方中的超时重传计时器就会发生超时,从而发生重传。

    发生超时重传后,发送方以此判断网络发生了拥塞,进而执行以下工作:

    1. 将慢开始门限ssthresh值更新为发生拥塞时拥塞窗口cwnd的值的一半,
    2. 之后将拥塞窗口的值设为1,
    3. 并重新开始执行慢开始算法。

    拥塞控制的四种方法

    • 慢开始(slow-start):发送方每收到一个对新报文段的确认时,就把拥塞窗口的值扩大一倍,然后开始下一轮的传输,当增大到慢开始门限是就会停止使用慢开始,转而使用拥塞避免。

    • 拥塞避免(congestion avoidance):发送方每收到一个对新报文段的确认时,就把拥塞窗口的值加1,然后开始下一轮的传输

    • 快重传(fast retransmit):就是使发送方尽快发生重传,而不是等超时重传计时器超时再重传。(应对错误的判断为网络拥塞的情况)

    • 快恢复(fast recovery):发送方将慢开始门限ssthresh和拥塞窗口cwnd调整为当前窗口的一半,从而避免执行慢开始算法,降低传输效率。

    有时,个别报文段在网络中丢失,但是实际上并未发生网络拥塞,进而错误的执行慢开始算法,从而降低了传输效率。因此,为了避免这种错误判断,进而出现了快重传和开恢复算法。

    快重传

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rHHAEqr2-1660988023951)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220817160325703.png)]

    快恢复

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZOjqS2IT-1660988023951)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220817160705931.png)]

    拥塞控制的整个过程

    在TCP双方建立逻辑连接关系时,拥塞窗口cwnd的初始值为设置为1,即传输轮次为0时,cwnd的值为1,同时,需要设置一个慢开始门限的初始值,假设为16;

    1. 第一次传输轮次完成后,cwnd=1*2=2 cwnd
    2. 第二次传输轮次完成后,cwnd=2*2=4 cwnd
    3. 第三次传输轮次完成后,cwnd=4*2=8 cwnd
    4. 第四次传输轮次完成后,cwnd=8*2=16 cwnd=ssthresh,转到使用拥塞避免算法
    5. 第五次传输轮次完成后,cwnd=16+1=17 cwnd>ssthresh,继续使用拥塞避免算法
    6. 第11次传输完成后,cwnd=22+1=23,cwnd>ssthresh,继续使用拥塞避免算法
    7. 执行第12次传输时,cwnd=23+1=24,但是期间发生丢包,发送方一直没有收到丢失的包的确认,超时计时器超时,进行重传,进行重传后,发送方判定为网络拥塞,将慢开始门限ssthresh值变为当前拥塞窗口的一半为12,更新拥塞窗口cwnd的值为1,然后继续使用慢开始算法
    8. 第13传输轮次完成后,cwnd=1*2=2 cwnd
    9. 第15传输轮次完成后,cwnd=4*2=8 cwnd
    10. 执行到第16次轮次时,cwnd=8*2=16>ssthresh=12,此时将cwnd=ssthresh=12,后面转向使用拥塞避免算法
    11. 第17传输轮次完成后,cwnd=12+1=13 cwnd>ssthresh,继续使用拥塞避免算法,但是此时发生丢包
    12. 第17传输轮次完成后,cwnd=13+1=14 cwnd>ssthresh,继续使用拥塞避免算法,但接收方收到重复确认1
    13. 第17传输轮次完成后,cwnd=14+1=15 cwnd>ssthresh,继续使用拥塞避免算法,但接收方收到重复确认2
    14. 第17传输轮次完成后,cwnd=15+1=16 cwnd>ssthresh,继续使用拥塞避免算法,但接收方收到重复确认3,此时已经收到了3次重复确认,发送方马上对丢失的包进行重传(此时超时重传计时器仍未超时),重传完成后,进行快恢复操作,将拥塞窗口cwnd和慢开始门限的的值都变为当前窗口的一半为8,然后继续使用拥塞避免算法
    15. 第17传输轮次完成后,cwnd=8+1=9 cwnd>ssthresh,继续使用拥塞避免算法
    16. 第18传输轮次完成后,cwnd=9+1=10 cwnd>ssthresh,继续使用拥塞避免算法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XkR8B4Tt-1660988023952)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220817160922690.png)]

    5.5域名解析过程

    域名解析的作用

    我们在上⽹的时候,通常使⽤的⽅式是域名,⽽不是 IP 地址,因为域名⽅便⼈类记忆。

    那么实现这⼀技术的就是 DNS 域名解析,DNS 可以将域名⽹址⾃动转换为具体的 IP 地址。

    域名的层级关系

    DNS 中的域名都是⽤句点来分隔的,⽐如 www.server.com ,这⾥的句点代表了不同层次之间的界限

    在域名中,越靠右的位置表示其层级越⾼

    根域是在最顶层,它的下⼀层就是 com 顶级域,再下⾯是 server.com。

    所以域名的层级关系类似⼀个树状结构:

    • 根 DNS 服务器

    • 顶级域 DNS 服务器(com)

    • 权威 DNS 服务器(server.com)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ynuzRsWb-1660988023952)(C:\Users\LLL03\AppData\Roaming\Typora\typora-user-images\image-20220818093830863.png)]

    浏览器⾸先看⼀下⾃⼰的缓存⾥有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析⽂件

    hosts ,如果还是没有,就会 DNS 服务器进⾏查询,查询的过程如下:

    1. 客户端⾸先会发出⼀个 DNS 请求,问 www.server.com 的 IP 是啥,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。

    2. 本地域名服务器收到客户端的请求后,如果缓存⾥的表格能找到 www.server.com,则它直接返回 IP 址。如果没有,本地 DNS 会去问它的根域名服务器:“⽼⼤, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最⾼层次的,它不直接⽤于域名解析,但能指明⼀条道路。

    3. 根 DNS 收到来⾃本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”

    4. 本地 DNS 收到顶级域名服务器的地址后,发起请求问“⽼⼆, 你能告诉我 www.server.com 的 IP 地址吗?”

    5. 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。

    6. 本地 DNS 于是转向问权威 DNS 服务器:“⽼三,www.server.com对应的IP是啥呀?” server.com 的权威DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。

    7. 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。

    8. 本地 DNS 再将 IP 地址返回客户端,客户端和⽬标建⽴连接。

    面试反问话术

    • 觉得自己还有什么可以提升的地方?
    • 所在部门主要用的语言?
    • 推荐的书籍?

    面试反问话术

    • 觉得自己还有什么可以提升的地方?
    • 所在部门主要用的语言?
    • 推荐的书籍?
  • 相关阅读:
    Android系统编译
    客户端打开浏览器post提交数据
    第5套.py
    2023医药微信公众号排名榜top100汇总合集
    WSL增加独立的虚拟磁盘VHDX
    vue3的ref和reactive
    2022年最新湖北水利水电施工安全员考试题库及答案
    智能合约安全漏洞与解决方案
    基于SSM的电子设备销售网站的设计与实现
    正点原子嵌入式linux驱动开发——TF-A初探
  • 原文地址:https://blog.csdn.net/max_LLL/article/details/126442318