2025 .net
1. 同步异步,阻塞非阻塞区别:
同步:调用者发出请求后,一直等待被调用者返回结果或通知,才进行下一步操作(即不释放主线程,后续操作请求无法处理,需要等待)。
异步:调用者发出请求后,不等待被调用者返回结果或通知,就进行下一步操作(即主线程释放。继续处理后续请求)。
阻塞:调用者发出请求后,被调用者不返回结果或通知,调用者就一直等待,不能进行其他操作。
非阻塞:调用者发出请求后,被调用者不返回结果或通知,调用者可以继续进行其他操作。
总之,同步异步是针对当前或其他的线程或者进程(多线程),阻塞非阻塞是针对一个线程里面需不需要等待。
2. rabbitmq原理:
RabbitMQ是一个开源的消息代理软件,它实现了AMQP(Advanced Message Queuing Protocol)协议。RabbitMQ使用消息队列来 实现异步通信,可以将消息发送到队列中,然后由消费者从队列中取出消息进行处理。
RabbitMQ消息持久化的方法:交换机持久化——队列持久化+消息持久化,这三个缺一不可。交换机持久化保证了路由策略的持久化,队列持久化保证了路由输送对象的存在,消息必须要发送到队列才能被消费,消息持久化则是你具体发送的消息
RabbitMQ支持多种消息确认机制,包括自动确认、手动确认(ACK机制)和批量确认。自动确认是指RabbitMQ在消息发送到队列后自动确认消息,不需要消费者手动确认。手动确认是指消费者在处理完消息后手动确认消息,这样RabbitMQ才会将消息从队列中删除。
3.垃圾回收机制:
1.引用计数法:每个对象都有一个引用计数器,当对象被引用时,计数器加1,当引用失效时,计数器减1,当计数器为0时,对象被回收。垃圾回收器定期(周期性)找出那些不再被引用的对象,然后释放这些对象所占用的内存。
2. 标记-清除法:从根节点开始遍历所有对象,标记所有可达的对象,然后清除所有未被标记的对象。垃圾回收器定期(周期性)找出那些未被标记的对象,然后释放这些对象所占用的内存。比如A引用B,那么A的可达对象就是B,那么A,B就被标记为可存在的对象不会被清除,但是有一种情况A和B互相引用,他们就没有可达对象,就会被清除。
3. 复制算法:将内存分为两个相等的区域,每次只使用其中一个区域。当垃圾回收器运行时,它会将所有可达的对象复制 到另一个区域,然后释放原来的区域。垃圾回收器定期(周期性)运行,以保持内存的可用性。
4. 分代收集法:将对象分为不同的代,每一代都有不同的存活时间。垃圾回收器定期(周期性)运行,以保持内存的可用性。 包括0,1,2代,0代是刚刚创建的对象,1代是经过一次垃圾回收后仍然存活的对象,2代是经过两次垃圾回收后仍然存活的对象。大对象一般都在稳定的2代,即使需要回收也只是清空数组,因为大对象申请和回收的代价很高。
4. Redis:
1.Redis是单线程的
redis是基于内存操作的,CPU不是操作瓶颈,redis的瓶颈是根据机器内存和网络宽带,那既然CPU不是瓶颈,那就意味着可以用单线程来实现,那就用单线程了!
Redis是C语言写的,官方提供的十万QPS
redis多线程:redis有多线程吗?是新版本的redis有的,但不是Redis本身,而是网络IO多线程,基于IO多路复用(就是同时可以监听多个网络请求,把监听到的请求扔到一个队列里面,redis会从里面读取),IO启用多线程同时监听更多的请求(cpu的发展越来越快,IO多路复用都满足不了)
redis为什么单线程就快?
1.误区:高性能的服务器一定是多线程的,多线程一定比单线程效率高,但其实多线程由于上下文切换是比较影响性能的
2.核心:redis是将所有数据放到内存中的,单线程就是比多线程效率高,省去了多线程的上下文切换的
Redis三大问题:击穿,穿透,雪崩
1.击穿:针对一个热点key,在key过期的瞬间,大量请求打到了数据库,给数据库造成巨大压力,这就是击穿
解决: 1.如果key特殊的话,可以设置永不过期
2.加锁,在第一个请求没有结束之前,拒绝后续请求,不过高并发分布式锁对是个考验
2.穿透:针对不存在的key在,瞬间大量请求打到数据库,给数据库造成压力
解决: 1.布隆过滤器,将所有可能存在的数据通过hash存储,不存在的请求直接拦截,但有一定的出错几率
2.设置空值,请求不存在的key直接返回null,不推荐,null也占内存而且万一将来有了数据就不统一了
3.雪崩:针对大量key同时失效或者直接缓存失效,大量请求打到数据库
解决: 1.事前:
给这些同一时间过期的加上随机数,是他们在不同时间过期
集群+主从-哨兵
2.事中:限流降级,限制请求数量,超过的降级,直接返回默认值或者空或者某些消息
3.事后:redis数据持久化快照或日志,一旦失效马上恢复
5. 索引:
精简版:由B+树实现,聚集索引:叶子节点存索引,非叶子节点存索引和数据。非聚集索引:叶子节点存索引,非叶子节点存索引和主键索引或者数据地址,如果存主键索引则回表,到聚集索引那颗树上在执行一次找到数据。
1.索引:实际上是物理上对数据库的一列或多列的值进行排序的一种存储结构。要想了解索引,首先了解下表。
2.表:表是由段组成,段由区组成,区由页组成,页由行组成,行存放数据。
其中段由存储引擎控制,DBA无法也没必要控制。
*(了解就行)区由连续页组成,大小为1M,一般页大小16K,即一个区64个页。为保证数据页的连续性,存储引擎每次从磁盘申请4~5个区。在独立表中,创建的默认表大小为96K,这96K并非由页组成,而是32个页大小的碎片页,这样做是为了对于一些小表,节省磁盘空间,当使用完这些碎片页后才是64个连续页。
页磁盘管理的最小单位,大小可以设置,一般16K,可设置2K,4K,8K。一旦设置完之后不能再修改。
行存放数据,一页16K/2-200行,即7992行。页与页之间通过双向链表链接。
3.每从磁盘读取一次数据称为一次磁盘IO,那么一次读取多少呢,这是不确定的,为了提高性能,对于数据请求,数据库会先放在缓存中,之后一次性将缓存里的所有请求合并成一个IO处理。当我们读取到数据的时候,该数据所在页也会被读取出来,这称为局部预读原理,即当一个数据被读取的时候,那么他相邻的数据也可能很快被访问到。
了解了数据读取后,我们来了解索引是怎么回事。
索引实际上是一个B+树,什么是B+树呢?
那就要从二叉树说起,我们查找某个数据的位置时,最快的方法是将这些数据排序,再从这些数据的中间开始进行对比,若小于中间值则往左边查找,反之从右边,之后也是以此类推直到找到数据位置。这种查找方法叫做二分查找法。二叉树就是基于二分查找法,二叉树可以随意构造,但是查找效率一般,于是出现了平衡二叉树,平衡二叉树必须以以中间值作为根节点,即将中间数据作为根节点提起,两边的数据分别再取中间值作为左右子节点,以此类推,形成一颗倒挂的树。
B树是一颗特别的平衡树,他属于多叉树,即一个节点允许有多个叶子节点。B+树是B树的升级版,相对于B树,B+树的非叶子节点并不保存数据,所有数据都在叶子节点。非叶子节点只保存数据指针,这样使得每个非叶子节点能存储的数据大大增大,使得这棵树的深度更加小,搜索时跟更加快。
B+树的叶子结点数据有序排列,每个数据的结尾都会保存下一个数据的指针,这样整个数据形成一个链表使得查找更快。

