项目背景

  1. 由于公司一项业务需要访问限量资源,但是此资源的数量有限不支持大规模用户同时使用,只能做一个排队功能来给用户提供服务。
  2. 此功能是在springboot项目下开发,配和使用redisson 来实现。
  3. 如果需要 redisson 配置文件可以参考这篇
  4. 如果需要 MyResult 结果集可以参考这篇。

项目需求

  1. 在用户访问资源时,如果有空闲资源则正常返回,如果没有空闲资源,则返回排队队列信息(队列总长度、当前处于位置)。
  2. 前端需轮训(几秒一次,根据业务来定,无需太频繁)访问尝试获取资源,保持心跳请求。如果在指定时间内没有保持心跳,则代表死亡,就要将此次请求踢出排队队列。所以后端除了排队队列还需要维护一个心跳请求过期踢出功能(在这使用的是定时任务每间隔30 S来获取已死亡的请求,通过循环来实现踢出队列)。

流程图 

 参考代码

  • 代码中用到的 redissonClient的配置可以参考这篇,结果集返回可以参考这篇。

  •  导入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
</dependency>
  •  业务层
@Service
public class QueueServiceImpl implements QueueService{

    @Resource
    private RedissonClient redissonClient;

    /**
     * 获取资源 service
     * @param clientId: 相当于一个应用,一个应用下有不同用户,一个应用一个队列,可以理解成队列的唯一标识
     * @param openId: 代表用户在应用中的唯一标识
     * @return ResourcesAbstractInfo: 这里可以使用继承来实现返回排队信息或 资源请求成功后的返回结果。
     */
    @Override
    public MyResult<ResourcesAbstractInfo> getResources(String clientId, String openId) {
        
        //根据应用id 获取队列,队列里存的是用户的openId。
        RList<String> listQueue = redissonClient.getList("resources:game:queue:" + clientId);

        //获取检测请求请求的带分数的set集合
        RScoredSortedSet<String> scoredSortedSet = redissonClient.getScoredSortedSet("resources:game:set");

        //如果队列不为空
        if (!listQueue.isEmpty()) {

            //判断用户是否在队列里
            if (!listQueue.contains(openId)) {

                //不在则添加到队尾,同时添加心跳检测集合中
                listQueue.add(openId);
                scoredSortedSet.add(TimeUtil.currentMilli(), buildResourcesSetValue(clientId, openId));

                //返回队列信息。
                return ResultUtil.result(ResultEnum.WAITING_IN_LINE.getCode(), ResultEnum.WAITING_IN_LINE.getMsg(), new ResourcesLineUpInfo(listQueue.indexOf(openId) + 1, listQueue.size()));

            //判断用户否则在第一位
            } else if (!openId.equals(listQueue.get(0))) {

                //不是在第一位则修改心跳检测分数,返回队列信息。
                scoredSortedSet.add(TimeUtil.currentMilli(), buildResourcesSetValue(clientId, openId));
                //返回队列信息。
                return ResultUtil.result(ResultEnum.WAITING_IN_LINE.getCode(), ResultEnum.WAITING_IN_LINE.getMsg(), new ResourcesLineUpInfo(listQueue.indexOf(openId) + 1, listQueue.size()));
            }
        }
        try {
            
            //TODO 具体拿资源业务逻辑

            //到这代表请求资源成功,判断是否在队列总,存在则踢出队列
            if (listQueue.contains(openId)) {
                boolean remove = listQueue.remove(openId);
            }

            //返回成功,可以自定义返回数据,这是只个示例。
            return MyResult.success(new ResourcesInfo());

        //获取资源时抛出的异常,可以自定义异常。这里只做示例。
        } catch (ResourceNotFoundNoIdleException e1) {

            //如果是因为没有空闲资源导致报错,则返回排队信息。
            if ("没有空闲资源...".equals(e1.getErrorCode())) {

                //如果用户不在排队队列里,将用户放如队尾
                if (!listQueue.contains(openId)) {
                    boolean addQueue = listQueue.add(openId);
                }

                //心跳检测set集合汇总添加此用户心跳检测,用的时间戳当做分数。 (这里注意:如果用户已经在队列里,同样也要重置心跳时间,保持心跳。)
                scoredSortedSet.add(TimeUtil.currentMilli(), buildResourcesSetValue(clientId, openId));

                //返回正在排队中的结果码、队列总长度、当前所处位置。
                return MyResult.fail(ResultEnum.WAITING_IN_LINE.getCode(), ResultEnum.WAITING_IN_LINE.getMsg(), new ResourcesLineUpInfo(listQueue.indexOf(openId) + 1, listQueue.size()));
            }
        } catch (Exception e2) {
            log.error("system error. message: {}", e2.getMessage());
        }
        return MyResult.fail(ResultEnum.YF_9999);
    }

    /**
     * 这里多个key用":" 拼接,后续定时任务踢出队列拆分使用。
     * @param keys
     * @return 
     */
    public static String buildResourcesSetValue(String... keys){
        return String.join(":",keys);
    }
}

  • 维护心跳请求检测的定时任务,可以和业务放在一起(注意:用定时任务注解需要在启动类中添加@EnableScheduling 注解)。
/**
 * 定时任务:用来检测心情请求。
 * 初始延迟为30s,之后每隔35s检测一次
 * 
 */
@Scheduled(fixedDelay = 35000, initialDelay = 30000)
public void checkHeartBeat() {
    //拿到心跳检测请求set集合。
    RScoredSortedSet<String> sortedSet = redissonClient.getScoredSortedSet("resources:game:set");

     //拿到一分钟前的时间戳。
     long endScore = TimeUtil.ldtToMilli(LocalDateTime.now().minusMinutes(1));

     //在set集合中拿到超过1分钟的集合(此时集合中的值为:clientId:openId)。
     Collection<String> strings = sortedSet.valueRangeReversed(0, false, endScore, true);

     //遍历集合,踢出队列。
     for (String s : strings) {
         String[] split = s.split(":");

         //先拿到clientId的队列。
         RList<String> list = redissonClient.getList(CacheKeyUtils.buildTianTanGemeKey(split[0]));

         //将用户从队列中踢出。
         boolean removeList = list.remove(split[1]);

         //将用户从心跳检测set中踢出。
         boolean removeSet = sortedSet.remove(s);
     }
     log.info("Kick out queue num: {}", strings.size());
    }

 

  •  ResourcesAbstractInfo: 抽象获取资源结果类(根据需求自定义属性
public class ResourcesAbstractInfo{

	//省略get、set、tostring...方法
}

 

  •  ResourcesLineUpInfo:排队信息结果类(根据需求自定义属性
public class ResourcesLineUpInfo extends ResourcesAbstractInfo{
	private Integer current;//当前位置
	private Integer queueLength; //队列总长度
}
  • ResourcesInfo: 获取资源成功结果类(根据需求自定义属性
    public class ResourcesInfo extends ResourcesAbstractInfo{
    	private String key1;
    	private String key2; 
    }

    最后

  1. 此排队功能是根据业务总结而来,具体设计需根据实际业务修改,在这只提供一个思路。
  2. 笔者提供的排队功能未在高并发下进行测试,如果用户量很大需反复测试功能可用性。
  3. 在文中编写不恰当或不规范的地方希望大家多多指正。
  4. 示例代码中有每行代码的详细注释,如果注释意思不理解可以点击下方评论或私聊进行交流。
  5. 如果感觉文章写的还可以的,可以留下你的赞和评论。希望大家在编程的路上一起进步。
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