常见的分布式锁-爱代码爱编程
使用MySQL的行级锁机制来实现分布式锁。 设计过程如下:
1.创建一张用于存储锁信息的表
,包含以下字段:锁的名称、锁的持有者、锁的过期时间等。
2.当需要获取锁
时,向该表插入一条记录,如果插入成功,则表示获取到了锁。
3.当需要释放锁
时,根据锁的名称和持有者信息,删除该表中的相应记录,释放锁。
1 创建数据库表
首先,在MySQL数据库中创建一个表用于存储锁的状态。假设有一个表 distributed_lock
,包含以下字段:
id
:锁的唯一标识,可以是自增主键或全局唯一标识符(GUID)。resource_key
:锁的资源标识符,用于唯一标识需要加锁的资源。owner
:锁的拥有者,表示当前持有锁的客户端。created_time
:锁的创建时间,表示锁的获取时间。expiration_time
:锁的过期时间,表示锁的有效期。
CREATE TABLE lock (
id INT PRIMARY KEY AUTO_INCREMENT, -- 锁的唯一标识
resource_key VARCHAR(255) NOT NULL, -- 锁的资源标识符
owner VARCHAR(255) NOT NULL, -- 锁的拥有者
created_time DATETIME NOT NULL, -- 锁的创建时间
expiration_time DATETIME NOT NULL -- 锁的过期时间
);
2 获取锁
在Java代码中,获取锁的过程可以通过执行MySQL的插入操作来实现。考虑到并发情况下可能会有多个客户端同时尝试获取锁,可以通过以下代码来实现:
import java.sql.*;
public class DistributedLock {
private static final String DB_URL = "jdbc:mysql://localhost:3306/my_db";
private static final String DB_USER = "root";
private static final String DB_PASSWORD = "password";
public boolean acquireLock(String resourceKey, int timeout) {
boolean lockAcquired = false;
Connection conn = null;
try {
// 创建数据库连接
conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
// 开始事务
conn.setAutoCommit(false);
// 查询是否已经有其他客户端持有锁
String selectSql = "SELECT * FROM distributed_lock WHERE resource_key = ? FOR UPDATE";
PreparedStatement selectStmt = conn.prepareStatement(selectSql);
selectStmt.setString(1, resourceKey);
ResultSet rs = selectStmt.executeQuery();
if (!rs.next()) {
// 如果没有其他客户端持有锁,则插入新的锁记录
String insertSql = "INSERT INTO distributed_lock (resource_key, owner, created_time, expiration_time) VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? SECOND))";
PreparedStatement insertStmt = conn.prepareStatement(insertSql);
insertStmt.setString(1, resourceKey);
insertStmt.setString(2, Thread.currentThread().getName());
insertStmt.setInt(3, timeout);
insertStmt.executeUpdate();
// 提交事务
conn.commit();
lockAcquired = true;
}
rs.close();
selectStmt.close();
} catch (SQLException e) {
// 处理异常
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 关闭数据库连接
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return lockAcquired;
}
}
在上述代码中,acquireLock
方法接收两个参数:resourceKey
代表需要加锁的资源标识符,timeout
代表获取锁的超时时间(以秒为单位)。
首先,代码创建数据库连接,然后开始事务。接下来,通过执行 SELECT 语句查询是否已经有其他客户端持有锁。如果没有其他客户端持有锁,则执行 INSERT 语句插入新的锁记录,并提交事务。最后,关闭数据库连接,并返回获取锁的结果。
3 释放锁
获取锁成功后,需要在使用完资源后释放锁。释放锁的过程可以通过执行 MySQL 的删除操作来实现。以下是释放锁的代码:
public class DistributedLock {
// ...
public void releaseLock(String resourceKey) {
Connection conn = null;
try {
// 创建数据库连接
conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
// 开始事务
conn.setAutoCommit(false);
// 删除锁记录
String deleteSql = "DELETE FROM distributed_lock WHERE resource_key = ?";
PreparedStatement deleteStmt = conn.prepareStatement(deleteSql);
deleteStmt.setString(1, resourceKey);
deleteStmt.executeUpdate();
// 提交事务
conn.commit();
deleteStmt.close();
} catch (SQLException e) {
// 处理异常
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 关闭数据库连接
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
在上述代码中,releaseLock
方法接收一个参数 resourceKey
,代表需要释放的锁的资源标识符。
代码创建数据库连接,然后开始事务。接下来,通过执行 DELETE 语句删除锁记录,并提交事务。最后,关闭数据库连接。
4 应用场景:
一个典型的应用场景是分布式系统中的任务调度。假设有多个任务调度节点,每个节点同时只能执行一个任务,当有多个节点同时尝试执行任务时,需要使用分布式锁来保证同一时间只有一个节点执行任务。
示例代码:
public class TaskScheduler {
private static final String TASK_RESOURCE_KEY = "task_resource";
private static final int LOCK_TIMEOUT = 10; // 10秒
public void scheduleTask() {
DistributedLock lock = new DistributedLock();
boolean lockAcquired = lock.acquireLock(TASK_RESOURCE_KEY, LOCK_TIMEOUT);
if (lockAcquired) {
try {
// 执行任务
System.out.println("Task scheduled by " + Thread.currentThread().getName());
Thread.sleep(5000); // 模拟任务执行时间
// 释放锁
lock.releaseLock(TASK_RESOURCE_KEY);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TaskScheduler scheduler = new TaskScheduler();
// 创建多个线程模拟任务调度节点
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(scheduler::scheduleTask);
thread.start();
}
}
}
在上述代码中,TaskScheduler
类负责任务调度。首先,创建一个 DistributedLock
对象,然后调用 acquireLock
方法尝试获取锁。如果获取锁成功,则执行任务,并在任务执行完毕后调用 releaseLock
方法释放锁。
在 main
方法中,创建多个线程模拟任务调度节点。每个线程调用 scheduleTask
方法来尝试执行任务。
5 MySQL实现分布式锁的优缺点
数据库实现分布式锁的优点
是简单易用,可以实现互斥访问共享资源。但是在高并发、高性能的场景中,数据库实现分布式锁的效率不高,会增加数据库的开销。另外,数据库实现分布式锁还有以下缺点
:
- 单点故障:如果使用单个数据库实现分布式锁,那么数据库成为了单点故障的风险。如果数据库出现故障,那么所有使用该锁的应用都无法正常运行。
- 阻塞:当有多个请求同时争夺锁时,只有一个请求能够成功获取到锁,其他请求需要等待。这样会导致请求被阻塞,延长了响应时间。
- 容错性差:数据库实现分布式锁只能在一个数据库中实现,如果该数据库发生故障或者网络中断,那么整个分布式锁功能就无法正常工作。
为了解决数据库实现分布式锁的缺点,可以考虑使用其他工具或技术,如分布式缓存(如Redis)、ZooKeeper等来实现分布式锁。这些工具或技术可以提供更高效、更可靠的分布式锁实现方式,适用于高并发、高性能的场景。