仿大众点评——秒杀系统部分02

news2025/1/15 12:46:29

秒杀系统优化

接口限流和安全措施

  • 令牌桶限流
  • 单用户访问频率限流
  • 抢购接口隐藏

接口限流:
在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。尤其是对于下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。

令牌桶限流

令牌桶算法与漏桶算法:漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。

  • 使用Guava的RateLimiter实现令牌桶限流接口:Guava是Google开源的Java工具类,里面包罗万象,也提供了限流工具类RateLimiter,该类里面实现了令牌桶算法。
// Guava令牌桶:每秒放行10个请求
    RateLimiter rateLimiter = RateLimiter.create(10);
    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        // 阻塞式获取令牌
        //LOGGER.info("等待时间" + rateLimiter.acquire());
        // 非阻塞式获取令牌
        if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
            LOGGER.warn("你被限流了,真不幸,直接返回失败");
            return Result.fail("购买失败,库存不足");
        }
        return voucherOrderService.seckillVoucher(voucherId);
    }

在接口中,可以看到有两种使用方法:

阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,就在这里阻塞住,等待令牌的发放。
非阻塞式获取令牌:请求进来后,若令牌桶里没有足够的令牌,会尝试等待设置好的时间(这里写了1000ms),其会自动判断在1000ms后,这个请求能不能拿到令牌,如果不能拿到,直接返回抢购失败。如果timeout设置为0,则等于阻塞时获取令牌。

  • Jmeter压测:
    阻塞式获取令牌结果:
    在这里插入图片描述
    在这里插入图片描述
    非阻塞式获取令牌结果:
    在这里插入图片描述
    在这里插入图片描述

抢购接口隐藏

抢购接口隐藏(接口加盐)的具体做法:

  • 每次点击秒杀按钮,先从服务器获取一个秒杀验证值(接口内判断是否到秒杀时间)。
  • Redis以缓存用户ID和商品ID为Key,秒杀地址为Value缓存验证值
  • 用户请求秒杀商品的时候,要带上秒杀验证值进行校验。

在该项目中增加两个接口:

  • 获取验证值接口:该接口要求传用户id和商品id,返回验证值,并且该验证值
/**
     * 获取验证值
     * @return
     */
    @GetMapping("getVerifyHash")
    public String getVerifyHash(@RequestParam(value = "sid") Integer sid,
                                @RequestParam(value = "userId") Integer userId) {
        String hash;
        try {
            hash = voucherOrderService.getVerifyHash(sid, userId);
        } catch (Exception e) {
            LOGGER.error("获取验证hash失败,原因:[{}]", e.getMessage());
            return "获取验证hash失败";
        }
        return String.format("请求抢购验证hash值为:%s", hash);
    }

@Override
    public String getVerifyHash(Integer sid, Integer userId) throws Exception {

        // 验证是否在抢购时间内
        LOGGER.info("验证是否在抢购时间内");

        // 检查用户合法性
        User user = userMapper.selectById(userId.longValue());
        if (user == null) {
            throw new Exception("用户不存在");
        }
        LOGGER.info("用户信息:[{}]", user.toString());

        // 检查商品合法性
        Voucher stock = voucherMapper.selectById(sid);
        if (stock == null) {
            throw new Exception("商品不存在");
        }
        LOGGER.info("商品信息:[{}]", stock.toString());

        // 生成hash
        String verify = SALT + sid + userId;
        String verifyHash = DigestUtils.md5DigestAsHex(verify.getBytes());

        // 将hash和用户商品信息存入redis
        String hashKey = RedisConstants.HASH_KEY + "_" + sid + "_" + userId;
        stringRedisTemplate.opsForValue().set(hashKey, verifyHash, 3600, TimeUnit.SECONDS);
        LOGGER.info("Redis写入:[{}] [{}]", hashKey, verifyHash);
        return verifyHash;
    }

