ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 队列的思考 ### 队列的本质 单纯说队列也可代指一种[数据接口](https://baike.baidu.com/item/%E9%98%9F%E5%88%97/14580481?fr=aladdin)。 队列MQ(Message Queue/消息队列)不是指某一项技术,而是项目中常用的解耦解决方案,或者说是一种中间件平台。 我们通常所说的队列(服务)一般都是指消息队列。 > 异步是一种编程模型,MQ是一种异步实现方式。队列实际上也是一种异步模型的应用。 **消息队列的组成:** - 数据结构(存/取任务:任务信息/任务参数payload) - 消息发布(任何客户端都可以往数据结构中新增消息) - 消息消费(需要worker来消费消息,即执行任务) > 所以现在理解了吧,队列并不是某一技术,而是多种技术的组合形成的解决方案。 异步是一种编程模型,队列是其实现。 (有点类似于[订阅/发布](http://mp.weixin.qq.com/s?__biz=MzA5NzkwNDk3MQ==&mid=2650585339&amp;idx=1&amp;sn=d3276dcd3637b6e9ed5491ce3c225214&source=41#wechat_redirect)) * * * * * ### 队列即服务 队列的本质就是服务/任务。任务执行需要payload(载荷/参数)。 既然是服务,那就是通用的,所以它即既可加入队列中执行,也可以直接执行。 ``` if ($if_quque) { QueueClient::push('cancelOrderUpdateStorage', $data); } else { Logic('queue')->cancelOrderUpdateStorage($data); } ``` 服务/任务可以被直接调用,也可在队列中调用。不论怎么调用,任务都是通用的。 ~~~ shopnc把队列的 【具体任务的业务实现】 全部都放在 逻辑文件中了。 $logic_queue = Logic('queue'); ... while (true) { $result = $logic_queue->$method($arg); } 但是这样有个问题:逻辑文件是在守护进程中载入内存了,如果后期有新的队列任务,需要修改逻辑文件,那么就要停止守护进程,重新运行服务才能生效了。 ~~~ * * * * * ### 队列要注意不能忽视的一个问题:worker如何执行 这里面有一个很重要的问题,又容易被忽视的问题就是,**worker如何执行。** **任务其实就是业务逻辑。** >[danger] 通常我们的业务逻辑代码是基于系统代码和框架环境(依赖于工具类/函数/应用配置/系统配置等等)的,所以队列的业务逻辑也要能保证在一样的环境下执行,否则就很麻烦,不统一会造成逻辑侵入严重。 那么问题来了: 如果还以传统的方式,worker是个无限循环,如果每次还从框架的入口进去: ```php > php index.php/queue/task/sendmail ``` **那么每次都要加载初始化框架,这将是很大的开销**(这用的还是访问式web的url模式)。 所以队列的运行就不能照搬**访问式web运行方式**了。 我们希望只需要一次加载系统框架环境就可以,此后就直接循环执行task就可以,而无需每次初始化环境,**这种方式就有点类似于Swoole的运行方式了——长生命周期(逻辑内存常驻)**。 其实,成熟的框架已经想好了这个问题,都支持控制台模式,url访问模式和控制台模式能能够引入框架环境。 可参考:think-queue、shopnc的queue 之前的记录: ```text **有两种观点:** 1. 队列处理程序为了性能有时候没必要完全引入整个MVC 2. 如果不引入MVC的话,那就相对独立了,系统配置什么的,环境,自动加载,数据模型等,这就很不方便了。 当然如果框架系统考虑到了队列,CLI那么提供一种CLI模式运行环境,那也是极好的。 另外需要注意的一个问题就是,假设a程序是处理程序,那么现在每个几秒钟去运行一次a还是运行a,在里面死循环比较好。**(这个和php的运行模式和生命周期有关,例子是Laravel和Swoole,下面有探讨。)** 如果每隔几秒去运行a,那就相当于另外的程序去主动调用a了(命令行/自动任务等),那么就要注意不能重复调用a,不能多个其他程序都去调用了a,这可以做一个文件锁来保证a同一时间内只能被调用一次,也就是不允许并发执行。死循环的话,一个脚本太长时间执行,担心影响性能,出现内存泄露,并且死循环不能是“死的”,需要提供可以控制的能力,可以停止执行,这可以用文件锁/配置/信号处理之类的来做到。不能强行的对处理程序做“热插拔”,否则如果再处理重要任务时异常中断就可能会出现意外。 还要有监控的能力,知道程序是否执行了,最后的执行时间,执行日志等。 所以两种方法各有优点和缺点,如果取其中间,调用&程序内循环,增加调用间隔,循环增加限制(可以是执行时间,也可以是循环次数),超过自动退出。 记住任何时候,最好的就是最合适的。 ``` ### 队列消费的顺序 有时,队列消息的消费顺序也至关重要,必须满足先进先出,即先入的队列要先被消费。 比如两条消息,存款和取款,正常顺序,先存款后是可以成功完成取款的,但是如果取款消息被先消费了,那么就会造成取款失败。这样来看的话,多工人抢占消息,多进程错开取消息,是不行的!因为不能保证消费顺序与入列顺序的同步。那么在这种对 队列消息消费顺序 有要求的情况的任务,只能单进程一条一条的取消息,串行、阻塞执行消息任务了。 * * * * * ### 队列:延伸思考——为什么要有队列 如果计算机是神,或许根本不需要异步这东西。没有任何东西是不需要,而我们非要平白无故的造出来的。 #### 队列不是神,而是农民 等待还是要等待的,不可能不等待的,**计算机又不是神,该耗的时还是要耗**(只不过放在另外一个地方去耗,不用你当前一直干等着而已),不可能一秒钟做完所有的事情,不可能无条件的满足你,只是告诉你操作放到队列中排队去做了(交给另外一个人去做),当前马上给你返回,不阻塞你,不用让你当前一直等着而不能干别的。 为了用户体验和系统效率,只能选择异步的方式。 #### 队列的组成: 1. 工人(执行者) 2. 任务(消费者) 3. 消息(生产—消费媒介) 客户端(生产者) #### 消息的安全性/队列的可靠性 >[danger] 队列是一种中间件服务,属于外部系统,它本身的作用是为了[解耦](https://www.zhihu.com/question/20278169)(让合适的人干合适的事情,并且相互之间没有依赖)。**既然属于外部系统,那么业务逻辑就不能依赖于队列服务,所以消息重发、消息丢失、并发取消息、防重复消费等问题都需要考虑到,无论任何情况下都要确保业务逻辑的正确性。** 消息放在Redis里面,如果服务器故障,消息全部丢失怎么办,不能出现取款永远不能到账吧,不能因为队列的稳定性和故障,导致业务出错,甚至出现BUG吧,所以在设计任何一条队列消息和任务时时,都要考虑容错性,比如要任务处理时必须要严格校验到账情况,有其他的业务记录表,消息表等,**要用锁,消息系统是不可靠的,即使出现打款消息重发,也不能导致系统多次打款,即要保证业务的幂等性**,同时在消息无法到达时,比如迟迟24小时还没到账,有可能是队列丢消息了,那么系统要检查出来,重发消息。这可以用计划任务做,每隔24小时检查一次,扫出超过24小时没有到账的订单,看看是否是出现丢消息了。 队列还要考虑一种情况,消息取了,但是任务进程却被杀死了,那么也会出现消息假消费的问题,还有任何时候,任何程序执行到任何地方都有可能失败,断电啊,地震啊,进程意外被杀死啊,硬盘报废啊,硬件不可逆损坏啊,……。 所以程序任何时候,任何部位都要考虑这些意外,都要做好容错。 * * * * * ### 安全的守护进程实践 #### 运行状态文件 守护程序状态可以用一个文件来控制表示,进程的生命周期中共有三种状态: 1. name-runing 2. name-end 3. name-ending 操作只有两种:启动进程 和 停止进程。 只有 `无状态文件` 和 `name-end` 时可以启动进程(已停止 时可以启动);只有 `name-runing` 时可以停止进程。 #### 确保安全,无并发问题:锁 **程序运行过程中,会一直锁住当前的状态文件。** 这样就能防止重复执行程序,确保没有并发问题,保证其安全性。 锁住当前状态文件,除了保证程序的安全性,还能确保用户不能手动误删除状态文件,保护程序不会受外部影响,保证其可用性。 #### 怎样判断程序的运行状态? **运行中:** `name-runing` **没有运行:** 停止中:`name-ending` 已停止:`name-end` 或 `没有任何状态文件`(停止后,由于没有锁,状态文件可能被用户删除) >[danger] 计划任务,比如每隔一分钟,扫描表,将没有IP位置的字段全部更新(IP \> IP城市),如果检测上次的任务进程还在执行,也就是还在岗,那么本次放弃执行,否则就会出现前一分钟的进程和当前进程一起执行,造成重复并发问题而重复消费了,所以每个进程开始前都需要检查状态。其实就相当于计划任务每次检测当前有无执行,有则不管了,没有则启动一个,相当于是一个唤醒,睡着的就把你唤醒,醒着的就不必唤了,**所以计划任务其实是一个唤醒任务。** > 这个不用文件记录状态,用MySQL表也可以,但是要用Inndb行锁,任务名字段唯一索引。 > > 后台进程表(id,任务名,进程ID:每次可能都不一样,本次执行之间,本次结束时间,状态,启动命令) #### 运行日志 出了程序的运行状态文件,我们还可以增加一个程序运行日志文件,记录程序生命周期的运行动作: ```log 2018-4-30 12:00:37 start 2018-4-30 12:01:59 runing 2018-4-30 12:02:27 ending 2018-4-30 12:02:34 end ``` (运行日志只记录程序的生命中期中的运行状态等动作,不记录程序本身日志信息) * * * * * ### 再谈阻塞 ![](http://cdn.aipin100.cn/18-5-1/11923029.jpg) [Go并发编程案例解析](http://www.imooc.com/learn/982) 虽然最后还是阻塞的,总的执行时间无论怎样都是一样的,只是采用这样非阻塞的方式运行可以异步的部分,在一定意义上达到了控制程序执行顺序的目的了,如提前返回部分数据给用户,使其尽早呈现结果,异步使得程序更加灵活,提高了程序整体的运行效率,虽然总的执行时间都是一样的。 说错了,串行的话总时间都是一样的,真正异步并行执行的话(同时做多件事),执行时间会少。 * * * * * ### 扩展 [队列 · php笔记 · 看云](https://www.kancloud.cn/xiak/php-node/347805) [RabbitMQ, ZeroMQ, Kafka 是一个层级的东西吗, 相互之间有哪些优缺点? - 知乎](https://www.zhihu.com/question/22480085/answer/23106407) [深入分析消息中间件的选型(AMQ,RMQ,Kafka,KMQ,ZMQ等)](https://www.toutiao.com/a6540024140257034759/?tt_from=weixin&utm_campaign=client_share&timestamp=1522769518&app=news_article_lite&utm_source=weixin&iid=25315997380&utm_medium=toutiao_android&wxshare_count=1) > 并且消息中间件服务器(一般简单的称之为Broker)中没有消息堆积,……,消息中间件大道至简:一发一存一消费,没有最好的消息中间件,只有最合适的消息中间件。 [【系统架构】Web系统大规模并发:电商秒杀与抢购](http://mp.weixin.qq.com/s/zDbcV_vJeBOnAYxK0WEJQQ) > 必须尽可能“快”,在最短的时间里返回用户的请求结果。为了实现尽可能快这一点,接口的后端存储使用内存级别的操作会更好一点。仍然直接面向 MySQL之类的存储是不合适的,如果有这种复杂业务的需求,都建议采用异步写入。 > > 当然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才可以从页面中看到用户是否秒杀成功。但是,这种属于“偷懒”行为,同时给用户的体验也不好,容易被用户认为是“暗箱操作”。(这个没有办法,不异步你就干等啊,异步就是告诉你正在排队啊) > > 更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩” > > 我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一 样的,结果和预期相同,就是线程安全的)。(即没有并发问题,也可客观的理解为操作的幂等性) > > FIFO队列:这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点**强行将多线程变成单线程**的感觉哈。(解决并发问题,锁,队列异步操作,其实都是将并行强制变为串行的解决方案。) [【系统架构】聊聊开源消息中间件的架构和原理](https://mp.weixin.qq.com/s/NwjYJde9_TC4PXMPpYw1Gw) [RocketMQ 源码合集](https://mp.weixin.qq.com/s/xrnEMQ07kuaE9oCK1Z71hg) [消息队列应用场景 - 13070113 - 博客园](http://www.cnblogs.com/stopfalling/p/5375492.html) > 按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。**注册邮件,发送短信写入消息队列后,直接返回,<span style="color:red;">因为写入消息队列的速度很快,基本可以忽略</span>**,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。 [消息队列使用的四种场景 - CSDN博客](https://blog.csdn.net/ntotl/article/details/72765713)(原文) [【原创】分布式之延时任务方案解析 - 孤独烟 - 博客园](https://www.cnblogs.com/rjzheng/p/8972725.html) [为什么分布式一定要有redis?](https://mp.weixin.qq.com/s/gEU8HtsQNPXY8bzkK-Qllg) > 一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾。 > 那么,经过周密的计算,一瞬间为0.36 秒,一刹那有 0.018 秒.一弹指长达 7.2 秒。 >[danger] **任何东西都要去深入思考,做到面面俱到,考虑每一种可能性,应对每一种突发情况,这样才能保证是严谨的。** 避重就轻,拈轻怕重,避实就虚,怕麻烦,不肯深入问题的本质,这样永远都只会停留在表面,无法解决核心问题。 [【原创】分布式之消息队列复习精讲 - 孤独烟 - 博客园](http://www.cnblogs.com/rjzheng/p/8994962.html) ~~~ 1:为什么使用消息队列? 2:使用消息队列有什么缺点? 3:消息队列如何选型? 4:如何保证消息队列是高可用的? 5:**如何保证消息不被重复消费?** 6:如何保证消费的可靠性传输? 7:如何保证消息的顺序性? ~~~ >[danger] 如何保证不会出现重复消费消息? > > 我们不要总是将目光聚集在**如何防止重复取消息**的问题上,虽然这是造成重复消费问题的根源(其实这只是会造成重复消费的原因之一,还有 消费确认失效 等情况也可能会造成这个问题),但是解决重复消费的问题并不是只有这一个办法,不要紧盯目标,而忘记自己本来是要干什么了,被自己狭窄的眼界给局限了。**不能死板,不能墨守成规,转变思路,侧面突围,用最令人意想不到的方式解决问题才更漂亮。** 多给自己提问,当你面对这个问题时,你实际是在面对什么,当你在解决某个问题时,你实际是在解决什么。 > > 将目光从如何防止重复取消息上移开,还有很多解决这个问题的办法:1. 增加消费记录表,记录消费的情况;2. 利用业务数据自己比对消费记录;3. 消息ID为主键,做insert;**其实方案都是消费时自己再确认一遍,这也符合了正确性从不依赖于外部或其他系统的原则(比如不依赖,不信任取消息操作是否会重复取),不过很重要的一点是不能忘记锁,查询时一定要互斥锁,不然还是于事无补,因为会出现并发问题。** [【系统架构】分布式之消息队列复习精讲(上)](https://mp.weixin.qq.com/s/uRaG2ZB8hBoxum73OHDHNQ) [从单一架构到分布式交易架构,网易严选的成功实践](https://mp.weixin.qq.com/s/nv3Ht7OqTYQw31QFDX3gNg) >[danger] **没有完美的架构设计,世上也没有绝对的事情,没有谁能保证绝对可靠、安全和高可用,但我们有补偿和容错(类似还有重试,确认等机制),也是能做到万无一失的。** [【消息队列 MQ 专栏】消息队列之 ActiveMQ](https://mp.weixin.qq.com/s/ngfCCsuYJHBc6gRTIROgMQ) [分布式消息队列 RocketMQ 源码分析 —— Message 拉取与消费(上)](https://mp.weixin.qq.com/s/EwZDg5IRHJ5TGfGvpfl9Ew) [数据结构 | Java 队列 —— Queue 详细分析](https://mp.weixin.qq.com/s/FH5lQac65CT9Pt_TpT1E7Q) mq和数据库区别主要是是否解决了通知问题 * * * * * 关键字:线程安全 * * * * * ### Lua:代码级的原子性 [Lua 是一个小巧的脚本语言 - HackerVirus - 博客园](https://www.cnblogs.com/Leo_wl/p/8405661.html) > lua脚本是用C语言写的,体积很小,运行速度很快,并且每次的执行都是作为一个原子事务来执行的 > > 原子性的操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。 [Lua 与 Redis - 挨踢人啊 - 博客园](https://www.cnblogs.com/itrena/p/5926878.html) [redis实现秒杀功能例子(采用lua的原子性保证数据的一致性) - CSDN博客](https://blog.csdn.net/futao127/article/details/80617214) [利用redis + lua解决抢红包高并发的问题 - CSDN博客](https://blog.csdn.net/hengyunabc/article/details/19433779/) * * * * * ### 定时计划:后知后觉 图 ![](http://cdn.aipin100.cn/18-6-15/11370565.jpg) * * * * * last update:2018-1-16 14:10:56