Redis
写时复制
- 是什么:Redis在持久化(
bgsave)或进行主从复制时,父进程(服务器进程)与子进程(RDB持久化进程)共享内存中的数据结构。只有在父进程接收到写命令并需要修改某块数据时,才会将被修改的数据页复制一份,然后在副本上进行修改。这是一种乐观的锁策略,旨在减少不必要的内存复制和提升性能。 - 为什么:
-
- 性能:极大减少了
fork()子进程时的阻塞时间和内存开销。fork()本身会复制进程页表,而非物理内存,本身已经很快。COW机制确保了只有在真正需要时才复制数据。 - 保证数据一致性:
bgsave子进程看到的是fork()那一刻的内存快照,之后父进程的修改不会影响子进程持久化的数据,从而保证了RDB文件数据的完整性。
- 性能:极大减少了
- 详细过程:
-
- 父进程调用
fork(),创建子进程。子进程与父进程共享同一份物理内存数据。 - 子进程开始将内存数据写入临时RDB文件。
- 在此期间,如果父进程接收到新的写命令(如
SET,DEL),CPU的内存管理单元会检测到该内存页是只读的(被父子进程共享)。 - 操作系统会触发页错误(Page Fault) ,将目标内存页复制一份新的副本给父进程,父进程在新的副本上进行修改。子进程依旧读写原来的数据页。
- 父进程调用
- 注意事项:
-
- 内存消耗:如果父进程在子进程持久化期间有大量写操作,会导致大量内存页被复制,内存使用量可能会膨胀至原来的两倍。这是Redis内存峰值评估的重要考量点。
- Linux大页(Huge Page) :需禁用。因为大页的单位是2MB,远大于4KB的常规页,轻微的写操作就会触发复制2MB的大块内存,反而会降低性能和增加内存消耗。
IO多路复用
- 是什么:一种同步IO模型,单个线程可以同时监听多个文件描述符(Socket连接)的读写事件。当某些描述符就绪(有数据可读或可写)时,应用程序再对其进行读写操作。
- 为什么:Redis是单线程处理命令(6.0后多线程仅用于网络IO,命令执行仍是单线程)。为了能高效处理数万、数十万的并发连接,必须使用IO多路复用技术,避免为每个连接创建一个线程的巨大开销。
- Redis的实现演进:
-
select->poll->epoll(Linux) ->kqueue(BSD/Mac)。Redis会优先选择性能最高的实现。epoll****的优势:
-
-
- 事件驱动:无需像
select/poll那样轮询所有文件描述符,而是通过回调机制只关注活跃的连接。 - 无数量限制:
epoll能监听的连接数远大于select(默认1024)。 - 高效的内存拷贝:
epoll_wait返回时只返回就绪的文件描述符列表,而select/poll需要传递整个描述符集合给内核,内核再完整拷贝回用户态。
- 事件驱动:无需像
-
- 工作流程:
-
- 监听套接字(监听端口)和已连接套接字都被注册到
epoll实例中。 - 主线程阻塞在
epoll_wait系统调用上。 - 当有连接到来或已有连接数据可读时,
epoll_wait返回这些就绪的事件。 - 主线程依次处理这些事件:接受新连接、读取命令、解析、执行、将回复写入缓冲区、监听可写事件并将回复发送出去。
- 监听套接字(监听端口)和已连接套接字都被注册到
RDB&AOF
- RDB (Redis Database)
-
- 机制:在指定时间间隔生成内存数据的二进制快照文件(dump.rdb)。
- 触发方式:
save(阻塞),bgsave(后台fork子进程,COW机制), 自动化配置(save m n)。 - 优点:
-
-
- 紧凑的二进制文件,体积小,加载速度快,非常适合灾难恢复和全量备份。
- 对性能影响小(
bgsave方式)。 - 最大化Redis性能,父进程无需进行任何磁盘IO。
-
-
- 缺点:
-
-
- 无法做到秒级甚至更细粒度的持久化,最后一次RDB后的数据有丢失风险。
bgsave时fork()操作,如果数据量巨大,可能导致父进程阻塞(虽短但需注意),且内存膨胀。
-
- AOF (Append Only File)
-
- 机制:记录每一个写命令到日志文件末尾。重启时重新执行AOF文件中的所有命令来恢复数据。
- 写回策略 (appendfsync) :
-
-
- always:同步写回,每个命令都fsync到磁盘。最安全,性能最差。
- everysec(默认):每秒批量fsync一次。兼顾性能和安全,最多丢失1秒数据。
- no:由操作系统决定何时刷盘。性能最好,但丢失数据风险最高。
-
-
- AOF重写 (Rewrite) :为了解决AOF文件膨胀问题。
bgrewriteaof命令会fork子进程,根据当前数据库状态,逆向出一条能重建当前数据的最精简命令集(如一个list最终状态由100条LPUSH形成,重写为1条LPUSH ... all elements ...),写入新的AOF文件,最终替换旧文件。 - 优点: durability。支持多种策略,默认配置下最多丢失1秒数据。
- 缺点:文件通常比RDB大,恢复速度慢。
everysec模式下性能仍很高,但always模式性能有下降。
- AOF重写 (Rewrite) :为了解决AOF文件膨胀问题。
- 混合持久化 (4.0+) :
aof-use-rdb-preamble yes。在AOF重写时,子进程将当前数据以RDB格式写入AOF文件头部,后续的写命令再以AOF格式追加。结合了RDB的快速加载和AOF的增量持久化的优点,是当前生产环境的推荐做法。
Bitmap
- 是什么:本质上是String类型,但提供了一套面向位的操作命令。可以将Stringvalue视为一个由比特位构成的数组,并能对数组中任意偏移量的比特位进行置位(setbit)、清零(getbit)、计数(bitcount)等操作。
- 内存计算:一个bit位占1bit。存储10,000,000个用户的签到状态,只需要
10^7 bit ≈ 1.19 MB。 - 应用场景:
-
- 用户签到统计:
SETBIT uid:sign:yyyyMM 日期偏移量 1。 - 活跃用户统计:
SETBIT active:yyyy-MM-dd userId 1,后续可对多日做BITOP OR运算求并集。 - 布隆过滤器 (Bloom Filter) :基于Bitmap实现,用于大规模数据的是否存在的快速判定(可能存在误判,但绝不会错杀)。
- 用户签到统计:
HyperLoglog
- 是什么:一种用于基数统计(统计一个集合中不重复元素的个数)的概率算法。在Redis中实现为一种数据结构。
- 特点:
-
- 极其省内存:统计1亿个不重复元素,仅需约12KB内存(每个HyperLogLog键),且固定大小。
- 不是100%精确:标准误差约为0.81%,足以应对绝大多数需要大概计数的场景(如UV统计)。
- 只能计数,不能取出元素。
- 命令:
PFADD(添加元素),PFCOUNT(获取基数估值),PFMERGE(合并多个HLL)。 - 应用场景:网站唯一访客(UV)统计、大型集合的近似基数统计。对于需要精确去重计数的场景,应使用
SET或Bitmap。
滑动窗口
Mysql
ACID
- A (Atomicity) 原子性:事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。由undo log保证。如果事务失败,InnoDB会使用undo log将数据回滚到事务开始前的状态。
- C (Consistency) 一致性:事务执行前后,数据库都必须保持一致性状态。例如,转账前后,总额不变。这是数据库的终极目标,由应用层、原子性、隔离性共同保证。
- I (Isolation) 隔离性:并发事务之间相互隔离,互不干扰。由锁机制和MVCC共同保证。标准SQL定义了4个隔离级别(读未提交、读已提交、可重复读、串行化)来解决脏读、不可重复读、幻读等问题。
- D (Durability) 持久性:事务一旦提交,其对数据的修改就是永久性的。由redo log保证。修改数据时,先写redo log再写磁盘(WAL)。即使宕机,重启后也能通过redo log重做来恢复已提交的事务。
乐观锁+悲观锁
- 乐观锁: version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
深度分页
- 子查询: 通过子查询利用主键索引先查出需要的20个ID, 然后外层只需要回表20条数据
SELECT *
FROM orders
INNER JOIN (
-- 子查询:只利用索引快速找到满足条件的id
SELECT id
FROM orders
WHERE ... -- 可以添加查询条件
ORDER BY id DESC -- 确保该字段有索引
LIMIT 1000000, 20
) AS tmp USING (id);
- 覆盖索引
事务失效的场景
- 异常被catch
- 抛出非运行时异常
- 未被spring管理
- final or private修饰
- 大事务
- 本身不支持
- 同类中方法内部调用
索引
- B+Tree结构:
-
- 非叶子节点:只存储键值和子节点的指针,不存储数据。这使得一个页能存放更多的索引项,树的高度更低,查询IO次数更少。
- 叶子节点:存储所有的键值和对应的行数据(聚簇索引)或主键ID(非聚簇索引) 。所有叶子节点通过指针串联成一个双向链表,非常适合范围查询。
- 聚簇索引 (Clustered Index) :
-
- 表数据本身按主键顺序存储在B+Tree的叶子节点上。一张表只有一个聚簇索引。
- 优点:基于主键的查询非常快。
- 缺点:插入速度严重依赖于插入顺序(可能导致页分裂)。
- 非聚簇索引 (Secondary Index / 辅助索引) :
-
- 叶子节点存储的是该索引列的值和对应的主键值。
- 查询过程:回表。先通过辅助索引树找到主键值,再拿主键值到聚簇索引树中查找完整的行数据。
- 最左前缀原则:联合索引
(a, b, c),其生效方式为a,a,b,a,b,c。如果查询条件不包含a,则该索引基本无效。 - 索引优化:
-
- 覆盖索引 (Covering Index) :查询的列都包含在索引中,无需回表。
EXPLAIN的Extra字段会出现Using index。 - 索引下推 (Index Condition Pushdown, ICP) (5.6+):在索引遍历过程中,对索引中包含的字段先做判断,过滤掉不满足条件的记录,减少回表次数。
EXPLAIN的Extra字段会出现Using index condition。
- 覆盖索引 (Covering Index) :查询的列都包含在索引中,无需回表。
JAVA
什么情况会触发FullGC
调用 System.gc()
老年代空间不足: 大对象直接分配进老年代、年轻代晋升到老年代
担保
死锁必要条件
- 互斥条件
- 占用并等待, 至少持有一个锁并且不会放弃, 同时需要获取另外一个锁时被阻塞
- 不可剥夺
- 循环等待
JVM内存结构
简单记忆:
- 线程私有的:程序计数器、两个栈(JVM 栈和本地方法栈)。
- 线程共享的:堆、方法区。
- 放什么东西:
-
new出来的对象和数组 -> 堆- 局部变量、方法参数 -> JVM 栈的栈帧里
- 类信息、静态变量、常量 -> 方法区
- 程序执行到哪了 -> 程序计数器
理解 JVM 内存结构是进行 JVM 性能调优(如设置堆大小 -Xmx, -Xms,设置元空间大小 -XX:MetaspaceSize 等)和排查内存溢出问题的基础。
JMM内存模型
- 注意区分:JVM内存结构(Runtime Data Areas:堆、栈等,是物理分区)和 Java内存模型(JMM,是规范,定义了线程如何与内存交互)。
- 核心概念:
-
- 主内存 (Main Memory) :所有共享变量都存储于此。
- 工作内存 (Working Memory) :每个线程都有自己的工作内存,存储该线程使用到的变量的主内存副本。
- 交互协议:线程不能直接读写主内存的变量,只能操作自己工作内存中的变量,然后再同步回主内存。JMM通过8种内存访问操作(lock, unlock, read, load, use, assign, store, write)定义了工作内存和主内存之间交互的细节。
- 目的:解决多线程环境下,通过可见性、原子性、有序性的保障,来屏蔽各种硬件和操作系统的内存访问差异,实现“一次编写,到处运行”的并发效果。
- happens-before原则:JMM定义的判断数据是否存在竞争、线程是否安全的核心规则。如程序次序规则、volatile变量规则(写先于读)、传递性等。
JVM类加载机制
- 过程:加载 -> 链接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载。
- 双亲委派模型 (Parents Delegation Model) :
-
- 工作流程:一个类加载器收到加载请求时,首先不会自己去加载,而是委派给父加载器去完成。只有当父加载器反馈无法完成时(在自己的搜索范围内没找到),子加载器才会尝试自己加载。
- 层次结构:Bootstrap ClassLoader (加载jre/lib/rt.jar) -> Extension ClassLoader (加载jre/lib/ext/*.jar) -> Application ClassLoader (加载classpath下的类) -> 自定义ClassLoader。
- 优点:
-
-
- 避免类重复加载:保证Java核心类的唯一性和安全性(如自定义的
java.lang.Object不会被加载)。 - 安全:防止核心API被篡改。
- 避免类重复加载:保证Java核心类的唯一性和安全性(如自定义的
-
- 打破双亲委派:
-
- 场景:如JDBC SPI(
DriverManager需要加载不同厂商的实现)、Tomcat(为每个Web应用提供独立的类加载器以实现隔离和热部署)。 - 方式:重写
ClassLoader的loadClass方法可以打破,但通常推荐重写findClass方法,这样既保持了双亲委派,又能自定义加载路径。
- 场景:如JDBC SPI(
G1 & CMS
- CMS (Concurrent Mark Sweep) - 老年代收集器
-
- 目标:获取最短回收停顿时间。
- 过程:
-
-
- 初始标记 (STW) :标记GC Roots能直接关联到的对象。速度极快。
- 并发标记:从GC Roots开始进行可达性分析。与用户线程并发执行,耗时最长。
- 重新标记 (STW) :修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。比初始标记稍长,但远短于并发标记。
- 并发清除:清理死亡对象。与用户线程并发执行。
-
-
- 缺点:
-
-
- 对CPU资源敏感:并发阶段会占用线程,导致应用吞吐量降低。
- 无法处理“浮动垃圾” :并发清理阶段用户线程还在运行,可能产生新的垃圾,本次GC无法处理,只能留到下次。
- 会产生空间碎片:标记-清除算法导致。可能触发Full GC进行压缩。
-
- G1 (Garbage First) - 全堆收集器
-
- 核心思想:将堆内存划分为多个大小固定的Region(不再是物理分代),优先回收垃圾价值最大的Region(Garbage First)。
- 过程:
-
-
- Young GC:Eden区满时触发,采用复制算法将存活对象拷贝到Survivor区或Old区。
- Mixed GC:老年代占用达到阈值(
-XX:InitiatingHeapOccupancyPercent)时触发。它不仅回收年轻代,还会优先回收一部分垃圾最多的老年代Region。这是G1的核心。 - Full GC (Serial GC) :当Mixed GC的速度跟不上对象分配的速度时,退化为Serial Old GC,是单线程的,必须避免。
-
-
- 优势:
-
-
- 可预测的停顿时间:通过
-XX:MaxGCPauseMillis设置目标停顿时间,G1会尽力达成。 - 空间整合:从整体看是基于“标记-整理”算法,Region之间是复制算法,都不会产生碎片。
- 更精细的控制。
- 可预测的停顿时间:通过
-
HashMap
- 数据结构:数组 + 链表 + 红黑树 (JDK8+)
- 核心参数:
-
capacity:数组长度,总是2的幂。loadFactor:负载因子,默认0.75,决定了扩容时机。threshold:扩容阈值,capacity * loadFactor。
- PUT流程:
-
- 计算key的hash值:
(h = key.hashCode()) ^ (h >>> 16)(高位参与运算,减少哈希冲突)。 - 判断数组是否为空,是则扩容。
- 计算数组下标:
i = (n - 1) & hash。 - 如果当前位置为空,直接插入。
- 不为空,则判断:
- 计算key的hash值:
-
-
- key是否相同(
==或equals),是则覆盖value。 - 当前节点是否是树节点,是则调用红黑树插入。
- 否则遍历链表,尾插。若链表长度>=8且数组长度>=64,则树化;否则只是扩容。
- key是否相同(
-
-
- 插入后,判断size是否超过threshold,超过则扩容(2倍)。
- 扩容机制:
-
- 创建新数组(2倍大小)。
- 遍历旧数组的每个位置,重新计算节点在新数组中的位置。JDK8优化:节点的新位置要么是原下标
i,要么是i + oldCap(利用hash值新增参与运算的那一位是0还是1),避免了JDK7的重新hash计算。
ThreadLocal
- 是什么:提供线程局部变量。每个线程都有一个该变量的独立副本,互不干扰。
- 原理:
-
- 每个
Thread对象内部都有一个ThreadLocalMap类型的变量threadLocals。 ThreadLocalMap的key是弱引用的ThreadLocal实例,value是设置的副本值。ThreadLocal.set()/get()操作的是当前线程的threadLocals。
- 每个
- 内存泄漏根源:
-
- key是弱引用,会在GC时被回收,但value是强引用。
- 如果
ThreadLocal实例被回收(如置为null),那么ThreadLocalMap中就会出现key为null的Entry,其value永远无法被访问到,造成内存泄漏。
- 最佳实践:
-
- 必须手动调用
remove()方法清理Entry。 - 将
ThreadLocal变量声明为private static final,使其强引用始终存在,这样弱引用的key就不会被回收,从而能在get/set时探测到key为null的Entry并清理它。
- 必须手动调用
synchronized
- 锁升级过程(偏向锁 -> 轻量级锁 -> 重量级锁):
-
- 无锁:新对象。
- 偏向锁:一段时间内只有一个线程访问。Mark Word中记录线程ID。执行CAS竞争锁,成功则进入偏向模式。
- 轻量级锁:有线程竞争偏向锁。线程在自己的栈帧中创建Lock Record,使用CAS自旋尝试将Mark Word指向自己的Lock Record。成功则获取锁。
- 重量级锁:轻量级锁自旋失败(或自旋次数超过阈值),锁膨胀。向操作系统申请互斥量(mutex),线程进入阻塞队列。Mark Word指向重量级锁(monitor对象)的指针。
- monitor对象:每个Java对象都与一个monitor关联(
ObjectMonitor,C++实现)。enter和exit指令对应获取和释放锁。
ReentrantLock
- 与synchronized对比:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现 | JVM层面,关键字 | JDK层面,API |
| 锁释放 | 自动释放 | 必须手动unlock(),通常在finally中 |
| 灵活性 | 基本,非公平 | 可公平/非公平,可尝试获取,可超时,可中断 |
| 条件队列 | 单个 | 可创建多个Condition |
- 核心原理AQS (AbstractQueuedSynchronizer) :
-
- state:volatile int,表示锁的状态。0表示未锁定,>0表示被重入次数。
- CLH队列:一个FIFO的双向队列,包装了等待线程。
- 获取锁:
lock()->acquire()->tryAcquire()(子类实现,尝试CAS修改state) -> 失败则入队并可能阻塞。 - 释放锁:
unlock()->release()->tryRelease()(子类实现) -> 唤醒后继节点。
ThreadPool
- 核心参数:
-
corePoolSize:核心线程数,即使空闲也会保留。maximumPoolSize:最大线程数。workQueue:任务队列。ArrayBlockingQueue(有界),LinkedBlockingQueue(无界),SynchronousQueue(不缓存,直接移交)。handler:拒绝策略。AbortPolicy(抛异常),CallerRunsPolicy(回退给调用者线程执行),DiscardPolicy(丢弃),DiscardOldestPolicy(丢弃队列最老任务)。
- 工作流程:
-
- 任务提交,如果运行线程数 < corePoolSize,创建新线程执行。
- 否则,尝试放入工作队列。
- 如果队列已满,且运行线程数 < maximumPoolSize,创建新非核心线程执行。
- 否则,触发拒绝策略。
- 注意事项:
-
- 线程池大小设置:CPU密集型:
Ncpu + 1;IO密集型:Ncpu * (1 + 平均等待时间/平均计算时间)。 - 资源耗尽风险:使用无界队列(如
LinkedBlockingQueue)可能导致任务无限堆积,最终OOM。 - 监控:可通过
ThreadPoolExecutor的钩子方法(beforeExecute,afterExecute)进行监控。
- 线程池大小设置:CPU密集型:
虚拟线程
零拷贝
Http
Rpc
- 核心思想:像调用本地方法一样调用远程服务。代理是核心。
- 核心模块:
-
- 客户端代理 (Stub) :动态代理,封装调用信息(接口、方法、参数)为消息。
- 序列化/反序列化:将消息对象 -> 二进制字节流。选型:Protobuf (高效), Kryo (Java, 更快), Hessian, JSON。
- 网络传输:通常基于Netty NIO。客户端发送序列化后的请求,服务端接收。
- 服务端代理 (Skeleton) :接收请求,反序列化,通过反射调用本地服务实现。
- 服务治理(微服务化RPC框架的核心):
-
-
- 服务注册与发现:ZooKeeper, Nacos, Consul。
- 负载均衡:Random, RoundRobin, 一致性Hash, 最少连接数。
- 容错:Failover(重试), Failfast(快速失败), Failsafe(忽略)。
- 熔断降级:Hystrix, Sentinel。
-
Kafka
- 架构:
-
- Producer:生产者,发送消息。
- Consumer:消费者,消费消息,主动pull模式。
- Broker:Kafka服务器节点。
- Topic:消息主题,逻辑概念。
- Partition:Topic的物理分片,每个Partition是一个有序的、不可变的消息序列。这是Kafka高吞吐和水平扩展的基础。
- Replica:Partition的副本,提供高可用。Leader负责读写,Follower同步数据。
- Consumer Group:一组消费者协同消费一个Topic,一个Partition只能被同一个Group内的一个Consumer消费(实现负载均衡)。
- 为什么快:
-
- 顺序IO:追加写日志文件,远快于随机IO。
- 零拷贝 (Zero-Copy) :使用
sendfile系统调用,数据直接从Page Cache通过DMA拷贝到网卡缓冲区, bypass用户态和CPU拷贝。 - 页缓存 (Page Cache) :直接利用操作系统的高速缓存,而不是JVM堆内存,减少GC压力且效率更高。
- 批量处理:Producer批量发送,Broker批量落盘,Consumer批量拉取。
- 消息可靠性:
-
- Producer端:设置
acks=all(或-1),确保所有ISR副本都确认才成功。 - Broker端:
min.insync.replicas(最小ISR数),防止只有一个Leader时宕机数据丢失。 - Consumer端:关闭自动提交
enable.auto.commit=false,手动在业务处理成功后异步提交偏移量。
- Producer端:设置