锁是针对某个资源的状态,保证其访问的互斥性,在实际使用当中,这个状态一般是一个字符串。使用 Redis 实现锁,主要是将状态放到 Redis 当中,利用其原子性,当其他线程访问时,如果 Redis 中已经存在这个状态,就不允许之后的一些操作。spring boot使用Redis的操作主要是通过RedisTemplate(或StringRedisTemplate )来实现。
现在我们来用spring boot + redis来实现Redis分布式锁
1.首先我们引用Spring-boot所带的Redis的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 了解Redis加锁主要命令
- SETNX(SET if Not exist):当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
- GETSET:将给定 key 的值设为 value ,并返回 key 的旧值。先根据key获取到旧的value,再set新的value。
- EXPIRE 为给定 key 设置生存时间,当 key 过期时,它会被自动删除。
3. Redis加锁
/**
* 加锁
* @param key 锁唯一标志
* @param value当前时间 + 超时时间
* @return
*/
public boolean lock(String key, String value){
if(stringRedisTemplate.opsForValue().setIfAbsent(key,value)){
return true;
}
/**
* 以下代码是防止加锁后出现操作异常,没有运行解锁操作 防止死锁
*/
//获取锁的过期时间
String currentValue = (String)stringRedisTemplate.opsForValue().get(key);
//假设锁过期
if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()){
//获取上一个锁的时间,并设置新锁的时间
String oldValue = (String) stringRedisTemplate.opsForValue().getAndSet(key,value);
//校验是否和上一个锁时间戳相同,如果相同才有权利加锁
if(!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue) ){
return true;
}
}
return false;
4. Redis解锁
/**
* 解锁
* @param key
* @param time
*/
public void unlock(String key,String time){
try {
// 获取锁的时间戳
String currentValue = stringRedisTemplate.opsForValue().get(key);
// 判断传进来的时间戳与锁的时间戳是否相同
if(!Strings.isNullOrEmpty(currentValue) && currentValue.equals(time) ){
// 相同则进行删除锁
stringRedisTemplate.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
log.error("解锁失败,异常{}",e);
}
}
5.模拟秒杀场景
@RestController
@Slf4j
public class RedisController {
@Autowired
private RedisLock redisLock;
/**
* 设置超时时间 5s
*/
private static final int TIMEOUT = 5 * 1000;
@Override
public void Spike(String productId){
//加锁
long time = System.currentTimeMillis() + TIMEOUT;
if(!redisLock.lock(productId,String.valueOf(time))){
return "排队人数太多,请稍后再试.";
}
int stockNum = stock.get(productId);
// 查询该商品库存,为0则活动结束
if(stockNum==0){
throw new OperationFailedException("活动结束");
}else {
// 下单(模拟不同的openId)
orders.put(KeyUtil.getOpenId(),productId);
//减库存 不做处理的话,高并发下会出现超卖的情况,下单数,大于减库存的情况。
//虽然这里减了,但由于并发,减的库存还没存到map中去。新的并发拿到的是原来的库存
stockNum -= 1;
try{
Thread.sleep(100);//模拟减库存的处理时间
}catch (InterruptedException e){
e.printStackTrace();
}
// 存储最新库存数量
stock.put(productId,stockNum);
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
}
}
注:测试并发量可以采用Apache ab并发负载压力进行测试