优惠券秒杀

1

在各类购物App中,都会遇到商家发放的优惠券
当用户抢购商品时,生成的订单会保存到tb_voucher_order表中,而订单表如果使用数据库自增ID就会存在一些问题
id规律性太明显
受单表数据量的限制
如果我们的订单id有太明显的规律,那么对于用户或者竞争对手,就很容易猜测出我们的一些敏感信息,例如商城一天之内能卖出多少单,这明显不合适
随着我们商城的规模越来越大,MySQL的单表容量不宜超过500W,数据量过大之后,我们就要进行拆库拆表,拆分表了之后,他们从逻辑上讲,是同一张表,所以他们的id不能重复,于是乎我们就要保证id的唯一性
那么这就引出我们的全局ID生成器了

全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足一下特性

  • 唯一性
  • 高可用
  • 高性能
  • 递增性
  • 安全性
    为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其他信息
    ID组成部分
    符号位:1bit,永远为0
    时间戳:31bit,以秒为单位,可以使用69年(2^31秒约等于69年)
    序列号:32bit,秒内的计数器,支持每秒传输2^32个不同ID
    那我们就根据我们分析的ID生成策略,来编写代码

业务编写

步骤一:全局唯一ID生成器

点击查看代码
package com.hmdp.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisIdWorker {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    //设置起始时间,我这里设定的是2022.01.01 00:00:00
    public static final Long BEGIN_TIMESTAMP = 1640995200L;
    //序列号长度
    public static final Long COUNT_BIT = 32L;

    //Redis唯一ID生成策略
    public long nextId(String keyPrefix){
        //1. 生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long currentSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = currentSecond - BEGIN_TIMESTAMP;
        //2. 生成序列号---------------------同一个业务加入date也就是说明一天一个key
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue().increment("inc:" + keyPrefix + ":" + date);//key不存在会自动创建一个
        //3. 拼接并返回,简单位运算
        return timeStamp << COUNT_BIT | count;
    }
}

其他策略:UUID,Redis自增,snowflake算法,数据库自增 ##实现优惠券秒杀 控制层
点击查看代码
@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
    @Resource
    private IVoucherOrderService voucherOrderService;
    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        return voucherOrderService.seckillVoucher(voucherId);
    }
}
实现层 涉及两个表所以加上事务
点击查看代码
 @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        //2.秒杀是否开启
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //尚未开始
            return Result.fail("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            //已经结束
            return Result.fail("秒杀已经结束");
        }

        //3.库存
        if (voucher.getStock() < 1) {
            //库存不足
            return Result.fail("库存不足");
        }

        //4.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .update();
        if (!success) {
            //扣减失败
            return Result.fail("库存不足");
        }

        //5.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setUserId(orderId);
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        //6.返回订单id
        return Result.ok(orderId);
    }

超卖问题

限制一人一单
但这会涉及多个问题:乐观锁,悲观锁
涉及一个Spring的锁
一个用户一个锁加上之后,如果redis放入集群,这个锁会失败,两个单独的不会共享这一把锁导致还是重复订单

posted @ 2025-03-13 18:40  TTDB  阅读(24)  评论(0)    收藏  举报