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?

防止对象的创建被指令重排,获取到还未实例化的对象

  1. 分配内存
  2. 初始化对象
  3. 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?

  1. 开启slow log
  2. 使用MySQL自带的工具,比如mysqldumpslow 对慢日志进行排序,找执行次数多且执行慢的SQL
  3. 使用explain获取执行计划
  4. 判断原因(可能是没有索引、索引失效、选错索引)
  5. 优化:加索引、改 SQL、覆盖索引、分页优化、缓存
#面经#
每日面经记录 文章被收录于专栏

记录每天Java和Agent面经学习

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务