文章目录
-
-
- 一、设计目标
- 二、基于数据库的分布式锁实现
-
- [1. 数据库表设计](#1. 数据库表设计)
- [2. 核心逻辑实现](#2. 核心逻辑实现)
- 三、关键问题解决
-
- [1. 互斥性保证](#1. 互斥性保证)
- [2. 安全性(防止误释放)](#2. 安全性(防止误释放))
- [3. 超时机制](#3. 超时机制)
- [4. 重入性支持](#4. 重入性支持)
- [5. 高可用优化](#5. 高可用优化)
- 四、局限性与改进方向
- 五、完整代码示例
- 六、使用示例
- 总结
-
要开发一个分布式锁,核心是实现跨进程、跨节点的互斥访问 ,确保同一时刻只有一个客户端能持有锁。不依赖Redis、ZooKeeper等中间件时,可基于数据库 或文件系统实现,但需解决分布式环境下的一致性、超时释放、重入性等问题。以下是基于数据库的分布式锁实现方案:
一、设计目标
一个可靠的分布式锁需满足:
- 互斥性:同一时刻只有一个客户端持有锁。
- 安全性:锁只能被持有者释放。
- 超时释放:防止持有者崩溃导致锁永久占用。
- 可用性:多数节点正常时,锁服务可用。
- 重入性(可选):同一客户端可重复获取已持有的锁。
二、基于数据库的分布式锁实现
利用数据库的唯一约束 和事务特性,通过表记录实现锁的抢占与释放。
1. 数据库表设计
创建一张锁表,存储锁标识、持有者信息、过期时间等:
sql
CREATE TABLE distributed_lock (
lock_key VARCHAR(64) NOT NULL PRIMARY KEY, -- 锁的唯一标识(如"order:1001")
holder_id VARCHAR(64) NOT NULL, -- 持有者ID(客户端唯一标识)
expire_time TIMESTAMP NOT NULL, -- 锁过期时间(防止永久占用)
version INT NOT NULL DEFAULT 0, -- 版本号(用于乐观锁,实现重入性)
UNIQUE KEY uk_lock_key (lock_key) -- 唯一约束,保证互斥
);
2. 核心逻辑实现
(1)获取锁(抢占锁)
通过
INSERT
语句的唯一约束实现互斥,结合过期时间避免死锁:
java
/**
* 获取分布式锁
* @param lockKey 锁标识
* @param holderId 客户端唯一ID(如UUID)
* @param expireSeconds 锁过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String holderId, int expireSeconds) {
// 1. 尝试插入锁记录(唯一约束保证只有一个客户端能成功)
String insertSql = "INSERT INTO distributed_lock (lock_key, holder_id, expire_time, version) " +
"VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND), 1) " +
"ON DUPLICATE KEY UPDATE " +
"holder_id = IF(holder_id = ? AND expire_time > NOW(), ?, holder_id), " +
"expire_time = IF(holder_id = ? AND expire_time > NOW(), DATE_ADD(NOW(), INTERVAL ? SECOND), expire_time), " +
"version = IF(holder_id = ? AND expire_time > NOW(), version + 1, version)";
// 2. 参数:lockKey, holderId, 过期秒数, 重入判断的holderId, 重入时的holderId, 重入判断的holderId, 过期秒数, 重入判断的holderId
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(insertSql)) {
ps.setString(1, lockKey);
ps.setString(2, holderId);
ps.setInt(3, expireSeconds);
ps.setString(4, holderId);
ps.setString(5, holderId);
ps.setString(6, holderId);
ps.setInt(7, expireSeconds);
ps.setString(8, holderId);
int rows = ps.executeUpdate();
// 3. 插入成功(rows=1)或重入更新成功(rows=2),均表示获取锁成功
return rows > 0;
} catch (SQLException e) {
// 唯一约束冲突时,说明锁已被其他客户端持有
return false;
}
}
逻辑说明:
- 首次获取锁:执行
INSERT
,若lock_key
不存在则成功(rows=1
);若已存在且未过期,触发唯一约束异常(返回false
)。- 重入锁:若持有者是当前客户端(
holder_id
匹配)且锁未过期,更新expire_time
和version
(rows=2
),实现重入。
(2)释放锁(主动释放)
通过
DELETE
或UPDATE
释放锁,需校验持有者身份(防止误释放他人锁):
java
/**
* 释放分布式锁
* @param lockKey 锁标识
* @param holderId 客户端唯一ID
* @return 是否释放成功
*/
public boolean unlock(String lockKey, String holderId) {
String deleteSql = "DELETE FROM distributed_lock " +
"WHERE lock_key = ? AND holder_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(deleteSql)) {
ps.setString(1, lockKey);
ps.setString(2, holderId);
int rows = ps.executeUpdate();
return rows > 0; // 只有持有者才能删除成功
} catch (SQLException e) {
return false;
}
}
(3)超时释放(被动释放)
为防止客户端崩溃导致锁永久占用,需定期清理过期锁(可通过定时任务实现):
java
/**
* 清理过期锁(定时任务,每30秒执行一次)
*/
@Scheduled(fixedRate = 30000)
public void cleanExpiredLocks() {
String cleanSql = "DELETE FROM distributed_lock WHERE expire_time < NOW()";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(cleanSql)) {
ps.executeUpdate();
} catch (SQLException e) {
// 日志记录
}
}
三、关键问题解决
1. 互斥性保证
通过
lock_key
的唯一约束,确保同一lock_key
只能被一个客户端插入成功,实现跨节点互斥。
2. 安全性(防止误释放)
释放锁时通过
holder_id
校验,只有锁的持有者才能删除记录,避免其他客户端释放不属于自己的锁。
3. 超时机制
- 主动续期:客户端持有锁期间,可定期调用
tryLock
(相同holderId
)更新expire_time
,防止锁过期。- 被动清理:定时任务删除过期锁,避免客户端崩溃后锁永久占用。
4. 重入性支持
通过
version
字段和ON DUPLICATE KEY UPDATE
逻辑,同一客户端可重复获取锁(version
递增),释放时需对应次数的unlock
(或一次全量释放,视需求而定)。
5. 高可用优化
- 数据库主从+读写分离:主库负责写(抢锁/释放锁),从库可分担定时任务的读压力。
- 分库分表 :若锁数量大,可按
lock_key
哈希分表,分散单表压力。- 连接池优化:使用高性能连接池(如HikariCP),避免数据库连接成为瓶颈。
四、局限性与改进方向
性能瓶颈 :数据库写入性能有限(单机每秒数万次),高并发场景下抢锁可能成为瓶颈。
改进:结合本地缓存(如Caffeine),先检查本地是否持有锁,减少数据库访问。
事务阻塞 :若数据库事务未及时提交,可能导致锁记录未生效,需确保
tryLock
和unlock
在独立事务中执行(autoCommit=true
)。主从延迟风险 :若使用主从架构,主库写入后未同步到从库,可能导致从库定时任务误删未过期锁。
改进:定时任务仅从主库读取数据。
五、完整代码示例
java
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.UUID;
public class DatabaseDistributedLock {
private final DataSource dataSource;
// 客户端唯一标识(启动时生成,确保同客户端唯一)
private final String holderId = UUID.randomUUID().toString();
public DatabaseDistributedLock(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取分布式锁
* @param lockKey 锁标识
* @param expireSeconds 过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, int expireSeconds) {
String sql = "INSERT INTO distributed_lock (lock_key, holder_id, expire_time, version) " +
"VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND), 1) " +
"ON DUPLICATE KEY UPDATE " +
"holder_id = IF(holder_id = ? AND expire_time > NOW(), ?, holder_id), " +
"expire_time = IF(holder_id = ? AND expire_time > NOW(), DATE_ADD(NOW(), INTERVAL ? SECOND), expire_time), " +
"version = IF(holder_id = ? AND expire_time > NOW(), version + 1, version)";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(true); // 独立事务,避免锁未提交
ps.setString(1, lockKey);
ps.setString(2, holderId);
ps.setInt(3, expireSeconds);
ps.setString(4, holderId);
ps.setString(5, holderId);
ps.setString(6, holderId);
ps.setInt(7, expireSeconds);
ps.setString(8, holderId);
int rows = ps.executeUpdate();
return rows > 0;
} catch (SQLException e) {
return false;
}
}
/**
* 释放分布式锁
* @param lockKey 锁标识
* @return 是否释放成功
*/
public boolean unlock(String lockKey) {
String sql = "DELETE FROM distributed_lock " +
"WHERE lock_key = ? AND holder_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(true);
ps.setString(1, lockKey);
ps.setString(2, holderId);
int rows = ps.executeUpdate();
return rows > 0;
} catch (SQLException e) {
return false;
}
}
/**
* 清理过期锁(定时任务调用)
*/
public void cleanExpiredLocks() {
String sql = "DELETE FROM distributed_lock WHERE expire_time < NOW()";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(true);
ps.executeUpdate();
} catch (SQLException e) {
// 记录日志
e.printStackTrace();
}
}
}
六、使用示例
java
// 初始化数据源(如HikariCP)
DataSource dataSource = createDataSource();
// 创建分布式锁实例
DatabaseDistributedLock lock = new DatabaseDistributedLock(dataSource);
// 尝试获取锁(过期时间30秒)
String lockKey = "order:1001";
if (lock.tryLock(lockKey, 30)) {
try {
// 执行临界区操作(如扣减库存)
processOrder();
} finally {
// 释放锁
lock.unlock(lockKey);
}
} else {
// 获取锁失败(如返回"系统繁忙,请重试")
}
总结
基于数据库的分布式锁实现简单,无需依赖额外中间件,适合中小规模分布式场景。其核心是通过唯一约束实现互斥 、过期时间防止死锁 、持有者校验保证安全。但在高并发场景下,需通过分库分表、本地缓存等方式优化性能,或考虑基于分布式文件系统(如NFS)的实现(原理类似,通过文件独占锁实现互斥)。