一、消息队列----应用场景
| 场景名称 |
场景描述 |
传统做法 |
消息队列做法 |
| 异步处理 |
用户注册后,需要发注册邮件和注册短信 |
1.串行的方式: 信息写入数据库50ms + 发送注册短信50ms + 发送注册邮件50ms =》 150ms 2.并行方式: 信息写入数据库50ms +【发送注册邮件的同时,发送注册短信】50ms=》100ms |
信息写入数据库50ms + 【注册邮件,发送短信写入消息队列】0.0001ms =>50ms 注:因此写入消息队列的速度很快,基本可以忽略; 中心思想:引入消息队列,将不是必须的业务逻辑,异步处理; |
| 应用解耦 |
用户下单后,订单系统需要通知库存系统 |
|
|
| 流量削峰 |
秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉; |
|
用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量, 则直接抛弃用户请求或跳转到错误页面 |
| 日志处理 |
将消息队列用在日志处理中; (我一般关注把日志记录下来,不怎么关注日志后续的处理,所以这个用的不多) |
|
|
| 消息通讯 |
消息通讯是指,【消息队列一般都内置了高效的通信机制】,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等 |
|
|
二、消息队列----优缺点
| 优点 |
缺点 |
| 解耦 |
引入复杂度(引入消息队列本身就有创建维护成本) |
| 提速 |
暂时的不一致性(你把消息给队列后,默认它一定会成功执行的,但实际上不一定) |
| 广播(一次生成,可多人订阅) |
|
| 削峰 |
|
三、消息队列----重试补偿,事务补偿
| 问题场景 |
解决思想 |
解决办法 |
| 消费者已经收到消息或消费消息了,但因为网络中断没给mq发送ack,导致消息重发重复消费; |
消费消息时先判断该消息是否已消费过(这个状态位如何存储读取?) |
【发送消息时】给消息分配一个全局id,只要消费过该消息,将 < id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可 |
| 客户下单,若订单创建成功,库存扣减失败,如何回滚订单?在一台服务器上用事务能解决,但分布式如何处理? |
既然不能把【订单创建】和【库存减扣】放到一个事务里,那就把【订单创建】和【库存减扣分身--消息事件表】放到一个事务里;
实现: 1.【订单创建】和【库存减扣分身--消息事件表】放到一个事务里; 2. 或者 消息事件表消费失败更新事件表状态,根据状态扔异常进行事务回滚; 3. 或者 库存服务定时扫描消息事件表,将未投递失败/消费 失败的消息进行消费,即补偿事务一致性 |
|
|
|
|
|
四、消息队列----幂等性(如何保证 重复消费的结果 与 消费一次的结果是相同的)
| 解决办法 |
举例说明 |
| 利用数据库唯一约束 |
将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等; |
| 去重表 |
首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重复消费,数据库会抛唯一约束异常,操作就会回滚 |
| 利用redis的原子性 |
每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中 |
| 多版本(乐观锁)控制 |
此方案多用于【更新】的场景下。大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1 |
| 状态机机制 |
此方案多用于更新且业务场景存在多种状态流转的场景 |
| token机制 |
生产者发送每条数据的时候,增加一个全局唯一的id,这个id通常是业务的唯一标识,比如订单编号。在消费端消费时,则验证该id是否被消费过,如果还没消费过,则进行业务处理。处理结束后,在把该id存入redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。 |
|
|
|
五、消息队列----消息堆积(要么是发送端变快,要么是消费端变慢造成)
| 产生原因: |
|
| 解决思想: |
设计MQ系统的时候,一定要保证 Consumer 端的消费性能要高于 Producer 端的发送性能 |
| 发送端性能优化: |
发送端性能低:检查是否因为业务逻辑耗时太久导致的 + 设置合适的 并发 和 批量 大小; |
| 消费端性能优化: |
消费端性能低:优化业务逻辑耗时 + 水平扩容 (扩充consumer端的 实例数 和 topic中的 partition 数) |
六、消息队列----有序性(产生原因:多个消费者/多线程)
| RabbitMQ 无序原因: |
一个queue,多个consumer |
| | |
| RabbitMQ无序解决办法: |
拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点; 或者就一个queue,但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理 |
| | |
| kafka 无序原因: |
一个topic,一个partition,一个consumer,但是内部多线程 |
| | |
| Kafka 无序解决办法: |
一个topic,一个partition,一个consumer,内部单线程消费,写N个内存queue,然后N个线程分别消费一个内存queue即可 |
| | |