前言
在单机应用多线程场景下,我们需要使用诸如synchronized、ReentrantLock可重入锁控制对临界资源的并发访问以保证我们线程安全。但是随着系统业务逐渐复杂,用户量不断加大,单机应用无法满足需求时,可能我们会开始使用多节点集群部署或者将业务拆分为分布式架构来扩展系统的性能,那么此时本地加锁就无法满足控制临界资源不被并发访问,所以分布式锁诞生。
一、分布式锁使用场景
- 效率
通过分布式锁可以避免集群部署情况下,多个节点重复相同的工作,浪费资源。 - 正确性
通过分布式锁可以保证临界资源不被并发访问,保证线程安全
二、分布式锁特点
- 互斥性
- 可重入性
- 锁超时
- 同时支持阻塞和非阻塞
- 高效、高可用
加锁和释放锁应该非常高效保证性能
三、常见分布式锁的实现
- mysql
- zookeeper
- redis
四、使用redis实现分布式锁
redis中提供了一个setNx(set if not exist)命令,该命令仅当key不存在时才可以设置成功,使用该命令设值可以保障众多客户端中只有一个客户端可以成功设置值(即:获取到锁)。
SETNX lock lock_value
释放锁就比较简单,只需要将此key删除掉,其他客户端就可以继续为此key设值从而获取到锁。
DEL lock
但是如果仅用上述命令实现加锁时,那么一般释放锁的操作也就是DEL KEY;假如某客户端在准备释放锁时发生异常或者突然宕机,那么这个锁将不会被释放,进而引起其他客户端无法获取到锁。
所以在获取锁时,需要给锁设置一个过期时间,可能最初加锁会这样设计:
SETNX lock lock_value
>ok
#设置过期时间
EXPIRE lock 5
上述实现还是会出现问题,由于上述两命令不是一个原子操作,假如当加锁成功,但是在设置过期时间的同时服务器突然挂了或者其他原因导致未设置成功,依然无法解决。
好在redis2.8之后给set命令提供了更多的参数用来在一个原子操作中完成setNx key value和expire key两个命令的结合:
set key value [expiration EX seconds|PX milliseconds] [NX|XX]
[expiration EX seconds|PX milliseconds] 可选值有EX seconds或者PX milliseconds。
- EX seconds:过期时间设置为xx秒。等同于setex
- PX milliseconds:过期时间设置为xx毫秒。等同于psetex
[NX|XX] 可选值有NX或者XX。
- NX 仅当键不存在时才可设置成功。等同于setnx命令
- XX 仅当键存在 时才可设置成功
锁超时
如果加锁和释放锁之间的业务逻辑执行时间较长,使得当锁已经过期了,而正好可以被第二个线程重新获取持有锁,然而此时正好当第一个线程业务执行完毕将锁释放了,又导致第三个线程可以获取持有锁,从而产生线程安全问题。
未避免上述问题,每个客户端线程在加锁时,可以设置值为标识当前线程的唯一值,在释放锁的时候,先判断即将释放锁的值是否匹配当前线程预期释放的锁后再进行key的删除。但是redis并没有提供if else之类语句去实现上述操作,但是可以通过lua脚本在一个事务中完成上述操作。
LUA脚本
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
通过上述可能还是会发生问题。当A线程获取到锁时,一直卡着直到锁超时开始执行临界区代码,而此时因为锁超时导致其他线程可以获取到锁,进而导致多个线程同时执行临界区代码带来线程安全问题。解决此场景的手段可以是当获取锁成功之后,可以在后台开启另一个线程每隔指定时间查看一下当前持有锁的剩余时间,如果发现剩余时间不多了,可以通过延迟锁的超时时间达到“续命”的效果。Redission中就是这么干的。
可重入性
锁的可重入性是指在持有锁的情况下再次请求当前锁,可以请求成功,那么这个锁就是可重入的。可通过ThreadLocal配合实现可重入锁。
代码实践
@Component
public class RedisLock {
private static Logger LOGGER = LoggerFactory.getLogger(RedisLock.class);
private static String RELEASE_LOCK_LUA_SCRIPT = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
@Autowired
private StringRedisTemplate stringRedisTemplate;
public boolean lock(String lockKey, String lockValue, Duration expireTime) {
return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime);
}
public boolean releaseLock(String lockKey, String lockValue) {
DefaultRedisScript releaseLockScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA_SCRIPT);
releaseLockScript.setResultType(Long.class);
Long executeResult = stringRedisTemplate.execute(releaseLockScript, Collections.singletonList(lockKey), lockValue);
return executeResult==1;
}
参考:
https://github.com/CyC2018/CS-Notes/blob/master/notes/%E5%88%86%E5%B8%83%E5%BC%8F.md#%E4%B8%80%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81
https://juejin.im/post/5b16148a518825136137c8db
再有人问你分布式锁,这篇文章扔给他
https://blog.csdn.net/long2010110/article/details/82911168
https://juejin.cn/post/6897414205071163400#heading-0