慢生产者快消费者场景下BlockingQueue无超时无魔法标识的优雅终止方案
业务背景
- 开发的Java命令行应用用于爬取网站并下载视频文件,文件大小覆盖几MB到20GB以上不等,下载耗时从几秒到数小时
- 初始采用生产者消费者模式处理流程:1个生产者线程爬取视频链接,封装为包含URL、存储路径等信息的对象后放入无界
BlockingQueue;N个消费者线程从队列取出对象,先检查本地是否已存在对应文件,存在则跳过下载,否则执行下载 - 初始错误处理逻辑:下载出现连接重置等错误时,将失败对象放入单独的失败队列,消费者休眠15分钟,活跃的生产者定期将失败队列的任务重新放回主队列
迭代过程中遇到的问题
初始设计缺陷
生产者完成爬取后不能直接退出,需要一直运行处理失败队列的重入,造成不必要的资源占用。
第一次优化后的新问题
去掉失败队列,让消费者直接将失败任务放回主队列,此时相当于有N+1个生产者,主生产者完成爬取后可以直接退出,但出现新问题:
BlockingQueue没有内置机制通知消费者不会再有新任务入队,无法判断何时可以安全退出。
曾尝试的方案:让消费者带超时轮询队列,同时主生产者退出时设置全局标记,消费者超时后检查标记,标记为真则退出。该方案的缺陷:需要用到魔法标记,破坏了生产者消费者仅通过队列交互的原则,且线程空闲等待的设计不够优雅。
第二次优化的失败
放弃BlockingQueue改用非阻塞队列,搭配CyclicBarrier控制:消费者启动后先在屏障等待,生产者往队列放入10*N个任务后打开屏障让消费者开始消费。
方案缺陷:消费者消费速度远快于生产者时会完全失效——比如大量文件本地已存在,消费者很快消费完队列就直接退出,而此时生产者还在爬取新的链接。
核心需求
仍使用BlockingQueue实现,求不依赖超时、魔法标记的优雅退出方案。
最终可行方案
生产者完成全部爬取后,往队列中放入N个URL为null的终止对象,消费者取出对象后若判断URL为null则直接退出。
为了解决生产者退出后消费者放回的失败任务会排在终止对象之后的问题,改用PriorityBlockingQueue优先级阻塞队列,让入队对象实现Comparable接口,compareTo逻辑设定为URL为null的对象永远排在队列末尾,确保所有正常任务和失败重入任务都能在终止对象被消费前处理完毕。
内容的提问来源于stack exchange,提问作者Justin Kredible

