黑马点评_优惠卷秒杀思路梳理_秒杀优化之前

系列博客目录



01.优惠券秒杀-全局唯一ID

02.优惠券秒杀-Redis实现全局唯一id

为了防止订单号出现重复或者暴露信息的情况,通过Redis生成全局唯一ID

03.优惠券秒杀-添加优惠券

手动添加优惠券,为之后做准备。

04.优惠券秒杀-实现秒杀下单

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

在这里插入图片描述
代码如下:

@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);//voucherId就是优惠券的Id
    }
}

IVoucherOrderService中创建方法

public interface IVoucherOrderService extends IService<VoucherOrder> {
    Result seckillVoucher(Long voucherId);
}

VoucherOrderServiceImpl中实现该方法

@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;        //获取全局唯一id

@Override
@Transactional  //设计对两张表进行操作,加上事务回滚,一旦出现问题可以进行事务回滚
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠券
    SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始
    if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
        //尚未开始
        return Result.fail("秒杀尚未开始!!");
    }
    //3.判断秒杀是否已经结束
    if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
        //已经结束
        return Result.fail("秒杀已经结束!!");
    }
    //4.判断库存是否充足
    if (seckillVoucher.getStock() < 1){
        //库存不足
        return Result.fail("库存不足!!");
    }
    //5.扣减库存
    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
    Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    //6.3代金券id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);

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

在这里插入图片描述
在数据库中的券订单表中会新增秒杀券的订单,同时数据库中券的库存也会减少一个。

05.优惠券秒杀-库存超卖问题分析(也就是线程安全问题)

如果我们使用Jmeter创建多个线程来抢券,库存可能会出现负数。这是个线程安全问题。也是并发安全问题,根本原因就是多个线程在操作共享的资源并且操作资源的代码有好多行,多个线程的代码没有按顺序执行,而是穿插执行。解决这个问题就是采用锁的方案。超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁。锁有两种(理念),悲观锁和乐观锁。
在这里插入图片描述
在这里插入图片描述

06.优惠券秒杀-乐观锁解决超卖

乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见方式有两种:
在这里插入图片描述
在这里插入图片描述
我们在编写代码的时候,代码如下:其实很简单

// 6.扣减库存
boolean success = seckillVoucherService.update()
        .setSql("stock = stock - 1") // set stock = stock - 1
        .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
        .update();
if (!success) {
    // 扣减失败
    return Result.fail("库存不足!");
}

再用JMeter测试,此时,数据库秒杀券库存为0,秒杀券订单数100

总结:

超卖这样的线程安全问题,解决方案有哪些?

  1. 悲观锁:添加同步锁,让线程串行执行  优点:简单粗暴  缺点:性能一般
  2. 乐观锁:不加锁,在更新时判断是否有其它线程在修改  优点:性能好  缺点:存在成功率低的问题

07.优惠券秒杀-实现一人一单功能

优化秒杀业务,要求同一个优惠券,一个用户只能下一单。思路如下图:
在这里插入图片描述
代码修改为如下:

// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
    // 用户已经购买过了
    return Result.fail("用户已经购买过一次!");
}

// 6.扣减库存
boolean success = seckillVoucherService.update()
        .setSql("stock = stock - 1") // set stock = stock - 1
        .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
        .update();
if (!success) {
    // 扣减失败
    return Result.fail("库存不足!");
}

// 7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.2.用户id
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);

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

但是用JMeter测试,一个用户去抢券,秒杀券的库存少了十个。原因就是多线程并发操作的安全问题,发生了代码穿插执行的问题(与超卖问题原因相同)。这里没法用乐观锁,因为我这里是要插入操作,不是判断是否被修改,本来这条记录没有,你怎么使用乐观锁去判断是否原来的数据被修改呢,所以不可以用乐观锁。这里只能用悲观锁方案。
我们把代码分为两部分,原来的通过查询来判断是否还有库存放在上面一个函数,创建优惠券订单放在下面一个新的函数createVoucherOrder中。
在这里插入图片描述
但是如果像上图中,那样在函数上加锁,那么任何一个用户来了都要加锁,而且是同一把锁,那整个方法只能被串行执行了,性能会很差。一人一单应该是同一个用户来了,才去判断并发安全问题。我们应该对用户(ID)进行加锁。

