常见面试题2

常见面试题2用过哪些 Linux 命令

欢迎大家来到IT世界,在知识的湖畔探索吧!

用过哪些Linux命令?

文件和目录操作命令: mv cp rm mkdir cd ls pwd

文件内容查看和编辑命令:cat、tail、less、more、head、vi

系统信息查看命令:ps、top、free、df、du

权限相关: chmod chown useradd groupadd

⽹络相关命令:tcpudmp、ifconfig、netstat、wget

测试相关: 测试⽹络连通性:ping 测试端⼝连通性:telnet

分布式锁除了redisson,还能怎么实现?

  • 基于MySQL:通过 for update 排他锁,对特定数据记录加排他锁。我们可以当做查询到数据时,则获取到分布式锁,接下来执行方法中的逻辑。在方法执行完毕后,提交事务,锁会自动释放。
  • 基于 ZooKeeper:利用临时顺序节点来实现分布式锁的获取和释放。

分布式锁算法Redlock(红锁)

它是基于多个 Redis 节点的分布式锁,即使有节点发⽣了故障,锁变量仍然是存在的,客户端还是 可以完成锁操作。官⽅推荐是⾄少部署 5 个 Redis 节点,⽽且都是主节点,它们之间没有任何关系,都是⼀个个孤⽴的节点。

Redlock 算法的基本思路,是让客户端和多个独⽴的 Redis 节点依次请求申请加锁,如果客户端能 够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加 锁失败。

这样⼀来,即使有某个 Redis 节点发⽣故障,因为锁的数据在其他节点上也有保存,所以客户端仍 然可以正常地进⾏锁操作,锁的数据也不会丢失。

如果有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。

mysql update 会上哪些锁?

如果是可重复读隔离级别下,会加行级锁,主要有:

记录锁:锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥

间隙锁:只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

当我范围查询的时候,他是怎么上锁的?

唯一索引的范围查询加锁规则

当唯⼀索引进⾏范围查询时,会对每⼀个扫描到的索引加 next-key 锁,然后如果遇到下⾯这些情 况,会退化成记录锁或者间隙锁:

情况⼀:针对「⼤于等于」的范围查询,因为存在等值查询的条件,那么如果等值查询的记录是 存在于表中,那么该记录的索引中的 next-key 锁会退化成记录锁。

情况⼆:针对「⼩于或者⼩于等于」的范围查询,要看条件值的记录是否存在于表中:

  • 当条件值的记录不在表中,那么不管是「⼩于」还是「⼩于等于」条件的范围查询,扫描到终⽌ 范围查询的记录时,该记录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这 些记录的索引上加 next-key 锁。
  • 当条件值的记录在表中,如果是「⼩于」条件的范围查询,扫描到终⽌范围查询的记录时,该记 录的索引的 next-key 锁会退化成间隙锁,其他扫描到的记录,都是在这些记录的索引上加 nextkey 锁;如果「⼩于等于」条件的范围查询,扫描到终⽌范围查询的记录时,该记录的索引 nextkey 锁不会退化成间隙锁。其他扫描到的记录,都是在这些记录的索引上加 next-key 锁。

非唯一索引的范围查询加锁规则

非唯一索引和主键索引的范围查询的加锁也有所不同,不同之处在于非唯一索引范围查询,索引的 next-key lock 不会有退化为间隙锁和记录锁的情况,也就是非唯一索引进行范围查询时,对二级索引记录加锁都是加 next-key 锁。

如何给数据库中数据加行级锁?

InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。

普通的 select 语句是不会对记录加锁的,因为它属于快照读。如果要在查询时对记录加行锁,可以使用下面这两个方式,这种查询会加锁的语句称为锁定读.

//对读取的记录加共享锁 select ... lock in share mode; //对读取的记录加独占锁 select ... for update;

欢迎大家来到IT世界,在知识的湖畔探索吧!

上面这两条语句必须在一个事务中,因为当事务提交了,锁就会被释放,所以在使用这两条语句的时候,要加上 begin、start transaction 或者 set autocommit = 0。

共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。

常见面试题2



欢迎大家来到IT世界,在知识的湖畔探索吧!

JWT 令牌都有哪些字段

JWT令牌由三个部分组成:头部(Header)载荷(Payload)签名(Signature)。其中,头部和载荷均为JSON格式,使⽤Base64编码进⾏序列化,⽽签名部分是对头部、载荷和密钥进⾏签名后的结果。

常见面试题2

JWT 令牌如果泄露了,怎么解决,JWT是怎么做的?

  • 及时失效令牌:当检测到JWT令牌泄露或存在⻛险时,可以⽴即将令牌标记为失效状态。服务器 在接收到带有失效标记的令牌时,会拒绝对其进⾏任何操作,从⽽保护⽤户的身份和数据安全。
  • 刷新令牌:JWT令牌通常具有⼀定的有效期,过期后需要重新获取新的令牌。当检测到令牌泄露 时,可以主动刷新令牌,即重新⽣成⼀个新的令牌,并将旧令牌标记为失效状态。这样,即使泄 露的令牌被恶意使⽤,也会很快失效,减少了被攻击者滥⽤的⻛险。
  • 使⽤⿊名单:服务器可以维护⼀个令牌的⿊名单,将泄露的令牌添加到⿊名单中。在接收到令牌时,先检查令牌是否在⿊名单中,如果在则拒绝操作。这种⽅法需要服务器维护⿊名单的状态,对性能有⼀定的影响,但可以有效地保护泄露的令牌不被滥⽤。

网关统一鉴权怎么做的?

  • 用户登录 -> 认证中心生成 JWT
  • 客户端请求时,将 JWT 放入请求头(Authorization: Bearer …)
  • 网关拦截请求,提取并校验 Token
  • 验证通过则放行,并把用户信息(userId、角色)传给下游服务
  • 失败则返回 401 或 403

Redis怎么实现的io多路复用?

这里“多路”指的是多个网络连接客户端,“复用”指的是复用同一个线程(单进程)。I/O 多路复用其实是使用一个线程来检查多个 Socket 的就绪状态,在单个线程中通过记录跟踪每一个 socket(I/O流)的状态来管理处理多个 I/O 流。如下图是 Redis 的 I/O 多路复用模型

常见面试题2

如上图对 Redis 的 I/O 多路复⽤模型进⾏⼀下描述说明:

  • ⼀个 socket 客户端与服务端连接时,会⽣成对应⼀个套接字描述符(套接字描述符是⽂件描述符 的⼀种),每⼀个 socket ⽹络连接其实都对应⼀个⽂件描述符。
  • 多个客户端与服务端连接时,Redis 使⽤ I/O 多路复⽤程序 将客户端 socket 对应的 FD 注册到监 听列表(⼀个队列)中。当客服端执⾏ read、write 等操作命令时,I/O 多路复⽤程序会将命令封装 成⼀个事件,并绑定到对应的 FD 上。
  • ⽂件事件处理器使⽤ I/O 多路复⽤模块同时监控多个⽂件描述符(fd)的读写情况,当 accept、 read、write 和 close ⽂件事件产⽣时,⽂件事件处理器就会回调 FD 绑定的事件处理器进⾏处理 相关命令操作。

例如:以 Redis 的 I/O 多路复⽤程序 epoll 函数为例。多个客户端连接服务端时,Redis 会将客户端 socket 对应的fd 注册进 epoll,然后 epoll 同时监听多个⽂件描述符(FD)是否有数据到来,如果有数据来了就通知事件处理器赶紧处理,这样就不会存在服务端⼀直等待某个客户端给数据的情形。

  • 整个⽂件事件处理器是在单线程上运⾏的,但是通过 I/O 多路复⽤模块的引⼊,实现了同时对多 个 FD 读写的监控,当其中⼀个 client 端达到写或读的状态,⽂件事件处理器就⻢上执⾏,从⽽ 就不会出现 I/O 堵塞的问题,提⾼了⽹络通信的性能。
  • Redis 的 I/O 多路复⽤模式使⽤的是 Reactor 设置模式的⽅式来实现。

Redis的网络模型是怎样的?

Redis 6.0 版本之前,是用的是单Reactor单线程的模式

常见面试题2

单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。但是,这种方案存在 2 个缺点:

  • 因为只有一个进程,无法充分利用 多核 CPU 的性能
  • Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;

单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。

到 Redis 6.0 之后,就将网络IO的处理改成多线程的方式了,目的是为了这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。

所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。

String 是使用什么存储的?为什么不用 c 语言中的字符串?

Redis 的 String 字符串是用 SDS 数据结构存储的。

常见面试题2

