【面试场景题】不使用redis、zk如何自己开发一个分布式锁
文章目录
- 一、设计目标
- 二、基于数据库的分布式锁实现
- 1. 数据库表设计
- 2. 核心逻辑实现
- (1)获取锁(抢占锁)
- (2)释放锁(主动释放)
- (3)超时释放(被动释放)
- 三、关键问题解决
- 1. 互斥性保证
- 2. 安全性(防止误释放)
- 3. 超时机制
- 4. 重入性支持
- 5. 高可用优化
- 四、局限性与改进方向
- 五、完整代码示例
- 六、使用示例
- 总结
要开发一个分布式锁,核心是实现跨进程、跨节点的互斥访问,确保同一时刻只有一个客户端能持有锁。不依赖Redis、ZooKeeper等中间件时,可基于数据库或文件系统实现,但需解决分布式环境下的一致性、超时释放、重入性等问题。以下是基于数据库的分布式锁实现方案:
一、设计目标
一个可靠的分布式锁需满足:
- 互斥性:同一时刻只有一个客户端持有锁。
- 安全性:锁只能被持有者释放。
- 超时释放:防止持有者崩溃导致锁永久占用。
- 可用性:多数节点正常时,锁服务可用。
- 重入性(可选):同一客户端可重复获取已持有的锁。
二、基于数据库的分布式锁实现
利用数据库的唯一约束和事务特性,通过表记录实现锁的抢占与释放。
1. 数据库表设计
创建一张锁表,存储锁标识、持有者信息、过期时间等:
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
语句的唯一约束实现互斥,结合过期时间避免死锁:
/*** 获取分布式锁* @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, 过期秒数, 重入判断的holderIdtry (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
释放锁,需校验持有者身份(防止误释放他人锁):
/*** 释放分布式锁* @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)超时释放(被动释放)
为防止客户端崩溃导致锁永久占用,需定期清理过期锁(可通过定时任务实现):
/*** 清理过期锁(定时任务,每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
)。主从延迟风险:若使用主从架构,主库写入后未同步到从库,可能导致从库定时任务误删未过期锁。
改进:定时任务仅从主库读取数据。
五、完整代码示例
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();}}
}
六、使用示例
// 初始化数据源(如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)的实现(原理类似,通过文件独占锁实现互斥)。