python实现RabbitMQ同步跟异步消费模型
1,消息推送类
1 import pika 2 3 4 # 同步消息推送类 5 class RabbitPublisher(object): 6 7 # 传入RabbitMQ的ip,用户名,密码,实例化一个管道 8 def __init__(self, host, user, password): 9 self.host = host 10 self.user = user 11 self.password = password 12 self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=self.host, credentials=pika.PlainCredentials(self.user, self.password))) 13 self.channel = self.connection.channel() 14 15 # 发送消息在队列中 16 def send(self, queue_name, body): 17 self.channel.queue_declare(queue=queue_name, durable=True) # 声明一个持久化队列 18 self.channel.basic_publish(exchange='', 19 routing_key=queue_name, # 队列名字 20 body=body, # 消息内容 21 properties=pika.BasicProperties( 22 delivery_mode=2, # 消息持久化 23 )) 24 25 # 清除指定队列的所有的消息 26 def purge(self, queue_name): 27 self.channel.queue_purge(queue_name) 28 29 # 删除指定队列 30 def delete(self, queue_name, if_unused=False, if_empty=False): 31 self.channel.queue_delete(queue_name, if_unused=if_unused, if_empty=if_empty) 32 33 # 断开连接 34 def stop(self): 35 self.connection.close()
2.消息消费类
(1)同步消息消费
在同步消息消费的时候可能会出现pika库断开的情况,原因是因为pika客户端没有及时发送心跳,连接就被server端断开了。解决方案就是做一个心跳线程来维护连接。
心跳线程类
1 class Heartbeat(threading.Thread): 2 3 def __init__(self, connection): 4 super(Heartbeat, self).__init__() 5 self.lock = threading.Lock() # 线程锁 6 self.connection = connection # rabbit连接 7 self.quitflag = False # 退出标志 8 self.stopflag = True # 暂停标志 9 self.setDaemon(True) # 设置为守护线程,当消息处理完,自动清除 10 11 # 间隔10s发送心跳 12 def run(self): 13 while not self.quitflag: 14 time.sleep(10) # 睡10s发一次心跳 15 self.lock.acquire() # 加线程锁 16 if self.stopflag: 17 self.lock.release() 18 continue 19 try: 20 self.connection.process_data_events() # 一直等待服务段发来的消息 21 except Exception as e: 22 print "Error format: %s" % (str(e)) 23 self.lock.release() 24 return 25 self.lock.release() 26 27 # 开启心跳保护 28 def startheartbeat(self): 29 self.lock.acquire() 30 if self.quitflag: 31 self.lock.release() 32 return 33 self.stopflag = False 34 self.lock.release()
消息消费类
1 # 同步消息消费类 2 class RabbitConsumer(object): 3 4 # 传入RabbitMQ的ip,用户名,密码,实例化一个管道 5 def __init__(self, host, user, password): 6 self.host = host 7 self.user = user 8 self.password = password 9 self.connection = pika.BlockingConnection(pika.ConnectionParameters(host=self.host, credentials=pika.PlainCredentials(self.user, self.password))) 10 self.channel = self.connection.channel() 11 12 # 进行消费 13 def receive(self, queue_name, callback_worker, prefetch_count=1): # callback_worker为消费的回调函数 14 self.channel.queue_declare(queue=queue_name, durable=True) 15 self.channel.basic_qos(prefetch_count=prefetch_count) # 设置预取的数量,如果为0则不预取,消费者处理越快,可以将这个这设置的越高 16 self.channel.basic_consume(callback_worker, queue=queue_name) # callback_worker为消费的回调函数 17 heartbeat = Heartbeat(self.connection) # 实例化一个心跳类 18 heartbeat.start() # 开启一个心跳线程,不传target的值默认运行run函数 19 heartbeat.startheartbeat() # 开启心跳保护 20 self.channel.start_consuming() # 开始消费
调用方法
# 消费回调函数
def callback(ch, method, properties, body):
print(" [x] Received %r" % body)
ch.basic_ack(delivery_tag=method.delivery_tag) # 告诉生产者处理完成
consumer = RabbitConsumer(host="12.12.12.12", user="test", password="123")
consumer.receive(queue_name="queue1", callback_worker=callback)
(2)异步消息消费(推荐)
pika提供了支持异步发送模式的selectconnection方法支持异步发送接收(通过回调的方式)
在连接的时候stop_ioloop_on_close=False需要低版本的pika,比如0.13.1,安装方式 pip install pika==0.13.1
connectioon建立时回调建立channel, channel建立时一次回调各种declare方法,declare建立时依次回调publish。
同使用blockconnection方法相比,通过wireshark抓包来看,使用 异步的方式会对发包进行一些优化,会将几个包合并成一个大包,然后做一次ack应答从而提高效率,与之相反使用blockconnection时将会做至少两次ack,head一次content一次等
因此再试用异步的方式时会获得一定的优化
异步消息消费类
1 # 异步消息消费类 2 class RabbitConsumerAsync(object): 3 EXCHANGE = 'amq.direct' 4 EXCHANGE_TYPE = 'direct' 5 6 def __init__(self, host, user, password, queue_name="fish_test", callback_worker=None, prefetch_count=1): 7 self.host = host 8 self.user = user 9 self.password = password 10 self._connection = None 11 self._channel = None 12 self._closing = False 13 self._consumer_tag = None 14 self.QUEUE = queue_name 15 self.callbackworker = callback_worker 16 self.prefetch_count = prefetch_count 17 18 def connect(self): 19 return pika.SelectConnection(pika.ConnectionParameters(host=self.host, credentials=pika.PlainCredentials(self.user, self.password)), self.on_connection_open, 20 stop_ioloop_on_close=False) 21 22 def on_connection_open(self, unused_connection): 23 self.add_on_connection_close_callback() 24 self.open_channel() 25 26 def add_on_connection_close_callback(self): 27 self._connection.add_on_close_callback(self.on_connection_closed) 28 29 def on_connection_closed(self, connection, reply_code, reply_text): 30 self._channel = None 31 if self._closing: 32 self._connection.ioloop.stop() 33 else: 34 self._connection.add_timeout(5, self.reconnect) 35 36 def reconnect(self): 37 self._connection.ioloop.stop() 38 if not self._closing: 39 self._connection = self.connect() 40 self._connection.ioloop.start() 41 42 def open_channel(self): 43 self._connection.channel(on_open_callback=self.on_channel_open) 44 45 def on_channel_open(self, channel): 46 self._channel = channel 47 self._channel.basic_qos(prefetch_count=self.prefetch_count) 48 self.add_on_channel_close_callback() 49 self.setup_exchange(self.EXCHANGE) 50 51 def add_on_channel_close_callback(self): 52 self._channel.add_on_close_callback(self.on_channel_closed) 53 54 def on_channel_closed(self, channel, reply_code, reply_text): 55 print reply_text 56 self._connection.close() 57 58 def setup_exchange(self, exchange_name): 59 self._channel.exchange_declare(self.on_exchange_declareok, exchange_name, self.EXCHANGE_TYPE, durable=True) 60 61 def on_exchange_declareok(self, unused_frame): 62 self.setup_queue() 63 64 def setup_queue(self): 65 self._channel.queue_declare(self.on_queue_declareok, self.QUEUE, durable=True) 66 67 def on_queue_declareok(self, method_frame): 68 self._channel.queue_bind(self.on_bindok, self.QUEUE, self.EXCHANGE, self.QUEUE) 69 70 def on_bindok(self, unused_frame): 71 self.start_consuming() 72 73 def start_consuming(self): 74 self.add_on_cancel_callback() 75 self._consumer_tag = self._channel.basic_consume(self.on_message, self.QUEUE) 76 77 def add_on_cancel_callback(self): 78 self._channel.add_on_cancel_callback(self.on_consumer_cancelled) 79 80 def on_consumer_cancelled(self, method_frame): 81 if self._channel: 82 self._channel.close() 83 84 def on_message(self, unused_channel, basic_deliver, properties, body): 85 self.callbackworker(body) 86 self.acknowledge_message(basic_deliver.delivery_tag) 87 88 def acknowledge_message(self, delivery_tag): 89 self._channel.basic_ack(delivery_tag) 90 91 def stop_consuming(self): 92 if self._channel: 93 self._channel.basic_cancel(self.on_cancelok, self._consumer_tag) 94 95 def on_cancelok(self, unused_frame): 96 self.close_channel() 97 98 def close_channel(self): 99 self._channel.close() 100 101 def run(self): 102 self._connection = self.connect() 103 self._connection.ioloop.start() 104 105 def stop(self): 106 self._closing = True 107 self.stop_consuming() 108 self._connection.ioloop.start() 109 110 def close_connection(self): 111 self._connection.close()
调用方法
# 消费回调函数
def callback(body):
print(" [x] Received %r" % body)
consumer = RabbitConsumerAsync(host="12.12.12.12", user="test", password="123", queue_name="fish_test", callback_worker=callback, prefetch_count=2)
consumer.run()
(后面这两个可不加入)守护进程类(保证消费运行)
class CDaemon(object): """ a generic daemon class. usage: subclass the CDaemon class and override the run() method stderr 表示错误日志文件绝对路径, 收集启动过程中的错误日志 verbose 表示将启动运行过程中的异常错误信息打印到终端,便于调试,建议非调试模式下关闭, 默认为1, 表示开启 save_path 表示守护进程pid文件的绝对路径 """ def __init__(self, save_path, stdin=os.devnull, stdout=os.devnull, stderr=os.devnull, home_dir='.', umask=022, verbose=1): self.stdin = stdin self.stdout = stdout self.stderr = stderr self.pidfile = save_path # pid文件绝对路径 self.home_dir = home_dir self.verbose = verbose # 调试开关 self.umask = umask self.daemon_alive = True def daemonize(self): try: pid = os.fork() if pid > 0: sys.exit(0) except OSError, e: sys.stderr.write('fork #1 failed: %d (%s)\n' % (e.errno, e.strerror)) sys.exit(1) os.chdir(self.home_dir) os.setsid() os.umask(self.umask) try: pid = os.fork() if pid > 0: sys.exit(0) except OSError, e: sys.stderr.write('fork #2 failed: %d (%s)\n' % (e.errno, e.strerror)) sys.exit(1) sys.stdout.flush() sys.stderr.flush() si = file(self.stdin, 'r') so = file(self.stdout, 'a+') if self.stderr: se = file(self.stderr, 'a+', 0) else: se = so os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) def sig_handler(signum, frame): self.daemon_alive = False signal.signal(signal.SIGTERM, sig_handler) signal.signal(signal.SIGINT, sig_handler) if self.verbose >= 1: print 'daemon process started ...' atexit.register(self.del_pid) pid = str(os.getpid()) file(self.pidfile, 'w+').write('%s\n' % pid) def get_pid(self): try: pf = file(self.pidfile, 'r') pid = int(pf.read().strip()) pf.close() except IOError: pid = None except SystemExit: pid = None return pid def del_pid(self): if os.path.exists(self.pidfile): os.remove(self.pidfile) def start(self, *args, **kwargs): if self.verbose >= 1: print 'ready to starting ......' # check for a pid file to see if the daemon already runs pid = self.get_pid() if pid: msg = 'pid file %s already exists, is it already running?\n' sys.stderr.write(msg % self.pidfile) sys.exit(0) # start the daemon self.daemonize() self.run(*args, **kwargs) def stop(self): if self.verbose >= 1: print 'stopping ...' pid = self.get_pid() if not pid: msg = 'pid file [%s] does not exist. Not running?\n' % self.pidfile sys.stderr.write(msg) if os.path.exists(self.pidfile): os.remove(self.pidfile) return # try to kill the daemon process try: i = 0 while 1: os.kill(pid, signal.SIGTERM) time.sleep(0.1) i = i + 1 if i % 10 == 0: os.kill(pid, signal.SIGHUP) except OSError, err: err = str(err) if err.find('No such process') > 0: if os.path.exists(self.pidfile): os.remove(self.pidfile) else: print str(err) sys.exit(1) if self.verbose >= 1: print 'Stopped!' def restart(self, *args, **kwargs): self.stop() self.start(*args, **kwargs) def is_running(self): pid = self.get_pid() # print(pid) return pid and os.path.exists('/proc/%d' % pid) def run(self, *args, **kwargs): # NOTE: override the method in subclass print 'base class run()'
调用
class RabbitDaemon(CDaemon): def __init__(self, name, save_path, stdin=os.devnull, stdout=os.devnull, stderr=os.devnull, home_dir='.', umask=022, verbose=1): CDaemon.__init__(self, save_path, stdin, stdout, stderr, home_dir, umask, verbose) self.name = name # 派生守护进程类的名称 def run(self, **kwargs): # 新建一个队列链接 rab_con = RabbitConsumerAysnc(queuename="test", callbackworker=liando_sf_consumer, prefetch_count=2) # 开启消费者进程 rab_con.run() p_name = 'test' # 守护进程名称 pid_fn = '/www/rabbit/test.pid' # 守护进程pid文件的绝对路径 err_fn = '/www/rabbit/test_err.log' # 守护进程启动过程中的错误日志,内部出错能从这里看到 cD = RabbitDaemon(p_name, pid_fn, stderr=err_fn, verbose=1)
参考链接
https://pika.readthedocs.io/en/0.10.0/examples.html