本篇主要内容如下:
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0qIXR5JPk1y5Tibuno3ialh2NRQGCg6htjV6R3flibZUktGKT8uahgAWibg/640?wx_fmt=png)
主要内容
现在我们都在讨论分布式,特别是面试的时候。不管是招初级软件工程师还是高级,都会要求懂分布式,甚至要求用过。
传得沸沸扬扬的分布式到底是什么东东,有什么优势?
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q05SbmicHZZ3kO8pomZocjGnhGcjRibHMANT4YRayHfGibGXMDsF9MhRIlA/640?wx_fmt=png)
风遁·螺旋手里剑
看过火影的同学肯定知道漩涡鸣人的招牌忍术:多重影分身之术。这个术有一个特别厉害的地方,就是过程和心得(多个分身的感受和经历都是相通的)。
比如 A 分身去找卡卡西(鸣人的老师)请教问题,那么其他分身也会知道 A 分身问的什么问题。
漩涡鸣人还有另外一个超级厉害的忍术:风遁·螺旋手里剑,需要由几个影分身完成。这个忍术是靠三个鸣人一起协作完成的。
那么这两个忍术和分布式有什么关系?看下面:
分布在不同地方的系统或服务,是彼此相互关联的;
分布式系统是分工合作的。
案例:
那多重影分身之术有什么缺点?
需要更多优质人才懂分布式,人力成本增加;
架构设计变得异常复杂,学习成本高;
运维部署和维护成本显著增加;
多服务间链路变长,开发排查问题难度加大;
环境高可靠性问题;
数据幂等性问题;
数据的顺序问题;
等等。
讲到分布式不得不知道 CAP 定理和 Base 理论,下面给不知道的同学做一个扫盲。
在理论计算机科学中,CAP 定理指出对于一个分布式计算系统来说,不可能通是满足以下三点:
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。
BASE 理论是对 CAP 中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用来保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。
满足 BASE 理论的事务,我们称之为柔性事务:
基本可用 :分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网址交易付款出现问题来,商品依然可以正常浏览;
软状态: 由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性。如订单中的“支付中”、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态;
最终一致性: 最终一致是指的经过一段时间后,所有节点数据都将会达到一致。如订单的“支付中”状态,最终会变为“支付成功”或者“支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。
消息队列如何做分布式?
将消息队列里面的消息分摊到多个节点(指某台机器或容器)上,所有节点的消息队列之和就包含了所有消息。
所谓幂等性就是无论多少次操作和第一次的操作结果一样。如果消息被多次消费,很有可能造成数据的不一致。
而如果消息不可避免地被消费多次,如果我们开发人员能通过技术手段保证数据的前后一致性,那也是可以接受的,这让我想起了 Java 并发编程中的 ABA 问题,如果出现了ABA问题,若能保证所有数据的前后一致性也能接受。
RabbitMQ、RocketMQ、Kafka 消息队列中间件都有可能出现消息重复消费问题。这种问题并不是 MQ 自己保证的,而需要开发人员来保证。
这几款消息队列中间都是全球最牛的分布式消息队列,那它们肯定考虑到了消息的幂等性。我们以 Kafka 为例,看看 Kafka 是怎么保证消息队列的幂等性:
坑:当消费完消息后,还没来得及提交偏移量,系统就被关机了,那么未提交偏移量的消息则会再次被消费。
如下图所示,队列中的数据 A、B、C,对应的偏移量分别为 100、101、102,都被消费者消费了,但是只有数据 A 的偏移量 100 提交成功,另外 2 个偏移量因系统重启而导致未及时提交。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0v5MDzPouQzIoL2OJ02UzUmrel9qw20fHFlibiaFmrWnCaxfiaia3lKYCEw/640?wx_fmt=png)
系统重启,偏移量未提交
重启后,消费者又是拿偏移量 100 以后的数据,从偏移量 101 开始拿消息。所以数据 B 和数据 C 被重复消息。
如下图所示:
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0Y05Wx9sPp5ugAv87jicoA1nQ3jTZBDnhOfCDAQDqWSBIdQ6H5lsxI6g/640?wx_fmt=png)
重启后,重复消费消息
微信官方文档上提到微信支付通知结果可能会推送多次,需要开发者自行保证幂等性。第一次我们可以直接修改订单状态(如支付中 -> 支付成功),第二次就根据订单状态来判断,如果不是支付中,则不进行订单处理逻辑。
每次插入数据时,先检查下数据库中是否有这条数据的主键 id,如果有,则进行更新操作。
Redis 的 Set 操作天然幂等性,所以不用考虑 Redis 写数据的问题。
生产者发送每条数据时,增加一个全局唯一 id,类似订单 id。每次消费时,先去 Redis 查下是否有这个 id,如果没有,则进行正常处理消息,且将 id 存到 Redis。如果查到有这个 id,说明之前消费过,则不要进行重复处理这条消息。
不同业务场景,可能会有不同的幂等性方案,大家选择合适的即可,上面的几种方案只是提供常见的解决思路。
坑:消息丢失会带来什么问题?如果是订单下单、支付结果通知、扣费相关的消息丢失,则可能造成财务损失,如果量很大,就会给甲方带来巨大损失。
那消息队列是否能保证消息不丢失呢?
答案:否。主要有三种场景会导致消息丢失。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0gSzLsrn6WR7sSYRbsLMP6edKlfry3E8piaKZPb7k83MfanIwYyA4V6Q/640?wx_fmt=png)
消息队列之消息丢失
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0cyRbSxLX19UVcMrPcBQLTrQrEee88AWib8x9oWJwLQibmibmAWQve2JLg/640?wx_fmt=png)
解决方案:
对于 RabbitMQ 来说,生产者发送数据之前开启 RabbitMQ 的事务机制 channel.txselect,如果消息没有进队列,则生产者受到异常报错,并进行回滚 channel.txRollback ,然后重试发送消息;如果收到了消息,则可以提交事务 channel.txCommit 。但这是一个同步的操作,会影响性能。
我们可以采用另外一种模式:confirm 模式来解决同步机制的性能问题。每次生产者发送的消息都会分配一个唯一的 id,如果写入到了 RabbitMQ 队列中,则 RabbitMQ 会回传一个 ack 消息,说明这个消息接收成功。如果 RabbitMQ 没能处理这个消息,则回调 nack 接口。说明需要重试发送消息。
也可以自定义超时时间 + 消息 id 来实现超时等待后重试机制。但可能出现的问题是调用 ack 接口时失败了,所以会出现消息被发送两次的问题,这个时候就需要保证消费者消费消息的幂等性。
事务模式 和 confirm 模式的区别:
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0J37TrjDibvI5rDvbE8J2FAX1xfofkpfpVlQUIF8wtUOKjJGTQLnvUbw/640?wx_fmt=png)
消息队列丢失消息
消息队列的消息可以放到内存中,或将内存中的消息转到硬盘(比如数据库)中,一般都是内存和硬盘中都存有消息。
如果只是放在内存中,那么当机器重启了,消息就全部丢失了。如果是硬盘中,则可能存在一种极端情况,就是将内存中的数据转换到硬盘的期间中,消息队列出问题了,未能将消息持久化到硬盘。
解决方案:
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0OSNdwlREM1bIA0tgVDAecw1Umy3AOwIgrxNKo9szhhClrHbE0gibH0g/640?wx_fmt=png)
消费者刚拿到数据,还没开始处理消息,结果进程因为异常退出了,消费者没有机会再次拿到消息。
解决方案:
问题:那这种主动 ack有什么漏洞了?如果 主动 ack 的时候挂了,怎么办?
则可能会被再次消费,这个时候就需要幂等处理了。
问题:如果这条消息一直被重复消费怎么办?
则需要有加上重试次数的监测,如果超过一定次数则将消息丢失,记录到异常表或发送异常通知给值班人员。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0w4Q4fZHmcDmyzzo7A5euHocVUSqDBzqUuSCV2LLdmWPJkX56G8rq4Q/640?wx_fmt=png)
RabbitMQ 丢失消息的处理方案
场景:
Kafka 的某个 broker(节点)宕机了,重新选举 leader (写入的节点)。如果 leader 挂了,follower 还有些数据未同步完,则 follower 成为 leader 后,消息队列会丢失一部分数据。
解决方案:
坑:用户先下单成功,然后取消订单,如果顺序颠倒,则最后数据库里面会有一条下单成功的订单。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0Zfwwia7AU4o07QfIZkL0EqTODzhCGCe2libtRDJXxHf3LfWSgqG2FIuw/640?wx_fmt=png)
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0rjUDJptrAz0s0JHTEjR9PPkGIzEdp0PE0BrtrIMz0zhRU3sFovH9Ug/640?wx_fmt=png)
RabbitMQ消息乱序场景
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0iaJIJeYH2zhKH5I2J9VF8JQw4lUU0CcibR4e5sRnZ8Zvhk2JmRYT2jKQ/640?wx_fmt=png)
① 场景
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q02RcficwmbVE24sw39qI1WcWDGTu3M9oTjAfz28BDDibX9TZiczWpNxYhA/640?wx_fmt=png)
Kafka 消息丢失场景
创建了 topic,有 3 个 partition;
创建一条订单记录,订单 id 作为 key,订单相关的消息都丢到同一个 partition 中,同一个生产者创建的消息,顺序是正确的;
为了快速消费消息,会创建多个消费者去处理消息,而为了提高效率,每个消费者可能会创建多个线程来并行的去拿消息及处理消息,处理消息的顺序可能就乱序了。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0ZWXHHg6o4vrhHCmTicNcQdfJaAGKEXwbkpIzDicCfEz6tcWSlvAkFolQ/640?wx_fmt=png)
Kafka 消息乱序解决方案
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0PyJ4jauXl3K3LfRbIgicDU51q17nC4azt29sgzVwhDPUCyS5lcBuLUQ/640?wx_fmt=png)
消息积压:消息队列里面有很多消息来不及消费。
场景:
坑:比如线上正在做订单活动,下单全部走消息队列,如果消息不断积压,订单都没有下单成功,那么将会损失很多交易。
解决方案:解铃还须系铃人
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0xwAwgbOrqjjQ0TgtQ5gLd6KjW46bBzsO6EMyZEWIicSKlUV1KWew7HA/640?wx_fmt=png)
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0wEq4XpyzQa4JNvx4dq85hC3Xq3XBO5ZSUaMuLJN7K9RfYmiaojRzCoQ/640?wx_fmt=png)
坑:RabbitMQ 可以设置过期时间,如果消息超过一定的时间还没有被消费,则会被 RabbitMQ 给清理掉。消息就丢失了。
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q00ETAnW2Uiab8RicgyDbiaXVq4yUzGwlJAILUw04jibfhHaxouMDmwWw8Yg/640?wx_fmt=png)
解决方案:
坑:当消息队列因消息积压导致的队列快写满,所以不能接收更多的消息了。生产者生产的消息将会被丢弃。
解决方案:
判断哪些是无用的消息,RabbitMQ 可以进行 Purge Message 操作;
如果是有用的消息,则需要将消息快速消费,将消息里面的内容转存到数据库;
准备好程序将转存在数据库中的消息再次重导到消息队列;
闲时重导消息到消息队列。
在高频访问数据库的场景中,我们会在业务层和数据层之间加入一套缓存机制,来分担数据库的访问压力,毕竟访问磁盘 I/O 的速度是很慢的。
比如利用缓存来查数据,可能5ms就能搞定,而去查数据库可能需要 50 ms,差了一个数量级。而在高并发的情况下,数据库还有可能对数据进行加锁,导致访问数据库的速度更慢。
分布式缓存我们用的最多的就是 Redis了,它可以提供分布式缓存服务。
Redis 可以实现利用哨兵机制实现集群的高可用,那什么是哨兵机制呢?
英文名:sentinel,中文名:哨兵;
集群监控:负责主副进程的正常工作;
消息通知:负责将故障信息报警给运维人员;
故障转移:负责将主节点转移到备用节点上;
配置中心:通知客户端更新主节点地址;
分布式:有多个哨兵分布在每个主备节点上,互相协同工作;
分布式选举:需要大部分哨兵都同意,才能进行主备切换;
高可用:即使部分哨兵节点宕机了,哨兵集群还是能正常工作。
坑:当主节点发生故障时,需要进行主备切换,可能会导致数据丢失。
主节点异步同步数据给备用节点的过程中,主节点宕机了,导致有部分数据未同步到备用节点。而这个从节点又被选举为主节点,这个时候就有部分数据丢失了。
主节点所在机器脱离了集群网络,实际上自身还是运行着的。但哨兵选举出了备用节点作为主节点,这个时候就有两个主节点都在运行,相当于两个大脑在指挥这个集群干活,但到底听谁的呢?这个就是脑裂。
那怎么脑裂怎么会导致数据丢失呢?如果发生脑裂后,客户端还没来得及切换到新的主节点,连的还是第一个主节点,那么有些数据还是写入到了第一个主节点里面,新的主节点没有这些数据。那等到第一个主节点恢复后,会被作为备用节点连到集群环境,而且自身数据会被清空,重新从新的主节点复制数据。而新的主节点因没有客户端之前写入的数据,所以导致数据丢失了一部分。
注意:缓存雪崩、缓存穿透、缓存击穿并不是分布式所独有的,单机的时候也会出现。所以不算在分布式的坑里。
分库:因一个数据库支持的最高并发访问数是有限的,可以将一个数据库的数据拆分到多个库中,来增加最高并发访问数;
分表:因一张表的数据量太大,用索引来查询数据都搞不定了,所以可以将一张表的数据拆分到多张表,查询时,只用查拆分后的某一张表,SQL 语句的查询性能得到提升;
分库分表优势:分库分表后,承受的并发增加了多倍;磁盘使用率大大降低;单表数据量减少,SQL 执行效率明显提升。
水平拆分:把一个表的数据拆分到多个数据库,每个数据库中的表结构不变,用多个库抗更高的并发。比如订单表每个月有500万条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库;
垂直拆分:把一个有很多字段的表,拆分成多张表到同一个库或多个库上面。高频访问字段放到一张表,低频访问的字段放到另外一张表。利用数据库缓存来缓存高频访问的行数据。比如将一张很多字段的订单表拆分成几张表分别存不同的字段(可以有冗余字段)。
分库、分表的方式:
1. 根据租户来分库、分表;
2. 利用时间范围来分库、分表;
3. 利用 ID 取模来分库、分表。
坑:分库分表是一个运维层面需要做的事情,有时会采取凌晨宕机开始升级。可能熬夜到天亮,结果升级失败,则需要回滚,其实对技术团队都是一种煎熬。
双写迁移方案:迁移时,新数据的增删改操作在新库和老库都做一遍;
使用分库分表工具 Sharding-jdbc 来完成分库分表的累活;
使用程序来对比两个库的数据是否一致,直到数据一致。
坑:分库分表看似光鲜亮丽,但分库分表会引入什么新的问题呢?
跨库的关联查询性能差;
数据多次扩容和维护量大;
跨分片的事务一致性难以保证。
坑:唯一 ID 的生成方式有 n 种,各有各的用途,别用错了。
获取系统当前时间作为唯一 ID。
①高并发时,1 ms内可能有多个相同的 ID。
②信息不安全
![](http://mmbiz.qpic.cn/sz_mmbiz_png/tibrg3AoIJTvm0JgI683LnageialuCV3Q0fBWrYas9Q0iaRZW1LGIMKcr4Q90mOzicNEd2LibLuDpH9uLmhIbo3uXbg/640?wx_fmt=png)