MySQL实现可重入锁

什么是可重入锁?

可重入锁(Reentrant Lock)是指同一个线程在持有锁的情况下,可以再次获取同一把锁而不会发生死锁的锁机制。简单来说,如果线程A已经获得了锁X,当它再次请求获取锁X时,可以直接获取成功。

为什么需要可重入锁?

在实际开发中,我们经常会遇到这样的场景:方法A调用了方法B,而方法A和方法B都需要获取同一把锁来保证线程安全。如果使用的是不可重入锁,那么当方法A获取锁后调用方法B时,方法B再次请求获取同一把锁就会导致死锁。

可重入锁解决了以下问题:

  1. 避免死锁:同一线程多次获取同一把锁不会死锁
  2. 提高代码可维护性:方法可以独立管理自己的锁,不需要知道调用方是否已经持有锁
  3. 支持方法的递归调用:递归方法中多次获取同一把锁不会死锁

MySQL中如何实现可重入锁?

MySQL本身没有直接提供可重入锁机制,但我们可以通过创建一个锁表来模拟实现:

CREATE TABLE `lock_table` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `lock_name` VARCHAR(255) NOT NULL,    -- 锁的名称,作为锁的唯一标识符
    `holder_thread` VARCHAR(255),         -- 持有锁的线程标识
    `reentry_count` INT DEFAULT 0         -- 重入次数计数器
);

加锁实现逻辑

  1. 开启事务
  2. 查询是否存在该锁记录:
    SELECT holder_thread, reentry_count 
    FROM lock_table 
    WHERE lock_name = ? 
    FOR UPDATE;
    
  3. 根据查询结果处理:
    • 如果记录不存在,则直接加锁:
      INSERT INTO lock_table (lock_name, holder_thread, reentry_count) 
      VALUES (?, ?, 1);
      
    • 如果记录存在且持有者是同一个线程,则增加重入次数:
      UPDATE lock_table 
      SET reentry_count = reentry_count + 1 
      WHERE lock_name = ?;
      
    • 如果记录存在但持有者不是当前线程,则等待(FOR UPDATE会阻塞)
  4. 提交事务

解锁实现逻辑

  1. 开启事务
  2. 查询锁记录:
    SELECT holder_thread, reentry_count 
    FROM lock_table 
    WHERE lock_name = ? 
    FOR UPDATE;
    
  3. 根据查询结果处理:
    • 如果记录存在且持有者是当前线程,且重入次数大于1,则减少重入次数:
      UPDATE lock_table 
      SET reentry_count = reentry_count - 1 
      WHERE lock_name = ?;
      
    • 如果记录存在且持有者是当前线程,且重入次数为1,则删除记录(完全释放锁):
      DELETE FROM lock_table 
      WHERE lock_name = ?;
      
  4. 提交事务

为什么使用FOR UPDATE?

FOR UPDATE 子句在查询时会对记录加排他锁,这样其他事务就无法修改这些记录,直到当前事务提交或回滚。这确保了在我们检查和更新锁状态的过程中,不会有其他线程干扰。

实际应用场景

场景一:中医药方系统中的药材库存管理

假设我们有一个中医药方系统,需要处理药材库存:

public class MedicineService {
    // 处理单个药材库存
    public void processMedicineInventory(String medicineId) {
        acquireLock("medicine_lock");
        try {
            // 更新药材库存
            updateInventory(medicineId);
        } finally {
            releaseLock("medicine_lock");
        }
    }

    // 处理整个药方(包含多个药材)
    public void processPrescription(String prescriptionId) {
        acquireLock("medicine_lock"); // 第一次加锁
        try {
            // 获取处方信息
            PrescriptionInfo info = getPrescriptionInfo(prescriptionId);
            
            // 处理处方中的每个药材
            // 这里会再次调用processMedicineInventory方法
            for (String medicineId : info.getMedicineIds()) {
                processMedicineInventory(medicineId); // 这里会再次请求相同的锁
            }
        } finally {
            releaseLock("medicine_lock");
        }
    }
}

如果没有可重入锁,当 processPrescription 方法获取锁后调用 processMedicineInventory 方法时,后者再次请求获取同一把锁就会导致死锁。

场景二:订单处理系统

