欧美三级国产三级日韩三级_亚洲熟妇丰满大屁股熟妇_欧美亚洲成人一区二区三区_国产精品久久久久久模特

暢購商城(十四):秒殺系統(tǒng)「下」 - 新聞資訊 - 云南小程序開發(fā)|云南軟件開發(fā)|云南網(wǎng)站建設-昆明葵宇信息科技有限公司

159-8711-8523

云南網(wǎng)建設/小程序開發(fā)/軟件開發(fā)

知識

不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價值,我們在追求其視覺表現(xiàn)的同時,更側(cè)重于功能的便捷,營銷的便利,運營的高效,讓網(wǎng)站成為營銷工具,讓軟件能切實提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序為后期升級提供便捷的支持!

您當前位置>首頁 » 新聞資訊 » 技術分享 >

暢購商城(十四):秒殺系統(tǒng)「下」

發(fā)表時間:2020-10-19

發(fā)布人:葵宇科技

瀏覽次數(shù):78

好好學習,天天向上

本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star

  • 暢購商城(一):環(huán)境搭建
  • 暢購商城(二):分布式文件系統(tǒng)FastDFS
  • 暢購商城(三):商品管理
  • 暢購商城(四):Lua、OpenResty、Canal實現(xiàn)廣告緩存與同步
  • 暢購商城(五):Elasticsearch實現(xiàn)商品搜索
  • 暢購商城(六):商品搜索
  • 暢購商城(七):Thymeleaf實現(xiàn)靜態(tài)頁
  • 暢購商城(八):微服務網(wǎng)關和JWT令牌
  • 暢購商城(九):Spring Security Oauth2
  • 暢購商城(十):購物車
  • 暢購商城(十一):訂單
  • 暢購商城(十二):接入微信支付
  • 暢購商城(十三):秒殺系統(tǒng)「上」
  • 暢購商城(十四):秒殺系統(tǒng)「下」

防止秒殺重復排隊

回顧一下上一篇文章中講到的下單的流程。當用戶點擊下單之后,用戶名和商品id就會組裝成一個SeckillStatus對象存入Redis隊列中等待被處理,這個過程叫做排隊。所以說,只要用戶點擊了一次下單后不論最后是否下單成功,他都會進入到排隊的狀態(tài)。如果用戶重復點擊下單,那么Redis隊列中就會有很多個相同的SeckillStatus對象,也就是一個用戶排隊多次,這顯然是不符合邏輯的,一個用戶應該只能排隊一次。

為了避免用戶重復排隊的情況,可以為每個用戶在Redis中設置一個自增值,每次排隊的時候加1,如果大于1,說明重復排隊了,那么直接拋出異常,告訴用戶重復排隊了。

//SeckillOrderServiceImpl
@Override
public boolean add(Long id, String time, String username) {
    Long increment = redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_QUEUE_COUNT).increment(username, 1);
    if (increment>1) {  //記錄指定hashkey的增量,大于1說明排隊次數(shù)超過1次,重復排隊
        throw new RuntimeException("重復排隊");
    }
	…………
}

這段代碼中,添加了對用戶重復排隊的判斷,先自增1,再進行判斷。這里的key設置的是username,因為一個用戶只能下單一件商品,如果去下單其它商品,同樣也是重復排隊。

測試了一下,是成功的。但是有一個問題:如果用戶在這里排隊未成功,該怎么清理排隊信息呢?這個下一步就會說,接著往下看👇

并發(fā)超賣問題解決

現(xiàn)在的代碼看似很完美,但是漏洞百出,比如就存在并發(fā)超賣的問題。為什么這么說,看代碼說話:

這個是多線程下單的方法,流程是查庫存——>下單——>減庫存。假如現(xiàn)在有件商品還剩1件,正好有多個線程同時走到了查詢庫存這一步,結果查出來都是一件,然后這三個線程就可以往下接著走,最后三個線程都成功下單了,不就多賣了兩件嘛。所以這段代碼還存在問題,那怎么解決呢?可不可以采用加鎖的方法,不可以。因為如果是在集群環(huán)境下,一臺機器上多個線程走到了同一步確實可以鎖住防止超賣,但是不同機器上的線程走到了同一部就鎖不住了。

所以可以采用Redis隊列的方式去解決。

