在这里插入图片描述
你的系统是一个简单的后台管理系统,部署了 2 个节点。
每天凌晨 3 点,需要执行一个定时任务:“统计昨日财务报表”
痛点:
由于是双节点部署,如果不加控制,两个节点的定时任务会同时触发,导致报表生成两份,或者数据重复计算。
通常解法: 引入 Redis,用 SETNX 做分布式锁。
尴尬之处: 整个系统只有这一个场景需要锁,为了这一个功能去搭建维护一套 Redis 集群,还要配置网络、鉴权,简直是“大炮打蚊子”。
极简解法:
其实,你的数据库 MySQL 早就为你准备好了 GET_LOCK() 函数。不需要引入任何新组件,一行 SQL 就能实现跨进程、跨节点的分布式锁。


1. 核心原理:用户锁 vs 行锁

很多人听到“MySQL 锁”,第一反应是 SELECT ... FOR UPDATE(行锁)。
行锁是锁住表里的数据行,开销大,且必须有真实数据存在。

GET_LOCK() 是“用户锁” (User Level Lock)

  • 它锁的不是表,也不是行,而是一个“字符串”。
  • 它完全独立于事务机制(Transaction),类似于编程语言中的 ReentrantLockMutex,只不过这个 Mutex 存在于 MySQL Server 的内存中。
函数三剑客:
  1. GET_LOCK(str, timeout): 尝试获取名为 str 的锁。
  • str: 锁的名字(字符串)。
  • timeout: 等待超时时间(秒)。
  • 返回值: 1 (成功), 0 (超时), NULL (发生错误)。
  1. RELEASE_LOCK(str): 释放名为 str 的锁。
  • 返回值: 1 (成功释放), 0 (锁不属于你), NULL (锁不存在)。
  1. IS_FREE_LOCK(str): 检查锁是否空闲。

2. 实战演练:一行代码的魔法

获取锁

假设我们要锁住“财务报表任务”:

-- 尝试获取锁 'lock_daily_report',如果被别人占了,最多等待 10 秒
SELECT GET_LOCK('lock_daily_report', 10);

  • 如果返回 1:恭喜,你拿到了锁,可以开始执行 Java 代码了。
  • 如果返回 0:说明别的节点正在跑,你直接退出即可。
释放锁

任务执行完毕后:

-- 释放锁
SELECT RELEASE_LOCK('lock_daily_report');

  • 隐式释放(关键特性): GET_LOCK 是绑定在 数据库连接 (Session) 上的。如果你的 Java 服务挂了、断电了,连接断开,MySQL 会自动释放该连接持有的所有锁。这完美解决了“死锁”问题(即获得锁的进程挂了没释放)。

3. 三大实战场景

GET_LOCK 不适合每秒几万 QPS 的秒杀场景(因为所有压力都压在 DB 上),但它在以下场景是 王者

场景一:分布式定时任务 (Cron Jobs)

背景: 多节点部署的 Spring Boot 应用,使用 @Scheduled 跑定时任务。
需求: 同一时间只能有一个节点跑任务。
方案: 任务开始前先 GET_LOCK。这是最经典的应用场景。

场景二:全局唯一资源初始化 (Startup Logic)

背景: 系统启动时,需要检查数据库并初始化一些基础配置(如创建默认管理员账号、加载字典缓存)。
需求: 多个实例同时启动,不能让初始化逻辑跑多次。
方案: 启动时争抢 GET_LOCK('init_resource'),抢到的执行,没抢到的跳过。

场景三:低频高价值的互斥操作 (Critical Section)

背景: 比如“生成全站数据快照”或者“归档历史数据”。这些操作耗时长,绝对不允许并发执行。
方案: 利用 GET_LOCK 配合较长的 timeout,实现跨进程的互斥。


4. 代码实战 (Java + Spring)

在 Java 中,我们通常使用 JdbcTemplate 或 MyBatis 来调用。

@Service
public class DistributedTaskService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void executeTask() {
        String lockKey = "my_distributed_lock";
        
        // 1. 尝试获取锁 (等待 5 秒)
        Integer result = jdbcTemplate.queryForObject(
            "SELECT GET_LOCK(?, ?)", Integer.class, lockKey, 5
        );

        if (result != null && result == 1) {
            try {
                // 2. 获取锁成功,执行业务逻辑
                System.out.println("执行任务中...");
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 3. 务必释放锁
                jdbcTemplate.update("SELECT RELEASE_LOCK(?)", lockKey);
                System.out.println("锁已释放");
            }
        } else {
            System.out.println("获取锁失败,任务被其他节点占用");
        }
    }
}


5. 避坑指南 (⚠️ 极其重要)

虽然简单,但有两个坑足以让系统崩溃。

坑点一:连接池复用导致的“锁泄漏”

GET_LOCK 是基于 Session(数据库连接) 的。

  • 现象: 线程 A 从连接池拿了一个连接,执行 GET_LOCK 成功。任务结束后,线程 A 忘记释放锁,直接把连接还回了连接池。
  • 后果: 锁依然被这个连接持有!接下来线程 B 从连接池借到了同一个连接,线程 B 莫名其妙就拥有了这个锁,或者导致后续逻辑混乱。
  • 解法: 必须在 finally 块中显式 RELEASE_LOCK
坑点二:事务无关性

GET_LOCK 不受 BEGIN / COMMIT / ROLLBACK 影响。
即使事务回滚了,锁不会自动释放。一定要手动释放。

坑点三:长度限制

在 MySQL 5.7 之前,锁名字的长度限制是 64 字符。MySQL 8.0 之后支持更长。


6. 总结

不要为了用技术而用技术。如果你的架构中本来就没有 Redis,且并发量仅限于定时任务或低频互斥,MySQL GET_LOCK() 是最优雅、最高性价比的方案

  • 优点: 零成本、强一致性、自动处理客户端宕机(断开即释放)。
  • 缺点: 依赖数据库单点性能,不适合高频锁竞争。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