大战熟女丰满人妻av-荡女精品导航-岛国aaaa级午夜福利片-岛国av动作片在线观看-岛国av无码免费无禁网站-岛国大片激情做爰视频

秒殺項目
秒殺項目基本環境搭建
商品展示模塊
請求執行秒殺模塊
秒殺流程總結

后臺Controller處理

1. 在GoodsController中定義seckill方法對秒殺請求進行處理,在處理的時候需要進行一些判斷

//執行秒殺
@PostMapping("/seckill/goods/{random}/{id}")
public @ResponseBody ReturnObject seckill(@PathVariable("random") String random,@PathVariable("id") Integer id){
    ReturnObject returnObject = new ReturnObject();
        return returnObject;
}

2. 請求參數random合法性驗證,我們這里采用的是長度判斷,有些公司將random的某個位置值固定,判斷是否為那個值

//1.random參數合法性驗證,我們這里采用的是長度判斷,有些公司將random的某個位置值固定,判斷是否為那個值
if(random.length() != 36){
    returnObject.setErrorCode(Constants.ZERO);
    returnObject.setErrorMessage("請求參數有誤");
    return returnObject;
}

3. 根據商品id從Redis中查詢出緩存的商品,判斷請求參數random和商品的randomName是否匹配

//2.根據商品id從Redis中查詢出緩存的商品,判斷請求參數random和商品的randomName是否匹配
String goodsJSON = redisTemplate.opsForValue().get(Constants.REDIS_GOODS+id);
Goods goods = JSONObject.parseObject(goodsJSON,Goods.class);
if (!random.equals(goods.getRandomname())){
    returnObject.setErrorCode(Constants.ZERO);
    returnObject.setErrorMessage("請求參數有誤");
    return returnObject;
}

4. 為了保險起見,我們再次驗證一下是否在秒殺時間內(這步可以省略)

這里既沒有操作磁盤,也沒有操作數據庫,也沒有走網絡,所以不會對性能產生影響

//3.為了保險起見,我們再次驗證一下是否在秒殺時間內
Long currentTime = System.currentTimeMillis();
Long startTime = goods.getStarttime().getTime();
Long endTime = goods.getEndtime().getTime();
if(currentTime < startTime){
    //秒殺尚未開始
    returnObject.setErrorCode(Constants.ZERO);
    returnObject.setErrorMessage("秒殺尚未開始");
    return returnObject;
}else if(currentTime > endTime){
    //秒殺已經結束
    returnObject.setErrorCode(Constants.ZERO);
    returnObject.setErrorMessage("秒殺已經結束");
    return returnObject;
}else{
    //如果秒殺已經開始,處理業務繼續寫在這里

    return returnObject;
}

5. 如果已經開始秒殺驗證商品是否已經賣光

需求:

如果商品已經賣光,那么提示用戶,不能參與秒殺了

常規思路

● 直接查詢數據庫中商品的庫存,如果直接操作數據庫,秒殺場景,高并發大流量會給數據庫帶來很大的壓力。

● 從Redis中緩存的商品信息中獲取,但是后續秒殺結束后,涉及對庫存做修改,操作Redis的商品信息比較麻煩。另外,如果我們5秒緩存預熱一次,數據庫中商品的庫存還沒有修改,會被再次把數據庫中的庫存更新到Redis中。

解決方案

所以我們在緩存預熱的時候,直接將商品的庫存單獨存放到Redis中。并且這個信息需要在緩存預熱的時候生成,而且只能生成一次,因為我們減庫存我的時候,也是操作Redis,數據庫暫時不會變,如果每5秒初始化一次,那么會將數據庫的原始庫存又初始化到Redis中。

設置值的時候使用setIfAbsent方法

如果key不存在,那么設置值,如果已經存在,不對其進行設置值了

? 在15-seckill-service緩存預熱的定時任務中緩存商品庫存

Key的格式 redis:store:商品id Value的值:就是商品的庫存

