一、什么是消息队列
在接触消息队列之前,我们需要了解消息队列到底是什么?为什么要用消息队列?这里我们找到一篇很有意思的案例来帮助我们初学者更好地了解消息,了解消息队列。
1.1 案例
有一天,产品跑来说:“我们要做一个用户注册功能,需要在用户注册成功后给用户发一封成功邮件。”
小明(攻城狮):“好,需求很明确了。” 不就提供一个注册接口,保存用户信息,同时发起邮件调用,待邮件发送成功后,返回用户操作成功。没一会功夫,代码就写完了。验证功能没问题后,就发布上线了。
线上正常运行了一段时间,产品匆匆地跑来说:“你做的功能不行啊,运营反馈注册操作响应太慢,已经有好多用户流失了。”
小明听得一身冷汗,赶紧回去改。他发现,原先的以单线程同步阻塞的方式进行邮件发送,确实存在问题。这次,他利用了 JAVA 多线程的特性,另起线程进行邮件发送,主线程直接返回保存结果。测试通过后,赶紧发布上线。小明心想,这下总没问题了吧。
没过多久,产品又跑来了,他说:“现在,注册操作响应是快多了。但是又有新的问题了,有用户反应,邮件收不到。能否在发送邮件时,保存一下发送的结果,对于发送失败的,进行补发。”
小明一听,哎,又得熬夜加班了。产品看他一脸苦逼的样子,忙说:“邮件服务这块,别的团队都已经做好了,你不用再自己搞了,直接用他们的服务。”
小明赶紧去和邮件团队沟通,谁知他们的服务根本就不对外开放。这下小明可开始犯愁了,明知道有这么一个服务,可是偏偏又调用不了。
邮件团队的人说,“看你愁的,我给你提供了一个类似邮局信箱的东西,你往这信箱里写上你要发送的消息,以及我们约定的地址。之后你就不用再操心了,我们自然能从约定的地址中取得消息,进行邮件的相应操作。”
后来,小明才知道,这就是外界广为流传的消息队列。你不用知道具体的服务在哪,如何调用。你要做的只是将该发送的消息,向你们约定好的地址进行发送,你的任务就完成了。对应的服务自然能监听到你发送的消息,进行后续的操作。这就是消息队列最大的特点,将同步操作转为异步处理,将多服务共同操作转为职责单一的单服务操作,做到了服务间的解耦。
哈哈,这下能高枕无忧了。太年轻,哪有万无一失的技术啊~
不久的一天,你会发现所有业务都替换了邮件发送的方式,统一使用了消息队列来进行发送。这下仅仅一个邮件服务模块,难以承受业务方源源不断的消息,大量的消息堆积在了队列中。这就需要更多的消费者(邮件服务)来共同处理队列中的消息,即所谓的分布式消息处理。
未完待续。。。
1.2 定义
有了上面的基础,再看非常官方的解释应该也能理解了。
消息队列(英语:Message queue)是一种进程间通信或同一进程的不同线程间的通信方式,软件的贮列用来处理一系列的输入,通常是来自用户。消息队列提供了异步的通信协议,每一个贮列中的纪录包含详细说明的数据,包含发生的时间,输入设备的种类,以及特定的输入参数,也就是说:消息的发送者和接收者不需要同时与消息队列互交。消息会保存在队列中,直到接收者取回它。 ——维基百科
1)名词解释
解释还是太官方了,我们来看一个最简单的架构模型:
- Producer:消息生产者,负责产生和发送消息到 Broker;
- Broker:消息处理中心。负责消息存储、确认、重试等,一般其中会包含多个 queue;
- Consumer:消息消费者,负责从 Broker 中获取消息,并进行相应处理;
2)特性
异步性
将耗时的同步操作,通过以发送消息的方式,进行了异步化处理。减少了同步等待的时间。
松耦合
消息队列减少了服务之间的耦合性,不同的服务可以通过消息队列进行通信,而不用关心彼此的实现细节,只要定义好消息的格式就行。
分布式
通过对消费者的横向扩展,降低了消息队列阻塞的风险,以及单个消费者产生单点故障的可能性(当然消息队列本身也可以做成分布式集群)。
可靠性
消息队列一般会把接收到的消息存储到本地硬盘上(当消息被处理完之后,存储信息根据不同的消息队列实现,有可能将其删除),这样即使应用挂掉或者消息队列本身挂掉,消息也能够重新加载。
那么消息队列又有哪些呢?
celery,rabbitMQ,redis的list,queue等,这里我们来通过 queue 来了解 Python 的消息队列
二、线程队列
现在假设你的程序中有多个线程,你需要在这些线程之间安全地交换信息或数据,那么可以用threading+Queue 实现线程队列。
从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 queue 对象,这些线程通过使用 put() 和 get() 操作来向队列中添加或者删除元素。 例如:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Time : 2018/5/24 20:49
# @Author : zhouyuyao
# @File : demon1.py
import time
import queue
import threading
def worker(i):
while True:
item = q.get()
if item is None:
print("线程%s发现了一个None,可以休息了^-^" % i)
break
# do_work(item)做具体的工作
time.sleep(0.5)
print("线程%s将任务<%s>完成了!" % (i, item))
# 做完后发出任务完成信号,然后继续下一个任务
q.task_done()
if __name__ == '__main__':
num_of_threads = 5
source = [i for i in range(1, 21)] # 模拟20个任务
# 创建一个FIFO队列对象,不设置上限
q = queue.Queue()
# 创建一个线程池
threads = []
# 创建指定个数的工作线程,并讲他们放到线程池threads中
for i in range(1, num_of_threads + 1):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
# 将任务源里的任务逐个放入队列
for item in source:
time.sleep(0.5) # 每隔0.5秒发布一个新任务
q.put(item)
# 阻塞队列直到队列里的任务都完成了
q.join()
print("-----工作都完成了-----")
# 停止工作线程
for i in range(num_of_threads):
q.put(None)
for t in threads:
t.join()
print(threads)
运行结果如下,值得注意的是,每次运行的结果可能不一样
线程1将任务<1>完成了!
线程2将任务<2>完成了!
线程3将任务<3>完成了!
线程4将任务<4>完成了!
线程5将任务<5>完成了!
线程5将任务<6>完成了!
线程2将任务<7>完成了!
线程3将任务<8>完成了!
线程3将任务<9>完成了!
线程1将任务<10>完成了!
线程5将任务<11>完成了!
线程2将任务<12>完成了!
线程4将任务<13>完成了!
线程3将任务<14>完成了!
线程1将任务<15>完成了!
线程5将任务<16>完成了!
线程2将任务<17>完成了!
线程4将任务<18>完成了!
线程3将任务<19>完成了!
线程1将任务<20>完成了!
-----工作都完成了-----
线程2发现了一个None,可以休息了^-^
线程4发现了一个None,可以休息了^-^
线程1发现了一个None,可以休息了^-^
线程3发现了一个None,可以休息了^-^
线程5发现了一个None,可以休息了^-^
[<Thread(Thread-1, stopped 2476)>, <Thread(Thread-2, stopped 3384)>, <Thread(Thread-3, stopped 6392)>, <Thread(Thread-4, stopped 12408)>, <Thread(Thread-5, stopped 16996)>]
三、Celery:任务调度利器
Celery 是 Python 开发的分布式任务调度模块。
Celery 本身不含消息服务,它使用第三方消息服务来传递任务,目前,Celery 支持的消息服务有RabbitMQ、Redis 甚至是数据库,当然 Redis 应该是最佳选择。
3.1 安装 Celery
可以使用 pip 或 easy_install 安装 celery 和 redis
[root@w test]# pip install celery
Collecting celery
Downloading http://mirrors.aliyun.com/pypi/packages/99/fa/4049b26bfe71992ecf979acd39b87e55b493608613054089d975418015b7/celery-4.1.1-py2.py3-none-any.whl (394kB)
100% |████████████████████████████████| 399kB 22.5MB/s
Collecting kombu<5.0,>=4.2.0 (from celery)
Downloading http://mirrors.aliyun.com/pypi/packages/2f/0d/416d396ee75b7e47e620005ef81a5b8a36c53c85bb148bd82459e8e61b6b/kombu-4.2.0-py2.py3-none-any.whl (853kB)
100% |████████████████████████████████| 860kB 36.9MB/s
Requirement already satisfied: pytz>dev in /usr/lib/python2.7/site-packages (from celery)
Collecting billiard<3.6.0,>=3.5.0.2 (from celery)
Downloading http://mirrors.aliyun.com/pypi/packages/39/ac/f5571210cca2e4f4532e38aaff242f26c8654c5e2436bee966c230647ccc/billiard-3.5.0.3.tar.gz (149kB)
100% |████████████████████████████████| 153kB 40.5MB/s
Collecting amqp<3.0,>=2.1.4 (from kombu<5.0,>=4.2.0->celery)
Downloading http://mirrors.aliyun.com/pypi/packages/88/4a/8c45a882d842678963516ebd9cf584a4ded51af719234c3b696c2e884c60/amqp-2.2.2-py2.py3-none-any.whl (48kB)
100% |████████████████████████████████| 51kB 38.3MB/s
Collecting vine>=1.1.3 (from amqp<3.0,>=2.1.4->kombu<5.0,>=4.2.0->celery)
Downloading http://mirrors.aliyun.com/pypi/packages/10/50/5b1ebe42843c19f35edb15022ecae339fbec6db5b241a7a13c924dabf2a3/vine-1.1.4-py2.py3-none-any.whl
Installing collected packages: vine, amqp, kombu, billiard, celery
Running setup.py install for billiard ... done
Successfully installed amqp-2.2.2 billiard-3.5.0.3 celery-4.1.1 kombu-4.2.0 vine-1.1.4
You are using pip version 9.0.1, however version 10.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
[root@w test]# pip install redis
Collecting redis
Downloading http://mirrors.aliyun.com/pypi/packages/3b/f6/7a76333cf0b9251ecf49efff635015171843d9b977e4ffcf59f9c4428052/redis-2.10.6-py2.py3-none-any.whl (64kB)
100% |████████████████████████████████| 71kB 1.3MB/s
Installing collected packages: redis
Successfully installed redis-2.10.6
You are using pip version 9.0.1, however version 10.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
如果需要在PyCharm上安装的话,如下所示
Celery 的安装相较还算顺利,前面 queue 总是安装不上,后来发现 queue 本身是 Python 自带的,无需安装。(尴尬脸)
使用 Redis 作为 Broker 时,再安装一个 celery-with-redis。
确保 redis 能够使用的情况下,下面我们就可以开始 celery 操作。
3.2 Celery异步执行定时任务
1)创建一个文件
[root@w test]# cat tasks.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# @Time : 2018/5/25 23:25
# @Author : zhouyuyao
# @File : demon2.py
# tasks.py
import time
from celery import Celery
from celery.schedules import crontab
app = Celery('tasks', broker='redis://xxx.xxx.xxx.xxx:11111/0')
@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
# Calls test('hello') every 10 seconds.
sender.add_periodic_task(10.0, test.s('hello'), name='add every 10')
# Calls test('world') every 30 seconds
sender.add_periodic_task(30.0, test.s('world'), expires=10)
# Executes every Monday morning at 7:30 a.m.
sender.add_periodic_task(
crontab(hour=7, minute=30, day_of_week=1),
test.s('Happy Mondays!'),
)
@app.task
def test(arg):
print(arg)
[root@w test]#
2)启动celery
进入你tasks.py所在目录,执行:
[root@w test]# celery -A tasks worker --loglevel=info
运行之后窗口显示如下
[root@w test]# celery -A tasks worker --loglevel=info
/usr/lib/python2.7/site-packages/celery/platforms.py:796: RuntimeWarning: You're running the worker with superuser privileges: this is
absolutely not recommended!
Please specify a different user using the -u option.
User information: uid=0 euid=0 gid=0 egid=0
uid=uid, euid=euid, gid=gid, egid=egid,
-------------- celery@wx240-2.4 v4.1.1 (latentcall)
---- **** -----
--- * *** * -- Linux-3.10.0-327.36.3.el7.x86_64-x86_64-with-centos-7.2.1511-Core 2018-05-25 23:49:42
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: tasks:0x133f110
- ** ---------- .> transport: redis://xxx.xxx.xxx.xxx:11111/0
- ** ---------- .> results: disabled://
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. tasks.test
[2018-05-25 23:49:42,657: INFO/MainProcess] Connected to redis://xxx.xxx.xxx.xxx11111/0
[2018-05-25 23:49:42,665: INFO/MainProcess] mingle: searching for neighbors
[2018-05-25 23:49:43,682: INFO/MainProcess] mingle: all alone
[2018-05-25 23:49:43,692: INFO/MainProcess] celery@wx240-2.4 ready.
此时,异步服务已经可用了,但定时任务并没有执行,怎么回事?你还需要再启动另一服务,如下
3)在新窗口中启动beat
还是要进入你tasks.py所在目录,执行:
[root@w test]# celery -A tasks beat
启动后窗口显示如下
[root@w test]# celery -A tasks beat
celery beat v4.1.1 (latentcall) is starting.
__ - ... __ - _
LocalTime -> 2018-05-25 23:41:20
Configuration ->
. broker -> redis://xxx.xxx.xxx.xxx:11111/0
. loader -> celery.loaders.app.AppLoader
. scheduler -> celery.beat.PersistentScheduler
. db -> celerybeat-schedule
. logfile -> [stderr]@%WARNING
. maxinterval -> 5.00 minutes (300s)
4)查看结果
看一下启动 celery的窗口,是不是有打印输出?good!每隔10秒打印一次hello,每隔30秒打印一次world!
[2018-05-25 23:53:40,372: INFO/ForkPoolWorker-3] Task tasks.test[6d81ae12-d97e-4374-a4fe-23d22db95f31] succeeded in 0.000573102384806s: None
[2018-05-25 23:53:50,370: INFO/MainProcess] Received task: tasks.test[a1fb5d59-2c6a-417a-93ab-e9785b21553d]
[2018-05-25 23:53:50,372: INFO/MainProcess] Received task: tasks.test[bb417166-abab-420a-a439-f7c90d067eed] expires:[2018-05-25 15:54:00.369650+00:00]
[2018-05-25 23:53:50,373: WARNING/ForkPoolWorker-1] world
[2018-05-25 23:53:50,373: WARNING/ForkPoolWorker-4] hello
[2018-05-25 23:53:50,374: INFO/ForkPoolWorker-1] Task tasks.test[bb417166-abab-420a-a439-f7c90d067eed] succeeded in 0.000634584575891s: None
[2018-05-25 23:53:50,374: INFO/ForkPoolWorker-4] Task tasks.test[a1fb5d59-2c6a-417a-93ab-e9785b21553d] succeeded in 0.000735998153687s: None
[2018-05-25 23:54:00,370: INFO/MainProcess] Received task: tasks.test[0a864649-4f5f-4f52-a2ed-fbbaf7b21046]
[2018-05-25 23:54:00,371: WARNING/ForkPoolWorker-2] hello
[2018-05-25 23:54:00,372: INFO/ForkPoolWorker-2] Task tasks.test[0a864649-4f5f-4f52-a2ed-fbbaf7b21046] succeeded in 0.000487040728331s: None
[2018-05-25 23:54:10,370: INFO/MainProcess] Received task: tasks.test[ec0b0db3-ea44-4ba4-9eba-0e65a0dc8766]
[2018-05-25 23:54:10,372: WARNING/ForkPoolWorker-1] hello
[2018-05-25 23:54:10,372: INFO/ForkPoolWorker-1] Task tasks.test[ec0b0db3-ea44-4ba4-9eba-0e65a0dc8766] succeeded in 0.000682167708874s: None
[2018-05-25 23:54:20,373: INFO/MainProcess] Received task: tasks.test[7cf8ff93-a4d9-4591-855f-133a161589b1] expires:[2018-05-25 15:54:30.370264+00:00]
[2018-05-25 23:54:20,375: INFO/MainProcess] Received task: tasks.test[5e2b7d15-f558-4331-b8c7-c85e3aadbf94]
[2018-05-25 23:54:20,376: WARNING/ForkPoolWorker-2] world
[2018-05-25 23:54:20,376: WARNING/ForkPoolWorker-4] hello
[2018-05-25 23:54:20,376: INFO/ForkPoolWorker-2] Task tasks.test[7cf8ff93-a4d9-4591-855f-133a161589b1] succeeded in 0.00051337480545s: None
[2018-05-25 23:54:20,376: INFO/ForkPoolWorker-4] Task tasks.test[5e2b7d15-f558-4331-b8c7-c85e3aadbf94] succeeded in 0.000639263540506s: None
[2018-05-25 23:54:30,375: INFO/MainProcess] Received task: tasks.test[13eab9ac-5174-4b28-acc2-014c53217108]
[2018-05-25 23:54:30,377: WARNING/ForkPoolWorker-3] hello
[2018-05-25 23:54:30,377: INFO/ForkPoolWorker-3] Task tasks.test[13eab9ac-5174-4b28-acc2-014c53217108] succeeded in 0.000575877726078s: None
[2018-05-25 23:54:40,377: INFO/MainProcess] Received task: tasks.test[6a14bb2d-40ab-431c-9a9f-e6edf93cb45d]
[2018-05-25 23:54:40,378: WARNING/ForkPoolWorker-4] hello
[2018-05-25 23:54:40,379: INFO/ForkPoolWorker-4] Task tasks.test[6a14bb2d-40ab-431c-9a9f-e6edf93cb45d] succeeded in 0.000566378235817s: None
[2018-05-25 23:54:50,376: INFO/MainProcess] Received task: tasks.test[c50fe5f6-4dde-4cbd-8d0b-37d06010f611]
[2018-05-25 23:54:50,379: INFO/MainProcess] Received task: tasks.test[73b79446-a785-4410-982e-0a0870c02e8b] expires:[2018-05-25 15:55:00.375598+00:00]
[2018-05-25 23:54:50,380: WARNING/ForkPoolWorker-3] hello
[2018-05-25 23:54:50,380: WARNING/ForkPoolWorker-2] world
[2018-05-25 23:54:50,380: INFO/ForkPoolWorker-3] Task tasks.test[c50fe5f6-4dde-4cbd-8d0b-37d06010f611] succeeded in 0.00058351457119s: None
[2018-05-25 23:54:50,381: INFO/ForkPoolWorker-2] Task tasks.test[73b79446-a785-4410-982e-0a0870c02e8b] succeeded in 0.000634059309959s: None
[2018-05-25 23:55:00,378: INFO/MainProcess] Received task: tasks.test[c71e464a-4f3a-4427-9a0f-fbca482c542e]
[2018-05-25 23:55:00,380: WARNING/ForkPoolWorker-1] hello
[2018-05-25 23:55:00,380: INFO/ForkPoolWorker-1] Task tasks.test[c71e464a-4f3a-4427-9a0f-fbca482c542e] succeeded in 0.000701323151588s: None
参考资料
1. https://github.com/jasonGeng88/blog 一个故事告诉你什么是消息队列
2. http://www.jb51.net/article/87629.htm Python中线程的MQ消息队列实现以及消息队列的优点解析
3.http://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p03_communicating_between_threads.html
4. https://www.cnblogs.com/Fordestiny/p/8901100.html 解决PyCharm安装queue出现module pip has no attribute 'main' ( 注意在修改的时候一定要确保4个空格换行,不要使用tab )
5. http://www.liujiangblog.com/course/python/59
6. http://python.jobbole.com/87238/ 分布式队列神器 Celery
7. https://www.liaoxuefeng.com/article/00137760323922531a8582c08814fb09e9930cede45e3cc000
8. https://blog.csdn.net/apple9005/article/details/54430104 Celery+python+redis异步执行定时任务