public class OrderService {
    public void processOrder(String orderId) {
        // 第一次加锁
        acquireLock("order_lock");
        try {
            // 处理订单基本信息
            processBasicInfo(orderId);
            
            // 同一个线程内再次加锁(重入)
            acquireLock("order_lock");
            try {
                // 处理订单支付信息
                processPayment(orderId);
            } finally {
                // 释放内层锁
                releaseLock("order_lock");
            }
        } finally {
            // 释放外层锁
            releaseLock("order_lock");
        }
    }
}

场景七:电商系统的订单处理

在电商系统中,订单处理通常涉及多个步骤:库存检查、价格计算、创建订单记录、支付处理等。这些步骤可能由不同的方法处理,但它们都需要操作同一个订单数据。

订单处理流程:
1. 主方法"处理订单"获取了订单锁
2. 调用"检查库存"方法,该方法也需要获取订单锁
3. 调用"价格计算"方法,同样需要获取订单锁
4. 调用"创建订单记录"方法
5. 调用"处理支付"方法

如果没有可重入锁,第二步调用"检查库存"时就会导致死锁,因为该方法无法获取已被主方法持有的锁。

场景八:银行系统的资金转账

银行系统中的转账操作通常需要锁定源账户和目标账户以保证一致性。假设有一个复杂的业务场景,需要在一个事务中完成多次小额转账(比如分期付款):

分期付款流程:
1. 主方法"执行分期付款"获取账户锁
2. 循环调用"单次转账"方法,每次方法也需要获取相同的账户锁
3. 最后提交整个事务

如果没有可重入锁,循环中的第二次转账就会被阻塞,因为无法再次获取已被主方法持有的锁。

场景九:CRM系统中的客户信息更新

在客户关系管理系统中,更新客户档案可能涉及多个子系统:

客户信息更新流程:
1. 主方法"更新客户信息"获取客户记录锁
2. 调用"更新基本信息"方法
3. 调用"更新联系方式"方法
4. 调用"更新合同信息"方法
5. 调用"记录变更历史"方法

每个子方法都需要获取客户记录锁以保证数据一致性。如果没有可重入锁,从第二步开始就会出现死锁。

场景十:物流系统的路径规划

物流系统在规划运输路线时,需要考虑多个因素:

路径规划流程:
1. 主方法"规划路线"获取运输任务锁
2. 调用"寻找最短路径"方法
3. 调用"检查道路限制条件"方法
4. 调用"计算预计到达时间"方法
5. 调用"分配运输资源"方法

这些步骤被封装在不同的方法中,但它们都需要锁定同一个运输任务。如果没有可重入锁,从第二步开始就会因为无法获取锁而阻塞。

场景十一:社交网络的消息发送

在社交网络平台中,发送一条消息可能涉及:

消息发送流程:
1. 主方法"发送消息"获取会话锁
2. 调用"保存消息内容"方法
3. 调用"处理特殊标签"方法(如@某人)
4. 调用"生成通知"方法
5. 调用"更新最近消息列表"方法

这些操作都需要获取会话锁来保证一致性。如果使用不可重入锁,就会在嵌套方法调用时导致死锁。

场景十二:文档协同编辑系统

在协同编辑系统中,用户的一次编辑操作可能包含:

文档编辑流程:
1. 主方法"应用编辑操作"获取文档锁
2. 调用"锁定文档区域"方法
3. 调用"应用内容修改"方法
4. 调用"记录修改历史"方法
5. 调用"通知其他协作者"方法

这些步骤可能由不同的方法处理,但都需要获取文档锁。如果没有可重入锁,就会在嵌套调用时导致死锁。

场景十三:游戏服务器中的玩家状态更新

在多人在线游戏中,更新玩家状态可能涉及:

玩家状态更新流程:
1. 主方法"处理玩家行为"获取玩家对象锁
2. 调用"更新位置信息"方法
3. 调用"计算战斗结果"方法
4. 调用"更新物品背包"方法
5. 调用"触发成就系统"方法

这些操作通常需要锁定玩家对象,而且往往存在调用关系。没有可重入锁就会在复杂逻辑中出现死锁。

场景十四:企业资源规划(ERP)系统的库存管理