/**
 * 把數據庫中商品的庫存也預熱到Redis
 * 注意:這里只能放一次,因為我們減庫存我的時候,也是操作Redis,數據庫暫時不會變
 * 如果每5秒初始化一次,那么會將數據庫的原始庫存又初始化到Redis中
 * setIfAbsent:如果key不存在,那么設置值,如果已經存在,不對其進行設置值了
 */
redisTemplate.opsForValue()
        .setIfAbsent(Constants.REDIS_STORE + goods.getId(),String.valueOf(goods.getStore()));

? 在15-seckill-interface的Constants常量類下定義商品庫存key的前綴

/**
 * 定義Redis中商品庫存的key的前綴
 * Redis中存放商品庫存的格式:redis:goods:商品id
 */
public static final String REDIS_STORE = "redis:store:";

? 重新運行15-seckill-service,通過Redis DeskTop Manager查看Redis數據

? 在GoodsControll編寫驗證商品是否賣光代碼

//4.驗證商品是否已經賣光了
//根據商品id,從Redis中獲取商品庫存
String redisStore = redisTemplate.opsForValue().get(Constants.REDIS_STORE + id);
//判斷是否為空 如果不為空將redis存放的庫存轉換為整形
Integer store = StringUtils.isEmpty(redisStore)? 0 :Integer.valueOf(redisStore);
//其實不會出現小于0的情況
if(store <= 0 ){
    returnObject.setErrorCode(Constants.ZERO);
    returnObject.setErrorMessage("來晚了,商品已經搶光了");
    return returnObject;
}

為了對String操作更加方便,在15-seckill-web中引入commons-lang的依賴

<!--對常用類操作的增強包-->
<dependency>
   <groupId>commons-lang</groupId>
   <artifactId>commons-lang</artifactId>
   <version>2.5</version>
</dependency>

6. 驗證該用戶是否已經秒殺過該商品

需求

同一件商品,同一個用戶只能秒殺一次

常規思路

去數據庫訂單表中查詢,是否有用戶對該商品的下單信息,但是秒殺場景,高并發大流量下,會給數據庫帶來很大的壓力

解決方案

我們這里還是查詢采用Redis,如果用戶秒殺了該商品,那就將用戶信息及商品信息組合放到Redis中,生成一條秒殺記錄,然后再秒殺的時候,從Redis中取數據進行判斷

格式:redis:buy:id:uid

? 在15-seckill-interface的Constants常量中添加用戶是否購買過商品的key的前綴

/**
 * 定義Redis中用戶是否買過該商品的key的前綴
 * Redis中存放用戶是否買過該商品的格式:redis:buy:商品id:用戶id
 */
public static final String REDIS_BUY = "redis:buy:";

? 在15-seckill-web的GoodsController中編寫驗證是否買過該商品的代碼

//5.驗證用戶是否買過該商品
//假設用戶的id為888888,實際開發的使用用戶的id可以從session中獲取
Integer uid = 888888; 
String redisBuy = redisTemplate.opsForValue().get(Constants.REDIS_BUY + id +":"+ uid);
//這里我們不需要關心redisBuy中放了什么,只要不為空,就說明用戶買個該商品
if(StringUtils.isNotEmpty(redisBuy)){
    returnObject.setErrorCode(Constants.ZERO);
    returnObject.setErrorMessage("您已經秒殺過該商品了,請換個商品秒殺");
    return returnObject;
}

7. 限流

需求

在秒殺場景中,為每一個商品限制最大的參與搶購的人數為10w

不能為所有商品整體一個限流,否則會不平衡的問題,很多人都去秒殺一件商品,但是另一件商品在秒殺的時候,被限制了,誤殺!

實現方式

一般有專門的限流算法

我們使用Redis的List類型或者Redis計數器實現

如果用戶參與秒殺,向Redis的List中放一條記錄,然后判斷List的長度,Redis格式: redis:limit:商品id

? 在15-seckill-interface的Constants類中,添加限流最大值以及商品秒殺限流key的前綴常量

//商品限流最大值
public static final int MAX_LIMIT = 100000; 
/**
 * 定義Redis中商品秒殺限流key的前綴
 * Redis中存放當前商品的流量訪問值的格式:redis:limit:商品id
 */
