iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > JAVA >《Redis实战篇》六、秒杀优化
  • 724
分享到

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

redis数据库java 2023-09-02 06:09:56 724人浏览 八月长安
摘要

6、秒杀优化 6.0 压力测试 目的:测试1000个用户抢购优惠券时秒杀功能的并发性能~ ①数据库中创建1000+用户 这里推荐使用开源工具:https://www.sqlfather.com/ ,导

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中)

        @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个线程请求接口,观察结果

image-20230207170914570

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

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

我们来回顾一下下单流程

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

查询优惠卷

判断秒杀库存是否足够

查询订单

校验是否是一人一单

扣减库存

创建订单

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

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

1653560986599

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

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

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

1653561657295

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

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

1653562234886

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

需求:

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

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

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

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

    1656080546603

VoucherServiceImpl

@Override@Transactionalpublic 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 优惠券Idlocal voucherId = ARGV[1]-- 1.2 用户idlocal userId = ARGV[2]-- 2.数据key-- 2.1 库存keylocal stockKey = 'seckill:stock:' .. voucherId-- 2.2 订单keylocal orderKey = 'seckill:order:' .. voucherId-- 3.脚本业务-- 3.1 判断库存是否充足 get stockKeyif (tonumber(redis.call('get', stockKey)) <= 0) then    -- 3.1.2 库存不足,返回1    return 1end-- 3.2 判断用户是否已经下过单if (redis.call('sismember', orderKey, userId) == 1) then    -- 3.2.2 存在,说明重复下单,返回2    return 2end-- 3.3 扣库存 incrby stockKey -1redis.call('incrby', stockKey, -1)-- 3.4 下单(保存用户) sadd orderKey userIdredis.call('sadd', orderKey, userId)-- 3.5 用户有下单资格,返回0return 0

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

VoucherOrderServiceImpl

@Overridepublic 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);}

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

image-20230207165653386

