【MYSQL】MySQL 锁机制与死锁处理深度解析

发布于:2025-08-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

MySQL 锁的分类

1. 按锁的粒度分类

1.1 全局锁(Global Lock)
  • 作用范围:整个MySQL实例
  • 典型场景:全库逻辑备份
  • 实现方式FLUSH TABLES WITH READ LOCK
  • 特点:阻塞所有DML和DDL操作
-- 加全局读锁
FLUSH TABLES WITH READ LOCK;
-- 执行备份操作
mysqldump --single-transaction --routines --triggers --all-databases > backup.sql
-- 释放锁
UNLOCK TABLES;
1.2 表锁(Table Lock)
  • 作用范围:整张表
  • 分类
    • 表共享读锁(Table Read Lock)
    • 表独占写锁(Table Write Lock)
    • 意向锁(Intention Lock)
-- 表级读锁
LOCK TABLES table_name READ;
-- 表级写锁
LOCK TABLES table_name WRITE;
-- 释放锁
UNLOCK TABLES;

实际生产应用场景:

场景1:数据库维护和批量导入
-- 场景:电商系统夜间批量导入商品数据
-- 防止导入过程中有用户查询,保证数据一致性
LOCK TABLES products WRITE;
LOAD DATA INFILE '/data/products_update.csv' 
INTO TABLE products 
FIELDS TERMINATED BY ',' 
LINES TERMINATED BY '\n';
-- 批量更新商品价格
UPDATE products SET price = price * 1.1 WHERE category = 'electronics';
UNLOCK TABLES;
场景2:报表生成时的数据一致性保证
-- 场景:金融系统生成月末对账报表
-- 确保生成报表期间数据不被修改
LOCK TABLES 
    transactions READ,
    accounts READ,
    balances READ;
    
-- 生成复杂的财务报表
SELECT 
    a.account_id,
    a.account_name,
    SUM(t.amount) as total_transactions,
    b.current_balance
FROM accounts a
JOIN transactions t ON a.account_id = t.account_id
JOIN balances b ON a.account_id = b.account_id
WHERE t.transaction_date BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY a.account_id;

UNLOCK TABLES;
场景3:数据迁移和结构变更
-- 场景:系统升级时的数据迁移
-- 保证迁移过程中数据不被修改
LOCK TABLES old_user_table READ, new_user_table WRITE;

-- 数据迁移逻辑
INSERT INTO new_user_table (user_id, username, email, created_at)
SELECT user_id, user_name, email_address, create_time
FROM old_user_table;

UNLOCK TABLES;
1.3 行锁(Row Lock)
  • 作用范围:表中的行记录
  • 引擎支持:InnoDB引擎
  • 分类
    • 共享锁(S Lock)
    • 排他锁(X Lock)

2. 按锁的性质分类

2.1 共享锁(Shared Lock, S Lock)
  • 特性:多个事务可以同时持有同一资源的共享锁
  • 兼容性:与共享锁兼容,与排他锁不兼容
  • SQL语句SELECT ... LOCK IN SHARE MODE
-- 加共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- MySQL 8.0 新语法
SELECT * FROM users WHERE id = 1 FOR SHARE;

实际生产应用场景:

场景1:订单库存检查和预扣减
-- 场景:电商下单时的库存检查
-- 多个用户同时下单同一商品,需要读取库存但防止被修改
BEGIN;
-- 使用共享锁读取库存,允许其他事务也读取,但不允许修改
SELECT stock_quantity 
FROM products 
WHERE product_id = 12345 
FOR SHARE;

-- 业务逻辑检查库存是否充足
-- 如果库存充足,再获取排他锁进行扣减
IF stock_quantity >= order_quantity THEN
    UPDATE products 
    SET stock_quantity = stock_quantity - order_quantity 
    WHERE product_id = 12345;
END IF;
COMMIT;
场景2:金融系统的余额查询
-- 场景:银行系统查询余额时防止余额被修改
-- 多个查询可以同时进行,但防止转账操作修改余额
BEGIN;
SELECT 
    account_id,
    balance,
    available_balance
