1. 介绍下ConcurrentHashMap的数据结构?
① JDK7
- ConcurrentHashMap由一个Segment数组构成(默认长度16),Segment继承自ReentrantLock,所以加锁时Segment数组元素互不影响,可实现分段加锁,性能高。
- Segment本身是一个HashEntry链表数组,所以每个Segment相当于是一个HashMap。

② JDK8+
- 为提升存取效率,摒弃Segment,使用Node数组+链表/红黑树的数据结构。
- Node和HashEntry的作用一样,但把值和next采用了volatile修饰,保证了可见性;同时,引入了红黑树,元素多时,存取效率高。
- 并发控制使用 synchronized + CAS 实现,整体看起来像是线程安全的JDK8 HashMap。

2. 介绍下HashMap put元素的详细过程?
① 计算Key的hash值:hash = (key==null) ? 0 : (h=key.hashCode()) ^ (h>>>16)
② 计算Key的数组index值:index = hash & (length – 1)
③ 如果有一样K,则覆盖V;如果没有一样K,JDK7采用头插法(认为刚添加的元素被访问的概率更大),但会引入循环引用问题,导致CPU高,JDK8采用尾插法,避免了这个问题。
④ 如果元素个数超过阈值,进行扩容,可能涉及数据结构变更(链表 → 红黑树)。

3. 介绍下线程池的工作原理?
① 向线程池提交任务
② 如果核心线程池没满,则创建核心线程执行任务
③ 如果核心线程池已满,但等待队列没满,则加入等待队列
④ 如果核心线程池已满,等待队列已满,但没达到最大线程数,则创建非核心线程执行任务
⑤ 如果核心线程池已满,等待队列已满,达到最大线程数,执行抛弃/拒绝策略

4. synchronized和ReentrantLock的区别?
java实现加锁主要有2种方式:synchronized 和 ReentrantLock
① 实现原理不同
(1) synchronized原理
- synchronized是java的1个关键字,底层由JVM实现加锁。
- synchronized 关键字编译后,会在同步块前后生成 monitorenter 和 monitorexit 两个字节码指令,作用是获取和释放对象的锁。
- 修饰方法,锁则是对象;修饰静态方法,锁的是当前类的Class实例;修饰代码块,锁的是传入synchronized的对象。
public synchronized void fun() {}
public static synchronized void fun() {}
synchronized(obj) {}
(2) ReentrantLock的原理
- ReentrantLock底层使用 CAS + AQS 队列来实现加锁,使用 lock()方法加锁,unlock()解锁。
- 当线程调用lock()方法,如果锁没被任何线程占用,则当前线程获取到锁,然后设置锁的拥有者为当前线程,并设置AQS的状态值为1;如果当前线程之前已获得该锁,则只把AQS的状态值加1;如果锁被其他线程持有,则线程会被放入AQS队列后阻塞挂起。
volatile ReentrantLock lock = new ReentrantLock(); lock.lock();
try {
//xxx
} finally {
lock.unlock();
}
② 是否公平锁
- 公平锁:先来先得,按照申请锁的顺序去获得锁
- synchronized为非公平锁
- ReentrantLock可以选择公平非公平,通过构造方法传入boolean值进行选择,默认false非公平,true为公平。
③ 是否可主动释放锁
- synchronized 不需要手动释放锁,优点是不会忘记释放锁,缺点是无法干预锁,只能等JVM释放。
- ReentrantLock需要手动释放锁,优点是灵活,不需要一直阻塞等待,缺点是可能忘记释放锁,导致死锁。
④ 锁是否可中断
- synchronized不可中断
- ReentrantLock可调用interrupt()方法进行中断,更加灵活。
如何选择?
如果对公平、中断等有特殊诉求,可以选ReentrantLock;否则都可以使用synchronized,JVM一直在对synchronized进行优化,性能不差。
5. volatile的作用和原理?
① 可见性

- 上图为Java内存模型JMM,它规定所有变量都存储在主内存,每个线程有自己的工作内存,其中保存了主内存变量的副本。
- 当变量被 volatile 修饰,变量发生修改时,值会立即更新到主内存,其他线程可以及时在内存中读取到最新值。
- 可见性原理:volatile变量转译为汇编代码后,会多出一条Lock前缀的指令,它会触发CPU的缓存一致性协议,可以保证线程内的修改会及时同步到主内存。
② 有序性
- 编译器和处理器为了优化程序性能而对指令序列进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
- 通过对volatile修饰的变量的读写操作前后加上各种特定的内存屏障,来禁止指令重排序来保障有序性的。

