当前位置: 首页 > news >正文

【面试场景题】不使用redis、zk如何自己开发一个分布式锁

文章目录

      • 一、设计目标
      • 二、基于数据库的分布式锁实现
        • 1. 数据库表设计
        • 2. 核心逻辑实现
          • (1)获取锁(抢占锁)
          • (2)释放锁(主动释放)
          • (3)超时释放(被动释放)
      • 三、关键问题解决
        • 1. 互斥性保证
        • 2. 安全性(防止误释放)
        • 3. 超时机制
        • 4. 重入性支持
        • 5. 高可用优化
      • 四、局限性与改进方向
      • 五、完整代码示例
      • 六、使用示例
      • 总结

要开发一个分布式锁,核心是实现跨进程、跨节点的互斥访问,确保同一时刻只有一个客户端能持有锁。不依赖Redis、ZooKeeper等中间件时,可基于数据库文件系统实现,但需解决分布式环境下的一致性、超时释放、重入性等问题。以下是基于数据库的分布式锁实现方案:

一、设计目标

一个可靠的分布式锁需满足:

  1. 互斥性:同一时刻只有一个客户端持有锁。
  2. 安全性:锁只能被持有者释放。
  3. 超时释放:防止持有者崩溃导致锁永久占用。
  4. 可用性:多数节点正常时,锁服务可用。
  5. 重入性(可选):同一客户端可重复获取已持有的锁。

二、基于数据库的分布式锁实现

利用数据库的唯一约束事务特性,通过表记录实现锁的抢占与释放。

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_timeversionrows=2),实现重入。
(2)释放锁(主动释放)

通过DELETEUPDATE释放锁,需校验持有者身份(防止误释放他人锁):

/*** 释放分布式锁* @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),避免数据库连接成为瓶颈。

四、局限性与改进方向

  1. 性能瓶颈:数据库写入性能有限(单机每秒数万次),高并发场景下抢锁可能成为瓶颈。
    改进:结合本地缓存(如Caffeine),先检查本地是否持有锁,减少数据库访问。

  2. 事务阻塞:若数据库事务未及时提交,可能导致锁记录未生效,需确保tryLockunlock在独立事务中执行(autoCommit=true)。

  3. 主从延迟风险:若使用主从架构,主库写入后未同步到从库,可能导致从库定时任务误删未过期锁。
    改进:定时任务仅从主库读取数据。

五、完整代码示例

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)的实现(原理类似,通过文件独占锁实现互斥)。

http://www.dtcms.com/a/361504.html

相关文章:

  • 数据库索引失效的原因+示例
  • 视觉引导机械手双夹爪抓取:偏心旋转补偿与逆运动学求解
  • 卷积神经网络训练全攻略:从理论到实战
  • 【K8s】整体认识K8s之Configmap、Secret/ResourceQuota资源配额/访问控制
  • HTTP/2 多路复用
  • [C语言] 结构体 内存对齐规则 内存大小计算
  • 基于springboot生鲜交易系统源码和论文
  • 一文读懂k8s的pv与pvc原理
  • 威科夫与高频因子
  • 2.充分条件与必要条件
  • Android Framework打电话禁止播放运营商视频彩铃
  • Coze源码分析-工作空间-资源库-前端源码
  • Frida Hook 算法
  • 音频数据集采样率选择建议
  • 从网络层接入控制过渡到应用层身份认证的过程
  • 电源相关零碎知识总结
  • 如何把指定阿里云文件夹下的所有文件移动到另一个文件夹下,移动文件时把文件名称(不包括文件后缀)进行md5编码
  • @Autowired注入底层原理
  • 吴恩达机器学习补充:决策树和随机森林
  • AUTOSAR AP R24-11 Log and Trace 文档总结
  • 贪心算法解决钱币找零问题(二)
  • CentOS10安装RabbitMQ
  • [特殊字符]【C语言】超全C语言字符串处理函数指南:从原理到实战
  • ARM的编程模型
  • TikTok Shop 物流拖后腿?海外仓系统破解物流困局
  • nginx是什么?
  • MQ使用场景分析
  • OpenHarmony 分布式感知中枢深度拆解:MSDP 框架从 0 到 1 的实战指南
  • 2025年- H104-Lc212--455.分发饼干(贪心)--Java版
  • 电动自行车淋水安全测试的关键利器:整车淋水性能测试装置的技术分析