当前位置: 首页 > news >正文

MySQL 锁机制全面解析:乐观锁与悲观锁实现及深度剖析

MySQL 锁机制是保证数据一致性和并发控制的核心组件,主要分为乐观锁和悲观锁两大类。以下从实现原理到实战应用进行全面解析。

一、悲观锁(Pessimistic Locking)

1. 基本概念

核心思想:假定并发冲突经常发生,访问数据前先加锁

特点:

由数据库原生支持

适合写多读少场景

保证强一致性

可能引发死锁

2. InnoDB 悲观锁实现方式

(1) 共享锁(S锁)

SELECT * FROM products WHERE id = 1 LOCK IN SHARE MODE;

-- 或

SELECT * FROM products WHERE id = 1 FOR SHARE; -- MySQL 8.0+

允许多事务并发读取

阻塞其他事务获取排他锁

事务提交/回滚后释放

(2) 排他锁(X锁)

SELECT * FROM products WHERE id = 1 FOR UPDATE;

独占锁,阻塞其他所有锁请求

自动应用于 UPDATE/DELETE 语句

(3) 意向锁(Intention Locks)

意向共享锁(IS):事务准备加共享锁前先获取

意向排他锁(IX):事务准备加排他锁前先获取

表级锁,用于快速判断表中是否有行锁

3. 行锁算法

锁类型 描述 SQL示例

记录锁(Record Lock) 锁定索引记录 WHERE id = 1 FOR UPDATE

间隙锁(Gap Lock) 锁定索引记录间隙 WHERE id BETWEEN 10 AND 20 FOR UPDATE

临键锁(Next-Key Lock) 记录锁+间隙锁组合 InnoDB 默认行锁算法

插入意向锁(Insert Intention Lock) INSERT操作设置的间隙锁 并发插入优化

4. 悲观锁实战案例

库存扣减场景:

START TRANSACTION;

-- 1. 查询并锁定商品

SELECT stock FROM products WHERE id = 1001 FOR UPDATE;

-- 2. 检查库存

-- 3. 更新库存

UPDATE products SET stock = stock - 1 WHERE id = 1001;

COMMIT;

二、乐观锁(Optimistic Locking)

1. 基本概念

核心思想:假定并发冲突很少发生,提交时检测冲突

特点:

应用层实现

适合读多写少场景

高并发性能好

需处理冲突重试

2. 实现方式

(1) 版本号机制

-- 表设计

ALTER TABLE products ADD COLUMN version INT DEFAULT 0;

-- 更新逻辑

UPDATE products

SET stock = stock - 1, version = version + 1

WHERE id = 1001 AND version = 当前版本;

-- 检查影响行数,0表示冲突

(2) 时间戳机制

ALTER TABLE products ADD COLUMN update_time TIMESTAMP;

UPDATE products

SET stock = stock - 1, update_time = CURRENT_TIMESTAMP

WHERE id = 1001 AND update_time = 上次查询时间;

(3) 条件判断(业务字段)

UPDATE accounts

SET balance = balance - 100

WHERE id = 1 AND balance >= 100;

-- 余额不足时自动失败

3. 乐观锁实战案例

Java 实现示例:

public boolean deductStock(Long productId, int quantity) {
    int retry = 3;
    while (retry-- > 0) {
        Product product = productDao.selectById(productId);
        int updated = productDao.updateStock(
            productId, 
            quantity, 
            product.getVersion());
        if (updated > 0) {
            return true;
        }
        // 短暂等待后重试
        Thread.sleep(50);
    }
    return false;
}

三、锁机制对比

维度

悲观锁

乐观锁

并发性能

差(锁竞争)

好(无阻塞)

冲突处理

预防冲突

检测冲突

实现层面

数据库层面

应用层面

适用场景

写多读少、强一致

读多写少、高并发

典型应用

银行转账

商品秒杀

四、InnoDB 锁的详细分类

1. 按锁粒度分类

锁类型

描述

开销

表级锁

锁定整张表

行级锁

锁定索引记录

页级锁

锁定数据页(InnoDB不支持)

2. InnoDB 特殊锁

锁类型

描述

作用

AUTO-INC锁

自增列插入时的表锁

保证自增顺序

谓词锁

空间索引使用的锁

GIS 操作

外键约束锁

外键检查时的锁

保证参照完整性

五、死锁处理与优化

1. 死锁产生条件

互斥条件

请求与保持条件

