分布式锁-Redis实现
1:Redis 分布式锁的原理
利用NX 的原子性,多线程并发时,只有一个线程可以设置成功
设置成功即获得锁,可以执行后续的业务处理
如果出现异常,过了锁的有效期,锁自动释放
释放锁用Redis 的delete 命令,然后释放锁的时候要校验锁的随机数,这个随机数相同才能释放,就是要证明Redis里面这个key 的值是你这个线程设置的,因为你这个线程在设置这个值得时候呢,是你的这个线程生成的这么一段随机数。删除的时候你要看程序设置的随机数和Redis 中的是不是相同的,如果相同就保证这个锁是你设置的。你才能够释放锁。确保你不会释放掉别人的锁。主要就是用作一个校验
释放锁采用 Lua 脚本,因为这个 delete 校验并没有提供值校验这么一个功能
获取锁的Redis命令:
set resource_name my_random_value NX PX 30000
- resource_name 资源名称,可根据不同业务区分不同的锁
- my_random_value 随机值,每个线程的随机值都不同,用于释放锁时的校验,要保证每一个线程的随机值都不相同。
- NX:key 不存在时设置成功,key 存在则设置不成功。我们就是用这个命令实现分布式锁,主要就是用 NX 这个特性。因为SET NX是一个原子性的操作,我们都知道Redis 是一个单线程的,当你多线程并发的给这个key设置值之后,那么这个时候只有第一个线程会设置成功。因为并发请求过来时这些命令呢,在Redis 里边都变成了顺序的了,因为Redis 是单线程的所有你的并行变成了串行,这个就是排队了,只有第一个执行这个命令的才可以设置成功
- PX:自动失效时间 ,当出现异常情况,锁可以过期失效。由于设置成功之后,后边的程序执行完成,你要把这个锁给释放了,释放成功以后其他线程才可以再次获得这个锁。如果没有设置失效时间,或者释放锁的过程出现异常,那你Redis 这条记录永远存在的,那么其他线程就永远无法获取到锁
为什么对值有这么高的要求?
主要是为了校验值一样了才可以删除锁
2:手写Redis分布式锁
- 新建一个项目,导入依赖jar包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.4.RELEASE</version> </dependency>
-
配置账号密码
spring.redis.host=xx spring.redis.password=xx
-
整个代码实现过程
package com.example.redislock.controller; import lombok.extern.slf4j.Slf4j; import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.data.redis.core.types.Expiration; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Arrays; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @Author: qiuj * @Description: * @Date: 2020-10-01 15:45 */ @Slf4j @RestController public class LockController { @Autowired private RedisTemplate redisTemplate; @Transactional(rollbackFor = Exception.class) @RequestMapping("/redisLock") public String redisLock() { log.info("进入方法"); String key = "lock"; String value = UUID.randomUUID().toString(); RedisCallback<Boolean> redisCallback = connection -> { RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent(); Expiration expiration = Expiration.seconds(10); byte[] keyByte = redisTemplate.getKeySerializer().serialize(key); byte[] valueByte = redisTemplate.getValueSerializer().serialize(value); Boolean result = connection.set(keyByte, valueByte, expiration, setOption); return result; }; Boolean lock = (Boolean) redisTemplate.execute(redisCallback); if (lock) { try { log.info("获得了锁"); Thread.sleep(5000); } catch (Exception e) { e.printStackTrace(); } finally { String script = " if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" + " return redis.call(\"del\",KEYS[1])\n" + " else\n" + " return 0\n" + " end"; RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class); Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), value); log.info("释放锁的结果:" + result); } } log.info("方法执行完成"); return "方法执行完成"; } }
-
使用 redisTemplate.execute(RedisCallback<T> action) 方法
- RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
需要实现 RedisCallback 回调接口, 设置选项为 SET_IF_ABSENT 也就是NX(如果没有这个key 则设置成功 ,如果已经存在则不成功)。 - Expiration expiration = Expiration.seconds(10);
设置锁失效时间为10秒,当持有锁超过10秒就会把锁释放。时间取决于你的业务代码块执行的时间进行相应的调整。 -
byte[] keyByte = redisTemplate.getKeySerializer().serialize(key); byte[] valueByte = redisTemplate.getValueSerializer().serialize(value); 将key value 转为 Byte数组
-
Boolean result = connection.set(keyByte, valueByte, expiration, setOption); 放入set 方法,返回结果true 就是设置key 成功,也就是获得到了锁。反之false 没有获得锁
RedisCallback<Boolean> redisCallback = connection -> { RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent(); Expiration expiration = Expiration.seconds(10); byte[] keyByte = redisTemplate.getKeySerializer().serialize(key); byte[] valueByte = redisTemplate.getValueSerializer().serialize(value); Boolean result = connection.set(keyByte, valueByte, expiration, setOption); return result; };
- 将我们实现的接口RedisCallback 放入 execute() 执行。获得到了锁执行业务代码当业务代码执行完需要释放锁。避免死锁
- 因为redis delete()并没有校验值这一方法,所以我们使用 Lua 脚本进行校验 value 匹配才允许删除 。并且使用脚本可以利用Redis 的原子性操作,取值、比较、删除是一个不可分割的操作。如果不使用脚本,3个步骤就分开了,会有并发的影响。例如我们在代码中获取值 if 判断 然后删除这并不是原子性操作
-
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
-
运行两个实例,8080在44 秒时获得了锁,8088在45秒时请求获得锁,但是已经被8080获得了,所以导致无法获取锁。所以在多应用,跨JVM 中实现了分布式锁,只有一个客户端能获取到锁
8080
8088
3:使用Redisson分布式锁
1:使用 api 实现
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.5</version>
</dependency>
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/redissonLock")
public String redissonLock() {
log.info("进入方法");
Config config = new Config();
config.useSingleServer().setAddress("redis://xxx:6379").setPassword("xxx");
RedissonClient redissonClient = Redisson.create(config);
RLock rLock = redissonClient.getLock("redissonLock");
try {
rLock.lock(10L, TimeUnit.SECONDS);
log.info("获得了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
log.info("释放锁");
}
log.info("方法执行完成");
return "方法执行完成";
}
2:使用 Spring Xml实现
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.5</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:redisson="http://redisson.org/schema/redisson"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://redisson.org/schema/redisson
http://redisson.org/schema/redisson/redisson.xsd
">
<!-- minimal requirement -->
<redisson:client>
<!-- defaults to 127.0.0.1:6379 -->
<redisson:single-server address="redis://xxx:6379" password="xxx"/>
</redisson:client>
<!-- or -->
<!-- <redisson:client>-->
<!-- <redisson:single-server address="${redisAddress}"/>-->
<!-- </redisson:client>-->
</beans>
package com.example.redislock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
@SpringBootApplication
@EnableScheduling
@ImportResource(locations = "classpath:redisson.xml")
public class RedisLockApplication {
public static void main(String[] args) {
SpringApplication.run(RedisLockApplication.class, args);
}
}
@Autowired
private RedissonClient redissonClient;
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/redissonSpringLock")
public String redissonSpringLock() {
log.info("进入方法");
RLock rLock = redissonClient.getLock("redissonLock");
try {
rLock.lock(10L, TimeUnit.SECONDS);
log.info("获得了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
log.info("释放锁");
}
log.info("方法执行完成");
return "方法执行完成";
}
3:使用Spring Boot 实现
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.5</version>
</dependency>
application.properties
spring.redis.host=xxx
spring.redis.password=xxx
@Autowired
private RedissonClient redissonClient;
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/redissonSpringLock")
public String redissonSpringLock() {
log.info("进入方法");
RLock rLock = redissonClient.getLock("redissonLock");
try {
rLock.lock(10L, TimeUnit.SECONDS);
log.info("获得了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
log.info("释放锁");
}
log.info("方法执行完成");
return "方法执行完成";
}
4:实操-基于分布式锁解决定时任务重复执行问题
1:redis 的实现
需求:在集群环境下,每个应用的定时任务中,只能有一个应用执行这个方法。其他应用无法重复执行
- Application 启动类添加 @EnableScheduling 注解 ,用于开启 spring 定时任务
- 封装Lock
package com.example.redislock.util; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.data.redis.core.types.Expiration; import java.util.Arrays; import java.util.UUID; /** * @Author: qiuj * @Description: * @Date: 2020-10-02 17:29 */ @Slf4j public class RedisLock implements AutoCloseable{ public RedisLock(String key,Integer timeOutTime,RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; this.key = key; this.timeOutTime = timeOutTime; this.value = UUID.randomUUID().toString(); } private RedisTemplate redisTemplate; String value; String key; Integer timeOutTime; public Boolean lock () { RedisCallback<Boolean> redisCallback = connection -> { RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent(); Expiration expiration = Expiration.seconds(timeOutTime); byte[] keyByte = redisTemplate.getKeySerializer().serialize(key); byte[] valueByte = redisTemplate.getValueSerializer().serialize(value); Boolean result = connection.set(keyByte, valueByte, expiration, setOption); return result; }; Boolean lock = (Boolean) redisTemplate.execute(redisCallback); return lock; } @Override public void close() throws Exception { String script = " if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" + " return redis.call(\"del\",KEYS[1])\n" + " else\n" + " return 0\n" + " end"; RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class); Boolean result = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), value); log.info("释放锁的结果:" + result); } }
- cron 设置为每隔5秒钟执行一次方法
package com.example.redislock.task; import com.example.redislock.util.RedisLock; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** * @Author: qiuj * @Description: * @Date: 2020-10-02 17:26 */ @Slf4j @Component public class SendTextMessage { @Autowired private RedisTemplate redisTemplate; /* 需求:每隔5秒发送一次短信 。但是在多应用情况下不能重复 */ @Scheduled(cron = "*/5 * * * * ?") public void send() { try (RedisLock redisLock = new RedisLock("textMessage",10,redisTemplate)){ if (redisLock.lock()) { String message = "向13888888888发送一条短信"; log.info(message); } } catch (Exception e) { e.printStackTrace(); } } }
- 开启两个应用 分别是 8080 8088
- 从 8080 从5秒-20秒 之间四次没有获得锁,8088则获取到锁。 8080从 20秒-35秒 之间成功获得锁,8088则没有获得锁
2:Redisson 的实现
/*
需求:每隔5秒发送一次短信 。但是在多应用情况下不能重复
*/
@Scheduled(cron = "*/5 * * * * ?")
public void send() throws InterruptedException {
RLock rLock = redissonClient.getLock("redissonLock");
// 尝试加锁,最多等待0秒,上锁以后30秒后自动释放锁
if (rLock.tryLock(0,30L, TimeUnit.SECONDS)) {
try {
String message = "向13888888888发送一条短信";
log.info(message);
} finally {
rLock.unlock();
log.info("释放锁");
}
}
}