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 的属性和方法。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。

多态
一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定
两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)
提高了代码的扩展性(由多态保证)

===

物理内存跟虚拟内存


  1. 物理内存 以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于 CPU 的地址线条数。很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作效率很低
  2. 虚拟内存 由于物理内存有很多问题,所以出现了虚拟内存。虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

===

异常处理


面向对象的方法进行异常处理
每个异常都是一个对象 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 )

===

线程跟进程的区别


  1. 进程有独立的地址空间,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间;
  2. 进程和线程切换时,需要切换进程和线程的上下文,进程的上下文切换时间开销远远大于线程上下文切换时间,耗费资源较大,效率要差一些;
  3. 进程的并发性较低,线程的并发性较高;
  4. 每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
  5. 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源;
  6. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

===

死锁


争夺共享资源、相互等待、互斥条件、请求和保持条件、不剥夺条件、环路等待条件

===

进程间的通信方式


  1. 管道 管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式
  2. 命名管道
  3. 信号 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制
  4. 消息队列 消息队列就是一个消息的链表,可以把消息看作一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读走消息,消息队列是随内核持续的。
  5. 共享内存 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。
  6. 内存映射 内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
  7. 信号量 信号量主要用来解决进程和线程间并发执行时的同步问题,进程同步是并发进程为了完成共同任务采用某个条件来协调它们的活动。
  8. Socket 套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。

===

请你说说线程和协程的区别


  1. 线程是操作系统的资源,线程的创建、切换、停止等都非常消耗资源,而创建协程不需要调用操作系统的功能,编程语言自身就能完成,所以协程也被称为用户态线程,协程比线程轻量很多;
  2. 线程在多核环境下是能做到真正意义上的并行,而协程是为并发而产生的;
  3. 一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行;
  4. 线程进程都是同步机制,而协程则是异步;
  5. 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力;
  6. 操作系统对于线程开辟数量限制在千的级别,而协程可以达到上万的级别。
    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 queue = new LinkedList<>();
private int capacity;

public synchronized void produce(Object item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 缓冲区满,等待
}
queue.add(item);
notifyAll(); // 唤醒消费者
}

public synchronized Object consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 缓冲区空,等待
}
Object item = queue.poll();
notifyAll(); // 唤醒生产者
return item;
}
}

// 生产者
class Producer {
public void run() {
while (true) {
Object item = produceItem();
buffer.produce(item);
}
}
}

// 消费者
class Consumer {
public void run() {
while (true) {
Object item = buffer.consume();
consumeItem(item);
}
}
}

===

公平锁、非公平锁


公平锁:根据线程请求锁的顺序来获取锁
非公平锁:抢占式获取锁

===

什么是死锁


具备以下4个条件就会产生死锁:
互斥条件:资源排它性使用,同时只由一个线程占用。
请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新的资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源
不可剥夺条件:资源在自己使用完之前不能被其他线程抢占
循环等待:线程一在等待线程二占用的资源,线程二在等待线程三等待的资源...

===

如何避免死锁


按照指定的顺序获取锁,并释放锁
设置获取锁超时时间
避免一个线程同时获取多个锁

===

sleep 、wait、yield的区别


sleep:
让当前线程休眠指定时间
不释放锁资源
可通过调用interrupt()方法来唤醒休眠线程
wait:
让当前线程进入等待状态,当其他线程调用notify或者notifyAll方法时,当前线程进入就绪状态
当前线程会释放已获取的锁资源,并进入等待队列
只能在synchronized中使用
yield:
让出线程当前的CPU执行时间,当前线程进入就绪状态,不阻塞当前线程,只是让同优先级或者更高优先级的线程优先执行
线程下次调度时依旧有可能执行到

===

类的生命周期


python生命周期:定义,使用,销毁
类的生命周期一个有7个阶段:加载、验证、准备、解析、初始化、使用、卸载

加载:
通过类的全限定名来获取此类的二进制字节流
将字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口

