GET_LOCK()不能直接用作生产级分布式锁,因其锁状态不跨MySQL实例同步、不支持TTL、仅限单实例会话级互斥,主从延迟或连接池复用时易导致锁失效或误判。

GET_LOCK() 不能直接用作生产级分布式锁,它只适合单 MySQL 实例内、短时、轻量的排他任务协调。
为什么 GET_LOCK() 在跨服务场景下会“失效”
根本原因是锁状态不跨实例同步:主库上 GET_LOCK('task_123', 10) 返回 1,从库执行 IS_USED_LOCK('task_123') 却返回 NULL——锁信息不会被复制到从库。这意味着依赖从库做锁检查的读服务永远看不到主库已持有的锁。
- 主从延迟下,多个应用可能同时在各自连接的(可能是从库)上判定“锁空闲”,一拥而入
- 连接池复用时,旧连接未释放锁,新业务线程复用该连接后误以为自己“刚拿到锁”,实则锁早已被前序逻辑持有
-
GET_LOCK()不支持 TTL,一旦应用崩溃且连接未及时关闭,锁会滞留直到wait_timeout触发断连(默认 28800 秒),期间所有请求全卡死
如何在单实例内安全执行排他任务
适用场景:同一应用多进程/多线程抢一个本地资源,比如初始化缓存表、触发定时汇总任务、避免重复发送通知。
- 必须在同一个数据库连接中完成
GET_LOCK()和RELEASE_LOCK(),跨连接调用RELEASE_LOCK()总是返回 0 - 超时参数单位是秒,
0表示立即返回(不等待),负数表示无限等待(危险,慎用) - 锁名最大 64 字符、大小写敏感,避免用纯数字或含斜杠路径(如
"123"或"user/1001"),易与其他系统冲突 - 务必用
try/finally或defer保证异常时也能释放,例如 Python 中:
cursor.execute("SELECT GET_LOCK(%s, 30)", ("batch_init_v2",))
locked = cursor.fetchone()[0]
if not locked:
raise RuntimeError("lock failed")
try:
# 执行初始化逻辑
do_init_work()
finally:
cursor.execute("SELECT RELEASE_LOCK(%s)", ("batch_init_v2",))能否和事务一起用?怎么配合才不翻车
可以配合,但 GET_LOCK() 本身不参与事务控制——它不会随 COMMIT 或 ROLLBACK 自动释放,也不受事务隔离级别影响。
- 真正防并发修改靠的是
SELECT ... FOR UPDATE,GET_LOCK()只是前置“准入筛选”,减少无谓的行锁竞争 - 必须先
GET_LOCK(),再开启事务并执行FOR UPDATE,否则锁住后事务卡住会导致锁长期占用 -
RELEASE_LOCK()建议放在事务外由应用层执行,因为事务提交后连接可能被复用,锁状态不会自动清理 - 示例关键顺序:
SELECT GET_LOCK('stock_456', 10); -- 成功才继续
START TRANSACTION;
SELECT stock FROM items WHERE id = 456 FOR UPDATE; -- 真正加行锁
UPDATE items SET stock = stock - 1 WHERE id = 456 AND stock >= 1;
COMMIT;
SELECT RELEASE_LOCK('stock_456');比 GET_LOCK() 更靠谱的替代方案有哪些
如果任务涉及跨机器、需持久化、或要求强一致性,GET_LOCK() 就不该是首选。
- 幂等占位:用带唯一索引的表 +
INSERT IGNORE INTO lock_table (key_name, acquired_at) VALUES ('task_x', NOW()),成功即获锁,失败说明已被占 - Redis 分布式锁:用
SET key value NX EX 30,支持原子性、TTL、可续期,成熟客户端(如 Redisson)已处理了脑裂、锁续期等细节 - ZooKeeper / etcd:提供临时有序节点和 Watch 机制,天然适合实现公平、可监控的分布式锁
真正容易被忽略的点是:锁名作用域和连接生命周期的耦合——你写的“释放锁”代码,可能根本没运行到;而你以为“连接断了锁就没了”,却忘了连接池会让这个连接活很久。


















