2、知道分布式锁吗?有哪些实现方案? 你谈谈对redis分布式锁的理解
Base案例
1、建Module
2、改POM
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sl</groupId>
<artifactId>boot_redis01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot_redis01</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.0.RELEASE</spring-boot.version>
</properties>
<dependencies>
<!--默认是lettuce客户端-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis依赖commons-pool 这个依赖一定要添加 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>1.6.5</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3、写YML
server:
port: 1111
spring:
redis:
port: 6379
host: 192.168.80.16
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
max-wait: 1000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
shutdown-timeout: 100
password: root
3、业务类
config
@Configuration
public class RedisConfig {
/**
* 保证不是序列化后的乱码配置
*/
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
controller
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
}else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
}
}
V2.0
没有加锁,并发下数字不对,出现超卖现象,于是加上🔒升级到V2.0。
public String buy_Goods(){
synchronized (this) {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
}else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
}
}
在单机环境下,可以使用synchronized或Lock来实现。
但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如redis或者zookeeper来构建;
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
V3.0
分布式部署后,单机锁还是出现超卖现象,需要分布式锁。用nginx搭建一下负载均衡和反向代理。
现在访问http://192.168.80.16/buy\_goods,可以点击看到效果,一边一个,默认轮询
现在用jmeter压测一下,可以发现出现了超卖现象。
解决:上redis分布式锁setnx
Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理.
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
}else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
}
}
}
V4.0
V3.0的问题:出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
try {
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
if (!lockFlag) {
return "抢锁失败,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
}else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
}
} finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY);//释放锁
}
}
V5.0
V4.0问题:部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块, 没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
解决:需要对lockKey有过期时间的设定
V6.0
V5.0问题:设置key+过期时间分开了,必须要合并成一行具备原子性
解决:将设置key+过期时间和为一行
//setIfAbsent() == setnx 就是如果不存在就新建,同时加上过期时间保证原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);
V7.0
V6.0的问题:可能会张冠李戴,删除了别人的锁。
比如A线程进来,拿到锁,锁的有效期为10s,但是A操作了12S,到第10S的时候这个锁会被自动删除,B一看没锁了,也进来了,A操作完了,一看还有一把锁,就给它删除了。其实这是B的锁。
解决:确保来解锁的线程就是持有这个锁的线程
V8.0
V7.0的问题:finally块的判断+del删除操作不是原子性的
解决:
①用redis自身的事务:大体思路就是用WATCH监视锁,然后开启事务,去删除锁,如果成功就UNWATCH。否则会一直尝试。
finally {
while (true)
{
stringRedisTemplate.watch(REDIS_LOCK_KEY); //加事务,乐观锁
if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))){
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();//开始事务
stringRedisTemplate.delete(REDIS_LOCK_KEY);
List list = stringRedisTemplate.exec();
if (list == null) { //如果等于null,就是没有删掉,删除失败,再回去while循环那再重新执行删除
continue;
}
}
//如果删除成功,释放监控器,并且breank跳出当前循环
stringRedisTemplate.unwatch();
break;
}
}
②用lua脚本
V9.0
①要解决Redis分布式锁续期的问题。
②Redis集群+CAP对比zookeeper:
V10.0
综合上述。redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现。
Config
import org.redisson.Redisson;
import org.redisson.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
/**
* 保证不是序列化后的乱码配置
*/
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress(redisHost+":6379").setDatabase(0).setPassword("root");
return (Redisson) Redisson.create(config);
}
}
Controller
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
redissonLock.lock();
try{
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort);
return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort;
}else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort;
}finally {
redissonLock.unlock();
}
}
}
V11.0
V10.0的Bug:是在并发多的时候就可能会遇到这种错误,可能会被重新抢占
解决:
总结
最开始没加锁,不ok
↓
synchronized 单机版oK,上分布式
↓
nginx分布式微服务 单机锁不行
↓
取消单机锁 上redis分布式锁setnx
↓
只加了锁,没有释放锁, 出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁
↓
宕机了,部署了微服务代码层面根本没有走到finally这块,
没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
↓
为redis的分布式锁key,增加过期时间。
此外,还必须要setnx+过期时间必须同一行的原子性操作
↓
必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2的锁,2删3的。
↓
为了解决上一步的问题,在finally块中加了一个if判断再进行删除,但是if和del不是原子性的。所以用lua或者事务来保证原子性
↓
redis集群环境下,我们自己写的也不OK。
直接上RedLock之Redisson落地实现