目录

  • 全局唯一ID
  • 实现优惠券秒杀下单
  • 超卖问题
  • 一人一单
  • 分布式锁
  • Redis优化秒杀
  • Redis消息队列实现异步秒杀

    全局唯一ID

    每个店铺都会发优惠券
    image.png
    用户抢购时,生成的订单保存到tb_voucher_order表,该表没有使用自增ID,因为会产生如下问题:

  • ID规律太明显

用户在我的订单种可以看到编号,暴露信息给用户

  • 受表单数据量限制

因为订单数据过多,数据保存到多张表,如果采用自增长,会出现ID重复的情况
所以引出概念:全局ID生成器
即一种在分布式系统下生成全局唯一ID的工具,一般满足特性;

  • 唯一性
  • 高可用
  • 高性能
  • 递增性
  • 安全性

image.png

Redis实现全局唯一ID

全局唯一ID生成策略:

  • UUID
  • Redis自增
  • snowflake算法
  • 数据库自增

Redis自增ID策略

  • 每天一个key,方便统计订单量,一天的订单量上限2^32位

    1. //基于Redis的ID生成器
    2. @Component
    3. public class RedisIdWorker {
    4. //在main函数里生成的2022年1月1日0点0分的秒数
    5. private static final long BEGIN_TIMESTAMP = 1640995200L;
    6. private static final int COUNT_BITS = 32;
    7. private StringRedisTemplate stringRedisTemplate;
    8. public long nextId(String KeyPrefix){
    9. //1.生成时间戳
    10. LocalDateTime now = LocalDateTime.now();
    11. long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
    12. long timestamp = nowSecond - BEGIN_TIMESTAMP;
    13. //2.生成序列号
    14. //2.1获取当前日期,精准到天
    15. String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
    16. //2.2自增长
    17. Long count = stringRedisTemplate.opsForValue().increment("icr:" + KeyPrefix + ":" + date);
    18. //3.拼接并返回(时间戳左移32位,再用或运算填充)
    19. return timestamp << COUNT_BITS | count;
    20. }
    21. public static void main(String[] args) {
    22. LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
    23. long second = time.toEpochSecond(ZoneOffset.UTC);
    24. System.out.println("second=" + second);
    25. }
    26. }

    测试并发生成ID,这里多线程玩的有点酷- -

    1. @Resource
    2. private RedisIdWorker redisIdWorker;
    3. private ExecutorService es = Executors.newFixedThreadPool(500);
    4. @Test
    5. void testIdWorker() throws InterruptedException {
    6. CountDownLatch latch = new CountDownLatch(300);
    7. Runnable task = () -> {
    8. for (int i = 0; i < 100; i++) {
    9. long id = redisIdWorker.nextId("order");
    10. System.out.println("id=" + id);
    11. }
    12. latch.countDown();
    13. };
    14. long begin = System.currentTimeMillis();
    15. for (int i = 0; i < 300; i++) {
    16. es.submit(task);
    17. }
    18. latch.await();
    19. long end = System.currentTimeMillis();
    20. System.out.println("time=" + (end - begin));
    21. }

    测试成功,redis中存在数据
    注意:此处如果一直转圈,可能是忘记注入Redis,需要使用构造函数
    image.png
    image.pngimage.png

    添加优惠券

    image.png
    有两种不同的券,秒杀券(有时间要求)需要抢购。
    数据库中tb_seckill_voucher表为空,需要新增秒杀券
    使用Postman进行添加
    image.png

    1. POSThttp://localhost:8081/voucher/seckill
    2. {
    3. "shopId": 1,
    4. "title": "100原代金券",
    5. "subTitle": "周一至周五可使用",
    6. "rules": "全场通用",
    7. "payValue": 8000,
    8. "actualValue": 10000,
    9. "type": 1,
    10. "stock": 100,
    11. "beginTime": "2022-05-12T22:00:00",
    12. "endTime": "2022-05-12T23:00:00"
    13. }

    添加成功后在数据库中看到新增记录
    image.png
    注意:如果数据库有数据但优惠券没有显示,查看当前时间是否正在指定时间范围。
    修改时间后,成功显示。
    image.png

    实现秒杀下单

    image.png
    下单需要判断:

  • 秒杀是否开始或结束

  • 库存是否充足

