详细解说基于mysql分布式锁的三种实现方式
更多备考资料-> 免费刷题网站
基于MySQL实现分布式锁主要有以下三种核心方法,每种方法均有其实现原理和适用场景:
一、基于唯一索引/约束的实现
定义
通过数据库唯一索引的排他性实现锁竞争。创建包含唯一字段(如lock_name)的表,加锁时尝试插入记录,成功则获取锁,失败则自旋重试。释放锁时删除对应记录
特点:
实现简单,依赖INSERT操作的原子性
需处理锁超时(通过expire_time字段和定时任务清理过期锁)
缺点包括非阻塞性、不可重入,且数据库单点故障会影响锁可用性。
实现步骤
步骤1:创建锁表
CREATE TABLE `distributed_lock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`lock_key` varchar(64) NOT NULL COMMENT '锁标识(需加唯一索引)',`holder_id` varchar(128) NOT NULL COMMENT '锁持有者标识(如IP+线程ID)',`expire_time` datetime NOT NULL COMMENT '锁过期时间',`create_time` datetime DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`),UNIQUE KEY `uk_lock_key` (`lock_key`) -- 关键:唯一索引保证互斥
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
步骤2:加锁逻辑(Java示例)
public boolean tryLock(String lockKey, String holderId, int expireSeconds) {Connection conn = null;try {conn = dataSource.getConnection();conn.setAutoCommit(false);// 1. 尝试插入锁记录String sql = "INSERT INTO distributed_lock (lock_key, holder_id, expire_time) " +"VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND))";try (PreparedStatement ps = conn.prepareStatement(sql)) {ps.setString(1, lockKey);ps.setString(2, holderId);ps.setInt(3, expireSeconds);int affectedRows = ps.executeUpdate();if (affectedRows > 0) {conn.commit();return true; // 加锁成功}}// 2. 插入失败时检查锁是否过期sql = "SELECT id FROM distributed_lock WHERE lock_key=? AND expire_time < NOW()";try (PreparedStatement ps = conn.prepareStatement(sql)) {ps.setString(1, lockKey);ResultSet rs = ps.executeQuery();if (rs.next()) {// 3. 锁已过期,尝试抢占sql = "UPDATE distributed_lock SET holder_id=?, expire_time=DATE_ADD(NOW(), INTERVAL ? SECOND) " +"WHERE lock_key=? AND expire_time < NOW()";try (PreparedStatement updatePs = conn.prepareStatement(sql)) {updatePs.setString(1, holderId);updatePs.setInt(2, expireSeconds);updatePs.setString(3, lockKey);if (updatePs.executeUpdate() > 0) {conn.commit();return true; // 抢占成功}}}}conn.rollback();return false; // 加锁失败} catch (SQLException e) {if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {}throw new RuntimeException("加锁异常", e);} finally {if (conn != null) try { conn.close(); } catch (SQLException ignored) {}}
}
步骤3:解锁逻辑
public void unlock(String lockKey, String holderId) {String sql = "DELETE FROM distributed_lock WHERE lock_key=? AND holder_id=?";try (Connection conn = dataSource.getConnection();PreparedStatement ps = conn.prepareStatement(sql)) {ps.setString(1, lockKey);ps.setString(2, holderId);ps.executeUpdate();} catch (SQLException e) {throw new RuntimeException("解锁异常", e);}
}
关键点
锁续期:需后台线程定期执行UPDATE distributed_lock SET expire_time=新时间 WHERE lock_key=? AND holder_id=?。
死锁处理:通过expire_time自动失效锁,避免死锁。
二、基于悲观锁(SELECT FOR UPDATE)
利用InnoDB行锁机制,通过SELECT … FOR UPDATE锁定特定记录。事务提交或回滚时自动释放锁
实现步骤:
预存锁记录到表(如lock表)
执行SELECT lock_name FROM lock WHERE lock_name=‘key’ FOR UPDATE获取锁
需手动管理事务提交以释放锁
优化点:
结合线程标识支持可重入
避免锁表需确保WHERE条件使用索引
步骤1:初始化锁记录
INSERT INTO `pessimistic_lock` (`lock_key`, `version`) VALUES ('order_lock', 1);
步骤2:加锁逻辑
public boolean tryLock(String lockKey, long timeoutMs) {Connection conn = null;try {conn = dataSource.getConnection();conn.setAutoCommit(false);// 1. 设置事务超时(避免长时间阻塞)try (Statement stmt = conn.createStatement()) {stmt.execute("SET SESSION innodb_lock_wait_timeout=" + (timeoutMs / 1000));}// 2. 尝试获取行锁String sql = "SELECT * FROM pessimistic_lock WHERE lock_key=? FOR UPDATE";try (PreparedStatement ps = conn.prepareStatement(sql)) {ps.setString(1, lockKey);ResultSet rs = ps.executeQuery();if (rs.next()) {return true; // 加锁成功(事务提交前持续持有锁)}}return false;} catch (SQLException e) {if (e.getErrorCode() == 1205) { // MySQL锁等待超时错误码return false;}throw new RuntimeException("加锁异常", e);}
}
步骤3:解锁逻辑
public void unlock(Connection lockedConn) {try {lockedConn.commit(); // 提交事务即释放锁} catch (SQLException e) {throw new RuntimeException("解锁异常", e);} finally {if (lockedConn != null) try { lockedConn.close(); } catch (SQLException ignored) {}}
}
关键点
**事务管理:**必须确保加锁和解锁在同一个事务中。
可重入支持:可在表中增加holder_id字段,校验当前线程是否已持有锁。
三、基于乐观锁(版本号机制)
通过版本号或时间戳实现CAS(Compare-And-Swap)。表中增加version字段,更新时校验版本号,匹配则更新成功并获取锁
流程:
查询当前版本号:SELECT version FROM lock WHERE resource_id=‘X’
更新时带版本条件:UPDATE lock SET version=version+1 WHERE resource_id=‘X’ AND version=old_version
返回影响行数判断加锁结果
适用场景:低冲突环境,如库存扣减
步骤1:建表
CREATE TABLE `optimistic_lock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`resource_id` varchar(64) NOT NULL COMMENT '业务资源ID',`version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',PRIMARY KEY (`id`),UNIQUE KEY `uk_resource_id` (`resource_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
步骤2:加锁逻辑
public boolean tryLock(String resourceId) {String selectSql = "SELECT version FROM optimistic_lock WHERE resource_id=?";String updateSql = "UPDATE optimistic_lock SET version=version+1 WHERE resource_id=? AND version=?";try (Connection conn = dataSource.getConnection()) {// 1. 查询当前版本号int currentVersion;try (PreparedStatement ps = conn.prepareStatement(selectSql)) {ps.setString(1, resourceId);ResultSet rs = ps.executeQuery();if (!rs.next()) {throw new RuntimeException("资源不存在");}currentVersion = rs.getInt("version");}// 2. 尝试更新版本号try (PreparedStatement ps = conn.prepareStatement(updateSql)) {ps.setString(1, resourceId);ps.setInt(2, currentVersion);return ps.executeUpdate() > 0; // 影响行数>0表示加锁成功}} catch (SQLException e) {throw new RuntimeException("乐观锁异常", e);}
}
关键点
自旋重试:需在业务代码中循环调用tryLock直到成功或超时。
无阻塞:失败后立即返回,适合低冲突场景。
总结对比
选择建议:
需要强一致性 → 悲观锁
高并发且冲突少 → 乐观锁
快速实现且容忍一定缺陷 → 唯一索引
更多备考资料-> 免费刷题网站