总结:B+树非叶子节点存储索引信息,不存储数据。
每一个非叶子节点可以理解为一个键值对数组,通过二分查找法可以快速定位到下一层子节点。
每一个叶子节点存储的是索引+数据。这也是聚集索引的原理。非聚集索引叶子节点存储的是索引+数据指针(一般是主键)。
聚集索引:按照B+树有序排列,叶子节点存放数据。只能有一个。(因为叶子节点要存放实际数据,占有空间较大,多个的话会浪费空间得不偿失,所以一般一个表只有一个聚集索引)
非聚集索引:按照B+树有序排列,叶子节点不存放数据,只存放地址。根据这个地址再从聚集索引树中查找数据(也叫回表),因此,非聚集索引也叫做辅助索引。可以有多个。
5.5 Mysql日志:
undolog日志:
回滚日志,提供事务回滚能力,保证事务原子性。也是实现mvcc的主要支撑,用于实现事务读已提交和可重复读的隔离性。原理:每次事务开启时,都会将当前数据“快照”一下,“快照”的这个版本的数据包含了该事务的id,undolog日志id信息,便于事物回滚时根据这个id找到这些数据,这个快照也叫做一致性视图,他的实现原理是:在你的事务里面的更新操作时,会将当前数据复制一份,原数据作为一份快照并绑定当前事务id,新数据去做更新操作。这样如果事务回滚的话就可以根据事务id找到对应的快照将数据恢复。这种“快照”(一致性视图)就是mvcc多版本并发控制的原理。当没有事务绑定该快照日志时,会有数据库线程自动清理。
redolog日志:
重做日志,提供数据库崩溃后数据恢复的能力,确保一般sql语句或者事务的持久性。他也是三种日志里面唯一一个物理性变化的日志(唯一可以讲数据记录到磁盘上的)。但是他是一个固定大小的空间,每次redolog刷进磁盘后,redolog就会清空等待下一次写入。
bindolog日志:
追加日志,是一个二进制文件,可以记录所有数据的sql操作,主要用于主从复制或者数据恢复,但是注意的是他是纯sql语句,大量数据的恢复是很慢的,而且他的文件大小很大,需要定时清理部分文件。
6.字典:
哈希原理(必读):https://www.cnblogs.com/lvqiang/p/18796004
字典的查询之所以非常快,主要是因为它基于哈希表实现,能够在常数时间复杂度(O(1))内完成查找。哈希表通过哈希函数将键映射到数组位置,使得查找操作可以直接访问对应的元素,并且通过冲突解决机制确保即使发生哈希冲突,也能高效查找。
通过哈希表(Hashtable)实现的。哈希表的基本原理是将键通过 哈希函数 转换为一个整数值(哈希值),然后根据这个哈希值直接计算出存储位置,从而实现快速查找。
哈希表的查询过程可以理解为:
- 通过哈希函数快速计算键的哈希值。
- 根据哈希值取模直接访问数组的位置。
- 如果没有冲突,查找操作就是直接访问数组元素,时间复杂度为 O(1)。
- 如果存在冲突(映射的位置已经有数据了)就需要通过寻址法或者链表解决,一般是通过链表。
Dictionary 实现通常通过内存优化的方式存储数据。它使用连续的内存块来存储桶和元素,这使得查询时可以高效地利用 CPU 缓存。连续的内存布局减少了缓存失效的可能性,因此查询性能得到进一步提升。
7.Task async await:
精简版:async标记为异步方法,生成状态机方法。await挂起方法,开启子线程去执行该方法,主线程返回继续执行主线程的任务,子线程执行完毕回调通知主线程,主线程继续执行该方法。
async 关键字
async用于标记一个方法,表示这个方法是异步的。在 C# 中,异步方法通常会返回Task或Task<T>(泛型版本),但是对于无返回值的异步方法,返回类型为Task;对于有返回值的异步方法,返回类型为Task<T>。- 本质上,
async方法会将方法的执行转为异步执行,使得你可以在方法内部使用await来等待异步操作完成。 - 转换机制:当一个方法被标记为
async时,编译器会自动将该方法转换为一个状态机。这个状态机用于处理异步操作的暂停、恢复和状态管理,确保在异步操作完成后能够恢复执行。
1.2 await 关键字
await用于等待一个异步操作的结果。它只能在async方法内使用。- 本质上:
await会使当前方法的执行暂停,直到异步操作完成。它不会阻塞线程,而是允许线程继续执行其他任务,直到异步操作完成时再回来继续执行后续代码。 await会"挂起"异步方法的执行,但它不会阻塞调用线程(如 UI 线程)。线程可以继续做其他事情,直到异步操作完成。
1. Task 的作用
Task 是 C# 中表示异步操作的一种类型。它是基于线程池的异步操作,它表示一个正在进行的操作,并且能够在操作完成时提供结果或者异常。
Task可以表示 没有返回值 的异步操作。Task<T>可以表示 有返回值 的异步操作,返回一个类型为T的结果。
Task 通过与线程池协同工作来实现异步操作。当调用异步方法时,操作被分派到线程池中的线程执行,调用者可以继续执行其它代码,而不需要阻塞等待任务完成。
3. 状态机原理(了解)
在 async 和 await 的背后,C# 编译器将异步方法转换成一个 状态机,这是理解其工作原理的核心。
假设你有一个简单的 async 方法,如下所示:
public async Task ExampleMethod() { await Task.Delay(1000); Console.WriteLine("Done!"); }
编译器会将这个方法转化为一个有限状态机(Finite State Machine)。原理如下:
-
编译器首先将
ExampleMethod方法转换为一个 异步方法的状态机,将它分解成多个部分。状态机会记录每个阶段的执行状态。 -
当执行到
await Task.Delay(1000)时,状态机会挂起方法的执行,等待Task.Delay完成。此时,方法的执行会返回给调用者,线程不会被阻塞。 -
在
Task.Delay完成后,状态机会恢复执行,继续执行后面的Console.WriteLine("Done!")语句。
public async Task<int> AsyncMethod() { int result = await Task.Run(() => 42); return result; }
这段代码的执行流程如下:
-
调用
AsyncMethod时,它会立即返回一个Task<int>,并异步执行方法体的内容。 -
执行到
await Task.Run(() => 42)时:Task.Run启动一个新线程(或线程池中的工作线程),并异步执行() => 42这个 lambda 表达式。await暂停当前方法的执行,允许控制权返回给调用者线程,避免阻塞。
-
等待
Task.Run返回的Task完成后,恢复执行,result变量将被赋值为 42。 -
最后返回
result,方法结束。
5. 线程与任务调度
async 和 await 的实现并不意味着每个异步操作都在不同的线程上运行。事实上,异步编程的关键并不在于线程切换,而是在于 非阻塞 操作。
-
线程池:C# 中的
Task通常使用线程池来执行异步任务。Task.Run会将工作任务提交给线程池,线程池管理的线程会执行任务,执行完成后线程会被返回给线程池。 -
I/O 操作:对于一些 I/O 密集型的操作(如网络请求、文件读取等),异步编程可以通过不占用线程的方式,提高效率。例如,
await操作会等待操作完成,但不会占用线程,线程池线程会释放出来,去执行其它任务。只有当 I/O 操作完成后,才会恢复执行当前任务。
8.集合和数组:
精简版:数组大小确定,不能动态增减,但是访问快。集合可以动态扩容,方便增删,但是扩容有性能开销,需要重新开辟空间。

