记一次使用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后就可以查询待处理的数据了,也就是等待处理的数据,可以根据时间排序,然后再次调用开辟的异步方法,以实现顺序消费