Day08
1. MySQL表中有十个字段,你主键用自增ID还是UUID,为什么?
选择自增 ID 作为主键。因为 InnoDB 的聚簇索引要求数据尽量顺序插入,自增 ID 是单调递增的,写入时总是追加在数据页的尾部,能避免也分裂,提升写入性能。而 UUID 是无序的,每次插入都需要到 B+ 树的不同位置,容易导致以下问题:
- 大量随机 IO:目标页不在内存中时,需要频繁从磁盘加载。
- 频繁页分裂:新数据插入中间,InnoDB 为腾出空间会分裂数据页。
- 数据碎片严重:页的空间利用率降低,查询和维护成本提升。
最终会导致写入性能下降,并影响查询效率。因此,InnoDB 推荐使用单调递增的主键,保持聚簇索引的结构稳定,提高系统整体性能。
2. 为什么自增ID更快一些,UUID不快吗,它在B+树里面存储是有序的吗?
自增 ID 更快,因为是递增的,插入数据时可以顺序写入 B+ 树的末尾,定位快、写入高效,几乎不会触发页分裂,页的填充率也更高。而 UUID 是无序的,每次插入都可能落在索引中间,容易导致频繁页分裂、页面碎片多、写入效率低。此外,UUID 占用更多内存和存储空间,导致索引树更高,查询性能下降。
3. 查询数据时,到了B+树的叶子节点,之后的查找数据是如何做?
- 如果是主键索引(聚簇索引):叶子节点就存储了整行数据。查找先从根节点开始,根据主键值一路查找知道叶子节点,叶子节点中直接就存有这一行的全部数据,无需再查找其他地方。
- 如果是普通二级索引(非聚簇索引):叶子节点只存储索引列值 + 对应主键的值(回表用)。查找先从二级索引树中查找目标索引值的叶子节点,拿到主键值,再去主键索引那棵树中查找对应主键,回表取出整行数据。
4. 可重复读有没有幻读的问题?
有的。
在标准的 SQL 定义中,可重复读是无法阻止幻读的;但 MySQL 的 InnoDB 引擎下的可重复读默认不会出现幻读,因为它通过 Next-Key Lock(间隙锁)机制进行了额外机制。比如事务 A 查询金额大于 10 的订单数返回 1,事务 B 插入一条金额为 20 的订单后提交,再次查询时事务 A 会读到新插入的数据,导致结果变成2,发生幻读。
5. MySQL的锁有哪些?
(1)全局锁:使用
FLUSH TABLES WITH READ LOCK
命令实现,会让整个数据库进入只读状态,阻塞所有写操作,常用于全库备份以保证数据一致性。(2)表级锁:
- 表锁:通过
LOCK TABLES
加锁,锁住整个表,其他线程无法读写该表。- 元数据锁(MDL):系统自动加锁,保证表结构操作与数据读写互斥。读写操作加读锁(共享),结构修改加写锁(独占)。
- 意向锁:在加行锁之前自动加在表上的锁,快速判断是否存在行锁冲突。分为意向共享锁(IS)和意向排他锁(IX)。
(3)行级锁:
- 记录锁(Record Lock):锁住某条具体的记录,分为共享锁(S)和排他锁(X),满足读写互斥、写写互斥。
- 间隙锁(Gap Lock):锁住一个范围之间的空隙,不包含记录本身,用于防止幻读,仅在可重复读隔离级别下使用。
- Next-Key Lock:是记录锁与间隙锁的组合,所著某条记录以及其前后的间隙,避免幻读并保证唯一性约束。
6. 设计一个行级锁的死锁,举一个实际的例子
死锁是指两个或多个事务在执行过程中,因争夺资源而互相等待,导致系统无法继续执行。
场景:事务 A 先更新 id = 1 的记录,再试图更新 id = 2;而事务 B 相反,先更新 id = 2,再试图更新 id = 1。此时两个事务各自持有对方需要的锁,形成互相等待,最终数据库检测到死锁并主动回滚其中一个事务。
7. Mybatis的 # 和 $ 有什么区别?
#
会将参数替换为?
占位符,生成预编译 SQL,执行时通过PreparedStatement
设置参数,不仅执行效率更高,还能有效防止 SQL 注入,适合传递值。
$
是将参数直接拼接进 SQL 字符串中,不具备预编译功能,也无法防止 SQL 注入,适合拼接字段名、表名等结构性内容。
8. 设计一个 SQL 注入,具体说表中的字段,然后 SQL 语句是怎样的?
// 表结构
CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50),password VARCHAR(50)
);
漏洞SQL语句
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
<select id="login" resultType="User">SELECT * FROM users WHERE username = '${username}' AND password = '${password}'
</select>
注入场景:用户在登录页面输入如下内容:
- 用户名:
' OR '1'='1
- 密码:任意
拼接后 SQL 变成:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '任意'
由于'1' = '1'
永远为真,这条 SQL 实际执行的是:
SELECT * FROM users WHERE TRUE
等价于全表扫描,绕过了用户名密码校验,成功登录。
正确做法(使用#
占位符):
<select id="login" resultType="User">SELECT * FROM users WHERE username = #{username} AND password = #{password}
</select>
这样就能自动使用 PreparedStatement 进行参数绑定,避免拼接 SQL,从而有效防止 SQL 注入。
9. 本地缓存和Redis缓存的区别
本地缓存是指将数据直接存储在应用服务器的内存中,访问速度非常快,延迟低,适合频繁访问、对实时性要求高的场景,如热点数据读取、本地计算中间结果等。但由于本地缓存是“单机私有”的,不具备跨节点共享能力,在分布式部署中可能会造成缓存不一致或数据重复加载问题,同时它受限于本地内存大小,扩展性有限。
分布式缓存是指将数据缓存到独立的缓存服务中,支持多客户端共享访问,具备良好的可扩展性和高并发处理能力。Redis 提供了丰富的数据结构、持久化机制以及分布式支持,适用于大规模数据访问、共享会话、排行榜等场景。缺点是由于访问需要跨网络,性能略慢于本地缓存,同时部署和维护成本相对较高。
因此,在实际应用中,我们往往采用本地缓存 + 分布式缓存双层缓存架构:热点数据优先查本地缓存,未命中则查 Redis,再回源数据库。这种结构兼顾了访问速度、数据一致性和系统可扩展性,是现代微服务架构中常用的缓存策略之一。
10. Redis的Key过期了是立马删除吗?
不是,Redis 的过期删除策略是选择惰性删除 + 定期删除。惰性删除是不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。定期删除是每隔一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中的过期 key。
11. Redis大Key问题是什么?
大 Key 问题是指某个 key 对应的 value 体积或元素数量过大,容易造成阻塞、延迟、内存浪费等问题。常见判定标准包括:字符串类型超过 1MB、集合类元素超过 1 万条,但并无绝对值,应结合具体业务场景、Redis 节点配置和延迟要求综合评估。大 Key 会影响命令执行时长、主从同步、内存淘汰、删除效率等多个方面,需提前规避或拆分处理。
12. 大Key的缺点?
- 阻塞主线程,影响性能:Redis 是单线程模型,大 key 的读写操作(如 get, lrange, hgetall, del 等)会占用主线程较长时间,导致其他请求阻塞,影响整体响应速度。
- 删除耗时,可能引发卡顿:对大 key 执行 DEL 操作时,Redis 会一次性释放大量内存,造成 CPU 突升或主线程卡顿,甚至导致客户端连接超时。
- 主从复制延迟大,影响高可用性:Redis 主从同步是命令级复制,如果主节点操作一个大 key,会导致主从间网络传输大数据包,引发复制延迟甚至阻塞。
- AOF 重写/持久化耗时变长:大 key 会增加 AOF 文件的大小,导致持久化时间变长,甚至导致 rewrite 时阻塞或阻塞时间过久。
- 内存分布不均,容易触发淘汰或OOM:如果一个 key 占用了大量内存,会导致内存分布不均,触发 Redis 内存淘汰机制不及时或频繁,甚至引发OOM。
- 迁移困难,影响扩容和运维操作:Redis 集群中,迁移 slot 时大 key 会导致迁移耗时变长,甚至迁移失败,影响扩容或实例间的负载均衡。
13. Redis的持久化
- RDB 是 Redis 的快照机制,会在特定时间点把 Redis 内存中的数据保存成一个二进制文件(.rdb文件)到磁盘中。
- AOF 是 Redis 的操作日志机制,会把每一个写操作(如 set、del)都追加记录到日志文件(.aof文件)中,重启时通过“重放操作”恢复数据。
- 混合持久化(RDB + AOF):Redis 4.0 之后引入的新机制,将 RDB 的数据快照和 AOF 的操作日志合并到一个文件中,用来兼顾两者的优点。
14. RDB是怎样做出来的?
RDB 是 Redis 提供的一种快照式持久化机制,它会将某一时刻的内存数据整体保存到一个二进制文件中(通常为 dump.rdb),用于在服务重启时快速恢复数据。RDB 的生成可以通过两种方式触发:一是
SAVE
命令在主线程中执行,直接保存快照,但会阻塞服务;二是BGSAVE
命令,Redis 会 fork 出一个子进程在后台生成快照,避免阻塞主线程,是生产环境的推荐方式。BGSAVE
利用了操作系统的写时复制(COW)机制,使子进程可以在内存页未变更前共享主线程的数据,从而提高效率。Redis 还可以配置在一定时间内有若干 key 被修改自动触发BGSAVE
。RDB的优点是恢复速度快、文件体积小、适合做全量备份,但其缺点是数据不够实时,一旦 Redis 异常宕机,最后一次快照之后的数据更改将丢失,不适用于对数据完整性要求极高的场景。
15. AOF的写入策略
AOF 通过将写命令追加记录到日志文件中,在服务器重启时可以重新执行这些命令来恢复数据。为了在性能和数据安全之间取得平衡,Redis 提供了三种 AOF 写入策略,通过配置参数 appendfsync 控制。
策略 | 机制说明 | 优点 | 缺点 |
---|---|---|---|
always | 每执行一条写命令,就立刻调用 fsync() 将日志写入磁盘。 | 安全性最高,几乎不丢数据。 | 性能最差,每次写操作都涉及磁盘 I/O,非常耗资源。 |
everysec | 写命令先写入内存缓冲区,每秒钟调用一次 fsync() 刷入磁盘。 | 默认策略,兼顾性能与安全,最多丢失 1 秒数据。 | 在宕机时可能丢失最近 1 秒的数据。 |
no | 写命令只写入内存缓冲区,由操作系统自行决定何时刷盘(操作系统缓冲区)。 | 性能最好,写入开销低。 | 安全性最低,宕机或断电时可能丢失大量数据,不可控。 |
项目 | always | everysec (默认) | no |
---|---|---|---|
刷盘时机 | 每条写命令后立即 | 每秒由后台异步执行一次 | 依赖操作系统内核调度 |
调用 fsync | 主线程同步执行 | 后台线程异步执行 | 操作系统自动触发 |
宕机丢数据 | 不丢 | 最多丢 1 秒数据 | 可能丢很多数据 |
性能影响 | 最大(同步 IO) | 较小(异步 IO) | 最小 |