数组(Array):数组是一个固定大小的集合。一旦数组的大小确定,就无法更改(即不能动态增加或减少元素数量)。数组的大小是由其初始化时确定的,并且在整个生命周期中是固定的。
List<T> 是一个动态集合,可以根据需要自动扩展和收缩。当元素添加到 List<T> 时,它会自动调整大小,并且可以动态地增加或减少元素。
-
数组:数组的性能通常较高,特别是在固定大小的情况下。因为它是一个连续的内存块,访问元素时,索引操作非常高效(O(1) 时间复杂度)。
-
List<T>:
List<T>由于其内部的动态调整机制,在频繁添加元素时,超过当前数组的容量时可能会重新分配数组,因此在某些情况下性能可能较低。特别是在容量超过当前数组时,List<T>需要分配一个更大的数组并将元素复制过去,这可能导致性能开销。然而,在实际应用中,List<T>的性能仍然足够高,且它的动态扩展特性使得它在大多数场景下非常有用。List<T>在容量不足时,通常会将容量翻倍,这样可以减少频繁扩展时的性能开销。
-
数组:数组的大小在创建时固定,并且在内存中连续分配。它占用的内存空间是连续的,因此访问速度非常快。
List<T>是一个动态扩展的集合,它内部使用一个数组来存储元素。当List<T>的容量不足时,会创建一个新的、更大的数组来存储元素,这个过程会涉及内存重新分配。
******
字典(在 Python 中是 dict)和集合(在 Python 中是 set)都使用哈希表(hash table)作为底层数据结构,它们的扩容机制在哈希表的负载因子(load factor)超过一定阈值时触发。这些数据结构的扩容机制旨在确保它们在保持高效操作的同时,不会因过多的元素导致性能下降。
字典和集合的扩容机制
精简版:靠阈值,超过则容量翻倍。
1. 哈希表基础
字典和集合内部通过哈希表来存储键或元素。哈希表的实现依赖于哈希函数,该函数将输入的键(或元素)映射到哈希表中的一个槽(bucket)。每当插入一个新元素时,哈希表就会计算该元素的哈希值,并把它存储在合适的槽中。
2. 负载因子与扩容
负载因子是指哈希表中元素的数量与哈希表大小(槽的数量)之比。为了保持查找、插入和删除操作的效率,Python 会根据负载因子决定何时扩容。具体来说:
- 当哈希表的负载因子超过预设的阈值时,哈希表会扩容(通常是扩展到原来大小的两倍)。
- 扩容操作时,所有的元素都需要重新哈希,因为新的槽数会导致哈希值的映射发生变化。因此,扩容的操作相对比较昂贵,通常会导致性能上的临时下降。
3. 字典(dict)的扩容
- 负载因子阈值:字典在 Python 中默认的负载因子阈值约为 2/3。当字典中的元素数量超过哈希表容量的 2/3 时,就会触发扩容。
- 扩容过程:扩容时,字典会将其内部的哈希表大小扩大一倍,并重新计算每个键的哈希值,然后将它们重新分布到新的哈希表中。
例如,当字典的元素数达到原容量的 2/3 时,就会扩容,并且容量翻倍。
d = {1: 'a', 2: 'b', 3: 'c'}
# 当插入新元素时,字典可能会触发扩容
d[4] = 'd'
4. 集合(set)的扩容
集合的扩容机制与字典类似,因为它们底层都是哈希表。集合的元素存储在哈希表中,并根据负载因子来决定是否扩容。
- 负载因子阈值:与字典类似,集合的负载因子也默认约为 2/3。当集合中的元素数量超过哈希表容量的 2/3 时,会触发扩容。
- 扩容过程:扩容时,集合会将哈希表的大小扩大一倍,并重新哈希所有元素,保证每个元素都被合理地映射到新的哈希表中。
例如:
s = {1, 2, 3}
# 当添加新元素时,集合可能会触发扩容
s.add(4)
5. 扩容后的性能
-
扩容操作的时间复杂度:扩容本身的时间复杂度是 O(n),其中 n 是哈希表中现有元素的数量。这是因为需要重新计算每个元素的哈希值并将其放入新的哈希表中。然而,这种操作并不是每次插入都会发生,因此在多次插入后,平均插入时间复杂度仍然为 O(1)。
-
动态调整容量:有时,哈希表会在负载因子较低时进行缩容(例如,当元素被删除时)。这有助于减少内存占用。
总结
字典和集合的扩容机制本质上是通过 哈希表 实现的,当哈希表的负载因子超过一定阈值时,会自动扩容。扩容过程会使哈希表的大小翻倍,并重新哈希现有元素,从而保证性能不会因为哈希表的过度填充而下降。这种机制的目的是平衡时间复杂度和内存消耗,确保字典和集合在操作时保持高效。
9.async void 和async Task 有什么区别:

