秒殺商品表:t_seckill_goodsjava
CREATE TABLE `t_seckill_goods` (
`id` bigint NOT NULL,
`goods_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`seckill_num` int DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
複製代碼
秒殺訂單表:t_seckill_ordergit
CREATE TABLE `t_seckill_order` (
`id` bigint NOT NULL,
`seckill_goods_id` bigint DEFAULT NULL,
`user_id` bigint DEFAULT NULL,
`seckill_goods_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`seckill_goods_price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
複製代碼
初始數據:redis
一個商品,庫存10spring
掩飾代碼結構使用的是 springboot+redis,其中使用了自定義異常,通用異常攔截,統一返回對象等sql
下載jmeter,修改配置文件 jmeter.properties數據庫
language=zh_CN # 把語言改成中文
sampleresult.default.encoding=UTF-8 # 默認編碼改成utf-8
複製代碼
@Service
public class TSeckillGoodsServiceImpl extends ServiceImpl<TSeckillGoodsMapper, TSeckillGoods> implements TSeckillGoodsService {
@Autowired
private TSeckillGoodsMapper seckillGoodsMapper;
@Autowired
private TSeckillOrderMapper seckillOrderMapper;
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 判斷該商品是否存在
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202, "商品不存在");
}
//2 判斷該用戶是否已經購買過
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已購買過該商品");
}
//3 減庫存操做
int i = 0;
if (goods.getSeckillNum() > 0) {
goods.setSeckillNum(goods.getSeckillNum() - 1);
i =seckillGoodsMapper.updateById(goods);
}
if (i > 0) {
//4 插入訂單
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("發送mq,告訴用戶儘快付款");
}
} else {
throw new BusinessException(203, "存庫不足,請搶購其餘商品");
}
}
}
複製代碼
輸出:設計模式
結論:安全
商品表沒有超賣可是訂單多了不少並且有重複購買現象,因此問題出現的位置在:springboot
//2 判斷該用戶是否已經購買過
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已購買過該商品");
}
//3 減庫存操做
int i = 0;
if (goods.getSeckillNum() > 0) {
goods.setSeckillNum(goods.getSeckillNum() - 1);
i =seckillGoodsMapper.updateById(goods);
}
if (i > 0) {
//4 插入訂單
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("發送mq,告訴用戶儘快付款");
}
} else {
throw new BusinessException(203, "存庫不足,請搶購其餘商品");
}
複製代碼
多個線程同時判斷當前用戶沒有購買過商品,而後數量>0,進而插入了訂單markdown
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 判斷該商品是否存在
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202, "商品不存在");
}
//2 判斷該用戶是否已經購買過
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已購買過該商品或商品不存在");
}
//3 減庫存操做
//update t_seckill_goods set seckill_num = seckill_num -1 where seckill_num > 0 and id = #{id}
int i = seckillGoodsMapper.updateInventory(seckillDto.getGoodsId());
if (i > 0) {
//4 插入訂單
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("發送mq,告訴用戶儘快付款");
}
} else {
throw new BusinessException(203, "存庫不足,請搶購其餘商品");
}
}
複製代碼
輸出:
結論:
修改了第三個步驟,把多步操做改成一條sql原子操做後,沒有了超賣現象;可是任有重複購買問題,主要就是第二步"判斷該用戶是否已經購買過"有線程安全問題
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 判斷該商品是否存在
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202, "商品不存在");
}
//2 判斷該用戶是否已經購買過
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已購買過該商品或商品不存在");
}
//3 減庫存操做
//update t_seckill_goods set seckill_num = seckill_num -1 where seckill_num > 0 and id = #{id}
int i = seckillGoodsMapper.updateInventory(seckillDto.getGoodsId());
//4 插入訂單
if (i > 0) {
//模擬volatile單例設計模式,雙重校驗;這裏的冗餘代碼就懶得改了
boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已購買過該商品或商品不存在");
}
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("發送mq,告訴用戶儘快付款");
}
} else {
throw new BusinessException(203, "存庫不足,請搶購其餘商品");
}
}
複製代碼
輸出:
爲了增長出錯率,我調大了庫存容量爲20
結論:
這一版改動是在第四步,模擬volatile單例設計模式的雙重校驗;能夠看得出來重複購買概率降低了不少,可是任然有,因此並不能徹底的解決問題
@Override
@Transactional
public void seckill(SeckillDto seckillDto) {
//1 判斷該商品是否存在
TSeckillGoods goods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<TSeckillGoods>()
.eq(TSeckillGoods::getId, seckillDto.getGoodsId()));
if (goods == null) {
throw new BusinessException(202, "商品不存在");
}
final String key = "lock:" + seckillDto.getUserId() + "-" + seckillDto.getGoodsId();
RLock lock = redissonClient.getLock(key);
try {
//默認30s的redis過時時間
lock.lock();
//2 判斷該用戶是否已經購買過
Integer boughtNum = seckillOrderMapper.selectCount(new LambdaQueryWrapper<TSeckillOrder>()
.eq(TSeckillOrder::getSeckillGoodsId, seckillDto.getGoodsId())
.eq(TSeckillOrder::getUserId, seckillDto.getUserId()));
if (boughtNum > 0) {
throw new BusinessException(201, "您已購買過該商品或商品不存在");
}
//3 減庫存操做
if (seckillGoodsMapper.updateInventory(seckillDto.getGoodsId()) > 0) {
//4 插入訂單
TSeckillOrder order = TSeckillOrder.builder()
.id(SnowIdUtils.nextId())
.seckillGoodsId(seckillDto.getGoodsId())
.userId(seckillDto.getUserId())
.seckillGoodsName(goods.getGoodsName())
.seckillGoodsPrice(goods.getPrice()).build();
int insertNum = seckillOrderMapper.insert(order);
if (insertNum > 0) {
System.out.println("發送mq,告訴用戶儘快付款");
}
} else {
throw new BusinessException(203, "存庫不足,請搶購其餘商品");
}
} finally {
lock.unlock();
}
}
複製代碼
輸出:
結論:
這一版,使用了redisson框架來作分佈式鎖,測試了好些次沒有問題;
爲何不適用redis來作分佈式鎖呢?
緣由主要仍是redis沒有智能處理過時時間的功能,依舊會引起線程安全甚至死鎖問題;那redisson就沒有
難道redisson就沒有問題了嗎?
在redis主從架構下,若是master宕機時沒有同步數據到salve中,依舊仍是會出現問題,可是概率很是小,因此redisson只能保證ap(partition tolerance分區容錯性),沒法保證consistency(一致性);使用zookeeper能夠解決數據一致性問題,可是avaliability(可用性)會差一些
補充:
爲何不把鎖的範圍只控制在第二步呢?問題的發生不就是插入了重複數據嗎?