FROM accounts 
WHERE account_id = 'ACC001' 
FOR SHARE;

-- 其他业务逻辑,如计算利息、生成报表等
COMMIT;
场景3:配置信息的并发读取
-- 场景:系统配置读取,允许多个服务同时读取但防止配置被修改
SELECT config_value 
FROM system_config 
WHERE config_key = 'payment_gateway_url' 
FOR SHARE;
2.2 排他锁(Exclusive Lock, X Lock)
  • 特性:只有一个事务可以持有排他锁
  • 兼容性:与任何锁都不兼容
  • SQL语句SELECT ... FOR UPDATEUPDATEDELETEINSERT
-- 加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- DML操作自动加排他锁
UPDATE users SET name = 'John' WHERE id = 1;

实际生产应用场景:

场景1:秒杀系统的库存扣减
-- 场景:秒杀活动,确保库存扣减的原子性
-- 防止超卖问题
BEGIN;
-- 使用排他锁锁定商品记录
SELECT stock_quantity 
FROM seckill_products 
WHERE product_id = 99999 AND activity_id = 12345
FOR UPDATE;

-- 检查库存并扣减
UPDATE seckill_products 
SET stock_quantity = stock_quantity - 1,
    sold_quantity = sold_quantity + 1
WHERE product_id = 99999 
  AND activity_id = 12345 
  AND stock_quantity > 0;

-- 如果影响行数为0,说明库存不足
-- 创建订单记录
INSERT INTO orders (user_id, product_id, quantity, order_time)
VALUES (12345, 99999, 1, NOW());

COMMIT;
场景2:银行转账操作
-- 场景:银行转账,确保账户余额操作的原子性
BEGIN;
-- 锁定转出账户
SELECT balance 
FROM accounts 
WHERE account_id = 'FROM_ACCOUNT' 
FOR UPDATE;

-- 锁定转入账户
SELECT balance 
FROM accounts 
WHERE account_id = 'TO_ACCOUNT' 
FOR UPDATE;

-- 扣减转出账户余额
UPDATE accounts 
SET balance = balance - 1000 
WHERE account_id = 'FROM_ACCOUNT' AND balance >= 1000;

-- 增加转入账户余额
UPDATE accounts 
SET balance = balance + 1000 
WHERE account_id = 'TO_ACCOUNT';

-- 记录转账流水
INSERT INTO transactions (from_account, to_account, amount, trans_time)
VALUES ('FROM_ACCOUNT', 'TO_ACCOUNT', 1000, NOW());

COMMIT;
场景3:唯一序列号生成
-- 场景:生成全局唯一的订单号
BEGIN;
-- 锁定序列号表
SELECT current_value 
FROM sequence_generator 
WHERE seq_name = 'ORDER_NO' 
FOR UPDATE;

-- 更新序列号
UPDATE sequence_generator 
SET current_value = current_value + 1 
WHERE seq_name = 'ORDER_NO';

-- 生成订单号:日期 + 序列号
SET @order_no = CONCAT(DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(current_value + 1, 8, '0'));

COMMIT;
场景4:状态机流转控制
-- 场景:工单状态流转,确保状态变更的唯一性
BEGIN;
-- 锁定工单记录
SELECT status, assigned_to 
FROM work_orders 
WHERE order_id = 'WO20241201001' 
FOR UPDATE;

-- 检查状态流转是否合法
-- 只有状态为'PENDING'才能转为'IN_PROGRESS'
UPDATE work_orders 
SET status = 'IN_PROGRESS',
    assigned_to = 'USER123',
    update_time = NOW()
WHERE order_id = 'WO20241201001' 
  AND status = 'PENDING';

-- 记录状态变更日志
INSERT INTO order_status_log (order_id, old_status, new_status, operator, change_time)
VALUES ('WO20241201001', 'PENDING', 'IN_PROGRESS', 'USER123', NOW());

COMMIT;

3. InnoDB特有的锁

