秒杀中的常见问题的解决
1)解决超卖的问题
1)Redis预减库存,有一个下单请求过来时预减库存,若减完后的redis库存小于0说明已经卖完,此时直接返回客户端已经卖完。后续使用内存标记,减少Redis访问。若预减库存成功,则异步下单,请求入队列,返回客户端排队中。
2)数据库层面防止超卖:Redis预减库存只是抢到了这个机会,真正是否购买成功还是要等到所有数据库操作的真正成功,即消息队列的消费端是否消费成功。
数据库层面,秒杀的订单表设置唯一索引,防止重复下单。
数据库层面,减库存的时候同时判断此时库存是否大于0。
由于是订单模块和库存模块,涉及到分布式事务的问题,使用seata框架。
2)如何保证Redis的库存与数据库库存的一致性
在我们的项目中,Redis的库存并不是真正的库存,而是用于阻挡多余的下单请求,用于保证有多少秒杀商品库存就放多少个请求到消息队列,大大减少数据库访问。
真正的下单和减库存操作还是操作数据库的。
所以我们不需要保证Redis缓存与数据库的一致性。
3)如何保证MQ不丢失消息
消息丢失的三种情况:
- **生产者丢失消息:**生产者发送消息到MQ时因为网络问题丢失消息。
- **MQ丢失消息:**没来得及持久化,就挂掉后消息丢失
- **消费端丢失消息:**刚从MQ获取消息,没处理完消费者就挂掉了
1 生产者丢失消息的解决方案:
1) rabbitmq提供事务支持,在生产者发送之前开启事务,然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会受到异常报错,这时就可以回滚事物,然后尝试重新发送;如果收到了消息,那么就可以提交事物。但是这种方案会阻塞生产者,吞吐量下降。
2)可以将channel开启confirm机制。在生产者哪里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后写入了rabbitmq之中;**如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。**如果成功发送到mq,也会回调一个ack的接口方法,告诉你成功发送消息。
这里使用方案二!这样吞吐量更高。
2 MQ自己丢失数据的解决方案
设置持久化!持久化有两个步骤:
1)创建queue时设置持久化,但是这时候持久化的是queue的元数据,不会持久化queue里面的数据。
2)发送消息的时候将消息的deliveryMode设置为2,表示将消息持久化。
而且可以将持久化与生产者的confirm机制结合,只有持久化成功后才回调ack方法。超时未持久化成功或持久化失败也会回调nack。
3 消费者丢失数据的解决方案:
消费者端丢失数据都是因为开启了rabbitmq的autoACK功能,即消费者获取了数据之后就自动告诉MQ已经消费。
**解决方案:**关闭rabbitmq的autoAck,在确保消息被消费成功之后才发送ACK。消息没有成功消费的话rabbitmq会重发消息,这样能保证消息不会再消费者端丢失。
4)MQ如何保证不重复消费
这里不保证不重复消费,因为保证了消息不丢失就有可能读取重复的消息。这里保证接口的幂等性即可。
在保证幂等性的基础上,因为写入MQ中的数据都有一个唯一编号,当MQ消费成功后立即往redis中写入该编号。在消费端,读取MQ数据后先判断是否已经消费过。
5)如何保证分布式事务
在项目中,MQ消费端需要保证下订单和减库存同时成功或失败,这就涉及到事务的问题。
由于是分布式架构,每一个模块对应自己的数据库,跨库之间的事务就需要分布式事务解决方案。
当然,还有一种简单的方案,秒杀模块单独建立自己的订单表、自己的秒杀商品库存表,即在秒杀下单业务中没有调用其他模块的接口,此时也就简单了,没有分布式事务的存在,采用本地事务解决问题。
在这里,暂且考虑分布式事务,学技术为主。尝试使用SpringCloudAlibaba提供的Seata组件去完成分布式事务,只需要加上@GlobalTransaction
注解
通过TC、TM、RM三个组件完成:全局事务管理者、事务发起方、事务的参与方。
Seata事务的执行流程(默认是使用二阶段提交):
- TM开启分布式事务(TM向TC注册全局事务记录)
- 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态)
- TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚事务)
- TC汇总事务信息,决定事务是提交还是回滚
- TC通知所有的RM提交或回滚资源,事务的二阶段结束。
在MQ消费端:
@RabbitListener(queues = MQConfig.SECKILL_QUEUE)
public void receiveSkInfo(Channel channel, MSMessage message, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
String courseId = message.getCourseId();
String id = message.getId();
String memberId = message.getMemberId();
//这里不需要判断库存,而是执行SQL的时候检查是否超卖
// 获取商品的库存
// QueryWrapper<MsCourse> wrapper = new QueryWrapper<>();
// wrapper.eq("course_id", courseId);
//
// MsCourse msCourse = msCourseService.getOne(wrapper);
// Integer stockCount = msCourse.getCount();
// if (stockCount <= 0) {
// //此时已经卖完了
// channel.basicAck(tag, false); //手动确认
// return;
// }
// 判断是否已经秒杀到了(保证幂等性)
QueryWrapper<MsOrder> wrapper = new QueryWrapper<>();
wrapper.eq("course_id", courseId);
wrapper.eq("member_id", memberId);
MsOrder one = msOrderService.getOne(wrapper);
if (one != null) {
channel.basicAck(tag, false); //手动确认
return;
}
// 1.减库存 2.写入订单 3.写入秒杀订单
try {
boolean flag = msOrderService.createOrders(id, courseId, memberId);
channel.basicAck(tag, false); //手动确认
}catch (Exception e){
e.printStackTrace();
channel.basicNack(tag, false, false); //
}
}
减库存、写订单的全局事务处理
@Override
@GlobalTransaction(rollbackFor=Exception.class) //分布式事务
public boolean createOrders(String orderId, String courseId, String memberId) throws Exception {
//减库存
int res = courseService.reduceCount(courseId);//这条SQL语句就会检查超卖的情况
if(res <= 0) throw new RuntimeException("减库存失败");
//写订单
MsOrder msOrder = new MsOrder();
msOrder.setId(orderId);
msOrder.setCourseId(courseId);
msOrder.setMemberId(memberId);
flag = orderService.save(msOrder);
if(!flag) throw new RuntimeException("写入订单失败");
return true;
}
-- 在减库存时同时查询库存数量是否大于0,返回结果如果为0,说明更新失败,库存此时不是大于0
update ms_course set count = count -1 where course_id=#{courseId} and count>0;
请求下订单时的逻辑
//用于标记商品是否卖完。HashMap虽然不是线程安全,但是不影响,因为只会写入true。
private volatile Map<String, Boolean> localMap = new HashMap<String, Boolean>();
@PostMapping("createOrder/{courseId}")
@ApiOperation("下订单")
public R createOrder(@PathVariable("courseId") String courseId, HttpServletRequest request) {
// 1 判断用户是否登录
// String memberId = JwtUtils.getMemberIdByJwtToken(request);
// 随机生成用户Id,用于压测
String memberId = UUID.randomUUID().toString().substring(0,18);
if(StringUtils.isEmpty(memberId)){
return R.ok().code(20001).message("请先登录");
}
//2 生成订单
// 2.1基于内存判断商品是否已经秒杀完毕,减少redis访问
Boolean flag = localMap.get(courseId);
if (flag != null && flag) {
return R.error().message("已卖完");
}
// 2.2 判断是否重复秒杀(单机redis不用分布式锁,而且后面会使用redis的setnx往redis中写入订单信息,不会导致重复下单)
String orderKey = "order::"+memberId+"::"+courseId;
Boolean hasOrder = redisTemplate.hasKey(orderKey);
if(hasOrder){
return R.error().message("重复秒杀");
}
// 2.3 在redis中预减库存
//预减库存
String stock = "msCourse::" + courseId + "::count";
Long count = redisTemplate.opsForValue().decrement(stock);
if (count >= 0) {
if(count == 0) localMap.put(courseId, true);
//生成订单信息,加入MQ去持久化
// 使用redis的setnx保证不会重复下单,虽然2.2中判断了是否重复下单,但这里在多线程下任然可能重复
if(redisTemplate.opsForValue().setIfAbsent(orderKey, "", 60, TimeUnit.SECONDS)){
//生成订单。
String orderId = UUID.randomUUID().toString().replace("-", "").substring(0, 18);
MSMessage msMessage = new MSMessage(orderId, courseId, memberId);
mqProvider.sendMessage(msMessage);
return R.ok().message("校验订单,请稍后");
}else {
redisTemplate.opsForValue().increment("msCourse::" + courseId + "::count");
return R.error().message("重复秒杀");
}
}else {
//小于0说明库存为空还去减,此时要将库存加回去
redisTemplate.opsForValue().increment("msCourse::" + courseId + "::count");
return R.error().message("已卖完");
}
}
}