快手商业化 Java后端 二面|面试官很nice
面试总结:没有那种纯八股问题,都是偏向于情景题。看到面试官最后出了一道多叉树的题目,我以为是想直接刷人,但还是尽力去尝试了一下,最后也没做出来,面试官很nice,在答不上来的时候会引导我去思考,并在我回答正确的时候给与充分的肯定。
1.线程池工作过程
线程池的工作过程主要包括以下几个步骤:
(1) 线程池创建时会初始化一定数量的核心线程,等待任务。
(2) 当有新任务提交时,会首先判断当前运行的线程数是否小于核心线程数。如果小于,则创建新的线程来执行任务。
(3) 如果当前运行的线程数大于或等于核心线程数,则将任务放入队列中等待执行。
(4) 如果队列已满,且当前线程数小于最大线程数,则创建新的线程来执行任务。
(5) 如果队列已满,且当前线程数等于最大线程数,则执行拒绝策略。
(6) 当线程完成任务时,会从队列中取出新的任务来执行。
(7) 当线程空闲时间超过keepAliveTime,如果当前运行的线程数大于核心线程数,则这些线程会被停止。
2.线程回收机制
线程池的线程回收机制主要依赖于线程池的工作队列和keepAliveTime参数:
(1) 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直到线程池中的线程数不超过corePoolSize。
(2) 如果设置了allowCoreThreadTimeOut(true),则核心线程在空闲时间超过keepAliveTime后也会被回收。
(3) 线程池使用一个HashSet存储工作线程的引用,在工作线程退出时,它会被从线程池中移除。
(4) 在ThreadPoolExecutor的getTask()方法中,如果线程获取任务时超时,说明该线程可以被回收。
(5) 线程回收的过程是通过线程自己执行完run方法来实现的,而不是由外部强制中断。
这种机制可以在执行大量短期异步任务时,提高线程的复用性,避免频繁创建和销毁线程带来的开销。
3.为什么标记核心线程和非核心线程
区分核心线程和非核心线程主要有以下几个原因:
(1) 资源管理:核心线程代表了线程池在空闲时刻的最小线程数,可以保证线程池的快速响应能力。
(2) 性能优化:核心线程通常不会被回收,可以减少线程创建和销毁的开销。
(3) 弹性伸缩:通过调整核心线程数和最大线程数,可以实现线程池的动态伸缩,适应不同的负载情况。
(4) 任务优先级:核心线程可以优先处理任务,非核心线程则在任务量增大时才会被创建。
(5) 资源控制:可以通过控制核心线程数来限制系统资源的使用,避免创建过多线程导致系统负载过高。
这种设计使得线程池可以在保证基本性能的同时,根据实际负载情况动态调整,既保证了响应速度,又避免了资源浪费。
4.数据库相关的查询优化
数据库查询优化是一个复杂的话题,主要包括以下几个方面:
(1) 索引优化:
- 为常用查询字段创建适当的索引
- 避免过多索引,可能影响插入和更新性能
- 使用复合索引时注意最左前缀原则
- 避免在索引列上使用函数或进行运算
(2) SQL语句优化:
- 避免使用SELECT *,只查询需要的字段
- 使用EXPLAIN分析SQL执行计划
- 善用LIMIT语句来限制结果集大小
- 使用合适的JOIN方式(INNER JOIN, LEFT JOIN等)
(3) 表结构优化:
- 选择合适的数据类型,如使用UNSIGNED INT代替INT
- 适当进行表的垂直拆分或水平拆分
- 使用合适的存储引擎(如InnoDB支持事务,MyISAM适合只读场景)
(4) 架构优化:
- 使用读写分离
- 采用分库分表策略
- 使用缓存层(如Redis)减轻数据库压力
在实际优化中,需要根据具体的业务场景和查询模式来选择合适的优化策略。
5.Redis主从集群相关问题
(1) 主从复制机制:
- 全量复制:slave首次连接master时,master会生成RDB文件发送给slave
- 增量复制:master将新的写命令异步发送给slave
- 断点续传:当主从连接断开后,可以从断点处继续复制
(2) 数据一致性问题:
- Redis采用异步复制,可能存在数据不一致的情况
- 可以通过wait命令来实现同步复制,但会影响性能
(3) 故障转移:
- 使用Sentinel进行自动故障检测和转移
- Sentinel通过投票机制选举新的master
(4) 读写分离:
- 可以将读请求分发到slave节点,减轻master压力
- 需要考虑数据一致性问题,可能读到旧数据
(5) 数据过期问题:
- 主节点负责key的过期删除
- 从节点不会主动删除过期key,而是等待主节点的DEL命令
(6) 数据持久化:
- 主节点通常配置AOF持久化,从节点可以只配置RDB持久化
- 需要权衡数据安全性和性能
在实际应用中,需要根据业务需求和系统规模来设计合适的Redis集群架构。
7.项目中比较精彩的点
缓存优化的考量
(1) 缓存预热:
- 系统启动时,提前将热点数据加载到缓存中
- 可以通过定时任务或者手动触发来实现
(2) 缓存更新策略:
- 实现缓存和数据库的双写一致性
- 可以采用先更新数据库,再删除缓存的策略
- 使用消息队列来保证最终一致性
(3) 缓存穿透优化:
- 对不存在的key也缓存null值,并设置较短的过期时间
- 使用布隆过滤器来快速判断key是否存在
(4) 缓存击穿优化:
- 对热点key使用互斥锁或分布式锁
- 实现请求合并(hystrix request cache)
(5) 缓存雪崩优化:
- 给缓存的失效时间添加随机值
- 实现熔断机制,当缓存失效量突增时,暂停使用缓存
(6) 缓存降级:
- 当缓存服务不可用时,直接返回默认值或旧数据
- 实现服务降级,保证核心功能可用
8.缓存优化的细节
针对缓存优化的细节,可以从以下几个方面深入讨论:
(1) 缓存key的设计:
- 使用有意义的前缀,便于管理和查找
- 考虑key的长度,过长的key会增加内存使用
- 对于复杂对象,可以使用哈希算法生成key
(2) 缓存粒度:
- 细粒度缓存:缓存具体字段,减少缓存内容,但增加缓存操作次数
- 粗粒度缓存:缓存整个对象,减少缓存操作,但可能缓存无用数据
(3) 缓存有效期:
- 根据数据更新频率设置合适的过期时间
- 对不同类型的数据设置不同的过期策略
- 考虑使用滑动过期时间(每次访问都刷新过期时间)
(4) 缓存预加载:
- 识别热点数据,在系统启动时预加载
- 实现智能预加载,根据访问模式动态调整预加载策略
(5) 缓存更新机制:
- 实现缓存更新的重试机制
- 使用分布式锁来保证并发更新的正确性
- 考虑使用异步更新来提高性能
(6) 缓存数据一致性:
- 实现最终一致性策略,如延迟双删
- 使用版本号机制来处理并发更新
- 考虑使用
Canal
等工具实现数据库和缓存的同步
这些细节需要在实际应用中不断调优和验证,以达到最佳的性能和可靠性。
9.设计消息队列(MQ)
设计一个消息队列系统需要考虑很多方面,以下是一些关键点:
(1) 消息存储:
- 使用高性能的存储引擎,如
RocksDB
或自研的LSM
树存储引擎 - 实现分段日志(
Log Segment
)存储方式,便于清理过期数据 - 考虑内存映射文件(
Memory Mapped File
)提高I/O性能
(2) 高可用性:
- 实现主从复制机制,保证数据不丢失
- 设计
Raft
或Paxos
等一致性协议,实现leader选举 - 实现故障自动转移(
Failover
)机制
(3) 高吞吐量:
- 使用零拷贝(
Zero-Copy
)技术减少数据复制 - 实现批量发送和消费机制
- 使用异步I/O提高并发性能
(4) 消息可靠性:
- 实现消息确认(
ACK
)机制 - 支持消息重试和死信队列
- 实现幂等消费,处理重复消息
(5) 扩展性:
- 设计分区(
Partition
)机制,支持横向扩展 - 实现动态扩缩容能力
- 支持多数据中心部署
(6) 消息顺序性:
- 实现分区内的顺序消费
- 支持全局顺序消费的特性
(7) 消息模型:
- 支持发布/订阅模型
- 支持点对点队列模型
- 考虑支持延迟队列、优先级队列等特性
(8) 客户端SDK:
- 设计简单易用的API
- 实现自动重连、负载均衡等特性
- 支持多种编程语言
实际情况还需要根据具体需求和场景进行取舍和优化。
11.使用消息队列的好处
(1) 异步处理:
- 将耗时操作异步化,提高系统响应速度
- 实现生产者和消费者的解耦,提高系统灵活性
(2) 流量削峰:
- 缓冲突发流量,保护后端系统
- 实现请求的平滑处理,提高系统稳定性
(3) 解耦系统:
- 降低系统间的直接依赖
- 提高系统的可扩展性和可维护性
(4) 扩展性:
- 易于横向扩展,提高系统处理能力
- 支持动态扩缩容,适应业务变化
(5) 可靠性投递:
- 保证消息至少被消费一次
- 支持消息重试机制,提高系统容错能力
(6) 顺序保证:
- 在特定场景下保证消息的顺序性
- 满足某些业务的顺序处理需求
(7) 流量控制:
- 实现背压(back-pressure)机制
- 控制消息生产和消费的速率,保护系统
(8) 事务消息:
- 支持分布式事务
- 保证跨系统操作的一致性
12.Redis使用场景
(1) 缓存:
- 热点数据缓存,减轻数据库压力
- 页面缓存,提高网站访问速度
(2) 计数器:
- 高并发计数,如文章阅读量、点赞数
- 限流计数,实现接口访问频率控制
(3) 分布式锁:
- 实现跨进程、跨服务器的互斥操作
- 控制并发访问,保证数据一致性
(4) 会话存储:
- 存储用户会话信息,实现分布式 Session
- 提高系统可扩展性,便于水平扩展
(5) 排行榜:
- 使用Sorted Set实现实时排行榜
- 高效的数据结构,支持大规模数据
(6) 消息队列:
- 利用List实现简单的消息队列
- 适用于轻量级的异步任务处理
(7) 分布式ID生成:
- 利用INCR命令实现全局唯一ID
- 高性能,适用于高并发场景
(8) 布隆过滤器:
- 使用Bitmap实现布隆过滤器
- 用于大规模数据的快速判重
(9) 延迟队列:
- 利用Sorted Set实现延迟任务
- 用于定时任务、订单超时处理等场景
13.Redis实现分布式锁
Redis
实现分布式锁的基本步骤如下:
(1) 获取锁: 使用 SETNX
命令尝试设置一个键值对,如果键不存在则设置成功并获得锁:
(2) 设置过期时间: 为了防止客户端崩溃导致锁无法释放,需要设置过期时间
(3) 执行业务逻辑
(4) 释放锁: 使用 Lua
脚本保证原子性,只有持有锁的客户端才能释放锁:
注意事项:
- 使用唯一值标识锁的持有者,防止误释放
- 设置合理的过期时间,避免死锁
- 考虑使用
Redisson
等成熟的实现,它们提供了自动续期等高级特性
这种实现方式适用于简单场景,但在高并发或对可靠性要求较高的场景下,建议使用Redisson等专业的分布式锁实现。
15.普通 Redis 锁与 Redisson 的区别
Redisson 是一个在 Java 客户端上使用 Redis 的工具库。它提供了一个易于使用且强大的一套 API,来简化分布式系统的开发。Redisson 提供了多种分布式锁机制,比如公平锁、读写锁、红锁等,适合各种不同的场景,帮助开发者解决并发控制问题。
redis 锁的局限性
- 单Redis实例:
- 这种锁机制在单一 Redis 实例环境下工作良好,但在集群环境下可能会面临问题。
- 没有自动续期:
- 锁的持有者必须自己管理锁的续期,以防锁被意外释放。
- 原子性限制:
- 在解锁时需要小心,因为
DEL
命令在分布式系统中可能会删除不是自己设置的锁。
- 在解锁时需要小心,因为
Redisson 锁
- 更高层次的抽象:
- Redisson 提供了类似 Java 原生
java.util.concurrent.locks.Lock
接口的抽象,让使用锁变得更加自然和简单。
- Redisson 提供了类似 Java 原生
- 内置功能:
- 公平锁、读写锁、可重入锁、红锁(一种分布式环境下的锁)等多种锁实现。
- 内置续期机制:锁的持有者可以自动续期,防止锁误释放。
Redisson 锁的优势
- 适用性广:
- 同时适用于单实例和分布式集群环境,可以处理更复杂的分布式锁需求。
- 简化开发:
- 提供了接口级别的锁实现,更易于集成到现有项目中,并支持自动续期和更复杂的锁策略。
- 高可用性:
- 支持 Redis 的集群、哨兵模式和主从模式,提供高可用性。
16.Redission 相关问题
(1) 是否会产生死锁
Redisson 锁在设计上考虑到避免死锁。它通过设置锁的过期时间和自动续期机制来防止持有锁的节点意外崩溃或长时间失去响应从而导致死锁。
(2) Redisson 锁如何实现
- 获取锁: 使用 Redis 的
SET
命令加上NX
(不存在时设置)和PX
(过期时间)参数来实现原子操作,从而确保只有一个客户端能够成功获得锁。 - 释放锁: Redisson 使用 Lua 脚本来确保释放锁操作的原子性,即只有持有锁的客户端才能释放锁。
- 自动续期: Redisson 提供了 Watchdog(看门狗)机制,一旦锁被某个客户端获取,Redisson 会启动一个后台线程,定期延长锁的过期时间,避免锁因为超时而被自动释放。
(3) 过期时间的作用及默认值
过期时间的作用是防止在锁持有者发生意外崩溃或长时间失去响应时,锁一直存在而导致其他客户端无法获得锁。过期时间确保了锁最终会在一段时间后自动释放,从而避免死锁。
Redisson 默认的锁过期时间是 30 秒。这意味着如果没有特殊配置,Redisson 锁在持有之后,如果不进行操作,30秒后会自动释放。
(4) 自动续期机制
Redisson 的自动续期机制依靠一个叫做 Watchdog(看门狗)的后台线程来实现。当一个客户端成功获取到锁时,一个后台的 Watchdog 线程会启动,默认每隔10秒执行一次续期操作,将锁的过期时间延长一定周期,以此确保持有锁的客户端在执行长时间任务时不会因为锁的过期而失去锁。
#快手##面经##面试##软件开发2024笔面经##我的求职思考#