Redisson分布式锁RedissonLock的原理原创
1 分布式锁诞生的背景
由于现在的服务器基本上都是分布式部署的,也就是同样的代码会部署到多台服务器上面。用户在访问接口时,由nginx通过负载均衡将用户的请求转发到多台服务器中的其中一台。每台服务器上面都有一个JVM实例去运行我们编写的代码,这种情况下你用java的synchronized去给一段代码加锁,只能锁住一台服务器。但是,不同的用户在访问同一个接口时,nginx会将多个用户的多个请求平均的发给每个服务器。假如我们总共有三条服务器,最坏的情况会出现,三个不同的用户分别锁住一台服务器,那么此时就相当于一个数据被三个人同时访问了,但是我们的业务要求是同一时间这个数据,只能被一个用户访问。此时就需要分布式锁了。
分布式锁的原理如下:
分布式锁用的不是Java的synchronized锁,而是用的数据库的锁。虽然,我们的代码被部署到多台服务器上面了,但是这多台服务器连接的数据库都是同一个,所以我们可以在数据库上面加锁。比如,让三个用户都往同一个表里面插入一条数据,然后在表中增加一个唯一索引,谁插入成功了,就算谁抢到锁了,可以访问数据。没抢到的用户不能访问数据。只不过,分布式锁一般都选择使用Redis来代替数据库。利用Redis执行命令的单线程特性,并且Redis也提供了setnx和set key value nx 这样的类似数据库的唯一索引命令。nx的意思就是not exists不存在才设置,如果key已经存在了就设置失败了,哪个用户设置成功,哪个用户就抢到锁了。Redis分布式锁,现在最流行的就是Redisson这个Redis客户端了。
2 Redisson分布式锁代码如下
- pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.study.demo</groupId>
<artifactId>redisson-rate-limit</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>redisson-rate-limit</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring-boot.version>2.2.8.RELEASE</spring-boot.version>
<project.name>RateLimit</project.name>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.11.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
- application.yml
server:
port: 8080
spring:
redis:
host: 192.168.212.142
port: 6379
password: 123321
application:
name: RedissonRateLimit
- Redisson配置类
package com.study.demo.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.redisson.config.SingleServerConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Value("${spring.application.name}")
private String serverName;
@Bean
public RedissonClient redissonClient(RedisProperties redisProperties) {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort());
singleServerConfig.setPassword(redisProperties.getPassword());
singleServerConfig.setKeepAlive(true);
singleServerConfig.setDatabase(redisProperties.getDatabase());
singleServerConfig.setClientName(serverName);
return Redisson.create(config);
}
}
- Service类
package com.study.demo.service;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class WatchDogService {
@Autowired
RedissonClient redissonClient;
/**
* Redisson分布式锁
*
*/
public void watchDogService() {
// 分布式锁key
String lockKey = "lockKey";
RLock lock = redissonClient.getLock(lockKey);
boolean isLock = false;
try {
// lock.tryLock() 尝试获取分布式锁,没有等待时间。获取失败就直接失败了。
// 如果获取锁成功,有看门狗。锁的默认有效时间为30秒,看门狗每10秒钟会给锁的有效时间重置为30秒。
// 如果你的代码有问题,比如陷入死循环了,看门狗将会一直续期,别的线程永远也拿到不到这个锁。
// 这不是看门狗的问题,是你自己代码写死循环的问题。
isLock = lock.tryLock();
// lock.tryLock(10, TimeUnit.SECONDS) 尝试获取分布式锁,如果在10秒钟内获取不到锁,返回失败。
// 如果获取不到锁,会死循环一直重试获取锁,直到超过10秒钟。
// 如果10秒钟内,获取锁成功,则有看门狗。锁的默认有效时间为30秒,看门狗每10秒钟会给锁的有效时间重置为30秒
// 注意这里的10秒钟,是等待获取锁的时间。不是锁本身的时间。
// isLock = lock.tryLock(10, TimeUnit.SECONDS);
// lock.tryLock(10, 15, TimeUnit.SECONDS) 尝试获取分布式锁,如果在10秒钟内获取不到锁,返回失败。
// 如果获取不到锁,会死循环一直重试获取锁,直到超过10秒钟。
// 如果10秒钟内,获取锁成功,则没有看门狗,注意没有看门狗。锁的有效时间为你设置的15秒。
// 如果15秒内,你的业务代码没有执行完成。别的线程是可以再次拿到这个锁的,就意味着你的分布式锁有问题,没锁住,相当于有俩个线程同时执行了。
// isLock = lock.tryLock(10, 15, TimeUnit.SECONDS);
if (isLock) {
// Thread.sleep(100000)模拟处理业务,耗时100秒钟
System.out.println("获取分布式锁成功,开始处理业务逻辑");
Thread.sleep(100000);
} else {
throw new RuntimeException("获取锁失败,请勿重复操作");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (isLock) {
lock.unlock();
}
}
}
/**
* Redisson的分布式锁是可重入的
*
* @param retry 重入次数
*/
public void watchDogRetryService(int retry) {
// 分布式锁key
String lockKey = "lockKeyRetry";
RLock lock = redissonClient.getLock(lockKey);
boolean isLock = false;
try {
// lock.tryLock() 尝试获取分布式锁,没有等待时间。获取失败就直接失败了。
// 如果获取锁成功,有看门狗。锁的默认有效时间为30秒,看门狗每10秒钟会给锁的有效时间重置为30秒。
// 如果你的代码有问题,比如陷入死循环了,看门狗将会一直续期,别的线程永远也拿到不到这个锁。
// 这不是看门狗的问题,是你自己代码写死循环的问题。
isLock = lock.tryLock();
// lock.tryLock(10, TimeUnit.SECONDS) 尝试获取分布式锁,如果在10秒钟内获取不到锁,返回失败。
// 如果获取不到锁,会死循环一直重试获取锁,直到超过10秒钟。
// 如果10秒钟内,获取锁成功,则有看门狗。锁的默认有效时间为30秒,看门狗每10秒钟会给锁的有效时间重置为30秒
// 注意这里的10秒钟,是等待获取锁的时间。不是锁本身的时间。
// isLock = lock.tryLock(10, TimeUnit.SECONDS);
// lock.tryLock(10, 15, TimeUnit.SECONDS) 尝试获取分布式锁,如果在10秒钟内获取不到锁,返回失败。
// 如果获取不到锁,会死循环一直重试获取锁,直到超过10秒钟。
// 如果10秒钟内,获取锁成功,则没有看门狗,注意没有看门狗。锁的有效时间为你设置的15秒。
// 如果15秒内,你的业务代码没有执行完成。别的线程是可以再次拿到这个锁的,就意味着你的分布式锁有问题,没锁住,相当于有俩个线程同时执行了。
// isLock = lock.tryLock(10, 15, TimeUnit.SECONDS);
if (isLock) {
Thread.sleep(500);
// Redisson分布式锁可以重入
System.out.println("Redisson分布式锁可以重入,添加锁成功:" + retry + ",锁key:" + lockKey);
if (retry < 0) {
// 终止递归
return;
}
watchDogRetryService(lockKey, --retry);
} else {
throw new RuntimeException("获取锁失败,请勿重复操作");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (isLock) {
System.out.println("Redisson分布式锁可以重入 释放锁:" + retry + ",锁key:" + lockKey);
lock.unlock();
}
}
}
}
- Controller类
package com.study.demo.controller;
import com.study.demo.service.WatchDogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
WatchDogService watchDogService;
@GetMapping("watchDog")
public String watchDog() {
watchDogService.watchDogService();
return "秒杀成功";
}
@GetMapping("watchDogRetry")
public String watchDogRetry() {
watchDogService.watchDogRetryService(5);
return "秒杀成功";
}
}
- SpringBoot项目启动类
package com.study.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
//@ComponentScan(basePackages = {"com.study.demo", ""})
//@EnableAspectJAutoProxy
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.1 把项目启动起来
- 在浏览器上面访问这个接口:http://localhost:8080/watchDog
- 然后再打开一个浏览器标签页继续访问这个接口:http://localhost:8080/watchDog,就会发现第二次访问时报错了,报错如下截图:
看到这个报错说明分布式锁起作用了。
- 演示一下可重入功能,在浏览器上面访问这个接口:http://localhost:8080/watchDogRetry
2.2 代码讲解
2.2.1 获取分布式锁代码
Lua脚本知识:就算你在Lua里写出花,在Redis里面执行也是一个命令(eval/evalsha)去执行的,一条命令没执行完,其他客户端是看不到的。并且Redis执行命令的时候是单线程的,有多个请求同时执行时,也是排队的,我执行完了,你才能执行。所以Redisson的分布式锁这里没有用setnx命令去完成分布式锁的功能。
- 核心代码就这下面的俩行代码
RLock lock = redissonClient.getLock("lockKey");
boolean isLock = lock.tryLock();
- redissonClient.getLock(“lockKey”); 这行代码就是从Redisson的客户端上面获取一个RLock对象,返回的真实对象为:org.redisson.RedissonLock类的实例对象。
- lock.tryLock();这个方法里面会执行几条Redis命令,我们来看下tryLock方法的源码:
我们来分析一下tryLockInnerAsync方法里面的Redis命令,这个方法里面的Redis命令总共可以分为三个功能:
-
第一个功能为:设置分布式锁的key,先使用exists KEYS[1]判断用户指定的key在Redis中是否存在。key如果不存在exists命令会返回0,存在会返回1。如果key不存在会执行:hset key ARGV[2] 1这个命令,ARGV[2]就是getLockName(threadId)方法的返回值。这个命令的意思就是将用户的key当做Redis的Hash数据机构的名字,然后往这个hash里面存入一个key-value的数据,key的值是线程的ID,value的值为1,代表该线程获取锁1次。接下来执行pexpire KEYS[1] ARGV[1]这个命令,意思是给用户指定的key设置过期时间,过期时间为ARGV[1]就是internalLockLeaseTime(看门狗的时间为30秒)。
-
第二个功能为:增加分布式锁的可重入功能。先使用hexists KEYS[1] ARGV2 意思就是判断一下分布式锁是否存在,其实这里主要判断是用户的key有没有过期,因为如果不存在直接就走第一个if判断并直接return了。返回值等于1说明没有过期,执行hincrby KEYS[1] ARGV2 1命令,给线程ID的值加1,代表当前这个线程重入了(又获取一次锁)。接下来执行pexpire KEYS[1] ARGV[1]这个命令,意思是给用户指定的key设置过期时间,过期时间为ARGV[1]就是internalLockLeaseTime(看门狗的时间为30秒)。
-
第三个功能:这个是一个兜底功能,代码走到这里就说明获取key过期了,重入时获取分布式锁失败了。我们想一下,什么情况下代码能走到这里呢:return redis.call(‘pttl’, KEYS[1]);只有一种情况,在第一个if里面判断key是否存在的时候,key还存在。然后走到第二个if的时候,key过期了,此时hexists命名返回的是0。然后才走到最后这行return redis.call(‘pttl’, KEYS[1]);代码。所以,代码走到这里说明锁重入时获取锁失败。如果有看门狗自动续期的情况下,这行代码永远都走不到这里。
-
看门狗自动续期代码如下:
我们来看自动续期的Redis命令:
- 首先还是使用hexists KEYS[1] ARGV[2]判断key是否存在。等于1说明存在。
- 接下来执行pexpire KEYS[1] ARGV[1],将key的时间重新设置为看门狗默认的时间30秒。
- 自动续期就完成了。
到这里获取分布式锁的代码也结束了。
2.2.1 释放分布式锁代码
- 释放分布式锁代码就一行
lock.unlock();
- 看下unlock()方法的源码
我们来看下释放分布式锁Redis命令:
-
第一条命令:hexists KEYS[1] ARGV[3] 判断key还是否存在,不存在直接return nil代表释放锁成功。
-
第二条命令:hincrby KEYS[1] ARGV[3] -1,对线程的值减1,执行这条命令的时候不用担心key是否过期,如果key过期hincrby命令会返回-1。如果返回值大于0,说明这个分布式锁被重入了,还不能释放。重入多少次,就需要释放多少次。值大于0,此时只是将线程的重入次数减1,代表已经释放1次了。并且执行pexpire KEYS[1] ARGV[2] 重新设置key的失效时间为internalLockLeaseTime的值,默认是30秒。
-
如果返回值小于等于0,执行del KEYS[1] 删除这个分布式锁,并执行publish KEYS[2], ARGV[1]命令,发布一个消息,通知别的线程可以来获取这个分布式锁了。
到这里释放锁的代码也结束了。
2.2.2 获取分布式锁的时候设置等待时间代码讲解
// lock.tryLock(10, TimeUnit.SECONDS) 尝试获取分布式锁,如果在10秒钟内获取不到锁,返回失败。
// 如果获取不到锁,会死循环一直重试获取锁,直到超过10秒钟。
// 如果10秒钟内,获取锁成功,则有看门狗。锁的默认有效时间为30秒,看门狗每10秒钟会给锁的有效时间重置为30秒
// 注意这里的10秒钟,是等待获取锁的时间。不是锁本身的时间。
isLock = lock.tryLock(10, TimeUnit.SECONDS);
- lock.tryLock(10, TimeUnit.SECONDS); 这个方法跟lock.tryLock();的整体逻辑是一样的,只不过多了一个等待获取锁时间的代码。来看代码把。
获取分布式锁的代码是一样的,都是调用tryAcquire(leaseTime, unit, threadId);这个方法去获取分布式锁的。只不过加了一个获取锁的时间判断。
2.2.3 获取分布式锁的时候设置等待时间同时也设置分布式锁的有效期leaseTime代码讲解
// lock.tryLock(10, 15, TimeUnit.SECONDS) 尝试获取分布式锁,如果在10秒钟内获取不到锁,返回失败。
// 如果获取不到锁,会死循环一直重试获取锁,直到超过10秒钟。
// 如果10秒钟内,获取锁成功,则没有看门狗,注意没有看门狗。锁的有效时间为你设置的15秒。
// 如果15秒内,你的业务代码没有执行完成。别的线程是可以再次拿到这个锁的,就意味着你的分布式锁有问题,没锁住,相当于有俩个线程同时执行了。
isLock = lock.tryLock(10, 15, TimeUnit.SECONDS);
- lock.tryLock(10, 15, TimeUnit.SECONDS);这个方法跟lock.tryLock(10, TimeUnit.SECONDS);的整体逻辑是一样的,只不过没有自动续期的功能了。来看代码把。
获取分布式锁的代码是一样的,都是调用tryAcquire(leaseTime, unit, threadId);这个方法去获取分布式锁的。只不过加了一个获取锁的时间判断,并且没有自动续期功能了。
到这里就代码就全部分析完毕了。你们最好自己去看看代码,代码还是非常容易看懂的。