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

文章目录

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

一、设计目标

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

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

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

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

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

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

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),避免数据库连接成为瓶颈。

四、局限性与改进方向

  1. 性能瓶颈 :数据库写入性能有限(单机每秒数万次),高并发场景下抢锁可能成为瓶颈。

    改进:结合本地缓存(如Caffeine),先检查本地是否持有锁,减少数据库访问。

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

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

    改进:定时任务仅从主库读取数据。

五、完整代码示例

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

相关推荐
Lee川16 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川19 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i21 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有1 天前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有1 天前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫1 天前
Looper.loop() 循环机制
面试
AAA梅狸猫1 天前
Handler基本概念
面试
Wect1 天前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼1 天前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼1 天前
Next.js 企业级落地
前端·javascript·面试