不剥夺条件

环路等待条件

2. 死锁检测

-- 查看最近死锁信息

SHOW ENGINE INNODB STATUS\G

-- 重点关注 LATEST DETECTED DEADLOCK 部分

3. 避免死锁策略

统一访问顺序:所有事务按相同顺序访问资源

减小事务粒度:缩短事务执行时间

设置超时:innodb_lock_wait_timeout = 50(默认50秒)

使用乐观锁:避免长时间持有锁

六、锁监控与性能优化

1. 锁等待监控

-- 查看当前锁等待

SELECT * FROM performance_schema.events_waits_current

WHERE EVENT_NAME LIKE '%lock%';

-- 查看被阻塞的事务

SELECT * FROM sys.innodb_lock_waits;

2. 锁相关参数优化

# 死锁检测开关(高并发时可临时关闭)

innodb_deadlock_detect = ON

# 锁等待超时(秒)

innodb_lock_wait_timeout = 30

# 行锁升级阈值(避免行锁过多升级为表锁)

innodb_row_lock_upgrade_threshold = 1000

七、实战场景锁选择指南

一、电商库存管理场景

案例1.1:秒杀系统实现(乐观锁方案)

业务需求

  • 1000件特价商品限时秒杀
  • 预计10万用户同时抢购
  • 必须防止超卖

数据库设计

CREATE TABLE `seckill_products` (
  `id` BIGINT NOT NULL,
  `name` VARCHAR(100),
  `stock` INT NOT NULL COMMENT '库存',
  `version` INT DEFAULT 0 COMMENT '乐观锁版本号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

INSERT INTO `seckill_products` VALUES (1001, 'iPhone 14', 1000, 0);

Java 实现代码

public boolean seckill(Long productId, Integer userId) {
    int retryTimes = 3;
    while (retryTimes-- > 0) {
        SeckillProduct product = productDao.getById(productId);
        if (product.getStock() <= 0) {
            return false;
        }
        
        int affectedRows = productDao.reduceStockWithVersion(
            productId, 
            product.getVersion());
            
        if (affectedRows > 0) {
            orderDao.createOrder(productId, userId);
            return true;
        }
        
        // 短暂等待后重试
        try { Thread.sleep(100); } 
        catch (InterruptedException e) { break; }
    }
    return false;
}

SQL 实现

<!-- MyBatis 映射 -->
<update id="reduceStockWithVersion">
    UPDATE seckill_products
    SET stock = stock - 1,
        version = version + 1
    WHERE id = #{id}
    AND stock > 0
    AND version = #{version}
</update>

压测结果

并发用户数

悲观锁方案TPS

乐观锁方案TPS

超卖情况

1,000

120

850

5,000

35

620

10,000

8

430

优化点

  1. 结合Redis预减库存,减轻数据库压力
  2. 异步记录订单,提高响应速度
  3. 设置分段锁(如将商品分成10个库存段)

二、银行转账场景(悲观锁方案)

案例2.1:跨账户资金转账

业务需求

  • 转账操作必须保证原子性
  • 高金额交易需要严格一致性
  • 防止并发操作导致余额错误

数据库表结构

CREATE TABLE `accounts` (
  `id` BIGINT PRIMARY KEY,
  `account_no` VARCHAR(20) UNIQUE,
  `balance` DECIMAL(15,2) NOT NULL,
  `frozen_amount` DECIMAL(15,2) DEFAULT 0
) ENGINE=InnoDB;

INSERT INTO `accounts` VALUES 
(1, '6225880111111111', 10000.00, 0),
(2, '6225880222222222', 5000.00, 0);

事务实现代码

@Transactional(rollbackFor = Exception.class)
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
    // 1. 锁定转出账户(避免死锁,按账号排序锁定)
    Account from = accountDao.lockByAccountNo(fromAccount);
    if (from.getBalance().compareTo(amount) < 0) {
        throw new BusinessException("余额不足");
    }
    
    // 2. 锁定转入账户
    Account to = accountDao.lockByAccountNo(toAccount);
    
    // 3. 执行转账
    accountDao.reduceBalance(fromAccount, amount);
    accountDao.addBalance(toAccount, amount);
    
    // 4. 记录交易流水
    transactionDao.record(from.getId(), to.getId(), amount);
}

SQL 锁定实现