结构中的每个成员变量分别介绍下:

  • len,记录了字符串⻓度。这样获取字符串⻓度的时候,只需要返回这个成员变量值就⾏,时间复 杂度只需要 O(1)。
  • alloc,分配给字符数组的空间⻓度。这样在修改字符串的时候,可以通过 alloc – len 计算出 剩余的空间⼤⼩,可以⽤来判断空间是否满⾜修改需求,如果不满⾜的话,就会⾃动将 SDS 的空 间扩展⾄执⾏修改所需的⼤⼩,然后才执⾏实际的修改操作,所以使⽤ SDS 既不需要⼿动修改 SDS 的空间⼤⼩,也不会出现前⾯所说的缓冲区溢出的问题。
  • flags,⽤来表示不同类型的 SDS。⼀共设计了 5 种类型,分别是 sdshdr5、sdshdr8、 sdshdr16、sdshdr32 和 sdshdr64,后⾯在说明区别之处。
  • buf[],字符数组,⽤来保存实际数据。不仅可以保存字符串,也可以保存⼆进制数据。

总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,⽤来解 决 C 语⾔字符串的缺陷

二进制安全

因为 SDS 不需要用 “\0” 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “\0” 的数据。但是 SDS 为了兼容部分 C 语言标准库的函数, SDS 字符串结尾还是会加上 “\0” 字符。

不会发生缓冲区溢出

所以,Redis 的 SDS 结构⾥引⼊了 alloc 和 len 成员变量,这样 SDS API 通过 alloc – len 计 算,可以算出剩余可⽤的空间⼤⼩,这样在对字符串做修改操作的时候,就可以由程序内部判断缓 冲区⼤⼩是否⾜够⽤。

⽽且,当判断出缓冲区⼤⼩不够⽤时,Redis 会⾃动将扩⼤ SDS 的空间⼤⼩,以满⾜修改所需的⼤⼩。

Zset 使用了什么数据结构?

Zset 类型的底层数据结构是由压缩列表跳表实现的:

  • 如果有序集合的元素个数⼩于 128 个,并且每个元素的值⼩于 64 字节时,Redis 会使⽤压缩列表作为 Zset 类型的底层数据结构;
  • 如果有序集合的元素不满⾜上⾯的条件,Redis 会使⽤跳表作为 Zset 类型的底层数据结构;

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

SkipList 了解吗?

链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据。

那跳表长什么样呢?我这里举个例子,下图展示了一个层级为 3 的跳表。

常见面试题2

如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点 4。

跳表中查找一个元素的时间复杂度为O(logn),空间复杂度是 O(n)。

压缩列表是怎么实现的

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。

常见面试题2

压缩列表在表头有三个字段:

  • zlbytes,记录整个压缩列表占用对内存字节数;
  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段(zllen)的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素

常见面试题2

压缩列表节点包含三部分内容:

  • prevlen,记录了「前⼀个节点」的⻓度,⽬的是为了实现从后向前遍历;
  • encoding,记录了当前节点实际数据的「类型和⻓度」,类型主要有两种:字符串和整数。
  • data,记录了当前节点的实际数据,类型和⻓度都由 encoding 决定;

当我们往压缩列表中插入数据时,压缩列表就会根据数据类型是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的。

压缩列表的缺点是会发生连锁更新的问题,因此连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。

因此,压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。

如何保证缓存的一致性?

常见面试题2

针对删除缓存异常的情况,可以使用 2 个方案避免:

  • 删除缓存重试策略(消息队列)
  • 订阅 binlog,再删除缓存(Canal+消息队列)

「先更新数据库,再删缓存」的策略的第⼀步是更新数据库,那么更新数据库成功,就会产⽣⼀条 变更⽇志,记录在 binlog ⾥。

于是我们就可以通过订阅 binlog ⽇志,拿到具体要操作的数据,然后再执⾏缓存删除,阿⾥巴巴开 源的 Canal 中间件就是基于这个实现的。

将binlog⽇志采集发送到MQ队列⾥⾯,然后编写⼀个简单的缓存删除消息者订阅binlog⽇志,根据 更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存⼀致性。

raft

Raft 是一种分布式一致性算法,用于在分布式系统中实现数据的一致性和高可用性。

  • 作用:
    • 用于 Consul Server 节点之间的数据一致性。
    • 选举 Leader 节点,负责处理写请求和日志复制。
  • 工作原理:
  • Leader 选举:
      • 当集群启动或 Leader 失效时,所有 Server 节点参与选举。
      • 获得多数票的节点成为 Leader。
  • 日志复制:
      • Leader 接收客户端请求,并将日志复制到其他 Server 节点。
      • 当日志被多数节点确认后,Leader 提交日志并通知客户端。
  • 故障恢复:
      • 如果 Leader 失效,剩余节点会重新选举新的 Leader。
  • 优点:
    • 强一致性:确保所有 Server 节点的数据一致。
    • 高可用性:即使部分节点失效,集群仍能正常工作。
    • 易于理解:Raft 的设计目标是易于理解和实现。
  • 在 Consul 中的应用:
    • 用于 Consul Server 节点之间的数据同步和一致性。
    • 确保服务注册、健康检查等数据的一致性。

死锁发生条件是什么?

死锁只有同时满⾜以下四个条件才会发⽣:

  • 互斥条件:互斥条件是指多个线程不能同时使⽤同⼀个资源。
  • 持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,⼜想申请资源 2,⽽资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释 放⾃⼰已经持有的资源 1。
  • 不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在⾃⼰使⽤完之前不能被其他线程 获取,线程 B 如果也想使⽤此资源,则只能在线程 A 使⽤完并释放后才能获取。
  • 环路等待条件:环路等待条件指的是,在死锁发⽣的时候,两个线程获取资源的顺序构成了环形链。

如何避免死锁?

避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件

那什么是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源

常见面试题2

银行家算法

银⾏家算法的核⼼思想,就是在分配给进程资源前,⾸先判断这个进程的安全性,也就是预执⾏, 判断分配后是否产⽣死锁现象。

如果系统当前资源能满⾜其执⾏,则尝试分配,如果不满⾜则让该进程等待。通过不断检查剩余可⽤资源是否满⾜某个进程的最⼤需求,如果可以则加⼊安全序列, 并把该进程当前持有的资源回收;不断重复这个过程,看最后能否实现让所有进程都加⼊安全序列。安全序列⼀定不会发⽣死锁,但没有死锁不⼀定是安全序列。

介绍一下操作系统内存管理

操作系统设计了虚拟内存,每个进程都有自己的独立的虚拟内存,我们所写的程序不会直接与物理内打交道。

常见面试题2

Linux 是通过对内存分页的方式来管理内存分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB。

虚拟地址与物理地址之间通过页表来映射,如下图:

常见面试题2

页表是存储在内存里的,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的工作。 而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

因为内存分⻚机制分配内存的最⼩单位是⼀⻚,即使程序不⾜⼀⻚⼤⼩,我们最少只能分配 ⼀个⻚,所以⻚内会出现内存浪费,所以针对内存分⻚机制会有内部内存碎⽚的现象。

在分⻚机制下,虚拟地址分为两部分,⻚号⻚内偏移。⻚号作为⻚表的索引,⻚表包含物理⻚每 ⻚所在物理内存的基地址,这个基地址与⻚内偏移的组合就形成了物理内存地址,⻅下图。

常见面试题2

总结⼀下,对于⼀个内存地址转换,其实就是这样三个步骤:

  • 把虚拟内存地址,切分成⻚号和偏移量;
  • 根据⻚号,从⻚表⾥⾯,查询对应的物理⻚号;
  • 直接拿物理⻚号,加上前⾯的偏移量,就得到了物理内存地址
常见面试题2

URL从输入到响应的流程?

