维护一个锁表
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