秒殺系統解決兩個核心問題
- 秒殺系統解決兩個核心問題:
- 一、解決庫存超賣的核心邏輯:
- 解釋:
- 原子性保證:
- 二、如何避免重復搶購:
- 使用 Redis 做唯一標識判斷
- 優點:
- 三、流程完整梳理:
- 四、通過數據庫建立唯一索引避免(用戶重復搶購)
- 原因背景
- 如何通過數據庫唯一索引避免重復下單?
- 程序中如何體現這一機制?
- 配合使用 Redis + 唯一索引實現雙重保障
秒殺系統解決兩個核心問題:
- 庫存超賣問題
- 用戶重復搶購問題
一、解決庫存超賣的核心邏輯:
關鍵點:數據庫層面的原子性更新 + 樂觀鎖判斷庫存是否充足
seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count", seckillGoods.getStockCount()).eq("id", seckillGoods.getId()).gt("stock_count", 0)
);
解釋:
eq("id", ...)
: 保證更新的是這條商品記錄。gt("stock_count", 0)
: 加了一條庫存大于 0 的條件,防止庫存為 0 仍被減。
這個 update()
方法只有在滿足條件(也就是庫存大于 0)時才會成功返回 true
,否則不更新。
原子性保證:
這個 UPDATE
操作由數據庫完成,是原子性的,同時加了條件判斷,避免并發環境下出現超賣問題。
二、如何避免重復搶購:
使用 Redis 做唯一標識判斷
String seckillOrderJson = (String) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (!StringUtils.isEmpty(seckillOrderJson)) {return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
優點:
- Redis 訪問速度極快,適合做高并發下的搶購標記。
- 提前判斷用戶是否已搶過,無需再查數據庫,提高性能。
三、流程完整梳理:
用戶請求 /seckill/doSeckill 接口↓
校驗用戶是否登錄↓
查詢商品庫存(goodsService.findGoodsVoByGoodsId)↓
判斷庫存是否為 0↓
從 Redis 判斷用戶是否已經下過單(防止重復搶購)↓
調用 service 層下單邏輯(orderService.seckill)↓① 減庫存(update + gt 判斷)② 生成訂單、秒殺訂單入庫③ 將訂單信息寫入 Redis 標記用戶已搶購↓
返回下單成功 / 失敗信息
四、通過數據庫建立唯一索引避免(用戶重復搶購)
在這個秒殺系統中,其實是非常關鍵的一步,是防止同一用戶對同一商品生成多個秒殺訂單的最終兜底措施。
原因背景
在高并發環境下,即使你在代碼中已經通過 Redis 判斷是否重復下單:
String seckillOrderJson = (String) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
if (!StringUtils.isEmpty(seckillOrderJson)) {return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
由于 Redis 與數據庫之間存在一定的 時延,仍可能出現“并發搶購成功但生成了兩個訂單”的情況 —— 即所謂的并發穿透檢查邏輯。
如何通過數據庫唯一索引避免重復下單?
seckill_order
表結構如下:
CREATE TABLE seckill_order (id BIGINT PRIMARY KEY AUTO_INCREMENT,user_id BIGINT NOT NULL,goods_id BIGINT NOT NULL,order_id BIGINT NOT NULL,-- 其他字段 ...UNIQUE KEY uniq_user_goods (user_id, goods_id)
);
關鍵:
UNIQUE KEY uniq_user_goods (user_id, goods_id)
它的作用是:同一個用戶,對同一件商品,只能有一條秒殺訂單記錄。
也就是說,如果你嘗試插入相同 user_id
和 goods_id
的數據,就會違反唯一索引,導致 SQL 執行失敗。
程序中如何體現這一機制?
在 OrderServiceImpl.java
的代碼中:
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setOrderId(order.getId());
seckillOrder.setUserId(user.getId());
seckillOrder.setGoodsId(goods.getId());
seckillOrderService.save(seckillOrder); // 插入數據庫
這個 save
方法其實最終是調用 MyBatis-Plus 的 INSERT
操作。如果用戶在極端并發下重復插入,就會因為違反唯一索引而拋出異常。
配合使用 Redis + 唯一索引實現雙重保障
- Redis:攔截大部分重復請求,提升性能
- 數據庫唯一索引:兜底保障,防止極端并發場景中出現重復訂單