多任务并发之生产者消费者模式应用

简介

生产者-消费者模式大家都很熟悉,生产者负责生产数据,并存放到队列中,消费者负责从队列中取出数据来消费。可以看出生产者和消费者之间不直接通讯,是通过队列来通讯的。
生产者和消费者是抽象的概念,可以是线程、进程、系统模块,而队列也可以是JVM中的Queue、Redis中的List、甚至是数据库表,这要求我们在不同的使用场景需要选择相应的实现。比如在线程池ThreadPoolExecutor的使用场景中,生产者就是提交任务的线程,消费者就是消费任务的线程,当然还有一个存放任务的阻塞队列。

常用的几种生产者-消费者模式如下图所示:

(经典版)


(链式版本)


(多个消费者版本,多个是指多个不同的实现)

实际上当然不止这几个版本,只要理解了生产者-消费者模式的关键要素是:生存者、消费者、数据队列,那就可以根据需求场景来组合。我个人觉得链式版本是比较有意思的,它实际上包含着拆分大任务成子任务的思想。另外,我还遇到过生产者和消费者的并发数都是1的经典版,这种情况到底有没有必要使用生产者-消费者模式,看看它本身所具备的特性你就知道了。

特性

关于生产者-消费者模式,我总结出一些特性及说明:
(1)解耦
生产者和消费者之间解耦,可以理解为生产者和消费者都不用关心对方的实现逻辑,只需约定好队列中数据的格式即可。这在不同功能模块之间的协作非常有用,能省去很多沟通成本,还利于模块的单元测试,属于懒人必备。

(2)并发
可以根据使用场景来调整生产者和消费者的并发数,给予慢的一方更多的并发数,来达到所期望的任务处理速度。由于在大多数情况下很难确定到底分别配置多少并发合适,就算配置好了,也会因为一些突发事件如节日访问量暴增而需要更改,因此强烈推荐将设置并发数实现为可动态配置的。

(3)异步
生产者和消费者之间不必同步。如生产者只需将数据放入队列即可,不必等待消费者处理完。通常需要异步的场景都是为了支持高并发,比如将一个耗时的流程拆分为生产和消费两个阶段,并将耗时处理交给消费者的实现,这样生产者因为处理时间短而支持高并发,比如在12306的抢票模块。

(4)分布式
由于生产者和消费者是通过队列来通讯的,因此它们不必运行在同一台机器中。在分布式环境中,往往会使用Redis的List,或数据库表来作为队列,而消费者只需轮询队列来查看是否有数据即可。如果有良好的设计,还能支持集群的伸缩性。比如集群中不会因为某台消费者机器宕掉而导致集群宕掉,也不会因为要增加一台消费者机器而需要重启整个集群。

其实以上特性只是我的个人总结,特性与特性之间也没有严格区分,有重叠的地方。我经常会看到某些场景的需求,其实只满足了其中几点特性,比如需要分布式,也选择了使用生产者-消费者模式来实现。

任务调度模块的设计


任务调度模块主要是根据条件触发、或定时的方式来启动线程处理一些任务。如果是简单类型那只需启动一个线程来处理就可以了,但要是那种处理逻辑复杂、耗时长、步骤很多的任务呢?那就要好好思考一下如何设计与开发:
  1. 从开发、测试、监控、运维的角度出发,都有必要将大任务拆分成子任务
  2. 任务的调度过程是可监控的
  3. 任务调度的每个阶段都要求能进行人工干预,比如暂停处理,人工触发启动,失败后重跑等
明白了以上需求,我就明白了为什么我们开发的任务调度系统很多地方都使用了生产者-消费者模式,因为它简单且能满足这些需求。还记得链式版本的生产者-消费者模式吗?假设我们通过该模式将一个复杂任务拆分成了3个步骤,整体设计下图所示:



我们看看这个设计是如果满足上面所提到的需求:
  1. 大任务拆分成的3个子任务,相当于将一个很复杂的流程拆成了3个处理阶段,每个阶段尽量只处理简单的逻辑,代码实现起来就会变得简单
  2. 每个子任务完成后,将数据保存到队列中,同时更新数据库中的状态。我们可以通过监控队列和数据库可以很方便的知道任务的运行情况
  3. 可以很方便地对每个子任务的运行进行人工干预:
    • 如果要暂停子任务2,可以让子任务1停止往队列A添加数据,或让子任务2停止从队列A拿数据
    • 如果要人工重跑子任务2,只需将数据放到队列中即可,然后等待子任务2消费
    • 如果通过监控发现任务3消费得慢,成为性能瓶颈,增加子任务3并发即可



常见问题及解决


怎么实现单实例运行

在集群多实例部署的场景下需要解决一些问题,比如怎么实现只存在一个生产者或消费者,也就是说怎么单机单线程运行?
单线程很简单,主要是怎么实现单机。简单的实现,可以根据IP或hostname来判断该线程要不要运行,但这样会存在单点故障问题。通用的做法的是在集群中选举出一个主,只有主才能运行,选举主的机制为通过争夺锁的方式来进行。分布式锁通常通过redis、zookeeper、数据库来实现。

怎么监控任务

这个问题其实空泛,而且和生产者消费者模式关系不大,只能说生产者消费者这种模式提供了很多最佳实现。比如将大任务拆分成多个子任务,通常用一个状态表示程序现在跑到第几个子任务,一个状态表示当前任务状态(初始?正在运行?中断?异常?结束?),这些都是粗粒度的信息。
更详细的信息,且频繁更新的信息,一般是不会持久化到数据库的。可以选择 打日志,比如我想监控一个变量的值,可以起一个定时线程来打日志输出这个变量的值,这也是很常用的。但这种方式只能监控本Java进程的信息,对于分布式的应用的总体监控就不行。
借助分布式缓存Redis,集群中的每个实例都可以将监控信息接入Redis中。可以把Redis提供的Hash类型当作一张表,那对应关系如下:
Redis的Hash数据库表
key表主键
field表字段
value表字段值

这里比较有意思的是key的命名规则,比如某个任务中的监控信息,那这个hash的key的命名可以是 taskId + hostname
并且我还会对这个hash设置一个存活时间,这样我就不必清理这些监控信息!!



参考



  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值