常见面试题2

  • 解析URL:分析 URL 所需要使⽤的传输协议和请求的资源路径。如果输⼊的 URL 中的协议或者 主机名不合法,将会把地址栏中输⼊的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了⾮法字符,则对⾮法字符进⾏转义后在进⾏下⼀过程。
  • 缓存判断:浏览器会判断所请求的资源是否在缓存⾥,如果请求的资源在缓存⾥且没有失效,那 么就直接使⽤,否则向服务器发起新的请求。
  • DNS解析:如果资源不在本地缓存,⾸先需要进⾏DNS解析。浏览器会向本地DNS服务器发送域 名解析请求,本地DNS服务器会逐级查询,最终找到对应的IP地址。
  • 获取MAC地址:当浏览器得到 IP 地址后,数据传输还需要知道⽬的主机 MAC 地址,因为应⽤层 下发数据给传输层,TCP 协议会指定源端⼝号和⽬的端⼝号,然后下发给⽹络层。⽹络层会将本 机地址作为源地址,获取的 IP 地址作为⽬的地址。然后将下发给数据链路层,数据链路层的发送 需要加⼊通信双⽅的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,⽬的 MAC 地址需要分情 况处理。通过将 IP 地址与本机的⼦⽹掩码相结合,可以判断是否与请求主机在同⼀个⼦⽹⾥,如 果在同⼀个⼦⽹⾥,可以使⽤ APR 协议获取到⽬的主机的 MAC 地址,如果不在⼀个⼦⽹⾥,那 么请求应该转发给⽹关,由它代为转发,此时同样可以通过 ARP 协议来获取⽹关的 MAC 地址, 此时⽬的主机的 MAC 地址应该为⽹关的地址。
  • 建⽴TCP连接:主机将使⽤⽬标 IP地址和⽬标MAC地址发送⼀个TCP SYN包,请求建⽴⼀个TCP 连接,然后交给路由器转发,等路由器转到⽬标服务器后,服务器回复⼀个SYN-ACK包,确认连 接请求。然后,主机发送⼀个ACK包,确认已收到服务器的确认,然后 TCP 连接建⽴完成。
  • HTTPS 的 TLS 四次握⼿:如果使⽤的是 HTTPS 协议,在通信前还存在 TLS 的四次握⼿。
  • 发送HTTP请求:连接建⽴后,浏览器会向服务器发送HTTP请求。请求中包含了⽤户需要获取的 资源的信息,例如⽹⻚的URL、请求⽅法(GET、POST等)等。
  • 服务器处理请求并返回响应:服务器收到请求后,会根据请求的内容进⾏相应的处理。例如,如 果是请求⽹⻚,服务器会读取相应的⽹⻚⽂件,并⽣成HTTP响应。

tcp连接过程?/ 三次握手

常见面试题2

  • 客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状态
  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP ⾸部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端,表示向服务端发起 连接,该报⽂不包含应⽤层数据,之后客户端处于 SYN-SENT 状态。
  • 服务端收到客户端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号(server_isn),将此序 号填⼊ TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报⽂发给客户端,该报⽂也不包含应⽤层数据,之后 服务端处于 SYN-RCVD 状态。
  • 客户端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先该应答报⽂ TCP ⾸部 ACK 标志位置为 1 ,其次「确认应答号」字段填⼊ server_isn + 1 ,最后把报⽂发送给服务端,这次报 ⽂可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务端收到客户端的应答报⽂后,也进⼊ ESTABLISHED 状态。

三次握手要实现什么目的?

  • 三次握⼿才可以阻⽌重复历史连接的初始化(主要原因)。
  • 三次握⼿才可以同步双⽅的初始序列号。
  • 三次握⼿才可以避免资源浪费。

四次挥手

常见面试题2

具体过程:

  • 客户端主动调⽤关闭连接的函数,于是就会发送 FIN 报⽂,这个 FIN 报⽂代表客户端不会再发送 数据了,进⼊ FIN_WAIT_1 状态;
  • 服务端收到了 FIN 报⽂,然后⻢上回复⼀个 ACK 确认报⽂,此时服务端进⼊ CLOSE_WAIT 状 态。在收到 FIN 报⽂的时候,TCP 协议栈会为 FIN 包插⼊⼀个⽂件结束符 EOF 到接收缓冲区中, 服务端应⽤程序可以通过 read 调⽤来感知这个 FIN 包,这个 EOF 会被放在已排队等候的其他已 接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
  • 接着,当服务端在 read 数据的时候,最后⾃然就会读到 EOF,接着 read() 就会返回 0,这时服 务端应⽤程序如果有数据要发送的话,就发完数据后才调⽤关闭连接的函数,如果服务端应⽤程 序没有数据要发送的话,可以直接调⽤关闭连接的函数,这时服务端就会发⼀个 FIN 包,这个 FIN 报⽂代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进⼊TIME_WAIT状态;
  • 服务端收到 ACK 确认包后,就进⼊了最后的 CLOSE 状态;
  • 客户端经过 2MSL 时间之后,也进⼊ CLOSE 状态;

第 3 次挥手后,主动关闭的一方会有一个TIME-WAIT的状态对吧, 了解吗?

TIME_WAIT 状态的存在是为了确保网络连接的可靠关闭。只有主动发起关闭连接的一方(即主动关闭方)才会有 TIME_WAIT 状态。

TIME_WAIT 状态的需求主要有两个原因:

  • 防止具有相同「四元组」的「旧」数据包被收到:在网络通信中,每个 TCP 连接都由源 IP 地址、源端口号、目标 IP 地址和目标端口号这四个元素唯一标识,称为「四元组」。当一方主动关闭连接后,进入 TIME_WAIT 状态,它仍然可以接收到一段时间内来自对方的延迟数据包。这是因为网络中可能存在被延迟传输的数据包,如果没有 TIME_WAIT 状态的存在,这些延迟数据包可能会被错误地传递给新的连接,导致数据混乱。通过保持 TIME_WAIT 状态,可以防止旧的数据包干扰新的连接
  • 保证「被动关闭连接」的一方能被正确关闭:当连接的被动关闭方接收到主动关闭方的 FIN 报文(表示关闭连接),它需要发送一个确认 ACK 报文给主动关闭方,以完成连接的关闭。然而,网络是不可靠的,ACK 报文可能会在传输过程中丢失。如果主动关闭方在收到 ACK 报文之前就关闭连接,被动关闭方将无法正常完成连接的关闭。TIME_WAIT 状态的存在确保了被动关闭方能够接收到最后的 ACK 报文,从而帮助其正常关闭连接。

被动关闭连接的一方无法正常关闭会有什么问题吗?

会导致被动关闭连接一方堆积大量处于CLOSE_WAIT状态的TCP连接。

通常出现大量CLOSE_WAIT状态连接是因为代码的问题,比如没有调用 close 来关闭连接,或者是程序在调用 close 发生了 fullgc 、死锁、死循环的问题,这时候排查的方向是排查代码为什么没有调用 close。

TCP这块有拥塞控制和流量控制, 这一块你了解吗?

流量控制的主要目的是防止发送方发送数据过快,导致接收方来不及处理,从而造成数据丢失。

TCP通过控制窗口(TCP Window)来实现流量控制。窗口的大小是接收方根据自身的缓冲区大小动态调整的:

  • 接收窗口(Receive Window):接收方告诉发送方它可以接收多少数据。这个窗口的大小由接收方计算,并在TCP报文头中传递给发送方。
  • 滑动窗口(Sliding Window):TCP使用滑动窗口机制,使得在发送确认之前,可以连续发送多个数据包,从而提高效率。

拥塞控制则是为了避免过多的数据被发送到⽹络中,以⾄于⽹络拥堵,导致数据包丢失和延迟。

TCP 的拥塞控制机制主要包括以下⼏个算法:

常见面试题2

  • 慢启动(Slow Start):初始时,拥塞窗⼝设置为⼀个⼩值(通常是⼀个MSS),然后每收到⼀ 个确认(ACK),拥塞窗⼝就加倍,直到达到⼀个阈值(ssthresh)。
  • 拥塞避免(Congestion Avoidance):当拥塞窗⼝达到阈值后,进⼊拥塞避免阶段,窗⼝增⼤速 度减缓,通常是线性增⼤(每经过⼀个RTT(往返时间),增加1)。
  • 快重传(Fast Retransmit):当发送⽅连续接收到三个重复的ACK时,认为丢包发⽣,⽴即重发 丢失的数据包,⽽不是等待超时。
  • 快恢复(Fast Recovery):在快重传后,不会将拥塞窗⼝减少到初始值,⽽是设置为ssthresh, 并进⼊拥塞避免阶段。

如果接收⽅接收能⼒不够, 导致TCP⾸部⾥标识的滑动窗⼝⼤⼩不断减⼩, 如果窗⼝减⼩到了0, 那怎么重新开始呢?

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。

常见面试题2

TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

常见面试题2

