摘要:误判率公式: P ≈ (1 - e^(-kn/m))^k,其中n为元素数量。需权衡m(位数组大小)、k(哈希函数数量)和误判率。
布隆过滤器(Bloom Filter)
理想答案: 核心概念:概率型数据结构,用于高效判断元素是否在集合中,允许误判但绝不漏判。
原理:
使用一个长度为m的位数组和k个独立的哈希函数。
插入元素:对元素进行k次哈希,将对应位数组位置设为1。
查询元素:检查k个位置是否全为1。若全为1,则认为元素可能存在(可能误判);否则一定不存在。
特点:
优点:空间效率极高,插入和查询时间复杂度为O(k)。
缺点:存在误判率(False Positive),无法删除元素。
误判率公式: P ≈ (1 - e^(-kn/m))^k,其中n为元素数量。需权衡m(位数组大小)、k(哈希函数数量)和误判率。
应用场景:
缓存穿透防护(如redis缓存未命中时的快速过滤)。
分布式系统去重(如爬虫URL去重)。
pidstat
pidstat1. 输出概览
pidstat是 Linux 性能监控工具sysstat的一部分, 用于按进程(或任务)统计 CPU、内存、I/O 等资源使用情 况。 用户提供的输出片段显示了 3 个进程的实时 CPU 使用 指标,各列含义如下:2. 各列字段解析
| 列名 | 说明 | | | | | | UID | 进程所有者的用户 ID(0表示root1systemd进程)。 | | %usr | 进程在用户态(应用程序代码)消 耗的 CPU 百分比。 | | %system | 进程在内核态(系统调用、中断处 理等)消耗的 CPU 百分比。 | | %guest | 进程运行虚拟 CPU(如 KVM 虚拟机)消耗的 CPU 百分比(通常为 0)。 | | %wait | 进程因等待 I/O 或资源阻塞的时间占比(高值可能表示 I/O 瓶颈)。 | | %CPU | 进程的总 CPU 使用率(%usr + %system + %guest)。 | | CPU | 进程当前运行的 CPU 核心编号(如 | |1表示运行在 CPU 1 上)。 | | Command | 进程名称或命令行(如systemd、kthreadd)。 |5. 常用命令扩展
# 监控所有进程的 CPU 和内存(1 秒间隔)pidstat -u -r 1# 查看特定进程的 I/O 统计(如 PID 123)pidstat -d -p 123# 结合上下文切换监控(-w 显示线程切换)pidstat -w -u 1虚拟地址空间 + 页表映射 + MMU硬件检查 + 权限控制,共同确保进程内存的隔离性和安全性
问题11:InnoDB和MyISAM的区别
理想答案:
维度 InnoDB MyISAM 事务支持 支持事务(ACID) 不支持 锁机制 行级锁(默认),支持并发写 表级锁,并发性能差 外键 支持外键约束 不支持 崩溃恢复 通过redo log实现崩溃恢复 无,表易损坏需手动修复 索引结构 聚簇索引(数据与主键绑定存储) 非聚簇索引(数据与索引分离) 全文索引 MySQL 5.6+支持 原生支持(5.6前仅MyISAM支持) 存储文件 .ibd(数据+索引) .frm+.MYD+.MYI 适用场景 高并发写、事务需求(如核心业务表) 读多写少、无事务(如报表分析)
15. 更新MySQL时,怎么保证Redis和MySQL数据一 致
理想答案:
确保MySQL和Redis数据一致性是分布式系统中的常见 挑战,有几种主要策略:
1. 双写模式(Write Through)
写MySQL的同时更新Redis实现方式:在应用代码中,将更新操作同时写入MySQL和Redis优点:实现简单缺点:无法保证原子性,如果一个成功一个失败会 导致不一致// 伪代码示例@Transactionalpublic void updateData(Data data) { // 更新MySQLmySQLMapper.update(data); // 更新RedisredisTemplate.opsForValue.set(data.getId, data);}2. 失效模式(Cache Aside)
更新MySQL后,删除Redis中的对应缓存下次读取时再从MySQL加载到Redis优点:实现相对简单,减少写操作的一致性问题缺点:可能出现缓存穿透,首次读取性能降低@Transactionalpublic void updateData(Data data) { // 更新MySQLmysqlMapper.update(data); // 删除Redis对应缓存redisTemplate.delete(data.getId);}3. 消息队列+异步更新模式
更新MySQL后发送消息到队列消费者接收消息后更新Redis优点:解耦合,提高系统可用性缺点:复杂度增加,有短暂不一致窗口@Transactionalpublic void updateData(Data data) { // 更新MySQLmysqlMapper.update(data); // 发送消息到队列kafkaTemplate.send("cache-update-topic", JSON.toJSONString(data));}// 消费者@KafkaListener(topics = "cache-update-topic")public void consumeMessage(String message) { Data data = JSON.parseObject(message, Data.class); // 更新RedisredisTemplate.opsForValue.set(data.getId, data);}4. 数据库变更捕获(CDC)
使用工具如Canal监听MySQL binlog捕获数据变更后自动更新Redis优点:对应用透明,不侵入业务代码缺点:引入额外组件,增加系统复杂度5. 最终一致性保障措施
设置Redis缓存过期时间,兜底保证最终一致性实现定时任务,周期性校验并修复不一致数据记录操作日志,便于问题排查和数据修复6. 分布式事务方案
对于强一致性要求场景,考虑使用TCC或SAGA等分布式事务方案通过补偿机制确保数据一致性方案一致性强度实现复杂度系统侵入性性能影响适用场景双写模式中等低高写操作延迟增 加对一致性要求适中,写操作不频繁的场景失效模式弱最终一致低中首次读 延迟增加读多写少,容忍短暂不一致的场景消息队列+异步更新最终一致中高中写性能好,一致性有延迟高并发写入,允许短时不一致的场景CDC方案强最终一致高低轻微影响,取决于日志处理速度不想修改代码,对一致性要求较高的场景分布式事务强一致很高高显著降 低系统吞吐量金融交易等要求强一致性的核心业务常见组合: - 读多写少场景:失效模式 + 缓存TTL过期 - 高并发场景:消息队列 + 失效模式 - 对业务无侵入要求:CDC + 异步更新缓存
理想答案:
是的,TCP和UDP可以绑定同一个端口号,因为它 们使用不同的协议栈。详细解释如下:
协议区分机制:在IP协议层,数据包通过协议字段(Protocol Field)区分TCP(6)和UDP(17)操作系统网络栈根据协议类型将数据包分发到不同的处理模块 套接字的五元组标识:(协议, 源IP, 源端口, 目标IP, 目标端口),其中协议不同即为不同套接字操作系统支持:
现代操作系统允许TCP和UDP套接字绑定到相同的端口号当接收到数据包时,根据IP头部的协议字段区分应交给TCP还是UDP处理
21. IO多路复用的epoll,ET和LT模式区别
理想答案:
EPOLL是Linux下高效的I/O多路复用机制,提供了两种工作模式:水平触发(Level Triggered, LT)和边缘触发(Edge Triggered, ET)。这两种模式在事件通知机 制上有本质区别。
1. 水平触发(LT)模式
原理: - 只要文件描述符上的条件满足,就会一直触发通知 - 如socket有数据可读,每次epoll_wait都会返回这 个事件,直到数据被完全读取
特点: - 默认工作模式 - 事件可以被多次触发,直到条件不再满足 - 可以不一次性读完所有数据,下次调用epoll_wait 依然会通知 - 不会漏报事件,更容易编程
示例代码:
// LT模式(默认模式)struct epoll_event ev;ev.events = EPOLLIN; // 关注读事件ev.data.fd = sockfd;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);// 事件处理while (1) { nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); for (i = 0; i2. 边缘触发(ET)模式
原理: - 只有当状态发生变化时才触发通知 - 如socket从无数据变为有数据时触发一次,之后不 再触发直到新数据到来
特点: - 需要显式设置EPOLLET标志 - 事件只会触发一次,必须一次性处理完所有数据 - 通常需要结合非阻塞I/O使用,防止阻塞 - 效率更高,可能减少epoll_wait的调用次数 - 编程难度较大,容易出现漏读数据的bug
示例代码:
// 设置ET模式struct epoll_event ev;ev.events = EPOLLIN | EPOLLET; // 边缘触发模式ev.data.fd = sockfd;// 设置非阻塞模式,ET模式下必须使用非阻塞int flags = fcntl(sockfd, F_GETFL, 0);fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);// 事件处理while (1) { nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); for (i = 0; i 0) { // 处理数据...} // 正确处理EAGAIN错误,表示暂时没有更多数据if (n3. 两种模式的详细对比
特性水平触发(LT)边缘触发(ET)触发时机只要条件满足就触发仅状态变化时 触发一次事件处理要求可以分多次处理必须一次性处 理完全I/O模式要求可以使用阻塞或非阻塞强烈建议使用非阻塞编程难度简单,不容易出错复杂,需要正确 处理EAGAIN系统调用频率可能较高通常较低适用场景大多数一般应用高性能服务器,需 要最小化系统调用漏报风险几乎不存在如果编程不当可能漏处 理数据4. 性能与实践建议
性能考量:ET模式理论上性能更高,减少了epoll_wait的调 用次数高并发场景下,ET模式可以显著减轻系统负担 Nginx、Redis等高性能服务器多采用ET模式实践建议:
一般应用优先使用LT模式,编程简单不易出错选择ET模式时必须:设置非阻塞模式循环读取直到返回EAGAIN正确处理EAGAIN和EINTR等错误 结合使用EPOLLONESHOT可防止多线程下的竞态条 件
内核实现区别:
LT模式:每次epoll_wait检查所有就绪fdET模式:内核维护就绪列表,状态变化时才加入 列表,处理后移除
复习epoll服务端工作流程
1. 服务端启动阶段
int epfd = epoll_create(1024); // 创建epoll实例内核会创建一个eventpoll结构体,包含两个重要成员:rbr(红黑树根节点):存储所有监控的文件描述符rdllist(双向链表):存储就绪的文件描述符2. 创建并注册监听套接字
int ssfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字bind(ssfd, ...); // 绑定地址listen(ssfd, 128); // 开始监听struct epoll_event ev;ev.events = EPOLLIN; // 监控可读事件ev.data.fd = ssfd;epoll_ctl(epfd, EPOLL_CTL_ADD, ssfd, &ev); // 注册到epoll内核会将ssfd添加到红黑树中为ssfd设置回调函数ep_poll_callbackexplain
epfd (epoll文件描述符):这是epoll_create创建的实例,相当于一个"监控中心" 内核确实会创建eventpoll结构体,其中:rbr红黑树:以O(logN)复杂度快速查找/添加/删除被监 控的fdrdllist就绪链表:当fd有事件发生时,内核会将其加入这个链表ssfd (监听socket):
这是服务器用来接收新连接的"大门"socket创建后,经过bind和listen进入监听状 态通过epoll_ctl(epfd, EPOLL_CTL_ADD, ssfd, &ev)将其加入epoll监控 当有新连接到达时,这个fd会触发EPOLLIN事件
为什么需要这么多fd:
并发处理:现代服务器需要同时处理成千上万的客户端 连接,每个连接对应一个fd事件分离:监听fd专门接收新连接每个客户端连接有独立的通信fd高效管理:epoll的红黑树可以管理数十万fd就绪链表确保只处理有实际事件的fd,避免无谓遍历
3. 核心事件循环
while(1) { int nready = epoll_wait(epfd, events, MAX_EVENTS, -1); for(int i=0; iepoll_wait内部工作原理
检查rdllist链表是否为空:不为空:直接返回就绪事件 为空:进程进入睡眠,直到有事件发生当网卡收到数据,触发中断处理程序
内核协议栈处理数据包,将数据放入对应socket的接收缓冲区调用sock_def_readable唤醒等待进程这会触发预先注册的ep_poll_callback回调函数
ep_poll_callback关键操作
static int ep_poll_callback(...) { // 将就绪的fd添加到rdllist链表list_add_tail(&epi->rdllink, &ep->rdllist); // 唤醒等待的进程wake_up_locked(&ep->wq);}4. 处理新连接
当ssfd时:int cfd = accept(ssfd, ...); // 接受新连接ev.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // 监控新连接每个新连接cfd都会被添加到红黑树为cfd设置相同的回调函数5. 处理客户端数据
对于客户端套接字的可读事件:
int n = read(events[i].data.fd, buf, sizeof(buf));// 处理请求并发送响应write(events[i].data.fd, response, strlen(response));内核数据结构关系
eventpoll├── rbr (红黑树根节点)│ ├── 节点1: ssfd│ ├── 节点2: cfd1│ └── 节点3: cfd2└── rdllist (就绪链表) ├── cfd1(有数据到达) └── cfd2(有数据到达)性能优势体现
红黑树:查找、插入、删除时间复杂度都是O(logN) ,适合管理大量连接就绪链表:epoll_wait返回时直接遍历链表,无需扫描所有连接回调机制:只在事件发生时激活回调,避免轮询开销这种设计使得epoll能够高效处理数十万并发连接,是高性能服务器的核心机制。
红黑树(rbr)和就绪链表(rdllist) 的分工
红黑树的使用场景:管理所有注册的fd(通过epoll_ctl(EPOLL_CTL_ADD)添加时)快速查找/修改:当需要删除(EPOLL_CTL_DEL)或修改(EPOLL_CTL_MOD)某个fd的监控事件时,红黑树的O(logN) 查找复杂度能高效定位目标fd去重保障:红黑树的自平衡特性保证fd不会重复插入(相同fd多次ADD会返回EEXIST错误)就绪链表的使用场景:
临时存储活跃事件:当被监控的fd有事件发生时(如数据到达、连接关闭等),内核会将该fd从红黑树节点引用到就绪链表批量交付用户态:epoll_wait调用时直接将该链表内容拷贝到用户空间,实现O(1)事件获取天然时序性:链表保持事件发生的先后顺序(而红黑树是按键值排序的)
23. Redis中有10亿个key,10000个设置了过期时 间,Redis会每次遍历10亿个key看有没有过期吗?
理想答案:
不会。Redis采用了多种策略结合的方式来处理key的过期,避免了对全部key的遍历,保证了效率。
Redis过期key处理机制
Redis使用以下三种策略组合处理过期key:
惰性删除(Lazy Expiration):当客户端尝试访问一个key时,Redis会检查该key是否过期如果已过期,Redis会立即删除这个key并返回nil/null优点:不额外消耗CPU资源,按需处理 缺点:过期key长期未访问会占用内存空间定期删除(Periodic Expiration):
Redis维护一个过期字典(expires dict),专门 记录设置了过期时间的key定时任务以一定频率随机抽样这个字典中的key 进行检查关键点:Redis不会遍历所有key,而是采用抽样方式 采样策略如下:
默认每100ms执行一次过期检查每次随机检查20个设置了过期时间的key删除其中已过期的key如果超过25%的key已过期,立即再次执行检查,直到过期key比例低于25%或达到时间上限内存压力触发删除(Eviction):
当Redis内存使用接近maxmemory配置值时根据maxmemory-policy策略,可能优先清除过期的key
源码层面的实现
Redis专门维护expires字典用于记录key的过期时间:
typedef struct redisDb { dict *dict; /* 所有key的字典 */ dict *expires; /* 设置了过期时间的key 的字典 */ // 其他字段...} redisDb;定期删除的核心代码逻辑(简化版):
void activeExpireCycle(void) { int keys_per_loop = 20; // 每次检查20个keylong long start = mstime; do { int expired = 0; sampleKeysForExpiry(keys_per_loop, &expired); // 随机采样并处理过期keyif (expired > keys_per_loop/4) { // 如果超过25%的key过期,继续执行检查continue; } } while(mstime - start性能保障
Redis对于大量key的过期处理非常高效,原因如下: 1. 不遍历全量key,只遍历expires字典中的key(本 例中只有10000个) 2. 采用随机抽样方式,而非全量扫描 3. 惰性删除和定期删除相结合,分散过期key的处理 压力 4. 通过增量方式执行过期扫描,控制单次CPU时间消 耗
实际应用建议
Redis过期策略适合大规模场景,无需担心过期key 检查的性能问题合理设置过期时间,避免大量key同时过期(可引入随机时间差)监控Redis内存使用情况,确保有足够空间对于需要精确控制过期的场景,可考虑结合Redis的Sorted Set实现更精细的过期管理3. 对静态类、静态方法、静态变量的理解
理想回答应包括:
静态类
只能包含静态成员不能被实例化主要用于组织相关的静态方法和静态变量不能继承其他类,也不能被继承适用于工具类、辅助类等不需要维护状态的场景静态方法
属于类而非实例,使用类名直接调用不能访问或修改非静态成员不能使用this和super关键字编译期绑定(早绑定),不支持多态适用于不依赖对象状态的功能性操作静态变量
被类的所有实例共享,内存中只有一份在类加载时初始化,优先于对象的创建!生命周期与应用程序相同不管创建多少个对象,静态变量只占用一份内存适用于需要在多个对象间共享的常量或计数器使用场景与示例
public class MathUtils { // 静态变量public static final double PI = 3.14159; private static int operationCount = 0; // 静态方法public static double calculateCircleArea(double radius) { operationCount++; return PI * radius * radius; } // 静态方法访问静态变量public static int getOperationCount { return operationCount; }}// 使用double area = MathUtils.calculateCircleArea(5);int count = MathUtils.getOperationCount;优缺点
优点: - 提高性能(不需要创建对象) - 便于组织工具方法 - 方便访问(不需要实例化)
缺点: - 过度使用会破坏面向对象原则 - 静态方法难以测试和模拟 - 静态变量可能导致全局状态问题 - 不支持多态,降低灵活性
一、HashMap相关问题
1. 底层数据结构
JDK7及以前:数组 + 链表数组为 Entry,每个位置称为一个桶(Bucket),通过哈希值计算下标。 链表用于解决哈希冲突,冲突元素以头插法加入链表。JDK8及以后:数组 + 链表/红黑树
链表长度超过 8 且数组长度 ≥ 64 时,链表转为红黑树(查询时间复杂度从O(n)优化到O(log n))。树节点数量减少到 6 时退化为链表,避免频繁转换的性能损耗。
2. 哈希冲突解决
链地址法(拉链法) 所有哈希值相同的元素以链表或树结构存储在同一个桶中。优势:实现简单,空间利用率高。对比开放寻址法:无需担心探测序列导致性能波动,适合高并发场景。哈希函数优化(key == null) ? 0 : (h = key.hashCode) ^ (h >>> 16) 通过高位参与运算,减少哈希碰撞概率。
3. 容量与扩容机制
初始容量为2的次方计算索引公式:index = (n - 1) & hash,其中 n 为数组长度。 位运算效率远高于取模(%),且当 n 为2的次方时,(n - 1) 的二进制全为1,哈希值分布更均匀。 用户指定初始容量时,HashMap通过 tableSizeFor 方法找到最近的2的次方值(如输入10,实际初始化为16)。扩容机制
触发条件:元素数量 > 容量 × 负载因子(默认0.75)。扩容步骤:新容量 = 旧容量 × 2,保证始终为2的次方。JDK8优化:通过 hash & oldCap 判断元素是否需要移动到新位置(高位为1则移动,否则留在原位置)。数据迁移时,链表拆分为高低位链表,提升效率。
二、JVM内存模型
1. 内存区域划分
区域作用线程安全程序计数器记录当前线程执行的字节码指令地址(Native方法时为空)。线程私有虚拟机栈存储栈帧(局部变量表、操作数栈、方法出口等),每个方法对应一个栈帧。线程私有本地方法栈为Native方法服务,与虚拟机栈类似。线程私有堆存储对象实例和数组,GC主要管理区域。共享方法区存储类信息、常量、静态变量、JIT编译后的代码(JDK8后由元空间实现)。共享2. 线程安全区域
线程私有区域:程序计数器、虚拟机栈、本地方法栈(无需同步)。线程共享区域:堆和方法区(需通过锁、CAS等机制保证安全)。3. 程序计数器核心作用
执行控制:线程切换后能恢复到正确的执行位置(多线程核心机制)。唯一无OOM区域:JVM规范中唯一不会发生 OutOfMemoryError 的区域。HashMap头插法导致环形链表的详细解释
假设有一个HashMap的某个桶中有三个节点组成的链表:A→B→C→null。现在有两个线程(线程1和线程2)同时对这个HashMap进行扩容。
初始状态
A → B → C → null并发扩容过程
步骤1: 线程1开始扩容,处理到节点A - 取出A并记录next=B - 将A放入新桶(使用头插法) - 线程1暂时挂起(假设发生了上下文切换)
此时线程1的状态: - 新桶中:A → null - 当前正准备处理:e=B
步骤2: 线程2完整执行扩容过程 - 处理A:新桶中 A → null - 处理B:头插法,新桶中 B → A → null - 处理C:头插法,新桶中 C → B → A → null
线程2完成扩容,新桶中链表顺序已完全反转:C → B → A → null
步骤3: 线程1恢复执行,继续处理节点B - 线程1之前记录的是next=B - 计算B在新桶中的位置 - 执行头插法:B.next = 新桶当前头节点 - 但此时新桶中已经是 C → B → A → null(由线程2建立) - 线程1不知道这一点,仍执行 B.next = A - 新桶指向B:新桶头节点 = B
此时链表变为:B → A → ?
步骤4: 线程1处理C节点 - 记录next=null - 执行头插法:C.next = B,新桶头节点 = C
关键问题: 由于线程2已经修改过链表结构,A.next指向B,这就形成了环形链表:
C → B → A → B → A → B → ...(无限循环)环形成因图解
初始: A → B → C → null线程1处理A后: [新桶] A → null (线程1挂起,e=B)线程2完成: [新桶] C → B → A → null线程1恢复处理B: B.next = A [新桶] B → A → ? 但因线程2已设置A.next=B 实际形成: B → A → B → A → ...(环形)线程1处理C: C.next = B [新桶] C → B → A → B → A → ...(环形)JDK8解决方案
JDK8通过两个关键改变解决了这个问题: 1. 尾插法:保持原链表顺序不变,新节点追加到链表尾部而非头部 2. 分组迁移:根据hash值将节点分成两组分别迁移,减少并发冲突
即使在并发情况下,链表顺序不会反转,从根本上避免了环形链表的问题。
尽管如此,HashMap仍不是线程安全的,多线程环境应使用ConcurrentHashMap或Collections.synchronizedMap。
虽然JDK8解决了扩容时的环形链表问题,但HashMap的基本操作(put、get、remove等)本质上仍是非线程安全的。在并发 环境下,应该使用专门的线程安全集合类。
更直观的方式解释 AQS 和 ReentrantLock 的关系:
什么是 AQS? AQS 就像一个管理员,负责管理多个线程访问共享资源时的排队秩序,主要包含:一个 state 变量:记录资源的使用状态一个等待队列:存放未能获取资源的线程 获取/释放资源的机制ReentrantLock 如何使用 AQS? 想象 ReentrantLock 是一个会议室的门锁:
// 创建锁ReentrantLock lock = new ReentrantLock;try { // 获取锁lock.lock; // 执行需要同步的代码doSomething;} finally { // 释放锁lock.unlock;}
当线程要使用这个锁时:
A. 获取锁的过程: 1. 线程调用 lock 方法尝试获取锁 2. ReentrantLock 内部通过 AQS 的 state 变量检查锁状态: - state = 0:表示锁空闲 - state > 0:表示锁被占用
如果锁空闲:设置 state = 1 记录当前获得锁的线程如果锁被占用:
如果是当前线程再次获取锁:state 值加 1(可重入特性)如果是其他线程:被 AQS 放入等待队列
B. 释放锁的过程: 1. 线程调用 unlock 方法释放锁 2. state 值减 1 3. 当 state 变为 0 时,锁完全释放 4. AQS 唤醒等待队列中的下一个线程
具体例子:
public class SharedResource { private ReentrantLock lock = new ReentrantLock; private int count = 0; public void increment { // 获取锁lock.lock; try { // 可重入示例:即使在已经获得锁的情况下,还可以再次获取incrementHelper; count++; } finally { // 释放锁lock.unlock; } } private void incrementHelper { // 同一个线程可以再次获取锁lock.lock; try { // 做一些辅助工作System.out.println("Helper method executed"); } finally { lock.unlock; } }}公平锁与非公平锁的区别:假设现在有三个线程要获取锁:
非公平锁(默认):
ReentrantLock lock = new ReentrantLock; // 默认非公平新来的线程可以直接尝试获取锁,不管队列中是否有等待的线程优点:吞吐量更高缺点:可能造成某些线程长时间等待ReentrantLock lock = new ReentrantLock(true); // 使用公平锁新来的线程必须先检查队列如果队列中有等待的线程,就必须排队优点:按照请求顺序获取锁,更公平缺点:性能较低总结: 1. AQS 提供了锁的基础架构(state变量 + 等待队列) 2. ReentrantLock 基于 AQS 实现具体的锁逻辑 3. 可重入是通过 state 计数实现 4. 公平性是通过是否检查等待队列实现
MVCC(多版本并发控制)深度解析
1. 核心机制:版本链与事务ID
事务ID(Trx ID) 每个事务启动时分配一个全局唯一且递增的ID,用于标识事务的时序性(如事务A的ID=100,事 务B的ID=110)。数据行版本链 每行数据隐式包含两个关键字段:DB_TRX_ID:创建/最后修改该行的事务ID。(Database Transaction ID)DB_ROLL_PTR:指向旧版本数据的指针,形成版本链。DEL_FLAG(隐式逻辑字段):标记该行是否被删除(删除事务ID)。2. 读操作(一致性非锁定读)
可见性规则 事务读取数据时,仅能访问满足以下条件的版本:版本创建时间 ≤ 事务开始时间:DB_TRX_ID ≤ current_trx_id。版本未被删除 或 删除时间 > 事务开始时间:若该行被删除,则删除事务ID必须 > 当前事务ID。
快照生成
ReadView结构:事务首次读取时生成快照,记录当前活跃事务ID集合。版本选择:沿版本链回溯,找到第一个满足可见性条件的版本。示例场景 - 事务A(ID=100)查询某行: - 行版本链:V3(Trx=90)← V2(Trx=80)← V1(Trx=50)。 - 选择V3(Trx=90 ≤ 100,且未被删除)。 - 事务B(ID=110)在事务A执行期间删除该行: - 事务A仍可读取V3(删除事务ID=110 > 100)。
3. 写操作(多版本维护)
插入操作 新行的DB_TRX_ID = 当前事务ID,DEL_FLAG = 0(未删除)。更新操作
将原行标记为删除:DEL_FLAG = 1,删除事务ID = 当前事务ID。 插入新版本行:DB_TRX_ID = 当前事务ID,DB_ROLL_PTR指向旧版本。
删除操作 标记该行DEL_FLAG = 1,删除事务ID = 当前事务ID,不立即物理删除。
4. 旧版本清理(Purge机制)
清理条件 旧版本数据需满足:无活跃事务可能访问该版本(即所有事务ID 无未提交事务依赖该版本。后台线程 MySQL通过purge线程定期清理无效版本,InnoDB引擎中由innodb_purge_threads参数控制并发数。
5. 隔离级别与MVCC行为
| 隔离级别 | 快照生成时机 | 可见性规则 | | | | | | | READ COMMITTED | 每次SELECT生成新快照 | 仅读取已提交的最新版本 | | | | REPEATABLE READ | 事务首次SELECT生成快照 | 始终读取事务开始时已提交的版本 |
示例对比 - RC级别:事务A两次读取同一行,若中间事务B提交修改,第二次读取会看到新版本。 - RR级别:事务A两次读取同一行,即使事务B提交修改,仍仅看到事务开始时的版本。
6. MVCC优势与局限性
优势读不阻塞写,写不阻塞读,提升并发性能。 避免幻读(RR级别下通过版本链控制)。局限性
版本链过长影响查询性能(需回溯多个版本)。存储空间占用增加(需保留历史版本)。
Redis
缓存穿透、击穿、雪崩及解决方案
1. 缓存穿透
定义:大量请求查询不存在的数据(缓存和数据库均无),导致直接冲击数据库。解决方案:布隆过滤器(Bloom Filter):在缓存前加一层布隆过滤器,拦截非法请求(如非法ID)。缓存空值:对查询结果为空的Key,缓存短时间空值并设置过期时间(如30秒)。接口限流/鉴权:对恶意请求进行限流或校验参数合法性。2. 缓存击穿
定义:热点Key突然失效,大量并发请求直接击穿到数据库。解决方案:互斥锁(Mutex Lock):使用Redis的SETNX命令实现锁,仅允许一个线程重建缓存。逻辑过期:不设置物理过期时间,在Value中存储逻辑过期时间,异步刷新。永不过期策略:后台定时更新热点数据,不依赖缓存自动过期。3. 缓存雪崩
定义:大量缓存Key同时失效或Redis宕机,导致数据库压力骤增。解决方案:随机过期时间:在基础过期时间上增加随机值(如基础时间 + 0~300s),分散失效时 间。多级缓存:本地缓存(如Caffeine) + Redis缓存 + 数据库,逐层降级。集群高可用:Redis Cluster或Sentinel模式保障高可用,避免全盘崩溃。熔断降级:Hystrix等工具监控数据库负载,超阈值时拒绝请求。Redisson的WatchDog(看门狗)机制
WatchDog机制的目的
在分布式系统中使用Redis锁时,会遇到一个关键问题:如果获取锁的客户端在执行业务逻辑时崩溃或网络异常,可能导致锁无法正常释放。为了避免这种情况,通常会为锁设置一个过期时间。但这又带来新问题 :如果业务执行时间超过了锁的过期时间,锁会被自动释放,其他线程可能获取锁并执行,导致并发安全 问题。
WatchDog如何工作
Redisson的WatchDog机制通过以下方式解决这一问题:
自动续期:当客户端获取锁后,WatchDog会在后台启动一个定时任务定时检查:这个任务会每隔一段时间(默认是锁过期时间的1/3)检查锁是否还被当前客户端持有延长有效期:如果锁仍被持有,WatchDog会自动延长锁的过期时间具体实现细节
默认情况下,Redisson的锁设置30秒过期时间WatchDog每10秒(30秒的1/3)检查一次,如果业务还未完成,就会重新设置锁的过期时间为30秒这个过程会一直重复,直到业务完成或客户端崩溃如果客户端崩溃,WatchDog线程也会终止,不再续期,锁会在30秒后自动释放优势
解决业务执行时间不确定问题:不需要提前预估业务执行时间提高可靠性:即使业务执行时间很长,锁也不会因为超时被其他线程获取自动释放资源:当持有锁的客户端崩溃时,锁会在一定时间后自动释放何时会触发WatchDog
Redisson的锁有多种获取方式,WatchDog只会在特定情况下启动: - 使用lock方法获取锁时会启动WatchDog - 使用tryLock但不指定超时时间时也会启动 - 使用tryLock(long time, TimeUnit unit)并指定具体等待时间时也会启动
但如果使用tryLock(long time, long leaseTime, TimeUnit unit)方法并明确指定了leaseTime(锁持有 时间),WatchDog就不会启动,因为这表明开发者希望锁在指定时间后自动释放。
Redisson的WatchDog机制巧妙地解决了分布式锁中的锁续期问题,确保了分布式锁的可靠性和安全性。
来源:好运连发