赞
踩
突然想整理下各个大厂的面经,做一个整理,在牛客网上看了一下23年4月到24年1月以来字节后段的面经,做了一个简单的整理。
持续更新中。。。。
这里给出两篇比较好的文章:
https://blog.csdn.net/yangtianle1/article/details/129064594
https://blog.csdn.net/hyg0811/article/details/102366854
本文tcp部分的主要内容也是来自于这两篇文章。
总体流程图
三次握手:
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:
客户端和服务端交换 ISN是三次握手中重要的环节,告诉对方数据是如何按照序号组装的,为了防止攻击者猜出后续的确认号,ISN是随时间变化
第一次、第二次握手不可以携带数据,第三次可以携带数据
防止攻击者在重复在第一次握手时在SYN报文中塞入大量数据,让服务器花费大量资源来接收处理这些报文。
第三次可以携带数据,是因为此时客户端已经是处于ESTABLISHED(连接)状态了,对于客户端来说已经建立连接了。
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的
所以客户端在短时间内伪造大量不存在的ip地址,不断向server发送syn包,server回复确认包并等待client确认时,由于源地址不存在,所以server不断重发最后超时
这些伪造的syn包长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪
检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstat 命令来检测 SYN 攻击。
服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的
但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,只有等服务端所有的报文都发送完了,才能发送FIN报文,因此不能一起发送。故需要四次挥手。
TIME_WAIT状态也称为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报文则有限制其生存时间的TTL字段。
对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。
MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
保证客户端发送的最后一个ACK报文段能够到达服务端
防止“已失效的连接请求报文段”出现在本连接中
理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
当你尝试使用Socket连接到一个未启动的服务器端口时,会发生连接失败的情况。这通常会导致一个错误码,通常是"Connection Refused"(连接被拒绝)
实现单次发送多条数据,极大的提高性能
当TCP开始启动的时候,慢启动阈值等于窗口最大值;
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
HTTP(超文本传输协议)是一种应用层通信协议,承载于TCP协议之上。它允许将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器。
HTTPS协议是一种基于传输层安全协议(TLS)的安全协议,用于保护互联网通信安全。HTTPS协议基于传输层安全协议(TLS)协议,并将HTTP协议封装在TLS协议之上,以实现网络通信安全。具体来说,HTTPS协议的实现包含以下步骤:
HTTP的不安全性
HTTP作为应用层协议,在客户端与服务端之间的通信传输采用的是明文传输,这就导致了其通信如果被第三方截取,那么通信的内容也就完全暴露了,甚至第三方截取后可以对通信内容进行篡改发给服务端,更有甚者,如钓鱼网站,从一开始可能就完全冒充了服务端来与客户端进行通信。这一系列的安全问题,证明了HTTP不够“可靠”,亟需一种解决方案来让HTTP变得安全可靠。这个解决方案,就是HTTPS
什么是HTTPS
其实我们只需要在TCP/IP五层模型中传输层和应用层中间在添加一个所谓的安全层,由安全层对应用层的数据进行安全加解密处理,就可以保证传输的可靠性了。HTTPS其实就是在应用层协议HTTP和传输层协议TCP/IP之间添加了一个SSL/TLS层,SSL即Secure Sockets Layer 安全套接层,TLS即Transport Layer Security 传输安全层,TLS是SSL的前身,因此统称为SSL/TLS层,其作用就是提供私密性,信息完整性和身份认证
相同:
GET 请求和 POST 请求底层都是基于 TCP/IP 协议实现的,使用二者中的任意一个,都可以实现客户端和服务器端的双向交互。
区别:
1、get主要用于获取数据(也可以修改),post主要用于修改数据
2、get将参数拼接到url上,长度也会被限制,post将参数写入到请求正文内,无参数大小限制。
3、get参数在url里明文传递,不安全
4、GET请求会被浏览器主动缓存,比如常见的CSS,JS,HTML请求都会被缓存,如果下次传输的数据相同,那么他们就会返回缓存中的内容,以求更快的展示所需要的数据。
1、数据存放位置不同:cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、安全程度不同:cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。
3、性能使用程度不同:session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
4、数据存储大小不同:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,而session则存储与服务端,浏览器对其没有限制。
tcp和udp属于传输层协议
SDLC、HDLC、PPP、STP、帧中继等。
Nginx是七层负载均衡
lvs是四层负载均衡
二层负载均衡
负载均衡服务器对外提供一个VIP(虚IP),集群中不同的机器采用相同IP地址,但是机器的MAC地址不一样。当负载均衡服务器接受到请求之后,通过改写报文的目标MAC地址的方式将请求转发到目标机器实现负载均衡。
三层负载均衡
和二层负载均衡类似负载均衡服务器对外依然提供一个VIP(虚IP),但是集群中不同的机器采用不同的IP地址。当负载均衡服务器接受到请求之后,根据不同的负载均衡算法,通过IP将请求转发至不同的真实服务器
四层负载均衡:
四层负载均衡只,建立一次TCP连接,工作在OSI模型的传输层,由于在传输层,只有TCP/UDP协议,这两种协议中除了包含源IP、目标IP以外,还包含源端口号及目的端口号,基于IP+端口的负载均衡。通过发布三层的IP地址(VIP),然后加四层的端口号,来决定哪些流量需要做负载均衡,对需要处理的流量进行NAT处理,通过修改数据包的地址信息(IP+端口号)将流量转发到应用服务器。并记录下这个TCP或者UDP的流量是由哪台服务器处理的,后续这个连接的所有流量都同样转发到同一台服务器处理;
七层负载均衡:
负载均衡器与客户端及后端的服务器会分别建立一个TCP连接。即两次TCP连接。就是在四层的基础上(没有四层是绝对不可能有七层的),再考虑应用层的特征,比如同一个Web服务器的负载均衡,除了根据VIP加80端口辨别是否需要处理的流量,还可根据七层的URL、浏览器类别、语言来决定是否要进行负载均衡。举个例子,如果你的Web服务器分成两组,一组是中文语言的,一组是英文语言的,那么七层负载均衡就可以当用户来访问你的域名时,自动辨别用户语言,然后选择对应的语言服务器组进行负载均衡处理。七层就是基于URL等应用层信息的负载均衡
HTTP重定向负载均衡
浏览器向HTTP重定向服务器发送请求后, 该服务器将一台真实的服务器地址写入HTTP响应头的location, 向浏览器返回一个重定向响应. 浏览器自动重新请求新的URL, 完成自动跳转.
缺点:浏览器需要两次请求服务器才能完成一次访问, 性能较差. 重定向服务器自身的负载较大, 可能成为性能瓶颈.
DNS负载均衡
DNS负责提供域名解析服务, 当访问某个站点时, 需要通过DNS服务器获取域名指向的IP地址. DNS会完成域名到IP地址的映射. 这样的映射也可以是一对多的.可以在DNS服务器中配置域名到多个真实IP地址的映射.
DNS服务器便充当了负载均衡调度器, 每次请求会根据负载均衡算法返回不同的IP地址. 将用户请求分发到多台机器上.
反向代理负载均衡
反向代理服务器处于Web服务器前面, 可以将请求根据负载均衡算法转发到不同的Web服务器上. Web服务器处理完后之后再返回给代理服务器, 代理服务器返回给用户. 反向代理负载均衡服务器工作在应用层. 比如Nginx
IP负载均衡
在网络层通过修改请求目标地址进行负载均衡.
数据包到达负载均衡服务器后, 负载均衡服务器在操作系统内核进程获取网络数据包. 修改数据包的IP地址, 将响应转发到真实服务器, 在响应完成之后, 将响应数据包回到负载均衡服务器. 将源IP地址修改为自身IP地址. 然后返回给用户.
数据链路层负载均衡(直接路由)
直接路由使工作在第二层. 通过修改数据包的MAC地址, 将数据包转发到实际的服务器上. 负载均衡服务器将请求的数据的目的MAC地址修改为真实服务器的地址, 由于Web服务器集群的虚拟IP地址和负载均衡服务器的IP地址相同, 数据可以正常传输到达真实MAC地址的服务器. 处理完后之后发送响应数据到网站网关服务器, 网关服务器直接将请求发送到用户.
静态负载均衡算法:
轮询, 权重, IP哈希, URL哈希.
动态负载均衡算法:
动态负载均衡算法: 根据CPU, IO, 网络的处理能力决定如何分发请求. 根据后端服务器的响应时间来分配请求.
区别有:用途不同。安全性不同。目的不同。代理不同。服务对象不同。功能不同。
Go使用协程(goroutines)和通道(channels)来实现并发编程。
groutine能拥有强大的并发实现是通过GPM调度模型实现,下面就来解释下goroutine的调度模型。
每个用户线程对应多个内核空间线程,同时也可以一个内核空间线程对应多个用户空间线程。Go采用这种模型,使用多个内核线程管理多个goroutine。这样结合了以上两种模型的优点,但缺点就是调度的复杂性。
M:M是对内核级线程的封装,数量对应真实的CPU数,一个M就是一个线程,goroutine就是跑在M之上的;
G:代表一个goroutine,它有自己的栈,用于调度。
P:P全称是Processor,处理器,它的主要用途就是用来执行goroutine的。每个Processor对象都拥有一个LRQ(Local Run Queue),未分配的Goroutine对象保存在**GRQ(Global Run Queue )**中,等待分配给某一个P的LRQ中,每个LRQ里面包含若干个用户创建的Goroutine对象。
Golang采用的是M:N线程模型,更详细的说他是一个两级线程模型,但它对系统线程(内核级线程)进行了封装,暴露了一个轻量级的协程goroutine(用户级线程)供用户使用,而用户级线程到内核级线程的调度由golang的runtime负责,调度逻辑对外透明。goroutine的优势在于上下文切换在完全用户态进行,无需像线程一样频繁在用户态与内核态之间切换,节约了资源消耗。
是以runtime中实现的底层同步机制(cas、atomic、spinlock、sem)为基础的。
1 cas(Compare And Swap)和原子运算是其他同步机制的基础
2 自旋锁(spinlock)
3 信号量
协程(coroutine)是一种比线程更轻量级的并发执行单元,它的切换开销和内存栈的占用大小都比线程要小。
只要内存足够,在一个线程中可有上万个或更多的协程。
在网络编程应用中,采用协程后的业务代码比那些采用异步或事件回调方式的代码更好维护。
使用协程的业务逻辑代码表面看上去是同步执行的,编写这些代码时思维是连贯的,更符合人类的思维习惯;而采用异步和回调方式后,业务逻辑代码被分割(分拆)在多个回调方法中,开发人员的思维需要切换,是跳跃的,这样显然容易出错,并且一些场景下还要设计事件或会话上下文数据结构,来保存那些需要在多个回调方法间共享的状态数据。
从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。
协程是在线程中执行的,在Golang中使用环境变量GOMAXPROCS来控制协程使用的线程数,它的缺省值是1,在1.5版本中改为cpu核数。也可在代码中调用
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态。这个过程完全由程序控制,不需要内核进行调度。
协程与线程的关系如下图所示:
资源控制和重用:通过使用协程池,可以控制并发任务的数量,避免资源被过度占用。协程池中预先创建的goroutine可以被重复使用,而不需要频繁地创建和销毁,从而降低了系统开销。
提高性能和吞吐量:协程池能够有效地管理并发任务的执行,通过合理调度和分配任务,可以最大限度地利用系统资源,提高并发性能和吞吐量。通过避免创建大量的goroutine和减少上下文切换,可以减少系统负载,提高处理能力。
控制并发度和资源限制:使用协程池可以限制并发任务的数量,确保系统资源不会被过度占用。可以根据系统的处理能力和资源限制,设置适当的协程池大小,避免资源耗尽和系统崩溃。
避免竞态条件:在多个goroutine并发执行时,如果它们之间共享某些资源,可能会出现竞态条件。使用协程池可以通过限制并发的数量,避免过多的goroutine同时访问共享资源,从而减少竞态条件的发生。
简化并发编程:使用协程池可以将任务的调度和管理逻辑与具体的任务逻辑分离开来,使得并发编程变得更加简单和直观。开发人员只需要关注具体的任务实现,而不需要手动管理goroutine的创建和销毁,以及任务的调度和排队。
变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,通过GC回收。
变量直接存储的值,内存通常在栈中分配。
make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配
内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。
内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。
例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。
应该是10个0还有一个1,因为make会初始化10len为0,append的话是从后面追加
CSP(Communicating Sequential Processes,通信顺序进程)并发模型倡导使用通信的手段来进行共享内存,继而实现多个线程之间的通信。这也是 Golang 倡导使用的并发模型,通过 Channel 来使用。
CSP 有两个核心概念:
go channel 关闭后,读取该 channel 永远不会阻塞,且只会输出对应类型的零值。
nil 可能也是需要 channel传输的值之一,通常我们无法通过判断是否为类型的零值确定 channel 是否关闭。所以为了避免输出无意义的值,我们需要一种合理的方式判断 channel 是否关闭。golang 官方为我们提供了两种方式。
如果发生重复关闭、关闭后发送等问题,会造成 channel panic。那么如何优雅地关闭 channel,是我们关心的一个问题。
这里给出涉及到的比较好的文章:
https://blog.csdn.net/weixin_39725844/article/details/111200870
B树和B+树都是平衡树数据结构,它们广泛用于数据库和文件系统中以支持高效的数据检索。以下是B树和B+树之间的几个主要区别:
节点存储:
B树的每个节点都存储键和数据,而且中间节点即存储键也可以导航到子节点。
B+树的内部节点只存储键,实际的数据则存储在叶节点中。所有的叶节点都是通过指针连接起来的,形成了一个有序链表。
数据访问:
B树允许在任何节点上进行数据访问和搜索。
B+树的数据只能在叶节点中找到,这导致了更加统一的数据访问性能。
磁盘读写:
B树由于数据分布在整个树中,可能会导致更频繁的磁盘读写操作。
B+树的内部节点不包含实际数据,只是索引部分,因此节点可以存更多的键,减少了树的高度,从而减少了磁盘I/O次数。
范围查询:
B树进行范围查询时可能需要回溯到父节点或者在非叶子节点之间遍历。
B+树的叶节点是双向链表,范围查询可以通过遍历链表来顺序访问,非常高效。
MySQL之所以使用B+树而不是B树,主要是因为B+树提供了更高的查询性能和更低的磁盘I/O成本。这在大型数据库系统中尤其重要,因为I/O成本是性能瓶颈的主要来源之一。
B+树在数据库中的广泛应用是由于其对于磁盘存储和查询性能的优化。在索引大量数据时,B+树更能够提供高效稳定的性能,特别是在执行范围查询和顺序访问时。
在存储同样多的数据时,B+树通常会更矮更胖。这是因为B+树的内部节点没有存储数据,可以保存更多的索引键,从而有更高的分支因子。这意味着对于同样多的数据,B+树的高度会比B树低,由此减少了查找时所需的磁盘访问次数,提高了效率。
查询计划:
explain + 查询SQL - 用于显示SQL执行信息参数,根据参考信息可以进行SQL优化:
执行计划:让mysql预估执行操作(一般正确)
type : 查询计划的连接类型, 有多个参数,先从最佳类型到最差类型介绍
性能: null > system/const > eq_ref > ref(索引) > ref_or_null > index_merge > range > index > all
慢:
explain select * from userinfo where email='alex';
type: ALL(全表扫描)
特别的: select * from userinfo limit 1;
快:
explain select * from userinfo where name='alex';
type: ref(走索引)
慢日志查询:
将mysql服务器中影响数据库性能的相关SQL语句记录到日志文件,通过对这些特殊的SQL语句分析,改进以达到提高数据库性能的目的。
慢查询日志参数:
long_query_time : 设定慢查询的阀值,超出设定值的SQL即被记录到慢查询日志,缺省值为10s
slow_query_log : 指定是否开启慢查询日志
log_slow_queries : 指定是否开启慢查询日志(该参数已经被slow_query_log取代,做兼容性保留)
slow_query_log_file : 指定慢日志文件存放位置,可以为空,系统会给一个缺省的文件host_name-slow.log
log_queries_not_using_indexes: 如果值设置为ON,则会记录所有没有利用索引的查询.
#.查询慢日志配置信息 :
show variables LIKE '%query%'
#.修改配置信息
SET GLOBAL slow_query_log = on;
#查看慢日志记录的方式
show variables like '%log_output%';
#设置慢日志在文件和表中同时记录
set global log_output='FILE,TABLE';
#查询时间超过10秒就会记录到慢查询日志中
SELECT sleep(10)
#查看表中的日志
select * from mysql.slow_log;
索引没起作用的情况
使用LIKE关键字的查询语句
在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为“%”,索引不会起作用。只有“%”不在第一个位置索引才会起作用。
使用多列索引的查询语句
MySQL可以为多个字段创建索引。一个索引最多可以包括16个字段。对于多列索引,只有查询条件使用了这些字段中的第一个字段时,索引才会被使用。
优化数据库结构
合理的数据库结构不仅可以使数据库占用更小的磁盘空间,而且能够使查询速度更快。数据库结构的设计,需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。
将字段很多的表分解成多个表
对于字段比较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。
增加中间表
对于需要经常联合查询的表,可以建立中间表以提高查询效率。通过建立中间表,把需要经常联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询,以此来提高查询效率。
分解关联查询
优化LIMIT分页
在系统中需要分页的操作通常会使用limit加上偏移量的方法实现,同时加上合适的order by 子句。如果有对应的索引,通常效率会不错,否则MySQL需要做大量的文件排序操作。
一个非常令人头疼问题就是当偏移量非常大的时候,例如可能是limit 10000,20这样的查询,这是mysql需要查询10020条然后只返回最后20条,前面的10000条记录都将被舍弃,这样的代价很高。
优化此类查询的一个最简单的方法是尽可能的使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候这样做的效率会得到很大提升。
为什么需要锁
在并发访问的数据库系统中,多个用户或线程可能同时对数据库进行读取和写入操作。如果没有合适的控制措施,就会导致数据的不一致性或错误的结果。因此,数据库引入了锁机制来控制对数据的访问和修改。锁的使用可以确保每个操作都按照预期执行,从而保证数据的完整性和一致性。
什么是乐观锁
乐观锁是一种基于乐观思想的并发控制策略。它假设事务之间的冲突很少发生,因此不主动对数据进行加锁。乐观锁的实现方式通常使用数据版本号或时间戳。
在MySQL中,常见的乐观锁实现方式是使用版本号。每个数据记录都有一个对应的版本号,事务在更新数据时,先读取数据的当前版本号,并在提交时检查该版本号是否发生变化。如果没有变化,说明操作是安全的,可以提交;如果发生变化,就需要进行回滚或重试操作。
乐观锁的优势在于减少锁竞争,提高并发性能,但也增加了冲突检测和处理的复杂性。
什么是悲观锁
相对于乐观锁,悲观锁是一种悲观的并发控制策略。它假设事务之间的冲突经常发生,因此采取主动加锁来保证事务的安全性。
在MySQL中,悲观锁可以分为行锁和表锁两种类型。
行锁
行锁是针对数据表中的行记录进行加锁的机制。它可以实现更细粒度的并发控制。
表锁
表锁是对整个数据表进行加锁的机制。在表锁模式下,锁的粒度比行锁大,控制并发的能力相对较弱。
在MySQL中,可以通过使用版本号或时间戳来实现乐观锁。
版本号(Version Number):每次更新数据时,将当前的版本号与要修改的记录的版本号进行比较。只有当两者相等时才能成功更新,否则表示其他事务已经对该记录进行了修改。这种机制需要在应用程序中手动处理并保持版本号的正确性。
时间戳(Timestamp):类似于版本号,不同之处在于使用时间戳而非版本号作为标识符。每次更新操作都会生成一个新的时间戳,然后将此时间戳与要修改的记录的时间戳进行比较。只有当两者相等时才能成功更新,否则表示其他事务已经对该记录进行了修改。
在MySQL中,事务通过以下两种机制来实现:
ACID属性(原子性、一致性、隔离性和持久性):ACID属性保证了数据库操作的可靠性。当开始一个新的事务时,MySQL会将所有对该事务进行修改的语句都记录到日志文件中,这样就能确保在发生故障或系统崩溃后能够正确地还原数据。同时,MySQL使用多版本并发控制技术来提供隔离性,从而防止不同事务之间相互影响。最后,MySQL使用**写前日志(Write-Ahead Logging,WAL)**技术来保证持久性,即只要事务成功提交,其结果将永久存储在磁盘上。
InnoDB引擎支持事务:InnoDB是MySQL默认的存储引擎,也是目前最常用的事务处理引擎。它通过MVCC(Multi-Version Concurrency Control,多版本并发控制)来实现事务的隔离性。每次更新数据时,InnoDB会为被修改的行创建一个新的版本,然后根据需要选择合适的版本返回给查询。此外,InnoDB还利用**redo log(重做日志)**来保证事务的持久性。当事务提交时,InnoDB会先将日志写入内存中的buffer pool,再定期将buffer pool中的日志写入磁盘上的物理日志文件。
两个:a、ab
如果是联合索引 (a,b,c),相当于建立了a, ab, abc 三个索引, 反正就是中间不能断。
String (字符串):这是Redis中最简单的数据结构,其内部表示为一个字符数组。字符串是动态的,即允许修改,且具有预分配内存的空间管理机制,以减少频繁的内存分配和扩容时的性能损耗。123
List (列表):Redis的列表类似于Java语言中的LinkedList,它是一种链表而非数组。列表的插入和删除操作非常快,因为它们的时间复杂度为O(1),但索引定位较慢,时间复杂度为O(n)。列表由压缩列表和快速列表组成,当列表元素较少时使用压缩列表,而当列表较大时则转换为快速列表。
Hash (字典):Redis的哈希或字典是一种键值对的映射结构,其内部使用散列算法来实现快速查找。哈希的结构与Java语言中的HashMap相似,可以通过hgetall
命令来获取所有键对应的值。
Set (集合):集合或无序集是一种不包含重复元素的集合。集合的操作通常涉及交集、并集、差集等运算,以及判断某个元素是否属于该集合。集合的内部结构也采用了散列算法,使得添加和删除元素的速度很快。
ZSet (有序集合):有序集合是一种特殊的集合,其中的元素按照特定的排序规则排列。有序集合提供了类似于数据库中B树索引的能力,用于快速地检索和定位到有序的元素。
使用跳表
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
提示:
1 <= n <= 105
nums.length == n + 1
1 <= nums[i] <= n
nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
官方题解代码:
func findDuplicate(nums []int) int { lo, hi := 1, len(nums)-1 for lo < hi { mid := (lo + hi) >> 1 count := 0 for i := 0; i < len(nums); i++ { if nums[i] <= mid { count++ } } if count > mid { hi = mid } else { lo = mid + 1 } } return lo }
一篇非常好的题解:https://leetcode.cn/problems/find-the-duplicate-number/solutions/262703/zhe-ge-shu-zu-you-dian-te-shu-suo-yi-ke-yi-yong-ku
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。
示例 2:
输入:s = “cbbd”
输出:2
解释:一个可能的最长回文子序列为 “bb” 。
提示:
1 <= s.length <= 1000
s 仅由小写英文字母组成
官方题解代码:
func longestPalindromeSubseq(s string) int { n := len(s) dp := make([][]int, n) for i := range dp { dp[i] = make([]int, n) } for i := n - 1; i >= 0; i-- { dp[i][i] = 1 for j := i + 1; j < n; j++ { if s[i] == s[j] { dp[i][j] = dp[i+1][j-1] + 2 } else { dp[i][j] = max(dp[i+1][j], dp[i][j-1]) } } } return dp[0][n-1] } func max(a, b int) int { if a > b { return a } return b }
一篇非常好的题解:https://leetcode.cn/problems/longest-palindromic-subsequence/solutions/67456/zi-xu-lie-wen-ti-tong-yong-si-lu-zui-chang-hui-wen
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
官方题解代码:
func longestConsecutive(nums []int) int { numSet := map[int]bool{} for _, num := range nums { numSet[num] = true } longestStreak := 0 for num := range numSet { if !numSet[num-1] { currentNum := num currentStreak := 1 for numSet[currentNum+1] { currentNum++ currentStreak++ } if longestStreak < currentStreak { longestStreak = currentStreak } } } return longestStreak }
加一下注释:
func longestConsecutive(nums []int) int { // 创建一个哈希集合,用于存储数组中的数字,这里是乱序的set numSet := make(map[int]bool) for _, num := range nums { numSet[num] = true } // 记录最长连续序列的长度 longestStreak := 0 // 遍历哈希集合中的每个数字 for num := range numSet { // 如果当前数字的前一个数字不在哈希集合中,则当前数字是连续序列的开头,set中可能同时存在多个序列,例如1、2、3、4是第一个序列,7、8、9、10、11、12是第二个序列,这里就会分别找到两个序列 if !numSet[num-1] { currentNum := num currentStreak := 1 // 继续递增当前数字,直到连续序列结束 for numSet[currentNum+1] { currentNum++ currentStreak++ } // 更新最长连续序列的长度 if currentStreak > longestStreak { longestStreak = currentStreak } } } // 返回最长连续序列的长度 return longestStreak }
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。
官方题解代码:
func longestCommonSubsequence(text1, text2 string) int { m, n := len(text1), len(text2) dp := make([][]int, m+1) for i := range dp { dp[i] = make([]int, n+1) } for i, c1 := range text1 { for j, c2 := range text2 { if c1 == c2 { dp[i+1][j+1] = dp[i][j] + 1 } else { dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j]) } } } return dp[m][n] } func max(a, b int) int { if a > b { return a } return b }
这里找到一个B站讲解的十分透彻的案例:最长公共子序列 LCS
案例伪代码:
案例里对应的表格示意图:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
官方题解代码:
func threeSum(nums []int) [][]int { n := len(nums) sort.Ints(nums) ans := make([][]int, 0) // 枚举 a for first := 0; first < n; first++ { // 需要和上一次枚举的数不相同 if first > 0 && nums[first] == nums[first - 1] { continue } // c 对应的指针初始指向数组的最右端 third := n - 1 target := -1 * nums[first] // 枚举 b for second := first + 1; second < n; second++ { // 需要和上一次枚举的数不相同 if second > first + 1 && nums[second] == nums[second - 1] { continue } // 需要保证 b 的指针在 c 的指针的左侧 for second < third && nums[second] + nums[third] > target { third-- } // 如果指针重合,随着 b 后续的增加 // 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环 if second == third { break } if nums[second] + nums[third] == target { ans = append(ans, []int{nums[first], nums[second], nums[third]}) } } } return ans }
官方题解:https://leetcode.cn/problems/3sum/solutions/284681/san-shu-zhi-he-by-leetcode-solution
这个整体比较好理解,有几个需要注意的点:
1、枚举a和b时都需要注意不能和上一次枚举的数不同
2、需要保证b的指针一直在c的左侧
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
提示:
链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000
官方题解代码:
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next
curr.Next = prev
prev = curr
curr = next
}
return prev
}
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
示例 1:
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
示例 2:
输入:head = [5], left = 1, right = 1
输出:[5]
提示:
链表中节点数目为 n
1 <= n <= 500
-500 <= Node.val <= 500
1 <= left <= right <= n
官方题解代码:
func reverseLinkedList(head *ListNode) { var pre *ListNode cur := head for cur != nil { next := cur.Next cur.Next = pre pre = cur cur = next } } func reverseBetween(head *ListNode, left, right int) *ListNode { // 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论 dummyNode := &ListNode{Val: -1} dummyNode.Next = head pre := dummyNode // 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点 // 建议写在 for 循环里,语义清晰 for i := 0; i < left-1; i++ { pre = pre.Next } // 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点 rightNode := pre for i := 0; i < right-left+1; i++ { rightNode = rightNode.Next } // 第 3 步:切断出一个子链表(截取链表) leftNode := pre.Next curr := rightNode.Next // 注意:切断链接 pre.Next = nil rightNode.Next = nil // 第 4 步:同第 206 题,反转链表的子区间 reverseLinkedList(leftNode) // 第 5 步:接回到原来的链表中 pre.Next = rightNode leftNode.Next = curr return dummyNode.Next }
https://www.nowcoder.com/discuss/566306341396951040?sourceSSR=enterprise
https://www.nowcoder.com/discuss/569116932234829824?sourceSSR=enterprise
https://www.nowcoder.com/discuss/572339408033124352?sourceSSR=enterprise
https://www.nowcoder.com/discuss/563096059170316288?sourceSSR=enterprise
https://www.nowcoder.com/discuss/561666823398121472?sourceSSR=enterprise
https://www.nowcoder.com/discuss/559376234627497984?sourceSSR=enterprise
https://www.nowcoder.com/discuss/555904859770314752?sourceSSR=enterprise
https://www.nowcoder.com/discuss/554371366380650496?sourceSSR=enterprise
https://www.nowcoder.com/discuss/550639195967000576?sourceSSR=enterprise
https://www.nowcoder.com/discuss/551074669743378432?sourceSSR=enterprise
https://www.nowcoder.com/discuss/544981012556607488?sourceSSR=enterprise
https://www.nowcoder.com/discuss/542697256915415040?sourceSSR=enterprise
https://www.nowcoder.com/discuss/537645679745757184?sourceSSR=enterprise
https://www.nowcoder.com/discuss/536351021895901184?sourceSSR=enterprise
https://www.nowcoder.com/discuss/536236744748826624?sourceSSR=enterprise
https://www.nowcoder.com/discuss/534130180848095232?sourceSSR=enterprise
https://www.nowcoder.com/discuss/532303076607197184?sourceSSR=enterprise
https://www.nowcoder.com/discuss/531123350265937920?sourceSSR=enterprise
https://www.nowcoder.com/discuss/527828062470144000?sourceSSR=enterprise
https://www.nowcoder.com/discuss/516012580041682944?sourceSSR=enterprise
https://www.nowcoder.com/discuss/513289737344389120?sourceSSR=enterprise
https://www.nowcoder.com/discuss/511213837740269568?sourceSSR=enterprise
https://www.nowcoder.com/discuss/492623452088651776?sourceSSR=enterprise
https://www.nowcoder.com/discuss/491932734969958400?sourceSSR=enterprise
https://www.nowcoder.com/discuss/488469089342533632?sourceSSR=enterprise
https://www.nowcoder.com/discuss/486648762086019072?sourceSSR=enterprise
https://www.nowcoder.com/discuss/478026030490402816?sourceSSR=enterprise
https://www.nowcoder.com/discuss/475259205662806016?sourceSSR=enterprise
https://www.nowcoder.com/discuss/471469172959080448?sourceSSR=enterprise
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。