Hello,我是一名在互聯網撿破爛的程序員,最近破爛還挺好撿的,天天都是東逛逛西逛逛,收了不少的破爛呢。。。java
收廢鐵了,十塊一斤。快拿來賣哦,什麼爛電冰箱,爛電視機,無論什麼破爛我都要。。。mysql
天天騎着個人爛三輪車,天天都是活的苟且偷生的,我好可憐。。。程序員
嗚嗚嗚嗚web
無論有錢木錢,都進來看一看瞧一瞧哦。。spring
好了~~~~sql
廢話咱們就很少說了,我最近在收廢鐵的時候收到一本武功祕籍,發現了新大陸,今天我就來和大家分享一下。數據庫
咱們一塊兒成爲武林霸主,來吧。氛圍搞起來。。。。。apache
首先呢,咱們在上一期中對 RabbitMQ作了一個小小的實踐,主要是對 RabbitMQ的異步特性進行了分析。windows
若是小夥伴尚未修煉的,趕忙去修煉去吧,咱們要慢慢的成長起來,要慢慢的修煉武功祕籍,不是一天兩天就能夠練成的,讓咱們一塊兒成爲世界的主宰吧,嗯哈哈哈哈哈api
若是你仍是一個一個剛剛入門的小夥伴呢,那你的加緊你的步伐,趕快去從武功祕籍的第一頁開始吧,你也不要慌張,咱們都是從小白開始的,只不過你要多花一點時間來完成前面的修煉,終有一天你會超過的,哈哈哈
RabbitMQ基本使用一(簡單介紹)/#more)
RabbitMQ入門呢?就是上面的這麼多啦。本身根據官網來理解的知識點,寫的不是那麼很好,不過很通俗易懂。代碼有詳細介紹。若有問題,請留言,多多包涵。
好吧,咱們就廢話很少說了,那就開始咱們的表演吧!!!!
本文篇幅比較長,請耐心閱讀,你會收穫不少的。
今天給你們帶來的是 RabbitMQ的消峯限流,咱們知道如今互聯網愈來愈強大,咱們的系統也是愈來愈完善,在高峯期呢,系統將要承受巨大的壓力,那麼也是咱們程序員的壓力。像淘寶、京東大型網站購物系統每逢雙十一,那就是程序員最忙的時候,當天要承受千萬級別的流量衝擊,不得不抵住壓力啊。
想必你們都知道哦我要說什麼了吧,沒錯,就是你想的那樣,咱們就是要對這千萬級別的流量打交道。咱們要抵住流量的衝擊。讓它可以緩解咱們的系統的壓力,系統壓力小了,咱們自身的壓力就小了。OK
咱們知道在咱們雙十一中都會有秒殺的商品,咱們所秒殺的商品價格都是很是低的,而且商品也是很是好,好比華爲Mate30只要999,、蘋果12只要99等等這些秒殺商品,不論是誰,看了都會心動。可是呢,商品的數量是有限的,不是每一個用戶都會搶到,全國14億用戶,就拿一半的人來搶,那個流量衝擊是咱們沒法想像的。那要是所有流量打在咱們的數據庫中,那就只有說再見。。涼涼,最後仍是咱們程序員扛下了全部,那麼咱們應該怎麼辦呢?
相信大多數人都搶過火車票吧,確定有沒有搶到票的吧。好比在咱們的12306搶票網站,每次到必定的時間段都會有大量用戶涌入搶票,可能我會遇到過服務器忙、或者加載失敗等狀況。那麼在這麼大的流量下,咱們是怎麼抗住的呢?
在咱們面對瞬時流量的狀況下,所有的流量都打在咱們的數據庫中,那是很難受的。
那麼咱們應該怎麼來解決這種瞬時流量下的併發狀況?
在咱們秒殺中,庫存只有一份,全部人會集中在時間讀和寫寫這些數據,多人讀取同一個數據
咱們不管在搶票或者秒殺商品中時,爲何我沒有搶到,別人卻搶到了?
下面帶你打開這扇門
那用戶不得吐槽啊,還不得上熱搜啊,某某什麼秒殺系統,垃圾的很,沒人秒殺成功,都是請求超時,哎,溜了溜了
那程序員就遭殃了啊,老闆還不得直接暴扣到頭上啊,那是很難受的,薪水都要大打折扣。
這下知道爲何有時候搶不到商品了吧
這樣的設計就是爲了犧牲用戶流量來換系統穩定
這個架構我是根據本身的理解來的,可能不怎麼符合理念,知道大概意思就是了哈
OK,說了這麼多,咱們就開始咱們的代碼設計吧
下面的全部代碼都是本人獨立完成,可能不是那麼完美,不過應該問題不大吧
若有錯誤之處,還請包涵,多多指出
這裏咱們加入咱們的 Redis緩存技術,有時間會更新的。。
首先咱們要有 RabbitMQ的客戶端,能夠在Linux上安裝,也能夠在Windows上安裝。
我使用的是Linux上安裝的,能夠去參考 RabbitMQ在Linux下安裝
這個就不用多說了吧,這個都是咱們的屢見不鮮了
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>2.2.4.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
主要是導入咱們咱們的 MQ依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>2.2.4.RELEASE</version> </dependency>
server: port: 9000 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test_rabbitmq?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8 username: root password: root rabbitmq: addresses: 192.168.2.2 virtual-host: / username: guest password: guest template: exchange: ORDER.EXCHANGE listener: simple: #指定最小的消費者數量 concurrency: 1 retry: enabled: false #是否支持重試 prefetch: 100 acknowledge-mode: manual logging: level: com.example.rabbitmq.mapper: debug mybatis: mapper-locations: classpath:mapper/*.xml
主要配置的說明
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test_rabbitmq?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8 username: root password: root
將用戶名和密碼修改成本身的
若是你的數據庫是在Linux上,那麼你須要將localhost
修改成你的IP地址,並開啓3306
端口
rabbitmq: addresses: 192.168.2.2 virtual-host: / username: guest password: guest
注意,我這裏沒有列出格式
rabbitmq
這是是在spring
下的
address
:這個是你的IP地址,一樣的若是你的 RabbitMQ
是在Linux
上,使用Linux
的IP地址。如果windows
版本,直接使用localhost
virtual-host
:這個是你rabbitmq
上的一個虛擬主機
username
:用戶名,若是你沒有添加用戶:默認的是guest
password
:密碼,一樣,若是你沒有添加用戶:默認是guest
具體說明請詳見 RabbitMQ在Linux下安裝
template: exchange: ORDER.EXCHANGE
這裏咱們能夠定義默認的交換機名稱,那麼在代碼中咱們就須要設置這樣的名稱
listener: simple: #指定最小的消費者數量 concurrency: 1 retry: enabled: false #是否支持重試 prefetch: 100 #每次處理的消息 acknowledge-mode: manual #手動確認
上面的註釋已經很清楚明白了吧,具體須要配置本身能夠自行配置。
logging: level: com.example.rabbitmq.mapper: debug
mybatis: mapper-locations: classpath:mapper/*.xml
DROP TABLE IF EXISTS `test_order`; CREATE TABLE `test_order` ( `order_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '訂單Id', `order_user_name` varchar(255) DEFAULT NULL COMMENT '訂單人的名稱', `order_user_email` varchar(255) DEFAULT NULL COMMENT '訂單人的郵箱', `order_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '訂單時間', PRIMARY KEY (`order_id`) ) ENGINE=InnoDB AUTO_INCREMEN
DROP TABLE IF EXISTS `test_goods`; CREATE TABLE `test_goods` ( `goods_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品Id', `goods_name` varchar(255) DEFAULT NULL COMMENT '商品名稱', `goods_stock` int(100) DEFAULT NULL COMMENT '商品庫存', PRIMARY KEY (`goods_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `test_goods`(`goods_id`,`goods_name`,`goods_stock`) values (1,'商品',0);
ApiResponse
package com.example.rabbitmq.common; import java.util.HashMap; /** * 數據響應返回 */ public class ApiResponse extends HashMap<String, Object> { /** * 狀態碼 */ private Integer code; /** * 信息 */ private String msg; @Override public Object put(String key, Object value) { super.put(key, value); return this; } public ApiResponse code(Integer code){ this.put("code", code); return this; } public ApiResponse msg(String msg){ this.put("msg",msg); return this; } }
用戶下單響應數據
搶單成功,返回給用戶信息code=200,msg='搶單成功'
搶單失敗,返回給用戶信息code=400,msg='搶單失敗,或者是請求超時'
Order
package com.example.rabbitmq.entity; import lombok.Data; import java.util.Date; /** * 基本類 */ @Data public class TestOrder { /** * 訂單Id */ private Long orderId; /** * 訂單人名稱 */ private String orderUserName; /** * 訂單人郵箱 */ private String orderUserEmail; /** * 訂單時間 */ private Date orderDate; }
這裏咱們使用的是一個註解@Data
,這個註解來源於依賴lombok
,要使用這個依賴不只須要添加這個依賴,並且在IDEA中還要下載這個插件
我相信如今絕大多數人都是使用的IDEA編譯器吧~~~
若是還有小夥伴不知道這個編譯器或者沒有使用的,那就趕快行動起來吧!!!
IDEA這款編譯器是真的很友好
哇哦,扯到一邊去了。。。
OK,咱們迴歸正題
Goods
package com.example.rabbitmq.entity; import lombok.Data; /** * 商品 */ @Data public class TestGoods { /** * 商品ID */ private Long goodsId; /** * 商品名稱 */ private String goodsName; /** * 商品庫存 */ private Integer goodsStock; }
首先呢咱們要模擬一個沒有限流的場景
咱們先要將goods表中的庫存修改成100(本身自定義),這裏咱們就模擬只有100個庫存的商品
個人習慣是從Controller層到持久層,這個因人而異吧。。沒有什麼強制要求
OrderController
package com.example.rabbitmq.controller; import com.example.rabbitmq.common.ApiResponse; import com.example.rabbitmq.entity.TestOrder; import com.example.rabbitmq.service.OrderService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * 訂單Controller */ @RestController @RequestMapping("order") public class OrderController { @Autowired(required = false) private OrderService orderService; private static Integer count = 0; private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class); /** * 無RabbitMQ建立訂單 * @return */ @GetMapping("/save/{goodsId}") public ApiResponse save(@PathVariable("goodsId") Long goodsId){ ApiResponse apiResponse = this.orderService.save(goodsId); LOGGER.info("流量請求:" + count++); return apiResponse; } }
上面代碼可能會報錯,不過不要慌,心不亂,手不抖,咱們跟着感受走
那是由於咱們有些類尚未建立
接下里就是咱們的Service類建立
OrderService
package com.example.rabbitmq.service; import com.example.rabbitmq.common.ApiResponse; import com.example.rabbitmq.entity.TestOrder; import java.util.List; public interface OrderService { /** * 無RabbitMQ消峯限流 * @return */ ApiResponse save(Long goodsId); }
OrderServiceImpl
package com.example.rabbitmq.service.impl; import com.example.rabbitmq.common.ApiResponse; import com.example.rabbitmq.entity.TestGoods; import com.example.rabbitmq.entity.TestOrder; import com.example.rabbitmq.mapper.GoodsMapper; import com.example.rabbitmq.mapper.OrderMapper; import com.example.rabbitmq.service.OrderService; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.List; import java.util.Random; import java.util.UUID; /** * OrderService */ @Service public class OrderServiceImpl implements OrderService { @Autowired(required = false) private AmqpTemplate amqpTemplate; @Autowired(required = false) private OrderMapper orderMapper; @Autowired(required = false) private GoodsMapper goodsMapper; /** * 無RabbitMQ消峯限流 * * @return */ @Override public ApiResponse save(Long goodsId) { if (goodsId == null){ return new ApiResponse().code(400).msg("參數錯誤"); } // 根據商品Id查詢商品 TestGoods testGoods = this.goodsMapper.selectStockById(goodsId); // 商品不存在或者商品庫存爲0 if (testGoods == null || testGoods.getGoodsStock() <= 0){ return new ApiResponse().code(400).msg("商品不存在或者庫存爲0"); } // 直接添加 // 建立訂單 TestOrder testOrder = new TestOrder(); // 設置參數 testOrder.setOrderUserEmail("111@qq.com"); testOrder.setOrderUserName("FC"); testOrder.setOrderDate(new Date()); this.orderMapper.save(testOrder); // 更新庫存 this.goodsMapper.updateGoodsStock(goodsId); return new ApiResponse().code(200).msg("訂單建立成功"); } }
咱們來解釋一下咱們的正常邏輯
if (goodsId == null){ return new ApiResponse().code(400).msg("參數錯誤"); }
// 根據商品Id查詢商品 TestGoods testGoods = this.goodsMapper.selectStockById(goodsId); // 商品不存在或者商品庫存爲0 if (testGoods == null || testGoods.getGoodsStock() <= 0){ return new ApiResponse().code(400).msg("商品不存在或者庫存爲0"); }
先從數據庫中查詢商品信息,返回查詢結果
判斷是否爲空或者是庫存是否爲0
// 直接添加 // 建立訂單 TestOrder testOrder = new TestOrder(); // 設置參數 testOrder.setOrderUserEmail("111@qq.com"); testOrder.setOrderUserName("FC"); testOrder.setOrderDate(new Date()); this.orderMapper.save(testOrder);
上面條件都知足,那麼咱們能夠下訂單了
// 更新庫存 this.goodsMapper.updateGoodsStock(goodsId);
固然呢咱們下單成功了,固然要更新咱們的庫存吶。
那這就是咱們下單的通常邏輯啦
這裏確定是有問題的啦,這樣咱們全部的請求都打在咱們的數據庫上了,那數據庫確定是罩不住的,那要崩啊
稍後會給出解決方案
GoodsService
package com.example.rabbitmq.service; import com.example.rabbitmq.entity.TestGoods; public interface GoodsService { /** * 根據商品Id查詢商品庫存 * @param goodsId * @return */ TestGoods selectGoodsById(Long goodsId); /** * 更新庫存 * @param goodsId */ void updateGoodsStock(Long goodsId); }
GoodsServiceImpl
package com.example.rabbitmq.service.impl; import com.example.rabbitmq.entity.TestGoods; import com.example.rabbitmq.mapper.GoodsMapper; import com.example.rabbitmq.service.GoodsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * 商品Service */ @Service public class GoodsServiceImpl implements GoodsService { @Autowired(required = false) private GoodsMapper goodsMapper; /** * 根據商品Id查詢商品 * @param goodsId * @return */ @Override public TestGoods selectGoodsById(Long goodsId) { if (goodsId == null){ return null; } TestGoods testGoods = this.goodsMapper.selectStockById(goodsId); return testGoods; } /** * 更新庫存 * * @param goodsId */ @Override public void updateGoodsStock(Long goodsId) { if (goodsId == null){ return; } try { this.goodsMapper.updateGoodsStock(goodsId); return; }catch (Exception e) { return; } } }
GoodsMapper
package com.example.rabbitmq.mapper; import com.example.rabbitmq.entity.TestGoods; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface GoodsMapper { /** * 根據商品Id查詢商品 * @param goodsId * @return */ TestGoods selectStockById(Long goodsId); /** * 更新庫存 * @param goodsId */ void updateGoodsStock(Long goodsId); }
GoodsMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.example.rabbitmq.mapper.GoodsMapper"> <resultMap id="BaseGoodsMap" type="com.example.rabbitmq.entity.TestGoods"> <id property="goodsId" column="goods_id"/> <result property="goodsName" column="goods_name"/> <result property="goodsStock" column="goods_stock"/> </resultMap> <!-- 根據商品Id查詢商品 --> <select id="selectStockById" resultMap="BaseGoodsMap" parameterType="java.lang.Long"> select goods_stock from test_goods where goods_id = #{goodsId}; </select> <!-- 更新庫存 --> <update id="updateGoodsStock" parameterType="java.lang.Long"> update test_goods set goods_stock = goods_stock - 1 where goods_id = #{goodsId} ; </update> </mapper>
OrderMapper
package com.example.rabbitmq.mapper; import com.example.rabbitmq.entity.TestOrder; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Repository; /** * */ @Mapper @Repository public interface OrderMapper { /** * 無RabbitMQ消峯限流保存訂單 */ void save(TestOrder testOrder); }
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.example.rabbitmq.mapper.OrderMapper"> <resultMap id="BaseOrderMap" type="com.example.rabbitmq.entity.TestOrder"> <id property="orderId" column="order_id"/> <result property="orderUserName" column="order_user_name"/> <result property="orderUserEmail" column="order_user_email"/> <result property="orderDate" column="order_date"/> </resultMap> <insert id="save" parameterType="com.example.rabbitmq.entity.TestOrder"> insert into test_order (order_user_name, order_user_email, order_date) values ( #{orderUserName}, #{orderUserEmail}, #{orderDate} ); </insert> </mapper>
OK,這裏咱們基本操做就完成了,這些都是咱們的正常操做
那麼咱們啓動項目來測試一下
這裏咱們先用postman來驗證咱們的接口
咱們能夠在控制檯看見
咱們的所有請求都到了數據庫上,咱們這裏只是發起了兩次請求,數據庫確定不會啊,數據庫應該每秒可以撐起2000次請求吧,那麼要是超過了2000,那就要出現問題了呀。。。
咱們來看數據庫中的數據
庫存信息
訂單信息
這樣咱們就能夠輕鬆的建立訂單了,哇哈哈哈哈
可是事實不撩人啊,咋個這麼輕鬆哦,,那是不可能的
那麼咱們就須要壓力測試來啦,這裏咱們使用的是jmeter
這裏咱們建立了4000個線程來請求咱們接口
這樣看咱們的數據庫壓力還大不大,哼
那就來瞧一瞧咱們數據庫怎麼來解決吧
媽呀,這是什麼哦,怎麼能夠亂來哦。
這裏咱們看見,雖然有查詢,有添加,還有更新庫存的操做
可是呢,別個還在執行的時候,我還在下訂單的時候,另一個就在更新庫存,Are You Sure?
這。。。。
這我看不下去了
那麼咱們再來看看絕望的時刻
哇哦,這就很尷尬了,這樣下去不得了啊,咱們不得虧死啊,盡然還超賣了。。。。
爲何會出現這樣的狀況呢?
當咱們的請求流量瞬時就來了,並且通常仍是同一時間來的,這樣咱們的所有流量就打到咱們的數據庫上,
當咱們一個線程還在查詢這一步,查詢出來哦,還有1個庫存,能夠下訂單,那麼另一個線程也來了,
查詢出來,哦,我也還有1個庫存,能夠下訂單。
這樣兩個線程都去更新庫存信息,這樣就會出現超賣的狀況那。
在咱們秒殺活動中,若是這樣去實現,那不得虧死哦。
那麼咱們應該怎麼辦呢?左想一想,右想一想
哦,咱們不是學過消息隊列嗎?咱們能夠用這個來進行優化啊。。。
消息隊列中不是能夠消峯限流嗎?
鼠標往上一劃,哇哦,原來寫了這麼多啦,那咱們就先這樣結束嗎?
不會,咱們在下一節會繼續講到的,咱們下期再見啦
拜拜