各位观众老爷们,大家好!今天咱来聊聊分布式锁这档子事儿。锁这玩意儿,单机玩得溜溜的,synchronized、ReentrantLock,哪个不是信手拈来? 可一旦到了分布式环境,就没那么简单了。想象一下,好几个服务器同时抢着修改同一条数据,那场面,简直比双十一零点抢购还激烈!
所以,今天就来掰扯掰扯MySQL自带的GET_LOCK()
和咱们常用的Redis分布式锁,看看它们各自的优缺点,以及在什么场景下该用哪个更合适。
开场白:锁,你这磨人的小妖精!
要说锁的重要性,那就好比高速公路上的收费站。没有收费站,大家一窝蜂往前冲,最后的结果就是堵成一锅粥。 锁的作用就是保证在同一时刻,只有一个线程(或者服务器节点)能够访问共享资源,避免数据错乱,保证数据一致性。
第一部分:MySQL的GET_LOCK()
:简单粗暴,但也够用!
先来说说MySQL自带的GET_LOCK()
函数。这玩意儿用起来非常简单,只需要执行一个SQL语句,就能尝试获取一个锁。
-
获取锁:
SELECT GET_LOCK('my_lock_name', 10);
这条语句的意思是:尝试获取名为
my_lock_name
的锁,最多等待10秒。如果获取成功,返回1;如果超时或者获取失败,返回0;如果遇到错误,返回NULL。 -
释放锁:
SELECT RELEASE_LOCK('my_lock_name');
这条语句释放名为
my_lock_name
的锁。释放成功返回1,如果锁不存在返回0,发生错误返回NULL。 -
查看锁是否被占用:
SELECT IS_LOCKED('my_lock_name');
这条语句检查名为
my_lock_name
的锁是否被占用。如果被占用返回1,未被占用返回0,发生错误返回NULL。
优点:
- 简单易用: 不需要引入额外的组件,直接使用SQL语句即可,学习成本低。
- 事务性:
GET_LOCK()
可以和MySQL的事务结合使用,保证锁的获取和释放与事务的完整性一致。 如果事务回滚,锁会自动释放。 - 无需额外依赖: 如果你的项目已经使用了MySQL,那么可以直接使用
GET_LOCK()
,不需要再引入Redis等其他组件,减少了系统的复杂度。
缺点:
- 性能问题:
GET_LOCK()
的性能相对较差。因为锁的信息存储在MySQL的内存中,锁的竞争会增加MySQL的负担。频繁的锁操作可能会影响MySQL的性能。 - 可靠性问题: 如果MySQL服务器宕机,那么所有持有的锁都会丢失。虽然可以通过主从复制来提高可靠性,但是主从切换期间仍然可能发生锁丢失的情况。
- 非阻塞:
GET_LOCK()
获取锁失败后只能选择等待或者放弃,不能像Redis那样实现更复杂的锁逻辑,比如可重入锁。 - 锁超时问题:
GET_LOCK()
依赖于客户端连接,如果客户端连接断开(比如程序崩溃),锁会自动释放。但如果客户端只是处理时间过长,超过了锁的超时时间,锁也会被释放,可能导致并发问题。 - 排查困难: 如果程序出现问题,需要排查锁的状态,使用MySQL的
SHOW OPEN TABLES
命令查看锁的信息比较麻烦。
代码示例:
import java.sql.*;
public class MySQLLockExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/testdb";
private static final String DB_USER = "root";
private static final String DB_PASSWORD = "password";
private static final String LOCK_NAME = "my_resource_lock";
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
if (acquireLock(conn, LOCK_NAME, 10)) {
try {
System.out.println("获取锁成功,开始执行业务逻辑...");
// 模拟业务逻辑执行
Thread.sleep(5000);
System.out.println("业务逻辑执行完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
releaseLock(conn, LOCK_NAME);
System.out.println("释放锁成功!");
}
} else {
System.out.println("获取锁失败!");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static boolean acquireLock(Connection conn, String lockName, int timeout) throws SQLException {
String sql = "SELECT GET_LOCK(?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, lockName);
pstmt.setInt(2, timeout);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1) == 1;
}
}
}
return false;
}
public static boolean releaseLock(Connection conn, String lockName) throws SQLException {
String sql = "SELECT RELEASE_LOCK(?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, lockName);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1) == 1;
}
}
}
return false;
}
}
适用场景:
- 对性能要求不高,并发量较低的场景: 例如,定时任务的调度,只需要保证同一时刻只有一个节点执行即可。
- 业务逻辑简单,不需要复杂的锁机制: 例如,简单的资源同步,只需要一个排他锁即可。
- 对可靠性要求不高,允许偶尔出现锁丢失的情况: 例如,一些容错性较高的业务,即使出现锁丢失,也不会造成严重的影响。
第二部分:Redis分布式锁:高性能,功能强大,但也复杂!
接下来,说说Redis分布式锁。这玩意儿是目前最常用的分布式锁解决方案之一。
实现原理:
利用Redis的SETNX
(Set If Not Exists)命令和EXPIRE
命令。
- 获取锁: 使用
SETNX
命令尝试设置一个key,如果key不存在,则设置成功,表示获取锁成功;如果key已存在,则设置失败,表示获取锁失败。 - 设置过期时间: 为了防止死锁,需要为锁设置一个过期时间。可以使用
EXPIRE
命令设置key的过期时间。 - 释放锁: 使用
DEL
命令删除key,表示释放锁。
优点:
- 高性能: Redis是基于内存的,读写速度非常快,适合高并发的场景。
- 高可用: Redis可以通过主从复制、Sentinel、Cluster等方式实现高可用,保证锁服务的稳定性。
- 功能丰富: Redis可以实现各种复杂的锁机制,例如可重入锁、公平锁等。
- 超时自动释放: Redis的key可以设置过期时间,即使客户端发生故障,锁也会自动释放,避免死锁。
缺点:
- 需要引入额外的组件: 需要部署和维护Redis集群,增加了系统的复杂度。
- 实现复杂: 实现一个可靠的Redis分布式锁需要考虑很多细节,例如锁的续期、防止误删锁等。
- 可能存在锁丢失的情况: 在极端情况下,Redis主从切换可能会导致锁丢失。 例如,客户端A在Master节点获取了锁,此时Master节点宕机,发生主从切换,客户端B在新的Master节点上也能获取到同样的锁,导致并发问题。 这就是Redlock试图解决的问题。
代码示例:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
public class RedisLockExample {
private static final String LOCK_NAME = "my_resource_lock";
private static final String LOCK_VALUE = "unique_lock_value"; // 使用UUID更安全
private static final int LOCK_EXPIRE_TIME = 30; // 单位:秒
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
try {
if (acquireLock(jedis, LOCK_NAME, LOCK_VALUE, LOCK_EXPIRE_TIME)) {
try {
System.out.println("获取锁成功,开始执行业务逻辑...");
// 模拟业务逻辑执行
Thread.sleep(5000);
System.out.println("业务逻辑执行完毕!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
releaseLock(jedis, LOCK_NAME, LOCK_VALUE);
System.out.println("释放锁成功!");
}
} else {
System.out.println("获取锁失败!");
}
} finally {
jedis.close();
}
}
public static boolean acquireLock(Jedis jedis, String lockName, String lockValue, int expireTime) {
SetParams setParams = new SetParams().nx().ex(expireTime);
String result = jedis.set(lockName, lockValue, setParams);
return "OK".equals(result);
}
public static boolean releaseLock(Jedis jedis, String lockName, String lockValue) {
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(lockName), Collections.singletonList(lockValue));
return "1".equals(result.toString());
}
}
注意点:
- 锁的续期: 为了防止锁过期,可以在获取锁之后,启动一个定时任务,定期刷新锁的过期时间。
- 防止误删锁: 释放锁的时候,需要验证当前线程是否持有该锁。可以使用UUID作为锁的值,释放锁的时候判断锁的值是否与当前线程的UUID一致。 上面的代码示例中,
releaseLock
方法使用了Lua脚本来原子性地判断锁的值和删除锁。 - Redlock: 为了提高锁的可靠性,可以使用Redlock算法。Redlock算法需要在多个独立的Redis节点上获取锁,只有当在超过半数的节点上获取锁成功,才认为获取锁成功。
适用场景:
- 对性能要求高,并发量高的场景: 例如,秒杀活动、抢购活动等。
- 需要复杂的锁机制: 例如,可重入锁、公平锁等。
- 对可靠性要求高: 例如,涉及到资金交易的业务。
第三部分:GET_LOCK()
vs Redis:一场公平的较量!
特性 | GET_LOCK() |
Redis分布式锁 |
---|---|---|
性能 | 较差 | 高 |
可靠性 | 较低 (MySQL宕机导致锁丢失) | 较高 (通过主从复制、Sentinel、Cluster) |
功能 | 简单 | 丰富 (可重入锁、公平锁等) |
实现复杂度 | 简单 | 复杂 |
依赖 | MySQL | Redis |
适用场景 | 并发量低、业务逻辑简单、可靠性要求不高的场景 | 并发量高、业务逻辑复杂、可靠性要求高的场景 |
锁超时处理 | 依赖客户端连接,可能因客户端长时间阻塞而提前释放 | 可设置过期时间,自动释放,避免死锁 |
事务支持 | 支持,可与MySQL事务结合 | 不直接支持,需要自行实现与事务的协调 |
锁续期 | 不支持,需自行实现 | 可通过定时任务续期 |
误删锁保护 | 不支持 | 可通过UUID等方式防止误删 |
排查难度 | 较高,需查看MySQL内部状态 | 较低,Redis命令可直接查看锁状态 |
总结:
GET_LOCK()
就像一把简单的机械锁,用起来方便,但是安全性一般。 Redis分布式锁就像一把智能锁,功能强大,安全性高,但是用起来也更复杂。
选择哪个取决于你的具体需求。 如果你的项目已经使用了MySQL,而且对性能要求不高,那么GET_LOCK()
是一个不错的选择。 如果你的项目需要高性能、高可用性,并且需要复杂的锁机制,那么Redis分布式锁是更好的选择。
最后的忠告:
无论选择哪种分布式锁方案,都要仔细考虑各种边界情况,做好充分的测试,确保锁的正确性和可靠性。 否则,锁不仅不能保护你的数据,反而会成为你的噩梦!
好了,今天的讲座就到这里。 感谢各位观众老爷的观看! 咱们下期再见! (挥手)