@Transactional
public Result createVoucherOrder(Long voucherId) {
    // 5.一人一单
    Long userId = UserHolder.getUser().getId();

    synchronized (userId.toString().intern()) {//每一次请求来Id对象都会改变,我们要对值进行加锁(toString也会new一个字符串对象,所以加上个intern())
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            return Result.fail("库存不足!");
        }

        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

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

又出现了一个新的问题,就是我们现在用了事务,我再整个方法的代码运行完毕,锁也释放了的时候,springboot才对事务进行提交(把数据写入数据库),可能还没来得及提交,已经有新的线程拿到了锁,开始查数据库了,发现没有针对同一个用户Id的订单,他也会继续执行针对相同用户Id创建订单的代码,这时候又会出现线程安全问题。这时候就是锁的范围太小了,我们应该在整个方法的外面加锁。优化方法如下。
在这里插入图片描述

这时候虽然已经线程安全了,但是还存在一个事务的问题。
this.createVoucherOrder(voucherId);他是没有事务功能的。因为我们是通过注解,对方法生成一个代理对象,来实现事务。你用了this就是使用(java类中的)它本身,不是它的代理对象。我们需要获得代理对象,代码如下。还需要暴露代理对象,之前还要引入依赖。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

08.优惠券秒杀-集群下的线程并发安全问题

在集群下synchronized并不能保证线程的并发安全。因为在集群模式下,每一个节点都是一个全新的JVM,每个JVM都有自己的锁。锁监视器只能在当前JVM的范围内,监视线程实现互斥。
在这里插入图片描述

需要实现多个JVM(多个Tomcat)使用相同的锁监视器,需要一把跨进程、跨JVM的锁(Redis刚好可以胜任)。下面介绍分布式锁。

09.分布式锁-基本原理和不同实现方式对比

在这里插入图片描述
什么是分布式锁?
分布式锁:满足分布式系统或集群模式下多进程可见(多个JVM可以看见他)并且互斥的锁。还有几点也要满足,如下图所示。
在这里插入图片描述
在这里插入图片描述

10.分布式锁-Redis的分布式锁实现思路

在这里插入图片描述
为了获取锁的原子性,如下图所示:
在这里插入图片描述
获取锁还有两种机制,一种是阻塞式的机制,我获取失败,我就等待,另一种是非阻塞的机制,我获取失败就返回false,我就不再获取,成功就返回true。我们先用非阻塞式,这种对CPU比较友好。
流程如下:
在这里插入图片描述

12.分布式锁-Redis分布式锁误删问题

但是上面那样可能会出现分布式锁误删的问题。
在这里插入图片描述
解决方法如下:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

11.分布式锁-实现Redis分布式锁版本(初级版本_已包含上面的改进)

先介绍一个初级版本。
在这里插入图片描述

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);//redis的key也就是锁名为业务名,value带有线程表示。
        return Boolean.TRUE.equals(success);//只用return success;自动拆箱,可能会有安全问题。
    }

   
    @Override
    public void unlock() {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();//拼接前缀实现全局唯一线程ID,防止误删别人的锁。
        // 获取锁中的标示
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标示是否一致
        if(threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

改进一人一单代码,使用Redis实现的分布式锁,代码如下:

// 5.一人一单
Long userId = UserHolder.getUser().getId();

// 创建锁对象
SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
// 尝试获取锁
boolean isLock = redisLock.tryLock(1200);
// 判断
if(!isLock){
    // 获取锁失败,直接返回失败或者重试
    return Result.fail("不允许重复下单!");
}

try {
    // 5.1.查询订单
    int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    // 5.2.判断是否存在
    if (count > 0) {
        // 用户已经购买过了
        return Result.fail("用户已经购买过一次!");
    }

    // 6.扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock - 1") // set stock = stock - 1
            .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
            .update();
    if (!success) {
        // 扣减失败
        return Result.fail("库存不足!");
    }

    // 7.创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    // 7.1.订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    // 7.2.用户id
    voucherOrder.setUserId(userId);
    // 7.3.代金券id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);

    // 7.返回订单id
    return Result.ok(orderId);
} finally {
    // 释放锁
    redisLock.unlock();
}

14.分布式锁-分布式锁的原子性问题

在这里插入图片描述
怎样保证原子性呢?

15.分布式锁-Lua脚本解决多条命令原子性问题

16.分布式锁-Java调用lua脚本改造分布式锁

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0

17-22 Redisson

Redisson详情参考该链接

@Transactional
public Result createVoucherOrder(Long voucherId) {
    // 5.一人一单
    Long userId = UserHolder.getUser().getId();

    // 创建锁对象
    RLock redisLock = redissonClient.getLock("lock:order:" + userId);
    // 尝试获取锁
    boolean isLock = redisLock.tryLock();
    // 判断
    if(!isLock){
        // 获取锁失败,直接返回失败或者重试
        return Result.fail("不允许重复下单!");
    }

    try {
        // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

        // 6.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1") // set stock = stock - 1
                .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                .update();
        if (!success) {
            // 扣减失败
            return Result.fail("库存不足!");
        }

        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 7.返回订单id
        return Result.ok(orderId);
    } finally {
        // 释放锁
        redisLock.unlock();
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值