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

带我看源码

相关文章
相关标签/搜索