自己做電影網(wǎng)站可以賺錢嗎新媒體運(yùn)營培訓(xùn)班
全局唯一ID
全局唯一ID生成策略:
- UUID
- Redis自增
- snowflake算法
- 數(shù)據(jù)庫自增
Redis自增ID策略: - 每天一個key,方便統(tǒng)計訂單量
- ID構(gòu)造是 時間戳 + 計數(shù)器
@Component
public class RedisIdWorker {// 2024的第一時刻private static final long BEGIN_TIMESTAMP = 1704067200L;private static final int COUNT_BITS = 32;private final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix){// 1. 獲取當(dāng)前時間戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timeStamp = nowSecond - BEGIN_TIMESTAMP;// 2. 獲取序列號// 2.1 獲取當(dāng)天日期,精確到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2 自增Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3. 拼接成IDreturn timeStamp << COUNT_BITS | count;}}
超賣問題
在處理大量請求時,可能會出現(xiàn)超賣問題。
可以通過加鎖解決。
這里采用的是樂觀鎖。
一人一單
該任務(wù)需要每名用戶只能搶到一張優(yōu)惠券。
同時還要考慮到后端部署在多個服務(wù)器上可能會出現(xiàn)的異常,此時需要使用分布式鎖進(jìn)行解決。
這里的分布式鎖基于Redis實現(xiàn)。
基于Redis的分布式鎖實現(xiàn)思路
基于Redis的分布式鎖實現(xiàn)思路:
- 利用set nx ex獲取鎖,并設(shè)置過期時間,保存線程標(biāo)示
- 釋放鎖時先判斷線程標(biāo)示是否與自己一致,一致則刪除鎖
特性:
- 利用set nx滿足互斥性
- 利用set ex保證故障時鎖依然能釋放,避免死鎖,提高安全性
- 利用Redis集群保證高可用和高并發(fā)特性
使用Redis優(yōu)化秒殺
這里將庫存判斷與一人一單的校驗使用Redis完成。
具體流程為:
- 新增秒殺優(yōu)惠券的同時,將優(yōu)惠券信息保存到Redis中
- 基于Lua腳本,判斷秒殺庫存、一人一單,決定用戶是否搶購成功
- 如果搶購成功,將優(yōu)惠券id和用戶id封裝后存入阻塞隊列
- 開啟線程任務(wù),不斷從阻塞隊列中獲取信息,實現(xiàn)異步下單功能
這里的阻塞隊列是基于Stream的消息隊列
STREAM類型消息隊列的XREADGROUP命令特點: - 消息可回溯
- 可以多消費者爭搶消息,加快消費速度
- 可以阻塞讀取
- 沒有消息漏讀的風(fēng)險
- 有消息確認(rèn)機(jī)制,保證消息至少被消費一次
新增秒殺優(yōu)惠券的同時,將優(yōu)惠券信息保存到Redis中
@Override@Transactionalpublic 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);// 保存優(yōu)惠券至RedisstringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY +voucher.getId(), voucher.getStock().toString());}
基于Lua腳本,判斷秒殺庫存、一人一單,決定用戶是否搶購成功
-- 1.參數(shù)列表
local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]
-- 2. 數(shù)據(jù)key
local stockKey = "seckill:stock:" .. voucherId
local orderKey = "seckill:order:" .. voucherId-- 3. 判斷庫存是否充足
if tonumber(redis.call('get', stockKey) )< 1 then-- 庫存不足return 1
end-- 4. 判斷用戶是否已經(jīng)搶購過
if redis.call('sismember', orderKey, userId) == 1 then-- 已經(jīng)搶購過return 2
end-- 5. 減庫存
redis.call('incrby', stockKey, -1)
-- 6. 記錄用戶搶購信息
redis.call('sadd', orderKey, userId)redis.call('xadd', "stream.orders", "*", "userId", userId, "voucherId", voucherId,"id", orderId)
return 0
異步下單
LUA腳本
-- 1.參數(shù)列表
local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]
-- 2. 數(shù)據(jù)key
local stockKey = "seckill:stock:" .. voucherId
local orderKey = "seckill:order:" .. voucherId-- 3. 判斷庫存是否充足
if tonumber(redis.call('get', stockKey) )< 1 then-- 庫存不足return 1
end-- 4. 判斷用戶是否已經(jīng)搶購過
if redis.call('sismember', orderKey, userId) == 1 then-- 已經(jīng)搶購過return 2
end-- 5. 減庫存
redis.call('incrby', stockKey, -1)
-- 6. 記錄用戶搶購信息
redis.call('sadd', orderKey, userId)redis.call('xadd', "stream.orders", "*", "userId", userId, "voucherId", voucherId,"id", orderId)
return 0
從消息隊列取出,處理代碼
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;private IVoucherOrderService proxy;// 靜態(tài)代碼塊加載Lua腳本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();@PostConstruct// 完成類的construct即執(zhí)行下面的函數(shù)public void init() {// 交給線程池做SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}String queueName = "stream.orders";// 消費線程private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while (true) {try {// 4.1從消息隊列中取出訂單 xreadgroupnngroup g1 c1 count 1 block 200 streams streams.order >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create(queueName, ReadOffset.lastConsumed()));// 判斷是否成功// 沒有if (list == null || list.isEmpty()) {continue;}// 有// 4.2創(chuàng)建訂單,解析消息MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);handleVoucherOrder(voucherOrder);// ACK確認(rèn)stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());} catch (Exception e) {log.error("訂單處理失敗", e);handlePendingList();}}}}private void handlePendingList() {while (true) {try {// 4.1從消息隊列中取出訂單 xreadgroupnngroup g1 c1 count 1 block 200 streams streams.order >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1),StreamOffset.create(queueName, ReadOffset.from("0")));// 判斷是否成功// 沒有if (list == null || list.isEmpty()) {// pendingList 沒有消息break;}// 有// 4.2創(chuàng)建訂單,解析消息MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);handleVoucherOrder(voucherOrder);// ACK確認(rèn)stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());} catch (Exception e) {log.error("訂單處理失敗", e);try {Thread.sleep(200);} catch (InterruptedException ex) {throw new RuntimeException(ex);}}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {// 因為為子線程,userId只能從數(shù)據(jù)中取Long userId = voucherOrder.getUserId();RLock lock = redissonClient.getLock("order:" + userId);boolean isLock = lock.tryLock();if (!isLock) {log.error("重復(fù)搶購");return ;}try {proxy.createVoucherOrder(voucherOrder);return;} finally {lock.unlock();}}
創(chuàng)建訂單
@Transactionalpublic Result createVoucherOrder(VoucherOrder voucherOrder) {// 4.一人一單Long userId = voucherOrder.getUserId();// 4.1 查詢訂單int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();if (count > 0) {return Result.fail("每人限購一張");}// 4.是// 4.1扣減庫存,基于樂觀鎖boolean success = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0).update();if (!success) {return Result.fail("庫存不足");}//4.2創(chuàng)建訂單save(voucherOrder);//4.3返回訂單idreturn Result.ok(voucherOrder.getId());}
@Overridepublic Result seckillVoucher(Long voucherId) {Long userId = UserHolder.getUser().getId();// 0. 生成訂單idlong 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));// 2. 結(jié)果是否為0int r = result.intValue();// 2.1 不為0if (r!=0) {return Result.fail(r == 1 ? "庫存不足" : "不能重復(fù)搶購");}// 注解底層基于aop實現(xiàn),需要獲得代理對象,進(jìn)行執(zhí)行proxy = (IVoucherOrderService)AopContext.currentProxy();// 3. 返回結(jié)果訂單idreturn Result.ok(orderId);}