3.1 意向锁(Intention Lock)
  • 意向共享锁(IS):事务准备在某些行上加共享锁
  • 意向排他锁(IX):事务准备在某些行上加排他锁
3.2 记录锁(Record Lock)
-- 对主键或唯一索引的等值查询
SELECT * FROM users WHERE id = 1 FOR UPDATE;

实际生产应用场景:

-- 场景:用户账户余额精确锁定
-- 只锁定特定用户记录,不影响其他用户操作
BEGIN;
SELECT balance 
FROM user_accounts 
WHERE user_id = 12345 
FOR UPDATE;  -- 只锁定user_id=12345这一行

UPDATE user_accounts 
SET balance = balance - 100 
WHERE user_id = 12345;
COMMIT;
3.3 间隙锁(Gap Lock)
-- 在RR隔离级别下,范围查询会产生间隙锁
SELECT * FROM users WHERE id BETWEEN 5 AND 10 FOR UPDATE;

实际生产应用场景:

场景1:防止幻读的范围查询
-- 场景:统计某个时间段的订单,防止统计期间有新订单插入
BEGIN;
-- 锁定时间范围,防止新记录插入到这个范围内
SELECT COUNT(*), SUM(amount) 
FROM orders 
WHERE create_time BETWEEN '2024-12-01 00:00:00' AND '2024-12-01 23:59:59'
FOR UPDATE;

-- 其他业务逻辑
COMMIT;
场景2:确保ID序列的连续性
-- 场景:确保批次号的连续性,防止中间插入其他批次
BEGIN;
-- 查询最大批次号并锁定间隙
SELECT MAX(batch_id) as max_batch 
FROM production_batches 
WHERE product_line = 'LINE_A'
FOR UPDATE;

-- 插入新的连续批次
INSERT INTO production_batches (batch_id, product_line, start_time)
VALUES (max_batch + 1, 'LINE_A', NOW());
COMMIT;
3.4 临键锁(Next-Key Lock)
  • 定义:记录锁 + 间隙锁的组合
  • 作用:解决幻读问题

实际生产应用场景:

场景1:防止幻读的分页查询
-- 场景:金融系统的交易记录分页,防止分页过程中数据变化
BEGIN;
-- 使用临键锁防止范围内的幻读
SELECT transaction_id, amount, transaction_time
FROM transactions 
WHERE account_id = 'ACC001' 
  AND transaction_time >= '2024-12-01'
ORDER BY transaction_time
LIMIT 20
FOR UPDATE;
COMMIT;
场景2:库存预留和释放
-- 场景:电商系统库存预留,防止同一商品的并发预留冲突
BEGIN;
-- 锁定商品ID范围,防止相关记录的插入和修改
SELECT * FROM inventory_reservations 
WHERE product_id BETWEEN 1000 AND 1100
  AND status = 'ACTIVE'
FOR UPDATE;

-- 插入新的预留记录
INSERT INTO inventory_reservations (product_id, user_id, quantity, expire_time)
VALUES (1050, 12345, 2, DATE_ADD(NOW(), INTERVAL 30 MINUTE));
COMMIT;
3.5 插入意向锁(Insert Intention Lock)
-- 插入操作会先尝试获取插入意向锁
INSERT INTO users (id, name) VALUES (15, 'Alice');

实际生产应用场景:

场景1:高并发订单创建
-- 场景:电商秒杀,大量用户同时创建订单
-- 插入意向锁允许多个事务同时插入不同的记录

-- 事务1
BEGIN;
INSERT INTO orders (order_id, user_id, product_id, create_time)
VALUES ('ORD001', 1001, 888, NOW());
COMMIT;

