高併發秒殺接口優化
秒殺業務場景,併發量很大,瓶頸在數據庫,怎麼解決,加緩存。用戶發起請求時,從瀏覽器開始,在瀏覽器上做頁面靜態化直接將頁面緩存到用戶的瀏覽器端,然後請求到達網站之前可以部署CDN節點,讓請求先訪問CDN,到達網站時候使用頁面緩存。頁面緩存再進一步的話,粒度再細一點的話就是對象緩存。緩存層依次請求完之後,纔是數據庫。通過一層一層的訪問緩存逐步的削減到達數據庫的請求數量,這樣才能保證網站在大併發之下抗住壓力。
但是僅僅依靠緩存還不夠,所以還需要進行接口優化。
接口優化核心思路:減少數據庫的訪問。(數據庫抗併發的能力有限)
- 使用Redis預減庫存減少對數據庫的訪問
- 使用內存標記減少Redis的訪問
- 使用RabbitMQ隊列緩衝,異步下單,增強用戶體驗
具體實現步驟:
- 統初始化,把商品庫存數量加載到Redis上面來
- 收到請求,Redis預減庫存(先減少Redis裏面的庫存數量,庫存不足,直接返回),如果庫存已經到達臨界值的時候,即=0,就不需要繼續往下走,直接返回失敗
- 請求入隊,立即返回排隊中
- 請求出隊,生成訂單,減少庫存
- 客戶端輪詢,是否秒殺成功
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
下載地址:
自己選擇適合自己系統的,進行下載安裝。
步驟二:安裝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;
}
}