秒杀系统优化

接口限流和安全措施

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

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

令牌桶限流

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

  • 使用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);}@Overridepublic 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());// 生成hashString verify = SALT + sid + userId;String verifyHash = DigestUtils.md5DigestAsHex(verify.getBytes());// 将hash和用户商品信息存入redisString 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})@ResponseBodypublic 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})@ResponseBodypublic 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);}@Overridepublic 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;}@Overridepublic 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做并发访问接口:

仿大众点评——秒杀系统部分02相关推荐

  1. 仿大众点评——秒杀系统部分01

    秒杀系统 代码已上传至gitee上,地址:https://gitee.com/lin-jinghao/dazuodianping 全局ID生成器 全局唯一ID生成策略: UUID Redis自增 sn ...

  2. 《仿大众点评仿美团做一个评价网站——Java SSM》项目研发阶段性总结

    <仿大众点评仿美团做一个评价网站--Java SSM>项目研发阶段性总结 一.后台功能实现 (一).注册商家 (二).登录商家中心 (三).商家登录后台操作模块 (1).用户管理模块 (1 ...

  3. android自定义引导页,Android仿大众点评引导页(ViewPage)+主页面(Fragment)的实现

    大家好,今天主要是实现仿大众点评引导页和主页面以及城市定位的实现,主要使用ViewPager+Fragment+SharedPreferences,实现了第一次打开程序出现引导页,再次打开跳过引导页, ...

  4. Android高仿大众点评(带服务端)

    2019独角兽企业重金招聘Python工程师标准>>> 实例讲解了一个类似大众点评的项目,项目包含服务端和android端源码, 服务端为php代码,如果没有接触过php, 文章中讲 ...

  5. 【Android重量级】高仿大众点评源码

    高仿大众点评源码   下载地址:http://url.cn/Nzj3Lc    源码简介 声明:本源码只用于个人研究使用,不可用于商业用途,由于本源码引起的纠纷皆与作者无关. 本套源码是本人在校的时候 ...

  6. android 高仿大众点评,高仿大众点评商家列表

    原生android,高仿大众点评商家列表; 废话不多说了,上代码,效果图 适配器 class MyAdapter extends BaseAdapter { protected final int m ...

  7. Vue 仿大众点评项目

    项目地址 vue-dianping 仿大众点评Vue项目 技术栈: Vue Vuex Vue-Router Vue-Cli Vant Express Sequelize PostgreSql 实现功能 ...

  8. 仿大众点评——商品的详情页面

    我写的仿大众点评中有许多猜你喜欢的商品,但是每个商品详情页面都是用的一个vue组件,这个时候就需要我们使用动态路由,根据动态路径键的变化从未进入不同的商品详情页面 动态路由 export defaul ...

  9. 大众点评订单系统分库分表实践

    原大众点评的订单单表早就已经突破两百G,由于查询维度较多,即使加了两个从库,优化索引,仍然存在很多查询不理想的情况.去年大量抢购活动的开展,使数据库达到瓶颈,应用只能通过限速.异步队列等对其进行保护: ...

最新文章

  1. 《监控》再起风云,连同创作中的《监控2》成功牵手影视公司
  2. Java学习总结:49(字符缓冲流:BufferedReader)
  3. Winform与Webform中的对话框
  4. 服务器连接硬盘出错,在服务器上使物理磁盘资源联机时出错 - Windows Server | Microsoft Docs...
  5. Python入门--按一定格式输出的字符串%d,%f,宽度精度的设置
  6. Java根据城市拼音首字母排序并进行分组
  7. 【数据库系统工程师复习笔记】0.考试大纲及教程目录
  8. 【保险类项目】开发必须了解知道的概念 / 术语
  9. AM调制解调的Matlab和Simulink实现
  10. 周三送书 | 白帽子讲WEB安全
  11. powerha_使用IBM PowerHA SystemMirror的Hitachi TrueCopy镜像
  12. 下一代 Web 应用模型 — Progressive Web App
  13. 如何修复win7蓝牙服务器,恢复Windows7系统超便捷蓝牙连接
  14. 什么是短效IP和长效IP?
  15. 计算机毕业设计ssm农村老人管理系统的设计与实现36jlv系统+程序+源码+lw+远程部署
  16. java考题_java考题 求助大神
  17. 2977 二叉堆练习1
  18. javascript实现图片上传实时显示上传图片
  19. java w3c dom api_org.w3c.dom 中文api
  20. 【网上国网】最新版算法

热门文章

  1. 2020-04-18 linux 软件的安装
  2. 【STC15】 STC15W408AS SBUS通信例程
  3. 说说我平时用的几个学习网站(网址)吧,希望可以给你帮助
  4. 数据库之查询表student——查询计算机系年龄在20岁以下的学生姓名
  5. IIS 7.5 以下版本 配置多域名同ip SSL 网站
  6. ES6 -- find 详解
  7. 【数据结构功法】第2话 · 一篇文章带你彻底吃透·算法复杂度
  8. arduino pwm电机调速程序
  9. 【博客117】内核如何巧妙实现:min与max函数
  10. 七夕节最深情表白文案从此告别搓衣板