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

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. 分层锁策略:结合分布式锁和数据库锁,减少竞争

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sunywz

~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值