由于本项目是专门学习Redis的,所以在这里将会使用Redis的setnx指令实现分布式锁解决超卖问题。
- 获取分布式锁
bash
# 添加锁,利用setnx互斥的特性
SETNX key value
# 为锁设置过期时间,超时释放,避免死锁
EXPIRE key time
# 防止在执行完setnx后Redis服务宕机,还没来得及执行expire设置过期时间的情况,保证了分布式锁的安全性。
SET key value EX seconds NX- 释放分布式锁
bash
# 手动释放(还可设置过期时间,超时剔除释放锁)
DEL key获取锁失败后,重试获取锁有两种机制,阻塞式获取和非阻塞式获取:
- 阻塞锁:没有获取到锁,则继续等待获取锁。浪费CPU,线程等待时间较长,实现较麻烦。
- 非阻塞锁:尝试一次,没有获取到锁后,不继续等待,直接返回锁失败。(本次采用非阻塞机制)

- 创建分布式锁
java
/**
* 锁接口
*/
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功; false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
public class SimpleRedisLock implements ILock{
private String name;
private StringRedisTemplate stringRedisTemplate;
/**
* 执行SimpleRedisLock相关业务逻辑。
*
* @param name名称
* @param stringRedisTemplate stringRedisTemplate参数
* @return处理结果
*/
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
private static final String KEY_PREFIX = "lock:";
/**
* 尝试获取Redis互斥锁。
*
* 作用:
* 1.获取线程标识;
* 2.获取锁;
*
* @param timeoutSec timeoutSec参数
* @return处理结果
*/
public boolean tryLock(long timeoutSec) {
//获取线程标识
long threadId=Thread.currentThread().getId();
//获取锁
Boolean success=stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX+name,threadId+"",timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
/**
* 释放Redis互斥锁。
*
* @return无返回值
*/
public void unlock() {
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}- 改造VoucherOrderServiceImpl
java
/**
* 处理优惠券秒杀下单请求。
*
* 作用:
* 1.查询优惠卷;
* 2.判断秒杀是否开始;
* 3.尚未开始;
* 4.判断秒杀是否已经结束;
* 5.已经结束;
*
* @param voucherId优惠券id
* @return处理结果
*/
@Override
public Result seckillVoucher(Long voucherId) {
//1.查询优惠卷
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
//尚未开始
return Result.fail("秒杀尚未开始");
}
//3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
//已经结束
return Result.fail("秒杀已结束");
}
//4.判断库存是否充足
if (voucher.getStock() < 1) {
//库存不足
return Result.fail("库存不足");
}
Long userId=UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order" + userId, stringRedisTemplate);
//获取锁
boolean isLock = lock.tryLock(1200);
//判断是否获取锁成功
if (!isLock) {
//获取锁失败,返回错误或重试
return Result.fail("不允许重复下单");
}
try {
//获取代理对象
IVoucherOrderService proxy= (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} catch (IllegalStateException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}因为锁的是用户id,即同一个用户不能同时下单多次,所以相同用户再次下单获取锁会失败,并且不会再次重试,直接返回错误信息。
- 测试效果
同一个用户只能成功获取一次锁,实现了Redis分布式锁的互斥效果,解决了一人一单集群超卖问题。