一、分布式锁的基本概念
1.1 什么是分布式锁
分布式锁是一种在分布式系统或集群环境中用于控制多个进程/线程对共享资源进行互斥访问的协调机制。在单机系统中,我们可以使用Java内置的synchronized
关键字或ReentrantLock
等实现线程间的互斥访问,但在分布式环境下,这些本地锁机制无法跨JVM工作,因此需要分布式锁来协调不同节点上的进程。
分布式锁的核心目的是在分布式系统中实现跨进程的互斥访问,确保在同一时间只有一个客户端(可能位于不同物理机器上)能够执行特定的操作或访问特定的资源。
1.2 为什么需要分布式锁
分布式锁的典型应用场景包括:
- 防止重复处理:确保定时任务在集群中只在一台机器上执行
- 避免资源冲突:如库存超卖、账户余额扣减等并发控制
- 系统幂等性:保证同一操作不会被重复执行
- 临界资源保护:如配置文件更新、全局计数器等共享资源访问
1.3 分布式锁的特性要求
一个完善的分布式锁应当具备以下特性:
特性 | 说明 | 重要性 |
---|---|---|
互斥性 | 同一时刻只有一个客户端能持有锁 | 基本要求 |
可重入性 | 同一客户端可多次获取同一把锁 | 便利性 |
锁超时 | 自动释放防止死锁 | 安全性 |
高可用 | 锁服务应高度可用 | 可靠性 |
非阻塞 | 获取锁失败应快速返回 | 性能 |
公平性 | 按请求顺序获取锁 | 可选 |
二、分布式锁的实现方式
Java生态中常见的分布式锁实现方式主要有以下几种:
2.1 基于数据库的实现
2.1.1 唯一索引方案
CREATE TABLE distributed_lock (
id int NOT NULL AUTO_INCREMENT,
lock_name varchar(64) NOT NULL,
owner varchar(255) NOT NULL,
expire_time datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_lock_name (lock_name)
);
获取锁:
INSERT INTO distributed_lock(lock_name, owner, expire_time)
VALUES ('order_lock', 'host1_ip', NOW() + INTERVAL 30 SECOND);
释放锁:
DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND owner = 'host1_ip';
优点:实现简单,依赖少
缺点:性能较差,无自动过期,需自行处理
2.1.2 乐观锁方案
基于数据版本号实现:
-- 获取当前版本
SELECT version FROM resource WHERE id = 1;
-- 更新时检查版本
UPDATE resource SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = #{oldVersion};
适用场景:冲突较少,重试成本低的场景
2.2 基于Redis的实现
2.2.1 SETNX方案
// 获取锁
public boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
// 释放锁 - Lua脚本保证原子性
public boolean releaseLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return Long.valueOf(1L).equals(result);
}
2.2.2 RedLock算法
Redisson实现示例:
Config config = new Config();
config.useSentinelServers()
.addSentinelAddress("redis://127.0.0.1:26389")
.addSentinelAddress("redis://127.0.0.1:26379")
.setMasterName("mymaster");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("anyLock");
// 尝试加锁,最多等待100秒,上锁后30秒自动解锁
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
RedLock要点:
- 获取当前时间
- 依次尝试从多个独立的Redis实例获取锁
- 计算获取锁花费的时间
- 只有在多数节点上获取成功且总耗时小于锁有效期时才认为获取成功
2.3 基于ZooKeeper的实现
2.3.1 临时节点方案
public class ZkDistributedLock implements Watcher {
private ZooKeeper zk;
private String lockPath;
private String currentPath;
private String waitPath;
public boolean tryLock() throws Exception {
// 创建临时有序节点
currentPath = zk.create(lockPath + "/lock-", null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,