redis解决秒杀问题(单应用)学习笔记

redis解决秒杀问题(单应用)

前提了解!

秒杀肯定离不开电商,那么需要了解用户下单的过程.

1、当用户下单时,会把下单的信息存入一张表中,表中有一个字段用于判断该订单当前的状态.

订单表

CREATE TABLE `tb_voucher_order` (
  `id` bigint NOT NULL COMMENT '主键',
  `user_id` bigint unsigned NOT NULL COMMENT '下单的用户id',
  `voucher_id` bigint unsigned NOT NULL COMMENT '购买的代金券id',
  `pay_type` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '支付方式 1:余额支付;2:支付宝;3:微信',
  `status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间',
  `pay_time` timestamp NULL DEFAULT NULL COMMENT '支付时间',
  `use_time` timestamp NULL DEFAULT NULL COMMENT '核销时间',
  `refund_time` timestamp NULL DEFAULT NULL COMMENT '退款时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT;

2、主键存在的问题。

该表中的主键id不能够是自增。会导致一些问题:

  • id的规律性太明显(容易被猜到,并进行攻击)
  • 受单表数据库限制

解决方案:使用全局唯一id生成器,它应该具有的特性:

  • 唯一性

  • 高可用(分布式中也可以用)

  • 高性能(能够快速生成)

  • 递增性(保证数据库能够高效的查询)

  • 安全性

3、全局唯一id生成策略:

  • UUID(不规律,字符串)
  • Redis自增
  • snowflake算法(性能很好,依赖于时钟)
  • 数据库自增(单独建一张表记id,相当于redis版,性能不如redis,但可以通过一些方式解决,如提前存入数据库)

redis实现全局唯一id生成器

符号位(0或1) + 时间戳(31 Bit)+ 序列号(32 Bit)

redis自增策略:

  • 每天一个key,方便统计订单量
  • ID构造是 时间戳 + 计数器

实现过程

1、自定义起始时间戳

public void test02() {
    //生成自定义时间秒
    LocalDateTime now = LocalDateTime.of(2021,8,24,0,0,0);
    long second = now.toEpochSecond(ZoneOffset.UTC);
    System.out.println(second);
}

//结果得:1629763200

2、全局唯一id生成器编写

/**
 * @Author: songshu
 * @Date: 2022/06/19/13:11
 * @version: v1.0
 */
public class RedisIdWorker {
    //开始时间戳
    private static final long BEGIN_TIMESTAMP = 1629763200L;
    //序列号位数
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate redisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
        //生成时间戳
        //当前时间
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        //生成序列号
        //定义规则,每天一个key,方便统计
        String data = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = redisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + data);

        //拼接并返回
        //时间戳左移32位,并与序列号或(前面的32位就留给了序列号)
        //可能count也会超过32位,所以定义COUNT_BITS
        return timestamp << COUNT_BITS | count;

    }
}

3、测试

@Autowired
private StringRedisTemplate redisTemplate;

private ExecutorService es = Executors.newFixedThreadPool(500);

@Test
public void test03() throws InterruptedException {
    //记时器
    CountDownLatch countDownLatch = new CountDownLatch(300);

    RedisIdWorker redisIdWorker = new RedisIdWorker(redisTemplate);

    Runnable task = () -> {
        for(int i = 0; i < 100; i++) {
            long id = redisIdWorker.nextId("order");
            System.out.println(id);
        }
        countDownLatch.countDown();
    };

    long begin = System.currentTimeMillis();
    //任务提交300次
    for (int i = 0; i < 300; i++) {
        es.submit(task);
    }
    countDownLatch.await();
    long end = System.currentTimeMillis();
    System.out.println("time = " + (end - begin));
}

查看reids中得到:

key为: icr:order:2022:06:19

value为: 30000

实现优惠卷秒杀

优惠卷表

CREATE TABLE `tb_voucher` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `shop_id` bigint unsigned DEFAULT NULL COMMENT '商铺id',
  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代金券标题',
  `sub_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '副标题',
  `rules` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '使用规则',
  `pay_value` bigint unsigned NOT NULL COMMENT '支付金额,单位是分。例如200代表2元',
  `actual_value` bigint NOT NULL COMMENT '抵扣金额,单位是分。例如200代表2元',
  `type` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '0,普通券;1,秒杀券',
  `status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '1,上架; 2,下架; 3,过期',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT;

库存表

CREATE TABLE `tb_seckill_voucher` (
  `voucher_id` bigint unsigned NOT NULL COMMENT '关联的优惠券的id',
  `stock` int NOT NULL COMMENT '库存',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `begin_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
  `end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '失效时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`voucher_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='秒杀优惠券表,与优惠券是一对一关系';

注意点:

  • 判断秒杀是否开始或结束
  • 库存是否充足

实现下单功能

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        // 获取优惠开始时间和结束时间
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //判断是否开始
        if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        //判断是否结束
        if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            //已结束
            return Result.fail("秒杀已经结束!");
        }
        //判断库存是否充足
        if(seckillVoucher.getStock() < 1) {
            //库存不足
            return Result.fail("秒杀卷库存不足!");
        }

        //符合以上要求,可以下单
        //库存减一
        LambdaUpdateWrapper<SeckillVoucher> voucherUpdateWrapper = new LambdaUpdateWrapper<>();
        voucherUpdateWrapper.eq(SeckillVoucher::getVoucherId, voucherId);
        voucherUpdateWrapper.setSql("stock = stock-1");
        boolean update = seckillVoucherService.update(voucherUpdateWrapper);
        if(!update) {
            return Result.fail("下单失败!");
        }

        VoucherOrder voucherOrder = new VoucherOrder();
        //生成订单id
        RedisIdWorker redisIdWorker = new RedisIdWorker(redisTemplate);
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //代金券id
        voucherOrder.setVoucherId(voucherId);
        boolean flag = save(voucherOrder);
        if(!flag) {
            return Result.fail("下单失败!");
        }
        return Result.ok(orderId);
    }
}

