锁产生原理、脏读、不可重复读、幻读、丢失更新
时序过程 完整可直接复制运行的 MySQL 语句包含锁产生原理、脏读、不可重复读、幻读、丢失更新每一步你开两个会话窗口跟着执行就能亲眼复现同时讲清并发测试为什么会压出锁、脏数据。基于 MySQL InnoDB默认引擎。前置准备先建测试表所有语句直接复制执行sqlCREATE TABLE account ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20), money INT COMMENT 余额 ); INSERT INTO account(name,money) VALUES (张三,100);操作要求打开 MySQL 两个客户端窗口会话 A、会话 B分开执行模拟两个并发事务。一、并发为什么会产生锁悲观锁、行锁、阻塞演示原理回顾多个事务同时修改同一行数据InnoDB 自动加X 排他行锁持有锁的事务可以读写其他事务阻塞等待直到锁释放并发线程越多锁等待越长、锁竞争越严重压测就会出现锁超时、接口卡顿、TPS 上不去。复现演示会话 AsqlBEGIN; -- 加排他行锁锁定张三这一行 SELECT * FROM account WHERE id1 FOR UPDATE;此时id1 这一行已经被上锁会话 B立刻执行sqlBEGIN; UPDATE account SET money50 WHERE id1;现象会话 B 直接卡住阻塞无法执行原因行锁被 A 占用写操作必须等待锁释放。回到会话 A 释放锁sqlCOMMIT;A 提交后锁释放会话 B 立刻继续执行完成。并发压测结论JMeter 几百线程同时 update 同一行 → 大量锁等待队列 → 数据库耗时飙升无索引 update 会直接锁整张表表锁整个表全部阻塞服务雪崩。二、脏读 Dirty Read最经典脏数据定义事务 A 修改数据未提交事务 B 读到了之后 A 回滚B 读到的数据就是无效脏数据MySQL 隔离级别控制读未提交READ UNCOMMITTED会出现脏读读已提交 / 可重复读禁止脏读完整复现步骤第一步先修改全局隔离级别会话单独设置会话 A、会话 B 都执行sqlSET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;会话 AsqlBEGIN; -- 张三余额 100 - 200**未提交** UPDATE account SET money200 WHERE id1;会话 B立刻查询sqlBEGIN; SELECT * FROM account WHERE id1;结果查到 money200重点A 还没提交B 已经读到修改后数据这就是脏读会话 A 回滚放弃修改sqlROLLBACK;张三余额恢复为100会话 B 再次查询sqlSELECT * FROM account WHERE id1;余额变回 100。结论B 之前读到的 200就是不存在的脏数据。三、不可重复读 Non-repeatable Read定义同一个事务内多次查询同一行结果不一样原因其他事务中途更新并提交了该行数据侧重行数据被修改隔离级别READ UNCOMMITTED、READ COMMITTED 会出现RR 可重复读解决不可重复读复现步骤两会话先设置隔离级别sqlSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;会话 BsqlBEGIN; -- 第一次查询 SELECT * FROM account WHERE id1;查到money100会话 AsqlBEGIN; UPDATE account SET money150 WHERE id1; COMMIT;A 修改并直接提交会话 B同一个事务内再次查询sqlSELECT * FROM account WHERE id1;查到money150现象同一个事务 B两次查询同一行结果不同→不可重复读四、幻读 Phantom Read定义同一个事务内前后查询记录总数不一样原因其他事务插入 / 删除数据并提交行数凭空增减像幻觉侧重数据条数变化MySQL InnoDB 默认 RR 级别基本解决幻读间隙锁防插入先新增测试语句sqlTRUNCATE account; INSERT INTO account(name,money) VALUES (张三,100),(李四,200);设置隔离级别sqlSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;会话 BsqlBEGIN; -- 第一次统计总数 SELECT COUNT(*) FROM account;结果2条会话 AsqlBEGIN; INSERT INTO account(name,money) VALUES (王五,300); COMMIT;插入新数据并提交会话 B同事务再次查询总数sqlSELECT COUNT(*) FROM account;结果3条现象同个事务前后条数不一样 →幻读五、丢失更新业务最严重脏数据并发压测必现这是接口并发 BUG、库存超卖、重复扣款的根源所有隔离级别都防不住必须加锁 / 乐观锁解决。场景张三余额100线程 A、线程 B 同时扣 50理想最终余额0不加锁并发执行后结果50丢失一次更新复现时序全过程会话 AsqlBEGIN; SELECT money FROM account WHERE id1; -- 读到 100几乎同时会话 BsqlBEGIN; SELECT money FROM account WHERE id1; -- 同样读到 100会话 A 计算100-5050更新sqlUPDATE account SET money50 WHERE id1;会话 B 同样计算100-5050更新sqlUPDATE account SET money50 WHERE id1; COMMIT;会话 A 提交sqlCOMMIT;最终结果余额 50第二次扣款直接被覆盖丢失这就是生产最致命的脏数据、并发覆盖问题。六、面试精简背诵版直接背并发为什么产生锁多事务同时修改同一行数据InnoDB 为保证数据安全自动加排他行锁并发越高锁竞争越激烈无索引会升级表锁进而锁等待、死锁、接口超时。脏读A 修改未提交B 读到A 回滚B 数据作废。不可重复读同事务多次查同一行中途别人修改提交前后结果不一致。幻读同事务统计数量中途别人插入删除条数变化。丢失更新多线程同时读取旧值各自计算更新后续覆盖前面修改数据异常。隔离级别对应问题读未提交脏读、不可重复读、幻读 全有读已提交无脏读有不可重复读、幻读可重复读 (MySQL 默认)无脏读、无不可重复读间隙锁基本解决幻读串行化全部问题解决但并发极差解决方案悲观锁for update、乐观锁 version 版本号、Redis 分布式锁、缩短事务、加索引避免表锁、接口幂等。