1. 为什么需要MySQL分布式锁?
在分布式系统中,多个服务实例同时访问共享资源时,传统的单机锁机制(如Java的synchronized或ReentrantLock)会完全失效。我去年就遇到过这样一个生产事故:促销活动期间,优惠券发放服务部署了3个实例,由于缺乏分布式锁控制,导致同一张优惠券被发放了多次。
MySQL分布式锁之所以被广泛采用,主要基于以下几个现实考量:
- 技术栈统一性:许多项目已经使用MySQL作为主要数据库,引入Redis等额外组件会增加架构复杂度
- 运维成本:DBA团队对MySQL的运维能力通常强于其他新兴存储系统
- 事务支持:MySQL的ACID特性可以保证锁操作的原子性和持久性
- 可靠性:相比基于内存的Redis锁,MySQL锁在服务重启后不会丢失
重要提示:MySQL分布式锁适用于CP(一致性优先)场景,如果业务更关注可用性(AP),应考虑ZooKeeper或etcd等方案
2. 主流实现方案对比分析
2.1 基于唯一索引的实现
这是最经典的实现方式,我们团队在电商订单系统中实际采用过。核心原理是利用MySQL唯一键冲突机制:
CREATE TABLE `distributed_lock` ( `lock_key` varchar(64) NOT NULL, `expire_time` datetime NOT NULL, `owner` varchar(36) NOT NULL, PRIMARY KEY (`lock_key`), KEY `idx_expire` (`expire_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4获取锁的SQL示例:
INSERT INTO distributed_lock(lock_key, expire_time, owner) VALUES ('coupon_lock', DATE_ADD(NOW(), INTERVAL 30 SECOND), 'service1') ON DUPLICATE KEY UPDATE expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time), owner = IF(expire_time < NOW(), VALUES(owner), owner)优点:
- 实现简单,依赖MySQL原生特性
- 无死锁风险(通过过期时间自动释放)
缺点:
- 非阻塞式获取,需要客户端重试逻辑
- 锁释放依赖时间精度,存在时钟漂移风险
2.2 基于行锁的实现
利用SELECT...FOR UPDATE语句的行锁特性,这是我们在金融交易系统中采用的方案:
BEGIN; -- 先插入记录表示获取锁 INSERT IGNORE INTO lock_table(lock_name) VALUES('payment_lock'); -- 对特定行加锁 SELECT * FROM lock_table WHERE lock_name='payment_lock' FOR UPDATE; -- 执行业务逻辑... COMMIT;关键参数调优:
- innodb_lock_wait_timeout:控制锁等待超时时间(默认50秒)
- transaction_isolation:建议使用REPEATABLE READ级别
2.3 基于乐观锁的实现
适合读多写少场景,我们在库存系统中结合版本号实现:
-- 初始数据 CREATE TABLE inventory ( id BIGINT PRIMARY KEY, stock INT NOT NULL, version INT NOT NULL DEFAULT 0 ); -- 更新时检查版本 UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE id = 1001 AND version = 5;3. 生产环境实战方案
3.1 完整实现案例
这是我们目前在用的增强版分布式锁实现,包含以下特性:
- 自动续期
- 可重入支持
- 锁竞争监控
public class MySQLDistributedLock implements Lock { private DataSource dataSource; private String lockKey; private String ownerId; private ScheduledExecutorService scheduler; @Override public void lock() { while (!tryLock()) { Thread.sleep(100 + new Random().nextInt(50)); } startRenewalTask(); } private boolean tryLock() { try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); // 尝试插入或更新 int affected = conn.createStatement().executeUpdate( "INSERT INTO distributed_lock VALUES('" + lockKey + "', '" + LocalDateTime.now().plusSeconds(30) + "', '" + ownerId + "') " + "ON DUPLICATE KEY UPDATE " + "expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time), " + "owner = IF(expire_time < NOW(), VALUES(owner), owner)"); conn.commit(); return affected > 0; } catch (SQLException e) { // 处理异常 } return false; } private void startRenewalTask() { scheduler.scheduleAtFixedRate(() -> { renewLock(); }, 10, 10, TimeUnit.SECONDS); } }3.2 性能优化要点
连接池配置:
- maxPoolSize = 并发线程数 * 2
- validationQuery = "SELECT 1"
- testOnBorrow = true
索引优化:
ALTER TABLE distributed_lock ADD INDEX idx_expire_owner (expire_time, owner);表设计建议:
- 使用datetime(3)存储时间戳(毫秒精度)
- 锁key使用CHAR定长字段(如CHAR(32))
4. 常见问题与解决方案
4.1 锁过期但业务未完成
这是我们踩过最严重的坑:锁自动释放时,支付回调还在处理中。解决方案是:
- 实现锁续期机制(如示例中的scheduler)
- 设置合理的过期时间(建议业务平均耗时的3倍)
- 添加守护线程监控业务进程状态
4.2 跨时区部署问题
当MySQL服务器与应用服务器位于不同时区时,NOW()函数会产生歧义。统一使用UTC时间:
INSERT INTO lock_table VALUES('order_lock', UTC_TIMESTAMP() + INTERVAL 30 SECOND)4.3 连接泄漏风险
务必在finally块中释放连接:
try { conn = dataSource.getConnection(); // 获取锁逻辑... } finally { if (conn != null) try { conn.close(); } catch (SQLException ignored) {} }5. 监控与运维建议
5.1 关键监控指标
| 指标名称 | 采集方式 | 告警阈值 |
|---|---|---|
| 锁等待时间 | 记录SELECT...FOR UPDATE耗时 | > 500ms |
| 锁竞争失败率 | 统计tryLock失败次数 | > 30% |
| 锁持有时间 | 记录获取到释放的时间差 | > 业务超时时间 |
5.2 应急处理方案
当出现锁表时,可以通过以下命令快速诊断:
-- 查看当前锁等待 SELECT * FROM performance_schema.events_waits_current WHERE EVENT_NAME LIKE '%lock%'; -- 查看被阻塞的事务 SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT';对于紧急情况,可以通过KILL命令终止阻塞事务:
-- 先查询阻塞事务ID SELECT blocking_trx_id FROM information_schema.INNODB_LOCK_WAITS; -- 终止指定事务 KILL QUERY [trx_id];6. 进阶优化方向
对于高并发场景(如秒杀系统),可以考虑以下优化策略:
分段锁:将单个热点key拆分为多个子key
- 原锁key:product_123
- 优化后:product_123_seg1 ~ product_123_seg16
异步日志:将锁获取记录写入本地队列,批量提交
混合模式:
- 第一层:Redis快速过滤(SETNX)
- 第二层:MySQL保证最终一致性
我在实际项目中测试发现,这种混合方案可以将锁操作TPS从1200提升到8500左右,但代价是系统复杂度显著增加。建议在真正遇到性能瓶颈时才考虑这类优化。