Mysql中的共享锁、排他锁、悲观锁、乐观锁等及使用场景
Mysql中的共享锁、排他锁、悲观锁、乐观锁等及使用场景
一、背景与问题
在高并发系统中,数据一致性是核心挑战之一。当多个事务同时访问同一数据时,会出现读-写冲突和写-写冲突,导致数据不一致或业务逻辑错误。MySQL通过锁机制来控制并发访问,其中共享锁(Shared Lock)、排他锁(Exclusive Lock)是行级锁的基础,而悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)是锁策略的两种范式。
核心问题在于:
- 如何在读写操作中保证事务的隔离性?
- 如何在高并发场景下避免死锁和锁等待?
- 如何在不同业务场景中选择合适的锁机制?
本文将从底层原理出发,结合真实业务场景,深入探讨这些锁机制的实现方式、使用场景及注意事项。
二、基本原理
1. 锁的分类
MySQL的锁机制分为共享锁(Shared Lock, S Lock)和排他锁(Exclusive Lock, X Lock),它们是行级锁的基础:
| 锁类型 | 读操作 | 写操作 | 适用场景 |
|---|---|---|---|
| 共享锁(S Lock) | 允许 | 不允许 | 读取数据时防止写入 |
| 排他锁(X Lock) | 不允许 | 允许 | 更新数据时防止读写 |
事务隔离级别决定了锁的持有时间和冲突处理方式,例如在REPEATABLE READ隔离级别下,共享锁和排他锁会持续到事务结束。
2. 悲观锁与乐观锁
- 悲观锁:假设冲突频繁,在读取数据时即加锁,典型实现是
SELECT ... FOR UPDATE。 - 乐观锁:假设冲突较少,在更新时检查版本号或时间戳,典型实现是
version字段。
三、环境准备
1. 数据库准备
创建测试表并插入数据:
-- 创建库存表
CREATE TABLE inventory (
id INT PRIMARY KEY,
product_name VARCHAR(50),
stock INT,
version INT DEFAULT 1
);
-- 插入初始数据
INSERT INTO inventory (id, product_name, stock, version) VALUES
(1, 'Laptop', 100, 1),
(2, 'Phone', 200, 1);2. 连接工具
使用mysql命令行客户端或Navicat等工具执行SQL,确保数据库隔离级别为REPEATABLE READ(默认)。
四、核心实现
1. 共享锁与排他锁
共享锁通过SELECT ... FOR SHARE实现,允许其他事务读取但禁止写入;排他锁通过SELECT ... FOR UPDATE实现,禁止所有并发操作。
-- 事务1: 使用共享锁
START TRANSACTION;
SELECT * FROM inventory WHERE id = 1 FOR SHARE;
-- 等待事务2执行
COMMIT;
-- 事务2: 使用排他锁
START TRANSACTION;
SELECT * FROM inventory WHERE id = 1 FOR UPDATE;
-- 此时事务1的共享锁会阻塞事务2的排他锁
COMMIT;关键点:
- 共享锁和排他锁的持有时间取决于事务的提交/回滚。
- 当事务1持有共享锁时,事务2的排他锁会等待,直到事务1提交或回滚。
2. 悲观锁实现(库存扣减)
在电商系统中,库存扣减需要保证原子性:
-- 事务1: 悲观锁实现
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1 FOR UPDATE;
SET @stock = 100;
SET @version = 1;
UPDATE inventory
SET stock = @stock - 10, version = @version + 1
WHERE id = 1 AND version = @version;
COMMIT;关键点:
FOR UPDATE会立即加锁,防止其他事务修改数据。- 适用于高并发写操作的场景,但可能导致锁等待。
3. 乐观锁实现(库存扣减)
通过版本号控制更新:
-- 事务1: 乐观锁实现
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1;
SET @stock = 100;
SET @version = 1;
UPDATE inventory
SET stock = @stock - 10, version = @version + 1
WHERE id = 1 AND version = @version;
COMMIT;关键点:
- 只在更新时检查版本号,避免了锁等待。
- 适用于读多写少的场景,但需要确保版本号更新逻辑正确。
五、完整案例
1. 电商库存扣减系统
业务场景:两个事务同时尝试扣减同一商品库存,需确保最终库存正确。
悲观锁实现:
-- 事务1: 悲观锁
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1 FOR UPDATE;
SET @stock = 100;
SET @version = 1;
UPDATE inventory
SET stock = @stock - 10, version = @version + 1
WHERE id = 1 AND version = @version;
COMMIT;
-- 事务2: 悲观锁
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1 FOR UPDATE;
SET @stock = 90;
SET @version = 2;
UPDATE inventory
SET stock = @stock - 10, version = @version + 1
WHERE id = 1 AND version = @version;
COMMIT;输出结果:库存变为80,版本号为3。
性能分析:悲观锁在高并发时可能导致锁等待,但能保证数据一致性。
乐观锁实现:
-- 事务1: 乐观锁
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1;
SET @stock = 100;
SET @version = 1;
UPDATE inventory
SET stock = @stock - 10, version = @version + 1
WHERE id = 1 AND version = @version;
COMMIT;
-- 事务2: 乐观锁
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1;
SET @stock = 90;
SET @version = 2;
UPDATE inventory
SET stock = @stock - 10, version = @version + 1
WHERE id = 1 AND version = @version;
COMMIT;输出结果:库存变为80,版本号为3。
性能分析:乐观锁避免了锁等待,但需要处理更新失败的场景(如版本号不匹配)。
六、源码解析
1. MySQL锁机制源码
MySQL的锁机制核心在trx0sys.cc中实现,核心逻辑如下:
void trx_lock_wait_for_lock(trx_t *trx, lock_t *lock) {
// 等待锁的获取
if (lock->type == LOCK_X) {
// 排他锁
lock_wait_for_lock(trx, lock, LOCK_WAIT_FOREVER);
} else {
// 共享锁
lock_wait_for_lock(trx, lock, LOCK_WAIT_FOREVER);
}
}关键点:
- 锁的等待机制通过
lock_wait_for_lock实现。 - 排他锁会阻塞所有其他事务的读写操作。
2. InnoDB行锁实现
InnoDB的行锁通过lock_rec_lock函数实现,核心逻辑如下:
void lock_rec_lock(
trx_t *trx, /*!< transaction */
ulint mode, /*!< lock mode */
ulint type, /*!< lock type */
ulint index_id, /*!< index id */
const byte *rec, /*!< record */
ulint rec_version) /*!< record version */
{
// 获取锁
if (mode == LOCK_X) {
// 排他锁
lock_rec_get_lock(trx, rec, mode, type, index_id, rec_version);
} else {
// 共享锁
lock_rec_get_lock(trx, rec, mode, type, index_id, rec_version);
}
}关键点:
- 行锁的获取与释放由
lock_rec_get_lock和lock_rec_unlock控制。 - 锁的粒度决定了并发性能。
七、进阶使用
1. 锁的超时设置
通过innodb_lock_wait_timeout参数控制锁等待时间:
SET innodb_lock_wait_timeout = 50; -- 设置为50秒适用场景:避免长时间等待导致的死锁。
2. 锁的粒度控制
- 行锁:精确到单行,适合高并发写操作。
- 表锁:适合批量操作,但会降低并发性能。
示例:
LOCK TABLES inventory WRITE; -- 表锁
UNLOCK TABLES;适用场景:数据导入导出等批量操作。
3. 乐观锁的版本号策略
- 自增版本号:适合简单场景。
- 时间戳:适合需要记录更新时间的场景。
示例:
ALTER TABLE inventory ADD COLUMN update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP;八、性能与工程实践
1. 性能优化
| 场景 | 优化策略 |
|---|---|
| 高并发写 | 使用排他锁,但控制事务粒度 |
| 高并发读 | 使用共享锁,避免锁等待 |
| 锁等待 | 设置合理的锁等待超时 |
| 死锁 | 使用SHOW ENGINE INNODB STATUS排查 |
2. 安全风险
- 死锁:事务A持有锁1,事务B持有锁2,两者相互等待。
- 锁丢失:未在事务中使用锁,导致数据不一致。
- 锁粒度过大:导致并发性能下降。
解决方案:
- 使用
SELECT ... FOR UPDATE控制写锁。 - 使用
version字段实现乐观锁。 - 使用
SHOW ENGINE INNODB STATUS监控死锁。
3. 异常处理
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
SELECT stock, version FROM inventory WHERE id = 1 FOR UPDATE;
-- 更新逻辑
COMMIT;
END;九、常见问题与踩坑
1. 锁等待导致的性能瓶颈
错误示例:
START TRANSACTION;
SELECT * FROM inventory WHERE id = 1 FOR UPDATE;
-- 长时间未提交问题:其他事务会等待该锁,导致阻塞。
解决办法:在事务中尽快完成操作,或使用SET innodb_lock_wait_timeout限制等待时间。
2. 乐观锁版本号不匹配
错误示例:
UPDATE inventory SET stock = 90 WHERE id = 1 AND version = 1;问题:如果版本号已更新,更新会失败。
解决办法:在事务中重新获取版本号并更新。
3. 未正确处理锁的事务提交
错误示例:
START TRANSACTION;
SELECT * FROM inventory WHERE id = 1 FOR UPDATE;
-- 未提交事务,导致锁一直存在问题:事务未提交,锁会一直持有,影响并发。
解决办法:确保事务在完成后正确提交或回滚。
十、最佳实践
1. 选择锁策略的依据
| 场景 | 推荐策略 |
|---|---|
| 高并发写 | 悲观锁(排他锁) |
| 高并发读 | 乐观锁(版本号) |
| 业务逻辑复杂 | 混合使用共享锁和排他锁 |
| 批量操作 | 表锁(需谨慎) |
2. 锁的粒度控制
- 行锁:适合高并发写操作。
- 乐观锁:适合读多写少的场景。
- 避免大范围锁:如
SELECT * FROM table FOR UPDATE。
3. 死锁预防
- 按顺序加锁:所有事务按相同顺序加锁。
- 避免长事务:事务应在最短时间完成。
- 监控死锁:定期使用
SHOW ENGINE INNODB STATUS。
十一、总结
MySQL的锁机制是保障数据一致性的重要工具,但需要根据业务场景选择合适的策略。共享锁和排他锁是行级锁的基础,而悲观锁和乐观锁是两种不同的锁策略。
在实际开发中:
- 悲观锁适合写多读少的场景,但可能导致锁等待;
- 乐观锁适合读多写少的场景,但需要处理版本号不匹配的异常;
- 死锁是必须避免的问题,需通过监控和锁顺序控制;
- 性能优化需要结合事务粒度、锁等待时间和锁粒度进行调整。
掌握这些锁机制的核心原理和使用场景,是构建高并发、高可靠系统的基石。在实际项目中,需要根据业务需求权衡选择合适的锁策略,同时注意异常处理和性能优化。
评论已关闭