-- 事务2(同时进行)
BEGIN;
INSERT INTO orders (order_id, user_id, product_id, create_time)
VALUES ('ORD002', 1002, 888, NOW());  -- 不会被事务1阻塞
COMMIT;
场景2:分布式ID生成
-- 场景:多个服务节点同时生成分布式ID
-- 不同节点可以同时插入不同范围的ID
CREATE TABLE distributed_ids (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    node_id INT,
    business_type VARCHAR(50),
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 节点1插入
INSERT INTO distributed_ids (node_id, business_type) 
VALUES (1, 'ORDER');

-- 节点2同时插入(不会冲突)
INSERT INTO distributed_ids (node_id, business_type) 
VALUES (2, 'ORDER');
3.6 意向锁的实际应用

实际生产应用场景:

场景1:表级操作与行级操作的协调
-- 场景:数据库维护期间,需要锁定整个表
-- 意向锁帮助快速判断表是否被行级锁占用

-- 某个事务正在执行行级操作
BEGIN;
SELECT * FROM orders WHERE order_id = 'ORD001' FOR UPDATE;  -- 自动加IS锁到表级别
-- 长时间的业务处理...

-- 另一个维护操作尝试锁定整个表
LOCK TABLES orders WRITE;  -- 会检查意向锁,发现表被占用,等待或失败
场景2:在线DDL操作的冲突检测
-- 场景:在线修改表结构时,检查是否有活跃的事务
-- 意向锁帮助快速判断表的使用情况

-- 正在进行的业务操作
BEGIN;
UPDATE orders SET status = 'SHIPPED' WHERE order_id = 'ORD001';  -- 加IX锁

-- DBA尝试修改表结构
ALTER TABLE orders ADD COLUMN shipping_address TEXT;  -- 会检查意向锁状态

死锁判定原理

1. 死锁的定义

死锁是指两个或多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象。

2. 死锁的四个必要条件

  1. 互斥条件:资源不能被多个事务同时使用
  2. 请求和保持条件:事务已经持有资源,又请求新的资源
  3. 不剥夺条件:已获得的资源不能被强制剥夺
  4. 环路等待条件:存在一个事务等待环路

3. MySQL死锁检测机制

3.1 等待图算法(Wait-for Graph)
事务T1 → 等待 → 资源R1 → 被持有 → 事务T2
事务T2 → 等待 → 资源R2 → 被持有 → 事务T1
3.2 死锁检测参数
-- 查看死锁检测相关参数
SHOW VARIABLES LIKE '%deadlock%';
SHOW VARIABLES LIKE '%timeout%';

-- 关键参数
-- innodb_deadlock_detect: 是否开启死锁检测(默认ON)
-- innodb_lock_wait_timeout: 锁等待超时时间(默认50秒)
3.3 死锁信息查看
-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS;

-- 或者查看错误日志
SHOW VARIABLES LIKE 'log_error';

死锁的具体场景

场景1:经典的两个事务相互等待

表结构:

CREATE TABLE account (
    id INT PRIMARY KEY,
    balance DECIMAL(10,2),
    name VARCHAR(50)
);

INSERT INTO account VALUES (1, 1000.00, 'Alice'), (2, 1000.00, 'Bob');

死锁场景:

-- 事务1
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;  -- 获得id=1的行锁
-- 此时等待事务2执行
UPDATE account SET balance = balance + 100 WHERE id = 2;  -- 等待id=2的行锁

-- 事务2
BEGIN;
UPDATE account SET balance = balance - 50 WHERE id = 2;   -- 获得id=2的行锁
UPDATE account SET balance = balance + 50 WHERE id = 1;   -- 等待id=1的行锁,死锁!

场景2:索引不当导致的死锁

表结构:

CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    status VARCHAR(20),
    amount DECIMAL(10,2),
    INDEX idx_user_id (user_id)
);

死锁场景:

-- 事务1
BEGIN;
UPDATE orders SET status = 'paid' WHERE user_id = 100;

-- 事务2
BEGIN;
UPDATE orders SET status = 'shipped' WHERE user_id = 100;
-- 如果user_id有多条记录,可能因为锁定顺序不同导致死锁

场景3:间隙锁导致的死锁

-- 事务1
BEGIN;
SELECT * FROM orders WHERE id = 15 FOR UPDATE;  -- 假设id=15不存在,产生间隙锁

