多条任务设计思想
需求:
最近工作中需要做一个类似百度网盘中的下载列表。在列表中实现任务置顶,任务上下,移动,删除等操作。
发送任务,任务进入到等待队列中,按照传入的顺序执行打包任务。打包任务能够实现,置顶,上下移动等操作。
我尝试了多种方案,包括Deque 双向队列、Redis列表中查询数据、Mysql实现思路。
1 Deque双向队列例子
import java.util.Deque;
import java.util.ArrayDeque;
public class DequeExample {
public static void main(String[] args) {
// 创建一个Deque对象
Deque<String> deque = new ArrayDeque<>();
// 添加元素到双端队列
deque.addFirst("Apple");
deque.addLast("Banana");
deque.addLast("Orange");
// 获取双端队列头部和尾部元素
String first = deque.getFirst();
String last = deque.getLast();
System.out.println("头部元素:" + first);
System.out.println("尾部元素:" + last);
// 遍历双端队列并输出元素
System.out.println("双端队列元素(从头到尾):");
for (String element : deque) {
System.out.println(element);
}
// 移除双端队列头部和尾部元素
String removedFirst = deque.removeFirst();
String removedLast = deque.removeLast();
System.out.println("移除的头部元素:" + removedFirst);
System.out.println("移除的尾部元素:" + removedLast);
// 双端队列大小
int size = deque.size();
System.out.println("双端队列大小:" + size);
// 判断双端队列是否为空
boolean isEmpty = deque.isEmpty();
System.out.println("双端队列是否为空:" + isEmpty);
}
}
最先想到的就是双向队列(先进先出),任务添加到队列后,放在任务尾部。
服务内部,设置了两个存储,分别是等待队列以及数据库。
数据库中只显示
- 打包完成 Done 、 打包失败 Failed、正在运行 Running
等待队列存放
- 等待队列 Queued
当我拿到所有的打包任务时,会把数据库中的队列、等待队列进行拼接展示。
该方法在只有一台服务器上是没有问题的,但是服务会部署到多台服务器上,在分布式项目系统中会做负载均衡,避免由于一台服务出现故障服务造成不可用的情况,所以同一个服务会部署到多台服务器上。当任务发送打包任务时,任务会分发到不同的服务上。
假设有服务器A和B,有6条打包任务(任务编号为1,2,3,4,5,6)。此时服务器A机器接收 1, 3 , 4打包任务,在服务B接受2,5,6任务。任务列表展示会出现闪烁现象。(原因当客户端,获取列表时,从A、B中获取列表不断轮询)所以列表会在(1,3,4 )、(2,5,6)闪烁。另外列表中会出现Running的任务会消失一会儿再出现,由于任务打包后,任务进入修改其状态时,需要消耗一些时间。
2 Redis list队列
为了解决上述中列表闪烁问题。主要原因是,两个服务之间没有进行数据共享,所以我采用了redis,对数据进行共享。设计思路与上述一样,先进先出
redisTemplate List、Redis详解、Redis 操作
@RequestMapping(value = "/list")
@ResponseBody
public String listTest(HttpServletRequest request) {
// 向列表中头部添加元素
redisTemplate.opsForList().leftPush("list", "第一个list元素");
// 通过index获取列表中的元素
System.out.println(redisTemplate.opsForList().index("list", 0));
// 把多个值存入列表头部
redisTemplate.opsForList().leftPushAll("list", "第二个list元素", "第三个list元素", "第四个list元素");
// 获取列表指定范围内元素,start起始位置,end结束位置,-1返回所有
System.out.println(redisTemplate.opsForList().range("list", 0, 3));
// list存在的时候再加入
redisTemplate.opsForList().leftPushIfPresent("list", "第六个list元素");
System.out.println(redisTemplate.opsForList().range("list", 0, -1));
// 在pivot值存在时在pivot前面添加
redisTemplate.opsForList().leftPush("list", "第六个list元素", "第五个半list元素");
System.out.println(redisTemplate.opsForList().range("list", 0, -1));
// 按照先进先出的原则添加多个值
redisTemplate.opsForList().rightPush("list", "第七个list元素");
redisTemplate.opsForList().rightPushAll("list", "第八个list元素", "第九个list元素");
System.out.println(redisTemplate.opsForList().range("list", 0, -1));
// 在pivot右边添加值
redisTemplate.opsForList().rightPush("list", "第七个list元素", "第七个半list元素");
System.out.println(redisTemplate.opsForList().range("list", 0, -1));
// 设置指定索引处元素的值
redisTemplate.opsForList().set("list", 5, "修改后的list元素");
// 移除并获取列表中第一个元素
System.out.println(redisTemplate.opsForList().leftPop("list"));
System.out.println(redisTemplate.opsForList().leftPop("list", 10, TimeUnit.SECONDS));
// 移除并获取列表中最后一个元素
System.out.println(redisTemplate.opsForList().rightPop("list"));
System.out.println(redisTemplate.opsForList().rightPop("list", 8, TimeUnit.SECONDS));
// 从一个列表右边弹出一个元素并将该元素插入另一个列表的左边
redisTemplate.opsForList().rightPopAndLeftPush("list", "list2");
redisTemplate.opsForList().rightPopAndLeftPush("list", "list2", 10, TimeUnit.SECONDS);
// 删除列表中值等于指定value的元素,index=0删除所有,index>0删除头部开始第一个,index<0删除尾部开始第一个
redisTemplate.opsForList().remove("list", 0, "第三个list元素");
System.out.println(redisTemplate.opsForList().range("list", 0, -1));
// 列表进行裁剪
redisTemplate.opsForList().trim("list", 1, 4);
System.out.println(redisTemplate.opsForList().range("list", 0, -1));
// 获取当前列表长度
System.out.println(redisTemplate.opsForList().size("list"));
return null;
}
但是该方法,仍然会出现正在运行的列表会消失。主要原因,发送打包任务该过程是一个很消耗时间的过程,任务状态的修改不及时,在等待队列任务弹出后(队列中就不存在该任务),可是数据库的任务状态仍然是Queued(不显示Queued)。只有当打包发送成功,任务有返回的数据,才能够及时修改任务状态,任务才展示到列表中**(添加线程,也可以解决上述问题)**。
3 Mysql数据库优化
上述问题中,Running消失,主要原因是打包任务耗时,照成Queued 状态不能及时被修改。
为了解决上述问题,我在数据库中添加了一个index 字段。
3.1 流程
index = 1 列表大小取决于,打包服务器的数量。假如有3台打包服务器,index = 1 列表就会有3个。最少为3个
3.2 设计目的:
如果没每一个数据设置编号(1,2,3,4,5,6,7)
假如 7 号任务置顶时,在数据库中就要修改7 条数据,需要将7 之前数据需要修改,会对数据库照成很大的压力。
该方法中,假设有2台打包服务器,有7条打包任务,将编号7 置顶,只需要将 7,7 的编号修改为 index = 1即可。
操作 | 将7号置顶 | |
---|---|---|
实际顺序 | (1, 2, 3, 4, 5, 6, 7) | (7, 1, 2, 3, 4, 5, 6) |
indexs | (1,1, 2, 2, 2, 2, 2 ) | (1, 1, 1, 2, 2, 2, 2) |
置顶,新创建一个更新时间,上下移动交换任务的更新时间。
index = 1 是按照更新时间从大到小排序
index = 2 按照更新时间的从小到大排序
order by indexs,
case when indexs = 1 then update_time end asc,
case when indexs = 2 then update_time end asc,
case when indexs = 3 then update_time end desc