<!-- 锁定账户 -->
<select id="lockByAccountNo" resultType="Account">
    SELECT * FROM accounts 
    WHERE account_no = #{accountNo}
    FOR UPDATE  -- 排他锁
</select>

死锁预防方案

  1. 统一获取锁的顺序(按账号排序)
List<String> accounts = Arrays.asList(fromAccount, toAccount);
accounts.sort(String::compareTo);
  1. 设置锁等待超时

SET innodb_lock_wait_timeout = 5; -- 5秒超时

  1. 添加重试机制
@Retryable(value = {DeadlockLoserDataAccessException.class}, 
           maxAttempts = 3, 
           backoff = @Backoff(delay = 100))
public void transferWithRetry(...) {
    // 转账逻辑
}

三、机票订票系统(混合锁方案)

案例3.1:高并发座位选择

业务特点

  • 读多写少(大量查询余票,少量下单)
  • 选座时需要临时锁定座位
  • 最终支付完成才真正扣减

数据库设计

CREATE TABLE `flights` (
  `id` BIGINT PRIMARY KEY,
  `flight_no` VARCHAR(20),
  `total_seats` INT,
  `available_seats` INT,
  `version` INT DEFAULT 0
);

CREATE TABLE `seat_locks` (
  `id` BIGINT PRIMARY KEY,
  `flight_id` BIGINT,
  `seat_no` VARCHAR(10),
  `user_id` BIGINT,
  `lock_time` DATETIME,
  `expire_time` DATETIME,
  INDEX `idx_flight_seat` (`flight_id`, `seat_no`)
);

分阶段锁策略

  1. 查询阶段(无锁)

SELECT available_seats FROM flights WHERE id = 1001;

  1. 选座阶段(乐观锁+记录锁)
// 1. 检查座位是否可用(无锁快照读)
SeatLock existLock = seatLockDao.findByFlightAndSeat(flightId, seatNo);
if (existLock != null && existLock.getExpireTime().after(now())) {
    throw new BusinessException("座位已被锁定");
}

// 2. 尝试锁定座位(乐观锁)
boolean locked = seatLockDao.tryLock(flightId, seatNo, userId, 10min);
if (!locked) {
    throw new BusinessException("锁定座位失败");
}
  1. 支付阶段(悲观锁)
@Transactional
public void confirmPayment(Long orderId) {
    // 1. 锁定订单和航班
    Order order = orderDao.lockById(orderId);
    Flight flight = flightDao.lockById(order.getFlightId());
    
    // 2. 验证座位锁定状态
    SeatLock lock = seatLockDao.findLock(order.getFlightId(), order.getSeatNo());
    if (lock == null || !lock.getUserId().equals(order.getUserId())) {
        throw new BusinessException("座位锁定已失效");
    }
    
    // 3. 扣减余票(悲观锁保证原子性)
    flightDao.reduceSeats(order.getFlightId(), 1);
    
    // 4. 删除临时锁定
    seatLockDao.unlock(lock.getId());
}

性能对比

方案

100并发查询

100并发下单

数据一致性

纯悲观锁

120 QPS

15 TPS

强一致

混合锁方案

850 QPS

80 TPS

最终一致

其他一些场景:

场景1:秒杀系统

推荐方案:乐观锁 + 缓存

UPDATE seckill_products

SET stock = stock - 1

WHERE id = 1001 AND stock > 0;

场景2:财务系统

推荐方案:悲观锁(SELECT FOR UPDATE)

原因:需要强一致性保证

场景3:报表生成

推荐方案:共享锁(LOCK IN SHARE MODE)

优势:允许多个查询并发读取

下面通过多个详细的业务场景案例,深入分析乐观锁和悲观锁的具体应用,以及在不同并发条件下的表现和优化方案。

四、分布式锁场景(跨服务锁定)

案例4.1:跨系统订单处理

业务需求

  • 订单服务与库存服务独立部署
  • 创建订单时需要锁定多个服务的资源
  • 必须避免不同服务间数据不一致

实现方案

  1. Redis分布式锁(初步锁定)
public boolean createOrder(OrderDTO order) {
    // 1. 获取分布式锁
    String lockKey = "lock:product:" + order.getProductId();
    boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, order.getUserId(), 10, TimeUnit.SECONDS);
    if (!locked) {
        throw new BusinessException("系统繁忙,请重试");
    }
    
    try {
        // 2. 调用库存服务(HTTP)
        inventoryService.reduceStock(order.getProductId(), order.getQuantity());
        
        // 3. 创建订单(本地事务)
        orderService.createLocalOrder(order);
    } finally {
        // 4. 释放锁
        redisTemplate.delete(lockKey);
    }
}
  1. 最终一致性方案(Saga模式)