讲中间件的作用?

  • 解耦:可以在多个系统之间进⾏解耦,将原本通过⽹络之间的调⽤的⽅式改为使⽤MQ进⾏消息的 异步通讯,只要该操作不是需要同步的,就可以改为使⽤MQ进⾏不同系统之间的联系,这样项⽬ 之间不会存在耦合,系统之间不会产⽣太⼤的影响,就算⼀个系统挂了,也只是消息挤压在MQ⾥ ⾯没⼈进⾏消费⽽已,不会对其他的系统产⽣影响。
  • 异步:加⼊⼀个操作设计到好⼏个步骤,这些步骤之间不需要同步完成,⽐如客户去创建了⼀个 订单,还要去客户轨迹系统添加⼀条轨迹、去库存系统更新库存、去客户系统修改客户的状态等 等。这样如果这个系统都直接进⾏调⽤,那么将会产⽣⼤量的时间,这样对于客户是⽆法接收 的;并且像添加客户轨迹这种操作是不需要去同步操作的,如果使⽤MQ将客户创建订单时,将后 ⾯的轨迹、库存、状态等信息的更新全都放到MQ⾥⾯然后去异步操作,这样就可加快系统的访问 速度,提供更好的客户体验。
  • 削峰:⼀个系统访问流量有⾼峰时期,也有低峰时期,⽐如说,中午整点有⼀个抢购活动等等。 ⽐如系统平时流量并不⾼,⼀秒钟只有100多个并发请求,系统处理没有任何压⼒,⼀切⻛平浪 静,到了某个抢购活动时间,系统并发访问了剧增,⽐如达到了每秒5000个并发请求,⽽我们的 系统每秒只能处理2000个请求,那么由于流量太⼤,我们的系统、数据库可能就会崩溃。这时如 果使⽤MQ进⾏流量削峰,将⽤户的⼤量消息直接放到MQ⾥⾯然后我们的系统去按⾃⼰的最⼤消费能⼒去消费这些消息,就可以保证系统的稳定,只是可能要跟进业务逻辑,给⽤户返回特定 ⻚⾯或者稍后通过其他⽅式通知其结果。

用的什么消息队列,消息队列怎么选型的?

项⽬⽤的是 RocketMQ 消息队列。选择RocketMQ的原因是:

  • 开发语⾔优势。RocketMQ 使⽤ Java 语⾔开发,⽐起使⽤ Erlang 开发的 RabbitMQ 来说,有着 更容易上⼿的阅读体验和受众。在遇到 RocketMQ 较为底层的问题时,⼤部分熟悉 Java 的同学 都可以深⼊阅读其源码,分析、排查问题。
  • 社区氛围活跃。RocketMQ 是阿⾥巴巴开源且内部在⼤量使⽤的消息队列,说明 RocketMQ 是的 确经得起残酷的⽣产环境考验的,并且能够针对线上环境复杂的需求场景提供相应的解决⽅案。
  • 特性丰富。根据 RocketMQ 官⽅⽂档的列举,其⾼级特性达到了 12 种 ,例如顺序消息、事务 消息、消息过滤、定时消息等。顺序消息、事务消息、消息过滤、定时消息。RocketMQ 丰富的 特性,能够为我们在复杂的业务场景下尽可能多地提供思路及解决⽅案。

介绍一下cap理论

CAP 原则又称 CAP 定理, 指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性), 三者不可得兼。

常见面试题2

一致性(C) : 在分布式系统中的所有数据备份, 在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)

可用性(A): 在集群中一部分节点故障后, 集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)

分区容忍性(P): 以实际效果而言, 分区相当于对通信的时限要求. 系统如果不能在时限内达成数据一致性, 就意味着发生了分区的情况, 必须就当前操作在 C 和 A 之间做出选择。

redis的hashset底层数据结构是什么?

Hash 类型的底层数据结构是由ziplist(压缩列表)hashtable(哈希表)实现的

  • 如果哈希类型元素个数小于 512 个,Redis 会使用压缩列表作为 Hash 类型的底层数据结构
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构

为什么选择两种结构?

  • 内存优化:小数据时用 ziplist 减少内存碎片。
  • 性能平衡:大数据时用 hashtable 保证操作效率。

redis分布式锁怎么实现?

SET lock_key unique_value NX PX 10000 +Lua

解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

欢迎大家来到IT世界,在知识的湖畔探索吧!// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end

给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url

⽅法:分治+hashmap

  • 遍历⽂件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个⼩⽂ 件(记为a0,a1,…,a999)中。这样每个⼩⽂件的⼤约为300M。
  • 遍历⽂件b,采取和a相同的⽅式将url分别存储到1000⼩⽂件(记为b0,b1,…,b999)。这样处理 后,所有可能相同的url都在对应的⼩⽂件(a0-b0, a1-b1,…,a999-b999)中,不对应的⼩⽂件 不可能有相同的url。然后我们只要求出1000对⼩⽂件中相同的url即可。
  • 求每对⼩⽂件中相同的url时,可以把其中⼀个⼩⽂件的url存储到hash_set中。然后遍历另⼀个⼩⽂件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到⽂件⾥⾯就可以了。

假如cpu跑到100%,你的解决思路是什么?

// 1. 找到高CPU进程 top -c // 2. 假设找到PID为1234的go进程占用高CPU top -H -p 1234 // 3. 发现线程4567占用高,转换为16进制(0x11d7) printf "%x\n" 4567 // 4. 使用pprof分析 go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile // 5. 在web界面查看火焰图,或终端中使用 pprof 或 goroutine 堆栈搜索 0x11d7 top10 list 函数名

Java方案

Golang对应方案

说明

top

top

相同

ps -T -p

top -H -p 或 ps -T -p

查看线程级CPU使用

jstack

pprof/gops/stack

Golang没有直接的jstack等价物,但pprof更强大

分析堆栈

pprof top/list

Golang提供更直观的代码级分析

堆和栈的区别?

  • 分配⽅式堆是动态分配内存,由程序员⼿动申请和释放内存,通常⽤于存储动态数据结构和对象。栈是静态分配内存,由编译器⾃动分配和释放内存,⽤于存储函数的局部变量和函数调⽤信息。
  • 内存管理:堆需要程序员⼿动管理内存的分配和释放,如果管理不当可能会导致内存泄漏或内存 溢出。栈由编译器⾃动管理内存,遵循后进先出的原则,变量的⽣命周期由其作⽤域决定,函数 调⽤时分配内存,函数返回时释放内存。
  • ⼤⼩和速度堆通常⽐栈⼤,内存空间较⼤,动态分配和释放内存需要时间开销。栈⼤⼩有限,通常⽐较⼩,内存分配和释放速度较快,因为是编译器⾃动管理。

线程、进程、协程区别是什么?

  • 进程是操作系统中进⾏资源分配和调度的基本单位,它拥有⾃⼰的独⽴ 内存空间和系统资源。每个进程都有独⽴的堆和栈,不与其他进程共享。进程间通信需要通过特 定的机制,如管道、消息队列、信号量等。由于进程拥有独⽴的内存空间,因此其稳定性和安全 性相对较⾼,但同时上下⽂切换的开销也较⼤,因为需要保存和恢复整个进程的状态。
  • 线程是进程内的⼀个执⾏单元,也是CPU调度和分派的基本单位。与进程不同, 线程共享进程的内存空间,包括堆和全局变量。线程之间通信更加⾼效,因为它们可以直接读写 共享内存。线程的上下⽂切换开销较⼩,因为只需要保存和恢复线程的上下⽂,⽽不是整个进程 的状态。然⽽,由于多个线程共享内存空间,因此存在数据竞争和线程安全的问题,需要通过同 步和互斥机制来解决。
  • 协程是⼀种⽤户态的轻量级线程,其调度完全由⽤户程序控制,⽽不需要内核的参 与。协程拥有⾃⼰的寄存器上下⽂和栈,但与其他协程共享堆内存。协程的切换开销⾮常⼩,因 为只需要保存和恢复协程的上下⽂,⽽⽆需进⾏内核级的上下⽂切换。这使得协程在处理⼤量并 发任务时具有⾮常⾼的效率。然⽽,协程需要程序员显式地进⾏调度和管理,相对于线程和进程 来说,其编程模型更为复杂。

有哪些进程调度算法 ?

  • 先来先服务调度算法:FCFS 对长作业有利,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。
  • 最短作业优先调度算法:SJF会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。
  • 高响应比优先调度算法权衡了短作业和⻓作业。 每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏, 「响应⽐优先级」的计算公式:
常见面试题2

  • 时间片轮转调度算法每个进程被分配一个时间段,称为时间片(*Quantum*),即允许该进程在该时间段中运行。
  • 最高优先级调度算法静态优先级:创建进程时候,就已经确定了优先级了,然后整个运⾏时间优先级都不会变化; 动态优先级:根据进程的动态变化调整优先级,⽐如如果进程运⾏时间增加,则降低其优先级, 如果进程等待时间(就绪队列的等待时间)增加,则升⾼其优先级,也就是随着时间的推移增加 等待进程的优先级
  • 多级反馈队列调度算法 多级反馈队列(*Multilevel Feedback Queue*)调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。多级」表示有多个队列,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越短。 「反馈」表示如果有新的进程加⼊优先级⾼的队列时,⽴刻停⽌当前正在运⾏的进程,转⽽去运⾏优先级⾼的队列;
常见面试题2

