MySQL實現(xiàn)可重入鎖的實踐指南
在分布式系統(tǒng)和并發(fā)編程中,鎖是保證數(shù)據(jù)一致性的關(guān)鍵工具。而基于 MySQL 實現(xiàn)的可重入鎖,不僅能滿足跨進程的互斥需求,還能支持同一個線程多次獲取鎖而不阻塞。記錄一下我對 MySQL 實現(xiàn)可重入鎖的思考:如何用 MySQL 實現(xiàn)可重入鎖?為什么實現(xiàn)過程中必須依賴事務(wù)?希望能解決你的一些疑惑。
一、先搞懂:什么是可重入鎖?
可重入鎖(也叫遞歸鎖)的核心特性是:同一個線程可以多次獲取同一把鎖,不會因為自己持有鎖而發(fā)生死鎖。
舉個例子:
- 線程 A 先獲取鎖,執(zhí)行業(yè)務(wù)邏輯;
- 業(yè)務(wù)邏輯中又調(diào)用了另一個需要同一把鎖的方法;
- 線程 A 可以再次成功獲取鎖,而不會被阻塞;
- 只有當(dāng)線程 A 釋放鎖的次數(shù)等于獲取鎖的次數(shù)時,鎖才會真正被釋放。
在 Java 里 ReentrantLock 就是典型的可重入鎖,而我們要做的,是用 MySQL 模擬出類似的效果。
二、MySQL 可重入鎖的基礎(chǔ):鎖表設(shè)計
要實現(xiàn)可重入鎖,我們需要一張表來記錄鎖的持有狀態(tài)、持有者和重入次數(shù):
CREATE TABLE `lock_table` ( `id` INT AUTO_INCREMENT PRIMARY KEY, -- 鎖的唯一標(biāo)識 `lock_name` VARCHAR(255) NOT NULL, -- 持有鎖的線程標(biāo)識 `holder_thread` VARCHAR(255), -- 鎖的重入次數(shù),用于實現(xiàn)可重入性 `reentry_count` INT DEFAULT 0 );
這張表的核心字段:
lock_name:鎖的唯一標(biāo)識,不同業(yè)務(wù)用不同的鎖名隔離。holder_thread:標(biāo)記當(dāng)前哪個線程持有這把鎖。reentry_count:記錄鎖被同一個線程重入的次數(shù),是實現(xiàn)可重入特性的關(guān)鍵。
三、核心實現(xiàn):加鎖與解鎖邏輯
1. 加鎖流程
1. 開啟事務(wù) 2. 執(zhí)行 SQL: SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name = ? FOR UPDATE; - 若記錄不存在:執(zhí)行 INSERT INTO lock_table (lock_name, holder_thread, reentry_count) VALUES (?, ?, 1) - 若記錄存在且持有者是當(dāng)前線程:執(zhí)行 UPDATE lock_table SET reentry_count = reentry_count + 1 WHERE lock_name = ? 3. 提交事務(wù)
2. 解鎖流程
1. 開啟事務(wù) 2. 執(zhí)行 SQL: SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name = ? FOR UPDATE; - 若記錄存在、持有者是當(dāng)前線程且重入次數(shù) > 1:執(zhí)行 UPDATE lock_table SET reentry_count = reentry_count - 1 WHERE lock_name = ? - 若記錄存在、持有者是當(dāng)前線程且重入次數(shù) ≤ 1:執(zhí)行 DELETE FROM lock_table WHERE lock_name = ? 3. 提交事務(wù)
四、靈魂拷問:為什么必須加事務(wù)?
很多同學(xué)會疑惑:“我不加事務(wù),直接執(zhí)行 SQL 不行嗎?” 答案是:不行。事務(wù)是 MySQL 可重入鎖的靈魂,沒有事務(wù),鎖的特性會直接失效。我們從三個維度拆解原因:
1. 控制鎖的生命周期:避免鎖提前釋放
MySQL InnoDB 默認 autocommit=1,單條 SQL 執(zhí)行完畢后會自動提交事務(wù)。
如果不加事務(wù):
- 執(zhí)行
SELECT ... FOR UPDATE時,會對目標(biāo)記錄加排他行鎖; - 這條 SQL 執(zhí)行完后,鎖會被自動釋放;
- 后續(xù)的
INSERT/UPDATE操作變成了全新的請求,需要重新競爭鎖。
這會導(dǎo)致:
- 鎖根本沒有被 “持有”,其他線程可以在兩次操作之間搶占鎖;
- 可重入特性直接失效,同一個線程第二次獲取鎖時可能被阻塞。
而事務(wù)的作用就是:在事務(wù)提交前,鎖不會被釋放。從查詢鎖狀態(tài)到完成寫入操作,整個過程中鎖都被當(dāng)前事務(wù)持有,保證了互斥性和可重入性。
2. 保證操作原子性:避免并發(fā)數(shù)據(jù)錯亂
加鎖 / 解鎖的核心邏輯是「查詢 → 判斷 → 寫入」,這三步必須是原子操作,否則在高并發(fā)場景下會出現(xiàn)數(shù)據(jù)錯亂。
舉個并發(fā)場景:
- 線程 A 和線程 B 同時請求同一把鎖;
- 線程 A 執(zhí)行
SELECT ... FOR UPDATE,發(fā)現(xiàn)記錄不存在,準(zhǔn)備INSERT; - 線程 B 在 A 還沒
INSERT之前,也執(zhí)行SELECT ... FOR UPDATE,同樣發(fā)現(xiàn)記錄不存在; - 兩個線程都去
INSERT,要么觸發(fā)唯一鍵沖突,要么都插入成功,導(dǎo)致鎖被同時持有。
事務(wù)的原子性可以完美解決這個問題:
- 把「查詢 → 判斷 → 寫入」封裝成一個不可分割的單元;
- 要么全部成功,要么全部失??;
- 只有當(dāng)前事務(wù)提交后,其他線程才能看到鎖的狀態(tài)變化,避免了競態(tài)條件。
3. 確??芍厝胝_性:重入次數(shù)的安全更新
可重入鎖的核心是維護 reentry_count 字段:
- 加鎖時:如果是當(dāng)前線程持有鎖,
reentry_count + 1; - 解鎖時:如果是當(dāng)前線程持有鎖,
reentry_count - 1,次數(shù)為 0 時刪除鎖記錄。
如果不加事務(wù):
- 線程 A 查詢到
reentry_count = 1,準(zhǔn)備執(zhí)行+1; - 線程 B 可能在此時修改了
reentry_count,導(dǎo)致更新后的數(shù)據(jù)錯誤; - 最終鎖的狀態(tài)混亂,甚至出現(xiàn)鎖無法釋放的情況。
事務(wù)的隔離性保證了:
- 在當(dāng)前事務(wù)更新
reentry_count時,其他線程無法修改這條記錄; - 只有事務(wù)提交后,新的重入次數(shù)才會被持久化,其他線程才能感知到。
五、完整示例:
我們用偽代碼把加鎖和解鎖流程串起來,更直觀地感受事務(wù)的作用:
加鎖偽代碼
public boolean lock(String lockName, String threadName) {
Connection conn = getConnection();
try {
// 1. 開啟事務(wù)
conn.setAutoCommit(false);
// 2. 查詢鎖狀態(tài)(加排他鎖)
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)) {
// 可重入:重入次數(shù)+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. 提交事務(wù)
conn.commit();
return true;
} catch (Exception e) {
// 異?;貪L
conn.rollback();
return false;
} finally {
conn.setAutoCommit(true);
closeConnection(conn);
}
}解鎖偽代碼
public boolean unlock(String lockName, String threadName) {
Connection conn = getConnection();
try {
// 1. 開啟事務(wù)
conn.setAutoCommit(false);
// 2. 查詢鎖狀態(tài)
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) {
// 重入次數(shù)>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 {
// 重入次數(shù)=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. 提交事務(wù)
conn.commit();
return true;
} catch (Exception e) {
conn.rollback();
return false;
} finally {
conn.setAutoCommit(true);
closeConnection(conn);
}
}六、總結(jié)
用 MySQL 實現(xiàn)可重入鎖,本質(zhì)是用數(shù)據(jù)庫表存儲鎖狀態(tài),用事務(wù)保證鎖的互斥性、原子性和生命周期。
事務(wù)的核心作用可以概括為三點:
- 鎖生命周期管理:事務(wù)提交前,鎖不會釋放,保證當(dāng)前線程持續(xù)持有鎖。
- 原子性保障:將「查詢 → 判斷 → 寫入」封裝為原子操作,避免并發(fā)數(shù)據(jù)錯亂。
- 可重入正確性:安全維護重入次數(shù),確保同一個線程可以多次獲取 / 釋放鎖。
這種實現(xiàn)方式雖然不如 Redis 等分布式鎖框架高效,但勝在簡單可靠,適合對性能要求不高、需要強一致性的場景,也能幫我們更好地理解事務(wù)和鎖的本質(zhì)。
以上就是MySQL實現(xiàn)可重入鎖的實踐指南的詳細內(nèi)容,更多關(guān)于MySQL可重入鎖實現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ubuntu系統(tǒng)中Mysql ERROR 1045 (28000): Acces
這篇文章主要介紹了ubuntu系統(tǒng)安裝mysql登陸提示 解決Mysql ERROR 1045 (28000): Access denied for user root@ localhost問題,需要的朋友可以參考下2017-05-05
mysql + nest.js 加鎖搞并發(fā)問題的實現(xiàn)
本文主要介紹了使用NestJS和MySQL實現(xiàn)秒殺系統(tǒng)的庫存扣減,并解決了高并發(fā)超賣問題,通過原子SQL、NestJS原生SQL實現(xiàn)以及使用Redis和消息隊列等方案,可以有效提高系統(tǒng)的性能和穩(wěn)定性,2026-03-03
SQL函數(shù)實現(xiàn)Group_concat用法
GROUP_CONCAT用于將每個分組內(nèi)的值連接成一個字符串,并通過指定的分隔符進行分隔,本文主要介紹了SQL函數(shù)實現(xiàn)Group_concat用法,具有一定的參考價值,感興趣的可以了解一下2024-08-08
my.cnf(my.ini)重要參數(shù)優(yōu)化配置說明
本文針對mysql不同存儲引擎,MyISAM與Innodb進行了講解如何進行my.cnf(my.ini)的參數(shù)優(yōu)化2018-03-03
MySQL數(shù)據(jù)庫社招必考題:索引如何優(yōu)化WHERE子句?
本文系統(tǒng)分享WHERE子句優(yōu)化方法,從定位低效SQL、分析原因到逐步改進,涵蓋索引、寫法調(diào)整及數(shù)據(jù)訪問優(yōu)化,通過實戰(zhàn)案例說明常見錯誤及解決策略,強調(diào)優(yōu)化需邏輯而非僅建索引,助力面試準(zhǔn)備2025-09-09
Mysql如何將數(shù)據(jù)按照年月分組的統(tǒng)計
這篇文章主要介紹了Mysql如何將數(shù)據(jù)按照年月分組的統(tǒng)計方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04

