1. 基于数据库的分布式锁
实现原理
- 加锁:在数据库表中创建一个记录来表示锁,通常是使用
INSERT
或UPDATE
语句完成。可以创建一个锁表,并在表中使用唯一的ID
字段表示资源,锁被持有的标志可以使用时间戳或状态字段标记。- 方式 1:利用数据库的行锁(如
SELECT FOR UPDATE
)。客户端尝试获取锁时,会查询该资源并使用行锁来锁定行。 - 方式 2:使用唯一索引来防止重复插入。通过
INSERT INTO
操作,如果插入成功则表示加锁成功,若失败则说明锁已存在。
- 方式 1:利用数据库的行锁(如
- 释放锁:任务完成时,删除锁记录或更新锁状态字段。
场景
- 适用于少量并发访问的场景。
- 常见于简单的分布式系统中,因为数据库操作对开发人员来说较为熟悉,并且可以满足基本锁需求。
优缺点
- 优点:
- 实现简单:无需引入额外组件,数据库在分布式系统中是常用的组件。
- 强一致性:大多数关系数据库提供强一致性,确保锁的可靠性。
- 缺点:
- 性能瓶颈:数据库锁的性能较差,特别在高并发场景中,事务开销大,扩展性有限。
- 死锁风险:锁释放异常或未及时释放时,可能造成资源的死锁。
代码示例
假设使用 MySQL 数据库来实现锁。可以创建一张锁表,每当需要加锁时,往该表插入数据,并利用唯一索引确保只有一个客户端能加锁成功。
数据库表结构
CREATE TABLE distributed_lock (
lock_key VARCHAR(255) PRIMARY KEY,
lock_value VARCHAR(255),
expire_time TIMESTAMP
);
Java 代码示例
使用 JDBC 进行数据库连接和操作。
import java.sql.*;
import java.time.LocalDateTime;
public class DatabaseDistributedLock {
private static final String LOCK_KEY = "my_lock";
private Connection connection;
public DatabaseDistributedLock(Connection connection) {
this.connection = connection;
}
public boolean acquireLock(String lockValue, int expireSeconds) throws SQLException {
String sql = "INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES (?, ?, ?) " +
"ON DUPLICATE KEY UPDATE lock_value = IF(expire_time < NOW(), ?, lock_value), " +
"expire_time = IF(expire_time < NOW(), ?, expire_time)";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, LOCK_KEY);
statement.setString(2, lockValue);
statement.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now().plusSeconds(expireSeconds)));
statement.setString(4, lockValue);
statement.setTimestamp(5, Timestamp.valueOf(LocalDateTime.now().plusSeconds(expireSeconds)));
return statement.executeUpdate() > 0;
}
}
public void releaseLock(String lockValue) throws SQLException {
String sql = "DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, LOCK_KEY);
statement.setString(2, lockValue);
statement.executeUpdate();
}
}
}
2. 基于 Redis 的分布式锁
Redis 具有单线程的特点,确保了操作的原子性,同时提供了丰富的命令和高性能,因此在分布式锁实现中广泛使用。
实现原理
- 加锁:使用 Redis 提供的
SETNX
命令(SET if Not Exists),可以原子地设置锁键及其过期时间,如SET key value NX EX seconds
,其中NX
表示仅当键不存在时才设置,EX
表示过期时间。 - 锁续期:如果任务超出预期时间未完成,可以通过守护进程定期延长锁的有效期,以避免锁被误释放。
- 释放锁:任务完成后,确保只由持有锁的客户端执行删除操作。通常使用 Lua 脚本保证操作原子性,比如对比当前锁值后再删除。
- Redlock 算法:为高可用性,Redis 分布式锁常使用 Redlock 算法,通过在多个 Redis 实例上分别加锁来提高锁的可靠性和容错能力。
场景
- 适合对并发和性能有较高要求的分布式系统。
- 用于较短时间的锁场景,如防止数据重复提交、限流、库存管理等。
优缺点
- 优点:
- 高性能:Redis 的内存操作速度快,能承受高并发请求。
- 支持过期自动释放:锁自动过期释放,避免死锁。
- 分布式支持:通过 Redlock 算法在多个 Redis 实例间实现高可用锁。
- 缺点:
- 锁误释放风险:如果客户端长时间持有锁,可能在任务未完成时锁过期被其他客户端获取。
- 数据一致性问题:Redis 默认是主从架构,可能存在主从数据同步延迟的问题。
代码示例
Redis 实现分布式锁的核心是 SETNX
命令。使用过期时间来确保锁在任务异常时能够自动释放。
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Collections;
import java.util.UUID;
@Component
public class RedisDistributedLock {
private final RedisTemplate<String, String> redisTemplate;
private static final String LOCK_KEY = "my_lock"; // 锁的键名
private static final Duration LOCK_EXPIRATION = Duration.ofSeconds(30); // 锁过期时间
public RedisDistributedLock(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 尝试获取锁
* @return 返回获取到的锁的唯一值,用于释放锁时进行校验
*/
public String acquireLock() {
String lockValue = UUID.randomUUID().toString(); // 唯一标识当前锁的值
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, lockValue, LOCK_EXPIRATION); // 尝试加锁,并设置过期时间
return Boolean.TRUE.equals(success) ? lockValue : null;
}
/**
* 释放锁
* @param lockValue 当前线程持有的锁的值
* @return 是否成功释放锁
*/
public boolean releaseLock(String lockValue) {
// 使用 Lua 脚本,保证解锁过程的原子性
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(LOCK_KEY), lockValue);
return result != null && result > 0;
}
}
说明
acquireLock
方法使用SETNX
和EX
实现加锁。releaseLock
方法通过 Lua 脚本确保只释放当前持有的锁。
3. 基于 ZooKeeper 的分布式锁
ZooKeeper 是一个分布式协调服务,通过顺序节点和会话超时机制可以提供可靠的分布式锁。
实现原理
- 加锁:客户端尝试在指定目录下创建一个临时顺序节点,每次加锁请求时,ZooKeeper 自动生成一个递增的序号,所有客户端都获取该目录下的节点列表。创建最小序号的节点即可获得锁,其他节点则监听比自己小的节点的删除事件。
- 释放锁:持有锁的客户端完成任务后删除自己创建的节点。监听到锁释放事件的客户端可以重新获取锁。
- 会话超时:ZooKeeper 提供会话超时机制,当客户端失联后,ZooKeeper 自动删除该客户端的临时节点,确保锁自动释放。
场景
- 适用于一致性要求高、分布式事务较多的场景,如金融系统和分布式数据库。
- 特别适合长时间锁的场景,ZooKeeper 的会话机制可保证锁在意外断开时自动释放。
优缺点
- 优点:
- 高可靠性:ZooKeeper 天然支持分布式一致性,适合多节点环境。
- 自动释放:通过会话机制避免死锁风险。
- 锁可见性:可以轻松实现分布式队列、选主等功能。
- 缺点:
- 较高延迟:ZooKeeper 使用磁盘保存节点信息,相较 Redis 性能略低。
- 复杂性较高:需要专门运维 ZooKeeper 集群,同时客户端与 ZooKeeper 需要保持心跳,增加了复杂度。
代码示例
ZooKeeper 使用临时节点实现锁,每个客户端创建临时顺序节点,通过判断最小节点来决定锁的持有者。
使用 Curator 框架:
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
public class ZookeeperDistributedLock {
private final InterProcessMutex lock;
private static final String LOCK_PATH = "/distributed_lock/my_lock";
public ZookeeperDistributedLock(CuratorFramework client) {
this.lock = new InterProcessMutex(client, LOCK_PATH);
}
public boolean acquireLock() {
try {
lock.acquire();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public void releaseLock() {
try {
if (lock.isAcquiredInThisProcess()) {
lock.release();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
说明
- Curator 库提供了
InterProcessMutex
,简化了分布式锁的实现。 acquireLock
和releaseLock
方法分别用于加锁和释放锁,通过会话超时机制实现锁自动释放。
对比总结
实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
数据库 | 简单实现,适用小规模系统 | 性能较低,易造成死锁 | 低频、小规模分布式锁需求 |
Redis | 高性能、自动过期、支持 Redlock 算法 | 锁误释放风险,数据一致性受主从架构影响 | 高并发、高性能场景 |
ZooKeeper | 高可靠性、自动释放,支持选主和分布式队列 | 性能略低,复杂性高 | 长时间锁和一致性要求高场景 |