腾讯史上最强!我们未来三年要招 28000 人!

腾讯史上最强!我们未来三年要招 28000 人!

腾讯(一面)1. TCP 和 UDP 协议的区别什么?

连接:TCP 是面向连接的传输层协议,传输数据前先要建立连接;UDP 是不需要连接,即刻传输数据。

服务对象:TCP 是一对一的两点服务,即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信

可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议

拥塞控制、流量控制:TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

首部开销:TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

传输方式:TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

TCP 在正式传输用户数据之前,需要先进行三次握手的连接建立,数据传输完成后,TCP 通过 四次挥手释放连接。

TCP 的 “面向连接” 本质是通过 预建立逻辑链路 + 全程状态管理 + 可靠机制,在不可靠的网络层(IP 协议)之上构建了一条 “虚拟的可靠通道”。

TCP 的 “面向连接” 特性支撑了其 可靠传输的核心能力,主要体现在以下方面:

数据有序性:连接建立后,双方通过序列号为每个字节数据编号,接收方按顺序缓存数据,对乱序数据要求重传。

确认与重传机制:接收方收到数据后,通过 ACK 包告知发送方 “已收到”,如果发送方未收到 ACK,超时后自动重传数据

流量控制与拥塞控制:流量控制方面是通过连接协商双方的 “接收窗口大小”,避免发送方发送过快导致接收方缓冲区溢出;拥塞控制方面是根据网络状况动态调整发送速率,防止连接占用过多网络资源。

阻塞I/O模型:应用程序发起I/O操作后会被阻塞,直到操作完成才返回结果。适用于对实时性要求不高的场景。

非阻塞I/O模型:应用程序发起I/O操作后立即返回,不会被阻塞,但需要不断轮询或者使用select/poll/epoll等系统调用来检查I/O操作是否完成。适合于需要进行多路复用的场景,例如需要同时处理多个socket连接的服务器程序。

I/O复用模型:通过select、poll、epoll等系统调用,应用程序可以同时等待多个I/O操作,当其中任何一个I/O操作准备就绪时,应用程序会被通知。适合于需要同时处理多个I/O操作的场景,比如高并发的服务端程序。

信号驱动I/O模型:应用程序发起I/O操作后,可以继续做其他事情,当I/O操作完成时,操作系统会向应用程序发送信号来通知其完成。适合于需要异步I/O通知的场景,可以提高系统的并发能力。

异步I/O模型:应用程序发起I/O操作后可以立即做其他事情,当I/O操作完成时,应用程序会得到通知。异步I/O模型由操作系统内核完成I/O操作,应用程序只需等待通知即可。适合于需要大量并发连接和高性能的场景,能够减少系统调用次数,提高系统效率。

简单了解过,针对 TCP 应该 Socket 编程过程如下:

服务端和客户端初始化 socket,得到文件描述符;

服务端调用 bind,将绑定在 IP 地址和端口;

服务端调用 listen,进行监听;

服务端调用 accept,等待客户端连接;

客户端调用 connect,向服务器端的地址和端口发起连接请求;

服务端 accept返回用于传输的 socket的文件描述符;

客户端调用 write写入数据;服务端调用 read读取数据;

客户端断开连接时,会调用 close,那么服务端 read读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

5. MySQL 的事务隔离级别有哪些?

读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;

读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;

可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;

串行化(serializable);会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

按隔离水平高低排序如下:

针对不同的隔离级别,并发事务时可能发生的现象也会不同。

也就是说:

在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象;

在「读提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;

在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象;

在「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生。

接下来,举个具体的例子来说明这四种隔离级别,有一张账户余额表,里面有一条账户余额为 100 万的记录。然后有两个并发的事务,事务 A 只负责查询余额,事务 B 则会将我的余额改成 200 万,下面是按照时间顺序执行两个事务的行为:

在不同隔离级别下,事务 A 执行过程中查询到的余额可能会不同:

在「读未提交」隔离级别下,事务 B 修改余额后,虽然没有提交事务,但是此时的余额已经可以被事务 A 看见了,于是事务 A 中余额 V1 查询的值是 200 万,余额 V2、V3 自然也是 200 万了;

在「读提交」隔离级别下,事务 B 修改余额后,因为没有提交事务,所以事务 A 中余额 V1 的值还是 100 万,等事务 B 提交完后,最新的余额数据才能被事务 A 看见,因此额 V2、V3 都是 200 万;

在「可重复读」隔离级别下,事务 A 只能看见启动事务时的数据,所以余额 V1、余额 V2 的值都是 100 万,当事务 A 提交事务后,就能看见最新的余额数据了,所以余额 V3 的值是 200 万;

在「串行化」隔离级别下,事务 B 在执行将余额 100 万修改为 200 万时,由于此前事务 A 执行了读操作,这样就发生了读写冲突,于是就会被锁住,直到事务 A 提交后,事务 B 才可以继续执行,所以从 A 的角度看,余额 V1、V2 的值是 100 万,余额 V3 的值是 200万。

