京东物流面试汇总(一)
京东物流面试汇总(一)
1. Redis 分布式锁使用的时候锁住任务异常怎么办?
异常分两种:
- Redis 链接异常: 本身使用 Lettuce 连接池进行的链接,内部已经完成了链接配置,包括重试机制、超时链接、健康检查等操作。
- 任务异常: 采取 fast-fail + retry 进行补偿。
其他点:
- Redis 分布式锁使用的
SETNX原子性不可分割,采用 Lua 脚本。 - Lettuce:
- Lettuce 和 Jedis 有什么区别? 它们各自的优缺点是什么?
- Lettuce 如何实现非阻塞 IO?
- 如何配置 Lettuce 的连接池?
- Lettuce 如何处理 Redis 连接异常?
- 如何在 Lettuce 中使用事务?
- 如何在 Lettuce 中执行 Lua 脚本?
- 什么是 Redis Sentinel? Lettuce 如何连接到 Redis Sentinel?
- 什么是 Redis Cluster? Lettuce 如何连接到 Redis Cluster?
- 如何使用 Lettuce 的 reactive API?
- 如何自定义 Lettuce 的编解码器?
- 你使用 Lettuce 做过哪些性能优化?
- 你在实际项目中如何使用 Redis 和 Lettuce?
2. Java 三大特性?
- 封装
- 继承
- 多态
3. MySQL 和 Redis 一致性问题(监听 binlog)
- 延迟双删
- Cache miss
- 加锁
- 先删除缓存,再更新数据,再监听 binlog 删除缓存
- 先删除缓存在更新数据库
- 为啥不能先更数据库在删除缓存,数据一致性问题?
- 还有为啥不用更新缓存,写写并发下数据一致性问题?
4. MySQL 索引
- 索引是为了快速定位到包含特定值的行,无需扫描整个表。
- 索引类型:
- B-Tree
- 哈希索引
- 空间索引
- 全文索引
- InnoDB 是 MySQL 的默认存储引擎,它支持事务、行级锁和外键等特性。 InnoDB 存储引擎的索引实现与其他存储引擎有所不同,尤其是聚集索引和非聚集索引的概念。
- 聚集索引
- 聚集索引决定了表中数据的物理存储顺序
- 缺点
- 插入速度依赖于插入顺序,按照聚集索引的顺序插入是速度最快的。 如果数据不是按照聚集索引的顺序插入,可能会导致页分裂,影响性能
- 更新聚集索引列的代价很高,因为需要移动数据行
- 非聚集索引
- 非聚集索引是一种独立于数据行的索引
- 优点
- 可以提高特定列的查询性能,无需扫描整个表。
- 创建和删除非聚集索引的代价相对较低
- 缺点
- 需要回表查询,性能不如聚集索引。
- 占用额外的存储空间。
- 覆盖索引
- 优化:
- 选择合适的索引列: 应该选择经常用于查询、排序和分组的列作为索引列。
- 使用联合索引: 联合索引可以提高多列查询的性能。 注意索引列的顺序,应该将选择性高的列放在前面。
- 避免在 WHERE 子句中使用函数或表达式: 这样会导致索引失效。
- 定期分析和优化索引: 可以使用
ANALYZE TABLE命令来分析表和索引的统计信息,MySQL 可以根据这些信息来选择更合适的执行计划。 - 注意索引的长度: 索引的长度越短,占用空间越小,查询速度越快。 可以对字符串类型的列使用前缀索引。
- 避免创建过多的索引: 索引会占用存储空间,并且会影响 INSERT 和 UPDATE 操作的性能。
- 考虑业务场景: 根据具体的业务场景选择合适的索引类型和策略。
SHOW PROFILES:查看每个 SQL 语句耗时- open table:耗时长需要检查表的状态、文件系统性能
- optimizing:如果花费时间过长,检查 SQL 复杂度和索引
- statistics:收集表索引和索引的统计信息,如果过长可能检查表的统计信息是否需要更新
- sending data:网络耗时
5. Java 异常分类
- 受检异常 (Checked Exceptions):
- 受检异常是在编译时必须处理的异常。 如果一个方法可能会抛出受检异常,则必须在方法的声明中使用
throws关键字声明,或者在方法内部使用try-catch块捕获并处理该异常FileNotFoundExceptionIOExceptionSQLExceptionClassNotFoundException
- 受检异常是在编译时必须处理的异常。 如果一个方法可能会抛出受检异常,则必须在方法的声明中使用
- 非受检异常 (Unchecked Exceptions) / 运行时异常 (Runtime Exceptions):
NullPointerExceptionArrayIndexOutOfBoundsExceptionClassCastExceptionArithmeticExceptionIllegalArgumentException
- ERROR 错误:
- 通常表示 JVM 级别的错误
- 内存溢出 (OutOfMemoryError)
- 栈溢出 (StackOverflowError)
- 虚拟机错误 (VirtualMachineError)
- 通常表示 JVM 级别的错误
6. 动态线程池能修改哪些参数?
- Core pool size
- Max core pool size
- Keep alive time
- Blocking queue (只能调整队列的大小,不能更换队列类型)
RejectExecutionHandlerAbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy
- 动态修改的影响: 可以根据不同的负载情况,动态调整拒绝策略。
- 在负载较高时,可以使用
CallerRunsPolicy或DiscardOldestPolicy来避免任务丢失或系统崩溃。 - 在负载较低时,可以使用
AbortPolicy来快速发现问题。
- 在负载较高时,可以使用
7. 介绍下 Java 中的队列有哪些?
QueuePriorityQueue:PriorityQueue中的元素按照优先级排序,优先级最高的元素位于队列的头部。 它不允许插入null元素。 如果没有指定Comparator,则使用元素的自然顺序进行排序 (元素必须实现Comparable接口)ArrayBlockingQueue:ArrayBlockingQueue是一个有界阻塞队列,它基于数组实现。ArrayBlockingQueue在创建时需要指定队列的容量,一旦队列达到容量上限,再添加元素的操作将会被阻塞,直到有元素被移除。LinkedBlockingQueue:LinkedBlockingQueue是一个阻塞队列,它基于链表实现。LinkedBlockingQueue可以是有界的,也可以是无界的。 如果创建时没有指定容量,则默认为无界队列。DelayQueue:DelayQueue是一个无界阻塞队列,其中的元素只有在其延迟期满时才能被取出。 队列中的元素必须实现Delayed接口,该接口定义了元素的延迟时间。DelayQueue适用于实现延迟任务或定时任务。 它不允许插入null元素。SynchronousQueue:SynchronousQueue是一个特殊的阻塞队列,它不存储任何元素。 每个插入操作必须等待一个相应的移除操作,反之亦然。SynchronousQueue可以看作是一个传递信息的通道,适用于线程之间直接传递数据。 它不允许插入null元素。 它是Executors.newCachedThreadPool()使用的队列类型。
8. 如果一个任务断断续续的每次流量变化不大,用线程池核心和最大线程数比例怎么设置?
- 目标是在资源利用率和响应速度之间找到平衡点
- 建议默认设置核心线程数等于核心数,资源利用率可控
- 队列选择使用固定长度,比如先选 1024 个任务,拒绝策略选择
CallerRunsPolicy,通过监控队列负载动态调整。
- 监控:
- 活跃线程数 (Active Threads): 反映了当前正在执行任务的线程数量。
- 任务队列长度 (Queue Size): 反映了等待执行的任务数量。
- 已完成任务数 (Completed Tasks): 反映了线程池已经完成的任务数量。
- 拒绝任务数 (Rejected Tasks): 反映了被拒绝的任务数量。
- 平均响应时间 (Average Response Time): 反映了任务的平均执行时间。
- 调优策略:
- 如果活跃线程数长期等于核心线程数,且任务队列为空: 可以适当减少核心线程数,节省资源。
- 如果活跃线程数经常达到最大线程数,且任务队列已满: 说明线程池的处理能力不足,可以适当增加最大线程数或调整任务队列的容量。 或者考虑优化任务的执行效率。
- 如果拒绝任务数持续增加: 说明线程池无法处理所有请求,需要增加线程池的处理能力,或者调整拒绝策略。 也需要检查系统是否存在瓶颈。
- 如果平均响应时间过长: 需要分析任务的执行时间,是否存在性能瓶颈,或者是否需要增加线程池的处理能力。
9. 死锁产生的原因和怎么避免?
- 四个条件:
- 互斥条件
- 循环等待条件
- 持有并等待条
- 不可剥夺条件
- 允许线程强行剥夺其他线程占用的资源。 Java 的
synchronized锁不支持剥夺。 可以使用ReentrantLock,并结合tryLock(long time, TimeUnit unit)方法来实现可剥夺的锁
- 允许线程强行剥夺其他线程占用的资源。 Java 的
- 避免:
- 超时机制
- 减少锁的力度
- 避免嵌套锁
- 死锁检查:Java 的
ThreadMXBean接口可以用于检测死锁 - 代码编写简洁清晰
10. Kafka 如何保证消息不丢失的?
- Broker
- 多副本机制 (Replication): 每个 Topic 可以配置多个副本 (Replicas)。 一个副本作为 Leader,负责处理所有的读写请求,其他副本作为 Follower,从 Leader 复制数据。
replication.factor参数: 设置 Topic 的副本数量。 通常设置为 3,以提高数据的可靠性。- ISR (In-Sync Replicas): 与 Leader 保持同步的副本集合。 只有 ISR 中的副本才能被选举为新的 Leader。
- Leader 选举 (Leader Election): 当 Leader Broker 宕机时,Kafka 会自动从 ISR 中选举一个新的 Leader。
- 最小 ISR 数量 (Min In-Sync Replicas):
min.insync.replicas参数设置一个 Topic 至少需要有多少个 ISR 副本才能接收生产者的消息。 如果 ISR 副本数量小于该值,则 Broker 会拒绝接收生产者的消息,防止数据丢失。 通常设置为replication.factor - 1。 - 刷盘机制 (Flush to Disk): Kafka 会定期将消息刷写到磁盘,保证数据的持久化。
log.flush.interval.messages参数: 设置每隔多少条消息刷写一次磁盘。log.flush.interval.ms参数: 设置每隔多少毫秒刷写一次磁盘。- 配置建议: 优先根据消息数量进行刷盘,时间间隔可以作为辅助配置。 极端情况下为了保证数据绝对不丢失,可以配置每次写入都刷盘,但是会严重影响性能。
- 多副本机制 (Replication): 每个 Topic 可以配置多个副本 (Replicas)。 一个副本作为 Leader,负责处理所有的读写请求,其他副本作为 Follower,从 Leader 复制数据。
- Producer
acks = 1:生产者只需要接收到 Leader Broker 的确认。- 重试机制
- 事务
- Consumer
commitSync()和commitAsync()
11. 说说缓存雪崩和缓存穿透的理解,以及如何避免?
- 缓存雪崩:同一时间多个缓存到期
- 解决方案:
- 增加过期时间,随机过期时间
- 使用多级缓存
- 熔断降级
- 缓存预热
12. ThreadLocal 是什么,它的实现原理是什么?
- 线程局部变量:
ThreadLocal是 Java 提供的一种线程隔离机制,它可以让你在每个线程中创建一个独立的变量副本。 换句话说,每个线程都可以访问和修改自己的ThreadLocal变量,而不会影响其他线程的变量。 ThreadLocal保证了线程安全,因为每个线程操作的都是自己的变量副本,避免了多线程并发访问共享变量时可能出现的数据竞争和同步问题。- 使用场景:
- 在线程内部存储一些状态信息,例如用户 ID、事务 ID 等。
- 避免在方法调用链中显式传递参数。
ThreadLocalMap使用弱引用来引用ThreadLocal对象,这意味着如果没有其他强引用指向ThreadLocal对象,ThreadLocal对象可能会被垃圾回收。- 但是,
ThreadLocalMap中的 entry 仍然持有对 value 的强引用,导致 value 无法被垃圾回收,从而造成内存泄漏。 - 如果线程一直存活(例如,在线程池中),并且没有手动调用
remove()方法,那么ThreadLocalMap中的 entry 就会一直存在,导致 value 无法被回收。 - 避免:
- 手动调用
remove()方法: 在使用完ThreadLocal变量后,务必调用remove()方法,移除当前线程的ThreadLocal变量。 - 使用 try-finally 语句: 在 try 块中使用
ThreadLocal变量,在 finally 块中调用remove()方法,确保remove()方法一定会被执行。 - 慎用
ThreadLocal: 只有在真正需要线程隔离的场景下才使用ThreadLocal。
- 手动调用
13. 简单说下受检异常和非受检异常?
(请参考第 5 题 Java 异常分类)
14. MySQL 的行级锁到底在锁什么东西?
- MySQL 的 InnoDB 存储引擎中的行级锁,锁的是索引记录,而不是直接锁行
- 锁定的对象:
- 索引记录: 行级锁锁定的实际上是索引记录。 当你更新或删除一行数据时,InnoDB 会锁定该行数据对应的主键索引记录。
- 间隙锁 (Gap Locks): 除了锁定索引记录本身,InnoDB 还可以锁定索引记录之间的间隙,防止其他事务插入新的记录。 间隙锁是防止幻读(Phantom Reads)的一种机制。
- Next-Key 锁: Next-Key 锁是记录锁和间隙锁的组合。 它锁定一个索引记录,并锁定该记录之前的间隙。
15. MySQL 数据库 CPU 飙升的话,要怎么处理?
- 杀死占用 CPU 最高的查询进程: 通过
SHOW PROCESSLIST;命令找到占用 CPU 时间最长的查询,然后使用KILL <process_id>;命令终止这些查询。 这是一个比重启服务更温和的方法,但需要判断被杀掉的查询是否是关键业务 - 监控 MySQL 性能: 使用监控工具(如
top,htop,iostat,vmstat,Prometheus + Grafana,Percona Monitoring and Management (PMM))查看 CPU 使用率、内存使用率、磁盘 I/O 等指标。 确定 CPU 飙升是否与其他资源瓶颈相关。 - 查询慢查询日志: 开启 MySQL 的慢查询日志,记录执行时间超过指定阈值的 SQL 语句。 检查慢查询日志,找出执行时间长的 SQL 语句。
long_query_time参数设置慢查询阈值,slow_query_log参数开启慢查询日志。 - 使用
SHOW PROCESSLIST命令: 执行SHOW PROCESSLIST;命令,查看当前 MySQL 服务器上的所有连接和正在执行的 SQL 语句。 重点关注State列,找出处于Sending data,Sorting result,Copying to tmp table,Locked等状态的查询。 这些状态通常表示查询正在执行大量的 I/O 操作、排序操作或等待锁。 - 使用
EXPLAIN分析 SQL 语句: 使用EXPLAIN命令分析慢查询日志中 SQL 语句的执行计划。 检查EXPLAIN的输出,重点关注type列和Extra列。type列表示 MySQL 访问数据的方式,常见的取值有ALL(全表扫描)、index(全索引扫描)、range(范围扫描)、ref(使用非唯一索引)等。 尽量避免ALL和index类型。Extra列提供有关 MySQL 执行计划的额外信息,常见的取值有Using filesort(需要进行文件排序)、Using temporary(需要使用临时表)等。 尽量避免Using filesort和Using temporary。
- 检查锁等待情况: 运行
SHOW ENGINE INNODB STATUS;命令,查看 InnoDB 存储引擎的状态信息。 重点关注LATEST DETECTED DEADLOCK和LATEST FOREIGN KEY ERROR部分,检查是否存在死锁或外键约束错误。 - 优化 SQL 语句:
- 添加索引: 为查询条件中的字段添加索引,加快查询速度。
- 避免全表扫描: 尽量使用索引,避免全表扫描。
- 优化
WHERE子句: 避免在WHERE子句中使用OR、IN、NOT IN等操作符,可以使用UNION ALL代替OR,使用JOIN代替IN。 - 避免
SELECT *: 只选择需要的字段,减少 I/O 操作。 - 优化排序: 尽量使用索引进行排序,避免文件排序。
- 分解大型查询: 将大型查询分解为多个小型查询,减少锁等待时间。
- 使用
SQL Hint: 在 SQL 语句中使用SQL Hint,强制 MySQL 使用特定的索引或优化器策略。
- 优化数据库配置:
- 调整缓冲区大小: 增大
innodb_buffer_pool_size参数,提高 InnoDB 存储引擎的缓存命中率。 - 调整连接数: 调整
max_connections参数,控制最大连接数。 - 调整线程池: 使用线程池(如
thread_pool插件),提高并发处理能力。 - 开启查询缓存: 开启查询缓存(
query_cache_type = 1),缓存查询结果,减少数据库压力。 注意: 在高并发场景下,查询缓存可能会成为瓶颈,建议谨慎使用。 在 MySQL 8.0 中,查询缓存已经被移除。
- 调整缓冲区大小: 增大
- 优化硬件资源:
- 升级 CPU: 更换性能更强的 CPU。
- 增加内存: 增加内存容量,提高缓存命中率。
- 使用 SSD 硬盘: 更换 SSD 硬盘,提高 I/O 性能。
- 使用 RAID 磁盘阵列: 使用 RAID 磁盘阵列,提高磁盘 I/O 性能和数据安全性。
- 优化数据库结构:
- 分表: 将大型表分解为多个小型表,减少查询范围。
- 分区: 将大型表按照某种规则划分为多个分区,提高查询效率。
- 读写分离: 将读操作和写操作分离到不同的数据库服务器上,减轻数据库压力。
- 检查应用程序代码:
- 优化 ORM 框架的使用: 避免过度使用 ORM 框架,手动编写 SQL 语句可以更好地控制执行计划。
- 避免循环查询: 在循环中执行数据库查询会导致性能问题,尽量使用批量操作。
- 使用连接池: 使用连接池可以减少数据库连接的开销。
16. 日常工作中是如何进行优化 SQL 的?
(未提供答案,请补充)
17. 分库分表方案中出现数据倾斜如何办?
- 选择合理的 Hash 算法: 尽量选择能让数据分布更均匀的 Hash 算法。 一些高级的 Hash 算法,例如一致性哈希,可以在节点动态增减时尽量减少数据的迁移。
- 了解业务数据特征: 在分库分表之前,尽可能了解业务数据的分布情况。 如果某个字段(例如用户 ID)的数据分布非常不均匀,就要考虑避免使用该字段作为分片键。
- 常见策略:
- 读多写少,热点集中: 热点数据缓存或复制是好选择。
- 写多读少,热点分散: 特殊路由规则或数据迁移可能更合适。
- 分片规则不合理: 动态调整分片算法。
- 数据量持续增长: 增加分片数量(扩容)。
- 秒杀系统是数据倾斜的典型场景。 解决方法可以组合使用:
- 缓存: 使用 Redis 缓存商品信息和库存。
- 队列: 将用户的请求放入队列中异步处理。
- 限流: 限制每个用户的请求频率。
- 特殊路由: 对于热门商品,可以将其数据分散到多个数据库中。
- 动态调整: 根据实际情况动态调整缓存和队列的大小。
18. 分布式锁加锁失败后等待逻辑是如何实现的?
- 短暂休眠重试 (Short Sleep Retry):
- 实现: 加锁失败后,线程休眠一个很短的时间(例如几毫秒到几十毫秒),然后立即重试。
- 优点: 实现简单,对资源消耗相对较小。
- 缺点: 容易造成 CPU 空转,在高并发场景下,会增加锁的竞争,可能导致大量的无效重试,降低系统吞吐量。 容易出现“活锁”现象,多个线程一直竞争但都无法获得锁。
- 指数退避 (Exponential Backoff):
- 实现: 每次重试前,线程休眠的时间呈指数增长。 例如,第一次休眠 1ms,第二次 2ms,第三次 4ms,以此类推。 通常会设置一个最大休眠时间。
- 优点: 缓解了短暂休眠重试的 CPU 空转问题,减少了无效重试。
- 缺点: 仍然存在一定的 CPU 消耗。 在高并发场景下,线程可能需要等待较长时间才能获得锁,影响系统的实时性。
- 阻塞式等待 (Blocking Wait) / 发布-订阅 (Pub-Sub):
- 实现: 加锁失败后,线程进入阻塞状态,等待锁释放的通知。 锁释放时,通过发布-订阅机制(例如 Redis 的 Pub/Sub 或 ZooKeeper 的 Watcher)通知等待的线程。
- 优点: 避免了 CPU 空转,提高了资源利用率。 可以实现更公平的锁竞争,先到先得。
- 缺点: 实现相对复杂,需要依赖消息队列或 ZooKeeper 等组件。 如果通知机制出现问题,可能会导致线程一直阻塞,无法获得锁。 增加了系统的复杂性。
- 令牌桶 (Token Bucket):
- 实现: 系统维护一个令牌桶,加锁失败的线程需要从令牌桶中获取令牌才能进行重试。 如果令牌桶为空,则线程需要等待一段时间才能获取令牌。
- 优点: 可以平滑地控制重试的频率,避免大量的并发重试。
- 缺点: 实现相对复杂,需要维护令牌桶的状态。
- 自适应等待 (Adaptive Wait):
- 实现: 根据锁的竞争情况动态调整等待策略。 例如,如果锁的竞争激烈,则采用指数退避策略;如果锁的竞争不激烈,则采用短暂休眠重试策略。
- 优点: 可以根据实际情况选择最合适的等待策略,提高系统的性能。
- 缺点: 实现非常复杂,需要实时监控锁的竞争情况。
19. 订单未支付过期如何自动关单?
- 采用定时任务进行扫表:
- 优点:降低系统复杂性,可靠性
- 缺点:扫表每次只能批量进行,延迟性高,数据库压力大
- 对于缺点进行优化:
- 优化扫描范围,避免全表扫描
- 增量扫描: 不要每次都扫描所有未支付的订单。 可以记录上次扫描的时间戳,只扫描在该时间戳之后创建的订单。
- 优点: 显著减少每次扫描的数据量。
- 缺点: 需要维护上次扫描的时间戳,并且要处理可能出现的丢失问题。
- 索引优化: 确保你的
orders表的status(订单状态)和create_time(创建时间)字段都有索引。 联合索引(status, create_time)通常效果更好。- 优点: 加快查询速度。
- 缺点: 需要额外的索引空间,并且会影响写入性能。
- 分批次扫描: 即使是增量扫描,一次性扫描的数据量也可能很大。 可以使用
LIMIT和OFFSET分批次处理。- 优点: 避免一次性加载大量数据到内存,减少数据库的压力。
- 缺点: 需要多次查询数据库。
- 增量扫描: 不要每次都扫描所有未支付的订单。 可以记录上次扫描的时间戳,只扫描在该时间戳之后创建的订单。
- 减少扫描频率,配合其他机制
- 缩短轮询间隔 + 限制每次处理的订单数量: 比如原来 10 分钟扫一次,每次最多处理 1000 单。 现在可以改成 2 分钟扫一次,每次最多处理 200 单。 虽然整体扫描频率提高了,但是每次对数据库的压力降低了,而且延迟也减少了。
- 结合 Redis 缓存: 将最近一段时间内创建的未支付订单 ID 缓存到 Redis 中。 定时任务先扫描 Redis,再根据 Redis 中的订单 ID 查询数据库。
- 优点: 可以进一步减少数据库的扫描范围。
- 缺点: 需要维护 Redis 缓存,并且要处理数据一致性问题。
- 使用数据库变更监听工具(例如 Canal): 监听
orders表的insert事件,如果订单状态为 待支付,则将订单 ID 放入一个待处理队列。 定时任务从队列中获取订单 ID 进行处理。 相当于把一部分扫描的压力分摊到insert事件上。- 优点: 更实时地发现新创建的订单。
- 缺点: 增加了系统的复杂性,需要引入 Canal 等工具。
- 减少数据库压力,提高更新效率
- 批量更新: 不要一条一条地更新订单状态。 将需要关闭的订单 ID 收集起来,使用
UPDATE orders SET status = '已关闭' WHERE order_id IN (...)批量更新。- 优点: 减少数据库的访问次数。
- 缺点:
IN子句的长度有限制,需要控制每次更新的订单数量。
- 优化更新逻辑: 确保
UPDATE语句的WHERE子句使用了索引。
- 批量更新: 不要一条一条地更新订单状态。 将需要关闭的订单 ID 收集起来,使用
- 优化扫描范围,避免全表扫描
20. 如何实现登录用户存 1000 SKU,未登录用户只能存 200 SKU 呢?
- 设计个表存储
- 登录用户 uid,未登录用户 session
- 未登录用户需要登录之后需要把 SKU 变更成登录用户的。
- 关键点:
- Session 管理: 对于未登录用户,使用 Session ID 来标识不同的用户。 Session ID 通常存储在 Cookie 中。
- 用户身份识别: 通过判断用户是否已登录来区分用户类型。
- 数据存储隔离: 使用不同的 Key 或表结构来存储登录用户和未登录用户的数据。
- 数量限制: 在添加 SKU 之前,先判断已存储的 SKU 数量是否超过限制。
- 登录后的数据迁移: 当未登录用户登录后,需要将该用户之前存储的 SKU 数据迁移到登录用户的账户下,并删除未登录用户的数据。
- CORS: 因为是前后端分离,需要设置 CORS 允许跨域
- 关键点:
21. 如何让系统抗住双 11 的预约抢购活动呢?
(未提供答案,请补充)
22. 假设数据库成为性能瓶颈,动态查询如何提升效率?
- 优化查询效率:索引优化、表结构优化
- 使用缓存
- 优化数据库结构和配置
- 反范式化: 在某些情况下,可以适当进行反范式化,减少 JOIN 操作。
- 分区表: 对于大型表,可以考虑使用分区表,将数据分散到多个物理文件中,提高查询效率。
- 垂直拆分: 将表拆分成多个列较少的表,减少 IO 操作。
- 水平拆分 (Sharding): 将表的数据分散到多个数据库服务器上,提高并发处理能力。
- 读写分离
- 分库分表
23. 如何保证你的消息只被消费 1 次?
- 消费者层面保证幂等:唯一 ID、版本号
- 优点:简单易行
- 缺点:修改消费者代码
24. 什么是服务网格?
(未提供答案,请补充)
25. 说一下分布式事务的理解和解决方案?
- 2PC
- 3PC
- TCC
- 性能比 2PC/3PC 好,资源锁定时间短
- 开发量大: 每个操作都要实现 Try、Confirm、Cancel 三个方法。
数据一致性: 难以保证强一致性,可能存在最终一致性问题。
适用场景: 允许最终一致性,且对性能有一定要求的场景。 常用于支付、电商等业务。
- 本地消息表
- 优点: 简单易于实现,可以保证最终一致性。
- 缺点: 需要额外的本地消息表,增加了数据库的负担。
- 最大努力通知
- 对数据一致性要求不高,允许少量消息丢失的场景。 例如,发送短信、邮件等通知。
- 可靠性较低,可能存在消息丢失的情况。
- Seata
26. 当给出第三方接口调用的时候需要注意哪些事情?
- 明确需求和目标:
- 清晰理解为什么要调用这个第三方接口? 解决什么问题?
- 预期的结果是什么?成功、失败的场景分别是什么?
- 性能要求如何? (例如:响应时间、吞吐量)
- 接口调研和评估:
- 功能评估: 接口是否满足你的业务需求? 覆盖了所有必要的场景吗?
- 性能评估: 接口的响应时间、并发处理能力如何? 是否有性能瓶颈?
- 稳定性评估: 接口的稳定性如何? 是否有历史故障记录?
- 安全评估: 接口是否安全? 如何进行身份验证和授权? 数据传输是否加密?
- 成本评估: 接口是免费的还是收费的? 收费模式是什么? (例如:按调用次数、按流量)
- SLA (Service Level Agreement): 是否有服务级别协议? 服务提供商承诺的可用性、响应时间等指标是什么?
- 文档完整性: 接口文档是否清晰、完整、易于理解? 是否提供了示例代码?
- 版本控制: 接口是否有版本控制? 如何处理接口版本升级?
- 设计容错机制:
- 超时设置: 设置合理的超时时间,避免长时间等待。
- 重试策略: 设计合理的重试策略,例如指数退避。 但要注意避免无限重试,造成死循环或对第三方服务造成压力。
- 熔断机制: 当接口出现故障时,快速熔断,避免服务雪崩。
- 降级方案: 当接口不可用时,提供备选方案或降级服务,保证核心业务的可用性。
- 限流: 限制对第三方接口的调用频率,防止超出其承受能力。
- 配置管理:
- API 密钥: 将 API 密钥等敏感信息存储在安全的地方,例如配置中心或密钥管理系统。
- 接口地址: 将接口地址配置化,方便修改和管理
- 监控:
- 接口调用次数: 监控接口的调用频率。
- 响应时间: 监控接口的响应时间。
- 错误率: 监控接口的错误率。
- 资源消耗: 监控调用接口的服务的 CPU、内存等资源消耗。
27. 如果 JVM 出现频繁 Full GC 应该如何解决?
- 老年代空间不足: 这是最常见的原因。
- 原因: 对象生命周期过长,大量对象晋升到老年代,导致老年代空间不足。
- 解决方法:
- 优化代码: 检查代码中是否存在内存泄漏,例如未关闭的连接、未释放的资源等。
- 缩短对象生命周期: 尽量缩短对象的生命周期,减少对象晋升到老年代的机会。
- 调整堆大小: 适当增加老年代的大小(-Xms, -Xmx, -XX:NewRatio)。
- 调整晋升阈值: 调整对象从新生代晋升到老年代的年龄阈值(-XX:MaxTenuringThreshold)
- 元空间(Metaspace)空间不足: 元空间用于存储类的元数据、常量池、方法信息等。
- 原因: 大量加载类、动态生成类(例如使用 CGLIB),导致元空间占用过多。
- 解决方法:
- 优化代码: 减少动态生成类的数量,尽量使用静态类。
- 卸载不再使用的类: 如果使用了动态类加载,确保及时卸载不再使用的类。
- 增加元空间大小: 增加元空间的大小(-XX:MaxMetaspaceSize)。
28. 数据中 TopK 问题?
(未提供答案,请补充)
29. MySQL 是在什么时候进行刷盘的?
(请参考 Redo Log, Undo Log, 脏页, Binlog 的相关知识,并补充答案)
30. MySQL 如何解决重复读?
- InnoDB 每行隐藏两个重要的列:
DB_TRX_ID:创建或最后一次修改该行的事务 ID。DB_ROLL_PTR:指向 undo log 记录的指针。
- Undo Log:保证事务多个版本链路
- 当一个事务修改一行数据时,InnoDB 会将修改前的旧值记录在 Undo log 中,并将
DB_ROLL_PTR指向该 Undo log 记录。
- 当一个事务修改一行数据时,InnoDB 会将修改前的旧值记录在 Undo log 中,并将
- ReadView:保证事务可见性
trx_id(当前事务 ID): 创建 Read View 的事务 ID。creator_trx_id(创建者事务 ID): 创建这个 Read View 的事务 IDm_ids(活跃事务集合): 在创建 Read View 时,当前系统中所有活跃的、未提交的事务 ID 集合。 它是一个列表,记录了所有可能对当前事务产生影响的事务。min_trx_id(最小事务 ID): m_ids 集合中最小的事务 ID。max_trx_id(最大事务 ID): 下一个将要分配的事务 ID。 注意,不是 m_ids 中最大的事务 ID,而是系统下一个即将分配的事务 ID。
- Read View 的可见性判断规则:
当事务需要读取一行数据时,InnoDB 会根据 Read View 中的信息,判断该行数据的哪个版本对当前事务是可见的。 判断规则如下:- 如果数据的
DB_TRX_ID<min_trx_id: 说明该版本是在创建 Read View 之前就已经提交的事务创建的,对当前事务可见。 (小于最小活跃事务ID,说明在创建ReadView之前已经提交) - 如果数据的
DB_TRX_ID>=max_trx_id: 说明该版本是在创建 Read View 之后才启动的事务创建的,对当前事务不可见。 (大于等于最大事务ID,说明在创建ReadView之后才启动) - 如果
min_trx_id<= 数据的DB_TRX_ID<max_trx_id: 说明该版本可能是在创建 Read View 时仍然活跃的事务创建的,需要进一步判断:- 如果
DB_TRX_ID存在于m_ids集合中: 说明创建该版本的事务在创建 Read View 时仍然活跃,还未提交,对当前事务不可见。 - 如果
DB_TRX_ID不存在于m_ids集合中: 说明创建该版本的事务在创建 Read View 之前已经提交,对当前事务可见。
- 如果
- 如果数据的
31. MySQL ACID 和事务隔离级别?
- ACID 是一组属性,用来保证数据库事务的可靠性。 它们是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)
- SQL 标准定义了四种事务隔离级别
- 读未提交 (Read Uncommitted)
- 读已提交 (Read Committed)
- 可重复读 (Repeatable Read)
- 串行化 (Serializable)
32. 比如我有 1 个 SQL 要 UPDATE 一个 user 表,ID > 1 的情况下,锁的范围是如何确定的?锁的逻辑是如何实现的?
- InnoDB 使用 id 索引找到 id 为 2、3 和 4 的记录。
- InnoDB 对 id 为 2、3 和 4 的索引记录分别加上行锁 (Record Lock)。
- 如果隔离级别是 "可重复读",并且不存在其他事务插入 id 在 1 和 2 之间的记录,InnoDB 可能不会加间隙锁。 但是,如果存在这种可能性,InnoDB 可能会在 id 为 1 和 2 之间的间隙上加间隙锁 (Gap Lock),防止其他事务插入 id 为 1.5 的记录。 这个行为依赖于
innodb_locks_unsafe_for_binlog的配置和优化器的判断。 - InnoDB 修改 id 为 2、3 和 4 的记录的 name 字段。
- 事务提交后,InnoDB 释放所有锁。
33. Java 中一个对象从创建到回收,经过哪些步骤?
- 类加载
- 加载: 当程序需要使用一个类时,JVM 会首先将该类的
.class文件加载到内存中。 这包括读取类的二进制数据、创建java.lang.Class对象等。 - 链接
- 验证: 检查加载的类是否符合 JVM 规范,包括文件格式、字节码指令等
- 准备: 为类的静态变量分配内存,并设置默认初始值 (例如,int 类型的静态变量初始化为 0)。
- 解析: 符号引用转换为直接引用。 符号引用是类、字段、方法等的名称,而直接引用是实际的内存地址。
- 初始化: 执行类的静态初始化器和静态变量赋值语句。 这是类加载的最后一步
- 加载: 当程序需要使用一个类时,JVM 会首先将该类的
- 对象创建
- 分配内存:
- JVM 在堆中为新对象分配内存空间。 堆是 JVM 中最大的内存区域,用于存储对象实例。
- 初始化:
- 将分配到的内存空间初始化为零值 (例如,int 类型的实例变量初始化为 0,boolean 类型初始化为 false,引用类型初始化为
null) - 设置对象头 (Setting Object Header): 设置对象的对象头信息,包括:
- Mark Word: 存储对象的哈希码、GC 分代年龄、锁状态标志、偏向线程 ID 等信息。
- Klass Pointer: 指向对象的类元数据的指针。
- 执行构造方法 (Constructor Execution):
- 执行类的构造方法,完成对象的实例变量初始化。
- 构造方法中可以调用其他构造方法,也可以执行自定义的初始化逻辑。
- 将分配到的内存空间初始化为零值 (例如,int 类型的实例变量初始化为 0,boolean 类型初始化为 false,引用类型初始化为
- 分配内存:
- 对象使用
- 对象销毁
34. 一个对象什么情况下会被回收掉?什么情况下进入老年代?
- 对象不可达: 这是最核心的条件。 对象需要通过可达性分析算法被判定为不可达。这意味着从 GC Roots 出发,没有任何引用链可以到达该对象。
- GC Roots:
- 栈帧中的局部变量表中的引用
- 静态变量
- GC Roots:
- 进入老年代:
- 经历一定次数 (默认 15 次,由
-XX:MaxTenuringThreshold参数控制) 的 Minor GC 后仍然存活的对象。 - 大对象直接进入老年代 (由
-XX:PretenureSizeThreshold参数控制,如果对象大小超过该值,则直接在老年代分配)。 - 动态年龄判断: 如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
- 经历一定次数 (默认 15 次,由
35. 如果项目中产生 CPU 飙高,你的排查思路怎么样的?怎么定位 CPU 飙高的代码问题?如何解决 CPU 飙高的代码原因?
(未提供答案,请补充)
36. 线程池核心参数,为什么要用到线程池?阻塞队列有哪些?
(请参考前面的问题,并补充答案)
37. 为什么线程池到达核心线程数后要添加任务到阻塞队列而不是继续创建线程直到最大呢?
(未提供答案,请补充)
38. IO 模型:Reactor 模型
(未提供答案,请补充)
39. 内核数据是如何到用户态的?
(未提供答案,请补充)
40. Select Epoll 原理和细节掌握
(未提供答案,请补充)
41. JVM 内存模型
(请参考前面的问题,并补充答案)
42. 垃圾回收算法有哪些?
(未提供答案,请补充)
43. 年轻代和老年代用什么垃圾算法?
(未提供答案,请补充)
44. JVM 调优一般从哪里入手?都需要调优什么?
- 目标:
- 降低延迟 (Latency): 减少请求的响应时间。
- 提高吞吐量 (Throughput): 增加单位时间内处理的请求数量。
- 减少垃圾回收 (GC) 停顿时间: 缩短 GC 造成的应用程序暂停时间。
- 减少内存占用: 降低 JVM 占用的内存空间。
- 提高稳定性: 减少 OOM (OutOfMemoryError) 错误和 JVM 崩溃。
- 性能问题识别:
- CPU 使用率过高: 可能存在死循环、频繁的计算或锁竞争。
- 内存泄漏: 对象无法被垃圾回收,导致内存占用不断增加。
- 频繁的 GC: GC 停顿时间过长,影响应用程序的响应速度。
- 线程阻塞: 线程长时间等待锁或其他资源。
- 数据库连接池耗尽: 无法获取数据库连接。
- 外部服务调用超时: 调用外部服务时间过长。
- 调优策略:
- 内存调优:
- 堆大小 (Heap Size):
-Xms(初始堆大小) 和-Xmx(最大堆大小) 通常将它们设置为相同的值,以避免 JVM 在运行时动态调整堆大小,从而减少 GC 压力。 需要根据应用程序的内存需求合理设置堆大小。 过小会导致频繁的 GC,过大会浪费内存。 - 新生代大小 (New Generation Size):
-Xmn(直接设置新生代大小) 或-XX:NewRatio(设置老年代与新生代的比例)。 新生代越大,Minor GC 的频率越低,但 Old GC 的频率可能增加。 - Eden 区和 Survivor 区比例:
-XX:SurvivorRatio。 Eden 区越大,Minor GC 的频率越低。 - 老年代大小 (Old Generation Size): 老年代的大小影响 Full GC 的频率。
- Metaspace 大小:
-XX:MetaspaceSize(初始 Metaspace 大小) 和-XX:MaxMetaspaceSize(最大 Metaspace 大小)。 Metaspace 用于存储类的元数据。 - 直接内存 (Direct Memory): 使用
java.nio包时,会分配直接内存。 需要监控直接内存的使用情况,避免 OOM 错误。 可以使用-XX:MaxDirectMemorySize来限制直接内存的大小。
- 堆大小 (Heap Size):
- 垃圾回收器调优:
- 选择合适的垃圾回收器: 根据应用程序的特点选择合适的垃圾回收器。
- Serial GC: 单线程 GC,适用于单核 CPU 或小型应用程序。
- Parallel GC (Parallel Scavenge GC): 多线程 GC,适用于多核 CPU,追求高吞吐量。 通过
-XX:+UseParallelGC启用。 - CMS (Concurrent Mark Sweep) GC: 并发 GC,尽量减少 GC 停顿时间,但可能产生内存碎片。 通过
-XX:+UseConcMarkSweepGC启用。 在 JDK 9 中已废弃,建议使用 G1。 - G1 (Garbage-First) GC: 兼顾吞吐量和低延迟的 GC,将堆划分为多个区域,优先回收垃圾最多的区域。 通过
-XX:+UseG1GC启用。 推荐使用。 - ZGC (Z Garbage Collector): JDK 11 引入,JDK 17 稳定,超低延迟 GC,适用于大堆内存和对延迟非常敏感的应用。
-XX:+UseZGC
- 调整 GC 参数: 根据选择的垃圾回收器,调整相应的参数。 例如,调整 G1 的最大 GC 停顿时间 (
-XX:MaxGCPauseMillis)、并发线程数 (-XX:ParallelGCThreads) 等。
- 选择合适的垃圾回收器: 根据应用程序的特点选择合适的垃圾回收器。
- 线程调优:
- 线程池大小: 合理设置线程池的大小,避免线程过多或过少。 线程过多会增加 CPU 上下文切换的开销,线程过少会导致请求处理速度慢。
- 减少锁竞争: 使用更细粒度的锁、无锁数据结构、CAS (Compare and Swap) 操作等方式来减少锁竞争。
- 避免死锁: 仔细设计锁的获取顺序,避免死锁。
- 线程优先级: 谨慎使用线程优先级,通常不建议修改线程优先级。
- 代码优化:
- 减少对象创建: 尽量重用对象,避免频繁创建对象。
- 使用高效的数据结构和算法: 选择合适的数据结构和算法,提高代码的执行效率。
- 减少 I/O 操作: 尽量减少磁盘 I/O 和网络 I/O 操作。
- 使用缓存: 将常用的数据缓存在内存中,减少数据库访问。
- 使用连接池: 使用数据库连接池和线程池,提高资源利用率。
- 优化 SQL 语句: 优化 SQL 语句,减少数据库查询时间。
- 内存调优:
- 示例 JVM 参数:
-Xms4g: 初始堆大小为 4GB。-Xmx4g: 最大堆大小为 4GB。-Xmn2g: 新生代大小为 2GB。-XX:+UseG1GC: 使用 G1 垃圾回收器。-XX:MaxGCPauseMillis=200: 设置最大 GC 停顿时间为 200 毫秒。-XX:G1HeapRegionSize=32m: 设置 G1 堆区域大小为 32MB。-XX:ConcGCThreads=4: 设置并发 GC 线程数为 4。-XX:InitiatingHeapOccupancyPercent=45: 设置当堆占用率达到 45% 时,启动并发 GC。-XX:+PrintGCDetails: 打印详细的 GC 日志。-XX:+PrintGCDateStamps: 在 GC 日志中打印日期戳。-XX:+PrintGCTimeStamps: 在 GC 日志中打印时间戳。-Xloggc:/path/to/gc.log: 将 GC 日志输出到指定文件。

浙公网安备 33010602011771号