-- 事务2
BEGIN;
SELECT * FROM orders WHERE id = 16 FOR UPDATE;  -- 假设id=16不存在,产生间隙锁
INSERT INTO orders (id, user_id, status, amount) VALUES (15, 100, 'pending', 100.00);
-- 事务2尝试插入,等待事务1的间隙锁

-- 事务1继续
INSERT INTO orders (id, user_id, status, amount) VALUES (16, 101, 'pending', 200.00);
-- 事务1尝试插入,等待事务2的间隙锁,形成死锁

场景4:批量操作导致的死锁

-- 事务1:按id正序更新
BEGIN;
UPDATE products SET stock = stock - 1 WHERE id IN (1, 2, 3, 4, 5);

-- 事务2:按id倒序更新
BEGIN;
UPDATE products SET stock = stock - 1 WHERE id IN (5, 4, 3, 2, 1);
-- 可能因为锁定顺序不同导致死锁

死锁的解决方案

1. 预防死锁

1.1 统一锁定顺序
-- 错误示例:不同的锁定顺序
-- 事务1: 先锁A后锁B
-- 事务2: 先锁B后锁A

-- 正确示例:统一按主键大小顺序锁定
BEGIN;
-- 总是按照id从小到大的顺序锁定
SELECT * FROM account WHERE id = 1 FOR UPDATE;
SELECT * FROM account WHERE id = 2 FOR UPDATE;
-- 执行业务逻辑
COMMIT;
1.2 减少事务持锁时间
-- 优化前:长事务
BEGIN;
SELECT * FROM orders WHERE user_id = 100 FOR UPDATE;
-- 执行复杂的业务逻辑(网络调用、文件操作等)
UPDATE orders SET status = 'processed' WHERE user_id = 100;
COMMIT;

-- 优化后:短事务
-- 先查询数据
SELECT * FROM orders WHERE user_id = 100;
-- 执行业务逻辑
-- 最后快速更新
BEGIN;
UPDATE orders SET status = 'processed' WHERE user_id = 100;
COMMIT;
1.3 使用较低的隔离级别
-- 在允许的业务场景下使用READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

2. 检测和处理死锁

2.1 应用层重试机制
@Service
public class OrderService {
    
    private static final int MAX_RETRY_TIMES = 3;
    
    @Transactional
    public void updateOrder(Long orderId) {
        int retryCount = 0;
        
        while (retryCount < MAX_RETRY_TIMES) {
            try {
                // 执行数据库操作
                doUpdateOrder(orderId);
                break; // 成功则跳出循环
                
            } catch (SQLException e) {
                if (isDeadlock(e) && retryCount < MAX_RETRY_TIMES - 1) {
                    retryCount++;
                    // 随机等待一段时间后重试
                    try {
                        Thread.sleep(50 + new Random().nextInt(100));
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException("操作被中断", ie);
                    }
                } else {
                    throw new RuntimeException("更新订单失败", e);
                }
            }
        }
    }
    
    private boolean isDeadlock(SQLException e) {
        return e.getErrorCode() == 1213; // MySQL死锁错误码
    }
}
2.2 设置合理的锁等待超时时间
-- 设置锁等待超时时间(单位:秒)
SET innodb_lock_wait_timeout = 5;

-- 全局设置
SET GLOBAL innodb_lock_wait_timeout = 10;
2.3 监控死锁
-- 创建死锁监控脚本
DELIMITER $$
CREATE PROCEDURE MonitorDeadlocks()
BEGIN
    DECLARE deadlock_count INT DEFAULT 0;
    
    -- 查询死锁计数
    SELECT VARIABLE_VALUE INTO deadlock_count 
    FROM performance_schema.global_status 
    WHERE VARIABLE_NAME = 'Innodb_deadlocks';
    
    -- 记录死锁信息
    INSERT INTO deadlock_log (deadlock_count, check_time) 
    VALUES (deadlock_count, NOW());
END$$
DELIMITER ;

-- 创建日志表
CREATE TABLE deadlock_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    deadlock_count INT,
    check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3. 数据库配置优化

-- 优化死锁检测
SET GLOBAL innodb_deadlock_detect = ON;

