SpringBoot秒殺系統實戰20-高併發秒殺接口優化

文章目錄

高併發秒殺接口優化

秒殺業務場景,併發量很大,瓶頸在數據庫,怎麼解決,加緩存。用戶發起請求時,從瀏覽器開始,在瀏覽器上做頁面靜態化直接將頁面緩存到用戶的瀏覽器端,然後請求到達網站之前可以部署CDN節點,讓請求先訪問CDN,到達網站時候使用頁面緩存。頁面緩存再進一步的話,粒度再細一點的話就是對象緩存。緩存層依次請求完之後,纔是數據庫。通過一層一層的訪問緩存逐步的削減到達數據庫的請求數量,這樣才能保證網站在大併發之下抗住壓力。

但是僅僅依靠緩存還不夠,所以還需要進行接口優化。

接口優化核心思路:減少數據庫的訪問。(數據庫抗併發的能力有限)

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

具體實現步驟:

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

1.商品庫存數量預加載庫存到Redis上

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

在容器啓動的時候,檢測到了實現了接口InitializingBean之後,就回去回調afterPropertiesSet方法。將每種商品的庫存數量加載到redis裏面去。

@RequestMapping("/miaosha")
@Controller
public class MiaoshaController implements InitializingBean{ 
public void afterPropertiesSet() throws Exception {
    List<GoodsVo> goodslist=goodsService.getGoodsVoList();
    if(goodslist==null) {
        return;
    }
    for(GoodsVo goods:goodslist) {
        //如果不是null的時候,將庫存加載到redis裏面去 prefix---GoodsKey:gs ,  key---商品id,     value
        redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
    }
}
}
	@Service
	public class GoodsService {
	public static final String COOKIE1_NAME_TOKEN="token";	
	@Autowired
	GoodsDao goodsDao;
	@Autowired
	RedisService redisService;
	
	public List<GoodsVo>  getGoodsVoList() {
		return goodsDao.getGoodsVoList();
	}
	}
@Mapper
public interface GoodsDao {
//兩個查詢
@Select("select g.*,mg.stock_count,mg.start_date,mg.end_date,mg.miaosha_price from miaosha_goods mg left join goods g on mg.goods_id=g.id")  
public List<GoodsVo> getGoodsVoList();
}

2.收到請求後預減庫存

後端接收秒殺請求的接口doMiaosha,收到請求,Redis預減庫存(先減少Redis裏面的庫存數量,庫存不足,直接返回),如果庫存已經到達臨界值的時候,即=0,就不需要繼續往下走,直接返回失敗

	@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)
	@ResponseBody
	public Result<Integer> doMiaosha(Model model,MiaoshaUser user,
			@RequestParam(value="goodsId",defaultValue="0") long goodsId,
			@PathVariable("path")String path) {
		model.addAttribute("user", user);
		//1.如果用戶爲空,則返回至登錄頁面
		if(user==null){
			return Result.error(CodeMsg.SESSION_ERROR);
		}	
		//2.預減少庫存,減少redis裏面的庫存
		long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
		//3.判斷減少數量1之後的stock,區別於查數據庫時候的stock<=0
		if(stock<0) {//線程不安全---庫存至臨界值1的時候,此時剛好來了加入10個線程,那麼庫存就會-10
			return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
		}
		//4.判斷這個秒殺訂單形成沒有,判斷是否已經秒殺到了,避免一個賬戶秒殺多個商品
		MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);
		if (order != null) {// 查詢到了已經有秒殺訂單,代表重複下單
			return Result.error(CodeMsg.REPEATE_MIAOSHA);
		}
		//5.可以秒殺,原子操作:1.庫存減1,2.下訂單,3.寫入秒殺訂單--->是一個事務
		OrderInfo orderinfo=miaoshaService.miaosha(user,goodsvo);
		return Result.success(orderinfo);
	}

RedisService裏面的decr方法:減少key對應的值

/**
 * 減少值
 * @param prefix
 * @param key
 * @return
 */
public <T> Long decr(KeyPrefix prefix,String key){
    Jedis jedis=null;
    try {
        jedis=jedisPool.getResource();
        String realKey=prefix.getPrefix()+key;
        return jedis.decr(realKey);
    }finally {
        returnToPool(jedis);
    }
}

