MySQL实现可重入锁的实践指南

 更新时间:2026年03月17日 09:27:29   作者:java修仙传  
本文介绍了如何在MySQL中实现可重入锁,包括锁表设计、加锁和解锁逻辑以及为什么必须使用事务来保证锁的互斥性、原子性和生命周期,需要的朋友可以参考下

在分布式系统和并发编程中,锁是保证数据一致性的关键工具。而基于 MySQL 实现的可重入锁,不仅能满足跨进程的互斥需求,还能支持同一个线程多次获取锁而不阻塞。记录一下我对 MySQL 实现可重入锁的思考:如何用 MySQL 实现可重入锁?为什么实现过程中必须依赖事务?希望能解决你的一些疑惑。

一、先搞懂:什么是可重入锁?

可重入锁(也叫递归锁)的核心特性是:同一个线程可以多次获取同一把锁,不会因为自己持有锁而发生死锁。

举个例子:

  • 线程 A 先获取锁,执行业务逻辑;
  • 业务逻辑中又调用了另一个需要同一把锁的方法;
  • 线程 A 可以再次成功获取锁,而不会被阻塞;
  • 只有当线程 A 释放锁的次数等于获取锁的次数时,锁才会真正被释放。

在 Java 里 ReentrantLock 就是典型的可重入锁,而我们要做的,是用 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
);

这张表的核心字段:

  • lock_name:锁的唯一标识,不同业务用不同的锁名隔离。
  • holder_thread:标记当前哪个线程持有这把锁。
  • reentry_count:记录锁被同一个线程重入的次数,是实现可重入特性的关键。

三、核心实现:加锁与解锁逻辑

1. 加锁流程

1. 开启事务
2. 执行 SQL:
   SELECT holder_thread, reentry_count 
   FROM lock_table 
   WHERE lock_name = ? FOR UPDATE;
   - 若记录不存在:执行 INSERT INTO lock_table (lock_name, holder_thread, reentry_count) VALUES (?, ?, 1)
   - 若记录存在且持有者是当前线程:执行 UPDATE lock_table SET reentry_count = reentry_count + 1 WHERE lock_name = ?
3. 提交事务

2. 解锁流程

1. 开启事务
2. 执行 SQL:
   SELECT holder_thread, reentry_count 
   FROM lock_table 
   WHERE lock_name = ? FOR UPDATE;
   - 若记录存在、持有者是当前线程且重入次数 > 1:执行 UPDATE lock_table SET reentry_count = reentry_count - 1 WHERE lock_name = ?
   - 若记录存在、持有者是当前线程且重入次数 ≤ 1:执行 DELETE FROM lock_table WHERE lock_name = ?
3. 提交事务

四、灵魂拷问:为什么必须加事务?

很多同学会疑惑:“我不加事务,直接执行 SQL 不行吗?” 答案是:不行。事务是 MySQL 可重入锁的灵魂,没有事务,锁的特性会直接失效。我们从三个维度拆解原因:

1. 控制锁的生命周期:避免锁提前释放

MySQL InnoDB 默认 autocommit=1,单条 SQL 执行完毕后会自动提交事务。

如果不加事务:

  • 执行 SELECT ... FOR UPDATE 时,会对目标记录加排他行锁;
  • 这条 SQL 执行完后,锁会被自动释放;
  • 后续的 INSERT/UPDATE 操作变成了全新的请求,需要重新竞争锁。

这会导致:

  • 锁根本没有被 “持有”,其他线程可以在两次操作之间抢占锁;
  • 可重入特性直接失效,同一个线程第二次获取锁时可能被阻塞。

而事务的作用就是:在事务提交前,锁不会被释放。从查询锁状态到完成写入操作,整个过程中锁都被当前事务持有,保证了互斥性和可重入性。

2. 保证操作原子性:避免并发数据错乱

加锁 / 解锁的核心逻辑是「查询 → 判断 → 写入」,这三步必须是原子操作,否则在高并发场景下会出现数据错乱。

举个并发场景:

  • 线程 A 和线程 B 同时请求同一把锁;
  • 线程 A 执行 SELECT ... FOR UPDATE,发现记录不存在,准备 INSERT
  • 线程 B 在 A 还没 INSERT 之前,也执行 SELECT ... FOR UPDATE,同样发现记录不存在;
  • 两个线程都去 INSERT,要么触发唯一键冲突,要么都插入成功,导致锁被同时持有。

