前言
在单机环境中,应用是同一进程下,我们只需要保证单进程、多线程线程安全性即可,通过Java提供的volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。但是在多机部署的环境中,不同机器不同进程,就需要在多进程下保证线程安全性,因此,分布式锁应允而生。本章节主要介绍SpringBoot中如何实现Redis实现分布式锁的?
1.使用StringRedisTemplate实现(未实现库存扣减功能)
2.使用Redisson实现(实现了库存扣减功能)
1.使用StringRedisTemplate实现
我们先模拟下并发场景:假设redis库存stock初始值为100,现在有5个并发的线程同时对库存进行扣减。
1、引入依赖
<!-- Reids注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、application.yml 配置文件
server:
port: 9999
spring:
data:
redis:
database: 15 #redis库
host: 127.0.0.1 #服务器地址
//password: "0211" #密码
port: 6379 #端口号
jedis:
pool:
max-active: 8 #连接池最大连接数
max-idle: 8 #连接池最大空闲连接数
min-idle: 0 #连接池最小空闲连接数
max-wait: -1 #端口号
timeout: 1000000 #连接超时时间(毫秒)
3、示例代码
@RestController
public class RedisLockController {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 模拟下单减库存的场景
* @return
*/
@RequestMapping(value = "/deduct_stock")
public String deductStock() {
redisTemplate.opsForValue().set("stock", String.valueOf(100));
for (int i = 0; i < 5; i++) {
CompletableFuture.runAsync(() -> {
// 从redis 中拿当前库存的值
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
redisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
});
}
return "success";
}
}
启动redis、设置密码授权、启动后端服务,使用postman请求:
正常执行完,我们看到了redis中的值应该为95,但实际上不是,结果却是99。
这个时候,大家都看出来,出现了线程安全的问题,我们首先能想到的肯定是给它加synchronized锁。
是的,没问题,但是我们知道,synchronized锁是属于JVM级别的,也就是我们所谓的“单机锁”,如果是多机部署的环境中,还能保证数据的一致性吗?
答案肯定是不能的。
这个时候,就需要用到了我们Redis分布式锁。
Redis分布式锁
@RestController
public class RedisLockController {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 模拟下单减库存的场景
*
* @return
*/
@RequestMapping(value = "/deduct_stock")
public String deductStock() {
redisTemplate.opsForValue().set("stock-c", String.valueOf(100));
for (int i = 0; i < 5; i++) {
CompletableFuture.runAsync(() -> {
String key = "lock_key";
String value = "ID_PREFIX" + Thread.currentThread().getId();
//key 要锁住的key;value 值;Duration.ofSeconds(60) 过期时间,单位:秒
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(60));
if (success){
// 从redis 中拿当前库存的值
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock-c"));
if (stock > 0) {
int realStock = stock - 1;
redisTemplate.opsForValue().set("stock-c", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
//释放锁
redisTemplate.delete(key);
}
});
}
return "success";
}
}
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(60));
如果success返回值为true,代表之前redis中没有key为local_key的值,表示加锁成功;反之为false 则代表已经存在当前key,则不执行扣减逻辑。
以上代码用我们的正常思维考虑,应该是没问题,但是并不完美,总结为以下几点:1、如果执行到业务代码出现异常,那释放锁的代码就无法执行,怎么解决?(可以加try … finally解决)
2、如果请求1 首先加锁需要执行15秒,过期时间设置的是10秒;请求2进入加锁执行到5秒的时候,请求1 执行完成要进行锁的释放,此时释放的就不是自己的锁,怎么解决?(释放锁的时,增加value值的判断)
问题1、2 的优化代码如下:
@RestController
public class RedisLockController {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 模拟下单减库存的场景
*
* @return
*/
@RequestMapping(value = "/deduct_stock")
public String deductStock() {
redisTemplate.opsForValue().set("stock-d", String.valueOf(100));
for (int i = 0; i < 5; i++) {
CompletableFuture.runAsync(() -> {
String key = "lock_key";
String value = "ID_PREFIX" + Thread.currentThread().getId();
//key 要锁住的key;value 值;
//Duration.ofSeconds(60) 过期时间,单位:秒
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(60));
if (success) {
try {
// 从redis 中拿当前库存的值
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock-d"));
if (stock > 0) {
int realStock = stock - 1;
redisTemplate.opsForValue().set("stock-d", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {//finally解决无法正常释放锁问题
//currentValue.equals(value) 解决请求释放锁的时候,
//释放掉的不是自己锁的问题
String currentValue = redisTemplate.opsForValue().get(key);
if (currentValue != null && currentValue.equals(value)) {
//释放锁
redisTemplate.delete(key);
}
}
}
});
}
return "success";
}
}
2.使用Redisson实现
1、引入依赖
<!-- Reids注解 StringRedisTemplate-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redisson依赖 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.20.0</version>
</dependency>
2、添加application.yml配置信息
server:
port: 9999
spring:
data:
redis:
database: 0 #redis库
host: 127.0.0.1 #服务器地址
#password: "0211" #密码
port: 6379 #端口号
jedis:
pool:
max-active: 8 #连接池最大连接数
max-idle: 8 #连接池最大空闲连接数
min-idle: 0 #连接池最小空闲连接数
max-wait: -1 #端口号
timeout: 100000000 #连接超时时间(毫秒)
3、添加Redis配置信息
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redisson(){
// useSingleServer 单机模式,还存在其他模式
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(1);
return Redisson.create(config);
}
}
4、添加库存扣减接口
@RestController
public class RedisLockController {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedissonClient redisson;
@RequestMapping(value = "/deduct_stock")
public String deductStock() {
redisTemplate.opsForValue().set("stock", String.valueOf(100));
for (int i = 0; i < 5; i++) {
int finalI = i;
CompletableFuture.runAsync(() -> {
String key = "lock_key";
//1、获取锁对象
RLock redissonLock = redisson.getLock(key);
try {
//2、加锁
redissonLock.lock();
// 从redis 中拿当前库存的值
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
redisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("线程" + finalI + "扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//3、释放锁
redissonLock.unlock();
}
});
}
return "success";
}
}
5、启动后端服务,启动Redis服务,客户端设置密码、授权连接Reids服务
设置密码:config set requirepass 0211
授权: config get requirepass
6、调用库存扣减接口测试
接口地址:http://localhost:9999/deduct_stock