字节抖音电商 后端开发 一面面经
笔者整理答案,以供参考
自我介绍
项目(20分钟)
RocketMQ延时消息的底层实现
回答: 延时消息的实现主要依赖于RocketMQ中的定时任务机制。消息被发送到Broker时,会先存储在一个特定的延时消息队列中。Broker会定时扫描这个队列,当消息的延时时间到了,就会把消息投递到目标消费队列中。
消息量太大导致读消息延迟时间很长怎么办
回答: 可以通过以下几种方式优化:
- 消息分区: 将消息分区存储,分散到不同的队列中,减小单个队列的压力。
- 水平扩展: 增加消费者的数量,提高消费能力。
- 批量消费: 合理设置批量消费的大小,减少每次I/O操作的次数。
- 异步处理: 将耗时的操作放到异步任务中执行,减小消费时间。
项目还有啥优化
回答: 项目的优化可以从多个方面入手,比如:
- 数据库优化: 添加索引,进行SQL查询优化,使用分库分表等。
- 缓存优化: 使用合适的缓存策略,避免缓存穿透、缓存雪崩等问题。
- 代码优化: 进行代码重构,减少重复代码,优化算法和数据结构。
- 架构优化: 采用微服务架构,拆分单体应用,提升系统的可扩展性。
讲一讲Redis
回答: Redis是一种基于内存的高性能键值对存储数据库,常用于缓存、会话存储、排行榜等场景。它支持丰富的数据结构,如字符串、哈希、列表、集合、有序集合等,提供了持久化、事务、Lua脚本、复制、高可用和分区等功能。
Redis为什么快
回答:
- 内存存储: 数据全部存储在内存中,读写速度非常快。
- 数据结构优化: 使用了高效的数据结构,如字典、跳表等。
- 单线程模型: 避免了多线程竞争,不需要加锁,减少了上下文切换的开销。
- I/O多路复用: 采用了epoll模型,可以同时处理大量客户端请求。
单线程模型有什么缺点,有什么不太适用的场景
回答: 缺点:
- CPU利用率: 无法充分利用多核CPU,CPU密集型任务性能不佳。
- 阻塞操作: 如果有阻塞操作,会阻塞整个线程,影响性能。
不适用的场景:
- 复杂的计算任务: 无法利用多核CPU的优势。
- 大量阻塞操作: 需要频繁的I/O操作时,性能下降明显。
Redis的大key问题,为什么会产生大key
回答: 大key是指单个键对应的数据量非常大,可能是一个包含大量元素的集合或列表。产生大key的原因可能是:
- 数据设计不合理: 没有合理分片,导致数据集中在单个key下。
- 误用数据结构: 使用了不适合的数据结构,比如将大量数据存储在一个列表或哈希表中。
Redis怎么设置过期时间,底层是怎么实现的,有哪些过期删除策略
回答: 设置过期时间可以通过EXPIRE
命令,或者在设置键值时直接指定过期时间,如SET key value EX 10
。
底层实现:
- 定期删除: Redis会定期扫描设置了过期时间的键,删除已过期的键。
- 惰性删除: 当访问一个键时,如果发现它已经过期,则删除这个键。
过期删除策略:
- 定期删除: 定期扫描一部分键,删除过期的。
- 惰性删除: 访问时检查是否过期,过期则删除。
- 主动删除: 内存不足时,主动删除过期键,腾出空间。
普通索引、(a b c)联合索引,如果只通过b等值查询能走索引吗,如果用a和c呢
回答:
- 只通过b等值查询: 不能走索引,因为联合索引需要从第一个字段开始匹配。
- 通过a和c: 如果是组合查询,可以走索引a,但是单独通过c无法走索引。
线程池主要解决什么问题,有什么优点
回答: 线程池主要解决了线程的创建和销毁开销大、线程数量不受控的问题。优点包括:
- 提高性能: 通过复用线程,减少线程创建和销毁的开销。
- 资源管理: 可以控制并发线程的数量,避免资源耗尽。
- 任务管理: 可以统一管理和调度任务,提高系统的响应速度。
线程池的原理、来了一个任务后的处理流程
回答: 线程池的原理是通过复用固定数量的线程来执行任务,而不是每次都创建新线程。处理流程如下:
线程池参数
- corePoolSize:核心线程数,即线程池中始终保持存活的线程数量。
- maximumPoolSize:最大线程数,即线程池中允许的最大线程数量。
- keepAliveTime:线程的存活时间。当线程池中的线程数量超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
- unit:时间单位,
keepAliveTime
的时间单位。 - workQueue:任务队列,用于保存等待执行的任务。
- threadFactory:线程工厂,用于创建新线程。
- handler:拒绝策略,当任务无法执行时如何处理。
任务处理流程
- 提交任务: 当一个新任务通过execute方法提交到线程池时,线程池会根据当前线程数量和任务队列的状态决定如何处理这个任务。
- 核心线程处理: 如果当前线程数量少于核心线程数corePoolSize,则创建一个新线程来处理这个任务。
- 任务队列处理: 如果当前线程数量已经达到或超过核心线程数,则将任务加入到任务队列workQueue中进行排队。
- 非核心线程处理: 如果任务队列已满且当前线程数小于最大线程数maximumPoolSize,则创建一个新线程来处理这个任务。如果任务队列已满且当前线程数已达到最大线程数,则执行拒绝策略handler。
- 任务执行: 核心线程和非核心线程会不断从任务队列中获取任务并执行。
- 线程回收: 如果一个非核心线程在等待时间超过keepAliveTime后仍未获得新任务,该线程将被终止,以节省资源。
keepAliveTime对核心线程是否生效,是否能杀死核心线程
回答: keepAliveTime默认对核心线程不生效,只对非核心线程生效。如果要对核心线程生效,需要调用allowCoreThreadTimeOut(true)
。
那如果我想杀死核心线程应该怎么做
回答: 可以通过设置核心线程的过期时间来实现。调用allowCoreThreadTimeOut(true)
,然后设置keepAliveTime
,核心线程在空闲时间超过keepAliveTime
后也会被回收。
线程安全问题怎么解决
回答:
- 加锁: 使用
sychronized
或ReentrantLock
等锁机制。 - 使用线程安全的集合: 如
ConcurrentHashMap
、CopyOnWriteArrayList
等。 - 原子类: 使用
AtomicInteger
、AtomicReference
等原子类进行操作。
除了加锁还有什么方法,有没有无锁化方法
回答:
- 线程局部变量: 使用
ThreadLocal
来存储线程私有的数据,避免线程间的数据竞争。 - 无锁算法: 使用CAS(Compare And Swap)等无锁算法,利用硬件支持的原子操作来保证线程安全。
读写锁听过吗,大概说说
回答: 读写锁是一种特殊的锁机制,允许多个线程同时读,但在写操作时,只有一个线程可以写,并且在写操作时,不允许读操作。常用的读写锁实现有ReentrantReadWriteLock
。
ThreadLocal说说
回答:ThreadLocal
提供了线程局部变量,每个线程都有自己独立的变量副本,互不干扰。主要用于解决多线程环境下的变量隔离问题。
线程池和ThreadLocal一起用会有什么问题吗
回答: 主要问题是内存泄漏。因为线程池中的线程是复用的,ThreadLocal
变量不会被回收,可能导致内存泄漏。此外,还可能有脏数据的问题,因为线程复用时,ThreadLocal
变量中的数据可能没有及时清理。
lc53 最大子数组和
回答: 这道题可以用动态规划解决。定义一个变量max_so_far
记录到当前位置的最大子数组和,一个变量max_ending_here
记录以当前元素结尾的最大子数组和。遍历数组,更新这两个变量,最终max_so_far
即为结果。
public int maxSubArray(int[] nums) { int max_so_far = nums[0]; int max_ending_here = nums[0]; for (int i = 1; i < nums.length; i++) { max_ending_here = Math.max(nums[i], max_ending_here + nums[i]); max_so_far = Math.max(max_so_far, max_ending_here); } return max_so_far; }