多级反馈队列是如何⼯作的:

  • 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越短;
  • 新的进程会被放⼊到第⼀级队列的末尾,按先来先服务的原则排队等待被调度,如果在第⼀级队 列规定的时间⽚没运⾏完成,则将其转⼊到第⼆级队列的末尾,以此类推,直⾄完成;
  • 当较⾼优先级的队列为空,才调度较低优先级的队列中的进程运⾏。如果进程运⾏时,有新进程 进⼊较⾼优先级的队列,则停⽌当前运⾏的进程并将其移⼊到原队列末尾,接着让较⾼优先级的 进程运⾏;

可以发现,对于短作业可能可以在第⼀级队列很快被处理完。对于⻓作业,如果在第⼀级队列处理 不完,可以移⼊下次队列等待被执⾏,虽然等待的时间变⻓了,但是运⾏时间也变更⻓了,所以该 算法很好的兼顾了⻓短作业,同时有较好的响应时间。

进程中通信的方式有哪些?

常见面试题2

Linux 内核提供了不少进程间通信的⽅式,其中最简单的⽅式就是管道,管道分为「匿名管道」和 「命名管道」。

匿名管道顾名思义,它没有名字标识,匿名管道是特殊⽂件只存在于内存,没有存在于⽂件系统 中,shell 命令中的「 | 」竖线就是匿名管道,通信的数据是⽆格式的流并且⼤⼩受限,通信的⽅式 是单向的,数据只能在⼀个⽅向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只 能⽤于存在⽗⼦关系的进程间通信,匿名管道的⽣命周期随着进程创建⽽建⽴,随着进程终⽌⽽消 失。

命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使⽤命名管道的前提,需要在⽂ 件系统创建⼀个类型为 p 的设备⽂件,那么毫⽆关系的进程就可以通过这个设备⽂件进⾏通信。另 外,不管是匿名管道还是命名管道,进程写⼊的数据都是缓存在内核中,另⼀个进程读取数据时候 ⾃然也是从内核中获取,同时通信数据都遵循先进先出原则,不⽀持 lseek 之类的⽂件定位操作。

消息队列克服了管道通信的数据是⽆格式的字节流的问题,消息队列实际上是保存在内核的「消息 链表」,消息队列的消息体是可以⽤户⾃定义的数据类型,发送数据时,会被分成⼀个⼀个独⽴的 消息体,当然接收数据时,也要与发送⽅发送的消息体的数据类型保持⼀致,这样才能保证读取的 数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写⼊和读取都需要经过⽤户态 与内核态之间的拷⻉过程。

共享内存可以解决消息队列通信中⽤户态与内核态之间数据拷⻉过程带来的开销,它直接分配⼀个 共享空间,每个进程都可以直接访问,就像访问进程⾃⼰的空间⼀样快捷⽅便,不需要陷⼊内核态 或者系统调⽤,⼤⼤提⾼了通信的速度,享有最快的进程间通信⽅式之名。但是便捷⾼效的共享内 存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。

那么,就需要信号量来保护共享资源,以确保任何时刻只能有⼀个进程访问共享资源,这种⽅式就 是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是⼀个计 数器,表示的是资源个数,其值可以通过两个原⼦操作来控制,分别是 P 操作和 V 操作

与信号量名字很相似的叫信号,它俩名字虽然相似,但功能⼀点⼉都不⼀样。信号是异步通信机 制,信号可以在应⽤进程和内核之间直接交互,内核也可以利⽤信号来通知⽤户空间的进程发⽣了 哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),⼀ 旦有信号发⽣,进程有三种⽅式响应信号 1. 执⾏默认操作、2. 捕捉信号、3. 忽略信号。有两个信号 是应⽤进程⽆法捕捉和忽略的,即 SIGKILL 和 SIGSTOP ,这是为了⽅便我们能在任何时候结束 或停⽌某个进程。

前⾯说到的通信机制,都是⼯作于同⼀台主机,如果要与不同主机的进程间通信, 那么就需要 Socket 通信了。Socket 实际上不仅⽤于不同的主机进程间通信,还可以⽤于本地主机进程间通信, 可根据创建 Socket 的类型不同,分为三种常⻅的通信⽅式,⼀个是基于 TCP 协议的通信⽅式,⼀ 个是基于 UDP 协议的通信⽅式,⼀个是本地进程间通信⽅式。

讲一下Nginx的负载均衡策略

  • 轮询按照顺序依次将请求分配给后端服务器。这种算法最简单,但是也无法处理某个节点变慢或者客户端操作有连续性的情况。
  • 加权轮询按照权重分配请求给后端服务器,权重越高的服务器获得更多的请求。适用于后端服务器性能不同的场景,可以根据服务器权重分配请求,提高高性能服务器的利用率。
  • URL哈希按访问的URL的哈希结果来分配请求,使每个URL定向到一台后端服务器,可以进一步提高后端缓存服务器的效率。
  • IP哈希根据客户端IP地址的哈希值来确定分配请求的后端服务器。适用于需要保持同一客户端的请求始终发送到同一台后端服务器的场景,如会话保持。
  • 最短响应时间按照后端服务器的响应时间来分配请求,响应时间短的优先分配。适用于后端服务器性能不均的场景,能够将请求发送到响应时间快的服务器,实现负载均衡。

如何实现一直均衡给一个用户?

可以通过「一致性哈希算法」来实现,根据请求的客户端 ip、或请求参数通过哈希算法得到一个数值,利用该数值取模映射出对应的后端服务器,这样能保证同一个客户端或相同参数的请求每次都使用同一台服务器。

容错模式

在高并发环境下,服务之间的依赖关系导致调用失败。

解决方案通常是:限流->熔断->隔离->降级,其目的是防止雪崩效应

常见面试题2

  • 主动超时:Http请求主动设置一个超时时间,超时就直接返回,不会造成服务堆积
  • 限流:限制最大并发数
  • 熔断:当错误数超过阈值时快速失败,不调用后端服务,同时隔一定时间放几个请求重试后端服务是否能正常调用,如果成功则关闭熔断状态,失败则继续快速失败,直接返回。(此处有个重试,重试就是弹性恢复的能力)
  • 隔离:把每个依赖或者调用的服务隔离开来,防止级联失败引起整体服务不可用
  • 降级:服务失败或者异常后,返回指定的默认信息

隔离

在微服务架构中,隔离是指将不同的服务或者请求之间相互隔离开来,以防止一个服务的异常或者高负载影响到其他服务的正常运行。隔离可以在多个层面进行,包括网络隔离、线程隔离、进程隔离和数据隔离等。

网络隔离:通过网络隔离,将不同的服务部署在不同的网络节点上,防止由于某个服务的异常或者负载过高导致网络拥堵,影响到其他服务的正常通信。

线程隔离:在单个服务内部,通过线程隔离将不同的任务或者请求分配到不同的线程中执行,避免由于某个任务的异常或者长时间阻塞导致整个服务的性能下降或者崩溃。

进程隔离:通过进程隔离,将不同的服务部署在不同的进程中运行,每个进程之间相互独立,互不干扰,从而提高系统的稳定性和可靠性。

数据隔离:在共享资源的情况下,通过数据隔离来保护数据的完整性和安全性,防止由于不同服务之间的竞争条件或者并发访问导致数据异常或者损坏

限流算法有哪些?

限流是当⾼并发或者瞬时⾼并发时,为了保证系统的稳定性、可⽤性,对超出服务处理能⼒之外的 请求进⾏拦截,对访问服务的流量进⾏限制。

常⻅的限流算法有四种:固定窗⼝限流算法、滑动窗⼝限流算法、漏桶限流算法和令牌桶限流算 法。

  • 固定窗⼝限流算法实现简单,容易理解,但是流量曲线可能不够平滑,有“突刺现象”,在窗⼝切 换时可能会产⽣两倍于阈值流量的请求。
  • 滑动窗⼝限流算法是对固定窗⼝限流算法的改进,有效解决了窗⼝切换时可能会产⽣两倍于阈值 流量请求的问题。
  • 漏桶限流算法能够对流量起到整流的作⽤,让随机不稳定的流量以固定的速率流出,但是不能解 决流量突发的问题。
  • 令牌桶算法作为漏⽃算法的⼀种改进,除了能够起到平滑流量的作⽤,还允许⼀定程度的流量突发。

介绍一下服务熔断

服务熔断是应对微服务雪崩效应的⼀种链路保护机制

⽐如说,微服务之间的数据交互是通过远程调⽤来完成的。服务A调⽤服务,服务B调⽤服务c,某⼀ 时间链路上对服务C的调⽤响应时间过⻓或者服务C不可⽤,随着时间的增⻓,对服务C的调⽤也越 来越多,然后服务C崩溃了,但是链路调⽤还在,对服务B的调⽤也在持续增多,然后服务B崩溃, 随之A也崩溃,导致雪崩效应。

