《Redis实战篇》六、秒杀优化

这篇具有很好参考价值的文章主要介绍了《Redis实战篇》六、秒杀优化。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

6、秒杀优化

6.0 压力测试

目的:测试1000个用户抢购优惠券时秒杀功能的并发性能~

①数据库中创建1000+用户

这里推荐使用开源工具:https://www.sqlfather.com/ ,导入以下配置即可一键生成模拟数据

{"dbName":"hmdp","tableName":"tb_user","tableComment":"用户表","mockNum":100,"fieldList":[{"fieldName":"id","fieldType":"bigint(20)","defaultValue":null,"notNull":true,"comment":"主键id","primaryKey":true,"autoIncrement":true,"mockType":"递增","mockParams":2,"onUpdate":null},{"fieldName":"phone","fieldType":"varchar(33)","defaultValue":null,"notNull":false,"comment":null,"primaryKey":false,"autoIncrement":false,"mockType":"随机","mockParams":"手机号","onUpdate":null},{"fieldName":"password","fieldType":"varchar(384)","defaultValue":null,"notNull":false,"comment":null,"primaryKey":false,"autoIncrement":false,"mockType":"随机","mockParams":"字符串","onUpdate":null},{"fieldName":"nick_name","fieldType":"varchar(96)","defaultValue":null,"notNull":false,"comment":null,"primaryKey":false,"autoIncrement":false,"mockType":"规则","mockParams":"user_\\w{10}$","onUpdate":null},{"fieldName":"icon","fieldType":"varchar(765)","defaultValue":null,"notNull":false,"comment":null,"primaryKey":false,"autoIncrement":false,"mockType":"固定","mockParams":"/imgs/blogs/blog1.jpg","onUpdate":null},{"fieldName":"create_time","fieldType":"timestamp","defaultValue":null,"notNull":false,"comment":null,"primaryKey":false,"autoIncrement":false,"mockType":"固定","mockParams":"2023-01-01 00:00:00","onUpdate":null},{"fieldName":"update_time","fieldType":"timestamp","defaultValue":null,"notNull":false,"comment":null,"primaryKey":false,"autoIncrement":false,"mockType":"固定","mockParams":"2023-01-01 00:00:01","onUpdate":null}]}

②将1000个用户处于登录状态(本质就是为1000个用户生成token,并保存到Redis中)

    /**
     * 在Redis中保存1000个用户信息并将其token写入文件中,方便测试多人秒杀业务
     */
    @Test
    void testMultiLogin() throws IOException {
        List <User> userList = userService.lambdaQuery().last("limit 1000").list();
        for (User user : userList) {
            String token = UUID.randomUUID().toString(true);
            UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
            Map <String,Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap <>(),
                    CopyOptions.create().ignoreNullValue()
                            .setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
            String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
            stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
            stringRedisTemplate.expire(tokenKey, 60,TimeUnit.MINUTES);
        }
        Set <String> keys = stringRedisTemplate.keys(RedisConstants.LOGIN_USER_KEY + "*");
        @Cleanup FileWriter fileWriter = new FileWriter(System.getProperty("user.dir") + "\\tokens.txt");
        @Cleanup BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
        assert keys != null;
        for (String key : keys) {
            String token = key.substring(RedisConstants.LOGIN_USER_KEY.length());
            String text = token + "\n";
            bufferedWriter.write(text);
        }
    }

③在Jmeter中进行压力测试:1000个线程请求接口,观察结果

redis6 优化,Redis从入门到入土,redis,数据库,java

这接口被Leader发现,估计要被骂死~

6.1 秒杀优化-异步秒杀思路

我们来回顾一下下单流程

当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤

1、查询优惠卷

2、判断秒杀库存是否足够

3、查询订单

4、校验是否是一人一单

5、扣减库存

6、创建订单

在这六步操作中,又有很多操作是要去操作数据库的,而且还是一个线程串行执行, 这样就会导致我们的程序执行的很慢,所以我们需要异步程序执行,那么如何加速呢?