验证:分4个验证
文件格式验证,验证是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
字节码验证,通过数据流和控制流分析,确定程序语义是否合法、符合逻辑
符合引用验证,是对类自身以外的信息进行匹配性校验(常量池中各种符合引用)

准备:正式为类变量分配内存并设置初始值的阶段,这里设置初始值是数据类型的默认值
解析:虚拟机将常量池中的符号引用替换为直接引用的过程
初始化:执行类构造器的过程

===

new一个对象过程


1、在堆区分配对象需要的内存
分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量
2、对所有实例变量赋默认值
将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值
3、执行实例初始化代码
初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法
4、如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它

===

get和post请求的区别


1.get请求:从服务器上获得资源 post:向服务器提交数据;
2.get将表单中数据按照name=value的形式,添加到action所指向的URL 后面,并且两者使用"?"连接,而各个变量之间使用"&"连接;
post是将表单中的数据放在HTTP协议的请求头或消息体中,传递到action所指向URL;
3.get传输的数据要受到URL长度限制(1024字节) post可以传输大量的数据,上传文件通常要使用post方式;
4.get时参数会显示在地址栏上 敏感数据还是应用使用post;
5.get使用MIME类型application/x-www-form-urlencoded的URL编码(也叫百分号编码)文本的格式传递参数

===


cookie数据存放在客户的浏览器上,session数据放在服务器上。
cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。
session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用COOKIE。
单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

===

数据库三大范式、反模式


第一范式:原子性约束,要求属性具有原子性,不可再分解
第二范式:唯一性约束,必须有一个主键,没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分
第三范式:冗余性的约束,非主键列必须直接依赖于主键

反模式:如果完全按照三大范式来设计表结构,会导致业务涉及表增多,查询数据需要多表联合查询,导致sql复杂,性能变差,不利于维护,也不利于分库分表

===

建立索引的原则


最左匹配原则,直到遇到范围查询(>, <, between, like)就停止,比如a = 1 and b = 2 and c >3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,abd的顺序可以任意调整
= 和 in可以乱序
尽量选择区分度高的索引,区分度公式count(distinct col)/count(*)
索引不能参与计算
尽量扩展索引,不要新建索引

===

索引失效情况总结


遵守最左匹配原则,中间断索引,使用范围查询
在索引列上做计算
索引字段使用 != 或者 < >
索引字段使用 is null 或者 is not null
使用通配符 %开头
索引字段是字符串,查询条件没有使用字符串
索引字段使用or

===

Msyql 执行SQL 过程


客户端发送一条查询给服务器
服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段
服务器端进行SQL解析,预处理,再由优化器生成对应的执行计划
MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询
将结果返回给客户端

===

如何优化sql翻页


只让用户一页页翻,不能跳页
确定每页的边界值,通过where条件查询来优化
使用延迟关联,通过使用覆盖索引查询返回需要的主键,再根据这些主键关联原有表获得需要的行

===

数据库ACID的特性


原子性:要么都发生,要么都不发生
一致性:事务前后数据的完整性必须保持一致。
隔离性:多个并发事务之间数据要相互隔离。
持久性:一旦提交,数据的改变就是永久性的

===

sql查询速度慢的原因


1、没有索引或者没有用到索引(这是查询慢最常见的问题,是程序设计的缺陷)
2、I/O吞吐量小,形成了瓶颈效应。
3、没有创建计算列导致查询不优化。
4、内存不足
5、网络速度慢
6、查询出的数据量过大(可以采用多次查询,其他的方法降低数据量)
7、锁或者死锁(这也是查询慢最常见的问题,是程序设计的缺陷)
8、sp_lock,sp_who,活动的用户查看,原因是读写竞争资源。
9、返回了不必要的行和列
10、查询语句不好,没有优化
优化查询方法
1、把数据、日志、索引放到不同的I/O设备上,增加读取速度,以前可以将Tempdb应放在RAID0上,SQL2000不在支持。数据量(尺寸)越大,提高I/O越重要.

