redisson 限流实战开发
文章目录
限流场景一般用于高并发,或者接口成本较高控制成本的一种手段,通常和配额一起使用,是一种有效的保护应用可用性的方法,当然限流的编码会加大开发成本,开发维护测试,软件开发的各个环节都会收到影响,不过限流作为有效面对流量突刺保护应用正常使用的有效手段之一,必要的时候还是有必要学习使用的.
ps: 其实大多数业务场景我都用不到这个,去年的项目就有一个提到了,剩下的就是教学项目中讲了下.
限流作为一种有效的防护手段,常见限流算法有以下四种.
- 固定窗口限流
- 滑动窗口限流
- 漏桶限流
- 令牌桶限流
限流的实现
单机限流 Guava RateLimiter
分布式限流: 中间件实现统一计算 , Spring Cloud Gateway , Redis 当然这里就是 Redis 的一种实战,
ps: 写的时候其实关于限流的架构还没有完全的弄好(按照我的成体系的标准来,但是已经够用了)
限流的本质,统一计算标记位频率,只接收指定频率内的调用,对频率外的采用其他响应,保护系统正常运行.
当然我们不废话了,贴一些关于实际限流开发中用到的代码.这里使用的是 redisson 进行限流开发(Redis 的一个Java版的客户端).
当然,在最后会贴成品代码,代码改了几个版本,但是还是缺少完整流程化的测试,以及和我预期理想中的完整限流系统还有一定的差异,这篇文章写了很长时间,最近也是刚做完平台项目的简单限流,配额,时间窗口限制,和预警工作,之前写一半的东西茅塞顿开,当然,代码还需要慢慢迭代
限流注解
这是早期的一个开发,当然这次回顾发现当时编码的时候带着bug ,今天刚被修掉,优点是只针对部分几个接口进行限流操作时,最简单的操作,对代码侵入度较小,只需要在需要限流的代码部分给个注解,配置相应的参数就好.
适合小规模,允许更改代码实现的场景下,不过限流参数的变动需要更改配置多少还是有影响的.
实战代码
依赖注入
ps: 当然这里还需要引入 Redis 相关的包,嗯不介意没有一点 Redis 基础的使用,可以去搜索博客,按照配置还是很快的.看看相应原理.
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.3</version>
</dependency>
注入配置 RedissonClient
这里采用了配置式的方式,并且我选择跟 Redis 不同的库来存储限流有关的数据.
有点是方便更改,数据存储更清晰,缺点是啊麻烦.
使用
@ConfigurationProperties
注解注入数据,注意使用这个注解,会自动找到给定参数下与该注解对应方法的成员变量名一致的修改器方法和访问器方法(ps: 我记得好像是可以忽略大小写,驼峰,但是我一般都一样的),所以一定要提供相应的修改器方法,不然没办法成功注入这里使用 lombok 的@Data
编译时自动生成修改器方法(ps:该注解生成的修改器方法可被自定义的同名修改器方法覆盖). 当然还有其他的方法可以注入配置文件里的配置进来.客户端的主要作用在于建立链接,通过自定义 Config 方法和注入进链接配置的方式实现建立自己的 Redis 客户端链接, 默认的配置是
127.0.0.1
除非本地 6379 端口有无需密码的 redis 不然都得覆盖 建立自己的 redissonClient.
@Configuration
@ConfigurationProperties(prefix = "spring.redisson")
@Data
public class RedissonConfig {
private Integer database;
private String host;
private Integer port;
private String password;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setDatabase(database)
.setAddress("redis://" + host + ":" + port)
.setPassword(password);
return Redisson.create(config);
}
}
配置文件配置
这里的格式稍微有点问题,一定要注意在配置文件中的缩进.这里其他数据使用的是0 库,我将限流有关的数据放入在 1 库.
redisson:
database: 1
host: xxxxxx
port: 6379
timeout: 5000
password: xxxxxx
注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
long value() default 2; // 限流阈值,表示允许通过的请求数量
long duration() default 1; // 限流时间窗口,单位为毫秒
String key(); // 添加一个key属性,用于接收genChartByAi_的值
}
注解切面
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Resource
private UserService userService;
private final RedisLimiterManager redisLimiterManager;
public RateLimitAspect(RedisLimiterManager redisLimiterManager) {
this.redisLimiterManager = redisLimiterManager;
}
@Around("@annotation(rateLimit)")
public Object applyRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
User loginUser = userService.getLoginUser(request);
String key = rateLimit.key(); // 获取genChartByAi_的值
long value = rateLimit.value();
long duration = rateLimit.duration();
redisLimiterManager.doRateLimit(key + "_" + loginUser.getId(), value, duration);
// 执行被限流的方法
return joinPoint.proceed();
}
}
Manager 类提供具体通用功能
这里我提供了一个 在该 Manager 类内提供了一个
/**
* 专门提供 RedisLimiter 限流基础服务的(提供了通用的能力)
*/
@Service
@Slf4j
public class RedisLimiterManager {
@Resource
private RedissonClient redissonClient;
/**
* 限流操作注解版本
*
* @param key 区分不同的限流器,比如不同的用户 id 应该分别统计
*/
public void doRateLimit(String key, long value, long duration) {
// 如果已有限流器不存在,根据注解创新的限流器加入到类中
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
if (rateLimiter.isExists()) {
rateLimiter.trySetRate(RateType.OVERALL, value, duration, RateIntervalUnit.SECONDS);
}
// 每当一个操作来了后,请求一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
if (!canOp) {
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
}
}
}
当然,以上的案例只是能用但是存在好多问题,比如无法做到更灵活的限流,所以出现了下一个版本以配置形限流,在下一个版本中考虑了限流器对象的复用,过期,redis 内限流器的过期.
配置式限流
通过统一存储配置,定义规则,灵活创建限流器,限流器配置由系统内部维护,动态执行限流规则,当然,下面的编码只是完整项目的一部分,最终目的是将限流器集成在应用网关,实际转发时限流(嗯,现在的写法如果限流程序部署在多个服务器会有问题,因为为了快
此处不严谨,因为没有实际测试快了多少,因为当时想读取那些接口的配置表每一个请求都得加一次额外的查询还不如将配置本地存储
,将限流配置存储在服务器内存上(当然也可以使用分布式缓存),请根据具体场景更改).
核心代码
/**
* 限流操作 sql 配置版本
*
* @param key 区分不同的限流器,比如不同的用户 id 应该分别统计
*/
public void interceptionAndCurrentLimiting(RateLimiterKeyInfo rateLimiterKeyInfo) {
Integer scene = rateLimiterKeyInfo.getScene();
String key = getRateLimiterKey(rateLimiterKeyInfo);
Map<String, RateLimiterAllocation> initOriginal = rateLimiterAllocationsMap.get("正常").get(scene);
RRateLimiter rateLimiter = null;
if (initOriginal.containsKey(rateLimiterKeyInfo.getUrl())) {
rateLimiter = redissonClient.getRateLimiter(key);
if (!rateLimiter.isExists()) {
String url = rateLimiterKeyInfo.getUrl();
RateLimiterAllocation rateLimiterAllocation = initOriginal.get(url);
rateLimiter.trySetRate(RateType.OVERALL, rateLimiterAllocation.getRate(),
rateLimiterAllocation.getRateInterval(),
getRateTimeUnit(rateLimiterAllocation.getRateIntervalUnit()));
rateLimiter.expire(72, TimeUnit.HOURS);
}
}
if (rateLimiter == null) {
return;
}
// 每当一个操作来了后,请求一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
if (!canOp) {
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
}
}
这里使用了一个 Map 存储配置,如果是限流的路径来了,从 Map 里拉取到配置,就会执行限流逻辑,当然之前不知道 getRateLimiter
方法如果在 redis 中有限流器就会直接设置好参数, isExists 方法检测当前限流器状态.当时使用 Caffeine
缓存过 RRateLimiter 对象,不过最近看懂了 api 然后将原来的实现暂时去除,不过使用 Caffeine 能减少一次请求 redis 也会带来一点点的性能提升吧,不过啊没测试,哪个编码挺麻烦的不过学到了东西;
当时为了更新删除限流配置,更新缓存的 RRateLimiter 写的一个批量删除缓存的代码.
/**
* 模糊批量删除前缀一致的缓存
* @param prefix 匹配前缀
*/
public void deleteLike(String prefix) {
List<@NonNull String> collect = localCache.asMap().keySet().stream()
.filter(res -> res.startsWith(prefix))
.collect(Collectors.toList());
localCache.invalidateAll(collect);
}
动态限流
使用限流注解需要在开发时候就开始准备限流工作,需要开发时就对要限流的代码的 QPS 了解,一旦要调整就需要重新编码,约定大于配置,配置大于编码,这里通过存储限流规则的方式实现了动态限流,使用了第三方限流器,但因为使用内存存储限流规则,所以直接改库的方式并不能直接重新初始化掉限流器,因为设计的时候除了对单个接口维度限流,也可以根据用户身份进行限流,这就得考虑更新机制了, 通过
rateLimiter.expire(72, TimeUnit.HOURS);
限制了限流器自动失效时间,当然也得在更改规则后手动删除这个.在设计之初想着尝试做反向压力所以设置了状态值(ps:当然也可以根绝这个限流状态值做 vip 付费部分),但是嗯那部分只是有了思路,还没有完善. 因为 rateLimiter没有模糊删除的选项,当时的决定是将所有的 key 模糊匹配查询出来一起删掉,这存在一定的风险,所以 redission 的限流key 不可以和别的太像.
完整版本代码
建表 sql
CREATE TABLE `rate_limiter_allocation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`roadSigns` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'name–速率限制器路标',
`rate` bigint(20) NOT NULL COMMENT '率',
`rateInterval` bigint(20) DEFAULT NULL COMMENT '速率时间间隔',
`rateIntervalUnit` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '速率时间间隔单位',
`createUserId` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建用户 id',
`createUserName` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建用户名',
`scene` int(11) DEFAULT NULL COMMENT '限流场景(1.路径 2.用户)',
`remark` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`state` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '状态值,可枚举(1.正常 3.拥堵 2.繁忙 4.超负载)',
`usageStatus` int(11) DEFAULT '0' COMMENT '使用状态(0.启用 1.禁用)',
`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`isDelete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_userAccount` (`roadSigns`)
) ENGINE=InnoDB AUTO_INCREMENT=1757795435956686850 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='限流配置'
RedisLimiterManager
/**
* 专门提供 RedisLimiter 限流基础服务的(提供了通用的能力)
*/
@Service
@Slf4j
public class RedisLimiterManager {
/**
* 初始化内存限流配置
*/
@PostConstruct
private void initRateLimitsMap() {
// 假设rateLimiterAllocations已经被填充了数据
LambdaQueryWrapper<RateLimiterAllocation> qw = new LambdaQueryWrapper<>();
qw.eq(RateLimiterAllocation::getUsageStatus, "0");
List<RateLimiterAllocation> rateLimiterAllocations = rateLimiterAllocationService.list(qw);
// 按照state和roadSigns将RateLimiterAllocation放入Map中
rateLimiterAllocationsMap = rateLimiterAllocations.stream()
.collect(Collectors.groupingBy(
RateLimiterAllocation::getState,
Collectors.groupingBy(
RateLimiterAllocation::getScene,
Collectors.toMap(
RateLimiterAllocation::getRoadSigns,
allocation -> allocation,
(existing, replacement) -> replacement
)
)
));
log.info("限流规则加载成功: " + rateLimiterAllocations.size() + "条");
}
@Resource
private RedissonClient redissonClient;
// @Resource
// private CacheManager cacheManager;
@Resource
private RateLimiterAllocationService rateLimiterAllocationService;
private Map<String, Map<Integer, Map<String, RateLimiterAllocation>>> rateLimiterAllocationsMap;
/**
* 内存中新增限流配置
*
* @param rateLimiterAllocation
* @return
*/
public boolean put(RateLimiterAllocation rateLimiterAllocation) {
String state = rateLimiterAllocation.getState();
Integer scene = rateLimiterAllocation.getScene();
String roadSigns = rateLimiterAllocation.getRoadSigns();
try {
Map<String, RateLimiterAllocation> stringRateLimiterAllocationMap = new HashMap<>();
stringRateLimiterAllocationMap.put(roadSigns, rateLimiterAllocation);
if (rateLimiterAllocationsMap.containsKey(state)) {
Map<Integer, Map<String, RateLimiterAllocation>> integerMapMap = rateLimiterAllocationsMap.get(state);
if (integerMapMap.containsKey(scene)) {
integerMapMap.get(scene).put(roadSigns, rateLimiterAllocation);
} else {
integerMapMap.put(scene, stringRateLimiterAllocationMap);
}
} else {
Map<Integer, Map<String, RateLimiterAllocation>> integerMapMap = new HashMap<>();
integerMapMap.put(scene, stringRateLimiterAllocationMap);
rateLimiterAllocationsMap.put(state, integerMapMap);
}
} catch (Exception e) {
log.error("添加失败", e.getMessage());
return false;
}
return true;
}
/**
* 内存中移除限流配置
*
* @param rateLimiterAllocation
* @return
*/
public boolean removed(RateLimiterAllocation rateLimiterAllocation) {
String state = rateLimiterAllocation.getState();
Integer scene = rateLimiterAllocation.getScene();
String roadSigns = rateLimiterAllocation.getRoadSigns();
try {
rateLimiterAllocationsMap.get(state).get(scene).remove(roadSigns);
// 此处没删除 redis 内存储限流器,因为做限流的时候先判断本地存储有关路径有没有限流规则存在
// 如果更改限流标注就删除缓存限流器,如果限流器路径更改根据 key 去取会获取新的限流器,旧的限流器交给时间过期机制淘汰
// 获取 key 列表
RKeys keys = redissonClient.getKeys();
RateLimiterKeyInfo build = RateLimiterKeyInfo.builder()
.url(roadSigns)
.scene(scene)
.build();
String rateLimiterKey = getRateLimiterKey(build);
Iterable<String> keysByPattern = keys.getKeysByPattern("*" + rateLimiterKey + "*");
// 移除删除限流器配置
for (String key : keysByPattern) {
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
rateLimiter.delete();
}
// cacheManager.deleteLike(roadSigns + scene);
} catch (Exception e) {
log.error("删除失败", e.getMessage());
return false;
}
return true;
}
/**
* 更新内存配置
*
* @param rateLimiterAllocation
* @return
*/
public boolean update(RateLimiterAllocation rateLimiterAllocation) {
String state = rateLimiterAllocation.getState();
Integer scene = rateLimiterAllocation.getScene();
String roadSigns = rateLimiterAllocation.getRoadSigns();
try {
// 移除内存内限流配置
rateLimiterAllocationsMap.get(state).get(scene).put(roadSigns, rateLimiterAllocation);
// 获取 key 列表
RKeys keys = redissonClient.getKeys();
RateLimiterKeyInfo build = RateLimiterKeyInfo.builder()
.url(roadSigns)
.scene(scene)
.build();
String rateLimiterKey = getRateLimiterKey(build);
Iterable<String> keysByPattern = keys.getKeysByPattern("*" + rateLimiterKey + "*");
// 移除删除限流器配置
for (String key : keysByPattern) {
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
rateLimiter.delete();
}
// cacheManager.deleteLike(roadSigns + scene);
} catch (Exception e) {
log.error("删除失败" + e.getMessage());
return false;
}
return true;
}
/**
* 限流操作 sql 配置版本
*
* @param key 区分不同的限流器,比如不同的用户 id 应该分别统计
*/
public void interceptionAndCurrentLimiting(RateLimiterKeyInfo rateLimiterKeyInfo) {
Integer scene = rateLimiterKeyInfo.getScene();
String key = getRateLimiterKey(rateLimiterKeyInfo);
Map<String, RateLimiterAllocation> initOriginal = rateLimiterAllocationsMap.get("正常").get(scene);
RRateLimiter rateLimiter = null;
if (initOriginal.containsKey(rateLimiterKeyInfo.getUrl())) {
rateLimiter = redissonClient.getRateLimiter(key);
if (!rateLimiter.isExists()) {
String url = rateLimiterKeyInfo.getUrl();
RateLimiterAllocation rateLimiterAllocation = initOriginal.get(url);
rateLimiter.trySetRate(RateType.OVERALL, rateLimiterAllocation.getRate(),
rateLimiterAllocation.getRateInterval(),
getRateTimeUnit(rateLimiterAllocation.getRateIntervalUnit()));
rateLimiter.expire(72, TimeUnit.HOURS);
}
}
// RRateLimiter rateLimiter = (RRateLimiter) cacheManager.get(key);
// // 如果不包含该限流器,去配置表内拉去限流器
// if (!cacheManager.containsKey(key)) {
// synchronized (RedisLimiterManager.class) {
// if (!cacheManager.containsKey(key)) {
// // 如果已有限流器不存在,根据注解创新的限流器加入到类中
// rateLimiter = redissonClient.getRateLimiter(key);
// rateLimiter.delete();
// // 设置限流器超时时间,清理 redis 内数据该方法不推荐使用,RExpirable
// rateLimiter.expire(72, TimeUnit.HOURS);
// String url = rateLimiterKeyInfo.getUrl();
// // 初始使用正常状态的限流参数限流器
// // 当提供方接口数据异常之后提取对应状态参数创建限流器更新 cacheManager
// Map<String, RateLimiterAllocation> initOriginal = rateLimiterAllocationsMap.get("正常").get(scene);
// if (initOriginal.containsKey(url)) {
// RateLimiterAllocation rateLimiterAllocation = initOriginal.get(url);
// rateLimiter.trySetRate(RateType.OVERALL, rateLimiterAllocation.getRate(),
// rateLimiterAllocation.getRateInterval(),
// getRateTimeUnit(rateLimiterAllocation.getRateIntervalUnit()));
// cacheManager.put(key, rateLimiter);
// } else {
// rateLimiter = null;
// }
// }
// }
// }
if (rateLimiter == null) {
return;
}
// 每当一个操作来了后,请求一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
if (!canOp) {
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
}
}
/**
* 获取限流器时间单位
*
* @param rateIntervalUnit
* @return
*/
private static RateIntervalUnit getRateTimeUnit(String rateIntervalUnit) {
switch (rateIntervalUnit.toUpperCase()) {
case "MILLISECONDS":
return RateIntervalUnit.MILLISECONDS;
case "SECONDS":
return RateIntervalUnit.SECONDS;
case "MINUTES":
return RateIntervalUnit.MINUTES;
case "HOURS":
return RateIntervalUnit.HOURS;
case "DAYS":
return RateIntervalUnit.DAYS;
default:
throw new IllegalArgumentException("Unsupported rate interval unit: " + rateIntervalUnit);
}
}
/**
* 获取限流 key
*
* @param rateLimiterKeyInfo 限流维度类
* @return redission 限流 key
*/
private static String getRateLimiterKey(RateLimiterKeyInfo rateLimiterKeyInfo) {
String rateLimiterKey = rateLimiterKeyInfo.getUrl() + ":"
+ rateLimiterKeyInfo.getScene() + ":";
String userSign = rateLimiterKeyInfo.getUserSign();
if (CharSequenceUtil.isNotBlank(userSign)) {
rateLimiterKey += userSign;
}
return rateLimiterKey;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class RateLimiterKeyInfo implements Serializable {
/**
* 请求路径
*/
private String url;
/**
* 用户唯一标识
*/
private String userSign;
/**
* 请求方式
*/
private String requestMethod;
/**
* 限流场景 1.接口限流 2.用户维度限流
*/
private Integer scene;
}
/**
* 限流操作注解版本
*
* @param key 区分不同的限流器,比如不同的用户 id 应该分别统计
*/
public void doRateLimit(String key, long value, long duration) {
// 如果已有限流器不存在,根据注解创新的限流器加入到类中
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
if (rateLimiter.isExists()) {
rateLimiter.trySetRate(RateType.OVERALL, value, duration, RateIntervalUnit.SECONDS);
}
// 每当一个操作来了后,请求一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
if (!canOp) {
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
}
}
}
RateLimit
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
long value() default 2; // 限流阈值,表示允许通过的请求数量
long duration() default 1; // 限流时间窗口,单位为毫秒
String key(); // 添加一个key属性,用于接收genChartByAi_的值
}
RateLimitAspect
package com.yidiansishiyi.aimodule.aop;
import com.yidiansishiyi.aimodule.annotation.RateLimit;
//import com.yidiansishiyi.aimodule.manager.RedisLimiterManager;
import com.yidiansishiyi.aimodule.model.entity.User;
import com.yidiansishiyi.aimodule.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Resource
private UserService userService;
// private final RedisLimiterManager redisLimiterManager;
//
// public RateLimitAspect(RedisLimiterManager redisLimiterManager) {
// this.redisLimiterManager = redisLimiterManager;
// }
@Around("@annotation(rateLimit)")
public Object applyRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
User loginUser = userService.getLoginUser(request);
String key = rateLimit.key(); // 获取genChartByAi_的值
long value = rateLimit.value();
long duration = rateLimit.duration();
// redisLimiterManager.doRateLimit(key + "_" + loginUser.getId(), value, duration);
// 执行被限流的方法
return joinPoint.proceed();
}
}
RateLimiterAllocationController
这个是在运行时更新限流状态的,用的是 mybatis-plus 当然这里就不贴全代码了,不走接口更新数据库无法在运行时更新配置,其实只要将获取配置的方式变为查询数据库就可以不用这个了,但是我的目标是完整版可用的限流平台,所以嗯暂时先这样
/**
* 图表接口
*
* @author sanqi
*/
@RestController
@RequestMapping("/rateLimiterAllocation")
@Slf4j
public class RateLimiterAllocationController {
@Resource
private RateLimiterAllocationService rateLimiterAllocationService;
@Resource
private UserService userService;
@Resource
private RedisLimiterManager redisLimiterManager;
/**
* 创建
*
* @param rateLimiterAllocationAddRequest
* @param request
* @return
*/
@PostMapping("/add")
public BaseResponse<Long> addRateLimiterAllocation(@RequestBody RateLimiterAllocationAddRequest rateLimiterAllocationAddRequest, HttpServletRequest request) {
if (rateLimiterAllocationAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
RateLimiterAllocation rateLimiterAllocation = new RateLimiterAllocation();
BeanUtils.copyProperties(rateLimiterAllocationAddRequest, rateLimiterAllocation);
User loginUser = userService.getLoginUser(request);
rateLimiterAllocation.setCreateUserId(loginUser.getId().toString());
rateLimiterAllocation.setCreateUserName(loginUser.getUserName());
// 缓存和实体都同步
Integer usageStatus = rateLimiterAllocationAddRequest.getUsageStatus();
if (usageStatus != 1) {
ThrowUtils.throwIf(!redisLimiterManager.put(rateLimiterAllocation), ErrorCode.SYSTEM_ERROR);
}
boolean result = rateLimiterAllocationService.save(rateLimiterAllocation);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
long newRateLimiterAllocationId = rateLimiterAllocation.getId();
return ResultUtils.success(newRateLimiterAllocationId);
}
/**
* 删除
*
* @param deleteRequest
* @param request
* @return
*/
@PostMapping("/delete")
public BaseResponse<Boolean> deleteRateLimiterAllocation(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = userService.getLoginUser(request);
long id = deleteRequest.getId();
// 判断是否存在
RateLimiterAllocation oldRateLimiterAllocation = rateLimiterAllocationService.getById(id);
ThrowUtils.throwIf(oldRateLimiterAllocation == null, ErrorCode.NOT_FOUND_ERROR);
// 仅本人或管理员可删除
if (!oldRateLimiterAllocation.getCreateUserId().equals(user.getId().toString()) && !userService.isAdmin(request)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
ThrowUtils.throwIf(!redisLimiterManager.removed(oldRateLimiterAllocation), ErrorCode.SYSTEM_ERROR);
boolean b = rateLimiterAllocationService.removeById(id);
return ResultUtils.success(b);
}
/**
* 更新(仅管理员)
*
* @param rateLimiterAllocationUpdateRequest
* @return
*/
@PostMapping("/update")
// @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateRateLimiterAllocation(@RequestBody RateLimiterAllocationUpdateRequest rateLimiterAllocationUpdateRequest) {
if (rateLimiterAllocationUpdateRequest == null || rateLimiterAllocationUpdateRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
RateLimiterAllocation rateLimiterAllocation = new RateLimiterAllocation();
BeanUtils.copyProperties(rateLimiterAllocationUpdateRequest, rateLimiterAllocation);
long id = rateLimiterAllocationUpdateRequest.getId();
// 判断是否存在
RateLimiterAllocation oldRateLimiterAllocation = rateLimiterAllocationService.getById(id);
ThrowUtils.throwIf(oldRateLimiterAllocation == null, ErrorCode.NOT_FOUND_ERROR);
boolean update = false;
Integer usageStatus = rateLimiterAllocationUpdateRequest.getUsageStatus();
// 为空处理
if (usageStatus == 1) {
// update = redisLimiterManager.removed(oldRateLimiterAllocation);
}else {
RateLimiterAllocation newEntity = rateLimiterAllocationUpdateRequest.getNewEntity(oldRateLimiterAllocation);
update = redisLimiterManager.update(newEntity);
}
ThrowUtils.throwIf(!update, ErrorCode.NOT_FOUND_ERROR);
boolean result = rateLimiterAllocationService.updateById(rateLimiterAllocation);
return ResultUtils.success(result);
}
/**
* 根据 id 获取
*
* @param id
* @return
*/
@GetMapping("/get")
public BaseResponse<RateLimiterAllocation> getRateLimiterAllocationById(long id, HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
RateLimiterAllocation rateLimiterAllocation = rateLimiterAllocationService.getById(id);
if (rateLimiterAllocation == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
return ResultUtils.success(rateLimiterAllocation);
}
/**
* 分页获取列表(封装类)
*
* @param rateLimiterAllocationQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page")
public BaseResponse<Page<RateLimiterAllocation>> listRateLimiterAllocationByPage(@RequestBody RateLimiterAllocationQueryRequest rateLimiterAllocationQueryRequest,
HttpServletRequest request) {
long current = rateLimiterAllocationQueryRequest.getCurrent();
long size = rateLimiterAllocationQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<RateLimiterAllocation> rateLimiterAllocationPage = rateLimiterAllocationService.page(new Page<>(current, size),
rateLimiterAllocationService.getQueryWrapper(rateLimiterAllocationQueryRequest));
return ResultUtils.success(rateLimiterAllocationPage);
}
}
使用
这里我将他放在了日值里,当然只是为了测试时方便,如果将其坠在 api 网关后面最合理,虽然网关也有限流,但是这种方式更灵活,不过具体实现还得学习尝试.
总结
首先是没想到写了这么久,这篇本来已经是年前发的,但是对第三方 api 不熟悉,找文档时候也没有太多相关的文章,我在写第一版的时候就因为对 api 的不熟悉导致了很大的乌龙,不过在一点点的想解决办法的时候也学到了挺多东西,因为英语水平有限有些简单的东西变得很头疼,例如在几个月之前我就知道 限流器在 redis 中一直存在,这不是一个好的现象,内存满了触发 内存淘汰机制 事件很危险的事情,对程序员来说不确定性有的时候很可怕.偶发性的 bug 才是最难搞的,关于这个在看不懂源码的情况下并没有翻到太多教程,最后在 github 的 Issuer 区找到了答案,嗯要学英语了,再比如之前一致以为用一次就消失的 RRateLimiter 每次都创建设置参数,会不会刷新限流器,(嗯,因为这个特意学了缓存,但后来看懂了正确的用法),还有找不到批量删除的 api 等各种问题,其实在大概 6 个月前我就发现,同一个 key 如果不正确的使用 RRateLimiter 的api 更改不了限流参数,但是最近才知道怎么做,不过在这个系统里选择了删除,然后下次限流创建新的限流器,没有使用修改 api.
最近做的一个平台项目就有一个针对 用户ak,调用接口,进行以一分钟一次为单位的限流,最后的实现是从请求 日志里查询记录,存在责通过,因为执行失败的日志也算在限流内部,写了一段类似如下结构的伪代码
新增日志 查询日志 查询一分钟内新增日志的数量是否大于一 大于一结束 并删除这条没通过的日志数据 等于一继续查询
以上的目的便是确保,一个用ak 通过一个接口一分钟内只有一条数据(不管是否成功执行) 存在日志表内
这也是一种限流,但是重所周知除了非常吃计算的 cpu 密集型外,大多时候性能的瓶颈都在数据库层面,mysql 嗯瓶颈更大,而且上面的代码在编码阶段当时没发现问题,但是在使用 jmeter 测试测试的时候,并发数大于 每秒50 次时 就会因为同时插入太多第一步进来的日志产生错误, 所有请求都不成功,啊然后我对查询和删除加了把锁,并发测试正常,但是我也不知会不会出现问题(还需深入研究).不过肉眼可见的就是,性能下降非常严重.
限流的本质是,对要限流粒度的请求标识计算频率,所以不一定要用第三方组件,可以手写,但是就像上面的实现一样,比起性能来, mysql 和 redis 的性能根本不一样,这是都有网络开销的时候,如果我使用在程序内部的限流组件,减少一次网络传输和查询,一定更快,不过嘛,优缺点也很明显,限流器我想玩明白得看人家的组件api ,找文档,但是数据库查询自己写简单实现完全是基本功,到头来还是要看是否适用这个场景,有的时候复杂意味着出错时排查难度更大.
题外话: 最近有朋友问我成为程序员了吗?我当时有点懵,因为我一直说自己是个菜鸡写代码的,但是学 Java 已经两年了,已经写企业开发代码一年多了,我应该是程序员了,还有就是有朋友问他学了几个月了,还是啥也不会写,嗯想想当时自己也是那样,现在写东西还磕磕巴巴,不过比当时一脸懵逼的时候强多了,如果小时候的我知道现在的我能做到应该也不太会失望.
坑
- 继续完善代码
- 反向压力部分
- 测试,对测试可用性之外,要测试性能对比
- 拆代码,(现阶段都写到同一个里)
- 新建开源项目,方便学习
- api 网关,整合之前的配额和最近做平台项目学到的东西
- 网络,对已经有好多人提到了你怎么不去做个黑客(之类的东西,啊我是学过网安的)最为一个程序员,基本功要基础
- api 使用,在这部分代码中,有很多需求的 api 都是一点点找的,根本不熟练,要练练阅读文档和源码的能力
- 看书,对我上个月书还没看完
- 学英语
https://github.com/redisson/redisson/issues/3149
https://github.com/oneone1995/blog/issues/13