public static final String REDIS_LIMIT = "redis:limit:";

? 在15-seckill-web的GoodsController中編寫限流代碼

//6.限流
//從Redis中查詢出當前商品的訪問量
Long currentSize = redisTemplate.opsForList().size(Constants.REDIS_LIMIT + id);
if(currentSize > Constants.MAX_LIMIT){
    //超過最大限流值,拒絕訪問
    returnObject.setErrorCode(Constants.ZERO);
    returnObject.setErrorMessage("服務器繁忙,請稍后再試!~");
    return returnObject;
}else{
    //可以繼續執行秒殺
    // 先向Redis的限流List中放一條數據  返回放完數據之后List的長度
    Long afterPushSize = redisTemplate.opsForList().leftPush(Constants.REDIS_LIMIT + id,String.valueOf(uid));
    /*放完元素之后再次判斷List的長度是否大于限流值
    主要處理多線程情況下,很多線程都滿足限流條件,都向Redis的List添加元素,避免List元素超出限流值
    */
    if(afterPushSize >Constants.MAX_LIMIT){
        redisTemplate.opsForList().rightPop(Constants.REDIS_LIMIT + id);
        //超過最大限流值,拒絕訪問
        returnObject.setErrorCode(Constants.ZERO);
        returnObject.setErrorMessage("服務器繁忙,請稍后再試!~");
        return returnObject;
    }
}

8. 秒殺

? 減庫存

需求

秒殺結束后,將商品的庫存信息減1

常規的方式

減庫存是直接操作數據庫,高并發大流量的秒殺場景下,會給數據庫瞬間帶來極大的壓力,可能數據庫無法支撐(單臺MySQL并發能力700左右,單臺Redis并發能力5w左右)

解決方案

所以減庫存在Redis中減

A、 15-seckill-web減庫存代碼

//減庫存
Long leftStore = redisTemplate.opsForValue().decrement(Constants.REDIS_STORE +"id",1);

? 下訂單(僅僅是將訂單發送給MQ)

需求

秒殺之后,僅僅將訂單發送給MQ,暫時不想數據庫訂單表中插入數據

常規的做法

直接是向數據庫中插入訂單信息

秒殺場景,可能有很多訂單可以插入到數據庫,而且主要是瞬間的操作,例如:1s,5s內向數據庫插入10w條數據。

所以下單的時候不能直接操作數據庫

解決方案

我們采用MQ,進行異步下單

同步是阻塞的,是需要等結果的,是可以拿到結果的

異步是非阻塞的,不需要等結果,但是有可能馬上拿不到結果

讓MQ接收瞬間的巨大的下單請求,但并不是馬上瞬間處理完畢,而是一個個處理,插入數據庫的頻率的降低。這個頻率的降低,我們叫做流量削峰,將單位時刻內,對數據庫的操作降緩

MQ處理完畢之后,僅僅是將消息發送到了ActiveMQ的消息隊列中,并沒有真正的同步數據庫,所以不能馬上給前臺結果,那么這個時候我們可以告訴前臺頁面一個中間結果,秒殺請求提交成功,正在處理……或一個圖片轉動

A、 在15-seckill-web的pom.xml文件添加ActiveMQ起步依賴

<!--SpringBoot集成ActiveMQ的起步依賴-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

B、 在15-seckill-web的核心配置文件中配置ActiveMQ信息

#配置activemq的連接信息
spring.activemq.broker-url=tcp://192.168.235.128:61616
# 用戶名
spring.activemq.user=system
# 密碼
spring.activemq.password=123456
#目的地
spring.jms.template.default-destination=seckillQueue

C、 在15-seckill-web的GoodsController類中注入JmsTemplate

@Autowired
private JmsTemplate jmsTemplate;

D、 在15-seckill-web的GoodsController類中編寫下訂單代碼

