得物
1.concurrenthashmap底层存储结构
ConcurrentHashMap1.8底层数据结构是数组+链表+红黑树。由CAS+Synchronized实现线程安全。代替1.7的分段锁segement
put原理:
- 判断数组是否为null,是则CAS初始化。
- key计算得出hash值,得到要添加的数组位置
- 判断该位置节点是否为null。是则CAS添加。否则判断头节点hash是否是-1,是则表示正在扩容,当前线程参与扩容
- 头节点hash不是-1,就synchronized锁住头节点,添加数据
- 判断是否需要扩容
扩容实现
扩容方式
- 链表转为红黑树,但数组长度不足64
- putAll 操作,原数组放不下传入的map
- 执行add操作,达到阈值
流程
允许协助扩容
- 计算扩容标识戳(transferIndex)
- 扩容标识戳左移16位,代表是第一个进来扩容的线程
- 线程开始扩容后,计算每个线程迁移的长度,最小值是16
- 计算好后构建新数组
- 线程开始领取任务,从多少索引位置迁移到多少索引位置
- 开始扩容,迁移数据,从老数组迁移到新数组
- 迁移完成后将原位置的hash设置为ForwardingNode节点。告知此位置的其它线程,此节点迁移完毕
- 线程2访问到ForwardingNode节点,如果它执行的是put或remove等写操作,就会帮其扩容。如果是get等读方法,会调用ForwardingNode的find方法,去nextTable里找相关元素
2.threadLocal
线程本地变量,线程隔离。
线程中维护了ThreadLocalMap属性,它是ThreadLocal的内部类,key是ThreadLocal,它是弱引用。value是线程本地变量。
内存泄露问题
ThreadLocal被回收 && 线程复用(线程池)&& 没有调用set、remove、get方法
3. redis怎么使用的?
- String类型,缓存数据、限流(发送短信次数)、分布式锁
- hashMap类型,缓存用户数据,方便存取用户信息
- Zset类型,延迟队列
- score为延迟时间,zadd score value 生产消息
- zRangeByScore查询符合条件的待处理的任务,轮询处理
- 原理
- 内部使用HashMap和跳跃表来保证数据的存储和有序。
- hashMap里放的是成员到score的映射
- 跳跃表存的则是所有成员
- 排序依据是hashMap里存的score
- 使用跳跃表可以获得比较高的查找效率,且实现比较简单
4.索引结构
索引:提高查询效率的数据结构
B+树
- 一个父节点可以多个子节点
- 树的高度最高为3层,降低io
- 查询性能稳定
- 非叶子节点不存储数据,叶子节点存储数据
- 扫表能力强,如果全表扫描,只用扫描叶子节点,无需遍历整棵树
- 效率、io稳定,每次都只在叶节点拿到数据
- 磁盘读写能力强,非叶子节点不存储数据,一个节点就能保存更多关键字,一次磁盘io就能加载更多关键字。io次数少
- 叶子节点之间使用双向链表
- 支持范围查找
- 排序能力强,叶子节点有指向下一个数据区的指针,数据形成链表
5.覆盖索引、聚簇索引 - 覆盖索引
- 一个涵盖了查询语句中的需要查询的,以及查询条件的字段的索引。不需要回表
- 聚簇索引
- 根据表主键构建的一棵B树
- 同时叶子节点存放的就是整张表的行记录数据
- 一张表只能有1个聚簇索引
- 非聚簇索引
- 叶子节点存储的是指向数据的地址
6.MQ事务消息
rocketMq的事务消息主要是通过halfMessage和commit log两个组件实现的
- 叶子节点存储的是指向数据的地址
- half message:在发送方发送事务消息时,先将消息发送到broker上,并记录该消息为“半消息”。此时broker将会给这个半消息生成唯一事务id,并返回给发送者
- 保证消息的顺序性和唯一性
- 在发送方确认消息预提交成功后,即执行本地事务操作,然后根据预提交结果决定提交还是回滚
- 如果需要提交,发送者向broker发送commit,携带之前生成的唯一id和消息状态。 broker接收到请求后将已接收的半消息标记为可投递状态,并从commitlog中删除对应消息记录
- 半消息提交成功,会发送给消费者
- 如果需要回滚,则向broker发送rollback请求,broker接收到后将已接收的半消息删除,并向producer返回失败响应
- 如果broker一直未接收到发送方的事务执行状态怎么办?
- rocketMq有定时任务,扫描consumeQueue内部topic中的half消息,回调系统的接口,判断事务状态。最多回调15次
rocketMq事务消息底层逻辑就是通过在broker上创建和管理半消息,通过commit log 记录事务提交或回滚状态,保证消息的可靠性和事务最终一致性
事务消息相对普通消息最大特点:一阶段发送的消息对用户不可见,消费者不能直接消费。实现方法是:把消息的主题改成RMQ_SYS_TRANS_HALF_TOPIC,这样消费者没有订阅这个主题,就不会消费
半消息提交成功,就把消息放入目标(原)主题
7.如何设计一个延迟队列?
使用redis的zSet,用Zadd命令生产消息,key作为消息内容,score为时间戳,member为业务数据;使用zRangeByScore获取时间戳内的数据,轮询处理
8.分布式事务
- 二阶段提交
- 一阶段:准备阶段
- 协调者向参与者询问是否可以提交
- 参与者回复协调者ack
- 如果协调者未接收到参与者的回复,会向参与者发出回滚
- 二阶段:提交阶段
- 协调者接收到参与者回复,向参与者发出提交或回滚
- 参与者接收到协调者的请求,执行提交或回滚,回复ACK
- 协调者收到ACK,结束事务,释放占用的事务资源
- 优点:实现简单,强一致性
- 缺点
- 容易单点故障
- 整个过程中,所有参与者阻塞,占用着事务(连接),直到结束。其它需要访问公共资源的线程就会阻塞。因此二阶段不适合高并发场景
- 数据一致性问题
- 协调者发送commit时,遭遇局部网络异常或在过程中宕机,只有部分参与者接收到commit
- 一阶段:准备阶段
- TCC
- 针对每个操作都要有相应的T(Try)、C(commit)、C(cancel)
- 实现
- 事务协调者向参与者发起请求
- 参与者进行try。根据try的结果,进行commit或cancel
- commit和cancel会重试,需要做好幂等
- 优点
- 性能高,具体业务实现,控制资源的锁粒度变小,不是锁定整个资源
- 可靠性高
- 缺点
- 代码侵入性强
- 适合银行金融等场景
- 本地事务表
- 将分布式事务拆成本地事务进行处理
- 事务主动方
- 需要建立额外的事务消息表,在本地完成业务处理,并记录事务消息
- 轮询事务消息表的数据发送事务消息
- 事务被动方
- 基于消息中间件消费消息表中的数据
- 优点
- 弱化对消息中间件的依赖,实现简单
- 缺点
- 与具体业务绑定,耦合高,不能复用
- mq事务消息
- 通过在broker上创建和管理半消息,通过commit log记录事务提交或回滚状态,保证消息的可靠性和事务最终一致性
- 最大努力通知
- 主动方有重试机制,以及提供查询接口
9.一个接口3000qps,接口RT为200ms,预估需要几台机器?(每台机器为4核CPU)
首先,每秒需要处理的请求数(QPS)与请求响应时间(RT)可以计算出每台机器需要处理的并发请求数(CPS):
CPS = QPS * (RT / 1000) = 3000 * (200 / 1000) = 600
然后,我们需要计算每台机器的处理能力。由于每台机器为4核CPU,并且没有给出单个核心的处理能力,因此我们使用一个经验公式来估计每台机器的处理能力:
处理能力 = CPU核数 * 100
因此,每台机器的处理能力为 4 * 100 = 400。
最后,我们可以通过将每台机器的处理能力除以每台机器需要处理的并发请求数,来计算需要的机器数量:
机器数量 = (每秒请求数 / 每台机器处理能力) 向上取整
机器数量 = ceil(600 / 400) = 2
因此,预估需要2台机器来处理3000qps,接口RT为200ms的请求。
华为
10.分布式id实现方式,如何解决时钟回退问题
- 雪花id
- 实现
- 41bit是时间戳
- 10bit表示机器id
- 12bit是自增序列号
- 原理
- 时间戳+机器id+序列号生成的64bit的id
- 优点
- 整体趋势递增,全局唯一
- 可不依赖数据库等第三方系统,以服务方式部署。稳定性高,生成的id性能高
- 可根据自身业务特性分配bit位,灵活
- 缺点
- 强依赖机器时钟。若时钟回拨,会导致发号重复或服务处于不可用状态
- id生成器使用方式
- 发号器
- 封装成一个服务。部署在多台服务器,由外界请求发号器服务获取id
- 缺点
- 需要远程请求获取id。会受到网络波动
- 本地生成
- 没有网络延迟,性能高,通过机器id保证唯一
- 需要提供足够多的机器id
- 每个服务部署在多台机器,需要分配不同机器id
- 服务重启需要重新分配机器id,这样机器id就有用后即毁的特点
- 发号器
- 时钟发生回拨怎么办
- 不依赖机器时钟驱动,定义一个初始时间戳。在初始时间戳上自增,不跟随机器时钟增加
- 当序列号增加到最大时,此时时间戳+1
- 缓存历史序列号缓解时钟回拨问题
- 被浪费的时间戳,每个毫秒数的序列可能只用了很少一部分。时间回退时可以利用这部分
- 缓存2000ms的序列号,如果发生时钟回拨,且回拨范围在2000ms内,就从缓存中取序列号自增。
- 在内存中建立一个数组,这个数组设定固定长度,存储上次该位置对应的毫秒数的messageId。
- 如果时间回退,就只用在上一次messageId进行+1操作,直到系统时间被追回。
- 如果超过了历史序列号,就取下一个时间戳,重试获取
- 实际生产中限定重新获取次数
- 如果时钟恢复正常,此时要退出循环
- 不依赖机器时钟驱动,定义一个初始时间戳。在初始时间戳上自增,不跟随机器时钟增加
- 实现
11.redis expire底层实现
Redis 的 expire 命令用于为一个键设置过期时间,即在指定时间之后自动删除该键。底层实现原理是使用一个叫做过期字典(expires dict)的数据结构,其中每个键都映射到它的过期时间。
redis过期删除策略
- 定期删除: Redis 会定期(默认每秒钟)扫描过期字典,删除已经过期的键。
- 惰性删除:此外,当对一个键进行读或写操作时,Redis 也会检查该键是否已过期,并在必要时删除它。通过这种方式,Redis 可以高效地管理键的过期并保证内存的使用效率。
12.redis setnx底层实现
Redis 的 SETNX 命令用于将一个键值对(key-value pair)设置到 Redis 中,但只有在该键不存在时才会进行设置。底层实现原理是使用了 Redis 的基础数据结构之一——字典(dictionary)。当调用 SETNX 命令时,Redis 会先检查要设置的键是否已经存在于字典中。
如果键不存在,则会将键值对添加到字典中,并返回 1 表示设置成功;否则不做任何操作,并返回 0 表示设置失败。
这个操作可以保证对于同一个键,SETNX 命令只会执行一次设置操作,避免了并发设置导致的竞争问题。
中国银行
1.redis实现分布式锁
Redis 可以通过使用 SETNX(SET if Not eXists)命令和 EXPIRE 命令来实现分布式锁。具体步骤如下:
在 Redis 中创建一个键,作为锁标识。
使用 SETNX 命令尝试将锁标识设置到 Redis 中。如果 SETNX 返回 1,则表示成功获取锁;如果返回 0,则表示锁已经被其他客户端持有,获取锁失败。
如果成功获取锁,则可以使用 EXPIRE 命令为该键设置过期时间,以防止锁一直被某个客户端持有,导致其他客户端无法获取锁。
同时需要设置客户端唯一标识,防止被其他客户端误释放
执行业务代码逻辑。
释放锁时,需要使用 DEL 命令将锁标识从 Redis 中删除,让其他客户端可以继续获取锁。
需要注意的是,由于分布式环境中存在网络延迟、进程崩溃等异常情况,因此在使用分布式锁时需要特别关注死锁和重入问题,并采取相应的措施进行处理。例如,在 SETNX 命令执行失败后,可以加入重试机制,或者使用 Lua 脚本保证 SETNX 和 EXPIRE 命令的原子性操作
2.哪些产品用到多线程?
3.面对多个线程竞争资源,如何处理
4.用过哪些设计模式
5.单例模式,哪些场景用?为什么要用?
6.sql如何调优
7.项目中哪里用到MQ
8.谈谈你对Spring cloud的理解
9.有对redis做持久化吗?怎么用的
10.对linux有了解吗
中电鸿信信息科技
1.乐观锁、悲观锁、共享锁、独占锁
2.说一说对微服务的理解
3.介绍微服务组件还有在项目中怎么用
4.排序算法
5.redis缓存穿透、击穿、雪崩
6.hashMap线程安全吗?想让他线程安全,要怎么解决?
7.写一个sql语句查询200W后的10条数据(深分页)
8.问项目业务上用技术怎么实现
兴盛优选
1.JVM相关知识点
2.集合框架原理
3.微服务相关组件
4.缓存相关知识
5.个人项目经验,遇到的困难和挑战
数字马力(杭州)信息技术有限公司
- hashMap数据结构,1.7和1.8区别
- 哪些java集合支持并发操作,是如何控制的?
- redis的跳表结构如何理解
- 如何理解mvcc
- 工作中有没有遇到印象深刻的问题,怎么分析解决的
其它
1.mysql索引b+树节点存的是什么?
2.redis set、zSet的数据结构什么时候会使用哈希表
set的元素数量小于512时使用整数集合,否则使用hash;
zSet的元素数量小于128个,且每个元素大小小于64字节时,使用哈希;否则使用跳表,使用跳表目的是查询效率高
3.线程池参数能够运行时动态调整吗?
能。
可以将线程池的参数(核心线程数、最大线程数,阻塞队列长度)迁移到分布式配置中心上,实现线程池参数可配置和即时生效
1)为什么需要动态设置?
因为存在流量不均衡的情况
2)动态更新的工作原理
- setCorePoolSize
- 在运行期间线程池调用此方法修改核心线程的数量,线程池会直接覆盖原来的值
- 并且基于当前值和原始值的比较结果,采取不同策略
- 如果当前值小于工作线程数,证明有多余工作线程,此时会向多余线程发起中断请求,实现回收
- 当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程执行队列任务
- setMaxmiunPoolSize
- 校验参数合法性
- 覆盖原来的值
- 判断工作线程是否大于最大线程数,是则对空闲线程发起中断请求
3)动态设置需要注意的点
获取任务时,当工作线程大于最大线程数,就会对工作线程数-1,并返回空,那么就没有获取到任务。一加一减线程,相当于失效;
解决:设置核心线程数的时候,同时设置最大线程数。
如果低峰期怎么办?当参数allowCoreThreadTimeOut设置为true时,空闲核心线程也会被回收。相当于线程池自动动态修改;
4)动态指定队列长度
LinkBlockingQueue的capacity是final修饰的。无法修改。那我们可以自己定义一个queue,可以修改capacity。
这道题涉及的线程池问题
1)线程池被创建后,就有线程吗?如果没有,可以对线程池进行预热吗?
线程池被创建后,没有任务时是没有线程的。预热有两个方法
- 全部启动:prestartAllCoreThreads()方法
- 仅启动一个:prestartCoreThread()方法
2)核心线程数会被回收吗需要什么设置?
默认是不能的。如果需要回收,则调用allowCoreThreadTimeOut(boolean value)