【面试场景题】不使用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)的实现(原理类似,通过文件独占锁实现互斥)。

相关推荐
青鱼入云5 小时前
【面试场景题】100M网络带宽能不能支撑QPS3000
面试·职场和发展
Goboy6 小时前
你刷网页的一瞬间,背后服务器在"排队抢活儿"?
后端·面试·架构
lecepin6 小时前
前端技术月刊-2025.9
前端·javascript·面试
绝无仅有6 小时前
Go 面试题:Goroutine 和 GMP 模型解析
后端·面试·github
掘金安东尼6 小时前
理解 Promise.any():一次成功就行
前端·javascript·面试
大模型真好玩6 小时前
大模型工程面试经典(三)—如何通过微调提升Agent性能?
人工智能·面试·agent
阿亮爱学代码7 小时前
面试 八股文 经典题目 - Mysql部分(一)
面试·职场和发展·八股文·实习·sql面试
凯子坚持 c8 小时前
Redis 核心概念解析:从渐进式遍历、数据库管理到客户端通信协议
数据库·redis·bootstrap
在未来等你9 小时前
Kafka面试精讲 Day 4:Consumer消费者模型与消费组
大数据·分布式·面试·kafka·消息队列