MySQL实现分布式锁:原理、步骤与代码示例

白榆
白榆
发布时间:2026-10-15| 5人浏览过
原创

在分布式系统中,如何实现跨进程的资源独占?本文将深入剖析基于MySQL InnoDB存储引擎的分布式锁方案,通过解析共享锁与排他锁的工作机制,结合Java代码实例,展示如何利用`SELECT ... FOR UPDATE`语句和唯一索引构建可靠的互斥锁,保障并发场景下的数据一致性。

MySQL锁机制基础

数据库中的锁是协调多个事务并发访问同一资源的机制。对于关系型数据库而言,除了操作系统层面的资源争用,数据本身作为共享资源,其并发一致性与有效性是核心挑战。锁冲突程度直接决定了数据库的并发性能,因此理解锁的底层逻辑是设计分布式锁的前提。

MySQL的性能很大程度上取决于InnoDB或MyISAM等存储引擎。其中,InnoDB支持多粒度锁定,默认采用行级锁,适合高并发的读写场景;而MyISAM仅支持表级锁。实现分布式锁通常依赖InnoDB的行锁特性。

行锁类型:共享锁与排他锁

InnoDB的行锁主要分为两种:

  • 共享锁(S锁/读锁):允许其他事务读取锁定数据,但禁止修改。不同事务可在同一行上共存多个共享锁,只要不申请排他锁即可。
  • 排他锁(X锁/写锁):独占资源,持有锁的事务可执行增删改查,其他事务对该行的读取会被阻塞,且不允许任何其他事务获取该行的共享锁。

共享锁通过LOCK IN SHARE MODE语法实现,排他锁通过FOR UPDATE语法实现。关键在于:FOR UPDATE会触发行级排他锁,如果指定列没有合适的索引,锁会升级(退化)为表级锁,从而丧失并发性。

基于InnoDB的唯一索引分布式锁实现

利用InnoDB的排他锁特性,结合业务唯一键,可以实现一种简易的分布式锁。当多个客户端尝试获取同一业务的锁时,只有第一个成功执行加锁查询的事务能继续,后续的请求因等待锁释放而产生阻塞,从而实现互斥。

第1步:创建锁控制表

首先需要准备一张专门用于管理锁状态的表。表中必须包含一个具有唯一约束(Unique Key)的业务字段,这是确保锁能精确到“行”级别的核心要素。

CREATE TABLE IF NOT EXISTS fud_distribute_lock (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    biz VARCHAR(50) COMMENT '业务Key',
    UNIQUE KEY uk_biz (biz)
) ENGINE=InnoDB;

说明:biz字段代表具体的业务标识(如订单ID),UNIQUE KEY uk_biz确保该值在全局唯一。由于使用了唯一索引,后续的FOR UPDATE操作将精准定位到该行,避免全表扫描导致的表锁。

第2步:Java客户端获取锁逻辑

获取锁的流程分为两步:首先尝试使用SELECT ... FOR UPDATE查询记录。如果记录不存在,则执行插入操作以预留锁占位;一旦插入成功或查询命中,即视为获得锁。在此期间需关闭自动提交(Auto Commit),保证事务持续占用锁。

// 省略了构造方法,需要传入DataSource和biz
private static final String SELECT_SQL = 
    "SELECT * FROM fud_distribute_lock WHERE `biz` = ? for update";
private static final String INSERT_SQL = 
    "INSERT INTO fud_distribute_lock(`biz`) values(?)";

// 从构造方法中传入
private final DataSource source;
private Connection connection;

public void lock() {
    PreparedStatement psmt = null;
    ResultSet rs = null;
    try {
        // 循环直到获取成功
        for (; ; ) {
            connection = this.source.getConnection();
            // 关闭自动提交事务,维持事务上下文以持有锁
            connection.setAutoCommit(false);
            
            psmt = connection.prepareStatement(SELECT_SQL);
            psmt.setString(1, biz);
            rs = psmt.executeQuery();
            
            // 如果查询到了数据,说明当前已有该业务的其他客户端持有锁,或者自己已获取
            if (rs.next()) {
                return; 
            }
            
            // 如果没有查到记录,需要先插入一行来“占据”这个位置
            // 因为for update只能锁定已经存在的行
            connection.commit(); // 先提交刚才的空事务
            close(connection, psmt, rs);
            
            Connection updConnection = this.source.getConnection();
            PreparedStatement insertStatement = null;
            try {
                insertStatement = updConnection.prepareStatement(INSERT_SQL);
                insertStatement.setString(1, biz);
                if (insertStatement.executeUpdate() == 1) {
                    LOGGER.info("创建锁记录成功");
                } else {
                    // 如果返回0,说明存在竞争,其他线程抢先插入了
                    continue;
                }
            } catch (Exception e) {
                // 如果发生死锁或其他异常导致插入失败,说明有竞争对手已持有该唯一键
                // 此时应当重试,重新进入循环去select...for update
                LOGGER.error("创建锁记录异常", e); 
                continue; 
            } finally {
                close(insertStatement, updConnection);
            }
            
            // 插入成功后,为了正式进入持有锁的状态,再次执行select for update
            psmt = connection.prepareStatement(SELECT_SQL);
            psmt.setString(1, biz);
            connection.setAutoCommit(false);
            rs = psmt.executeQuery();
            // 再次确认获取锁状态
            rs.next(); 
            return;
        }
    } catch (Exception e) {
        LOGGER.error("lock异常信息", e); 
        throw new RuntimeException(e);
    } finally {
        // 资源清理需注意保留当前连接connection供后续unlock使用
        if(psmt != null) { try { psmt.close(); } catch (Exception ignore) {} }
        if(rs != null) { try { rs.close(); } catch (Exception ignore) {} }
    }
}

public void unlock() {
    try {
        // 事务提交之后自动解锁(释放行上的X锁)
        connection.commit(); 
        close(connection);
    } catch (Exception e) {
        LOGGER.error("unlock异常信息", e); 
        throw new RuntimeException(e);
    }
}

public void close(AutoCloseable... closeables) {
    Arrays.stream(closeables).forEach(closeable -> {
        if (null != closeable) {
            try {
                closeable.close();
            } catch (Exception e) {
                LOGGER.error("close关闭异常", e);
            }
        }
    });
}

第3步:为什么不能直接使用SELECT FOR UPDATE?

很多初学者误以为直接执行SELECT ... FOR UPDATE就能加锁。实际上,FOR UPDATE只会锁定查询结果集中已经存在的行。如果数据库中还没有对应业务的记录,SELECT返回空集,自然也就没有任何行被锁定,无法起到互斥作用。

因此,标准的操作流程是:
1. 开启事务。
2. 尝试插入一行带有唯一业务Key的记录。若插入成功,即拿到锁;若插入失败(违反唯一键约束),则说明有人先拿到了锁。
3. 若插入成功,紧接着执行一次SELECT ... FOR UPDATE查询该行,将“插入产生的隐式锁”转化为明确的行级排他锁,随后保持事务不关闭,直至业务处理完成调用commit()释放锁。

总结

虽然在实际生产环境中,Rediss 或 Zookeeper 是实现分布式锁的主流选择,但掌握基于 MySQL 的实现原理依然具有重要价值。它展示了如何利用数据库自带的 ACID 特性和行锁机制来解决并发控制问题。在使用时请务必注意,由于数据库连接池的持有以及事务的时间成本,这种方式吞吐量相对较低,且建议业务侧做好超时防死锁处理。以上就是基于MySQL实现分布式锁的详细内容,更多关于MySQL分布式锁的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。