③ 不保证原子性
- 原子性操作就是一个操作或多个操作要么执行都成功,要么执行都失败,不可中断,不存在中间状态。volatile不能保证原子性,所以多线程下存在线程安全问题。
- synchronized虽然能实现原子性,但锁的性能较差,所以我们可利用 volatile的可见性 + CAS 形成一种高性能的无锁机制,也能保证线程安全。
6. JVM调优的方法有哪些?
① 内存调优:调整-Xms 和 -Xmx 参数,即初始堆和最大堆大小;调整-Xmn或-XX:NewRatio参数,即新生代与老年代的比例;调整-XX:SurvivorRatio参数,即Eden区和Survivor区的比例。
② 垃圾回收调优:使用-Xloggc:<file-path>参数 分析GC日志,了解GC事件的频率和持续时间,以及内存分配和回收的模式。
③ 性能监控和分析:使用性能监控工具,如 JConsole,对 JVM 运行时进行监控和分析,发现性能瓶颈和问题点。
④ 调整线程栈大小:使用-Xss参数设置每个线程的栈大小,以减少内存使用或避免栈溢出错误。
⑤ 代码层面的优化:及时释放不需要的资源、避免创建大对象
7. 索引失效的场景有哪些?
① 使用联合索引时,没有遵循最左匹配原则,例如:创建了 (a, b, c) 联合索引,符合最左匹配原则的是:
where a = 1;
where a = 1 and b = 2;
where a = 1 and b = 2 and c = 3;
② 左like语句:”%ABC” 索引失效
③ 索引使用了函数:列如查询条件中对 name 字段使用了 LENGTH 函数
④ OR条件中有非索引:OR 的含义是两个只要满足一个即可,只要有一个条件列不是索引,就会进行全表扫描。
⑤ mysql认为全表扫描更快
8. 什么是回表?如何减少回表?
什么是回表?
select * from one_piece where name = “x”
通过name这个普通索引只能获取到该记录的主键id值为2,需要再到id索引树搜索一次。即先定位主键值,再定位行记录。
所以,基于非主键索引的查询需要多扫描一次索引树,回表操作会影响查询性能。
如何避免回表?覆盖索引包含了查询所需的所有列,不需要回表。
9. binlog、redo log、undo log的区别?

binlog:记录数据库的数据变更操作,以二进制的形式保存在磁盘中。使用场景:主从复制和故障恢复 。
redo log:事务操作执行时,会同时生成redo log,由于事务的数据先写入内存,再写入磁盘,当写入磁盘过程中出现数据库异常,可利用redo log 确保数据被持久化。
undo log:数据库事务开始前,会将修改先存放到 undo log,当事务回滚或数据库崩溃,可利用undo log 撤销操作。
10. 介绍下MySQL主从复制的作用和原理?
为什么需要主从复制?
- 主从复制是读写分离的前提,可提升性能
- 主从复制是故障切换的前提,提升可用性
主从复制的原理:

- 主记录binlog日志(记录所有修改操作)
- 从启动IO线程,读取主的binlog日志,写入relay log(中继日志)
- 从启动SQL线程,回放relay log
11. 如何提升数据库的查询速度?
① 定位缘由
- MySQL提供了 慢查询日志 功能,可以记录运行时间超过设定值的查询语句相关信息。一般,执行时常超过1s认为是慢
- 使用 EXPLAIN 查看 SQL 语句执行过程
- 也可以使用 慢SQL分析工具 来协助定位问题