在这里笔者想给大家分享一下课程内没有的思路,看看有没有小伙伴这么想,比如,我们可以不可以使用异步编排来做,或者说我开启N多线程,N多个线程,一个线程执行查询优惠卷,一个执行判断扣减库存,一个去创建订单等等,然后再统一做返回,这种做法和课程中有哪种好呢?答案是课程中的好,因为如果你采用我刚说的方式,如果访问的人很多,那么线程池中的线程可能一下子就被消耗完了,而且你使用上述方案,最大的特点在于,你觉得时效性会非常重要,但是你想想是吗?并不是,比如我只要确定他能做这件事,然后我后边慢慢做就可以了,我并不需要他一口气做完这件事,所以我们应当采用的是课程中,类似消息队列的方式来完成我们的需求,而不是使用线程池或者是异步编排的方式来完成这个需求

redis6 优化,Redis从入门到入土,redis,数据库,java

**优化方案:**我们将耗时比较短的逻辑判断放入到redis中,比如是否库存足够,比如是否一人一单,这样的操作,只要这种逻辑可以完成,就意味着我们是一定可以下单完成的,我们只需要进行快速的逻辑判断,根本就不用等下单逻辑走完,我们直接给用户返回成功, 再在后台开一个线程,后台线程慢慢的去执行queue里边的消息,这样程序不就超级快了吗?而且也不用担心线程池消耗殆尽的问题,因为这里我们的程序中并没有手动使用任何线程池。当然这里边有两个难点

第一个难点是我们怎么在redis中去快速校验一人一单,还有库存判断

第二个难点是由于我们校验和tomct下单是两个线程,那么我们如何知道到底哪个单他最后是否成功,或者是下单完成,为了完成这件事我们在redis操作完之后,我们会将一些信息返回给前端,同时也会把这些信息丢到异步queue中去,后续操作中,可以通过这个id来查询我们tomcat中的下单逻辑是否完成了。【饭店的运营流程】

redis6 优化,Redis从入门到入土,redis,数据库,java

我们现在来看看整体思路:当用户下单之后,判断库存是否充足只需要到redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作

当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。

redis6 优化,Redis从入门到入土,redis,数据库,java

6.2 秒杀优化-Redis完成秒杀资格判断

需求:

  • 新增秒杀优惠券的同时,将优惠券信息保存到Redis中

  • 基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功

  • 如果抢购成功,将优惠券id和用户id封装后存入阻塞队列

  • 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

    redis6 优化,Redis从入门到入土,redis,数据库,java

VoucherServiceImpl