給每個sku創(chuàng)建一個隊列,比如id為4399的商品數(shù)量為4,那么就在4399的隊列里放入4件商品。然后每次查詢就從隊列里去取,假如現(xiàn)在有五個線程去查庫存,因為只有4件商品,所以5個線程只有4個線程能夠查詢出庫存。因為Redis是單線程的,所以不會出現(xiàn)多個線程同時訪問數(shù)據(jù)出錯的情況,這樣就可以避免并發(fā)超賣的問題。

之前在SeckillGoodsPushTask中只是將商品存入Redis中,現(xiàn)在再加一步,為每個sku都創(chuàng)建一個隊列并存入庫存數(shù)量的數(shù)據(jù)到隊列中。

//定時將秒殺商品加載到redis中
@Scheduled(cron = "0/5 * * * * ?")
public void loadGoodsPushRedis() {
		…………
        for (SeckillGoods seckillGood : seckillGoods) {
            boundHashOperations.put(seckillGood.getId(),seckillGood);   //把商品存入到redis
            redisTemplate.boundListOps(SystemConstants.SEC_KILL_GOODS_COUNT_LIST + seckillGood.getId())
                    .leftPushAll(getGoodsNumber(seckillGood.getNum()));	//存到Redis隊列
        }
    }
}

//獲取秒殺商品數(shù)量的數(shù)組
public Byte[] getGoodsNumber(int num) {
    Byte[] arr = new Byte[num];
    for (int i = 0; i < num; i++) {
        arr[i] = '0';
    }
    return arr;
}

隊列的內(nèi)容就是商品數(shù)量的Byte,視頻中用的是商品id,但是商品id是Long型的,Byte比Long要省空間,而且放什么無所謂關鍵是放幾個,所以我就放了對應數(shù)量的Byte進去。

接下來就該在下單之前獲取庫存的信息:

@Async
public void createOrder() {
	…………
    //從秒殺商品隊列中獲取數(shù)據(jù),如果獲取不到則說明已經(jīng)賣完了,清除掉排隊信息
    Object o = redisTemplate.boundListOps(SystemConstants.SEC_KILL_GOODS_COUNT_LIST + seckillGoods.getId())
            .rightPop();
    if (o == null) {
        redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_QUEUE_COUNT).delete(seckillStatus.getUsername());  //清除排隊隊列
        redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY).delete(seckillStatus.getUsername());   //排隊狀態(tài)隊列
        return;
    }
    //創(chuàng)建秒殺訂單
    …………
}

如果商品庫存不足,那么應該清除掉排隊的信息,否則用戶該商品下不了單還不能下單其它商品。這里將排隊的隊列以及查詢狀態(tài)的隊列清除了。

同步庫存不精準問題

解決了并發(fā)超賣的問題之后,還有一個庫存數(shù)量不精準的問題。這個問題出現(xiàn)的原因和超賣問題類似,假如現(xiàn)在同時有兩個線程下單完成了開始遞減庫存,A線程查詢出庫存有3個,B線程也查詢出庫存有3個,然后它們同時遞減,都是2個,寫到了數(shù)據(jù)庫中。其實此時庫存應該還剩一個。

解決的辦法也很簡單,因為現(xiàn)在是調(diào)用**seckillGoods.getStockCount()**查詢出的庫存,那我們就不用這個查詢,直接用上一節(jié)中的隊列,隊列中剩余多少就說明現(xiàn)在的庫存是多少,絕對準確。

@Async
public void createOrder() {
	…………
	//減庫存,如果庫存沒了就從redis中刪除,并將庫存數(shù)據(jù)寫到MySQL中
    //seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
    Long size = redisTemplate.boundListOps(SystemConstants.SEC_KILL_GOODS_COUNT_LIST + seckillGoods.getId()).size();//獲取庫存
    //if (seckillGoods.getStockCount() <= 0) {
    seckillGoods.setNum(size.intValue());
    if (size <= 0) {
        seckillGoodsBoundHashOps.delete(seckillStatus.getGoodsId());
        seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);
    } else {
        seckillGoodsBoundHashOps.put(seckillStatus.getGoodsId(),seckillGoods);
    }
    //創(chuàng)建秒殺訂單
    …………
}    

秒殺支付

改造二維碼創(chuàng)建及支付結果通知方法

