分布式锁
为什么需要分布式锁
锁这个名次在开发中很常见,操作系统、数据库这些软件中都有锁的实现。
我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的锁进行处理,并且可以完美的运行。说白了,锁就是一个“指挥交通“的存在,它可以规定谁可以通行(访问数据)。但是像操作系统、数据库等软件中的锁都只是在单机上,他不能指挥其他主机。
因为业务的需求,分布式、微服务出现了,那么在多台服务器的场景下,就需要一个锁来指挥全部的服务器。某台服务器上的锁不能管到其他服务器,分布式锁就出现。
分布式CAP理论
AP理论是分布式系统设计中的一个重要理论,它指出一个分布式系统不可能同时满足一致性、可用性和分区容错性这三个特性。一致性是指系统中的数据能够保持一致的状态,可用性是指系统能够对每个请求做出响应,分区容错性是指系统能够在网络分区的情况下继续运行。
分布式锁需要具备的条件
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁的实现
基于数据库
数据库也是一个可以公共访问的资源,所有服务器都能够访问它。那么借助数据库的唯一索引和锁机制就能实现分布式锁的功能。
核心思想:
在数据库中创建一个表,表中包含方法名等字段,将方法名设置为唯一索引,用于区分争夺的是哪个方法的服务(抢占的资源)。要执行某个方法,就向表中插入一行数据,插入成功则代表获得锁,反之则获取失败。当使用完后就删除该行数据,代表释放锁。
基于数据库实现的优点:
- 实现简单,直接由数据库的机制就能实现,不需要自己写额外的代码。
基于数据库实现的缺点:
- 因为数据库读写涉及磁盘读写,而且数据库可能还涉及数据同步、主从复制等问题,所以分布式锁的性能及可用性会受到影响。
- 因为是基于数据库自己的唯一索引机制,我们也不能实现锁可重入的需求。
- 不能实现锁失效机制,如果在成功插入数据后服务器宕机了,那么锁就会一直存在。
- 无法确保获取锁以及删除锁的服务器是同一个。
基于Redis的实现
Redis作为一个内存存储系统,在数据读写效率上远大于数据库。
setnx 命令:当key不存在时才插入,否则插入失败。那么插入是否成功可作为是否抢到锁的依据。
expire 命令:可设置锁的过期时间。
delete 命令:可之间删除锁
核心思想:
在获取锁时,使用
setnx
尝试取获得锁,并设置一个锁过期的时间,超过该事件则自动释放锁,锁的value
可以为一个随机生成的UUID;最后删除锁时,可通过UUID判读是否同一服务器。但是按照以上思想实现的话,还存在一些问题:
- 如果服务器还不想释放锁(因为还未使用完资源),怎么实现锁的可重入性。
- 如何实现A取到锁,只能由A释放锁
- 如何保证命令的原子性
实现锁的可重入性
可以使用Hash结构来实现,key表示公共资源,hashfield为UUID,hash field的value表示获取锁的次数。(类似于操作系统信号量)
实现A取到的锁只能由自己释放
// 以下为伪代码
setnx lock Alock; // A取到锁
if 'get lock' == Alock{
delete lock; // 如果是A自己的锁,则可以释放
} else{
// 不是自己的锁
}
这个方法是可以解决自己的锁只能由自己释放,但是还存在问题。
因为
get
和delete
两个命令不是原子执行的。
if 'get lock' == Alock{
// 如果这时,锁突然不再是自己的,而是变成别人的,就会出现误解锁 delete lock; // 如果是A自己的锁,则可以释放 } else{ // 不是自己的锁 }
通过lua脚本实现原子操作
// 获取锁的 value 与 ARGV[1] 是否匹配,匹配则执行 del
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Redis
执行Lua
脚本:Redis执行Lua脚本
通过Redisson实现分布式锁
上述讨论Redis实现分布式锁会出现的问题,Redisson都已帮我们解决了,我们直接用就行了。
配置Redisson
// 获得锁
// 拿到锁才会执行的代码
RLock lock = redissonClient.getLock("lockName");
try{
lock.lock();
// TODO job
}catch(Exception e){
}finally {
// 解锁自己的锁
if (lock.isHeldByCurrentThread()){
lock.unlock();
}
}