redis的依赖先引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
并发锁相关命令介绍
说到redis的并发锁就要先介绍一下这两个redis命令
SETNX key value
将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。
返回值
Integer reply, 特定值:
1 如果key被设置了
0 如果key没有被设置
demo
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis>
GETSET key value
自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误。
demo
redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
redis>
那么下面开始进入redis锁的实现
处理流程
- 1-尝试加锁
- 2-加锁成功,处理业务
- 3-解锁
加锁代码
这里做个demo介绍
假如已经加锁了,但是还没来得及释放锁,因为不可抗力因素,比如停电了,服务突然挂了,导致锁没释放
然后有2个A和B线程进来了,第一步判断加锁肯定失败了,因为之前那个锁没释放嘛,然后进入下面的判断,都获取到原先那个锁的值,并且判断出锁已经超时,
那么这个时候肯定A和B有一个先后顺序,因为getset是原子性的,假设A线程处理先设置值,那么这个时候它拿到的返回值是原先的值,进入判断成功获取锁,然后B线程也接着getSet后拿到了返回值,这个时候B拿到的返回值是A刚set进去的值,和最开始的那个超时key的value不一致,所以获取锁失败
package com.chan.wechatshop.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key,String value){
//这里的setIfAbsent即对应redis的setnx
if(redisTemplate.opsForValue().setIfAbsent(key,value)){
return true;
}
String currentValue = redisTemplate.opsForValue().get(key);
//判断锁是否过期
if(!StringUtils.isEmpty(currentValue) || Long.parseLong(currentValue) < System.currentTimeMillis()){
//获取上一个过期时间
String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
return true;
}
}
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key,String value){
try {
String currentValue = redisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
e.printStackTrace();
log.error("[redis分布式锁] 解锁异常: {}",e.getMessage());
}
}
}
上线这种方式一般情况下是ok的,但是有一种特殊情况下解锁的时候会记录大量的[redis分布式锁] 解锁异常
当一个锁过期的时候,2个线程同时进入加锁流程,因为锁因为其他原因过期了,然后没删除掉,此时进入锁超时判断流程,此时2个线程对于getAndSet肯定会有一个先后的执行顺序,因为其实原子性的.假设第一个线程getAndSet成功了,那么它可以进入方法return true从而加锁成功.第二个线程getAndSet后获得的之前的值是第一个线程set进去的值所以不会进入方法从而返回false.不过此时这个key在redis中存在的值已经是第二个线程set进去的值了,所以第一个线程在解锁的时候因为set进去的value是第二个线程set进去的值了,所以这里用第一个线程自己的value去解锁肯定失败.考虑到显示生产肯定有很多并发的情况,所以这里还是有点问题的.