redisson分佈式鎖解決秒殺問題

1 數據庫設計

秒殺商品表: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

image-20210811105924367

2 架構介紹

掩飾代碼結構使用的是 springboot+redis,其中使用了自定義異常,通用異常攔截,統一返回對象等sql

3 jmeter使用

3.1 修改配置

下載jmeter,修改配置文件 jmeter.properties數據庫

language=zh_CN # 把語言改成中文
sampleresult.default.encoding=UTF-8 # 默認編碼改成utf-8
複製代碼

3.2 添加測試線程組

image-20210811104348316

3.2.1 設置參數從CSV文件中來->添加配置元件->CSV data set config

image-20210811104444779

image-20210811104629367

3.2.2 添加取樣器->http請求

image-20210811105125251

3.2.3 添加配置元件->http信息頭管理器

image-20210811105409845

3.2.4 添加監聽器->查看結果樹

4 秒殺代碼

4.1 version 1

@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, "存庫不足,請搶購其餘商品");
        }

    }
}
複製代碼

輸出:設計模式

image-20210811114439586

結論:安全

商品表沒有超賣可是訂單多了不少並且有重複購買現象,因此問題出現的位置在: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

4.2 version 2

@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, "存庫不足,請搶購其餘商品");
    }

}
複製代碼

輸出:

image-20210811140910251

結論:

修改了第三個步驟,把多步操做改成一條sql原子操做後,沒有了超賣現象;可是任有重複購買問題,主要就是第二步"判斷該用戶是否已經購買過"有線程安全問題

4.3 version 3

@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

image-20210811143810642

結論:

這一版改動是在第四步,模擬volatile單例設計模式的雙重校驗;能夠看得出來重複購買概率降低了不少,可是任然有,因此並不能徹底的解決問題

4.4 version 4

@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();
        }
    }
複製代碼

輸出:

image-20210811162803927

結論:

這一版,使用了redisson框架來作分佈式鎖,測試了好些次沒有問題;

  1. 爲何不適用redis來作分佈式鎖呢?

    緣由主要仍是redis沒有智能處理過時時間的功能,依舊會引起線程安全甚至死鎖問題;那redisson就沒有

  2. 難道redisson就沒有問題了嗎?

    在redis主從架構下,若是master宕機時沒有同步數據到salve中,依舊仍是會出現問題,可是概率很是小,因此redisson只能保證ap(partition tolerance分區容錯性),沒法保證consistency(一致性);使用zookeeper能夠解決數據一致性問題,可是avaliability(可用性)會差一些

補充:

爲何不把鎖的範圍只控制在第二步呢?問題的發生不就是插入了重複數據嗎?

image-20210811163613828

帶我看源碼

相關文章
相關標籤/搜索