高并发系统之队列术

队列在数据结构中是一种线性表,从一端插入数据,然后从另一端删除数据。本文目的不是讲解各种队列算法,而是在应用层面使用队列能解决哪些场景问题。

 

在我开发过的系统中,不是所有的业务都必须实时处理、不是所有的请求都必须实时反馈结果给用户、不是所有的请求/处理都必须100%处理成功、不知道谁依赖“我”的处理结果、不关心其他系统如何处理后续业务、不需要强一致性,只需保证最终一致性即可、想要保证数据处理的有序性;此时你应该考虑使用队列来解决这些问题。在实际开发时我们经常使用队列进行异步处理、系统解耦、数据同步、流量削峰、缓冲、限流等。

 

异步处理:使用队列的一个主要原因是进行异步处理,比如缓存过期时先返回老的数据,然后异步更新缓存、异步写日志等;通过异步处理,可以提升主流程响应速度,而非主流程/非重要业务可以异步集中处理,这样还可以将任务聚合然后批量处理;因此可以使用消息队列/任务队列来进行异步处理。

 

系统解耦:比如用户成功支付完成订单后,需要通知生产配货系统、发票系统、库存系统、推荐系统、搜索系统、风控系统等进行业务处理;而未来需要添加/支持哪些业务是不清楚的,而且这些业务处理不需要实时处理、不需要强一致,只需要最终一致性即可,因此可以通过消息队列/任务队列进行系统解耦。

 

数据同步:比如想把Mysql变更的数据同步到Redis、或者将Mysql数据同步到Mongodb、或者机房间数据同步、或者主从数据同步等,此时可以考虑使用如databus、canal、otter。使用数据总线队列进行数据同步的好处是可以保证数据修改的有序性。下载

 

流量削峰:系统瓶颈一般在数据库上,比如扣减库存、下单等;此时可以考虑使用队列将变更请求暂时放入队列,通过缓存+队列暂存的方式将数据库流量削峰;还有如秒杀系统,下单服务会是该系统的瓶颈,此时会使用队列进行排队和限流,从而保护下单服务。通过队列暂存或者队列限流来削峰。

 

比如减库存,可以考虑这样设计:下载

高并发系统之队列术
 

直接在Redis中扣减,然后记录下扣减日志(FIFO队列),通过Worker去同步到DB。

 

实际队列的应用场景还是非常多的,本文列举了笔者遇到过比较多的场景。

 

典型的如Log4j的日志缓冲区,当我们使用log4j记录日志时,可以配置字节缓冲区,字节缓存区满时会立即同步到磁盘(flush操作)。Log4j使用BufferedWriter实现的;此模式不是异步写,在缓冲区满的时候还是会阻塞主线程。如果需要异步模式可以使用AsyncAppender,然后通过bufferSize控制日志事件缓冲区大小。

 

通过缓冲区队列可以实现:批量处理、异步处理。

 

使用任务队列将一些不需要与主线程同步执行的任务扔到任务队列异步处理即可;笔者用的最多的是线程池任务队列(默认LinkedBlockingQueue)和Disruptor任务队列(RingBuffer)。如刷数据时,将任务扔到队列异步处理即可,处理成功后再异步通知用户;还有如删除SKU操作,用户请求时直接将任务分解并扔到队列,异步处理,处理成功后异步通知用户即可;还有如查询聚合,将多个可并行处理的任务扔到队列然后等待最慢的一个返回。如果使用的是内存任务队列请记住可能存在系统重启等问题造成的数据丢失。下载

 

通过任务队列可以实现:异步处理、任务分解/聚合处理。

 

注:JDK7提供了ExecutorService的新的实现ForkJoinPool,其提供了Work-stealing机制,可以更好地提升并发效率。

 

在使用Executors.newFixedThreadPool时,其没有设置队列大小(默认Integer.null

高并发系统之队列术