-- 设置合理的锁等待超时
SET GLOBAL innodb_lock_wait_timeout = 10;

-- 优化事务隔离级别
SET GLOBAL transaction_isolation = 'READ-COMMITTED';

-- 调整锁相关参数
SET GLOBAL innodb_table_locks = OFF;

最佳实践

1. 设计层面

1.1 合理的索引设计
-- 为经常用于WHERE条件的列创建索引
CREATE INDEX idx_user_status ON orders(user_id, status);

-- 避免过多的索引,减少锁竞争
-- 定期检查和清理不必要的索引
1.2 表结构优化
-- 使用合适的数据类型
-- 避免过长的行,减少锁的粒度影响
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    status TINYINT NOT NULL DEFAULT 0,
    amount DECIMAL(10,2) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_status (user_id, status)
);

2. 应用层面

2.1 事务设计原则
// 1. 保持事务简短
@Transactional
public void shortTransaction() {
    // 只包含必要的数据库操作
    orderRepository.updateStatus(orderId, "PAID");
}

// 2. 避免在事务中进行外部调用
public void processOrder(Long orderId) {
    // 外部调用放在事务外
    PaymentResult result = paymentService.process(orderId);
    
    // 事务内只做数据库操作
    updateOrderStatus(orderId, result.getStatus());
}
2.2 批量操作优化
// 优化批量操作,使用统一的排序
public void batchUpdateOrders(List<Long> orderIds) {
    // 对ID进行排序,保证锁定顺序一致
    Collections.sort(orderIds);
    
    for (Long orderId : orderIds) {
        updateOrder(orderId);
    }
}

3. 监控和排查

3.1 死锁监控SQL
-- 查看死锁状态
SELECT 
    ENGINE_TRANSACTION_ID,
    THREAD_ID,
    EVENT_NAME,
    CURRENT_SCHEMA,
    SQL_TEXT,
    BLOCKING_ENGINE_TRANSACTION_ID
FROM performance_schema.events_statements_current 
WHERE SQL_TEXT IS NOT NULL;

-- 查看锁等待情况
SELECT 
    waiting.ENGINE_TRANSACTION_ID as waiting_trx_id,
    waiting.requesting_engine_lock_id,
    blocking.ENGINE_TRANSACTION_ID as blocking_trx_id,
    blocking.blocking_engine_lock_id
FROM performance_schema.data_lock_waits waiting
JOIN performance_schema.data_locks blocking 
    ON waiting.blocking_engine_lock_id = blocking.engine_lock_id;
3.2 性能分析
-- 启用慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

-- 分析锁竞争
SELECT 
    object_schema,
    object_name,
    count_read,
    count_write,
    count_read_with_shared_locks,
    count_read_high_priority,
    count_read_no_insert,
    count_read_external
FROM performance_schema.table_io_waits_summary_by_table
WHERE object_schema NOT IN ('mysql', 'information_schema', 'performance_schema')
ORDER BY count_read + count_write DESC;

综合实际应用案例

电商秒杀系统的完整锁策略

这是一个综合运用多种锁机制的实际生产案例:

@Service
@Transactional
public class SeckillService {
    
    /**
     * 秒杀下单 - 综合锁策略应用
     */
    public SeckillResult processSeckill(Long userId, Long productId, Long activityId) {
        try {
            // 1. 使用共享锁检查活动状态(允许多个用户同时检查)
            String activityStatus = checkActivityStatus(activityId);
            if (!"ACTIVE".equals(activityStatus)) {
                return SeckillResult.fail("活动未开始或已结束");
            }
            
            // 2. 使用排他锁锁定库存记录(防止超卖)
            if (!lockAndCheckStock(productId, activityId)) {
                return SeckillResult.fail("库存不足");
            }
            
            // 3. 使用记录锁检查用户是否已参与(避免重复购买)
            if (hasUserParticipated(userId, activityId)) {
                return SeckillResult.fail("您已参与过此活动");
            }
            
            // 4. 创建订单(插入意向锁允许并发插入不同订单)
            String orderId = createOrder(userId, productId, activityId);
            
            // 5. 扣减库存
            updateStock(productId, activityId);
            
            return SeckillResult.success(orderId);
            
        } catch (Exception e) {
            // 死锁重试机制
            if (isDeadlock(e)) {
                return retrySeckill(userId, productId, activityId);
            }
            throw e;
        }
    }
    