@Override
@Transactional
public void addSeckillVoucher(Voucher voucher) {
    // 保存优惠券
    save(voucher);
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
    // 保存秒杀库存到Redis中
    stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

完整lua表达式

-- 1.参数列表
-- 1.1 优惠券Id
local voucherId = ARGV[1]
-- 1.2 用户id
local userId = ARGV[2]

-- 2.数据key
-- 2.1 库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2 订单key
local orderKey = 'seckill:order:' .. voucherId

-- 3.脚本业务
-- 3.1 判断库存是否充足 get stockKey
if (tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.1.2 库存不足,返回1
    return 1
end

-- 3.2 判断用户是否已经下过单
if (redis.call('sismember', orderKey, userId) == 1) then
    -- 3.2.2 存在,说明重复下单,返回2
    return 2
end

-- 3.3 扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)

-- 3.4 下单(保存用户) sadd orderKey userId
redis.call('sadd', orderKey, userId)

-- 3.5 用户有下单资格,返回0
return 0

当以上lua表达式执行完毕后,剩下的就是根据步骤3,4来执行我们接下来的任务了

VoucherOrderServiceImpl

/**
     * 使用Lua脚本+消息队列实现秒杀下单
     *
     * @param voucherId
     * @return
     */
@Override
public Result seckillVoucher(Long voucherId) {
    // 获取用户id
    Long userId = UserHolder.getUser().getId();
    // 1.执行Lua脚本
    Long result = stringRedisTemplate.execute(
        SECKILL_SCRIPT,
        Collections.emptyList(),
        voucherId.toString(),
        userId.toString()
    );

    // 2.判断结果是否为0
    if (result != 0) {
        // 2.1 不为0,代表没有购买资格
        return Result.fail(result == 1 ? "库存不足" : "不能重复下单");
    }
    //TODO 保存阻塞队列

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

**压力测试:**因为目前前两步骤做完,后面的加入阻塞队列执行时间就很短了~

redis6 优化,Redis从入门到入土,redis,数据库,java

可以看到并发性能大大提升,请求响应值在0.1s左右,吞吐量可达到1500/sec~ 速度飞起

6.3 秒杀优化-基于阻塞队列实现秒杀优化

VoucherOrderServiceImpl

修改下单动作,现在我们去下单时,是通过lua表达式去原子执行判断逻辑,如果判断我出来不为0 ,则要么是库存不足,要么是重复下单,返回错误信息,如果是0,则把下单的逻辑保存到队列中去,然后异步执行

    /**
     * 将代理对象声明成全局
     */
    private IVoucherOrderService proxy;

    /**
     * 存放任务的阻塞队列
     * 特点:当一个线程尝试从队列中获取元素,没有元素,线程就会被阻塞,直到队列中有元素,线程才会被唤醒,并去获取元素
     */
    private BlockingQueue <VoucherOrder> orderTasks = new ArrayBlockingQueue <>(1024 * 1024);

    /**
     * 思考一个问题:为什么要使用线程池呢,而不是直接创建一个线程?
     * 其实直接创建一个线程也行,但是创建一个线程开销很大的,用阻塞队列+线程池的形式实现了线程的的复用
     */
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    /**
     * 由于用户秒杀的时间可能是随时的,所以需要我们项目已启动 线程池就应该从消息队列获取任务,然后工作...
     *
     * @PostConstruct类初始花后立刻执行
     */
    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    // 1.获取队列中的订单信息
                    // take():获取和删除该队列的头部,如果没有则阻塞等待,直到有元素可用。所以使用该方法,如果有元素,线程就工作,没有线程就阻塞(卡)在这里,不用担心CPU会空转~
                    VoucherOrder voucherOrder = orderTasks.take();
                    // 2.创建订单
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("处理订单异常:", e);
                }
            }
        }
    }

    /**
     * 创建订单
     *
     * @param voucherOrder
     */
    private void handleVoucherOrder(VoucherOrder voucherOrder) {

        /**
         * 其实这里可以不加锁了:(方式一)
         * ①:前面的Lua脚本已经进判断过库存和一人一单了,并且也可以保证执行的原子性(一次只有一个线程执行)。
         * ②:此时线程池中只有一个线程,是单线程哦~
         * ③:之后从消息队列取任务执行并不需要保证其原子性,因为就不存在并发安全问题了
         * 加锁算是一种兜底~
         */

        // 方式一:加分布式锁再创建订单
        // // 1.获取用户
        // // 注意:这里userId不能从UserHolder中去取,因为当前并不是主线程,而是子线程,无法拿到父线程ThreadLocal中的数据
        // Long userId = voucherOrder.getUserId();
        // // 2.获取分布式锁
        // RLock lock = redissonClient.getLock("lock:order:" + userId);
        // boolean isLock = lock.tryLock();
        // // 3.判断是否获取锁成功
        // if (!isLock) {
        //     // 获取锁失败,返回错误和重试
        //     log.error("不允许重复下单~");
        // }
        // try {
        //     // 获取代理对象(只有通过代理对象调用方法,事务才会生效)
        //     // 注意:这里直接通过以下方式获取肯定是不行的。因为方法底层也是基于ThreadLocal获取的,子线程是无法获取父线程ThreadLocal中的对象的
        //     // 解决办法:在seckillVoucher中提前获取,然后通过消息队列传入或者声明成全局变量,从而就可以使用了
        //     // IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        //     proxy.createVoucherOrder(voucherOrder.getVoucherId());
        // } finally {
        //     lock.unlock();
        // }

        // 方式二:直接创建订单
        proxy.createVoucherOrder(voucherOrder);
    }

    // RedisScript需要加载seckill.lua文件,为了避免每次释放锁时都加载,我们可以提前加载好。否则每次读取文件就会产生IO,效率很低
    static {
        SECKILL_SCRIPT = new DefaultRedisScript <>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
     
    /**
     * 使用Lua脚本+消息队列实现秒杀下单
     *
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        // 获取用户id
        Long userId = UserHolder.getUser().getId();
        // 1.执行Lua脚本
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(),
                userId.toString()
        );

        // 2.判断结果是否为0
        if (result != 0) {
            // 2.1 不为0,代表没有购买资格
            return Result.fail(result == 1 ? "库存不足" : "不能重复下单");
        }
        // 2.2 为0,有购买资格,把下单信息保存到消息队列
        // 2.3 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 2.4 订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 2.5 用户id
        voucherOrder.setUserId(userId);
        // 2.6代金券id
        voucherOrder.setVoucherId(voucherId);
        // 2.7放入阻塞队列【理论上只要放入消息队列就有购买资格】
        orderTasks.add(voucherOrder);

        // 3.获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();

        // 4. 返回订单id
        return Result.ok(orderId);
    }
   @Override
    public void createVoucherOrder(VoucherOrder voucherOrder) {

        //注意:因为我们在Lua中已经校验过库存和一人一单了,这里就不需要校验拉~
        // 1.扣减库存
        boolean success = seckillVoucherService.update().setSql("stock = stock - 1").
                eq("voucher_id", voucherOrder.getVoucherId())
                .gt("stock", 0)
                .update();
        //这里其实不判断也是OK的,因为Lua脚本中校验过了,所以一定是充足的
        if (!success) {
            log.error("库存不足!");
        }

        // 2.保存订单
        this.save(voucherOrder);
    }

并发测试:

redis6 优化,Redis从入门到入土,redis,数据库,java

可以看出平均每个请求40ms,并发达到1000/sec,速度非常快。

小总结:

秒杀业务的优化思路是什么?文章来源地址https://www.toymoban.com/news/detail-814792.html

  • 先利用Redis完成库存余量、一人一单判断,完成抢单业务
  • 再将下单业务放入阻塞队列,利用独立线程异步下单
  • 基于阻塞队列的异步秒杀存在哪些问题?
    • 内存限制问题:因为我们使用的是JDK的阻塞队列,它使用的是内存。不加以限制的时候,在高并发的情况下,无数订单进入队列,可能导致内存溢出。所以我们在创建队列的时候设置了上限。另外如果此时队列已经存满了,又有新的任务忘里面塞,就放不进去了。
    • 数据安全问题:目前是基于内存来保存这些订单信息的,
      • ①如果内存突然宕机,那么内存中所有的订单信息都丢失了。从而就可能出现用户下单成功但是数据库里面并没有订单记录,造成数据不一致的问题。
      • ②如果有一个线程从队列中取出了下单的任务,即将执行的时候发生了严重的事故(异常等),那么这个任务就没有执行,而且因为这个任务已经取出队列了,以后就再也不会执行了。从而这个任务就丢失了,再次出现数据不一致的问题。

到了这里,关于《Redis实战篇》六、秒杀优化的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Redis入门到入土(day01)

    在90年代,一个网站的访问量一般不大,用单个数据库完全可以轻松应付! 在那个时候,更多的都是静态网页,动态交互类型的网站不多。 上述架构下,我们来看看数据存储的瓶颈是什么? 数据量的总大小,一个机器放不下时 数据的索引(B+ Tree)一个机器的内存放不下时

    2023年04月26日
    浏览(42)
  • 【Redis】电商项目秒杀问题之下单接口优化:Redis缓存、MQ以及lua脚本优化高并发背景下的秒杀下单问题

    目录 一、优化思路 二、缓存库存与订单 1、库存缓存的redis数据结构 2、订单信息缓存的redis数据结构 三、整体流程 四、lua脚本确保权限校验操作的原子性 【Redis】电商项目秒杀问题之超卖问题与一人一单问题_1373i的博客-CSDN博客 https://blog.csdn.net/qq_61903414/article/details/1305689

    2024年02月05日
    浏览(48)
  • Redis项目实战——优惠券秒杀

    如果用MySQL的自增长ID,ID的规律性太明显, 会暴漏一些信息 (比如销量等) 数据量太大时一张表存不下,需要多张表,MySQL多张表的自增长都是独立的, 会出现重复ID 需要一种在分布式系统下可以生成全局唯一ID的工具,必须唯一且递增 在某项目里,不管数据库的表有多少

    2024年02月10日
    浏览(39)
  • 黑马点评Redis实战(优惠卷秒杀)

    本文是上一篇文章的后续,上一篇文章链接 马点评Redis实战(短信登录;商户查询缓存) id是一个订单必备的属性,而订单的id属性是必须唯一的,首先我们会想到使用数据库主键id,并设置为自增。这样似乎就能满足唯一性。 但是,这样会存在一些问题: id的规律太过明显,因

    2024年02月04日
    浏览(35)
  • Redis:原理速成+项目实战——Redis实战7(优惠券秒杀+细节解决超卖、一人一单问题)

    👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习 🌌上期文章:Redis:原理速成+项目实战——Redis实战6(封装缓存工具(高级写法)缓存总结) 📚订阅专栏:Redis:原理速成+项目实战 希望文章对你们有所帮助 这篇文章写了很久。我自己在边实现、边用

    2024年01月24日
    浏览(98)
  • 微服务---Redis实用篇-黑马头条项目-优惠卷秒杀功能(使用java阻塞队列对秒杀进行异步优化)

    1.1 秒杀优化-异步秒杀思路 我们来回顾一下下单流程 当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤 1、查询优惠卷 2、判断秒杀库存是否足够 3、查询订单 4、校验是否是一人一单 5、扣减库存 6、创建订单 在这六

    2024年02月05日
    浏览(52)
  • CentOS 7安装Redis6

    若系统没有安装 wget ,需要先安装 wget 从官网下载redis 将下载的文件解压至 /usr/local redis编译需要依赖 gcc ,安装 gcc 检查 gcc 版本,若 gcc 版本过低5.3以下,则无法编译 redis6 升级到 gcc 9 若没有安装 tcl ,还需安装 tcl ,或者下载压缩包编译安装 切换到解压后的redis目录,开始

    2024年02月10日
    浏览(38)
  • 4.Docker 搭建 redis6

    1.下载redis 2.创建需要挂载的宿主机文件夹 3.配置redis 切换到/data/redis/conf文件夹下,创建redis.conf,复制redis.conf配置文件内容到redis.conf文件中,然后按下键盘 esc 按键,退出编辑界面;再同时按下 shift + : ,再输入wq,保存并退出文件界面即可 redis.conf里的部分配置说明: requirep

    2024年02月07日
    浏览(30)
  • Redis6集群安装及其扩容缩容

    虚拟机拷贝2-3台centos机器,作为Redis6集群的主备节点 部署规划: master 192.168.28.132 8001 192.168.28.132 8002 192.168.28.132 8003 slaver 192.168.28.135 8001 192.168.28.135 8002 192.168.28.135 8003 一、安装依赖环境 1、安装编译依赖 yum install gcc tcl tcl-devel   此处使用yum,也可以下载RPM包后安装 2、安装

    2024年02月09日
    浏览(47)
  • Centos下安装Redis6.X

             如果提示找不到gcc程序,说明没有安装,可以用dnf命令安装:         准备好gcc,可接下来进行下载安装。         下载后执行解压:         PREFIX=/usr/local/soft/redis6:用来指定安装目录,这里我们指定安装到 /usr/local/soft/redis6

    2024年03月10日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包