参考链接
https://www.jianshu.com/p/9c04890615ba
https://www.jianshu.com/p/83d17d48c2c5
任务异步化
打开浏览器,输入地址,按下回车,打开了页面。于是一个HTTP请求(request)就由客户端发送到服务器,服务器处理请求,返回响应(response)内容。
我们每天都在浏览网页,发送大大小小的请求给服务器。有时候,服务器接到了请求,会发现他也需要给另外的服务器发送请求,或者服务器也需要做另外一些事情,于是最初们发送的请求就被阻塞了,也就是要等待服务器完成其他的事情。
更多的时候,服务器做的额外事情,并不需要客户端等待,这时候就可以把这些额外的事情异步去做。从事异步任务的工具有很多。主要原理还是处理通知消息,针对通知消息通常采取是队列结构。生产和消费消息进行通信和业务实现。
自己项目中:客户端可能同时上传多个应用去执行,这时候服务端可能无法快速处理完所有哦请求。但是这时候客户端可能会处于阻塞状态。需要进行异步处理,让客户端去做其他事情。
主要原理还是处理通知消息,针对通知消息通常采取是队列结构。生产和消费消息进行通信和业务实现。
生产消费与队列
上述异步任务的实现,可以抽象为生产者消费模型。如同一个餐馆,厨师在做饭,吃货在吃饭。如果厨师做了很多,暂时卖不完,厨师就会休息;如果客户很多,厨师马不停蹄的忙碌,客户则需要慢慢等待。实现生产者和消费者的方式用很多
Redis 队列
Python内置了一个好用的队列结构。我们也可以是用redis实现类似的操作。并做一个简单的异步任务。
Redis提供了两种方式来作消息队列。一个是使用生产者消费模式模式,另外一个方法就是发布订阅者模式。
前者会让一个或者多个客户端监听消息队列,一旦消息到达,消费者马上消费,谁先抢到算谁的,如果队列里没有消息,则消费者继续监听。
后者也是一个或多个客户端订阅消息频道,只要发布者发布消息,所有订阅者都能收到消息,订阅者都是平等的。
生产消费模式(不建议使用)
主要使用了redis提供的blpop获取队列数据,如果队列没有数据则阻塞等待,也就是监听。
import redis
class Task(object):
def __init__(self):
self.rcon = redis.StrictRedis(host='localhost', db=5)
self.queue = 'task:prodcons:queue'
def listen_task(self):
while True:
task = self.rcon.blpop(self.queue, 0)[1]
print "Task get", task
if __name__ == '__main__':
print 'listen task queue'
Task().listen_task()
发布订阅模式
使用redis的pubsub功能,订阅者订阅频道,发布者发布消息到频道了,频道就是一个消息队列
import redis
class Task(object):
def __init__(self):
self.rcon = redis.StrictRedis(host='localhost', db=5)
self.ps = self.rcon.pubsub()
self.ps.subscribe('task:pubsub:channel')
def listen_task(self):
for i in self.ps.listen():
if i['type'] == 'message':
print "Task get", i['data']
if __name__ == '__main__':
print 'listen task channel'
Task().listen_task()
Spring中使用消息队列
1.下载对应的包
jar:jedis-2.6.2.jar , spring+data+redis-1.4.2.jar
2.配置spring中支持的XML文件
<bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="127.0.0.1"></property>
<property name="port" value="6379"></property>
<property name="usePool" value="true"></property>
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="redisConnectionFactory"></property>
</bean>
<bean id="jdkSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
<bean id="messageListener" class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter">
<property name="delegate" ref="messageDelegateListener" /> <!--这里的messageDelegateListener在后面的文件中注解的-->
<property name="serializer" ref="jdkSerializer" />
</bean>
<redis:listener-container>
<redis:listener ref="messageListener" method="handleMessage" serializer="jdkSerializer" topic="java"/>
</redis:listener-container>
3.接收回调的类
package com.moensun.laipengtou.webapi.redis;
import java.io.Serializable;
import org.springframework.stereotype.Component;
@Component(value="messageDelegateListener")
public class ListenMessage {
public void handleMessage(Serializable message){
System.out.println(message);
}
}
4.发送消息的类
package com.moensun.laipengtou.webapi.redis;
import java.io.Serializable;
import javax.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class SendMessage {
@Resource(name="redisTemplate")
private RedisTemplate<String, Object> redisTemplate;
public void sendMessage(String channel, Serializable message) {
redisTemplate.convertAndSend(channel, message);
}
}
controller里调用
package com.moensun.laipengtou.webapi.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.moensun.laipengtou.webapi.redis.SendMessage;
@Controller
@RequestMapping(value="/test")
public class TestController {
@Autowired SendMessage sendMessage;
@RequestMapping(value="/redis")
public void redis(){
for (int i = 0; i <1000; i++) {
sendMessage.sendMessage("java",i);
}
}
}
学习笔记
Redis不仅可作为缓存服务器,还可用作消息队列。它的列表类型天生支持用作消息队列。如下图所示:
由于Redis的列表是使用双向链表实现的,保存了头尾节点,所以在列表头尾两边插取元素都是非常快的。
所以可以直接使用Redis的List实现消息队列,只需简单的两个指令lpush和rpop或者rpush和lpop。简单示例如下:
但上述例子中消息消费者有一个问题存在,即需要不停的调用rpop方法查看List中是否有待处理消息。每调用一次都会发起一次连接,这会造成不必要的浪费。也许你会使用Thread.sleep()等方法让消费者线程隔一段时间再消费,但这样做有两个问题:
1)、如果生产者速度大于消费者消费速度,消息队列长度会一直增大,时间久了会占用大量内存空间。
2)、如果睡眠时间过长,这样不能处理一些时效性的消息,睡眠时间过短,也会在连接上造成比较大的开销。
所以可以使用brpop指令,这个指令只有在有元素时才返回,没有则会阻塞直到超时返回null
实例代码
jedis = MyJedisFactory.getLocalJedis();
String message = jedis.rpop(Producer.MESSAGE_KEY);
发布/订阅模式
Redis除了对消息队列提供支持外,还提供了一组命令用于支持发布/订阅模式。
1)发布
PUBLISH指令可用于发布一条消息,格式 PUBLISH channel message
返回值表示订阅了该消息的数量。
2)订阅
SUBSCRIBE指令用于接收一条消息,格式 SUBSCRIBE channel
可以看到使用SUBSCRIBE指令后进入了订阅模式,但没有接收到publish发送的消息,这是因为只有在消息发出去前订阅才会接收到。在这个模式下其他指令,只能看到回复。回复分为三种类型:
1、如果为subscribe,第二个值表示订阅的频道,第三个值表示是第几个订阅的频道?(理解成序号?)
2、如果为message(消息),第二个值为产生该消息的频道,第三个值为消息
3、如果为unsubscribe,第二个值表示取消订阅的频道,第三个值表示当前客户端的订阅数量。
实例代码
jedis.publish(CHANNEL_KEY, message);
JedisPubSub jps = new JedisPubSub() {
/**
* JedisPubSub类是一个没有抽象方法的抽象类,里面方法都是一些空实现
* 所以可以选择需要的方法覆盖,这儿使用的是SUBSCRIBE指令,所以覆盖了onMessage
* 如果使用PSUBSCRIBE指令,则覆盖onPMessage方法
* 当然也可以选择BinaryJedisPubSub,同样是抽象类,但方法参数为byte[]
*/
@Override
public void onMessage(String channel, String message) {
if(Publisher.CHANNEL_KEY.equals(channel)) {
System.out.println("接收到消息: channel : " + message);
//接收到exit消息后退出
if(EXIT_COMMAND.equals(message)) {
System.exit(0);
}
}
}