【商城秒殺項目】-- 秒殺的業務邏輯、接口的優化

秒殺業務場景併發量很大,瓶頸在數據庫,怎麼解決?可以加緩存。用戶在發起請求時,從瀏覽器開始,在瀏覽器上做頁面靜態化直接將頁面緩存到用戶的瀏覽器端,然後請求到達網站之前可以部署CDN節點,讓請求先訪問CDN,到達網站的時候使用頁面緩存。頁面緩存再進一步,粒度再細一點的話就是對象緩存,緩存層依次請求完之後,纔是數據庫。通過一層一層的訪問緩存逐步的削減到達數據庫的請求數量,這樣才能保證網站在高併發之下扛住壓力,但是僅僅依靠緩存還不夠,所以還需要進行接口優化,接口優化的核心思路:減少數據庫的訪問(因爲數據庫抗併發的能力有限)

  • 使用Redis預減庫存減少對數據庫的訪問
  • 使用內存標記減少Redis的訪問
  • 使用RabbitMQ消息隊列緩衝,異步下單,增強用戶體驗

具體實現步驟:

  1. 系統初始化時,把商品庫存數量加載到Redis裏面去
  2. 收到秒殺請求時,Redis預減庫存(先減少Redis裏面的庫存數量,庫存不足,則直接返回),如果庫存已經到達臨界值的時候,即=0,就不需要繼續往下走,直接返回秒殺失敗
  3. 將請求放入消息隊列,立即返回排隊中
  4. 將請求從消息隊列取出來,生成訂單,減少庫存
  5. 客戶端輪詢秒殺的結果,看是否秒殺成功

流程圖如下:

將商品庫存數量預加載庫存到Redis裏面並標記到內存裏

將MiaoshaController實現InitializingBean接口,重寫afterPropertiesSet方法:

收到秒殺請求後的具體業務邏輯

後端收到秒殺請求,實行Redis預減庫存(先減少Redis裏面的庫存數量,庫存不足,直接返回),如果庫存到達臨界值的時候,即庫存=0,就不需要繼續往下走,直接返回秒殺失敗;如果所有的判斷都通過,則將請求放入消息隊列,具體業務邏輯分析如下圖:

秒殺接口代碼如下:

@RequestMapping(value = "/{path}/do_miaosha", method = RequestMethod.POST)
@ResponseBody
public Result<Integer> miaosha(Model model, MiaoshaUser user,
                               @RequestParam("goodsId") long goodsId,
                               @PathVariable("path") String path) {
    model.addAttribute("user", user);
    try {
        //內存標記,減少redis訪問,從map中取出
        boolean over = localOverMap.get(goodsId);
        if (over) {
            return Result.error(CodeMsg.MIAO_SHA_OVER);
        }
        //預減庫存:從緩存中減去庫存
        //利用redis中的方法,減去庫存,返回值爲減去1之後的值
        long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
        //這裏判斷不能小於等於,因爲減去之後等於0說明還有是正常範圍
        if (stock < 0) {
            localOverMap.put(goodsId, true);
            //返回秒殺完畢
            return Result.error(CodeMsg.MIAO_SHA_OVER);
        }
        //判斷是否已經秒殺到了,避免一個賬戶秒殺多個商品
        MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order != null) {
            //還原庫存
            redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
            return Result.error(CodeMsg.REPEATE_MIAOSHA);
        }
    } catch (Exception e) {
        /**
         * 當最後一個商品下單出現錯誤時,數據庫減少庫存失敗,redis減少庫存成功
         * 這時就會出現庫存售不完的情況,所以要將redis緩存還原,即redis庫存數加1
         */
        redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
        return Result.error(CodeMsg.MIAOSHA_FAIL);
    }

    //將請求入隊
    MiaoshaMessage mm = new MiaoshaMessage();
    mm.setUser(user);
    mm.setGoodsId(goodsId);
    sender.sendMiaoshaMessage(mm);
    return Result.success(0);//返回排隊中
}

MiaoshaMessage代碼(消息的封裝類):

package com.javaxl.miaosha_05.rabbitmq;

import com.javaxl.miaosha_05.domain.MiaoshaUser;

public class MiaoshaMessage {
    private MiaoshaUser user;
    private long goodsId;

    public MiaoshaUser getUser() {
        return user;
    }

    public void setUser(MiaoshaUser user) {
        this.user = user;
    }

    public long getGoodsId() {
        return goodsId;
    }

    public void setGoodsId(long goodsId) {
        this.goodsId = goodsId;
    }
}

MQSender代碼(發送消息到rabbitmq去):

package com.javaxl.miaosha_05.rabbitmq;

import com.javaxl.miaosha_05.redis.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MQSender {

	private static Logger log = LoggerFactory.getLogger(MQSender.class);
	
	@Autowired
	AmqpTemplate amqpTemplate ;
	
	public void sendMiaoshaMessage(MiaoshaMessage mm) {
		String msg = RedisService.beanToString(mm);
		log.info("send message:"+msg);
		amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
	}
}

注:消息隊列這裏的消息只能傳字符串,MiaoshaMessage是個Bean對象,先用beanToString方法,將其轉換爲String,放入隊列,再使用AmqpTemplate發送

MQConfig代碼(創建MQ的config類):

package com.javaxl.miaosha_05.rabbitmq;

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MQConfig {
	
	public static final String MIAOSHA_QUEUE = "miaosha.queue";
	public static final String QUEUE = "queue";

	public static final String MIAOSHA_EXCHANGE = "miaosha.exchange";
	
	/**
	 * Direct模式 交換機Exchange
	 * */
	@Bean
	public Queue queue() {
		return new Queue(QUEUE, true);
	}
}

application.properties中關於rabbitmq的配置:

#rabbitmq
spring.rabbitmq.host=xxx
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#消費者數量
spring.rabbitmq.listener.simple.concurrency= 10
spring.rabbitmq.listener.simple.max-concurrency= 10
#消費者每次從隊列獲取的消息數量
spring.rabbitmq.listener.simple.prefetch= 1
#消費者自動啓動
spring.rabbitmq.listener.simple.auto-startup=true
#消費失敗,自動重新入隊
spring.rabbitmq.listener.simple.default-requeue-rejected= true
#啓用發送重試
spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1000
spring.rabbitmq.template.retry.max-attempts=3
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章