//7.減庫存
Long leftStore = redisTemplate.opsForValue().decrement(Constants.REDIS_STORE +id,1);
//8.下單到MQ
if(leftStore >= 0){
    //可以秒殺,執行下單操作
    //標記用戶已經買過該商品
    redisTemplate.opsForValue().set(Constants.REDIS_BUY + id +":" +uid,String.valueOf(uid));
    //創建訂單對象
    Orders orders = new Orders();
    orders.setBuynum(1);
    orders.setBuyprice(goods.getPrice());
    orders.setCreatetime(new Date());
    orders.setGoodsid(id);
    orders.setOrdermoney(goods.getPrice().multiply(new BigDecimal(1)));
    orders.setStatus(1);//待支付
    orders.setUid(uid);
    //將訂單對象轉換為json字符串
    String ordersJSON = JSONObject.toJSONString(orders);
    //通過JmsTemplate向ActiveMQ發送消息
    jmsTemplate.send(new MessageCreator() {
        @Override
        public Message createMessage(Session session) throws JMSException {
            return session.createTextMessage(ordersJSON);
        }
    });
    returnObject.setErrorCode(Constants.ONE);
    returnObject.setErrorMessage("秒殺請求提交成功,正在處理....");
    return returnObject;
}else{
    //不可以賣了,不能執行下單操作
    /*
    此時Redis中的商品庫存可能已經減成負數了,但是對我們業務的處理沒有任何影響
    但為了保持數據的一致性,我們將值再恢復一下
     */
    redisTemplate.opsForValue().increment(Constants.REDIS_STORE + id,1);
    returnObject.setErrorCode(Constants.ZERO);
    returnObject.setErrorMessage("來晚了,商品已經搶光了");
    return returnObject;
}

E、在15-seckill-web的seckill.js的execseckill函數中處理返回信息

//執行秒殺請求
execSeckill:function (random,id) {
    $.ajax({
        //url格式:   /15-seckill-web/seckill/goods/Ffdaskfjkadlsjklfa/1
        url: seckillObj.url.seckillURL() + random +"/" +id,
        type:"post",
        dataType:"json",
        success:function (rtnMessage) {
            //處理響應結果
            if(rtnMessage.errorCode == 1){
                //秒殺成功,已經下單到MQ,返回中間結果  可以做動畫處理
                $("#seckillTip").html("<span style='color:red;'>"+ rtnMessage.errorMessage +"</span>");
                //接下來再發送一個請求獲取最終秒殺的結果
            }else{
                //秒殺失敗 展示失敗信息
                $("#seckillTip").html("<span style='color:red;'>"+ rtnMessage.errorMessage +"</span>");
            }
        }
    });
}

F、 啟動ActiveMQ,Redis,MySQL,15-seckill-service,15-seckill-web測試

 

? 在15-seckill-service中的RedisTask中同步MySQL數據庫庫存

/**
 * 每3秒同步一次Redis中的庫存到數據庫
 */
@Scheduled(cron = "0/3 * * * * *")
public void syncRedisStoreToDB(){
    System.out.println("同步Redis中的庫存到數據庫...........");
    //1.查詢出所有秒殺商品在Redis中的庫存值
    Set<String> keys = redisTemplate.keys(Constants.REDIS_STORE + "*");
    for (String key : keys) {
        //根據Redis的商品庫存key,獲取商品的庫存
        int store = Integer.valueOf(redisTemplate.opsForValue().get(key));
        //獲取商品的id   在Redis中存放商品庫存的格式  redis:store:id
        int goodsId = Integer.valueOf(key.split(":")[2]);
        //同步到數據庫
        Goods goods = new Goods();
        goods.setId(goodsId);
        goods.setStore(store);
        goodsMapper.updateByPrimaryKeySelective(goods);
    }
}

9. 異步下單的處理

需求

將MQ中的訂單同步到數據庫

實現思路

● 在15-seckill-service中使用異步接收消息的方式對秒殺的訂單消息進行消費

● 為了方便對事務的處理,我們在消息消費者MyMessageListener中不直接調用Mapper,而是調用訂單的Service

● 如果下單成功

在Service中將秒殺的最終結果返回給前臺頁面,這里存在一個問題,就是如何將秒殺的結果響應給前臺頁面?