秒殺支付的流程和之前做的類似,只不過現(xiàn)在秒殺訂單的支付狀態(tài)發(fā)送到Queue2中,普通訂單還是發(fā)送到queue1中,但是我們怎么知道該將訂單的支付狀態(tài)發(fā)送給queue1還是queue2呢?如果微信服務器可以將MQ隊列的exchange和routingKey返回給我們就好了,這樣我們就可以動態(tài)地指定要發(fā)送的MQ了。

從微信支付的官方文檔中我們可以知道在創(chuàng)建二維碼和接收支付結果的參數(shù)中都有一個attach參數(shù)

這個是自定義的數(shù)據(jù),也就是說我們在創(chuàng)建二維碼的時候發(fā)送給微信服務器什么,返回支付結果的時候就會返回給我們什么。所以在創(chuàng)建二維碼的時候由前端將指定的exchange和routingKey發(fā)送給后端,然后再添加到attach參數(shù)中。就可以實現(xiàn)將不同的訂單動態(tài)地發(fā)送到指定的隊列了。

普通訂單:exchange:exchange.order routingKey:routing.order

秒殺訂單:exchange:exchange.seckill_order routingKey:routing.seckill_order

由于之前寫的createNative方法是接收一個order對象,所以在Order里面添加兩個字段:

private String exchange;    //mq交換機的名稱
private String routingKey; 	//mq的路由鍵

修改之前**createNative()**的代碼,添加attach參數(shù),

@Override
public Map<String, String> createNative(Order order) {
		…………
        //獲取exchange和routingKey,封裝程map集合,添加到attach參數(shù)中
        String exchange = order.getExchange();
        String routingKey = order.getRoutingKey();
        Map<String,String> attachMap = new HashMap<>(2);
        attachMap.put("exchange",exchange);
        attachMap.put("routingKey",routingKey);
        String attach = JSON.toJSONString(attachMap);
        map.put("attach",attach);
		…………
}

然后再修改**WeChatPayController.notifyUrl()**方法,從服務器返回的Map集合中獲取attach,并從attach中獲取exchange和routingKey。

@RequestMapping("/notify/url")
public String notifyUrl(HttpServletRequest request) throws Exception {
	…………
    Map<String, String> xmlMap = WXPayUtil.xmlToMap(xmlString);
    String attach = xmlMap.get("attach");
    Map<String, String> attachMap = JSONObject.parseObject(attach, Map.class);

    //將java對象轉(zhuǎn)換成amqp消息發(fā)送出去,調(diào)用的是send方法
    //rabbitTemplate.convertAndSend("exchange.order","routing.order", xmlString);
    rabbitTemplate.convertAndSend(attachMap.get("exchange"),attachMap.get("routingKey"), xmlString);
	…………
}

監(jiān)聽秒殺

前面已經(jīng)將消息發(fā)送到消息隊列中了現(xiàn)在就可以去監(jiān)聽消息隊列了。

從流程圖中可以看到,在寫監(jiān)聽的方法之前,需要有兩個方法:改訂單狀態(tài)和刪除訂單。

SeckillOrderServiceImpl.updatePayStatus

