分布式系统中的分布式锁实现方式
2023-02-252.4k 阅读
分布式锁的重要性
在分布式系统中,多个节点可能会同时尝试访问和修改共享资源。例如,在一个电商系统中,库存是一个共享资源,多个订单处理服务可能同时尝试减少库存。如果没有适当的控制,可能会出现超卖等问题。分布式锁就是为了解决这类问题而引入的机制,它确保在分布式环境下,同一时间只有一个节点能够访问共享资源,就如同在单机环境中使用普通锁来保护临界区一样。
基于数据库的分布式锁实现方式
数据库表方式
- 原理: 通过在数据库中创建一张表,表中包含锁的标识(如业务相关的 key)、持有锁的节点信息、锁的过期时间等字段。当一个节点想要获取锁时,向表中插入一条记录,如果插入成功,说明获取锁成功;如果插入失败,说明锁已被其他节点持有。例如,假设有一个电商系统,不同的订单处理服务分布在多个节点上,要对库存进行操作,就可以使用这种方式来获取锁。当某个订单处理节点尝试插入与库存操作相关的锁记录时,若成功则可以进行库存减少操作,否则等待。
- 示例代码(以 MySQL 和 Java 为例):
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DatabaseLock {
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
public boolean tryLock(String lockKey, String clientId, long expireTime) {
try (Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
String insertSql = "INSERT INTO distributed_locks (lock_key, client_id, expire_time) " +
"VALUES (?,?,?) ON DUPLICATE KEY UPDATE client_id = VALUES(client_id), expire_time = VALUES(expire_time)";
try (PreparedStatement insertStmt = connection.prepareStatement(insertSql)) {
insertStmt.setString(1, lockKey);
insertStmt.setString(2, clientId);
insertStmt.setLong(3, expireTime);
int rowsAffected = insertStmt.executeUpdate();
return rowsAffected == 1;
}
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public void unlock(String lockKey, String clientId) {
try (Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
String deleteSql = "DELETE FROM distributed_locks WHERE lock_key =? AND client_id =?";
try (PreparedStatement deleteStmt = connection.prepareStatement(deleteSql)) {
deleteStmt.setString(1, lockKey);
deleteStmt.setString(2, clientId);
deleteStmt.executeUpdate();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public boolean isLocked(String lockKey) {
try (Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
String selectSql = "SELECT COUNT(*) FROM distributed_locks WHERE lock_key =?";
try (PreparedStatement selectStmt = connection.prepareStatement(selectSql)) {
selectStmt.setString(1, lockKey);
try (ResultSet resultSet = selectStmt.executeQuery()) {
if (resultSet.next()) {
return resultSet.getInt(1) > 0;
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
}
- 优缺点:
- 优点:实现简单,对技术栈要求不高,只要会操作数据库就能实现。而且数据库本身有持久化功能,即使系统重启,锁的状态也能保留。
- 缺点:性能较低,每次获取和释放锁都需要进行数据库的读写操作,在高并发场景下,数据库可能成为性能瓶颈。同时,锁的可靠性依赖于数据库的可用性,如果数据库出现故障,锁机制就会失效。另外,由于数据库操作的原子性基于事务,在某些情况下,事务的隔离级别可能会影响锁的正确性。
排他锁方式
- 原理:
利用数据库的排他锁(如 MySQL 中的
SELECT... FOR UPDATE
语句)来实现分布式锁。当一个节点执行SELECT... FOR UPDATE
语句时,数据库会对满足条件的记录加上排他锁,其他节点如果再执行相同条件的SELECT... FOR UPDATE
语句,就会被阻塞,直到持有锁的节点提交事务释放锁。例如,在一个分布式文件系统中,不同的节点可能同时尝试修改某个文件的元数据,通过这种方式可以确保同一时间只有一个节点能进行修改操作。 - 示例代码(以 MySQL 和 Java 为例):
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DatabaseExclusiveLock {
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
public boolean tryLock(String lockKey, String clientId) {
Connection connection = null;
try {
connection = DriverManager.getConnection(URL, USER, PASSWORD);
connection.setAutoCommit(false);
String selectSql = "SELECT id FROM distributed_locks WHERE lock_key =? FOR UPDATE";
try (PreparedStatement selectStmt = connection.prepareStatement(selectSql)) {
selectStmt.setString(1, lockKey);
try (ResultSet resultSet = selectStmt.executeQuery()) {
if (resultSet.next()) {
// 锁已被持有,可根据情况处理,这里简单返回失败
return false;
} else {
// 插入锁记录
String insertSql = "INSERT INTO distributed_locks (lock_key, client_id) VALUES (?,?)";
try (PreparedStatement insertStmt = connection.prepareStatement(insertSql)) {
insertStmt.setString(1, lockKey);
insertStmt.setString(2, clientId);
insertStmt.executeUpdate();
connection.commit();
return true;
}
}
}
}
} catch (SQLException e) {
if (connection!= null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
return false;
} finally {
if (connection!= null) {
try {
connection.setAutoCommit(true);
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
public void unlock(String lockKey, String clientId) {
Connection connection = null;
try {
connection = DriverManager.getConnection(URL, USER, PASSWORD);
connection.setAutoCommit(false);
String deleteSql = "DELETE FROM distributed_locks WHERE lock_key =? AND client_id =?";
try (PreparedStatement deleteStmt = connection.prepareStatement(deleteSql)) {
deleteStmt.setString(1, lockKey);
deleteStmt.setString(2, clientId);
deleteStmt.executeUpdate();
connection.commit();
}
} catch (SQLException e) {
if (connection!= null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
if (connection!= null) {
try {
connection.setAutoCommit(true);
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
- 优缺点:
- 优点:相比数据库表方式,在并发性能上有所提升,因为只需要一次数据库操作(先查询加锁,再插入记录)。同时,利用数据库的事务机制,保证了锁操作的原子性和一致性。
- 缺点:依然依赖数据库,数据库故障会影响锁的可用性。并且长时间持有排他锁可能会导致其他节点长时间等待,容易引发死锁问题。另外,在高并发场景下,数据库的压力依然较大。
基于缓存的分布式锁实现方式
Redis 实现分布式锁
- 原理:
Redis 是一个高性能的键值对存储系统,支持原子操作。利用 Redis 的
SETNX
(SET if Not eXists)命令可以实现分布式锁。SETNX key value
命令会在键key
不存在时,为键设置指定的值value
,并返回 1,表示设置成功;如果键key
已经存在,则不做任何操作,返回 0,表示设置失败。在分布式系统中,不同节点尝试使用SETNX
命令设置同一个键,如果某个节点设置成功,就表示该节点获取到了锁。为了防止死锁,通常会给锁设置一个过期时间。例如,在一个分布式任务调度系统中,不同的节点可能同时尝试获取任务执行权,就可以使用 Redis 锁来保证同一任务同一时间只有一个节点执行。 - 示例代码(以 Java 和 Jedis 为例):
import redis.clients.jedis.Jedis;
public class RedisLock {
private static final String LOCK_KEY = "your_lock_key";
private static final String LOCK_VALUE = "your_lock_value";
private static final int EXPIRE_TIME = 10; // 锁过期时间,单位秒
public boolean tryLock(Jedis jedis) {
Long result = jedis.setnx(LOCK_KEY, LOCK_VALUE);
if (result == 1) {
// 设置成功,获取到锁,设置过期时间
jedis.expire(LOCK_KEY, EXPIRE_TIME);
return true;
}
return false;
}
public void unlock(Jedis jedis) {
if (jedis.exists(LOCK_KEY)) {
jedis.del(LOCK_KEY);
}
}
}
- 优缺点:
- 优点:性能高,Redis 是基于内存的,操作速度非常快,能够支持高并发场景。并且实现相对简单,利用 Redis 的基本命令即可完成。
- 缺点:存在锁丢失的风险。例如,在设置锁成功后,还未来得及设置过期时间时,节点发生故障,锁就会一直存在,其他节点无法获取。另外,如果 Redis 采用主从复制架构,在主节点设置锁后,还未同步到从节点时主节点发生故障,从节点晋升为主节点,新的主节点没有该锁记录,其他节点就可能再次获取到锁,导致锁的安全性问题。
Redisson 实现分布式锁
- 原理: Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它对 Redis 的分布式锁进行了更完善的封装和扩展。Redisson 的分布式锁采用了 Lua 脚本保证操作的原子性,并且支持可重入锁、公平锁等特性。在获取锁时,Redisson 会向 Redis 发送 Lua 脚本,脚本中包含了复杂的锁获取逻辑,如检查锁是否存在、设置锁的持有时间、处理可重入等。例如,在一个分布式微服务系统中,不同的微服务可能需要调用共享资源,Redisson 可以提供可靠的分布式锁机制。
- 示例代码(以 Java 和 Redisson 为例):
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class RedissonLockExample {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
RLock lock = redissonClient.getLock("your_lock_key");
try {
boolean isLocked = lock.tryLock();
if (isLocked) {
// 获取到锁,执行业务逻辑
System.out.println("Got the lock, doing business...");
} else {
System.out.println("Failed to get the lock.");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
redissonClient.shutdown();
}
}
}
- 优缺点:
- 优点:功能强大,支持多种类型的锁,如可重入锁、公平锁等,满足不同业务场景需求。同时,通过 Lua 脚本保证了操作的原子性,提高了锁的可靠性,解决了 Redis 原生实现中可能出现的锁丢失等问题。
- 缺点:引入了 Redisson 框架,增加了项目的依赖和复杂度。并且 Redisson 的性能在一定程度上依赖于 Redis 的性能,虽然 Redis 本身性能较高,但在极端高并发场景下,依然可能存在性能瓶颈。
基于 ZooKeeper 的分布式锁实现方式
- 原理: ZooKeeper 是一个分布式协调服务,它基于树形结构存储数据。利用 ZooKeeper 的临时顺序节点特性可以实现分布式锁。当一个节点想要获取锁时,在 ZooKeeper 的某个指定目录下创建一个临时顺序节点。所有节点创建的临时顺序节点按照创建顺序编号,编号最小的节点表示获取到了锁。如果当前节点没有获取到锁,就监听比它编号小的前一个节点,当前一个节点删除时(即释放锁),ZooKeeper 会通知监听的节点,该节点就可以尝试获取锁。例如,在一个分布式数据同步系统中,不同的节点需要按照顺序同步数据,就可以使用这种锁机制来保证同步的有序性。
- 示例代码(以 Java 和 Curator 为例):
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class ZooKeeperLockExample {
private static final String ZK_SERVERS = "127.0.0.1:2181";
private static final String LOCK_PATH = "/your_lock_path";
public static void main(String[] args) {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(ZK_SERVERS)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH);
try {
boolean acquired = lock.acquire(10, java.util.concurrent.TimeUnit.SECONDS);
if (acquired) {
// 获取到锁,执行业务逻辑
System.out.println("Got the lock, doing business...");
} else {
System.out.println("Failed to get the lock.");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (lock.isAcquiredInThisProcess()) {
lock.release();
}
} catch (Exception e) {
e.printStackTrace();
}
client.close();
}
}
}
- 优缺点:
- 优点:可靠性高,ZooKeeper 采用了集群模式,通过选举产生 leader 节点,即使部分节点故障,依然能够保证服务的可用性。并且由于采用临时顺序节点,锁的获取和释放具有顺序性,适合一些对顺序有要求的场景。
- 缺点:性能相对较低,ZooKeeper 的写操作(如创建和删除节点)会涉及到集群间的同步,在高并发场景下,性能不如基于 Redis 的分布式锁。同时,ZooKeeper 的部署和维护相对复杂,需要考虑集群的搭建、节点的选举等问题。
分布式锁实现方式的对比与选择
- 性能对比: 基于缓存(如 Redis)的分布式锁性能最高,因为 Redis 基于内存操作,速度极快,能够满足高并发场景的需求。基于数据库的分布式锁性能较低,尤其是在高并发下,数据库的读写操作容易成为瓶颈。ZooKeeper 由于写操作需要集群同步,性能介于两者之间,但在高并发写场景下不如 Redis。
- 可靠性对比: ZooKeeper 的可靠性较高,其集群模式和节点选举机制保证了即使部分节点故障,服务依然可用。Redis 如果采用主从复制架构,可能存在锁丢失的风险,不过通过 Redisson 等框架可以在一定程度上提高可靠性。基于数据库的分布式锁可靠性依赖于数据库的可用性,数据库故障会导致锁机制失效。
- 功能特性对比: Redisson 对 Redis 进行封装后,支持多种类型的锁,如可重入锁、公平锁等,功能丰富。ZooKeeper 基于临时顺序节点,天然支持顺序锁等特性。基于数据库的分布式锁功能相对单一,主要实现基本的锁功能。
- 选择建议:
- 如果系统对性能要求极高,对锁的功能要求相对简单,且能够接受一定的锁丢失风险,可以选择基于 Redis 原生实现的分布式锁。
- 如果系统对锁的功能要求丰富,如需要可重入锁、公平锁等,且对可靠性有一定要求,可以选择 Redisson 实现的分布式锁。
- 如果系统对可靠性要求极高,对顺序有要求,如分布式任务调度需要按照顺序执行任务,且对性能要求不是极端高,可以选择基于 ZooKeeper 的分布式锁。
- 如果系统技术栈简单,对性能要求不是特别高,且对数据库的依赖度较高,可以选择基于数据库的分布式锁。
在实际应用中,还需要根据系统的具体业务场景、性能需求、可靠性要求等多方面因素综合考虑,选择最适合的分布式锁实现方式。同时,无论选择哪种方式,都需要对可能出现的异常情况进行充分的考虑和处理,以确保分布式系统的稳定性和数据的一致性。