先看基本的業(yè)務(wù)流程
?那么我們可以看到整個流程都是一個線程來完成的,這樣的話耗時還是很長的,那么可不可以采用多線程去實現(xiàn)呢?
首先我們要思考怎么對業(yè)務(wù)進行拆分,可以想象一個我們?nèi)ワ埖挈c餐,會有前臺接待,詢問訂單,之后將小票傳給后廚去做飯,這樣就會快很多,也可以接待更多的客人
也就是說 一個線程負責去讀數(shù)據(jù)庫做準備工作,另一個線程去實現(xiàn)寫操作,如下圖中所示:
? ? 確定了我們可以將判斷庫存和檢驗一人一單業(yè)務(wù)抽取出來之后,我們在想一下 還能不能優(yōu)化,這個時候我們會發(fā)現(xiàn),這兩個操作還是在數(shù)據(jù)庫進行的,那么mysql的并發(fā)本身也是不高的,現(xiàn)在我們就要通過另一個性能更好的數(shù)據(jù)庫進行實現(xiàn),就是redis
? ? ?
?這樣只需要業(yè)務(wù)進行到校驗完成就可以給用戶返回下單完成的信息,之后在通過另一個線程異步進行扣減庫存操作
redis中實現(xiàn)上面兩個操作的業(yè)務(wù)流程如下:
?由于操作流程較長,應(yīng)該使用lua腳本來保證原子性
將上面的邏輯采用lua腳本進行編寫,之后程序運行首先判斷返回值如果是0就說明用戶有下單資格,如果是1或者2就說明用戶沒有資格下單
如果有下單資格就可以將用戶id,優(yōu)惠券id,和訂單id存入一個阻塞隊列里面,之后異步進行寫入數(shù)據(jù)庫操作
整體流程:
提供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è)務(wù)
-- 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.存在,說明是重復(fù)下單,返回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
使用方式
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
@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 ? "庫存不足" : "不能重復(fù)下單");
}
// 3.返回訂單id
return Result.ok(orderId);
}
?redis的流程到此就完結(jié)了,接下來就是使用阻塞隊列存儲要進行寫操作的信息
阻塞隊列的實現(xiàn)方式通常是使用一個先進先出的隊列來存儲元素,同時使用鎖來實現(xiàn)線程安全。當隊列為空時,put()方法會被阻塞,直到有元素被添加到隊列中;當隊列滿時,put()方法同樣會被阻塞,直到隊列中有元素被移除。
阻塞隊列通常用于生產(chǎn)者-消費者模型中,生產(chǎn)者將元素添加到隊列中,消費者從隊列中取出元素進行處理。通過使用阻塞隊列,可以避免生產(chǎn)者和消費者之間的直接交互,從而簡化了代碼的設(shè)計和維護。
首先我們可以可以使用java自帶的阻塞隊列實現(xiàn),提供一個樣例:文章來源:http://www.zghlxwxcb.cn/news/detail-672797.html
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // 創(chuàng)建一個容量為10的阻塞隊列
Thread producer = new Thread(new Producer(queue));
Thread consumer = new Thread(new Consumer(queue));
producer.start();
consumer.start();
}
}
class Producer implements Runnable {
private BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
System.out.println("Producing " + i);
queue.put(i); // 將元素添加到隊列中
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
Integer item = queue.take(); // 從隊列中取出元素進行處理
System.out.println("Consuming " + item);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
當然如果想要性能更好的話,我們可以采用消息隊列來做文章來源地址http://www.zghlxwxcb.cn/news/detail-672797.html
到了這里,關(guān)于秒殺系統(tǒng)的業(yè)務(wù)流程以及優(yōu)化方案(實現(xiàn)異步秒殺)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!