一 高并发情况 模拟单机并发环境
使用测试工具jmeter实现功能测试
步骤一 创建线程组
步骤二 更改线程数量
步骤三 添加请求方式,并设置参数
步骤四 添加请求结果页面
步骤五 开启后端服务点击测试
我们后端测试内容,每次执行程序就将redis里面的string类型的key:num加一。发现问题。在多线程的时候会重复加。
二 单机并发解决方法---本地锁
就是在改变num值的方法加上 synchronized 同步代码块。结果当然没问题。但是上面测试方式,在一个服务器里面进行的测试,单机版服务,本地锁才能发挥作用。到了微服务,分布式,集群的情况,本地锁就失去了作用
三 高并发环境 模拟集群并发环境
使用idea部署集群效果,里面还是本地锁
步骤一 修改nacos配置中心端口配置,端口部分注释掉
我们使用service-order这个微服务来做测试
步骤二 项目配置文件中添加端口号 bootstrap.properties
将我们的service-order服务在 bootstrap.properties 下面设置端口号,提高他的优先级
步骤三 操作idea添加多个端口号
将service-order复制多份,并改他的端口号。这样就有集群效果。复制在我鼠标停留的地方
步骤四 网关模块配置文件添加路由规则
步骤五 启动集群服务,还需要启动网关服务
步骤六 开始测试
1 将请求的端口号设置成网关的,并设置线程数为1000
2 修改num的值为0,用于测试
3 启动请求,查看num值.我们期望拿到的是1000,但是只有519.很明显本地锁在这里失效了。应该说,本地锁对当前的服务生效了,但是对多服务失效
四 redis分布式锁
1 加锁流程(有问题)
在我线程1 ,我用setnx方法加锁,将num的值设置成1。此时,我在线程2中用setnx方法将num设置成2.此时线程2的操作不会成功。线程一设置完后,我用del方法将锁给删除了之后,我在线程2再次设置num的值,才会成功。
2 代码实现(有问题)
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void testLock() {
//从redis里面获取数据
//1 获取当前锁 setnx
Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
//2 如果获取到锁,从redis获取数据 数据+1 放回redis里面
if(ifAbsent) {
//获取锁成功,执行业务代码
//1.先从redis中通过key num获取值 key提前手动设置 num 初始值:0
String value = redisTemplate.opsForValue().get("num");
//2.如果值为空则非法直接返回即可
if (StringUtils.isBlank(value)) {
return;
}
//3.对num值进行自增加一
int num = Integer.parseInt(value);
redisTemplate.opsForValue().set("num", String.valueOf(++num));
//3 释放锁
redisTemplate.delete("lock");
} else {
try {
Thread.sleep(100);
this.testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3 问题解决
我们在上面加锁的时候如果在加锁和删除锁之间出现了bug,那么我们的锁将会一直在。
解决:
我们可以用try-catch-finally来释放锁
我们也可以设置过期时间,到时间立马删除锁。
4 设置过期时间(也有问题)
在setnx设置锁的地方设置过期时间,其余代码一样。这里设置了三秒
@Override
public void testLock() {
//从redis里面获取数据
//1 获取当前锁 setnx + 设置过期时间
// Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
Boolean ifAbsent =
redisTemplate.opsForValue()
.setIfAbsent("lock", "lock",3, TimeUnit.SECONDS);
//2 如果获取到锁,从redis获取数据 数据+1 放回redis里面
if(ifAbsent) {
//获取锁成功,执行业务代码
//1.先从redis中通过key num获取值 key提前手动设置 num 初始值:0
String value = redisTemplate.opsForValue().get("num");
//2.如果值为空则非法直接返回即可
if (StringUtils.isBlank(value)) {
return;
}
//3.对num值进行自增加一
int num = Integer.parseInt(value);
redisTemplate.opsForValue().set("num", String.valueOf(++num));
// ???????bug???????
//3 释放锁
redisTemplate.delete("lock");
} else {
try {
Thread.sleep(100);
this.testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5 还有一件事!!!
使用setnx+过期时间实现分布式锁,存在问题:删除不是自己的锁,锁误删
场景:如果业务逻辑的执行时间是7s。执行流程如下
1. index1业务逻辑没执行完,3秒后锁被自动释放。
2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
3. index3获取到锁,执行业务逻辑
index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁, 导致index3的业务只执行1s就被别人释放。
最终等于没锁的情况
6 改正(有问题)
给锁加一个自己独一无二的名字,删锁的时候先判断删的是不是自己的锁.设置value为uuid
@Autowired
private StringRedisTemplate redisTemplate;
//uuid防止误删
@Override
public void testLock() {
//从redis里面获取数据
String uuid = UUID.randomUUID().toString();
//1 获取当前锁 setnx + 设置过期时间
// Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
Boolean ifAbsent =
redisTemplate.opsForValue()
.setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
//2 如果获取到锁,从redis获取数据 数据+1 放回redis里面
if (ifAbsent) {
//获取锁成功,执行业务代码
//1.先从redis中通过key num获取值 key提前手动设置 num 初始值:0
String value = redisTemplate.opsForValue().get("num");
//2.如果值为空则非法直接返回即可
if (StringUtils.isBlank(value)) {
return;
}
//3.对num值进行自增加一
int num = Integer.parseInt(value);
redisTemplate.opsForValue().set("num", String.valueOf(++num));
//出现异常
//3 释放锁
String redisUuid = redisTemplate.opsForValue().get("lock");
if (uuid.equals(redisUuid)) {
redisTemplate.delete("lock");
}
} else {
try {
Thread.sleep(100);
this.testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7 最后一遍找错!
通过uuid防止误删,但是还是存在问题,他不具备原子性的。
如果在第一行代码执行后放行,突然间锁过时了或者发生其他事,导致锁被另外一个线程b得到了,那么第二行代码就删了那个线程b的锁。
8 最终版(正确版本)(看就看这个版本)(上面都是推导过程)
我们将删除锁的代码用lua脚本代替。我们需要在执行删除锁的时候保证只有一个线程在执行任务,这就需要程序的原子性
Lua 中的原子性
在 Lua 脚本中,原子性并不是直接由语言本身提供的特性,而是由 Lua 的执行环境和与其交互的系统决定的。例如:
-
Lua 虚拟机:Lua 的虚拟机以解释器的形式逐行执行脚本代码。Lua 本身不会将脚本的执行分割成更小的操作,因此在执行 Lua 代码时,每个语句通常被视为原子操作。
-
数据一致性:Lua 提供了事务性和一致性操作的工具,例如在表操作中,如果你在一个表中进行插入或更新操作,这些操作要么完全成功,要么在发生错误时不会有部分数据修改。
-
并发和同步:Lua 本身并不原生支持多线程。Lua 的标准库中没有提供多线程支持,通常在嵌入 Lua 的应用程序中,线程管理和原子性保障由宿主程序负责。比如在多线程环境中,Lua 代码通常需要与宿主程序进行适当的同步,以保证线程安全。
-
元表和元方法:Lua 的元表和元方法允许你在操作表时实现自定义的行为。虽然 Lua 的表操作在大多数情况下是原子的,但如果你使用元表和元方法来定义复杂的行为,可能会涉及到一些操作的原子性问题,这取决于你如何设计和实现这些元方法。
事务性和错误处理
在 Lua 中处理事务性操作和错误处理通常依赖于使用 pcall
(保护调用)函数来捕捉运行时错误。pcall
允许你安全地执行代码块,并且如果出现错误,不会导致整个程序崩溃。这种机制可以帮助你在一定程度上实现操作的“原子性”,通过确保即使在执行过程中发生错误,也可以控制错误的处理方式。
9 最终版本执行代码
我们用lua脚本代替原来的java语言来删除锁
//lua脚本保证原子性
@Override
public void testLock() {
//从redis里面获取数据
String uuid = UUID.randomUUID().toString();
//1 获取当前锁 setnx + 设置过期时间
// Boolean ifAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "lock");
Boolean ifAbsent =
redisTemplate.opsForValue()
.setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);
//2 如果获取到锁,从redis获取数据 数据+1 放回redis里面
if(ifAbsent) {
//获取锁成功,执行业务代码
//1.先从redis中通过key num获取值 key提前手动设置 num 初始值:0
String value = redisTemplate.opsForValue().get("num");
//2.如果值为空则非法直接返回即可
if (StringUtils.isBlank(value)) {
return;
}
//3.对num值进行自增加一
int num = Integer.parseInt(value);
redisTemplate.opsForValue().set("num", String.valueOf(++num));
//出现异常
//3 释放锁 lua脚本实现
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
//lua脚本
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
redisScript.setScriptText(script);
//设置返回结果
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
} else {
try {
Thread.sleep(100);
this.testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
10 总结
10.1 加锁并设置过期时间
// 1. 从Redis中获取锁,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
.setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);
10.2 使用lua释放锁
// 2. 释放锁 del
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 设置lua脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置lua脚本返回类型为Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
```
10.3 重试
Thread.sleep(500);
testLock();
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
第一个:互斥性,在任何时刻,只有一个客户端能持有锁。
第二个:不会发生死锁,即使有一个客户端在获取锁操作时候崩溃了,也能保证其他客户端能获取到锁。
第三个:解铃还须系铃人,解锁加锁必须同一个客户端操作。
第四个:加锁和解锁必须具备原子性
五 Redisoon分布式锁(框架)
1 概述
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。
Redisson的宗旨是:促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
2 快速入门
2.1 引入依赖,版本号自己写
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
2.2 创建redisson配置类
注意读取配置的时候留意名字
package com.atguigu.daijia.common.config.redisson;
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.data.redis")
public class RedissonConfig {
private String host;
private String password;
private String port;
private int timeout = 3000;
private static String ADDRESS_PREFIX = "redis://";
/**
* 自动装配
*
*/
@Bean
RedissonClient redissonSingle() {
Config config = new Config();
if(!StringUtils.hasText(host)){
throw new RuntimeException("host is empty");
}
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(ADDRESS_PREFIX + this.host + ":" + port)
.setTimeout(this.timeout);
if(StringUtils.hasText(this.password)) {
serverConfig.setPassword(this.password);
}
return Redisson.create(config);
}
}
2.3 使用redisson加锁释放锁
有三种加锁方式自己选择
@Autowired
private RedissonClient redissonClient;
//uuid防止误删
@Override
public void testLock() {
//1 通过redisson创建锁对象
RLock lock = redissonClient.getLock("lock1");
//2 尝试获取锁
//(1) 阻塞一直等待直到获取到,获取锁之后,默认过期时间30s
lock.lock();
//(2) 获取到锁,锁过期时间10s
// lock.lock(10,TimeUnit.SECONDS);
//(3) 第一个参数获取锁等待时间
// 第二个参数获取到锁,锁过期时间
// try {
// // true
// boolean tryLock = lock.tryLock(30, 10, TimeUnit.SECONDS);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
//获取锁成功,执行业务代码
//1.先从redis中通过key num获取值 key提前手动设置 num 初始值:0
String value = redisTemplate.opsForValue().get("num");
//2.如果值为空则非法直接返回即可
//3.对num值进行自增加一
int num = Integer.parseInt(value);
redisTemplate.opsForValue().set("num", String.valueOf(++num));
//4 释放锁
lock.unlock();
}