1 基於唯一索引(insert
)實現
記錄鎖的樂觀鎖方案。基於數據庫的實現方式的核心思想是:在數據庫中創建一個表,表中包含方法名等字段,並在方法名字段上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖。
1.1 優缺點
優點
- 實現簡單、易於理解
缺點
- 沒有線程喚醒,獲取失敗就被丟掉了
- 沒有超時保護,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖
- 這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用
- 併發量大的時候請求量大,獲取鎖的間隔,如果較小會給系統和數據庫造成壓力
- 這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯,沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作
- 這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖,因爲數據中數據已經存在了
- 這把鎖是非公平鎖,所有等待鎖的線程憑運氣去爭奪鎖
1.2 實現方案
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`lock_key` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖的鍵值',
`lock_timeout` datetime NOT NULL DEFAULT NOW() COMMENT '鎖的超時時間',
`remarks` varchar(255) NOT NULL COMMENT '備註信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_lock_key` (`lock_key`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
① 獲取鎖:想要執行某個方法,就使用這個方法名向表中插入數據
INSERT INTO method_lock (lock_key, lock_timeout, remarks) VALUES ('methodName', '2021-07-19 18:20:00', '測試的methodName');
② 釋放鎖:釋放鎖的時候就刪除記錄
DELETE FROM method_lock WHERE lock_key ='methodName';
1.3 問題與解決
- 強依賴數據庫可用性,是一個單點(部署雙實例)
- 沒有失效時間,一旦解鎖失敗,就會導致死鎖(添加定時任務掃描表)
- 一旦插入失敗就會直接報錯,不會進入排隊隊列(使用while循環,成功後才返回)
- 是非重入鎖,同一線程在沒有釋放鎖之前無法再次獲得該鎖(添加字段記錄機器和線程信息,查詢時相同則直接分配)
- 非公平鎖(建中間表記錄等待鎖的線程,根據創建時間排序後進行依次處理)
- 採用唯一索引衝突防重,在大併發情況下有可能會造成鎖表現象(採用程序生產主鍵進行防重)
2 基於表字段版本號實現
版本號對比更新的樂觀鎖方案。一般是通過爲數據庫表添加一個 version
字段來實現讀取出數據時,將此版本號一同讀出。之後更新時,對此版本號加 1
,在更新過程中,會對版本號進行比較,如果是一致的,沒有發生改變,則會成功執行本次操作;如果版本號不一致,則會更新失敗。實際就是個CAS
過程。
2.1 優缺點
缺點
- 該方式使原本一次的update操作,必須變爲2次操作:select版本號一次、update一次。增加了數據庫操作的次數
- 如果業務場景中的一次業務流程中,多個資源都需要用保證數據一致性,那麼如果全部使用基於數據庫資源表的樂觀鎖,就要讓每個資源都有一張資源表,這個在實際使用場景中肯定是無法滿足的。而且這些都基於數據庫操作,在高併發的要求下,對數據庫連接的開銷一定是無法忍受的
- 樂觀鎖機制往往基於系統中的數據存儲邏輯,因此可能會造成髒數據被更新到數據庫中
3 基於排他鎖(for update
)實現
基於排它鎖的悲觀鎖方案。通過在select語句後增加for update
來獲取鎖,數據庫會在查詢過程中給數據庫表增加排他鎖。當某條記錄被加上排他鎖之後,其他線程無法再在該行記錄上增加排他鎖,我們可以認爲獲得排它鎖的線程即可獲得分佈式鎖。釋放鎖通過connection.commit();
操作,提交事務來實現。
3.1 優缺點
優點
- 實現簡單、易於理解
缺點
- 排他鎖會佔用連接,產生連接爆滿的問題
- 如果表不大,可能並不會使用行鎖
- 同樣存在單點問題、併發量問題
3.2 實現方案
**
CREATE TABLE `methodLock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`lock_key` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖的鍵值',
`lock_timeout` datetime NOT NULL DEFAULT NOW() COMMENT '鎖的超時時間',
`remarks` varchar(255) NOT NULL COMMENT '備註信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY ( `id` ),
UNIQUE KEY `uidx_lock_key` ( `lock_key ` ) USING BTREE
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '鎖定中的方法';
加解鎖操作
/**
* 加鎖
*/
public boolean lock() {
// 開啓事務
connection.setAutoCommit(false);
// 循環阻塞,等待獲取鎖
while (true) {
// 執行獲取鎖的sql
String sql = "select * from methodLock where lock_key = xxx for update";
// 創建prepareStatement對象,用於執行SQL
ps = conn.prepareStatement(sql);
// 獲取查詢結果集
int result = ps.executeQuery();
// 結果非空,加鎖成功
if (result != null) {
return true;
}
}
// 加鎖失敗
return false;
}
/**
* 解鎖
*/
public void unlock() {
// 提交事務,解鎖
connection.commit();
}
更多JAVA、高併發、微服務、架構、解決方案、中間件的總結在:https://github.com/yu120/lemon-guide