Spring boot distributed lock optimization distributed lock
One: Preparation
1. Configuration file
server:
port: 8080
servlet:
session:
timeout: 30m
spring:
application:
name: spring-boot-redis
cache:
# 使用了Spring Cache后,能指定spring.cache.type就手动指定一下,虽然它会自动去适配已有Cache的依赖,但先后顺序会对Redis使用有影响(JCache -> EhCache -> Redis -> Guava)
type: REDIS
redis:
host: 192.168.0.1
port: 6379
password: 123
# 连接超时时间(ms)
timeout: 10000
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
database: 0
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 100
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
2. pox file
//为了方便测试 使用了web-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
//以下三个必须
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
Two: Test
1.contoller layer code
Distributed lock test control layer
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author WJL
* @version 1.0
* @Date: 2022/8/20 23:18
* @功能描述
**/
@RestController
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/testLock")
public void testLock(){
//1获取锁,setne
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3, TimeUnit.SECONDS);
//2获取锁成功、查询num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
redisTemplate.delete("lock");
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
First set num "0" in the server's redis
2. Service area test results
Use the ab test tool: httpd-tools (yum install -y httpd-tools)
ab -n (the number of requests sent at one time) -c (the number of concurrent requests) access path
The test is as follows: 5000 requests, 100 concurrent
ab -n 5000 -c 100 http://127.0.0.1:8206/testLock
You can see that the num at this time becomes 1000
Three: The summary was deleted by mistake
1. The process of code execution is sorted out. For example, A first operates 1. Locks 2. Specific operations. Assume that the server is stuck when A is operating, and the time has passed the set expiration time 3. Automatically release the key B grabs the lock 1. Lock it 2. Perform specific operations When the A server responds, continue to operate , the lock needs to be released manually, but the lock is in B at this time, and the lock of B is released before the operation of B is completed!
2. The solution uses UUID to prevent accidental deletion, and uses uuid to identify different operations
Four: Optimize uuid to identify different operations
Mainly add uuid when generating lock
Perform a uuid check when releasing the lock
String uuid= UUID.randomUUID().toString();
//1获取锁,setne
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid , 3, TimeUnit.SECONDS);
//2.4释放锁,del
//判断比较uuid值是否一样
if(uuid.equals(redisTemplate.opsForValue().get("lock"))){
redisTemplate.delete("lock");
}
Five: Continue to optimize using lua script
Delete operations are atomic
Problem: A will release B's lock
Solution
It has certain atomicity, will not be queued by other commands, and can complete some redis transactional operations
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String skuId = "25"; // 访问skuId 为25号的商品 100008348542
String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Six: Summary
In order to ensure that distributed locks are available, we must at least ensure that the implementation of locks meets the following four conditions :
-
mutual exclusion. At any one time, only one client can hold the lock.
-
No deadlock will occur. Even if a client crashes while holding the lock and does not actively unlock it, it can ensure that other clients can lock it later.
-
The trouble should end it. Locking and unlocking must be the same client, and the client itself cannot unlock the lock added by others.
-
Locking and unlocking must be atomic.