Redis
1.redis的使用场景,以及为什么用redis
缓存高频查询数据
在“黑马点评”中,用户经常会查询热门店铺、评论列表或者评分排行榜,这些数据更新频率不高,但访问量很大。如果每次都直接查数据库,会给数据库带来很大压力。所以我用 Redis 做缓存,把这些热点数据存储在 Redis 中,比如店铺的基本信息、热门评论等。Redis 的内存存储和单线程模型让它的读写速度非常快
分布式锁
在“黑马点评”项目中,有一个下单扣减库存的功能。因为是分布式系统,多用户同时下单可能会导致超卖问题。我用 Redis 实现了分布式锁,通过 SETNX 命令(set if not exists)来保证同一时刻只有一个线程能修改库存
会话管理
在“黑马点评”项目中,用户登录后的会话信息我都存在 Redis 里。比如用户的 token 和基本信息,用 Hash 结构存储,设置一个过期时间(比如 30 分钟)。
2.Zset的底层数据结构
Redis 的 ZSet(Sorted Set,有序集合)的底层实现主要依赖两种数据结构:跳表(Skip List)和哈希表(Hash Table),这两种结构共同协作来实现 ZSet 的功能。
- 跳表(Skip List)
跳表是 ZSet 的核心数据结构,用来维护元素的有序性。它的设计类似于多层链表,最底层是一个普通的有序链表,包含所有元素;上层是稀疏的索引层,通过指针跳跃式访问,减少查找时间。- 在我的“智能点评系统”中,点赞排行榜用 ZSet 实现,跳表的作用是按照 score(比如点赞数)从小到大排序。
- 跳表的查找、插入和删除操作平均时间复杂度是 O(log n),因为它通过多级索引快速定位元素位置,比普通链表的 O(n) 效率高很多。
- Redis 的跳表实现还带有一个随机层数机制,每次插入元素时会随机决定它出现在几层索引中,这样保持了结构的平衡性,避免退化。
- 哈希表(Hash Table)
哈希表用来存储 ZSet 中元素和 score 的映射关系。它的键是 ZSet 的成员(比如用户 ID),值是对应的 score(比如点赞数)。- 在我的项目中,当我用 ZINCRBY 更新某个用户的点赞数时,Redis 内部通过哈希表快速定位该用户,然后更新 score,再通过跳表调整排序。
- 哈希表的查询复杂度是 O(1),保证了元素查找的高效性。
- 两者的结合
- 跳表负责排序和范围查询(比如 ZRANGE 获取 TopN 排行榜),哈希表负责快速定位元素和 score。
- 这种组合让 ZSet 既能高效维护有序性,又能快速操作单个元素,非常适合我的点赞排行榜需求。比如,我用 ZREVRANGE 0 9 就能实时返回点赞数前 10 的用户,性能非常优异。
- 小细节补充
- 当 ZSet 元素数量较少时(小于 128 个,且每个元素长度小于 64 字节),Redis 会用一种叫 ziplist 的紧凑结构代替跳表和哈希表,节省内存。但我的排行榜数据量较大,所以实际用的是跳表+哈希表的标准实现。
- Redis 源码里,跳表节点(zskiplistNode)和哈希表(dict)是 ZSet 的具体实现,感兴趣的话可以深入研究。
总结
ZSet 的底层是跳表和哈希表的组合,跳表保证有序性和范围查询的高效,哈希表保证元素访问的高效。在“智能点评系统”的点赞排行榜中,这种设计让我既能实时更新点赞数,又能快速生成排行榜,非常契合需求。
3.Zset的底层数据结构为什么设计两种数据
ZSet 用跳表和哈希表两种数据结构,是为了兼顾排序(跳表)和快速查找(哈希表)的需求,单靠一种结构无法同时满足性能和功能要求。在“智能点评系统”的点赞排行榜中,这种设计让我能高效实现实时更新和 TopN 查询,完美契合了项目需求。
4.介绍一下秒杀的流程
5.如果库存扣减成功了但是异步调用失败了怎么
库存扣减成功但异步调用失败,我会优先用消息队列重试。如果业务允许,也可以回滚库存。这种设计在我的项目中有效平衡了性能和一致性。
6.redis的k和v的结构怎么设计
设计原则
- Key 的命名:
- 用冒号 : 分隔模块、功能和标识符,层次清晰,比如 coupon:stock:123。
- 尽量简洁,避免过长,减少内存占用。
- Value 的选择:
- 根据业务需求选类型:计数用 String,结构化数据用 Hash,排序用 ZSet,队列用 List,集合用 Set。
- 数据量小时用简单结构(如 String),量大时用复杂结构(如 ZSet)。
- 过期时间:
- 会话、锁等临时数据设过期(如 30 分钟或 10 秒),避免内存浪费。
- 缓存数据根据更新频率设过期(如 1 小时),配合定时刷新。
- 性能优化:
- 用 Lua 脚本操作库存,减少网络开销。
- 避免大 value(比如超长 List),必要时分片存储。
项目效果
在“智能点评系统”中,秒杀库存用 String 扣减,ZSet 实时更新排行榜,高并发下稳定运行;在“类外卖项目”中,菜品分类用 List 缓存,响应速度提升 80%。这些设计让 Redis 在性能和功能上都发挥了最大作用。
7.怎么保证redis数据不丢失
Redis 提供了两种持久化方式,我根据场景选择使用:
RDB(快照)
- 原理:RDB 是将某时刻的内存数据快照保存到磁盘,生成一个二进制文件(dump.rdb)。
- 配置:通过 save 配置触发条件,比如 save 900 1(900 秒内至少 1 次变更就保存)。
AOF(追加日志)
- 原理:AOF 记录每条写操作命令(比如 SET、ZADD),重启时通过重放命令恢复数据。
- 配置:我设置 appendonly yes 开启 AOF,appendfsync everysec(每秒同步一次磁盘),在性能和可靠性间取平衡。
混合持久化
- Redis 4.0 后支持 RDB 和 AOF 结合,我在项目中用了这个特性。启动时先加载 RDB 快照,再重放增量 AOF,既加快恢复速度,又减少丢失。
主从复制
- 原理:通过配置主从模式(Master-Slave),主节点实时同步数据到从节点,主节点宕机后从节点可接管。
哨兵模式(高可用)
- 原理:哨兵(Sentinel)监控主从节点状态,主节点宕机时自动切换从节点为主,并通知客户端更新连接。
总结
我通过持久化(RDB 和 AOF)、主从复制、哨兵模式和数据双写结合项目需求来保证 Redis 数据不丢失。具体用哪种方案取决于数据的关键性和性能要求。在“智能点评系统”和“类外卖项目”中,这些设计让我在高并发下既保证了性能,又最大限度降低了数据丢失风险。
8.如果主节点刚存放数据没有同步就挂了,这是从节点没有数据怎么办
结合持久化(AOF)恢复
- 方法:主节点开启 AOF 持久化(appendonly yes 和 appendfsync everysec),记录每条写命令。主节点挂了后,从节点提升为主时,用主节点的 AOF 文件同步数据。
- 实现:
- 主节点宕机后,找到其 AOF 文件。
- 将 AOF 文件传输到新主节点(原从节点)。
- 新主节点加载 AOF,重放命令恢复数据。
- 项目应用:在“智能点评系统”的秒杀场景中,我用 AOF 记录库存变更,即使主节点未同步,从节点也能通过 AOF 补齐数据。
9.如果活动很火爆,有 100 万 QPS 的访问量,只持续一分钟或五分钟,只有一个节点扛不住怎么处理?
1. 架构优化:分布式部署
单个节点无法承受 100 万 QPS,我会将 Redis 升级为分布式架构:
- Redis Cluster(集群模式)
- 方法:部署 Redis Cluster,将数据分片到多个节点。Redis Cluster 用 16384 个槽(slot)分配数据,每个节点负责一部分槽。
- 实现:假设有 10 个节点,每个节点处理 10 万 QPS,总容量达到 100 万 QPS。配置 cluster-enabled yes,用 CLUSTER MEET 连接节点,分片存储库存数据(如 coupon:stock:123)。
- 项目应用:秒杀库存分片到不同节点,客户端用一致性哈希访问对应节点。
- 优点:水平扩展,单节点压力分散,吞吐量线性提升。
- 不足:部署复杂,客户端需要支持集群协议。
-
主从复制 + 读写分离
方法:一个主节点写数据,多个从节点读数据,用负载均衡分担读请求。- 实现:部署 1 主 9 从,主节点写库存,从节点提供查询,配合 Nginx 或 HAProxy 做负载均衡。
- 项目应用:库存扣减走主节点,查询库存走从节点,减轻主节点压力。
- 优点:简单易部署,适合读多写少场景。
- 不足:主节点仍是瓶颈,写请求仍可能超载。
-
- 异步扣减库存
- 方法:用消息队列解耦库存扣减,同步返回“抢券中”,异步处理结果。
- 实现:请求到达后,写入 RabbitMQ,消费者集群(多节点)从 Redis 扣库存,成功后再通知用户。
- 项目应用:100 万 QPS 分散到队列,10 个消费者每秒处理 10 万,压力均摊。
- 优点:削峰填谷,单节点不超载。
- 不足:用户体验稍差,需轮询结果。
10.什么样的场景适合用消息队列
1. 高并发削峰
- 场景特点:瞬时流量激增,系统无法实时处理所有请求,需要平滑流量。
- 项目应用:在“智能点评系统”的秒杀优惠券功能中,活动火爆时可能短时间内达到几十万 QPS,单节点 Redis 或数据库扛不住。我用消息队列削峰,把抢券请求先写入队列,消费者异步扣减库存。
2. 异步处理
- 场景特点:某些操作耗时长但不需要实时返回结果,可以异步执行。
- 项目应用:在“类外卖项目”中,用户下单后需要发送通知(如短信或 WebSocket 推送),但通知不影响下单主流程。我用消息队列异步处理通知逻辑。
3. 系统解耦
- 场景特点:多个模块间强耦合,修改一个模块影响其他模块,需要松耦合设计。
- 项目应用:在“类外卖项目”的后台管理端,菜品更新后需要同步到用户端缓存。我用消息队列解耦更新逻辑。
4. 数据一致性保障
- 场景特点:分布式系统中需要保证多数据源一致性,同步操作风险高。
- 项目应用:在“智能点评系统”的秒杀中,库存扣减要同时更新 Redis 和数据库。我用消息队列确保一致性。
5. 任务调度
- 场景特点:需要定时或延时执行某些任务,逻辑复杂时不适合直接用定时器。
- 项目应用:在“类外卖项目”中,订单超时未支付要自动取消。我用消息队列的延时功能处理。
- 实现:下单时将订单 ID 写入延时队列(RabbitMQ 插件支持延时),设置 15 分钟后投递,消费者检查状态并取消超时订单。
11.缓存怎么保持与数据库的一致性
先更新后删除,延迟双删。
12.redis在使用锁的过程中怎么避免死锁的
1. 设置锁的过期时间
- 问题:如果获取锁的进程在释放锁前崩溃,锁可能永远不释放,导致其他进程无限等待,形成死锁。
- 解决方法:用 Redis 的 SET 命令加 EX 参数设置锁的过期时间。
2. 锁的正确释放(避免误删)
- 问题:如果不检查锁的所有者,进程可能释放别人的锁,导致死锁或并发问题。
- 解决方法:用 Lua 脚本确保只有锁的持有者能释放锁。
- 项目应用:释放锁时,我用 Lua 脚本检查 value 是否匹配
13.怎么设计高并发架构
流量入口层:
1.CDN + 静态化,缓存静态数据信息
2.用 Nginx 配置每秒限制请求量
3.用 Nginx负载均衡分配到多个springboot节点
业务处理层:异步与解耦
通过消息队列异步处理主流程中影响性能的逻辑
数据存储层:分布式与优化
Redis 分布式
详细实现:
- 部署 Redis Cluster,10 个 master 节点,每节点 3GB 内存。
- 数据分片:16384 个槽均分,coupon:stock:123 按哈希落入某节点。(16384 个槽除以 10 个节点,每个节点平均负责 1638 个槽)
-
哈希计算:
- Redis 用 CRC16 算法计算 key 的哈希值,然后对 16384 取模,得到槽号。
- 公式:slot = CRC16(key) % 16384
数据库分库分表
MySQL 主从:1 主 3 从,主写从读。
缓存预热:秒杀开始前,库存预热到 Redis,用户请求不穿透数据库。
容错与恢复层:高可用与降级:用 Sentinel 配置熔断:超时 500ms 或错误率 50% 触发。
Java基础八股
1. Java 里面 String 类型,它能不能继承?
回答:
Java 中的 String 类是不能被继承的,因为它被设计为 final 类。
JVM八股
1.JVM,你有一些了解吗?
是的,我对 JVM(Java 虚拟机)有一定了解,主要是它的内存结构、垃圾回收和运行时机制。
JVM 的核心组成:
- 内存结构:
- 堆(Heap):存放对象实例,是垃圾回收的主要区域。我用 -Xmx 和 -Xms 设置堆大小,比如 -Xmx4g -Xms4g。
- 栈(Stack):每个线程有独立栈,存局部变量和方法调用信息。秒杀接口线程多时,我会调小 -Xss(如 256k)节省内存。
- 方法区(Method Area):存类信息、常量池等,JDK 8 后用元空间(Metaspace)实现,调 MaxMetaspaceSize 控制。
- 程序计数器(PC Register):记录线程执行的字节码位置,线程私有。
- 本地方法栈:支持 native 方法调用。
-
垃圾回收(GC):
-
分代回收:堆分新生代(Eden、Survivor)和老年代,用 -XX:+UseG1GC 启用 G1 垃圾回收器,适合高吞吐场景。
-
2.类加载的过程
类的生命周期:
类加载有 7 个阶段:加载(Loading) → 验证(Verification) → 准备(Preparation) → 解析(Resolution) → 初始化(Initialization) → 使用(Using) → 卸载(Unloading)。
加载:
- 通过类加载器读取 .class 文件到内存,生成 Class 对象。
验证:
- 检查字节码格式、权限等,确保安全。比如验证 final 类不可继承。
准备:
- 为静态变量分配内存,赋默认值(int 给 0,对象给 null)。
解析:
- 将符号引用转为直接引用,比如把字符串常量池的引用解析为内存地址。
初始化:
- 执行静态代码块和静态变量赋值,比如 count 变为 10。
工作流程(双亲委派模型):
- 类加载器:
- Bootstrap ClassLoader:加载核心库(rt.jar,如 String)。
- Extension ClassLoader:加载扩展库(jre/lib/ext)。
- Application ClassLoader:加载应用 classpath 的类。
3.垃圾回收机制知道吗?说一下原理,以及目前这边里面一些比较主流的一些回收器?
回答:
它是 JVM 自动管理内存的核心功能。垃圾回收的本质是识别并清理不再使用的对象,释放内存,防止内存泄漏。它的原理可以分为三个步骤:标记(找垃圾)、回收(清内存)、整理(可选)。
1.1 如何判断对象是垃圾?
可达性分析(主流方法):
- 原理:从 GC Roots(根对象)出发,沿着引用链遍历,能到达的对象是“活的”,未到达的是“垃圾”。
GC Roots 包括:
- 栈中的局部变量(如方法里的对象引用)。
- 方法区中的静态变量和常量。
- 本地方法栈中的 JNI 引用。
回收时机:
- Minor GC:新生代满时触发,回收 Eden 和一个 Survivor。
- Major GC/Full GC:老年代满或显式调用 System.gc() 时触发,清理整个堆。
2. 主流垃圾回收器
2.1 Serial(串行回收器)
- 原理:
- 单线程回收,标记-清除-整理全由一个线程完成,回收时暂停所有用户线程(STW,Stop The World)。
-
新生代用复制算法,老年代用标记-整理。
2.2 Parallel Scavenge + Parallel Old(并行回收器)
- 原理:
- 多线程并行回收,关注吞吐量(运行时间占比)。
- 新生代用复制算法,老年代用标记-整理。
2.3 CMS(Concurrent Mark Sweep,并发标记清除)
- 原理:
- 老年代回收器,尽量与用户线程并发执行,减少 STW。
- 步骤:初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除。
- 用标记-清除算法,不整理碎片。
2.4 G1(Garbage First)
- 原理:
- 将堆分成多个 Region(默认 2048 个),优先回收垃圾最多的区域。
- 新生代用复制,老年代用标记-整理,整体并发执行。
现代收集器(如CMS、G1)在标记阶段常用三色标记法:对象分为白色(未访问)、灰色(待检查)、黑色(已确认存活)。
JAVA多线程并发
1. 什么是线程安全?
线程安全是指在多线程环境下,多个线程同时访问共享资源时,程序仍能正确执行,不会出现数据不一致或意外行为。
如何实现:
- 加锁(如 synchronized)、使用线程安全类(如 ConcurrentHashMap)、避免共享状态。
2.Java 里面有哪些加锁的方式?
1.synchronized 关键字
同步方法:锁住整个对象。
同步块:锁住指定对象。
JVM 通过 monitor(监视器)实现,进入时获取锁,退出时释放。
2.ReentrantLock(可重入锁)实现了Lock
原理:
- 基于 AQS(AbstractQueuedSynchronizer),支持公平锁和非公平锁。
- 可重入:同一线程可多次获取锁,计数器加 1。
3. volatile(间接加锁)
原理:
- 保证可见性和有序性(禁止指令重排序),但不保证原子性。
Java 加锁有 synchronized(简单)、ReentrantLock(灵活)、和 volatile(轻量)
功能特性
- synchronized:
- 可重入:支持同一线程多次获取锁(如递归调用)。
- 不可中断:线程阻塞时无法响应中断(Thread.interrupt() 无效)。
- 无条件变量:只能用 wait() 和 notify(),控制粗糙。
- 公平性:非公平锁,先到先得。
- ReentrantLock(Lock 的实现):
- 可重入:同 synchronized,基于 AQS 的计数器实现。
- 可中断:支持 lockInterruptibly(),线程可被中断退出。
-
条件变量:支持 Condition(如 newCondition()),可精确控制多个等待队列。
-
支持超时(tryLock)
3.死锁的必要条件有哪些?
- 互斥条件(Mutual Exclusion)
- 定义:资源只能被一个线程独占,其他线程无法同时访问。
- 例子:秒杀中,锁 lock:coupon:123:1001 被线程 A 持有,线程 B 无法获取。
- 解释:这是锁的基本特性,无法避免。
- 请求与保持条件(Hold and Wait)
- 定义:线程持有至少一个资源,同时请求其他资源。
- 例子:线程 A 持有订单锁 lock:order:1,请求库存锁 lock:stock:123;线程 B 反之。
- 解释:多资源竞争时常见。
- 不可抢占条件(No Preemption)
- 定义:资源不能被强制剥夺,只能由持有者主动释放。
- 例子:线程 A 的锁 lock:order:1 只能等 A 释放,B 无法抢占。
- 解释:锁机制默认如此。
- 循环等待条件(Circular Wait)
- 定义:线程间形成环路,每个线程等待下一个线程的资源。
- 例子:A 等 B 的 lock:stock:123,B 等 A 的 lock:order:1,形成闭环。
- 解释:这是死锁的直接表现。
避免死锁:
- 设置超时(如 ReentrantLock.tryLock)。
- 统一资源获取顺序。
- 用原子操作(如 Redis SETNX)减少锁使用。
4. 说一下 ThreadLocal
ThreadLocal 是 Java 提供的一种线程局部变量工具,用于在多线程环境下为每个线程提供独立的变量副本,避免共享资源竞争。
ThreadLocal 的实现原理基于线程内部的存储结构,核心是每个线程持有一个 ThreadLocalMap,用它管理线程局部变量。
ThreadLocalMap 的细节:
- 键值对:键是 ThreadLocal 实例(弱引用),值是用户设置的对象。
- 哈希表:用开放定址法解决冲突,数组长度是 2 的幂,扩容时翻倍。
- 弱引用:键用 WeakReference,方便 GC 回收 ThreadLocal 对象。
4.1ThreadLocal 会内存泄露,原因是什么?
键的弱引用设计:
- ThreadLocalMap 的键(ThreadLocal 对象)是弱引用,当外部不再引用 ThreadLocal(如静态变量销毁),GC 会回收键。
- 但值(用户设置的对象)是强引用,即使键被回收,值仍留在 map 中,无法被 GC 清理。
未手动清理:
- 如果不调用 remove(),线程复用时,旧值一直占用内存,形成泄漏。
5.了解过线程安全容器吗?
Vector Hashtable ConcurrentHashMap ConcurrentLinkedQueue BlockingQueue
6.说一下 ConcurrentHashMap
实现原理(以 JDK 8 为例):
- 数据结构:
- 底层是数组 + 链表 + 红黑树。
- 数组存 Node(键值对),链表解决冲突,链表超 8 个转为红黑树。
- 并发控制:
- 读操作:无锁,直接访问数组,靠 volatile 保证可见性。
- 写操作:
- 用 CAS(Compare-And-Swap)+ synchronized 组合。
- 锁粒度是数组的每个槽(bucket),只锁当前槽,不影响其他槽。
- 扩容:多线程协作,动态调整数组大小(默认 16,扩到 32)。
-
默认容量 16,负载因子 0.75,扩容时数组翻倍。(数组被使用)
-
-
数组存 Node(键值对),链表解决冲突,链表超 8 个转为红黑树。
7.为什么重写 equals 一定要写 hashCode
- 默认行为:
- Object 的 hashCode 返回对象的内存地址(或类似唯一标识),两个内容相同但地址不同的对象,hashCode 不同。
- 如果只重写 equals 判断内容相等,hashCode 仍基于地址,两者不一致。
8. Java 里面有哪些创建线程的方法?
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口(结合 Future)
使用线程池(间接创建)
总结:
- 继承 Thread 和实现 Runnable 是基础方式,Callable 适合有返回值的任务,线程池是生产环境主流。我在项目中多用 Runnable 和线程池,兼顾灵活性和性能。
写一个代码可以两个线程交替打印1-10
public class AlternatePrint {
private static final Object lock = new Object(); // 锁对象
private static int number = 1; // 当前数字
private static final int MAX = 10; // 最大值
public static void main(String[] args) {
Thread threadA = new Thread(new PrintTask("Thread-A", 1));
Thread threadB = new Thread(new PrintTask("Thread-B", 0));
threadA.start();
threadB.start();
}
static class PrintTask implements Runnable {
private final String name;
private final int flag; // 0 或 1,表示轮到谁打印
public PrintTask(String name, int flag) {
this.name = name;
this.flag = flag;
}
@Override
public void run() {
while (number <= MAX) {
synchronized (lock) {
// 判断是否轮到自己打印
if (number % 2 == flag) {
System.out.println(name + ": " + number);
number++;
lock.notify(); // 唤醒另一个线程
} else {
try {
lock.wait(); // 等待,释放锁
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}
}
}
9.线程池的参数有哪些
int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
线程池的拒绝策略
ThreadPoolExecutor.AbortPolicy
:抛出RejectedExecutionException
来拒绝新任务的处理。(直接拒绝)ThreadPoolExecutor.CallerRunsPolicy
:调用执行者自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。(让执行者调用自己的线程)ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。(直接丢弃)ThreadPoolExecutor.DiscardOldestPolicy
:此策略将丢弃最早的未处理的任务请求。(丢弃最早未处理的任务)
线程池中的阻塞队列有哪些
1.LinkedBlockingQueue(有界阻塞队列,默认容量 Integer.MAX_VALUE)
特点:基于链表实现,默认容量是 Integer.MAX_VALUE,几乎可以看作无界队列,但也可以手动指定容量。
2. SynchronousQueue(同步队列)
特点:没有容量,不存储任务。提交任务时,如果有空闲线程就直接交给线程处理;如果没有,就创建新线程。
3. DelayedWorkQueue(延迟阻塞队列)
特点:基于堆实现,任务按延迟时间排序,每次出队的是延迟时间最短的任务。容量满了会自动扩容(增加 50%),最大可达 Integer.MAX_VALUE,不会阻塞。
4. ArrayBlockingQueue(有界阻塞队列)
- 特点:基于数组实现,容量固定,初始化后不可修改。任务满了就触发拒绝策略。
常见的线程池:
FixedThreadPool
:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。SingleThreadExecutor
: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。CachedThreadPool
: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。ScheduledThreadPool
:给定的延迟后运行任务或者定期执行任务的线程池。
如何设置线程池的线程数大小
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
MySQL
1.说一下数据库里面的那个 ACID
A - 原子性(Atomicity)
- 定义:事务的所有操作要么全成功,要么全失败,回滚到初始状态。
- 实现:通过 undo log 记录操作前状态,失败时回滚。
C - 一致性(Consistency)
- 定义:事务执行前后,数据库从一个一致状态转移到另一个一致状态,满足约束(如库存非负)。
- 实现:靠数据库的约束(主键、外键)和事务逻辑保证。
I - 隔离性(Isolation)
- 定义:多个事务并发执行时,互不干扰,每个事务像独立运行。
- 实现:通过锁(如行锁)或 MVCC(多版本并发控制)实现。
D - 持久性(Durability)
- 定义:事务提交后,数据永久保存,即使系统故障也不会丢失。
- 实现:通过 redo log 记录提交操作,宕机后恢复。
2.索引它的一个原理你有了解吗?
- 数据结构:
- B+ 树:一种多路平衡查找树,叶子节点存数据,非叶子节点存键值和指针。
- 特点:
- 所有数据在叶子节点,顺序排列,支持范围查询。
- 非叶子节点只存键,占用空间小,一次 IO 可加载更多索引。
- 工作过程:
- 普通查询:全表扫描,时间复杂度 O(n)。
- 索引查询:从 B+ 树根节点开始,按键值二分查找,定位叶子节点,复杂度 O(log n)。
- 存储:
- InnoDB 中,主键索引(聚簇索引)存整行数据,辅助索引存主键值,需回表。
3.索引创建的原则
回答:
创建索引需要权衡查询性能和维护成本,我在“智能点评系统”和“类外卖项目”中总结了以下原则:
1.高选择性字段(性别就不是)
- 原则:索引字段应区分度高,避免重复值过多。
2.频繁查询的字段
4.说一下最左前缀匹配原则
回答:
最左前缀匹配是 B+ 树索引的一个特性,指在使用联合索引时,查询条件必须从索引的最左列开始匹配。
5.什么时候索引失效
“联合索引在查询时是否有效取决于是否遵循最左前缀原则。有效的情况是查询条件从索引第一列开始连续匹配,比如索引(a, b, c),WHERE a=1、WHERE a=1 AND b=1都能用上,范围查询或排序如WHERE a>0 ORDER BY b也有效,因为a在前。无效的情况包括:
1) 跳过第一列,如WHERE b=1,索引按a排序,b无法定位;
2) 中间断开,如WHERE a=1 AND c=1,跳过b后c失效;
3) 对列使用函数,如WHERE UPPER(a)='1',破坏排序;
4) 类型转换,如字符串a比整数。举例:表有索引(a, b, c),SELECT * FROM t WHERE a=1 AND b=1有效,EXPLAIN显示用索引;但SELECT * FROM t WHERE b=1无效,全表扫描。联合索引设计要匹配常见查询条件,确保最左列被使用。”
Spring
1.说一下 Spring IOC
Spring IOC(Inversion of Control,控制反转)是 Spring 框架的核心机制,传统方式中对象由程序主动创建(如 new Service()),IOC:将对象创建和管理的控制权交给 Spring 容器,程序通过依赖注入(DI)获取对象。
核心思想:反转控制,开发者只定义依赖关系,容器负责实例化和注入。
- 依赖注入:
- 构造器注入:通过构造函数传入依赖。
- Setter 注入:通过 setter 方法注入。
- 注解注入:用 @Autowired 或 @Resource 自动装配。
- 配置文件:XML(早期)或注解(如 @Component、@Bean)定义 Bean。
2. 说一下 @Resource 和 @Autowired 区别
@Autowired 是 Spring 的主力注解,灵活支持构造器注入;@Resource 是标准注解,按名称注入更直观。
3.bean的生命周期
总体来说分为实例化、属性注入、初始化、销毁这几个步骤:具体如下
1. 解析xml配置或者注解的类,得到BeanDefinition,通过BeanDefinition反射创建Bean对象。
2. 对Bean对象进行属性填充
3. 回调实现Aware接口的方法,比如BeanNameAware
4. 调用BeanPostProcessor的初始化前方法,调用init初始化方法,调用BeanPostProcessor的初始化后方法,会进行AOP。
5. 将创建的Bean对象放入一个Map,业务中使用Bean对象。
6. Spring容器关闭时调用DisposableBean的destory方法。