1. 为什么需要基于MySQL表字段实现分布式锁在分布式系统中协调多个服务实例对共享资源的访问是一个经典难题。传统的单机锁机制如Java的synchronized在分布式环境下完全失效我们需要一种跨JVM、跨主机的锁解决方案。Redis的SETNX命令和Redisson框架确实是目前最流行的分布式锁实现方案但很多传统企业环境中MySQL才是基础设施的标配。在这些场景下基于MySQL实现分布式锁有几个独特优势无需引入新组件对于已经重度依赖MySQL的系统可以避免Redis的运维复杂度强一致性保证MySQL的ACID特性比Redis更可靠特别是在主从切换场景可视化监控锁状态可以直接通过SQL查询方便问题排查与业务数据同库对于需要与业务数据强一致的操作可以共用事务提示当你的系统TPS低于5000且已经使用MySQL时可以考虑这种方案。高并发场景还是建议使用Redis或Zookeeper。2. 表结构设计与核心字段解析我们先来看一个典型的分布式锁表设计以MySQL 8.0为例CREATE TABLE distributed_lock ( id bigint NOT NULL AUTO_INCREMENT, lock_key varchar(64) NOT NULL COMMENT 锁定的资源标识, lock_owner varchar(128) NOT NULL COMMENT 锁持有者标识如IP线程ID, lock_count int NOT NULL DEFAULT 0 COMMENT 可重入计数, expire_time datetime NOT NULL COMMENT 过期时间, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_lock_key (lock_key) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;2.1 关键字段设计考量lock_key必须是唯一索引这是锁的全局唯一标识。实践中常用业务ID资源类型拼接如order_12345lock_owner需要能唯一标识请求来源。推荐格式IP地址线程ID进程ID如192.168.1.100123456789expire_time必须设置过期时间这是避免死锁的关键。时间长度要根据业务操作耗时合理设置通常1-30秒lock_count支持锁重入的重要字段。每次重入加1释放时减1归零才真正释放2.2 为什么不用MySQL的GET_LOCK()MySQL原生提供了GET_LOCK()函数但它有几个致命缺陷连接断开时锁自动释放网络闪断会导致意外释放无法查看当前锁的持有者信息不支持设置超时时间只能等待或立即返回不同版本实现差异大特别是主从切换场景3. 核心SQL操作与实现原理3.1 获取锁的实现-- 使用INSERT ON DUPLICATE KEY UPDATE实现原子化的获取锁操作 INSERT INTO distributed_lock(lock_key, lock_owner, lock_count, expire_time) VALUES (order_12345, 192.168.1.100123456789, 1, DATE_ADD(NOW(), INTERVAL 30 SECOND)) ON DUPLICATE KEY UPDATE lock_count IF(lock_owner VALUES(lock_owner) AND expire_time NOW(), lock_count 1, lock_count), expire_time IF(lock_owner VALUES(lock_owner) AND expire_time NOW(), VALUES(expire_time), expire_time), lock_owner IF(lock_owner VALUES(lock_owner) AND expire_time NOW(), VALUES(lock_owner), lock_owner);这个SQL的精妙之处在于首次获取直接插入记录重入场景相同owner且未过期时计数1竞争场景不同owner或已过期时不做任何修改3.2 释放锁的实现UPDATE distributed_lock SET lock_count lock_count - 1, expire_time CASE WHEN lock_count - 1 0 THEN 1970-01-01 ELSE expire_time END WHERE lock_key order_12345 AND lock_owner 192.168.1.100123456789;关键点只有锁的持有者才能释放计数归零时将过期时间设为过去相当于释放非零时只减少计数保持锁状态3.3 锁续期机制UPDATE distributed_lock SET expire_time DATE_ADD(NOW(), INTERVAL 30 SECOND) WHERE lock_key order_12345 AND lock_owner 192.168.1.100123456789 AND expire_time NOW();建议在获取锁后启动后台线程定期如过期时间的1/3间隔执行续期操作。4. 完整Java实现方案下面是一个生产可用的Java实现public class MySQLDistributedLock implements Lock { private final DataSource dataSource; private final String lockKey; private final String lockOwner; private final long expireMillis; // 使用ThreadLocal维护重入计数 private final ThreadLocalInteger holdCount ThreadLocal.withInitial(() - 0); public boolean tryLock(long timeout, TimeUnit unit) throws SQLException { long start System.currentTimeMillis(); long timeoutMillis unit.toMillis(timeout); while (true) { if (acquireLock()) { holdCount.set(holdCount.get() 1); startRenewalTask(); return true; } if (System.currentTimeMillis() - start timeoutMillis) { return false; } try { Thread.sleep(100); // 避免CPU空转 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } } private boolean acquireLock() throws SQLException { try (Connection conn dataSource.getConnection()) { String sql INSERT INTO distributed_lock(lock_key, lock_owner, lock_count, expire_time) VALUES (?, ?, 1, DATE_ADD(NOW(), INTERVAL ? SECOND)) ON DUPLICATE KEY UPDATE lock_count IF(lock_owner ? AND expire_time NOW(), lock_count 1, lock_count), expire_time IF(lock_owner ? AND expire_time NOW(), VALUES(expire_time), expire_time), lock_owner IF(lock_owner ? AND expire_time NOW(), VALUES(lock_owner), lock_owner); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, lockKey); ps.setString(2, lockOwner); ps.setLong(3, expireMillis / 1000); ps.setString(4, lockOwner); ps.setString(5, lockOwner); ps.setString(6, lockOwner); int affectedRows ps.executeUpdate(); return affectedRows 0; } } } private void startRenewalTask() { ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { try { renewLock(); } catch (SQLException e) { // 续期失败处理 } }, expireMillis / 3, expireMillis / 3, TimeUnit.MILLISECONDS); } // 其他方法实现... }5. 生产环境中的注意事项5.1 时钟同步问题MySQL服务器与应用服务器的时钟必须同步否则会导致过早判断锁过期时钟快过晚释放锁时钟慢建议部署NTP服务保持时钟同步误差控制在100ms内。5.2 连接池配置必须为锁操作配置独立连接池与常规业务操作隔离。关键参数最大连接数建议等于应用实例数 × 2获取连接超时小于锁等待超时验证查询SELECT 15.3 性能优化建议索引优化确保lock_key字段有唯一索引表分离高并发场景下单独使用一个库实例监控指标锁等待时间锁竞争频率锁持有时间分布5.4 常见问题排查问题1锁无法释放检查应用是否正常关闭需要实现ShutdownHook检查锁续期线程是否存活检查MySQL连接是否泄漏问题2锁被意外抢占检查时钟同步状态检查expire_time更新逻辑检查lock_owner生成规则6. 与Redis分布式锁的对比特性MySQL实现Redis (Redisson)实现性能较低约1000 TPS高约50000 TPS一致性强一致最终一致可重入支持支持自动续期需自行实现内置支持看门狗机制需自行实现内置支持主从切换半同步复制可保证可能丢锁适用场景中低频强一致场景高频最终一致场景在实际项目中我通常会这样选择金融交易等强一致场景 → MySQL实现秒杀等高并发场景 → Redis实现极端情况下可以两者结合使用先抢Redis锁关键操作再用MySQL锁