1、分布式锁的实现方式
大概有三种:1.基于关系型数据库,2.基于缓存,3基于zookeeper
大部分网站使用的是基于缓存的,有更好的性能,而缓存一般是以集群方式部署,保证了高可用性
总体来说,支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,都可以给你完美实现。
2.基于缓存redis,使用开源 redisson 实现分布式锁
3、关于redisson 锁的几点说明,
1、通过阅读redission锁的API可以得知,其获取锁释放锁的使用和JDK里面的lock很相似,底层的实现采用了类似lock的处理方式
2、redisson 依赖redis,因此使用redisson 锁需要服务端安装redis,而且redisson 支持单机和集群两种模式下的锁的实现
3、redisson 在多线程或者说是分布式环境下实现机制,其实是通过设置key的方式进行实现,也就是说多个线程为了抢占同一个锁,其实就是争抢设置key,这个和zookeeper的锁是不是有点儿相似?
通过下面这张图来简单看看redisson 锁的实现原理,
1)加锁机制
咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器
这里注意,仅仅只是选择一台机器!这点很关键!
紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:
简单解释一下这段lua脚本要做的事情,
1、锁不存在的情况下加锁,
KEYS[1]代表的是你加锁的那个key,比如说:
RLock lock = redisson.getLock(“myLock”);
这里你自己设置了加锁的那个锁key就是“myLock”
ARGV[1]代表的就是锁key的默认生存时间,默认30秒
ARGV[2]代表的是加锁的客户端的ID,类似于下面这样:
8743c9c0-0795-4907-87fd-6c719a6b4586:1
给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁
如何加锁呢?很简单,用下面的命令:
hset myLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
mylock{
"8743c9c0-0795-4907-87fd-6c719a6b4586:1":1
}
- 上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。
接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。
好了,到此为止,ok,加锁完成了。
(2)锁互斥机制
那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?
很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。
接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。
所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。
此时客户端2会进入一个while循环,不停的尝试加锁。
(3)watch dog自动延期机制
客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?
简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。
(4)可重入加锁机制
那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?
第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。
第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”
此时就会执行可重入加锁的逻辑,他会用:
incrby myLock
8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
通过这个命令,对客户端1的加锁次数,累加1。
此时myLock数据结构变为下面这样:
大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数
(5)释放锁机制
如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。
其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。
如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:
“del myLock”命令,从redis里删除这个key。
然后呢,另外的客户端2就可以尝试完成加锁了。
这就是所谓的分布式锁的开源Redisson框架的实现机制。
一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁
有了上面的概念,下面来具体说一下springboot整合redisson实现分布式锁的代码整合,
1、pom文件需要添加如下依赖,
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.0</version>
</dependency>
</dependencies>
2、redisson锁需要依赖redis,因此需要在配置文件中添加redis的响应配置,这里为了演示方便没有加其他的配置,
server.port=8082
redisson.address=redis://127.0.0.1:6379
3、定义一个接口,里面是关于redisson操作锁的API接口,
public interface DistributedLocker {
RLock lock(String lockKey);
RLock lock(String lockKey, long timeout);
RLock lock(String lockKey, TimeUnit unit, long timeout);
boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);
void unlock(String lockKey);
void unlock(RLock lock);
}
4、接口实现类,
@Component
public class RedissonDistributedLocker implements DistributedLocker {
@Autowired
private RedissonClient redissonClient; // RedissonClient已经由配置类生成,这里自动装配即可
// lock(), 拿不到lock就不罢休,不然线程就一直block
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
return lock;
}
// leaseTime为加锁时间,单位为秒
@Override
public RLock lock(String lockKey, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(leaseTime, TimeUnit.SECONDS);
return null;
}
// timeout为加锁时间,时间单位由unit确定
@Override
public RLock lock(String lockKey, TimeUnit unit, long timeout) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, unit);
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, leaseTime, unit);
} catch (InterruptedException e) {
return false;
}
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
}
5、redisson基本配置类,也是大家熟悉的套路,因为redisson支持多种模式下的配置,比如单机、集群、哨兵模式等,都可以根据实际业务需要进行配置,这里为演示方便使用单机配置,
@Configuration
public class RedissonManager {
@Value("${redisson.address}")
private String addressUrl;
@Bean
public RedissonClient getRedisson() throws Exception{
RedissonClient redisson = null;
Config config = new Config();
config.useSingleServer()
.setAddress(addressUrl);
redisson = Redisson.create(config);
System.out.println(redisson.getConfig().toJSON().toString());
return redisson;
}
}
注意的是,在这个配置类里面,关于redis连接配置还有很多其他参数,比如像连接的用户名、密码、超时时间、连接的库信息等,可以根据需要往里面添加,
6、接下来,写一个测试类,使用100个线程模拟一下获取锁的动作,第一次假如我们不释放锁,可以比较清楚看到哪个线程获取到了锁,接口请求一下,看看控制台打印结果,
@RestController
@RequestMapping("/redisson")
public class LockTestController {
@Autowired
private DistributedLocker distributedLocker;
@RequestMapping("/test")
public void redissonTest() {
String key = "redisson_key";
for (int i = 0; i < 100; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
System.err.println("=============线程开启============" + Thread.currentThread().getName());
/*
* distributedLocker.lock(key,10L); //直接加锁,获取不到锁则一直等待获取锁
* Thread.sleep(100); //获得锁之后可以进行相应的处理
* System.err.println("======获得锁后进行相应的操作======"+Thread.
* currentThread().getName());
* distributedLocker.unlock(key); //解锁
* System.err.println("============================="+
* Thread.currentThread().getName());
*/
boolean isGetLock = distributedLocker.tryLock(key, TimeUnit.SECONDS, 5L, 10L); // 尝试获取锁,等待5秒,自己获得锁后一直不解锁则10秒后自动解锁
if (isGetLock) {
System.out.println("线程:" + Thread.currentThread().getName() + ",获取到了锁");
Thread.sleep(100); // 获得锁之后可以进行相应的处理
System.err.println("======获得锁后进行相应的操作======" + Thread.currentThread().getName());
//distributedLocker.unlock(key);
System.err.println("=============================" + Thread.currentThread().getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
项目启动一下,浏览器输入,http://localhost:8082/redisson/test,查看控制台,可以看到在这100个线程中,只有第24个线程获取到了锁,
然后我们将释放锁的动作放开,重启项目再访问一下,这时可以看到只要某个抢到锁的线程执行完毕并且释放了锁资源,其他的线程很快就会获取到锁,速度还是很快的,
到这里,关于redisson锁的基本使用整合就结束了,感谢观看!