该方法存在高并发问题!!!

需要优化!

解决高并发问题

解决方案:

1、通过锁解决

  • 悲观锁
    • Syschronized
    • Lock
  • 乐观锁
    • 数据库加字段 version(版本号法)

乐观锁解决高并发

修改库存表,添加version字段:

ALTER TABLE `tb_seckill_voucher`
ADD COLUMN `version` int(4) NULL DEFAULT 0 COMMENT '乐观锁';

MyBatis-Plus中添加乐观锁插件

注意:

  • 仅支持 updateById(id)update(entity, wrapper) 方法
  • update(entity, wrapper) 方法下, wrapper 不能复用!!!
@Configuration
public class MybatisConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //分页
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

修改下单代码

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        // 获取优惠开始时间和结束时间
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //判断是否开始
        if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        //判断是否结束
        if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            //已结束
            return Result.fail("秒杀已经结束!");
        }
        //判断库存是否充足
        if(seckillVoucher.getStock() < 1) {
            //库存不足
            return Result.fail("秒杀卷库存不足!");
        }


        //符合以上要求,可以下单
        //库存减一
        SeckillVoucher byId = seckillVoucherService.getById(voucherId);
        UpdateWrapper<SeckillVoucher> updateWrapper = new UpdateWrapper<>();
        updateWrapper.setSql("stock = stock-1");
        boolean update = seckillVoucherService.update(byId, updateWrapper);
        if(!update) {
            return Result.fail("下单失败!");
        }

        VoucherOrder voucherOrder = new VoucherOrder();
        //生成订单id
        RedisIdWorker redisIdWorker = new RedisIdWorker(redisTemplate);
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //代金券id
        voucherOrder.setVoucherId(voucherId);
        boolean flag = save(voucherOrder);
        if(!flag) {
            return Result.fail("下单失败!");
        }
        return Result.ok(orderId);
    }

注意这里会出现问题!

会有大量线程同时拿到同一个version,这就会导致乐观锁的成功率低!!!

乐观锁的特点:

  • 成功率低,效率高

由于是抢票,所以可以直接判断库存是否大于0即可,代码不演示。

新需求:一人抢一票

解决方式:加悲观锁

会遇到的问题:

问题一:

//加悲观锁,
//userid.toString() 底层是重新new一个对象,所以需要使用intern()方法到常量池中获取.

问题二:

//下面方法的事务不能够启用
//由于createVoucherOrder是VoucherOrderServiceImpl的对象,这里调用的是this,
//而不是VoucherOrderServiceImpl的代理对象
//由于spring需要代理对象才能够启用事务

引依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

在启动类上加注解

//暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true)

代码:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 获取优惠开始时间和结束时间
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //判断是否开始
        if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        //判断是否结束
        if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
            //已结束
            return Result.fail("秒杀已经结束!");
        }
        //判断库存是否充足
        if(seckillVoucher.getStock() < 1) {
            //库存不足
            return Result.fail("秒杀卷库存不足!");
        }

        //解决一人一票
        Long userid = UserHolder.getUser().getId();
        //加悲观锁,
        //userid.toString() 底层是重新new一个对象,所以需要使用intern()方法到常量池中获取
        synchronized (userid.toString().intern()) {
            //下面方法的事务不能够启用
            //由于createVoucherOrder是VoucherOrderServiceImpl的对象,这里调用的是this,
            //而不是VoucherOrderServiceImpl的代理对象
            //由于spring需要代理对象才能够启用事务
            IVoucherOrderService currentProxy = (IVoucherOrderService) AopContext.currentProxy();
            return currentProxy.createVoucherOrder(voucherId, userid);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId, Long userid) {
        // 判断是否存在该用户够的票
        LambdaQueryWrapper<VoucherOrder> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(VoucherOrder::getUserId, userid)
                .eq(VoucherOrder::getVoucherId, voucherId);
        VoucherOrder one = getOne(queryWrapper);
        if(BeanUtil.isNotEmpty(one)) {
            return Result.fail("不可重复抢购!");
        }
        //符合以上要求,可以下单
        //库存减一
        UpdateWrapper<SeckillVoucher> updateWrapper = new UpdateWrapper<>();
        updateWrapper.setSql("stock = stock-1").gt("stock",0);
        boolean update = seckillVoucherService.update(updateWrapper);
        if(!update) {
            return Result.fail("下单失败!");
        }

        VoucherOrder voucherOrder = new VoucherOrder();
        //生成订单id
        RedisIdWorker redisIdWorker = new RedisIdWorker(redisTemplate);
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //代金券id
        voucherOrder.setVoucherId(voucherId);
        boolean flag = save(voucherOrder);
        if(!flag) {
            return Result.fail("下单失败!");
        }
        return Result.ok(orderId);
    }
}

注意:

该方法是能在单应用中使用,集群中就会出问题!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值