事务的原子性可以完美解决这个问题:

  • 把「查询 → 判断 → 写入」封装成一个不可分割的单元;
  • 要么全部成功,要么全部失败;
  • 只有当前事务提交后,其他线程才能看到锁的状态变化,避免了竞态条件。

3. 确保可重入正确性:重入次数的安全更新

可重入锁的核心是维护 reentry_count 字段:

  • 加锁时:如果是当前线程持有锁,reentry_count + 1
  • 解锁时:如果是当前线程持有锁,reentry_count - 1,次数为 0 时删除锁记录。

如果不加事务:

  • 线程 A 查询到 reentry_count = 1,准备执行 +1
  • 线程 B 可能在此时修改了 reentry_count,导致更新后的数据错误;
  • 最终锁的状态混乱,甚至出现锁无法释放的情况。

事务的隔离性保证了:

  • 在当前事务更新 reentry_count 时,其他线程无法修改这条记录;
  • 只有事务提交后,新的重入次数才会被持久化,其他线程才能感知到。

五、完整示例:

我们用伪代码把加锁和解锁流程串起来,更直观地感受事务的作用:

加锁伪代码

public boolean lock(String lockName, String threadName) {
    Connection conn = getConnection();
    try {
        // 1. 开启事务
        conn.setAutoCommit(false);
        // 2. 查询锁状态(加排他锁)
        String querySql = "SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name = ? FOR UPDATE";
        try (PreparedStatement ps = conn.prepareStatement(querySql)) {
            ps.setString(1, lockName);
            ResultSet rs = ps.executeQuery();
            if (rs.next()) {
                String holder = rs.getString("holder_thread");
                int count = rs.getInt("reentry_count");
                if (threadName.equals(holder)) {
                    // 可重入:重入次数+1
                    String updateSql = "UPDATE lock_table SET reentry_count = reentry_count + 1 WHERE lock_name = ?";
                    try (PreparedStatement updatePs = conn.prepareStatement(updateSql)) {
                        updatePs.setString(1, lockName);
                        updatePs.executeUpdate();
                    }
                } else {
                    // 被其他线程持有,获取锁失败
                    conn.rollback();
                    return false;
                }
            } else {
                // 锁不存在,直接加锁
                String insertSql = "INSERT INTO lock_table (lock_name, holder_thread, reentry_count) VALUES (?, ?, 1)";
                try (PreparedStatement insertPs = conn.prepareStatement(insertSql)) {
                    insertPs.setString(1, lockName);
                    insertPs.setString(2, threadName);
                    insertPs.executeUpdate();
                }
            }
        }
        // 3. 提交事务
        conn.commit();
        return true;
    } catch (Exception e) {
        // 异常回滚
        conn.rollback();
        return false;
    } finally {
        conn.setAutoCommit(true);
        closeConnection(conn);
    }
}

解锁伪代码

public boolean unlock(String lockName, String threadName) {
    Connection conn = getConnection();
    try {
        // 1. 开启事务
        conn.setAutoCommit(false);
        // 2. 查询锁状态
        String querySql = "SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name = ? FOR UPDATE";
        try (PreparedStatement ps = conn.prepareStatement(querySql)) {
            ps.setString(1, lockName);
            ResultSet rs = ps.executeQuery();
            if (rs.next()) {
                String holder = rs.getString("holder_thread");
                int count = rs.getInt("reentry_count");
                if (!threadName.equals(holder)) {
                    // 不是锁持有者,解锁失败
                    conn.rollback();
                    return false;
                }
                if (count > 1) {
                    // 重入次数>1,仅减1
                    String updateSql = "UPDATE lock_table SET reentry_count = reentry_count - 1 WHERE lock_name = ?";
                    try (PreparedStatement updatePs = conn.prepareStatement(updateSql)) {
                        updatePs.setString(1, lockName);
                        updatePs.executeUpdate();
                    }
                } else {
                    // 重入次数=1,删除锁记录
                    String deleteSql = "DELETE FROM lock_table WHERE lock_name = ?";
                    try (PreparedStatement deletePs = conn.prepareStatement(deleteSql)) {
                        deletePs.setString(1, lockName);
                        deletePs.executeUpdate();
                    }
                }
            } else {
                // 锁不存在,解锁失败
                conn.rollback();
                return false;
            }
        }
        // 3. 提交事务
        conn.commit();
        return true;
    } catch (Exception e) {
        conn.rollback();
        return false;
    } finally {
        conn.setAutoCommit(true);
        closeConnection(conn);
    }
}

