【分布式锁】数据库锁实现分布式锁

基于 INSERT ON DUPLICATE KEY UPDATE 的分布式锁实现原理

核心SQL实现

INSERT INTO distributed_lock (lock_key, client_id, expire_time) 
VALUES (?, ?, NOW() + INTERVAL ? SECOND)
ON DUPLICATE KEY UPDATE 
   client_id = IF(expire_time < NOW(), VALUES(client_id), client_id),
   expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time)

实现原理分步解析

  1. 唯一索引约束

    • 表结构要求 lock_key 字段必须有唯一约束(UNIQUE
    • 同一时刻只能存在一个 lock_key 记录
  2. 首次获取锁

    • 当锁不存在时,INSERT 成功插入新记录
    • 示例数据:
      lock_key: "order_lock_123", 
      client_id: "client_A", 
      expire_time: 2023-05-20 15:30:00
      
  3. 锁竞争场景

    • 当其他客户端尝试获取相同锁时,触发 ON DUPLICATE KEY UPDATE
    • 通过 IF(expire_time < NOW(), ...) 判断锁是否已过期:
      -- 如果当前锁已过期(expire_time < NOW())
      -- 则更新为新的客户端ID和过期时间(抢锁成功)
      -- 否则保持原值(抢锁失败)
      
  4. 返回值判断

    • 执行后检查影响行数:
      • 1:插入新锁成功
      • 2:更新过期锁成功
      • 0:锁被其他客户端持有且未过期

完整工作流程

sequenceDiagram participant ClientA participant ClientB participant MySQL ClientA->>MySQL: INSERT锁记录(lock_key=order_123) MySQL-->>ClientA: 成功(影响行数=1) ClientB->>MySQL: INSERT相同lock_key MySQL->>MySQL: 发现重复key,检查expire_time alt 锁已过期 MySQL-->>ClientB: 更新记录(影响行数=2) else 锁未过期 MySQL-->>ClientB: 保持原记录(影响行数=0) end

关键设计要点

  1. 过期时间机制

    • 必须设置 expire_time 避免死锁
    • 客户端崩溃后,锁会自动失效
  2. 客户端标识

    • client_id 需全局唯一(建议使用IP+进程ID+时间戳)
    • 解锁时需验证 client_id 防止误删他人锁
  3. 原子性保证

    • 整个操作在单条SQL中完成
    • 无需事务,利用数据库原子操作特性
  4. 时钟同步问题

    • 依赖数据库服务器时间(NOW()
    • 避免使用客户端时间防止时钟不同步

完整Java实现示例

@Repository
public class DatabaseDistributedLock {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 尝试获取锁
     * @param lockKey 锁标识
     * @param clientId 客户端唯一ID
     * @param expireSeconds 锁过期时间(秒)
     * @return 是否获取成功
     */
    public boolean tryLock(String lockKey, String clientId, int expireSeconds) {
        String sql = "INSERT INTO distributed_lock (lock_key, client_id, expire_time) " +
                     "VALUES (?, ?, NOW() + INTERVAL ? SECOND) " +
                     "ON DUPLICATE KEY UPDATE " +
                     "   client_id = IF(expire_time < NOW(), VALUES(client_id), client_id), " +
                     "   expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time)";
        
        int affectedRows = jdbcTemplate.update(sql, lockKey, clientId, expireSeconds);
        
        // affectedRows=1: 新插入锁成功
        // affectedRows=2: 更新过期锁成功
        return affectedRows > 0;
    }

    /**
     * 释放锁
     * @param lockKey 锁标识
     * @param clientId 客户端唯一ID
     */
    public void unlock(String lockKey, String clientId) {
        jdbcTemplate.update(
            "DELETE FROM distributed_lock WHERE lock_key = ? AND client_id = ?",
            lockKey, clientId
        );
    }
}

优缺点分析

优点

  • 实现简单,仅依赖数据库
  • 无额外中间件依赖
  • 自动处理锁过期

缺点

  • 性能较差(约500-1000 TPS)
  • 数据库压力大
  • 需要处理连接失败情况
  • 不适用于高并发场景

适用场景

  • 并发量低的系统(<100 TPS)
  • 需要强一致性的场景
  • 基础设施受限(无法使用Redis/ZooKeeper)

对比其他方案

特性 数据库锁 Redis锁 ZooKeeper锁
性能 低(500 TPS) 高(10万+ TPS) 中(1万 TPS)
实现复杂度 简单 中等 复杂
自动释放 需手动清理 自动过期 Session断开自动释放
一致性 强一致 最终一致 强一致
适用场景 低并发简单系统 高并发短任务 强一致性要求场景

生产建议:在允许的情况下,优先选择Redis或ZooKeeper实现。数据库锁仅作为备用方案。

posted @ 2025-06-12 16:11  佛祖让我来巡山  阅读(44)  评论(0)    收藏  举报

佛祖让我来巡山博客站 - 创建于 2018-08-15

开发工程师个人站,内容主要是网站开发方面的技术文章,大部分来自学习或工作,部分来源于网络,希望对大家有所帮助。

Bootstrap中文网