这四种隔离级别具体是如何实现的呢?

对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;

对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;

对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View来实现的,它们的区别在于创建 Read View 的时机不同,「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。

对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。

「读提交」隔离级别是在「每个select语句执行前」都会重新生成一个 Read View;

「可重复读」隔离级别是执行第一条select时,生成一个 Read View,然后整个事务期间都在用这个 Read View。

Read View 有四个重要的字段:

m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。

min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。

max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;

creator_trx_id :指的是创建该 Read View 的事务的事务 id。

对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:

trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;

roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:

如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。

如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。

如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:

如果记录的 trx_id 在m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。

如果记录的 trx_id 不在m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

7. 什么叫服务的并行?

服务的并行指的是多个服务或任务能够同时运行、互不阻塞地处理请求,服务并行的优势主要有:

应对高并发:例如电商秒杀、社交平台热点事件时,用户请求激增,单一服务实例无法处理。

提升吞吐量:通过并行处理,单位时间完成更多任务(如批量数据处理、视频转码)。

增强容错性:避免单点故障,部分实例宕机不影响整体服务。

资源利用最大化:利用多核CPU、多节点集群的计算能力。

实现服务并行的常见的方式有:

单机并行:在同一台服务器上启动多个线程或进程,比如Web服务器(如Nginx)用多进程处理HTTP请求

分布式并行:在多台机器上部署多个服务实例,比如云计算平台通过弹性伸缩组自动增减实例。

任务拆分:将大任务拆分为子任务,分配给不同服务并行处理,比如MapReduce中将数据分片,多个节点并行计算后汇总结果。

异步通信:通过Kafka、RabbitMQ等传递任务,多个消费者并行处理,比如订单系统生成消息,库存服务和支付服务并行消费并处理。

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:

Redis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁,而且 Redis 的读写性能高,可以应对高并发的锁操作场景。Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:

如果 key 不存在,则显示插入成功,可以用来表示加锁成功;

如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。

加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;

锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;

锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;

满足这三个条件的分布式命令如下:

SET lock_key unique_value NX PX 10000

lock_key 就是 key 键;

unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;

NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;

PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

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

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放

ifredis.call("get",KEYS[1]) == ARGV[1] then

returnredis.call("del",KEYS[1])

else

return0

end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

9. 什么是缓存穿透?解决方法有什么?

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

缓存穿透解决方案:

非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

缓存空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

布隆过滤器:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。对于写数据,我会选择更新 db 后,再删除缓存。

缓存是通过牺牲强一致性来提高性能的。这是由CAP理论决定的。缓存系统适用的场景就是非强一致性的场景,它属于CAP中的AP。所以,如果需要数据库和缓存数据保持强一致,就不适合使用缓存。

所以使用缓存提升性能,就是会有数据更新的延迟。这需要我们在设计时结合业务仔细思考是否适合用缓存。然后缓存一定要设置过期时间,这个时间太短、或者太长都不好:

太短的话请求可能会比较多的落到数据库上,这也意味着失去了缓存的优势。

太长的话缓存中的脏数据会使系统长时间处于一个延迟的状态,而且系统中长时间没有人访问的数据一直存在内存中不过期,浪费内存。

但是,通过一些方案优化处理,是可以最终一致性的。

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

删除缓存重试策略(消息队列)

消息队列方案

我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。

如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。

如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

举个例子,来说明重试机制的过程。

重试删除缓存机制还可以,就是会造成好多业务代码入侵。

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

下图是 Canal 的工作原理:

11. 手撕算法

Leetcode 153 寻找旋转排序数组的最小值

项目用了短链,问了几个短链项目的问题

短链接的生成原理是什么?

如何解决短链接生成的哈希冲突问题?

如何加快从短链接到原链接的重定向过程返回搜狐,查看更多

相关推荐

微信公众号改名指南:认证与否都能修改,手把手教会账号名字变更
《绝地求生》背包操作指南:开启键位与使用技巧
office365无法登录激活

《绝地求生》背包操作指南:开启键位与使用技巧

📅 11-03 👁️ 9837
PicsArt注册账号的操作流程
365bet投注盘口

PicsArt注册账号的操作流程

📅 09-08 👁️ 6284
光荣使命游戏电脑版下载2025最新
office365无法登录激活

光荣使命游戏电脑版下载2025最新

📅 07-21 👁️ 978
特种车辆驾驶证在哪里考
office365无法登录激活

特种车辆驾驶证在哪里考

📅 09-29 👁️ 270
HoRain云--BT种子、迅雷下载链接、磁力链接详解
365bet投注盘口

HoRain云--BT种子、迅雷下载链接、磁力链接详解

📅 09-26 👁️ 5662