队列-高并发系统之队列术-转
队列应用
转自开涛:http://geek.csdn.net/news/detail/100577
应用
异步处理
进行异步处理,比如用户注册成功后需要发送注册成功邮件/新用户积分/优惠券等等;
缓存过期时先返回老的数据,然后异步更新缓存、异步写日志等;
通过异步处理,可以提升主流程响应速度,而非主流程/非重要业务可以异步集中处理,这样还可以将任务聚合然后批量处理;
系统解耦
比如用户成功支付完成订单后:
需要通知生产配货系统、发票系统、库存系统、推荐系统、搜索系统、风控系统等进行业务处理;
这些业务处理不需要实时处理、不需要强一致,只需要最终一致性即可,因此可以通过消息队列/任务队列进行系统解耦;
数据同步
比如想把Mysql变更的数据同步到Redis、或者将Mysql数据同步到Mongodb、或者机房间数据同步、或者主从数据同步等,此时可以考虑使用如databus、canal、otter。使用数据总线队列进行数据同步的好处是可以保证数据修改的有序性
流量削峰
系统瓶颈一般在数据库上,比如扣减库存、下单等;此时可以考虑使用队列将变更请求暂时放入队列,通过缓存+队列暂存的方式将数据库流量削峰;还有如秒杀系统,下单服务会是该系统的瓶颈,此时会使用队列进行排队和限流,从而保护下单服务。通过队列暂存或者队列限流来削峰。
比如减库存,可以考虑这样设计:
直接在Redis中扣减,然后记录下扣减日志(FIFO队列),通过Worker去同步到DB;
队列类型
缓冲区队列
Log4j日志缓存队列,log4j字节缓存去,字节满了同步到磁盘flush;
log4j使用BufferedWriter,不是异步写,使用AsyncAppender是异步的,然后通过bufferSize控制日志事件缓冲大小;
缓冲队列:批量处理、异步处理
任务队列
将一些不需要与主线程同步执行的任务扔到任务队列异步处理即可;
线程池队列:LinkedBlockingQueue
Disruptor队列:RingBuffer
比如刷数据将请求扔到任务队列,处理成功返回给用户即可;
查询聚合,将多个可并行处理的任务扔到队列然后等待最慢的一个返回;
内存任务队列在系统重启时会造成数据丢失;
通过任务队列可以实现:异步处理、任务分解/聚合处理。
JDK7提供了ExecutorService的新的实现ForkJoinPool,其提供了Work-stealing机制,可以更好地提升并发效率。
newFixedThreadPool并没有限制大小,大量任务缓存到LinkedBlockingQueue会造成GC慢问题;
需要指定任务队列大小,并设置合理的RejectedExecutionHandler;
消息队列
ActiveMQ、Kafka、Redis
消息队列存储业务数据,其他系统根据需要订阅;
常见的模式是:
- 点对点:一个消息只有一个消费者
- 发布订阅:一个消息可以有多个消费者
发布订阅模式更为常见;
比如用户注册成功、修改商品数据、订单状态变更等都应该将变更发送到消息队列,从而其他系统根据需要订阅该消息,然后按照自己的需求进行业务逻辑开发;
在添加新功能时,消息消费者只需要订阅该消息,然后开发相应的业务逻辑,消息生产者根本不关心你怎么使用消息和你做什么业务处理;
同步调用,添加什么新功能都需要到用户系统提需求。其中一个服务出现问题了,整个服务就不可用了。
消息队列,用户系统只需要发布用户注册成功的消息即可,相关系统订阅该消息,然后执行相关的业务逻辑。相关服务出问题不影响到注册主流程。
通过消息队列可以实现:异步处理、系统解耦。
请求队列
如在web环境下对用户请求队列,从而进行一些特殊控制:流量控制、请求分级、请求隔离;
如将请求按照功能划分到不同的队列,从而使得不同的队列出现问题后相互不影响;
还可以对请求分级,一些重要请求可以优先处理(发展到一定程度应将功能物理分离);
还有服务器处理能力有限,在接近服务器瓶颈时需要考虑限流,最简单的限流时丢弃处理不了的请求,此时可以使用队列进行流量控制。
数据总线队列
一般消息队列中的消息都是业务维度的,比如业务键或者业务状态等,比如哪个SKU变更了,而有些订阅者需要再查一遍来获取最新的修改数据(比如缓存同步);
通过现有的消息队列方式的缺点是很难只进行修改部分的推送和保证数据有序性。而此种场景比较适合使用数据总线队列实现。如数据库数据修改后需要同步数据到缓存,或者需要将一个机房数据同步到另一个机房,只是数据维度的同步,此时应该使用数据总线队列如canal、otter、databus;使用数据总线队列的好处是可以保证数据的有序性。
混合队列
http://mp.weixin.qq.com/s?__biz=MzIwODA4NjMwNA==&mid=2652897931&idx=1&sn=36a2e7fc1ea619bf985a1b873447cc33&scene=21#wechat_redirect
此处MQ是使用京东自研的JMQ,消息是可靠持久化存储的;应用会按照不同的维度发布消息到JMQ;下游应用接收到该消息后会放入到Redis,使用Redis List来存储这些任务;应用将Redis消息消费处理后,会按照不同的维度聚合商品消息然后再次发送出去。
使用Redis队列的主要原因是想提升消息堆积能力和并发处理能力。另外在使用Redis构建消息队列时需要考虑网络抖动造成的消息丢失问题,因为Redis是没有回滚事务的,或者说是确认机制。我们使用如下方式防止消息丢失:
try {
    id = queueRedis.opsForList().rightPopAndLeftPush(queueName, processingQueueName);
} catch (Exception e) {
    //发生了网络异常,需要把processing中的id再放回到waiting queue中
    String msg = queueName + " to " + processingQueueName + " rpoplpush error";
    LOG.error(msg, e);
    //报警代码
}
而对于失败我们会重试三次,重试失败后放入失败队列,而失败队列是具有防重功能的(从本地队列和失败队列排重),使用的是Redis Lua脚本实现:
static EventQueueScript ADD_TO_FAIL_QUEUE_REDIS_SCRIPT = new EventQueueScript(
        "redis.call('lrem', KEYS[1], 1, ARGV[1]) redis.call('lrem', KEYS[2], 1, ARGV[1]) return redis.call('lpush', KEYS[2], ARGV[1])"
);
Redis作者Antirez开发的内存分布式消息队列Disque是未来更好的内存消息队列选择。
其他队列
优先级队列
在实际开发时肯定有些任务是紧急的,此时应该优先处理紧急的任务;所以请考虑对队列进行分级。
副本队列
在进行一些系统重构或者上新的功能时,如果没有足够的信心保证业务逻辑正确,可以考虑存储一份队列的副本(比如1小时、1天的),从而当业务出现问题时可以对这些消息进行回放。
镜像队列
每个队列不会无限制订阅数量,一定会有一个极限的;当到达极限时请考虑使用镜像队列方式解决该问题。
考虑的问题
队列并发数
队列服务端的并发数是不一样的,需要根据消费能力设置并发数;
推还是拉
消息体内容不是越全越好,根据具体业务设计消息体;
如有些系统依赖商品变更消息:(SKU)
有些系统依赖商品状态消息:(SKU、状态)
有些系统依赖商品属性变更消息:(SKU、变更的属性)
如果让所有系统都消费商品变更消息,那么这些系统都会调用商品查询服务拉一下最新的商品信息然后进行处理。
因此要根据实际情况来决定是使用推送方式(将系统需要的所有信息推过去)还是拉取方式(只推送ID,然后再查一遍。
消息合并
如果消息写入量非常大,应该考虑将消息合并写,可以”写应用本地磁盘队列”–>“同步本地磁盘队列到消息中间件”;同步时可以根据需求制定同步策略,如1秒同步1次。
 
                    
                     
                    
                 
                    
                
 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号