2026.4.27 第五篇
MySQL的ACID怎么保证?
原子性:通过undo log来实现,在修改数据之前记录旧值,事务回滚的时候根据undo log执行与之相反的操作即可。
- 注意:事务回滚后undo log并不会被清除,而是确保undo log不再被事务使用的时候才会清除 持久性:主要依赖redo log + WAL机制和CheckPoint机制来实现
- WAL即修改bufferpool 中的数据后先将修改写入到redo log中,然后找机会刷新到磁盘上。
- 将 buffer pool 中的脏页刷新到磁盘记录当前刷盘位置(LSN) 隔离性:通过锁和MVCC(多版本并发控制)实现 一致性:事务执行前后数据满足约束,由原子性、隔离性、持久性共同保证
mysql崩了怎么保证持久性
MySQL 通过 redo log + WAL 机制保证事务提交时日志先落盘,崩溃恢复时通过重放 redo log 恢复数据,从而保证持久性。
单例模式(双重检测锁)
class Singleton{
private static volatile Singleton instance; //禁止指令重排防止获取还未实例化的对象
Singleton(){}
public static Singleton getInstance(){
if(instance == null){ //如果已经实例化了就不用再获取锁了,提高性能
synchronized(Singleton instance){
if(instance == null){ //防止多个线程同时通过第一次判空,在排队拿锁后重复创建对象
instance = new Singleton();
}
}
}
return instance;
}
}
为什么加volatile?
防止对象的创建被指令重排,获取到还未实例化的对象
- 分配内存
- 初始化对象
- instance 指向内存
如果没有 volatile,可能变成:
1 -> 3 -> 2
导致其他线程拿到“未初始化完成”的对象
Volatile的实现原理
volatile写会在volatile写后面加Store内存屏障 volatile读会在volatile读前面加Load内存屏障
为什么 volatile 写只在后面加 Store 屏障不在前面加?为什么 volatile 读只在前面加 Load 屏障不在后面加?
写前如果加屏障,是防止:volatile 写 → 跑到前面,但: JMM 本身就不允许“写往前越过前面的代码”破坏程序顺序 读后屏障是防止:volatile 读 → 跑到后面,但: 读本身不会影响其他线程 因此:
- volatile 写通过写后屏障,保证之前的写对其他线程可见;
- volatile 读通过读前屏障,保证之后的读不会读取到旧值;
- 只加一侧屏障是因为只需要保证 happens-before 的单向约束,避免不必要的性能开销。
SQL优化
这个表数据量500w条,满足 seller_id=12345678 的数据量1500条,查询比较慢(>100ms)
① 分析原因
② 解决方案
CREATE TABLE `t_seller_count` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`dt` varchar(32) NOT NULL COMMENT '统计日期',
`seller_id` bigint(20) NOT NULL COMMENT '卖家id',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
`pay_order_num` int(11) DEFAULT NULL COMMENT '支付订单数',
`complete_order_num` int(11) NOT NULL COMMENT '完成订单数',
`apply_refund_num` int(11) DEFAULT NULL COMMENT '退款订单数',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_seller` (`seller_id`, `dt`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='卖家订单统计表';
select sum(complete_order_num)
from t_seller_count
where seller_id = 12345678;
为什么慢?
使用了索引但不是覆盖索引,会回表
注意:索引是否生效,核心看的是“是否参与查找(过滤 / 定位 / 排序 / 分组),索引失效通常发生在 where 条件里(join两个不同编码格式的表导致的隐式字符转换)。这里的sum()是对结果进行聚合。
1. 对索引列做函数/运算
where seller_id + 1 = 123
2. 隐式类型转换
where seller_id = '123' -- 字符串 vs bigint
3. 违反最左前缀原则
index(a, b, c)
where b = 1 -- a 没用 → 失效
4. 范围查询后断裂
where a = 1 and b > 2 and c = 3
-- c 用不到索引
解决方案
优化方式是建立覆盖索引:
ALTER TABLE t_seller_count
ADD INDEX idx_seller_complete_order_num (seller_id, complete_order_num);
SQL语句的书写顺序
SELECT
列名,
聚合函数(列名)
FROM
表名
JOIN
关联表 ON 关联条件
WHERE
行过滤条件
GROUP BY
分组字段
HAVING
分组后的过滤条件
ORDER BY
排序字段
LIMIT
分页数量;
跳表为什么快?
跳表通过多层索引结构,使查找时可以跳跃式前进,
将时间复杂度从 O(n) 降到 O(log n),这是其性能高的根本原因。
相比红黑树,跳表实现简单,且天然支持范围查询。
RDB怎么写入?
RDB 通过 fork 子进程生成内存数据的快照文件,
子进程遍历内存并写入 RDB 文件。
在生成过程中,主进程通过写时复制(COW)机制保证数据一致性,不会阻塞读操作。
RDB 不记录增量日志,只保存某一时刻的全量数据。
Redis的 COW(写时复制)
和Java的CopyOnWriteArrayList一样
只有在主进程发生写操作时,才会复制对应的内存页进行修改。
为什么Cluster集群的分片为什么是16384个槽?
Redis Cluster 采用 16384 个槽,是在数据分布均匀性和集群通信开销之间的权衡。
槽数量足够多,可以保证数据分布均匀、迁移粒度细; 同时又不会太多,使得节点之间通过 bitmap 同步 slot 状态时开销较小(约 2KB)。
此外 16384 是 2 的幂,便于通过位运算快速计算 hash slot。
Cluster 集群
Redis Cluster 是 Redis 的分布式方案,用于解决单机内存瓶颈和高可用问题。 Redis Cluster 通过 16384 个 hash slot 实现数据分片, 每个节点负责一部分 slot,客户端通过计算 key 的 slot 直接路由请求。
集群采用主从结构实现高可用,主节点宕机后从节点自动提升, 节点之间通过 gossip 协议同步状态。
相比一致性哈希,slot 机制使得扩容和数据迁移更加灵活。
请求是怎么路由的
计算 key 的 slot
2. 找到对应节点
3. 直接访问该节点
如果访问错节点:Redis 返回 MOVED 重定向
怎么排查慢sql?
- 开启slow log
- 使用MySQL自带的工具,比如mysqldumpslow 对慢日志进行排序,找执行次数多且执行慢的SQL
- 使用explain获取执行计划
- 判断原因(可能是没有索引、索引失效、选错索引)
- 优化:加索引、改 SQL、覆盖索引、分页优化、缓存
记录每天Java和Agent面经学习

查看22道真题和解析