postman测试:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 携带验证值下单接口:用户在前台拿到了验证值后,点击下单按钮,前端携带着特征值,即可进行下单操作。
/**
     * 要求验证的抢购接口
     * @param sid
     * @return
     */
    @RequestMapping(value = "/createOrderWithVerifiedUrl", method = {RequestMethod.GET})
    @ResponseBody
    public String createOrderWithVerifiedUrl(@RequestParam(value = "sid") Integer sid,
                                             @RequestParam(value = "userId") Integer userId,
                                             @RequestParam(value = "verifyHash") String verifyHash) {
        int stockLeft;
        try {
            stockLeft = voucherOrderService.createVerifiedOrder(sid, userId, verifyHash);
            LOGGER.info("购买成功,剩余库存为: [{}]", stockLeft);
        } catch (Exception e) {
            LOGGER.error("购买失败:[{}]", e.getMessage());
            return e.getMessage();
        }
        return String.format("购买成功,剩余库存为:%d", stockLeft);
    }

public int createVerifiedOrder(Integer sid, Integer userId, String verifyHash) throws Exception {

        // 验证是否在抢购时间内
        LOGGER.info("请自行验证是否在抢购时间内,假设此处验证成功");

        // 验证hash值合法性
        String hashKey = RedisConstants.HASH_KEY + "_" + sid + "_" + userId;
        String verifyHashInRedis = stringRedisTemplate.opsForValue().get(hashKey);
        if (!verifyHash.equals(verifyHashInRedis)) {
            throw new Exception("hash值与Redis中不符合");
        }
        LOGGER.info("验证hash值合法性成功");

        // 检查用户合法性
        User user = userMapper.selectById(userId.longValue());
        if (user == null) {
            throw new Exception("用户不存在");
        }
        LOGGER.info("用户信息验证成功:[{}]", user.toString());

        // 检查商品合法性
        Voucher stock = voucherMapper.selectById(sid);
        if (stock == null) {
            throw new Exception("商品不存在");
        }
        LOGGER.info("商品信息验证成功:[{}]", stock.toString());

        //乐观锁更新库存
        LOGGER.info("乐观锁更新库存成功");

        //创建订单
        LOGGER.info("创建订单成功");

        return 1;
    }

postman测试:

  • 未携带验证值或验证值错误,结果如下:
    在这里插入图片描述
    在这里插入图片描述
  • 携带验证值,结果如下:
    在这里插入图片描述
    在这里插入图片描述

单用户限制频率

用redis给每个用户做访问统计,带上商品id,对单个商品做访问统计,实现一个对用户的访问频率限制,我们在用户申请下单时,检查用户的访问次数,超过访问次数,则不让他下单!

/**
     * 要求验证的抢购接口 + 单用户限制访问频率
     * @param sid
     * @return
     */
    @RequestMapping(value = "/createOrderWithVerifiedUrlAndLimit", method = {RequestMethod.GET})
    @ResponseBody
    public String createOrderWithVerifiedUrlAndLimit(@RequestParam(value = "sid") Integer sid,
                                                     @RequestParam(value = "userId") Integer userId,
                                                     @RequestParam(value = "verifyHash") String verifyHash) {
        // 阻塞式获取令牌
        //LOGGER.info("等待时间" + rateLimiter.acquire());
        // 非阻塞式获取令牌
        /*if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
            LOGGER.warn("你被限流了,真不幸,直接返回失败");
            return "购买失败,库存不足";
        }*/
        int stockLeft;
        try {
            int count = userService.addUserCount(userId);
            LOGGER.info("用户截至该次的访问次数为: [{}]", count);
            boolean isBanned = userService.getUserIsBanned(userId);
            if (isBanned) {
                return "购买失败,超过频率限制";
            }
            stockLeft = voucherOrderService.createVerifiedOrder(sid, userId, verifyHash);
            LOGGER.info("购买成功,剩余库存为: [{}]", stockLeft);
        } catch (Exception e) {
            LOGGER.error("购买失败:[{}]", e.getMessage());
            return e.getMessage();
        }
        return String.format("购买成功,剩余库存为:%d", stockLeft);
    }



 @Override
    public int addUserCount(Integer userId) throws Exception {
        String limitKey = RedisConstants.LIMIT_KEY + "_" + userId;
        String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
        int limit = -1;
        if (limitNum == null) {
            stringRedisTemplate.opsForValue().set(limitKey, "0", 3600, TimeUnit.SECONDS);
        } else {
            limit = Integer.parseInt(limitNum) + 1;
            stringRedisTemplate.opsForValue().set(limitKey, String.valueOf(limit), 3600, TimeUnit.SECONDS);
        }
        return limit;
    }

    @Override
    public boolean getUserIsBanned(Integer userId) {
        String limitKey = RedisConstants.LIMIT_KEY + "_" + userId;
        String limitNum = stringRedisTemplate.opsForValue().get(limitKey);
        if (limitNum == null) {
            LOGGER.error("该用户没有访问申请验证值记录,疑似异常");
            return true;
        }
        return Integer.parseInt(limitNum) > 10;
    }

