SpringBoot(19)學習之使用RabbitMQ實現高併發介面優化
阿新 • • 發佈:2019-02-13
使用RabbitMQ改寫秒殺功能
實現思路
思路:減少資料庫訪問
具體的實現流程就是
1.系統初始化,把商品庫存數量載入到Redis
2.收到請求,Redis預減庫存,庫存不足,直接返回,否則3
3.請求入隊,立即返回排隊中
4.請求出隊,生成訂單,減少庫存
5.客戶端輪詢,是否秒殺成功
其中4和5是同時併發處理的。
具體實現
系統初始化,把商品庫存數量載入到Redis
如何在初始化的時候就將庫存資料存入快取中
通過實現InitializingBean介面中的一個方法:afterPropertiesSet()
系統初始化會首先呼叫該函式:
/**
* 系統初始化會呼叫該函式
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsVoList = goodsService.listGoodsVo();
if (goodsVoList == null){
return;
}
for (GoodsVo goodsVo:goodsVoList){
//預先把商品庫存載入到redis中
redisService.set(GoodsKey.getSeckillGoodsStock,"" +goodsVo.getId(),goodsVo.getStockCount());
localOverMap.put(goodsVo.getId(),false);
}
}
收到請求,Redis預減庫存,庫存不足,直接返回,否則請求入隊,立即返回排隊中
首先需要一個RabbitMQ的佇列
使用Direct交換機模式
/**
* Direct 交換機模式
*/
//佇列
@Bean
public Queue secKill_QUEUE() {
return new Queue(SECKILL_QUEUE,true );
}
佇列訊息的傳送
public void sendSecKillMessage(SecKillMessage secKillMessage) {
String msg = RedisService.Bean2String(secKillMessage);
logger.info("send SecKill message: " + msg);
amqpTemplate.convertAndSend(MQConfig.SECKILL_QUEUE, msg);
}
秒殺的實現
//預先減庫存
long stock = redisService.decr(GoodsKey.getSeckillGoodsStock,""+goodsId);
if (stock < 0){
localOverMap.put(goodsId,true);
return Result.error(CodeMsg.SECKILL_OVER);
}
//判斷是否已經秒殺到了
SecKillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(),goodsId);
if (order != null){
return Result.error( CodeMsg.SECKILL_REPEATE);
}
//壓入RabbitMQ佇列
SecKillMessage secKillMessage = new SecKillMessage();
secKillMessage.setUser(user);
secKillMessage.setGoodsId(goodsId);
mqSender.sendSecKillMessage(secKillMessage);
return Result.success(0); //排隊中
請求出隊,生成訂單,減少庫存
其實就是RabbitMQ的隊列出隊去處理相關的業務
@RabbitListener(queues = MQConfig.SECKILL_QUEUE)
public void receive(String message){
logger.info("receive message" + message);
SecKillMessage secKillMessage = RedisService.String2Bean(message,SecKillMessage.class);
SecKillUser user = secKillMessage.getUser();
long goodsId = secKillMessage.getGoodsId();
//判斷庫存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if (stock <= 0){
return;
}
//判斷是否已經秒殺到了
SecKillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(),goodsId);
if (order != null){
return;
}
//減庫存 下訂單 寫入秒殺訂單
//訂單的詳細資訊
OrderInfo orderInfo = secKillService.secKill(user, goods);
}
客戶端輪詢,是否秒殺成功
//秒殺的結果
/**
* orderId:秒殺成功
* -1: 秒殺失敗
* 0:排隊中
* @param model
* @param user
* @param goodsId
* @return
*/
@RequestMapping(value = "/result",method = RequestMethod.GET)
@ResponseBody
public Result<Long> miaoshaResult(Model model, SecKillUser user, @RequestParam("goodsId") long goodsId){
model.addAttribute("user",user);
if (user == null){
return Result.error(CodeMsg.SESSION_ERROR);
}
long result = secKillService.getSecKillResult(user.getId(),goodsId);
return Result.success(result);
}
secKillService.getSecKillResult():
//獲取結果
/**
* orderId :成功
* -1:失敗
* 0: 排隊中
* @param userId
* @param goodsId
* @return
*/
public long getSecKillResult(Long userId, long goodsId) {
SecKillOrder order = orderService.getOrderByUserIdGoodsId(userId,goodsId);
if (order != null){
return order.getOrderId();
}else {
boolean isOver = getGoodsOver(goodsId);
if (isOver){
return -1;
}else {
return 0;
}
}
}
這裡涉及到了redis的訪問,就是redis中有商品的數量,通過該引數判斷賣沒賣完,當一次性來了多於商品數目的請求的時候,redis預減庫存,減為負數,其實在這個時候在來商品購買請求的時候就不需要在訪問redis了。因為商品已經賣完了,這個時候就做一個標記,先判斷記憶體這個標記,如果庫存已經小於0了,就不再訪問redis,這樣就減少了redis的訪問次數。
沒有訂單有兩種情況,賣完了失敗,和排隊中,
在上面的秒殺那做個標記。這個商品是否秒殺完了。存入redis中。
之後去判斷是否存在這個key就知道是哪種情況,這樣
//事務,原子性操作
@Transactional
public OrderInfo secKill(SecKillUser user, GoodsVo goods) {
//減庫存 下訂單 寫入秒殺訂單 必須是同時完成的
boolean success = goodsService.reduceStock(goods);
//減庫存成功了才進行下訂單
if (success) {
return orderService.createOrder(user, goods);
}else{ //說明商品秒殺完了。做一個標記
setGoodsOver(goods.getId());
return null;
}
}
//獲取結果
/**
* orderId :成功
* -1:失敗
* 0: 排隊中
* @param userId
* @param goodsId
* @return
*/
public long getSecKillResult(Long userId, long goodsId) {
SecKillOrder order = orderService.getOrderByUserIdGoodsId(userId,goodsId);
if (order != null){
return order.getOrderId();
}else {
boolean isOver = getGoodsOver(goodsId);
if (isOver){
return -1;
}else {
return 0;
}
}
}
public void setGoodsOver(Long goodsId) {
redisService.set(SecKillKey.isGoodsOver,""+goodsId,true);
}
public boolean getGoodsOver(Long goodsId) {
return redisService.exists(SecKillKey.isGoodsOver,""+goodsId);
}
}
相對應的前端的修改
原來的detail頁面中秒殺事件函式:
function doMiaosha(){
$.ajax({
url:"/miaosha/do_miaosha",
type:"POST",
data:{
goodsId:$("#goodsId").val(),
},
success:function(data){
if(data.code == 0){
window.location.href="/order_detail.htm?orderId="+data.data.id;
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客戶端請求有誤");
}
});
}
秒殺到商品就直接返回,現在後端改為訊息佇列,所以需要增加函式進行判斷,必要時需要輪詢:
if(data.code == 0){
window.location.href="/order_detail.htm?orderId="+data.data.id;
}else{
layer.msg(data.msg);
}
所以將其改為:
//其他的部分省略
...
if(data.code == 0){
//window.location.href="/order_detail.htm?orderId="+data.data.id;
//秒殺到商品的時候,這個時候不是直接返回成功,後端是進入訊息佇列,所以前端是輪詢結果,顯示排隊中
getMiaoshaResult($("#goodsId").val());
}else{
layer.msg(data.msg);
}
...
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;
//失敗--- -1
if(result <= 0){
layer.msg("對不起,秒殺失敗!");
}
//排隊等待,輪詢--- 0
else if(result == 0){//繼續輪詢
setTimeout(function () {
getMiaoshaResult(goodsId);
},50);
}
//成功---- 1
else {
layer.msg("恭喜你,秒殺成功,檢視訂單?",{btn:["確定","取消"]},
function () {
window.location.href="/order_detail.htm?orderId="+result;
},
function () {
layer.closeAll();
}
);
}
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.msg("客戶端請求有誤");
}
});
}
壓測
測試環境 1g + 4核 + 50000個請求