数据库Day04
一、脏读、不可重复读、幻读 示例
先建一张测试表 test_table
,插入基础数据:
CREATE TABLE test_table (id INT PRIMARY KEY,name VARCHAR(50),balance DECIMAL(10, 2)
);
INSERT INTO test_table (id, name, balance) VALUES (1, 'Alice', 1000.00);
1. 脏读(Dirty Read)
场景:事务 T1 修改数据但未提交,事务 T2 读到未提交的 “脏数据”,之后 T1 回滚,导致 T2 数据无效。
-- 开启事务 T1(故意不提交,模拟回滚场景)
START TRANSACTION;
UPDATE test_table
SET balance = balance - 100
WHERE id = 1; -- Alice 余额改为 900,但不提交-- 新开窗口/事务 T2,查询数据(此时能读到 T1 未提交的修改,即脏读)
START TRANSACTION;
SELECT balance FROM test_table WHERE id = 1; -- 会读到 900(脏数据)-- 回到 T1 窗口,回滚事务
ROLLBACK;-- 再回到 T2 窗口,重新查询(数据变回 1000,验证脏读)
SELECT balance FROM test_table WHERE id = 1; -- 结果为 1000
2. 不可重复读(Non - repeatable Read)
场景:事务 T1 内多次读同一数据,事务 T2 在 T1 未结束时修改并提交,导致 T1 两次读结果不同。
-- 事务 T1:先读数据,等待一会再读
START TRANSACTION;
SELECT balance FROM test_table WHERE id = 1; -- 第一次读:1000-- 新开窗口/事务 T2,修改并提交
START TRANSACTION;
UPDATE test_table
SET balance = balance - 200
WHERE id = 1; -- 改为 800
COMMIT;-- 回到 T1 窗口,再次读(结果变为 800,出现不可重复读)
SELECT balance FROM test_table WHERE id = 1; -- 第二次读:800
COMMIT;
3. 幻读(Phantom Read)
场景:事务 T1 按条件查询数据,事务 T2 插入满足条件的新数据并提交,T1 再次查询时 “多出” 数据,像幻觉。
-- 事务 T1:先按条件查询,等待一会再查
START TRANSACTION;
SELECT * FROM test_table WHERE id > 0; -- 第一次查:只有 id=1 的数据-- 新开窗口/事务 T2,插入满足条件的数据并提交
START TRANSACTION;
INSERT INTO test_table (id, name, balance) VALUES (2, 'Bob', 1500.00);
COMMIT;-- 回到 T1 窗口,再次按条件查询(多出 id=2 的数据,出现幻读)
SELECT * FROM test_table WHERE id > 0; -- 第二次查:id=1、id=2
COMMIT;
二、MySQL 隔离级别 验证
MySQL 默认隔离级别是 Repeatable Read(可重复读),可通过 SET TRANSACTION
切换隔离级别,验证不同级别对 脏读 / 不可重复读 / 幻读 的影响。
1. 读未提交(Read Uncommitted)
-- 会话 1:设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE test_table SET balance = 900 WHERE id = 1; -- 不提交-- 会话 2:查询(会读到脏数据,验证脏读)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT balance FROM test_table WHERE id = 1; -- 结果:900(脏读)
2. 读已提交(Read Committed)
-- 会话 1:修改数据但不提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE test_table SET balance = 800 WHERE id = 1; -- 不提交-- 会话 2:查询(读不到未提交数据,解决脏读)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM test_table WHERE id = 1; -- 结果:1000(脏读被解决)-- 会话 1 提交后,会话 2 再次查询(出现不可重复读)
COMMIT;
SELECT balance FROM test_table WHERE id = 1; -- 结果:800(不可重复读)
3. 可重复读(Repeatable Read,MySQL 默认)
-- 会话 1:查询数据,不提交
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM test_table WHERE id = 1; -- 第一次读:1000-- 会话 2:修改并提交
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
UPDATE test_table SET balance = 700 WHERE id = 1;
COMMIT;-- 会话 1 再次查询(结果仍为 1000,解决不可重复读;但幻读需特殊验证)
SELECT balance FROM test_table WHERE id = 1; -- 第二次读:1000(可重复读生效)
COMMIT;
4. 可串行化(Serializable)
最严格隔离级别,会锁表 / 行,避免所有并发问题,但性能最低:
-- 会话 1:查询时,表被隐式加锁
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT * FROM test_table; -- 执行后,表被锁定-- 会话 2:尝试插入数据(会阻塞,直到会话 1 提交/回滚)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
INSERT INTO test_table (id, name, balance) VALUES (3, 'Charlie', 2000.00);
-- 此处会卡住,等待会话 1 结束
三、事务日志(redo/undo log) 原理验证
日志是 “隐式” 工作的,无法直接用 SQL 触发,但可通过 崩溃恢复测试 验证 redo log 作用,或模拟回滚看 undo log 效果。
1. redo log 验证(模拟 MySQL 崩溃恢复)
核心逻辑:数据先写内存 Buffer Pool
,再异步刷盘;redo log 保证崩溃时,未刷盘的修改可通过日志恢复。
-- 1. 关闭 MySQL 双 1 安全模式(测试用,生产别关!)
SET GLOBAL innodb_flush_log_at_trx_commit = 0; -- redo log 先写内存,再定时刷盘-- 2. 插入数据(数据写入 Buffer Pool,redo log 写入内存缓冲)
START TRANSACTION;
INSERT INTO test_table (id, name, balance) VALUES (4, 'David', 3000.00);
COMMIT; -- redo log buffer 标记为“需要刷盘”,但未立即刷-- 3. 模拟 MySQL 崩溃(手动重启服务,或用命令杀进程)
-- 重启后,MySQL 自动检测 redo log,恢复未刷盘的插入操作
-- 重新连接后查询:SELECT * FROM test_table; -- id=4 的数据还在(redo log 恢复)
2. undo log 验证(回滚时的逻辑恢复)
核心逻辑:修改数据时,undo log 记录 “反向操作”,回滚时用它撤销修改。
START TRANSACTION;
UPDATE test_table SET balance = 600 WHERE id = 1; -- 产生 undo log(记录“回滚时改回 1000”)
ROLLBACK; -- 触发 undo log,balance 变回 1000
SELECT balance FROM test_table WHERE id = 1; -- 结果:1000(undo 生效)
四、总结
- 脏读 / 不可重复读 / 幻读:用多事务并发操作,演示 “读未提交数据”“多次读结果不同”“读新增数据” 的现象。
- 隔离级别:通过
SET TRANSACTION
切换级别,验证不同级别对并发问题的解决能力。 - 事务日志:redo log 保障崩溃恢复,undo log 保障回滚,可通过 “模拟崩溃”“手动回滚” 间接验证。
五、MySQL优化
1. 索引优化
- 作用:大幅提升查询速度,减少磁盘 I/O 操作
- 关键要点:
- 优先在频繁作为查询条件(WHERE)、排序(ORDER BY)、分组(GROUP BY)的字段上建立索引
- 避免过度索引(索引会增加写入 / 更新成本,占用存储空间)
- 联合索引遵循 "最左前缀原则",合理安排字段顺序
- 定期使用
EXPLAIN
分析索引使用情况,删除无用索引 - 对于长字符串,可考虑前缀索引减少索引体积
2. 集群和读写分离
- 集群架构:
- 主从复制:一主多从,主库负责写入,从库负责读取
- 分担单库压力,提高系统吞吐量和可用性
- 实现数据备份和故障转移,增强系统健壮性
- 读写分离:
- 写操作走主库,读操作走从库,分离读写压力
- 需处理主从同步延迟问题,可通过业务设计规避
- 适合读多写少的场景(如电商商品详情页、新闻网站)
3. 避免使用 SELECT *
- 优化点:
- 只查询需要的字段,减少数据传输量和内存占用
- 避免读取无用字段,降低磁盘 I/O 和网络传输开销
- 当表结构变更时,可减少潜在的兼容性问题
有助于利用覆盖索引,避免回表查询,提高查询效率
4. 分库分表
- 分库:
- 按业务模块(如用户库、订单库)或数据量(如历史库、当前库)拆分
- 降低单库数据量,减少数据库服务器资源竞争
- 分表:
- 纵向分表:将大表按字段拆分(如基础信息表 + 详细信息表),减少宽表影响
- 横向分表:按规则(如用户 ID 哈希、时间范围)拆分数据到多个表
- 解决单表数据量过大导致的查询缓慢、索引失效等问题
5. 选用合适的数据类型
- 原则:在满足业务需求的前提下,选择最小、最合适的类型
- 示例:
- 整数用 INT 而非 BIGINT,小范围用 TINYINT/SMALLINT
- 字符串长度固定用 CHAR,不固定用 VARCHAR(合理设置长度)
- 日期时间用 DATE/DATETIME/TIMESTAMP 而非字符串
- 存储金额可用 DECIMAL 而非 FLOAT/DOUBLE,避免精度问题
- 好处:减少存储空间,提高查询效率,降低内存消耗
6. 编写更优的 SQL 语句
- 优化方向:
- 避免嵌套子查询,可改为 JOIN 操作
- 减少使用 OR,可改用 UNION(需保证索引有效)
- 合理使用 LIMIT 分页,避免一次性查询大量数据
- 避免在 WHERE 子句中对字段进行函数运算(会导致索引失效)
- 控制 JOIN 表的数量,多表连接会增加查询复杂度和资源消耗
- 使用批量插入(INSERT ... VALUES (...), (...))替代循环单条插入