【分布式锁】数据库锁实现分布式锁
基于 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)
实现原理分步解析
- 
唯一索引约束 - 表结构要求 lock_key字段必须有唯一约束(UNIQUE)
- 同一时刻只能存在一个 lock_key记录
 
- 表结构要求 
- 
首次获取锁 - 当锁不存在时,INSERT成功插入新记录
- 示例数据:lock_key: "order_lock_123", client_id: "client_A", expire_time: 2023-05-20 15:30:00
 
- 当锁不存在时,
- 
锁竞争场景 - 当其他客户端尝试获取相同锁时,触发 ON DUPLICATE KEY UPDATE
- 通过 IF(expire_time < NOW(), ...)判断锁是否已过期:-- 如果当前锁已过期(expire_time < NOW()) -- 则更新为新的客户端ID和过期时间(抢锁成功) -- 否则保持原值(抢锁失败)
 
- 当其他客户端尝试获取相同锁时,触发 
- 
返回值判断 - 执行后检查影响行数:
- 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
关键设计要点
- 
过期时间机制 - 必须设置 expire_time避免死锁
- 客户端崩溃后,锁会自动失效
 
- 必须设置 
- 
客户端标识 - client_id需全局唯一(建议使用IP+进程ID+时间戳)
- 解锁时需验证 client_id防止误删他人锁
 
- 
原子性保证 - 整个操作在单条SQL中完成
- 无需事务,利用数据库原子操作特性
 
- 
时钟同步问题 - 依赖数据库服务器时间(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实现。数据库锁仅作为备用方案。
❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!
本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/18925663

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号