一、环境搭建
在redis服务器存储了一个key,值为100
Java业务代码,业务逻辑为如果redis中的key值大于0,则减1并更新
@GetMapping("reduce/stock")
public String reduceStock(){
String stock = redisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock)){
Integer stock_num = Integer.parseInt(stock);
if (stock_num > 0){
stock_num = stock_num - 1;
redisTemplate.opsForValue().set("stock",String.valueOf(stock_num));
System.out.println("扣减成功,余额为" + stock_num);
return "扣减成功,余额为" + stock_num;
}else {
System.out.println("库存不足");
return "库存不足";
}
}else {
return "error";
}
}
二、压力测试
使用Jmeter模拟多线程环境下的库存资源问题。可参考该篇文章使用JMeter创建多线程环境。
JMeter的使用
这种问题如果是单体应用,则我们添加synchronized关键字即可。但如果项目是集群的话,就会有多个项目独立于不同的JVM进程之间,此时单单使用synchronized关键字也是不行的,这就引入了分布式锁的概念,即使在不同的JVM进程中也能保证某一时刻只有一个线程获取某个资源。
三、Redis方式实现
一、判断某个key是否在redis中存在
@GetMapping("reduce/stock")
public String reduceStock(){
//setIfAbsent 如果不存在则设置lockKey,返回true,如果存在lockKey,则放弃更新,返回false
Boolean success = redisTemplate.opsForValue().setIfAbsent("lockKey", UUID.randomUUID().toString());
if (!success){
//表示key存在,此时已经被某个线程操作
return "Being operated by another thread";
}
String stock = redisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock)){
Integer stock_num = Integer.parseInt(stock);
if (stock_num > 0){
stock_num = stock_num - 1;
redisTemplate.opsForValue().set("stock",String.valueOf(stock_num));
System.out.println("扣减成功,余额为" + stock_num);
}else {
System.out.println("库存不足");
}
}else {
System.out.println("error");
}
//释放锁,也就是删除key
redisTemplate.delete("lockKey");
return "success";
}
此时的代码不需要synchronized关键字,因为redis就是单线程的,保证只会有一个线程执行redisTemplate.opsForValue().setIfAbsent(“lockKey”, UUID.randomUUID().toString());
二、防止程序出现异常而增加finally语句块
将上面代码升级,防止程序出现异常而没有删除key,造成死锁。
@GetMapping("reduce/stock")
public String reduceStock(){
String lockKey = "lockKey";
try {
//setIfAbsent 如果不存在则设置lockKey,返回true,如果存在lockKey,则放弃更新,返回false
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.randomUUID().toString());
if (!success){
//表示key存在,此时已经被某个线程操作
return "Being operated by another thread";
}
String stock = redisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock)){
Integer stock_num = Integer.parseInt(stock);
if (stock_num > 0){
stock_num = stock_num - 1;
redisTemplate.opsForValue().set("stock",String.valueOf(stock_num));
System.out.println("扣减成功,余额为" + stock_num);
}else {
System.out.println("库存不足");
}
}else {
System.out.println("error");
}
}finally {
//释放锁,也就是删除key
redisTemplate.delete("lockKey");
}
return "success";
}
三、此时如果在执行try语句块的过程中集群中的某个应用突然宕机,则无法执行finally语句块的内容,依然造成了死锁
解决:为key设置过期时间,超过该时间若没有删除,则由redis服务器删除
@GetMapping("reduce/stock")
public String reduceStock(){
String lockKey = "lockKey";
try {
//setIfAbsent 如果不存在则设置lockKey,返回true,如果存在lockKey,则放弃更新,返回false
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, UUID.randomUUID().toString(),30, TimeUnit.SECONDS);
if (!success){
//表示key存在,此时已经被某个线程操作
return "Being operated by another thread";
}
String stock = redisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock)){
Integer stock_num = Integer.parseInt(stock);
if (stock_num > 0){
stock_num = stock_num - 1;
redisTemplate.opsForValue().set("stock",String.valueOf(stock_num));
System.out.println("扣减成功,余额为" + stock_num);
}else {
System.out.println("库存不足");
}
}else {
System.out.println("error");
}
}finally {
//释放锁,也就是删除key
redisTemplate.delete("lockKey");
}
return "success";
}
四、过期时间的问题,假设给某个key设置的过期时间为15s,其执行业务代码假设需要20s,也就是说,当该线程还没有全部执行完该代码时,redis服务器就把key删除了,此时第二个线程进来,又设置了该key,假设第二个线程执行代码的时间为17s,当第二个线程在执行代码的过程中,第一个线程把第二个线程设置的key给删了,那此时又有第三个线程进来,这样就造成在高并发的情况下依然锁不住资源的问题
解决:每个线程设置一个特定的value,在删除该key,判断是不是自己的value,如果相等,则删除
@GetMapping("reduce/stock")
public String reduceStock(){
String lockKey = "lockKey";
String value = UUID.randomUUID().toString();
try {
//setIfAbsent 如果不存在则设置lockKey,返回true,如果存在lockKey,则放弃更新,返回false
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, value,30, TimeUnit.SECONDS);
if (!success){
//表示key存在,此时已经被某个线程操作
return "Being operated by another thread";
}
String stock = redisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock)){
Integer stock_num = Integer.parseInt(stock);
if (stock_num > 0){
stock_num = stock_num - 1;
redisTemplate.opsForValue().set("stock",String.valueOf(stock_num));
System.out.println("扣减成功,余额为" + stock_num);
}else {
System.out.println("库存不足");
}
}else {
System.out.println("error");
}
}finally {
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (value.equals(currentValue)){
//释放锁,也就是删除key
redisTemplate.delete("lockKey");
}
}
return "success";
}
五、异步任务解决key由于超时被redis删除的问题
@GetMapping("reduce/stock")
public String reduceStock(){
String lockKey = "lockKey";
String value = UUID.randomUUID().toString();
Long exprieTime = 30l;
try {
//setIfAbsent 如果不存在则设置lockKey,返回true,如果存在lockKey,则放弃更新,返回false
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, value,exprieTime, TimeUnit.SECONDS);
if (!success){
//表示key存在,此时已经被某个线程操作
return "Being operated by another thread";
}
isExpire(lockKey,exprieTime);
String stock = redisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock)){
Integer stock_num = Integer.parseInt(stock);
if (stock_num > 0){
stock_num = stock_num - 1;
redisTemplate.opsForValue().set("stock",String.valueOf(stock_num));
System.out.println("扣减成功,余额为" + stock_num);
}else {
System.out.println("库存不足");
}
}else {
System.out.println("error");
}
}finally {
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (value.equals(currentValue)){
//释放锁,也就是删除key
redisTemplate.delete("lockKey");
}
}
return "success";
}
@Async
public void isExpire(String key,Long expireTime){
while (true){
String value = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(value)){
//延长key的时间
Long leftTime = redisTemplate.opsForValue().getOperations().getExpire(key);
Long addTime = expireTime / 3;
leftTime = leftTime + addTime;
redisTemplate.expire(key,leftTime,TimeUnit.SECONDS);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
return;
}
}
}
Redisson方式实现
引入redission依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.5</version>
</dependency>
编写配置类
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
使用redisson锁
@GetMapping("reduce/stock")
public String reduceStock(){
String lockKey = "lockKey";
//redisson获取锁对象
RLock lock = redisson.getLock(lockKey);
try {
//执行锁
lock.lock();
String stock = redisTemplate.opsForValue().get("stock");
if (!StringUtils.isEmpty(stock)){
Integer stock_num = Integer.parseInt(stock);
if (stock_num > 0){
stock_num = stock_num - 1;
redisTemplate.opsForValue().set("stock",String.valueOf(stock_num));
System.out.println("扣减成功,余额为" + stock_num);
}else {
System.out.println("库存不足");
}
}else {
System.out.println("error");
}
}finally {
//redisson释放锁
lock.unlock();
}
return "success";
}