可以看到并发性能大大提升,请求响应值在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    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);                }            }        }    }        private void handleVoucherOrder(VoucherOrder voucherOrder) {                // 方式一:加分布式再创建订单        // // 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);    }             @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);    }

并发测试:

image-20230208233049812

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

小总结:

秒杀业务的优化思路是什么?

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

来源地址:https://blog.csdn.net/LXYDSF/article/details/128983338

--结束END--

本文标题: 《Redis实战篇》六、秒杀优化

本文链接: https://www.lsjlt.com/news/390137.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 《Redis实战篇》六、秒杀优化
    6、秒杀优化 6.0 压力测试 目的:测试1000个用户抢购优惠券时秒杀功能的并发性能~ ①数据库中创建1000+用户 这里推荐使用开源工具:https://www.sqlfather.com/ ,导...
    99+
    2023-09-02
    redis 数据库 java
  • 《Redis实战篇》三、优惠券秒杀
    文章目录 3.1 全局唯一ID3.2 Redis实现全局唯一Id3.3 添加优惠卷3.4 实现秒杀下单3.5 库存超卖问题分析3.6 乐观锁解决超卖问题3.7 优惠券秒杀-一人一单3.8 集群环境下的并发问题 3.1 全局唯...
    99+
    2023-08-23
    redis 数据库 java
  • go zero微服务实战性能优化极致秒杀
    目录引言批量数据聚合降低消息的消费延迟怎么保证不会超卖结束语引言 上一篇文章中引入了消息队列对秒杀流量做削峰的处理,我们使用的是Kafka,看起来似乎工作的不错,但其实还是有很多隐患...
    99+
    2024-04-02
  • Redis优惠券秒杀解决方案
    目录1 实现优惠券秒杀功能2 超卖问题(重点)1.版本号法2.CAS法1 实现优惠券秒杀功能 下单时需要判断两点:1.秒杀是否开始或者结束2.库存是否充足 所以,我们的业务逻辑如下 1. 通过优惠券id获取优惠券信息 ...
    99+
    2022-12-06
    Redis优惠券秒杀 Redis优惠券 Redis秒杀
  • redis秒杀系统的实现
    目录1.如何设计一个秒杀系统2.秒杀流程2.1 前端处理2.2 后端处理3.超卖问题4.总体思路1.如何设计一个秒杀系统 在设计任何系统之前,我们首先都需要先理解秒杀系统的业务背景 ...
    99+
    2024-04-02
  • Redis解决优惠券秒杀应用案例
    目录【前端页面】【分析代码】一人一单展望虽然本文是针对黑马点评的优惠券秒杀业务的实现,但是是适用于各种抢购活动,保证线程安全。 摘要:本文先讲了抢购问题,指出其中会出现的多线程问题,提出解决方案采用悲观锁和乐观锁两种方式...
    99+
    2024-04-02
  • springboot +rabbitmq+redis实现秒杀示例
    目录实现说明1、工具准备2、数据表3、pom4、代码结构5、配置config6、订单业务层7、redis实现层8、mq实现层9、redis模拟初始化库存量10、controller控...
    99+
    2024-04-02
  • redis怎么实现秒杀功能
    在Redis中实现秒杀功能的一种常见方法是使用Redis的原子操作和事务来控制并发访问和更新库存数量。 以下是一个简单的秒杀功能的实...
    99+
    2024-04-02
  • Redis优惠券秒杀问题怎么解决
    本篇内容主要讲解“Redis优惠券秒杀问题怎么解决”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Redis优惠券秒杀问题怎么解决”吧!1 实现优惠券秒杀功能下单时需要判断两点:1.秒杀是否开始或...
    99+
    2023-07-04
  • Redis高并发防止秒杀超卖实战源码解决方案
    目录1:解决思路2:添加 redis 常量3:添加 redis 配置类4:修改业务层1:秒杀业务逻辑层2:添加需要抢购的代金券3:抢购代金券5:postman 测试6:压力测试8:配...
    99+
    2024-04-02
  • Springboot+redis+Vue实现秒杀的项目实践
    目录1、Redis简介2、实现代码3、启动步骤4、使用ab进行并发测试5、线程安全6、总结7、参考资料1、Redis简介 Redis是一个开源的key-value存储系统。 Redi...
    99+
    2022-11-13
    Springboot+redis+Vue 秒杀 Springboot redis秒杀
  • Redis消息队列怎么实现秒杀
    要实现秒杀功能,可以使用Redis的消息队列来进行异步处理。下面是一种基本的实现方法:1. 准备工作:创建一个商品库存键值对,如"s...
    99+
    2023-10-11
    Redis
  • 如何使用Redis实现秒杀功能
    这篇文章主要介绍如何使用Redis实现秒杀功能,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1. 怎样预防数据库超售现象设置数据库事务的隔离级别为Serializable(不可用)Serializable就是让数据库...
    99+
    2023-06-14
  • PHP中使用Redis实现秒杀活动
    随着电商行业的发展,秒杀活动成为了各大平台吸引用户的重要方式之一。而随着用户数量的增加,原有的服务器无法承受瞬时的访问量,导致服务器崩溃,无法继续进行秒杀活动。为了解决这一问题,我们可以采用Redis进行秒杀活动的实现。Redis是一个基于...
    99+
    2023-05-16
    PHP redis 秒杀活动
  • 怎样用Redis轻松实现秒杀系统
    这篇文章将为大家详细讲解有关怎样用Redis轻松实现秒杀系统,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。秒杀系统的架构设计秒杀系统,是典型的短时大量突发访问类问题。对这类问题,有三种优化性...
    99+
    2023-06-02
  • redislua脚本实战秒杀和减库存的实现
    目录前言1.redisson介绍2. redis lua脚本编写与执行3.redis减库存lua脚本4.实战4.1 减库存逻辑4.2 压测前言 我们都知道redis是高性能高并发系统...
    99+
    2024-04-02
  • Redis实现秒杀的问题怎么解决
    本篇内容介绍了“Redis实现秒杀的问题怎么解决”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1、秒杀逻辑...
    99+
    2024-04-02
  • Redis实现商品秒杀功能页面流程
    目录全局唯一ID 业务逻辑分析代码实现优惠券秒杀业务逻辑分析代码实现定量商品多卖问题业务逻辑分析乐观锁与悲观锁乐观锁代码实现一个用户限买一单业务逻辑分析代码实现全局唯一ID...
    99+
    2024-04-02
  • go zero微服务性能优化极致秒杀实例分析
    这篇“go zero微服务性能优化极致秒杀实例分析”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“go z...
    99+
    2023-07-02
  • Spring Boot 整合Redis 实现优惠卷秒杀 一人一单功能
    目录一、什么是全局唯一ID⛅全局唯一ID⚡Redis实现全局唯一ID二、环境准备三、实现秒杀下单四、库存超卖问题⏳问题分析⌚ 乐观锁解决库存超卖✅Jmeter 测试五、优惠卷秒杀 实...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作