    @Transactional(readOnly = true)
    private String checkActivityStatus(Long activityId) {
        // 使用共享锁,允许多个事务同时读取活动状态
        return jdbcTemplate.queryForObject(
            "SELECT status FROM seckill_activities WHERE id = ? FOR SHARE",
            String.class, activityId);
    }
    
    private boolean lockAndCheckStock(Long productId, Long activityId) {
        // 使用排他锁锁定库存记录
        Integer stock = jdbcTemplate.queryForObject(
            "SELECT stock_quantity FROM seckill_products " +
            "WHERE product_id = ? AND activity_id = ? FOR UPDATE",
            Integer.class, productId, activityId);
        
        return stock != null && stock > 0;
    }
    
    private boolean hasUserParticipated(Long userId, Long activityId) {
        // 使用记录锁检查用户参与记录
        Integer count = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM seckill_orders " +
            "WHERE user_id = ? AND activity_id = ? FOR UPDATE",
            Integer.class, userId, activityId);
        
        return count > 0;
    }
    
    private String createOrder(Long userId, Long productId, Long activityId) {
        String orderId = generateOrderId();
        
        // 插入订单,插入意向锁允许并发插入
        jdbcTemplate.update(
            "INSERT INTO seckill_orders (order_id, user_id, product_id, activity_id, status, create_time) " +
            "VALUES (?, ?, ?, ?, 'PENDING', NOW())",
            orderId, userId, productId, activityId);
        
        return orderId;
    }
    
    private void updateStock(Long productId, Long activityId) {
        // 扣减库存
        int affected = jdbcTemplate.update(
            "UPDATE seckill_products " +
            "SET stock_quantity = stock_quantity - 1, sold_quantity = sold_quantity + 1 " +
            "WHERE product_id = ? AND activity_id = ? AND stock_quantity > 0",
            productId, activityId);
        
        if (affected == 0) {
            throw new RuntimeException("库存扣减失败");
        }
    }
}

分布式锁与数据库锁的配合使用

@Service
public class DistributedLockWithDbLockService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 分布式锁 + 数据库锁的组合策略
     * 外层使用Redis分布式锁减少数据库压力
     * 内层使用数据库锁保证最终一致性
     */
    public boolean transferMoney(String fromAccount, String toAccount, BigDecimal amount) {
        // 1. 按账户ID排序,避免死锁
        List<String> accounts = Arrays.asList(fromAccount, toAccount);
        Collections.sort(accounts);
        
        // 2. 获取分布式锁(减少数据库锁竞争)
        String lockKey = "transfer:" + String.join(":", accounts);
        
        return executeWithDistributedLock(lockKey, () -> {
            return executeTransfer(fromAccount, toAccount, amount);
        });
    }
    
    @Transactional
    private boolean executeTransfer(String fromAccount, String toAccount, BigDecimal amount) {
        // 3. 按固定顺序获取数据库锁,避免死锁
        List<String> accounts = Arrays.asList(fromAccount, toAccount);
        Collections.sort(accounts);
        
        Map<String, BigDecimal> balances = new HashMap<>();
        
        // 4. 依次锁定账户(排他锁)
        for (String account : accounts) {
            BigDecimal balance = jdbcTemplate.queryForObject(
                "SELECT balance FROM accounts WHERE account_id = ? FOR UPDATE",
                BigDecimal.class, account);
            balances.put(account, balance);
        }
        
        // 5. 检查余额
        if (balances.get(fromAccount).compareTo(amount) < 0) {
            return false;
        }
        
        // 6. 执行转账
        jdbcTemplate.update(
            "UPDATE accounts SET balance = balance - ? WHERE account_id = ?",
            amount, fromAccount);
        
        jdbcTemplate.update(
            "UPDATE accounts SET balance = balance + ? WHERE account_id = ?",
            amount, toAccount);
        
        return true;
    }
    
    private <T> T executeWithDistributedLock(String lockKey, Supplier<T> task) {
        String lockValue = UUID.randomUUID().toString();
        try {
            // 获取分布式锁
            boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30));
            
            if (!locked) {
                throw new RuntimeException("获取锁失败");
            }
            
            return task.get();
            
        } finally {
            // 释放分布式锁
            releaseLock(lockKey, lockValue);
        }
    }
}

