记一次使用java实现请求排队的实现逻辑
业务需求:刚接到的需求是需要导出大数据量的excel表格,一次大概30-100多万不等,然后接口一次只能处理一个请求,如果多个请求要实现请求排队,意思就是你请求的时候如果这个接口在处理别的请求,那么你这个请求要在接口处理完后再处理你这个请求,当然,这也是为了防止高并发导致oom.导出excel表格很简单,用easyExcel就行了,可以自行搜索,关于这个请求排队还是第一次做,所以也想到了很多方案.比如用mq的队列,消费者只能一个一个消费,消费完发ack,然后再消费,这个方案之前也搜索了相关资料,做起来比较复杂就pass了,然后又想到了线程等待,然后处理完后在notify,这样太费性能了,而且容易数据丢失,想来想去其实有时候看着复杂的东西往往很简单,最后决定采取的方案是接口加分布式锁,谁拿锁谁执行,线程在拿锁执行期间别的请求先记录,等数据处理完后在发mq就行消费就可以了,具体实现如下:
第一步,每一个请求先记录
这里也用了reids对用户做了访问次数的限制:
@Override
public void allExport(VehicleCardAllExportDTO vehicleCardAllExportDTO) {//根据用户id查询缓存,今日点击量是否大于三次
String key = BIND_CLICK_COUNT_KEY+":"+vehicleCardAllExportDTO.getOperationPersonId();
Boolean aBoolean = invokeExceededTimes(key,CLICK_COUNT);
if (Boolean.FALSE.equals(aBoolean)) {
throw new BadRequestException(ErrorCode.V_REQUEST_IS_LIMIT);
}
//生成压缩文件名,保存mongo,状态处理中
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Constants.DATE_FORMAT);
String format = simpleDateFormat.format(new Date());
String fileName = Constants.ALL_DATA_NAME_FORMAT+Constants.FILE_NAME_SIGN+format;
//生成mongoDB导出文件记录
String fileId = UUID.randomUUID().toString().replace("-", "").toLowerCase();
VehicleCardAllExportFile vehicleCardAllExportFile = new VehicleCardAllExportFile();
vehicleCardAllExportFile.setFileId(fileId);
vehicleCardAllExportFile.setFileName(fileName+ZIP_FORMAT);
vehicleCardAllExportFile.setOperationPersonId(vehicleCardAllExportDTO.getOperationPersonId());
vehicleCardAllExportFile.setOperationPersonName(vehicleCardAllExportDTO.getOperationPersonName());
vehicleCardAllExportFile.setAdmin(vehicleCardAllExportDTO.getAdmin());
vehicleCardAllExportFile.setExportStatus(Constants.VEHICLE_CARD_FILE_EXECUTE);
vehicleCardAllExportFile.setCreatedTime(new Date());
vehicleCardAllExportFile.setUpdatedTime(new Date());
vehicleCardAllExportFile.setType(ZIP_FORMAT);
vehicleCardAllExportDTO.setFileId(fileId);
vehicleCardAllExportDTO.setFileName(fileName);
vehicleCardAllExportFile.setVehicleCardAllExportDTO(vehicleCardAllExportDTO);
mongoTemplate.insert(vehicleCardAllExportFile, Constants.MONGO_VEHICLE_CARD_EXPORT_FILE);
vehicleCardResourcesExport.exportExportCard(vehicleCardAllExportDTO);
}
第二步,开辟异步线程拿锁就行处理:
这里仅展现拿锁的逻辑了,剩下的就是业务逻辑代码,就不展示了:
@Async
public void exportExportCard(VehicleCardAllExportDTO vehicleCardAllExportDTO) {RLock allExportLock = null;
//查询初始化页数
int pageNum = 1;
FileOutputStream out = null;
InputStream fileInputStream = null;
try {
//此处加分布式锁 拿到锁处理
allExportLock = redisson.getLock(Constants.REDIS_ALL_EXPORT_KEY);
if (!allExportLock.tryLock()) {
log.info("************全量导出,获取分布式锁失败!任务结束执行end************");
return;
}
第三步:业务处理完成,释放分布式锁,发送mq
下面的代码要写在finally代码块里:
finally {
// 关闭流
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != fileInputStream) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
//删除本地临时文件
File file1 = new File(Constants.TEMP_PATH + vehicleCardAllExportDTO.getFileName());
if (file1.exists()) {
FileDeleteUtil.delFolder(Constants.TEMP_PATH + vehicleCardAllExportDTO.getFileName() + “/”);
}
File file = new File(Constants.TEMP_ZIP_PATH + vehicleCardAllExportDTO.getFileName() + ZIP_FORMAT);
if (file.isFile() && file.exists()) {
Files.delete(file.toPath());
}
} catch (Exception e) {
log.info(“删除本地文件失败:{}”, e.getMessage());
}if (null != allExportLock && allExportLock.isLocked() && allExportLock.isHeldByCurrentThread()) {
allExportLock.unlock();
log.info("全量导出结束-锁已释放------------》");
}
//发送mq消息 实现顺序消费
SendResult sendResult = producer.send(Constants.ROCKET_TOPIC, Constants.VEHICLE_CARD_RESOURCES_ALL_EXPORT, vehicleCardAllExportDTO, null);
}
第四部,消费mq,实现顺序消费
接收到mq后就可以查询待处理的数据了,也就是等待处理的数据,可以根据时间排序,然后再次调用开辟的异步方法,以实现顺序消费