在处理Web客户端发送的命令请求时,某些操作的执行时间可能会比我们预期的更长一些,通过将待执行任务的相关信息放入队列里面,并在之后对队列进行处理,用户可以推迟执行那些需要一段时间才能能完成的操作,这种将工作交给任务处理器来执行的做法被称为任务队列(task queue)。现在有很多专门的任务队列软件(如ActiveMQ,RabbitMQ,Gearman,AmazoSQS,等等),另外在缺少专门的任务队列可用的情况下,也有 一些临时性的方法可以创建任务队列。比方说使用定期作业来扫描一个数据表,查找那些在给定时间/日期之前或者之后被修改过/被检查过的用户帐号,并根据扫描的结果执行某些操作,这也是在创建任务队列。
接下来会介绍两种不通类型非任务队列,第一种队列会根据任务被插入队列的顺序来尽快地执行任务,而第二种队列则具有安排任务在未来某个特定时间执行的能力。
先进先出队列
延迟任务
假设,游戏公司增加了“延迟销售”的特性,让玩家可以在未来的某个时候才开始销售自己的商品,而不是立即进行销售。为了实现这个延迟销售特性,我们需要替换并修改现有的队列实现。
有几种不同的方法可以为队列中的任务添加延迟性质,以下是3种最直截了当的做法。
1.在任务信息中包含任务的执行时间,如果工作进程发现任务的执行时间尚未来临,那么它将在短暂等待之后,把任务重新推入队列里面。
2.工作进程使用一个本地的等待列表来记录所有需要在未来执行的任务,并在每次进行while循环的时候,检查等待列表并执行那些已经到期的任务。
3.把所有需要在未来执行的任务都添加到有序集合里面,并将任务的执行时间设置为分值,另外再使用一个进程来查找有序集合里面是否存在可以立即被执行的任务,如果有的话,就从有序集合里面移除哪个任务,并将它添加到适当的任务队列里面。
因为无论是短暂等待,还是将任务 重新推入队列里面,都会浪费工作进程的时间,所以我们不会采用第一种方法。此外,因为工作进程可能会因为崩溃而丢失本地记录的 所有待执行任务,我们也不会采用第二种方法。最后,因为使用有序集合的第三种方法最简单直接,我们将采取这种方法。
接下来会介绍两种不通类型非任务队列,第一种队列会根据任务被插入队列的顺序来尽快地执行任务,而第二种队列则具有安排任务在未来某个特定时间执行的能力。
先进先出队列
/**
* 通过电子邮件订阅游戏交易市场已售出和已过期商品,使用JSON对象推入邮件队列中。
*/
private void sendSoldEmailViaQueue(Jedis jedis, String seller, String item, double price, String buyer) {
Map<String ,Object> map = Maps.newHashMap();
map.put("seller_id", seller);
map.put("item_id", item);
map.put("price", price);
map.put("buyer_id", buyer);
map.put("time", System.currentTimeMillis());
String json = JSONObject.toJSONString(map);
jedis.rpush("queue:email", json);
}
/**
* 从队列中取出Json对象,并根据其中信息发送邮件
*/
private void processSoldEmailQueue(Jedis jedis) {
List<String> list = jedis.blpop(30000, "queue:email");
for (String json : list) {
//从Json对象中解码出邮件信息
JSONObject jsonObject = JSONObject.parseObject(json);
fetchDataAndSendSoldEmail(JSONObject);
}
}
延迟任务
假设,游戏公司增加了“延迟销售”的特性,让玩家可以在未来的某个时候才开始销售自己的商品,而不是立即进行销售。为了实现这个延迟销售特性,我们需要替换并修改现有的队列实现。
有几种不同的方法可以为队列中的任务添加延迟性质,以下是3种最直截了当的做法。
1.在任务信息中包含任务的执行时间,如果工作进程发现任务的执行时间尚未来临,那么它将在短暂等待之后,把任务重新推入队列里面。
2.工作进程使用一个本地的等待列表来记录所有需要在未来执行的任务,并在每次进行while循环的时候,检查等待列表并执行那些已经到期的任务。
3.把所有需要在未来执行的任务都添加到有序集合里面,并将任务的执行时间设置为分值,另外再使用一个进程来查找有序集合里面是否存在可以立即被执行的任务,如果有的话,就从有序集合里面移除哪个任务,并将它添加到适当的任务队列里面。
因为无论是短暂等待,还是将任务 重新推入队列里面,都会浪费工作进程的时间,所以我们不会采用第一种方法。此外,因为工作进程可能会因为崩溃而丢失本地记录的 所有待执行任务,我们也不会采用第二种方法。最后,因为使用有序集合的第三种方法最简单直接,我们将采取这种方法。
/**
* 我们使用有序集合实现第三种方式,参数中的资格值分别是,jedis,队列名字,回调函数名字,回调函数参数
* 当任务无需被延迟而是可以立即执行的时候,execute_later()函数会直接将任务推入任务队列里面,而需要延迟执行的任务则会被添加到延迟有序集合里面。
*/
private String executeLater(Jedis jedis, String queue, String name, List<String> args,long delay) {
String identifier = UUID.randomUUID().toString();
//准备好需要入队的任务
String item = JSONObject.toJSONString(args);
if (delay > 0) {
jedis.zadd("delayed:", System.currentTimeMillis() + delay, item);
}else{
jedis.rpush("queue:" + queue, item);
}
return identifier;
}
因为所有被延迟的任务都存储在同一个有序集合队列里面,所以程序只需要获取 有序集合里面排名第一的元素以及该元素的分值就可以了:如果队列里面没有任何任务,或者任务的执行时间尚未来临,那么程序将在短暂等待之后重试;如果任务的执行时间已到,那么程序将根据任务包含的标识符来获取一个细粒度锁,接着从有序集合里面移除要被执行的任务,并将它添加到适当的任务队列里面。通过将可执行的队列添加到任务队列里面而不是直接执行它们,我们可以 把获取可执行任务的进程数量限制在1两个之内。而不必根据工作进程的数量来决定运行多少个获取进程,这减少了获取可执行任务所需的花销。
/**
* 因为有序集合不具备列表那样的阻塞弹出机制,所以程序需要不断进行循环,并尝试从队列厘米那获取要被执行的任务,
* 虽然这一操作会增大网络和处理器的负载,但因为我们只会进行一两个这样的程序,所以并不会耗费太多资源
*/
class PollQueueThread extends Thread{
private Jedis jedis;
PollQueueThread(Jedis jedis){
this.jedis = jedis;
}
public void run(){
Set<Tuple> items = jedis.zrangeWithScores("delayed:", 0, 0);
Tuple item = items.size() > 0 ? items.iterator().next() : null;
if (item == null || item.getScore() > System.currentTimeMillis()) {
try{
sleep(10);
}catch(InterruptedException ie){
Thread.interrupted();
}
}
String json = item.getElement();
String[] values = gson.fromJson(json, String[].class);
String identifier = values[0];
String queue = values[1];
String locked = acquireLock(jedis, identifier);
if (jedis.zrem("delayed:", json) == 1){
jedis.rpush("queue:" + queue, json);
}
releaseLock(jedis, identifier, locked);
}
}