3.消息入隊(並將用戶信息和商品信息封裝起來傳入隊列)

再次改造doMiaosha接口方法,在收到請求之後,請求入隊:

@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)
@ResponseBody
public Result<Integer> doMiaosha(Model model,MiaoshaUser user,
        @RequestParam(value="goodsId",defaultValue="0") long goodsId,
        @PathVariable("path")String path) {
    model.addAttribute("user", user);
    //1.如果用戶爲空,則返回至登錄頁面
    if(user==null){
        return Result.error(CodeMsg.SESSION_ERROR);
    }   
    //2.預減少庫存,減少redis裏面的庫存
    long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);
    //3.判斷減少數量1之後的stock,減少到0一下,則代表之後的請求都失敗,直接返回
    if(stock<0) {
        return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);
    }
    //4.判斷這個秒殺訂單形成沒有,判斷是否已經秒殺到了,避免一個賬戶秒殺多個商品
    MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);
    if (order != null) {// 查詢到了已經有秒殺訂單,代表重複下單
        return Result.error(CodeMsg.REPEATE_MIAOSHA);
    }
    //5.正常請求,入隊,發送一個秒殺message到隊列裏面去,入隊之後客戶端應該進行輪詢。
    MiaoshaMessage mms=new MiaoshaMessage();
    mms.setUser(user);
    mms.setGoodsId(goodsId);
    mQSender.sendMiaoshaMessage(mms);
    //返回0代表排隊中
    return Result.success(0);
}

MiaoshaMessage 消息的封裝類:

//MiaoshaMessage 消息的封裝  MiaoshaMessage Bean
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;
}
}

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

@Autowired
	RedisService redisService;
	@Autowired
	AmqpTemplate amqpTemplate;
	public void sendMiaoshaMessage(MiaoshaMessage mmessage) {
		// 將對象轉換爲字符串
		String msg = RedisService.beanToString(mmessage);
		log.info("send message:" + msg);
		// 第一個參數隊列的名字,第二個參數發出的信息
		amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
	}
	/**
	 * 將Bean對象轉換爲字符串類型
	 * @param <T>
	 */
	public static <T> String beanToString(T value) {
		//如果是null
		if(value==null) return null;
		//如果不是null
		Class<?> clazz=value.getClass();
		if(clazz==int.class||clazz==Integer.class) {
			return ""+value;
		}else if(clazz==String.class) {
			return ""+value;
		}else if(clazz==long.class||clazz==Long.class) {
			return ""+value;
		}else {
			return JSON.toJSONString(value);
		}		
	}

redis多線程情況下是否安全?

1、 新建RedisConcurrentTestUtil類來測試redis多線程是否安全,如果安全,那麼應該只有10個線程能通過if(stock>=0)的判斷,進行後面的秒殺操作!

@Service
public class RedisConcurrentTestUtil {
	@Autowired
	RedisService redisService;	//會出現循環依賴---Circular reference
	class ThreadTest implements Runnable{
		@Override
		public void run() {
			long stock=redisService.get("GoodsKey:gs1",Long.class);
			String name=Thread.currentThread().getName();
			System.out.println("當前線程 :"+name+"  stock:"+stock);
			//2.預減少庫存,減少redis裏面的庫存
			//stock最初爲10,100個線程同時去減少1次,最終stock應該爲-90
			stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+1);
			//是否線程安全?
			if(stock<0) {
				System.out.println("結束!!!");
				return;
			}
			//應該只有10個線程能從這裏通過
			System.out.println("驗證當前有幾個線程通過if(stock<0)  當前線程 :"+name+"  減1之後的stock:"+stock);
		}
	}
	public  void test(){
		ThreadTest t1=new ThreadTest();
		//開啓50個線程
		for(int i=1;i<=100;i++){
			new Thread(t1,"Thread-"+i).start();
		}
	}

}

2、 在某一個類(DemoController)裏面定義一個接收請求的方法。

3、進入redis,設置GoodsKey:gs1這個鍵的值爲10。

