docs: update UUID desc to fix #22, rename images
- Update UUID desc to fix #22 - Rename img to images - Fix typo
@ -1,4 +1,4 @@
|
||||
![logo](img/icon.png)
|
||||
![logo](images/icon.png)
|
||||
|
||||
# Java 进阶扫盲
|
||||
|
||||
|
@ -53,7 +53,7 @@ end
|
||||
5. 要是锁建立失败了,那么就依次之前建立过的锁删除;
|
||||
6. 只要别人建立了一把分布式锁,你就得**不断轮询去尝试获取锁**。
|
||||
|
||||
![redis-redlock](/img/redis-redlock.png)
|
||||
![redis-redlock](/images/redis-redlock.png)
|
||||
|
||||
### zk 分布式锁
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
分布式业务系统,就是把原来用 Java 开发的一个大块系统,给拆分成**多个子系统**,多个子系统之间互相调用,形成一个大系统的整体。假设原来你做了一个 OA 系统,里面包含了权限模块、员工模块、请假模块、财务模块,一个工程,里面包含了一堆模块,模块与模块之间会互相去调用,1 台机器部署。现在如果你把这个系统给拆开,权限系统、员工系统、请假系统、财务系统 4 个系统,4 个工程,分别在 4 台机器上部署。一个请求过来,完成这个请求,这个员工系统,调用权限系统,调用请假系统,调用财务系统,4 个系统分别完成了一部分的事情,最后 4 个系统都干完了以后,才认为是这个请求已经完成了。
|
||||
|
||||
![simple-distributed-system-oa](/img/simple-distributed-system-oa.png)
|
||||
![simple-distributed-system-oa](/images/simple-distributed-system-oa.png)
|
||||
|
||||
> 这两年开始兴起和流行 Spring Cloud,刚流行,还没开始普及,目前普及的是 dubbo,因此这里也主要讲 dubbo。
|
||||
|
||||
|
@ -8,13 +8,12 @@
|
||||
|
||||
所以这都是分布式系统一些很常见的问题。
|
||||
|
||||
|
||||
## 面试题剖析
|
||||
首先,一般来说,个人建议是,你们从业务逻辑上设计的这个系统最好是不需要这种顺序性的保证,因为一旦引入顺序性保障,比如使用**分布式锁**,会**导致系统复杂度上升**,而且会带来**效率低下**,热点数据压力过大等问题。
|
||||
|
||||
下面我给个我们用过的方案吧,简单来说,首先你得用 dubbo 的一致性 hash 负载均衡策略,将比如某一个订单 id 对应的请求都给分发到某个机器上去,接着就是在那个机器上因为可能还是多线程并发执行的,你可能得立即将某个订单 id 对应的请求扔一个**内存队列**里去,强制排队,这样来确保他们的顺序性。
|
||||
|
||||
![distributed-system-request-sequence](/img/distributed-system-request-sequence.png)
|
||||
![distributed-system-request-sequence](/images/distributed-system-request-sequence.png)
|
||||
|
||||
但是这样引发的后续问题就很多,比如说要是某个订单对应的请求特别多,造成某台机器成**热点**怎么办?解决这些问题又要开启后续一连串的复杂技术方案......曾经这类问题弄的我们头疼不已,所以,还是建议什么呢?
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
|
||||
如果你要操作别人的服务的库,你必须是通过**调用别的服务的接口**来实现,绝对不允许交叉访问别人的数据库。
|
||||
|
||||
![distributed-transacion-XA](/img/distributed-transaction-XA.png)
|
||||
![distributed-transacion-XA](/images/distributed-transaction-XA.png)
|
||||
|
||||
### TCC 方案
|
||||
TCC 的全称是:Try、Confirm、Cancel。
|
||||
@ -43,7 +43,7 @@ TCC 的全称是:Try、Confirm、Cancel。
|
||||
|
||||
但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码很难维护。
|
||||
|
||||
![distributed-transacion-TCC](/img/distributed-transaction-TCC.png)
|
||||
![distributed-transacion-TCC](/images/distributed-transaction-TCC.png)
|
||||
|
||||
### 本地消息表
|
||||
本地消息表其实是国外的 ebay 搞出来的这么一套思想。
|
||||
@ -59,7 +59,7 @@ TCC 的全称是:Try、Confirm、Cancel。
|
||||
|
||||
这个方案说实话最大的问题就在于**严重依赖于数据库的消息表来管理事务**啥的,会导致如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用。
|
||||
|
||||
![distributed-transaction-local-message-table](/img/distributed-transaction-local-message-table.png)
|
||||
![distributed-transaction-local-message-table](/images/distributed-transaction-local-message-table.png)
|
||||
|
||||
### 可靠消息最终一致性方案
|
||||
这个的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。
|
||||
@ -73,7 +73,7 @@ TCC 的全称是:Try、Confirm、Cancel。
|
||||
5. 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
|
||||
6. 这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的。
|
||||
|
||||
![distributed-transaction-reliable-message](/img/distributed-transaction-reliable-message.png)
|
||||
![distributed-transaction-reliable-message](/images/distributed-transaction-reliable-message.png)
|
||||
|
||||
### 最大努力通知方案
|
||||
这个方案的大致意思就是:
|
||||
|
@ -29,7 +29,7 @@ MQ、ES、Redis、Dubbo,上来先问你一些思考的问题,原理(kafka
|
||||
- 第三步:consumer 调用 provider
|
||||
- 第四步:consumer 和 provider 都异步通知监控中心
|
||||
|
||||
![dubbo-operating-principle](/img/dubbo-operating-principle.png)
|
||||
![dubbo-operating-principle](/images/dubbo-operating-principle.png)
|
||||
|
||||
### 注册中心挂了可以继续通信吗?
|
||||
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息**拉取到本地缓存**,所以注册中心挂了可以继续通信。
|
@ -17,7 +17,7 @@
|
||||
|
||||
那就需要基于 dubbo 做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将**各个服务之间的依赖关系和调用链路生成出来**,做成一张图,显示出来,大家才可以看到对吧。
|
||||
|
||||
![dubbo-service-invoke-road](/img/dubbo-service-invoke-road.png)
|
||||
![dubbo-service-invoke-road](/images/dubbo-service-invoke-road.png)
|
||||
|
||||
#### 2. 服务访问压力以及时长统计
|
||||
需要自动统计**各个接口和服务之间的调用次数以及访问延时**,而且要分成两个级别。
|
||||
|
@ -81,7 +81,7 @@ hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
|
||||
|
||||
provider 启动的时候,就会加载到我们 jar 包里的`my=com.bingo.MyProtocol` 这行配置里,接着会根据你的配置使用你定义好的 MyProtocol 了,这个就是简单说明一下,你通过上述方式,可以替换掉大量的 dubbo 内部的组件,就是扔个你自己的 jar 包,然后配置一下即可。
|
||||
|
||||
![dubbo-spi](/img/dubbo-spi.png)
|
||||
![dubbo-spi](/images/dubbo-spi.png)
|
||||
|
||||
dubbo 里面提供了大量的类似上面的扩展点,就是说,你如果要扩展一个东西,只要自己写个 jar,让你的 consumer 或者是 provider 工程,依赖你的那个 jar,在你的 jar 里指定目录下配置好接口名称对应的文件,里面通过 `key=实现类`。
|
||||
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@ -4,12 +4,12 @@ zookeeper 都有哪些使用场景?
|
||||
## 面试官心理分析
|
||||
现在聊的 topic 是分布式系统,面试官跟你聊完了 dubbo 相关的一些问题之后,已经确认你对分布式服务框架/RPC框架基本都有一些认知了。那么他可能开始要跟你聊分布式相关的其它问题了。
|
||||
|
||||
分布式锁这个东西,很常用的,你做 Java系统开发,分布式系统,可能会有一些场景会用到。最常用的分布式锁就是基于 zookeeper 来实现的。
|
||||
分布式锁这个东西,很常用的,你做 Java 系统开发,分布式系统,可能会有一些场景会用到。最常用的分布式锁就是基于 zookeeper 来实现的。
|
||||
|
||||
其实说实话,问这个问题,一般就是看看你是否了解 zookeeper,因为 zk 是分布式系统中很常见的一个基础系统。而且问的话常问的就是说 zk 的使用场景是什么?看你知道不知道一些基本的使用场景。但是其实 zk 挖深了自然是可以问的很深很深的。
|
||||
其实说实话,问这个问题,一般就是看看你是否了解 zookeeper,因为 zookeeper 是分布式系统中很常见的一个基础系统。而且问的话常问的就是说 zookeeper 的使用场景是什么?看你知道不知道一些基本的使用场景。但是其实 zookeeper 挖深了自然是可以问的很深很深的。
|
||||
|
||||
## 面试题剖析
|
||||
大致来说,zk 的使用场景如下,我就举几个简单的,大家能说几个就好了:
|
||||
大致来说,zookeeper 的使用场景如下,我就举几个简单的,大家能说几个就好了:
|
||||
|
||||
- 分布式协调
|
||||
- 分布式锁
|
||||
@ -17,21 +17,21 @@ zookeeper 都有哪些使用场景?
|
||||
- HA高可用性
|
||||
|
||||
### 分布式协调
|
||||
这个其实是 zk 很经典的一个用法,简单来说,就好比,你 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zk 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zk 上**对某个节点的值注册个监听器**,一旦 B 系统处理完了就修改 zk 那个节点的值,A 立马就可以收到通知,完美解决。
|
||||
这个其实是 zookeeper 很经典的一个用法,简单来说,就好比,你 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上**对某个节点的值注册个监听器**,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 立马就可以收到通知,完美解决。
|
||||
|
||||
![zookeeper-distributed-coordination](/img/zookeeper-distributed-coordination.png)
|
||||
![zookeeper-distributed-coordination](/images/zookeeper-distributed-coordination.png)
|
||||
|
||||
### 分布式锁
|
||||
举个栗子。对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zk 分布式锁,一个机器接收到了请求之后先获取 zk 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也**尝试去创建**那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。
|
||||
举个栗子。对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也**尝试去创建**那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。
|
||||
|
||||
![zookeeper-distributed-lock-demo](/img/zookeeper-distributed-lock-demo.png)
|
||||
![zookeeper-distributed-lock-demo](/images/zookeeper-distributed-lock-demo.png)
|
||||
|
||||
### 元数据/配置信息管理
|
||||
zk 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zk 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zk 么?
|
||||
zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zookeeper 么?
|
||||
|
||||
![zookeeper-meta-data-manage](/img/zookeeper-meta-data-manage.png)
|
||||
![zookeeper-meta-data-manage](/images/zookeeper-meta-data-manage.png)
|
||||
|
||||
### HA高可用性
|
||||
这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zk 来开发 HA 高可用机制,就是一个**重要进程一般会做主备**两个,主进程挂了立马通过 zk 感知到切换到备用进程。
|
||||
这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个**重要进程一般会做主备**两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。
|
||||
|
||||
![zookeeper-active-standby](/img/zookeeper-active-standby.png)
|
||||
![zookeeper-active-standby](/images/zookeeper-active-standby.png)
|
@ -3,7 +3,7 @@
|
||||
### 小型电商网站的商品详情页系统架构
|
||||
小型电商网站的页面展示采用页面全量静态化的思想。数据库中存放了所有的商品信息,页面静态化系统,将数据填充进静态模板中,形成静态化页面,推入 Nginx 服务器。用户浏览网站页面时,取用一个已经静态化好的 html 页面,直接返回回去,不涉及任何的业务逻辑处理。
|
||||
|
||||
![e-commerce-website-detail-page-architecture-1](/img/e-commerce-website-detail-page-architecture-1.png)
|
||||
![e-commerce-website-detail-page-architecture-1](/images/e-commerce-website-detail-page-architecture-1.png)
|
||||
|
||||
- 好处:用户每次浏览一个页面,不需要进行任何的跟数据库的交互逻辑,也不需要执行任何的代码,直接返回一个 html 页面就可以了,速度和性能非常高。
|
||||
- 坏处:仅仅适用于一些小型的网站,比如页面的规模在几十到几万不等。对于一些大型的电商网站,亿级数量的页面,你说你每次页面模板修改了,都需要将这么多页面全量静态化,靠谱吗?
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
用户浏览网页时,动态将 Nginx 本地数据渲染到本地 html 模板并返回给用户。
|
||||
|
||||
![e-commerce-website-detail-page-architecture-2](/img/e-commerce-website-detail-page-architecture-2.png)
|
||||
![e-commerce-website-detail-page-architecture-2](/images/e-commerce-website-detail-page-architecture-2.png)
|
||||
|
||||
|
||||
虽然没有直接返回 html 页面那么快,但是因为数据在本地缓存,所以也很快,其实耗费的也就是动态渲染一个 html 页面的性能。如果 html 模板发生了变更,不需要将所有的页面重新静态化,直接将数据渲染进最新的 html 页面模板后响应即可。
|
||||
|
@ -86,7 +86,7 @@ HystrixThreadPoolProperties.Setter().withCoreSize(int value);
|
||||
### queueSizeRejectionThreshold
|
||||
如果说线程池中的 10 个线程都在工作中,没有空闲的线程来做其它的事情,此时再有请求过来,会先进入队列积压。如果说队列积压满了,再有请求过来,就直接 reject,拒绝请求,执行 fallback 降级的逻辑,快速返回。
|
||||
|
||||
![hystrix-thread-pool-queue](/img/hystrix-thread-pool-queue.png)
|
||||
![hystrix-thread-pool-queue](/images/hystrix-thread-pool-queue.png)
|
||||
|
||||
控制 queue 满了之后 reject 的 threshold,因为 maxQueueSize 不允许热修改,因此提供这个参数可以热修改,控制队列的最大大小。
|
||||
|
||||
|
@ -33,7 +33,7 @@ Hystrix 是高可用性保障的一个框架。Netflix(可以认为是国外
|
||||
|
||||
调用服务 C,只需要 20ms,现在因为服务 C 故障了,比如延迟,或者挂了,此时线程会 hang 住 2s 左右。40 个线程全部被卡住,由于请求不断涌入,其它的线程也用来调用服务 C,同样也会被卡住。这样导致服务 B 的线程资源被耗尽,无法接收新的请求,甚至可能因为大量线程不断的运转,导致自己宕机。服务 A 也挂。
|
||||
|
||||
![service-invoke-road](/img/service-invoke-road.png)
|
||||
![service-invoke-road](/images/service-invoke-road.png)
|
||||
|
||||
Hystrix 可以对其进行资源隔离,比如限制服务 B 只有 40 个线程调用服务 C。当此 40 个线程被 hang 住时,其它 60 个线程依然能正常调用工作。从而确保整个系统不会被拖垮。
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
这里是整个 8 大步骤的流程图,我会对每个步骤进行细致的讲解。学习的过程中,对照着这个流程图,相信思路会比较清晰。
|
||||
|
||||
![hystrix-process](/img/hystrix-process.png)
|
||||
![hystrix-process](/images/hystrix-process.png)
|
||||
|
||||
### 步骤一:创建 command
|
||||
一个 HystrixCommand 或 HystrixObservableCommand 对象,代表了对某个依赖服务发起的一次请求或者调用。创建的时候,可以在构造函数中传入任何需要的参数。
|
||||
@ -64,7 +64,7 @@ final Future<R> delegate = toObservable().toBlocking().toFuture();
|
||||
|
||||
也就是说,先通过 toObservable() 获得 Future 对象,然后调用 Future 的 get() 方法。那么,其实无论是哪种方式执行 command,最终都是依赖于 toObservable() 去执行的。
|
||||
|
||||
![hystrix-process](/img/hystrix-process.png)
|
||||
![hystrix-process](/images/hystrix-process.png)
|
||||
|
||||
### 步骤三:检查是否开启缓存
|
||||
从这一步开始,就进入到 Hystrix 底层运行原理啦,看一下 Hystrix 一些更高级的功能和特性。
|
||||
@ -121,7 +121,7 @@ observable.subscribe(new Observer<ProductInfo>() {
|
||||
|
||||
如果没有 timeout,也正常执行的话,那么调用线程就会拿到一些调用依赖服务获取到的结果,然后 Hystrix 也会做一些 logging 记录和 metric 度量统计。
|
||||
|
||||
![hystrix-process](/img/hystrix-process.png)
|
||||
![hystrix-process](/images/hystrix-process.png)
|
||||
|
||||
### 步骤七:断路健康检查
|
||||
Hystrix 会把每一个依赖服务的调用成功、失败、Reject、Timeout 等事件发送给 circuit breaker 断路器。断路器就会对这些事件的次数进行统计,根据异常事件发生的比例来决定是否要进行断路(熔断)。如果打开了断路器,那么在接下来一段时间内,会直接断路,返回降级结果。
|
||||
|
@ -9,7 +9,7 @@ Hystrix command 执行时 8 大步骤第三步,就是检查 Request cache 是
|
||||
|
||||
举个栗子。比如说我们在一次请求上下文中,请求获取 productId 为 1 的数据,第一次缓存中没有,那么会从商品服务中获取数据,返回最新数据结果,同时将数据缓存在内存中。后续同一次请求上下文中,如果还有获取 productId 为 1 的数据的请求,直接从缓存中取就好了。
|
||||
|
||||
![hystrix-request-cache](/img/hystrix-request-cache.png)
|
||||
![hystrix-request-cache](/images/hystrix-request-cache.png)
|
||||
|
||||
HystrixCommand 和 HystrixObservableCommand 都可以指定一个缓存 key,然后 Hystrix 会自动进行缓存,接着在同一个 request context 内,再次访问的话,就会直接取用缓存。
|
||||
|
||||
|
@ -13,14 +13,14 @@ Hystrix 实现资源隔离,主要有两种技术:
|
||||
### 信号量机制
|
||||
信号量的资源隔离只是起到一个开关的作用,比如,服务 A 的信号量大小为 10,那么就是说它同时只允许有 10 个 tomcat 线程来访问服务 A,其它的请求都会被拒绝,从而达到资源隔离和限流保护的作用。
|
||||
|
||||
![hystrix-semphore](/img/hystrix-semphore.png)
|
||||
![hystrix-semphore](/images/hystrix-semphore.png)
|
||||
|
||||
### 线程池与信号量区别
|
||||
线程池隔离技术,并不是说去控制类似 tomcat 这种 web 容器的线程。更加严格的意义上来说,Hystrix 的线程池隔离技术,控制的是 tomcat 线程的执行。Hystrix 线程池满后,会确保说,tomcat 的线程不会因为依赖服务的接口调用延迟或故障而被 hang 住,tomcat 其它的线程不会卡死,可以快速返回,然后支撑其它的事情。
|
||||
|
||||
线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。
|
||||
|
||||
![hystrix-semphore-thread-pool](/img/hystrix-semphore-thread-pool.png)
|
||||
![hystrix-semphore-thread-pool](/images/hystrix-semphore-thread-pool.png)
|
||||
|
||||
**适用场景**:
|
||||
- **线程池技术**,适合绝大多数场景,比如说我们对依赖服务的网络请求的调用和访问、需要对调用的 timeout 进行控制(捕捉 timeout 超时异常)。
|
||||
|
@ -1,7 +1,7 @@
|
||||
## 深入 Hystrix 线程池隔离与接口限流
|
||||
前面讲了 Hystrix 的 request cache 请求缓存、fallback 优雅降级、circuit breaker 断路器快速熔断,这一讲,我们来详细说说 Hystrix 的线程池隔离与接口限流。
|
||||
|
||||
![hystrix-process](/img/hystrix-process.png)
|
||||
![hystrix-process](/images/hystrix-process.png)
|
||||
|
||||
Hystrix 通过判断线程池或者信号量是否已满,超出容量的请求,直接 Reject 走降级,从而达到限流的作用。
|
||||
|
||||
@ -12,7 +12,7 @@ Hystrix 采用了 Bulkhead Partition 舱壁隔离技术,来将外部依赖进
|
||||
|
||||
**舱壁隔离**,是说将船体内部空间区隔划分成若干个隔舱,一旦某几个隔舱发生破损进水,水流不会在其间相互流动,如此一来船舶在受损时,依然能具有足够的浮力和稳定性,进而减低立即沉船的危险。
|
||||
|
||||
![bulkhead-partition](/img/bulkhead-partition.jpg)
|
||||
![bulkhead-partition](/images/bulkhead-partition.jpg)
|
||||
|
||||
Hystrix 对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重,最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用。
|
||||
|
||||
@ -121,7 +121,7 @@ public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {
|
||||
}
|
||||
```
|
||||
|
||||
我们模拟 25 个请求。前 8 个请求,调用接口时会直接被 hang 住 3s,那么后面的 10 的请求会先进入等待队列中等待前面的请求执行完毕。最后的 7 个请求过来,会直接被 reject,调用 fallback 降级逻辑。
|
||||
我们模拟 25 个请求。前 8 个请求,调用接口时会直接被 hang 住 3s,那么后面的 10 个请求会先进入等待队列中等待前面的请求执行完毕。最后的 7 个请求过来,会直接被 reject,调用 fallback 降级逻辑。
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
|
@ -111,6 +111,6 @@ public String getProductInfos(String productIds) {
|
||||
|
||||
我们回过头来,看看 Hystrix 线程池技术是如何实现资源隔离的。
|
||||
|
||||
![hystrix-thread-pool-isolation](/img/hystrix-thread-pool-isolation.png)
|
||||
![hystrix-thread-pool-isolation](/images/hystrix-thread-pool-isolation.png)
|
||||
|
||||
从 Nginx 开始,缓存都失效了,那么 Nginx 通过缓存服务去调用商品服务。缓存服务默认的线程大小是 10 个,最多就只有 10 个线程去调用商品服务的接口。即使商品服务接口故障了,最多就只有 10 个线程会 hang 死在调用商品服务接口的路上,缓存服务的 tomcat 内其它的线程还是可以用来调用其它的服务,干其它的事情。
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@ -12,10 +12,10 @@
|
||||
|
||||
**适合的场景**:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你**并发不高,但是数据量太大**导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。
|
||||
|
||||
### uuid
|
||||
好处就是本地生成,不要基于数据库来了;不好之处就是,uuid 太长了,**作为主键性能太差**了,不适合用于主键。
|
||||
### UUID
|
||||
好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长了,**作为主键性能太差**了,另外 UUID 不具有有序性,会造成 B+ 树索引在写的时候有过多的随机写操作,频繁修改树结构,从而导致性能下降。
|
||||
|
||||
适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的。
|
||||
适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键是不能用 UUID 的。
|
||||
|
||||
```java
|
||||
UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf
|
||||
@ -24,10 +24,10 @@ UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf
|
||||
### 获取系统当前时间
|
||||
这个就是获取当前时间即可,但是问题是,**并发很高的时候**,比如一秒并发几千,**会有重复的情况**,这个是肯定不合适的。基本就不用考虑了。
|
||||
|
||||
适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。
|
||||
适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。
|
||||
|
||||
### snowflake 算法
|
||||
snowflake 算法是 twitter 开源的分布式 id 生成算法,就是把一个 64 位的 long 型的 id,1 个bit是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。
|
||||
snowflake 算法是 twitter 开源的分布式 id 生成算法,就是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。
|
||||
- 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
|
||||
- 41 bit:表示的是时间戳,单位是毫秒。41 bit 可以表示的数字多达 `2^41 - 1`,也就是可以标识 `2^41 - 1` 个毫秒值,换算成年就是表示69年的时间。
|
||||
- 10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10台机器上哪,也就是1024台机器。但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 `2^5`个机房(32个机房),每个机房里可以代表 `2^5` 个机器(32台机器)。
|
||||
@ -151,10 +151,10 @@ public class IdWorker {
|
||||
|
||||
```
|
||||
|
||||
怎么说呢,大概这个意思吧,就是说 41 bit 是当前毫秒单位的一个时间戳,就这意思;然后 5 bit 是你传递进来的一个机房 id(但是最大只能是32以内),5 bit 是你传递进来的机器 id(但是最大只能是32以内),剩下的那个 12 bit序列号,就是如果跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号以内。
|
||||
怎么说呢,大概这个意思吧,就是说 41 bit 是当前毫秒单位的一个时间戳,就这意思;然后 5 bit 是你传递进来的一个**机房** id(但是最大只能是 32 以内),另外 5 bit 是你传递进来的**机器** id(但是最大只能是 32 以内),剩下的那个 12 bit序列号,就是如果跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号以内。
|
||||
|
||||
所以你自己利用这个工具类,自己搞一个服务,然后对每个机房的每个机器都初始化这么一个东西,刚开始这个机房的这个机器的序号就是 0。然后每次接收到一个请求,说这个机房的这个机器要生成一个 id,你就找到对应的 Worker 生成。
|
||||
|
||||
利用这个 snowflake 算法,你可以开发自己公司的服务,甚至对于机房 id 和机器 id,反正给你预留了5 bit + 5 bit,你换成别的有业务含义的东西也可以的。
|
||||
利用这个 snowflake 算法,你可以开发自己公司的服务,甚至对于机房 id 和机器 id,反正给你预留了 5 bit + 5 bit,你换成别的有业务含义的东西也可以的。
|
||||
|
||||
这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。
|
@ -20,7 +20,7 @@
|
||||
|
||||
但是这个方案比较 low,谁都能干,我们来看看高大上一点的方案。
|
||||
|
||||
![database-shard-method-1](/img/database-shard-method-1.png)
|
||||
![database-shard-method-1](/images/database-shard-method-1.png)
|
||||
|
||||
### 双写迁移方案
|
||||
这个是我们常用的一种迁移方案,比较靠谱一些,不用停机,不用看北京凌晨 4 点的风景。
|
||||
@ -33,4 +33,4 @@
|
||||
|
||||
接着当数据完全一致了,就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干的。
|
||||
|
||||
![database-shard-method-2](/img/database-shard-method-2.png)
|
||||
![database-shard-method-2](/images/database-shard-method-2.png)
|
@ -80,11 +80,11 @@ mycat 这种 proxy 层方案的**缺点在于需要部署**,自己运维一套
|
||||
|
||||
**水平拆分**的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。
|
||||
|
||||
![database-split-horizon](/img/database-split-horizon.png)
|
||||
![database-split-horizon](/images/database-split-horizon.png)
|
||||
|
||||
**垂直拆分**的意思,就是**把一个有很多字段的表给拆分成多个表**,**或者是多个库上去**。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会**将较少的访问频率很高的字段放到一个表里去**,然后**将较多的访问频率很低的字段放到另外一个表里去**。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。
|
||||
|
||||
![database-split-vertically](/img/database-split-vertically.png)
|
||||
![database-split-vertically](/images/database-split-vertically.png)
|
||||
|
||||
这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。
|
||||
|
||||
|
@ -27,14 +27,14 @@ index 相当于 mysql 里的一张表。而 type 没法跟 mysql 里去对比,
|
||||
|
||||
很多情况下,一个 index 里可能就一个 type,但是确实如果说是一个 index 里有多个 type 的情况,你可以认为 index 是一个类别的表,具体的每个 type 代表了具体的一个 mysql 中的表。每个 type 有一个 mapping,如果你认为一个 type 是一个具体的一个表,index 代表多个 type 的同属于的一个类型,mapping 就是这个 type 的**表结构定义**,你在 mysql 中创建一个表,肯定是要定义表结构的,里面有哪些字段,每个字段是什么类型。实际上你往 index 里的一个 type 里面写的一条数据,叫做一条 document,一条 document 就代表了 mysql 中某个表里的一行,每个 document 有多个 field,每个 field 就代表了这个 document 中的一个字段的值。
|
||||
|
||||
![es-index-type-mapping-document-field](/img/es-index-type-mapping-document-field.png)
|
||||
![es-index-type-mapping-document-field](/images/es-index-type-mapping-document-field.png)
|
||||
|
||||
你搞一个索引,这个索引可以拆分成多个 `shard`,每个 shard 存储部分数据。
|
||||
|
||||
|
||||
接着就是这个 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 `primary shard`,负责写入数据,但是还有几个 `replica shard`。`primary shard` 写入数据之后,会将数据同步到其他几个 `replica shard` 上去。
|
||||
|
||||
![es-cluster](/img/es-cluster.png)
|
||||
![es-cluster](/images/es-cluster.png)
|
||||
|
||||
通过这个 replica 的方案,每个 shard 的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。
|
||||
|
||||
|
@ -47,7 +47,7 @@ Node 是集群中的一个节点,节点也有一个名称,默认是随机分
|
||||
|
||||
这么说吧,shard 分为 primary shard 和 replica shard。而 primary shard 一般简称为 shard,而 replica shard 一般简称为 replica。
|
||||
|
||||
![es-cluster-0](/img/es-cluster-0.png)
|
||||
![es-cluster-0](/images/es-cluster-0.png)
|
||||
|
||||
## es 核心概念 vs. db 核心概念
|
||||
| es | db |
|
||||
|
@ -12,7 +12,7 @@ es 在数据量很大的情况下(数十亿级别)如何提高查询效率
|
||||
### 性能优化的杀手锏——filesystem cache
|
||||
你往 es 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 `filesystem cache` 里面去。
|
||||
|
||||
![es-search-process](/img/es-search-process.png)
|
||||
![es-search-process](/images/es-search-process.png)
|
||||
|
||||
es 的搜索引擎严重依赖于底层的 `filesystem cache`,你如果给 `filesystem cache` 更多的内存,尽量让内存可以容纳所有的 `idx segment file ` 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。
|
||||
|
||||
|
@ -13,7 +13,7 @@ es 写入数据的工作原理是什么啊?es 查询数据的工作原理是
|
||||
- 实际的 node 上的 `primary shard` 处理请求,然后将数据同步到 `replica node`。
|
||||
- `coordinating node` 如果发现 `primary node` 和所有 `replica node` 都搞定之后,就返回响应结果给客户端。
|
||||
|
||||
![es-write](/img/es-write.png)
|
||||
![es-write](/images/es-write.png)
|
||||
|
||||
### es 读数据过程
|
||||
可以通过 `doc id` 来查询,会根据 `doc id` 进行 hash,判断出来当时把 `doc id` 分配到了哪个 shard 上面去,从那个 shard 去查询。
|
||||
@ -41,7 +41,7 @@ j2ee特别牛
|
||||
|
||||
### 写数据底层原理
|
||||
|
||||
![es-write-detail](/img/es-write-detail.png)
|
||||
![es-write-detail](/images/es-write-detail.png)
|
||||
|
||||
先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
||||
- 读写分离
|
||||
- ElasticSearch
|
||||
|
||||
![high-concurrency-system-design](/img/high-concurrency-system-design.png)
|
||||
![high-concurrency-system-design](/images/high-concurrency-system-design.png)
|
||||
|
||||
### 系统拆分
|
||||
将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。
|
||||
|
@ -22,7 +22,7 @@ RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模
|
||||
#### 普通集群模式(无高可用性)
|
||||
普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你**创建的 queue,只会放在一个 RabbitMQ 实例上**,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。
|
||||
|
||||
![mq-7](/img/mq-7.png)
|
||||
![mq-7](/images/mq-7.png)
|
||||
|
||||
这种方式确实很麻烦,也不怎么好,**没做到所谓的分布式**,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有**数据拉取的开销**,后者导致**单实例性能瓶颈**。
|
||||
|
||||
@ -33,7 +33,7 @@ RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模
|
||||
#### 镜像集群模式(高可用性)
|
||||
这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个**完整镜像**,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把**消息同步**到多个实例的 queue 上。
|
||||
|
||||
![mq-8](/img/mq-8.png)
|
||||
![mq-8](/images/mq-8.png)
|
||||
|
||||
那么**如何开启这个镜像集群模式**呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是**镜像集群模式的策略**,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
|
||||
|
||||
@ -50,11 +50,11 @@ Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了
|
||||
|
||||
比如说,我们假设创建了一个 topic,指定其 partition 数量是 3 个,分别在三台机器上。但是,如果第二台机器宕机了,会导致这个 topic 的 1/3 的数据就丢了,因此这个是做不到高可用的。
|
||||
|
||||
![kafka-before](/img/kafka-before.png)
|
||||
![kafka-before](/images/kafka-before.png)
|
||||
|
||||
Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,**要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题**,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。
|
||||
|
||||
![kafka-after](/img/kafka-after.png)
|
||||
![kafka-after](/images/kafka-after.png)
|
||||
|
||||
这么搞,就有所谓的**高可用性**了,因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的,如果这上面有某个 partition 的 leader,那么此时会从 follower 中**重新选举**一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。
|
||||
|
||||
|
@ -13,7 +13,7 @@ Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一
|
||||
|
||||
但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次。
|
||||
|
||||
![mq-10](/img/mq-10.png)
|
||||
![mq-10](/images/mq-10.png)
|
||||
|
||||
举个栗子。
|
||||
|
||||
@ -39,6 +39,6 @@ Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一
|
||||
- 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
|
||||
- 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。
|
||||
|
||||
![mq-11](/img/mq-11.png)
|
||||
![mq-11](/images/mq-11.png)
|
||||
|
||||
当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。
|
@ -14,19 +14,19 @@
|
||||
先看看顺序会错乱的俩场景:
|
||||
- **RabbitMQ**:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。
|
||||
|
||||
![rabbitmq-order-01](/img/rabbitmq-order-01.png)
|
||||
![rabbitmq-order-01](/images/rabbitmq-order-01.png)
|
||||
|
||||
- **Kafka**:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。<br>消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞**多个线程来并发处理消息**。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
|
||||
|
||||
![kafka-order-01](/img/kafka-order-01.png)
|
||||
![kafka-order-01](/images/kafka-order-01.png)
|
||||
|
||||
### 解决方案
|
||||
#### RabbitMQ
|
||||
拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
|
||||
![rabbitmq-order-02](/img/rabbitmq-order-02.png)
|
||||
![rabbitmq-order-02](/images/rabbitmq-order-02.png)
|
||||
|
||||
#### Kafka
|
||||
- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
|
||||
- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
|
||||
|
||||
![kafka-order-02](/img/kafka-order-02.png)
|
||||
![kafka-order-02](/images/kafka-order-02.png)
|
@ -10,7 +10,7 @@
|
||||
数据的丢失问题,可能出现在生产者、MQ、消费者中,咱们从 RabbitMQ 和 Kafka 分别来分析一下吧。
|
||||
|
||||
### RabbitMQ
|
||||
![rabbitmq-message-lose](/img/rabbitmq-message-lose.png)
|
||||
![rabbitmq-message-lose](/images/rabbitmq-message-lose.png)
|
||||
|
||||
#### 生产者弄丢了数据
|
||||
|
||||
@ -61,7 +61,7 @@ RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费
|
||||
|
||||
这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack`,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
|
||||
|
||||
![rabbitmq-message-lose-solution](/img/rabbitmq-message-lose-solution.png)
|
||||
![rabbitmq-message-lose-solution](/images/rabbitmq-message-lose-solution.png)
|
||||
|
||||
### Kafka
|
||||
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |