MySQL事务与锁:并发控制的艺术

引言 “并发是计算机科学中最难的问题之一,因为它涉及时间、顺序和不确定性。” —— Leslie Lamport 在前两篇文章中,我们了解了MySQL如何通过索引实现快速查询,如何通过WAL日志保证数据持久化。但还有一个核心问题没有解决: 如何在高并发场景下保证数据一致性? 想象这样的场景: 双11零点,100万用户同时抢购一件库存只有10个的商品 每个用户都执行: 1. 读取库存 → 10 2. 判断库存足够 → 是 3. 扣减库存 → 库存 - 1 4. 创建订单 结果:卖出了100万件,但库存只扣了10个 💥 这就是并发控制的核心难题:如何让多个并发事务互不干扰,同时保证数据一致性? 今天,我们从第一性原理出发,深度剖析MySQL的并发控制机制: 无控制 → 锁机制 → MVCC → 隔离级别 → 死锁处理 混乱 串行化 读写分离 灵活平衡 自动恢复 ❌ ⚠️ ✅ ✅ ✅ 我们还将手写MVCC核心逻辑,彻底理解MySQL如何实现读写不阻塞。 一、问题的起点:并发导致的数据混乱 让我们从一个最经典的并发问题开始:电商库存扣减。 1.1 场景:秒杀商品超卖问题 需求: 商品:iPhone 16 Pro Max(库存10件) 活动:双11零点秒杀,原价9999元,秒杀价1元 预期:10个用户抢到,其余用户提示"已抢完" 无并发控制的实现: /** * 秒杀服务(无并发控制) */ @Service public class SeckillService { @Autowired private ProductMapper productMapper; @Autowired private OrderMapper orderMapper; /** * 秒杀下单(存在并发问题) */ public boolean seckill(Long productId, Long userId) { // 1. 读取库存 Product product = productMapper.selectById(productId); int stock = product.getStock(); // 2. 判断库存是否足够 if (stock <= 0) { return false; // 库存不足 } // 3. 扣减库存 product.setStock(stock - 1); productMapper.updateById(product); // 4. 创建订单 Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setAmount(1.00); // 秒杀价1元 orderMapper.insert(order); return true; } } 并发测试: ...

2025-11-03 · maneng

MySQL事务与锁:并发控制的艺术

引言 “并发是计算机科学中最难的问题之一,因为它涉及时间、顺序和不确定性。” —— Leslie Lamport 在前两篇文章中,我们了解了MySQL如何通过索引实现快速查询,如何通过WAL日志保证数据持久化。但还有一个核心问题没有解决: 如何在高并发场景下保证数据一致性? 想象这样的场景: 双11零点,100万用户同时抢购一件库存只有10个的商品 每个用户都执行: 1. 读取库存 → 10 2. 判断库存足够 → 是 3. 扣减库存 → 库存 - 1 4. 创建订单 结果:卖出了100万件,但库存只扣了10个 💥 这就是并发控制的核心难题:如何让多个并发事务互不干扰,同时保证数据一致性? 今天,我们从第一性原理出发,深度剖析MySQL的并发控制机制: 无控制 → 锁机制 → MVCC → 隔离级别 → 死锁处理 混乱 串行化 读写分离 灵活平衡 自动恢复 ❌ ⚠️ ✅ ✅ ✅ 我们还将手写MVCC核心逻辑,彻底理解MySQL如何实现读写不阻塞。 一、问题的起点:并发导致的数据混乱 让我们从一个最经典的并发问题开始:电商库存扣减。 1.1 场景:秒杀商品超卖问题 需求: 商品:iPhone 16 Pro Max(库存10件) 活动:双11零点秒杀,原价9999元,秒杀价1元 预期:10个用户抢到,其余用户提示"已抢完" 无并发控制的实现: /** * 秒杀服务(无并发控制) */ @Service public class SeckillService { @Autowired private ProductMapper productMapper; @Autowired private OrderMapper orderMapper; /** * 秒杀下单(存在并发问题) */ public boolean seckill(Long productId, Long userId) { // 1. 读取库存 Product product = productMapper.selectById(productId); int stock = product.getStock(); // 2. 判断库存是否足够 if (stock <= 0) { return false; // 库存不足 } // 3. 扣减库存 product.setStock(stock - 1); productMapper.updateById(product); // 4. 创建订单 Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setAmount(1.00); // 秒杀价1元 orderMapper.insert(order); return true; } } 并发测试: ...

2025-11-03 · maneng

事务与原子性:MULTI/EXEC命令详解

引言 在关系型数据库中,事务是保证数据一致性的重要手段(ACID)。那么Redis有事务吗? 答案是:有,但不完全是你理解的那种事务。 Redis的事务更像是批量命令,提供的是有限的原子性,而不是ACID中的那种强事务。 一、Redis事务的本质 1.1 什么是Redis事务? Redis事务是一组命令的集合,这些命令会: 顺序执行:按队列顺序依次执行 不被打断:执行期间不会插入其他客户端的命令 要么全执行,要么全不执行(有限制) 示例: 127.0.0.1:6379> MULTI # 开始事务 OK 127.0.0.1:6379> SET account:1 100 QUEUED 127.0.0.1:6379> SET account:2 200 QUEUED 127.0.0.1:6379> EXEC # 执行事务 1) OK 2) OK 1.2 Redis事务 vs 关系型数据库事务 特性 MySQL事务 Redis事务 原子性(A) ✅ 全部成功或回滚 ⚠️ 部分支持 一致性(C) ✅ 约束检查 ⚠️ 无约束 隔离性(I) ✅ 多种隔离级别 ⚠️ 无隔离级别 持久性(D) ✅ 提交后持久化 ⚠️ 取决于配置 回滚 ✅ 支持 ❌ 不支持 关键区别: MySQL: BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; COMMIT; -- 要么都成功,要么都回滚 Redis: MULTI DECR account:1:balance 100 INCR account:2:balance 100 EXEC -- 命令出错也不回滚! 二、事务命令详解 2.1 MULTI - 开始事务 127.0.0.1:6379> MULTI OK # 此后的命令不会立即执行,而是进入队列 127.0.0.1:6379> SET key1 "value1" QUEUED # 入队 127.0.0.1:6379> SET key2 "value2" QUEUED 2.2 EXEC - 执行事务 127.0.0.1:6379> EXEC 1) OK # SET key1的返回值 2) OK # SET key2的返回值 # 所有命令顺序执行,一次性返回结果 2.3 DISCARD - 取消事务 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET key1 "value1" QUEUED 127.0.0.1:6379> DISCARD # 取消事务 OK # 队列中的命令全部丢弃,不执行 127.0.0.1:6379> GET key1 (nil) # 没有执行 2.4 WATCH - 乐观锁 问题场景: ...

2025-01-21 · maneng

事务实战:转账案例与并发控制

实战案例概览 案例 核心问题 解决方案 难点 转账业务 数据一致性、死锁 固定加锁顺序、悲观锁 多账户并发转账 秒杀抢购 超卖、高并发 乐观锁 + 限流 10000人抢100件商品 订单支付 重复支付、幂等性 悲观锁 + 唯一约束 防止重复扣款 红包发放 余额不足、公平性 悲观锁 + 事务隔离 1个红包被多人抢 积分扣减 负数积分 乐观锁 + 余额检查 并发扣减积分 案例1:转账业务 需求 用户A向用户B转账100元,要求: 余额不能为负数 转账过程中不能被打断 防止死锁 方案1:基础实现(有死锁风险) -- ❌ 可能死锁 -- 事务A:A向B转100 START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; -- 锁A UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; -- 等待锁B COMMIT; -- 事务B:B向A转50(并发执行) START TRANSACTION; UPDATE account SET balance = balance - 50 WHERE user_id = 'B'; -- 锁B UPDATE account SET balance = balance + 50 WHERE user_id = 'A'; -- 等待锁A(死锁!) COMMIT; 方案2:固定加锁顺序(推荐) -- ✅ 避免死锁:按user_id升序加锁 -- 转账函数(伪代码) FUNCTION transfer(from_user, to_user, amount): -- 1. 固定加锁顺序(按user_id升序) first_user = MIN(from_user, to_user) second_user = MAX(from_user, to_user) START TRANSACTION; -- 2. 按顺序锁定账户 SELECT balance FROM account WHERE user_id = first_user FOR UPDATE; SELECT balance FROM account WHERE user_id = second_user FOR UPDATE; -- 3. 检查余额 IF from_user.balance < amount THEN ROLLBACK; RETURN "余额不足"; END IF; -- 4. 扣款和到账 UPDATE account SET balance = balance - amount WHERE user_id = from_user; UPDATE account SET balance = balance + amount WHERE user_id = to_user; COMMIT; RETURN "转账成功"; END FUNCTION; 方案3:Java实现(完整代码) @Service public class TransferService { @Autowired private AccountMapper accountMapper; @Transactional(rollbackFor = Exception.class) public void transfer(String fromUser, String toUser, BigDecimal amount) { // 1. 固定加锁顺序(避免死锁) String firstUser = fromUser.compareTo(toUser) < 0 ? fromUser : toUser; String secondUser = fromUser.compareTo(toUser) < 0 ? toUser : fromUser; // 2. 按顺序锁定账户(悲观锁) Account first = accountMapper.selectForUpdate(firstUser); Account second = accountMapper.selectForUpdate(secondUser); // 3. 检查余额 Account fromAccount = fromUser.equals(firstUser) ? first : second; if (fromAccount.getBalance().compareTo(amount) < 0) { throw new BusinessException("余额不足"); } // 4. 扣款和到账 accountMapper.updateBalance(fromUser, amount.negate()); // 扣款 accountMapper.updateBalance(toUser, amount); // 到账 // 5. 记录流水(可选) recordTransferLog(fromUser, toUser, amount); } } <!-- MyBatis Mapper --> <select id="selectForUpdate" resultType="Account"> SELECT * FROM account WHERE user_id = #{userId} FOR UPDATE </select> <update id="updateBalance"> UPDATE account SET balance = balance + #{amount} WHERE user_id = #{userId} </update> 案例2:秒杀抢购 需求 10000个用户抢购100件商品,要求: ...

2025-01-14 · maneng

死锁:产生原因与解决方案

什么是死锁? 死锁(Deadlock):两个或多个事务互相等待对方释放锁,形成循环等待,导致所有事务都无法继续执行。 -- 经典死锁场景 事务A:持有锁1,等待锁2 事务B:持有锁2,等待锁1 → 互相等待,形成死锁 死锁产生条件 必须同时满足4个条件: 互斥:资源不能被多个事务同时占用 持有并等待:事务持有锁的同时,等待其他锁 不可剥夺:已获得的锁不能被强制释放 循环等待:事务形成循环等待链 死锁示例 示例1:经典死锁 -- 建表 CREATE TABLE account ( id INT PRIMARY KEY, balance INT ); INSERT INTO account VALUES (1, 1000), (2, 2000); -- 时间线 ┌─────────────────┬─────────────────────────────────┐ │ 事务A │ 事务B │ ├─────────────────┼─────────────────────────────────┤ │ START TRANSACTION│ │ │ UPDATE account │ │ │ SET balance=900 │ │ │ WHERE id=1; │ │ │ -- 持有id=1的锁 │ │ │ │ START TRANSACTION │ │ │ UPDATE account │ │ │ SET balance=1800 │ │ │ WHERE id=2; │ │ │ -- 持有id=2的锁 │ │ UPDATE account │ │ │ SET balance=800 │ │ │ WHERE id=2; │ │ │ -- 等待id=2的锁 │ │ │ │ UPDATE account │ │ │ SET balance=1100 │ │ │ WHERE id=1; │ │ │ -- 等待id=1的锁 │ │ │ -- 死锁! │ └─────────────────┴─────────────────────────────────┘ 死锁检测与处理: ...

2025-01-14 · maneng

undo log与redo log:事务日志详解

MySQL的三种日志 日志类型 作用 实现层 记录内容 刷盘时机 undo log 保证原子性(回滚) InnoDB 修改前的旧值 事务执行时 redo log 保证持久性(崩溃恢复) InnoDB 修改后的新值 事务提交时 binlog 主从复制、数据恢复 Server层 逻辑SQL或行变更 事务提交时 1. undo log(回滚日志) 作用 事务回滚:保证原子性(ROLLBACK时恢复数据) MVCC实现:提供历史版本数据(快照读) 记录内容 记录数据修改前的旧值,用于回滚。 -- 执行UPDATE前,记录旧值到undo log UPDATE account SET balance = 900 WHERE id = 1; -- undo log记录: INSERT INTO undo_log VALUES ( trx_id = 101, table_id = account, row_id = 1, old_balance = 1000 -- 修改前的旧值 ); undo log类型 类型 操作类型 回滚方式 INSERT undo INSERT 删除插入的行 UPDATE undo UPDATE/DELETE 恢复修改前的值 -- INSERT undo log INSERT INTO account VALUES (2, 'B', 500); -- undo log:DELETE FROM account WHERE id = 2; -- UPDATE undo log UPDATE account SET balance = 900 WHERE id = 1; -- undo log:UPDATE account SET balance = 1000 WHERE id = 1; -- DELETE undo log DELETE FROM account WHERE id = 1; -- undo log:INSERT INTO account VALUES (1, 'A', 1000); 版本链 undo log通过DB_ROLL_PTR形成版本链,用于MVCC。 ...

2025-01-14 · maneng

脏读、不可重复读、幻读问题解析

三大并发问题概述 问题 定义 影响范围 解决隔离级别 脏读 读到未提交的数据 单行数据 READ COMMITTED 不可重复读 同一查询两次结果不同(UPDATE) 单行数据 REPEATABLE READ 幻读 范围查询两次结果不同(INSERT) 多行数据 SERIALIZABLE 1. 脏读(Dirty Read) 定义 读取到其他事务未提交的数据,如果该事务回滚,就会读到"脏"数据。 场景演示 -- 设置为最低隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 时间线:事务A和事务B并发执行 ┌─────────────────┬─────────────────────────────────┐ │ 事务A │ 事务B │ ├─────────────────┼─────────────────────────────────┤ │ START TRANSACTION│ │ │ │ START TRANSACTION │ │ SELECT balance │ │ │ FROM account │ │ │ WHERE id=1; │ │ │ -- 读到1000 │ │ │ │ UPDATE account │ │ │ SET balance=500 │ │ │ WHERE id=1; │ │ │ -- 未提交 │ │ SELECT balance │ │ │ FROM account │ │ │ WHERE id=1; │ │ │ -- 读到500(脏读)│ │ │ │ ROLLBACK; │ │ │ -- 余额回滚到1000 │ │ -- 但事务A已经基于 │ │ │ -- 500做决策,错误!│ │ │ COMMIT; │ │ └─────────────────┴─────────────────────────────────┘ 真实案例 -- 场景:电商库存扣减 -- 事务A:查询库存并下单 START TRANSACTION; SELECT stock FROM product WHERE id = 1001; -- 读到库存50 -- 事务B:库存更正(发现统计错误) START TRANSACTION; UPDATE product SET stock = 10 WHERE id = 1001; -- 实际只有10件 -- 未提交 -- 事务A继续 -- 基于库存50的判断,允许用户下单40件 INSERT INTO orders (product_id, quantity) VALUES (1001, 40); COMMIT; -- 事务B回滚 ROLLBACK; -- 库存恢复到50 -- 结果:用户下单40件,但实际库存只有10件,超卖! 解决方案 提高隔离级别到READ COMMITTED或更高: ...

2025-01-14 · maneng

事务的隔离级别:Read Uncommitted、Read Committed、Repeatable Read、Serializable

为什么需要隔离级别? 并发事务可能产生三大问题: 脏读(Dirty Read):读到未提交的数据 不可重复读(Non-Repeatable Read):同一查询两次结果不同 幻读(Phantom Read):范围查询两次结果不同 隔离级别就是用来控制在多大程度上解决这些问题。 四种隔离级别 级别对比表 隔离级别 脏读 不可重复读 幻读 性能 应用场景 READ UNCOMMITTED(读未提交) ❌ 会 ❌ 会 ❌ 会 ⭐⭐⭐⭐ 几乎不用 READ COMMITTED(读已提交) ✅ 避免 ❌ 会 ❌ 会 ⭐⭐⭐ Oracle/PostgreSQL默认 REPEATABLE READ(可重复读) ✅ 避免 ✅ 避免 ⚠️ 部分避免 ⭐⭐ MySQL默认(推荐) SERIALIZABLE(串行化) ✅ 避免 ✅ 避免 ✅ 避免 ⭐ 严格一致性要求 1. READ UNCOMMITTED(读未提交) 特点:事务可以读取其他事务未提交的数据(脏读)。 演示:脏读问题 -- 设置隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 事务A START TRANSACTION; SELECT balance FROM account WHERE user_id = 'A'; -- 读到1000 -- 事务B(并发执行) START TRANSACTION; UPDATE account SET balance = 500 WHERE user_id = 'A'; -- 未提交 -- 此时事务A再次查询 -- 事务A SELECT balance FROM account WHERE user_id = 'A'; -- 读到500(脏读!) COMMIT; -- 事务B ROLLBACK; -- 回滚,余额恢复到1000 问题:事务A读到了事务B未提交的数据(500),但事务B最终回滚了,导致数据不一致。 ...

2025-01-14 · maneng

事务的四大特性:ACID详解

什么是事务? 事务(Transaction) 是数据库操作的最小工作单元,是一组不可分割的SQL语句集合。 -- 转账场景:A向B转账100元 START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; COMMIT; ACID四大特性 1. 原子性(Atomicity) 定义:事务是不可分割的最小单元,要么全部成功,要么全部失败回滚。 实现机制:undo log(回滚日志) 每次修改前,记录原始值到undo log 回滚时,读取undo log恢复数据 -- 示例:转账失败自动回滚 START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; -- 成功 UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; -- 失败(余额不足) ROLLBACK; -- 自动回滚,A的余额恢复 应用场景: 订单支付(扣库存 + 创建订单 + 扣款) 批量数据导入(全部成功或全部失败) 2. 一致性(Consistency) 定义:事务执行前后,数据库从一个一致性状态转换到另一个一致性状态。 一致性约束: 数据完整性约束:主键、外键、唯一索引 业务规则约束:账户余额>=0,库存>=0 应用层约束:总金额守恒 -- 转账前:A有1000,B有500,总额1500 -- 转账后:A有900,B有600,总额仍是1500 -- 违反一致性的例子 UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; -- 如果这里系统崩溃,没有执行下一条,总额变成1400,违反一致性 UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; 如何保证一致性? ...

2025-01-14 · maneng

如约数科科技工作室

浙ICP备2025203501号

👀 本站总访问量 ...| 👤 访客数 ...| 📅 今日访问 ...