1. 秒殺優化-異步秒殺思路
我們來回顧一下下單流程
當用戶發起請求,此時會請求nginx
,nginx
會訪問到tomcat
,而tomcat
中的程序,會進行串行操作,分成如下幾個步驟
-
1、查詢優惠卷
-
2、判斷秒殺庫存是否足夠
-
3、查詢訂單
-
4、校驗是否是一人一單
-
5、扣減庫存
-
6、創建訂單
在這六步操作中,由于有很多操作是要去操作數據庫的,而且還是一個線程串行執行,添加了分布式鎖, 這樣就會導致我們這段程序執行耗時比較長,并發能力變弱了,所以我們需要異步程序執行,那么如何優化呢?
在這里筆者想給大家分享一下課程內沒有的思路,看看有沒有小伙伴這么想,比如,我們可以不可以使用異步編排來做,或者說我開啟N多個線程,一個線程執行查詢優惠卷,一個執行判斷扣減庫存,一個去創建訂單等等,然后再統一做返回,這種做法和課程中有哪種好呢?答案是課程中的好,因為如果你采用我剛說的方式,如果訪問的人很多,那么線程池中的線程可能一下子就被消耗完了,而且你使用上述方案,最大的特點在于,你覺得時效性會非常重要,但是你想想是嗎?并不是,比如我只要確定他能做這件事,然后我后邊慢慢做就可以了,我并不需要他一口氣做完這件事,所以我們應當采用的是課程中,類似消息隊列的方式來完成我們的需求,而不是使用線程池或者是異步編排的方式來完成這個需求。
優化方案:我們將耗時比較短的邏輯判斷放入到redis
中,比如是否庫存足夠,比如是否一人一單,這樣的操作,只要這種邏輯可以完成,就意味著我們是一定可以下單完成的,我們只需要進行快速的邏輯判斷,根本就不用等下單邏輯走完,我們直接給用戶返回成功, 再在后臺開一個線程,后臺線程慢慢的去執行阻塞queue
里邊的消息,這樣程序不就超級快了嗎?而且也不用擔心線程池消耗殆盡的問題,因為這里我們的程序中并沒有手動使用任何線程池,當然這里邊有兩個難點:
-
第一個難點是我們怎么在
redis
中去快速校驗一人一單,還有庫存判斷 -
第二個難點是由于我們校驗和
tomcat
下單是兩個線程,那么我們如何知道到底哪個單他最后是否成功,或者是下單完成,為了完成這件事我們在redis
操作完之后,我們會將一些信息返回給前端,同時也會把這些信息丟到異步queue
中去,后續操作中,可以通過這個id來查詢我們tomcat
中的下單邏輯是否完成了。
我們現在來看看整體思路:
- 選擇Redis存儲結構: 用戶下單我們只需要存儲庫存變量即可,可以直接使用
string
類型結構,而一人一單問題,我們是需要存儲很多用戶的購買記錄,并且用戶不能重復下單,所以這里我們選擇set
存儲結構再適合不過。 - 邏輯:當用戶下單之后,判斷庫存是否充足只需要到
redis
中去根據key
找對應的value
是否大于0即可,如果不充足,則直接結束,如果充足,繼續在redis
中判斷用戶是否可以下單,如果set
集合中沒有這條數據,說明他可以下單,并扣減庫存,再將userId存入當前優惠券的set
中,并且返回0,整個過程需要保證是原子性的,我們可以使用lua
來操作。當以上判斷邏輯走完之后,我們可以判斷當前redis
中返回的結果是否是0 ,如果是0,則表示可以下單,則將之前說的信息存入到阻塞queue
中去,此時開啟單獨的線程異步寫入數據庫中,最后返回訂單id,前端可以通過返回的訂單id來判斷是否下單成功。
2. 秒殺優化-Redis完成秒殺資格判斷
需求:
-
新增秒殺優惠券的同時,將優惠券信息保存到Redis中
-
基于Lua腳本,判斷秒殺庫存、一人一單,決定用戶是否搶購成功
-
如果搶購成功,將優惠券id和用戶id封裝后存入阻塞隊列
-
開啟線程任務,不斷從阻塞隊列中獲取信息,實現異步下單功能
VoucherServiceImpl
👇 將用戶資格判斷放入redis中,用戶響應信息只有和redis操作,性能大幅提高,新增秒殺優惠券的同時,將優惠券庫存同步到redis中
@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {// 保存優惠券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_KEY 這個變量定義在RedisConstans中//private static final String SECKILL_STOCK_KEY ="seckill:stock:"stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
seckill.lua腳本
👇 對redis的查詢、判斷、數據操作寫入一個lua腳本,防止線程并發安全問題
-- 1.參數列表
-- 1.1.優惠券id
local voucherId = ARGV[1]
-- 1.2.用戶id
local userId = ARGV[2]
-- 1.3.訂單id
local orderId = ARGV[3]-- 2.數據key
-- 2.1.庫存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.訂單key
local orderKey = 'seckill:order:' .. voucherId-- 3.腳本業務
-- 3.1.判斷庫存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then-- 3.2.庫存不足,返回1return 1
end
-- 3.2.判斷用戶是否下單 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,說明是重復下單,返回2return 2
end
-- 3.4.扣庫存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下單(保存用戶)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
寫完lua
腳本,剩下的就是執行lua
腳本,根據腳本拿到的結果,為非0
,根據情況返回"庫存不足"
或者 "不能重復下單
;為0
,返回全局id生成器生成的訂單id。至于訂單保存到阻塞隊列中的邏輯,咱們稍后實現。
VoucherOrderServiceImpl
3. 秒殺優化-基于阻塞隊列實現秒殺優化
創建阻塞隊列,保存數據類型為VoucherOrder
,并設置大小為 1024 * 1024
處理訂單操作的邏輯:👇
4. 總結
秒殺業務的優化思路是什么?
- 先利用Redis完成庫存余量、一人一單判斷,完成搶單業務
- 再將下單業務放入阻塞隊列,利用獨立線程異步下單
基于阻塞隊列的異步秒殺存在哪些問題?
- 內存限制問題(使用jdk中的阻塞隊列,以后如果有大量的訂單需要創建,很容易出現OOM問題)
- 數據安全問題(數據是存儲在內存里的,若出現服務宕機了,任務還沒執行完畢,導致用戶的訂單數據丟失)
針對此問題,我們將會再下一篇解決。