MySQL 某个表字段实现分布式锁

文章目录

  • 0.背景
  • [1. 实现原理](#1. 实现原理)
  • [2. 核心要点](#2. 核心要点)
  • [3. 完整使用流程](#3. 完整使用流程)
  • [4. 优缺点分析](#4. 优缺点分析)
  • [5. 适用场景](#5. 适用场景)
  • [6. 与其他方案的对比](#6. 与其他方案的对比)
  • [7. 总结](#7. 总结)
  • 参考文献

0.背景

比如使用 MySQL 的 locked_at 字段(bigint unsigned 时间戳)来实现分布式锁或防止并发处理,是一种基于数据库行锁 + 乐观锁 的常见设计。它不依赖外部中间件(如 Redis),实现简单,非常适合低并发、对性能要求不苛刻的后台任务。

核心思想是:获取锁就是尝试更新 locked_at 字段 ,利用 MySQL 的原子更新特性保证同一时间只有一个线程/进程能更新成功。

1. 实现原理

假设有一张任务表 jobs,结构如下:

sql 复制代码
CREATE TABLE `jobs` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `status` tinyint NOT NULL COMMENT '0=待处理 1=处理中 2=已完成',
    `locked_at` bigint unsigned DEFAULT NULL COMMENT '锁定时间戳(毫秒),NULL表示未锁定',
    `locked_by` varchar(100) DEFAULT NULL COMMENT '锁定者标识(IP/机器名/PID)',
    `retry_count` int DEFAULT 0,
    PRIMARY KEY (`id`),
    KEY `idx_status_lock` (`status`, `locked_at`)
);

获取锁的 SQL(关键操作):

sql 复制代码
-- 原子操作:尝试锁定一条待处理的任务
UPDATE jobs 
SET 
    locked_at = UNIX_TIMESTAMP(NOW(3)) * 1000,  -- 当前毫秒时间戳
    locked_by = 'service-a:pid12345',
    status = 1  -- 可选:更新状态
WHERE 
    status = 0                      -- 待处理
    AND (locked_at IS NULL          -- 从未被锁定
         OR locked_at < UNIX_TIMESTAMP(NOW(3)) * 1000 - 60000) -- 锁超时(60秒)
LIMIT 1;
  • UPDATE 语句在 MySQL 中具备行锁原子性,当多个进程同时执行时,只有一个能成功更新,获取到锁。
  • 成功更新的行数 (rows affected) 为 1,表示获取锁成功。

释放锁的 SQL:

sql 复制代码
-- 只有当自己的锁才能释放
UPDATE jobs 
SET locked_at = NULL, locked_by = NULL, status = 0
WHERE id = 123 AND locked_by = 'service-a:pid12345';

2. 核心要点

  • 原子性UPDATE ... WHERE 是原子操作,数据库行锁确保了并发安全。
  • 超时机制 :通过 locked_at 时间戳判断锁是否过期,防止死锁。
  • 锁归属 :通过 locked_by 记录锁持有者,确保只有加锁者能解锁,避免误删。

3. 完整使用流程

go 复制代码
type JobLocker struct {
    db *sql.DB
}

// TryLock 尝试获取锁,返回是否成功及锁定的任务ID
func (l *JobLocker) TryLock(lockerID string, timeoutMs int64) (jobID int64, err error) {
    // 1. 尝试锁定
    result, err := l.db.Exec(`
        UPDATE jobs 
        SET locked_at = ?, locked_by = ?, status = 1
        WHERE status = 0 
          AND (locked_at IS NULL OR locked_at < ?)
        LIMIT 1
    `, time.Now().UnixMilli(), lockerID, time.Now().UnixMilli()-timeoutMs)
    
    if err != nil {
        return 0, err
    }
    
    rowsAffected, _ := result.RowsAffected()
    if rowsAffected == 0 {
        return 0, nil // 没抢到锁
    }
    
    // 2. 查询被锁定的任务ID
    var id int64
    err = l.db.QueryRow(`
        SELECT id FROM jobs 
        WHERE locked_by = ? AND locked_at > ?
    `, lockerID, time.Now().UnixMilli()-5000).Scan(&id)
    
    return id, err
}

// Unlock 释放锁
func (l *JobLocker) Unlock(jobID int64, lockerID string) error {
    _, err := l.db.Exec(`
        UPDATE jobs 
        SET locked_at = NULL, locked_by = NULL, status = 0
        WHERE id = ? AND locked_by = ?
    `, jobID, lockerID)
    return err
}

4. 优缺点分析

优点 缺点
实现简单:无需引入额外组件 性能瓶颈:不适合高并发场景,数据库压力大
可靠性高:基于 ACID 事务保证 依赖时钟:需要服务器时间准确
易于调试:锁状态可直接 SQL 查询 清理负担:需要定时任务清理过期锁
事务友好:可与其他操作组合 连接池压力:长任务占用数据库连接

5. 适用场景

  • 分布式定时任务调度:多实例环境下,确保同一任务只被执行一次。
  • 消息消费幂等控制:防止消息被重复消费。
  • 后台数据处理:如数据归档、报表生成等低频操作。
  • 并发量较低的场景(QPS < 100)。

6. 与其他方案的对比

方案 优点 缺点 适用场景
MySQL 乐观锁 实现简单,无外部依赖 性能较低,有单点风险 低并发,简单任务
Redis 分布式锁 高性能,支持自动过期 需要额外组件,实现复杂 高并发,要求响应快
ZooKeeper 强一致性,支持故障转移 重量级,维护成本高 对一致性要求极高

7. 总结

使用 locked_at 字段实现 MySQL 分布式锁是工程中非常实用的技巧 。它虽然不如 Redis 或 ZooKeeper 那样高性能,但对于大多数内部后台管理系统、定时任务、低频并发控制 场景,是一种简单、可靠、低成本的解决方案。

关键点是正确利用 UPDATE ... WHERE 的原子性,并设计好超时和归属机制。


参考文献

《锁系列三:优雅的通过数据库行锁代替Redis实现分布式锁》
《详解分布式锁的三种实现方式:MySQL vs Redis vs ZooKeeper》

相关推荐
lifewange2 小时前
GaussDB /openGauss 与 MySQL、Oracle、PostgreSQL 核心对比表
mysql·oracle·gaussdb
fundoit2 小时前
MySQL Workbench中的权限设置不生效
数据库·mysql
ZzzZZzzzZZZzzzz…2 小时前
MySQL备份还原方法2----LVM
linux·运维·数据库·mysql·备份还原
路baby2 小时前
Pikachu安装过程中常见问题(apache和MySQL无法正常启动)
计算机网络·mysql·网络安全·adb·靶场·apache·pikachu
XDHCOM2 小时前
MySQL ER_IB_MSG_919报错解析,故障修复与远程处理指南
数据库·mysql·adb
小小程序员.¥2 小时前
oracle--函数
数据库·sql·mysql
不剪发的Tony老师11 小时前
MyCLI:一个增强型MySQL命令行客户端
数据库·mysql
XDHCOM12 小时前
MySQL ER_DD_VERSION_INSTALLED报错解析,数据字典版本问题,故障修复与远程处理指南
数据库·mysql
努力的小郑12 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化