傳統的做法,前臺頁面可以直接查詢數據庫的訂單表,獲取最終的秒殺結果,但是會對數據庫造成壓力,我們這里借助第三方Redis,將返回的結果保存到Redis中,然后讓前臺頁面到Redis中進行查詢。

● 如果下單失敗

在Service層中拋出異常,在MyMessageListener中捕獲異常,對之前做的處理進行恢復,主要包括庫存恢復、購買標記、限流列表中刪除一個元素

恢復的操作我們也專門在Service中封裝方法

? 在15-seckill-service中添加ActiveMQ相關依賴

<!--SpringBoot集成ActiveMQ的起步依賴-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

? 在15-seckill-service中的pom.xml文件中添加ActiveMQ配置信息

#配置activemq的連接信息
spring.activemq.broker-url=tcp://192.168.235.128:61616
spring.activemq.user=system
spring.activemq.password=123456
#目的地
spring.jms.template.default-destination=seckillQueue
# 消息發送模式 true發布訂閱 false點對點  默認false點對點
spring.jms.pub-sub-domain=false
# SpringBoot 2.1.3之后需要配置
spring.jms.cache.enabled=false

? 從13-activemq-boot-receiver-async-02中拷貝ActiveMQ異步接收的代碼config和listener目錄下的內容

? ActiveMQConfig代碼(不需要修改)

@Configuration//相當于applicationContext-jms.xml文件
public class ActiveMQConfig {

    @Autowired
    private ActiveMQConnectionFactory connectionFactory;

    @Autowired
    private MyMessageListener myMessageListener;
    
    @Value("${spring.jms.template.default-destination}")
    private String destination;

    @Value("${spring.jms.pub-sub-domain}")
    private boolean pubSubDomain;
    
    @Bean //@Bean注解就相當于配置文件的bean標簽
    public DefaultMessageListenerContainer defaultMessageListenerContainer(){
        DefaultMessageListenerContainer listenerContainer = new DefaultMessageListenerContainer();
        listenerContainer.setConnectionFactory(connectionFactory);
        listenerContainer.setDestinationName(destination);
        listenerContainer.setMessageListener(myMessageListener);
        //設置消息發送模式方式為發布訂閱
        listenerContainer.setPubSubDomain(pubSubDomain);
        return listenerContainer;
    }
}

? 修改15-seckill-service中的MyMessageListener消費消息

@Component
public class MyMessageListener implements MessageListener{
    @Autowired
    private OrdersService ordersService;

