前言
在上一篇,通过实例演示了在高并发场景下多人抢购优惠券的超卖问题,并且利用redis+lua解决了超卖问题,但是一人只能抢一单的问题是否还在呢?
一人抢多单压测
可以发现,在经过改造时候,虽然解决了超卖问题,但是一人可以抢购多单的问题仍然存在
为什么会存在这个问题呢?简单分析下面这段代码,扣减库存和下单操作分割为2步,在多线程并发抢购时,第二步判断用户是否抢过和下面的第三步,第四步都是隔开的,
假设多个线程都走到第二步,这时候发现当前1111这个用户还没有抢过,继续走到第二步时候,当库存不为0时,从这一步到下单这一步,又是两个不同的步骤,因此多个线程存在交叉的情况,于是就出现了一人抢多单的情况
因此,理解了这一点,想必解决办法就出来了,将扣库存和下单的两步加锁不就可以解决吗?下面我们使用redis分布式锁进行处理,为简单起见,这里就直接使用redis比较简单的一种分布式锁redisson
1、添加依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.0</version>
</dependency>
2、使用redisson 的锁改造抢购代码
@Transactional(rollbackFor = Exception.class)
public String doSkill(int vocherId,int userId){
//1、判断代金券是否加入活动了
String key = RedisKeyConstants.skill_vochers.getKey() + vocherId;
Map<String,Object> entries = redisTemplate.opsForHash().entries(key);
SkillVocher skillVocher = BeanUtil.mapToBean(entries,SkillVocher.class,true);
if(skillVocher==null){
throw new RuntimeException("该券未加入抢购活动");
}
//2、判断当前用户是否已经抢过
VochersOrders defineOrder = vocherOrderMapper.findDefineOrder(userId, skillVocher.getId());
if(defineOrder != null){
throw new RuntimeException("该用户已经抢到过代金券,无需再抢了");
}
//3、扣减库存
/*long count = redisTemplate.opsForHash().increment(key, "amount", -1);
if(count < 0){
throw new RuntimeException("该券已经卖完了");
}*/
String keyLock = RedisKeyConstants.skill_lock.getKey() + userId + ":" + vocherId;
RLock lock = redissonClient.getLock(keyLock);
lock.lock();
try{
//3、扣减库存采用 redis + lua
List<String> keys = new ArrayList<>();
keys.add(key);
keys.add("amount");
Long amount = (Long)redisTemplate.execute(defaultRedisScript, keys);
if(amount == null || amount <1) {
throw new RuntimeException("该券已经卖完了");
}
//4、下单
VochersOrders vochersOrders = new VochersOrders();
vochersOrders.setId(atomicInteger2.incrementAndGet());
vochersOrders.setUserId(userId);
vochersOrders.setSkillOrderId(skillVocher.getId());
vochersOrders.setVocherId(skillVocher.getVocherId());
String orderNo = UUID.randomUUID().toString().replaceAll("-","").substring(4,9);
vochersOrders.setOrderNo(orderNo);
vochersOrders.setOrderType(1);
vochersOrders.setStatus(0);
int saveCount = vocherOrderMapper.save(vochersOrders);
if(saveCount !=0){
return "skill success";
}else {
return "skill fail";
}
}catch (Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
lock.unlock();
}
return "null";
}
关于redisson实现分布式锁的讲解在我的另一篇博客中有提到,有兴趣的同学可以查阅,redisson提供了丰富已用的API供我们使用,相比其他的方式显得更简单而且容易上手,但在使用redisson的分布式锁的时候,最好使用tryLock并且带有尝试时间的那种方式
这样修改之后,我们再次使用jemeter的一人抢多单的线程组进行压测,观察数据结果
amount扣减1
数据库订单也只产生了一个
我们不妨再压测一次,最后发现数据仍然是正确的
通过上面的案例演示,采用redis的分布式锁解决了秒杀抢购中的一人一单的问题,有兴趣的同学可以继续深入探究,本篇到此结束,最后感谢观看!