生产环境最佳实践总结

1. 锁选择策略矩阵

应用场景 推荐锁类型 原因 注意事项
库存扣减 排他锁(FOR UPDATE) 防止超卖 事务要短,及时提交
余额查询 共享锁(FOR SHARE) 允许并发读,防止读脏数据 避免长时间持锁
批量导入 表写锁(WRITE) 保证数据一致性 选择业务低峰期
报表生成 表读锁(READ) 保证报表数据一致性 使用只读副本更好
范围统计 临键锁 防止幻读 考虑使用快照读
高并发插入 插入意向锁 提高并发性能 避免间隙锁冲突

2. 死锁预防检查清单

-- 1. 检查事务隔离级别
SELECT @@transaction_isolation;

-- 2. 检查死锁检测设置
SHOW VARIABLES LIKE '%deadlock%';

-- 3. 检查锁等待超时设置
SHOW VARIABLES LIKE '%lock_wait_timeout%';

-- 4. 监控死锁发生情况
SELECT VARIABLE_VALUE FROM performance_schema.global_status 
WHERE VARIABLE_NAME = 'Innodb_deadlocks';

3. 代码层面最佳实践

// ✅ 正确:统一锁定顺序
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
    Long firstId = Math.min(fromId, toId);
    Long secondId = Math.max(fromId, toId);
    
    // 总是按ID大小顺序锁定
    lockAccount(firstId);
    lockAccount(secondId);
    // 执行转账逻辑
}

// ❌ 错误:随意的锁定顺序
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
    lockAccount(fromId);   // 可能导致死锁
    lockAccount(toId);
}

// ✅ 正确:短事务
@Transactional
public void quickUpdate(Long id, String status) {
    orderService.updateStatus(id, status);  // 只包含数据库操作
}

public void processOrder(Long id) {
    // 复杂逻辑在事务外执行
    PaymentResult result = paymentService.process(id);
    sendNotification(id);
    
    // 快速更新数据库
    quickUpdate(id, result.getStatus());
}

// ❌ 错误:长事务
@Transactional
public void processOrder(Long id) {
    orderService.updateStatus(id, "PROCESSING");
    paymentService.process(id);        // 外部调用
    Thread.sleep(5000);               // 长时间等待
    sendNotification(id);             // 网络调用
    orderService.updateStatus(id, "COMPLETED");
}

总结

MySQL的锁机制是保证数据一致性的重要手段,但也是死锁产生的根源。理解不同类型的锁、死锁的形成原理和解决方案,对于开发高并发、高可靠的数据库应用至关重要。

关键要点:

  1. 预防优于治疗:通过合理的设计和编码规范预防死锁
  2. 统一锁定顺序:避免不同事务以不同顺序锁定资源
  3. 保持事务简短:减少锁持有时间
  4. 合理使用索引:提高查询效率,减少锁范围
  5. 建立监控机制:及时发现和处理死锁问题
  6. 应用层重试:在业务允许的情况下实现重试机制
  7. 选择合适的锁:根据业务场景选择最适合的锁类型
  8. 分层锁策略:结合分布式锁和数据库锁,减少竞争

通过这些最佳实践和实际应用场景的理解,可以大大降低死锁的发生概率,提高系统的稳定性和性能。在实际生产环境中,要根据具体的业务场景和性能要求,灵活运用各种锁机制,构建高效、可靠的数据库应用系统。