    public void onMessage(Message message) {
        if(message instanceof TextMessage){
            try {
                String ordersJSON = ((TextMessage) message).getText();
                System.out.println("SpringBoot監聽器異步接收到的消息為:" + ordersJSON);
                Orders orders = JSONObject.parseObject(ordersJSON,Orders.class);
                try {
                    //接收到消息,下訂單
                    ordersService.addOrders(orders);
                } catch (Exception e) {
                    e.printStackTrace();
                    //下單失敗了,要將之前的一些處理恢復一下
                    ordersService.processException(orders);
                }
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
    }
}

? 在15-seckill-interface的com.bjpowernode.seckill.service包下創建訂單接口OrdersService

public interface OrdersService {
    /**
     * 下訂單
     */
    int addOrders(Orders orders);
    /**
     * 下單失敗對異常的處理
     */
    void processException(Orders orders);
}

? 在15-seckill-service的com.bjpowernode.seckill.service.impl包下中創建訂單接口實現類OrdersServiceImpl

@Service
public class OrdersServiceImpl implements OrdersService{
    @Autowired
    private OrdersMapper ordersMapper;

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Transactional
    @Override
    public int addOrders(Orders  orders) {
        int addRow = ordersMapper.insertSelective(orders);
        if(addRow >0){
            /*下單成功,告知前臺秒殺最終結果,我們這是service項目,由消息消費者調用,不能
            直接和前臺打交道,所以需要前臺重新發送請求,去數據庫訂單表中查詢結果,但是這樣
            對數據庫帶來壓力,所以我們將秒殺最終結果放到Redis中,然后前臺頁面去Redis中查詢
             */
            //用我們自定義我的RTO對象封裝秒殺結果
            ReturnObject returnObject = new ReturnObject();
            returnObject.setErrorCode(Constants.ONE);
            returnObject.setErrorMessage("秒殺成功");
            returnObject.setData(orders);
            String returnJSON = JSONObject.toJSONString(returnObject);
            redisTemplate.opsForValue().set(Constants.REDIS_RESULT +
                    orders.getGoodsid() +":" + orders.getUid(),returnJSON);
            //當前這個人秒殺全部結束,應該把當前這個人從限流列表中刪除,讓后面的人再進來秒殺
            redisTemplate.opsForList().rightPop(Constants.REDIS_LIMIT + orders.getGoodsid());
        }else{
            //下單失敗,拋出運行時異常
            throw new RuntimeException("秒殺下單失敗");
        }
        return addRow;
    }
    /**
     下單失敗之后,進行之前處理數據的恢復
     */
    @Override
    public void processException(Orders orders) {
       // 1.庫存恢復
       redisTemplate.opsForValue().increment(Constants.REDIS_STORE + orders.getGoodsid(),1);
       //2.購買標記清除
       redisTemplate.delete(Constants.REDIS_BUY + orders.getGoodsid() +":" + orders.getUid());
       // 3.限流列表中刪除一個元素
       redisTemplate.opsForList().rightPop(Constants.REDIS_LIMIT + orders.getGoodsid());
	//4.將失敗信息放到Redis中,便于前臺頁面再次獲取
	ReturnObject returnObject = new ReturnObject();
	returnObject.setErrorCode(Constants.ZERO);
	returnObject.setErrorMessage("秒殺失敗");
	returnObject.setData(orders);	
	String returnJSON = JSONObject.toJSONString(returnObject);
	redisTemplate.opsForValue().set(Constants.REDIS_RESULT +
	orders.getGoodsid() + ":" + orders.getUid(), returnJSON);
    }
}

? 在15-seckill-interface的Constants類中添加存放最終秒殺結果Key的前綴

/**
 * 定義Redis中商品秒殺秒殺結果key的前綴
 * Redis中存放當前商品的流量訪問值的格式:redis:result:商品id:用戶id
 */
public static final String REDIS_RESULT  = "redis:result:";

? 在15-seckill-service的Application類上開啟事務

@EnableTransactionManagement
@SpringBootApplication
public class Application {
   public static void main(String[] args) {
      SpringApplication.run(Application.class, args);
   }
}

10.  庫存超賣的解讀

參照面試題11-Summary\互聯網金融項目-面試.docx

全部教程
主站蜘蛛池模板: 999精品视频在线观看 | 91精品日本久久久久久牛牛 | 亚洲一区二区三 | 久久精品店 | 国内精品久久久久久久aa护士 | 久草丁香| 四虎亚洲国产成人久久精品 | 日韩精品一区二区三区中文精品 | 亚洲婷婷综合中文字幕第一页 | 青草青青在线观看免费视频 | 久草视频国产 | 伊人免费视频网 | 中文字幕婷婷 | 亚洲激情在线播放 | 91一区二区三区四区五区 | 一级毛片免费视频观看 | 国产一级淫 | 99久久精品国产国产毛片 | 亚洲精品一区二区久久这里 | 中文字幕日韩精品一区口 | 国产一级毛片免 | 日本人一级大毛片 | 国内外成人在线视频 | 色综合久久91| 91最新免费地址入口 | 国产一级毛片网站 | 四虎影视在线观看永久地址 | 国产高清国产专区国产精品 | 亚洲另类图 | 欧美精品福利在线视频 | 亚洲一区在线视频 | 欧美另类亚洲一区二区 | 麻豆精品成人免费国产片 | 欧洲一级 | 网曝门精品国产事件在线观看 | 国产一区二区三区免费在线视频 | 四虎国产精品永久在线看 | 国产高清久久 | 88国产精品视频一区二区三区 | 天天爽天天狼久久久综合 | 狠狠色狠狠色综合久久第一次 |