前言:
在40岁老架构师尼恩的(50+)读者社群中,经常有小伙伴需要面试饿了么、 头条、美团、阿里、京东等大厂。有很多的小伙伴,完成了人生的逆袭,拿到了高端的offer。
最近一个6年经验的小伙伴,年薪拿到 60W, 非常牛掰。
下面是一个小伙伴成功拿到饿了么 高级 Java 的offer ,其面试经历,还是两个字:
- 深: 问的很深
- 宽: 范围很宽
下面,从小伙的面试正题看看,收个饿了么Offer需要学点啥?
下面的这些面试题,对于面试其他的 高级java岗位,也很有参考意义。
这里也把题目以及参考答案,收入咱们的《尼恩Java面试宝典》 V71,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请到文末公号 【技术自由圈】获取
文章目录
- 前言:
- 饿了么面试正题:
- 1、说说数据库事务的隔离级别?
- 2、说说事务的几大特性,并谈一下实现原理
- ACID
- 实现原理
- 3、如何用redis实现消息的发布订阅?
- 原理
- 具体步骤
- 4、java为什么要在内存结构中设计自己的程序计数器,为什么不使用内核的?
- 5、说说分布式事务2PC的过程?
- 准备阶段(Prepare Phase)
- 提交阶段(Commit Phase)
- 优点
- 缺点
- 6、redis是单线程的,为什么会这么快?
- 7、谈谈NIO的实现,以及Netty是如何设计的?
- NIO的实现
- Netty是如何设计的
- 8、微服务化的时候,什么时候应该拆分,什么情况应该合并
- 什么时候拆分微服务
- 什么时候合并微服务
- 9、什么时候应该使用消息,什么时候适合接口调用?
- 什么时候使用消息队列
- 什么时候使用接口调用
- 10、分库分表中如果让你设计全局id,如何设计?百度对雪花算法的优化了解过没?
- 雪花算法
- 百度对雪花算法的优化
- 11、redis如何进行单机热点数据的统计?
- 12、redis集群中新加节点以后,如何给新节点分配数据?
- 13、如何从含有100亿个整数的文件中找出其中最大的100个?
- 1.使用分治法
- 2.使用堆排序算法
- 3.使用快速选择算法
- 4.使用BitMap算法
- 说在最后:
- 技术自由的实现路径 PDF:
- 实现你的 架构自由:
- 实现你的 响应式 自由:
- 实现你的 spring cloud 自由:
- 实现你的 linux 自由:
- 实现你的 网络 自由:
- 实现你的 分布式锁 自由:
- 实现你的 王者组件 自由:
- 实现你的 面试题 自由:
饿了么面试正题:
1、说说数据库事务的隔离级别?
数据库事务的隔离级别是指在并发访问数据库时,各个事务之间隔离程度的不同。常见的隔离级别有以下四种:
- 读未提交(Read Uncommitted):这是最低的隔离级别,一个事务可以读取另一个未提交事务的数据,可能会导致脏读、不可重复读和幻读问题。
适用于读多写少的场景,可以提高并发性能。但是,如果一个事务读取了未提交的数据,其他事务可能会受到影响,因此需要谨慎使用。 - 读已提交(Read Committed):这是一种较高的隔离级别,一个事务只能读取另一个已提交事务的数据,可以避免脏读问题,但是仍可能出现不可重复读和幻读问题。
适用于读多写少的场景,可以保证数据的一致性,但可能会降低并发性能。 - 可重复读(Repeatable Read):这是一种更高的隔离级别,一个事务在执行过程中,多次读取同一数据会得到相同结果,可以避免脏读和不可重复读问题,但是仍可能出现幻读问题。
适用于需要保证数据一致性的场景,如银行交易、订单处理等。但是,由于需要在事务执行期间锁定数据,可能会降低并发性能。 - 串行化(Serializable):最高的隔离级别,所有事务串行执行,可以避免脏读、不可重复读和幻读问题,但是对性能有较大影响。
适用于对数据一致性要求非常高的场景,如金融交易、医疗诊断等。但是,由于串行执行,可能会降低并发性能。
在实际开发中,根据具体的业务需求和性能要求,可以选择不同的隔离级别来平衡数据一致性和并发性能。
隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交 | 最低级别,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
读已提交 | 语句级 | 否 | 是 | 是 |
可重复读 | 事务级 | 否 | 否 | 是 |
串行化 | 最高级别,事务级 | 否 | 否 | 否 |
表中列出了四种常见的数据库事务隔离级别,以及它们对于脏读、不可重复读和幻读的处理情况。其中,脏读指的是一个事务读取到了另一个事务尚未提交的数据;不可重复读指的是一个事务多次读取同一数据,但是由于其他事务的修改,每次读取的结果都不同;幻读指的是一个事务多次读取同一范围的数据,但是由于其他事务的插入或删除,每次读取的结果都不同。
2、说说事务的几大特性,并谈一下实现原理
事务是指作为单个逻辑工作单元执行的一系列操作,要么全部执行,要么全部不执行。
ACID
事务具有四个关键特性,即ACID:
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚,不允许只执行其中的一部分操作。
- 一致性(Consistency):事务执行前后,数据库的状态必须保持一致,即满足所有的约束条件。
- 隔离性(Isolation):事务之间是相互隔离的,一个事务的执行不应该影响其他事务的执行。每个事务都应该认为它是唯一在执行的事务,每个事务都应该感觉不到其他事务的存在。
- 持久性(Durability):事务一旦提交,对数据库中的数据修改就是永久性的,即使系统崩溃也不会丢失。
实现原理
事务的实现需要数据库管理系统支持,通常通过日志记录和锁机制来实现。
日志记录:在事务执行过程中,数据库管理系统会将所有的操作记录在日志中,如果事务执行失败,可以通过日志进行回滚,保证数据的一致性。
锁机制:为了保证事务之间的隔离性,数据库管理系统会使用锁机制,对事务进行隔离。当一个事务对某个数据进行修改时,会对该数据进行加锁,其他事务需要等待该事务释放锁后才能对该数据进行修改。
3、如何用redis实现消息的发布订阅?
Redis可以通过发布订阅(Pub/Sub)模式来实现消息的发布和订阅。
原理
Redis是使用C实现的,可以通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现
Redis通过PUBLISH,SUBSCRIBE和PSUBSCRIBE等命令实现发布和订阅功能
通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个频道,字典的值则是一个链表,链表中保存了所有订阅这个频道的客户端。SUBSCRIBE命令的关键,就是将客户端添加到给定频道的订阅链表中。
通过PUBLISH命令向订阅者发送消息,redis-server会使用给定频道作为键,在它维护的频道字典中查找记录了订阅这个频道的所有客户端的链表,将消息发布给所有订阅者
Pub和Sub从字面上理解就是发布(Publish)和订阅(Subscribe),在redis中,可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的信息,这一功能最明显的用法就是实时消息系统,比如普通的即时聊天,群聊等功能。
具体步骤
1. 创建订阅者集合
首先,需要在Redis中创建一个订阅者集合,用于存储所有订阅者的相关信息。可以使用Redis中的SET命令创建一个集合,其中键为订阅者的名字,值为该订阅者的ID。
2. 发布消息
然后,使用Redis的PUBLISH命令向指定的主题发布一条消息。主题是一个字符串,可以是任意名称,用于标识要发布的消息。可以使用Redis的JSON格式来表示消息内容,例如:
PUBLISH topic "Hello World"
3. 订阅消息
接下来,订阅者可以使用Redis的SUBSCRIBE命令订阅指定的主题。同样,主题也是一个字符串,可以是任意名称。订阅后,Redis会返回一个包含当前订阅者集合信息的响应。可以使用Redis的PSUBSCRIBE命令来订阅多个主题,例如:
PSUBSCRIBE "topic1", "topic2"
4. 处理消息
当有消息发布到指定的主题时,Redis会自动将消息发送给所有已订阅该主题的订阅者。订阅者可以使用Redis的LPUSH、RPUSH等命令来接收并处理消息,例如:
LPUSH "my-subscriber-channel" '{"message": "Hello World"}'
以上代码将消息发布到名为"my-subscriber-channel"的频道中,并传递了一个JSON格式的消息对象。其他订阅者可以使用相同的方式接收并处理该消息。
更多详细内容,请 参考 尼恩《Java高并发核心编程 卷1 加强版:NIO、Netty、Redis、ZooKeeper》,书里做了 详细的介绍,非常细致
4、java为什么要在内存结构中设计自己的程序计数器,为什么不使用内核的?
Java中的程序计数器(Program Counter Register)是一块内存区域,用于存储当前线程正在执行的字节码指令地址。Java虚拟机之所以要在内存结构中设计自己的程序计数器,而不使用内核的程序计数器,主要有以下原因:
- 内核提供的程序计数器是一个内核态的计数器,它只能简单地记录线程的执行次数,而不能像Java程序计数器一样可以动态地修改计数器的值。如果使用内核的程序计数器,由于多个线程可能同时访问内核,会导致竞争和冲突,从而导致程序出现错误或崩溃。
- Java程序计数器可以更好地支持多线程并发。在Java中,线程之间的切换是通过内核态的上下文切换来实现的,而程序计数器正是上下文切换的一个重要参数。当线程执行完一段代码后,需要将计数器的值加1,以便下次执行该代码时可以恢复到之前的状态。如果没有程序计数器,就无法实现这种动态的上下文切换。
- Java中的程序计数器可以通过内存屏障(Memory Barrier)等机制来保证线程之间的可见性和原子性,从而实现高效的并发执行。
另外,Java中的程序计数器还有以下优点:
- 跨平台性:Java的设计目标之一是实现跨平台性,即Java程序可以在不同的操作系统和硬件平台上运行。为了实现这一目标,Java虚拟机需要自己实现程序计数器,而不依赖于操作系统提供的程序计数器。
- 线程私有性:Java虚拟机中的程序计数器是线程私有的,每个线程都有自己的程序计数器。线程切换时,虚拟机会将当前线程的程序计数器保存起来,并恢复下一个线程的程序计数器。如果使用操作系统的程序计数器,就无法实现线程私有性。
- 快速访问:程序计数器是Java虚拟机执行引擎中的一个重要组成部分,用于指示当前线程正在执行的字节码指令地址。如果使用操作系统的程序计数器,就需要进行系统调用和内核态的切换,会影响性能。而Java虚拟机中的程序计数器是直接访问内存,速度更快。
综上所述,Java使用自己的程序计数器是为了支持多线程并发执行,并且通过内存结构来进行管理,以提高程序的稳定性和可靠性。Java虚拟机需要在内存结构中设计自己的程序计数器,以实现跨平台性、线程私有性和快速访问。
5、说说分布式事务2PC的过程?
分布式事务是指在分布式系统中,多个事务操作涉及到多个数据库或资源,需要保证这些事务操作要么全部成功,要么全部失败。2PC(Two-Phase Commit)是一种分布式事务协议,用于协调分布式事务的提交和回滚。其过程主要分为两个阶段:
准备阶段(Prepare Phase)
在这个阶段,协调者(Coordinator)向所有参与者(Participant)发送“准备”请求,询问它们是否可以执行事务,并将其执行结果保存在日志中。参与者执行事务,并将执行结果反馈给协调者。如果所有参与者都可以执行事务,则协调者发送“提交”请求,否则发送“回滚”请求。
提交阶段(Commit Phase)
在这个阶段,如果协调者发送的是“提交”请求,则所有参与者执行事务,并将执行结果提交。如果协调者发送的是“回滚”请求,则所有参与者撤销事务,并将执行结果回滚。最后,协调者向所有参与者发送“完成”请求,表示事务已经完成。
优点
在2PC的过程中,协调者是必须是强一致性的,即它需要对所有参与者的数据进行一致性检查,以确保所有参与者的数据都能正确地被提交或回滚。
2PC协议的优点是可以保证事务的原子性和一致性,即要么全部提交,要么全部回滚。
缺点
它也存在一些缺点,如:
- 性能问题:2PC需要进行多次网络通信和等待,会影响性能。
- 单点故障问题:协调者是2PC协议的关键,如果协调者出现故障,整个系统将无法正常工作。
- 同步阻塞问题:在准备阶段,所有参与者都需要等待协调者的响应,如果协调者响应时间过长,将会导致参与者的阻塞。
因此,在实际应用中,需要根据具体业务场景选择合适的分布式事务方案,如TCC、Saga等。
6、redis是单线程的,为什么会这么快?
Redis之所以能够高效地处理请求,主要是因为它采用了以下几种优化措施:
- 基于内存:Redis将所有数据存储在内存中,这样可以避免了磁盘I/O操作的开销,从而提高了数据读写的速度。
- 单线程模型:Redis采用单线程模型,避免了多线程之间的竞争和锁的开销,从而减少了上下文切换的开销。然 Redis 是单线程的,但是它使用了事件驱动机制和异步 I/O 技术,通过将任务分解为多个小任务,并行执行来提高并发能力。此外,Redis 还使用了多路复用技术,可以同时处理多个客户端请求。
- 异步非阻塞:Redis采用异步非阻塞的方式处理客户端请求,当客户端发起请求后,Redis会立即响应并将请求放入队列中,然后再异步地处理请求,这样可以避免了线程的阻塞和等待。
- 数据结构优化:Redis内置了多种数据结构,如哈希表、有序集合等,这些数据结构经过了优化,可以快速地进行数据的存储和检索。
- 高效的编码和解码:Redis 使用了一些高效的编码和解码算法,如 Deflate、Snappy、LZ4 等,可以压缩和解压缩数据,减少网络传输的数据量。
综合上述优化措施,使得Redis能够在单线程的情况下,处理大量的请求,并且保持高效的性能。
7、谈谈NIO的实现,以及Netty是如何设计的?
NIO的实现
NIO(Non-blocking I/O)是Java提供的一种新的I/O模型,它支持非阻塞式的、基于事件驱动的I/O操作。相比于传统的阻塞式I/O模型,NIO能够更好地处理高并发的网络请求,提高系统的吞吐量和响应速度。
NIO 的实现主要依赖于两个类:Channel
和 Buffer
。
Channel
表示一个连接到某个端口的实体,它可以与另一个 Channel 或服务端通信;Buffer
则表示一种数据结构,用于存储读入的数据,并提供了一些方法来处理这些数据。
NIO 通过Selector
(选择器)来实现事件驱动。它可以同时监听多个 Channel
的状态变化,并在有数据可读或可写时通知应用程序进行处理。Selector
会不断地轮询注册在其上的Channel
,当Channel
有数据可读或者可写时,Selector
会通知应用程序进行相应的处理。在NIO中,可以使用Channel
和Buffer
来进行数据的读写操作,而且可以使用单线程来处理多个Channel
的读写操作,从而避免了多线程之间的竞争和锁的开销。
Netty是如何设计的
Netty是一个基于NIO的客户端/服务器框架,它提供了高度可定制化的网络编程API,可以帮助开发者快速地构建高性能、高可靠性的网络应用程序。Netty的设计思路是基于“Reactor模式”,它采用了线程池、缓冲区池、内存池等技术来优化网络通信的性能,同时提供了丰富的编解码器和协议支持,使得开发者可以轻松地实现各种协议的数据交换。
Netty 主要的设计思想包括:
- 可扩展性:Netty 的组件化设计使得它非常容易扩展和定制。用户可以根据自己的需求选择合适的组件,并通过组合使用来实现复杂的功能。
- 高性能:Netty 采用了一些优化策略,如事件驱动模型、零拷贝技术、内存池等,从而提高了系统的吞吐量和响应能力。
- 可移植性:Netty 支持多种操作系统和平台,如 Windows、Linux、Unix、MacOS 等,并且可以在不同的语言中使用,如 Java、Scala、Python、Golang 等。
- 可维护性:Netty 的代码结构清晰、易于理解,同时提供了丰富的文档和示例代码,使得开发人员可以轻松地维护和修改代码。
Netty的核心组件包括Channel
、EventLoop
、ChannelFuture
、ChannelHandler
等。
Channel
是Netty的核心概念,它代表了一个网络连接,可以进行数据的读写操作;EventLoop
是Netty的事件循环组件,它负责处理所有的I/O事件,并将事件分发给对应的Channel进行处理;ChannelFuture
是Netty的异步操作结果的封装类,可以用来获取异步操作的结果;ChannelHandler
是Netty的数据处理器,它负责对Channel中的数据进行编解码、处理和转发。
总之,NIO和Netty的实现都是基于事件驱动的异步非阻塞模型,能够更好地处理高并发的网络请求,提高系统的吞吐量和响应速度。
更多详细内容,请 参考 尼恩《Java高并发核心编程 卷1 加强版:NIO、Netty、Redis、ZooKeeper》,书里做了 详细的介绍,非常细致
8、微服务化的时候,什么时候应该拆分,什么情况应该合并
微服务架构的拆分和合并需要考虑多个因素,如业务复杂度、团队规模、技术栈、可维护性、性能等。
什么时候拆分微服务
- 业务复杂度高:当业务逻辑十分复杂时,可以考虑将其拆分成多个微服务,每个微服务专注于某个子领域的业务逻辑。
- 团队规模大:当团队规模较大时,可以将团队拆分成多个小团队,每个小团队负责维护一个微服务,以提高开发效率和质量。
- 技术栈不同:当不同的微服务使用不同的技术栈时,可以将其拆分成多个微服务,以便于团队专注于自己擅长的技术栈。
- 可维护性差:当某个微服务的代码难以维护时,可以将其拆分成多个微服务,以便于团队更好地维护和管理代码。
什么时候合并微服务
- 业务逻辑简单:当业务逻辑较为简单时,可以将多个微服务合并成一个,以减少系统的复杂度和维护成本。
- 性能问题:当多个微服务之间的调用频繁时,可以将其合并成一个微服务,以减少网络延迟和提高性能。
- 数据共享:当多个微服务需要共享同一份数据时,可以将其合并成一个微服务,以便于数据的管理和维护。
需要注意的是,微服务的拆分和合并需要谨慎考虑,应该根据具体情况进行决策。
9、什么时候应该使用消息,什么时候适合接口调用?
在微服务架构中,我们可以使用消息队列或接口调用来实现不同微服务之间的通信。
什么时候使用消息队列
- 异步通信:当两个微服务之间需要异步通信时,可以使用消息队列。例如,当一个微服务需要将某个事件通知给其他微服务时,可以使用消息队列来实现异步通信。
- 解耦:当两个微服务之间需要解耦时,可以使用消息队列。例如,当一个微服务需要将某个任务交给其他微服务处理时,可以使用消息队列来实现任务的解耦。
- 流量控制:当两个微服务之间的流量需要控制时,可以使用消息队列。例如,当一个微服务需要将大量数据传输给其他微服务时,可以使用消息队列来控制流量。
什么时候使用接口调用
- 同步通信:当两个微服务之间需要同步通信时,可以使用接口调用。例如,当一个微服务需要获取其他微服务的数据时,可以使用接口调用来实现同步通信。
- 高性能:当两个微服务之间的通信需要高性能时,可以使用接口调用。例如,当一个微服务需要频繁地调用其他微服务时,可以使用接口调用来提高性能。
- 数据安全:当两个微服务之间的通信需要保证数据安全时,可以使用接口调用。例如,当一个微服务需要传输敏感数据时,可以使用接口调用来保证数据的安全性。
需要注意的是,消息队列和接口调用各有优缺点,应该根据具体情况选择合适的通信方式。同时,在实际应用中,我们也可以将消息队列和接口调用结合起来使用,以实现更加灵活和高效的通信方式。
10、分库分表中如果让你设计全局id,如何设计?百度对雪花算法的优化了解过没?
雪花算法
在分库分表中,为了避免不同的数据库中出现相同的ID,需要设计全局唯一的ID。一种常见的方案是使用雪花算法 (SnowFlake) 生成全局唯一ID。
Snowflake算法是Twitter开源的一个分布式ID生成算法,它可以保证在分布式环境下生成唯一的ID。Snowflake算法生成的ID是一个64位的整数,其中1位是符号位,41位是时间戳,10位是工作机器ID,12位是序列号。
Snowflake算法的ID生成规则如下:
- 第一位是符号位,始终为0,表示生成的是正整数
- 接下来的41位是时间戳,精确到毫秒级别,可以使用当前时间减去一个固定的起始时间,得到一个相对时间戳
- 接下来的10位是机器标识符,可以根据需要自行设计,比如可以使用IP地址、MAC地址、数据中心ID等信息来生成
- 最后的12位是序列号,可以使用计数器来实现,每次生成ID时自增,当序列号达到最大值时,可以等待下一毫秒再继续生成
使用Snowflake算法生成的ID具有以下优点:
- 全局唯一,可以在分布式系统中生成唯一的ID
- 时间戳有序,可以根据ID的时间戳来进行排序,方便数据库的查询和分析
- 高性能,生成ID的速度非常快,可以支持高并发的场景
- 易于实现,Snowflake算法的实现比较简单,可以使用Java等语言来实现
需要注意的是,在分库分表的场景下,如果使用Snowflake算法生成ID,需要保证每个分库分表的机器标识符不同,否则可能会导致生成重复的ID。可以考虑使用数据中心ID和机器ID来生成机器标识符,以保证每个分库分表的机器标识符不同。
以下是Java实现Snowflake算法生成全局唯一ID的示例代码:
public class SnowflakeIdGenerator {
// 起始的时间戳
private final static long START_TIMESTAMP = 1480166465631L;
// 每一部分占用的位数
private final static long SEQUENCE_BIT = 12; // 序列号占用的位数
private final static long MACHINE_BIT = 10; // 机器标识占用的位数
private final static long DATACENTER_BIT = 1; // 数据中心占用的位数
// 每一部分的最大值
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
// 每一部分向左的位移
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTAMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId; // 数据中心
private long machineId; // 机器标识
private long sequence = 0L; // 序列号
private long lastTimestamp = -1L; // 上一次时间戳
public SnowflakeIdGenerator(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0L) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT) |
(datacenterId << DATACENTER_LEFT) |
(machineId << MACHINE_LEFT) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
使用示例:
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
long id = idGenerator.nextId();
System.out.println(id);
这里的datacenterId和machineId可以根据实际情况进行设定,比如可以使用Zookeeper来管理datacenterId和machineId的分配。
百度对雪花算法的优化
Snowflake算法是一种常用的分布式ID生成算法,但是在高并发场景下,可能会出现ID重复的问题,这会导致数据的错误和不一致。为了解决这个问题,百度在Snowflake算法的基础上进行了一些优化,使得生成的ID更加稳定和唯一。
百度对Snowflake算法的优化主要有以下几点:
1. 增加数据中心ID和机器ID的位数
原始的Snowflake算法中,数据中心ID和机器ID的位数分别为5位和5位,总共10位。百度将数据中心ID和机器ID的位数分别增加到了8位和8位,总共16位。这样可以支持更多的数据中心和机器,也可以减少ID重复的可能性。
2. 使用Zookeeper来管理数据中心ID和机器ID
在原始的Snowflake算法中,数据中心ID和机器ID是静态配置的,需要在每个应用程序中进行配置。这样会带来一些问题,比如在扩容或缩容时需要修改配置文件,容易出错,而且不够灵活。为了解决这个问题,百度使用Zookeeper来管理数据中心ID和机器ID。每个应用程序在启动时,都会向Zookeeper注册自己的ID,Zookeeper会分配一个唯一的ID给应用程序。这样可以避免手动配置的问题,也可以支持动态扩容和缩容。
3. 改进哈希函数
百度使用了MurmurHash3哈希函数来存储雪花序列。MurmurHash3哈希函数是一种高效的哈希函数,可以快速地将一组数字映射到一个固定的数组位置。
使用线程安全的哈希表:在生成全局唯一标识符时,需要在多个线程中同时使用哈希表来存储雪花序列。为了保证哈希表的线程安全性,百度使用了C++11的标准库中提供的线程安全的哈希表。
增加哈希表的大小:为了提高哈希表的效率,百度在实际应用中增加了哈希表的大小。当哈希表的大小达到一定程度时,就会自动扩容,以保证哈希表的性能和稳定性。
4. 时间戳精度
在雪花算法中,时间戳的精度为毫秒级别。为了进一步提高时间戳的精度,百度对雪花算法进行了优化,将时间戳的精度提高到了微秒级别。这样可以更好地支持分布式系统中的时间同步和时序控制。
5. 序列号范围:
在雪花算法中,序列号的范围是0到4095。为了支持更大的并发量和更高的性能,百度对雪花算法进行了优化,将序列号的范围扩展到了1到4096。这样可以更好地支持高并发场景下的数据写入和查询操作。
6. 机器标识码:
在雪花算法中,机器标识码用于表示当前机器的唯一标识符。为了避免机器标识码冲突,百度对雪花算法进行了优化,将机器标识码的范围从0到32位扩展到了128位。这样可以更好地支持多台机器之间的唯一标识符冲突问题。
7. 并发控制:
在雪花算法中,为了保证并发写入时的正确性,百度对雪花算法进行了优化,引入了写入锁和读锁等机制。这样可以更好地支持高并发场景下的写入操作,并且可以避免写入冲突和数据丢失的问题。
通过以上优化,百度实现了一个更加稳定和可靠的分布式ID生成算法,可以在高并发场景下生成唯一的ID,保证数据的正确性和一致性。
11、redis如何进行单机热点数据的统计?
Redis可以通过以下几种方式进行单机热点数据的统计:
- 使用
INFO
命令查看Redis实例的各种性能指标,如内存使用情况、连接数、执行命令数等。INFO
命令是Redis自带的一个命令,可以在任何Redis客户端中使用。
1)使用INFO
命令获取Redis服务器的统计信息。
2)解析统计信息,获取内存使用情况相关的数据。
3)根据内存使用情况,计算出每个key的内存占用情况。
4)对所有key的内存占用情况进行排序,获取前N个内存占用最大的key,即为热点数据。 - 使用
MONITOR
命令实时监测Redis实例的性能指标,并将结果输出到标准输出流。MONITOR
命令可以设置监控周期和输出格式,非常灵活。 - 使用Redis集群中的
CLUSTER INFO
命令查看集群中各个节点的性能指标,包括内存使用情况、连接数、执行命令数等。CLUSTER INFO
命令只能在Redis集群中使用。 - 在应用程序中集成Redis监控工具,如New Relic、Datadog等。这些工具可以帮助你实时监测Redis实例的性能指标,并提供详细的报告和警报功能。
12、redis集群中新加节点以后,如何给新节点分配数据?
在Redis集群中,当新加入一个节点时,需要将集群中的数据进行重新分片,以保证各个节点负载均衡。具体步骤如下:
- 确定新节点的插槽范围。在Redis集群中,数据被分成16384个插槽,每个插槽都有一个编号,从0到16383。新节点需要被分配一定范围的插槽,可以根据当前集群中的节点数量和插槽数量来计算。
- 将新节点加入集群。可以使用Redis的
CLUSTER MEET
命令将新节点加入集群,例如:
CLUSTER MEET <new_node_ip> <new_node_port>
- 将新节点分配插槽。可以使用Redis的
CLUSTER ADDSLOTS
命令将一定范围的插槽分配给新节点,例如:
CLUSTER ADDSLOTS 0 1 2 3 4 ... 100
其中,0 1 2 3 4 ... 100
表示要分配的插槽编号。
- 等待集群重新分片。当新节点加入集群并分配了插槽后,集群会自动进行重新分片,将相应的数据迁移到新节点上。这个过程需要一定的时间,可以使用
CLUSTER INFO
命令来查看集群状态,直到集群状态为ok
。 - 重复上述步骤,直到所有节点都加入集群并分配了插槽。
需要注意的是,Redis集群具有自动平衡数据的功能,当某个节点的插槽数量过多或过少时,集群会自动将一些插槽迁移到其他节点上,以保持各个节点的负载均衡。因此,在进行节点的添加和删除时,可以让集群自动进行数据迁移,以减少手动操作的复杂性。
13、如何从含有100亿个整数的文件中找出其中最大的100个?
答案是:分别可以用分治法、堆排序、快速选择算法、BitMap算法,
下面是用java写出几种算法的代码
1.使用分治法
分治法的思路是将大问题分解为小问题,然后分别解决小问题,最后将小问题的解合并起来得到大问题的解。在找出100亿个整数中最大的100个数时,可以将整个数据集分成若干个小数据集,分别找出每个小数据集中最大的100个数,然后将这些最大的100个数合并起来,再找出其中最大的100个数即可。
Java代码实现如下:
import java.io.*;
import java.util.*;
public class Top100NumbersByDivideAndConquer {
private static final int MAX_NUMBERS = 1000000000; // 最多处理10亿个数
private static final int MAX_NUMBERS_PER_FILE = 10000000; // 每个文件最多处理1千万个数
private static final int MAX_NUMBERS_PER_GROUP = 1000000; // 每个小数据集最多处理100万个数
private static final int MAX_GROUPS = MAX_NUMBERS / MAX_NUMBERS_PER_GROUP; // 最多分成10000个小数据集
private static final int MAX_TOP_NUMBERS = 100; // 找出最大的100个数
public static void main(String[] args) throws Exception {
// 生成随机数文件
generateRandomNumbersFile("numbers.txt", MAX_NUMBERS);
// 将随机数文件分成若干个小文件
List<String> files = splitNumbersFile("numbers.txt", MAX_NUMBERS_PER_FILE);
// 找出每个小文件中最大的100个数
List<List<Integer>> topNumbersPerFile = new ArrayList<>();
for (String file : files) {
List<Integer> numbers = readNumbersFromFile(file);
List<Integer> topNumbers = findTopNumbersByHeapSort(numbers, MAX_TOP_NUMBERS);
topNumbersPerFile.add(topNumbers);
}
// 将每个小文件中最大的100个数合并起来
List<Integer> topNumbers = mergeTopNumbers(topNumbersPerFile, MAX_TOP_NUMBERS);
// 输出最大的100个数
System.out.println("Top " + MAX_TOP_NUMBERS + " numbers:");
for (int i = 0; i < MAX_TOP_NUMBERS; i++) {
System.out.println(topNumbers.get(i));
}
}
// 生成随机数文件
private static void generateRandomNumbersFile(String fileName, int count) throws Exception {
Random random = new Random();
BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
for (int i = 0; i < count; i++) {
writer.write(String.valueOf(random.nextInt()));
writer.newLine();
}
writer.close();
}
// 将随机数文件分成若干个小文件
private static List<String> splitNumbersFile(String fileName, int maxNumbersPerFile) throws Exception {
List<String> files = new ArrayList<>();
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
int count = 0;
int fileIndex = 0;
BufferedWriter writer = new BufferedWriter(new FileWriter("numbers_" + fileIndex + ".txt"));
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
count++;
if (count >= maxNumbersPerFile) {
writer.close();
files.add("numbers_" + fileIndex + ".txt");
fileIndex++;
writer = new BufferedWriter(new FileWriter("numbers_" + fileIndex + ".txt"));
count = 0;
}
}
writer.close();
files.add("numbers_" + fileIndex + ".txt");
reader.close();
return files;
}
// 从文件中读取数字
private static List<Integer> readNumbersFromFile(String fileName) throws Exception {
List<Integer> numbers = new ArrayList<>();
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
while ((line = reader.readLine()) != null) {
numbers.add(Integer.parseInt(line));
}
reader.close();
return numbers;
}
// 使用堆排序算法找出最大的k个数
private static List<Integer> findTopNumbersByHeapSort(List<Integer> numbers, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(k);
for (int number : numbers) {
if (heap.size() < k) {
heap.offer(number);
} else if (number > heap.peek()) {
heap.poll();
heap.offer(number);
}
}
List<Integer> topNumbers = new ArrayList<>(heap);
Collections.sort(topNumbers, Collections.reverseOrder());
return topNumbers;
}
// 合并每个小文件中最大的k个数
private static List<Integer> mergeTopNumbers(List<List<Integer>> topNumbersPerFile, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(k);
for (List<Integer> topNumbers : topNumbersPerFile) {
for (int number : topNumbers) {
if (heap.size() < k) {
heap.offer(number);
} else if (number > heap.peek()) {
heap.poll();
heap.offer(number);
}
}
}
List<Integer> topNumbers = new ArrayList<>(heap);
Collections.sort(topNumbers, Collections.reverseOrder());
return topNumbers;
}
}
2.使用堆排序算法
堆排序算法的思路是使用一个小根堆来存储当前已经找到的最大的k个数,然后遍历剩余的数,如果比堆顶元素大,则将堆顶元素替换为该数,然后重新调整堆。
Java代码实现如下:
import java.io.*;
import java.util.*;
public class Top100NumbersByHeapSort {
private static final int MAX_NUMBERS = 1000000000; // 最多处理10亿个数
private static final int MAX_TOP_NUMBERS = 100; // 找出最大的100个数
public static void main(String[] args) throws Exception {
// 生成随机数文件
generateRandomNumbersFile("numbers.txt", MAX_NUMBERS);
// 找出最大的100个数
List<Integer> numbers = readNumbersFromFile("numbers.txt");
List<Integer> topNumbers = findTopNumbersByHeapSort(numbers, MAX_TOP_NUMBERS);
// 输出最大的100个数
System.out.println("Top " + MAX_TOP_NUMBERS + " numbers:");
for (int i = 0; i < MAX_TOP_NUMBERS; i++) {
System.out.println(topNumbers.get(i));
}
}
// 生成随机数文件
private static void generateRandomNumbersFile(String fileName, int count) throws Exception {
Random random = new Random();
BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
for (int i = 0; i < count; i++) {
writer.write(String.valueOf(random.nextInt()));
writer.newLine();
}
writer.close();
}
// 从文件中读取数字
private static List<Integer> readNumbersFromFile(String fileName) throws Exception {
List<Integer> numbers = new ArrayList<>();
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
while ((line = reader.readLine()) != null) {
numbers.add(Integer.parseInt(line));
}
reader.close();
return numbers;
}
// 使用堆排序算法找出最大的k个数
private static List<Integer> findTopNumbersByHeapSort(List<Integer> numbers, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(k);
for (int number : numbers) {
if (heap.size() < k) {
heap.offer(number);
} else if (number > heap.peek()) {
heap.poll();
heap.offer(number);
}
}
List<Integer> topNumbers = new ArrayList<>(heap);
Collections.sort(topNumbers, Collections.reverseOrder());
return topNumbers;
}
}
3.使用快速选择算法
快速选择算法的思路是使用快速排序的思路,将数据集分成两部分,然后只对包含最大的k个数的那一部分继续递归,直到找到最大的k个数。
Java代码实现如下:
import java.io.*;
import java.util.*;
public class Top100NumbersByQuickSelect {
private static final int MAX_NUMBERS = 1000000000; // 最多处理10亿个数
private static final int MAX_TOP_NUMBERS = 100; // 找出最大的100个数
public static void main(String[] args) throws Exception {
// 生成随机数文件
generateRandomNumbersFile("numbers.txt", MAX_NUMBERS);
// 找出最大的100个数
List<Integer> numbers = readNumbersFromFile("numbers.txt");
List<Integer> topNumbers = findTopNumbersByQuickSelect(numbers, MAX_TOP_NUMBERS);
// 输出最大的100个数
System.out.println("Top " + MAX_TOP_NUMBERS + " numbers:");
for (int i = 0; i < MAX_TOP_NUMBERS; i++) {
System.out.println(topNumbers.get(i));
}
}
// 生成随机数文件
private static void generateRandomNumbersFile(String fileName, int count) throws Exception {
Random random = new Random();
BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
for (int i = 0; i < count; i++) {
writer.write(String.valueOf(random.nextInt()));
writer.newLine();
}
writer.close();
}
// 从文件中读取数字
private static List<Integer> readNumbersFromFile(String fileName) throws Exception {
List<Integer> numbers = new ArrayList<>();
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
while ((line = reader.readLine()) != null) {
numbers.add(Integer.parseInt(line));
}
reader.close();
return numbers;
}
// 使用快速选择算法找出最大的k个数
private static List<Integer> findTopNumbersByQuickSelect(List<Integer> numbers, int k) {
int left = 0;
int right = numbers.size() - 1;
while (left <= right) {
int pivotIndex = partition(numbers, left, right);
if (pivotIndex == k) {
break;
} else if (pivotIndex < k) {
left = pivotIndex + 1;
} else {
right = pivotIndex - 1;
}
}
List<Integer> topNumbers = new ArrayList<>(numbers.subList(0, k));
Collections.sort(topNumbers, Collections.reverseOrder());
return topNumbers;
}
private static int partition(List<Integer> numbers, int left, int right) {
int pivotIndex = left;
int pivotValue = numbers.get(pivotIndex);
swap(numbers, pivotIndex, right);
int storeIndex = left;
for (int i = left; i < right; i++) {
if (numbers.get(i) > pivotValue) {
swap(numbers, i, storeIndex);
storeIndex++;
}
}
swap(numbers, storeIndex, right);
return storeIndex;
}
private static void swap(List<Integer> numbers, int i, int j) {
int temp = numbers.get(i);
numbers.set(i, numbers.get(j));
numbers.set(j, temp);
}
}
4.使用BitMap算法
BitMap算法的思路是使用一个BitMap来记录每个数是否出现过,然后遍历BitMap,找出出现次数最多的k个数。
Java代码实现如下:
import java.io.*;
import java.util.*;
public class Top100NumbersByBitMap {
private static final int MAX_NUMBERS = 1000000000; // 最多处理10亿个数
private static final int MAX_TOP_NUMBERS = 100; // 找出最大的100个数
public static void main(String[] args) throws Exception {
// 生成随机数文件
generateRandomNumbersFile("numbers.txt", MAX_NUMBERS);
// 找出最大的100个数
List<Integer> numbers = readNumbersFromFile("numbers.txt");
List<Integer> topNumbers = findTopNumbersByBitMap(numbers, MAX_TOP_NUMBERS);
// 输出最大的100个数
System.out.println("Top " + MAX_TOP_NUMBERS + " numbers:");
for (int i = 0; i < MAX_TOP_NUMBERS; i++) {
System.out.println(topNumbers.get(i));
}
}
// 生成随机数文件
private static void generateRandomNumbersFile(String fileName, int count) throws Exception {
Random random = new Random();
BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
for (int i = 0; i < count; i++) {
writer.write(String.valueOf(random.nextInt()));
writer.newLine();
}
writer.close();
}
// 从文件中读取数字
private static List<Integer> readNumbersFromFile(String fileName) throws Exception {
List<Integer> numbers = new ArrayList<>();
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line;
while ((line = reader.readLine()) != null) {
numbers.add(Integer.parseInt(line));
}
reader.close();
return numbers;
}
// 使用BitMap算法找出最大的k个数
private static List<Integer> findTopNumbersByBitMap(List<Integer> numbers, int k) {
int[] bitMap = new int[Integer.MAX_VALUE / 32 + 1];
for (int number : numbers) {
int index = number / 32;
int bit = number % 32;
bitMap[index] |= (1 << bit);
}
List<Integer> topNumbers = new ArrayList<>();
while (topNumbers.size() < k) {
int maxCount = 0;
int maxNumber = 0;
for (int i = 0; i < bitMap.length; i++) {
for (int j = 0; j < 32; j++) {
if ((bitMap[i] & (1 << j)) != 0) {
int number = i * 32 + j;
int count = countNumberInList(numbers, number);
if (count > maxCount) {
maxCount = count;
maxNumber = number;
}
}
}
}
topNumbers.add(maxNumber);
removeNumberFromList(numbers, maxNumber);
}
return topNumbers;
}
private static int countNumberInList(List<Integer> numbers, int number) {
int count = 0;
for (int n : numbers) {
if (n == number) {
count++;
}
}
return count;
}
private static void removeNumberFromList(List<Integer> numbers, int number) {
for (Iterator<Integer> iterator = numbers.iterator(); iterator.hasNext();) {
if (iterator.next() == number) {
iterator.remove();
}
}
}
}
以上四种算法都可以用来解决从100亿个整数的文件中找出其中最大的100个数的问题。
其中,分治法和BitMap算法适用于分布式环境下的数据处理,
而堆排序算法和快速选择算法则适用于单机环境下的数据处理。
说在最后:
在尼恩的(50+)读者社群中,很多、很多小伙伴需要进大厂、拿高薪。
尼恩团队,会持续结合一些大厂的面试真题,给大家梳理一下学习路径,看看大家需要学点啥?
前面用2篇文章,给大家介绍了大厂面试真题的知识要点:
《字节狂问1小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
这些面试真题,都会收入到 史上最全、持续升级的 PDF电子书 《尼恩Java面试宝典》。
本文题目以及参考答案,收入咱们的 《尼恩Java面试宝典》V71版本,可以找尼恩领取,暗号:领电子书
基本上,把尼恩的 《尼恩Java面试宝典》吃透,大厂offer很容易到滴。
另外,下一期的 大厂面经大家有啥需求,可以发消息给尼恩。
技术自由的实现路径 PDF:
实现你的 架构自由:
《吃透8图1模板,人人可以做架构》
《10Wqps评论中台,如何架构?B站是这么做的!!!》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《100亿级订单怎么调度,来一个大厂的极品方案》
《2个大厂 100亿级 超大流量 红包 架构方案》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
《响应式圣经:10W字,实现Spring响应式编程自由》
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
《Linux命令大全:2W多字,一次实现Linux自由》
实现你的 网络 自由:
《TCP协议详解 (史上最全)》
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
《Redis分布式锁(图解 - 秒懂 - 史上最全)》
《Zookeeper 分布式锁 - 图解 - 秒懂》
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《缓存之王:Caffeine 的使用(史上最全)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》
实现你的 面试题 自由:
4000页《尼恩Java面试宝典 》 40个专题
以上尼恩 架构笔记、面试题 的PDF文件更新,▼请到下面【技术自由圈】公号取 ▼