基於mysql實現分佈式鎖

維護一個鎖表

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章