Java通过Redis实现排队功能
SpringBoot项目使用Redis实现排队。同时使用定时任务来自动踢出期请求。
·
项目背景
- 由于公司一项业务需要访问限量资源,但是此资源的数量有限不支持大规模用户同时使用,只能做一个排队功能来给用户提供服务。
- 此功能是在springboot项目下开发,配和使用redisson 来实现。
- 如果需要 redisson 配置文件可以参考这篇。
- 如果需要 MyResult 结果集可以参考这篇。
项目需求
- 在用户访问资源时,如果有空闲资源则正常返回,如果没有空闲资源,则返回排队队列信息(队列总长度、当前处于位置)。
- 前端需轮训(几秒一次,根据业务来定,无需太频繁)访问尝试获取资源,保持心跳请求。如果在指定时间内没有保持心跳,则代表死亡,就要将此次请求踢出排队队列。所以后端除了排队队列还需要维护一个心跳请求过期踢出功能(在这使用的是定时任务每间隔30 S来获取已死亡的请求,通过循环来实现踢出队列)。
流程图
参考代码
<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; }
最后
- 此排队功能是根据业务总结而来,具体设计需根据实际业务修改,在这只提供一个思路。
- 笔者提供的排队功能未在高并发下进行测试,如果用户量很大需反复测试功能可用性。
- 在文中编写不恰当或不规范的地方希望大家多多指正。
- 示例代码中有每行代码的详细注释,如果注释意思不理解可以点击下方评论或私聊进行交流。
- 如果感觉文章写的还可以的,可以留下你的赞和评论。希望大家在编程的路上一起进步。
更多推荐
已为社区贡献2条内容
所有评论(0)