image.png

  1. @Service
  2. public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
  3. @Resource
  4. private ISeckillVoucherService seckillVoucherService;
  5. @Resource
  6. private RedisIdWorker redisIdWorker;
  7. @Override
  8. @Transactional
  9. public Result seckillVoucher(Long voucherId) {
  10. //1.查询优惠券
  11. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
  12. //2.判断秒杀是否开始
  13. if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
  14. //尚未开始
  15. return Result.fail("秒杀尚未开始!");
  16. }
  17. //3.判断秒杀是否结束
  18. if(voucher.getEndTime().isBefore(LocalDateTime.now())){
  19. return Result.fail("秒杀已经结束!");
  20. }
  21. //4.判断库存是否充足
  22. if (voucher.getStock() < 1) {
  23. //库存不足
  24. return Result.fail("库存不足!");
  25. }
  26. //5.扣减库存
  27. boolean success = seckillVoucherService.update()
  28. .setSql("stock = stock - 1")
  29. .eq("voucher_id", voucherId).update();
  30. if(!success){
  31. return Result.fail("库存不足!");
  32. }
  33. //6.创建订单
  34. VoucherOrder voucherOrder = new VoucherOrder();
  35. //6.1订单id
  36. long orderId = redisIdWorker.nextId("order");
  37. voucherOrder.setId(orderId);
  38. //6.2用户id
  39. Long userId = UserHolder.getUser().getId();
  40. voucherOrder.setUserId(userId);
  41. //6.3代金券id
  42. voucherOrder.setVoucherId(voucherId);
  43. save(voucherOrder);
  44. //7.返回订单id
  45. return Result.ok(orderId);
  46. }
  47. }

⭐超卖问题

即并发问题,
情景:通过JMeter做高并发测试,100张优惠券卖出去变成负数,生成了两百来个订单
image.png
解决方案:加锁
image.png
乐观锁!
方法一:版本号法
image.png
方法二:CAS法
image.png

乐观锁解决超卖

image.png

image.png
image.png

一人一单

image.png

  1. //一人一单
  2. Long userId = UserHolder.getUser().getId();
  3. Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  4. if(count > 0){
  5. //用户已经购买过
  6. return Result.fail("用户已经购买过!");
  7. }

问题依然存在,一个用户下了10单,同样是并发问题
(因为是新增数据,无法判断是否修改过,不能使用乐观锁)

  1. @Override
  2. public Result seckillVoucher(Long voucherId) {
  3. //1.查询优惠券
  4. SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
  5. //2.判断秒杀是否开始
  6. if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
  7. //尚未开始
  8. return Result.fail("秒杀尚未开始!");
  9. }
  10. //3.判断秒杀是否结束
  11. if(voucher.getEndTime().isBefore(LocalDateTime.now())){
  12. return Result.fail("秒杀已经结束!");
  13. }
  14. //4.判断库存是否充足
  15. if (voucher.getStock() < 1) {
  16. //库存不足
  17. return Result.fail("库存不足!");
  18. }
  19. //7.返回订单id
  20. return createVoucherOrder(voucherId);
  21. }
  22. @Transactional
  23. public Result createVoucherOrder(Long voucherId) {
  24. //一人一单
  25. Long userId = UserHolder.getUser().getId();
  26. synchronized (userId.toString().intern()){
  27. }
  28. Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  29. if(count > 0){
  30. //用户已经购买过
  31. return Result.fail("用户已经购买过!");
  32. }
  33. //5.扣减库存
  34. boolean success = seckillVoucherService.update()
  35. .setSql("stock = stock - 1")
  36. .eq("voucher_id", voucherId).gt("stock",0)
  37. .update();
  38. if(!success) {
  39. return Result.fail("库存不足!");
  40. }
  41. //6.创建订单
  42. VoucherOrder voucherOrder = new VoucherOrder();
  43. //6.1订单id
  44. long orderId = redisIdWorker.nextId("order");
  45. voucherOrder.setId(orderId);
  46. //6.2用户id
  47. voucherOrder.setUserId(userId);
  48. //6.3代金券id
  49. voucherOrder.setVoucherId(voucherId);
  50. save(voucherOrder);
  51. //7.返回订单id
  52. return Result.ok(orderId);
  53. }

⭐一人一单并发安全问题

image.png
image.png
集群模式下,有多个JVM存在,每个JVM内部都有自己的锁,导致每个锁都可以有一个线程获取,变成并行运行。