【分布式锁】数据库锁实现分布式锁
基于 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号