② 索引优化
- 避免全表扫描,优先思考对where、order by的字段建立索引。
- 使用覆盖索引减少回表
- 避免索引失效的场景:联合索引没有遵循最左原则、左like、索引字段使用函数等
③ SQL语句优化
- 避免使用 SELECT * ,只查询需要的列
- 使用 LIMIT 分页查询
- 使用 join(inner join或left join)时,小表在前,大表在后,重复关联键少的表放在前面可以提高join的效率。
④ 数据库服务器配置优化:内存、连接池
⑤ 分库分表
12. 使用缓存时,有哪些常见的问题及其解法?
问题1:缓存穿透/缓存雪崩
如果访问数据库不存在的数据,会先缓存查一次,再数据库查一次,如果这类请求过多,会造成性能降低,甚至数据库崩溃。
解法:
- key的过期时间增加随机值,不会同时失效
- 对不存在的key,在缓存中置value为null,快速返回,但TTL不能太长,防止key真有数据录入了数据库。
- 布隆过滤器bloom filter:通过 bloom filter 判断 key 是否存在,如果不存在直接返回即可,无需查缓存。
- 请求做参数校验,过滤无效请求。
问题2:数据不一致
更新数据后,数据库和缓存中同1个key的value不一致。
解法:
- 数据库更新成功后立即删除缓存
- 缩短TTL,及时读取DB最新数据
问题3:HotKey/热key
同一时间大量请求访问特定key,打爆带宽,影响整体redis集群。
解法:
- 凭借业务经验、实时监控,及时识别HotKey,下发到客户端缓存,减少请求量。
- 采用redis集群部署,提升可用性。
问题4:BigKey
BigKey指一个K的value很大,会导致占用过多内存空间、处理时间长导致阻塞后续请求,影响整体redis集群性能。
解法:
- 对大Key进行拆分:将一个Big Key拆分为多个key-value这样的小Key,通过get不同的key或者使用mget批量获取。
- 对大Key进行清理:Redis提供UNLINK命令,能够以非阻塞的方式缓慢逐步的清理传入的Key,可以安全的删除大Key甚至特大Key。
- 压缩value:用压缩算法控制key的大小
13. 介绍下redis的数据持久化机制?
① RDB
RDB:Redis DataBase,是redis的一种持久化技术。执行流程:
- 执行bgsave命令,父进程fork创建子进程,子进程根据父进程指向的内存地址生成快照文件
- RDB可以在指定的时间间隔对数据进行快照存储,列如15分钟备份一次。
缺点:快照期间,如果父进程修改数据,子进程无法感知,可能存在数据不一致
② AOF
AOF:Append Only File,将redis的每一条写命令追加到磁盘文件appendonly.aof中,当 redis启动时,从AOF文件恢复数据。
AOF的同步策略:不保存、每秒保存一次(默认)、每执行一个命令保存一次(不推荐)
AOF的缺点:
- 频繁记录会对性能有必定的影响
- AOF文件大,修复速度也比 RDB 慢,所以redis的默认持久化配置是 RDB
③ 混合持久化模式
如何开启?redis.conf配置文件中 aof-use-rdb-preamble 参数设置为yes
混合持久化过程:
- 如果没有AOF文件,则加载 RDB文件
- 如果AOF文件开头为RDB格式,则加载 RDB 内容,再加载剩余 AOF 内容
- 如果AOF文件开头不是RDB格式,则以AOF格式加载整个文件
混合持久化的优点:RDB文件较小,读取快,但实时性差;AOF实时记录写操作,数据丢失少,但文件大,读取慢。混合持久化结合了RDB和AOF的优点,文件开头为RDB格式数据,使得Redis 可以更快启动,同时追加AOF格式数据,减低数据丢失风险。
14. 介绍下redis主从复制的原理?
2种形式:
- 全量复制:第一次主从同步时,会把所有数据以RDB形式同步给从库。
- 增量复制:之后每条命令,以增量形式同步给从库。

主从复制的作用:
- 主节点宕机后,可以从从节点切换到主节点,保证服务的可用性。
- 增加从节点可以提高读负载,减轻主节点的压力。
15. redis集群是如何保证各个节点的数据是一致的?
1. 槽分区:Redis集群将数据分成16384个槽,每个槽分配到不同的节点上,这样每个节点只需要处理自己分配到的槽,可以有效避免数据冲突的问题。当客户端向redis集群发送写操作时,该节点会先将操作转发给负责相应槽的主节点,主节点再将操作同步到所有从节点上,最后返回客户端。

2. 一致性hash算法:Redis集群中使用了一致性哈希算法来将槽映射到节点上,这样可以保证当集群中增加或减少节点时,只有部分槽需要重新分配,不会影响整个集群的数据一致性。
3. 主从复制:主节点会将自己的写操作同步到所有从节点上,从节点再执行一样的写操作。
4. 哨兵机制:Redis集群中的哨兵节点会监控主节点的健康状况,如果主节点故障了,哨兵会自动将某个从节点提升为主节点,这样可以保证集群中始终有可用的主节点。

