在第一次的版本中,使用redis模拟秒杀,最终结果,单个线程可以执行,使用了阿帕奇的ab工具,进行了压力测试后,出现了超卖问题,本代码中,针对此问题进行解决。
在redis中,提供了事务的概念,redis的事务在执行过程中,不会被打断,multi开启事务,在此之后的才做将被添加至操作队列中,如图
添加完成后,可以使用exec进行执行
如果想放弃,则可以使用discard取消执行的事务
要想完成秒杀超卖的问题,当然还有一个非常重要的点,redis的watch方法,它是一个乐观锁的命令,会未监控的key增加版本号,在事务执行前,监控key,如果执行中,key的版本号发生变化,则事务取消,不会执行
import com.lixl.redis.utils.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author lixl
* @description 秒杀相关controller
* @date 2022/2/10
*/
@RestController
public class SecKillController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("doSecKill")
public String doSeckill(String goodId){
String userId = RandomUtils.getUserId()+"";
if (!StringUtils.hasText(goodId)){
System.out.println("没有该类商品的秒杀");
return "没有该类商品的秒杀";
}
// 拼接库存key
String kcKey = "good:"+goodId+":kc";
// 拼接秒杀成功用户key
String userKey = "good:"+goodId+":user";
// 判断是否有库存
Boolean hasKey = redisTemplate.hasKey(kcKey);
if (!hasKey){
System.out.println("没有该类商品的秒杀");
return "没有该类商品的秒杀";
}
// 判断用户是否已经完成过秒杀
Boolean isMember = redisTemplate.opsForSet().isMember(userKey, userId);
if (isMember){
System.out.println("您已经秒杀成功,请勿重复秒杀!");
return "您已经秒杀成功,请勿重复秒杀!";
}
// 开启redis的事务,使用redisTemplate开启事务,需要用到SessionCallback,具体事务在它的模块中实现
SessionCallback sessionCallback = new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
// 使用redis提供的监控数量功能(乐观锁,给每个数据一个版本号,不符合版本号的,无法执行)
redisOperations.watch(kcKey);
// 判断库存数量是否够
String goodsNum = redisOperations.opsForValue().get(kcKey).toString();
System.out.println(goodsNum);
if (Integer.parseInt(goodsNum)<=0){
return "秒杀已经结束,抢购失败!";
}
redisOperations.multi();
// 库存数量-1
redisOperations.opsForValue().decrement(kcKey);
// 设置用户
redisOperations.opsForSet().add(userKey,userId);
// 执行事务
List exec = redisOperations.exec();
if (exec == null || exec.size() == 0){
return "很遗憾,您未秒杀到该商品";
} else {
return "恭喜您,秒杀成功!";
}
}
};
// 执行事务
Object execute = redisTemplate.execute(sessionCallback);
System.out.println(execute.toString());
return execute.toString();
}
}
使用ab工具
ab -n 1000 -c 100 http://localhost:8080/doSecKill?goodId=1001
发送1000次请求 并发100
可以看到,最后只有0个,并未出现超卖问题,但是在情况下,如果设置多一点的库存,多一点的并发,又会怎样(500库存,1000请求,500并发)
此时可以看到,还有342个商品,未被秒杀到,还需要接着完善代码