2、纵向、横向分割表,减少表的尺寸(sp_spaceuse)

3、升级硬件

4、根据查询条件,建立索引,优化索引、优化访问方式,限制结果集的数据量。注意填充因子要适当(最好是使用默认值0)。索引应该尽量小,使用字节数小的列建索引好(参照索引的创建),不要对有限的几个值的字段建单一索引如性别字段

===

MySQL索引

索引就像指向表行的指针,是一种允许查询操作快速确定哪些行符合WHERE子句中的条件,并检索到这些行的其他列值的数据结构; 索引主要有普通索引、唯一索引、主键索引、外键索引、全文索引、复合索引几种; 在大数据量的查询中,合理使用索引的优点非常明显,不仅能大幅提高匹配where条件的检索效率,还能用于排序和分组操作的加速。 当时索引如果使用不当也有比较大的坏处:比如索引必定会增加存储资源的消耗;同时也增大了插入、更新和删除操作的维护成本,因为每个增删改操作后相应列的索引都必须被更新。 加分回答 只要创建了索引,就一定会走索引吗? 不一定。 比如,在使用组合索引的时候,如果没有遵从“最左前缀”的原则进行搜索,则索引是不起作用的。 举例,假设在id、name、age字段上已经成功建立了一个名为MultiIdx的组合索引。索引行中按id、name、age的顺序存放,索引可以搜索id、(id,name)、(id, name, age)字段组合。如果列不构成索引最左面的前缀,那么MySQL不能使用局部索引,如(age)或者(name,age)组合则不能使用该索引查询。

===

MySQL主从同步是如何实现的

复制(replication)是MySQL数据库提供的一种高可用高性能的解决方案,一般用来建立大型的应用。总体来说,replication的工作原理分为以下3个步骤: 1. 主服务器(master)把数据更改记录到二进制日志(binlog)中。 2. 从服务器(slave)把主服务器的二进制日志复制到自己的中继日志(relay log)中。 3. 从服务器重做中继日志中的日志,把更改应用到自己的数据库上,以达到数据的最终一致性。 复制的工作原理并不复杂,其实就是一个完全备份加上二进制日志备份的还原。不同的是这个二进制日志的还原操作基本上实时在进行中。这里特别需要注意的是,复制不是完全实时地进行同步,而是异步实时。这中间存在主从服务器之间的执行延时,如果主服务器的压力很大,则可能导致主从服务器延时较大。

===

那么同步阻塞、同步非阻塞和异步非阻塞又代表什么意思呢?


举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在哪里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(异步非阻塞)。

伪代码示例
result = function_call() # 调用函数
↑ 程序卡在这里,直到函数返回
print(result) # 拿到结果后才能继续

伪代码示例
while True:
if is_ready(): # 不断检查是否就绪
result = get_result()
break
do_other_things() # 可以做其他事

伪代码示例(回调方式)
def callback(result): # 定义回调函数
print(f"结果:{result}")

async_function_call(callback) # 异步调用
do_other_things() # 立即返回,继续执行
当异步操作完成时,会自动调用callback

===

操作系统里的内存碎片


内存碎片分为:内部碎片和外部碎片。

内部碎片:已经被分配(能明确指出属于哪个进程),却不能被利用
外部碎片:还没有被分配出去(不属于任何进程),太小了无法分配

单道连续分配只有内部碎片。多道固定连续分配既有内部碎片,又有外部碎片。
单道连续分配:一个程序占据一个内存
多道固定连续分配:划分内存块

===

内存碎片解决办法


伙伴系统算法

假设要请求一个256 个页框的块(即1MB)。

  1. 查找256页框链表:若存在空闲块,直接分配;否则进入下一步。
  2. 查找512页框链表:若存在空闲块,将其分割为两个256页框的块,一个用于分配,另一个插入256页框链表。
  3. 查找1024页框链表:
    · 若存在空闲块,标准操作如下:
    · 将1024页框块分割为两个512页框块(记为A和B)。
    · 将其中一个512页框块(如A)进一步分割为两个256页框块(记为A1和A2)。
    · 使用其中一个256页框块(如A1)满足请求。
    · 将剩余的256页框块(A2)插入256页框链表。
    · 将另一个512页框块(B)插入512页框链表。

