Agent开发/AI应用后端开发八股整理
Agent开发/AI应用后端开发八股整理
这里给出博主近期整理的八股知识点,包括部分后端八股+博主项目通用八股+博主实习八股,部分八股由于实习和项目的专有性做了删减,希望能帮助到需要的朋友
postgresql事务隔离级别
PostgreSQL 支持的四种隔离级别
读未提交(Read Uncommitted)
在 PostgreSQL 中,该级别实际表现等同于读已提交,不允许脏读。这是出于历史兼容而保留的语法。
读已提交(Read Committed) —— 默认级别
一个事务中的每条语句只能看到该语句开始前已提交的数据。不可重复读和幻读都可能发生。
可重复读(Repeatable Read)
一个事务只能看到事务开始前已提交的数据,整个事务期间看到的快照不变。这样避免了不可重复读,但在 PostgreSQL 中不会阻止幻读(其他数据库如 MySQL 的 RR 会通过间隙锁阻止幻读,PG 的实现不同)。
可序列化(Serializable)
提供最严格隔离,事务看起来像是串行执行的。
在读已提交级别下,每个 SQL 语句在执行时,都会获取一个该语句开始执行瞬间的数据库快照。因此,同一个事务内的不同语句可能看到不同的数据,因为其他事务可能在它们执行间隙提交了修改。
在可重复读级别下,快照是在事务开始时(执行第一条语句时)获取的,并且整个事务期间都使用这个快照。因此,无论其他事务如何提交修改,该事务始终看到的是它开始那一刻的数据状态。
一个事务读到了另一个事务尚未提交的修改数据。如果那个未提交的事务最终回滚了,那么读到的数据就是“脏”的、无效的,会引发错误的业务决策。
一个事务内,两次执行相同的范围查询,第二次读到了其他事务新插入的满足查询条件的行,这些多出来的行就像“幻影”一样。
===
MVCC
MVCC 的思路是不直接原地修改数据行,而是通过维护数据的多个版本来实现非阻塞读与写。
写操作不直接覆盖旧版本,而是创建一个新版本的数据行,同时保留旧版本。
读操作在事务开始时获取一个快照,读取满足可见性规则的某个历史版本,不需要加读锁,因此读不会阻塞写,写也不会阻塞读(除了写锁之间的冲突)。
当事务完成并提交后,其修改的版本对其他事务变为可见;未提交事务的版本对其他事务不可见。
xmin:创建该行版本的事务 ID。
xmax:删除该行版本的事务 ID(如果该行未被删除,则为 0 或未提交事务的 ID)。
可见性判断规则简化如下:
一个行版本对当前事务可见,当且仅当:
xmin 对应的事务已提交,并且 xmin < 当前事务ID(在 RR 级别下是事务开始时刻的快照边界),且
xmax 为 0,或 xmax 对应事务未提交,或该事务是在当前事务开始之后开始的(根据隔离级别处理)。
关键实现机制(InnoDB)
隐藏字段
DB_TRX_ID:记录修改行的事务ID。
DB_ROLL_PTR:指向历史版本链(Undo Log)。
Undo Log
存储数据历史版本,形成版本链供事务回溯。
Read View(读视图)
事务启动时生成快照,结合版本链确定可见的数据版本。
===
事务的四个属性
原子性 — Atomicity
定义:事务是一个不可分割的最小工作单元,其中的操作要么全部成功,要么全部失败回滚,不存在部分执行的状态。
一致性 — Consistency
定义:事务必须使数据库从一个一致性状态转换到另一个一致性状态,保证所有数据满足预定义的规则(约束、触发器、外键等)。
隔离性 — Isolation
定义:多个事务并发执行时,它们之间不应相互干扰,各个事务感觉不到其他事务的存在,就像串行执行一样。
持久性 — Durability
定义:一旦事务提交成功,其对数据库的修改就是永久的,即使系统崩溃也不会丢失。
===
并发事务的影响
脏读
影响:读到未提交数据,可能基于无效信息做决策。
不可重复读
影响:同一事务内两次读取同一行,值发生变化(被其他事务 UPDATE 并提交)
快照保证一致性读,不保证一致性写
幻读
影响:同一事务内两次范围查询,行数变化(其他事务 INSERT/DELETE 并提交)。
===
PostgreSQL 支持的索引类型和存储引擎
B-tree(默认)
用途:最通用,适用于等值、范围、排序(=、<、>、BETWEEN、ORDER BY)。
在我们的方案中:所有主键、唯一约束(如 uk_task_image)和常规查询外键(task_id、status)都使用 B-tree 索引,它是保证行锁高效定位和数据完整性约束的基础。
Heap(默认):传统的行存储,遵循 MVCC,更新会创建新元组
===
postgresql在机器故障时也能保证事务吗
是的,PostgreSQL 在机器故障时依然可以保证事务的 ACID 属性,这主要依赖其核心机制 WAL(Write-Ahead Logging,预写式日志)。
重启时,数据库会读取 WAL 日志,发现这个事务已经有“提交记录”,就会重放(Redo)这些操作,把数据文件恢复到事务完成后的样子。
恢复时,PostgreSQL 会结合 CLOG(提交日志) 判断,凡是没有提交的事务,其产生的所有变更都会被撤销(Undo)或直接丢弃,绝不会留下一半的数据。
===
postgresql主从架构
主库将 WAL 日志实时发送给从库,从库不断应用这些日志,保持和主库数据一致。
支持异步复制(性能高,但故障时可能丢少量数据)和同步复制(事务提交需等待从库确认,数据零丢失)。
实现读写分离。
===
事务和锁的区别是什么?什么情况是事务可以实现但是锁实现不了的?
(可重复读)事务的本质是空间换时间(不同的事务在不同版本的数据上进行读取,这些事务是在不同的空间中进行操作的,自然没有并发安全问题),而对于有些特殊情况(增删改和select for update)来说,只有读取当前最新的版本数据才有意义,所以多个事物在执行这些操作时都需要在一个共享的数据上进行操作,只有用锁来互斥才能保证并发安全。
===
悲观锁和乐观锁的区别及使用场景?
悲观锁(Pessimistic Locking)
假设每次操作都会冲突,所以在数据上直接加锁,强制其他事务等待或失败。
典型实现:数据库的 SELECT ... FOR UPDATE(行锁)、表锁、分布式锁(Redis/ZK)。
乐观锁(Optimistic Locking)
假设冲突是小概率事件,所以不加锁,只在最终提交时检查数据是否被改动过。如果发现被改了,就回滚并重试。
典型实现:数据表增加一个 version 字段,更新时条件带 version = 期望值,同时 version + 1。若影响行数为0,说明被抢先修改,重试整个业务逻辑。
修订前需要查询已有记录、计算新汇总值,如果乐观重试,这些计算和查询都要重来,吞吐会降低。
冲突少、能重试,就用乐观;冲突多、怕重试,上悲观。
===
行锁和表锁有什么区别
表锁:一次性锁住整张表。事务期间,其他会话无法对该表进行写操作(写锁互斥),甚至可能阻塞读(取决于锁类型)。粒度最粗。不容易死锁,大部分串行化
行锁:只锁住被访问的那几行。事务期间,其他会话可以同时访问并修改未被锁住的行,并发度最高。粒度最细。容易死锁
===
什么是幂等键
幂等键是调用方为一次业务操作生成的唯一标识,用来让服务端判断重复请求是否属于同一次操作。比如支付、退款、下单这类接口,
客户端带上 Idempotency-Key,服务端用唯一索引保存这个 key。
第一次请求插入成功后执行业务逻辑,并保存处理状态和响应结果;后续相同 key 的请求不再重复执行业务,而是返回第一次的结果。
实现时通常会用幂等表或 Redis 加数据库唯一约束
===
Workflow 和 Agent 的区别
Workflow 是人预先编排流程,模型执行其中的部分能力;
Agent 是人给目标和边界,模型自己决定执行路径。
控制粒度:Workflow每个步骤都是硬编码,Agent每个步骤都是模型推理。
可解释性:Workflow执行路径固定,出了bug好查。Agent执行路径不固定,不好查但灵活。
成本:Workflow调度成本低,Agent每次决策都要调模型,贵。
===
skill和workflow
Workflow 适合描述完整业务流程,比如“提交巡检工单 -> 派单 -> 维修 -> 验收 -> 关闭”。它强调流程状态、审批、重试和节点顺序。Skill 更像可复用能力,比如“查询能耗曲线”“判断设备是否异常”“生成维修建议”“压缩上下文记忆”。
如果一个能力会被多个流程复用,就不应该写死在某个 workflow 里。比如“设备异常归因”既可以用于告警处理,也可以用于能耗诊断,还可以用于周报生成,这种能力更适合做成 skill。Agent 可以根据用户问题动态组合 skill,而不是每个场景都写一套 workflow。
Workflow:面向完整业务流程,状态驱动
Skill:面向可复用能力,输入输出稳定
Tool:面向底层动作,比如查库、调用接口
===
线程池是如何实现线程复用的,其核心参数一般如何设置
线程池实现线程复用的核心思想是:线程创建后不立即销毁,而是在线程池中循环从任务队列里取任务执行;执行完一个任务后,线程继续等待下一个任务。
提交任务
-> 如果当前线程数 < corePoolSize,创建核心线程执行
-> 否则任务进入 workQueue 等待
-> 如果队列满了,且当前线程数 < maximumPoolSize,创建非核心线程执行
-> 如果线程数已达 maximumPoolSize,触发拒绝策略
CPU 密集型任务:
corePoolSize ≈ CPU 核数
maximumPoolSize ≈ CPU 核数 或 CPU 核数 + 1
queue 使用有界队列
IO 密集型任务:
corePoolSize 可以大于 CPU 核数
maximumPoolSize 可以是 CPU 核数的 2~4 倍,甚至更高
queue 仍建议使用有界队列
===
信号量的底层实现原理
信号量是一个用于控制并发访问数量的工具。
线程调用 acquire 时,会尝试获取一个许可。如果当前计数器大于 0,就通过原子操作把计数减 1,然后继续执行;如果计数器等于 0,说明没有可用许可,线程就会被加入等待队列并阻塞。
线程调用 release 时,会归还一个许可,也就是把计数加 1,然后唤醒等待队列中的一个或多个线程,让它们重新竞争许可。
acquire() {
lock();
while (permits == 0) {
waitQueue.add(currentThread);
park(currentThread);
}
permits--;
unlock();
}
release() {
lock();
permits++;
if (waitQueue not empty) {
unpark(oneWaitingThread);
}
unlock();
}
使用异步时 可以用async with 保证许可一致
===
面向对象的"六原则一法则"
单一职责原则:一个类只做它该做的事情 高内聚 高内聚就是一个代码模块只完成一项功能
开闭原则:对扩展开放,对修改关闭 要点:抽象是关键 封装可变性,可变因素封装到一个继承结构中
依赖倒转原则:面向接口编程 尽可能使用抽象类型
接口隔离原则:接口要小而专,绝不能大而全
合成聚合复用原则:优先使用聚合或合成关系复用代码。Is-A关系、Has-A关系、Use-A关系分别是继承、关联和依赖。关联关系根据其关联的强度又可以进一步划分为关联、聚合和合成。优先考虑Has-A关系而不是Is-A关系复用代码。不要继承工具类,工具是可以拥有并可以使用的,而不是拿来继承的
迪米特法则:最少知识原则 一个对象应当对其他对象有尽可能少的了解。减小了系统的耦合度和复杂度
===
面向对象和面向过程的区别
面向过程:
以函数为中心,把问题分解为一系列步骤
性能高 不易维护、复用、扩展
面向对象:
以对象为中心,把问题分解为多个对象
易维护、易复用、易扩展,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 性能低
===
冒泡排序
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
public class Bubble {
public int[] sort(int[] array) {
int temp = 0;
// 外层循环,它决定一共走几趟 //-1为了防止溢出
for (int i = 0; i < array.length - 1; i++) {
int flag = 0;
//通过符号位可以减少无谓的比较,如果已经有序了,就退出循环
//内层循环,它决定每趟走一次
for (int j = 0; j < array.length - i - 1; j++) { //如果后一个大于前一个,则换位
if (array[j + 1] > array[j]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp; flag = 1;
}
}
if(flag == 0){
break;
}
}
return array;
}
public static void main(String[] args) {
Bubble bubble = new Bubble();
int[] array = {2,5,1,6,4,9,8,5,3,1,2,0};
int[] sort = bubble.sort(array);
for (int num:sort){
System.out.print(num+"\t");
}
}
}
===
选择排序
每一次 选出最小(或最大)的 存放在序列的起始位置 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
不稳定
[Pic#ID/13Pr#]
public class SelectSort {
public int[] sort(int arr[]) {
int temp = 0;
for (int i = 0; i < arr.length - 1; i++) {
// 认为目前的数就是最小的, 记录最小数的下标
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
// 修改最小值的下标
minIndex = j;
}
}
// 当退出for就找到这次的最小值,就需要交换位置了
if (i != minIndex) {
//交换当前值和找到的最小值的位置
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
public static void main(String[] args) {
SelectSort selectSort = new SelectSort();
int[] array = {2,5,1,6,4,9,8,5,3,1,2,0};
int[] sort = selectSort.sort(array);
for (int num:sort){
System.out.print(num+"\t");
}
}
}
不稳定:
选快希堆
===
插入排序
将一个数据插入到已经排好序的有序数据中 稳定的排序
包括:直接插入排序,二分插入排序(又称折半插入排序),链表插入排序,希尔排序(又称缩小增量排序)。属于稳定排序的一种(通俗地讲,就是两个相等的数不会交换位置)
不能打乱相对顺序
public class InsertSort {
private int[] sort(int[]arr){
//如果传入的数组为空或者只有一个值,就直接返回
if(arr == null || arr.length < 2){
return arr;
}
//不为空则进循环判断
//外层循环控制总数量
for(int i=1;i<arr.length;i++){
//内层循环依次减少并提出结果
for(int j=i;j>0;j--){
//如果当前数字小于前一个,则交换,否则不变
if(arr[j]<arr[j-1]){
int temp=arr[j];
arr[j]=arr[j-1];
arr[j-1]=temp;
}else{
break;
}
}
}
return arr;
}
public static void main(String[] args) {
InsertSort insertSort = new InsertSort();
int[] array = {2,5,1,6,4,9,8,5,3,1,2,0};
int[] sort = insertSort.sort(array);
for (int num:sort){
System.out.print(num+"\t");
}
}
===
封装 继承 多态
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,
继承
子类拥有父类非 private 的属性和方法。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态
一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定
两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)
提高了代码的扩展性(由多态保证)
===
物理内存跟虚拟内存
- 物理内存 以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于 CPU 的地址线条数。很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作效率很低
- 虚拟内存 由于物理内存有很多问题,所以出现了虚拟内存。虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
===
异常处理
面向对象的方法进行异常处理
每个异常都是一个对象 Throwable类或其它子类的实例
出现异常后便抛出一个异常对象 调用这个对象的方法可以捕获到这个异常并进行处理
try来执行 出现异常 抛出(throws)一个异常 捕捉(catch)它 或者(finally)由缺省处理器来处理
throw明确地抛出一个”异常” throws用来标明一个成员函数可能抛出的各种”异常”
遇到一个try语句,"异常"的框架就放到堆栈上面,直到所有的try语句都完成
def inner():
try:
print("inner try")
raise ValueError()
except ValueError:
print("inner caught")
def outer():
try:
inner()
except:
print("outer caught")
outer()
当 inner() 执行到 try 时,其栈帧中压入一个 try 记录。
异常抛出后,解释器在当前栈帧(inner)找到匹配的 except,于是异常被处理,不会传播到 outer。
inner 执行完毕后,其栈帧被销毁,相应的 try 记录也随之消失。
如果 inner 中没有 except,则异常会沿着调用栈向上传播,outer 的 try 记录才会被检查。
每个 try 块的信息被压入一个类似栈的结构,异常处理时从最内层开始依次尝试。
===
如何保证线程安全
合理的时间调度,避开共享资源的存取冲突
保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成
===
线程的基本状态以及状态之间的关系
Running 运行
Runnable 就绪 只欠CPU
Blocked 阻塞 可能是调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断
===
为什么需要线程池(thread pool)
创建和销毁对象是很费时间的
事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程,使用完毕放回池中
===
同步和异步
存在临界资源,必须进行同步存取
调用了一个需要花费很长时间来执行的方法,不希望让程序等待方法的返回时,使用异步编程
同步就是指阻塞式操作,而异步就是非阻塞式操作
import asyncio
import threading
import time
async def 异步工作(名字, 耗时):
print(f"异步:{名字}开始,时间:{time.time()-start_time:.1f}")
await asyncio.sleep(耗时)
print(f"异步:{名字}结束,时间:{time.time()-start_time:.1f}")
def 同步工作(名字, 耗时):
print(f"线程:{名字}开始,时间:{time.time()-start_time:.1f}")
time.sleep(耗时)
print(f"线程:{名字}结束,时间:{time.time()-start_time:.1f}")
async def 异步主函数():
print("异步主函数开始")
tasks = [
asyncio.create_task(异步工作("A", 3)),
asyncio.create_task(异步工作("B", 1)),
asyncio.create_task(异步工作("C", 2))
]
print("异步:已创建任务,即将调用await gather")
await asyncio.gather(*tasks) # ⭐ 这里确实会等待,但只等待异步任务
print("异步主函数结束")
def 多线程主函数():
print("多线程主函数开始")
threads = [
threading.Thread(target=同步工作, args=("X", 2)),
threading.Thread(target=同步工作, args=("Y", 1)),
threading.Thread(target=同步工作, args=("Z", 3))
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print("多线程主函数结束")
start_time = time.time()
print("=== 开始 =")
asyncio.run(异步主函数()) # 这个调用会阻塞,直到异步主函数完成
多线程主函数() # 然后才执行这个
print("= 结束 ===")
=== 开始 ===
异步主函数开始
异步:已创建任务,即将调用await gather
异步:A开始,时间:0.0
异步:B开始,时间:0.0
异步:C开始,时间:0.0
异步:B结束,时间:1.0
异步:C结束,时间:2.0
异步:A结束,时间:3.0
异步主函数结束
多线程主函数开始
线程:X开始,时间:3.0
线程:Y开始,时间:3.0
线程:Z开始,时间:3.0
线程:Y结束,时间:4.0
线程:X结束,时间:5.0
线程:Z结束,时间:6.0
多线程主函数结束
=== 结束 ===
===
线程同步和线程调度的相关方法
wait():等待(阻塞)状态,释放锁
sleep():睡眠状态,静态方法,要处理InterruptedException异常
notify():唤醒处于等待状态的线程,由JVM确定唤醒哪个线程
notityAll():唤醒所有处于等待状态的线程,只有获得锁的线程才能进入就绪状态
===
线程的sleep()方法和yield()方法有什么区别
优先级:sleep不考虑,yield考虑
sleep()方法后转入阻塞(blocked)状态 yield()方法后转入就绪(ready)状态(只是暂时让出cpu
InterruptedException异常:sleep有,yield没有
可移植性:sleep好
===
sleep() 和 wait() 有什么区别
sleep:此线程暂停,执行机会给其他线程,监控状态依然保持,到时自动恢复,不会释放对象锁。
wait:本线程放弃对象锁,进入等待锁定池,只有notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态
wt()和sleep()方法主要有如下三个区别: 1. 所属的类型不同 - wt()是Object类的实例方法,调用该方法的线程将进入WTING状态。 - sleep()是Thread类的静态方法,调用该方法的线程将进入TIMED_WTING状态。 2. 对锁的依赖不同 - wt()依赖于synchronized锁,它必须通过监视器进行调用,在调用后线程会释放锁。 - sleep()不依赖于任何锁,所以在调用后它也不会释放锁。 3. 返回的条件不同 - 调用wt()进入等待状态的线程,需要由notify()/notifyAll()唤醒,从而返回。 - 调用sleep()进入超时等待的线程,需要在超时时间到达后自动返回。
===
线程从创建到死亡的几种状态
新建( new )
就绪
运行( running )
阻塞( block ):等待阻塞、同步阻塞、其他阻塞
死亡( dead )
===
线程跟进程的区别
- 进程有独立的地址空间,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间;
- 进程和线程切换时,需要切换进程和线程的上下文,进程的上下文切换时间开销远远大于线程上下文切换时间,耗费资源较大,效率要差一些;
- 进程的并发性较低,线程的并发性较高;
- 每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
- 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源;
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
===
死锁
争夺共享资源、相互等待、互斥条件、请求和保持条件、不剥夺条件、环路等待条件
===
进程间的通信方式
- 管道 管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式
- 命名管道
- 信号 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制
- 消息队列 消息队列就是一个消息的链表,可以把消息看作一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读走消息,消息队列是随内核持续的。
- 共享内存 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。
- 内存映射 内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
- 信号量 信号量主要用来解决进程和线程间并发执行时的同步问题,进程同步是并发进程为了完成共同任务采用某个条件来协调它们的活动。
- Socket 套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
===
请你说说线程和协程的区别
- 线程是操作系统的资源,线程的创建、切换、停止等都非常消耗资源,而创建协程不需要调用操作系统的功能,编程语言自身就能完成,所以协程也被称为用户态线程,协程比线程轻量很多;
- 线程在多核环境下是能做到真正意义上的并行,而协程是为并发而产生的;
- 一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行;
- 线程进程都是同步机制,而协程则是异步;
- 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力;
- 操作系统对于线程开辟数量限制在千的级别,而协程可以达到上万的级别。
import asyncio
async def fetch_data():
print("开始获取数据")
await asyncio.sleep(2) # ⭐ 这里发生异步挂起
print("数据获取完成")
return "数据"
在事件循环中
async def main():
task1 = fetch_data() # 创建协程对象
task2 = fetch_data()
同时等待多个协程 - 异步执行
results = await asyncio.gather(task1, task2)
===
解释一下多线程
在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈、局部变量,并且能够共享进程内的资源。
由于共享资源,处理器便可以在这些线程之间快速切换,从而让使用者感觉这些线程在同时执行。 (在多核下可以达到真正的并行)
===
线程通信方式
Python线程通信主要通过threading模块的同步原语实现,例如使用Event让一个线程等待另一个线程的信号:线程A执行event.wait()阻塞,线程B在条件满足时调用event.set()即可唤醒A;使用Condition可实现更精细的等待/通知,典型的生产者-消费者模式中,生产者在缓冲区满时调用condition.wait()释放锁并等待,生产后调用condition.notify()唤醒消费者;而queue.Queue是最推荐的通信方式,生产线程直接put()数据,消费线程get(),队列内部自动管理锁和等待,无需手动协调;此外Semaphore可以控制同时访问资源的线程数,如semaphore.acquire()和release()。这些工具覆盖了从简单标志到复杂协调的所有场景,确保多线程安全协作。
===
OSI七层协议模型、TCP/IP四层模型和五层协议体系结构之间的关系
OSI的七层协议主要包括:
物理层(physical layer)
数据链路层(data link layer)
网络层(network layer)
运输层(transport layer)
会话层(session layer)
表示层(presentation layer)
应用层(application layer)
TCP/IP是一个四层的体系结构,他包括(从下到上顺序):网络接口层、网际层(用网际层这个名字是强调这一层是为了解决不同的网络的互联问题)、运输层、应用层。最下面的网络接口层并没有具体内容
五层协议体系结构
物理层、数据链路层、网络层、运输层、应用层。
OSI由于体系比较复杂,许多设计过于思想化,应用的范围有限
TCP/IP协议已成为目前互联网事实上的国际标准和工业标准。
===
七层协议模式每一层的作用如下:
1、物理层:定义物理设备标准
2、数据链路层:定义了如何让数据格式化进行传输,以及如何控制对物理介质的访问
3、网络层:不同地理位置的网络中的两个主机之间提供连接和路径选择
4、运输层:定义了一些传输数据的协议和端口号(WWW端口80等)
5、会话层:通过运输层(端口号:传输端口与接收端口)建立数据传输的通路
6、表示层:确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取
7、应用层:最靠近用户,为应用程序提供网络服务
===
TCP三次握手过程
第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
===
TCP四次挥手
客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
===
为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。
但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
===
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
网络是不可靠,可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文
===
为什么不能用两次握手进行连接
3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认
===
如果已经建立了连接,但是客户端突然出现故障了怎么办?
保活计时器,每收到一次客户端的请求后都会重新复位这个计时器,若两小时还没有收到客户端的任何数据,就会发送一个探测报文段
若一连发送10个探测报文仍然没反应,认为故障,关闭连接
===
TCP 和 UDP区别
UDP是无连接的,即发送数据之前不需要建立连接
UDP使用尽最大努力交付,即不保证可靠交付,同时也不使用拥塞控制
UDP是面向报文的,没有拥塞控制,适合多媒体通信要求
UDP支持一对一,一对多,多对一和多对多的交互通信
UDP首部开销小,只有8个字节
TCP是面向连接的运输层协议
TCP只能一对一连接
TCP提供可靠的交付服务,提供全双工通信
TCP 面向字节流,头部最低20个字节
===
使用TCP协议的意义
对网络通讯质量有要求的时候
比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议
===
http和https的区别
1)https协议要申请证书到ca,需要一定经济成本;
2) http是明文传输,https是加密的安全传输;
3) 连接的端口不一样,http是80,https是443;
5)https是ssl加密的传输,身份认证的网络协议,相对http传输比较安全。
===
浏览器从接收到一个URL,到最后展示出页面,经历了哪些过程
DNS解析
TCP连接
发送HTTP请求
服务器处理请求并返回HTTP报文
浏览器解析渲染页面
===
路由器和交换机
交换机用于同一网络内部数据的快速传输转发决策通过查看二层头部完成转发不需要修改数据帧工作在 TCP/IP 协议的二层 —— 数据链路层工作简单,直接使用硬件处理
路由器用于不同网络间数据的跨网络传输转发决策通过查看三层头部完成转发需要修改 TTL ,IP 头部校验和需要重新计算,数据帧需要重新封装工作在 TCP/IP 协议的三层 —— 网络层工作复杂,使用软件处理。
===
负载均衡 反向代理模式的优点、缺点
(1)反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。
(2)反向代理负载均衡技术是把将来自internet上的连接请求以反向代理的方式动态地转发给内部网络上的多台服务器进行处理,从而达到负载均衡的目的。
(3)反向代理负载均衡能以软件方式来实现,如apache mod_proxy、netscape proxy等,也可以在高速缓存器、负载均衡器等硬件设备上实现。反向代理负载均衡可以将优化的负载均衡策略和代理服务器的高速缓存技术结合在一起,提升静态网页的访问速度,提供有益的性能;由于网络外部用户不能直接访问真实的服务器,具备额外的安全性(同理,NAT负载均衡技术也有此优点)。
(4)其缺点主要表现在以下两个方面
反向代理是处于OSI参考模型第七层应用的,所以就必须为每一种应用服务专门开发一个反向代理服务器,这样就限制了反向代理负载均衡技术的应用范围,现在一般都用于对web服务器的负载均衡。
针对每一次代理,代理服务器就必须打开两个连接,一个对外,一个对内,因此在并发连接请求数量非常大的时候,代理服务器的负载也就非常大了,在最后代理服务器本身会成为服务的瓶颈。
一般来讲,可以用它来对连接数量不是特别大,但每次连接都需要消耗大量处理资源的站点进行负载均衡,如search等。
===
DNS寻址过程
浏览器输入 www.example.com
│
▼
① 浏览器 DNS 缓存 ──── 有? → 直接用 IP
│ 无
▼
② 操作系统 DNS 缓存 ── 有? → 直接用 IP
│ 无
▼
③ 本地 hosts 文件 ──── 有? → 直接用 IP
│ 无
▼
④ 查本地域名服务器(Local DNS,如 8.8.8.8 / 公司DNS)
│ 本地DNS自己没有,开始往下查 ↓
│
│ ⑤ 问 根域名服务器(.)
│ → 根说:我不知道最终IP,但.com归TLD管,给你TLD地址
│ ⑥ 问 顶级域名服务器(.com)
│ → TLD说:我不知道IP,但example.com归权威DNS管,给你权威地址
│ ⑦ 问 权威域名服务器(example.com)
│ → 权威说:www.example.com 的IP是 93.184.216.34,给你!
│
▼
⑧ 本地DNS拿到IP → 返回给OS → OS给浏览器 → 并各自缓存
│
▼
⑨ 浏览器用IP建立TCP连接
===
Semaphore 原理
控制同时访问特定资源的线程数量,协调各个线程,以保证合理的使用公共资源,AQS实现,AQS的状态变量state做为许可证数量,每次通过acquire()/tryAcquire(),许可证数量通过CAS原子性递减,调用release()释放许可证,原子性递增,只要有许可证就可以重复使用
===
如何实现一个生产者与消费者模型
一共5种方法
同步对象的 wait() / notify() 方法
ReetrantLock Condition 的 await() / signal()方法
BlockingQueue阻塞队列 put() 和take方法
Semaphore 基于计数的信号量
PipedInputStream / PipedOutputStream 管道输入输出流
// 共享缓冲区
class Buffer {
private Queue
一些通用八股整理
浙公网安备 33010602011771号