Jmeter做并发访问接口:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/10533.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MVCC 底层实现原理

文章目录概述事务并发出现的问题脏读不可重复读幻读事务隔离级别MVCC 底层实现原理隐式字段undo 日志Read View总结概述 MVCC(Multi-Version Concurrency Control) ,叫做基于多版本的并发控制协议。 MVCC 是乐观锁的一种实现方式,它在很多情况下&#…

多线程增量下载K线数据

准备一份股票列表的CSV文件,文件格式如下 codenameclosecmvdate_ipo300434金石亚药12.89427982959020150424300380安硕信息19.31241993416320140128688123聚辰股份132.821114087266620191223300586美联新材20.34790882138120170104300534陇神戎发12.96389465063120…

Arduino与Proteus仿真实例-密码输入、验证与更新仿真

密码输入、验证与更新仿真 本次实例将通过4X4矩阵键盘、LCD1602、EEPROM实现一个密码输入匹配、储存、更新。 1、仿真电路原理图 在仿真电路原理图中,4X4矩阵键盘通过PCF8574 IO扩展器驱动,请参考前面文章: Arduino与Proteus仿真实例-PCF8574驱动4x4矩阵键盘仿真Arduino与…

大数据:Flume安装部署和配置

文章目录Flume 简介一,Flume下载和安装1)登录[Flume官网](https://flume.apache.org/),下载 apache-flume-1.9.0-bin.tar.gz2)解压文件到 /opt 目录下3)改名为 flume二,Flume配置1)修改 /conf/ …

.Net开发——EFCore

1 EFCore是一个ORM框架 EFCore(EntityFramworkCore)是对底层ADO.NET重新封装的一个数据操作框架,因此ADO.NET支持的多种数据库都可以被EFCore支持。 EF Core 可用作对象关系映射程序 (O/RM),这可以实现以下两点: 使 .NET 开发人员能够使用…

Python医学数据分析入门,推荐你学习这本书

医学生学习Python的难点通常在于 没有系统的编程教育,很难短时间内使用编程语言描述问题及其解答过程相关教程、案例少,想要练习缺少素材 所以这里就给大家推荐一本比较新比较前沿的教医学生学习Python的书《Python医学数据分析入门》 这本书的切入点为…

代码随想录——分割回文串 II

题目 给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。 返回符合要求的 最少分割次数 。 示例 1: 输入:s “aab” 输出:1 解释:只需一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子…

Oracle技术分享 创建外键报错ORA-00906

问题描述:给表创建外键报错ORA-00906,具体过程如下: 数据库:oracle 11.2.0.464位 scottORCL2022-10-18 19:10:40> selectindex_name,table_name,tablespace_name,status,last_analyzed from user_indexes; INDEX_NAME TABLE…

事务【mysql】

1、事务的概念 事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部失败。 在不同的环境中,都可以有事务。对应在数据库中,就是数据库事务。 把多个操作打包成一个整体,要么全部都执行完…

【#HDC2022】HarmonyOS体验官活动正式开启,赶快投稿赢限量奖品吧!

1. 【活动简介】 HDC 2022 于11月4日线上线下正式开启。历时一年,在无数开发者的共同努力下,我们汇聚了HarmonyOS生态的新成果、新体验、新开放能力,邀你参与到HarmonyOS的每一步成长和构建中。 本次HarmonyOS体验官活动,将以文章…

MySQL自连接和内连接和外连接_左外连接+右外连接

自连接:将一张表看作两张表 练习:查询员工id,员工姓名及其管理者的id和姓名select emp.employee_id,emp.last_name,mgr.employee_id,mgr.last_name from employees emp,employees mgr where emp.manager_id mgr.employee_id;内连接 只是把左…

多肽Caerulein (desulfated)、pGlu-QDYTGWMDF-NH2、20994-83-6

Caerulein, desulfated 是脱硫后的Caerulein。Caerulein 是一种十肽,与胃泌素和胆囊收缩素 (CCK) 具有相同的五个羧基末端氨基酸。 Caerulein, desulfated is the desulfurated form of Caerulein. Caerulein is a decapeptide having the same five carboxyl-termi…

DevOps Master课程总结:知否知否,应是DevOps肥ITIL瘦

1.ITIL定义及挑战 ITIL即IT基础架构库(Information Technology Infrastructure Library, ITIL,信息技术基础架构库)由英国政府部门CCTA(Central Computing and Telecommunications Agency)在20世纪80年代末制订,现由英国商务部OGC(Office of Government…

Vue3 - computed 计算属性(详细教程)

简介 相信大家在 Vue2 中已经领略到了其功能,不再过多赘述了。 计算属性处理一些复杂的运算非常合适,对于任何包含响应式数据的复杂逻辑的表达式,都建议使用计算属性来完成,而不是臃肿的在模板中直接书写。 回忆 Vue2 先来看个 …

UE4插件-读取png图片

一、简介 在UE4中有多种类型的插件,适用于不同的场景。既可以放到引擎中还可以放到项目中,放到引擎中的插件可以被所有项目使用,放到项目中的插件只能被当前项目使用。插件的类型可以在【Edit】->【Plugins】->【New Plugin】中查看 …

06 在MSYS2中编译树莓派裸机程序,并在QEMU中运行

作者将狼才鲸创建日期2022-11-14 Gitee源码和工程地址:才鲸嵌入式 / 开源安防摄像机(嵌入式软件) CSDN文章地址:项目介绍:开源安防摄像机(嵌入式软件) 4)完整的环境安装步骤 写在前…

软件产品测试的准入准出标准有哪些?

软件产品的测试并不是测试人员简单的执行测试过程便可以交付的,而是有着一定的标准,什么条件可以开始测试,什么时候结束测试等等也就是测试的准入准出标准。 一、软件测试的准入标准 1、开发人员编码结束并且已在开发环境中完成单元测试&a…

OpUtils局域网唤醒:远程引导计算机

局域网唤醒 (LAN),通常称为 LAN 唤醒,是一种计算机网络标准,有助于远程启动有线和无线网络。也称为远程唤醒、LAN 唤醒、无线 LAN 唤醒或简称 WOL,LAN 唤醒技术可帮助网络管理员优化能源使用,并…

一文教你搞定Python如何自定义标准排序

文章目录1.字典自定义排序按照value排序按照key值排序先按照value降序,再按key升序2.多维列表自定义标准排序案例题目描述输入输出代码:1.字典自定义排序 按照value排序 sdict() s[a]45 s[b]44 s[c]78 s[d]34 asorted(s.items(),keylambda x:x[1],reve…

手摸手教你使用 Docker 快速搭建 Gitlab 服务

前言 Glitlab 比较占用内存,官方建议最低配置为 2核4G。我的服务器刚刚好是2核4G,但是还装了 Docker 跑一些其他服务,所以以防万一,还是选择在本地的虚拟机中进行安装。 如果你有一台配置较高的服务器那就更好了,一步…