Redis實用篇-黑馬頭條項目-優(yōu)惠卷秒殺功能(使用java阻塞隊列對秒殺進行異步優(yōu)化)
1、秒殺優(yōu)化
1.1 秒殺優(yōu)化-異步秒殺思路
我們來回顧一下下單流程
當用戶發(fā)起請求,此時會請求nginx,nginx會訪問到tomcat,而tomcat中的程序,會進行串行操作,分成如下幾個步驟
1、查詢優(yōu)惠卷
2、判斷秒殺庫存是否足夠
3、查詢訂單
4、校驗是否是一人一單
5、扣減庫存
6、創(chuàng)建訂單
在這六步操作中,又有很多操作是要去操作數(shù)據(jù)庫的,而且還是一個線程串行執(zhí)行, 這樣就會導致我們的程序執(zhí)行的很慢,所以我們需要異步程序執(zhí)行,那么如何加速呢?
在這里筆者想給大家分享一下課程內(nèi)沒有的思路,看看有沒有小伙伴這么想,比如,我們可以不可以使用異步編排來做,或者說我開啟N多線程,N多個線程,一個線程執(zhí)行查詢優(yōu)惠卷,一個執(zhí)行判斷扣減庫存,一個去創(chuàng)建訂單等等,然后再統(tǒng)一做返回,這種做法和課程中有哪種好呢?答案是課程中的好,因為如果你采用我剛說的方式,如果訪問的人很多,那么線程池中的線程可能一下子就被消耗完了,而且你使用上述方案,最大的特點在于,你覺得時效性會非常重要,但是你想想是嗎?并不是,比如我只要確定他能做這件事,然后我后邊慢慢做就可以了,我并不需要他一口氣做完這件事,所以我們應當采用的是課程中,類似消息隊列的方式來完成我們的需求,而不是使用線程池或者是異步編排的方式來完成這個需求
優(yōu)化方案:我們將耗時比較短的邏輯判斷放入到redis中,比如是否庫存足夠,比如是否一人一單,這樣的操作,只要這種邏輯可以完成,就意味著我們是一定可以下單完成的,我們只需要進行快速的邏輯判斷,根本就不用等下單邏輯走完,我們直接給用戶返回成功, 再在后臺開一個線程,后臺線程慢慢的去執(zhí)行queue里邊的消息,這樣程序不就超級快了嗎?而且也不用擔心線程池消耗殆盡的問題,因為這里我們的程序中并沒有手動使用任何線程池,當然這里邊有兩個難點
第一個難點是我們怎么在redis中去快速校驗一人一單,還有庫存判斷
第二個難點是由于我們校驗和tomct下單是兩個線程,那么我們?nèi)绾沃赖降啄膫€單他最后是否成功,或者是下單完成,為了完成這件事我們在redis操作完之后,我們會將一些信息返回給前端,同時也會把這些信息丟到異步queue中去,后續(xù)操作中,可以通過這個id來查詢我們tomcat中的下單邏輯是否完成了。
我們現(xiàn)在來看看整體思路:當用戶下單之后,判斷庫存是否充足只需要導redis中去根據(jù)key找對應的value是否大于0即可,如果不充足,則直接結(jié)束,如果充足,繼續(xù)在redis中判斷用戶是否可以下單,如果set集合中沒有這條數(shù)據(jù),說明他可以下單,如果set集合中沒有這條記錄,則將userId和優(yōu)惠卷存入到redis中,并且返回0,整個過程需要保證是原子性的,我們可以使用lua來操作
當以上判斷邏輯走完之后,我們可以判斷當前redis中返回的結(jié)果是否是0 ,如果是0,則表示可以下單,則將之前說的信息存入到到queue中去,然后返回,然后再來個線程異步的下單,前端可以通過返回的訂單id來判斷是否下單成功。
1.2 秒殺優(yōu)化-Redis完成秒殺資格判斷
需求:
-
新增秒殺優(yōu)惠券的同時,將優(yōu)惠券信息保存到Redis中
-
基于Lua腳本,判斷秒殺庫存、一人一單,決定用戶是否搶購成功
-
如果搶購成功,將優(yōu)惠券id和用戶id封裝后存入阻塞隊列
-
開啟線程任務,不斷從阻塞隊列中獲取信息,實現(xiàn)異步下單功能
VoucherServiceImpl
@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_KEY 這個變量定義在RedisConstans中
//private static final String SECKILL_STOCK_KEY ="seckill:stock:"
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}
完整lua表達式
-- 1.參數(shù)列表
-- 1.1.優(yōu)惠券id
local voucherId = ARGV[1]
-- 1.2.用戶id
local userId = ARGV[2]
-- 1.3.訂單id
local orderId = ARGV[3]
-- 2.數(shù)據(jù)key
-- 2.1.庫存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.訂單key
local orderKey = 'seckill:order:' .. voucherId
-- 3.腳本業(yè)務
-- 3.1.判斷庫存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.庫存不足,返回1
return 1
end
-- 3.2.判斷用戶是否下單 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,說明是重復下單,返回2
return 2
end
-- 3.4.扣庫存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下單(保存用戶)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.發(fā)送消息到隊列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0
當以上lua表達式執(zhí)行完畢后,剩下的就是根據(jù)步驟3,4來執(zhí)行我們接下來的任務了
VoucherOrderServiceImpl
@Override
public Result seckillVoucher(Long voucherId) {
//獲取用戶
Long userId = UserHolder.getUser().getId();
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 ? "庫存不足" : "不能重復下單");
}
//TODO 保存阻塞隊列
// 3.返回訂單id
return Result.ok(orderId);
}
1.3 秒殺優(yōu)化-基于阻塞隊列實現(xiàn)秒殺優(yōu)化
VoucherOrderServiceImpl
修改下單動作,現(xiàn)在我們?nèi)ハ聠螘r,是通過lua表達式去原子執(zhí)行判斷邏輯,如果判斷我出來不為0 ,則要么是庫存不足,要么是重復下單,返回錯誤信息,如果是0,則把下單的邏輯保存到隊列中去,然后異步執(zhí)行
//異步處理線程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
//在類初始化之后執(zhí)行,因為當這個類初始化好了之后,隨時都是有可能要執(zhí)行的
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
// 用于線程池處理的任務
// 當初始化完畢后,就會去從對列中去拿信息
private class VoucherOrderHandler implements Runnable{
@Override
public void run() {
while (true){
try {
// 1.獲取隊列中的訂單信息
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 redisLock = redissonClient.getLock("lock:order:" + userId);
// 3.嘗試獲取鎖
boolean isLock = redisLock.lock();
// 4.判斷是否獲得鎖成功
if (!isLock) {
// 獲取鎖失敗,直接返回失敗或者重試
log.error("不允許重復下單!");
return;
}
try {
//注意:由于是spring的事務是放在threadLocal中,此時的是多線程,事務會失效
proxy.createVoucherOrder(voucherOrder);
} finally {
// 釋放鎖
redisLock.unlock();
}
}
//a
private BlockingQueue<VoucherOrder> orderTasks =new ArrayBlockingQueue<>(1024 * 1024);
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
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 ? "庫存不足" : "不能重復下單");
}
VoucherOrder voucherOrder = new VoucherOrder();
// 2.3.訂單id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 2.4.用戶id
voucherOrder.setUserId(userId);
// 2.5.代金券id
voucherOrder.setVoucherId(voucherId);
// 2.6.放入阻塞隊列
orderTasks.add(voucherOrder);
//3.獲取代理對象
proxy = (IVoucherOrderService)AopContext.currentProxy();
//4.返回訂單id
return Result.ok(orderId);
}
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
// 5.1.查詢訂單
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
// 5.2.判斷是否存在
if (count > 0) {
// 用戶已經(jīng)購買過了
log.error("用戶已經(jīng)購買過了");
return ;
}
// 6.扣減庫存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣減失敗
log.error("庫存不足");
return ;
}
save(voucherOrder);
}
小總結(jié):文章來源:http://www.zghlxwxcb.cn/news/detail-453046.html
秒殺業(yè)務的優(yōu)化思路是什么?文章來源地址http://www.zghlxwxcb.cn/news/detail-453046.html
- 先利用Redis完成庫存余量、一人一單判斷,完成搶單業(yè)務
- 再將下單業(yè)務放入阻塞隊列,利用獨立線程異步下單
- 基于阻塞隊列的異步秒殺存在哪些問題?
- 內(nèi)存限制問題
- 數(shù)據(jù)安全問題
到了這里,關于微服務---Redis實用篇-黑馬頭條項目-優(yōu)惠卷秒殺功能(使用java阻塞隊列對秒殺進行異步優(yōu)化)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!