仿大众点评——秒杀系统部分02
秒杀系统优化
接口限流和安全措施
- 令牌桶限流
- 单用户访问频率限流
- 抢购接口隐藏
接口限流:
在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。尤其是对于下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。
令牌桶限流
令牌桶算法与漏桶算法:漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。
- 使用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相关推荐
- 仿大众点评——秒杀系统部分01
秒杀系统 代码已上传至gitee上,地址:https://gitee.com/lin-jinghao/dazuodianping 全局ID生成器 全局唯一ID生成策略: UUID Redis自增 sn ...
- 《仿大众点评仿美团做一个评价网站——Java SSM》项目研发阶段性总结
<仿大众点评仿美团做一个评价网站--Java SSM>项目研发阶段性总结 一.后台功能实现 (一).注册商家 (二).登录商家中心 (三).商家登录后台操作模块 (1).用户管理模块 (1 ...
- android自定义引导页,Android仿大众点评引导页(ViewPage)+主页面(Fragment)的实现
大家好,今天主要是实现仿大众点评引导页和主页面以及城市定位的实现,主要使用ViewPager+Fragment+SharedPreferences,实现了第一次打开程序出现引导页,再次打开跳过引导页, ...
- Android高仿大众点评(带服务端)
2019独角兽企业重金招聘Python工程师标准>>> 实例讲解了一个类似大众点评的项目,项目包含服务端和android端源码, 服务端为php代码,如果没有接触过php, 文章中讲 ...
- 【Android重量级】高仿大众点评源码
高仿大众点评源码 下载地址:http://url.cn/Nzj3Lc 源码简介 声明:本源码只用于个人研究使用,不可用于商业用途,由于本源码引起的纠纷皆与作者无关. 本套源码是本人在校的时候 ...
- android 高仿大众点评,高仿大众点评商家列表
原生android,高仿大众点评商家列表; 废话不多说了,上代码,效果图 适配器 class MyAdapter extends BaseAdapter { protected final int m ...
- Vue 仿大众点评项目
项目地址 vue-dianping 仿大众点评Vue项目 技术栈: Vue Vuex Vue-Router Vue-Cli Vant Express Sequelize PostgreSql 实现功能 ...
- 仿大众点评——商品的详情页面
我写的仿大众点评中有许多猜你喜欢的商品,但是每个商品详情页面都是用的一个vue组件,这个时候就需要我们使用动态路由,根据动态路径键的变化从未进入不同的商品详情页面 动态路由 export defaul ...
- 大众点评订单系统分库分表实践
原大众点评的订单单表早就已经突破两百G,由于查询维度较多,即使加了两个从库,优化索引,仍然存在很多查询不理想的情况.去年大量抢购活动的开展,使数据库达到瓶颈,应用只能通过限速.异步队列等对其进行保护: ...
最新文章
- 《监控》再起风云,连同创作中的《监控2》成功牵手影视公司
- Java学习总结:49(字符缓冲流:BufferedReader)
- Winform与Webform中的对话框
- 服务器连接硬盘出错,在服务器上使物理磁盘资源联机时出错 - Windows Server | Microsoft Docs...
- Python入门--按一定格式输出的字符串%d,%f,宽度精度的设置
- Java根据城市拼音首字母排序并进行分组
- 【数据库系统工程师复习笔记】0.考试大纲及教程目录
- 【保险类项目】开发必须了解知道的概念 / 术语
- AM调制解调的Matlab和Simulink实现
- 周三送书 | 白帽子讲WEB安全
- powerha_使用IBM PowerHA SystemMirror的Hitachi TrueCopy镜像
- 下一代 Web 应用模型 — Progressive Web App
- 如何修复win7蓝牙服务器,恢复Windows7系统超便捷蓝牙连接
- 什么是短效IP和长效IP?
- 计算机毕业设计ssm农村老人管理系统的设计与实现36jlv系统+程序+源码+lw+远程部署
- java考题_java考题 求助大神
- 2977 二叉堆练习1
- javascript实现图片上传实时显示上传图片
- java w3c dom api_org.w3c.dom 中文api
- 【网上国网】最新版算法