总的来说,除非有特殊需要(如事件处理程序等),否则应该尽量避免使用 async void,而是使用 async Task 来实现异步操作,这样能使得你的代码更易于控制、处理异常以及确保异步任务的正确执行。
10.中间件 (Middleware) 在 ASP.NET Core 中的作用
中间件原理(必读):https://www.cnblogs.com/lvqiang/p/18789084
中间件是一个处理 HTTP 请求和响应的组件,它在 ASP.NET Core 应用程序的请求管道中按顺序执行。每个中间件都可以处理请求、修改请求、调用下一个中间件,或者终止请求处理链并生成响应。
中间件的用途:
- 请求处理:处理传入的请求,例如身份验证、日志记录、路由选择等。
- 响应修改:处理响应,修改响应内容或头信息,例如压缩响应、添加 CORS 头等。
- 异常处理:捕获和处理应用程序中的异常,防止应用程序崩溃。
- 性能优化:例如缓存中间件,它可以提高性能,通过缓存重复请求的结果来减少服务器负担。
- 安全性:添加认证和授权中间件,确保只有经过授权的用户能够访问特定资源。
中间件的工作原理
ASP.NET Core 中的中间件按顺序执行,请求进入管道时会经过每一个中间件,直至最终的处理或者返回响应。当请求到达中间件时,中间件可以选择:
- 继续将请求传递给管道中的下一个中间件。
- 或者终止请求链,并直接生成响应返回客户端。
当响应返回时,中间件也可以处理响应并修改它。中间件的执行顺序是非常重要的,必须按照正确的顺序来配置和执行。
1. 统一的请求处理管道
在 ASP.NET Core 中,所有的 HTTP 请求都会经过一系列的中间件组件(Middleware),这些中间件按顺序处理请求和响应。每个中间件都可以执行特定的任务,比如身份验证、授权、日志、CORS、静态文件处理等。这个 请求管道 是平台无关的,意味着它可以在不同操作系统上有相同的行为。
- 平台无关性:无论是在 Windows、Linux、macOS 上,ASP.NET Core 的请求管道都可以以相同的方式执行。开发者只需在任意平台上定义管道(添加中间件)即可,ASP.NET Core 会根据运行平台处理请求,而不需要针对不同平台编写不同的代码。
2. 通过中间件实现灵活扩展
中间件使得 ASP.NET Core 变得非常灵活且可扩展。在一个跨平台的环境中,能够在管道中灵活添加或移除不同的中间件组件,能够根据不同的需求定制应用的行为。这种灵活性有助于跨平台支持,因为它允许开发者根据运行环境来调整和配置管道,而不需要修改底层的框架代码。
例如,开发者可以为不同的操作系统配置不同的日志机制或文件系统访问方法,但核心的管道结构和机制保持不变,确保了跨平台的一致性。
3. 跨平台的一致性
ASP.NET Core 通过中间件机制将请求处理与平台和服务器的实现细节分离。这意味着,开发者不需要关心底层操作系统如何处理请求和响应,而是专注于配置和顺序设置中间件。这种设计使得 ASP.NET Core 在不同平台上都能以一致的方式运行。
4. 与平台无关的抽象
ASP.NET Core 的请求管道实际上是通过 Kestrel(ASP.NET Core 的跨平台 Web 服务器)来承载的。Kestrel 作为一个高性能的跨平台 HTTP 服务器,负责处理所有请求,ASP.NET Core 提供的管道中间件可以在 Kestrel 上灵活运作,而不会被特定平台的细节所限制。
因此,管道中间件为 ASP.NET Core 提供了一种非常强大且一致的处理方式,这个机制抽象了许多操作系统和底层细节,使得它能在多个平台上无缝运行。
跨平台区别:https://www.cnblogs.com/lvqiang/p/18710084

浙公网安备 33010602011771号