常见面试题2

熔断器原理

服务治理中的熔断机制,指的是在发起服务调用的时候,如果被调用方返回的错误率超过一定的阈值,那么后续将不会真正地发起请求,而是在调用方直接返回错误

常见面试题2

  • 关闭(Closed):在这种状态下,我们需要一个计数器来记录调用失败的次数和总的请求次数,如果在某个时间窗口内,失败的失败率达到预设的阈值,则切换到断开状态,此时开启一个超时时间,当到达该时间则切换到半关闭状态,该超时时间是给了系统一次机会来修正导致调用失败的错误,以回到正常的工作状态。在关闭状态下,调用错误是基于时间的,在特定的时间间隔内会重置,这能够防止偶然错误导致熔断器进入断开状态
  • 打开(Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复
  • 半打开(Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到打开状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮

熔断框架(hystrix-go)

常见面试题2

网关服务熔断设计

  1. 定义熔断策略:触发条件为当服务的失败率超过 50% 或响应时间超过 500ms 时触发熔断。
  2. 监控服务调用:网关服务使用 Prometheus 进行监控,定期收集并记录微服务的成功率和响应时间。
  3. 实现熔断逻辑:当监控数据达到触发条件时,网关服务暂停对故障服务的转发请求,并记录日志。
  4. 熔断状态管理:网关服务维护一个熔断状态表,记录每个服务的熔断状态、触发时间和持续时间。
  5. 自动恢复机制:在熔断状态持续 1 分钟后,网关服务尝试自动恢复对故障服务的调用。每隔一段时间检查服务是否恢复正常,若恢复则退出熔断状态。
  6. 告警和通知:设计告警机制,当发生熔断事件时发送邮件或短信通知运维团队。同时记录熔断事件和恢复事件,以便后续分析和优化。
  7. 监控和优化:定期分析监控数据,优化熔断策略和参数设置。根据实际情况调整失败率和响应时间的阈值,提高系统的稳定性和可用性

介绍一下服务降级

服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行

降级方式

  • 延迟服务:比如发表了评论,重要服务,比如在文章中显示正常,但是延迟给用户增加积分,只是放到一个缓存中,等服务平稳之后再执行。
  • 在粒度范围内关闭服务(片段降级或服务功能降级):比如关闭相关文章的推荐,直接关闭推荐区
  • 页面异步请求降级:比如商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,可以进行降级;
  • 页面跳转(页面降级):比如可以有相关文章推荐,但是更多的页面则直接跳转到某一个地址
  • 写降级:比如秒杀抢购,我们可以只进行Cache的更新,然后异步同步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
  • 读降级:比如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。

链表和数组有什么区别?

访问效率:数组可以通过索引直接访问任何位置的元素,访问效率⾼,时间复杂度为O(1),⽽链 表需要从头节点开始遍历到⽬标位置,访问效率较低,时间复杂度为O(n)。

插⼊和删除操作效率:数组插⼊和删除操作可能需要移动其他元素,时间复杂度为O(n),⽽链表 只需要修改指针指向,时间复杂度为O(1)。

缓存命中率:由于数组元素在内存中连续存储,可以提⾼CPU缓存的命中率,⽽链表节点不 连续存储,可能导致CPU缓存的命中率较低,频繁的缓存失效会影响性能。

应⽤场景:数组适合静态⼤⼩、频繁访问元素的场景,⽽链表适合动态⼤⼩、频繁插⼊、删除操 作的场景。

如何使用两个栈实现队列?

使⽤两个栈实现队列的⽅法如下:

  1. 准备两个栈,分别称为 stackPush 和 stackPop 。
  2. 当需要⼊队时,将元素压⼊ stackPush 栈。
  3. 当需要出队时,先判断 stackPop 是否为空,如果不为空,则直接弹出栈顶元素;如果为空,则 将 stackPush 中的所有元素依次弹出并压⼊ stackPop 中,然后再从 stackPop 中弹出栈顶元 素作为出队元素。
  4. 当需要查询队⾸元素时,同样需要先将 stackPush 中的元素转移到 stackPop 中,然后取出 stackPop 的栈顶元素但不弹出。
  5. 通过上述⽅法,可以实现⽤两个栈来模拟队列的先进先出(FIFO)特性。

ElasticSearch如何进行全文检索的?

主要是利用了倒排索引的查询结构,倒排索引是一种用于快速搜索的数据结构,它将文档中的每个单词与包含该单词的文档进行关联。通常,倒排索引由单词(terms)和包含这些单词的文档(document)列表组成。

如何理解倒排索引呢?假如现有三份数据文档,文档的内容如下分别是

常见面试题2

通过分词器将每个文档的内容域拆分成单独的「词汇」

常见面试题2

然后再构建从词汇到文档ID的映射,就形成了倒排索引。

常见面试题2

当进行搜索时,系统只需查找倒排索引中包含搜索关键词的文档列表,比如用户输入”秋水”,通过倒排索引,可以快速的找到含有”秋水”的文档是id为 1,2 的文档,从而达到快速的全文检索的目的。

了解过 es 分词器有哪些?

常⻅的分词器如下:

standard 默认分词器,对单个字符进⾏切分,查全率⾼,准确度较低

IK 分词器 ik_max_word:查全率与准确度较⾼,性能也⾼,是业务中普遍采⽤的中⽂分词器 IK 分词器

ik_smart:切分⼒度较⼤,准确度与查全率不⾼,但是查询性能较⾼

Smart Chinese 分词器:查全率与准确率性能较⾼

hanlp 中⽂分词器:切分⼒度较⼤,准确度与查全率不⾼,但是查询性能较⾼

Pinyin 分词器:针对汉字拼⾳进⾏的分词器,与上⾯介绍的分词器稍有不同,在⽤拼⾳进⾏查询 时查全率准确度较⾼

常见面试题2

Linux操作系统中哪个命令可以查看端口被哪个应用占用?

欢迎大家来到IT世界,在知识的湖畔探索吧!lsof -i :端口号 或者 netstat -tulnp | grep 端口号

如果服务应用部署在 Linux 上,CPU 打满后,想查看哪个进程导致的,用什么命令?

top,然后可以按 P 键来按 CPU 使用率排序,查看哪些进程占用了最多的 CPU 资源。

常见面试题2

用shell命令替换一个文件中的字符串

sed - i 's / 旧字符串/新字符串/g' ⽂件名 // 例如,假设要将文件example.txt中的字符串Hello替换为Hi,可以运行以下命令 sed -i 's/Hello/Hi/g' example.txt

linux中有一个日志文件,日志文件中记录了访问请求的信息,第一列是访问的日期,第二列是请求的ip,第三列是请求的耗时,写一条shell命令来查到请求耗时最高的10条记录

欢迎大家来到IT世界,在知识的湖畔探索吧!sort -k3 -nr 日志文件 | head -n 10

sort -k3 -nr用于按第三列(请求耗时)进行倒序排序。-k3表示按第三列排序,-n表示按数字排序,-r表示倒序排序。然后,使用head -n 10来获取排序后的前10行,即耗时最高的10条记录。

慢查询是如何调试解决的?

  • 确认慢查询:⾸先,通过MySQL的慢查询⽇志或性能监控⼯具,确认哪些SQL查询较慢,需要进 ⾏调优。
  • 分析执⾏计划:通过使⽤ EXPLAIN 关键字,可以获取SQL查询的执⾏计划。执⾏计划可以告诉您 MySQL是如何执⾏查询的,包括使⽤的索引、连接⽅式等。分析执⾏计划可帮助您理解查询的性 能瓶颈所在。
  • 优化查询语句:根据执⾏计划的分析结果,考虑对查询语句进⾏优化。例如,添加合适的索引、 调整查询条件、避免全表扫描等。优化查询语句可以提⾼查询性能。
  • 优化数据库结构:有时,慢查询的性能问题可能与数据库结构有关。考虑对表结构进⾏优化,例 如拆分⼤表、规范化设计等。合理的数据库结构可以提⾼查询性能。
  • 缓存和查询缓存:对于⼀些频繁查询的结果,可以考虑使⽤缓存机制,避免重复的查询操作。

MySQL是如何保障数据不丢失的?

主要是通过 redolog 来实现事务持久性的,事务执行过程,会把对 innodb 存储引擎中数据页修改操作记录到 redolog 里,事务提交的时候,就直接把 redolog 刷入磁盘,即使脏页中途没有刷盘成功, mysql 宕机了,也能通过 redolog 重放,恢复到之前事务修改数据页后的状态,从而保障了数据不丢失。

RedoLog是在内存里吗?

事务执行过程中,生成的 redolog 会在 redolog buffer 中,也就是在内存中,等事务提交的时候,会把 redolog 写入磁盘。

为什么要写RedoLog,而不是直接写到B+树里面?

  • 因为 redolog 写⼊磁盘是顺序写,⽽ b+树⾥数据⻚写⼊磁盘是随机写,顺序写的性能会⽐随机写 好,这样可以提升事务提交的效率。
  • 最重要的是redolog具备故障恢复的能⼒,Redo Log 记录的是物理级别的修改,包括⻚的修改,如 插⼊、更新、删除操作在磁盘上的物理位置和修改内容。例如,当执⾏⼀个更新操作时,Redo Log 会记录修改的数据⻚的地址和更新后的数据,⽽不是 SQL 语句本身。
  • 在数据⻚实际更新之前,先将修改操作写⼊ Redo Log。当数据库重启时,会进⾏恢复操作。⾸先, 根据 Redo Log 检查哪些事务已经提交但数据⻚尚未完全写⼊磁盘。然后,使⽤ Redo Log 中的记录 对这些事务进⾏重做(Redo)操作,将未完成的数据⻚修改完成,确保事务的修改⽣效。

mysql 两次写(double write buffer)了解吗?

我们常见的服务器一般都是Linux操作系统,Linux文件系统页(OS Page)的大小默认是4KB。而MySQL的页(Page)大小默认是16KB。

MySQL程序是跑在Linux操作系统上的,需要跟操作系统交互,所以MySQL中一页数据刷到磁盘,要写4个文件系统里的页。

常见面试题2

需要注意的是,这个操作并非原子操作,比如操作系统写到第二个页的时候,Linux机器断电了,这时候就会出现问题了。造成”页数据损坏“。并且这种”页数据损坏“靠 redo日志是无法修复的。

常见面试题2

Doublewrite Buffer 作用是,在把页写到数据文件之前,InnoDB先把它们写到一个叫doublewrite buffer(双写缓冲区)的共享表空间内,在写doublewrite buffer完成后,InnoDB才会把页写到数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer中找到完好的page副本用于恢复,所以本质上是一个最近写回的页面的备份拷贝。

如上图所示,当有页数据要刷盘时:

  • 页数据先通过memcpy函数拷贝至内存中的Doublewrite Buffer(大小为约 2MB)中,Doublewrite Buffer 分为两个区域,每次写入一个区域(最多 1MB 的数据)。
  • Doublewrite Buffer的内存里的数据页,会fsync刷到Doublewrite Buffer的磁盘上,写两次到到共享表空间中(连续存储,顺序写,性能很高),每次写1MB;
  • 写入完成后,再将脏页刷到数据磁盘存储.ibd文件上(随机写);

当MySQL出现异常崩溃时,有如下几种情况发生:

  • 情况一:步骤1前宕机,刷盘未开始,数据在redo log,后期可以恢复
  • 情况二:步骤1后,步骤2前宕机,因为是在内存中,宕机清空内存,和情况1一样
  • 情况三:步骤2后,步骤3前宕机,因为DWB的磁盘有完整的数据,可以修复损坏的页数据

mysql的explain有什么作用

explain 是查看 sql 的执行计划,主要用来分析 sql 语句的执行过程,比如有没有走索引,有没有外部排序,有没有索引覆盖等等。

如下图,就是一个没有使用索引,并且是一个全表扫描的查询语句。

常见面试题2

对于执⾏计划,参数有:

  • possible_keys 字段表示可能⽤到的索引;
  • key 字段表示实际⽤的索引,如果这⼀项为 NULL,说明没有使⽤索引;
  • key_len 表示索引的⻓度;
  • rows 表示扫描的数据⾏数。
  • type 表示数据扫描类型,我们需要重点看这个。

type 字段就是描述了找到所需数据时使⽤的扫描⽅式是什么,常⻅扫描类型的执⾏效率从低到⾼的 顺序为:

  • All(全表扫描);
  • index(全索引扫描);
  • range(索引范围扫描);
  • ref(⾮唯⼀索引扫描);
  • eq_ref(唯⼀索引扫描);
  • const(结果只有⼀条的主键或唯⼀索引扫描)

all 是最坏的情况,因为采⽤了全表扫描的⽅式。index 和 all 差不多,只不过 index 对索引表进⾏全扫描,这样做的好处是不再需要对数据进⾏排序,但是开销依然很⼤。所以,要尽量避免全表扫描和全索引扫描。

range 表示采⽤了索引范围扫描,⼀般在 where ⼦句中使⽤ < 、>、in、between 等关键词,只检索 给定范围的⾏,属于范围查找。从这⼀级别开始,索引的作⽤会越来越明显,因此我们需要尽量让 SQL 查询可以使⽤到 range 这⼀级别及以上的 type 访问⽅式。

ref 类型表示采⽤了⾮唯⼀索引,或者是唯⼀索引的⾮唯⼀性前缀,返回数据返回可能是多条。因为 虽然使⽤了索引,但该索引列的值并不唯⼀,有重复。这样即使使⽤索引快速查找到了第⼀条数 据,仍然不能停⽌,要进⾏⽬标值附近的⼩范围扫描。但它的好处是它并不需要扫全表,因为索引 是有序的,即便有重复值,也是在⼀个⾮常⼩的范围内扫描。

eq_ref 类型是使⽤主键或唯⼀索引时产⽣的访问⽅式,通常使⽤在多表联查中。⽐如,对两张表进 ⾏联查,关联条件是两张表的 user_id 相等,且 user_id 是唯⼀索引,那么使⽤ EXPLAIN 进⾏执⾏ 计划查看的时候,type 就会显示 eq_ref。

const 类型表示使⽤了主键或者唯⼀索引与常量值进⾏⽐较,⽐如 select name from product where id=1。 需要说明的是 const 类型和 eq_ref 都使⽤了主键或唯⼀索引,不过这两个类型有所区别,const 是 与常量进⾏⽐较,查询效率会更快,⽽ eq_ref 通常⽤于多表联查中。

extra 显示的结果,这⾥说⼏个重要的参考指标:

Using filesort :当查询语句中包含 group by 操作,⽽且⽆法利⽤索引完成排序操作的时候, 这 时不得不选择相应的排序算法进⾏,甚⾄可能会通过⽂件排序,效率是很低的,所以要避免这种 问题的出现。

Using temporary:使了⽤临时表保存中间结果,MySQL 在对查询结果排序时使⽤临时表,常⻅ 于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。

Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使⽤了覆盖索 引,避免了回表操作,效率不错。

如何用 MySQL 实现一个可重入的锁?

CREATE TABLE `lock_table` ( `id` INT AUTO_INCREMENT PRIMARY KEY, //该字段⽤于存储锁的名称,作为锁的唯⼀标识符。 `lock_name` VARCHAR(255) NOT NULL, // holder_thread该字段存储当前持有锁的线程的名称,⽤于标识哪个线程持有该锁。 `holder_thread` VARCHAR(255), // reentry_count 该字段存储锁的重⼊次数,⽤于实现锁的可重⼊性 `reentry_count` INT DEFAULT 0 )

加锁的实现逻辑

开启事务

  • 执⾏ SQL SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name =? FOR UPDATE ,查询是否存在该记录:
  • 如果记录不存在,则直接加锁,执⾏ INSERT INTO lock_table (lock_name, holder_thread, reentry_count) VALUES (?,?, 1)
  • 如果记录存在,且持有者是同⼀个线程,则可冲⼊,增加重⼊次数,执⾏ UPDATE lock_table SET reentry_count = reentry_count + 1 WHERE lock_name =?

提交事务

解锁的逻辑:

开启事务

  • 执⾏ SQL SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name =? FOR UPDATE ,查询是否存在该记录:
  • 如果记录存在,且持有者是同⼀个线程,且可重⼊数⼤于 1 ,则减少重⼊次数 UPDATE lock_table SET reentry_count = reentry_count – 1 WHERE lock_name =?
  • 如果记录存在,且持有者是同⼀个线程,且可重⼊数⼩于等于 0 ,则完全释放锁, DELETE FROM lock_table WHERE lock_name =?

提交事务

设计一个行级锁的死锁,举一个实际的例子

假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下

常见面试题2

可以看到,两个事务都陷入了等待状态,也就是发生了死锁,因为都在相互等待对方释放锁。

在执⾏下⾯这条语句的时候:

欢迎大家来到IT世界,在知识的湖畔探索吧!select id from t_order where order_no = 1008 for update;

因为 order_no 不是唯⼀索引,所以⾏锁的类型是间隙锁,于是间隙锁的范围是 (1006, +∞) 。那么,当事务 B 往间隙锁⾥插⼊ id = 1008 的记录就会被锁住。因为当我们执⾏以下插⼊语句时,会在插⼊间隙上再次获取插⼊意向锁。

insert into t_order (order_no, create_date) values (1008, now());

插⼊意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间 隙锁之后,才能获取到插⼊意向锁。⽽间隙锁与间隙锁之间是兼容的,所以所以两个事务中 select … for update 语句并不会相互影响。

案例中的事务 A 和事务 B 在执⾏完后 select … for update 语句后都持有范围为 (1006,+∞) 的间隙锁,⽽接下来的插⼊操作为了获取到插⼊意向锁,都在等待对⽅事务的间隙锁释放,于是就 造成了循环等待,导致死锁。

网络有什么常用的通信协议?

HTTP:用于在Web浏览器和Web服务器之间传输超文本的协议,是目前最常见的应用层协议。 HTTPS:在HTTP的基础上添加了SSL/TLS加密层,用于在不安全的网络上安全地传输数据。

TCP:面向连接的传输层协议,提供可靠的数据传输服务,保证数据的顺序和完整性。

UDP:无连接的传输层协议,提供了数据包传输的简单服务,适用于实时性要求高的应用。

IP:网络层协议,用于在网络中传输数据包,定义了数据包的格式和传输规则。

http无状态体现在哪?

HTTP的⽆状态体现在每个请求之间相互独⽴,服务器不会保留之前请求的状态信息。每次客户端向 服务器发送请求时,服务器都会独⽴处理该请求,不会记住之前的请求信息或状态

这意味着服务器⽆法知道两次请求是否来⾃同⼀个客户端,也⽆法知道客户端的历史状态,需要通 过其他机制(如Cookies、Session)来维护和管理状态信息

Cookie 通过在请求和响应报⽂中写⼊ Cookie 信息来控制客户端的状态。 相当于,在客户端第⼀次请求后,服务器会下发⼀个装有客户信息的「⼩贴纸」,后续客户端请求 服务器的时候,带上「⼩贴纸」,服务器就能认得了了。

常见面试题2

HTTP1.1怎么对请求做拆包,具体来说怎么拆的?

在HTTP/1.1中,请求的拆包是通过”Content-Length“头字段来进行的。该字段指示了请求正文的长度,服务器可以根据该长度来正确接收和解析请求。

单核CPU如何执行多个程序?

操作系统会为每个程序分配一个时间片,单核 CPU 会轮流执行每个程序,在一个时间片内执行一个程序,当这个时间片用完后,就切换到下一个程序。

CPU的流水线设计有了解吗?

一条指令的执行需要经过取指令,翻译指令,执行指令三个基本流程。CPU内部的电路也分为不同单元:取指单元、译码单元、执行单元等,指令的执行也是按照流水线的工序一步一步执行的。

如果采用流水线技术,则每个时钟周期内只有一个单元在工作,其余两个单元在“观望”。

流水线设计将这些操作分成多个独立的阶段,每个阶段由专门的硬件单元负责,使不同指令的不同阶段可以并行执行,流水线的本质就是拿空间换时间。将每条指令的步骤分解到不同的电路单元,从而使得多个指令并行执行。

常见面试题2

线程间的通信方式

Linux系统提供了五种⽤于线程通信的⽅式:互斥锁、读写锁、条件变量、⾃旋锁和信号量。

信号量(Semaphores):信号量可以是命名的(有名信号量)或⽆名的(仅限于当前进程内的 线程),⽤于控制对资源的访问次数。通常信号量表示资源的数量,对应的变量是⼀个整型 (sem)变量。另外,还有两个原⼦操作的系统调⽤函数来控制信号量的,分别是:P 操作:将 sem 减 1,相减后,如果 sem < 0,则进程/线程进⼊阻塞等待,否则继续,表明 P 操作可能会阻 塞;V 操作:将 sem 加 1,相加后,如果 sem <= 0,唤醒⼀个等待中的进程/线程,表明 V 操作 不会阻塞;

docker底层是怎么实现的

基于 Namespace 的视图隔离:Docker利⽤Linux命名空间(Namespace)来实现不同容器之间 的隔离。每个容器都运⾏在⾃⼰的⼀组命名空间中,包括PID(进程)、⽹络、挂载点、IPC(进 程间通信)等。这样,容器中的进程只能看到⾃⼰所在命名空间内的进程,⽽不会影响其他容器 中的进程。

基于 cgroups 的资源隔离:cgroups 是Linux内核的⼀个功能,允许在进程组之间分配、限制 和优先处理系统资源,如CPU、内存和磁盘I/O。它们提供了⼀种机制,⽤于管理和隔离进程集合 的资源使⽤,有助于资源限制、⼯作负载隔离以及在不同进程组之间进⾏资源优先处理。

秒杀系统设计如何做?

系统架构分层设计如下。

前端层

  • ⻚⾯静态化:将商品展示⻚⾯等静态内容进⾏缓存,⽤户请求时可以直接从缓存中获取,减少服 务器的渲染压⼒。例如,使⽤内容分发⽹络(CDN)缓存商品图⽚、详情介绍等静态资源。
  • 防刷机制:通过验证码、限制⽤户请求频率等⽅式防⽌恶意刷请求。例如,在秒杀开始前要求⽤ 户输⼊验证码,并且在⼀定时间内限制单个⽤户的请求次数,如每秒最多允许 3 次请求。

应⽤层

  • 负载均衡:采⽤负载均衡器将⽤户请求均匀地分配到多个后端服务器,避免单点服务器过载。如 使⽤ Nginx 作为负载均衡器,根据服务器的负载情况和性能动态分配请求。
  • 服务拆分与微服务化:将秒杀系统的不同功能模块拆分成独⽴的微服务,如⽤户服务、商品服 务、订单服务等。这样可以独⽴部署和扩展各个模块,提⾼系统的灵活性和可维护性。
  • 缓存策略:在应⽤层使⽤缓存来提⾼系统性能。例如,使⽤ Redis 缓存商品库存信息,⽤户下单 前先从 Redis 中查询库存,减少对数据库的直接访问。

数据层

  • 数据库优化:对数据库进⾏性能优化,如数据库索引优化、SQL 语句优化等。对于库存表,可以 为库存字段添加索引,加快库存查询和更新的速度。
  • 数据库集群与读写分离:采⽤数据库集群来提⾼数据库的处理能⼒,同时进⾏读写分离。将读操 作(如查询商品信息)和写操作(如库存扣减、订单⽣成)分布到不同的数据库节点上,提⾼系 统的并发处理能⼒。

⾼并发场景下扣减库存的⽅式

  • 预扣库存:在⽤户下单时,先预扣库存,将库存数量在缓存(如 Redis)中进⾏减 1 操作。同时设 置⼀个较短的过期时间,如 1 – 2 分钟。如果⽤户在过期时间内完成⽀付,正式扣减库存;如果未 完成⽀付,库存⾃动回补。
  • 异步更新数据库:通过 Redis 判断之后,去更新数据库的请求都是必要的请求,这些请求数据库 必须要处理,但是如果数据库还是处理不过来这些请求怎么办呢?这个时候就可以考虑削峰填⾕ 操作了,削峰填⾕最好的实践就是 MQ 了。经过 Redis 库存扣减判断之后,我们已经确保这次请 求需要⽣成订单,我们就可以通过异步的形式通知订单服务⽣成订单并扣减库存。
  • 数据库乐观锁防⽌超卖:更新数据库减库存的时候,采⽤乐观锁⽅式,进⾏库存限制条件, update goods set stock = stock – 1 where goods_id = ? and stock >0。

Redis 分布式锁怎么解决超卖问题的?

同⼀个锁key,同⼀时间只能有⼀个客户端拿到锁,其他客户端会陷⼊⽆限的等待来尝试获取那个 锁,只有获取到锁的客户端才能执⾏下⾯的业务逻辑。

⽐如说,⽤户要⼀次性买 10 台⼿机,那么避免超卖的流程如下:

  • 只有⼀个订单系统实例可以成功加分布式锁,然后只有他⼀个实例可以查库存、判断库存是否充⾜、下单扣减库存,接着释放锁。
  • 释放锁之后,另外⼀个订单系统实例才能加锁,接着查库存,⼀下发现库存只有 2 个了,库存不⾜,⽆法购买,下单失败,不会将库存扣减为-8的,就避免超卖的问题。

这种⽅案的缺点是同⼀个商品在多⽤户同时下单的情况下,会基于分布式锁串⾏化处理,导致没法 同时处理同⼀个商品的⼤量下单的请求。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/127829.html

(0)
上一篇 23分钟前
下一篇 8分钟前

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们YX

mu99908888

在线咨询: 微信交谈

邮件:itzsgw@126.com

工作时间:时刻准备着!

关注微信