码云地址
GitHub地址
“秒杀”这一词多半出现在购物方面,但是又不单单只是购物,比如12306购票和学校抢课(大学生的痛苦)也可以看成一个秒杀。秒杀应该是一个“三高”,这个三高不是指高血脂,高血压和高血糖。而是指“高并发”,“高性能”和“高可用”。
超卖是秒杀系统最大的一个问题,如果出现超卖这个锅只有程序员进行背了,有些秒杀系统宁愿用户等待时间长点或者体验稍微的降低一点也不愿意出现超卖的限制。(系统每出现一次超卖,就损失一位程序员)
秒杀系统会在一瞬间发送一(亿)点点请求,这时候如果服务器蹦了那就会出现常见的页面了,所以通常一个秒杀系统都是一个单独的服务(单一职责)。这个可以通过限流和负载均衡处理。
恶意请求的意思就是一个人可能一次性购买很多(有时候全部也不在话下),然后再将这些东西转手卖出去。这时候是不是浮现出两个字“黄牛”,这tm不是黄牛是什么。逼近一个人的手速再快(多年单身的手速),也比不过机器请求。不要小看一些黄牛可能他们使用的系统比一些大公司的系统都要NB。
如果一个秒杀系统在你点击了抢购按钮的时候然后出现一个loading的图标一直在那里转啊转一直转了几十分钟,然后通知你商品已售空(哈哈哈哈),刺激。
用户表(user)
商品表(goods)
订单表(commodity_order)
注意:订单表表名不要叫order,会出大问题。order是数据库的一个关键之,如果真的叫这个名字则需要在查询的时候加上 ``
/*
Navicat Premium Data Transfer
Source Server : 本地
Source Server Type : MySQL
Source Server Version : 80021
Source Host : localhost:3306
Source Schema : seckill_demo
Target Server Type : MySQL
Target Server Version : 80021
File Encoding : 65001
Date: 13/09/2020 19:46:01
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for commodity_order
-- ----------------------------
DROP TABLE IF EXISTS `commodity_order`;
CREATE TABLE `commodity_order` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '订单Id',
`user_id` int(0) NOT NULL COMMENT '用户Id',
`goods_id` int(0) NOT NULL COMMENT '商品Id',
`gmt_create` datetime(0) NOT NULL COMMENT '创建时间',
`gmt_modified` datetime(0) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '商品名',
`price` decimal(10, 2) NOT NULL COMMENT '商品价格',
`stock` int(0) NOT NULL COMMENT '库存',
`sale` int(0) NOT NULL COMMENT '售卖数量',
`version` int(0) NOT NULL COMMENT '乐观锁版本号',
`gmt_create` datetime(0) NOT NULL COMMENT '创建时间',
`gmt_modified` datetime(0) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '用户Id',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '姓名',
`usernam` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
`gmt_create` datetime(0) NOT NULL COMMENT '创建时间',
`gmt_modified` datetime(0) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
这里使用了SpringBoot创建项目,自己的项目使用了Mybatis-Plus的代码生成器这里依赖就不写出来了,还有基本的SpringBoot依赖也没有写出来。
导入的依赖有:
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.10.5version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.22version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.20version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>29.0-jreversion>
dependency>
Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外,JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。为了最大限度的灵活性,JMeter允许使用正则表达式创建断言。
Apache JMeter可以用于对静态的和动态的资源(文件,Servlet,Perl脚本,java 对象,数据库和查询,FTP服务器等等)的性能进行测试。它可以用于对服务器、网络或对象模拟繁重的负载来测试它们的强度或分析不同压力类型下的整体性能。你可以使用它做性能的图形分析或在大并发负载测试你的服务器/脚本/对象。
下载地址
解压进入bin路径双击jmeter.bat
等待UI界面启动
可以改中文
添加线程组
创建HTTP请求
添加结果树
启动
**翻译:**不要使用GUI模式进行负载测试!,仅用于测试创建和测试调试。对于负载测试,请使用CLI模式(非GUI)。
jmeter -n -t [jmx file](压力测试的文件) -l [results file](结果文件) -e -o [Path to web report folder](html版的压力测试报告)
# 示例
jmeter.bat -n -t E:\JavaSoftware\JMeter\jmx\miaosha.jmx -l E:\JavaSoftware\JMeter\jmx\miaosha.txt -e -o E:\JavaSoftware\JMeter\html
详细使用在后面测试的时候一起讲解
CommodityOrderService文件
package top.ddandang.seckill.service;
import top.ddandang.seckill.model.pojo.CommodityOrder;
import com.baomidou.mybatisplus.extension.service.IService;
/**
*
* 服务类
*
*
* @author D
* @since 2020-09-13
*/
public interface CommodityOrderService extends IService<CommodityOrder> {
/**
* 演示超卖现象
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 生成的订单Id
*/
int overSold(Integer userId, Integer goodsId);
}
CommodityOrderServiceImpl文件
package top.ddandang.seckill.service.impl;
import org.springframework.transaction.annotation.Transactional;
import top.ddandang.seckill.mapper.GoodsMapper;
import top.ddandang.seckill.model.pojo.CommodityOrder;
import top.ddandang.seckill.mapper.CommodityOrderMapper;
import top.ddandang.seckill.model.pojo.Goods;
import top.ddandang.seckill.service.CommodityOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
*
* 服务实现类
*
*
* @author D
* @since 2020-09-13
*/
@Service
public class CommodityOrderServiceImpl extends ServiceImpl<CommodityOrderMapper, CommodityOrder> implements CommodityOrderService {
@Resource
private GoodsMapper goodsMapper;
@Resource
private CommodityOrderMapper commodityOrderMapper;
@Transactional(rollbackFor=Exception.class)
@Override
public int overSold(Integer userId, Integer goodsId) {
//判断库存是否充足
Goods goods = checkInventory(goodsId);
//扣除库存
deductInventory(goods);
//生成订单
return generateOrders(userId, goodsId);
}
/**
* 检测商品的库存
*
* @param goodsId 商品Id
* @return 如果库存充足则返回商品的信息 否则抛出异常
*/
private Goods checkInventory(Integer goodsId) {
Goods goods = goodsMapper.selectById(goodsId);
// 如果库存等于售卖 则商品售空
if (goods.getSale() >= goods.getStock()) {
throw new RuntimeException(goods.getName() + "已经售空!!");
}
return goods;
}
/**
* 给用户生成商品订单
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 生成的订单Id
*/
private Integer generateOrders(Integer userId, Integer goodsId) {
CommodityOrder order = new CommodityOrder()
.setUserId(userId)
.setGoodsId(goodsId);
System.out.println(order);
int row = commodityOrderMapper.insert(order);
if (row == 0) {
throw new RuntimeException("生成订单失败!!");
}
return order.getId();
}
/**
* 扣除库存(增加售卖数量)这里没有使用乐观锁
* @param goods 商品信息
*/
private void deductInventory(Goods goods) {
int updateRows = goodsMapper.updateSaleNoOptimisticLock(goods);
if(updateRows == 0) {
throw new RuntimeException("抢购失败,请重试!");
}
}
}
CommodityOrderController文件
package top.ddandang.seckill.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.ddandang.seckill.service.CommodityOrderService;
import top.ddandang.seckill.utils.R;
import javax.annotation.Resource;
/**
*
* 前端控制器
*
*
* @author D
* @since 2020-09-13
*/
@RestController
@RequestMapping("/commodity-order")
@Slf4j
public class CommodityOrderController {
@Resource
private CommodityOrderService commodityOrderService;
/**
* 会出现超卖的接口
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 订单编号
*/
@PostMapping("/overSold")
public R overSold(Integer userId, Integer goodsId) {
log.info("用户Id = {},商品Id = {}", userId, goodsId);
try {
int orderId = commodityOrderService.overSold(userId, goodsId);
return R.success().data("orderId", orderId);
} catch (RuntimeException e) {
e.printStackTrace();
return R.failed().message(e.getMessage());
}
}
}
这里使用Swagger进行测试。
再进行点击的时候出现商品已售空的情况
由此看来整个接口是没有问题的,在单点测试中也没有任何问题。然后这里将数据都重置(售卖数改为0,和订单都删除)。
测试: 这里发送1000个请求,模拟不同的用户购买不同的商品(使用UI界面),这里用户随机Id和商品随机Id都是数据库存在的数据,没有进一步校验。
商家:都售空了销量不错,心里开心
看了下订单打印机发现怎么售空了还在一直打印,然后看了看数据库(商家不可能看数据库的)
商家
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QllK708W-1600009472823)(http://image.dbbqb.com/202009132055/20f49fb6d82235ea786d1e14727f04de/gO6pl)]
程序员
愿天堂永无超卖
使用synchronized
进行加锁,这个也就是悲观锁。悲观锁一次性只允许一个线程进入,其他的线程都是在阻塞状态,这也就是解决了超卖,然后负优化了用户的体验。
注意:这里加锁不能和事务一起使用。
synchronized (this) {
int orderId = commodityOrderService.overSold(userId, goodsId);
return R.success().data("orderId", orderId);
}
百度百科:
悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
package top.ddandang.seckill.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.ddandang.seckill.service.CommodityOrderService;
import top.ddandang.seckill.utils.R;
import javax.annotation.Resource;
/**
*
* 前端控制器
*
*
* @author D
* @since 2020-09-13
*/
@RestController
@RequestMapping("/commodity-order")
@Slf4j
public class CommodityOrderController {
@Resource
private CommodityOrderService commodityOrderService;
/**
* 会出现超卖的接口
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 订单编号
*/
@PostMapping("/overSold")
public R overSold(Integer userId, Integer goodsId) {
log.info("用户Id = {},商品Id = {}", userId, goodsId);
try {
int orderId = commodityOrderService.overSold(userId, goodsId);
return R.success().data("orderId", orderId);
} catch (RuntimeException e) {
e.printStackTrace();
return R.failed().message(e.getMessage());
}
}
/**
* 悲观锁解决超卖,一次只有一个线程进入后面的线程都在阻塞
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 订单编号
*/
@PostMapping("/pessimisticLockSold")
public R pessimisticLockSold(Integer userId, Integer goodsId) {
log.info("用户Id = {},商品Id = {}", userId, goodsId);
try {
synchronized (this) {
int orderId = commodityOrderService.overSold(userId, goodsId);
return R.success().data("orderId", orderId);
}
} catch (RuntimeException e) {
e.printStackTrace();
return R.failed().message(e.getMessage());
}
}
}
测试之前记得更改HTTP请求的地址,或者新建一个HTTP请求同时删除数据库的数据(销售量清零,订单删除)
商家:
用户:
这里在数据库设计的时候给商品表增加了version
字段。
百度百科:
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号等于数据库表当前版本号,则予以更新,否则认为是过期数据。
package top.ddandang.seckill.mapper;
import org.apache.ibatis.annotations.Update;
import top.ddandang.seckill.model.pojo.Goods;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
*
* Mapper 接口
*
*
* @author D
* @since 2020-09-13
*/
public interface GoodsMapper extends BaseMapper<Goods> {
/**
* 不包含乐观锁
* 根据商品Id去扣除库存 数据库操作只有一个线程去操作
*
* @param goods 商品
* @return 影响行数
*/
@Update("update goods set sale = sale + 1, gmt_modified = now() where id = #{id}")
int updateSaleNoOptimisticLock(Goods goods);
/**
* 包含乐观锁
* 根据商品Id去扣除库存 数据库操作只有一个线程去操作
* 注意version++的时候不要在java里面,应该直接在mysql语句中写
*
* @param goods 商品
* @return 影响行数
*/
@Update("update goods set sale = sale + 1,version = version + 1, gmt_modified = now() where id = #{id} and version = #{version}")
int updateSaleOptimisticLock(Goods goods);
}
CommodityOrderServiceImpl文件增加代码
@Transactional(rollbackFor=Exception.class)
@Override
public int optimisticLockSold(Integer userId, Integer goodsId) {
//判断库存是否充足
Goods goods = checkInventory(goodsId);
//扣除库存 使用了乐观锁
deductInventoryOptimisticLock(goods);
//生成订单
return generateOrders(userId, goodsId);
}
/**
* 扣除库存(增加售卖数量)使用了乐观锁
* @param goods 商品信息
*/
private void deductInventoryOptimisticLock(Goods goods) {
int updateRows = goodsMapper.updateSaleOptimisticLock(goods);
if(updateRows == 0) {
throw new RuntimeException("抢购失败,请重试!");
}
}
package top.ddandang.seckill.service;
import top.ddandang.seckill.model.pojo.CommodityOrder;
import com.baomidou.mybatisplus.extension.service.IService;
/**
*
* 服务类
*
*
* @author D
* @since 2020-09-13
*/
public interface CommodityOrderService extends IService<CommodityOrder> {
/**
* 演示超卖现象
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 生成的订单Id
*/
int overSold(Integer userId, Integer goodsId);
/**
* 使用乐观锁防止超卖,乐观锁用户体验也更好
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 生成的订单Id
*/
int optimisticLockSold(Integer userId, Integer goodsId);
}
CommodityOrderController文件增加代码
/**
* 乐观锁解决超卖
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 订单编号
*/
@PostMapping("/optimisticLockSold")
public R optimisticLockSold(Integer userId, Integer goodsId) {
log.info("用户Id = {},商品Id = {}", userId, goodsId);
try {
int orderId = commodityOrderService.optimisticLockSold(userId, goodsId);
return R.success().data("orderId", orderId);
} catch (RuntimeException e) {
e.printStackTrace();
return R.failed().message(e.getMessage());
}
}
测试之前记得更改HTTP请求的地址,或者新建一个HTTP请求同时删除数据库的数据(销售量清零,订单删除)
接口限流:字面意思就是对一个接口的请求数据进行限制,如果一次性来了亿点点请求直接打在数据库上结果怎么样就不用说了,所以要对流量进行限制。
常见的接口限流有以下几种。
漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。
也就是说不管什么情况都是以一定的速率进行输出的,如果再高并发的时候速率还是不会变,就会导致许多的请求被抛弃,不能应对突发情况(高并发),所有在限流的时候一般不使用这个方法。
package top.ddandang.seckill.utils;
import lombok.extern.slf4j.Slf4j;
/**
*
* 漏桶算法
*
*
* @author: D
* @since: 2020/9/13
* @version: 1
*/
@Slf4j
public class LeakyBucket {
/**
* 漏桶输出的速率
*/
private final double rate;
/**
* 桶的容量
*/
private final double capacity;
/**
* 桶中现在有的水量
*/
private int storage;
/**
* 上一次刷新时间的时间戳
*/
private long refreshTime;
public LeakyBucket(double rate, double capacity) {
this.rate = rate;
this.capacity = capacity;
this.storage = 0;
this.refreshTime = System.currentTimeMillis();
}
/**
* 每次请求都会刷新桶中水的存储量
*/
private void refreshStorage() {
// 获取当前的时间戳
long nowTime = System.currentTimeMillis();
// 想象成水以一定的速率流出 但是如果加水的速度过快(高并发),水就满出来了
storage = (int) Math.max(0, storage - (nowTime - refreshTime) * rate);
refreshTime = nowTime;
}
/**
* 请求是否进入桶(该请求是否有效)
*
* @return true代表请求有效 反之无效
*/
public synchronized boolean enterBucket() {
refreshStorage();
log.info("桶的存储量 = {}", storage);
// 桶内水的存储量小于桶的容量则成功请求并将桶内水的容量加一
if (storage < capacity) {
storage++;
return true;
} else {
return false;
}
}
}
在CommodityOrderController类中增加以下方法
// 设置流速和桶的容量
private final static LeakyBucket leakyBucket = new LeakyBucket(0.01, 10);
/**
* 乐观锁解决超卖 + 漏桶限流
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 订单编号
*/
private static int count = 0;
@PostMapping("/optimisticLockAndLeakyBucketSold")
public R optimisticLockAndLeakyBucketSold(Integer userId, Integer goodsId) {
try {
boolean enterBucket = leakyBucket.enterBucket();
if (!enterBucket) {
log.error("系统繁忙请稍后再试");
return R.failed().message("系统繁忙请稍后再试");
}
count++;
log.error("请求的次数 = {}", count);
//这里没有调用购买的方法,不想一直改数据库
//int orderId = commodityOrderService.optimisticLockSold(userId, goodsId);
return R.success().data("orderId", 1);
} catch (RuntimeException e) {
return R.failed().message(e.getMessage());
}
}
count是没有拦截的请求的个数。
private final static LeakyBucket leakyBucket = new LeakyBucket(1, 5);
1000个请求大概拦截的165个请求,可以通过调节流速和桶的容量进行调节拦截请求的个数(拦截率)
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
工作过程包括3个阶段:产生令牌、消耗令牌和判断数据包是否通过。其中涉及到2个参数:令牌产生的速率CIR(Committed Information Rate)/EIR(Excess Information Rate)和令牌桶的大小CBS(Committed Burst Size)/EBS(Excess Burst Size)。
<!--guava-->
>
>com.google.guava >
>guava >
>29.0-jre >
>
可以通过调整参数来控制速率
/**
* 创建令牌桶的实例
*/
private final RateLimiter rateLimiter = RateLimiter.create(100);
rateLimiter.acquire()
rateLimiter.acquire();
不填写参数默认每次获取一个令牌,依然可以填写参数,这个方法并不会拦截请求只是限制了速度,==也就是说全部的请求都会进去。==该方法返回的时等待时间。
/**
* 乐观锁解决超卖 + 令牌桶限流
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 订单编号
*/
@PostMapping("/optimisticLockAndTokenBucketSold")
public R optimisticLockAndTokenBucketSold(Integer userId, Integer goodsId) {
try {
double acquire = rateLimiter.acquire();
log.info("等待时间:{}", acquire);
count++;
log.error("请求的次数 = {}", count);
// int orderId = commodityOrderService.optimisticLockSold(userId, goodsId);
return R.success().data("orderId", 1);
} catch (RuntimeException e) {
return R.failed().message(e.getMessage());
}
}
rateLimiter.tryAcquire
rateLimiter.tryAcquire(1, 1, TimeUnit.SECONDS);
第一个参数不填写也默认一次获取一个令牌,第二个参数是数值,第三个是单位,也就是说如果获取令牌的时候等待时间超过1s则返回false,可以根据这个返回值进行拦截请求。
/**
* 乐观锁解决超卖 + 令牌桶限流
*
* @param userId 用户Id
* @param goodsId 商品Id
* @return 订单编号
*/
@PostMapping("/optimisticLockAndTokenBucketSold")
public R optimisticLockAndTokenBucketSold(Integer userId, Integer goodsId) {
try {
// double acquire = rateLimiter.acquire();
// log.info("等待时间:{}", acquire);
//获取1个令牌 如果等待时间超过了1秒则拒绝
boolean acquire = rateLimiter.tryAcquire(1, 1, TimeUnit.SECONDS);
if (!acquire) {
log.error("系统繁忙请稍后再试");
return R.failed().message("系统繁忙请稍后再试");
}
count++;
log.info("请求的次数 = {}", count);
// int orderId = commodityOrderService.optimisticLockSold(userId, goodsId);
return R.success().data("orderId", 1);
} catch (RuntimeException e) {
return R.failed().message(e.getMessage());
}
}
相比上一个方法这个方法并不是全部的请求都执行,而是可以自行进行控制拦截的。
加入购买商品业务测试(记得清空数据)
漏桶算法:漏桶算法是强行限制数据的传输速率,也就是说就算当前桶内是空的它也是以固定的速率进行输出的,因此没有应对突发情况的能力,大量的请求过来也都是以一定的速率输出。
令牌桶算法:由于是获取令牌的方式来达到限流的,因此如果令牌数满的话这时候来了大量的请求这些请求会把令牌全部取走,这种情况也就是应对突发情况的能力(漏桶在这种情况输出的速率还是固定的)。
这里使用Redis计数器,在请求过来的时候使计数器+1,并判断是否到达上限,如果到达上限则进行拦截。并开启一个定时任务使计数器减一个值(这里为2)。
这个拦截器会拦截大量的请求
可以通过调整最大值,和定时器的时间或者定时器中减的数量来控制拦截率。
/**
* 使用redis计数器进行限流
* @return true可以正常访问 false为限制访问
*/
@Override
@Synchronized
public boolean redisCurrentLimit() {
// 返回加一之后的值
long incr = redisUtil.incr(LIMIT_KEY, 1);
if (incr >= 200) {
redisUtil.decr(LIMIT_KEY, 1);
return false;
}
return true;
}
/**
* initialDelay 服务启动100秒后执行一次
* fixedRate 每隔50毫秒执行一次
*/
@Scheduled(initialDelay = 100,fixedRate = 50)
public void decrease() {
Integer count = (Integer) redisUtil.get(LIMIT_KEY);
log.info("count = {}", count);
if (count != null && count > 0) {
redisUtil.decr(LIMIT_KEY, 2);
}
}
1000个请求只有300多点的请求进入了,而已拦截的请求都集中在一起。
Sentinel: 分布式系统的流量防卫兵。
Sentinel 具有以下特征:
下载的是一个独立的jar包,可以直接运行(需要java环境)。下载慢的时候可以使用迅雷下载。
官方下载地址
注意这里是8080端口,账号密码都是sentinel
application.yaml
cloud:
sentinel:
transport:
// sentinel地址
dashboard: localhost:8080
进入sentinel UI页面会出现当前的项目已经在里面了(可能需要发送一个请求才出现)
请求数 | 阈值类型 | 阈值 | 流控模式 | 流控效果 |
---|---|---|---|---|
500 | QPS | 10 | 直接 | 快速失败 |
接受到的请求30次左右
错误信息查看JMeter中的结果树
请求数 | 阈值类型 | 阈值 | 流控模式 | 流控效果 |
---|---|---|---|---|
500 | 线程数 | 10 | 直接 |
如果关联的资源达到了限流的条件,则将当前配置的接口(自己)进行限制(直接抛异常)。
Warm Up: 默认 coldFactor
为 3,即请求 QPS 从 threshold / 3
开始,经预热时长逐渐升至设定的 QPS 阈值。
以上图进行解释:意思就是在5秒前阈值是 30/3 = 10(前5s,QPS到达10就抛出异常),5秒后阈值慢慢升到设定的阈值(30
等待时间超过1s则直接拒绝,这个和那个令牌桶的差不多的,都是有一个等待时间