上一章结束说到最后进行redis扣减库存和写入MQ
public ResultGeekQ<Boolean> deductStockCache(String goodsId) {
ResultGeekQ<Boolean> resultGeekQ = ResultGeekQ.build();
try {
//redis操作原子性
Long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
if (stock == null) {
log.error("***数据还未准备好***");
resultGeekQ.withError(MIAOSHA_DEDUCT_FAIL.getCode(), MIAOSHA_DEDUCT_FAIL.getMessage());
return resultGeekQ;
}
//redis显示没有库存代表秒杀结束
if (stock < 0) {
log.info("***stock 扣减减少*** stock:{}",stock);
redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
ProductSoutOutMap.productSoldOutMap.put(goodsId, true);
//写zk的商品售完标记true
if (zooKeeper.exists(CustomerConstant.ZookeeperPathPrefix.PRODUCT_SOLD_OUT, false) == null) {
zooKeeper.createNode(CustomerConstant.ZookeeperPathPrefix.PRODUCT_SOLD_OUT,"");
}
//不管zk对应结点原值多少,改成true
if (zooKeeper.exists(CustomerConstant.ZookeeperPathPrefix.getZKSoldOutProductPath(goodsId), true) == null) {
zooKeeper.createNode(CustomerConstant.ZookeeperPathPrefix.getZKSoldOutProductPath(goodsId), "true");
}
if ("false".equals(new String(zooKeeper.getData(CustomerConstant.
ZookeeperPathPrefix.getZKSoldOutProductPath(goodsId), new WatcherApi())))) {
zooKeeper.updateNode(CustomerConstant.ZookeeperPathPrefix.getZKSoldOutProductPath(goodsId), "true");
//监听zk售完标记节点
zooKeeper.exists(CustomerConstant.ZookeeperPathPrefix.getZKSoldOutProductPath(goodsId), true);
}
resultGeekQ.withError(MIAO_SHA_OVER.getCode(), MIAO_SHA_OVER.getMessage());
return resultGeekQ;
}
} catch (Exception e) {
log.error("***deductStockCache error***");
resultGeekQ.withError(MIAO_SHA_OVER.getCode(), MIAO_SHA_OVER.getMessage());
return resultGeekQ;
}
return resultGeekQ;
}
意思就是直接减redis库存,如果减完小于0则需要回弹,redis 减库存成功后就
可以发送消息队列了
public class MQSender {
@Autowired
AmqpTemplate amqpTemplate ;
public void sendMiaoshaMessage(MiaoShaMessage mm) {
try {
String msg = RedisClient.beanToString(mm);
log.info("mq发送订单信息:msg{}",msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
} catch (AmqpException e) {
throw new AmqpException("***mq信息发送失败!***");
}
}
这里就直接发就行,没什么好说的地方,后面是MQ监听端监听消息并减库存
@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
public void receive(String message) {
log.info("receive message:" + message);
MiaoShaMessage mm = RedisClient.stringToBean(message, MiaoShaMessage.class);
MiaoShaUser user = mm.getUser();
long goodsId = mm.getGoodsId();
GoodsVo goods = goodsLogic.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if (stock <= 0) {
return;
}
//判断是否已经秒杀到了
MiaoShaOrder order = mSLogic.getMiaoshaOrderByUserIdGoodsId(Long.valueOf(user.getNickname()),
goodsId);
if (order != null) {
throw new MqOrderException(ResultStatus.GOOD_EXIST);
}
//减库存 下订单 写入秒杀订单
MiaoShaUserVo userVo = new MiaoShaUserVo();
BeanUtils.copyProperties(user, userVo);
// 开始秒杀
ResultGeekQ<OrderInfoVo> msR = miaoshaService.miaosha(userVo, goods);
if(!ResultGeekQ.isSuccess(msR)){
//************************ 秒杀失败 回退操作 **************************************
redisClient.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
if (ProductSoutOutMap.productSoldOutMap.get(goodsId) != null) {
ProductSoutOutMap.productSoldOutMap.remove(goodsId);
}
//修改zk的商品售完标记为false
try {
if (zkApi.exists(CustomerConstant.ZookeeperPathPrefix.getZKSoldOutProductPath(String.valueOf(goodsId)), true) != null) {
zkApi.updateNode(CustomerConstant.ZookeeperPathPrefix.getZKSoldOutProductPath(String.valueOf(goodsId)), "false");
}
} catch (Exception e1) {
log.error("修改zk商品售完标记异常", e1);
}
return;
}
OrderInfoVo orderInfo = msR.getData();
//****************** 如果成功则进行保存redis + flag ****************************
// 将订单信息写入redis
String msKey = CommonMethod.getMiaoshaOrderRedisKey(String.valueOf(orderInfo.getUserId()), String.valueOf(goodsId));
redisClient.set(msKey, msR.getData());
}
判断是否已经秒杀到了,如果没有则进行最后的秒杀,
@Transactional(rollbackFor = Exception.class)
@Override
public ResultGeekQ<OrderInfoVo> miaosha(MiaoShaUserVo user, GoodsVo goods) {
ResultGeekQ<OrderInfoVo> resultGeekQ = ResultGeekQ.build();
try{
//减库存
ResultGeekQ<Boolean> result = goodsService.reduceStock(goods);
if(!ResultGeekQ.isSuccess(result)){
resultGeekQ.withErrorCodeAndMessage(ResultStatus.MIAOSHA_REDUCE_FAIL);
return resultGeekQ;
}
// 下订单
MiaoShaUser Muser = new MiaoShaUser();
BeanUtils.copyProperties(user,Muser);
OrderInfo orderInfo = mSLogic.createOrder(Muser, goods);
OrderInfoVo orderInfoVo = new OrderInfoVo();
BeanUtils.copyProperties(orderInfo,orderInfoVo);
resultGeekQ.setData(orderInfoVo);
return resultGeekQ;
}catch(Exception e){
log.error("***秒杀下订单失败*** error:{}",e);
resultGeekQ.withErrorCodeAndMessage(ResultStatus.MIAOSHA_FAIL);
return resultGeekQ;
}
}
首先数据库减库存,如果成功则生成订单,可以看到这段代码是使用了事务来进行的,其中可能出现回滚的点是扣减库存失败和单人多次生成相同商品的订单
,如果成功彻底成功了
秒杀过程中,前端页面会不断轮询查看是否已经有结果了
function getMiaoshaResult(goodsId){
g_showLoading();
$.ajax({
url:"/miaosha/result",
type:"GET",
data:{
goodsId:$("#goodsId").val(),
},
success:function(data){
if(data.code == 0){
var result = data.data;
if(result < 0){
layer.msg("对不起,秒杀失败");
}else if(result == 0){//继续轮询
setTimeout(function(){
getMiaoshaResult(goodsId);
}, 2000);
}else{
layer.confirm("恭喜你,秒杀成功!查看订单?", {btn:["确定","取消"]},
function(){
window.location.href="/order_detail.htm?orderId="+result;
},
function(){
layer.closeAll();
});
}
}else{
layer.msg(data.message);
}
},
error:function(){
layer.msg("客户端请求有误");
}
});
}
这里可以看到调用了/miaosha/result
public ResultGeekQ<Long> miaoshaResult(Model model, MiaoShaUser user,
@RequestParam("goodsId") long goodsId) {
ResultGeekQ result = ResultGeekQ.build();
model.addAttribute("user", user);
try {
if (user == null) {
result.withError(SESSION_ERROR.getCode(), SESSION_ERROR.getMessage());
return result;
}
// TODO 这里好像和设置的时候的redis的key对不上?
String redisK = CommonMethod.getMiaoshaOrderWaitFlagRedisKey(String.valueOf(user.getNickname()), String.valueOf(goodsId));
//判断redis里的排队标记,排队标记不为空返回还在排队中
//一定要先判断排队标记再判断是否已生成订单,不然又会存在并发的时间差问题
if (redisService.get(redisK+String.valueOf(goodsId),String.class)!=null) {
result.withError(MIAOSHA_QUEUE_ING.getCode(),MIAOSHA_QUEUE_ING.getMessage());
return result;
}
String redisMr = CommonMethod.getMiaoshaOrderRedisKey(String.valueOf(user.getNickname()), String.valueOf(goodsId));
//查询用户秒杀商品订单是否创建成功
Object order = redisService.get(redisMr, OrderInfo.class);
//秒杀成功
if(order != null) {
OrderInfo info = (OrderInfo)order;
result.setData(info.getId());
return result;
}
result.withErrorCodeAndMessage(ResultStatus.MIAOSHA_FAIL);
return result;
} catch (Exception e) {
result.withErrorCodeAndMessage(ResultStatus.MIAOSHA_FAIL);
return result;
}
}
在上一章已经说了,排队标记有点问题没太看懂,后面就是判断是否生成订单
,如果生成订单秒杀完成,返回给前端即可
总结下秒杀系统的流程
仔细看可以发现这里可能有并发问题
如果有三个线程,线程A首先将redis_stock减为0,另外两个线程BC发现redis_stock如果这个时候成功拿到redis库存的线程出现MQ错误,并且在这个时候没拿到redis库存的线程C将内存标记此商品设置为true,线程A发现内存标记已经为true就设置false,这个时候线程B再设置内存标记为true则会出现还有商品存在但是内存已经标记售罄的情况,如下图
但其实这种情况出现的次数应该非常低,因为出现了MQ错误,如果出现错误也不是一个订单的问题了,相比于MQ错误这个应该就是小问题了