招银网络一面,还算基础!
JavaGuide 官方网站:javaguide.cn
JavaGuide&Java面试交流圈:JavaGuide 官方知识星球
前段时间分享了阿里淘天和快手一面的面经,都是附带详细参考答案那种。
今天再给大家分享一位武汉理工大学同学的招银网络一面面经,同样附带超详细的参考答案。大家可以用来查漏补缺,针对性地补短板。
招银网络一面还是比较简单的,基本都是一些比较重要且高频的常规八股,项目问的不多。到了二面的时候, 会开始主要考察你的项目。
很多同学觉得这种基础问题的考查意义不大,实际上还是很有意义的,这种基础性的知识在日常开发中也会需要经常用到。例如,线程池这块的拒绝策略、核心参数配置什么的,如果你不了解,实际项目中使用线程池可能就用的不是很明白,容易出现问题。而且,其实这种基础性的问题是最容易准备的,像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的!
1、自我介绍
自我介绍一般是你和面试官的第一次面对面正式交流,换位思考一下,假如你是面试官的话,你想听到被你面试的人如何介绍自己呢?一定不是客套地说说自己喜欢编程、平时花了很多时间来学习、自己的兴趣爱好是打球吧?
我觉得一个好的自我介绍应该包含这几点要素:
-
用简单的话说清楚自己主要的技术栈于擅长的领域; -
把重点放在自己在行的地方以及自己的优势之处; -
重点突出自己的能力比如自己的定位的 bug 的能力特别厉害;
自我介绍并不需要死记硬背,记住要说的要点,面试的时候根据公司的情况临场发挥也是没问题的。另外,网上一般建议的是准备好两份自我介绍:一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节和项目经验。
自我介绍模板:
2、项目中的 Dubbo 用的什么通信协议
协议是两个网络实体进行通信的基础,它规定了数据在网络中的传输内容和格式。除了必要的请求和响应数据之外,协议通常还包含一些控制数据,例如版本号、事件类型、序列化方式、压缩方式、超时时间等等。
Dubbo2 的时候,一般使用 Dubbo 协议(这里指的是 Dubbo2 默认协议) + 自定义序列化(如 Hessian2、ProtoBuf、Kryo、FST)。
<!-- 协议使用 Dubbo,序列化使用Kryo-->
<dubbo:protocol name="dubbo" serialization="kryo"/>
<!-- 协议使用 Dubbo,序列化使用FST-->
<dubbo:protocol name="dubbo" serialization="fst"/>
Dubbo 协议的格式如下:
-
0-15(Magic - Magic High & Magic Low): 魔数,判断是否是 dubbo 协议 -
16(Req/Res): 标识是请求或响应。请求:1; 响应:0。 -
17(2 Way): 仅在 Req/Res 为 1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器的返回值,则设置为 1。 -
18(Event): 表示是否为事件消息,如心跳事件 -
19-23(Serialization ID): 标识序列化类型,为数字,每个数字指代一种序列化类型。 -
24-31(Status): 标识响应状态(类似 Http Status)。 -
32-63(Request ID):请求 id,类型为 long。 -
64-95(Data Length): 内容长度(字节) -
96-?(Variable Part): 序列化后的内容(换行符分隔)
可以看出,Dubbo 协议设计上还是挺紧凑的,是由头部和内容组成的格式。头部占用 96 字节,96 字节之后的内容就是序列化后的内容,默认采用 Hessian2 序列化方式,也可以根据需要更换为 Fastjson、ProtoBuf、Kryo、FST 等其他序列化方式。
如果我们自己要写一个 RPC 框架的,也可以参考着 Dubbo 协议的格式来设计。我之前写的玩具级别的 RPC 框架就是模仿的 Dubbo 协议。
Triple 协议是 Dubbo3 推出的主力协议,被称为下一代 RPC 协议。它完全兼容 gRPC 协议,支持 Request-Response、Streaming 流式等通信模型,可同时运行在 HTTP/1 和 HTTP/2 之上,让你可以直接使用 curl、浏览器访问后端 Dubbo 服务。并且,Triple 协议的还具备下面这些特性:
-
具备跨语言交互的能力 -
支持 TLS 加密、Plaintext 明文数据传输 -
支持反压与限流 -
……
Triple 协议是 Dubbo 协议的升级版,它解决了 Dubbo 协议在跨语言、云原生、网关代理等方面的互通性问题。
Dubbo 框架实现专注在 Triple 协议自身,而对于底层的网络通信、HTTP/2 协议解析等选择依赖那些经过长期检验的网络库。比如 Dubbo Java 基于 Netty 构建,而 Dubbo Go 则是直接使用的 Go 官方 HTTP 库。
如果决定要升级到 Dubbo3 的 Triple 协议,只需要修改配置中的协议名称为 tri
(注意: 不是 triple)即可。Dubbo3 为了完美兼容 Dubbo2,做了很多工作来保证升级的无感,目前默认的序列化和 Dubbo2 保持一致(Hessian2)。
更多关于 Dubbo 通信协议的介绍,建议阅读官网文档:https://cn.dubbo.apache.org/zh-cn/overview/home/。
3、讲讲 BIO,NIO,AIO,IO 多路复用了解吗?
BIO (Blocking I/O)
BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (Non-blocking/New I/O)
Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio
包,提供了 Channel
, Selector
,Buffer
等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它是支持面向缓冲的,基于通道的 I/O 操作方法。对于高负载、高并发的(网络)应用,应使用 NIO 。
Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
跟着我的思路往下看看,相信你会得到答案!
我们先来看看 同步非阻塞 IO 模型。
同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
这个时候,I/O 多路复用模型 就上场了。
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,目前几乎在所有的操作系统上都有支持。
select 调用:内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。 epoll 调用:linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
AIO (Asynchronous I/O)
AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。
最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。
关于 Java IO 模型的详细介绍,推荐阅读 常见的 IO 模型有哪些?Java 中 BIO、NIO、AIO 的区别?这篇原创好文。
4、TCP 三握四挥
建立连接-TCP 三次握手:
建立一个 TCP 连接需要“三次握手”,缺一不可:
-
一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务器的确认; -
二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态 -
三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入ESTABLISHED 状态,完成 TCP 三次握手。
当建立了 3 次握手之后,客户端和服务端就可以传输数据啦!
断开连接-TCP 四次挥手:
断开一个 TCP 连接则需要“四次挥手”,缺一不可:
-
第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后客户端进入 FIN-WAIT-1 状态。 -
第二次挥手:服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。 -
第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。 -
第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入TIME-WAIT状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
只要四次挥手没有结束,客户端和服务端就可以继续传输数据!
这里顺带总结一下 TCP 三次握手和四次挥手(非常重要)相关的面试题:
-
为什么要三次握手? -
第 2 次握手传回了 ACK,为什么还要传回 SYN? -
为什么要四次挥手? -
为什么不能把服务器发送的 ACK 和 FIN 合并起来,变成三次挥手? -
如果第二次挥手时服务器的 ACK 没有送达客户端,会怎样? -
为什么第四次挥手客户端需要等待 2*MSL(报文段最长寿命)时间后才进入 CLOSED 状态?
参考答案:TCP 为什么要三次握手和四次挥手?(原创好文)。
5、CopyOnWrite 了解吗
下面是维基百科对 Copy-On-Write 的介绍,介绍的挺不错:
写入时复制(英语:Copy-on-write,简称 COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
CopyOnWriteArrayList
线程安全的核心在于其采用了 写时复制(Copy-On-Write) 的策略,这里再以 CopyOnWriteArrayList
为例介绍:当需要修改( add
,set
、remove
等操作) CopyOnWriteArrayList
的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了(推荐大家顺便去看一下 CopyOnWriteArrayList 源码分析这篇原创好文)。
可以看出,写时复制机制非常适合读多写少的并发场景,能够极大地提高系统的并发性能。
不过,写时复制机制并不是银弹,其依然存在一些缺点,下面列举几点:
-
内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。 -
写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。 -
数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。 -
……
最后,建议大家顺便去看看下面这几篇原创并发面试题总结:
-
Java 并发常见面试题总结(上) -
Java 并发常见面试题总结(中) -
Java 并发常见面试题总结(下)
6、Redis 持久化了解吗,讲一下 AOF
Redis 持久化机制属于后端面试超高频的面试知识点,老生常谈了,需要重点花时间掌握。即使不是准备面试,日常开发也是需要经常用到的。
关于 Redis 持久化的详细介绍,还请移步这篇原创好文:美团面试:宕机了,Redis 如何避免数据丢失?,介绍的比较全面。
7、Redis 的缓存击穿,穿透,雪崩及其解决方案
缓存穿透
什么是缓存穿透?
缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
举个例子:某个黑客故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据。也就是说这些请求最终都落到了数据库上,对数据库造成了巨大的压力。
有哪些解决办法?
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
1)缓存无效 key
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:SET key value EX 10086
。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
另外,这里多说一嘴,一般情况下我们是这样设计 key 的:表名:列名:主键名:主键值
。
如果用 Java 代码展示的话,差不多是下面这样的:
public Object getObjectInclNullById(Integer id) {
// 从缓存中获取数据
Object cacheValue = cache.get(id);
// 缓存为空
if (cacheValue == null) {
// 从数据库中获取
Object storageValue = storage.get(key);
// 缓存空对象
cache.set(key, storageValue);
// 如果存储数据为空,需要设置一个过期时间(300秒)
if (storageValue == null) {
// 必须设置过期时间,否则有被攻击的风险
cache.expire(key, 60 * 5);
}
return storageValue;
}
return cacheValue;
}
2)布隆过滤器
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
加入布隆过滤器之后的缓存处理流程图如下。
但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!
我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:
-
使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 -
根据得到的哈希值,在位数组中把对应下标的值置为 1。
我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
-
对给定元素再次进行相同的哈希计算; -
得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)
更多关于布隆过滤器的内容可以看我的这篇原创:《不了解布隆过滤器?一文给你整的明明白白!》 ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。
缓存击穿
什么是缓存击穿?
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
举个例子:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。
有哪些解决办法?
-
设置热点数据永不过期或者过期时间比较长。 -
针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。 -
请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
缓存穿透和缓存击穿有什么区别?
缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。
缓存雪崩
什么是缓存雪崩?
我发现缓存雪崩这名字起的有点意思,哈哈。
实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。
举个例子:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。
有哪些解决办法?
针对 Redis 服务不可用的情况:
-
采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。 -
限流,避免同时处理大量的请求。
针对热点缓存失效的情况:
-
设置不同的失效时间比如随机设置缓存的失效时间。 -
缓存永不失效(不太推荐,实用性太差)。 -
设置二级缓存。
缓存雪崩和缓存击穿有什么区别?
缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。
8、Spring 循环依赖了解吗,怎么解决
循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用,例如 CircularDependencyA → CircularDependencyB → CircularDependencyA。
@Component
public class CircularDependencyA {
@Autowired
private CircularDependencyB circB;
}
@Component
public class CircularDependencyB {
@Autowired
private CircularDependencyA circA;
}
单个对象的自我依赖也会出现循环依赖,但这种概率极低,属于是代码编写错误。
@Component
public class CircularDependencyA {
@Autowired
private CircularDependencyA circA;
}
SpringBoot 2.6.x 以前是默认允许循环依赖的,也就是说你的代码出现了循环依赖问题,一般情况下也不会报错。解决办法是三级缓存机制,提前暴露的对象存放在三级缓存中,二级缓存存放过渡 bean(半成品),一级缓存存放最终形态的 bean。但是这种机制也有一些缺点,比如增加了内存开销(需要维护三级缓存,也就是三个 Map),降低了性能(需要进行多次检查和转换)。并且,还有少部分情况是不支持循环依赖的,比如非单例的 bean 和@Async
注解的 bean 无法支持循环依赖(具体可以参考聊透 Spring 循环依赖这篇文章,关于为什么是三级缓存而不是二级缓存里面也有提到)。
SpringBoot 2.6.x 以后官方不再推荐编写存在循环依赖的代码,建议开发者自己写代码的时候去减少不必要的互相依赖。这其实也是我们最应该去做的,循环依赖本身就是一种设计缺陷,我们不应该过度依赖 Spring 而忽视了编码的规范和质量,说不定未来某个 SpringBoot 版本就彻底禁止循环依赖的代码了。
SpringBoot 2.6.x 以后,如果你不想重构循环依赖的代码的话,也可以采用下面这些方法:
-
在全局配置文件中设置允许循环依赖存在: spring.main.allow-circular-references=true
。最简单粗暴的方式,不太推荐。 -
在导致循环依赖的 Bean 上添加 @Lazy
注解,这是一种比较推荐的方式。@Lazy
用来标识类是否需要延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。 -
……
更多关于 Spring 循环依赖的内容可以看看下面这几篇文章:
-
Spring 循环依赖那些事儿(含 Spring 详细流程图) - 阿里开发者 -
浅谈 Spring 如何解决 Bean 的循环依赖问题
9、MySQL 索引了解吗?底层数据结构是什么?
MySQL 索引这个知识点真的太重要了,面试高频高点,性价非常高的 SQL 优化手段。
我专门写了一篇文章来总结 MySQL 索引常见的问题,传送门:MySQL 索引详解。更多 MySQL 相关的文章,可以前往 JavaGuide 官方网站进行阅读,地址:javaguide.cn。
10、MySQL 的日志讲一下, binlog 作用是什么?
MySQL 中常见的日志类型主要有下面几类(针对的是 InnoDB 存储引擎):
-
错误日志(error log) :对 MySQL 的启动、运行、关闭过程进行了记录。 -
二进制日志(binary log,binlog) :主要记录的是更改数据库数据的 SQL 语句。 -
一般查询日志(general query log) :已建立连接的客户端发送给 MySQL 服务器的所有 SQL 记录,因为 SQL 的量比较大,默认是不开启的,也不建议开启。 -
慢查询日志(slow query log) :执行时间超过 long_query_time
秒钟的查询,解决 SQL 慢查询问题的时候会用到。 -
事务日志(redo log 和 undo log) :redo log 是重做日志,undo log 是回滚日志。 -
中继日志(relay log) :relay log 是复制过程中产生的日志,很多方面都跟 binary log 差不多。不过,relay log 针对的是主从复制中的从库。 -
DDL 日志(metadata log) :DDL 语句执行的元数据操作。
二进制日志(binlog)和事务日志(redo log 和 undo log)比较重要,需要我们重点关注。
binlog(binary log 即二进制日志文件) 主要记录了对 MySQL 数据库执行了更改的所有操作(数据库执行的所有 DDL 和 DML 语句),包括表结构变更(CREATE、ALTER、DROP TABLE……)、表数据修改(INSERT、UPDATE、DELETE...),但不包括 SELECT、SHOW 这类不会对数据库造成更改的操作。
不过,并不是不对数据库造成修改就不会被记录进 binlog。即使表结构变更和表数据修改操作并未对数据库造成更改,依然会被记录进 binlog。
binlog 最主要的应用场景是 主从复制 ,主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
主从复制的原理如下图所示:
-
主库将数据库中数据的变化写入到 binlog -
从库连接主库 -
从库会创建一个 I/O 线程向主库请求更新的 binlog -
主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收 -
从库的 I/O 线程将接收的 binlog 写入到 relay log 中。 -
从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。
关于 MySQL binlog 的详细介绍,可以查看这篇文章:面试官:MySQL binlog 有什么作用?主从延迟的了解么?。
11、给定一个字符串,统计字符串中每个字符的出现次数,按照字母表顺序输出,a2b2c4 这种
利用 TreeMap
可以比较简单地解决这个问题:
public static String countCharacters(String str) {
//使用 TreeMap 实现有序输出
Map<Character, Integer> countMap = new TreeMap<>();
// 统计每个字符串出现的次数
for (char c : str.toCharArray()) {
countMap.put(c, countMap.getOrDefault(c, 0) + 1);
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<Character, Integer> entry : countMap.entrySet()) {
sb.append(entry.getKey()).append(entry.getValue());
}
return sb.toString();
}
测试代码:
String str = "ababccdccddddf";
String result = countCharacters(str);
System.out.println(result);
输出:
a2b2c4d5f1
·············· END ··············
👉专属面试小册/一对一提问/简历修改/专属求职指南/学习打卡,欢迎加入 JavaGuide 官方知识星球 。
👉近期文章精选: