在分布式系统和并发编程中,锁是保证数据一致性的关键工具。而基于 mysql 实现的可重入锁,不仅能满足跨进程的互斥需求,还能支持同一个线程多次获取锁而不阻塞。记录一下我对 mysql 实现可重入锁的思考:如何用 mysql 实现可重入锁?为什么实现过程中必须依赖事务?希望能解决你的一些疑惑。
一、先搞懂:什么是可重入锁?
可重入锁(也叫递归锁)的核心特性是:同一个线程可以多次获取同一把锁,不会因为自己持有锁而发生死锁。
举个例子:
- 线程 a 先获取锁,执行业务逻辑;
- 业务逻辑中又调用了另一个需要同一把锁的方法;
- 线程 a 可以再次成功获取锁,而不会被阻塞;
- 只有当线程 a 释放锁的次数等于获取锁的次数时,锁才会真正被释放。
在 java 里 reentrantlock 就是典型的可重入锁,而我们要做的,是用 mysql 模拟出类似的效果。
二、mysql 可重入锁的基础:锁表设计
要实现可重入锁,我们需要一张表来记录锁的持有状态、持有者和重入次数:
create table `lock_table` ( `id` int auto_increment primary key, -- 锁的唯一标识 `lock_name` varchar(255) not null, -- 持有锁的线程标识 `holder_thread` varchar(255), -- 锁的重入次数,用于实现可重入性 `reentry_count` int default 0 );
这张表的核心字段:
lock_name:锁的唯一标识,不同业务用不同的锁名隔离。holder_thread:标记当前哪个线程持有这把锁。reentry_count:记录锁被同一个线程重入的次数,是实现可重入特性的关键。
三、核心实现:加锁与解锁逻辑
1. 加锁流程
1. 开启事务 2. 执行 sql: select holder_thread, reentry_count from lock_table where lock_name = ? for update; - 若记录不存在:执行 insert into lock_table (lock_name, holder_thread, reentry_count) values (?, ?, 1) - 若记录存在且持有者是当前线程:执行 update lock_table set reentry_count = reentry_count + 1 where lock_name = ? 3. 提交事务
2. 解锁流程
1. 开启事务 2. 执行 sql: select holder_thread, reentry_count from lock_table where lock_name = ? for update; - 若记录存在、持有者是当前线程且重入次数 > 1:执行 update lock_table set reentry_count = reentry_count - 1 where lock_name = ? - 若记录存在、持有者是当前线程且重入次数 ≤ 1:执行 delete from lock_table where lock_name = ? 3. 提交事务
四、灵魂拷问:为什么必须加事务?
很多同学会疑惑:“我不加事务,直接执行 sql 不行吗?” 答案是:不行。事务是 mysql 可重入锁的灵魂,没有事务,锁的特性会直接失效。我们从三个维度拆解原因:
1. 控制锁的生命周期:避免锁提前释放
mysql innodb 默认 autocommit=1,单条 sql 执行完毕后会自动提交事务。
如果不加事务:
- 执行
select ... for update时,会对目标记录加排他行锁; - 这条 sql 执行完后,锁会被自动释放;
- 后续的
insert/update操作变成了全新的请求,需要重新竞争锁。
这会导致:
- 锁根本没有被 “持有”,其他线程可以在两次操作之间抢占锁;
- 可重入特性直接失效,同一个线程第二次获取锁时可能被阻塞。
而事务的作用就是:在事务提交前,锁不会被释放。从查询锁状态到完成写入操作,整个过程中锁都被当前事务持有,保证了互斥性和可重入性。
2. 保证操作原子性:避免并发数据错乱
加锁 / 解锁的核心逻辑是「查询 → 判断 → 写入」,这三步必须是原子操作,否则在高并发场景下会出现数据错乱。
举个并发场景:
- 线程 a 和线程 b 同时请求同一把锁;
- 线程 a 执行
select ... for update,发现记录不存在,准备insert; - 线程 b 在 a 还没
insert之前,也执行select ... for update,同样发现记录不存在; - 两个线程都去
insert,要么触发唯一键冲突,要么都插入成功,导致锁被同时持有。
事务的原子性可以完美解决这个问题:
- 把「查询 → 判断 → 写入」封装成一个不可分割的单元;
- 要么全部成功,要么全部失败;
- 只有当前事务提交后,其他线程才能看到锁的状态变化,避免了竞态条件。
3. 确保可重入正确性:重入次数的安全更新
可重入锁的核心是维护 reentry_count 字段:
- 加锁时:如果是当前线程持有锁,
reentry_count + 1; - 解锁时:如果是当前线程持有锁,
reentry_count - 1,次数为 0 时删除锁记录。
如果不加事务:
- 线程 a 查询到
reentry_count = 1,准备执行+1; - 线程 b 可能在此时修改了
reentry_count,导致更新后的数据错误; - 最终锁的状态混乱,甚至出现锁无法释放的情况。
事务的隔离性保证了:
- 在当前事务更新
reentry_count时,其他线程无法修改这条记录; - 只有事务提交后,新的重入次数才会被持久化,其他线程才能感知到。
五、完整示例:
我们用伪代码把加锁和解锁流程串起来,更直观地感受事务的作用:
加锁伪代码
public boolean lock(string lockname, string threadname) {
connection conn = getconnection();
try {
// 1. 开启事务
conn.setautocommit(false);
// 2. 查询锁状态(加排他锁)
string querysql = "select holder_thread, reentry_count from lock_table where lock_name = ? for update";
try (preparedstatement ps = conn.preparestatement(querysql)) {
ps.setstring(1, lockname);
resultset rs = ps.executequery();
if (rs.next()) {
string holder = rs.getstring("holder_thread");
int count = rs.getint("reentry_count");
if (threadname.equals(holder)) {
// 可重入:重入次数+1
string updatesql = "update lock_table set reentry_count = reentry_count + 1 where lock_name = ?";
try (preparedstatement updateps = conn.preparestatement(updatesql)) {
updateps.setstring(1, lockname);
updateps.executeupdate();
}
} else {
// 被其他线程持有,获取锁失败
conn.rollback();
return false;
}
} else {
// 锁不存在,直接加锁
string insertsql = "insert into lock_table (lock_name, holder_thread, reentry_count) values (?, ?, 1)";
try (preparedstatement insertps = conn.preparestatement(insertsql)) {
insertps.setstring(1, lockname);
insertps.setstring(2, threadname);
insertps.executeupdate();
}
}
}
// 3. 提交事务
conn.commit();
return true;
} catch (exception e) {
// 异常回滚
conn.rollback();
return false;
} finally {
conn.setautocommit(true);
closeconnection(conn);
}
}解锁伪代码
public boolean unlock(string lockname, string threadname) {
connection conn = getconnection();
try {
// 1. 开启事务
conn.setautocommit(false);
// 2. 查询锁状态
string querysql = "select holder_thread, reentry_count from lock_table where lock_name = ? for update";
try (preparedstatement ps = conn.preparestatement(querysql)) {
ps.setstring(1, lockname);
resultset rs = ps.executequery();
if (rs.next()) {
string holder = rs.getstring("holder_thread");
int count = rs.getint("reentry_count");
if (!threadname.equals(holder)) {
// 不是锁持有者,解锁失败
conn.rollback();
return false;
}
if (count > 1) {
// 重入次数>1,仅减1
string updatesql = "update lock_table set reentry_count = reentry_count - 1 where lock_name = ?";
try (preparedstatement updateps = conn.preparestatement(updatesql)) {
updateps.setstring(1, lockname);
updateps.executeupdate();
}
} else {
// 重入次数=1,删除锁记录
string deletesql = "delete from lock_table where lock_name = ?";
try (preparedstatement deleteps = conn.preparestatement(deletesql)) {
deleteps.setstring(1, lockname);
deleteps.executeupdate();
}
}
} else {
// 锁不存在,解锁失败
conn.rollback();
return false;
}
}
// 3. 提交事务
conn.commit();
return true;
} catch (exception e) {
conn.rollback();
return false;
} finally {
conn.setautocommit(true);
closeconnection(conn);
}
}六、总结
用 mysql 实现可重入锁,本质是用数据库表存储锁状态,用事务保证锁的互斥性、原子性和生命周期。
事务的核心作用可以概括为三点:
- 锁生命周期管理:事务提交前,锁不会释放,保证当前线程持续持有锁。
- 原子性保障:将「查询 → 判断 → 写入」封装为原子操作,避免并发数据错乱。
- 可重入正确性:安全维护重入次数,确保同一个线程可以多次获取 / 释放锁。
这种实现方式虽然不如 redis 等分布式锁框架高效,但胜在简单可靠,适合对性能要求不高、需要强一致性的场景,也能帮我们更好地理解事务和锁的本质。
以上就是mysql实现可重入锁的实践指南的详细内容,更多关于mysql可重入锁实现的资料请关注代码网其它相关文章!
发表评论