16. 使用消息队列时,有哪些常见的问题需要思考,以及如何解决?
问题1:消息丢失
缘由:
- 消息的生产者没有成功发送消息到MQ Broker
- 消息发送到MQ Broker后,Broker宕机
- 消费者消费消息时出现异常
解决方案:
- MQ Broker收到消息后回复ACK,没有收到ACK可以再次发送消息
- MQ收到消息后,进行消息持久化,出现故障可恢复
- 消费者在处理完消息后手动返回ACK,MQ收到消费者ACK后再删除持久化的消息。
问题2:消息重复
缘由:
- 生成端:由于网络延迟,Broker没有收到消息,没有返回ACK,生产者会重新发送,最终Broker收到两条一样的消息。
- 消息端:消费者处理消息后,由于消费端异常或网络缘由,ACK没有返回Broker,消息没有被删除,会再次被消费。
解法:消息的处理逻辑设计为幂等性,列如根据消息内的数据,查询是否已存在消费记录。
问题3:消息积压
缘由:发送端或消费端性能不足,导致消息发送积压、发送消费积压。
解法:
- 提升发送性能:并发发送、批量发送
- 提升消费性能:Comsumer扩容、并发处理
17. 常用的负载均衡策略有哪些?

18. 介绍下一致性hash算法?
普通hash算法:hashcode % size,如果size发生变化,所有数据需要重新hash计算,代价大。
一致性hash算法主要应用在分布式系统,在增加或删除服务器节点时,能够尽可能小地改变已存在的服务请求与服务器之间的映射关系,解决普通hash算法在动态伸缩时的代价问题:
- 定义一个 0~2^32-1 的 hash环
- hash函数不按照服务器节点数量取模,而是按照 2^32 取模,这样请求会落在环上的某个固定位置。
- 服务器节点按照IP或域名进行hash函数,分配到hash环上。
- 请求通过hash函数计算,确定环上位置,沿顺时针遇到的第一个节点就是命中的服务器节点。

节点的增删,只会影响了系统中的一小部分数据,容错性超级好。增加节点E后,只需要变更原来指向C的部分请求。

但如果服务器节点太少,或出现热点数据,会导致请求分布不均匀。可引入虚拟节点,将每个实际节点映射到多个虚拟节点,虚拟节点可以在hash环上均匀分布。

19. ZK是如何保证各个节点的数据一致?

zk使用了zab协议,ZAB协议主要包括两个阶段:
1. Leader选举:所有节点都尝试成为Leader,最终只有一个节点成为Leader。
2. 原子广播:Leader将客户端请求以FIFO的顺序进行广播,所有Follower节点按照一样的顺序执行请求,并反馈给Leader。当大多数Follower节点成功执行后,Leader将请求执行结果广播给所有节点。
20. 如何设计一个秒杀系统?
要点1:负载瞬时流量
客户端:
- 前端防抖,避免大量重复请求
- 增加本地缓存,减少网络请求
网关层:
- 限流/熔断,保障系统可用
服务层:
- 分布式服务、分布式缓存
- 热点数据预热
- 消息队列异步处理
数据层
- 主从集群
- 分库分表
- 读写分离
要点2:避免超卖
- redis+DB,双库存设计
- redis对客,保证性能,使用redis lua脚本 或 redission 保证原子性
- 通过消息队列,异步同步数据到DB
lua脚本案例:
-- 调用get指令,查询活动库存
local c_s = redis.call('get', KEYS[1])
-- 判断活动库存是否充足,其中KEYS[2]是当前抢购数量
if not c_s or tonumber(c_s) < tonumber(KEYS[2]) then
return 0
end
-- 如果库存充足,则进行扣减操作。redis.call('decrby',KEYS[1], KEYS[2])
要点3:防刷风控
- 特征识别:频率异常、设备异常、账号异常
- 风险验证:滑块、短信
- 拦截请求
要点4:处理重复下单
- 一锁:redis分布式锁
- 二判:基于唯一ID、订单状态等,判断是否属于重复请求,业务服务要具备幂等性
- 三更新:执行更新操作
















- 最新
- 最热
只看作者