分布式锁
为什么需要分布式锁
当我们遇到同一方法在同一时间只能让一个线程来执行,那么在单机环境下,我们不需要考虑分布式锁,只需要使用 Java 相应的 API 即可。但是当我们的系统是分布式部署的时候,就不能仅在线程方面考虑锁的问题了,分布式环境下与单机环境最大的不同不是多线程而是多进程。
多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置,而进程之间可能彼此都不在一个物理机上,因此需要将标记存储在一个所有进程都能看得到的地方。
基于数据库的分布式锁
基于数据库表
要使用数据库实现分布式锁,最简单的就是创建一个锁表,然后通过该表中的数据实现获取和释放锁的操作。
当我们要锁住某个方法或者某个资源的时候,我们就在该表中加入一条记录,想要释放的时候就删除这条记录。
可以创建一张表:
CREATE TABLE `resource_lock` (
`key_resource` varchar(45) COLLATE utf8_bin NOT NULL DEFAULT '资源主键',
`status` char(1) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT 'S,F,P',
`lock_flag` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '1是已经锁 0是未锁',
`begin_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`client_ip` varchar(45) COLLATE utf8_bin NOT NULL DEFAULT '抢到锁的IP',
`time` int(10) unsigned NOT NULL DEFAULT '60' COMMENT '方法生命周期内只允许一个结点获取一次锁,单位:分钟',
PRIMARY KEY (`key_resource`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin
当我们想要加锁就执行相应的 insert 语句,解锁就执行相应的 delete 语句
这张表之所以可以用来当做分布式锁,主要使用的是 主键的唯一性,如果有针对同一资源的加锁请求同时提交到数据库的话,数据库会保证只有一个操作可以执行成功。
存在的问题:
- 锁强依赖数据库的可用性,若数据库是一个单点,数据库挂掉,整个系统就不能用了
- 锁没有失效时间,一旦解锁失败,记录会一直在数据库中,其他线程无法获得到锁
- 锁是非阻塞的,数据库 insert 失败只会直接报错,没有获得锁的线程不会进入队列排队等待
- 锁是非重入的
基于 Redis 实现分布式锁
利用 Setnx + expire命令(错误做法)
Redis 的 Setnx key value
命令可以用来当 key 不存在时才能成功添加新值,若 key 存在就什么也不做,考虑超时机制,所以我们利用 expire 命令来设置超时时间
public boolean lock(String key,String value,int timeout) {
Long result = jedis.setnx(key, value);
if (result == 1) {
return jedis.expire(key, timeout) == 1;
} else {
return false;
}
}
但是因为 setnx 和 expire 是分开操作的,并不是原子操作,如果 setnx 指令做完了就宕机了,那么这个锁就不会过期了
利用 Lua 脚本
针对上面的问题,我们可以使用 Lua 脚本解决,将 sentnx 和 expire 操作写进一个 Lua 脚本中,即可实现原子操作。
使用 set key value [EX sencond] [PX millisecond] [NX|XX]命令
这个命令是一个原子操作
public boolean tryLock_with_set(String key, String UniqueId, int seconds) {
return "OK".equals(jedis.set(key, UniqueId, "NX", "EX", seconds));
}
value 需要具备唯一性,这样可以防止其他线程将自己的锁删除掉