docs: update UUID desc to fix #22, rename images

- Update UUID desc to fix #22
- Rename img to images
- Fix typo
This commit is contained in:
yanglbme
2019-01-07 15:44:02 +08:00
parent 1caf071b59
commit a7cb243259
248 changed files with 93 additions and 94 deletions

View File

@@ -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 型的 id1 个bit是不用的用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id12 bit 作为序列号。
snowflake 算法是 twitter 开源的分布式 id 生成算法,就是把一个 64 位的 long 型的 id1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id12 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 代表机房 id5 个 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 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。

View File

@@ -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)

View File

@@ -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)
这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。

View File

@@ -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 的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。

View File

@@ -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 |

View File

@@ -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 ` 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。

View 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 日志文件。

View File

@@ -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 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。

View File

@@ -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 即可。这就有所谓的高可用性了。

View File

@@ -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 的消费是幂等性的,需要结合具体的业务来看。

View File

@@ -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)

View File

@@ -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

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -11,7 +11,7 @@
### MySQL 主从复制原理的是啥?
主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay 中继日志中。接着从库中有一个 SQL 线程会从中继日志读取 binlog然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL这样就可以保证自己跟主库的数据是一样的。
![mysql-master-slave](/img/mysql-master-slave.png)
![mysql-master-slave](/images/mysql-master-slave.png)
这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是**有延时**的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

View File

@@ -10,7 +10,7 @@
这就是缓存雪崩。
![redis-caching-avalanche](/img/redis-caching-avalanche.png)
![redis-caching-avalanche](/images/redis-caching-avalanche.png)
大约在 3 年前,国内比较知名的一个互联网公司,曾因为缓存事故,导致雪崩,后台系统全部崩溃,事故从当天下午持续到晚上凌晨 3~4 点,公司损失了几千万。
@@ -19,7 +19,7 @@
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 事后redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
![redis-caching-avalanche-solution](/img/redis-caching-avalanche-solution.png)
![redis-caching-avalanche-solution](/images/redis-caching-avalanche-solution.png)
用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。
@@ -37,6 +37,6 @@
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
![redis-caching-penetration](/img/redis-caching-penetration.png)
![redis-caching-penetration](/images/redis-caching-penetration.png)
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 `set -999 UNKNOWN`。这样的话,下次便能走缓存了。

View File

@@ -9,7 +9,7 @@ redis 的并发竞争问题是什么?如何解决这个问题?了解 redis
## 面试题剖析
某个时刻,多个系统实例都去更新某个 key。可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key别人都不允许读和写。
![zookeeper-distributed-lock](/img/zookeeper-distributed-lock.png)
![zookeeper-distributed-lock](/images/zookeeper-distributed-lock.png)
你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。

View File

@@ -26,11 +26,11 @@ redis cluster主要是针对**海量数据+高并发+高可用**的场景。r
- redis cluster 节点间采用 gossip 协议进行通信
集中式是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 `storm`。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper分布式协调的中间件对所有元数据进行存储维护。
![zookeeper-centralized-storage](/img/zookeeper-centralized-storage.png)
![zookeeper-centralized-storage](/images/zookeeper-centralized-storage.png)
redis 维护集群元数据采用另一个方式, `gossip` 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
![redis-gossip](/img/redis-gossip.png)
![redis-gossip](/images/redis-gossip.png)
**集中式**的**好处**在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;**不好**在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
@@ -69,7 +69,7 @@ ping 时要携带一些元数据,如果很频繁,可能会加重网络负担
#### hash 算法
来了一个 key首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致**大部分的请求过来,全部无法拿到有效的缓存**,导致大量的流量涌入数据库。
![hash](/img/hash.png)
![hash](/images/hash.png)
#### 一致性 hash 算法
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
@@ -80,7 +80,7 @@ ping 时要携带一些元数据,如果很频繁,可能会加重网络负担
燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成**缓存热点**的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布负载均衡。
![consistent-hashing-algorithm](/img/consistent-hashing-algorithm.png)
![consistent-hashing-algorithm](/images/consistent-hashing-algorithm.png)
#### redis cluster 的 hash slot 算法
redis cluster 有固定的 `16384` 个 hash slot对每个 `key` 计算 `CRC16` 值,然后对 `16384` 取模,可以获取 key 对应的 hash slot。
@@ -89,7 +89,7 @@ redis cluster 中每个 master 都会持有部分 slot比如有 3 个 master
任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot不是机器。
![hash-slot](/img/hash-slot.png)
![hash-slot](/images/hash-slot.png)
### redis cluster 的高可用与主备切换原理
redis cluster 的高可用的原理,几乎跟哨兵是类似的

View File

@@ -29,7 +29,7 @@
### 最初级的缓存不一致问题及解决方案
问题:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
![redis-junior-inconsistent](/img/redis-junior-inconsistent.png)
![redis-junior-inconsistent](/images/redis-junior-inconsistent.png)
解决思路:先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。

View File

@@ -2,7 +2,7 @@
单机的 redis能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑**读高并发**的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的**读请求全部走从节点**。这样也可以很轻松实现水平扩容,**支撑读高并发**。
![redis-master-slave](/img/redis-master-slave.png)
![redis-master-slave](/images/redis-master-slave.png)
redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发
@@ -23,7 +23,7 @@ redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并
如果这是 slave node 初次连接到 master node那么会触发一次 `full resynchronization` 全量复制。此时 master 会启动一个后台线程,开始生成一份 `RDB` 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。`RDB` 文件生成完毕后, master 会将这个 `RDB` 发送给 slaveslave 会先**写入本地磁盘,然后再从本地磁盘加载到内存**中,接着 master 会将内存中缓存的写命令发送到 slaveslave 也会同步这些数据。slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。
![redis-master-slave-replication](/img/redis-master-slave-replication.png)
![redis-master-slave-replication](/images/redis-master-slave-replication.png)
### 主从复制的断点续传
从 redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
@@ -49,7 +49,7 @@ slave node 启动时,会在自己本地保存 master node 的信息,包括 m
slave node 内部有个定时任务,每秒检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立 socket 网络连接。然后 slave node 发送 `ping` 命令给 master node。如果 master 设置了 requirepass那么 slave node 必须发送 masterauth 的口令过去进行认证。master node **第一次执行全量复制**将所有数据发给slave node。而在后续master node 持续将写命令,异步复制给 slave node。
![redis-master-slave-replication-detail](/img/redis-master-slave-replication-detail.png)
![redis-master-slave-replication-detail](/images/redis-master-slave-replication-detail.png)
### 全量复制
- master 执行 bgsave ,在本地生成一份 rdb 快照文件。

View File

@@ -54,14 +54,14 @@ sentinel中文名是哨兵。哨兵是 redis 集群机构中非常重要的
- 异步复制导致的数据丢失
因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slavemaster 就宕机了,此时这部分数据就丢失了。
![async-replication-data-lose-case](/img/async-replication-data-lose-case.png)
![async-replication-data-lose-case](/images/async-replication-data-lose-case.png)
- 脑裂导致的数据丢失
脑裂,也就是说,某个 master 所在机器突然**脱离了正常的网络**,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会**认为** master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候集群里就会有两个 master ,也就是所谓的**脑裂**。
此时虽然某个 slave 被切换成了master但是可能 client 还没来得及切换到新的 master还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
![redis-cluster-split-brain](/img/redis-cluster-split-brain.png)
![redis-cluster-split-brain](/images/redis-cluster-split-brain.png)
### 数据丢失问题的解决方案
进行如下配置:

View File

@@ -32,7 +32,7 @@ redis 内部使用文件事件处理器 `file event handler`,这个文件事
来看客户端与 redis 的一次通信过程:
![redis-single-thread-model](/img/redis-single-thread-model.png)
![redis-single-thread-model](/images/redis-single-thread-model.png)
客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个 `AE_READABLE` 事件IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给**连接应答处理器**。连接应答处理器会创建一个能与客户端通信的 socket01并将该 socket01 的 `AE_READABLE` 事件与命令请求处理器关联。

View File

@@ -28,13 +28,13 @@
#### 解耦
看这么个场景。A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢A 系统负责人几乎崩溃......
![mq-1](/img/mq-1.png)
![mq-1](/images/mq-1.png)
在这个场景中A 系统跟其它各种乱七八糟的系统严重耦合A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊!
如果使用 MQA 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。
![mq-2](/img/mq-2.png)
![mq-2](/images/mq-2.png)
**总结**:通过一个 MQPub/Sub 发布订阅消息这么一个模型A 系统就跟其它系统彻底解耦了。
@@ -43,13 +43,13 @@
#### 异步
再来看一个场景A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3msBCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms接近 1s用户感觉搞个什么东西慢死了慢死了。用户通过浏览器发起请求等待个 1s这几乎是不可接受的。
![mq-3](/img/mq-3.png)
![mq-3](/images/mq-3.png)
一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。
如果**使用 MQ**,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5msA 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms对于用户而言其实感觉上就是点个按钮8ms 以后就直接返回了,爽!网站做得真好,真快!
![mq-4](/img/mq-4.png)
![mq-4](/images/mq-4.png)
#### 削峰
每天 0:00 到 12:00A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL的大量的请求涌入 MySQL每秒钟对 MySQL 执行约 5k 条 SQL。
@@ -58,11 +58,11 @@
但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。
![mq-5](/img/mq-5.png)
![mq-5](/images/mq-5.png)
如果使用 MQ每秒 5k 个请求写入 MQA 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok这样下来哪怕是高峰期的时候A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去结果就导致在中午高峰期1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。
![mq-6](/img/mq-6.png)
![mq-6](/images/mq-6.png)
这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说只要高峰期一过A 系统就会快速将积压的消息给解决掉。