===

系统如何提高并发性


1、提高CPU并发计算能力
(1)多进程&多线程
(2)减少进程切换,使用线程,考虑进程绑定CPU
(3)减少使用不必要的锁,考虑无锁编程
(4)考虑进程优先级
(5)关注系统负载
2、改进I/O模型
(1)DMA技术
(2)异步I/O
(3)改进多路I/O就绪通知策略,epoll
(4)Sendfile
(5)内存映射
(6)直接I/O

===

linux内存管理


Linux 操作系统是采用段页式内存管理方式: 页式存储管理能有效地提高内存利用率(解决内存碎片),而分段存储管理能反映程序的逻辑结构并有利于段的共享。将这两种存储管理方法结合起来,就形成了段页式存储管理方式。 段页式存储管理方式即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。在段页式系统中,为了实现从逻辑地址到物理地址的转换,系统中需要同时配置段表和页表,利用段表和页表进行从用户地址空间到物理内存空间的映射。 系统为每一个进程建立一张段表,每个分段有一张页表。段表表项中至少包括段号、页表长度和页表始址,页表表项中至少包括页号和块号。在进行地址转换时,首先通过段表查到页表始址,然后通过页表找到页帧号,最终形成物理地址。

===

Redis 为什么是单线程? 为什么单线程还能这么快?


避免线程切换和竞态产生的消耗,单线程可以简化数据结构和算法的实现

Redis是基于内存的数据库,内存响应速度是很快;采用epoll作为I/O多路复用技术;Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多时间

===

Redis 使用场景


最多的应用于缓存,其他可以用于排行榜、计数器、消息队列等

===

Redis的RDB持久化原理


当前进程数据生成快照保存到硬盘,触发方式有手动触发和自动触发

手动触发命令:save和bgsave命令
save命令:阻塞当前Redis服务,直到RDB过程完成为止,不建议线上环境使用
bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束,阻塞只发生在fork阶段,一般时间很短

自动触发:
使用save相关配置,如"save m n"。表示m秒内数据集存在n次修改时,自动触发
如果从节点执行全量复制操作,主节点自动执行bgsave 生成RDB文件并发送给从节点
执行debug reload命令重新加载Redis时,也会自动触发save操作
默认情况下执行shutdown命令时,如果没有开始AOF持久化则自动执行bgsave

redis默认采用LZF算法对生成的RDB文件进行压缩

===

RDB优缺点:


RDB优点:
RDB是一个紧凑的二进制文件,代表Redis在某个时间点上的数据快照,适合备份,全量复制等场景
Redis加载RDB恢复数据远远快于AOF方式
RDB缺点:
没办法做到实时持久化/秒级持久化
RDB文件使用特定二进制保存,存在兼容问题

===

redis的AOF持久化


以独立日志的方式记录每次写命令,写入的内容直接是文本协议格式,重启时再重新执行AOF文件中的命令达到恢复数据的目的,解决了数据持久化实时性问题

AOF工作流程:
所有的写入命令追加到aof_buf(缓冲区)中
AOF缓冲区根据对应的策略向硬盘做同步操作
随着AOF文件越来越大,需要定期对AOF文件进行重写,压缩,父进程执行fork创建子进程,由子进程根据内存快照执行AOF重写,父进行继续响应后面的命令,在子进程完成重写后,父进程再把新增的写入命令写入到新的AOF文件中
Redis服务重启,加载AOF文件进行数据恢复

===

redis主从模式


主(master)和 从(slave)部署在不同的服务器上,当主节点服务器写入数据时会同步到从节点的服务器上,一般主节点负责写入数据,从节点负责读取数据