六、总结

用 MySQL 实现可重入锁,本质是用数据库表存储锁状态,用事务保证锁的互斥性、原子性和生命周期。

事务的核心作用可以概括为三点:

  1. 锁生命周期管理:事务提交前,锁不会释放,保证当前线程持续持有锁。
  2. 原子性保障:将「查询 → 判断 → 写入」封装为原子操作,避免并发数据错乱。
  3. 可重入正确性:安全维护重入次数,确保同一个线程可以多次获取 / 释放锁。

这种实现方式虽然不如 Redis 等分布式锁框架高效,但胜在简单可靠,适合对性能要求不高、需要强一致性的场景,也能帮我们更好地理解事务和锁的本质。

以上就是MySQL实现可重入锁的实践指南的详细内容,更多关于MySQL可重入锁实现的资料请关注脚本之家其它相关文章!

相关文章

  • MySQL数据库约束操作示例讲解

    MySQL数据库约束操作示例讲解

    约束是用来限制表中的数据长什么样子的,即什么样的数据可以插入到表中,什么样的数据插入不到表中,下面这篇文章主要给大家介绍了关于如何通过一文理解MySQL数据库的约束与表的设计的相关资料,需要的朋友可以参考下
    2022-11-11
  • 一文搞懂mysql如何处理json格式的字段(解析json数据)

    一文搞懂mysql如何处理json格式的字段(解析json数据)

    这篇文章主要给大家介绍了关于mysql如何处理json格式的字段的相关资料,MySQL中的JSON类型是一种数据类型,用于存储和处理JSON(JavaScript Object Notation)格式的数据,需要的朋友可以参考下
    2023-12-12
  • 解决MySQL报错:The last packet sent successfully to the server was 0 milliseconds ago.

    解决MySQL报错:The last packet sent successfu

    这篇文章主要介绍了解决MySQL报错:The last packet sent successfully to the server was 0 milliseconds ago问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • MySQL命令行方式进行数据备份与恢复

    MySQL命令行方式进行数据备份与恢复

    本文主要介绍了MySQL命令行方式进行数据备份与恢复,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Windows下MySQL 5.7无法启动的解决方法

    Windows下MySQL 5.7无法启动的解决方法

    从网上下了5.7 的MySQL,在bin目录下执行 start mysqld ,弹出个cmd窗口一闪就没了,也看不清是什么报错。mysqld --install安装了服务,也启动不了,下面通过本文给大家分享下解决办法
    2016-12-12
  • MySQL5.7中的JSON基本操作指南

    MySQL5.7中的JSON基本操作指南

    这篇文章主要给大家介绍了关于MySQL5.7中JSON的基本操作,文中通过示例代码介绍的非常详细,对大家学习或者使用Mysql具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-03-03
  • mysql自动化安装脚本(ubuntu and centos64)

    mysql自动化安装脚本(ubuntu and centos64)

    这篇文章主要介绍了mysql自动化安装脚本(ubuntu and centos64),需要的朋友可以参考下
    2014-05-05
  • MySQL中like模糊查询的优化方案

    MySQL中like模糊查询的优化方案

    在 MySQL 中,like 模糊查询是一种常用的查询方式,但在某些情况下可能会导致性能问题,本文将介绍八种优化 MySQL 中 like 模糊查询的方法,需要的朋友可以参考下
    2025-05-05
  • MYSQL SET类型字段的SQL操作知识介绍

    MYSQL SET类型字段的SQL操作知识介绍

    本篇文章是对MYSQL中SET类型字段的SQL操作知识进行了详细的分析介绍,需要的朋友参考下
    2013-07-07
  • 五分钟让你快速弄懂MySQL索引下推

    五分钟让你快速弄懂MySQL索引下推

    ICP(Index Condition Pushdown)是在MySQL 5.6版本上推出的查询优化策略,把本来由Server层做的索引条件检查下推给存储引擎层来做,下面这篇文章主要给大家介绍了关于MySQL索引下推的相关资料,需要的朋友可以参考下
    2021-09-09

最新评论