幾個常用的redis命令:
auth [password] 輸入密碼
keys *查詢所有鍵
flushdb 清空數據

4、發送請求,驗證結果!

5、 結果如下:

可以得出多線程下redis的操作是線程安全的:

redis裏面庫存結果:

RabbitMQ在Windows上面的安裝與集成

安裝rabbitmq首先要首先安裝erlang。

1.在Win環境下安裝Erlang

步驟一:下載erlang

下載地址:

64位:http://erlang.org/download/otp_win64_20.3.exe

32位:http://erlang.org/download/otp_win32_20.3.exe

自己選擇適合自己系統的,進行下載安裝。

步驟二:安裝erlang

直接點擊exe安裝,安裝路徑自己配的要記住在哪裏,最好自己指定一個安裝目錄,等等會用到。

步驟三:最重要的一步了,配置環境變量

配置ERLANG_HOME環境變量,其值指向erlang的安裝目錄(就是步驟二的路徑)。另外將 ;%ERLANG_HOME%\bin 加入到Path中。

新建添加即可:

步驟四:檢查安裝是否成功

打開cmd,輸入erl

2.在Win環境下安裝RabbitMQ

下載資源:

RabbitMQ,下載地址 http://www.rabbitmq.com/install-windows.html

對應版本(必須是與mq版本適應)的erlang,下載地址 http://www.erlang.org/downloads/20.2
首先安裝erlang,然後安裝rabbitmq。

安裝完RabbitMQ以後,服務會自動運行,這時環境變量裏的ERLANG_HOME會自動生成,在”環境變量”中檢查是否存在,如果不存在,請在”環境變量”中手動添加,配置Erlang環境變量ERLANG_HOME=E:\Apply\Erlang213\erl10.2。如果沒有,點擊”新建”。

詳細RabbitMQ安裝請參考:https://blog.csdn.net/h363659487/article/details/80913354

3.集成RabbitMQ

  • 添加依賴
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • 在application.properties配置文件裏面添加RabbitMQ配置信息
#RabbitMQ配置
spring.rabbitmq.host=127.0.0.1
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.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
#最大間隔 10s
spring.rabbitmq.template.retry.max-interval=10000
spring.rabbitmq.template.retry.multiplier=1.0
  • 創建消息發送者與接收者

//發送者
@Service
public class MQSender {

private static Logger log=LoggerFactory.getLogger(MQSender.class);
@Autowired
RedisService redisService;
@Autowired
AmqpTemplate amqpTemplate;
/**
 * 發送秒殺信息,使用derict模式的交換機。(包含秒殺用戶信息,秒殺商品id)
 */
public void sendMiaoshaMessage(MiaoshaMessage mmessage) {
    // 將對象轉換爲字符串
    String msg = RedisService.beanToString(mmessage);
    log.info("send message:" + msg);
    // 第一個參數隊列的名字,第二個參數發出的信息
    amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}   
}
  • 創建MQ的config,然後創建隊列
@Configuration
public class MQConfig {
	public static final String QUEUE="queue";
	public static final String MIAOSHA_QUEUE="miaosha.queue";
	public static final String TOPIC_QUEUE1="topic.queue1";
	public static final String TOPIC_QUEUE2="topic.queue2";
	public static final String HEADER_QUEUE="header.queue";
	public static final String TOPIC_EXCHANGE="topic.exchange";
	public static final String FANOUT_EXCHANGE="fanout.exchange";
	public static final String HEADER_EXCHANGE="header.exchange";
	public static final String ROUTINIG_KEY1="topic.key1";
	public static final String ROUTINIG_KEY2="topic.#";
	/**
	 * Direct模式,交換機Exchange:
	 * 發送者,將消息往外面發送的時候,並不是直接投遞到隊列裏面去,而是先發送到交換機上面,然後由交換機發送數據到queue上面去,
	 * 做了依次路由。
	 */
	@Bean
	public Queue queue() {
		//名稱,是否持久化
		return new Queue(QUEUE,true);
	}
	
	@Bean
	public Queue miaoshaqueue() {
		//名稱,是否持久化
		return new Queue(MIAOSHA_QUEUE,true);
	}
}
  • MiaoshaMessage類
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;
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章