維護一個鎖表
CREATE TABLE `dist_lock`
(
`id` bigint PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '資源名字',
`node_info` varchar(64) NOT NULL DEFAULT '' COMMENT '搶到鎖的機器和線程信息',
`count` int NOT NULL DEFAULT 0 COMMENT '重入次數',
`create_at` bigint NOT NULL DEFAULT 0 COMMENT '創建時間戳',
UNIQUE KEY uk_resource_name (`resource_name`)
) ENGINE = InnoDB DEFAULT CHARSET utf8mb4 COMMENT '分佈式鎖';
獲取鎖
public boolean lock(String resourceName, String nodeInfo) {
String retNodeInfo = @Select("select * from dist_lock where resource_name=#{resourceName}");
if (nodeInfo == null) {
// 還沒人往裏寫,嘗試獲取鎖
int ret = @Insert("insert into dist_lock (resource_name, node_info, count) VALUES (#{resourceName}, #{nodeInfo}, 1) ") ;
if (ret == 1) {
// 插入成功,認爲獲取到鎖
return true;
} else {
// 插入失敗,認爲獲取鎖失敗
return false;
}
}
if (nodeInfo.equals(retNodeInfo)) {
// 是自己寫入的,可重入
@Update("update dist_lock set count = count + 1 where resource_name = #{resourceName} and node_info = #{nodeInfo}")
return true;
} else {
// 是別人寫入的,獲取鎖失敗
return false;
}
}
釋放鎖
public boolean unlock(String resourceName, String nodeInfo) {
String count = @Select("select count from dist_lock where resource_name=#{resourceName} and node_info = #{nodeInfo}");
if (count == null) {
// 已經被釋放了
return false;
}
if (count == 1)) {
// 只剩下一次,可以直接刪除
@Delete("delete from dist_lock where resource_name = #{resourceName} and node_info = #{nodeInfo};")
return true;
} else {
// 可重入次數-1
@("update dist_lock set count = count - 1 where resourceName = #{resourceName} and nodeInfo = #{nodeInfo}")
}
}
超時自動釋放鎖
定時任務:
delete from dist_lock where create_time > ${now} - ${lockTimeOut};
需要加事務嗎?
在獲取鎖的僞代碼中:
假設c1 c2同時select發現resource是空,沒人加鎖,然後同時去insert,由於unique index的原因,實際上也只有一個線程可以真正插入成功獲得鎖。所以不需要加事務。
在釋放鎖的僞代碼中:
假設現在可重入次數爲2,c1和c2同時讀到了次數爲2,那都不滿足=1的條件,於是都只會順序去-1,最終count經過2次-1,==0了,還是沒刪掉這個鎖,有bug。那有沒有別的辦法呢?假設我們將釋放鎖改成這樣:
public boolean unlock(String resourceName, String nodeInfo) {
// 每次無論如何都會將可重入次數-1
@Update("update dist_lock set count = count - 1 where resource_name = #{resourceName} and node_info = #{nodeInfo}")
// 每次無論如何都把<=0的鎖幹掉
@Delete("delete from dist_lock where resource_name = #{resourceName} and node_info = #{nodeInfo} and count <= 0;")
}
在這種情況下,假設可重入次數count爲2
併發情況1:
C1 update, count=1,C2 update, count=0; C1 delete 刪除成功,C2 delete 刪除失敗,最終成功釋放了鎖
併發情況2:
C1 update, count=1,C1 delete 刪除失敗;C2 update count=0; C2 delete 刪除成功,最終成功釋放了鎖
測試
可以看到,支持了可重入”reentrant“,同時也能比較好地釋放鎖。
參考code
https://github.com/SteveWongBUAA/learning-spring-framework/blob/master/src/main/java/com/vava/distlock/DistLock.java