public void updatePayStatus(String username, String transactionId, String endTime) {
    //從Redis中將訂單信息查詢出來
    SeckillOrder order = (SeckillOrder) redisTemplate
        .boundHashOps(SystemConstants.SEC_KILL_ORDER_KEY)
        .get(username);
    if (order != null) {
        try {
            order.setStatus("1");
            order.setTransactionId(transactionId);
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
            order.setPayTime(simpleDateFormat.parse(endTime));
            seckillOrderMapper.insertSelective(order);  //將訂單信息存到mysql中
			
            //刪除redis中的訂單信息
            redisTemplate.boundHashOps(SystemConstants.SEC_KILL_ORDER_KEY).delete(username);    

            //刪除用戶的排隊信息
            redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_QUEUE_COUNT).delete(username);  //清除排隊隊列
            redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY).delete(username);   //排隊狀態(tài)隊列
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

首先將訂單信息從Redis中查詢出來,將訂單狀態(tài)改為已支付,然后將交易流水號和支付時間補充完整存入MySQL。這時候交易已經(jīng)完成了,可以將訂單信息從Redis中刪除,并將用戶的排隊信息也一并刪除。

SeckillOrderServiceImpl.deleteOrder

public void deleteOrder(String username) {
    //刪除Redis中的訂單
    redisTemplate.boundHashOps(SystemConstants.SEC_KILL_ORDER_KEY).delete(username);

    //刪除用戶的排隊信息
    redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_QUEUE_COUNT).delete(username);  //清除排隊隊列
    redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY).delete(username);   //排隊狀態(tài)隊列

    //查詢出秒殺的狀態(tài)信息
    SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundHashOps(SystemConstants.SEC_KILL_USER_STATUS_KEY)
            .get(username);

    //回滾庫存
    SeckillGoods seckillGoods = (SeckillGoods) redisTemplate
            .boundHashOps(SystemConstants.SEC_KILL_GOODS_PREFIX + seckillStatus.getTime())
            .get(seckillStatus.getGoodsId());
    if (seckillGoods == null) {
        seckillGoodsMapper.selectByPrimaryKey(seckillGoods.getId());
        seckillGoods.setStockCount(seckillGoods.getStockCount()+1);
        seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);
    } else {
        seckillGoods.setStockCount(seckillGoods.getStockCount()+1);
    }
    redisTemplate
            .boundHashOps(SystemConstants.SEC_KILL_GOODS_PREFIX + seckillStatus.getTime())
            .put(seckillGoods.getId(),seckillGoods);

    //將商品放入隊列
    redisTemplate.boundListOps(SystemConstants.SEC_KILL_GOODS_COUNT_LIST + seckillGoods.getId())
            .leftPush("0");
}

支付失敗了,應該將訂單刪除掉。首先將Redis中的訂單刪除,然后刪除用戶的排隊信息。接著回滾庫存,如果Redis中沒有則說明已經(jīng)賣完了,就從MySQL中查詢出來然后將商品數(shù)量加1再存入MySQL;如果Redis中有數(shù)據(jù)就將Redis中的商品數(shù)量加1即可。上面講防止并發(fā)超賣的時候不是為每個商品都在Redis隊列中存放了一下么,所以最后將商品放回到隊列中。

SeckillMessageListener

@Component
@RabbitListener(queues = "queue.seckillorder")
public class SeckillMessageListener {

    @Autowired
    private SeckillOrderService seckillOrderService;

    //https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8
    @RabbitHandler
    public void getMessage(String message) {
        try {
            Map<String, String> resultMap = JSON.parseObject(message,Map.class);
            String returnCode = resultMap.get("return_code");   //狀態(tài)碼
            if ("SUCCESS".equals(returnCode)) {
                String resultCode = resultMap.get("result_code");   //業(yè)務結果
                String attach = resultMap.get("attach");
                Map<String,String> attachMap = JSON.parseObject(attach,Map.class);
                if ("SUCCESS".equals(resultCode)) {
                    //改訂單狀態(tài)
                    seckillOrderService.updatePayStatus(attachMap.get("username"),
                            resultMap.get("transaction_id"),resultMap.get("time_end"));
                } else {
                    //刪除訂單
                    seckillOrderService.deleteOrder(attachMap.get("username"));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

這個方法使用來監(jiān)聽秒殺隊列的消息的,exchange和queue需要我們手動地在RabbitMQ的網(wǎng)頁中創(chuàng)建并進行綁定

在該方法中,首先去讀取狀態(tài)碼和業(yè)務結果,如果都為“SUCCESS”的話則說明訂單支付成功,修改訂單的狀態(tài)。反之訂單支付失敗,刪除訂單。

總結

文章鴿了快一個月了,終于補上了,主要是上篇文章寫完后就在做個小東西,然后就是國慶節(jié)放假,在家待著有點懶。回校后又在參加電賽,沒時間。所以一路鴿到現(xiàn)在。

這篇文章主要是將之前的秒殺流程進行一個完善,實現(xiàn)了防止秒殺重復排隊,解決并發(fā)超賣的問題,并解決了同步庫存不精準的問題。最后實現(xiàn)了秒殺支付。

碼字不易,可以的話,給我來個點贊收藏關注

如果你喜歡我的文章,歡迎關注微信公眾號 『 R o b o d 』

代碼:https://github.com/RobodLee/changgou

本文已收錄至我的Github倉庫DayDayUP:github.com/RobodLee/DayDayUP,歡迎Star

相關案例查看更多