// Saga执行器
@Saga
public void createOrderSaga(OrderDTO order) {
    // 步骤1:预留库存
    saga.addStep(
        () -> inventoryService.blockStock(order),
        () -> inventoryService.unblockStock(order)
    );
    
    // 步骤2:创建订单
    saga.addStep(
        () -> orderService.createPendingOrder(order),
        () -> orderService.cancelPendingOrder(order)
    );
    
    // 步骤3:确认操作
    saga.addStep(
        () -> {
            inventoryService.confirmBlock(order);
            orderService.confirmOrder(order);
        }
    );
}

异常处理对比

方案

网络分区处理

服务宕机恢复

实现复杂度

分布式锁

依赖超时

可能死锁

中等

Saga模式

自动补偿

状态可恢复

五、监控与排查工具

5.1 锁等待分析

sql

复制

-- 查看当前锁等待
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread
FROM performance_schema.events_waits_current w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_instance
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_instance;

5.2 锁争用热点分析

sql

复制

-- 找出等待时间最长的锁
SELECT 
    object_schema,
    object_name,
    index_name,
    lock_type,
    lock_mode,
    COUNT(*) as conflict_count,
    AVG(TIMESTAMPDIFF(SECOND, wait_started, now())) as avg_wait_seconds
FROM sys.innodb_lock_waits
GROUP BY object_schema, object_name, index_name
ORDER BY avg_wait_seconds DESC;

六、最佳实践总结

  1. 锁选择原则
    • 读多写少 → 乐观锁
    • 写多读少 → 悲观锁
    • 跨服务操作 → 分布式锁+Saga
  1. 性能优化技巧
    • 减小锁粒度(行锁而非表锁)
    • 缩短锁持有时间(避免事务中包含RPC调用)
    • 添加合理的重试机制
  1. 避免常见陷阱
    • 乐观锁的重试次数不宜过多(通常3次)
    • 悲观锁必须按固定顺序获取
    • 分布式锁必须设置超时时间

通过以上实际场景的深度分析,可以看出没有放之四海而皆准的锁方案,必须根据具体业务特点、并发规模和数据一致性要求来选择最合适的并发控制策略。


 

八、最佳实践建议

索引设计:

确保锁定的行有索引,否则会锁表

事务设计:

尽量短小

避免用户交互

合理设置隔离级别

监控:

定期检查 information_schema.INNODB_LOCKS

混合使用:

关键业务可结合悲观锁+乐观锁

通过深入理解 MySQL 锁机制,可以针对不同业务场景选择最合适的并发控制策略,在保证数据一致性的同时获得最佳性能。

相关文章:

  • ubuntu 2204键盘按键映射修改
  • DataGear 5.3.0 制作支持导出表格数据的数据可视化看板
  • OceanBase的闪回查询功能实践
  • IP数据报报文格式
  • 英伟达「虚拟轨道+AI调度」专利:开启自动驾驶3.0时代的隐形革命
  • 离散的数据及参数适合用什么算法做模型
  • vscode_拼写关闭
  • 从 WPF 到 MAUI:跨平台 UI 开发的进化之路
  • C++使用do {} while(false)的好处
  • 机器学习模型类型
  • OpenCV 图形API(2)为什么需要图形API?
  • 关于《数据资源建设费用测算标准》(工作组讨论稿)意见征集的通知
  • 白盒测试用例的设计
  • 【学Rust写CAD】16 0、1、-1代数单位元(algebraic_units.rs)
  • Redis:概念与常用命令
  • mysql-分区和性能
  • Java项目生成接口文档的方案
  • Linux环境上传本地文件安装mysql
  • MySQL(数据表创建)
  • 【持续集成和持续部署】
  • 长安汽车辟谣作为二级企业并入东风集团:将追究相关方责任
  • 福特汽车撤回业绩指引,警告关税或造成15亿美元利润损失
  • 胡祥|人工智能时代:文艺评论何为?
  • “五一”假期预计全社会跨区域人员流动累计14.67亿人次
  • 科普|治疗腰椎间盘突出症,筋骨平衡理论如何提供新视角?
  • 解锁川北底色密码,“文化三地”志愿宣讲员招募计划启动报名