优点
读写分离,提高效率
数据热备份,提供多个副本

缺点
主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预
单点容易造成性能低下
主节点的存储能力受到限制
主节点的写受到限制(只有一个主节点)
全量同步可能会造成毫秒或者秒级的卡顿现象

===

Redis哨兵模式


在主从模式的Redis系统中,从数据库在整个系统中起到了数据冗余备份和读写分离的作用,但是当数据库遇到异常中断服务后,我们只能通过手动的方式选择一个从数据库来升格为主数据库,显然这种方式很麻烦需要人工介入,这时通过哨兵模式可以实现自动化的系统监控和故障恢复。

===

redis主从复制过程


从节点内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接(tcp
通过验证后,主从可正常通信了,主节点会把数据持续发给从节点,同步方式有全量同步和部分同步,刚建立建立的时候,会进行全量同步,同步结束后,进行部分同步
当主节点与从节点同步完当前的数据后,主节点会把后续新增的命令持续发送给从节点进行同步

===

缓存穿透


缓存穿透是指,缓存中不存在该key的数据,于是就是去数据库中查询,数据库也不存在该数据,导致循环查询数据

优化:
缓存空对象
对于不存在的数据,依旧将空值缓存起来。但这会造成内存空间的浪费,可以针对这类数据加一个过期时间。对于缓存和存储层数据的一致性,可以在过期的时候,请求存储层,或者通过消息系统更新缓存
布隆过滤器
将所有存在的数据做成布隆过滤器,可以使用Bitmaps实现,其在大数据量下空间占用小。当有新的请求时,先到布隆过滤器中查询是否存在,如果不存在该条数据直接返回;如果存在该条数据再查询缓存、查询数据库。
Redis4.0以上采用插件集成,https://github.com/RedisBloom...
原理:
在redis中是一个大型的位数组,通过计算key的hash然后对数组长度取模得到一个位置,进行写入;在判断是否存在时,判断位数组中几个位置是否都为1,只要一个位为0,就说明这个key不存在。布隆过滤器也会存在一定的误判,如果位数组比较稀疏,概率就会很大,否则就会降低。

===

缓存击穿


缓存击穿指,某key突然变成了热点key,大量请求到该key,但key刚好又失效,导致从数据库中去查询数据
优化:
​ 通过互斥锁方式,在请求数据库之前设置setnx,在查询完数据库,并更新缓存后,删除setnx

===

缓存雪崩


指缓存由于大量请求,造成缓存挂掉,大量请求直接打到存储层,造成存储层挂机
优化:
使用主从,哨兵,集群模式保证缓存高可用
依赖隔离组件为后端限流并降级
提前演练,做好后备方案

===

MySQL和Redis的区别


(1)类型上

从类型上来说,mysql是关系型数据库,redis是缓存数据库

(2)作用上

mysql用于持久化的存储数据到硬盘,功能强大,速度较慢,基于磁盘,读写速度没有Redis快,但是不受空间容量限制,性价比高

redis用于存储使用较为频繁的数据到缓存中,读取速度快,基于内存,读写速度快,也可做持久化,但是内存空间有限,当数据量超过内存空间时,需扩充内存,但内存价格贵

(3)需求上

mysql和redis因为需求的不同,一般都是配合使用。
需要高性能的地方使用Redis,不需要高性能的地方使用MySQL。存储数据在MySQL和Redis之间做同步。

===

Redis和其他数据库缓存一致性


失效模式
做法顺序:先写数据库,在删除缓存
双写模式
做法顺序:先写数据库,再写缓存
最终兜底:缓存设过期时间
删缓存失败:重试消息队列
binlog日志订阅删缓存
读写同一个key加分布式锁

===

zset


有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数Redis 正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数 ( score ) 却可以重复。

===

Redis哨兵模式


哨兵: 哨兵模式是Redis的高可用的解决方案,它由一个或多个Sentinel实例组成Sentinel系统,可以监视任意多个主服务器以及这些主服务器属下的所有从服务器。当哨兵节点发现有节点不可达时,会对该节点做下线标识。如果是主节点下线,它还会和其他Sentinel节点进行“协商”,当大多数Sentinel节点都认为主节点不可达时,它们会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给Redis应用方。 哨兵节点包含如下的特征:
 1. 哨兵节点会定期监控数据节点,其他哨兵节点是否可达;
 2. 哨兵节点会将故障转移的结果通知给应用方;
 3. 哨兵节点可以将从节点晋升为主节点,并维护后续正确的主从关系;
 4. 哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点信息;
 5. 节点的故障判断是由多个哨兵节点共同完成的,可有效地防止误判;
 6. 哨兵节点集合是由多个哨兵节点组成的,即使个别哨兵节点不可用,整个集合依然是健壮的;
 7. 哨兵节点也是独立的Redis节点,是特殊的Redis节点,它们不存储数据,只支持部分命令。 集群: Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:。
 1. 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
 2. 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
 3. 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

===

epoll


epoll是同步io
epoll 是一种更加高效的 IO 复用技术,epoll 的使用步骤及原理如下: 1. 调用 epoll_create() 会在内核中创建一个 eventpoll 结构体数据,称之为 epoll 对象,在这个结构体中有 2 个比较重要的数据成员,一个是需要检测的文件描述符的信息 struct_root rbr(红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发送改变的文件描述符信息(双向链表); 2. 调用 epoll_ctrl() 可以向 epoll 对象中添加、删除、修改要监听的文件描述符及事件; 3. 调用 epoll_wt() 可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。 epoll 的两种工作模式: 1. LT 模式(水平触发) LT(Level - Triggered)是缺省的工作方式,并且同时支持 Block 和 Nonblock Socket。在这种做法中,内核检测到一个文件描述符就绪了,然后可以对这个就绪的 fd 进行 IO 操作,如果不作任何操作,内核还是会继续通知。 2. ET 模式(边沿触发) ET(Edge - Triggered)是高速工作方式,只支持 Nonblock socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 检测到。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 进行 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。 ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
lt 一次没读完,下次还通知
et 通知次数少,要一次性读完

===

说说IO多路复用


在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。 目前支持I/O多路复用的系统调用有select、pselect、poll、epoll,在Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select的一些固有缺陷导致了它的应用受到了很大的限制,最终Linux不得不在新的内核版本中寻找select的替代方案,最终选择了epoll。

===

解释一下Harness和Loop Engineer


Agent = Model + Harness
其中 Model 是大语言模型本身,Harness 是让模型真正能执行任务、使用工具、保持状态、接受约束、被验证和被调度的那一整套系统。harness使得模型能够更可靠的工作
Loop Engineering 关注的是如何让 Agent 形成一个持续运行的闭环。系统可以根据时间、事件或条件自动触发 agent

===

像claude code或者codex这样的工具里的harness是如何解决模型输出不理想的问题的


在 Claude Code 或 Codex 这类工具里,harness 主要通过上下文管理、工具调用、自动验证、反馈循环、重试恢复来解决模型输出不理想的问题。它不假设模型一次生成就是正确的,而是把模型放进一个真实工程环境中,让它能够读取代码、修改代码、运行测试、观察失败并继续修正。这样模型的错误会被测试、编译器、权限系统和人工确认机制拦截,最终把不稳定的自然语言生成转化成相对可靠的工程变更。
实际编码中可以用harness来规范化行为
Harness 是一个 Agent 工程化框架,核心思想是通过 分层规范文档 来约束编码 Agent 的行为:
AGENTS.md:定义 Agent 的角色边界和能力范围;
ARCHITECTURE:定义代码仓库的架构约束;
TASKS.md:定义当前任务拆解。
它的本质是把“提示词工程”升级为“规范工程”——让 Agent 不是靠一段又长又臭的系统提示词来理解任务,而是通过读取结构化文档来获得上下文。这样更可控、更可审计

===

Token 和上下文窗口是什么?它们在工程上意味着什么?


Token 是大模型处理文本时的基本单位。中文里一个汉字可能是一个 token,也可能多个汉字组成一个 token,具体取决于 tokenizer。
上下文窗口是一次模型调用中输入和输出 token 的总容量限制。
在工程上,这意味着模型不能无限读取代码、日志和历史对话。Agent 系统必须做 token budget 和上下文管理,预留输出空间

===

1个汉字、1 个英文单词大概是多少 token,为什么不能死记一个固定值?


一个汉字通常可以粗略估为 1 个 token,一个英文单词通常大约是 1 到 2 个 token。但 token 不是字或词,而是模型 tokenizer 切出来的子词单元。不同模型的 tokenizer、语言、标点、空格、生僻词、代码符号都会影响切分结果,所以不能死记固定值。工程上只能用经验值做粗估,精确场景要用目标模型的 tokenizer 实际计算。

===

长期和短期记忆分别怎么设计?


可借鉴思考:
短期记忆:当前会话的消息列表 + 工具调用结果,存在内存里
长期记忆:用户偏好、历史事实、周期性任务等,存在向量数据库里

===

skill和tool有什么区别


Tool 和 Skill 的区别在于抽象层级不同。Tool 是 agent 可以调用的外部能力接口,通常是一个函数、API、插件或命令,输入输出边界比较明确;Skill 是 agent 完成某类任务的能力或流程,通常包含任务拆解、决策逻辑、工具选择、结果校验等。

===

多个skill如何进行路由


多 skill 路由一般有三种方式:规则路由、语义分类路由、planner 路由。规则适合高确定性场景,LLM/embedding 适合自然语言语义匹配,planner 适合需要多个 skill 协作的复杂任务。生产系统通常是 hybrid:先召回候选 skill,再让 LLM 做结构化选择,并结合置信度、权限、安全和成本做最终决策。

===

skill的渐进式披露是什么


Skill 的渐进式披露是一种上下文管理和能力加载策略。系统先向模型暴露轻量的 skill 元数据,用于路由;确定目标 skill 后,再加载详细指令;执行过程中根据需要加载工具、参考文档和脚本。这样可以减少 token 消耗和模型干扰,同时提升多 skill 系统的扩展性和可维护性。

===

动态加载Tool和Tool注册怎么做的? 未来Tool变得很多怎么优化动态加载?


Tool 注册一般通过 Tool Registry 实现,注册的不是单纯函数,而是 tool 的 name、description、input/output schema、版本等元数据。
基于意图的动态加载
基于向量检索的动态加载
基于skill的动态加载

===

MCP 和 Tool Calling 有什么区别?两者是什么关系


Tool Calling 和 MCP 是不同层级的概念。Tool Calling 是 LLM 或 Agent Runtime 提供给模型的一种结构化调用机制,让模型可以输出 tool name 和 arguments,由宿主应用执行工具并返回结果。MCP 是 Model Context Protocol,它标准化了外部工具、资源和 prompt 如何暴露给 Agent,包括 list_tools、call_tool、resources、prompts 等能力。
MCP 可以作为 Tool Calling 的后端工具来源。Agent 通过 MCP Client 从 MCP Server 动态发现工具,把 MCP tool schema 转成模型支持的 tool calling schema;模型选择工具并生成参数后,Agent Runtime 再通过 MCP call_tool 调用对应 MCP Server。

===

Tool Schema 设计得过细或过粗分别有什么问题?


Schema 过粗时,一个工具承担太多语义,参数之间存在大量条件依赖,模型很容易生成“格式合法但业务非法”的调用。Schema 过细时,模型需要进行大量连续调用,错误会逐步累积,同时增加延迟和 token 消耗。

===

checkpoint是什么


Checkpoint 是 Agent 或工作流执行过程中的状态快照,用来支持恢复、重试、回滚、人工审批和调试回放。

===

Redis常见的数据结构,还有各自适用的场景


Redis 常见数据结构包括 String、Hash、List、Set、zset,以及 Stream、Bitmap 等扩展结构。选择时主要看数据之间的关系:单值用 String,对象字段用 Hash,队列用 List 或 Stream,去重用 Set,排行榜用 zset

===

redis如何删除过期key


Redis 的 key 过期不是到期立即删除,而是通过惰性删除和定期删除结合处理。惰性删除是在访问 key 时检查是否过期,过期就删除;定期删除是 Redis 后台周期性从设置了过期时间的 key 中抽样检查并删除过期 key。

===

如何设计redis分布式锁,需要考虑什么


Redis 分布式锁一般用 SET key value NX EX timeout 实现,value 必须是唯一请求 ID,释放锁时要用 Lua 脚本先比对 value 再删除,避免误删别人的锁。还要考虑锁超时、锁续期、重试退避、锁粒度设计,以及 Redis 主从切换带来的一致性风险。
如果锁保护的是重要写操作,最好再加 fencing token 或数据库侧幂等校验,因为 Redis 锁只能尽量保证互斥,不能单独保证“过期后旧任务不会写坏数据”。

===

Redis 数据持久化策略有哪些?


Redis 的持久化主要有 RDB、AOF 和混合持久化三种。RDB 是定期生成某个时间点的数据快照,文件小、恢复快,但可能丢失两次快照之间的数据。AOF 是记录写命令,可以控制刷盘策略,数据安全性更高,但文件更大、恢复相对较慢。

===

redis的雪崩,击穿,穿透说一下


缓存雪崩是大量 key 同时失效或 Redis 不可用,解决重点是过期时间随机化、高可用和限流降级。
缓存击穿是热点 key 过期,大量请求同时回源,解决重点是互斥锁、热点 key 不过期。
缓存穿透是请求不存在的数据,缓存和数据库都没有,解决重点是缓存空值、布隆过滤器和参数校验。

===

##zset实现的原理


Redis 的 ZSet 底层有两种实现。数据量小、元素短时使用 listpack ,以节省内存;当元素数量或元素长度超过阈值后,会转换成 skiplist + dict。
在 skiplist + dict 结构中,dict 保存 member 到 score 的映射,时间复杂度接近 O(1);skiplist 按 score 排序,平均复杂度是 O(logN)。
Redis 用跳表而不是红黑树,是因为跳表实现简单,范围查询方便,性能也能达到平均 O(logN),非常适合排行榜、延迟队列、Top N 这类场景。

===

docker常用命令


Docker 常用命令主要分几类:镜像用 docker images、docker pull、docker build、docker rmi;
容器用 docker ps、docker run、docker start、docker stop、docker rm;
排查问题常用 docker logs、docker exec、docker inspect、docker stats;
数据持久化用 -v 和 docker volume;
多容器编排一般用 docker compose up -d、docker compose down、docker compose logs -f。

===

docker的基本原理


Docker 的底层原理主要是 Linux 的 Namespace、Cgroups 和 Union FS。Namespace 负责隔离,让容器拥有独立的进程、网络、文件系统视图;Cgroups 负责资源限制,比如 CPU、内存、IO;Union FS 负责镜像分层和可写层叠加。
Docker 本质上是在宿主机内核上运行的一个隔离进程,不是完整虚拟机。

===

docker和虚拟机的区别


Docker 是基于 Linux Namespace、Cgroups 和镜像分层技术实现的轻量级容器,容器之间共享宿主机内核;
虚拟机是通过 Hypervisor 虚拟化硬件,每个虚拟机都有完整操作系统和独立内核。Docker 启动快、资源占用少,适合应用部署和微服务;虚拟机隔离更强,适合运行不同操作系统或安全隔离要求更高的场景。

===

以上是整理的一些通用八股,如果有疑问或者补充可在评论区留言

posted @ 2026-08-24 16:56  Sun-Wind  阅读(11)  评论(0)    收藏  举报