微服務(wù)Spring?Boot?整合Redis?阻塞隊(duì)列實(shí)現(xiàn)異步秒殺下單思路詳解
?引言
本章節(jié),介紹使用阻塞隊(duì)列實(shí)現(xiàn)秒殺的優(yōu)化,采用異步秒殺完成下單的優(yōu)化!
一、秒殺優(yōu)化 - 異步秒殺思路
當(dāng)用戶發(fā)起請求,此時(shí)會(huì)請求nginx,nginx會(huì)訪問到tomcat,而tomcat中的程序,會(huì)進(jìn)行串行操作,分成如下幾個(gè)步驟
- 查詢優(yōu)惠卷
- 判斷秒殺庫存是否足夠
- 查詢訂單
- 校驗(yàn)是否是一人一單
- 扣減庫存
- 創(chuàng)建訂單,完成
在以上6個(gè)步驟中,我們可以采用怎樣的方式來優(yōu)化呢?
整體思路:當(dāng)用戶下單之后,判斷庫存是否充足只需要導(dǎo)redis中去根據(jù)key找對應(yīng)的value是否大于0即可,如果不充足,則直接結(jié)束,如果充足,繼續(xù)在redis中判斷用戶是否可以下單,如果set集合中沒有這條數(shù)據(jù),說明他可以下單,如果set集合中沒有這條記錄,則將userId和優(yōu)惠卷存入到redis中,并且返回0,整個(gè)過程需要保證是原子性的,我們可以使用Lua來操作
當(dāng)以上邏輯走完后,我們可以根據(jù)返回的結(jié)果來判斷是否是0,如果是0,則可以下單,可以存入 queue 隊(duì)列中,然后返回,前端可以通過返回的訂單id來判斷是否下單成功。
二、秒殺優(yōu)化 - 基于Redis完成秒殺資格判斷
需求:
- 新增秒殺優(yōu)惠卷的同時(shí),需要將優(yōu)惠卷信息保存在redis中
- 基于Lua腳本實(shí)現(xiàn),判斷秒殺庫存、一人一單,決定用戶是否搶購成功
- 如果搶購成功,將優(yōu)惠卷id和用戶id封裝后存入阻塞隊(duì)列
- 開啟線程任務(wù),不斷從阻塞隊(duì)列中獲取信息,實(shí)現(xiàn)異步下單功能
新增優(yōu)惠卷時(shí),將優(yōu)惠卷信息存入Redis
VoucherService
@Override @Transactional public void addSeckillVoucher(Voucher voucher) { // 保存優(yōu)惠券 save(voucher); // 保存秒殺信息 SeckillVoucher seckillVoucher = new SeckillVoucher(); seckillVoucher.setVoucherId(voucher.getId()); seckillVoucher.setStock(voucher.getStock()); seckillVoucher.setBeginTime(voucher.getBeginTime()); seckillVoucher.setEndTime(voucher.getEndTime()); seckillVoucherService.save(seckillVoucher); // 保存秒殺庫至redis seckill:stock stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString()); }
新增優(yōu)惠卷時(shí),可存入redis信息
編寫 Lua 腳本,實(shí)現(xiàn)秒殺資格判斷
seckill Lua 秒殺腳本
-- 1.參數(shù)列表 -- 1.1 優(yōu)惠卷id local voucherId = ARGV[1] -- 1.2 用戶id local userId = ARGV[2] -- 2. 數(shù)據(jù)key -- 2.1 庫存key 拼接 .. local stockKey = 'seckill:stock:' .. voucherId -- 2.2 訂單key 拼接 .. local orderKey = "seckill:order" .. voucherId -- 3. 腳本業(yè)務(wù) -- 3.1 判斷庫存是否充足 if (tonumber(redis.call('get', stockKey)) <= 0) then -- 3.2 庫存不足 return 1 end -- 3.2 判斷用戶是否下單 SISMEMBER orderKey userId if (redis.call('sismember', orderKey, userId) == 1) then -- 3.3 存在,證明是重復(fù)下單 return 2 end -- 3.4 扣庫存 incrby stockKey -1 redis.call('incrby', stockKey, -1) -- 3.5 下單 保存用戶 sadd orderKey userId redis.call('sadd', orderKey, userId) return 0
三、基于阻塞隊(duì)列完成異步秒殺下單
基于阻塞隊(duì)列實(shí)現(xiàn)異步秒殺下單
核心思路:將請求存入阻塞隊(duì)列中 進(jìn)行緩存,開啟線程池讀取任務(wù)并依次處理。
VoucherOrderService
private static final DefaultRedisScript<Long> SECKILL_SCRIPT; static { SECKILL_SCRIPT = new DefaultRedisScript<>(); SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); SECKILL_SCRIPT.setResultType(Long.class); } private BlockingQueue<VoucherOrder> orderTasks =new ArrayBlockingQueue<>(1024 * 1024); private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor(); //項(xiàng)目啟動(dòng)后執(zhí)行該方法 @PostConstruct private void init() { SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); } // 用于線程池處理的任務(wù) // 當(dāng)初始化完畢后 就會(huì)去從對列中去拿信息 private class VoucherOrderHandler implements Runnable { @Override public void run() { while (true){ try { // 1.獲取隊(duì)列中的訂單信息 VoucherOrder voucherOrder = orderTasks.take(); // 2.創(chuàng)建訂單 handleVoucherOrder(voucherOrder); } catch (Exception e) { log.error("處理訂單異常", e); } } } } private void handleVoucherOrder(VoucherOrder voucherOrder) { //1.獲取用戶 Long userId = voucherOrder.getUserId(); // 2.創(chuàng)建鎖對象 RLock lock = redissonClient.getLock("lock:order:" + userId); // 3.嘗試獲取鎖 boolean isLock = lock.tryLock(); // 4.判斷是否獲得鎖成功 if (!isLock) { // 獲取鎖失敗,直接返回失敗或者重試 log.error("不允許重復(fù)下單!"); return; } try { //注意:由于是spring的事務(wù)是放在threadLocal中,此時(shí)的是多線程,事務(wù)會(huì)失效 proxy.createVoucherOrder(voucherOrder); } finally { // 釋放鎖 lock.unlock(); } } // 代理對象 private IVoucherOrderService proxy; @Override public Result seckillVoucher(Long voucherId) { // 獲取用戶 Long userId = UserHolder.getUser().getId(); // 獲取訂單id long orderId = redisIdWorker.nextId("order"); // 1. 執(zhí)行l(wèi)ua 腳本 Long result = stringRedisTemplate.execute( SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString(), String.valueOf(orderId) ); int r = result.intValue(); // 2. 判斷結(jié)果是否為0 if (r != 0) { // 2.1 不為0 代表沒有購買資格 return Result.fail(r == 1 ? "庫存不足" : "不允許重復(fù)下單"); } // 2.2 為0,有購買資格 把下單信息保存到阻塞隊(duì)列 // 2.2 有購買的資格,創(chuàng)建訂單放入阻塞隊(duì)列中 VoucherOrder voucherOrder = new VoucherOrder(); // 2.3.訂單id voucherOrder.setId(orderId); // 2.4.用戶id voucherOrder.setUserId(userId); // 2.5.代金券id voucherOrder.setVoucherId(voucherId); // 2.6.放入阻塞隊(duì)列 orderTasks.add(voucherOrder); //3.獲取代理對象 proxy = (IVoucherOrderService)AopContext.currentProxy(); // 2.3 返回訂單id return Result.ok(orderId); } @Transactional public void createVoucherOrder (VoucherOrder voucherOrder){ // 5.一人一單邏輯 // 5.1.用戶id Long userId = voucherOrder.getUserId(); // 判斷是否存在 int count = query().eq("user_id", userId) .eq("voucher_id", voucherOrder.getId()).count(); // 5.2.判斷是否存在 if (count > 0) { // 用戶已經(jīng)購買過了 log.error("用戶已經(jīng)購買過了"); } //6,扣減庫存 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") //set stock = stock -1 .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0).update(); //where id = ? and stock > 0 // .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); //where id = ? and stock = ? if (!success) { //扣減庫存 log.error("庫存不足!"); } save(voucherOrder); }
四、測試程序
ApiFox 測試程序
測試成功,查看Redis
成功添加訂單信息
庫存信息
數(shù)據(jù)庫信息
Jmeter 進(jìn)行壓力測試
恢復(fù)數(shù)據(jù),進(jìn)行壓力測試
關(guān)于測試:新增了1000條用戶信息,存入數(shù)據(jù)庫和Redis,token,Jmeter使用Tokens文件測試1000條并發(fā)
相關(guān)資料見下文
進(jìn)行壓測
經(jīng)過檢測,性能提升了幾十倍!
數(shù)據(jù)庫
五、源碼地址
Jmeter測試文件:https://www.bilibili.com/video/av251263036/
以上就是【Bug 終結(jié)者】對 微服務(wù)Spring Boot 整合Redis 阻塞隊(duì)列實(shí)現(xiàn)異步秒殺下單 的簡單介紹,在分布式系統(tǒng)下,高并發(fā)的場景下,使用阻塞隊(duì)列來優(yōu)化秒殺下單,但依舊不是最優(yōu)解,持續(xù)更新中!下章節(jié) 采用消息隊(duì)列優(yōu)化秒殺下單!
到此這篇關(guān)于微服務(wù)Spring Boot 整合Redis 阻塞隊(duì)列實(shí)現(xiàn)異步秒殺下單的文章就介紹到這了,更多相關(guān)Spring Boot 整合Redis 阻塞隊(duì)列內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot項(xiàng)目打包鏡像方式以及區(qū)分環(huán)境打包的方法
本文主要介紹了springboot項(xiàng)目打包鏡像方式以及區(qū)分環(huán)境打包的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-03-03快速學(xué)習(xí)JavaWeb中監(jiān)聽器(Listener)的使用方法
這篇文章主要幫助大家快速學(xué)習(xí)JavaWeb中監(jiān)聽器(Listener)的使用方法,感興趣的小伙伴們可以參考一下2016-09-09SpringBoot項(xiàng)目使用mybatis-plus逆向自動(dòng)生成全套代碼
在JavaWeb工程中,每一個(gè)SSM新項(xiàng)目或者說是SpringBoot項(xiàng)目也好,都少不了model、controller、service、dao等層次的構(gòu)建。使用mybatis-plus逆向可以自動(dòng)生成,感興趣的可以了解一下2021-09-09Java基礎(chǔ)教程之類數(shù)據(jù)與類方法
這篇文章主要介紹了Java基礎(chǔ)教程之類數(shù)據(jù)與類方法,本文是對類的深入探討,類數(shù)據(jù)指類的一些屬性、參數(shù)等,類方法就是類包含的功能方法,需要的朋友可以參考下2014-08-08Java純代碼實(shí)現(xiàn)導(dǎo)出pdf合并單元格
這篇文章主要為大家詳細(xì)介紹了Java如何純代碼實(shí)現(xiàn)導(dǎo)出pdf與合并單元格功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12java中Date和Timestamp類型的相互轉(zhuǎn)換方式
這篇文章主要介紹了java中Date和Timestamp類型的相互轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07Spring Boot中@RequestParam參數(shù)的5種情況說明
這篇文章主要介紹了Spring Boot中@RequestParam參數(shù)的5種情況說明,具有很好的參考價(jià)值,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08SpringBoot+mybatis實(shí)現(xiàn)多數(shù)據(jù)源支持操作
這篇文章主要介紹了SpringBoot+mybatis實(shí)現(xiàn)多數(shù)據(jù)源支持操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10Spring系列中的beanFactory與ApplicationContext
這篇文章主要介紹了Spring系列中的beanFactory與ApplicationContext,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09