在ERP系统中,处理一批货物入库可能需要:

入库处理流程:
1. 主方法"处理入库"获取库存锁
2. 调用"验证入库单"方法
3. 调用"分配库位"方法
4. 调用"更新库存数量"方法
5. 调用"记录财务信息"方法

这些操作需要锁定相关的库存记录,而且这些操作之间可能存在调用关系。可重入锁可以确保这些操作能够在同一事务中顺利完成。

可重入锁的实现代码示例

下面是一个完整的Java实现示例:

public class MySQLReentrantLock {
    private final DataSource dataSource;
    private final String lockName;
    private final ThreadLocal<Integer> lockCount = new ThreadLocal<>();

    public MySQLReentrantLock(DataSource dataSource, String lockName) {
        this.dataSource = dataSource;
        this.lockName = lockName;
    }

    public boolean lock() {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            
            // 查询锁记录
            ps = conn.prepareStatement(
                "SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name = ? FOR UPDATE");
            ps.setString(1, lockName);
            rs = ps.executeQuery();
            
            String currentThread = Thread.currentThread().getName();
            
            if (rs.next()) {
                String holderThread = rs.getString("holder_thread");
                int reentryCount = rs.getInt("reentry_count");
                
                if (currentThread.equals(holderThread)) {
                    // 当前线程已持有锁,增加重入次数
                    ps = conn.prepareStatement(
                        "UPDATE lock_table SET reentry_count = reentry_count + 1 WHERE lock_name = ?");
                    ps.setString(1, lockName);
                    ps.executeUpdate();
                    
                    // 更新本地计数
                    lockCount.set(lockCount.get() + 1);
                } else {
                    // 锁被其他线程持有,获取锁失败
                    conn.rollback();
                    return false;
                }
            } else {
                // 锁不存在,创建新锁
                ps = conn.prepareStatement(
                    "INSERT INTO lock_table (lock_name, holder_thread, reentry_count) VALUES (?, ?, 1)");
                ps.setString(1, lockName);
                ps.setString(2, currentThread);
                ps.executeUpdate();
                
                // 初始化本地计数
                lockCount.set(1);
            }
            
            conn.commit();
            return true;
        } catch (Exception e) {
            try {
                if (conn != null) {
                    conn.rollback();
                }
            } catch (SQLException ex) {
                // 忽略
            }
            return false;
        } finally {
            // 关闭资源
            try {
                if (rs != null) rs.close();
                if (ps != null) ps.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                // 忽略
            }
        }
    }

    public boolean unlock() {
        // 检查当前线程是否持有锁
        Integer count = lockCount.get();
        if (count == null || count == 0) {
            return false;
        }
        
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            
            if (count > 1) {
                // 减少重入次数
                ps = conn.prepareStatement(
                    "UPDATE lock_table SET reentry_count = reentry_count - 1 WHERE lock_name = ?");
                ps.setString(1, lockName);
                ps.executeUpdate();
                
                // 更新本地计数
                lockCount.set(count - 1);
            } else {
                // 完全释放锁
                ps = conn.prepareStatement(
                    "DELETE FROM lock_table WHERE lock_name = ?");
                ps.setString(1, lockName);
                ps.executeUpdate();
                
                // 清除本地计数
                lockCount.remove();
            }
            
            conn.commit();
            return true;
        } catch (Exception e) {
            try {
                if (conn != null) {
                    conn.rollback();
                }
            } catch (SQLException ex) {
                // 忽略
            }
            return false;
        } finally {
            // 关闭资源
            try {
                if (ps != null) ps.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                // 忽略
            }
        }
    }
}

总结

MySQL实现的可重入锁通过以下机制工作:

  1. 使用锁表记录锁的状态
  2. 使用 FOR UPDATE 确保操作的原子性
  3. 通过重入计数器实现可重入特性
  4. 只有当重入计数为0时才真正释放锁

这种实现方式适用于分布式环境,可以确保在多个服务器之间共享锁的状态。但需要注意,由于涉及数据库操作,性能相对较低,适合对性能要求不是特别高的场景。

在实际应用中,可重入锁极大地提高了代码的可维护性和可复用性,使得方法可以独立管理自己的锁,不需要关心调用环境是否已经持有锁。