Redis实现商品秒杀 (1) 单体架构实现——黑马点评代码分析

订单的唯一Id问题

对于每一个订单,其id是必不可少的。

唯一Id(分布式 全局ID生成器)需要满足的特点

  • 唯一性
    • 订单Id
    • Redis.increment()
  • 高可用
    • 随时都可以生成,否则会导致其他业务出问题
    • Redis集群、主从
  • 高性能
    • 不要把别的业务拖慢
  • 递增性
    • 替代数据库自增Id,有利于数据库创建索引
  • 安全性
    • 规律性不能太明显,不能让他人猜到Id特征

一种实现方法——Redis自增

Redis实现商品秒杀 (1) 单体架构实现——黑马点评代码分析_第1张图片
策略:

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

其他的方案

  • UUID
    • 返回16进制串,字母数字都有
    • 没有规律,不符合前面的特性
  • Snowflake算法
  • 数据库自增
    • 单独一张表控制自增
    • Redis自增的数据库版
    • 性能不比Redis。企业方案:批量获取Id,缓存到内存中

具体实现

生成特定时间戳:

    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long timeStamp = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println(timeStamp);
    }

生成唯一Id值

    public long nextId(String keyPrefix){
        // 生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = nowSecond - BEGIN_TIMESTAMP;
        // 生成序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        // 拼接并返回
        return timeStamp << COUNT_BITS | count;
    }

在这里插入图片描述
插入数据的时候发现中文乱码了,解决:

  • 发现提供的基础代码配置文件application.properties中mysql地址不全,加了&useUnicode=yes&characterEncoding=utf8
  • 数据库编码有问题,改成了utf8

实现秒杀下单

一般的秒杀下单逻辑
Redis实现商品秒杀 (1) 单体架构实现——黑马点评代码分析_第2张图片

	@Transactional
    public Result seckillVoucher(Long voucherId) {

        // 1. 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);

        // 2. 判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始!");
        }
        // 3. 判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束!");
        }
        // 4. 判断库存是否充足
        if (voucher.getStock() < 1) {
            return Result.fail("库存不足!");
        }
        // 5. 减库存
        /*      
        SeckillVoucher updateVoucher = voucher.setStock(voucher.getStock() - 1);
        seckillVoucherService.updateById(updateVoucher);
        */
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .update();
        if (!success) {
            return Result.fail("购买失败");
        }
        // 6. 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2 用户id
        voucherOrder.setUserId(UserHolder.getUser().getId());
        // 6.3 代金券id
        voucherOrder.setVoucherId(voucher.getVoucherId());
        // 6.4 保存订单到数据库
        save(voucherOrder);
        return Result.ok(orderId);
    }

利用jmeter进行压测步骤

  1. 设置200个线程
    Redis实现商品秒杀 (1) 单体架构实现——黑马点评代码分析_第3张图片
  2. 设置http请求
    Redis实现商品秒杀 (1) 单体架构实现——黑马点评代码分析_第4张图片
  3. 设置身份token
    Redis实现商品秒杀 (1) 单体架构实现——黑马点评代码分析_第5张图片
  4. 设置json断言(对于返回的json串,什么情况下视为返回错误)
    Redis实现商品秒杀 (1) 单体架构实现——黑马点评代码分析_第6张图片
  5. 运行结果
    Redis实现商品秒杀 (1) 单体架构实现——黑马点评代码分析_第7张图片
    在这里插入图片描述
    在这里插入图片描述
    由此可见,出现了超卖问题,这次测试超卖了9单
  6. 测试可能用到的代码
    • TRUNCATE `tb_voucher_order` 清空这个表

乐观锁解决超卖

乐观锁的关键是判断之前查询得到的数据是否有被修改过

普通乐观锁

boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).eq("stock", voucher.getStock())
                .update();

结果:

在这里插入图片描述
由于多线程并发访问stock数值,而一个线程修改成功会导致其他线程同时失败,失败率高,造成并发安全问题

对于此情况的优化方法:由于库存数量的要求并不是特别严格,可以将成功条件改为库存 > 0

boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock", 0)
                .update();

结果(成功):

在这里插入图片描述
而对于必须要求相等的数值,可以采用分段锁,多张表分别抢的方法。

实现一人一单

在减库存前进行判断代码

     // 4-1 判断是否购买过
     int count = query().eq("user_id", UserHolder.getUser().getId())
             .eq("voucher_id", voucherId).count();

     if (count > 0) {
         return Result.fail("已经购买过!");
     }

结果
在这里插入图片描述
在此处,乐观锁仍会造成并发问题,所以需要转用悲观锁

锁的范围:

	// 1-1
    // 这样是所有的用户来了都加锁。我们只需要对当前用户加锁(userId)
    @Transactional
    public synchronized Result createVoucherOrder(Long voucherId) {

提取出修改库存的部分(注意Spring事务失效)

 synchronized (userId.toString().intern()) {
      /*
          事务生效是因为spring做了动态代理
          this没有事务功能
          不能直接 return this.createVoucherOrder(voucherId);
      */
	  // 获取代理对象(事务)
	  IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
	  return proxy.createVoucherOrder(voucherId);
}

springboot启动类加注解@EnableAspectJAutoProxy(exposeProxy = true)

结果(成功)

在这里插入图片描述

然而,当架构变为分布式时,每个锁都在不同的机器上,当同样的请求打到不同的服务器上时仍会造成线程安全问题。这时就要用到分布式锁

来源: 黑马程序员Redis入门到实战教程 https://www.bilibili.com/video/BV1cr4y1671t

你可能感兴趣的:(redis,架构,数据库)