本节是此项目核心问题,保证在高并发情况下选课业务能够高效、正确的完成。
1.在进行选课前将课程库存提前加载到Redis中:
//在抢课Controller中实现InitializingBean接口
//初始化时执行将库存预加载到Redis
@Override
public void afterPropertiesSet() throws Exception {
List<CourseVo> list = courseService.findCourseVo();
if (CollectionUtils.isEmpty(list)) {
return;
}
list.forEach(CourseVo-> {
redisTemplate.opsForValue().set("seckillCourese:"+CourseVo.getId(),CourseVo.getStockCount());
if(CourseVo.getStockCount() > 0)
emptyStockMap.put(CourseVo.getId(),false);
else
emptyStockMap.put(CourseVo.getId(),true);
});
}
2.抢课Controller
//使用前后端分离,对象缓存减少前端页面的数据访问,同时使用Redis判断是否重复抢购
@RequestMapping(value = "/{path}/doSeckill",method = RequestMethod.POST)
@ResponseBody
public RespBean doSecKill(@PathVariable String path,User user,Long CourseId){
if(user == null)
return RespBean.error(RespBeanEnum.SESSION_ERROR);
boolean check = orderService.checkPath(user,CourseId,path);
if(!check){
return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);
}
//判断是否重复抢购
TSeckillOrder seckillOrder =
(TSeckillOrder) redisTemplate.opsForValue().get("order:"+user.getId()+":"+CourseId);
if(seckillOrder != null){
return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
if(emptyStockMap.get(CourseId)){
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
//通过Redis预减库存
ValueOperations valueOperations = redisTemplate.opsForValue();
//原子性预减库存操作
Long stock = valueOperations.decrement("seckillCourse:"+CourseId);
//也可以使用Redis结合lua脚本
// Long stock = (Long) redisTemplate.execute(script, Collections.singletonList("seckillCourse:"+CourseId),
// Collections.EMPTY_LIST);
if(stock < 0){
emptyStockMap.put(CourseId,true);
valueOperations.increment("seckillCourse:"+CourseId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
SeckillMessage seckillMessage = new SeckillMessage(user, CourseId);
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
// 通过RabbitMQ消息队列下单,0状态表示排队中
return RespBean.success(0);
}
3.配置RabbitMQ
@Configuration
public class RabbitMQTopicConfig {
private static final String QUEUE = "seckillQueue";
private static final String EXCHANGE = "seckillExchange";
@Bean
public Queue queue() {
return new Queue(QUEUE);
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(EXCHANGE);
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
}
4.消息发送
@Service
@Slf4j
public class MQSender {
@Autowired
private RabbitTemplate rabbitTemplate;
//发送秒杀信息
public void sendSeckillMessage(String msg){
log.info("发送"+msg);
rabbitTemplate.convertAndSend("seckillExchange","seckill.msg",msg);
}
}
5.消息接收
@Service
@Slf4j
public class MQReceiver {
@Autowired
private ITCourseService courseService;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ITOrderService orderService;
//接收消息,实际上进行下单操作
@RabbitListener(queues = "seckillQueue")
public void receive(String msg){
log.info("接收"+msg);
SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(msg, SeckillMessage.class);
Long courseId = seckillMessage.getCourseId();
User user = seckillMessage.getUser();
CourseVo courseVobyCourseId= courseService.findCourseVobyCourseId(courseId );
if(courseVobyCourseId.getStockCount() < 1){
return;
}
//判断是否重复抢购
TSeckillOrder seckillOrder =
(TSeckillOrder) redisTemplate.opsForValue().get("order:"+user.getId()+":"+courseId );
if(seckillOrder != null){
return ;
}
//下单
orderService.secKill(user,courseVobyCourseId);
}
}
6.选课成功sevice下单逻辑
@Transactional
@Override
public TOrder secKill(User user, CourseVo CourseVo) {
ValueOperations valueOperations = redisTemplate.opsForValue();
//从后端重新查询库存,拿到秒殺商品信息
TSeckillCourse seckillCourse = itSeckillCourseService.getOne(new QueryWrapper<TSeckillCourse>().eq("Course_id", CourseVo.getId()));
//库存减一
seckillCourse.setStockCount(seckillCourse.getStockCount() - 1);
//id没问题同时库存>0才更新
boolean seckillCourseResult = itSeckillCourseService.update(new UpdateWrapper<TSeckillCourse>()
.setSql("stock_count = " + "stock_count-1")
.eq("Course_id", CourseVo.getId())
.gt("stock_count", 0)
);
if (seckillCourse.getStockCount() < 1) {
valueOperations.set("isStockEmapty:"+ CourseVo.getId(),"0");
return null;
}
//生成订单
TOrder order = new TOrder();
order.setUserId(user.getId());
order.setCourseId(CourseVo.getId());
order.setDeliveryAddrId(0L);
order.setCourseName(CourseVo.getCourseName());
order.setCourseCount(1);
order.setCoursePrice(seckillCourse.getSeckillPrice());
order.setOrderChannel(1);
order.setStatus(0);
order.setCreateDate(new Date());
tOrderMapper.insert(order);
TSeckillOrder tSeckillOrder = new TSeckillOrder();
tSeckillOrder.setUserId(user.getId());
tSeckillOrder.setOrderId(order.getId());
tSeckillOrder.setCourseId(CourseVo.getId());
itSeckillOrderService.save(tSeckillOrder);
//这里使用Redis缓存用户id和订单id作为联合key,在高并发时订单控制器可以先进行查询防止一个人多次
redisTemplate.opsForValue().set("order:" + user.getId() + ":" + CourseVo.getId(),
tSeckillOrder, 1, TimeUnit.MINUTES);
return order;
}
7.前端通过轮询得知抢课是否成功
function doSeckill(path) {
$.ajax({
url: '/seckill/'+path+'/doSeckill',
type: "POST",
data: {
courseId: $('#courseId').val()
// path:path改用注解接收
},
success: function (data) {
if (data.code == 200)
//使用RabbitMQ轮询时
getResult($("#courseId").val());
} else {
layer.msg(data.message);
}
}, error: function () {
layer.msg("客户端请求出错");
}
});
}
function getResult(courseId){
g_showLoading();
$.ajax({
url:"/seckill/result",
type:"GET",
data: {
courseId: courseId
},
success: function (data) {
if (data.code == 200) {
var result = data.object;
if (result < 0) {
layer.msg("对不起,秒杀失败");
} else if (result == 0) {
setTimeout(function () {
getResult(courseId)
},50);
} else {
layer.confirm("恭喜您,秒杀成功!查看订单?", {btn: ["确定", "取消"]},
function () {
window.location.href = "/orderDetail.htm?orderId=" + result;
},
function () {
layer.close();
}
)
}
}
},
error: function () {
layer.msg("客户端请求错误");
}
});
}
//获取抢课结果,成功返回订单id,失败-1,正在排队0
@RequestMapping(value = "/result",method = RequestMethod.GET)
@ResponseBody
public RespBean getResult(User user, Long goodsId){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
Long orderId = seckillOrderService.getResult(user,goodsId);
return RespBean.success(orderId);
}