通常情况,文件导出在我们的系统中是必不可少的一项功能。如果短时间内有大量的导出请求,并且导出的数据又比较多,则很可能导致系统性能下降,甚至使系统崩溃。下面我来介绍一种方法,供大家参考,如有不对的地方,欢迎各位大佬指正。
场景假设
如下图,一个到到处服务,部署了3个实例,为了防止短时间内有大量的导出请求,每个实例上同时最多有10个导出请求,以保护系统稳定运行。
解决方案
- 为每个实例创建一个唯一的标识。
- 使用redis的list,实现一个队列。
- 在导出前,将请求通过leftpush放入队列。
- 导出完成后,通过rightpop弹出。
代码及配置
实例唯一标识
spring:
application:
# 应用名称
name: exporter-server
# 实例ID, 用于区分多个实例
instanceId: ${spring.application.name}:01
创建队列检查工具
instanceId:因为每个实例的ID是唯一的,所以用instanceId作为redis的key,可以确保每个实例都维护一个队列,且互不影响。
日志: 将请求参数和用户ID作为队列元素,放入队列,请求前后作为日志记录。
限制:在每次请求时,检查队列长度,如果 >=max限制,则拒绝用户的导出请求。
@Slf4j
@Component
public class ExportQueueCheck {
/**
* 每个实例的唯一ID,作为队列的key, 每个实例最多同时支持MAX个导出任务
*/
@Value("${spring.application.instanceId}")
private String instanceId;
/**
* 导出队列最大长度
*/
private static final long MAX = 10;
@Resource
private RedisService redisService;
/**
* 检查队列是否已满
*
* @return 如果队列已满,则返回true;否则返回false
*/
public boolean isFull(){
// 获取当前实例队列的大小,并判断是否达到最大容量
return redisService.getQueueSize(instanceId) >= MAX;
}
/**
* 将指定的元素添加到Redis列表的左侧
*/
public void checkAndAdd(Object value){
if (isFull()) {
throw new ServiceException("导出队列已满,请稍后再试!");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null) {
throw new ServiceException("获取当前用户信息失败!");
}
Map<String, Object> element = new HashMap<>();
element.put("user", loginUser.getSysUser().getUserId());
element.put("param", value);
redisService.leftPush(instanceId, JSONUtil.toJsonStr(element));
log.warn("队列长度:{}", redisService.getQueueSize(instanceId));
}
/**
* 从Redis列表的右侧弹出一个元素
*/
public void pop(){
Object element = redisService.rightPop(instanceId);
log.warn("从队列中弹出元素:{}", element);
}
}
导出业务限制
@PostMapping("/export")
public void export(EventInfo eventInfo, HttpServletResponse response) {
// 导出队列检查, 防止短时间内大量导出
exportQueueCheck.checkAndAdd(eventInfo);
// 导出数据
eventService.export(eventInfo, response);
// 从队列弹出已完成的任务
exportQueueCheck.pop();
}