Merge branch 'master' into master
24
.github/workflows/sync.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: Sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Sync to Gitee
|
||||
uses: wearerequired/git-mirror-action@master
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
|
||||
with:
|
||||
source-repo: "git@github.com:doocs/advanced-java.git"
|
||||
destination-repo: "git@gitee.com:Doocs/advanced-java.git"
|
||||
|
||||
- name: Build Gitee Pages
|
||||
uses: yanglbme/gitee-pages-action@master
|
||||
with:
|
||||
gitee-username: yanglbme
|
||||
gitee-password: ${{ secrets.GITEE_PASSWORD }}
|
||||
gitee-repo: doocs/advanced-java
|
248
README.md
@ -1,126 +1,218 @@
|
||||
# 互联网 Java 工程师进阶知识完全扫盲<sup>[©](https://github.com/yanglbme)</sup>
|
||||
|
||||
[](https://github.com/doocs/advanced-java/actions)
|
||||
[](https://github.com/doocs/advanced-java/blob/master/LICENSE)
|
||||
[](https://github.com/doocs/advanced-java)
|
||||
[](/docs/from-readers/rights-defending-movement.md)
|
||||
[](https://github.com/doocs/advanced-java/issues/70)
|
||||
[](#公众号)
|
||||
[](https://github.com/doocs/technical-books)
|
||||
[](https://github.com/doocs/leetcode)
|
||||
[](https://doocs.github.io/#/?id=how-to-join)
|
||||
[](/docs/extra-page/rights-defending-action.md)
|
||||
[](https://github.com/doocs/advanced-java/stargazers)
|
||||
[](https://github.com/doocs/advanced-java/tree/master/docs/from-readers#contributors)
|
||||
[](https://github.com/doocs/advanced-java/labels/help%20wanted)
|
||||
[](https://github.com/doocs/advanced-java/issues)
|
||||
[](http://makeapullrequest.com)
|
||||
[](https://doocs.github.io/#/?id=how-to-join)
|
||||
|
||||
本项目大部分内容来自中华石杉,版权归作者所有,内容涵盖[高并发](#高并发架构)、[分布式](#分布式系统)、[高可用](#高可用架构)、[微服务](#微服务架构)等领域知识。我([@yanglbme](https://github.com/yanglbme))对这部分知识做了一个系统的整理,方便学习查阅。配合《[大型网站技术架构](https://github.com/doocs/technical-books#architecture)——李智慧》、《[Redis 设计与实现](https://github.com/doocs/technical-books#database)——[黄健宏](https://github.com/huangz1990)》、《[Redis 深度历险](https://github.com/doocs/technical-books#database)——钱文品》、《[亿级流量网站架构核心技术](https://github.com/doocs/technical-books#architecture)——张开涛》食用,[效果更佳](https://doocs.gitee.io/advanced-java/#/offer)。
|
||||
本项目大部分内容来自中华石杉,版权归作者所有,内容涵盖[高并发](#高并发架构)、[分布式](#分布式系统)、[高可用](#高可用架构)、[微服务](#微服务架构)、[海量数据处理](#海量数据处理)等领域知识。[我](https://github.com/yanglbme)对这部分知识做了一个系统的整理,方便学习查阅。
|
||||
|
||||
学习之前,先来看看 [Issues 讨论区](https://github.com/doocs/advanced-java/issues/9#issue-394275038)的技术面试官是怎么说的吧。本项目也欢迎各位开发者朋友到 [Issues 讨论区](https://github.com/doocs/advanced-java/issues)分享自己的一些想法和实践经验,参与或加入开源组织请看[这里](https://github.com/doocs/advanced-java/issues/61),你也访问 [GitHub Page](https://doocs.github.io) 详细了解一下 Doocs。
|
||||
学习之前,先来看看 [Issues 讨论区](https://github.com/doocs/advanced-java/issues/9#issue-394275038)的技术面试官是怎么说的吧。本项目也欢迎各位开发者朋友到 Issues 讨论区分享自己的一些想法和实践经验。
|
||||
|
||||
* Netlify: https://adjava.netlify.app
|
||||
* Gitee Pages: https://doocs.gitee.io/advanced-java
|
||||
* GitHub Pages: https://doocs.github.io/advanced-java
|
||||
|
||||
## 高并发架构
|
||||
### [消息队列](/docs/high-concurrency/mq-interview.md)
|
||||
- [为什么使用消息队列?消息队列有什么优点和缺点?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?](/docs/high-concurrency/why-mq.md)
|
||||
- [如何保证消息队列的高可用?](/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)
|
||||
- [如何保证消息不被重复消费?(如何保证消息消费的幂等性)](/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)
|
||||
- [如何保证消息的可靠性传输?(如何处理消息丢失的问题)](/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)
|
||||
- [如何保证消息的顺序性?](/docs/high-concurrency/how-to-ensure-the-order-of-messages.md)
|
||||
- [如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?](/docs/high-concurrency/mq-time-delay-and-expired-failure.md)
|
||||
- [如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。](/docs/high-concurrency/mq-design.md)
|
||||
|
||||
### [搜索引擎](/docs/high-concurrency/es-introduction.md)
|
||||
- [es 的分布式架构原理能说一下么(es 是如何实现分布式的啊)?](/docs/high-concurrency/es-architecture.md)
|
||||
- [es 写入数据的工作原理是什么啊?es 查询数据的工作原理是什么啊?底层的 lucene 介绍一下呗?倒排索引了解吗?](/docs/high-concurrency/es-write-query-search.md)
|
||||
- [es 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?](/docs/high-concurrency/es-optimizing-query-performance.md)
|
||||
- [es 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?](/docs/high-concurrency/es-production-cluster.md)
|
||||
### [消息队列](./docs/high-concurrency/mq-interview.md)
|
||||
|
||||
* [为什么使用消息队列?消息队列有什么优点和缺点?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?](./docs/high-concurrency/why-mq.md)
|
||||
* [如何保证消息队列的高可用?](./docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)
|
||||
* [如何保证消息不被重复消费?(如何保证消息消费的幂等性)](./docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)
|
||||
* [如何保证消息的可靠性传输?(如何处理消息丢失的问题)](./docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)
|
||||
* [如何保证消息的顺序性?](./docs/high-concurrency/how-to-ensure-the-order-of-messages.md)
|
||||
* [如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?](./docs/high-concurrency/mq-time-delay-and-expired-failure.md)
|
||||
* [如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。](./docs/high-concurrency/mq-design.md)
|
||||
|
||||
### [搜索引擎](./docs/high-concurrency/es-introduction.md)
|
||||
|
||||
* [ES 的分布式架构原理能说一下么(ES 是如何实现分布式的啊)?](./docs/high-concurrency/es-architecture.md)
|
||||
* [ES 写入数据的工作原理是什么啊?ES 查询数据的工作原理是什么啊?底层的 Lucene 介绍一下呗?倒排索引了解吗?](./docs/high-concurrency/es-write-query-search.md)
|
||||
* [ES 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?](./docs/high-concurrency/es-optimizing-query-performance.md)
|
||||
* [ES 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?](./docs/high-concurrency/es-production-cluster.md)
|
||||
|
||||
### 缓存
|
||||
- [在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?](/docs/high-concurrency/why-cache.md)
|
||||
- [Redis 和 Memcached 有什么区别?Redis 的线程模型是什么?为什么单线程的 Redis 比多线程的 Memcached 效率要高得多?](/docs/high-concurrency/redis-single-thread-model.md)
|
||||
- [Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?](/docs/high-concurrency/redis-data-types.md)
|
||||
- [Redis 的过期策略都有哪些?手写一下 LRU 代码实现?](/docs/high-concurrency/redis-expiration-policies-and-lru.md)
|
||||
- [如何保证 Redis 高并发、高可用?Redis 的主从复制原理能介绍一下么?Redis 的哨兵原理能介绍一下么?](/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md)
|
||||
- [Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?](/docs/high-concurrency/redis-persistence.md)
|
||||
- [Redis 集群模式的工作原理能说一下么?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?如何动态增加和删除一个节点?](/docs/high-concurrency/redis-cluster.md)
|
||||
- [了解什么是 redis 的雪崩、穿透和击穿?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?](/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md)
|
||||
- [如何保证缓存与数据库的双写一致性?](/docs/high-concurrency/redis-consistence.md)
|
||||
- [Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?](/docs/high-concurrency/redis-cas.md)
|
||||
- [生产环境中的 Redis 是怎么部署的?](/docs/high-concurrency/redis-production-environment.md)
|
||||
|
||||
* [在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?](./docs/high-concurrency/why-cache.md)
|
||||
* [Redis 和 Memcached 有什么区别?Redis 的线程模型是什么?为什么单线程的 Redis 比多线程的 Memcached 效率要高得多?](./docs/high-concurrency/redis-single-thread-model.md)
|
||||
* [Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?](./docs/high-concurrency/redis-data-types.md)
|
||||
* [Redis 的过期策略都有哪些?手写一下 LRU 代码实现?](./docs/high-concurrency/redis-expiration-policies-and-lru.md)
|
||||
* [如何保证 Redis 高并发、高可用?Redis 的主从复制原理能介绍一下么?Redis 的哨兵原理能介绍一下么?](./docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md)
|
||||
* [Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?](./docs/high-concurrency/redis-persistence.md)
|
||||
* [Redis 集群模式的工作原理能说一下么?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?如何动态增加和删除一个节点?](./docs/high-concurrency/redis-cluster.md)
|
||||
* [了解什么是 Redis 的雪崩、穿透和击穿?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?](./docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md)
|
||||
* [如何保证缓存与数据库的双写一致性?](./docs/high-concurrency/redis-consistence.md)
|
||||
* [Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?](./docs/high-concurrency/redis-cas.md)
|
||||
* [生产环境中的 Redis 是怎么部署的?](./docs/high-concurrency/redis-production-environment.md)
|
||||
|
||||
### 分库分表
|
||||
- [为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?](/docs/high-concurrency/database-shard.md)
|
||||
- [现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?](/docs/high-concurrency/database-shard-method.md)
|
||||
- [如何设计可以动态扩容缩容的分库分表方案?](/docs/high-concurrency/database-shard-dynamic-expand.md)
|
||||
- [分库分表之后,id 主键如何处理?](/docs/high-concurrency/database-shard-global-id-generate.md)
|
||||
|
||||
* [为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?](./docs/high-concurrency/database-shard.md)
|
||||
* [现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?](./docs/high-concurrency/database-shard-method.md)
|
||||
* [如何设计可以动态扩容缩容的分库分表方案?](./docs/high-concurrency/database-shard-dynamic-expand.md)
|
||||
* [分库分表之后,id 主键如何处理?](./docs/high-concurrency/database-shard-global-id-generate.md)
|
||||
|
||||
### 读写分离
|
||||
- [如何实现 MySQL 的读写分离?MySQL 主从复制原理是啥?如何解决 MySQL 主从同步的延时问题?](/docs/high-concurrency/mysql-read-write-separation.md)
|
||||
|
||||
* [如何实现 MySQL 的读写分离?MySQL 主从复制原理是啥?如何解决 MySQL 主从同步的延时问题?](./docs/high-concurrency/mysql-read-write-separation.md)
|
||||
|
||||
### 高并发系统
|
||||
- [如何设计一个高并发系统?](/docs/high-concurrency/high-concurrency-design.md)
|
||||
|
||||
* [如何设计一个高并发系统?](./docs/high-concurrency/high-concurrency-design.md)
|
||||
|
||||
## 分布式系统
|
||||
### [面试连环炮](/docs/distributed-system/distributed-system-interview.md)
|
||||
|
||||
### [面试连环炮](./docs/distributed-system/distributed-system-interview.md)
|
||||
|
||||
### 系统拆分
|
||||
- [为什么要进行系统拆分?如何进行系统拆分?拆分后不用 Dubbo 可以吗?](/docs/distributed-system/why-dubbo.md)
|
||||
|
||||
* [为什么要进行系统拆分?如何进行系统拆分?拆分后不用 Dubbo 可以吗?](./docs/distributed-system/why-dubbo.md)
|
||||
|
||||
### 分布式服务框架
|
||||
- [说一下 Dubbo 的工作原理?注册中心挂了可以继续通信吗?](/docs/distributed-system/dubbo-operating-principle.md)
|
||||
- [Dubbo 支持哪些序列化协议?说一下 Hessian 的数据结构?PB 知道吗?为什么 PB 的效率是最高的?](/docs/distributed-system/dubbo-serialization-protocol.md)
|
||||
- [Dubbo 负载均衡策略和集群容错策略都有哪些?动态代理策略呢?](/docs/distributed-system/dubbo-load-balancing.md)
|
||||
- [Dubbo 的 spi 思想是什么?](/docs/distributed-system/dubbo-spi.md)
|
||||
- [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](/docs/distributed-system/dubbo-service-management.md)
|
||||
- [分布式服务接口的幂等性如何设计(比如不能重复扣款)?](/docs/distributed-system/distributed-system-idempotency.md)
|
||||
- [分布式服务接口请求的顺序性如何保证?](/docs/distributed-system/distributed-system-request-sequence.md)
|
||||
- [如何自己设计一个类似 Dubbo 的 RPC 框架?](/docs/distributed-system/dubbo-rpc-design.md)
|
||||
|
||||
* [说一下 Dubbo 的工作原理?注册中心挂了可以继续通信吗?](./docs/distributed-system/dubbo-operating-principle.md)
|
||||
* [Dubbo 支持哪些序列化协议?说一下 Hessian 的数据结构?PB 知道吗?为什么 PB 的效率是最高的?](./docs/distributed-system/dubbo-serialization-protocol.md)
|
||||
* [Dubbo 负载均衡策略和集群容错策略都有哪些?动态代理策略呢?](./docs/distributed-system/dubbo-load-balancing.md)
|
||||
* [Dubbo 的 spi 思想是什么?](./docs/distributed-system/dubbo-spi.md)
|
||||
* [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](./docs/distributed-system/dubbo-service-management.md)
|
||||
* [分布式服务接口的幂等性如何设计(比如不能重复扣款)?](./docs/distributed-system/distributed-system-idempotency.md)
|
||||
* [分布式服务接口请求的顺序性如何保证?](./docs/distributed-system/distributed-system-request-sequence.md)
|
||||
* [如何自己设计一个类似 Dubbo 的 RPC 框架?](./docs/distributed-system/dubbo-rpc-design.md)
|
||||
* [CAP定理的P是什么](./docs/distributed-system/distributed-system-cap.md)
|
||||
|
||||
### 分布式锁
|
||||
- [Zookeeper 都有哪些应用场景?](/docs/distributed-system/zookeeper-application-scenarios.md)
|
||||
- [使用 Redis 如何设计分布式锁?使用 Zookeeper 来设计分布式锁可以吗?以上两种分布式锁的实现方式哪种效率比较高?](/docs/distributed-system/distributed-lock-redis-vs-zookeeper.md)
|
||||
|
||||
* [Zookeeper 都有哪些应用场景?](./docs/distributed-system/zookeeper-application-scenarios.md)
|
||||
* [使用 Redis 如何设计分布式锁?使用 Zookeeper 来设计分布式锁可以吗?以上两种分布式锁的实现方式哪种效率比较高?](./docs/distributed-system/distributed-lock-redis-vs-zookeeper.md)
|
||||
|
||||
### 分布式事务
|
||||
- [分布式事务了解吗?你们如何解决分布式事务问题的?TCC 如果出现网络连不通怎么办?XA 的一致性如何保证?](/docs/distributed-system/distributed-transaction.md)
|
||||
|
||||
* [分布式事务了解吗?你们如何解决分布式事务问题的?TCC 如果出现网络连不通怎么办?XA 的一致性如何保证?](./docs/distributed-system/distributed-transaction.md)
|
||||
|
||||
### 分布式会话
|
||||
- [集群部署时的分布式 Session 如何实现?](/docs/distributed-system/distributed-session.md)
|
||||
|
||||
* [集群部署时的分布式 Session 如何实现?](./docs/distributed-system/distributed-session.md)
|
||||
|
||||
## 高可用架构
|
||||
- [Hystrix 介绍](/docs/high-availability/hystrix-introduction.md)
|
||||
- [电商网站详情页系统架构](/docs/high-availability/e-commerce-website-detail-page-architecture.md)
|
||||
- [Hystrix 线程池技术实现资源隔离](/docs/high-availability/hystrix-thread-pool-isolation.md)
|
||||
- [Hystrix 信号量机制实现资源隔离](/docs/high-availability/hystrix-semphore-isolation.md)
|
||||
- [Hystrix 隔离策略细粒度控制](/docs/high-availability/hystrix-execution-isolation.md)
|
||||
- [深入 Hystrix 执行时内部原理](/docs/high-availability/hystrix-process.md)
|
||||
- [基于 request cache 请求缓存技术优化批量商品数据查询接口](/docs/high-availability/hystrix-request-cache.md)
|
||||
- [基于本地缓存的 fallback 降级机制](/docs/high-availability/hystrix-fallback.md)
|
||||
- [深入 Hystrix 断路器执行原理](/docs/high-availability/hystrix-circuit-breaker.md)
|
||||
- [深入 Hystrix 线程池隔离与接口限流](/docs/high-availability/hystrix-thread-pool-current-limiting.md)
|
||||
- [基于 timeout 机制为服务接口调用超时提供安全保护](/docs/high-availability/hystrix-timeout.md)
|
||||
|
||||
* [Hystrix 介绍](./docs/high-availability/hystrix-introduction.md)
|
||||
* [电商网站详情页系统架构](./docs/high-availability/e-commerce-website-detail-page-architecture.md)
|
||||
* [Hystrix 线程池技术实现资源隔离](./docs/high-availability/hystrix-thread-pool-isolation.md)
|
||||
* [Hystrix 信号量机制实现资源隔离](./docs/high-availability/hystrix-semphore-isolation.md)
|
||||
* [Hystrix 隔离策略细粒度控制](./docs/high-availability/hystrix-execution-isolation.md)
|
||||
* [深入 Hystrix 执行时内部原理](./docs/high-availability/hystrix-process.md)
|
||||
* [基于 request cache 请求缓存技术优化批量商品数据查询接口](./docs/high-availability/hystrix-request-cache.md)
|
||||
* [基于本地缓存的 fallback 降级机制](./docs/high-availability/hystrix-fallback.md)
|
||||
* [深入 Hystrix 断路器执行原理](./docs/high-availability/hystrix-circuit-breaker.md)
|
||||
* [深入 Hystrix 线程池隔离与接口限流](./docs/high-availability/hystrix-thread-pool-current-limiting.md)
|
||||
* [基于 timeout 机制为服务接口调用超时提供安全保护](./docs/high-availability/hystrix-timeout.md)
|
||||
|
||||
### 高可用系统
|
||||
- 如何设计一个高可用系统?
|
||||
|
||||
* 如何设计一个高可用系统?
|
||||
|
||||
### 限流
|
||||
- 如何限流?在工作中是怎么做的?说一下具体的实现?
|
||||
|
||||
* [如何限流?在工作中是怎么做的?说一下具体的实现?](/docs/high-concurrency/huifer-how-to-limit-current.md)
|
||||
|
||||
### 熔断
|
||||
- 如何进行熔断?
|
||||
- 熔断框架都有哪些?具体实现原理知道吗?
|
||||
- [熔断框架如何做技术选型?选用 Sentinel 还是 Hystrix?](/docs/high-availability/sentinel-vs-hystrix.md)
|
||||
|
||||
* 如何进行熔断?
|
||||
* 熔断框架都有哪些?具体实现原理知道吗?
|
||||
* [熔断框架如何做技术选型?选用 Sentinel 还是 Hystrix?](./docs/high-availability/sentinel-vs-hystrix.md)
|
||||
|
||||
### 降级
|
||||
- 如何进行降级?
|
||||
|
||||
* 如何进行降级?
|
||||
|
||||
## 微服务架构
|
||||
- [微服务架构整个章节内容属额外新增,后续抽空更新,也欢迎读者们参与补充完善](https://github.com/doocs/advanced-java)
|
||||
- [关于微服务架构的描述](/docs/micro-services/microservices-introduction.md)
|
||||
- [从单体式架构迁移到微服务架构](/docs/micro-services/migrating-from-a-monolithic-architecture-to-a-microservices-architecture.md)
|
||||
- [微服务的事件驱动数据管理](/docs/micro-services/event-driven-data-management-for-microservices.md)
|
||||
|
||||
* [微服务架构整个章节内容属额外新增,后续抽空更新,也欢迎读者们参与补充完善](https://github.com/doocs/advanced-java)
|
||||
* [关于微服务架构的描述](./docs/micro-services/microservices-introduction.md)
|
||||
* [从单体式架构迁移到微服务架构](./docs/micro-services/migrating-from-a-monolithic-architecture-to-a-microservices-architecture.md)
|
||||
* [微服务的事件驱动数据管理](./docs/micro-services/event-driven-data-management-for-microservices.md)
|
||||
* [选择微服务部署策略](./docs/micro-services/choose-microservice-deployment-strategy.md)
|
||||
* [微服务架构的优势与不足](./docs/micro-services/advantages-and-disadvantages-of-microservice.md)
|
||||
|
||||
### Spring Cloud 微服务架构
|
||||
- 什么是微服务?微服务之间是如何独立通讯的?
|
||||
- Spring Cloud 和 Dubbo 有哪些区别?
|
||||
- Spring Boot 和 Spring Cloud,谈谈你对它们的理解?
|
||||
- 什么是服务熔断?什么是服务降级?
|
||||
- 微服务的优缺点分别是什么?说一下你在项目开发中碰到的坑?
|
||||
- 你所知道的微服务技术栈都有哪些?
|
||||
- Eureka 和 Zookeeper 都可以提供服务注册与发现的功能,它们有什么区别?
|
||||
- ......
|
||||
|
||||
* [什么是微服务?微服务之间是如何独立通讯的?](/docs/micro-services/huifer-what's-microservice-how-to-communicate.md)
|
||||
* Spring Cloud 和 Dubbo 有哪些区别?
|
||||
* Spring Boot 和 Spring Cloud,谈谈你对它们的理解?
|
||||
* 什么是服务熔断?什么是服务降级?
|
||||
* 微服务的优缺点分别是什么?说一下你在项目开发中碰到的坑?
|
||||
* [你所知道的微服务技术栈都有哪些?](/docs/micro-services/huifer-micro-services-technology-stack.md)
|
||||
* [微服务治理策略](/docs/micro-services/huifer-micro-service-governance.md)
|
||||
* Eureka 和 Zookeeper 都可以提供服务注册与发现的功能,它们有什么区别?
|
||||
* [谈谈服务发现组件 Eureka 的主要调用过程?](/docs/micro-services/how-eureka-enable-service-discovery-and-service-registration.md)
|
||||
* ......
|
||||
|
||||
## 海量数据处理
|
||||
|
||||
* [如何从大量的 URL 中找出相同的 URL?](./docs/big-data/find-common-urls.md)
|
||||
* [如何从大量数据中找出高频词?](./docs/big-data/find-top-100-words.md)
|
||||
* [如何找出某一天访问百度网站最多的 IP?](./docs/big-data/find-top-1-ip.md)
|
||||
* [如何在大量的数据中找出不重复的整数?](./docs/big-data/find-no-repeat-number.md)
|
||||
* [如何在大量的数据中判断一个数是否存在?](./docs/big-data/find-a-number-if-exists.md)
|
||||
* [如何查询最热门的查询串?](./docs/big-data/find-hotest-query-string.md)
|
||||
* [如何统计不同电话号码的个数?](./docs/big-data/count-different-phone-numbers.md)
|
||||
* [如何从 5 亿个数中找出中位数?](./docs/big-data/find-mid-value-in-500-millions.md)
|
||||
* [如何按照 query 的频度排序?](./docs/big-data/sort-the-query-strings-by-counts.md)
|
||||
* [如何找出排名前 500 的数?](./docs/big-data/find-rank-top-500-numbers.md)
|
||||
|
||||
---
|
||||
|
||||
## Doocs 社区优质项目
|
||||
|
||||
Doocs 技术社区,致力于打造一个内容完整、持续成长的互联网开发者学习生态圈!以下是 Doocs 旗下的一些优秀项目,欢迎各位开发者朋友持续保持关注。
|
||||
|
||||
| # | 项目 | 描述 | 热度 |
|
||||
|---|---|---|---|
|
||||
| 1 | [advanced-java](https://github.com/doocs/advanced-java) | 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识。 |  <br> |
|
||||
| 2 | [leetcode](https://github.com/doocs/leetcode) | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解。 |  <br> |
|
||||
| 3 | [source-code-hunter](https://github.com/doocs/source-code-hunter) | 互联网常用组件框架源码分析。 |  <br> |
|
||||
| 4 | [jvm](https://github.com/doocs/jvm) | Java 虚拟机底层原理知识总结。 |  <br> |
|
||||
| 5 | [coding-interview](https://github.com/doocs/coding-interview) | 代码面试题集,包括《剑指 Offer》、《编程之美》等。 |  <br> |
|
||||
| 6 | [md](https://github.com/doocs/md) | 一款高度简洁的微信 Markdown 编辑器。 |  <br> |
|
||||
| 7 | [technical-books](https://github.com/doocs/technical-books) | 值得一看的技术书籍列表。 |  <br> |
|
||||
|
||||
## 贡献者
|
||||
|
||||
感谢以下所有朋友对 [Doocs 技术社区](https://github.com/doocs) 所做出的贡献,[参与项目维护请戳这儿](https://doocs.github.io/#/?id=how-to-join)。
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST: START - Do not remove or modify this section -->
|
||||
|
||||
<a href="https://opencollective.com/doocs/contributors.svg?width=890&button=true"><img src="https://opencollective.com/doocs/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST: END -->
|
||||
|
||||
## 公众号
|
||||
|
||||
[Doocs](https://github.com/doocs) 技术社区旗下唯一公众号「**Doocs开源社区**」,欢迎扫码关注,**专注分享技术领域相关知识及行业最新资讯**。当然,也可以加我个人微信(备注:GitHub),拉你进技术交流群。
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/doocs">
|
||||
<img src="./images/qrcode-for-doocs.jpg" style="width: 400px;"><br>
|
||||
<sub>公众平台</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/yanglbme">
|
||||
<img src="./images/qrcode-for-yanglbme.jpg" style="width: 400px;"><br>
|
||||
<sub>个人微信</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
35
docs/big-data/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# 海量数据处理
|
||||
|
||||
* [如何从大量的 URL 中找出相同的 URL?](/docs/big-data/find-common-urls.md)
|
||||
* [如何从大量数据中找出高频词?](/docs/big-data/find-top-100-words.md)
|
||||
* [如何找出某一天访问百度网站最多的 IP?](/docs/big-data/find-top-1-ip.md)
|
||||
* [如何在大量的数据中找出不重复的整数?](/docs/big-data/find-no-repeat-number.md)
|
||||
* [如何在大量的数据中判断一个数是否存在?](/docs/big-data/find-a-number-if-exists.md)
|
||||
* [如何查询最热门的查询串?](/docs/big-data/find-hotest-query-string.md)
|
||||
* [如何统计不同电话号码的个数?](/docs/big-data/count-different-phone-numbers.md)
|
||||
* [如何从 5 亿个数中找出中位数?](/docs/big-data/find-mid-value-in-500-millions.md)
|
||||
* [如何按照 query 的频度排序?](/docs/big-data/sort-the-query-strings-by-counts.md)
|
||||
* [如何找出排名前 500 的数?](/docs/big-data/find-rank-top-500-numbers.md)
|
||||
|
||||
---
|
||||
|
||||
## 公众号
|
||||
|
||||
GitHub 技术社区 [Doocs](https://github.com/doocs) 旗下唯一公众号「**Doocs开源社区**」,欢迎扫码关注,**专注分享技术领域相关知识及行业最新资讯**。当然,也可以加我个人微信(备注:GitHub),拉你进技术交流群。
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/doocs">
|
||||
<img src="./images/qrcode-for-doocs.jpg" style="width: 400px;"><br>
|
||||
<sub>公众平台</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/yanglbme">
|
||||
<img src="./images/qrcode-for-yanglbme.jpg" style="width: 400px;"><br>
|
||||
<sub>个人微信</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
19
docs/big-data/count-different-phone-numbers.md
Normal file
@ -0,0 +1,19 @@
|
||||
## 如何统计不同电话号码的个数?
|
||||
|
||||
### 题目描述
|
||||
|
||||
已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。
|
||||
|
||||
### 解答思路
|
||||
|
||||
这道题本质还是求解**数据重复**的问题,对于这类问题,一般首先考虑位图法。
|
||||
|
||||
对于本题,8 位电话号码可以表示的号码个数为 10<sup>8</sup> 个,即 1 亿个。我们每个号码用一个 bit 来表示,则总共需要 1 亿个 bit,内存占用约 100M。
|
||||
|
||||
**思路如下**:
|
||||
|
||||
申请一个位图数组,长度为 1 亿,初始化为 0。然后遍历所有电话号码,把号码对应的位图中的位置置为 1。遍历完成后,如果 bit 为 1,则表示这个电话号码在文件中存在,否则不存在。bit 值为 1 的数量即为 不同电话号码的个数。
|
||||
|
||||
### 方法总结
|
||||
|
||||
求解数据重复问题,记得考虑位图法。
|
20
docs/big-data/find-a-number-if-exists.md
Normal file
@ -0,0 +1,20 @@
|
||||
## 如何在大量的数据中判断一个数是否存在?
|
||||
|
||||
### 题目描述
|
||||
|
||||
给定 40 亿个不重复的没排过序的 unsigned int 型整数,然后再给定一个数,如何快速判断这个数是否在这 40 亿个整数当中?
|
||||
|
||||
### 解答思路
|
||||
|
||||
#### 方法一:分治法
|
||||
依然可以用分治法解决,方法与前面类似,就不再次赘述了。
|
||||
|
||||
#### 方法二:位图法
|
||||
|
||||
40 亿个不重复整数,我们用 40 亿个 bit 来表示,初始位均为 0,那么总共需要内存:4, 000, 000, 000b≈512M。
|
||||
|
||||
我们读取这 40 亿个整数,将对应的 bit 设置为 1。接着读取要查询的数,查看相应位是否为 1,如果为 1 表示存在,如果为 0 表示不存在。
|
||||
|
||||
### 方法总结
|
||||
|
||||
**判断数字是否存在、判断数字是否重复的问题**,位图法是一种非常高效的方法。
|
24
docs/big-data/find-common-urls.md
Normal file
@ -0,0 +1,24 @@
|
||||
## 如何从大量的 URL 中找出相同的 URL?
|
||||
|
||||
### 题目描述
|
||||
|
||||
给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,内存限制是 4G。请找出 a、b 两个文件共同的 URL。
|
||||
|
||||
### 解答思路
|
||||
|
||||
每个 URL 占 64B,那么 50 亿个 URL占用的空间大小约为 320GB。
|
||||
|
||||
> 5, 000, 000, 000 * 64B ≈ 5GB * 64 = 320GB
|
||||
|
||||
由于内存大小只有 4G,因此,我们不可能一次性把所有 URL 加载到内存中处理。对于这种类型的题目,一般采用**分治策略**,即:把一个文件中的 URL 按照某个特征划分为多个小文件,使得每个小文件大小不超过 4G,这样就可以把这个小文件读到内存中进行处理了。
|
||||
|
||||
**思路如下**:
|
||||
|
||||
首先遍历文件 a,对遍历到的 URL 求 `hash(URL) % 1000` ,根据计算结果把遍历到的 URL 存储到 a<sub>0</sub>, a<sub>1</sub>, a<sub>2</sub>, ..., a<sub>999</sub>,这样每个大小约为 300MB。使用同样的方法遍历文件 b,把文件 b 中的 URL 分别存储到文件 b<sub>0</sub>, b<sub>1</sub>, b<sub>2</sub>, ..., b<sub>999</sub> 中。这样处理过后,所有可能相同的 URL 都在对应的小文件中,即 a<sub>0</sub> 对应 b<sub>0</sub>, ..., a<sub>999</sub> 对应 b<sub>999</sub>,不对应的小文件不可能有相同的 URL。那么接下来,我们只需要求出这 1000 对小文件中相同的 URL 就好了。
|
||||
|
||||
接着遍历 a<sub>i</sub>( `i∈[0,999]` ),把 URL 存储到一个 HashSet 集合中。然后遍历 b<sub>i</sub> 中每个 URL,看在 HashSet 集合中是否存在,若存在,说明这就是共同的 URL,可以把这个 URL 保存到一个单独的文件中。
|
||||
|
||||
### 方法总结
|
||||
|
||||
1. 分而治之,进行哈希取余;
|
||||
2. 对每个子文件进行 HashSet 统计。
|
45
docs/big-data/find-hotest-query-string.md
Normal file
@ -0,0 +1,45 @@
|
||||
## 如何查询最热门的查询串?
|
||||
|
||||
### 题目描述
|
||||
|
||||
搜索引擎会通过日志文件把用户每次检索使用的所有查询串都记录下来,每个查询串的长度不超过 255 字节。
|
||||
|
||||
假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。(一个查询串的重复度越高,说明查询它的用户越多,也就越热门。)
|
||||
|
||||
### 解答思路
|
||||
|
||||
每个查询串最长为 255B,1000w 个串需要占用 约 2.55G 内存,因此,我们无法将所有字符串全部读入到内存中处理。
|
||||
|
||||
#### 方法一:分治法
|
||||
|
||||
分治法依然是一个非常实用的方法。
|
||||
|
||||
划分为多个小文件,保证单个小文件中的字符串能被直接加载到内存中处理,然后求出每个文件中出现次数最多的 10 个字符串;最后通过一个小顶堆统计出所有文件中出现最多的 10 个字符串。
|
||||
|
||||
方法可行,但不是最好,下面介绍其他方法。
|
||||
|
||||
#### 方法二:HashMap 法
|
||||
|
||||
虽然字符串总数比较多,但去重后不超过 300w,因此,可以考虑把所有字符串及出现次数保存在一个 HashMap 中,所占用的空间为 300w*(255+4)≈777M(其中,4表示整数占用的4个字节)。由此可见,1G 的内存空间完全够用。
|
||||
|
||||
**思路如下**:
|
||||
|
||||
首先,遍历字符串,若不在 map 中,直接存入 map,value 记为 1;若在 map 中,则把对应的 value 加 1,这一步时间复杂度 `O(N)` 。
|
||||
|
||||
接着遍历 map,构建一个 10 个元素的小顶堆,若遍历到的字符串的出现次数大于堆顶字符串的出现次数,则进行替换,并将堆调整为小顶堆。
|
||||
|
||||
遍历结束后,堆中 10 个字符串就是出现次数最多的字符串。这一步时间复杂度 `O(Nlog10)` 。
|
||||
|
||||
#### 方法三:前缀树法
|
||||
|
||||
方法二使用了 HashMap 来统计次数,当这些字符串有大量相同前缀时,可以考虑使用前缀树来统计字符串出现的次数,树的结点保存字符串出现次数,0 表示没有出现。
|
||||
|
||||
**思路如下**:
|
||||
|
||||
在遍历字符串时,在前缀树中查找,如果找到,则把结点中保存的字符串次数加 1,否则为这个字符串构建新结点,构建完成后把叶子结点中字符串的出现次数置为 1。
|
||||
|
||||
最后依然使用小顶堆来对字符串的出现次数进行排序。
|
||||
|
||||
### 方法总结
|
||||
|
||||
前缀树经常被用来统计字符串的出现次数。它的另外一个大的用途是字符串查找,判断是否有重复的字符串等。
|
76
docs/big-data/find-mid-value-in-500-millions.md
Normal file
@ -0,0 +1,76 @@
|
||||
## 如何从 5 亿个数中找出中位数?
|
||||
|
||||
### 题目描述
|
||||
|
||||
从 5 亿个数中找出中位数。数据排序后,位置在最中间的数就是中位数。当样本数为奇数时,中位数为 第 `(N+1)/2` 个数;当样本数为偶数时,中位数为 第 `N/2` 个数与第 `1+N/2` 个数的均值。
|
||||
|
||||
### 解答思路
|
||||
|
||||
如果这道题没有内存大小限制,则可以把所有数读到内存中排序后找出中位数。但是最好的排序算法的时间复杂度都为 `O(NlogN)` 。这里使用其他方法。
|
||||
|
||||
#### 方法一:双堆法
|
||||
|
||||
维护两个堆,一个大顶堆,一个小顶堆。大顶堆中最大的数**小于等于**小顶堆中最小的数;保证这两个堆中的元素个数的差不超过 1。
|
||||
|
||||
若数据总数为**偶数**,当这两个堆建好之后,**中位数就是这两个堆顶元素的平均值**。当数据总数为**奇数**时,根据两个堆的大小,**中位数一定在数据多的堆的堆顶**。
|
||||
|
||||
``` java
|
||||
class MedianFinder {
|
||||
|
||||
private PriorityQueue<Integer> maxHeap;
|
||||
private PriorityQueue<Integer> minHeap;
|
||||
|
||||
/** initialize your data structure here. */
|
||||
public MedianFinder() {
|
||||
maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
|
||||
minHeap = new PriorityQueue<>(Integer::compareTo);
|
||||
}
|
||||
|
||||
public void addNum(int num) {
|
||||
if (maxHeap.isEmpty() || maxHeap.peek() > num) {
|
||||
maxHeap.offer(num);
|
||||
} else {
|
||||
minHeap.offer(num);
|
||||
}
|
||||
|
||||
int size1 = maxHeap.size();
|
||||
int size2 = minHeap.size();
|
||||
if (size1 - size2 > 1) {
|
||||
minHeap.offer(maxHeap.poll());
|
||||
} else if (size2 - size1 > 1) {
|
||||
maxHeap.offer(minHeap.poll());
|
||||
}
|
||||
}
|
||||
|
||||
public double findMedian() {
|
||||
int size1 = maxHeap.size();
|
||||
int size2 = minHeap.size();
|
||||
|
||||
return size1 == size2
|
||||
? (maxHeap.peek() + minHeap.peek()) * 1.0 / 2
|
||||
: (size1 > size2 ? maxHeap.peek() : minHeap.peek());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 见 LeetCode No.295:https://leetcode.com/problems/find-median-from-data-stream/
|
||||
|
||||
以上这种方法,需要把所有数据都加载到内存中。当数据量很大时,就不能这样了,因此,这种方法**适用于数据量较小的情况**。5 亿个数,每个数字占用 4B,总共需要 2G 内存。如果可用内存不足 2G,就不能使用这种方法了,下面介绍另一种方法。
|
||||
|
||||
#### 方法二:分治法
|
||||
|
||||
分治法的思想是把一个大的问题逐渐转换为规模较小的问题来求解。
|
||||
|
||||
对于这道题,顺序读取这 5 亿个数字,对于读取到的数字 num,如果它对应的二进制中最高位为 1,则把这个数字写到 f1 中,否则写入 f0 中。通过这一步,可以把这 5 亿个数划分为两部分,而且 f0 中的数都大于 f1 中的数(最高位是符号位)。
|
||||
|
||||
划分之后,可以非常容易地知道中位数是在 f0 还是 f1 中。假设 f1 中有 1 亿个数,那么中位数一定在 f0 中,且是在 f0 中,从小到大排列的第 1.5 亿个数与它后面的一个数的平均值。
|
||||
|
||||
> **提示**,5 亿数的中位数是第 2.5 亿与右边相邻一个数求平均值。若 f1 有一亿个数,那么中位数就是 f0 中从第 1.5 亿个数开始的两个数求得的平均值。
|
||||
|
||||
对于 f0 可以用次高位的二进制继续将文件一分为二,如此划分下去,直到划分后的文件可以被加载到内存中,把数据加载到内存中以后直接排序,找出中位数。
|
||||
|
||||
> **注意**,当数据总数为偶数,如果划分后两个文件中的数据有相同个数,那么中位数就是数据较小的文件中的最大值与数据较大的文件中的最小值的平均值。
|
||||
|
||||
### 方法总结
|
||||
|
||||
分治法,真香!
|
60
docs/big-data/find-no-repeat-number.md
Normal file
@ -0,0 +1,60 @@
|
||||
## 如何在大量的数据中找出不重复的整数?
|
||||
|
||||
### 题目描述
|
||||
|
||||
在 2.5 亿个整数中找出不重复的整数。注意:内存不足以容纳这 2.5 亿个整数。
|
||||
|
||||
### 解答思路
|
||||
|
||||
#### 方法一:分治法
|
||||
与前面的题目方法类似,先将 2.5 亿个数划分到多个小文件,用 HashSet/HashMap 找出每个小文件中不重复的整数,再合并每个子结果,即为最终结果。
|
||||
|
||||
#### 方法二:位图法
|
||||
|
||||
**位图**,就是用一个或多个 bit 来标记某个元素对应的值,而键就是该元素。采用位作为单位来存储数据,可以大大节省存储空间。
|
||||
|
||||
位图通过使用位数组来表示某些元素是否存在。它可以用于快速查找,判重,排序等。不是很清楚?我先举个小例子。
|
||||
|
||||
假设我们要对 `[0,7]` 中的 5 个元素 (6, 4, 2, 1, 5) 进行排序,可以采用位图法。0~7 范围总共有 8 个数,只需要 8bit,即 1 个字节。首先将每个位都置 0:
|
||||
|
||||
```
|
||||
0 0 0 0 0 0 0 0
|
||||
```
|
||||
|
||||
然后遍历 5 个元素,首先遇到 6,那么将下标为 6 的位的 0 置为 1;接着遇到 4,把下标为 4 的位 的 0 置为 1:
|
||||
|
||||
```
|
||||
0 0 0 0 1 0 1 0
|
||||
```
|
||||
|
||||
依次遍历,结束后,位数组是这样的:
|
||||
|
||||
```
|
||||
0 1 1 0 1 1 1 0
|
||||
```
|
||||
|
||||
每个为 1 的位,它的下标都表示了一个数:
|
||||
|
||||
```
|
||||
for i in range(8):
|
||||
if bits[i] == 1:
|
||||
print(i)
|
||||
```
|
||||
|
||||
这样我们其实就已经实现了排序。
|
||||
|
||||
对于整数相关的算法的求解,**位图法**是一种非常实用的算法。假设 int 整数占用 4B,即 32bit,那么我们可以表示的整数的个数为 2<sup>32</sup>。
|
||||
|
||||
**那么对于这道题**,我们用 2 个 bit 来表示各个数字的状态:
|
||||
|
||||
* 00 表示这个数字没出现过;
|
||||
* 01 表示这个数字出现过一次(即为题目所找的不重复整数);
|
||||
* 10 表示这个数字出现了多次。
|
||||
|
||||
那么这 2<sup>32</sup> 个整数,总共所需内存为 2<sup>32</sup>*2b=1GB。因此,当可用内存超过 1GB 时,可以采用位图法。假设内存满足位图法需求,进行下面的操作:
|
||||
|
||||
遍历 2.5 亿个整数,查看位图中对应的位,如果是 00,则变为 01,如果是 01 则变为 10,如果是 10 则保持不变。遍历结束后,查看位图,把对应位是 01 的整数输出即可。
|
||||
|
||||
### 方法总结
|
||||
|
||||
**判断数字是否重复的问题**,位图法是一种非常高效的方法。
|
109
docs/big-data/find-rank-top-500-numbers.md
Normal file
@ -0,0 +1,109 @@
|
||||
## 如何找出排名前 500 的数?
|
||||
|
||||
### 题目描述
|
||||
|
||||
有 20 个数组,每个数组有 500 个元素,并且有序排列。如何在这 20*500 个数中找出前 500 的数?
|
||||
|
||||
### 解答思路
|
||||
|
||||
对于 TopK 问题,最常用的方法是使用堆排序。对本题而言,假设数组降序排列,可以采用以下方法:
|
||||
|
||||
首先建立大顶堆,堆的大小为数组的个数,即为 20,把每个数组最大的值存到堆中。
|
||||
|
||||
接着删除堆顶元素,保存到另一个大小为 500 的数组中,然后向大顶堆插入删除的元素所在数组的下一个元素。
|
||||
|
||||
重复上面的步骤,直到删除完第 500 个元素,也即找出了最大的前 500 个数。
|
||||
|
||||
> 为了在堆中取出一个数据后,能知道它是从哪个数组中取出的,从而可以从这个数组中取下一个值,可以把数组的指针存放到堆中,对这个指针提供比较大小的方法。
|
||||
|
||||
``` java
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.PriorityQueue;
|
||||
|
||||
/**
|
||||
* @author https://github.com/yanglbme
|
||||
*/
|
||||
@Data
|
||||
public class DataWithSource implements Comparable<DataWithSource> {
|
||||
/**
|
||||
* 数值
|
||||
*/
|
||||
private int value;
|
||||
|
||||
/**
|
||||
* 记录数值来源的数组
|
||||
*/
|
||||
private int source;
|
||||
|
||||
/**
|
||||
* 记录数值在数组中的索引
|
||||
*/
|
||||
private int index;
|
||||
|
||||
public DataWithSource(int value, int source, int index) {
|
||||
this.value = value;
|
||||
this.source = source;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 由于 PriorityQueue 使用小顶堆来实现,这里通过修改
|
||||
* 两个整数的比较逻辑来让 PriorityQueue 变成大顶堆
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(DataWithSource o) {
|
||||
return Integer.compare(o.getValue(), this.value);
|
||||
}
|
||||
}
|
||||
|
||||
class Test {
|
||||
public static int[] getTop(int[][] data) {
|
||||
int rowSize = data.length;
|
||||
int columnSize = data[0].length;
|
||||
|
||||
// 创建一个columnSize大小的数组,存放结果
|
||||
int[] result = new int[columnSize];
|
||||
|
||||
PriorityQueue<DataWithSource> maxHeap = new PriorityQueue<>();
|
||||
for (int i = 0; i < rowSize; ++i) {
|
||||
// 将每个数组的最大一个元素放入堆中
|
||||
DataWithSource d = new DataWithSource(data[i][0], i, 0);
|
||||
maxHeap.add(d);
|
||||
}
|
||||
|
||||
int num = 0;
|
||||
while (num < columnSize) {
|
||||
// 删除堆顶元素
|
||||
DataWithSource d = maxHeap.poll();
|
||||
result[num++] = d.getValue();
|
||||
if (num >= columnSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
d.setValue(data[d.getSource()][d.getIndex() + 1]);
|
||||
d.setIndex(d.getIndex() + 1);
|
||||
maxHeap.add(d);
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int[][] data = {
|
||||
{29, 17, 14, 2, 1},
|
||||
{19, 17, 16, 15, 6},
|
||||
{30, 25, 20, 14, 5},
|
||||
};
|
||||
|
||||
int[] top = getTop(data);
|
||||
System.out.println(Arrays.toString(top)); // [30, 29, 25, 20, 19]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方法总结
|
||||
|
||||
求 TopK,不妨考虑一下堆排序?
|
17
docs/big-data/find-top-1-ip.md
Normal file
@ -0,0 +1,17 @@
|
||||
## 如何找出某一天访问百度网站最多的 IP?
|
||||
|
||||
### 题目描述
|
||||
|
||||
现有海量日志数据保存在一个超大文件中,该文件无法直接读入内存,要求从中提取某天访问百度次数最多的那个 IP。
|
||||
|
||||
### 解答思路
|
||||
|
||||
这道题只关心某一天访问百度最多的 IP,因此,可以首先对文件进行一次遍历,把这一天访问百度 IP 的相关信息记录到一个单独的大文件中。接下来采用的方法与上一题一样,大致就是先对 IP 进行哈希映射,接着使用 HashMap 统计重复 IP 的次数,最后计算出重复次数最多的 IP。
|
||||
|
||||
> 注:这里只需要找出出现次数最多的 IP,可以不必使用堆,直接用一个变量 max 即可。
|
||||
|
||||
### 方法总结
|
||||
|
||||
1. 分而治之,进行哈希取余;
|
||||
2. 使用 HashMap 统计频数;
|
||||
3. 求解**最大**的 TopN 个,用**小顶堆**;求解**最小**的 TopN 个,用**大顶堆**。
|
23
docs/big-data/find-top-100-words.md
Normal file
@ -0,0 +1,23 @@
|
||||
## 如何从大量数据中找出高频词?
|
||||
|
||||
### 题目描述
|
||||
|
||||
有一个 1GB 大小的文件,文件里每一行是一个词,每个词的大小不超过 16B,内存大小限制是 1MB,要求返回频数最高的 100 个词(Top 100)。
|
||||
|
||||
### 解答思路
|
||||
|
||||
由于内存限制,我们依然无法直接将大文件的所有词一次读到内存中。因此,同样可以采用**分治策略**,把一个大文件分解成多个小文件,保证每个文件的大小小于 1MB,进而直接将单个小文件读取到内存中进行处理。
|
||||
|
||||
**思路如下**:
|
||||
|
||||
首先遍历大文件,对遍历到的每个词x,执行 `hash(x) % 5000` ,将结果为 i 的词存放到文件 a<sub>i</sub> 中。遍历结束后,我们可以得到 5000 个小文件。每个小文件的大小为 200KB 左右。如果有的小文件大小仍然超过 1MB,则采用同样的方式继续进行分解。
|
||||
|
||||
接着统计每个小文件中出现频数最高的 100 个词。最简单的方式是使用 HashMap 来实现。其中 key 为词,value 为该词出现的频率。具体方法是:对于遍历到的词 x,如果在 map 中不存在,则执行 `map.put(x, 1)` ;若存在,则执行 `map.put(x, map.get(x)+1)` ,将该词频数加 1。
|
||||
|
||||
上面我们统计了每个小文件单词出现的频数。接下来,我们可以通过维护一个**小顶堆**来找出所有词中出现频数最高的 100 个。具体方法是:依次遍历每个小文件,构建一个**小顶堆**,堆大小为 100。如果遍历到的词的出现次数大于堆顶词的出现次数,则用新词替换堆顶的词,然后重新调整为**小顶堆**,遍历结束后,小顶堆上的词就是出现频数最高的 100 个词。
|
||||
|
||||
### 方法总结
|
||||
|
||||
1. 分而治之,进行哈希取余;
|
||||
2. 使用 HashMap 统计频数;
|
||||
3. 求解**最大**的 TopN 个,用**小顶堆**;求解**最小**的 TopN 个,用**大顶堆**。
|
BIN
docs/big-data/images/qrcode-for-doocs.jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/big-data/images/qrcode-for-yanglbme.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
24
docs/big-data/sort-the-query-strings-by-counts.md
Normal file
@ -0,0 +1,24 @@
|
||||
## 如何按照 query 的频度排序?
|
||||
|
||||
### 题目描述
|
||||
|
||||
有 10 个文件,每个文件大小为 1G,每个文件的每一行存放的都是用户的 query,每个文件的 query 都可能重复。要求按照 query 的频度排序。
|
||||
|
||||
### 解答思路
|
||||
|
||||
如果 query 的重复度比较大,可以考虑一次性把所有 query 读入内存中处理;如果 query 的重复率不高,那么可用内存不足以容纳所有的 query,这时候就需要采用分治法或其他的方法来解决。
|
||||
|
||||
#### 方法一:HashMap 法
|
||||
|
||||
如果 query 重复率高,说明不同 query 总数比较小,可以考虑把所有的 query 都加载到内存中的 HashMap 中。接着就可以按照 query 出现的次数进行排序。
|
||||
|
||||
#### 方法二:分治法
|
||||
|
||||
分治法需要根据数据量大小以及可用内存的大小来确定问题划分的规模。对于这道题,可以顺序遍历 10 个文件中的 query,通过 Hash 函数 `hash(query) % 10` 把这些 query 划分到 10 个小文件中。之后对每个小文件使用 HashMap 统计 query 出现次数,根据次数排序并写入到零外一个单独文件中。
|
||||
|
||||
接着对所有文件按照 query 的次数进行排序,这里可以使用归并排序(由于无法把所有 query 都读入内存,因此需要使用外排序)。
|
||||
|
||||
### 方法总结
|
||||
|
||||
* 内存若够,直接读入进行排序;
|
||||
* 内存不够,先划分为小文件,小文件排好序后,整理使用外排序进行归并。
|
@ -1,24 +1,55 @@
|
||||
# 分布式系统
|
||||
|
||||
## [面试连环炮](/docs/distributed-system/distributed-system-interview.md)
|
||||
|
||||
## 系统拆分
|
||||
- [为什么要进行系统拆分?如何进行系统拆分?拆分后不用 Dubbo 可以吗?](/docs/distributed-system/why-dubbo.md)
|
||||
|
||||
* [为什么要进行系统拆分?如何进行系统拆分?拆分后不用 Dubbo 可以吗?](/docs/distributed-system/why-dubbo.md)
|
||||
|
||||
## 分布式服务框架
|
||||
- [说一下 Dubbo 的工作原理?注册中心挂了可以继续通信吗?](/docs/distributed-system/dubbo-operating-principle.md)
|
||||
- [Dubbo 支持哪些序列化协议?说一下 Hessian 的数据结构?PB 知道吗?为什么 PB 的效率是最高的?](/docs/distributed-system/dubbo-serialization-protocol.md)
|
||||
- [Dubbo 负载均衡策略和集群容错策略都有哪些?动态代理策略呢?](/docs/distributed-system/dubbo-load-balancing.md)
|
||||
- [Dubbo 的 spi 思想是什么?](/docs/distributed-system/dubbo-spi.md)
|
||||
- [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](/docs/distributed-system/dubbo-service-management.md)
|
||||
- [分布式服务接口的幂等性如何设计(比如不能重复扣款)?](/docs/distributed-system/distributed-system-idempotency.md)
|
||||
- [分布式服务接口请求的顺序性如何保证?](/docs/distributed-system/distributed-system-request-sequence.md)
|
||||
- [如何自己设计一个类似 Dubbo 的 RPC 框架?](/docs/distributed-system/dubbo-rpc-design.md)
|
||||
|
||||
* [说一下 Dubbo 的工作原理?注册中心挂了可以继续通信吗?](/docs/distributed-system/dubbo-operating-principle.md)
|
||||
* [Dubbo 支持哪些序列化协议?说一下 Hessian 的数据结构?PB 知道吗?为什么 PB 的效率是最高的?](/docs/distributed-system/dubbo-serialization-protocol.md)
|
||||
* [Dubbo 负载均衡策略和集群容错策略都有哪些?动态代理策略呢?](/docs/distributed-system/dubbo-load-balancing.md)
|
||||
* [Dubbo 的 SPI 思想是什么?](/docs/distributed-system/dubbo-spi.md)
|
||||
* [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](/docs/distributed-system/dubbo-service-management.md)
|
||||
* [分布式服务接口的幂等性如何设计(比如不能重复扣款)?](/docs/distributed-system/distributed-system-idempotency.md)
|
||||
* [分布式服务接口请求的顺序性如何保证?](/docs/distributed-system/distributed-system-request-sequence.md)
|
||||
* [如何自己设计一个类似 Dubbo 的 RPC 框架?](/docs/distributed-system/dubbo-rpc-design.md)
|
||||
* [CAP定理的P是什么](/docs/distributed-system/distributed-system-cap.md)
|
||||
|
||||
## 分布式锁
|
||||
- [Zookeeper 都有哪些应用场景?](/docs/distributed-system/zookeeper-application-scenarios.md)
|
||||
- [使用 Redis 如何设计分布式锁?使用 Zookeeper 来设计分布式锁可以吗?以上两种分布式锁的实现方式哪种效率比较高?](/docs/distributed-system/distributed-lock-redis-vs-zookeeper.md)
|
||||
|
||||
* [Zookeeper 都有哪些应用场景?](/docs/distributed-system/zookeeper-application-scenarios.md)
|
||||
* [使用 Redis 如何设计分布式锁?使用 Zookeeper 来设计分布式锁可以吗?以上两种分布式锁的实现方式哪种效率比较高?](/docs/distributed-system/distributed-lock-redis-vs-zookeeper.md)
|
||||
|
||||
## 分布式事务
|
||||
- [分布式事务了解吗?你们如何解决分布式事务问题的?TCC 如果出现网络连不通怎么办?XA 的一致性如何保证?](/docs/distributed-system/distributed-transaction.md)
|
||||
|
||||
* [分布式事务了解吗?你们如何解决分布式事务问题的?TCC 如果出现网络连不通怎么办?XA 的一致性如何保证?](/docs/distributed-system/distributed-transaction.md)
|
||||
|
||||
## 分布式会话
|
||||
- [集群部署时的分布式 Session 如何实现?](/docs/distributed-system/distributed-session.md)
|
||||
|
||||
* [集群部署时的分布式 Session 如何实现?](/docs/distributed-system/distributed-session.md)
|
||||
|
||||
---
|
||||
|
||||
## 公众号
|
||||
|
||||
GitHub 技术社区 [Doocs](https://github.com/doocs) 旗下唯一公众号「**Doocs开源社区**」,欢迎扫码关注,**专注分享技术领域相关知识及行业最新资讯**。当然,也可以加我个人微信(备注:GitHub),拉你进技术交流群。
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/doocs">
|
||||
<img src="./images/qrcode-for-doocs.jpg" style="width: 400px;"><br>
|
||||
<sub>公众平台</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/yanglbme">
|
||||
<img src="./images/qrcode-for-yanglbme.jpg" style="width: 400px;"><br>
|
||||
<sub>个人微信</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -1,37 +1,39 @@
|
||||
## 面试题
|
||||
一般实现分布式锁都有哪些方式?使用 redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
|
||||
一般实现分布式锁都有哪些方式?使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实一般问问题,都是这么问的,先问问你 zk,然后其实是要过渡到 zk 相关的一些问题里去,比如分布式锁。因为在分布式系统开发中,分布式锁的使用场景还是很常见的。
|
||||
|
||||
## 面试题剖析
|
||||
### redis 分布式锁
|
||||
|
||||
官方叫做 `RedLock` 算法,是 redis 官方支持的分布式锁算法。
|
||||
### Redis 分布式锁
|
||||
|
||||
官方叫做 `RedLock` 算法,是 Redis 官方支持的分布式锁算法。
|
||||
|
||||
这个分布式锁有 3 个重要的考量点:
|
||||
|
||||
- 互斥(只能有一个客户端获取锁)
|
||||
- 不能死锁
|
||||
- 容错(只要大部分 redis 节点创建了这把锁就可以)
|
||||
* 互斥(只能有一个客户端获取锁)
|
||||
* 不能死锁
|
||||
* 容错(只要大部分 Redis 节点创建了这把锁就可以)
|
||||
|
||||
#### redis 最普通的分布式锁
|
||||
#### Redis 最普通的分布式锁
|
||||
|
||||
第一个最普通的实现方式,就是在 redis 里使用 `setnx` 命令创建一个 key,这样就算加锁。
|
||||
第一个最普通的实现方式,就是在 Redis 里使用 `SET key value [EX seconds] [PX milliseconds] NX` 创建一个 key,这样就算加锁。其中:
|
||||
|
||||
- `NX`:表示只有 `key` 不存在的时候才会设置成功,如果此时 redis 中存在这个 `key`,那么设置失败,返回 `nil`。
|
||||
- `EX seconds`:设置 `key` 的过期时间,精确到秒级。意思是 `seconds` 秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。
|
||||
- `PX milliseconds`:同样是设置 `key` 的过期时间,精确到毫秒级。
|
||||
|
||||
比如执行以下命令:
|
||||
|
||||
```r
|
||||
SET resource_name my_random_value NX PX 30000
|
||||
SET resource_name my_random_value PX 30000 NX
|
||||
```
|
||||
|
||||
执行这个命令就 ok。
|
||||
|
||||
- `NX`:表示只有 `key` 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 `nil`)
|
||||
- `PX 30000`:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。
|
||||
|
||||
释放锁就是删除 key ,但是一般可以用 `lua` 脚本删除,判断 value 一样才删除:
|
||||
|
||||
```lua
|
||||
``` lua
|
||||
-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。
|
||||
if redis.call("get",KEYS[1]) == ARGV[1] then
|
||||
return redis.call("del",KEYS[1])
|
||||
@ -42,19 +44,20 @@ end
|
||||
|
||||
为啥要用 `random_value` 随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,比如说超过了 30s,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除 key 的话会有问题,所以得用随机值加上面的 `lua` 脚本来释放锁。
|
||||
|
||||
但是这样是肯定不行的。因为如果是普通的 redis 单实例,那就是单点故障。或者是 redis 普通主从,那 redis 主从异步复制,如果主节点挂了(key 就没有了),key 还没同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。
|
||||
但是这样是肯定不行的。因为如果是普通的 Redis 单实例,那就是单点故障。或者是 Redis 普通主从,那 Redis 主从异步复制,如果主节点挂了(key 就没有了),key 还没同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。
|
||||
|
||||
#### RedLock 算法
|
||||
这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。然后执行如下步骤获取一把锁:
|
||||
|
||||
这个场景是假设有一个 Redis cluster,有 5 个 Redis master 实例。然后执行如下步骤获取一把锁:
|
||||
|
||||
1. 获取当前时间戳,单位是毫秒;
|
||||
2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;
|
||||
3. 尝试在**大多数节点**上建立一个锁,比如 5 个节点就要求是 3 个节点 `n / 2 + 1`;
|
||||
3. 尝试在**大多数节点**上建立一个锁,比如 5 个节点就要求是 3 个节点 `n / 2 + 1` ;
|
||||
4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
|
||||
5. 要是锁建立失败了,那么就依次之前建立过的锁删除;
|
||||
6. 只要别人建立了一把分布式锁,你就得**不断轮询去尝试获取锁**。
|
||||
|
||||

|
||||

|
||||
|
||||
[Redis 官方](https://redis.io/)给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看:https://redis.io/topics/distlock 。
|
||||
|
||||
@ -62,13 +65,9 @@ end
|
||||
|
||||
zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能**注册个监听器**监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。
|
||||
|
||||
```java
|
||||
``` java
|
||||
/**
|
||||
* ZooKeeperSession
|
||||
*
|
||||
* @author bingo
|
||||
* @since 2018/11/29
|
||||
*
|
||||
*/
|
||||
public class ZooKeeperSession {
|
||||
|
||||
@ -141,11 +140,7 @@ public class ZooKeeperSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立zk session的watcher
|
||||
*
|
||||
* @author bingo
|
||||
* @since 2018/11/29
|
||||
*
|
||||
* 建立 zk session 的 watcher
|
||||
*/
|
||||
private class ZooKeeperWatcher implements Watcher {
|
||||
|
||||
@ -165,10 +160,6 @@ public class ZooKeeperSession {
|
||||
|
||||
/**
|
||||
* 封装单例的静态内部类
|
||||
*
|
||||
* @author bingo
|
||||
* @since 2018/11/29
|
||||
*
|
||||
*/
|
||||
private static class Singleton {
|
||||
|
||||
@ -203,11 +194,11 @@ public class ZooKeeperSession {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
也可以采用另一种方式,创建临时顺序节点:
|
||||
|
||||
如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听**排在自己前面**的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 zookeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。
|
||||
```java
|
||||
如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听**排在自己前面**的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 ZooKeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。
|
||||
|
||||
``` java
|
||||
public class ZooKeeperDistributedLock implements Watcher {
|
||||
|
||||
private ZooKeeper zk;
|
||||
@ -336,11 +327,11 @@ public class ZooKeeperDistributedLock implements Watcher {
|
||||
|
||||
### redis 分布式锁和 zk 分布式锁的对比
|
||||
|
||||
- redis 分布式锁,其实**需要自己不断去尝试获取锁**,比较消耗性能。
|
||||
- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
|
||||
* redis 分布式锁,其实**需要自己不断去尝试获取锁**,比较消耗性能。
|
||||
* zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
|
||||
|
||||
另外一点就是,如果是 redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。
|
||||
另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。
|
||||
|
||||
redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等......zk 的分布式锁语义清晰实现简单。
|
||||
Redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等......zk 的分布式锁语义清晰实现简单。
|
||||
|
||||
所以先不分析太多的东西,就说这两点,我个人实践认为 zk 的分布式锁比 redis 的分布式锁牢靠、而且模型简单易用。
|
||||
所以先不分析太多的东西,就说这两点,我个人实践认为 zk 的分布式锁比 Redis 的分布式锁牢靠、而且模型简单易用。
|
||||
|
@ -1,31 +1,35 @@
|
||||
## 面试题
|
||||
集群部署时的分布式 session 如何实现?
|
||||
集群部署时的分布式 Session 如何实现?
|
||||
|
||||
## 面试官心理分析
|
||||
面试官问了你一堆 dubbo 是怎么玩儿的,你会玩儿 dubbo 就可以把单块系统弄成分布式系统,然后分布式之后接踵而来的就是一堆问题,最大的问题就是**分布式事务**、**接口幂等性**、**分布式锁**,还有最后一个就是**分布式 session**。
|
||||
|
||||
面试官问了你一堆 Dubbo 是怎么玩儿的,你会玩儿 Dubbo 就可以把单块系统弄成分布式系统,然后分布式之后接踵而来的就是一堆问题,最大的问题就是**分布式事务**、**接口幂等性**、**分布式锁**,还有最后一个就是**分布式 Session**。
|
||||
|
||||
当然了,分布式系统中的问题何止这么一点,非常之多,复杂度很高,这里只是说一下常见的几个问题,也是面试的时候常问的几个。
|
||||
|
||||
## 面试题剖析
|
||||
session 是啥?浏览器有个 cookie,在一段时间内这个 cookie 都存在,然后每次发请求过来都带上一个特殊的 `jsessionid cookie`,就根据这个东西,在服务端可以维护一个对应的 session 域,里面可以放点数据。
|
||||
|
||||
一般的话只要你没关掉浏览器,cookie 还在,那么对应的那个 session 就在,但是如果 cookie 没了,session 也就没了。常见于什么购物车之类的东西,还有登录状态保存之类的。
|
||||
Session 是啥?浏览器有个 Cookie,在一段时间内这个 Cookie 都存在,然后每次发请求过来都带上一个特殊的 `jsessionid cookie` ,就根据这个东西,在服务端可以维护一个对应的 Session 域,里面可以放点数据。
|
||||
|
||||
一般的话只要你没关掉浏览器,Cookie 还在,那么对应的那个 Session 就在,但是如果 Cookie 没了,Session 也就没了。常见于什么购物车之类的东西,还有登录状态保存之类的。
|
||||
|
||||
这个不多说了,懂 Java 的都该知道这个。
|
||||
|
||||
单块系统的时候这么玩儿 session 没问题,但是你要是分布式系统呢,那么多的服务,session 状态在哪儿维护啊?
|
||||
单块系统的时候这么玩儿 Session 没问题,但是你要是分布式系统呢,那么多的服务,Session 状态在哪儿维护啊?
|
||||
|
||||
其实方法很多,但是常见常用的是以下几种:
|
||||
|
||||
### 完全不用 session
|
||||
### 完全不用 Session
|
||||
|
||||
使用 JWT Token 储存用户身份,然后再从数据库或者 cache 中获取其他的信息。这样无论请求分配到哪个服务器都无所谓。
|
||||
|
||||
### tomcat + redis
|
||||
这个其实还挺方便的,就是使用 session 的代码,跟以前一样,还是基于 tomcat 原生的 session 支持即可,然后就是用一个叫做 `Tomcat RedisSessionManager` 的东西,让所有我们部署的 tomcat 都将 session 数据存储到 redis 即可。
|
||||
### Tomcat + Redis
|
||||
|
||||
在 tomcat 的配置文件中配置:
|
||||
这个其实还挺方便的,就是使用 Session 的代码,跟以前一样,还是基于 Tomcat 原生的 Session 支持即可,然后就是用一个叫做 `Tomcat RedisSessionManager` 的东西,让所有我们部署的 Tomcat 都将 Session 数据存储到 Redis 即可。
|
||||
|
||||
```xml
|
||||
在 Tomcat 的配置文件中配置:
|
||||
|
||||
``` xml
|
||||
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
|
||||
|
||||
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
|
||||
@ -35,7 +39,7 @@ session 是啥?浏览器有个 cookie,在一段时间内这个 cookie 都存
|
||||
maxInactiveInterval="60"/>
|
||||
```
|
||||
|
||||
然后指定 redis 的 host 和 port 就 ok 了。
|
||||
然后指定 Redis 的 host 和 port 就 ok 了。
|
||||
|
||||
```xml
|
||||
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
|
||||
@ -45,17 +49,19 @@ session 是啥?浏览器有个 cookie,在一段时间内这个 cookie 都存
|
||||
maxInactiveInterval="60"/>
|
||||
```
|
||||
|
||||
还可以用上面这种方式基于 redis 哨兵支持的 redis 高可用集群来保存 session 数据,都是 ok 的。
|
||||
还可以用上面这种方式基于 Redis 哨兵支持的 Redis 高可用集群来保存 Session 数据,都是 ok 的。
|
||||
|
||||
### spring session + redis
|
||||
上面所说的第二种方式会与 tomcat 容器重耦合,如果我要将 web 容器迁移成 jetty,难道还要重新把 jetty 都配置一遍?
|
||||
### Spring Session + Redis
|
||||
|
||||
因为上面那种 tomcat + redis 的方式好用,但是会**严重依赖于web容器**,不好将代码移植到其他 web 容器上去,尤其是你要是换了技术栈咋整?比如换成了 spring cloud 或者是 spring boot 之类的呢?
|
||||
上面所说的第二种方式会与 Tomcat 容器重耦合,如果我要将 Web 容器迁移成 Jetty,难道还要重新把 Jetty 都配置一遍?
|
||||
|
||||
所以现在比较好的还是基于 Java 一站式解决方案,也就是 spring。人家 spring 基本上承包了大部分我们需要使用的框架,spirng cloud 做微服务,spring boot 做脚手架,所以用 sping session 是一个很好的选择。
|
||||
因为上面那种 Tomcat + Redis 的方式好用,但是会**严重依赖于 Web 容器**,不好将代码移植到其他 Web 容器上去,尤其是你要是换了技术栈咋整?比如换成了 Spring Cloud 或者是 Spring Boot 之类的呢?
|
||||
|
||||
所以现在比较好的还是基于 Java 一站式解决方案,也就是 Spring。人家 Spring 基本上承包了大部分我们需要使用的框架,Spirng Cloud 做微服务,Spring Boot 做脚手架,所以用 Spring Session 是一个很好的选择。
|
||||
|
||||
在 pom.xml 中配置:
|
||||
```xml
|
||||
|
||||
``` xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session-data-redis</artifactId>
|
||||
@ -68,8 +74,9 @@ session 是啥?浏览器有个 cookie,在一段时间内这个 cookie 都存
|
||||
</dependency>
|
||||
```
|
||||
|
||||
在 spring 配置文件中配置:
|
||||
```xml
|
||||
在 Spring 配置文件中配置:
|
||||
|
||||
``` xml
|
||||
<bean id="redisHttpSessionConfiguration"
|
||||
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
|
||||
<property name="maxInactiveIntervalInSeconds" value="600"/>
|
||||
@ -92,7 +99,8 @@ session 是啥?浏览器有个 cookie,在一段时间内这个 cookie 都存
|
||||
```
|
||||
|
||||
在 web.xml 中配置:
|
||||
```xml
|
||||
|
||||
``` xml
|
||||
<filter>
|
||||
<filter-name>springSessionRepositoryFilter</filter-name>
|
||||
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
|
||||
@ -104,7 +112,8 @@ session 是啥?浏览器有个 cookie,在一段时间内这个 cookie 都存
|
||||
```
|
||||
|
||||
示例代码:
|
||||
```java
|
||||
|
||||
``` java
|
||||
@RestController
|
||||
@RequestMapping("/test")
|
||||
public class TestController {
|
||||
@ -123,7 +132,6 @@ public class TestController {
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码就是 ok 的,给 Spring Session 配置基于 Redis 来存储 Session 数据,然后配置了一个 Spring Session 的过滤器,这样的话,Session 相关操作都会交给 Spring Session 来管了。接着在代码中,就用原生的 Session 操作,就是直接基于 Spring Session 从 Redis 中获取数据了。
|
||||
|
||||
上面的代码就是 ok 的,给 sping session 配置基于 redis 来存储 session 数据,然后配置了一个 spring session 的过滤器,这样的话,session 相关操作都会交给 spring session 来管了。接着在代码中,就用原生的 session 操作,就是直接基于 spring sesion 从 redis 中获取数据了。
|
||||
|
||||
实现分布式的会话有很多种方式,我说的只不过是比较常见的几种方式,tomcat + redis 早期比较常用,但是会重耦合到 tomcat 中;近些年,通过 spring session 来实现。
|
||||
实现分布式的会话有很多种方式,我说的只不过是比较常见的几种方式,Tomcat + Redis 早期比较常用,但是会重耦合到 Tomcat 中;近些年,通过 Spring Session 来实现。
|
||||
|
23
docs/distributed-system/distributed-system-cap.md
Normal file
@ -0,0 +1,23 @@
|
||||
## 分布式系统 CAP 定理 P 代表什么含义
|
||||
|
||||
作者之前在看 CAP 定理时抱有很大的疑惑,CAP 定理的定义是指在分布式系统中三者只能满足其二,也就是存在分布式 CA 系统的。作者在网络上查阅了很多关于 CAP 文章,虽然这些文章对于 P 的解释五花八门,但总结下来这些观点大多都是指 P 是不可缺少的,也就是说在分布式系统只能是 AP 或者 CP,这种理论与我之前所认识的理论(存在分布式 CA 系统)是冲突的,所以才有了疑惑。
|
||||
|
||||
> 这个定理起源于加州大学柏克莱分校(University of California, Berkeley)的计算机科学家埃里克·布鲁尔在 2000 年的分布式计算原理研讨会(PODC)上提出的一个猜想。 在 2002 年,麻省理工学院(MIT)的赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明,使之成为一个定理。
|
||||
|
||||
### 什么是 CAP 定理(CAP theorem)
|
||||
|
||||
在理论计算机科学中,CAP 定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
|
||||
|
||||
- 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本)
|
||||
- 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
|
||||
- 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。)
|
||||
|
||||
### 分区容错性(Partition tolerance)
|
||||
|
||||
理解 CAP 理论的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了 C 性质。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了 A 性质。除非两个节点可以互相通信,才能既保证 C 又保证 A,这又会导致丧失 P 性质。
|
||||
|
||||
- P 指的是分区容错性,分区现象产生后需要容错,容错是指在 A 与 C 之间选择。如果分布式系统没有分区现象(没有出现不一致不可用情况) 本身就没有分区 ,既然没有分区则就更没有分区容错性 P。
|
||||
- 无论我设计的系统是 AP 还是 CP 系统如果没有出现不一致不可用。 则该系统就处于 CA 状态
|
||||
- P 的体现前提是得有分区情况存在
|
||||
|
||||
> 文章来源:[维基百科 CAP 定理](https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86)
|
@ -2,6 +2,7 @@
|
||||
分布式服务接口的幂等性如何设计(比如不能重复扣款)?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
从这个问题开始,面试官就已经进入了**实际的生产问题**的面试了。
|
||||
|
||||
一个分布式系统中的某个接口,该如何保证幂等性?这个事儿其实是你做分布式系统的时候必须要考虑的一个生产环境的技术问题。啥意思呢?
|
||||
@ -13,15 +14,17 @@
|
||||
所以你肯定得知道这事儿,否则你做出来的分布式系统恐怕容易埋坑。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
这个不是技术问题,这个没有通用的一个方法,这个应该**结合业务**来保证幂等性。
|
||||
|
||||
所谓**幂等性**,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款、不能多插入一条数据、不能将统计值多加了 1。这就是幂等性。
|
||||
|
||||
其实保证幂等性主要是三点:
|
||||
- 对于每个请求必须有一个唯一的标识,举个栗子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次,对吧。
|
||||
- 每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录个状态啥的,比如支付之前记录一条这个订单的支付流水。
|
||||
- 每次接收请求需要进行判断,判断之前是否处理过。比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。
|
||||
|
||||
实际运作过程中,你要结合自己的业务来,比如说利用 redis,用 orderId 作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。
|
||||
* 对于每个请求必须有一个唯一的标识,举个栗子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次,对吧。
|
||||
* 每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录个状态啥的,比如支付之前记录一条这个订单的支付流水。
|
||||
* 每次接收请求需要进行判断,判断之前是否处理过。比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。
|
||||
|
||||
要求是支付一个订单,必须插入一条支付流水,order_id 建一个唯一键 `unique key`。你在支付一个订单之前,先插入一条支付流水,order_id 就已经进去了。你就可以写一个标识到 redis 里面去,`set order_id payed`,下一次重复请求过来了,先查 redis 的 order_id 对应的 value,如果是 `payed` 就说明已经支付过了,你就别重复支付了。
|
||||
实际运作过程中,你要结合自己的业务来,比如说利用 Redis,用 orderId 作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。
|
||||
|
||||
要求是支付一个订单,必须插入一条支付流水,order_id 建一个唯一键 `unique key` 。你在支付一个订单之前,先插入一条支付流水,order_id 就已经进去了。你就可以写一个标识到 Redis 里面去, `set order_id payed` ,下一次重复请求过来了,先查 Redis 的 order_id 对应的 value,如果是 `payed` 就说明已经支付过了,你就别重复支付了。
|
||||
|
@ -1,30 +1,37 @@
|
||||
## 分布式系统面试连环炮
|
||||
有一些同学,之前呢主要是做传统行业,或者外包项目,一直是在那种小的公司,技术一直都搞的比较简单。他们有共同的一个问题,就是都没怎么搞过分布式系统,现在互联网公司,一般都是做分布式的系统,大家都不是做底层的分布式系统、分布式存储系统 hadoop hdfs、分布式计算系统 hadoop mapreduce / spark、分布式流式计算系统 storm。
|
||||
有一些同学,之前呢主要是做传统行业,或者外包项目,一直是在那种小的公司,技术一直都搞的比较简单。他们有共同的一个问题,就是都没怎么搞过分布式系统,现在互联网公司,一般都是做分布式的系统,大家都不是做底层的分布式系统、分布式存储系统 Hadoop HDFS、分布式计算系统 Hadoop MapReduce / Spark、分布式流式计算系统 Storm。
|
||||
|
||||
分布式业务系统,就是把原来用 Java 开发的一个大块系统,给拆分成**多个子系统**,多个子系统之间互相调用,形成一个大系统的整体。假设原来你做了一个 OA 系统,里面包含了权限模块、员工模块、请假模块、财务模块,一个工程,里面包含了一堆模块,模块与模块之间会互相去调用,1 台机器部署。现在如果你把这个系统给拆开,权限系统、员工系统、请假系统、财务系统 4 个系统,4 个工程,分别在 4 台机器上部署。一个请求过来,完成这个请求,这个员工系统,调用权限系统,调用请假系统,调用财务系统,4 个系统分别完成了一部分的事情,最后 4 个系统都干完了以后,才认为是这个请求已经完成了。
|
||||
|
||||

|
||||

|
||||
|
||||
> 近几年开始兴起和流行 Spring Cloud,刚流行,还没开始普及,目前普及的是 dubbo,因此这里也主要讲 dubbo。
|
||||
> 近几年开始兴起和流行 Spring Cloud,刚流行,还没开始普及,目前普及的是 Dubbo,因此这里也主要讲 Dubbo。
|
||||
|
||||
面试官可能会问你以下问题。
|
||||
|
||||
### 为什么要进行系统拆分?
|
||||
- 为什么要进行系统拆分?如何进行系统拆分?拆分后不用dubbo可以吗?dubbo和thrift有什么区别呢?
|
||||
|
||||
* 为什么要进行系统拆分?如何进行系统拆分?拆分后不用 Dubbo 可以吗?Dubbo 和 thrift 有什么区别呢?
|
||||
|
||||
### 分布式服务框架
|
||||
- 说一下的 dubbo 的工作原理?注册中心挂了可以继续通信吗?
|
||||
- dubbo 支持哪些序列化协议?说一下 hessian 的数据结构?PB 知道吗?为什么 PB 的效率是最高的?
|
||||
- dubbo 负载均衡策略和高可用策略都有哪些?动态代理策略呢?
|
||||
- dubbo 的 spi 思想是什么?
|
||||
- 如何基于 dubbo 进行服务治理、服务降级、失败重试以及超时重试?
|
||||
- 分布式服务接口的幂等性如何设计(比如不能重复扣款)?
|
||||
- 分布式服务接口请求的顺序性如何保证?
|
||||
- 如何自己设计一个类似 dubbo 的 rpc 框架?
|
||||
|
||||
* 说一下的 Dubbo 的工作原理?注册中心挂了可以继续通信吗?
|
||||
* Dubbo 支持哪些序列化协议?说一下 Hessian 的数据结构?PB 知道吗?为什么 PB 的效率是最高的?
|
||||
* Dubbo 负载均衡策略和高可用策略都有哪些?动态代理策略呢?
|
||||
* Dubbo 的 SPI 思想是什么?
|
||||
* 如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?
|
||||
* 分布式服务接口的幂等性如何设计(比如不能重复扣款)?
|
||||
* 分布式服务接口请求的顺序性如何保证?
|
||||
* 如何自己设计一个类似 Dubbo 的 RPC 框架?
|
||||
|
||||
### 分布式锁
|
||||
- 使用 redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
|
||||
|
||||
* 使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
|
||||
|
||||
### 分布式事务
|
||||
- 分布式事务了解吗?你们如何解决分布式事务问题的?TCC 如果出现网络连不通怎么办?XA 的一致性如何保证?
|
||||
|
||||
* 分布式事务了解吗?你们如何解决分布式事务问题的?TCC 如果出现网络连不通怎么办?XA 的一致性如何保证?
|
||||
|
||||
### 分布式会话
|
||||
- 集群部署时的分布式 session 如何实现?
|
||||
|
||||
* 集群部署时的分布式 Session 如何实现?
|
||||
|
@ -2,6 +2,7 @@
|
||||
分布式服务接口请求的顺序性如何保证?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实分布式系统接口的调用顺序,也是个问题,一般来说是不用保证顺序的。但是**有时候**可能确实是需要**严格的顺序**保证。给大家举个例子,你服务 A 调用服务 B,先插入再删除。好,结果俩请求过去了,落在不同机器上,可能插入请求因为某些原因执行慢了一些,导致删除请求先执行了,此时因为没数据所以啥效果也没有;结果这个时候插入请求过来了,好,数据插入进去了,那就尴尬了。
|
||||
|
||||
本来应该是 “先插入 -> 再删除”,这条数据应该没了,结果现在 “先删除 -> 再插入”,数据还存在,最后你死都想不明白是怎么回事。
|
||||
@ -9,12 +10,13 @@
|
||||
所以这都是分布式系统一些很常见的问题。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
首先,一般来说,个人建议是,你们从业务逻辑上设计的这个系统最好是不需要这种顺序性的保证,因为一旦引入顺序性保障,比如使用**分布式锁**,会**导致系统复杂度上升**,而且会带来**效率低下**,热点数据压力过大等问题。
|
||||
|
||||
下面我给个我们用过的方案吧,简单来说,首先你得用 dubbo 的一致性 hash 负载均衡策略,将比如某一个订单 id 对应的请求都给分发到某个机器上去,接着就是在那个机器上,因为可能还是多线程并发执行的,你可能得立即将某个订单 id 对应的请求扔一个**内存队列**里去,强制排队,这样来确保他们的顺序性。
|
||||
下面我给个我们用过的方案吧,简单来说,首先你得用 Dubbo 的一致性 hash 负载均衡策略,将比如某一个订单 id 对应的请求都给分发到某个机器上去,接着就是在那个机器上,因为可能还是多线程并发执行的,你可能得立即将某个订单 id 对应的请求扔一个**内存队列**里去,强制排队,这样来确保他们的顺序性。
|
||||
|
||||

|
||||

|
||||
|
||||
但是这样引发的后续问题就很多,比如说要是某个订单对应的请求特别多,造成某台机器成**热点**怎么办?解决这些问题又要开启后续一连串的复杂技术方案......曾经这类问题弄的我们头疼不已,所以,还是建议什么呢?
|
||||
但是这样引发的后续问题就很多,比如说要是某个订单对应的请求特别多,造成某台机器成**热点**怎么办?解决这些问题又要开启后续一连串的复杂技术方案...... 曾经这类问题弄的我们头疼不已,所以,还是建议什么呢?
|
||||
|
||||
最好是比如说刚才那种,一个订单的插入和删除操作,能不能合并成一个操作,就是一个删除,或者是其它什么,避免这种问题的产生。
|
||||
最好是比如说刚才那种,一个订单的插入和删除操作,能不能合并成一个操作,就是一个删除,或者是其它什么,避免这种问题的产生。
|
||||
|
@ -37,9 +37,9 @@
|
||||
|
||||
TCC 的全称是: `Try` 、 `Confirm` 、 `Cancel` 。
|
||||
|
||||
- Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行**锁定或者预留**。
|
||||
- Confirm 阶段:这个阶段说的是在各个服务中**执行实际的操作**。
|
||||
- Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要**进行补偿**,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)
|
||||
* Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行**锁定或者预留**。
|
||||
* Confirm 阶段:这个阶段说的是在各个服务中**执行实际的操作**。
|
||||
* Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要**进行补偿**,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)
|
||||
|
||||
这种方案说实话几乎很少人使用,我们用的也比较少,但是也有使用的场景。因为这个**事务回滚**实际上是**严重依赖于你自己写代码来回滚和补偿**了,会造成补偿代码巨大,非常之恶心。
|
||||
|
||||
|
@ -2,20 +2,26 @@
|
||||
dubbo 负载均衡策略和集群容错策略都有哪些?动态代理策略呢?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
继续深问吧,这些都是用 dubbo 必须知道的一些东西,你得知道基本原理,知道序列化是什么协议,还得知道具体用 dubbo 的时候,如何负载均衡,如何高可用,如何动态代理。
|
||||
|
||||
说白了,就是看你对 dubbo 熟悉不熟悉:
|
||||
- dubbo 工作原理:服务注册、注册中心、消费者、代理通信、负载均衡;
|
||||
- 网络通信、序列化:dubbo 协议、长连接、NIO、hessian 序列化协议;
|
||||
- 负载均衡策略、集群容错策略、动态代理策略:dubbo 跑起来的时候一些功能是如何运转的?怎么做负载均衡?怎么做集群容错?怎么生成动态代理?
|
||||
- dubbo SPI 机制:你了解不了解 dubbo 的 SPI 机制?如何基于 SPI 机制对 dubbo 进行扩展?
|
||||
|
||||
* dubbo 工作原理:服务注册、注册中心、消费者、代理通信、负载均衡;
|
||||
* 网络通信、序列化:dubbo 协议、长连接、NIO、hessian 序列化协议;
|
||||
* 负载均衡策略、集群容错策略、动态代理策略:dubbo 跑起来的时候一些功能是如何运转的?怎么做负载均衡?怎么做集群容错?怎么生成动态代理?
|
||||
* dubbo SPI 机制:你了解不了解 dubbo 的 SPI 机制?如何基于 SPI 机制对 dubbo 进行扩展?
|
||||
|
||||
## 面试题剖析
|
||||
### dubbo 负载均衡策略
|
||||
#### random loadbalance
|
||||
默认情况下,dubbo 是 random load balance ,即**随机**调用实现负载均衡,可以对 provider 不同实例**设置不同的权重**,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。
|
||||
|
||||
#### roundrobin loadbalance
|
||||
### dubbo 负载均衡策略
|
||||
|
||||
#### RandomLoadBalance
|
||||
|
||||
默认情况下,dubbo 是 RandomLoadBalance ,即**随机**调用实现负载均衡,可以对 provider 不同实例**设置不同的权重**,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。
|
||||
|
||||
#### RoundRobinLoadBalance
|
||||
|
||||
这个的话默认就是均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高。所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。
|
||||
|
||||
举个栗子。
|
||||
@ -24,62 +30,75 @@ dubbo 负载均衡策略和集群容错策略都有哪些?动态代理策略
|
||||
|
||||
这个时候,可以给两台 8 核 16G 的机器设置权重 4,给剩余 1 台 4 核 8G 的机器设置权重 2。
|
||||
|
||||
#### leastactive loadbalance
|
||||
#### LeastActiveLoadBalance
|
||||
|
||||
这个就是自动感知一下,如果某个机器性能越差,那么接收的请求越少,越不活跃,此时就会给**不活跃的性能差的机器更少的请求**。
|
||||
|
||||
#### consistanthash loadbalance
|
||||
#### ConsistentHashLoadBalance
|
||||
|
||||
一致性 Hash 算法,相同参数的请求一定分发到一个 provider 上去,provider 挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。**如果你需要的不是随机负载均衡**,是要一类请求都到一个节点,那就走这个一致性 Hash 策略。
|
||||
|
||||
> 关于 dubbo 负载均衡策略更加详细的描述,可以查看官网 http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html 。
|
||||
|
||||
### dubbo 集群容错策略
|
||||
#### failover cluster 模式
|
||||
|
||||
#### Failover Cluster 模式
|
||||
失败自动切换,自动重试其他机器,**默认**就是这个,常见于读操作。(失败重试其它机器)
|
||||
|
||||
可以通过以下几种方式配置重试次数:
|
||||
|
||||
```xml
|
||||
``` xml
|
||||
<dubbo:service retries="2" />
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```xml
|
||||
``` xml
|
||||
<dubbo:reference retries="2" />
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```xml
|
||||
``` xml
|
||||
<dubbo:reference>
|
||||
<dubbo:method name="findFoo" retries="2" />
|
||||
</dubbo:reference>
|
||||
```
|
||||
|
||||
#### failfast cluster 模式
|
||||
#### Failfast Cluster 模式
|
||||
|
||||
一次调用失败就立即失败,常见于非幂等性的写操作,比如新增一条记录(调用失败就立即失败)
|
||||
|
||||
#### failsafe cluster 模式
|
||||
#### Failsafe Cluster 模式
|
||||
|
||||
出现异常时忽略掉,常用于不重要的接口调用,比如记录日志。
|
||||
|
||||
配置示例如下:
|
||||
|
||||
```xml
|
||||
``` xml
|
||||
<dubbo:service cluster="failsafe" />
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```xml
|
||||
``` xml
|
||||
<dubbo:reference cluster="failsafe" />
|
||||
```
|
||||
|
||||
#### failback cluster 模式
|
||||
#### Failback Cluster 模式
|
||||
|
||||
失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种。
|
||||
|
||||
#### forking cluster 模式
|
||||
#### Forking Cluster 模式
|
||||
|
||||
**并行调用**多个 provider,只要一个成功就立即返回。常用于实时性要求比较高的读操作,但是会浪费更多的服务资源,可通过 `forks="2"` 来设置最大并行数。
|
||||
|
||||
#### broadcacst cluster
|
||||
逐个调用所有的 provider。任何一个 provider 出错则报错(从`2.1.0` 版本开始支持)。通常用于通知所有提供者更新缓存或日志等本地资源信息。
|
||||
#### Broadcast Cluster 模式
|
||||
|
||||
### dubbo动态代理策略
|
||||
默认使用 javassist 动态字节码生成,创建代理类。但是可以通过 spi 扩展机制配置自己的动态代理策略。
|
||||
逐个调用所有的 provider。任何一个 provider 出错则报错(从 `2.1.0` 版本开始支持)。通常用于通知所有提供者更新缓存或日志等本地资源信息。
|
||||
|
||||
> 关于 dubbo 集群容错策略更加详细的描述,可以查看官网 http://dubbo.apache.org/zh-cn/docs/source_code_guide/cluster.html 。
|
||||
|
||||
### dubbo 动态代理策略
|
||||
|
||||
默认使用 javassist 动态字节码生成,创建代理类。但是可以通过 spi 扩展机制配置自己的动态代理策略。
|
||||
|
@ -2,6 +2,7 @@
|
||||
说一下的 dubbo 的工作原理?注册中心挂了可以继续通信吗?说说一次 rpc 请求的流程?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
MQ、ES、Redis、Dubbo,上来先问你一些**思考性的问题**、**原理**,比如 kafka 高可用架构原理、es 分布式架构原理、redis 线程模型原理、Dubbo 工作原理;之后就是生产环境里可能会碰到的一些问题,因为每种技术引入之后生产环境都可能会碰到一些问题;再来点综合的,就是系统设计,比如让你设计一个 MQ、设计一个搜索引擎、设计一个缓存、设计一个 rpc 框架等等。
|
||||
|
||||
那既然开始聊分布式系统了,自然重点先聊聊 dubbo 了,毕竟 dubbo 是目前事实上大部分公司的分布式系统的 rpc 框架标准,基于 dubbo 也可以构建一整套的微服务架构。但是需要自己大量开发。
|
||||
@ -11,25 +12,29 @@ MQ、ES、Redis、Dubbo,上来先问你一些**思考性的问题**、**原理
|
||||
既然聊 dubbo,那肯定是先从 dubbo 原理开始聊了,你先说说 dubbo 支撑 rpc 分布式调用的架构啥的,然后说说一次 rpc 请求 dubbo 是怎么给你完成的,对吧。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### dubbo 工作原理
|
||||
- 第一层:service 层,接口层,给服务提供者和消费者来实现的
|
||||
- 第二层:config 层,配置层,主要是对 dubbo 进行各种配置的
|
||||
- 第三层:proxy 层,服务代理层,无论是 consumer 还是 provider,dubbo 都会给你生成代理,代理之间进行网络通信
|
||||
- 第四层:registry 层,服务注册层,负责服务的注册与发现
|
||||
- 第五层:cluster 层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
|
||||
- 第六层:monitor 层,监控层,对 rpc 接口的调用次数和调用时间进行监控
|
||||
- 第七层:protocal 层,远程调用层,封装 rpc 调用
|
||||
- 第八层:exchange 层,信息交换层,封装请求响应模式,同步转异步
|
||||
- 第九层:transport 层,网络传输层,抽象 mina 和 netty 为统一接口
|
||||
- 第十层:serialize 层,数据序列化层
|
||||
|
||||
* 第一层:service 层,接口层,给服务提供者和消费者来实现的
|
||||
* 第二层:config 层,配置层,主要是对 dubbo 进行各种配置的
|
||||
* 第三层:proxy 层,服务代理层,无论是 consumer 还是 provider,dubbo 都会给你生成代理,代理之间进行网络通信
|
||||
* 第四层:registry 层,服务注册层,负责服务的注册与发现
|
||||
* 第五层:cluster 层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
|
||||
* 第六层:monitor 层,监控层,对 rpc 接口的调用次数和调用时间进行监控
|
||||
* 第七层:protocal 层,远程调用层,封装 rpc 调用
|
||||
* 第八层:exchange 层,信息交换层,封装请求响应模式,同步转异步
|
||||
* 第九层:transport 层,网络传输层,抽象 mina 和 netty 为统一接口
|
||||
* 第十层:serialize 层,数据序列化层
|
||||
|
||||
### 工作流程
|
||||
- 第一步:provider 向注册中心去注册
|
||||
- 第二步:consumer 从注册中心订阅服务,注册中心会通知 consumer 注册好的服务
|
||||
- 第三步:consumer 调用 provider
|
||||
- 第四步:consumer 和 provider 都异步通知监控中心
|
||||
|
||||

|
||||
* 第一步:provider 向注册中心去注册
|
||||
* 第二步:consumer 从注册中心订阅服务,注册中心会通知 consumer 注册好的服务
|
||||
* 第三步:consumer 调用 provider
|
||||
* 第四步:consumer 和 provider 都异步通知监控中心
|
||||
|
||||

|
||||
|
||||
### 注册中心挂了可以继续通信吗?
|
||||
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息**拉取到本地缓存**,所以注册中心挂了可以继续通信。
|
||||
|
||||
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息**拉取到本地缓存**,所以注册中心挂了可以继续通信。
|
||||
|
@ -2,21 +2,25 @@
|
||||
如何自己设计一个类似 Dubbo 的 RPC 框架?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
说实话,就这问题,其实就跟问你如何自己设计一个 MQ 一样的道理,就考两个:
|
||||
- 你有没有对某个 rpc 框架原理有非常深入的理解。
|
||||
- 你能不能从整体上来思考一下,如何设计一个 rpc 框架,考考你的系统设计能力。
|
||||
|
||||
* 你有没有对某个 rpc 框架原理有非常深入的理解。
|
||||
* 你能不能从整体上来思考一下,如何设计一个 rpc 框架,考考你的系统设计能力。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
其实问到你这问题,你起码不能认怂,因为是知识的扫盲,那我不可能给你深入讲解什么 kafka 源码剖析,dubbo 源码剖析,何况我就算讲了,你要真的消化理解和吸收,起码个把月以后了。
|
||||
|
||||
所以我给大家一个建议,遇到这类问题,起码从你了解的类似框架的原理入手,自己说说参照 dubbo 的原理,你来设计一下,举个例子,dubbo 不是有那么多分层么?而且每个分层是干啥的,你大概是不是知道?那就按照这个思路大致说一下吧,起码你不能懵逼,要比那些上来就懵,啥也说不出来的人要好一些。
|
||||
|
||||
举个栗子,我给大家说个最简单的回答思路:
|
||||
- 上来你的服务就得去注册中心注册吧,你是不是得有个注册中心,保留各个服务的信息,可以用 zookeeper 来做,对吧。
|
||||
- 然后你的消费者需要去注册中心拿对应的服务信息吧,对吧,而且每个服务可能会存在于多台机器上。
|
||||
- 接着你就该发起一次请求了,咋发起?当然是基于动态代理了,你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址。
|
||||
- 然后找哪个机器发送请求?那肯定得有个负载均衡算法了,比如最简单的可以随机轮询是不是。
|
||||
- 接着找到一台机器,就可以跟它发送请求了,第一个问题咋发送?你可以说用 netty 了,nio 方式;第二个问题发送啥格式数据?你可以说用 hessian 序列化协议了,或者是别的,对吧。然后请求过去了。
|
||||
- 服务器那边一样的,需要针对你自己的服务生成一个动态代理,监听某个网络端口了,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码,对吧。
|
||||
|
||||
这就是一个最最基本的 rpc 框架的思路,先不说你有多牛逼的技术功底,哪怕这个最简单的思路你先给出来行不行?
|
||||
* 上来你的服务就得去注册中心注册吧,你是不是得有个注册中心,保留各个服务的信息,可以用 zookeeper 来做,对吧。
|
||||
* 然后你的消费者需要去注册中心拿对应的服务信息吧,对吧,而且每个服务可能会存在于多台机器上。
|
||||
* 接着你就该发起一次请求了,咋发起?当然是基于动态代理了,你面向接口获取到一个动态代理,这个动态代理就是接口在本地的一个代理,然后这个代理会找到服务对应的机器地址。
|
||||
* 然后找哪个机器发送请求?那肯定得有个负载均衡算法了,比如最简单的可以随机轮询是不是。
|
||||
* 接着找到一台机器,就可以跟它发送请求了,第一个问题咋发送?你可以说用 netty 了,nio 方式;第二个问题发送啥格式数据?你可以说用 hessian 序列化协议了,或者是别的,对吧。然后请求过去了。
|
||||
* 服务器那边一样的,需要针对你自己的服务生成一个动态代理,监听某个网络端口了,然后代理你本地的服务代码。接收到请求的时候,就调用对应的服务代码,对吧。
|
||||
|
||||
这就是一个最最基本的 rpc 框架的思路,先不说你有多牛逼的技术功底,哪怕这个最简单的思路你先给出来行不行?
|
||||
|
@ -2,17 +2,20 @@
|
||||
dubbo 支持哪些通信协议?支持哪些序列化协议?说一下 Hessian 的数据结构?PB 知道吗?为什么 PB 的效率是最高的?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
上一个问题,说说 dubbo 的基本工作原理,那是你必须知道的,至少要知道 dubbo 分成哪些层,然后平时怎么发起 rpc 请求的,注册、发现、调用,这些是基本的。
|
||||
|
||||
接着就可以针对底层进行深入的问问了,比如第一步就可以先问问序列化协议这块,就是平时 RPC 的时候怎么走的?
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
**序列化**,就是把数据结构或者是一些对象,转换为二进制串的过程,而**反序列化**是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
|
||||
|
||||

|
||||

|
||||
|
||||
### dubbo 支持不同的通信协议
|
||||
- dubbo 协议
|
||||
|
||||
* dubbo 协议
|
||||
|
||||
**默认**就是走 dubbo 协议,单一长连接,进行的是 NIO 异步通信,基于 hessian 作为序列化协议。使用的场景是:传输数据量小(每次请求在 100kb 以内),但是并发量很高。
|
||||
|
||||
@ -20,54 +23,55 @@ dubbo 支持哪些通信协议?支持哪些序列化协议?说一下 Hessian
|
||||
|
||||
长连接,通俗点说,就是建立连接过后可以持续发送请求,无须再建立连接。
|
||||
|
||||

|
||||

|
||||
|
||||
而短连接,每次要发送请求之前,需要先重新建立一次连接。
|
||||
|
||||

|
||||

|
||||
|
||||
- rmi 协议
|
||||
* rmi 协议
|
||||
|
||||
走 Java 二进制序列化,多个短连接,适合消费者和提供者数量差不多的情况,适用于文件的传输,一般较少用。
|
||||
|
||||
- hessian 协议
|
||||
* hessian 协议
|
||||
|
||||
走 hessian 序列化协议,多个短连接,适用于提供者数量比消费者数量还多的情况,适用于文件的传输,一般较少用。
|
||||
|
||||
- http 协议
|
||||
* http 协议
|
||||
|
||||
走 json 序列化。
|
||||
走表单序列化。
|
||||
|
||||
- webservice
|
||||
* webservice
|
||||
|
||||
走 SOAP 文本序列化。
|
||||
|
||||
### dubbo 支持的序列化协议
|
||||
|
||||
dubbo 支持 hession、Java 二进制序列化、json、SOAP 文本序列化多种序列化协议。但是 hessian 是其默认的序列化协议。
|
||||
|
||||
### 说一下 Hessian 的数据结构
|
||||
|
||||
Hessian 的对象序列化机制有 8 种原始类型:
|
||||
|
||||
- 原始二进制数据
|
||||
- boolean
|
||||
- 64-bit date(64 位毫秒值的日期)
|
||||
- 64-bit double
|
||||
- 32-bit int
|
||||
- 64-bit long
|
||||
- null
|
||||
- UTF-8 编码的 string
|
||||
* 原始二进制数据
|
||||
* boolean
|
||||
* 64-bit date(64 位毫秒值的日期)
|
||||
* 64-bit double
|
||||
* 32-bit int
|
||||
* 64-bit long
|
||||
* null
|
||||
* UTF-8 编码的 string
|
||||
|
||||
另外还包括 3 种递归类型:
|
||||
|
||||
- list for lists and arrays
|
||||
- map for maps and dictionaries
|
||||
- object for objects
|
||||
* list for lists and arrays
|
||||
* map for maps and dictionaries
|
||||
* object for objects
|
||||
|
||||
还有一种特殊的类型:
|
||||
|
||||
- ref:用来表示对共享对象的引用。
|
||||
* ref:用来表示对共享对象的引用。
|
||||
|
||||
### 为什么 PB 的效率是最高的?
|
||||
可能有一些同学比较习惯于 `JSON` or `XML` 数据存储格式,对于 `Protocol Buffer` 还比较陌生。`Protocol Buffer` 其实是 Google 出品的一种轻量并且高效的结构化数据存储格式,性能比 `JSON`、`XML` 要高很多。
|
||||
|
||||
其实 PB 之所以性能如此好,主要得益于两个:**第一**,它使用 proto 编译器,自动进行序列化和反序列化,速度非常快,应该比 `XML` 和 `JSON` 快上了 `20~100` 倍;**第二**,它的数据压缩效果好,就是说它序列化后的数据量体积小。因为体积小,传输起来带宽和速度上会有优化。
|
||||
其实 PB 之所以性能如此好,主要得益于两个:**第一**,它使用 proto 编译器,自动进行序列化和反序列化,速度非常快,应该比 `XML` 和 `JSON` 快上了 `20~100` 倍;**第二**,它的数据压缩效果好,就是说它序列化后的数据量体积小。因为体积小,传输起来带宽和速度上会有优化。
|
||||
|
@ -2,6 +2,7 @@
|
||||
如何基于 dubbo 进行服务治理、服务降级、失败重试以及超时重试?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
服务治理,这个问题如果问你,其实就是看看你有没有**服务治理**的思想,因为这个是做过复杂微服务的人肯定会遇到的一个问题。
|
||||
|
||||
**服务降级**,这个是涉及到复杂分布式系统中必备的一个话题,因为分布式系统互相来回调用,任何一个系统故障了,你不降级,直接就全盘崩溃?那就太坑爹了吧。
|
||||
@ -11,34 +12,40 @@
|
||||
**超时重试**,跟上面一样,如果不小心网络慢一点,超时了,如何重试?
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 服务治理
|
||||
|
||||
#### 1. 调用链路自动生成
|
||||
|
||||
一个大型的分布式系统,或者说是用现在流行的微服务架构来说吧,**分布式系统由大量的服务组成**。那么这些服务之间互相是如何调用的?调用链路是啥?说实话,几乎到后面没人搞的清楚了,因为服务实在太多了,可能几百个甚至几千个服务。
|
||||
|
||||
那就需要基于 dubbo 做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将**各个服务之间的依赖关系和调用链路生成出来**,做成一张图,显示出来,大家才可以看到对吧。
|
||||
|
||||

|
||||

|
||||
|
||||
#### 2. 服务访问压力以及时长统计
|
||||
|
||||
需要自动统计**各个接口和服务之间的调用次数以及访问延时**,而且要分成两个级别。
|
||||
|
||||
- 一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次,TP50/TP90/TP99,三个档次的请求延时分别是多少;
|
||||
- 第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延时的 TP50/TP90/TP99,分别是多少。
|
||||
* 一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次,TP50/TP90/TP99,三个档次的请求延时分别是多少;
|
||||
* 第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延时的 TP50/TP90/TP99,分别是多少。
|
||||
|
||||
这些东西都搞定了之后,后面才可以来看当前系统的压力主要在哪里,如何来扩容和优化啊。
|
||||
|
||||
#### 3. 其它
|
||||
- 服务分层(避免循环依赖)
|
||||
- 调用链路失败监控和报警
|
||||
- 服务鉴权
|
||||
- 每个服务的可用性的监控(接口调用成功率?几个 9?99.99%,99.9%,99%)
|
||||
|
||||
* 服务分层(避免循环依赖)
|
||||
* 调用链路失败监控和报警
|
||||
* 服务鉴权
|
||||
* 每个服务的可用性的监控(接口调用成功率?几个 9?99.99%,99.9%,99%)
|
||||
|
||||
### 服务降级
|
||||
|
||||
比如说服务 A 调用服务 B,结果服务 B 挂掉了,服务 A 重试几次调用服务 B,还是不行,那么直接降级,走一个备用的逻辑,给用户返回响应。
|
||||
|
||||
举个栗子,我们有接口 `HelloService`。`HelloServiceImpl` 有该接口的具体实现。
|
||||
举个栗子,我们有接口 `HelloService` 。 `HelloServiceImpl` 有该接口的具体实现。
|
||||
|
||||
```java
|
||||
``` java
|
||||
public interface HelloService {
|
||||
void sayHello();
|
||||
}
|
||||
@ -50,7 +57,7 @@ public class HelloServiceImpl implements HelloService {
|
||||
}
|
||||
```
|
||||
|
||||
```xml
|
||||
``` xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
|
||||
@ -83,9 +90,9 @@ public class HelloServiceImpl implements HelloService {
|
||||
|
||||
我们调用接口失败的时候,可以通过 `mock` 统一返回 null。
|
||||
|
||||
mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+`Mock`” 后缀。然后在 Mock 类里实现自己的降级逻辑。
|
||||
mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+ `Mock` ” 后缀。然后在 Mock 类里实现自己的降级逻辑。
|
||||
|
||||
```java
|
||||
``` java
|
||||
public class HelloServiceMock implements HelloService {
|
||||
public void sayHello() {
|
||||
// 降级逻辑
|
||||
@ -94,9 +101,10 @@ public class HelloServiceMock implements HelloService {
|
||||
```
|
||||
|
||||
### 失败重试和超时重试
|
||||
|
||||
所谓失败重试,就是 consumer 调用 provider 要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。配置如下:
|
||||
|
||||
```xml
|
||||
``` xml
|
||||
<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>
|
||||
```
|
||||
|
||||
@ -106,5 +114,5 @@ public class HelloServiceMock implements HelloService {
|
||||
|
||||
可以结合你们公司具体的场景来说说你是怎么设置这些参数的:
|
||||
|
||||
- `timeout`:一般设置为 `200ms`,我们认为不能超过 `200ms` 还没返回。
|
||||
- `retries`:设置 retries,一般是在读请求的时候,比如你要查询个数据,你可以设置个 retries,如果第一次没读到,报错,重试指定的次数,尝试再次读取。
|
||||
* `timeout` :一般设置为 `200ms` ,我们认为不能超过 `200ms` 还没返回。
|
||||
* `retries` :设置 retries,一般是在读请求的时候,比如你要查询个数据,你可以设置个 retries,如果第一次没读到,报错,重试指定的次数,尝试再次读取。
|
||||
|
@ -2,21 +2,24 @@
|
||||
dubbo 的 spi 思想是什么?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
继续深入问呗,前面一些基础性的东西问完了,确定你应该都 ok,了解 dubbo 的一些基本东西,那么问个稍微难一点点的问题,就是 spi,先问问你 spi 是啥?然后问问你 dubbo 的 spi 是怎么实现的?
|
||||
|
||||
其实就是看看你对 dubbo 的掌握如何。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### spi 是啥?
|
||||
spi,简单来说,就是 `service provider interface`,说白了是什么意思呢,比如你有个接口,现在这个接口有 3 个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要 spi 了,需要**根据指定的配置**或者是**默认的配置**,去**找到对应的实现类**加载进来,然后用这个实现类的实例对象。
|
||||
spi,简单来说,就是 `service provider interface` ,说白了是什么意思呢,比如你有个接口,现在这个接口有 3 个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要 spi 了,需要**根据指定的配置**或者是**默认的配置**,去**找到对应的实现类**加载进来,然后用这个实现类的实例对象。
|
||||
|
||||
举个栗子。
|
||||
|
||||
你有一个接口 A。A1/A2/A3 分别是接口A的不同实现。你通过配置 `接口 A = 实现 A2`,那么在系统实际运行的时候,会加载你的配置,用实现 A2 实例化一个对象来提供服务。
|
||||
你有一个接口 A。A1/A2/A3 分别是接口A的不同实现。你通过配置 `接口 A = 实现 A2` ,那么在系统实际运行的时候,会加载你的配置,用实现 A2 实例化一个对象来提供服务。
|
||||
|
||||
spi 机制一般用在哪儿?**插件扩展的场景**,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,插到你的开源框架里面,从而扩展某个功能,这个时候 spi 思想就用上了。
|
||||
|
||||
### Java spi 思想的体现
|
||||
|
||||
spi 经典的思想体现,大家平时都在用,比如说 jdbc。
|
||||
|
||||
Java 定义了一套 jdbc 的接口,但是 Java 并没有提供 jdbc 的实现类。
|
||||
@ -26,8 +29,10 @@ Java 定义了一套 jdbc 的接口,但是 Java 并没有提供 jdbc 的实现
|
||||
在系统跑的时候,碰到你使用 jdbc 的接口,他会在底层使用你引入的那个 jar 中提供的实现类。
|
||||
|
||||
### dubbo 的 spi 思想
|
||||
|
||||
dubbo 也用了 spi 思想,不过没有用 jdk 的 spi 机制,是自己实现的一套 spi 机制。
|
||||
```java
|
||||
|
||||
``` java
|
||||
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
|
||||
```
|
||||
|
||||
@ -35,9 +40,9 @@ Protocol 接口,在系统运行的时候,,dubbo 会判断一下应该选
|
||||
|
||||
它会去找一个你配置的 Protocol,将你配置的 Protocol 实现类,加载到 jvm 中来,然后实例化对象,就用你的那个 Protocol 实现类就可以了。
|
||||
|
||||
|
||||
上面那行代码就是 dubbo 里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现好了,没问题。
|
||||
```java
|
||||
|
||||
``` java
|
||||
@SPI("dubbo")
|
||||
public interface Protocol {
|
||||
|
||||
@ -54,15 +59,15 @@ public interface Protocol {
|
||||
}
|
||||
```
|
||||
|
||||
在 dubbo 自己的 jar 里,在`/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol`文件中:
|
||||
```xml
|
||||
在 dubbo 自己的 jar 里,在 `/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol` 文件中:
|
||||
|
||||
``` xml
|
||||
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
|
||||
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
|
||||
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
|
||||
```
|
||||
|
||||
所以说,这就看到了 dubbo 的 spi 机制默认是怎么玩儿的了,其实就是 Protocol 接口,`@SPI("dubbo")` 说的是,通过 SPI 机制来提供实现类,实现类是通过 dubbo 作为默认 key 去配置文件里找到的,配置文件名称与接口全限定名一样的,通过 dubbo 作为 key 可以找到默认的实现类就是 `com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol`。
|
||||
|
||||
所以说,这就看到了 dubbo 的 spi 机制默认是怎么玩儿的了,其实就是 Protocol 接口, `@SPI("dubbo")` 说的是,通过 SPI 机制来提供实现类,实现类是通过 dubbo 作为默认 key 去配置文件里找到的,配置文件名称与接口全限定名一样的,通过 dubbo 作为 key 可以找到默认的实现类就是 `com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol` 。
|
||||
|
||||
如果想要动态替换掉默认的实现类,需要使用 `@Adaptive` 接口,Protocol 接口中,有两个方法加了 `@Adaptive` 注解,就是说那俩接口会被代理实现。
|
||||
|
||||
@ -71,19 +76,21 @@ hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
|
||||
比如这个 Protocol 接口搞了俩 `@Adaptive` 注解标注了方法,在运行的时候会针对 Protocol 生成代理类,这个代理类的那俩方法里面会有代理代码,代理代码会在运行的时候动态根据 url 中的 protocol 来获取那个 key,默认是 dubbo,你也可以自己指定,你如果指定了别的 key,那么就会获取别的实现类的实例了。
|
||||
|
||||
### 如何自己扩展 dubbo 中的组件
|
||||
|
||||
下面来说说怎么来自己扩展 dubbo 中的组件。
|
||||
|
||||
自己写个工程,要是那种可以打成 jar 包的,里面的 `src/main/resources` 目录下,搞一个 `META-INF/services`,里面放个文件叫:`com.alibaba.dubbo.rpc.Protocol`,文件里搞一个`my=com.bingo.MyProtocol`。自己把 jar 弄到 nexus 私服里去。
|
||||
自己写个工程,要是那种可以打成 jar 包的,里面的 `src/main/resources` 目录下,搞一个 `META-INF/services` ,里面放个文件叫: `com.alibaba.dubbo.rpc.Protocol` ,文件里搞一个 `my=com.bingo.MyProtocol` 。自己把 jar 弄到 nexus 私服里去。
|
||||
|
||||
然后自己搞一个 `dubbo provider` 工程,在这个工程里面依赖你自己搞的那个 jar,然后在 spring 配置文件里给个配置:
|
||||
|
||||
```xml
|
||||
``` xml
|
||||
<dubbo:protocol name=”my” port=”20000” />
|
||||
```
|
||||
provider 启动的时候,就会加载到我们 jar 包里的`my=com.bingo.MyProtocol` 这行配置里,接着会根据你的配置使用你定义好的 MyProtocol 了,这个就是简单说明一下,你通过上述方式,可以替换掉大量的 dubbo 内部的组件,就是扔个你自己的 jar 包,然后配置一下即可。
|
||||
|
||||

|
||||
provider 启动的时候,就会加载到我们 jar 包里的 `my=com.bingo.MyProtocol` 这行配置里,接着会根据你的配置使用你定义好的 MyProtocol 了,这个就是简单说明一下,你通过上述方式,可以替换掉大量的 dubbo 内部的组件,就是扔个你自己的 jar 包,然后配置一下即可。
|
||||
|
||||
dubbo 里面提供了大量的类似上面的扩展点,就是说,你如果要扩展一个东西,只要自己写个 jar,让你的 consumer 或者是 provider 工程,依赖你的那个 jar,在你的 jar 里指定目录下配置好接口名称对应的文件,里面通过 `key=实现类`。
|
||||

|
||||
|
||||
然后对于对应的组件,类似 `<dubbo:protocol>` 用你的那个 key 对应的实现类来实现某个接口,你可以自己去扩展 dubbo 的各种功能,提供你自己的实现。
|
||||
dubbo 里面提供了大量的类似上面的扩展点,就是说,你如果要扩展一个东西,只要自己写个 jar,让你的 consumer 或者是 provider 工程,依赖你的那个 jar,在你的 jar 里指定目录下配置好接口名称对应的文件,里面通过 `key=实现类` 。
|
||||
|
||||
然后对于对应的组件,类似 `<dubbo:protocol>` 用你的那个 key 对应的实现类来实现某个接口,你可以自己去扩展 dubbo 的各种功能,提供你自己的实现。
|
||||
|
BIN
docs/distributed-system/images/qrcode-for-doocs.jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/distributed-system/images/qrcode-for-yanglbme.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
@ -2,6 +2,7 @@
|
||||
为什么要进行系统拆分?如何进行系统拆分?拆分后不用 dubbo 可以吗?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
从这个问题开始就进行分布式系统环节了,现在出去面试分布式都成标配了,没有哪个公司不问问你分布式的事儿。你要是不会分布式的东西,简直这简历没法看,没人会让你去面试。
|
||||
|
||||
其实为啥会这样呢?这就是因为整个大行业技术发展的原因。
|
||||
@ -17,6 +18,7 @@
|
||||
直到今日,很高兴看到分布式系统都成行业面试标配了,任何一个普通的程序员都该掌握这个东西,其实这是行业的进步,也是所有 IT 码农的技术进步。所以既然分布式都成标配了,那么面试官当然会问了,因为很多公司现在都是分布式、微服务的架构,那面试官当然得考察考察你了。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 为什么要将系统进行拆分?
|
||||
网上查查,答案极度零散和复杂,很琐碎,原因一大坨。但是我这里给大家直观的感受:
|
||||
|
||||
@ -35,6 +37,7 @@ A 就检查了自己负责的 1 万行代码对应的功能,确保 ok 就闪
|
||||
但是同时,也要**提醒**的一点是,系统拆分成分布式系统之后,大量的分布式系统面临的问题也是接踵而来,所以后面的问题都是在**围绕分布式系统带来的复杂技术挑战**在说。
|
||||
|
||||
### 如何进行系统拆分?
|
||||
|
||||
这个问题说大可以很大,可以扯到领域驱动模型设计上去,说小了也很小,我不太想给大家太过于学术的说法,因为你也不可能背这个答案,过去了直接说吧。还是说的简单一点,大家自己到时候知道怎么回答就行了。
|
||||
|
||||
系统拆分为分布式系统,拆成多个服务,拆成微服务的架构,是需要拆很多轮的。并不是说上来一个架构师一次就给拆好了,而以后都不用拆。
|
||||
@ -52,6 +55,7 @@ A 就检查了自己负责的 1 万行代码对应的功能,确保 ok 就闪
|
||||
扯深了实在很深,所以这里先给大家举个例子,你自己感受一下,**核心意思就是根据情况,先拆分一轮,后面如果系统更复杂了,可以继续分拆**。你根据自己负责系统的例子,来考虑一下就好了。
|
||||
|
||||
### 拆分后不用 dubbo 可以吗?
|
||||
|
||||
当然可以了,大不了最次,就是各个系统之间,直接基于 spring mvc,就纯 http 接口互相通信呗,还能咋样。但是这个肯定是有问题的,因为 http 接口通信维护起来成本很高,你要考虑**超时重试**、**负载均衡**等等各种乱七八糟的问题,比如说你的订单系统调用商品系统,商品系统部署了 5 台机器,你怎么把请求均匀地甩给那 5 台机器?这不就是负载均衡?你要是都自己搞那是可以的,但是确实很痛苦。
|
||||
|
||||
所以 dubbo 说白了,是一种 rpc 框架,就是说本地就是进行接口调用,但是 dubbo 会代理这个调用请求,跟远程机器网络通信,给你处理掉负载均衡、服务实例上下线自动感知、超时重试等等乱七八糟的问题。那你就不用自己做了,用 dubbo 就可以了。
|
||||
所以 dubbo 说白了,是一种 rpc 框架,就是说本地就是进行接口调用,但是 dubbo 会代理这个调用请求,跟远程机器网络通信,给你处理掉负载均衡、服务实例上下线自动感知、超时重试等等乱七八糟的问题。那你就不用自己做了,用 dubbo 就可以了。
|
||||
|
@ -2,6 +2,7 @@
|
||||
zookeeper 都有哪些使用场景?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
现在聊的 topic 是分布式系统,面试官跟你聊完了 dubbo 相关的一些问题之后,已经确认你对分布式服务框架/RPC框架基本都有一些认知了。那么他可能开始要跟你聊分布式相关的其它问题了。
|
||||
|
||||
分布式锁这个东西,很常用的,你做 Java 系统开发,分布式系统,可能会有一些场景会用到。最常用的分布式锁就是基于 zookeeper 来实现的。
|
||||
@ -9,29 +10,34 @@ zookeeper 都有哪些使用场景?
|
||||
其实说实话,问这个问题,一般就是看看你是否了解 zookeeper,因为 zookeeper 是分布式系统中很常见的一个基础系统。而且问的话常问的就是说 zookeeper 的使用场景是什么?看你知道不知道一些基本的使用场景。但是其实 zookeeper 挖深了自然是可以问的很深很深的。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
大致来说,zookeeper 的使用场景如下,我就举几个简单的,大家能说几个就好了:
|
||||
|
||||
- 分布式协调
|
||||
- 分布式锁
|
||||
- 元数据/配置信息管理
|
||||
- HA高可用性
|
||||
* 分布式协调
|
||||
* 分布式锁
|
||||
* 元数据/配置信息管理
|
||||
* HA高可用性
|
||||
|
||||
### 分布式协调
|
||||
|
||||
这个其实是 zookeeper 很经典的一个用法,简单来说,就好比,你 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上**对某个节点的值注册个监听器**,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 系统立马就可以收到通知,完美解决。
|
||||
|
||||

|
||||

|
||||
|
||||
### 分布式锁
|
||||
|
||||
举个栗子。对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也**尝试去创建**那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。
|
||||
|
||||

|
||||

|
||||
|
||||
### 元数据/配置信息管理
|
||||
|
||||
zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zookeeper 么?
|
||||
|
||||

|
||||

|
||||
|
||||
### HA高可用性
|
||||
|
||||
这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个**重要进程一般会做主备**两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。
|
||||
|
||||

|
||||

|
||||
|
@ -1,8 +1,34 @@
|
||||
# 项目额外页面
|
||||
|
||||
## Offer 与进阶
|
||||
- [我的 Offer 在哪里?](https://doocs.gitee.io/advanced-java/#/docs/extra-page/offer)
|
||||
- [让我们同步进阶!](https://doocs.gitee.io/advanced-java/#/docs/extra-page/advanced)
|
||||
|
||||
* [我的 Offer 在哪里?](https://doocs.gitee.io/advanced-java/#/docs/extra-page/offer)
|
||||
* [让我们同步进阶!](https://doocs.gitee.io/advanced-java/#/docs/extra-page/advanced)
|
||||
|
||||
## 项目 Page 页
|
||||
- [GitHub Page](https://doocs.github.io/advanced-java/#/)
|
||||
- [Gitee Page](https://doocs.gitee.io/advanced-java/#/)
|
||||
|
||||
* [GitHub Page](https://doocs.github.io/advanced-java/#/)
|
||||
* [Gitee Page](https://doocs.gitee.io/advanced-java/#/)
|
||||
|
||||
---
|
||||
|
||||
## 公众号
|
||||
|
||||
GitHub 技术社区 [Doocs](https://github.com/doocs) 旗下唯一公众号「**Doocs开源社区**」,欢迎扫码关注,**专注分享技术领域相关知识及行业最新资讯**。当然,也可以加我个人微信(备注:GitHub),拉你进技术交流群。
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/doocs">
|
||||
<img src="./images/qrcode-for-doocs.jpg" style="width: 400px;"><br>
|
||||
<sub>公众平台</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/yanglbme">
|
||||
<img src="./images/qrcode-for-yanglbme.jpg" style="width: 400px;"><br>
|
||||
<sub>个人微信</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -1,9 +1,8 @@
|
||||
[](https://github.com/doocs/advanced-java)
|
||||
[](https://github.com/doocs/advanced-java)
|
||||
|
||||
# 互联网 Java 工程师进阶知识完全扫盲
|
||||
|
||||
> 本系列知识由 Doocs 开源社区总结发布,内容涵盖高并发、分布式、高可用、微服务等
|
||||
> 本系列知识由 Doocs 技术社区总结发布,内容涵盖高并发、分布式、高可用、微服务、海量数据处理等
|
||||
|
||||
[Organization](https://github.com/doocs/doocs.github.io)
|
||||
[Author](https://github.com/yanglbme)
|
||||
[Get Started](#互联网-java-工程师进阶知识完全扫盲©)
|
||||
[社区首页](https://doocs.github.io)
|
||||
[开始学习](#互联网-java-工程师进阶知识完全扫盲©)
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
BIN
docs/extra-page/images/article-demo.png
Normal file
After Width: | Height: | Size: 380 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
BIN
docs/extra-page/images/qrcode-for-doocs.jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/extra-page/images/qrcode-for-yanglbme.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/extra-page/images/qrcode_for_doocs.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
docs/extra-page/images/wechat-group-for-doocs.png
Normal file
After Width: | Height: | Size: 394 KiB |
@ -1,4 +1,4 @@
|
||||
[](https://doocs.github.io/advanced-java/#/offer)
|
||||
[](https://doocs.github.io/advanced-java/#/offer)
|
||||
|
||||
<p align="center"><iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=1321616516&auto=1&height=66"></iframe></p>
|
||||
|
||||
@ -122,4 +122,4 @@ HR
|
||||
|
||||
```
|
||||
|
||||
[](https://doocs.github.io/advanced-java)
|
||||
[](https://doocs.github.io/advanced-java)
|
@ -3,17 +3,19 @@
|
||||
</p>
|
||||
|
||||
## 声明
|
||||
|
||||
读者朋友们,你们好。[advanced-java](https://github.com/doocs/advanced-java) 项目自创建以来,一直收到很多读者的反馈,也在不断改进、完善内容,只希望可以用心做得更好。然而,网上抄袭、侵权的现象普遍存在,我想,不能任由这种恶劣行为肆虐。
|
||||
|
||||
因此,在此说明,除了 [doocs/advanced-java](https://github.com/doocs/advanced-java)、“**石杉的架构笔记**”,网上其它平台上如若出现了与本项目内容雷同甚至完全一致的文章,**不注明出处、甚至打着原创的标签忽悠读者**的,欢迎举报,也欢迎在此提供侵权名单,曝光抄袭者,谢谢。
|
||||
|
||||
希望各位朋友都注重**维护他人知识产权,尊重他人劳动成果**,我们共同构建一个健康的知识分享生态圈。
|
||||
|
||||
|
||||
## 抄袭名单列表
|
||||
|
||||
注:若以下某些链接失效,说明内容已被抄袭者删除,或者已被所在内容平台进行违规清除。
|
||||
|
||||
### 博客
|
||||
|
||||
| # | 文章 | 抄袭者 |
|
||||
|---|---|---|
|
||||
| 1 | [如何保证缓存与数据库的双写一致性](https://blog.51cto.com/14230003/2363051) | Java_老男孩-51CTO |
|
||||
@ -25,20 +27,23 @@
|
||||
| 7 | [消息中间件面试题:消息中间件的高可用](https://www.jianshu.com/p/92862edc7c51) | jsbintask-简书 |
|
||||
| 8 | [深入 Hystrix 执行时内部原理](https://www.jianshu.com/p/1a14401e219f) | kevin0016-简书 |
|
||||
|
||||
|
||||
### 公众号
|
||||
|
||||
| # | 文章 | 抄袭者 |
|
||||
|---|---|---|
|
||||
|
||||
### 头条号
|
||||
|
||||
| # | 文章 | 抄袭者 |
|
||||
|---|---|---|
|
||||
|
||||
### 掘金
|
||||
|
||||
| # | 文章 | 抄袭者 |
|
||||
|---|---|---|
|
||||
|
||||
### 知乎
|
||||
|
||||
| # | 文章 | 抄袭者 | 备注 |
|
||||
|---|---|---|---|
|
||||
| 1 | [Java消息队列三道面试题详解!](https://zhuanlan.zhihu.com/p/62739616) | Java高级架构解析 | 严重抄袭 |
|
||||
| 1 | [Java消息队列三道面试题详解!](https://zhuanlan.zhihu.com/p/62739616) | Java高级架构解析 | 严重抄袭 |
|
55
docs/extra-page/subscriptions-for-doocs.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Doocs 开源社区的公众号来了
|
||||
GitHub 开源社区 Doocs 旗下唯一公众号“**Doocs开源社区**”,专注于挖掘 IT 技术知识,助力开发者成长。
|
||||
|
||||
<div style="text-align:center; ">
|
||||
<img src="./images/qrcode_for_doocs.jpg" width="200px; "/>
|
||||
</div>
|
||||
|
||||
来成为公众号的首批粉丝吗?一定不会辜负你们的期待。
|
||||
|
||||
## 为什么选择公众号?
|
||||
|
||||
微信公众号的**原创保护功能**做得比较好,内容阅读也比较方便。
|
||||
|
||||
后续我的所有原创文章,将第一时间通过微信公众号“Doocs开源社区”发布。如果其他公众号号主希望转载内容,请在公众号聊天窗口处发消息与我说明,或者直接添加我的个人微信(YLB0109)进行交流。
|
||||
|
||||
## 公众号的定位是怎样的?
|
||||
|
||||
* **内容高质**:不随意从网上复制粘贴一些文章,即便是转载的技术文章,也要确保质量。
|
||||
* **定期更新**:尽量在每周输出 1-2 篇文章,保证一定的更新频率。
|
||||
|
||||
<div style="text-align:center; ">
|
||||
<img src="./images/article-demo.png" width="300px; "/>
|
||||
</div>
|
||||
|
||||
## 目前都有哪些文章?
|
||||
|
||||
因为刚刚推出公众号,目前有以下几篇文章,来先睹为快吧:
|
||||
|
||||
* [硬核!亿级流量秒杀系统设计](https://mp.weixin.qq.com/s/Mo_knIRBQQL2s-D2aieZLg)
|
||||
* [技术面试是否要看面经?面试官/面试者有话说!](https://mp.weixin.qq.com/s/fNiUmbY395rsPdEC0QDIrw)
|
||||
* [如何破解极验滑动验证码?成功率 100%!](https://mp.weixin.qq.com/s/Fsl6qYN5Dw4s6Du893MkFQ)
|
||||
* [免费且好用的图床,就你了,「图壳」!](https://mp.weixin.qq.com/s/0HhgHLo_tTRFZcC-CVjDbw)
|
||||
* [阿里又一个 20k+ stars 开源项目诞生,恭喜 fastjson!](https://mp.weixin.qq.com/s/RNKDCK2KoyeuMeEs6GUrow)
|
||||
* [刷掉 90% 候选人的互联网大厂海量数据面试题(附题解 + 方法总结)](https://mp.weixin.qq.com/s/rjGqxUvrEqJNlo09GrT1Dw)
|
||||
* [好用!期待已久的文本块功能究竟如何在 Java 13 中发挥作用?](https://mp.weixin.qq.com/s/kalGv5T8AZGxTnLHr2wDsA)
|
||||
* [2019 GitHub 开源贡献排行榜新鲜出炉!微软谷歌领头,阿里跻身前 12!](https://mp.weixin.qq.com/s/_q812aGD1b9QvZ2WFI0Qgw)
|
||||
* [Google 搜索的即时自动补全功能究竟是如何“工作”的?](https://mp.weixin.qq.com/s/YlMISSc3Sn890BzTLytcLA)
|
||||
* [厉害了,原来 Redisson 这么好用!](https://mp.weixin.qq.com/s/lpZ7eRdImy0MyTEVH68HYw)
|
||||
* [一文带你搞懂 “缓存策略”](https://mp.weixin.qq.com/s/47A_iXY_nArURwUTPHr2IQ)
|
||||
* [Java Getter/Setter “防坑指南”](https://mp.weixin.qq.com/s/TZqcAw7NTlcvU-p930-eHA)
|
||||
* [太棒了,GitHub Review 代码能力小升级](https://mp.weixin.qq.com/s/Lok0epqn91Q51ygZo_FLkg)
|
||||
* [巧用 Redis Hyperloglog,轻松统计 UV 数据](https://mp.weixin.qq.com/s/w1r-M6YVvQSfUtzO_xe44Q)
|
||||
* [如何开启「GitHub+码云」双工作流模式?](https://mp.weixin.qq.com/s/byxAjr3-ifWfDYQcR7YA8Q)
|
||||
|
||||
后续将推出一系列原创干货文章,敬请期待。
|
||||
|
||||
## 是否有交流群?
|
||||
|
||||
有的,目前微信群“**Doocs的技术朋友们**”已经接近 200 号人,如果你希望加入,请通过微信与我联系。
|
||||
|
||||
**注意**,群内禁止一切垃圾广告信息,包括小程序助力信息、小游戏、社群推广、公众号推广、支付宝推广码等;交流 GitHub、开发相关,可以自由分享一些开发相关知识,但不提倡整天水群,建议还是多花点时间提升自己。
|
||||
|
||||
<div style="text-align:center; ">
|
||||
<img src="./images/wechat-group-for-doocs.png" width="300px; "/>
|
||||
</div>
|
@ -1,15 +0,0 @@
|
||||
# GitHub 开发者参与专区
|
||||
[Doocs/advanced-java](https://github.com/doocs/advanced-java) 欢迎各位开发朋友们分享自己或他人的实践经验与总结。如果你想参与,请参考[提交注意事项](/docs/from-readers/doocs-advanced-java-attention.md)。感谢 [@jerryldh](https://github.com/jerryldh), [@BigBlackSheep](https://github.com/BigBlackSheep), [@sunyuanpinggithub](https://github.com/sunyuanpinggithub) 等多位朋友的反馈,具体请参考 [#46](https://github.com/doocs/advanced-java/issues/46)。
|
||||
|
||||
## Articles
|
||||
- [示例文章](/docs/from-readers/doocs-advanced-java-attention.md)
|
||||
- [示例文章](/docs/from-readers/doocs-advanced-java-attention.md)
|
||||
|
||||
## Contributors
|
||||
This project exists thanks to all the people who contribute.
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
|
||||
<a href="https://github.com/doocs/advanced-java/graphs/contributors"><img src="https://opencollective.com/advanced-java/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
@ -1,52 +0,0 @@
|
||||
# 提交注意事项
|
||||
项目需要有一个统一的内容提交规范,没有规范的项目将会是一团乱麻,维护起来也会很费劲儿。以下列出了几个小点,看似很多,实际上都非常容易做到,供朋友们参考。
|
||||
|
||||
> 如果你有好的 idea,欢迎 issues 交流。
|
||||
|
||||
## 关于提交形式
|
||||
本项目**不希望以外链的形式引入内容**。如果你有好的内容推荐,请在此项目基础上创建新的文件,完善内容后再提交。
|
||||
|
||||
## 关于文件命名与存放位置
|
||||
文件请以 “`GitHub ID` + 文章主题” 命名,确保每位朋友的提交内容不会冲突。文章主题统一采用**英文**命名,请勿使用中文或者汉语拼音,文件类型统一选择 `.md`。
|
||||
|
||||
给个示例。某位朋友的 GitHub ID 是 [SnailClimb](https://github.com/snailclimb),想分享一篇关于 Kafka 实践相关的文章,那么文件名可以是 `snailclimb-kafka-in-action.md`。
|
||||
|
||||
最终文件存放于 `docs/from-readers/` 目录下,即与[本文件](/docs/from-readers/doocs-advanced-java-attention.md)处于同一级别。**文件命名、存放位置不规范的文章将不予采纳**。
|
||||
|
||||
## 关于文章内容
|
||||
仅收录与此项目主题相关的优质文章,可以是[高并发](https://github.com/doocs/advanced-java#高并发架构)、[分布式](https://github.com/doocs/advanced-java#分布式系统)、[高可用](https://github.com/doocs/advanced-java#高可用架构)、[微服务](https://github.com/doocs/advanced-java#高并发架构微服务架构)等相关领域的内容。**其它主题的文章将不会被采纳**。
|
||||
|
||||
## 关于文章排版
|
||||
文章排版保持整洁美观。中英文之间、中文与数字之间用空格隔开是最基本的。
|
||||
|
||||
> 有研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情跟书写都需要适时地留白。
|
||||
|
||||
图片统一使用 `` 进行相对路径的引用,并同时存放于根目录 `images/` 和本专区目录 `docs/from-readers/images/` **两个位置**之下(这是为了确保在 GitHub 和 GitHub Page 都能正常显示图片;图片并不限定 `.png` 格式),作图推荐使用在线工具 [ProcessOn](https://www.processon.com/i/594a16f7e4b0e1bb14fe2fac)。具体文章书写规范请参考《[中文技术文档的写作规范](https://github.com/ruanyf/document-style-guide)》。
|
||||
|
||||
以下是文章基本的结构,供朋友们参考。
|
||||
|
||||
```markdown
|
||||
# 这是文章标题
|
||||
- Author: [GitHub ID](https://github.com/your-github-id)
|
||||
- Description: 文章的简单描述信息。
|
||||
- ...
|
||||
|
||||
## 这是一级索引
|
||||
...
|
||||
### 这是二级索引
|
||||
...
|
||||
|
||||
## 这是一级索引
|
||||
...
|
||||
### 这是二级索引
|
||||
...
|
||||
```
|
||||
|
||||
## 关于 Git 提交信息
|
||||
Git 提交信息统一使用英文,本项目遵从 [Angular JS Git 提交规范](https://github.com/angular/angular.js/commits/master)。e.g.
|
||||
|
||||
```bash
|
||||
git commit -m "docs(from-readers): add an article about Kafka"
|
||||
```
|
||||
|
||||
Git 提交信息不规范的 PR 将不予合并。
|
@ -1,15 +1,15 @@
|
||||
# 高可用架构
|
||||
- [Hystrix 介绍](/docs/high-availability/hystrix-introduction.md)
|
||||
- [电商网站详情页系统架构](/docs/high-availability/e-commerce-website-detail-page-architecture.md)
|
||||
- [Hystrix 线程池技术实现资源隔离](/docs/high-availability/hystrix-thread-pool-isolation.md)
|
||||
- [Hystrix 信号量机制实现资源隔离](/docs/high-availability/hystrix-semphore-isolation.md)
|
||||
- [Hystrix 隔离策略细粒度控制](/docs/high-availability/hystrix-execution-isolation.md)
|
||||
- [深入 Hystrix 执行时内部原理](/docs/high-availability/hystrix-process.md)
|
||||
- [基于 request cache 请求缓存技术优化批量商品数据查询接口](/docs/high-availability/hystrix-request-cache.md)
|
||||
- [基于本地缓存的 fallback 降级机制](/docs/high-availability/hystrix-fallback.md)
|
||||
- [深入 Hystrix 断路器执行原理](/docs/high-availability/hystrix-circuit-breaker.md)
|
||||
- [深入 Hystrix 线程池隔离与接口限流](/docs/high-availability/hystrix-thread-pool-current-limiting.md)
|
||||
- [基于 timeout 机制为服务接口调用超时提供安全保护](/docs/high-availability/hystrix-timeout.md)
|
||||
- [Hystrix 介绍](./hystrix-introduction.md)
|
||||
- [电商网站详情页系统架构](./e-commerce-website-detail-page-architecture.md)
|
||||
- [Hystrix 线程池技术实现资源隔离](./hystrix-thread-pool-isolation.md)
|
||||
- [Hystrix 信号量机制实现资源隔离](./hystrix-semphore-isolation.md)
|
||||
- [Hystrix 隔离策略细粒度控制](./hystrix-execution-isolation.md)
|
||||
- [深入 Hystrix 执行时内部原理](./hystrix-process.md)
|
||||
- [基于 request cache 请求缓存技术优化批量商品数据查询接口](./hystrix-request-cache.md)
|
||||
- [基于本地缓存的 fallback 降级机制](./hystrix-fallback.md)
|
||||
- [深入 Hystrix 断路器执行原理](./hystrix-circuit-breaker.md)
|
||||
- [深入 Hystrix 线程池隔离与接口限流](./hystrix-thread-pool-current-limiting.md)
|
||||
- [基于 timeout 机制为服务接口调用超时提供安全保护](./hystrix-timeout.md)
|
||||
|
||||
## 高可用系统
|
||||
- 如何设计一个高可用系统?
|
||||
@ -23,4 +23,26 @@
|
||||
- [熔断框架如何做技术选型?选用 Sentinel 还是 Hystrix?](/docs/high-availability/sentinel-vs-hystrix.md)
|
||||
|
||||
## 降级
|
||||
- 如何进行降级?
|
||||
- 如何进行降级?
|
||||
|
||||
---
|
||||
|
||||
## 公众号
|
||||
GitHub 技术社区 [Doocs](https://github.com/doocs) 旗下唯一公众号「**Doocs开源社区**」,欢迎扫码关注,**专注分享技术领域相关知识及行业最新资讯**。当然,也可以加我个人微信(备注:GitHub),拉你进技术交流群。
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/doocs">
|
||||
<img src="./images/qrcode-for-doocs.jpg" style="width: 400px;"><br>
|
||||
<sub>公众平台</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/yanglbme">
|
||||
<img src="./images/qrcode-for-yanglbme.jpg" style="width: 400px;"><br>
|
||||
<sub>个人微信</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
@ -3,7 +3,7 @@
|
||||
### 小型电商网站的商品详情页系统架构
|
||||
小型电商网站的页面展示采用页面全量静态化的思想。数据库中存放了所有的商品信息,页面静态化系统,将数据填充进静态模板中,形成静态化页面,推入 Nginx 服务器。用户浏览网站页面时,取用一个已经静态化好的 html 页面,直接返回回去,不涉及任何的业务逻辑处理。
|
||||
|
||||

|
||||

|
||||
|
||||
下面是页面模板的简单 Demo 。
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
用户浏览网页时,动态将 Nginx 本地数据渲染到本地 html 模板并返回给用户。
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
虽然没有直接返回 html 页面那么快,但是因为数据在本地缓存,所以也很快,其实耗费的也就是动态渲染一个 html 页面的性能。如果 html 模板发生了变更,不需要将所有的页面重新静态化,也不需要发送请求,没有网络请求的开销,直接将数据渲染进最新的 html 页面模板后响应即可。
|
||||
|
@ -1,17 +1,34 @@
|
||||
## 深入 Hystrix 断路器执行原理
|
||||
|
||||
### RequestVolumeThreshold
|
||||
### 状态机
|
||||
|
||||
Hystrix 断路器有三种状态,分别是关闭(Closed)、打开(Open)与半开(Half-Open),三种状态转化关系如下:
|
||||
|
||||

|
||||
|
||||
1. `Closed` 断路器关闭:调用下游的请求正常通过
|
||||
2. `Open` 断路器打开:阻断对下游服务的调用,直接走 Fallback 逻辑
|
||||
3. `Half-Open` 断路器处于半开状态:[SleepWindowInMilliseconds](#circuitBreaker.sleepWindowInMilliseconds)
|
||||
|
||||
### [Enabled](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitbreakerenabled)
|
||||
|
||||
```java
|
||||
HystrixCommandProperties.Setter()
|
||||
.withCircuitBreakerEnabled(boolean)
|
||||
```
|
||||
|
||||
控制断路器是否工作,包括跟踪依赖服务调用的健康状况,以及对异常情况过多时是否允许触发断路。默认值 `true`。
|
||||
|
||||
### [circuitBreaker.requestVolumeThreshold](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitbreakerrequestvolumethreshold)
|
||||
|
||||
```java
|
||||
HystrixCommandProperties.Setter()
|
||||
.withCircuitBreakerRequestVolumeThreshold(int)
|
||||
```
|
||||
|
||||
表示在滑动窗口中,至少有多少个请求,才可能触发断路。
|
||||
表示在一次统计的**时间滑动窗口中(这个参数也很重要,下面有说到)**,至少经过多少个请求,才可能触发断路,默认值 20。**经过 Hystrix 断路器的流量只有在超过了一定阈值后,才有可能触发断路。**比如说,要求在 10s 内经过断路器的流量必须达到 20 个,而实际经过断路器的请求有 19 个,即使这 19 个请求全都失败,也不会去判断要不要断路。
|
||||
|
||||
Hystrix 经过断路器的流量超过了一定的阈值,才有可能触发断路。比如说,要求在 10s 内经过断路器的流量必须达到 20 个,而实际经过断路器的流量才 10 个,那么根本不会去判断要不要断路。
|
||||
|
||||
### ErrorThresholdPercentage
|
||||
### [circuitBreaker.errorThresholdPercentage](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitBreaker.errorThresholdPercentage)
|
||||
|
||||
```java
|
||||
HystrixCommandProperties.Setter()
|
||||
@ -20,29 +37,18 @@ HystrixCommandProperties.Setter()
|
||||
|
||||
表示异常比例达到多少,才会触发断路,默认值是 50(%)。
|
||||
|
||||
如果断路器统计到的异常调用的占比超过了一定的阈值,比如说在 10s 内,经过断路器的流量达到了 30 个,同时其中异常访问的数量也达到了一定的比例,比如 60% 的请求都是异常(报错 / 超时 / reject),就会开启断路。
|
||||
|
||||
### SleepWindowInMilliseconds
|
||||
#### [circuitBreaker.sleepWindowInMilliseconds](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitbreakersleepwindowinmilliseconds)
|
||||
|
||||
```java
|
||||
HystrixCommandProperties.Setter()
|
||||
.withCircuitBreakerSleepWindowInMilliseconds(int)
|
||||
```
|
||||
|
||||
断路开启,也就是由 close 转换到 open 状态(close -> open)。那么之后在 `SleepWindowInMilliseconds` 时间内,所有经过该断路器的请求全部都会被断路,不调用后端服务,直接走 fallback 降级机制。
|
||||
断路器状态由 Close 转换到 Open,在之后 `SleepWindowInMilliseconds` 时间内,所有经过该断路器的请求会被断路,不调用后端服务,直接走 Fallback 降级机制,默认值 5000(ms)。
|
||||
|
||||
而在该参数时间过后,断路器会变为 `half-open` 半开闭状态,尝试让一条请求经过断路器,看能不能正常调用。如果调用成功了,那么就自动恢复,断路器转为 close 状态。
|
||||
而在该参数时间过后,断路器会变为 `Half-Open` 半开闭状态,尝试让一条请求经过断路器,看能不能正常调用。如果调用成功了,那么就自动恢复,断路器转为 Close 状态。
|
||||
|
||||
### Enabled
|
||||
|
||||
```java
|
||||
HystrixCommandProperties.Setter()
|
||||
.withCircuitBreakerEnabled(boolean)
|
||||
```
|
||||
|
||||
控制是否允许断路器工作,包括跟踪依赖服务调用的健康状况,以及对异常情况过多时是否允许触发断路。默认值是 `true`。
|
||||
|
||||
### ForceOpen
|
||||
### [ForceOpen](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitbreakerforceopen)
|
||||
|
||||
```java
|
||||
HystrixCommandProperties.Setter()
|
||||
@ -51,7 +57,7 @@ HystrixCommandProperties.Setter()
|
||||
|
||||
如果设置为 true 的话,直接强迫打开断路器,相当于是手动断路了,手动降级,默认值是 `false`。
|
||||
|
||||
### ForceClosed
|
||||
### [ForceClosed](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitbreakerforceclosed)
|
||||
|
||||
```java
|
||||
HystrixCommandProperties.Setter()
|
||||
@ -60,6 +66,14 @@ HystrixCommandProperties.Setter()
|
||||
|
||||
如果设置为 true,直接强迫关闭断路器,相当于手动停止断路了,手动升级,默认值是 `false`。
|
||||
|
||||
### Metrics 统计器
|
||||
|
||||
与 Hystrix 断路器紧密协作的,还有另一个重要组件 —— **统计器(Metrics)**。统计器中最重要的参数要数滑动窗口([metrics.rollingStats.timeInMilliseconds](https://github.com/Netflix/Hystrix/wiki/Configuration#metricsrollingstatstimeinmilliseconds))以及桶([metrics.rollingStats.numBuckets](https://github.com/Netflix/Hystrix/wiki/Configuration#metricsrollingstatsnumbuckets))了,这里引用[一段博文](https://zhenbianshu.github.io/2018/09/hystrix_configuration_analysis.html)来解释滑动窗口(默认值是 10000 ms):
|
||||
|
||||
> 一位乘客坐在正在行驶的列车的靠窗座位上,列车行驶的公路两侧种着一排挺拔的白杨树,随着列车的前进,路边的白杨树迅速从窗口滑过。我们用每棵树来代表一个请求,用列车的行驶代表时间的流逝,那么,列车上的这个窗口就是一个典型的滑动窗口,这个乘客能通过窗口看到的白杨树就是 Hystrix 要统计的数据。
|
||||
|
||||
Hystrix 并不是只要有一条请求经过就去统计,而是将整个滑动窗口均分为 numBuckets 份,时间每经过一份就去统计一次。**在经过一个时间窗口后,才会判断断路器状态要不要开启,请看下面的例子。**
|
||||
|
||||
## 实例 Demo
|
||||
|
||||
### HystrixCommand 配置参数
|
||||
@ -178,4 +192,9 @@ ProductInfo(id=1, name=iphone7手机, price=5599.0, pictureList=a.jpg,b.jpg, spe
|
||||
|
||||
而是直接走降级逻辑,调用 getFallback() 执行。
|
||||
|
||||
休眠了 3s 后,我们在之后的 70 次请求中,都传入 productId 为 1。由于我们前面设置了 3000ms 过后断路器变为 `half-open` 状态。因此 Hystrix 会尝试执行请求,发现成功了,那么断路器关闭,之后的所有请求也都能正常调用了。
|
||||
休眠了 3s 后,我们在之后的 70 次请求中,都传入 productId 为 1。由于我们前面设置了 3000ms 过后断路器变为 `half-open` 状态。因此 Hystrix 会尝试执行请求,发现成功了,那么断路器关闭,之后的所有请求也都能正常调用了。
|
||||
|
||||
### 参考内容
|
||||
|
||||
1. [Hystrix issue 1459](https://github.com/Netflix/Hystrix/issues/1459)
|
||||
2. [Hystrix Metrics](https://github.com/Netflix/Hystrix/wiki/Configuration#metrics)
|
@ -17,7 +17,7 @@ HystrixCommandProperties.Setter().withExecutionIsolationStrategy(ExecutionIsolat
|
||||
HystrixCommandProperties.Setter().withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)
|
||||
```
|
||||
|
||||
线程池机制,每个 command 运行在一个线程中,限流是通过线程池的大小来控制的;信号量机制,command 是运行在调用线程中,通过信号量的容量来进行限流。
|
||||
线程池机制,每个 command 运行在一个线程中,限流是通过线程池的大小来控制的;信号量机制,command 是运行在调用线程中(也就是 Tomcat 的线程池),通过信号量的容量来进行限流。
|
||||
|
||||
如何在线程池和信号量之间做选择?
|
||||
|
||||
@ -33,7 +33,7 @@ HystrixCommandProperties.Setter().withExecutionIsolationStrategy(ExecutionIsolat
|
||||
每一个 command,都可以设置一个自己的名称 command key,同时可以设置一个自己的组 command group。
|
||||
```java
|
||||
private static final Setter cachedSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
|
||||
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"));
|
||||
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"));
|
||||
|
||||
public CommandHelloWorld(String name) {
|
||||
super(cachedSetter);
|
||||
@ -49,8 +49,8 @@ ThreadPoolKey 代表了一个 HystrixThreadPool,用来进行统一监控、统
|
||||
如果不想直接用 command group,也可以手动设置 ThreadPool 的名称。
|
||||
```java
|
||||
private static final Setter cachedSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
|
||||
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
|
||||
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool"));
|
||||
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
|
||||
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool"));
|
||||
|
||||
public CommandHelloWorld(String name) {
|
||||
super(cachedSetter);
|
||||
@ -59,13 +59,13 @@ public CommandHelloWorld(String name) {
|
||||
```
|
||||
|
||||
### command key & command group & command thread pool
|
||||
**command key** ,代表了一类 command,一般来说,代表了底层的依赖服务的一个接口。
|
||||
**command key** ,代表了一类 command,一般来说,代表了下游依赖服务的某个接口。
|
||||
|
||||
**command group** ,代表了某一个底层的依赖服务,这是很合理的,一个依赖服务可能会暴露出来多个接口,每个接口就是一个 command key。command group 在逻辑上去组织起来一堆 command key 的调用、统计信息、成功次数、timeout 超时次数、失败次数等,可以看到某一个服务整体的一些访问情况。一般来说,**推荐**根据一个服务区划分出一个线程池,command key 默认都是属于同一个线程池的。
|
||||
**command group** ,代表了某一个下游依赖服务,这是很合理的,一个依赖服务可能会暴露出来多个接口,每个接口就是一个 command key。command group 在逻辑上对一堆 command key 的调用次数、成功次数、timeout 次数、失败次数等进行统计,可以看到某一个服务整体的一些访问情况。**一般来说,推荐根据一个服务区划分出一个线程池,command key 默认都是属于同一个线程池的。**
|
||||
|
||||
比如说你以一个服务为粒度,估算出来这个服务每秒的所有接口加起来的整体 `QPS` 在 100 左右,你调用这个服务,当前这个服务部署了 10 个服务实例,每个服务实例上,其实用这个 command group 对应这个服务,给一个线程池,量大概在 10 个左右就可以了,你对整个服务的整体的访问 QPS 就大概在每秒 100 左右。
|
||||
比如说有一个服务 A,你估算出来服务 A 每秒所有接口加起来的整体 `QPS` 在 100 左右,你有一个服务 B 去调用服务 A。你的服务 B 部署了 10 个实例,每个实例上,用 command group 去对应下游服务 A。给一个线程池,量大概是 10 就可以了,这样服务 B 对服务 A 整体的访问 QPS 就大概是每秒 100 了。
|
||||
|
||||
但是,如果说 command group 对应了一个服务,而这个服务暴露出来的几个接口,访问量很不一样,差异非常之大。你可能就希望在这个服务 command group 内部,包含的对应多个接口的 command key,做一些细粒度的资源隔离。就是说,对同一个服务的不同接口,使用不同的线程池。
|
||||
但是,如果说 command group 对应了一个服务,而这个服务暴露出来的几个接口,访问量很不一样,差异非常之大。你可能就希望在这个服务对应 command group 的内部,包含对应多个接口的 command key,做一些细粒度的资源隔离。**就是说,希望对同一个服务的不同接口,使用不同的线程池。**
|
||||
|
||||
```
|
||||
command key -> command group
|
||||
@ -86,7 +86,7 @@ HystrixThreadPoolProperties.Setter().withCoreSize(int value);
|
||||
### queueSizeRejectionThreshold
|
||||
如果说线程池中的 10 个线程都在工作中,没有空闲的线程来做其它的事情,此时再有请求过来,会先进入队列积压。如果说队列积压满了,再有请求过来,就直接 reject,拒绝请求,执行 fallback 降级的逻辑,快速返回。
|
||||
|
||||

|
||||

|
||||
|
||||
控制 queue 满了之后 reject 的 threshold,因为 maxQueueSize 不允许热修改,因此提供这个参数可以热修改,控制队列的最大大小。
|
||||
|
||||
|
@ -6,9 +6,9 @@
|
||||
|
||||
Hystrix 可以让我们在分布式系统中对服务间的调用进行控制,加入一些**调用延迟**或者**依赖故障**的**容错机制**。
|
||||
|
||||
Hystrix 通过将依赖服务进行**资源隔离**,进而阻止某个依赖服务出现故障时在整个系统所有的依赖服务调用中进行蔓延;同时Hystrix 还提供故障时的 fallback 降级机制。
|
||||
Hystrix 通过将依赖服务进行**资源隔离**,进而阻止某个依赖服务出现故障时在整个系统所有的依赖服务调用中进行蔓延;同时 Hystrix 还提供故障时的 fallback 降级机制。
|
||||
|
||||
总而言之,Hystrix 通过这些方法帮助我们提升分布式系统的可用性和稳定性。
|
||||
**总而言之,Hystrix 通过这些方法帮助我们提升分布式系统的可用性和稳定性。**
|
||||
|
||||
### Hystrix 的历史
|
||||
Hystrix 是高可用性保障的一个框架。Netflix(可以认为是国外的优酷或者爱奇艺之类的视频网站)的 API 团队从 2011 年开始做一些提升系统可用性和稳定性的工作,Hystrix 就是从那时候开始发展出来的。
|
||||
@ -31,9 +31,9 @@ Hystrix 是高可用性保障的一个框架。Netflix(可以认为是国外
|
||||
|
||||
有这样一个分布式系统,服务 A 依赖于服务 B,服务 B 依赖于服务 C/D/E。在这样一个成熟的系统内,比如说最多可能只有 100 个线程资源。正常情况下,40 个线程并发调用服务 C,各 30 个线程并发调用 D/E。
|
||||
|
||||
调用服务 C,只需要 20ms,现在因为服务 C 故障了,比如延迟,或者挂了,此时线程会 hang 住 2s 左右。40 个线程全部被卡住,由于请求不断涌入,其它的线程也用来调用服务 C,同样也会被卡住。这样导致服务 B 的线程资源被耗尽,无法接收新的请求,甚至可能因为大量线程不断的运转,导致自己宕机。服务 A 也挂。
|
||||
调用服务 C,只需要 20ms,现在因为服务 C 故障了,比如延迟,或者挂了,此时线程会 hang 住 2s 左右。40 个线程全部被卡住,由于请求不断涌入,其它的线程也用来调用服务 C,同样也会被卡住。这样导致服务 B 的线程资源被耗尽,无法接收新的请求,甚至可能因为大量线程不断的运转,导致自己宕机。这种影响势必会蔓延至服务 A,导致服务 A 也跟着挂掉。
|
||||
|
||||

|
||||

|
||||
|
||||
Hystrix 可以对其进行资源隔离,比如限制服务 B 只有 40 个线程调用服务 C。当此 40 个线程被 hang 住时,其它 60 个线程依然能正常调用工作。从而确保整个系统不会被拖垮。
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
这里是整个 8 大步骤的流程图,我会对每个步骤进行细致的讲解。学习的过程中,对照着这个流程图,相信思路会比较清晰。
|
||||
|
||||

|
||||

|
||||
|
||||
### 步骤一:创建 command
|
||||
一个 HystrixCommand 或 HystrixObservableCommand 对象,代表了对某个依赖服务发起的一次请求或者调用。创建的时候,可以在构造函数中传入任何需要的参数。
|
||||
@ -64,9 +64,7 @@ final Future<R> delegate = toObservable().toBlocking().toFuture();
|
||||
|
||||
也就是说,先通过 toObservable() 获得 Future 对象,然后调用 Future 的 get() 方法。那么,其实无论是哪种方式执行 command,最终都是依赖于 toObservable() 去执行的。
|
||||
|
||||

|
||||
|
||||
### 步骤三:检查是否开启缓存
|
||||
### 步骤三:检查是否开启缓存(不太常用)
|
||||
从这一步开始,就进入到 Hystrix 底层运行原理啦,看一下 Hystrix 一些更高级的功能和特性。
|
||||
|
||||
如果这个 command 开启了请求缓存 Request Cache,而且这个调用的结果在缓存中存在,那么直接从缓存中返回结果。否则,继续往后的步骤。
|
||||
@ -122,8 +120,6 @@ observable.subscribe(new Observer<ProductInfo>() {
|
||||
|
||||
如果没有 timeout,也正常执行的话,那么调用线程就会拿到一些调用依赖服务获取到的结果,然后 Hystrix 也会做一些 logging 记录和 metric 度量统计。
|
||||
|
||||

|
||||
|
||||
### 步骤七:断路健康检查
|
||||
Hystrix 会把每一个依赖服务的调用成功、失败、Reject、Timeout 等事件发送给 circuit breaker 断路器。断路器就会对这些事件的次数进行统计,根据异常事件发生的比例来决定是否要进行断路(熔断)。如果打开了断路器,那么在接下来一段时间内,会直接断路,返回降级结果。
|
||||
|
||||
|
@ -9,7 +9,7 @@ Hystrix command 执行时 8 大步骤第三步,就是检查 Request cache 是
|
||||
|
||||
举个栗子。比如说我们在一次请求上下文中,请求获取 productId 为 1 的数据,第一次缓存中没有,那么会从商品服务中获取数据,返回最新数据结果,同时将数据缓存在内存中。后续同一次请求上下文中,如果还有获取 productId 为 1 的数据的请求,直接从缓存中取就好了。
|
||||
|
||||

|
||||

|
||||
|
||||
HystrixCommand 和 HystrixObservableCommand 都可以指定一个缓存 key,然后 Hystrix 会自动进行缓存,接着在同一个 request context 内,再次访问的话,就会直接取用缓存。
|
||||
|
||||
|
@ -13,14 +13,14 @@ Hystrix 实现资源隔离,主要有两种技术:
|
||||
### 信号量机制
|
||||
信号量的资源隔离只是起到一个开关的作用,比如,服务 A 的信号量大小为 10,那么就是说它同时只允许有 10 个 tomcat 线程来访问服务 A,其它的请求都会被拒绝,从而达到资源隔离和限流保护的作用。
|
||||
|
||||

|
||||

|
||||
|
||||
### 线程池与信号量区别
|
||||
线程池隔离技术,并不是说去控制类似 tomcat 这种 web 容器的线程。更加严格的意义上来说,Hystrix 的线程池隔离技术,控制的是 tomcat 线程的执行。Hystrix 线程池满后,会确保说,tomcat 的线程不会因为依赖服务的接口调用延迟或故障而被 hang 住,tomcat 其它的线程不会卡死,可以快速返回,然后支撑其它的事情。
|
||||
|
||||
线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。
|
||||
|
||||

|
||||

|
||||
|
||||
**适用场景**:
|
||||
- **线程池技术**,适合绝大多数场景,比如说我们对依赖服务的网络请求的调用和访问、需要对调用的 timeout 进行控制(捕捉 timeout 超时异常)。
|
||||
|
@ -1,7 +1,7 @@
|
||||
## 深入 Hystrix 线程池隔离与接口限流
|
||||
前面讲了 Hystrix 的 request cache 请求缓存、fallback 优雅降级、circuit breaker 断路器快速熔断,这一讲,我们来详细说说 Hystrix 的线程池隔离与接口限流。
|
||||
|
||||

|
||||

|
||||
|
||||
Hystrix 通过判断线程池或者信号量是否已满,超出容量的请求,直接 Reject 走降级,从而达到限流的作用。
|
||||
|
||||
@ -12,7 +12,7 @@ Hystrix 采用了 Bulkhead Partition 舱壁隔离技术,来将外部依赖进
|
||||
|
||||
**舱壁隔离**,是说将船体内部空间区隔划分成若干个隔舱,一旦某几个隔舱发生破损进水,水流不会在其间相互流动,如此一来船舶在受损时,依然能具有足够的浮力和稳定性,进而减低立即沉船的危险。
|
||||
|
||||

|
||||

|
||||
|
||||
Hystrix 对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重,最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用。
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
## 基于 Hystrix 线程池技术实现资源隔离
|
||||
上一讲提到,如果从 Nginx 开始,缓存都失效了,Nginx 会直接通过缓存服务调用商品服务获取最新商品数据(我们基于电商项目做个讨论),有可能出现调用延时而把缓存服务资源耗尽的情况。这里,我们就来说说,怎么通过 Hystrix 线程池技术实现资源隔离。
|
||||
[上一讲](./e-commerce-website-detail-page-architecture.md)提到,如果从 Nginx 开始,缓存都失效了,Nginx 会直接通过缓存服务调用商品服务获取最新商品数据(我们基于电商项目做个讨论),有可能出现调用延时而把缓存服务资源耗尽的情况。这里,我们就来说说,怎么通过 Hystrix 线程池技术实现资源隔离。
|
||||
|
||||
资源隔离,就是说,你如果要把对某一个依赖服务的所有调用请求,全部隔离在同一份资源池内,不会去用其它资源了,这就叫资源隔离。哪怕对这个依赖服务,比如说商品服务,现在同时发起的调用量已经到了 1000,但是线程池内就 10 个线程,最多就只会用这 10 个线程去执行,不会说,对商品服务的请求,因为接口调用延时,将 tomcat 内部所有的线程资源全部耗尽。
|
||||
资源隔离,就是说,你如果要把对某一个依赖服务的所有调用请求,全部隔离在同一份资源池内,不会去用其它资源了,这就叫资源隔离。哪怕对这个依赖服务,比如说商品服务,现在同时发起的调用量已经到了 1000,但是分配给商品服务线程池内就 10 个线程,最多就只会用这 10 个线程去执行。不会因为对商品服务调用的延迟,将 Tomcat 内部所有的线程资源全部耗尽。
|
||||
|
||||
Hystrix 进行资源隔离,其实是提供了一个抽象,叫做 command。这也是 Hystrix 最最基本的资源隔离技术。
|
||||
Hystrix 进行资源隔离,其实是提供了一个抽象,叫做 Command。这也是 Hystrix 最最基本的资源隔离技术。
|
||||
|
||||
### 利用 HystrixCommand 获取单条数据
|
||||
我们通过将调用商品服务的操作封装在 HystrixCommand 中,限定一个 key,比如下面的 `GetProductInfoCommandGroup`,在这里我们可以简单认为这是一个线程池,每次调用商品服务,就只会用该线程池中的资源,不会再去用其它线程资源了。
|
||||
@ -28,7 +28,7 @@ public class GetProductInfoCommand extends HystrixCommand<ProductInfo> {
|
||||
}
|
||||
```
|
||||
|
||||
我们在缓存服务接口中,根据 productId 创建 command 并执行,获取到商品数据。
|
||||
我们在缓存服务接口中,根据 productId 创建 Command 并执行,获取到商品数据。
|
||||
|
||||
```java
|
||||
@RequestMapping("/getProductInfo")
|
||||
@ -110,6 +110,6 @@ public String getProductInfos(String productIds) {
|
||||
|
||||
我们回过头来,看看 Hystrix 线程池技术是如何实现资源隔离的。
|
||||
|
||||

|
||||

|
||||
|
||||
从 Nginx 开始,缓存都失效了,那么 Nginx 通过缓存服务去调用商品服务。缓存服务默认的线程大小是 10 个,最多就只有 10 个线程去调用商品服务的接口。即使商品服务接口故障了,最多就只有 10 个线程会 hang 死在调用商品服务接口的路上,缓存服务的 tomcat 内其它的线程还是可以用来调用其它的服务,干其它的事情。
|
||||
从 Nginx 开始,缓存都失效了,那么 Nginx 通过缓存服务去调用商品服务。缓存服务默认的线程大小是 10 个,最多就只有 10 个线程去调用商品服务的接口。即使商品服务接口故障了,最多就只有 10 个线程会 hang 死在调用商品服务接口的路上,缓存服务的 Tomcat 内其它的线程还是可以用来调用其它的服务,干其它的事情。
|
@ -5,7 +5,7 @@
|
||||
|
||||
Peter Steiner 说过,"[On the Internet, nobody knows you're a dog](https://en.wikipedia.org/wiki/On_the_Internet,_nobody_knows_you%27re_a_dog)",也就是说在互联网的另外一头,你都不知道甚至坐着一条狗。
|
||||
|
||||

|
||||

|
||||
|
||||
像特别复杂的分布式系统,特别是在大公司里,多个团队、大型协作,你可能都不知道服务是谁的,很可能说开发服务的那个哥儿们甚至是一个实习生。依赖服务的接口性能可能很不稳定,有时候 2ms,有时候 200ms,甚至 2s,都有可能。
|
||||
|
||||
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/high-availability/images/new-hystrix-process.jpg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
docs/high-availability/images/qrcode-for-doocs.jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/high-availability/images/qrcode-for-yanglbme.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
@ -63,10 +63,10 @@ Sentinel 支持多样化的流量整形策略,在 QPS 过高的时候可以自
|
||||
|
||||
- **直接拒绝模式**:即超出的请求直接拒绝。
|
||||
- **慢启动预热模式**:当流量激增的时候,控制流量通过的速率,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
|
||||

|
||||

|
||||
|
||||
- **匀速器模式**:利用 Leaky Bucket 算法实现的匀速模式,严格控制了请求通过的时间间隔,同时堆积的请求将会排队,超过超时时长的请求直接被拒绝。Sentinel 还支持基于调用关系的限流,包括基于调用方限流、基于调用链入口限流、关联流量限流等,依托于 Sentinel 强大的调用链路统计信息,可以提供精准的不同维度的限流。
|
||||

|
||||

|
||||
|
||||
|
||||
目前 Sentinel 对异步调用链路的支持还不是很好,后续版本会着重改善支持异步调用。
|
||||
@ -74,14 +74,14 @@ Sentinel 支持多样化的流量整形策略,在 QPS 过高的时候可以自
|
||||
### 3. 系统负载保护
|
||||
Sentinel 对系统的维度提供保护,负载保护算法借鉴了 TCP BBR 的思想。当系统负载较高的时候,如果仍持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
|
||||
|
||||

|
||||

|
||||
|
||||
### 4. 实时监控和控制面板
|
||||
Sentinel 提供 HTTP API 用于获取实时的监控信息,如调用链路统计信息、簇点信息、规则信息等。如果用户正在使用 Spring Boot/Spring Cloud 并使用了Sentinel Spring Cloud Starter,还可以方便地通过其暴露的 Actuator Endpoint 来获取运行时的一些信息,如动态规则等。未来 Sentinel 还会支持标准化的指标监控 API,可以方便地整合各种监控系统和可视化系统,如 Prometheus、Grafana 等。
|
||||
|
||||
Sentinel 控制台(Dashboard)提供了机器发现、配置规则、查看实时监控、查看调用链路信息等功能,使得用户可以非常方便地去查看监控和进行配置。
|
||||
|
||||

|
||||

|
||||
|
||||
### 5. 生态
|
||||
Sentinel 目前已经针对 Servlet、Dubbo、Spring Boot/Spring Cloud、gRPC 等进行了适配,用户只需引入相应依赖并进行简单配置即可非常方便地享受 Sentinel 的高可用流量防护能力。未来 Sentinel 还会对更多常用框架进行适配,并且会为 Service Mesh 提供集群流量防护的能力。
|
||||
|
@ -1,40 +1,70 @@
|
||||
# 高并发架构
|
||||
|
||||
## [消息队列](/docs/high-concurrency/mq-interview.md)
|
||||
- [为什么使用消息队列?消息队列有什么优点和缺点?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?](/docs/high-concurrency/why-mq.md)
|
||||
- [如何保证消息队列的高可用?](/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)
|
||||
- [如何保证消息不被重复消费?(如何保证消息消费的幂等性)](/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)
|
||||
- [如何保证消息的可靠性传输?(如何处理消息丢失的问题)](/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)
|
||||
- [如何保证消息的顺序性?](/docs/high-concurrency/how-to-ensure-the-order-of-messages.md)
|
||||
- [如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?](/docs/high-concurrency/mq-time-delay-and-expired-failure.md)
|
||||
- [如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。](/docs/high-concurrency/mq-design.md)
|
||||
|
||||
* [为什么使用消息队列?消息队列有什么优点和缺点?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?](/docs/high-concurrency/why-mq.md)
|
||||
* [如何保证消息队列的高可用?](/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)
|
||||
* [如何保证消息不被重复消费?(如何保证消息消费的幂等性)](/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)
|
||||
* [如何保证消息的可靠性传输?(如何处理消息丢失的问题)](/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)
|
||||
* [如何保证消息的顺序性?](/docs/high-concurrency/how-to-ensure-the-order-of-messages.md)
|
||||
* [如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?](/docs/high-concurrency/mq-time-delay-and-expired-failure.md)
|
||||
* [如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。](/docs/high-concurrency/mq-design.md)
|
||||
|
||||
## [搜索引擎](/docs/high-concurrency/es-introduction.md)
|
||||
- [es 的分布式架构原理能说一下么(es 是如何实现分布式的啊)?](/docs/high-concurrency/es-architecture.md)
|
||||
- [es 写入数据的工作原理是什么啊?es 查询数据的工作原理是什么啊?底层的 lucene 介绍一下呗?倒排索引了解吗?](/docs/high-concurrency/es-write-query-search.md)
|
||||
- [es 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?](/docs/high-concurrency/es-optimizing-query-performance.md)
|
||||
- [es 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?](/docs/high-concurrency/es-production-cluster.md)
|
||||
|
||||
* [ES 的分布式架构原理能说一下么(ES 是如何实现分布式的啊)?](/docs/high-concurrency/es-architecture.md)
|
||||
* [ES 写入数据的工作原理是什么啊?ES 查询数据的工作原理是什么啊?底层的 Lucene 介绍一下呗?倒排索引了解吗?](/docs/high-concurrency/es-write-query-search.md)
|
||||
* [ES 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?](/docs/high-concurrency/es-optimizing-query-performance.md)
|
||||
* [ES 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?](/docs/high-concurrency/es-production-cluster.md)
|
||||
|
||||
## 缓存
|
||||
- [在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?](/docs/high-concurrency/why-cache.md)
|
||||
- [Redis 和 Memcached 有什么区别?Redis 的线程模型是什么?为什么单线程的 Redis 比多线程的 Memcached 效率要高得多?](/docs/high-concurrency/redis-single-thread-model.md)
|
||||
- [Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?](/docs/high-concurrency/redis-data-types.md)
|
||||
- [Redis 的过期策略都有哪些?手写一下 LRU 代码实现?](/docs/high-concurrency/redis-expiration-policies-and-lru.md)
|
||||
- [如何保证 Redis 高并发、高可用?Redis 的主从复制原理能介绍一下么?Redis 的哨兵原理能介绍一下么?](/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md)
|
||||
- [Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?](/docs/high-concurrency/redis-persistence.md)
|
||||
- [Redis 集群模式的工作原理能说一下么?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?如何动态增加和删除一个节点?](/docs/high-concurrency/redis-cluster.md)
|
||||
- [了解什么是 redis 的雪崩、穿透和击穿?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?](/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md)
|
||||
- [如何保证缓存与数据库的双写一致性?](/docs/high-concurrency/redis-consistence.md)
|
||||
- [Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?](/docs/high-concurrency/redis-cas.md)
|
||||
- [生产环境中的 Redis 是怎么部署的?](/docs/high-concurrency/redis-production-environment.md)
|
||||
|
||||
* [在项目中缓存是如何使用的?缓存如果使用不当会造成什么后果?](/docs/high-concurrency/why-cache.md)
|
||||
* [Redis 和 Memcached 有什么区别?Redis 的线程模型是什么?为什么单线程的 Redis 比多线程的 Memcached 效率要高得多?](/docs/high-concurrency/redis-single-thread-model.md)
|
||||
* [Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?](/docs/high-concurrency/redis-data-types.md)
|
||||
* [Redis 的过期策略都有哪些?手写一下 LRU 代码实现?](/docs/high-concurrency/redis-expiration-policies-and-lru.md)
|
||||
* [如何保证 Redis 高并发、高可用?Redis 的主从复制原理能介绍一下么?Redis 的哨兵原理能介绍一下么?](/docs/high-concurrency/how-to-ensure-high-concurrency-and-high-availability-of-redis.md)
|
||||
* [Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?](/docs/high-concurrency/redis-persistence.md)
|
||||
* [Redis 集群模式的工作原理能说一下么?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?如何动态增加和删除一个节点?](/docs/high-concurrency/redis-cluster.md)
|
||||
* [了解什么是 redis 的雪崩、穿透和击穿?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?](/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md)
|
||||
* [如何保证缓存与数据库的双写一致性?](/docs/high-concurrency/redis-consistence.md)
|
||||
* [Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?](/docs/high-concurrency/redis-cas.md)
|
||||
* [生产环境中的 Redis 是怎么部署的?](/docs/high-concurrency/redis-production-environment.md)
|
||||
|
||||
## 分库分表
|
||||
- [为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?](/docs/high-concurrency/database-shard.md)
|
||||
- [现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?](/docs/high-concurrency/database-shard-method.md)
|
||||
- [如何设计可以动态扩容缩容的分库分表方案?](/docs/high-concurrency/database-shard-dynamic-expand.md)
|
||||
- [分库分表之后,id 主键如何处理?](/docs/high-concurrency/database-shard-global-id-generate.md)
|
||||
|
||||
* [为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?](/docs/high-concurrency/database-shard.md)
|
||||
* [现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?](/docs/high-concurrency/database-shard-method.md)
|
||||
* [如何设计可以动态扩容缩容的分库分表方案?](/docs/high-concurrency/database-shard-dynamic-expand.md)
|
||||
* [分库分表之后,id 主键如何处理?](/docs/high-concurrency/database-shard-global-id-generate.md)
|
||||
|
||||
## 读写分离
|
||||
- [如何实现 MySQL 的读写分离?MySQL 主从复制原理是啥?如何解决 MySQL 主从同步的延时问题?](/docs/high-concurrency/mysql-read-write-separation.md)
|
||||
|
||||
* [如何实现 MySQL 的读写分离?MySQL 主从复制原理是啥?如何解决 MySQL 主从同步的延时问题?](/docs/high-concurrency/mysql-read-write-separation.md)
|
||||
|
||||
## 高并发系统
|
||||
- [如何设计一个高并发系统?](/docs/high-concurrency/high-concurrency-design.md)
|
||||
|
||||
* [如何设计一个高并发系统?](/docs/high-concurrency/high-concurrency-design.md)
|
||||
|
||||
---
|
||||
|
||||
## 公众号
|
||||
|
||||
GitHub 技术社区 [Doocs](https://github.com/doocs) 旗下唯一公众号「**Doocs开源社区**」,欢迎扫码关注,**专注分享技术领域相关知识及行业最新资讯**。当然,也可以加我个人微信(备注:GitHub),拉你进技术交流群。
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/doocs">
|
||||
<img src="./images/qrcode-for-doocs.jpg" style="width: 400px;"><br>
|
||||
<sub>公众平台</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
<td align="center" style="width: 200px;">
|
||||
<a href="https://github.com/yanglbme">
|
||||
<img src="./images/qrcode-for-yanglbme.jpg" style="width: 400px;"><br>
|
||||
<sub>个人微信</sub>
|
||||
</a><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -2,14 +2,15 @@
|
||||
如何设计可以动态扩容缩容的分库分表方案?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
对于分库分表来说,主要是面对以下问题:
|
||||
|
||||
- 选择一个数据库中间件,调研、学习、测试;
|
||||
- 设计你的分库分表的一个方案,你要分成多少个库,每个库分成多少个表,比如 3 个库,每个库 4 个表;
|
||||
- 基于选择好的数据库中间件,以及在测试环境建立好的分库分表的环境,然后测试一下能否正常进行分库分表的读写;
|
||||
- 完成单库单表到分库分表的**迁移**,双写方案;
|
||||
- 线上系统开始基于分库分表对外提供服务;
|
||||
- 扩容了,扩容成 6 个库,每个库需要 12 个表,你怎么来增加更多库和表呢?
|
||||
* 选择一个数据库中间件,调研、学习、测试;
|
||||
* 设计你的分库分表的一个方案,你要分成多少个库,每个库分成多少个表,比如 3 个库,每个库 4 个表;
|
||||
* 基于选择好的数据库中间件,以及在测试环境建立好的分库分表的环境,然后测试一下能否正常进行分库分表的读写;
|
||||
* 完成单库单表到分库分表的**迁移**,双写方案;
|
||||
* 线上系统开始基于分库分表对外提供服务;
|
||||
* 扩容了,扩容成 6 个库,每个库需要 12 个表,你怎么来增加更多库和表呢?
|
||||
|
||||
这个是你必须面对的一个事儿,就是你已经弄好分库分表方案了,然后一堆库和表都建好了,基于分库分表中间件的代码开发啥的都好了,测试都 ok 了,数据能均匀分布到各个库和各个表里去,而且接着你还通过双写的方案咔嚓一下上了系统,已经直接基于分库分表方案在搞了。
|
||||
|
||||
@ -18,6 +19,7 @@
|
||||
这都是玩儿分库分表线上必须经历的事儿。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 停机扩容(不推荐)
|
||||
这个方案就跟停机迁移一样,步骤几乎一致,唯一的一点就是那个导数的工具,是把现有库表的数据抽出来慢慢倒入到新的库和表里去。但是最好别这么玩儿,有点不太靠谱,因为既然**分库分表**就说明数据量实在是太大了,可能多达几亿条,甚至几十亿,你这么玩儿,可能会出问题。
|
||||
|
||||
@ -26,6 +28,7 @@
|
||||
如果 3 个库 + 12 个表,跑了一段时间了,数据量都 1~2 亿了。光是导 2 亿数据,都要导个几个小时,6 点,刚刚导完数据,还要搞后续的修改配置,重启系统,测试验证,10 点才可以搞完。所以不能这么搞。
|
||||
|
||||
### 优化后的方案
|
||||
|
||||
一开始上来就是 32 个库,每个库 32 个表,那么总共是 1024 张表。
|
||||
|
||||
我可以告诉各位同学,这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题。
|
||||
@ -49,12 +52,11 @@
|
||||
| 352 | 0 | 11 |
|
||||
| 4593 | 17 | 15 |
|
||||
|
||||
|
||||
刚开始的时候,这个库可能就是逻辑库,建在一个数据库上的,就是一个 mysql 服务器可能建了 n 个库,比如 32 个库。后面如果要拆分,就是不断在库和 mysql 服务器之间做迁移就可以了。然后系统配合改一下配置即可。
|
||||
刚开始的时候,这个库可能就是逻辑库,建在一个数据库上的,就是一个 MySQL 服务器可能建了 n 个库,比如 32 个库。后面如果要拆分,就是不断在库和 MySQL 服务器之间做迁移就可以了。然后系统配合改一下配置即可。
|
||||
|
||||
比如说最多可以扩展到 32 个数据库服务器,每个数据库服务器是一个库。如果还是不够?最多可以扩展到 1024 个数据库服务器,每个数据库服务器上面一个库一个表。因为最多是 1024 个表。
|
||||
|
||||
这么搞,是不用自己写代码做数据迁移的,都交给 dba 来搞好了,但是 dba 确实是需要做一些库表迁移的工作,但是总比你自己写代码,然后抽数据导数据来的效率高得多吧。
|
||||
这么搞,是不用自己写代码做数据迁移的,都交给 DBA 来搞好了,但是 DBA 确实是需要做一些库表迁移的工作,但是总比你自己写代码,然后抽数据导数据来的效率高得多吧。
|
||||
|
||||
哪怕是要减少库的数量,也很简单,其实说白了就是按倍数缩容就可以了,然后修改一下路由规则。
|
||||
|
||||
@ -62,7 +64,7 @@
|
||||
|
||||
1. 设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是 32 库 * 32 表,对于大部分公司来说,可能几年都够了。
|
||||
2. 路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表
|
||||
3. 扩容的时候,申请增加更多的数据库服务器,装好 mysql,呈倍数扩容,4 台服务器,扩到 8 台服务器,再到 16 台服务器。
|
||||
4. 由 dba 负责将原先数据库服务器的库,迁移到新的数据库服务器上去,库迁移是有一些便捷的工具的。
|
||||
3. 扩容的时候,申请增加更多的数据库服务器,装好 MySQL,呈倍数扩容,4 台服务器,扩到 8 台服务器,再到 16 台服务器。
|
||||
4. 由 DBA 负责将原先数据库服务器的库,迁移到新的数据库服务器上去,库迁移是有一些便捷的工具的。
|
||||
5. 我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址。
|
||||
6. 重新发布系统,上线,原先的路由规则变都不用变,直接可以基于 n 倍的数据库服务器的资源,继续进行线上系统的提供服务。
|
||||
6. 重新发布系统,上线,原先的路由规则变都不用变,直接可以基于 n 倍的数据库服务器的资源,继续进行线上系统的提供服务。
|
||||
|
@ -2,11 +2,15 @@
|
||||
分库分表之后,id 主键如何处理?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实这是分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个**全局唯一**的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 基于数据库的实现方案
|
||||
|
||||
#### 数据库自增 id
|
||||
|
||||
这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个 id。拿到这个 id 之后再往对应的分库分表里去写入。
|
||||
|
||||
这个方案的好处就是方便简单,谁都会用;**缺点就是单库生成**自增 id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前 id 最大值,然后自己递增几个 id,一次性返回一批 id,然后再把当前最大 id 值修改成递增几个 id 之后的一个值;但是**无论如何都是基于单个数据库**。
|
||||
@ -14,41 +18,45 @@
|
||||
**适合的场景**:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你**并发不高,但是数据量太大**导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。
|
||||
|
||||
#### 设置数据库 sequence 或者表自增字段步长
|
||||
|
||||
可以通过设置数据库 sequence 或者表的自增字段步长来进行水平伸缩。
|
||||
|
||||
比如说,现在有 8 个服务节点,每个服务节点使用一个 sequence 功能来产生 ID,每个 sequence 的起始 ID 不同,并且依次递增,步长都是 8。
|
||||
|
||||

|
||||

|
||||
|
||||
**适合的场景**:在用户防止产生的 ID 重复时,这种方案实现起来比较简单,也能达到性能目标。但是服务节点固定,步长也固定,将来如果还要增加服务节点,就不好搞了。
|
||||
|
||||
### UUID
|
||||
|
||||
好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长了、占用空间大,**作为主键性能太差**了;更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。
|
||||
|
||||
适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键是不能用 UUID 的。
|
||||
|
||||
```java
|
||||
UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf
|
||||
``` java
|
||||
UUID.randomUUID().toString().replace("-", "") -> sfsdf23423rr234sfdaf
|
||||
```
|
||||
|
||||
### 获取系统当前时间
|
||||
|
||||
这个就是获取当前时间即可,但是问题是,**并发很高的时候**,比如一秒并发几千,**会有重复的情况**,这个是肯定不合适的。基本就不用考虑了。
|
||||
|
||||
适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。
|
||||
|
||||
### snowflake 算法
|
||||
snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 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台机器)。
|
||||
- 12 bit:这个是用来记录同一个毫秒内产生的不同 id,12 bit 可以代表的最大正整数是 `2^12 - 1 = 4096`,也就是说可以用这个 12 bit 代表的数字来区分**同一个毫秒内**的 4096 个不同的 id。
|
||||
snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bits 作为毫秒数,用 10 bits 作为工作机器 id,12 bits 作为序列号。
|
||||
|
||||
```
|
||||
* 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
|
||||
* 41 bits:表示的是时间戳,单位是毫秒。41 bits 可以表示的数字多达 `2^41 - 1` ,也就是可以标识 `2^41 - 1` 个毫秒值,换算成年就是表示69年的时间。
|
||||
* 10 bits:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。但是 10 bits 里 5 个 bits 代表机房 id,5 个 bits 代表机器 id。意思就是最多代表 `2^5` 个机房(32 个机房),每个机房里可以代表 `2^5` 个机器(32台机器)。
|
||||
* 12 bits:这个是用来记录同一个毫秒内产生的不同 id,12 bits 可以代表的最大正整数是 `2^12 - 1 = 4096` ,也就是说可以用这个 12 bits 代表的数字来区分**同一个毫秒内**的 4096 个不同的 id。
|
||||
|
||||
```
|
||||
0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000
|
||||
```
|
||||
|
||||
```java
|
||||
``` java
|
||||
public class IdWorker {
|
||||
|
||||
private long workerId;
|
||||
@ -168,4 +176,4 @@ public class IdWorker {
|
||||
|
||||
利用这个 snowflake 算法,你可以开发自己公司的服务,甚至对于机房 id 和机器 id,反正给你预留了 5 bit + 5 bit,你换成别的有业务含义的东西也可以的。
|
||||
|
||||
这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。
|
||||
这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。
|
||||
|
@ -2,14 +2,17 @@
|
||||
现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表**动态切换**到分库分表上?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
你看看,你现在已经明白为啥要分库分表了,你也知道常用的分库分表中间件了,你也设计好你们如何分库分表的方案了(水平拆分、垂直拆分、分表),那问题来了,你接下来该怎么把你那个单库单表的系统给迁移到分库分表上去?
|
||||
|
||||
所以这都是一环扣一环的,就是看你有没有全流程经历过这个过程。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
这个其实从 low 到高大上有好几种方案,我们都玩儿过,我都给你说一下。
|
||||
|
||||
### 停机迁移方案
|
||||
|
||||
我先给你说一个最 low 的方案,就是很简单,大家伙儿凌晨 12 点开始运维,网站或者 app 挂个公告,说 0 点到早上 6 点进行运维,无法访问。
|
||||
|
||||
接着到 0 点停机,系统停掉,没有流量写入了,此时老的单库单表数据库静止了。然后你之前得写好一个**导数的一次性工具**,此时直接跑起来,然后将单库单表的数据哗哗哗读出来,写到分库分表里面去。
|
||||
@ -20,9 +23,10 @@
|
||||
|
||||
但是这个方案比较 low,谁都能干,我们来看看高大上一点的方案。
|
||||
|
||||

|
||||

|
||||
|
||||
### 双写迁移方案
|
||||
|
||||
这个是我们常用的一种迁移方案,比较靠谱一些,不用停机,不用看北京凌晨 4 点的风景。
|
||||
|
||||
简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,**除了对老库增删改,都加上对新库的增删改**,这就是所谓的**双写**,同时写俩库,老库和新库。
|
||||
@ -33,4 +37,4 @@
|
||||
|
||||
接着当数据完全一致了,就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干的。
|
||||
|
||||

|
||||

|
||||
|
@ -2,15 +2,17 @@
|
||||
为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实这块肯定是扯到**高并发**了,因为分库分表一定是为了**支撑高并发、数据量大**两个问题的。而且现在说实话,尤其是互联网类的公司面试,基本上都会来这么一下,分库分表如此普遍的技术问题,不问实在是不行,而如果你不知道那也实在是说不过去!
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 为什么要分库分表?(设计高并发系统的时候,数据库层面该如何设计?)
|
||||
说白了,分库分表是两回事儿,大家可别搞混了,可能是光分库不分表,也可能是光分表不分库,都有可能。
|
||||
|
||||
我先给大家抛出来一个场景。
|
||||
|
||||
假如我们现在是一个小创业公司(或者是一个 BAT 公司刚兴起的一个新部门),现在注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10。天,就这种系统,随便找一个有几年工作经验的,然后带几个刚培训出来的,随便干干都可以。
|
||||
假如我们现在是一个小创业公司(或者是一个 BAT 公司刚兴起的一个新部门),现在注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10 个。我的天,就这种系统,随便找一个有几年工作经验的,然后带几个刚培训出来的,随便干干都可以。
|
||||
|
||||
结果没想到我们运气居然这么好,碰上个 CEO 带着我们走上了康庄大道,业务发展迅猛,过了几个月,注册用户数达到了 2000 万!每天活跃用户数 100 万!每天单表数据量 10 万条!高峰期每秒最大请求达到 1000!同时公司还顺带着融资了两轮,进账了几个亿人民币啊!公司估值达到了惊人的几亿美金!这是小独角兽的节奏!
|
||||
|
||||
@ -18,7 +20,7 @@
|
||||
|
||||
再接下来几个月,我的天,CEO 太牛逼了,公司用户数已经达到 1 亿,公司继续融资几十亿人民币啊!公司估值达到了惊人的几十亿美金,成为了国内今年最牛逼的明星创业公司!天,我们太幸运了。
|
||||
|
||||
但是我们同时也是不幸的,因为此时每天活跃用户数上千万,每天单表新增数据多达 50 万,目前一个表总数据量都已经达到了两三千万了!扛不住啊!数据库磁盘容量不断消耗掉!高峰期并发达到惊人的 `5000~8000`!别开玩笑了,哥。我跟你保证,你的系统支撑不到现在,已经挂掉了!
|
||||
但是我们同时也是不幸的,因为此时每天活跃用户数上千万,每天单表新增数据多达 50 万,目前一个表总数据量都已经达到了两三千万了!扛不住啊!数据库磁盘容量不断消耗掉!高峰期并发达到惊人的 `5000~8000` !别开玩笑了,哥。我跟你保证,你的系统支撑不到现在,已经挂掉了!
|
||||
|
||||
好吧,所以你看到这里差不多就理解分库分表是怎么回事儿了,实际上这是跟着你的公司业务发展走的,你公司业务发展越好,用户就越多,数据量越大,请求量越大,那你单个数据库一定扛不住。
|
||||
|
||||
@ -34,7 +36,6 @@
|
||||
|
||||
这就是所谓的**分库分表**,为啥要分库分表?你明白了吧。
|
||||
|
||||
|
||||
| # | 分库分表前 | 分库分表后 |
|
||||
|---|---|---|
|
||||
| 并发支撑情况 | MySQL 单机部署,扛不住高并发 | MySQL从单机到多机,能承受的并发增加了多倍 |
|
||||
@ -42,32 +43,39 @@
|
||||
| SQL 执行性能 | 单表数据量太大,SQL 越跑越慢 | 单表数据量减少,SQL 执行效率明显提升 |
|
||||
|
||||
### 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?
|
||||
|
||||
这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。
|
||||
|
||||
比较常见的包括:
|
||||
|
||||
- Cobar
|
||||
- TDDL
|
||||
- Atlas
|
||||
- Sharding-jdbc
|
||||
- Mycat
|
||||
* Cobar
|
||||
* TDDL
|
||||
* Atlas
|
||||
* Sharding-jdbc
|
||||
* Mycat
|
||||
|
||||
#### Cobar
|
||||
|
||||
阿里 b2b 团队开发和开源的,属于 proxy 层方案,就是介于应用服务器和数据库服务器之间。应用程序通过 JDBC 驱动访问 Cobar 集群,Cobar 根据 SQL 和分库规则对 SQL 做分解,然后分发到 MySQL 集群不同的数据库实例上执行。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。
|
||||
|
||||
#### TDDL
|
||||
|
||||
淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。
|
||||
|
||||
#### Atlas
|
||||
|
||||
360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。
|
||||
|
||||
#### Sharding-jdbc
|
||||
当当开源的,属于 client 层方案,目前已经更名为 [`ShardingSphere`](https://github.com/apache/incubator-shardingsphere)(后文所提到的 `Sharding-jdbc`,等同于 `ShardingSphere`)。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且截至 2019.4,已经推出到了 `4.0.0-RC1` 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也**可以选择的方案**。
|
||||
|
||||
当当开源的,属于 client 层方案,是[ `ShardingSphere` ](https://shardingsphere.apache.org)的 client 层方案,[ `ShardingSphere` ](https://shardingsphere.apache.org)还提供 proxy 层的方案 Sharding-Proxy。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且截至 2019.4,已经推出到了 `4.0.0-RC1` 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也**可以选择的方案**。
|
||||
|
||||
#### Mycat
|
||||
|
||||
基于 Cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 Sharding jdbc 来说,年轻一些,经历的锤炼少一些。
|
||||
|
||||
#### 总结
|
||||
|
||||
综上,现在其实建议考量的,就是 Sharding-jdbc 和 Mycat,这两个都可以去考虑使用。
|
||||
|
||||
Sharding-jdbc 这种 client 层方案的**优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高**,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要**耦合** Sharding-jdbc 的依赖;
|
||||
@ -80,11 +88,11 @@ Mycat 这种 proxy 层方案的**缺点在于需要部署**,自己运维一套
|
||||
|
||||
**水平拆分**的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。
|
||||
|
||||

|
||||

|
||||
|
||||
**垂直拆分**的意思,就是**把一个有很多字段的表给拆分成多个表**,**或者是多个库上去**。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会**将较少的访问频率很高的字段放到一个表里去**,然后**将较多的访问频率很低的字段放到另外一个表里去**。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。
|
||||
|
||||

|
||||

|
||||
|
||||
这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。
|
||||
|
||||
@ -94,11 +102,10 @@ Mycat 这种 proxy 层方案的**缺点在于需要部署**,自己运维一套
|
||||
|
||||
你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都 ok 了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。
|
||||
|
||||
|
||||
而且这儿还有两种**分库分表的方式**:
|
||||
|
||||
- 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如**时间范围**来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
|
||||
- 或者是按照某个字段 hash 一下均匀分散,这个较为常用。
|
||||
* 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如**时间范围**来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
|
||||
* 或者是按照某个字段 hash 一下均匀分散,这个较为常用。
|
||||
|
||||
range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。
|
||||
|
||||
|
@ -1,21 +1,23 @@
|
||||
## 面试题
|
||||
es 的分布式架构原理能说一下么(es 是如何实现分布式的啊)?
|
||||
ES 的分布式架构原理能说一下么(ES 是如何实现分布式的啊)?
|
||||
|
||||
## 面试官心理分析
|
||||
在搜索这块,lucene 是最流行的搜索库。几年前业内一般都问,你了解 lucene 吗?你知道倒排索引的原理吗?现在早已经 out 了,因为现在很多项目都是直接用基于 lucene 的分布式搜索引擎—— ElasticSearch,简称为 es。
|
||||
|
||||
而现在分布式搜索基本已经成为大部分互联网行业的 Java 系统的标配,其中尤为流行的就是 es,前几年 es 没火的时候,大家一般用 solr。但是这两年基本大部分企业和项目都开始转向 es 了。
|
||||
在搜索这块,lucene 是最流行的搜索库。几年前业内一般都问,你了解 lucene 吗?你知道倒排索引的原理吗?现在早已经 out 了,因为现在很多项目都是直接用基于 lucene 的分布式搜索引擎—— ElasticSearch,简称为 ES。
|
||||
|
||||
所以互联网面试,肯定会跟你聊聊分布式搜索引擎,也就一定会聊聊 es,如果你确实不知道,那你真的就 out 了。
|
||||
而现在分布式搜索基本已经成为大部分互联网行业的 Java 系统的标配,其中尤为流行的就是 ES,前几年 ES 没火的时候,大家一般用 solr。但是这两年基本大部分企业和项目都开始转向 ES 了。
|
||||
|
||||
如果面试官问你第一个问题,确实一般都会问你 es 的分布式架构设计能介绍一下么?就看看你对分布式搜索引擎架构的一个基本理解。
|
||||
所以互联网面试,肯定会跟你聊聊分布式搜索引擎,也就一定会聊聊 ES,如果你确实不知道,那你真的就 out 了。
|
||||
|
||||
如果面试官问你第一个问题,确实一般都会问你 ES 的分布式架构设计能介绍一下么?就看看你对分布式搜索引擎架构的一个基本理解。
|
||||
|
||||
## 面试题剖析
|
||||
ElasticSearch 设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。核心思想就是在多台机器上启动多个 es 进程实例,组成了一个 es 集群。
|
||||
|
||||
es 中存储数据的**基本单位是索引**,比如说你现在要在 es 中存储一些订单数据,你就应该在 es 中创建一个索引 `order_idx`,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是 mysql 里的一张表。
|
||||
ElasticSearch 设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。核心思想就是在多台机器上启动多个 ES 进程实例,组成了一个 ES 集群。
|
||||
|
||||
```
|
||||
ES 中存储数据的**基本单位是索引**,比如说你现在要在 ES 中存储一些订单数据,你就应该在 ES 中创建一个索引 `order_idx` ,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是 mysql 里的一张表。
|
||||
|
||||
```
|
||||
index -> type -> mapping -> document -> field。
|
||||
```
|
||||
|
||||
@ -25,22 +27,22 @@ index 相当于 mysql 里的一张表。而 type 没法跟 mysql 里去对比,
|
||||
|
||||
所以就会在订单 index 里,建两个 type,一个是实物商品订单 type,一个是虚拟商品订单 type,这两个 type 大部分字段是一样的,少部分字段是不一样的。
|
||||
|
||||
很多情况下,一个 index 里可能就一个 type,但是确实如果说是一个 index 里有多个 type 的情况(**注意**,`mapping types` 这个概念在 ElasticSearch 7.X 已被完全移除,详细说明可以参考[官方文档](https://github.com/elastic/elasticsearch/blob/6.5/docs/reference/mapping/removal_of_types.asciidoc)),你可以认为 index 是一个类别的表,具体的每个 type 代表了 mysql 中的一个表。每个 type 有一个 mapping,如果你认为一个 type 是具体的一个表,index 就代表多个 type 同属于的一个类型,而 mapping 就是这个 type 的**表结构定义**,你在 mysql 中创建一个表,肯定是要定义表结构的,里面有哪些字段,每个字段是什么类型。实际上你往 index 里的一个 type 里面写的一条数据,叫做一条 document,一条 document 就代表了 mysql 中某个表里的一行,每个 document 有多个 field,每个 field 就代表了这个 document 中的一个字段的值。
|
||||
很多情况下,一个 index 里可能就一个 type,但是确实如果说是一个 index 里有多个 type 的情况(**注意**, `mapping types` 这个概念在 ElasticSearch 7. X 已被完全移除,详细说明可以参考[官方文档](https://github.com/elastic/elasticsearch/blob/6.5/docs/reference/mapping/removal_of_types.asciidoc)),你可以认为 index 是一个类别的表,具体的每个 type 代表了 mysql 中的一个表。每个 type 有一个 mapping,如果你认为一个 type 是具体的一个表,index 就代表多个 type 同属于的一个类型,而 mapping 就是这个 type 的**表结构定义**,你在 mysql 中创建一个表,肯定是要定义表结构的,里面有哪些字段,每个字段是什么类型。实际上你往 index 里的一个 type 里面写的一条数据,叫做一条 document,一条 document 就代表了 mysql 中某个表里的一行,每个 document 有多个 field,每个 field 就代表了这个 document 中的一个字段的值。
|
||||
|
||||

|
||||

|
||||
|
||||
你搞一个索引,这个索引可以拆分成多个 `shard`,每个 shard 存储部分数据。拆分多个 shard 是有好处的,一是**支持横向扩展**,比如你数据量是 3T,3 个 shard,每个 shard 就 1T 的数据,若现在数据量增加到 4T,怎么扩展,很简单,重新建一个有 4 个 shard 的索引,将数据导进去;二是**提高性能**,数据分布在多个 shard,即多台服务器上,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能。
|
||||
你搞一个索引,这个索引可以拆分成多个 `shard` ,每个 shard 存储部分数据。拆分多个 shard 是有好处的,一是**支持横向扩展**,比如你数据量是 3T,3 个 shard,每个 shard 就 1T 的数据,若现在数据量增加到 4T,怎么扩展,很简单,重新建一个有 4 个 shard 的索引,将数据导进去;二是**提高性能**,数据分布在多个 shard,即多台服务器上,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能。
|
||||
|
||||
接着就是这个 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 `primary shard`,负责写入数据,但是还有几个 `replica shard`。`primary shard` 写入数据之后,会将数据同步到其他几个 `replica shard` 上去。
|
||||
接着就是这个 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 `primary shard` ,负责写入数据,但是还有几个 `replica shard` 。 `primary shard` 写入数据之后,会将数据同步到其他几个 `replica shard` 上去。
|
||||
|
||||

|
||||

|
||||
|
||||
通过这个 replica 的方案,每个 shard 的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。
|
||||
|
||||
es 集群多个节点,会自动选举一个节点为 master 节点,这个 master 节点其实就是干一些管理的工作的,比如维护索引元数据、负责切换 primary shard 和 replica shard 身份等。要是 master 节点宕机了,那么会重新选举一个节点为 master 节点。
|
||||
ES 集群多个节点,会自动选举一个节点为 master 节点,这个 master 节点其实就是干一些管理的工作的,比如维护索引元数据、负责切换 primary shard 和 replica shard 身份等。要是 master 节点宕机了,那么会重新选举一个节点为 master 节点。
|
||||
|
||||
如果是非 master节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。接着你要是修复了那个宕机机器,重启了之后,master 节点会控制将缺失的 replica shard 分配过去,同步后续修改的数据之类的,让集群恢复正常。
|
||||
|
||||
说得更简单一点,就是说如果某个非 master 节点宕机了。那么此节点上的 primary shard 不就没了。那好,master 会让 primary shard 对应的 replica shard(在其他机器上)切换为 primary shard。如果宕机的机器修复了,修复后的节点也不再是 primary shard,而是 replica shard。
|
||||
|
||||
其实上述就是 ElasticSearch 作为分布式搜索引擎最基本的一个架构设计。
|
||||
其实上述就是 ElasticSearch 作为分布式搜索引擎最基本的一个架构设计。
|
||||
|
@ -1,29 +1,33 @@
|
||||
## lucene 和 es 的前世今生
|
||||
lucene 是最先进、功能最强大的搜索库。如果直接基于 lucene 开发,非常复杂,即便写一些简单的功能,也要写大量的 Java 代码,需要深入理解原理。
|
||||
## Lucene 和 ES 的前世今生
|
||||
Lucene 是最先进、功能最强大的搜索库。如果直接基于 Lucene 开发,非常复杂,即便写一些简单的功能,也要写大量的 Java 代码,需要深入理解原理。
|
||||
|
||||
elasticsearch 基于 lucene,隐藏了 lucene 的复杂性,提供了简单易用的 restful api / Java api 接口(另外还有其他语言的 api 接口)。
|
||||
ElasticSearch 基于 Lucene,隐藏了 lucene 的复杂性,提供了简单易用的 RESTful api / Java api 接口(另外还有其他语言的 api 接口)。
|
||||
|
||||
- 分布式的文档存储引擎
|
||||
- 分布式的搜索引擎和分析引擎
|
||||
- 分布式,支持 PB 级数据
|
||||
* 分布式的文档存储引擎
|
||||
* 分布式的搜索引擎和分析引擎
|
||||
* 分布式,支持 PB 级数据
|
||||
|
||||
## ES 的核心概念
|
||||
|
||||
## es 的核心概念
|
||||
### Near Realtime
|
||||
近实时,有两层意思:
|
||||
|
||||
- 从写入数据到数据可以被搜索到有一个小延迟(大概是 1s)
|
||||
- 基于 es 执行搜索和分析可以达到秒级
|
||||
* 从写入数据到数据可以被搜索到有一个小延迟(大概是 1s)
|
||||
* 基于 ES 执行搜索和分析可以达到秒级
|
||||
|
||||
### Cluster 集群
|
||||
|
||||
集群包含多个节点,每个节点属于哪个集群都是通过一个配置来决定的,对于中小型应用来说,刚开始一个集群就一个节点很正常。
|
||||
|
||||
### Node 节点
|
||||
|
||||
Node 是集群中的一个节点,节点也有一个名称,默认是随机分配的。默认节点会去加入一个名称为 `elasticsearch` 的集群。如果直接启动一堆节点,那么它们会自动组成一个 elasticsearch 集群,当然一个节点也可以组成 elasticsearch 集群。
|
||||
|
||||
### Document & field
|
||||
文档是 es 中最小的数据单元,一个 document 可以是一条客户数据、一条商品分类数据、一条订单数据,通常用 json 数据结构来表示。每个 index 下的 type,都可以存储多条 document。一个 document 里面有多个 field,每个 field 就是一个数据字段。
|
||||
|
||||
```json
|
||||
文档是 ES 中最小的数据单元,一个 document 可以是一条客户数据、一条商品分类数据、一条订单数据,通常用 json 数据结构来表示。每个 index 下的 type,都可以存储多条 document。一个 document 里面有多个 field,每个 field 就是一个数据字段。
|
||||
|
||||
``` json
|
||||
{
|
||||
"product_id": "1",
|
||||
"product_name": "iPhone X",
|
||||
@ -34,26 +38,31 @@ Node 是集群中的一个节点,节点也有一个名称,默认是随机分
|
||||
```
|
||||
|
||||
### Index
|
||||
|
||||
索引包含了一堆有相似结构的文档数据,比如商品索引。一个索引包含很多 document,一个索引就代表了一类相似或者相同的 ducument。
|
||||
|
||||
### Type
|
||||
|
||||
类型,每个索引里可以有一个或者多个 type,type 是 index 的一个逻辑分类,比如商品 index 下有多个 type:日化商品 type、电器商品 type、生鲜商品 type。每个 type 下的 document 的 field 可能不太一样。
|
||||
|
||||
### shard
|
||||
单台机器无法存储大量数据,es 可以将一个索引中的数据切分为多个 shard,分布在多台服务器上存储。有了 shard 就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个 shard 都是一个 lucene index。
|
||||
|
||||
单台机器无法存储大量数据,ES 可以将一个索引中的数据切分为多个 shard,分布在多台服务器上存储。有了 shard 就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个 shard 都是一个 lucene index。
|
||||
|
||||
### replica
|
||||
|
||||
任何一个服务器随时可能故障或宕机,此时 shard 可能就会丢失,因此可以为每个 shard 创建多个 replica 副本。replica 可以在 shard 故障时提供备用服务,保证数据不丢失,多个 replica 还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认 5 个),replica shard(随时修改数量,默认 1 个),默认每个索引 10 个 shard,5 个 primary shard,5个 replica shard,最小的高可用配置,是 2 台服务器。
|
||||
|
||||
这么说吧,shard 分为 primary shard 和 replica shard。而 primary shard 一般简称为 shard,而 replica shard 一般简称为 replica。
|
||||
|
||||

|
||||

|
||||
|
||||
## es 核心概念 vs. db 核心概念
|
||||
| es | db |
|
||||
## ES 核心概念 vs. DB 核心概念
|
||||
|
||||
| ES | DB |
|
||||
|---|---|
|
||||
| index | 数据库 |
|
||||
| type | 数据表 |
|
||||
| docuemnt | 一行数据 |
|
||||
|
||||
以上是一个简单的类比。
|
||||
以上是一个简单的类比。
|
||||
|
@ -1,36 +1,40 @@
|
||||
## 面试题
|
||||
es 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?
|
||||
ES 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?
|
||||
|
||||
## 面试官心理分析
|
||||
这个问题是肯定要问的,说白了,就是看你有没有实际干过 es,因为啥?其实 es 性能并没有你想象中那么好的。很多时候数据量大了,特别是有几亿条数据的时候,可能你会懵逼的发现,跑个搜索怎么一下 `5~10s`,坑爹了。第一次搜索的时候,是 `5~10s`,后面反而就快了,可能就几百毫秒。
|
||||
|
||||
这个问题是肯定要问的,说白了,就是看你有没有实际干过 es,因为啥?其实 es 性能并没有你想象中那么好的。很多时候数据量大了,特别是有几亿条数据的时候,可能你会懵逼的发现,跑个搜索怎么一下 `5~10s` ,坑爹了。第一次搜索的时候,是 `5~10s` ,后面反而就快了,可能就几百毫秒。
|
||||
|
||||
你就很懵,每个用户第一次访问都会比较慢,比较卡么?所以你要是没玩儿过 es,或者就是自己玩玩儿 demo,被问到这个问题容易懵逼,显示出你对 es 确实玩儿的不怎么样?
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
说实话,es 性能优化是没有什么银弹的,啥意思呢?就是**不要期待着随手调一个参数,就可以万能的应对所有的性能慢的场景**。也许有的场景是你换个参数,或者调整一下语法,就可以搞定,但是绝对不是所有场景都可以这样。
|
||||
|
||||
### 性能优化的杀手锏——filesystem cache
|
||||
|
||||
你往 es 里写的数据,实际上都写到磁盘文件里去了,**查询的时候**,操作系统会将磁盘文件里的数据自动缓存到 `filesystem cache` 里面去。
|
||||
|
||||

|
||||

|
||||
|
||||
es 的搜索引擎严重依赖于底层的 `filesystem cache`,你如果给 `filesystem cache` 更多的内存,尽量让内存可以容纳所有的 `idx segment file ` 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。
|
||||
es 的搜索引擎严重依赖于底层的 `filesystem cache` ,你如果给 `filesystem cache` 更多的内存,尽量让内存可以容纳所有的 `idx segment file ` 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。
|
||||
|
||||
性能差距究竟可以有多大?我们之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,1秒、5秒、10秒。但如果是走 `filesystem cache`,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。
|
||||
性能差距究竟可以有多大?我们之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,1秒、5秒、10秒。但如果是走 `filesystem cache` ,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。
|
||||
|
||||
这里有个真实的案例。某个公司 es 节点有 3 台机器,每台机器看起来内存很多,64G,总内存就是 `64 * 3 = 192G`。每台机器给 es jvm heap 是 `32G`,那么剩下来留给 `filesystem cache` 的就是每台机器才 `32G`,总共集群里给 `filesystem cache` 的就是 `32 * 3 = 96G` 内存。而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 `1T` 的磁盘容量,es 数据量是 `1T`,那么每台机器的数据量是 `300G`。这样性能好吗? `filesystem cache` 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。
|
||||
这里有个真实的案例。某个公司 es 节点有 3 台机器,每台机器看起来内存很多,64G,总内存就是 `64 * 3 = 192G` 。每台机器给 es jvm heap 是 `32G` ,那么剩下来留给 `filesystem cache` 的就是每台机器才 `32G` ,总共集群里给 `filesystem cache` 的就是 `32 * 3 = 96G` 内存。而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 `1T` 的磁盘容量,es 数据量是 `1T` ,那么每台机器的数据量是 `300G` 。这样性能好吗? `filesystem cache` 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。
|
||||
|
||||
归根结底,你要让 es 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。
|
||||
|
||||
根据我们自己的生产环境实践经验,最佳的情况下,是仅仅在 es 中就存少量的数据,就是你要**用来搜索的那些索引**,如果内存留给 `filesystem cache` 的是 100G,那么你就将索引数据控制在 `100G` 以内,这样的话,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内。
|
||||
|
||||
比如说你现在有一行数据。`id,name,age ....` 30 个字段。但是你现在搜索,只需要根据 `id,name,age` 三个字段来搜索。如果你傻乎乎往 es 里写入一行数据所有的字段,就会导致说 `90%` 的数据是不用来搜索的,结果硬是占据了 es 机器上的 `filesystem cache` 的空间,单条数据的数据量越大,就会导致 `filesystem cahce` 能缓存的数据就越少。其实,仅仅写入 es 中要用来检索的**少数几个字段**就可以了,比如说就写入 es `id,name,age` 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 `es + hbase` 这么一个架构。
|
||||
比如说你现在有一行数据。 `id,name,age ....` 30 个字段。但是你现在搜索,只需要根据 `id,name,age` 三个字段来搜索。如果你傻乎乎往 es 里写入一行数据所有的字段,就会导致说 `90%` 的数据是不用来搜索的,结果硬是占据了 es 机器上的 `filesystem cache` 的空间,单条数据的数据量越大,就会导致 `filesystem cahce` 能缓存的数据就越少。其实,仅仅写入 es 中要用来检索的**少数几个字段**就可以了,比如说就写入 es `id,name,age` 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 `es + hbase` 这么一个架构。
|
||||
|
||||
hbase 的特点是**适用于海量数据的在线存储**,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 `doc id`,然后根据 `doc id` 到 hbase 里去查询每个 `doc id` 对应的**完整的数据**,给查出来,再返回给前端。
|
||||
hbase 的特点是**适用于海量数据的在线存储**,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 `doc id` ,然后根据 `doc id` 到 hbase 里去查询每个 `doc id` 对应的**完整的数据**,给查出来,再返回给前端。
|
||||
|
||||
写入 es 的数据最好小于等于,或者是略微大于 es 的 filesystem cache 的内存容量。然后你从 es 检索可能就花费 20ms,然后再根据 es 返回的 id 去 hbase 里查询,查 20 条数据,可能也就耗费个 30ms,可能你原来那么玩儿,1T 数据都放 es,会每次查询都是 5~10s,现在可能性能就会很高,每次查询就是 50ms。
|
||||
|
||||
### 数据预热
|
||||
|
||||
假如说,哪怕是你就按照上述的方案去做了,es 集群中每个机器写入的数据量还是超过了 `filesystem cache` 一倍,比如说你写入一台机器 60G 数据,结果 `filesystem cache` 就 30G,还是有 30G 数据留在了磁盘上。
|
||||
|
||||
其实可以做**数据预热**。
|
||||
@ -42,11 +46,13 @@ hbase 的特点是**适用于海量数据的在线存储**,就是对 hbase 可
|
||||
对于那些你觉得比较热的、经常会有人访问的数据,最好**做一个专门的缓存预热子系统**,就是对热数据每隔一段时间,就提前访问一下,让数据进入 `filesystem cache` 里面去。这样下次别人访问的时候,性能一定会好很多。
|
||||
|
||||
### 冷热分离
|
||||
|
||||
es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将**冷数据写入一个索引中,然后热数据写入另外一个索引中**,这样可以确保热数据在被预热之后,尽量都让他们留在 `filesystem os cache` 里,**别让冷数据给冲刷掉**。
|
||||
|
||||
你看,假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。然后这样的话,你大量的时间是在访问热数据 index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 `filesystem cache` 里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的 index 里的,跟热数据 index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。
|
||||
|
||||
### document 模型设计
|
||||
|
||||
对于 MySQL,我们经常有一些复杂的关联查询。在 es 里该怎么玩儿,es 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。
|
||||
|
||||
最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索了。
|
||||
@ -54,6 +60,7 @@ es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少
|
||||
document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
|
||||
|
||||
### 分页性能优化
|
||||
|
||||
es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。
|
||||
|
||||
分布式的,你要查第 100 页的 10 条数据,不可能说从 5 个 shard,每个 shard 就查 2 条数据,最后到协调节点合并成 10 条数据吧?你**必须**得从每个 shard 都查 1000 条数据过来,然后根据你的需求进行排序、筛选等等操作,最后再次分页,拿到里面第 100 页的数据。你翻页的时候,翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用 es 做分页的时候,你会发现越翻到后面,就越是慢。
|
||||
@ -61,11 +68,14 @@ es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10
|
||||
我们之前也是遇到过这个问题,用 es 作分页,前几页就几十毫秒,翻到 10 页或者几十页的时候,基本上就要 5~10 秒才能查出来一页数据了。
|
||||
|
||||
有什么解决方案吗?
|
||||
|
||||
#### 不允许深度分页(默认深度分页性能很差)
|
||||
|
||||
跟产品经理说,你系统不允许翻那么深的页,默认翻的越深,性能就越差。
|
||||
|
||||
#### 类似于 app 里的推荐商品不断下拉出来一页一页的
|
||||
类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 `scroll api`,关于如何使用,自行上网搜索。
|
||||
|
||||
类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 `scroll api` ,关于如何使用,自行上网搜索。
|
||||
|
||||
scroll 会一次性给你生成**所有数据的一个快照**,然后每次滑动向后翻页就是通过**游标** `scroll_id` 移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。
|
||||
|
||||
@ -73,4 +83,4 @@ scroll 会一次性给你生成**所有数据的一个快照**,然后每次滑
|
||||
|
||||
初始化时必须指定 `scroll` 参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。
|
||||
|
||||
除了用 `scroll api`,你也可以用 `search_after` 来做,`search_after` 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。
|
||||
除了用 `scroll api` ,你也可以用 `search_after` 来做, `search_after` 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。
|
||||
|
@ -1,7 +1,8 @@
|
||||
## 面试题
|
||||
es 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?
|
||||
ES 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
这个问题,包括后面的 redis 什么的,谈到 es、redis、mysql 分库分表等等技术,面试必问!就是你生产环境咋部署的?说白了,这个问题没啥技术含量,就是看你有没有在真正的生产环境里干过这事儿!
|
||||
|
||||
有些同学可能是没在生产环境中干过的,没实际去拿线上机器部署过 es 集群,也没实际玩儿过,也没往 es 集群里面导入过几千万甚至是几亿的数据量,可能你就不太清楚这里面的一些生产项目中的细节。
|
||||
@ -9,12 +10,13 @@ es 生产集群的部署架构是什么?每个索引的数据量大概有多
|
||||
如果你是自己就玩儿过 demo,没碰过真实的 es 集群,那你可能此时会懵。别懵,你一定要云淡风轻的回答出来这个问题,表示你确实干过这事儿。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
其实这个问题没啥,如果你确实干过 es,那你肯定了解你们生产 es 集群的实际情况,部署了几台机器?有多少个索引?每个索引有多大数据量?每个索引给了多少个分片?你肯定知道!
|
||||
|
||||
但是如果你确实没干过,也别虚,我给你说一个基本的版本,你到时候就简单说一下就好了。
|
||||
|
||||
- es 生产集群我们部署了 5 台机器,每台机器是 6 核 64G 的,集群总内存是 320G。
|
||||
- 我们 es 集群的日增量数据大概是 2000 万条,每天日增量数据大概是 500MB,每月增量数据大概是 6 亿,15G。目前系统已经运行了几个月,现在 es 集群里数据总量大概是 100G 左右。
|
||||
- 目前线上有 5 个索引(这个结合你们自己业务来,看看自己有哪些数据可以放 es 的),每个索引的数据量大概是 20G,所以这个数据量之内,我们每个索引分配的是 8 个 shard,比默认的 5 个 shard 多了 3 个 shard。
|
||||
* es 生产集群我们部署了 5 台机器,每台机器是 6 核 64G 的,集群总内存是 320G。
|
||||
* 我们 es 集群的日增量数据大概是 2000 万条,每天日增量数据大概是 500MB,每月增量数据大概是 6 亿,15G。目前系统已经运行了几个月,现在 es 集群里数据总量大概是 100G 左右。
|
||||
* 目前线上有 5 个索引(这个结合你们自己业务来,看看自己有哪些数据可以放 es 的),每个索引的数据量大概是 20G,所以这个数据量之内,我们每个索引分配的是 8 个 shard,比默认的 5 个 shard 多了 3 个 shard。
|
||||
|
||||
大概就这么说一下就行了。
|
||||
大概就这么说一下就行了。
|
||||
|
@ -1,66 +1,72 @@
|
||||
## 面试题
|
||||
es 写入数据的工作原理是什么啊?es 查询数据的工作原理是什么啊?底层的 lucene 介绍一下呗?倒排索引了解吗?
|
||||
ES 写入数据的工作原理是什么啊?ES 查询数据的工作原理是什么啊?底层的 Lucene 介绍一下呗?倒排索引了解吗?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
问这个,其实面试官就是要看看你了解不了解 es 的一些基本原理,因为用 es 无非就是写入数据,搜索数据。你要是不明白你发起一个写入和搜索请求的时候,es 在干什么,那你真的是......
|
||||
|
||||
对 es 基本就是个黑盒,你还能干啥?你唯一能干的就是用 es 的 api 读写数据了。要是出点什么问题,你啥都不知道,那还能指望你什么呢?
|
||||
|
||||
## 面试题剖析
|
||||
### es 写数据过程
|
||||
- 客户端选择一个 node 发送请求过去,这个 node 就是 `coordinating node`(协调节点)。
|
||||
- `coordinating node` 对 document 进行**路由**,将请求转发给对应的 node(有 primary shard)。
|
||||
- 实际的 node 上的 `primary shard` 处理请求,然后将数据同步到 `replica node`。
|
||||
- `coordinating node` 如果发现 `primary node` 和所有 `replica node` 都搞定之后,就返回响应结果给客户端。
|
||||
|
||||

|
||||
### es 写数据过程
|
||||
|
||||
* 客户端选择一个 node 发送请求过去,这个 node 就是 `coordinating node` (协调节点)。
|
||||
* `coordinating node` 对 document 进行**路由**,将请求转发给对应的 node(有 primary shard)。
|
||||
* 实际的 node 上的 `primary shard` 处理请求,然后将数据同步到 `replica node` 。
|
||||
* `coordinating node` 如果发现 `primary node` 和所有 `replica node` 都搞定之后,就返回响应结果给客户端。
|
||||
|
||||

|
||||
|
||||
### es 读数据过程
|
||||
|
||||
可以通过 `doc id` 来查询,会根据 `doc id` 进行 hash,判断出来当时把 `doc id` 分配到了哪个 shard 上面去,从那个 shard 去查询。
|
||||
|
||||
- 客户端发送请求到**任意**一个 node,成为 `coordinate node`。
|
||||
- `coordinate node` 对 `doc id` 进行哈希路由,将请求转发到对应的 node,此时会使用 `round-robin` **随机轮询算法**,在 `primary shard` 以及其所有 replica 中随机选择一个,让读请求负载均衡。
|
||||
- 接收请求的 node 返回 document 给 `coordinate node`。
|
||||
- `coordinate node` 返回 document 给客户端。
|
||||
* 客户端发送请求到**任意**一个 node,成为 `coordinate node` 。
|
||||
* `coordinate node` 对 `doc id` 进行哈希路由,将请求转发到对应的 node,此时会使用 `round-robin` **随机轮询算法**,在 `primary shard` 以及其所有 replica 中随机选择一个,让读请求负载均衡。
|
||||
* 接收请求的 node 返回 document 给 `coordinate node` 。
|
||||
* `coordinate node` 返回 document 给客户端。
|
||||
|
||||
### es 搜索数据过程
|
||||
|
||||
es 最强大的是做全文检索,就是比如你有三条数据:
|
||||
```
|
||||
|
||||
```
|
||||
java真好玩儿啊
|
||||
java好难学啊
|
||||
j2ee特别牛
|
||||
```
|
||||
|
||||
你根据 `java` 关键词来搜索,将包含 `java`的 `document` 给搜索出来。es 就会给你返回:java真好玩儿啊,java好难学啊。
|
||||
你根据 `java` 关键词来搜索,将包含 `java` 的 `document` 给搜索出来。es 就会给你返回:java真好玩儿啊,java好难学啊。
|
||||
|
||||
- 客户端发送请求到一个 `coordinate node`。
|
||||
- 协调节点将搜索请求转发到**所有**的 shard 对应的 `primary shard` 或 `replica shard`,都可以。
|
||||
- query phase:每个 shard 将自己的搜索结果(其实就是一些 `doc id`)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。
|
||||
- fetch phase:接着由协调节点根据 `doc id` 去各个节点上**拉取实际**的 `document` 数据,最终返回给客户端。
|
||||
* 客户端发送请求到一个 `coordinate node` 。
|
||||
* 协调节点将搜索请求转发到**所有**的 shard 对应的 `primary shard` 或 `replica shard` ,都可以。
|
||||
* query phase:每个 shard 将自己的搜索结果(其实就是一些 `doc id` )返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。
|
||||
* fetch phase:接着由协调节点根据 `doc id` 去各个节点上**拉取实际**的 `document` 数据,最终返回给客户端。
|
||||
|
||||
> 写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。
|
||||
|
||||
### 写数据底层原理
|
||||
|
||||

|
||||

|
||||
|
||||
先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。
|
||||
|
||||
如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 `refresh` 到一个新的 `segment file` 中,但是此时数据不是直接进入 `segment file` 磁盘文件,而是先进入 `os cache` 。这个过程就是 `refresh`。
|
||||
如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 `refresh` 到一个新的 `segment file` 中,但是此时数据不是直接进入 `segment file` 磁盘文件,而是先进入 `os cache` 。这个过程就是 `refresh` 。
|
||||
|
||||
每隔 1 秒钟,es 将 buffer 中的数据写入一个**新的** `segment file`,每秒钟会产生一个**新的磁盘文件** `segment file`,这个 `segment file` 中就存储最近 1 秒内 buffer 中写入的数据。
|
||||
每隔 1 秒钟,es 将 buffer 中的数据写入一个**新的** `segment file` ,每秒钟会产生一个**新的磁盘文件** `segment file` ,这个 `segment file` 中就存储最近 1 秒内 buffer 中写入的数据。
|
||||
|
||||
但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。
|
||||
|
||||
操作系统里面,磁盘文件其实都有一个东西,叫做 `os cache`,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 `os cache`,先进入操作系统级别的一个内存缓存中去。只要 `buffer` 中的数据被 refresh 操作刷入 `os cache`中,这个数据就可以被搜索到了。
|
||||
操作系统里面,磁盘文件其实都有一个东西,叫做 `os cache` ,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 `os cache` ,先进入操作系统级别的一个内存缓存中去。只要 `buffer` 中的数据被 refresh 操作刷入 `os cache` 中,这个数据就可以被搜索到了。
|
||||
|
||||
为什么叫 es 是**准实时**的? `NRT`,全称 `near real-time`。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 `restful api` 或者 `java api`,**手动**执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 `os cache`中,让数据立马就可以被搜索到。只要数据被输入 `os cache` 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。
|
||||
为什么叫 es 是**准实时**的? `NRT` ,全称 `near real-time` 。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 `restful api` 或者 `java api` ,**手动**执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 `os cache` 中,让数据立马就可以被搜索到。只要数据被输入 `os cache` 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。
|
||||
|
||||
重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 `buffer` 数据写入一个又一个新的 `segment file` 中去,每次 `refresh` 完 buffer 清空,translog 保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 `commit` 操作。
|
||||
|
||||
commit 操作发生第一步,就是将 buffer 中现有数据 `refresh` 到 `os cache` 中去,清空 buffer。然后,将一个 `commit point` 写入磁盘文件,里面标识着这个 `commit point` 对应的所有 `segment file`,同时强行将 `os cache` 中目前所有的数据都 `fsync` 到磁盘文件中去。最后**清空** 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。
|
||||
commit 操作发生第一步,就是将 buffer 中现有数据 `refresh` 到 `os cache` 中去,清空 buffer。然后,将一个 `commit point` 写入磁盘文件,里面标识着这个 `commit point` 对应的所有 `segment file` ,同时强行将 `os cache` 中目前所有的数据都 `fsync` 到磁盘文件中去。最后**清空** 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。
|
||||
|
||||
这个 commit 操作叫做 `flush`。默认 30 分钟自动执行一次 `flush`,但如果 translog 过大,也会触发 `flush`。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。
|
||||
这个 commit 操作叫做 `flush` 。默认 30 分钟自动执行一次 `flush` ,但如果 translog 过大,也会触发 `flush` 。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。
|
||||
|
||||
translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 `translog` 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。
|
||||
|
||||
@ -73,18 +79,21 @@ translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁
|
||||
> 数据写入 segment file 之后,同时就建立好了倒排索引。
|
||||
|
||||
### 删除/更新数据底层原理
|
||||
|
||||
如果是删除操作,commit 的时候会生成一个 `.del` 文件,里面将某个 doc 标识为 `deleted` 状态,那么搜索的时候根据 `.del` 文件就知道这个 doc 是否被删除了。
|
||||
|
||||
如果是更新操作,就是将原来的 doc 标识为 `deleted` 状态,然后新写入一条数据。
|
||||
|
||||
buffer 每 refresh 一次,就会产生一个 `segment file`,所以默认情况下是 1 秒钟一个 `segment file`,这样下来 `segment file` 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 `segment file` 合并成一个,同时这里会将标识为 `deleted` 的 doc 给**物理删除掉**,然后将新的 `segment file` 写入磁盘,这里会写一个 `commit point`,标识所有新的 `segment file`,然后打开 `segment file` 供搜索使用,同时删除旧的 `segment file`。
|
||||
buffer 每 refresh 一次,就会产生一个 `segment file` ,所以默认情况下是 1 秒钟一个 `segment file` ,这样下来 `segment file` 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 `segment file` 合并成一个,同时这里会将标识为 `deleted` 的 doc 给**物理删除掉**,然后将新的 `segment file` 写入磁盘,这里会写一个 `commit point` ,标识所有新的 `segment file` ,然后打开 `segment file` 供搜索使用,同时删除旧的 `segment file` 。
|
||||
|
||||
### 底层 lucene
|
||||
|
||||
简单来说,lucene 就是一个 jar 包,里面包含了封装好的各种建立倒排索引的算法代码。我们用 Java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 去开发就可以了。
|
||||
|
||||
通过 lucene,我们可以将已有的数据建立索引,lucene 会在本地磁盘上面,给我们组织索引的数据结构。
|
||||
|
||||
### 倒排索引
|
||||
|
||||
在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,文档 1 经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。
|
||||
|
||||
那么,倒排索引就是**关键词到文档** ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了关键词。
|
||||
@ -105,25 +114,25 @@ buffer 每 refresh 一次,就会产生一个 `segment file`,所以默认情
|
||||
|
||||
| WordId | Word | DocIds |
|
||||
|---|---|---|
|
||||
| 1 | 谷歌 | 1,2,3,4,5 |
|
||||
| 2 | 地图 | 1,2,3,4,5 |
|
||||
| 3 | 之父 | 1,2,4,5 |
|
||||
| 4 | 跳槽 | 1,4 |
|
||||
| 5 | Facebook | 1,2,3,4,5 |
|
||||
| 6 | 加盟 | 2,3,5 |
|
||||
| 1 | 谷歌 | 1, 2, 3, 4, 5 |
|
||||
| 2 | 地图 | 1, 2, 3, 4, 5 |
|
||||
| 3 | 之父 | 1, 2, 4, 5 |
|
||||
| 4 | 跳槽 | 1, 4 |
|
||||
| 5 | Facebook | 1, 2, 3, 4, 5 |
|
||||
| 6 | 加盟 | 2, 3, 5 |
|
||||
| 7 | 创始人 | 3 |
|
||||
| 8 | 拉斯 | 3,5 |
|
||||
| 8 | 拉斯 | 3, 5 |
|
||||
| 9 | 离开 | 3 |
|
||||
| 10 | 与 | 4 |
|
||||
| .. | .. | .. |
|
||||
|
||||
另外,实用的倒排索引还可以记录更多的信息,比如文档频率信息,表示在文档集合中有多少个文档包含某个单词。
|
||||
|
||||
那么,有了倒排索引,搜索引擎可以很方便地响应用户的查询。比如用户输入查询 `Facebook`,搜索系统查找倒排索引,从中读出包含这个单词的文档,这些文档就是提供给用户的搜索结果。
|
||||
那么,有了倒排索引,搜索引擎可以很方便地响应用户的查询。比如用户输入查询 `Facebook` ,搜索系统查找倒排索引,从中读出包含这个单词的文档,这些文档就是提供给用户的搜索结果。
|
||||
|
||||
要注意倒排索引的两个重要细节:
|
||||
|
||||
- 倒排索引中的所有词项对应一个或多个文档;
|
||||
- 倒排索引中的词项**根据字典顺序升序排列**
|
||||
* 倒排索引中的所有词项对应一个或多个文档;
|
||||
* 倒排索引中的词项**根据字典顺序升序排列**
|
||||
|
||||
> 上面只是一个简单的栗子,并没有严格按照字典顺序升序排列。
|
||||
> 上面只是一个简单的栗子,并没有严格按照字典顺序升序排列。
|
||||
|
@ -2,6 +2,7 @@
|
||||
如何设计一个高并发系统?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
说实话,如果面试官问你这个题目,那么你必须要使出全身吃奶劲了。为啥?因为你没看到现在很多公司招聘的 JD 里都是说啥,有高并发就经验者优先。
|
||||
|
||||
如果你确实有真才实学,在互联网公司里干过高并发系统,那你确实拿 offer 基本如探囊取物,没啥问题。面试官也绝对不会这样来问你,否则他就是蠢。
|
||||
@ -16,8 +17,8 @@
|
||||
|
||||
所以这个时候你必须得做一把个人秀了,秀出你所有关于高并发的知识!
|
||||
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
其实所谓的高并发,如果你要理解这个问题呢,其实就得从高并发的根源出发,为啥会有高并发?为啥高并发就很牛逼?
|
||||
|
||||
我说的浅显一点,很简单,就是因为刚开始系统都是连接数据库的,但是要知道数据库支撑到每秒并发两三千的时候,基本就快完了。所以才有说,很多公司,刚开始干的时候,技术比较 low,结果业务发展太快,有的时候系统扛不住压力就挂了。
|
||||
@ -30,36 +31,41 @@
|
||||
|
||||
可以分为以下 6 点:
|
||||
|
||||
- 系统拆分
|
||||
- 缓存
|
||||
- MQ
|
||||
- 分库分表
|
||||
- 读写分离
|
||||
- ElasticSearch
|
||||
* 系统拆分
|
||||
* 缓存
|
||||
* MQ
|
||||
* 分库分表
|
||||
* 读写分离
|
||||
* ElasticSearch
|
||||
|
||||

|
||||

|
||||
|
||||
### 系统拆分
|
||||
|
||||
将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。
|
||||
|
||||
### 缓存
|
||||
|
||||
缓存,必须得用缓存。大部分的高并发场景,都是**读多写少**,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家 redis 轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的**读场景,怎么用缓存来抗高并发**。
|
||||
|
||||
### MQ
|
||||
|
||||
MQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,**后边系统消费后慢慢写**,控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机抗几万并发也是 ok 的,这个之前还特意说过。
|
||||
|
||||
### 分库分表
|
||||
|
||||
分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表**拆分为多个表**,每个表的数据量保持少一点,提高 sql 跑的性能。
|
||||
|
||||
### 读写分离
|
||||
|
||||
读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,**主库写**入,**从库读**取,搞一个读写分离。**读流量太多**的时候,还可以**加更多的从库**。
|
||||
|
||||
### ElasticSearch
|
||||
Elasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。
|
||||
|
||||
Elasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。
|
||||
|
||||
上面的 6 点,基本就是高并发系统肯定要干的一些事儿,大家可以仔细结合之前讲过的知识考虑一下,到时候你可以系统的把这块阐述一下,然后每个部分要注意哪些问题,之前都讲过了,你都可以阐述阐述,表明你对这块是有点积累的。
|
||||
|
||||
说句实话,毕竟你真正厉害的一点,不是在于弄明白一些技术,或者大概知道一个高并发系统应该长什么样?其实实际上在真正的复杂的业务系统里,做高并发要远远比上面提到的点要复杂几十倍到上百倍。你需要考虑:哪些需要分库分表,哪些不需要分库分表,单库单表跟分库分表如何 join,哪些数据要放到缓存里去,放哪些数据才可以扛住高并发的请求,你需要完成对一个复杂业务系统的分析之后,然后逐步逐步的加入高并发的系统架构的改造,这个过程是无比复杂的,一旦做过一次,并且做好了,你在这个市场上就会非常的吃香。
|
||||
|
||||
其实大部分公司,真正看重的,不是说你掌握高并发相关的一些基本的架构知识,架构中的一些技术,RocketMQ、Kafka、Redis、Elasticsearch,高并发这一块,你了解了,也只能是次一等的人才。对一个有几十万行代码的复杂的分布式系统,一步一步架构、设计以及实践过高并发架构的人,这个经验是难能可贵的。
|
||||
其实大部分公司,真正看重的,不是说你掌握高并发相关的一些基本的架构知识,架构中的一些技术,RocketMQ、Kafka、Redis、Elasticsearch,高并发这一块,你了解了,也只能是次一等的人才。对一个有几十万行代码的复杂的分布式系统,一步一步架构、设计以及实践过高并发架构的人,这个经验是难能可贵的。
|
||||
|
@ -2,27 +2,32 @@
|
||||
如何保证消息队列的高可用?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
如果有人问到你 MQ 的知识,**高可用是必问的**。[上一讲](/docs/high-concurrency/why-mq.md)提到,MQ 会导致**系统可用性降低**。所以只要你用了 MQ,接下来问的一些要点肯定就是围绕着 MQ 的那些缺点怎么来解决了。
|
||||
|
||||
要是你傻乎乎的就干用了一个 MQ,各种问题从来没考虑过,那你就杯具了,面试官对你的感觉就是,只会简单使用一些技术,没任何思考,马上对你的印象就不太好了。这样的同学招进来要是做个 20k 薪资以内的普通小弟还凑合,要是做薪资 20k+ 的高工,那就惨了,让你设计个系统,里面肯定一堆坑,出了事故公司受损失,团队一起背锅。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
这个问题这么问是很好的,因为不能问你 Kafka 的高可用性怎么保证?ActiveMQ 的高可用性怎么保证?一个面试官要是这么问就显得很没水平,人家可能用的就是 RabbitMQ,没用过 Kafka,你上来问人家 Kafka 干什么?这不是摆明了刁难人么。
|
||||
|
||||
所以有水平的面试官,问的是 MQ 的高可用性怎么保证?这样就是你用过哪个 MQ,你就说说你对那个 MQ 的高可用性的理解。
|
||||
|
||||
### RabbitMQ 的高可用性
|
||||
|
||||
RabbitMQ 是比较有代表性的,因为是**基于主从**(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。
|
||||
|
||||
RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。
|
||||
|
||||
#### 单机模式
|
||||
|
||||
单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的😄,没人生产用单机模式。
|
||||
|
||||
#### 普通集群模式(无高可用性)
|
||||
|
||||
普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你**创建的 queue,只会放在一个 RabbitMQ 实例上**,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。
|
||||
|
||||

|
||||

|
||||
|
||||
这种方式确实很麻烦,也不怎么好,**没做到所谓的分布式**,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有**数据拉取的开销**,后者导致**单实例性能瓶颈**。
|
||||
|
||||
@ -31,30 +36,32 @@ RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模
|
||||
所以这个事儿就比较尴尬了,这就**没有什么所谓的高可用性**,**这方案主要是提高吞吐量的**,就是说让集群中多个节点来服务某个 queue 的读写操作。
|
||||
|
||||
#### 镜像集群模式(高可用性)
|
||||
|
||||
这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个**完整镜像**,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把**消息同步**到多个实例的 queue 上。
|
||||
|
||||

|
||||

|
||||
|
||||
那么**如何开启这个镜像集群模式**呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是**镜像集群模式的策略**,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
|
||||
|
||||
这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就**没有扩展性可言**了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并**没有办法线性扩展**你的 queue。你想,如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢?
|
||||
|
||||
### Kafka 的高可用性
|
||||
|
||||
Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。
|
||||
|
||||
这就是**天然的分布式消息队列**,就是说一个 topic 的数据,是**分散放在多个机器上的,每个机器就放一部分数据**。
|
||||
|
||||
实际上 RabbmitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。
|
||||
实际上 RabbitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。
|
||||
|
||||
Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。
|
||||
|
||||
比如说,我们假设创建了一个 topic,指定其 partition 数量是 3 个,分别在三台机器上。但是,如果第二台机器宕机了,会导致这个 topic 的 1/3 的数据就丢了,因此这个是做不到高可用的。
|
||||
|
||||

|
||||

|
||||
|
||||
Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,**要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题**,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。
|
||||
|
||||

|
||||

|
||||
|
||||
这么搞,就有所谓的**高可用性**了,因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中**重新选举**一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。
|
||||
|
||||
@ -62,4 +69,4 @@ Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机
|
||||
|
||||
**消费**的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。
|
||||
|
||||
看到这里,相信你大致明白了 Kafka 是如何保证高可用机制的了,对吧?不至于一无所知,现场还能给面试官画画图。要是遇上面试官确实是 Kafka 高手,深挖了问,那你只能说不好意思,太深入的你没研究过。
|
||||
看到这里,相信你大致明白了 Kafka 是如何保证高可用机制的了,对吧?不至于一无所知,现场还能给面试官画画图。要是遇上面试官确实是 Kafka 高手,深挖了问,那你只能说不好意思,太深入的你没研究过。
|
||||
|
@ -2,19 +2,22 @@
|
||||
如何保证 redis 的高并发和高可用?redis 的主从复制原理能介绍一下么?redis 的哨兵原理能介绍一下么?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实问这个问题,主要是考考你,redis 单机能承载多高并发?如果单机扛不住如何扩容扛更多的并发?redis 会不会挂?既然 redis 会挂那怎么保证 redis 是高可用的?
|
||||
|
||||
其实针对的都是项目中你肯定要考虑的一些问题,如果你没考虑过,那确实你对生产系统中的问题思考太少。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
如果你用 redis 缓存技术的话,肯定要考虑如何用 redis 来加多台机器,保证 redis 是高并发的,还有就是如何让 redis 保证自己不是挂掉以后就直接死掉了,即 redis 高可用。
|
||||
|
||||
由于此节内容较多,因此,会分为两个小节进行讲解。
|
||||
- [redis 主从架构](/docs/high-concurrency/redis-master-slave.md)
|
||||
- [redis 基于哨兵实现高可用](/docs/high-concurrency/redis-sentinel.md)
|
||||
|
||||
* [redis 主从架构](/docs/high-concurrency/redis-master-slave.md)
|
||||
* [redis 基于哨兵实现高可用](/docs/high-concurrency/redis-sentinel.md)
|
||||
|
||||
redis 实现**高并发**主要依靠**主从架构**,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万 QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。
|
||||
|
||||
如果想要在实现高并发的同时,容纳大量的数据,那么就需要 redis 集群,使用 redis 集群之后,可以提供每秒几十万的读写并发。
|
||||
|
||||
redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。
|
||||
redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。
|
||||
|
@ -2,9 +2,11 @@
|
||||
如何保证消息不被重复消费?或者说,如何保证消息消费的幂等性?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实这是很常见的一个问题,这俩问题基本可以连起来问。既然是消费消息,那肯定要考虑会不会重复消费?能不能避免重复消费?或者重复消费了也别造成系统异常可以吗?这个是 MQ 领域的基本问题,其实本质上还是问你**使用消息队列如何保证幂等性**,这个是你架构里要考虑的一个问题。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
回答这个问题,首先你别听到重复消息这个事儿,就一无所知吧,你**先大概说一说可能会有哪些重复消费的问题**。
|
||||
|
||||
首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是 MQ 自己保证的,是由我们开发来保证的。挑一个 Kafka 来举个例子,说说怎么重复消费吧。
|
||||
@ -17,7 +19,7 @@ Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一
|
||||
|
||||
有这么个场景。数据 1/2/3 依次进入 kafka,kafka 会给这三条数据每条分配一个 offset,代表这条数据的序号,我们就假设分配的 offset 依次是 152/153/154。消费者从 kafka 去消费的时候,也是按照这个顺序去消费。假如当消费者消费了 `offset=153` 的这条数据,刚准备去提交 offset 到 zookeeper,此时消费者进程被重启了。那么此时消费过的数据 1/2 的 offset 并没有提交,kafka 也就不知道你已经消费了 `offset=153` 这条数据。那么重启之后,消费者会找 kafka 说,嘿,哥儿们,你给我接着把上次我消费到的那个地方后面的数据继续给我传递过来。由于之前的 offset 没有提交成功,那么数据 1/2 会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。
|
||||
|
||||

|
||||

|
||||
|
||||
如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据 1/2 在数据库里插入了 2 次,那么数据就错啦。
|
||||
|
||||
@ -33,11 +35,11 @@ Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一
|
||||
|
||||
其实还是得结合业务来思考,我这里给几个思路:
|
||||
|
||||
- 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
|
||||
- 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
|
||||
- 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
|
||||
- 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。
|
||||
* 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
|
||||
* 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
|
||||
* 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
|
||||
* 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。
|
||||
|
||||

|
||||

|
||||
|
||||
当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。
|
||||
当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。
|
||||
|
@ -2,31 +2,37 @@
|
||||
如何保证消息的顺序性?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实这个也是用 MQ 的时候必问的话题,第一看看你了不了解顺序这个事儿?第二看看你有没有办法保证消息是有顺序的?这是生产系统中常见的问题。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
我举个例子,我们以前做过一个 mysql `binlog` 同步的系统,压力还是非常大的,日同步数据要达到上亿,就是说数据从一个 mysql 库原封不动地同步到另一个 mysql 库里面去(mysql -> mysql)。常见的一点在于说比如大数据 team,就需要同步一个 mysql 库过来,对公司的业务系统的数据做各种复杂的操作。
|
||||
|
||||
你在 mysql 里增删改一条数据,对应出来了增删改 3 条 `binlog` 日志,接着这三条 `binlog` 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。
|
||||
你在 mysql 里增删改一条数据,对应出来了增删改 3 条 `binlog` 日志,接着这三条 `binlog` 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你愣是换了顺序给执行成删除、修改、增加,不全错了么。
|
||||
|
||||
本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。
|
||||
|
||||
先看看顺序会错乱的俩场景:
|
||||
- **RabbitMQ**:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。
|
||||
|
||||

|
||||
* **RabbitMQ**:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。
|
||||
|
||||
- **Kafka**:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。<br>消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞**多个线程来并发处理消息**。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
|
||||

|
||||
|
||||

|
||||
* **Kafka**:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。<br>消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞**多个线程来并发处理消息**。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
|
||||
|
||||

|
||||
|
||||
### 解决方案
|
||||
|
||||
#### RabbitMQ
|
||||
拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
|
||||

|
||||
|
||||

|
||||
|
||||
#### Kafka
|
||||
- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
|
||||
- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
|
||||
|
||||

|
||||
* 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。
|
||||
* 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
|
||||
|
||||

|
||||
|
@ -2,22 +2,26 @@
|
||||
如何保证消息的可靠性传输?或者说,如何处理消息丢失的问题?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
这个是肯定的,用 MQ 有个基本原则,就是**数据不能多一条,也不能少一条**,不能多,就是前面说的[重复消费和幂等性问题](/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)。不能少,就是说这数据别搞丢了。那这个问题你必须得考虑一下。
|
||||
|
||||
如果说你这个是用 MQ 来传递非常核心的消息,比如说计费、扣费的一些消息,那必须确保这个 MQ 传递过程中**绝对不会把计费消息给弄丢**。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
数据的丢失问题,可能出现在生产者、MQ、消费者中,咱们从 RabbitMQ 和 Kafka 分别来分析一下吧。
|
||||
|
||||
### RabbitMQ
|
||||

|
||||
|
||||

|
||||
|
||||
#### 生产者弄丢了数据
|
||||
|
||||
生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。
|
||||
|
||||
此时可以选择用 RabbitMQ 提供的事务功能,就是生产者**发送数据之前**开启 RabbitMQ 事务`channel.txSelect`,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务`channel.txRollback`,然后重试发送消息;如果收到了消息,那么可以提交事务`channel.txCommit`。
|
||||
```java
|
||||
此时可以选择用 RabbitMQ 提供的事务功能,就是生产者**发送数据之前**开启 RabbitMQ 事务 `channel.txSelect` ,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务 `channel.txRollback` ,然后重试发送消息;如果收到了消息,那么可以提交事务 `channel.txCommit` 。
|
||||
|
||||
``` java
|
||||
// 开启事务
|
||||
channel.txSelect
|
||||
try {
|
||||
@ -41,27 +45,32 @@ channel.txCommit
|
||||
所以一般在生产者这块**避免数据丢失**,都是用 `confirm` 机制的。
|
||||
|
||||
#### RabbitMQ 弄丢了数据
|
||||
|
||||
就是 RabbitMQ 自己弄丢了数据,这个你必须**开启 RabbitMQ 的持久化**,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,**恢复之后会自动读取之前存储的数据**,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,**可能导致少量数据丢失**,但是这个概率较小。
|
||||
|
||||
设置持久化有**两个步骤**:
|
||||
|
||||
- 创建 queue 的时候将其设置为持久化<br>
|
||||
* 创建 queue 的时候将其设置为持久化<br>
|
||||
|
||||
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
|
||||
- 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2<br>
|
||||
|
||||
* 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2<br>
|
||||
|
||||
就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
|
||||
|
||||
必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
|
||||
|
||||
注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。
|
||||
|
||||
所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 `ack`,你也是可以自己重发的。
|
||||
所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 `ack` ,你也是可以自己重发的。
|
||||
|
||||
#### 消费端弄丢了数据
|
||||
|
||||
RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费到,还没处理,结果进程挂了**,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。
|
||||
|
||||
这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack`,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
|
||||
这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack` ,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
|
||||
|
||||

|
||||

|
||||
|
||||
### Kafka
|
||||
|
||||
@ -80,12 +89,13 @@ RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费
|
||||
|
||||
所以此时一般是要求起码设置如下 4 个参数:
|
||||
|
||||
- 给 topic 设置 `replication.factor` 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
|
||||
- 在 Kafka 服务端设置 `min.insync.replicas` 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
|
||||
- 在 producer 端设置 `acks=all`:这个是要求每条数据,必须是**写入所有 replica 之后,才能认为是写成功了**。
|
||||
- 在 producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个是**要求一旦写入失败,就无限重试**,卡在这里了。
|
||||
* 给 topic 设置 `replication.factor` 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
|
||||
* 在 Kafka 服务端设置 `min.insync.replicas` 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
|
||||
* 在 producer 端设置 `acks=all` :这个是要求每条数据,必须是**写入所有 replica 之后,才能认为是写成功了**。
|
||||
* 在 producer 端设置 `retries=MAX` (很大很大很大的一个值,无限次重试的意思):这个是**要求一旦写入失败,就无限重试**,卡在这里了。
|
||||
|
||||
我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。
|
||||
|
||||
#### 生产者会不会弄丢数据?
|
||||
如果按照上述的思路设置了 `acks=all`,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。
|
||||
|
||||
如果按照上述的思路设置了 `acks=all` ,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。
|
||||
|
382
docs/high-concurrency/huifer-how-to-limit-current.md
Normal file
@ -0,0 +1,382 @@
|
||||
# 如何限流?在工作中是怎么做的?说一下具体的实现?
|
||||
|
||||
* Author: [HuiFer](https://github.com/huifer)
|
||||
* Description: 该文简单介绍限流相关技术以及实现
|
||||
|
||||
## 什么是限流
|
||||
|
||||
> 限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。
|
||||
|
||||
## 限流方法
|
||||
|
||||
### 计数器
|
||||
|
||||
#### 实现方式
|
||||
|
||||
* 控制单位时间内的请求数量
|
||||
|
||||
``` java
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class Counter {
|
||||
/**
|
||||
* 最大访问数量
|
||||
*/
|
||||
private final int limit = 10;
|
||||
/**
|
||||
* 访问时间差
|
||||
*/
|
||||
private final long timeout = 1000;
|
||||
/**
|
||||
* 请求时间
|
||||
*/
|
||||
private long time;
|
||||
/**
|
||||
* 当前计数器
|
||||
*/
|
||||
private AtomicInteger reqCount = new AtomicInteger(0);
|
||||
|
||||
public boolean limit() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now < time + timeout) {
|
||||
// 单位时间内
|
||||
reqCount.addAndGet(1);
|
||||
return reqCount.get() <= limit;
|
||||
} else {
|
||||
// 超出单位时间
|
||||
time = now;
|
||||
reqCount = new AtomicInteger(0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
* 劣势
|
||||
- 假设在 00:01 时发生一个请求,在 00:01-00:58 之间不在发送请求,在 00:59 时发送剩下的所有请求 `n-1` (n为限流请求数量),在下一分钟的 00:01 发送n个请求,这样在2秒钟内请求到达了 `2n - 1` 个.
|
||||
- 设每分钟请求数量为60个,每秒可以处理1个请求,用户在 00:59 发送 60 个请求,在 01:00 发送 60 个请求 此时2秒钟有120个请求(每秒60个请求),远远大于了每秒钟处理数量的阈值
|
||||
|
||||
### 滑动窗口
|
||||
|
||||
#### 实现方式
|
||||
|
||||
* 滑动窗口是对计数器方式的改进, 增加一个时间粒度的度量单位
|
||||
- 把一分钟分成若干等分(6份,每份10秒), 在每一份上设置独立计数器,在 00:00-00:09 之间发生请求计数器累加1.当等分数量越大限流统计就越详细
|
||||
|
||||
``` java
|
||||
package com.example.demo1.service;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class TimeWindow {
|
||||
private ConcurrentLinkedQueue<Long> queue = new ConcurrentLinkedQueue<Long>();
|
||||
|
||||
/**
|
||||
* 间隔秒数
|
||||
*/
|
||||
private int seconds;
|
||||
|
||||
/**
|
||||
* 最大限流
|
||||
*/
|
||||
private int max;
|
||||
|
||||
public TimeWindow(int max, int seconds) {
|
||||
this.seconds = seconds;
|
||||
this.max = max;
|
||||
|
||||
/**
|
||||
* 永续线程执行清理queue 任务
|
||||
*/
|
||||
new Thread(() -> {
|
||||
while (true) {
|
||||
try {
|
||||
// 等待 间隔秒数-1 执行清理操作
|
||||
Thread.sleep((seconds - 1) * 1000L);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
clean();
|
||||
}
|
||||
}).start();
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
final TimeWindow timeWindow = new TimeWindow(10, 1);
|
||||
|
||||
// 测试3个线程
|
||||
IntStream.range(0, 3).forEach((i) -> {
|
||||
new Thread(() -> {
|
||||
|
||||
while (true) {
|
||||
|
||||
try {
|
||||
Thread.sleep(new Random().nextInt(20) * 100);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
timeWindow.take();
|
||||
}
|
||||
|
||||
}).start();
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取令牌,并且添加时间
|
||||
*/
|
||||
public void take() {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
|
||||
int size = sizeOfValid();
|
||||
if (size > max) {
|
||||
System.err.println("超限");
|
||||
|
||||
}
|
||||
synchronized (queue) {
|
||||
if (sizeOfValid() > max) {
|
||||
System.err.println("超限");
|
||||
System.err.println("queue中有 " + queue.size() + " 最大数量 " + max);
|
||||
}
|
||||
this.queue.offer(System.currentTimeMillis());
|
||||
}
|
||||
System.out.println("queue中有 " + queue.size() + " 最大数量 " + max);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public int sizeOfValid() {
|
||||
Iterator<Long> it = queue.iterator();
|
||||
Long ms = System.currentTimeMillis() - seconds * 1000;
|
||||
int count = 0;
|
||||
while (it.hasNext()) {
|
||||
long t = it.next();
|
||||
if (t > ms) {
|
||||
// 在当前的统计时间范围内
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的时间
|
||||
*/
|
||||
public void clean() {
|
||||
Long c = System.currentTimeMillis() - seconds * 1000;
|
||||
|
||||
Long tl = null;
|
||||
while ((tl = queue.peek()) != null && tl < c) {
|
||||
System.out.println("清理数据");
|
||||
queue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Leaky Bucket 漏桶
|
||||
|
||||
#### 实现方式
|
||||
|
||||
* 规定固定容量的桶, 有水进入, 有水流出. 对于流进的水我们无法估计进来的数量、速度, 对于流出的水我们可以控制速度.
|
||||
|
||||
``` java
|
||||
public class LeakBucket {
|
||||
/**
|
||||
* 时间
|
||||
*/
|
||||
private long time;
|
||||
/**
|
||||
* 总量
|
||||
*/
|
||||
private Double total;
|
||||
/**
|
||||
* 水流出去的速度
|
||||
*/
|
||||
private Double rate;
|
||||
/**
|
||||
* 当前总量
|
||||
*/
|
||||
private Double nowSize;
|
||||
|
||||
public boolean limit() {
|
||||
long now = System.currentTimeMillis();
|
||||
nowSize = Math.max(0, (nowSize - (now - time) * rate));
|
||||
time = now;
|
||||
if ((nowSize + 1) < total) {
|
||||
nowSize++;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Token Bucket 令牌桶
|
||||
|
||||
#### 实现方式
|
||||
|
||||
* 规定固定容量的桶, token 以固定速度往桶内填充, 当桶满时 token 不会被继续放入, 每过来一个请求把 token 从桶中移除, 如果桶中没有 token 不能请求
|
||||
|
||||
``` java
|
||||
public class TokenBucket {
|
||||
/**
|
||||
* 时间
|
||||
*/
|
||||
private long time;
|
||||
/**
|
||||
* 总量
|
||||
*/
|
||||
private Double total;
|
||||
/**
|
||||
* token 放入速度
|
||||
*/
|
||||
private Double rate;
|
||||
/**
|
||||
* 当前总量
|
||||
*/
|
||||
private Double nowSize;
|
||||
|
||||
public boolean limit() {
|
||||
long now = System.currentTimeMillis();
|
||||
nowSize = Math.min(total, nowSize + (now - time) * rate);
|
||||
time = now;
|
||||
if (nowSize < 1) {
|
||||
// 桶里没有token
|
||||
return false;
|
||||
} else {
|
||||
// 存在token
|
||||
nowSize -= 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## 工作中的使用
|
||||
|
||||
### spring cloud gateway
|
||||
|
||||
* spring cloud gateway 默认使用redis进行限流, 笔者一般只是修改修改参数属于拿来即用. 并没有去从头实现上述那些算法.
|
||||
|
||||
``` xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
``` yaml
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
|
||||
- id: requestratelimiter_route
|
||||
|
||||
uri: lb://pigx-upms
|
||||
order: 10000
|
||||
predicates:
|
||||
|
||||
- Path=/admin/**
|
||||
|
||||
filters:
|
||||
|
||||
- name: RequestRateLimiter
|
||||
|
||||
args:
|
||||
redis-rate-limiter.replenishRate: 1 # 令牌桶的容积
|
||||
redis-rate-limiter.burstCapacity: 3 # 流速 每秒
|
||||
key-resolver: "#{@remoteAddrKeyResolver}" #SPEL表达式去的对应的bean
|
||||
|
||||
- StripPrefix=1
|
||||
|
||||
```
|
||||
|
||||
``` java
|
||||
@Bean
|
||||
KeyResolver remoteAddrKeyResolver() {
|
||||
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
|
||||
}
|
||||
```
|
||||
|
||||
### sentinel
|
||||
|
||||
* 通过配置来控制每个url的流量
|
||||
|
||||
``` xml
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
``` yaml
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
sentinel:
|
||||
transport:
|
||||
dashboard: localhost:8080
|
||||
port: 8720
|
||||
datasource:
|
||||
ds:
|
||||
nacos:
|
||||
server-addr: localhost:8848
|
||||
dataId: spring-cloud-sentinel-nacos
|
||||
groupId: DEFAULT_GROUP
|
||||
rule-type: flow
|
||||
namespace: xxxxxxxx
|
||||
```
|
||||
|
||||
* 配置内容在nacos上进行编辑
|
||||
|
||||
``` json
|
||||
[
|
||||
{
|
||||
"resource": "/hello",
|
||||
"limitApp": "default",
|
||||
"grade": 1,
|
||||
"count": 1,
|
||||
"strategy": 0,
|
||||
"controlBehavior": 0,
|
||||
"clusterMode": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
* resource:资源名,即限流规则的作用对象。
|
||||
* limitApp:流控针对的调用来源,若为 default 则不区分调用来源。
|
||||
* grade:限流阈值类型,QPS 或线程数模式,0代表根据并发数量来限流,1代表根据QPS来进行流量控制。
|
||||
* count:限流阈值
|
||||
* strategy:判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口
|
||||
* controlBehavior:流控效果(直接拒绝 / 排队等待 / 慢启动模式)
|
||||
* clusterMode:是否为集群模式
|
||||
|
||||
### 总结
|
||||
|
||||
> sentinel和spring cloud gateway两个框架都是很好的限流框架, 但是在我使用中还没有将[spring-cloud-alibaba](https://github.com/alibaba/spring-cloud-alibaba)接入到项目中进行使用, 所以我会选择**spring cloud gateway**, 当接入完整的或者接入Nacos项目使用setinel会有更加好的体验.
|
BIN
docs/high-concurrency/images/qrcode-for-doocs.jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/high-concurrency/images/qrcode-for-yanglbme.jpg
Normal file
After Width: | Height: | Size: 70 KiB |
@ -2,25 +2,26 @@
|
||||
如果让你写一个消息队列,该如何进行架构设计?说一下你的思路。
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实聊到这个问题,一般面试官要考察两块:
|
||||
|
||||
- 你有没有对某一个消息队列做过较为深入的原理的了解,或者从整体了解把握住一个消息队列的架构原理。
|
||||
- 看看你的设计能力,给你一个常见的系统,就是消息队列系统,看看你能不能从全局把握一下整体架构设计,给出一些关键点出来。
|
||||
* 你有没有对某一个消息队列做过较为深入的原理的了解,或者从整体了解把握住一个消息队列的架构原理。
|
||||
* 看看你的设计能力,给你一个常见的系统,就是消息队列系统,看看你能不能从全局把握一下整体架构设计,给出一些关键点出来。
|
||||
|
||||
说实话,问类似问题的时候,大部分人基本都会蒙,因为平时从来没有思考过类似的问题,**大多数人就是平时埋头用,从来不去思考背后的一些东西**。类似的问题,比如,如果让你来设计一个 Spring 框架你会怎么做?如果让你来设计一个 Dubbo 框架你会怎么做?如果让你来设计一个 MyBatis 框架你会怎么做?
|
||||
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
其实回答这类问题,说白了,不求你看过那技术的源码,起码你要大概知道那个技术的基本原理、核心组成部分、基本架构构成,然后参照一些开源的技术把一个系统设计出来的思路说一下就好。
|
||||
|
||||
比如说这个消息队列系统,我们从以下几个角度来考虑一下:
|
||||
|
||||
- 首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
|
||||
* 首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
|
||||
|
||||
- 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。
|
||||
* 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。
|
||||
|
||||
- 其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。
|
||||
* 其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。
|
||||
|
||||
- 能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。
|
||||
* 能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。
|
||||
|
||||
mq 肯定是很复杂的,面试官问你这个问题,其实是个开放题,他就是看看你有没有从架构角度整体构思和设计的思维以及能力。确实这个问题可以刷掉一大批人,因为大部分人平时不思考这些东西。
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
**候选人**:你好。
|
||||
|
||||
(面试官在你的简历上面看到了,呦,有个亮点,你在项目里用过 `MQ`,比如说你用过 `ActiveMQ`)
|
||||
(面试官在你的简历上面看到了,呦,有个亮点,你在项目里用过 `MQ` ,比如说你用过 `ActiveMQ` )
|
||||
|
||||
**面试官**:你在系统里用过消息队列吗?(面试官在随和的语气中展开了面试)
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
(部分同学在这里会进入一个误区,就是你仅仅就是知道以及回答你们是怎么用这个消息队列的,用这个消息队列来干了个什么事情?)
|
||||
|
||||
**面试官**:那你们为什么使用消息队列啊?你的订单系统不发送消息到 `MQ`,直接订单系统调用库存系统一个接口,咔嚓一下,直接就调用成功,库存不也就更新了。
|
||||
**面试官**:那你们为什么使用消息队列啊?你的订单系统不发送消息到 `MQ` ,直接订单系统调用库存系统一个接口,咔嚓一下,直接就调用成功,库存不也就更新了。
|
||||
|
||||
**候选人**:额。。。(楞了一下,为什么?我没怎么仔细想过啊,老大让用就用了),硬着头皮胡言乱语了几句。
|
||||
|
||||
@ -30,11 +30,11 @@
|
||||
|
||||
(面试官此时心里已经更觉得你这哥儿们不行,平时都没什么思考)
|
||||
|
||||
**面试官**:`Kafka`、`ActiveMQ`、`RabbitMQ`、`RocketMQ` 都有什么区别?
|
||||
**面试官**: `Kafka` 、 `ActiveMQ` 、 `RabbitMQ` 、 `RocketMQ` 都有什么区别?
|
||||
|
||||
(面试官问你这个问题,就是说,绕过比较虚的话题,直接看看你对各种 `MQ` 中间件是否了解,是否做过功课,是否做过调研)
|
||||
|
||||
**候选人**:我们就用过 `ActiveMQ`,所以别的没用过。。。区别,也不太清楚。。。
|
||||
**候选人**:我们就用过 `ActiveMQ` ,所以别的没用过。。。区别,也不太清楚。。。
|
||||
|
||||
(面试官此时更是觉得你这哥儿们平时就是瞎用,根本就没什么思考,觉得不行)
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
|
||||
**面试官**:如何保证消息不被重复消费啊?如何保证消费的时候是幂等的啊?
|
||||
|
||||
**候选人**:啥?(`MQ` 不就是写入&消费就可以了,哪来这么多问题)
|
||||
**候选人**:啥?( `MQ` 不就是写入&消费就可以了,哪来这么多问题)
|
||||
|
||||
**面试官**:如何保证消息的可靠性传输啊?要是消息丢失了怎么办啊?
|
||||
|
||||
@ -64,6 +64,6 @@
|
||||
|
||||
---
|
||||
|
||||
这其实是面试官的一种面试风格,就是说面试官的问题不是发散的,而是从一个小点慢慢铺开。比如说面试官可能会跟你聊聊高并发话题,就这个话题里面跟你聊聊缓存、`MQ` 等等东西,**由浅入深,一步步深挖**。
|
||||
这其实是面试官的一种面试风格,就是说面试官的问题不是发散的,而是从一个小点慢慢铺开。比如说面试官可能会跟你聊聊高并发话题,就这个话题里面跟你聊聊缓存、 `MQ` 等等东西,**由浅入深,一步步深挖**。
|
||||
|
||||
其实上面是一个非常典型的关于消息队列的技术考察过程,好的面试官一定是从你做过的某一个点切入,然后层层展开深入考察,一个接一个问,直到把这个技术点刨根问底,问到最底层。
|
||||
其实上面是一个非常典型的关于消息队列的技术考察过程,好的面试官一定是从你做过的某一个点切入,然后层层展开深入考察,一个接一个问,直到把这个技术点刨根问底,问到最底层。
|
||||
|
@ -2,26 +2,31 @@
|
||||
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
你看这问法,其实本质针对的场景,都是说,可能你的消费端出了问题,不消费了;或者消费的速度极其慢。接着就坑爹了,可能你的消息队列集群的磁盘都快写满了,都没人消费,这个时候怎么办?或者是这整个就积压了几个小时,你这个时候怎么办?或者是你积压的时间太长了,导致比如 RabbitMQ 设置了消息过期时间后就没了怎么办?
|
||||
|
||||
所以就这事儿,其实线上挺常见的,一般不出,一出就是大 case。一般常见于,举个例子,消费端每次消费之后要写 mysql,结果 mysql 挂了,消费端 hang 那儿了,不动了;或者是消费端出了个什么岔子,导致消费速度极其慢。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
关于这个事儿,我们一个一个来梳理吧,先假设一个场景,我们现在消费端出故障了,然后大量消息在 mq 里积压,现在出事故了,慌了。
|
||||
|
||||
### 大量消息在 mq 里积压了几个小时了还没解决
|
||||
|
||||
几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11 点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer 的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。
|
||||
|
||||
一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。
|
||||
|
||||
一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:
|
||||
- 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
|
||||
- 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
|
||||
- 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,**消费之后不做耗时的处理**,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
|
||||
- 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
|
||||
- 等快速消费完积压数据之后,**得恢复原先部署的架构**,**重新**用原先的 consumer 机器来消费消息。
|
||||
|
||||
* 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
|
||||
* 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
|
||||
* 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,**消费之后不做耗时的处理**,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
|
||||
* 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
|
||||
* 等快速消费完积压数据之后,**得恢复原先部署的架构**,**重新**用原先的 consumer 机器来消费消息。
|
||||
|
||||
### mq 中的消息过期失效了
|
||||
|
||||
假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是**大量的数据会直接搞丢**。
|
||||
|
||||
这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是**批量重导**,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。
|
||||
@ -29,4 +34,5 @@
|
||||
假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
|
||||
|
||||
### mq 都快写满了
|
||||
|
||||
如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,**消费一个丢弃一个,都不要了**,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
|
||||
|
@ -2,16 +2,19 @@
|
||||
你们有没有做 MySQL 读写分离?如何实现 MySQL 的读写分离?MySQL 主从复制原理的是啥?如何解决 MySQL 主从同步的延时问题?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
高并发这个阶段,肯定是需要做读写分离的,啥意思?因为实际上大部分的互联网公司,一些网站,或者是 app,其实都是读多写少。所以针对这个情况,就是写一个主库,但是主库挂多个从库,然后从多个从库来读,那不就可以支撑更高的读并发压力了吗?
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 如何实现 MySQL 的读写分离?
|
||||
其实很简单,就是基于主从复制架构,简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。
|
||||
|
||||
### MySQL 主从复制原理的是啥?
|
||||
|
||||
主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay 中继日志中。接着从库中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL,这样就可以保证自己跟主库的数据是一样的。
|
||||
|
||||

|
||||

|
||||
|
||||
这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是**有延时**的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。
|
||||
|
||||
@ -24,18 +27,22 @@
|
||||
所谓**并行复制**,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后**并行重放不同库的日志**,这是库级别的并行。
|
||||
|
||||
### MySQL 主从同步延时问题(精华)
|
||||
|
||||
以前线上确实处理过因为主从同步延时问题而导致的线上的 bug,属于小型的生产事故。
|
||||
|
||||
是这个么场景。有个同学是这样写代码逻辑的。先插入一条数据,再把它查出来,然后更新这条数据。在生产环境高峰期,写并发达到了 2000/s,这个时候,主从复制延时大概是在小几十毫秒。线上会发现,每天总有那么一些数据,我们期望更新一些重要的数据状态,但在高峰期时候却没更新。用户跟客服反馈,而客服就会反馈给我们。
|
||||
|
||||
我们通过 MySQL 命令:
|
||||
```sql
|
||||
|
||||
``` sql
|
||||
show status
|
||||
```
|
||||
查看 `Seconds_Behind_Master`,可以看到从库复制主库的数据落后了几 ms。
|
||||
|
||||
查看 `Seconds_Behind_Master` ,可以看到从库复制主库的数据落后了几 ms。
|
||||
|
||||
一般来说,如果主从延迟较为严重,有以下解决方案:
|
||||
- 分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。
|
||||
- 打开 MySQL 支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发达到了 2000/s,并行复制还是没意义。
|
||||
- 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。
|
||||
- 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询**设置直连主库**。**不推荐**这种方法,你要是这么搞,读写分离的意义就丧失了。
|
||||
|
||||
* 分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。
|
||||
* 打开 MySQL 支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发达到了 2000/s,并行复制还是没意义。
|
||||
* 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。
|
||||
* 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询**设置直连主库**。**不推荐**这种方法,你要是这么搞,读写分离的意义就丧失了。
|
||||
|
@ -1,47 +1,57 @@
|
||||
## 面试题
|
||||
了解什么是 redis 的雪崩、穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透?
|
||||
了解什么是 Redis 的雪崩、穿透和击穿?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实这是问到缓存必问的,因为缓存雪崩和穿透,是缓存最大的两个问题,要么不出现,一旦出现就是致命性的问题,所以面试官一定会问你。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 缓存雪崩
|
||||
对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
|
||||
|
||||
这就是缓存雪崩。
|
||||
|
||||

|
||||

|
||||
|
||||
大约在 3 年前,国内比较知名的一个互联网公司,曾因为缓存事故,导致雪崩,后台系统全部崩溃,事故从当天下午持续到晚上凌晨 3~4 点,公司损失了几千万。
|
||||
|
||||
缓存雪崩的事前事中事后的解决方案如下。
|
||||
- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
|
||||
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
|
||||
- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
|
||||
缓存雪崩的事前事中事后的解决方案如下:
|
||||
|
||||

|
||||
* 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
|
||||
* 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
|
||||
* 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
|
||||
|
||||
用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。
|
||||

|
||||
|
||||
限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?**走降级**!可以返回一些默认的值,或者友情提示,或者空白的值。
|
||||
用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 Redis。如果 ehcache 和 Redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 Redis 中。
|
||||
|
||||
限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?**走降级**!可以返回一些默认的值,或者友情提示,或者空值。
|
||||
|
||||
好处:
|
||||
- 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
|
||||
- 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。
|
||||
- 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。
|
||||
|
||||
* 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
|
||||
* 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。
|
||||
* 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来了。
|
||||
|
||||
### 缓存穿透
|
||||
|
||||
对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。
|
||||
|
||||
黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
|
||||
|
||||
举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“**视缓存于无物**”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
|
||||
|
||||

|
||||

|
||||
|
||||
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 `set -999 UNKNOWN`。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
|
||||
解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 `set -999 UNKNOWN` 。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
|
||||
|
||||
### 缓存击穿
|
||||
|
||||
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
|
||||
|
||||
解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
|
||||
不同场景下的解决方式可如下:
|
||||
|
||||
* 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
|
||||
* 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
|
||||
* 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
|
||||
|
@ -1,16 +1,18 @@
|
||||
## 面试题
|
||||
redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗?
|
||||
Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
这个也是线上非常常见的一个问题,就是**多客户端同时并发写**一个 key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。
|
||||
|
||||
而且 redis 自己就有天然解决这个问题的 CAS 类的乐观锁方案。
|
||||
而且 Redis 自己就有天然解决这个问题的 CAS 类的乐观锁方案。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
某个时刻,多个系统实例都去更新某个 key。可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。
|
||||
|
||||

|
||||

|
||||
|
||||
你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。
|
||||
|
||||
每次要**写之前,先判断**一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
|
||||
每次要**写之前,先判断**一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
|
||||
|
@ -1,79 +1,88 @@
|
||||
## 面试题
|
||||
redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
|
||||
Redis 集群模式的工作原理能说一下么?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
|
||||
|
||||
## 面试官心理分析
|
||||
在前几年,redis 如果要搞几个节点,每个节点存储一部分的数据,得**借助一些中间件**来实现,比如说有 `codis`,或者 `twemproxy`,都有。有一些 redis 中间件,你读写 redis 中间件,redis 中间件负责将你的数据分布式存储在多台机器上的 redis 实例中。
|
||||
|
||||
这两年,redis 不断在发展,redis 也不断有新的版本,现在的 redis 集群模式,可以做到在多台机器上,部署多个 redis 实例,每个实例存储一部分的数据,同时每个 redis 主实例可以挂 redis 从实例,自动确保说,如果 redis 主实例挂了,会自动切换到 redis 从实例上来。
|
||||
在前几年,Redis 如果要搞几个节点,每个节点存储一部分的数据,得**借助一些中间件**来实现,比如说有 `codis` ,或者 `twemproxy` ,都有。有一些 Redis 中间件,你读写 Redis 中间件,Redis 中间件负责将你的数据分布式存储在多台机器上的 Redis 实例中。
|
||||
|
||||
现在 redis 的新版本,大家都是用 redis cluster 的,也就是 redis 原生支持的 redis 集群模式,那么面试官肯定会就 redis cluster 对你来个几连炮。要是你没用过 redis cluster,正常,以前很多人用 codis 之类的客户端来支持集群,但是起码你得研究一下 redis cluster 吧。
|
||||
这两年,Redis 不断在发展,Redis 也不断有新的版本,现在的 Redis 集群模式,可以做到在多台机器上,部署多个 Redis 实例,每个实例存储一部分的数据,同时每个 Redis 主实例可以挂 Redis 从实例,自动确保说,如果 Redis 主实例挂了,会自动切换到 Redis 从实例上来。
|
||||
|
||||
如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个 G,单机就足够了,可以使用 replication,一个 master 多个 slaves,要几个 slave 跟你要求的读吞吐量有关,然后自己搭建一个 sentinel 集群去保证 redis 主从架构的高可用性。
|
||||
现在 Redis 的新版本,大家都是用 Redis cluster 的,也就是 Redis 原生支持的 Redis 集群模式,那么面试官肯定会就 Redis cluster 对你来个几连炮。要是你没用过 Redis cluster,正常,以前很多人用 codis 之类的客户端来支持集群,但是起码你得研究一下 Redis cluster 吧。
|
||||
|
||||
redis cluster,主要是针对**海量数据+高并发+高可用**的场景。redis cluster 支撑 N 个 redis master node,每个 master node 都可以挂载多个 slave node。这样整个 redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。
|
||||
如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个 G,单机就足够了,可以使用 replication,一个 master 多个 slaves,要几个 slave 跟你要求的读吞吐量有关,然后自己搭建一个 sentinel 集群去保证 Redis 主从架构的高可用性。
|
||||
|
||||
Redis cluster,主要是针对**海量数据+高并发+高可用**的场景。Redis cluster 支撑 N 个 Redis master node,每个 master node 都可以挂载多个 slave node。这样整个 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。
|
||||
|
||||
## 面试题剖析
|
||||
### redis cluster 介绍
|
||||
- 自动将数据进行分片,每个 master 上放一部分数据
|
||||
- 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的
|
||||
|
||||
在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。
|
||||
### Redis cluster 介绍
|
||||
|
||||
16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,`gossip` 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
|
||||
* 自动将数据进行分片,每个 master 上放一部分数据
|
||||
* 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的
|
||||
|
||||
在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。
|
||||
|
||||
16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议, `gossip` 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
|
||||
|
||||
### 节点间的内部通信机制
|
||||
|
||||
#### 基本通信原理
|
||||
集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。
|
||||
集群元数据的维护有两种方式:集中式、Gossip 协议。Redis cluster 节点间采用 gossip 协议进行通信。
|
||||
|
||||
**集中式**是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 `storm`。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。
|
||||
**集中式**是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 `storm` 。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。
|
||||
|
||||

|
||||

|
||||
|
||||
redis 维护集群元数据采用另一个方式, `gossip` 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
|
||||
Redis 维护集群元数据采用另一个方式, `gossip` 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
|
||||
|
||||

|
||||

|
||||
|
||||
**集中式**的**好处**在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;**不好**在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
|
||||
|
||||
gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。
|
||||
|
||||
- 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口。每个节点每隔一段时间都会往另外几个节点发送 `ping` 消息,同时其它几个节点接收到 `ping` 之后返回 `pong`。
|
||||
* 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口。每个节点每隔一段时间都会往另外几个节点发送 `ping` 消息,同时其它几个节点接收到 `ping` 之后返回 `pong` 。
|
||||
|
||||
- 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。
|
||||
* 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。
|
||||
|
||||
#### gossip 协议
|
||||
gossip 协议包含多种消息,包含 `ping`,`pong`,`meet`,`fail` 等等。
|
||||
|
||||
- meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。
|
||||
gossip 协议包含多种消息,包含 `ping` , `pong` , `meet` , `fail` 等等。
|
||||
|
||||
```bash
|
||||
redis-trib.rb add-node
|
||||
* meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。
|
||||
|
||||
``` bash
|
||||
Redis-trib.rb add-node
|
||||
```
|
||||
|
||||
其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。
|
||||
|
||||
- ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。
|
||||
- pong:返回 ping 和 meeet,包含自己的状态和其它信息,也用于信息广播和更新。
|
||||
- fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。
|
||||
* ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。
|
||||
* pong:返回 ping 和 meeet,包含自己的状态和其它信息,也用于信息广播和更新。
|
||||
* fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。
|
||||
|
||||
#### ping 消息深入
|
||||
|
||||
ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。
|
||||
|
||||
每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2`,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 `cluster_node_timeout` 可以调节,如果调得比较大,那么会降低 ping 的频率。
|
||||
每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2` ,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 `cluster_node_timeout` 可以调节,如果调得比较大,那么会降低 ping 的频率。
|
||||
|
||||
每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 `3` 个其它节点的信息,最多包含 `总节点数减 2` 个其它节点的信息。
|
||||
|
||||
### 分布式寻址算法
|
||||
- hash 算法(大量缓存重建)
|
||||
- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
|
||||
- redis cluster 的 hash slot 算法
|
||||
|
||||
* hash 算法(大量缓存重建)
|
||||
* 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
|
||||
* Redis cluster 的 hash slot 算法
|
||||
|
||||
#### hash 算法
|
||||
|
||||
来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致**大部分的请求过来,全部无法拿到有效的缓存**,导致大量的流量涌入数据库。
|
||||
|
||||

|
||||

|
||||
|
||||
#### 一致性 hash 算法
|
||||
|
||||
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
|
||||
|
||||
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环**顺时针“行走”**,遇到的第一个 master 节点就是 key 所在位置。
|
||||
@ -82,38 +91,44 @@ ping 时要携带一些元数据,如果很频繁,可能会加重网络负担
|
||||
|
||||
燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成**缓存热点**的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
|
||||
|
||||

|
||||

|
||||
|
||||
#### redis cluster 的 hash slot 算法
|
||||
redis cluster 有固定的 `16384` 个 hash slot,对每个 `key` 计算 `CRC16` 值,然后对 `16384` 取模,可以获取 key 对应的 hash slot。
|
||||
#### Redis cluster 的 hash slot 算法
|
||||
|
||||
redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 `hash tag` 来实现。
|
||||
Redis cluster 有固定的 `16384` 个 hash slot,对每个 `key` 计算 `CRC16` 值,然后对 `16384` 取模,可以获取 key 对应的 hash slot。
|
||||
|
||||
Redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 `hash tag` 来实现。
|
||||
|
||||
任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。
|
||||
|
||||

|
||||

|
||||
|
||||
### redis cluster 的高可用与主备切换原理
|
||||
redis cluster 的高可用的原理,几乎跟哨兵是类似的。
|
||||
### Redis cluster 的高可用与主备切换原理
|
||||
|
||||
Redis cluster 的高可用的原理,几乎跟哨兵是类似的。
|
||||
|
||||
#### 判断节点宕机
|
||||
如果一个节点认为另外一个节点宕机,那么就是 `pfail`,**主观宕机**。如果多个节点都认为另外一个节点宕机了,那么就是 `fail`,**客观宕机**,跟哨兵的原理几乎一样,sdown,odown。
|
||||
|
||||
在 `cluster-node-timeout` 内,某个节点一直没有返回 `pong`,那么就被认为 `pfail`。
|
||||
如果一个节点认为另外一个节点宕机,那么就是 `pfail` ,**主观宕机**。如果多个节点都认为另外一个节点宕机了,那么就是 `fail` ,**客观宕机**,跟哨兵的原理几乎一样,sdown,odown。
|
||||
|
||||
如果一个节点认为某个节点 `pfail` 了,那么会在 `gossip ping` 消息中,`ping` 给其他节点,如果**超过半数**的节点都认为 `pfail` 了,那么就会变成 `fail`。
|
||||
在 `cluster-node-timeout` 内,某个节点一直没有返回 `pong` ,那么就被认为 `pfail` 。
|
||||
|
||||
如果一个节点认为某个节点 `pfail` 了,那么会在 `gossip ping` 消息中, `ping` 给其他节点,如果**超过半数**的节点都认为 `pfail` 了,那么就会变成 `fail` 。
|
||||
|
||||
#### 从节点过滤
|
||||
|
||||
对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。
|
||||
|
||||
检查每个 slave node 与 master node 断开连接的时间,如果超过了 `cluster-node-timeout * cluster-slave-validity-factor`,那么就**没有资格**切换成 `master`。
|
||||
检查每个 slave node 与 master node 断开连接的时间,如果超过了 `cluster-node-timeout * cluster-slave-validity-factor` ,那么就**没有资格**切换成 `master` 。
|
||||
|
||||
#### 从节点选举
|
||||
|
||||
每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。
|
||||
|
||||
所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node`(N/2 + 1)`都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。
|
||||
所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node `(N/2 + 1)` 都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。
|
||||
|
||||
从节点执行主备切换,从节点切换为主节点。
|
||||
|
||||
#### 与哨兵比较
|
||||
整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
|
||||
|
||||
整个流程跟哨兵相比,非常类似,所以说,Redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
|
||||
|
@ -2,17 +2,21 @@
|
||||
如何保证缓存与数据库的双写一致性?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统**不是严格要求** “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:**读请求和写请求串行化**,串到一个**内存队列**里去。
|
||||
|
||||
串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
|
||||
|
||||
### Cache Aside Pattern
|
||||
|
||||
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
|
||||
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
|
||||
- 更新的时候,**先更新数据库,然后再删除缓存**。
|
||||
|
||||
* 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
|
||||
* 更新的时候,**先更新数据库,然后再删除缓存**。
|
||||
|
||||
**为什么是删除缓存,而不是更新缓存?**
|
||||
|
||||
@ -27,13 +31,15 @@
|
||||
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都把里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。
|
||||
|
||||
### 最初级的缓存不一致问题及解决方案
|
||||
|
||||
问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
|
||||
|
||||

|
||||

|
||||
|
||||
解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。
|
||||
|
||||
### 比较复杂的数据不一致问题分析
|
||||
|
||||
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,**查到了修改前的旧数据**,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了...
|
||||
|
||||
**为什么上亿流量高并发场景下,缓存会出现这个问题?**
|
||||
@ -54,7 +60,7 @@
|
||||
|
||||
高并发的场景下,该解决方案要注意的问题:
|
||||
|
||||
- 读请求长时阻塞
|
||||
* 读请求长时阻塞
|
||||
|
||||
由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。
|
||||
|
||||
@ -74,18 +80,22 @@
|
||||
|
||||
经过刚才简单的测算,我们知道,单机支撑的写 QPS 在几百是没问题的,如果写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每个机器 20 个队列。
|
||||
|
||||
- 读请求并发量过高
|
||||
* 读请求并发量过高
|
||||
|
||||
这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。
|
||||
|
||||
但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。
|
||||
|
||||
- 多服务实例部署的请求路由
|
||||
* 多服务实例部署的请求路由
|
||||
|
||||
可能这个服务部署了多个实例,那么必须**保证**说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器**路由到相同的服务实例上**。
|
||||
|
||||
比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。
|
||||
|
||||
- 热点商品的路由问题,导致请求的倾斜
|
||||
* 热点商品的路由问题,导致请求的倾斜
|
||||
|
||||
万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。
|
||||
|
||||
---
|
||||
|
||||
关于这道面试题的详细讨论,见 [#54](https://github.com/doocs/advanced-java/issues/54)。
|
||||
|
@ -1,40 +1,49 @@
|
||||
## 面试题
|
||||
redis 都有哪些数据类型?分别在哪些场景下使用比较合适?
|
||||
Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
除非是面试官感觉看你简历,是工作 3 年以内的比较初级的同学,可能对技术没有很深入的研究,面试官才会问这类问题。否则,在宝贵的面试时间里,面试官实在不想多问。
|
||||
|
||||
其实问这个问题,主要有两个原因:
|
||||
- 看看你到底有没有全面的了解 redis 有哪些功能,一般怎么来用,啥场景用什么,就怕你别就会最简单的 KV 操作;
|
||||
- 看看你在实际项目里都怎么玩儿过 redis。
|
||||
|
||||
* 看看你到底有没有全面的了解 Redis 有哪些功能,一般怎么来用,啥场景用什么,就怕你别就会最简单的 KV 操作;
|
||||
* 看看你在实际项目里都怎么玩儿过 Redis。
|
||||
|
||||
要是你回答的不好,没说出几种数据类型,也没说什么场景,你完了,面试官对你印象肯定不好,觉得你平时就是做个简单的 set 和 get。
|
||||
|
||||
## 面试题剖析
|
||||
redis 主要有以下几种数据类型:
|
||||
- string
|
||||
- hash
|
||||
- list
|
||||
- set
|
||||
- sorted set
|
||||
|
||||
### string
|
||||
Redis 主要有以下几种数据类型:
|
||||
|
||||
* Strings
|
||||
* Hashes
|
||||
* Lists
|
||||
* Sets
|
||||
* Sorted Sets
|
||||
|
||||
> Redis 除了这 5 种数据类型之外,还有 Bitmaps、HyperLogLogs、Streams 等。
|
||||
|
||||
### Strings
|
||||
|
||||
这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。
|
||||
```bash
|
||||
|
||||
``` bash
|
||||
set college szu
|
||||
```
|
||||
|
||||
### hash
|
||||
这个是类似 map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是**这个对象没嵌套其他的对象**)给缓存在 redis 里,然后每次读写缓存的时候,可以就操作 hash 里的**某个字段**。
|
||||
### Hashes
|
||||
|
||||
```bash
|
||||
这个是类似 map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是**这个对象没嵌套其他的对象**)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 hash 里的**某个字段**。
|
||||
|
||||
``` bash
|
||||
hset person name bingo
|
||||
hset person age 20
|
||||
hset person id 1
|
||||
hget person name
|
||||
```
|
||||
|
||||
```json
|
||||
``` json
|
||||
person = {
|
||||
"name": "bingo",
|
||||
"age": 20,
|
||||
@ -42,19 +51,22 @@ person = {
|
||||
}
|
||||
```
|
||||
|
||||
### list
|
||||
list 是有序列表,这个可以玩儿出很多花样。
|
||||
### Lists
|
||||
|
||||
Lists 是有序列表,这个可以玩儿出很多花样。
|
||||
|
||||
比如可以通过 list 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。
|
||||
|
||||
比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 list 实现分页查询,这个是很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
|
||||
```bash
|
||||
比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 list 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
|
||||
|
||||
``` bash
|
||||
# 0开始位置,-1结束位置,结束位置为-1时,表示列表的最后一个位置,即查看所有。
|
||||
lrange mylist 0 -1
|
||||
```
|
||||
|
||||
比如可以搞个简单的消息队列,从 list 头怼进去,从 list 尾巴那里弄出来。
|
||||
```bash
|
||||
|
||||
``` bash
|
||||
lpush mylist 1
|
||||
lpush mylist 2
|
||||
lpush mylist 3 4 5
|
||||
@ -63,15 +75,17 @@ lpush mylist 3 4 5
|
||||
rpop mylist
|
||||
```
|
||||
|
||||
### set
|
||||
set 是无序集合,自动去重。
|
||||
### Sets
|
||||
|
||||
直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 redis 进行全局的 set 去重。
|
||||
Sets 是无序集合,自动去重。
|
||||
|
||||
直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 Redis 进行全局的 set 去重。
|
||||
|
||||
可以基于 set 玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧。
|
||||
|
||||
把两个大 V 的粉丝都放在两个 set 中,对两个 set 做交集。
|
||||
```bash
|
||||
|
||||
``` bash
|
||||
#-------操作一个set-------
|
||||
# 添加元素
|
||||
sadd mySet 1
|
||||
@ -106,9 +120,11 @@ sunion yourSet mySet
|
||||
sdiff yourSet mySet
|
||||
```
|
||||
|
||||
### sorted set
|
||||
sorted set 是排序的 set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。
|
||||
```bash
|
||||
### Sorted Sets
|
||||
|
||||
Sorted Sets 是排序的 set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。
|
||||
|
||||
``` bash
|
||||
zadd board 85 zhangsan
|
||||
zadd board 72 lisi
|
||||
zadd board 96 wangwu
|
||||
@ -119,4 +135,4 @@ zrevrange board 0 3
|
||||
|
||||
# 获取某用户的排名
|
||||
zrank board zhaoliu
|
||||
```
|
||||
```
|
||||
|
@ -1,53 +1,59 @@
|
||||
## 面试题
|
||||
redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?
|
||||
Redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?
|
||||
|
||||
## 面试官心理分析
|
||||
如果你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想当然的认为写进 redis 的数据就一定会存在,后面导致系统各种 bug,谁来负责?
|
||||
|
||||
如果你连这个问题都不知道,上来就懵了,回答不出来,那线上你写代码的时候,想当然的认为写进 Redis 的数据就一定会存在,后面导致系统各种 bug,谁来负责?
|
||||
|
||||
常见的有两个问题:
|
||||
- 往 redis 写入的数据怎么没了?
|
||||
|
||||
可能有同学会遇到,在生产环境的 redis 经常会丢掉一些数据,写进去了,过一会儿可能就没了。我的天,同学,你问这个问题就说明 redis 你就没用对啊。redis 是缓存,你给当存储了是吧?
|
||||
* 往 Redis 写入的数据怎么没了?
|
||||
|
||||
啥叫缓存?用内存当缓存。内存是无限的吗,内存是很宝贵而且是有限的,磁盘是廉价而且是大量的。可能一台机器就几十个 G 的内存,但是可以有几个 T 的硬盘空间。redis 主要是基于内存来进行高性能、高并发的读写操作的。
|
||||
可能有同学会遇到,在生产环境的 Redis 经常会丢掉一些数据,写进去了,过一会儿可能就没了。我的天,同学,你问这个问题就说明 Redis 你就没用对啊。Redis 是缓存,你给当存储了是吧?
|
||||
|
||||
那既然内存是有限的,比如 redis 就只能用 10G,你要是往里面写了 20G 的数据,会咋办?当然会干掉 10G 的数据,然后就保留 10G 的数据了。那干掉哪些数据?保留哪些数据?当然是干掉不常用的数据,保留常用的数据了。
|
||||
啥叫缓存?用内存当缓存。内存是无限的吗,内存是很宝贵而且是有限的,磁盘是廉价而且是大量的。可能一台机器就几十个 G 的内存,但是可以有几个 T 的硬盘空间。Redis 主要是基于内存来进行高性能、高并发的读写操作的。
|
||||
|
||||
- 数据明明过期了,怎么还占用着内存?
|
||||
那既然内存是有限的,比如 Redis 就只能用 10G,你要是往里面写了 20G 的数据,会咋办?当然会干掉 10G 的数据,然后就保留 10G 的数据了。那干掉哪些数据?保留哪些数据?当然是干掉不常用的数据,保留常用的数据了。
|
||||
|
||||
这是由 redis 的过期策略来决定。
|
||||
* 数据明明过期了,怎么还占用着内存?
|
||||
|
||||
这是由 Redis 的过期策略来决定。
|
||||
|
||||
## 面试题剖析
|
||||
### redis 过期策略
|
||||
redis 过期策略是:**定期删除+惰性删除**。
|
||||
|
||||
所谓**定期删除**,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
|
||||
### Redis 过期策略
|
||||
Redis 过期策略是:**定期删除+惰性删除**。
|
||||
|
||||
假设 redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的**灾难**。实际上 redis 是每隔 100ms **随机抽取**一些 key 来检查和删除的。
|
||||
所谓**定期删除**,指的是 Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
|
||||
|
||||
但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
|
||||
假设 Redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 Redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的**灾难**。实际上 Redis 是每隔 100ms **随机抽取**一些 key 来检查和删除的。
|
||||
|
||||
但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,Redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
|
||||
|
||||
> 获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。
|
||||
|
||||
但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整?
|
||||
但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 Redis 内存块耗尽了,咋整?
|
||||
|
||||
答案是:**走内存淘汰机制**。
|
||||
|
||||
### 内存淘汰机制
|
||||
redis 内存淘汰机制有以下几个:
|
||||
- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
|
||||
- **allkeys-lru**:当内存不足以容纳新写入数据时,在**键空间**中,移除最近最少使用的 key(这个是**最常用**的)。
|
||||
- allkeys-random:当内存不足以容纳新写入数据时,在**键空间**中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
|
||||
- volatile-lru:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,移除最近最少使用的 key(这个一般不太合适)。
|
||||
- volatile-random:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,**随机移除**某个 key。
|
||||
- volatile-ttl:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,有**更早过期时间**的 key 优先移除。
|
||||
|
||||
Redis 内存淘汰机制有以下几个:
|
||||
|
||||
* noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
|
||||
* **allkeys-lru**:当内存不足以容纳新写入数据时,在**键空间**中,移除最近最少使用的 key(这个是**最常用**的)。
|
||||
* allkeys-random:当内存不足以容纳新写入数据时,在**键空间**中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
|
||||
* volatile-lru:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,移除最近最少使用的 key(这个一般不太合适)。
|
||||
* volatile-random:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,**随机移除**某个 key。
|
||||
* volatile-ttl:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,有**更早过期时间**的 key 优先移除。
|
||||
|
||||
### 手写一个 LRU 算法
|
||||
|
||||
你可以现场手写最原始的 LRU 算法,那个代码量太大了,似乎不太现实。
|
||||
|
||||
不求自己纯手工从底层开始打造出自己的 LRU,但是起码要知道如何利用已有的 JDK 数据结构实现一个 Java 版的 LRU。
|
||||
|
||||
```java
|
||||
``` java
|
||||
class LRUCache<K, V> extends LinkedHashMap<K, V> {
|
||||
private final int CACHE_SIZE;
|
||||
|
||||
@ -62,10 +68,14 @@ class LRUCache<K, V> extends LinkedHashMap<K, V> {
|
||||
CACHE_SIZE = cacheSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 钩子方法,通过put新增键值对的时候,若该方法返回true
|
||||
* 便移除该map中最老的键和值
|
||||
*/
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||||
// 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
|
||||
return size() > CACHE_SIZE;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
@ -1,40 +1,45 @@
|
||||
# Redis 主从架构
|
||||
|
||||
单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑**读高并发**的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的**读请求全部走从节点**。这样也可以很轻松实现水平扩容,**支撑读高并发**。
|
||||
单机的 Redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑**读高并发**的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的**读请求全部走从节点**。这样也可以很轻松实现水平扩容,**支撑读高并发**。
|
||||
|
||||

|
||||

|
||||
|
||||
redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发
|
||||
Redis replication -> 主从架构 -> 读写分离 -> 水平扩容支撑读高并发
|
||||
|
||||
## redis replication 的核心机制
|
||||
- redis 采用**异步方式**复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
|
||||
- 一个 master node 是可以配置多个 slave node 的;
|
||||
- slave node 也可以连接其他的 slave node;
|
||||
- slave node 做复制的时候,不会 block master node 的正常工作;
|
||||
- slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
|
||||
- slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。
|
||||
## Redis replication 的核心机制
|
||||
|
||||
* Redis 采用**异步方式**复制数据到 slave 节点,不过 Redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
|
||||
* 一个 master node 是可以配置多个 slave node 的;
|
||||
* slave node 也可以连接其他的 slave node;
|
||||
* slave node 做复制的时候,不会 block master node 的正常工作;
|
||||
* slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
|
||||
* slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。
|
||||
|
||||
注意,如果采用了主从架构,那么建议必须**开启** master node 的[持久化](/docs/high-concurrency/redis-persistence.md),不建议用 slave node 作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能一经过复制, slave node 的数据也丢了。
|
||||
|
||||
另外,master 的各种备份方案,也需要做。万一本地的所有文件丢失了,从备份中挑选一份 rdb 去恢复 master,这样才能**确保启动的时候,是有数据的**,即使采用了后续讲解的[高可用机制](/docs/high-concurrency/redis-sentinel.md),slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。
|
||||
|
||||
## redis 主从复制的核心原理
|
||||
## Redis 主从复制的核心原理
|
||||
|
||||
当启动一个 slave node 的时候,它会发送一个 `PSYNC` 命令给 master node。
|
||||
|
||||
如果这是 slave node 初次连接到 master node,那么会触发一次 `full resynchronization` 全量复制。此时 master 会启动一个后台线程,开始生成一份 `RDB` 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。`RDB` 文件生成完毕后, master 会将这个 `RDB` 发送给 slave,slave 会先**写入本地磁盘,然后再从本地磁盘加载到内存**中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。
|
||||
如果这是 slave node 初次连接到 master node,那么会触发一次 `full resynchronization` 全量复制。此时 master 会启动一个后台线程,开始生成一份 `RDB` 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。 `RDB` 文件生成完毕后, master 会将这个 `RDB` 发送给 slave,slave 会先**写入本地磁盘,然后再从本地磁盘加载到内存**中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。
|
||||
|
||||

|
||||

|
||||
|
||||
### 主从复制的断点续传
|
||||
从 redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
|
||||
|
||||
master node 会在内存中维护一个 backlog,master 和 slave 都会保存一个 replica offset 还有一个 master run id,offset 就是保存在 backlog 中的。如果 master 和 slave 网络连接断掉了,slave 会让 master 从上次 replica offset 开始继续复制,如果没有找到对应的 offset,那么就会执行一次 `resynchronization`。
|
||||
从 Redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
|
||||
|
||||
master node 会在内存中维护一个 backlog,master 和 slave 都会保存一个 replica offset 还有一个 master run id,offset 就是保存在 backlog 中的。如果 master 和 slave 网络连接断掉了,slave 会让 master 从上次 replica offset 开始继续复制,如果没有找到对应的 offset,那么就会执行一次 `resynchronization` 。
|
||||
|
||||
> 如果根据 host+ip 定位 master node,是不靠谱的,如果 master node 重启或者数据出现了变化,那么 slave node 应该根据不同的 run id 区分。
|
||||
|
||||
### 无磁盘化复制
|
||||
master 在内存中直接创建 `RDB`,然后发送给 slave,不会在自己本地落地磁盘了。只需要在配置文件中开启 `repl-diskless-sync yes` 即可。
|
||||
```bash
|
||||
|
||||
master 在内存中直接创建 `RDB` ,然后发送给 slave,不会在自己本地落地磁盘了。只需要在配置文件中开启 `repl-diskless-sync yes` 即可。
|
||||
|
||||
``` bash
|
||||
repl-diskless-sync yes
|
||||
|
||||
# 等待 5s 后再开始复制,因为要等更多 slave 重新连接过来
|
||||
@ -42,48 +47,57 @@ repl-diskless-sync-delay 5
|
||||
```
|
||||
|
||||
### 过期 key 处理
|
||||
|
||||
slave 不会过期 key,只会等待 master 过期 key。如果 master 过期了一个 key,或者通过 LRU 淘汰了一个 key,那么会模拟一条 del 命令发送给 slave。
|
||||
|
||||
## 复制的完整流程
|
||||
slave node 启动时,会在自己本地保存 master node 的信息,包括 master node 的`host`和`ip`,但是复制流程没开始。
|
||||
|
||||
slave node 启动时,会在自己本地保存 master node 的信息,包括 master node 的 `host` 和 `ip` ,但是复制流程没开始。
|
||||
|
||||
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。
|
||||
|
||||

|
||||

|
||||
|
||||
### 全量复制
|
||||
- master 执行 bgsave ,在本地生成一份 rdb 快照文件。
|
||||
- master node 将 rdb 快照文件发送给 slave node,如果 rdb 复制时间超过 60秒(repl-timeout),那么 slave node 就会认为复制失败,可以适当调大这个参数(对于千兆网卡的机器,一般每秒传输 100MB,6G 文件,很可能超过 60s)
|
||||
- master node 在生成 rdb 时,会将所有新的写命令缓存在内存中,在 slave node 保存了 rdb 之后,再将新的写命令复制给 slave node。
|
||||
- 如果在复制期间,内存缓冲区持续消耗超过 64MB,或者一次性超过 256MB,那么停止复制,复制失败。
|
||||
```bash
|
||||
|
||||
* master 执行 bgsave ,在本地生成一份 rdb 快照文件。
|
||||
* master node 将 rdb 快照文件发送给 slave node,如果 rdb 复制时间超过 60秒(repl-timeout),那么 slave node 就会认为复制失败,可以适当调大这个参数(对于千兆网卡的机器,一般每秒传输 100MB,6G 文件,很可能超过 60s)
|
||||
* master node 在生成 rdb 时,会将所有新的写命令缓存在内存中,在 slave node 保存了 rdb 之后,再将新的写命令复制给 slave node。
|
||||
* 如果在复制期间,内存缓冲区持续消耗超过 64MB,或者一次性超过 256MB,那么停止复制,复制失败。
|
||||
|
||||
``` bash
|
||||
client-output-buffer-limit slave 256MB 64MB 60
|
||||
```
|
||||
- slave node 接收到 rdb 之后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,同时**基于旧的数据版本**对外提供服务。
|
||||
- 如果 slave node 开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。
|
||||
|
||||
* slave node 接收到 rdb 之后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,同时**基于旧的数据版本**对外提供服务。
|
||||
* 如果 slave node 开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。
|
||||
|
||||
### 增量复制
|
||||
- 如果全量复制过程中,master-slave 网络连接断掉,那么 slave 重新连接 master 时,会触发增量复制。
|
||||
- master 直接从自己的 backlog 中获取部分丢失的数据,发送给 slave node,默认 backlog 就是 1MB。
|
||||
- master 就是根据 slave 发送的 psync 中的 offset 来从 backlog 中获取数据的。
|
||||
|
||||
* 如果全量复制过程中,master-slave 网络连接断掉,那么 slave 重新连接 master 时,会触发增量复制。
|
||||
* master 直接从自己的 backlog 中获取部分丢失的数据,发送给 slave node,默认 backlog 就是 1MB。
|
||||
* master 就是根据 slave 发送的 psync 中的 offset 来从 backlog 中获取数据的。
|
||||
|
||||
### heartbeat
|
||||
|
||||
主从节点互相都会发送 heartbeat 信息。
|
||||
|
||||
master 默认每隔 10秒 发送一次 heartbeat,slave node 每隔 1秒 发送一个 heartbeat。
|
||||
|
||||
### 异步复制
|
||||
|
||||
master 每次接收到写命令之后,先在内部写入数据,然后异步发送给 slave node。
|
||||
|
||||
## redis 如何才能做到高可用
|
||||
## Redis 如何才能做到高可用
|
||||
|
||||
如果系统在 365 天内,有 99.99% 的时间,都是可以哗哗对外提供服务的,那么就说系统是高可用的。
|
||||
|
||||
一个 slave 挂掉了,是不会影响可用性的,还有其它的 slave 在提供相同数据下的相同的对外的查询服务。
|
||||
|
||||
但是,如果 master node 死掉了,会怎么样?没法写数据了,写缓存的时候,全部失效了。slave node 还有什么用呢,没有 master 给它们复制数据了,系统相当于不可用了。
|
||||
|
||||
redis 的高可用架构,叫做 `failover` **故障转移**,也可以叫做主备切换。
|
||||
Redis 的高可用架构,叫做 `failover` **故障转移**,也可以叫做主备切换。
|
||||
|
||||
master node 在故障时,自动检测,并且将某个 slave node 自动切换为 master node 的过程,叫做主备切换。这个过程,实现了 redis 的主从架构下的高可用。
|
||||
master node 在故障时,自动检测,并且将某个 slave node 自动切换为 master node 的过程,叫做主备切换。这个过程,实现了 Redis 的主从架构下的高可用。
|
||||
|
||||
后面会详细说明 redis [基于哨兵的高可用性](/docs/high-concurrency/redis-sentinel.md)。
|
||||
后面会详细说明 Redis [基于哨兵的高可用性](/docs/high-concurrency/redis-sentinel.md)。
|
||||
|
@ -1,50 +1,56 @@
|
||||
## 面试题
|
||||
redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?
|
||||
Redis 的持久化有哪几种方式?不同的持久化机制都有什么优缺点?持久化机制具体底层是如何实现的?
|
||||
|
||||
## 面试官心理分析
|
||||
redis 如果仅仅只是将数据缓存在内存里面,如果 redis 宕机了再重启,内存里的数据就全部都弄丢了啊。你必须得用 redis 的持久化机制,将数据写入内存的同时,异步的慢慢的将数据写入磁盘文件里,进行持久化。
|
||||
|
||||
如果 redis 宕机重启,自动从磁盘上加载之前持久化的一些数据就可以了,也许会丢失少许数据,但是至少不会将所有数据都弄丢。
|
||||
Redis 如果仅仅只是将数据缓存在内存里面,如果 Redis 宕机了再重启,内存里的数据就全部都弄丢了啊。你必须得用 Redis 的持久化机制,将数据写入内存的同时,异步的慢慢的将数据写入磁盘文件里,进行持久化。
|
||||
|
||||
这个其实一样,针对的都是 redis 的生产环境可能遇到的一些问题,就是 redis 要是挂了再重启,内存里的数据不就全丢了?能不能重启的时候把数据给恢复了?
|
||||
如果 Redis 宕机重启,自动从磁盘上加载之前持久化的一些数据就可以了,也许会丢失少许数据,但是至少不会将所有数据都弄丢。
|
||||
|
||||
这个其实一样,针对的都是 Redis 的生产环境可能遇到的一些问题,就是 Redis 要是挂了再重启,内存里的数据不就全丢了?能不能重启的时候把数据给恢复了?
|
||||
|
||||
## 面试题剖析
|
||||
持久化主要是做灾难恢复、数据恢复,也可以归类到高可用的一个环节中去,比如你 redis 整个挂了,然后 redis 就不可用了,你要做的事情就是让 redis 变得可用,尽快变得可用。
|
||||
|
||||
重启 redis,尽快让它对外提供服务,如果没做数据备份,这时候 redis 启动了,也不可用啊,数据都没了。
|
||||
持久化主要是做灾难恢复、数据恢复,也可以归类到高可用的一个环节中去,比如你 Redis 整个挂了,然后 Redis 就不可用了,你要做的事情就是让 Redis 变得可用,尽快变得可用。
|
||||
|
||||
很可能说,大量的请求过来,缓存全部无法命中,在 redis 里根本找不到数据,这个时候就死定了,出现**缓存雪崩**问题。所有请求没有在 redis 命中,就会去 mysql 数据库这种数据源头中去找,一下子 mysql 承接高并发,然后就挂了...
|
||||
重启 Redis,尽快让它对外提供服务,如果没做数据备份,这时候 Redis 启动了,也不可用啊,数据都没了。
|
||||
|
||||
如果你把 redis 持久化做好,备份和恢复方案做到企业级的程度,那么即使你的 redis 故障了,也可以通过备份数据,快速恢复,一旦恢复立即对外提供服务。
|
||||
很可能说,大量的请求过来,缓存全部无法命中,在 Redis 里根本找不到数据,这个时候就死定了,出现**缓存雪崩**问题。所有请求没有在 Redis 命中,就会去 mysql 数据库这种数据源头中去找,一下子 mysql 承接高并发,然后就挂了...
|
||||
|
||||
### redis 持久化的两种方式
|
||||
- RDB:RDB 持久化机制,是对 redis 中的数据执行**周期性**的持久化。
|
||||
- AOF:AOF 机制对每条写入命令作为日志,以 `append-only` 的模式写入一个日志文件中,在 redis 重启的时候,可以通过**回放** AOF 日志中的写入指令来重新构建整个数据集。
|
||||
如果你把 Redis 持久化做好,备份和恢复方案做到企业级的程度,那么即使你的 Redis 故障了,也可以通过备份数据,快速恢复,一旦恢复立即对外提供服务。
|
||||
|
||||
通过 RDB 或 AOF,都可以将 redis 内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云等云服务。
|
||||
### Redis 持久化的两种方式
|
||||
|
||||
如果 redis 挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动 redis,redis 就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务。
|
||||
* RDB:RDB 持久化机制,是对 Redis 中的数据执行**周期性**的持久化。
|
||||
* AOF:AOF 机制对每条写入命令作为日志,以 `append-only` 的模式写入一个日志文件中,在 Redis 重启的时候,可以通过**回放** AOF 日志中的写入指令来重新构建整个数据集。
|
||||
|
||||
如果同时使用 RDB 和 AOF 两种持久化机制,那么在 redis 重启的时候,会使用 **AOF** 来重新构建数据,因为 AOF 中的**数据更加完整**。
|
||||
通过 RDB 或 AOF,都可以将 Redis 内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云等云服务。
|
||||
|
||||
如果 Redis 挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动 Redis,Redis 就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务。
|
||||
|
||||
如果同时使用 RDB 和 AOF 两种持久化机制,那么在 Redis 重启的时候,会使用 **AOF** 来重新构建数据,因为 AOF 中的**数据更加完整**。
|
||||
|
||||
#### RDB 优缺点
|
||||
- RDB 会生成多个数据文件,每个数据文件都代表了某一个时刻中 redis 的数据,这种多个数据文件的方式,**非常适合做冷备**,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说 Amazon 的 S3 云服务上去,在国内可以是阿里云的 ODPS 分布式存储上,以预定好的备份策略来定期备份 redis 中的数据。
|
||||
- RDB 对 redis 对外提供的读写服务,影响非常小,可以让 redis **保持高性能**,因为 redis 主进程只需要 fork 一个子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化即可。
|
||||
- 相对于 AOF 持久化机制来说,直接基于 RDB 数据文件来重启和恢复 redis 进程,更加快速。
|
||||
|
||||
- 如果想要在 redis 故障时,尽可能少的丢失数据,那么 RDB 没有 AOF 好。一般来说,RDB 数据快照文件,都是每隔 5 分钟,或者更长时间生成一次,这个时候就得接受一旦 redis 进程宕机,那么会丢失最近 5 分钟的数据。
|
||||
- RDB 每次在 fork 子进程来执行 RDB 快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。
|
||||
* RDB 会生成多个数据文件,每个数据文件都代表了某一个时刻中 Redis 的数据,这种多个数据文件的方式,**非常适合做冷备**,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说 Amazon 的 S3 云服务上去,在国内可以是阿里云的 ODPS 分布式存储上,以预定好的备份策略来定期备份 Redis 中的数据。
|
||||
* RDB 对 Redis 对外提供的读写服务,影响非常小,可以让 Redis **保持高性能**,因为 Redis 主进程只需要 fork 一个子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化即可。
|
||||
* 相对于 AOF 持久化机制来说,直接基于 RDB 数据文件来重启和恢复 Redis 进程,更加快速。
|
||||
|
||||
* 如果想要在 Redis 故障时,尽可能少的丢失数据,那么 RDB 没有 AOF 好。一般来说,RDB 数据快照文件,都是每隔 5 分钟,或者更长时间生成一次,这个时候就得接受一旦 Redis 进程宕机,那么会丢失最近 5 分钟的数据。
|
||||
* RDB 每次在 fork 子进程来执行 RDB 快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。
|
||||
|
||||
#### AOF 优缺点
|
||||
- AOF 可以更好的保护数据不丢失,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次`fsync`操作,最多丢失 1 秒钟的数据。
|
||||
- AOF 日志文件以 `append-only` 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
|
||||
- AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在 `rewrite` log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。
|
||||
- AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常**适合做灾难性的误删除的紧急恢复**。比如某人不小心用 `flushall` 命令清空了所有数据,只要这个时候后台 `rewrite` 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 `flushall` 命令给删了,然后再将该 `AOF` 文件放回去,就可以通过恢复机制,自动恢复所有数据。
|
||||
- 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。
|
||||
- AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低,因为 AOF 一般会配置成每秒 `fsync` 一次日志文件,当然,每秒一次 `fsync`,性能也还是很高的。(如果实时写入,那么 QPS 会大降,redis 性能会大大降低)
|
||||
- 以前 AOF 发生过 bug,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令日志 / merge / 回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug。不过 AOF 就是为了避免 rewrite 过程导致的 bug,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是**基于当时内存中的数据进行指令的重新构建**,这样健壮性会好很多。
|
||||
|
||||
* AOF 可以更好的保护数据不丢失,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次 `fsync` 操作,最多丢失 1 秒钟的数据。
|
||||
* AOF 日志文件以 `append-only` 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
|
||||
* AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在 `rewrite` log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。
|
||||
* AOF 日志文件的命令通过可读较强的方式进行记录,这个特性非常**适合做灾难性的误删除的紧急恢复**。比如某人不小心用 `flushall` 命令清空了所有数据,只要这个时候后台 `rewrite` 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 `flushall` 命令给删了,然后再将该 `AOF` 文件放回去,就可以通过恢复机制,自动恢复所有数据。
|
||||
* 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。
|
||||
* AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低,因为 AOF 一般会配置成每秒 `fsync` 一次日志文件,当然,每秒一次 `fsync` ,性能也还是很高的。(如果实时写入,那么 QPS 会大降,Redis 性能会大大降低)
|
||||
* 以前 AOF 发生过 bug,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令日志 / merge / 回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug。不过 AOF 就是为了避免 rewrite 过程导致的 bug,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是**基于当时内存中的数据进行指令的重新构建**,这样健壮性会好很多。
|
||||
|
||||
### RDB 和 AOF 到底该如何选择
|
||||
- 不要仅仅使用 RDB,因为那样会导致你丢失很多数据;
|
||||
- 也不要仅仅使用 AOF,因为那样有两个问题:第一,你通过 AOF 做冷备,没有 RDB 做冷备来的恢复速度更快;第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug;
|
||||
- redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
|
||||
|
||||
* 不要仅仅使用 RDB,因为那样会导致你丢失很多数据;
|
||||
* 也不要仅仅使用 AOF,因为那样有两个问题:第一,你通过 AOF 做冷备,没有 RDB 做冷备来的恢复速度更快;第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug;
|
||||
* Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
|
||||
|
@ -1,20 +1,22 @@
|
||||
## 面试题
|
||||
生产环境中的 redis 是怎么部署的?
|
||||
生产环境中的 Redis 是怎么部署的?
|
||||
|
||||
## 面试官心理分析
|
||||
看看你了解不了解你们公司的 redis 生产集群的部署架构,如果你不了解,那么确实你就很失职了,你的 redis 是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?有没有开启持久化机制确保可以进行数据恢复?线上 redis 给几个 G 的内存?设置了哪些参数?压测后你们 redis 集群承载多少 QPS?
|
||||
|
||||
看看你了解不了解你们公司的 Redis 生产集群的部署架构,如果你不了解,那么确实你就很失职了,你的 Redis 是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?有没有开启持久化机制确保可以进行数据恢复?线上 Redis 给几个 G 的内存?设置了哪些参数?压测后你们 Redis 集群承载多少 QPS?
|
||||
|
||||
兄弟,这些你必须是门儿清的,否则你确实是没好好思考过。
|
||||
|
||||
## 面试题剖析
|
||||
redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。
|
||||
|
||||
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
|
||||
Redis cluster,10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。
|
||||
|
||||
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是10g内存,一般线上生产环境,Redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
|
||||
|
||||
5 台机器对外提供读写,一共有 50g 内存。
|
||||
|
||||
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
|
||||
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。
|
||||
|
||||
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
|
||||
|
||||
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
|
||||
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
|
||||
|
@ -1,35 +1,37 @@
|
||||
# Redis 哨兵集群实现高可用
|
||||
|
||||
## 哨兵的介绍
|
||||
sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:
|
||||
|
||||
- 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
|
||||
- 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
|
||||
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
|
||||
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
|
||||
sentinel,中文名是哨兵。哨兵是 Redis 集群架构中非常重要的一个组件,主要有以下功能:
|
||||
|
||||
哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
|
||||
* 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
|
||||
* 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
|
||||
* 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
|
||||
* 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
|
||||
|
||||
- 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
|
||||
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
|
||||
哨兵用于实现 Redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
|
||||
|
||||
* 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
|
||||
* 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
|
||||
|
||||
## 哨兵的核心知识
|
||||
- 哨兵至少需要 3 个实例,来保证自己的健壮性。
|
||||
- 哨兵 + redis 主从的部署架构,是**不保证数据零丢失**的,只能保证 redis 集群的高可用性。
|
||||
- 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
|
||||
|
||||
* 哨兵至少需要 3 个实例,来保证自己的健壮性。
|
||||
* 哨兵 + Redis 主从的部署架构,是**不保证数据零丢失**的,只能保证 Redis 集群的高可用性。
|
||||
* 对于哨兵 + Redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
|
||||
|
||||
哨兵集群必须部署 2 个以上节点,如果哨兵集群仅仅部署了 2 个哨兵实例,quorum = 1。
|
||||
|
||||
```
|
||||
```
|
||||
+----+ +----+
|
||||
| M1 |---------| R1 |
|
||||
| S1 | | S2 |
|
||||
+----+ +----+
|
||||
```
|
||||
|
||||
配置 `quorum=1`,如果 master 宕机, s1 和 s2 中只要有 1 个哨兵认为 master 宕机了,就可以进行切换,同时 s1 和 s2 会选举出一个哨兵来执行故障转移。但是同时这个时候,需要 majority,也就是大多数哨兵都是运行的。
|
||||
配置 `quorum=1` ,如果 master 宕机, s1 和 s2 中只要有 1 个哨兵认为 master 宕机了,就可以进行切换,同时 s1 和 s2 会选举出一个哨兵来执行故障转移。但是同时这个时候,需要 majority,也就是大多数哨兵都是运行的。
|
||||
|
||||
```
|
||||
```
|
||||
2 个哨兵,majority=2
|
||||
3 个哨兵,majority=2
|
||||
4 个哨兵,majority=2
|
||||
@ -41,7 +43,7 @@ sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的
|
||||
|
||||
经典的 3 节点哨兵集群是这样的:
|
||||
|
||||
```
|
||||
```
|
||||
+----+
|
||||
| M1 |
|
||||
| S1 |
|
||||
@ -53,30 +55,32 @@ sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的
|
||||
+----+ +----+
|
||||
```
|
||||
|
||||
配置 `quorum=2`,如果 M1 所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 master 宕机了,然后选举出一个来执行故障转移,同时 3 个哨兵的 majority 是 2,所以还剩下的 2 个哨兵运行着,就可以允许执行故障转移。
|
||||
配置 `quorum=2` ,如果 M1 所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 master 宕机了,然后选举出一个来执行故障转移,同时 3 个哨兵的 majority 是 2,所以还剩下的 2 个哨兵运行着,就可以允许执行故障转移。
|
||||
|
||||
## redis 哨兵主备切换的数据丢失问题
|
||||
### 两种情况和导致数据丢失
|
||||
## Redis 哨兵主备切换的数据丢失问题
|
||||
|
||||
### 导致数据丢失的两种情况
|
||||
主备切换的过程,可能会导致数据丢失:
|
||||
|
||||
- 异步复制导致的数据丢失
|
||||
* 异步复制导致的数据丢失
|
||||
|
||||
因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。
|
||||
|
||||

|
||||

|
||||
|
||||
- 脑裂导致的数据丢失
|
||||
* 脑裂导致的数据丢失
|
||||
|
||||
脑裂,也就是说,某个 master 所在机器突然**脱离了正常的网络**,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会**认为** master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的**脑裂**。
|
||||
|
||||
此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
|
||||
|
||||

|
||||

|
||||
|
||||
### 数据丢失问题的解决方案
|
||||
|
||||
进行如下配置:
|
||||
|
||||
```bash
|
||||
``` bash
|
||||
min-slaves-to-write 1
|
||||
min-slaves-max-lag 10
|
||||
```
|
||||
@ -85,22 +89,24 @@ min-slaves-max-lag 10
|
||||
|
||||
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。
|
||||
|
||||
- 减少异步复制数据的丢失
|
||||
* 减少异步复制数据的丢失
|
||||
|
||||
有了 `min-slaves-max-lag` 这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低的可控范围内。
|
||||
|
||||
- 减少脑裂的数据丢失
|
||||
* 减少脑裂的数据丢失
|
||||
|
||||
如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。
|
||||
|
||||
## sdown 和 odown 转换机制
|
||||
- sdown 是主观宕机,就一个哨兵如果自己觉得一个 master 宕机了,那么就是主观宕机
|
||||
- odown 是客观宕机,如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就是客观宕机
|
||||
|
||||
* sdown 是主观宕机,就一个哨兵如果自己觉得一个 master 宕机了,那么就是主观宕机
|
||||
* odown 是客观宕机,如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就是客观宕机
|
||||
|
||||
sdown 达成的条件很简单,如果一个哨兵 ping 一个 master,超过了 `is-master-down-after-milliseconds` 指定的毫秒数之后,就主观认为 master 宕机了;如果一个哨兵在指定时间内,收到了 quorum 数量的其它哨兵也认为那个 master 是 sdown 的,那么就认为是 odown 了。
|
||||
|
||||
## 哨兵集群的自动发现机制
|
||||
哨兵互相之间的发现,是通过 redis 的 `pub/sub` 系统实现的,每个哨兵都会往 `__sentinel__:hello` 这个 channel 里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
|
||||
|
||||
哨兵互相之间的发现,是通过 Redis 的 `pub/sub` 系统实现的,每个哨兵都会往 `__sentinel__:hello` 这个 channel 里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
|
||||
|
||||
每隔两秒钟,每个哨兵都会往自己监控的某个 master+slaves 对应的 `__sentinel__:hello` channel 里**发送一个消息**,内容是自己的 host、ip 和 runid 还有对这个 master 的监控配置。
|
||||
|
||||
@ -109,29 +115,32 @@ sdown 达成的条件很简单,如果一个哨兵 ping 一个 master,超过
|
||||
每个哨兵还会跟其他哨兵交换对 `master` 的监控配置,互相进行监控配置的同步。
|
||||
|
||||
## slave 配置的自动纠正
|
||||
|
||||
哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要成为潜在的 master 候选人,哨兵会确保 slave 复制现有 master 的数据;如果 slave 连接到了一个错误的 master 上,比如故障转移之后,那么哨兵会确保它们连接到正确的 master 上。
|
||||
|
||||
## slave->master 选举算法
|
||||
|
||||
如果一个 master 被认为 odown 了,而且 majority 数量的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave 来,会考虑 slave 的一些信息:
|
||||
|
||||
- 跟 master 断开连接的时长
|
||||
- slave 优先级
|
||||
- 复制 offset
|
||||
- run id
|
||||
* 跟 master 断开连接的时长
|
||||
* slave 优先级
|
||||
* 复制 offset
|
||||
* run id
|
||||
|
||||
如果一个 slave 跟 master 断开连接的时间已经超过了 `down-after-milliseconds` 的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master。
|
||||
|
||||
```
|
||||
```
|
||||
(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
|
||||
```
|
||||
|
||||
接下来会对 slave 进行排序:
|
||||
|
||||
- 按照 slave 优先级进行排序,slave priority 越低,优先级就越高。
|
||||
- 如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高。
|
||||
- 如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。
|
||||
* 按照 slave 优先级进行排序,slave priority 越低,优先级就越高。
|
||||
* 如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高。
|
||||
* 如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。
|
||||
|
||||
## quorum 和 majority
|
||||
|
||||
每次一个哨兵要做主备切换,首先需要 quorum 数量的哨兵认为 odown,然后选举出一个哨兵来做切换,这个哨兵还需要得到 majority 哨兵的授权,才能正式执行切换。
|
||||
|
||||
如果 quorum < majority,比如 5 个哨兵,majority 就是 3,quorum 设置为 2,那么就 3 个哨兵授权就可以执行切换。
|
||||
@ -139,13 +148,15 @@ sdown 达成的条件很简单,如果一个哨兵 ping 一个 master,超过
|
||||
但是如果 quorum >= majority,那么必须 quorum 数量的哨兵都授权,比如 5 个哨兵,quorum 是 5,那么必须 5 个哨兵都同意授权,才能执行切换。
|
||||
|
||||
## configuration epoch
|
||||
哨兵会对一套 redis master+slaves 进行监控,有相应的监控的配置。
|
||||
|
||||
哨兵会对一套 Redis master+slaves 进行监控,有相应的监控的配置。
|
||||
|
||||
执行切换的那个哨兵,会从要切换到的新 master(salve->master)那里得到一个 configuration epoch,这就是一个 version 号,每次切换的 version 号都必须是唯一的。
|
||||
|
||||
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待 failover-timeout 时间,然后接替继续执行切换,此时会重新获取一个新的 configuration epoch,作为新的 version 号。
|
||||
|
||||
## configuration 传播
|
||||
|
||||
哨兵完成切换之后,会在自己本地更新生成最新的 master 配置,然后同步给其他的哨兵,就是通过之前说的 `pub/sub` 消息机制。
|
||||
|
||||
这里之前的 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,新的 master 配置是跟着新的 version 号的。其他的哨兵都是根据版本号的大小来更新自己的 master 配置的。
|
||||
这里之前的 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,新的 master 配置是跟着新的 version 号的。其他的哨兵都是根据版本号的大小来更新自己的 master 配置的。
|
||||
|
@ -1,54 +1,60 @@
|
||||
## 面试题
|
||||
redis 和 memcached 有什么区别?redis 的线程模型是什么?为什么 redis 单线程却能支撑高并发?
|
||||
Redis 和 Memcached 有什么区别?Redis 的线程模型是什么?为什么 Redis 单线程却能支撑高并发?
|
||||
|
||||
## 面试官心理分析
|
||||
这个是问 redis 的时候,最基本的问题吧,redis 最基本的一个内部原理和特点,就是 redis 实际上是个**单线程工作模型**,你要是这个都不知道,那后面玩儿 redis 的时候,出了问题岂不是什么都不知道?
|
||||
|
||||
还有可能面试官会问问你 redis 和 memcached 的区别,但是 memcached 是早些年各大互联网公司常用的缓存方案,但是现在近几年基本都是 redis,没什么公司用 memcached 了。
|
||||
这个是问 Redis 的时候,最基本的问题吧,Redis 最基本的一个内部原理和特点,就是 Redis 实际上是个**单线程工作模型**,你要是这个都不知道,那后面玩儿 Redis 的时候,出了问题岂不是什么都不知道?
|
||||
|
||||
还有可能面试官会问问你 Redis 和 Memcached 的区别,但是 Memcached 是早些年各大互联网公司常用的缓存方案,但是现在近几年基本都是 Redis,没什么公司用 Memcached 了。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### redis 和 memcached 有啥区别?
|
||||
### Redis 和 Memcached 有啥区别?
|
||||
|
||||
#### redis 支持复杂的数据结构
|
||||
redis 相比 memcached 来说,拥有[更多的数据结构](/docs/high-concurrency/redis-data-types.md),能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择。
|
||||
#### Redis 支持复杂的数据结构
|
||||
|
||||
#### redis 原生支持集群模式
|
||||
在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
|
||||
Redis 相比 Memcached 来说,拥有[更多的数据结构](/docs/high-concurrency/redis-data-types.md),能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, Redis 会是不错的选择。
|
||||
|
||||
#### Redis 原生支持集群模式
|
||||
|
||||
在 Redis3.x 版本中,便能支持 cluster 模式,而 Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
|
||||
|
||||
#### 性能对比
|
||||
由于 redis 只使用**单核**,而 memcached 可以使用**多核**,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis。虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。
|
||||
|
||||
### redis 的线程模型
|
||||
redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
|
||||
由于 Redis 只使用**单核**,而 Memcached 可以使用**多核**,所以平均每一个核上 Redis 在存储小数据时比 Memcached 性能更高。而在 100k 以上的数据中,Memcached 性能要高于 Redis。虽然 Redis 最近也在存储大数据的性能上进行优化,但是比起 Memcached,还是稍有逊色。
|
||||
|
||||
### Redis 的线程模型
|
||||
|
||||
Redis 内部使用文件事件处理器 `file event handler` ,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
|
||||
|
||||
文件事件处理器的结构包含 4 个部分:
|
||||
|
||||
- 多个 socket
|
||||
- IO 多路复用程序
|
||||
- 文件事件分派器
|
||||
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
|
||||
* 多个 socket
|
||||
* IO 多路复用程序
|
||||
* 文件事件分派器
|
||||
* 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
|
||||
|
||||
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
|
||||
|
||||
来看客户端与 redis 的一次通信过程:
|
||||
来看客户端与 Redis 的一次通信过程:
|
||||
|
||||

|
||||

|
||||
|
||||
要明白,通信是通过 socket 来完成的,不懂的同学可以先去看一看 socket 网络编程。
|
||||
|
||||
首先,redis 服务端进程初始化的时候,会将 server socket 的 `AE_READABLE` 事件与连接应答处理器关联。
|
||||
首先,Redis 服务端进程初始化的时候,会将 server socket 的 `AE_READABLE` 事件与连接应答处理器关联。
|
||||
|
||||
客户端 socket01 向 redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 `AE_READABLE` 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给**连接应答处理器**。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 `AE_READABLE` 事件与命令请求处理器关联。
|
||||
客户端 socket01 向 Redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 `AE_READABLE` 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给**连接应答处理器**。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 `AE_READABLE` 事件与命令请求处理器关联。
|
||||
|
||||
假设此时客户端发送了一个 `set key value` 请求,此时 redis 中的 socket01 会产生 `AE_READABLE` 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 `AE_READABLE` 事件,由于前面 socket01 的 `AE_READABLE` 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 `key value` 并在自己内存中完成 `key value` 的设置。操作完成后,它会将 socket01 的 `AE_WRITABLE` 事件与命令回复处理器关联。
|
||||
假设此时客户端发送了一个 `set key value` 请求,此时 Redis 中的 socket01 会产生 `AE_READABLE` 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 `AE_READABLE` 事件,由于前面 socket01 的 `AE_READABLE` 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 `key value` 并在自己内存中完成 `key value` 的设置。操作完成后,它会将 socket01 的 `AE_WRITABLE` 事件与命令回复处理器关联。
|
||||
|
||||
如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 `AE_WRITABLE` 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 `ok`,之后解除 socket01 的 `AE_WRITABLE` 事件与命令回复处理器的关联。
|
||||
如果此时客户端准备好接收返回结果了,那么 Redis 中的 socket01 会产生一个 `AE_WRITABLE` 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 `ok` ,之后解除 socket01 的 `AE_WRITABLE` 事件与命令回复处理器的关联。
|
||||
|
||||
这样便完成了一次通信。关于 Redis 的一次通信过程,推荐读者阅读《[Redis 设计与实现——黄健宏](https://github.com/doocs/technical-books#database)》进行系统学习。
|
||||
|
||||
### 为啥 redis 单线程模型也能效率这么高?
|
||||
- 纯内存操作。
|
||||
- 核心是基于非阻塞的 IO 多路复用机制。
|
||||
- C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
|
||||
- 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
|
||||
### 为啥 Redis 单线程模型也能效率这么高?
|
||||
|
||||
* 纯内存操作。
|
||||
* 核心是基于非阻塞的 IO 多路复用机制。
|
||||
* C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
|
||||
* 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
|
||||
|
@ -2,21 +2,25 @@
|
||||
项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
这个问题,互联网公司必问,要是一个人连缓存都不太清楚,那确实比较尴尬。
|
||||
|
||||
只要问到缓存,上来第一个问题,肯定是先问问你项目哪里用了缓存?为啥要用?不用行不行?如果用了以后可能会有什么不良的后果?
|
||||
|
||||
这就是看看你对缓存这个东西背后有没有思考,如果你就是傻乎乎的瞎用,没法给面试官一个合理的解答,那面试官对你印象肯定不太好,觉得你平时思考太少,就知道干活儿。
|
||||
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 项目中缓存是如何使用的?
|
||||
|
||||
这个,需要结合自己项目的业务来。
|
||||
|
||||
### 为什么要用缓存?
|
||||
|
||||
用缓存,主要有两个用途:**高性能**、**高并发**。
|
||||
|
||||
#### 高性能
|
||||
|
||||
假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 mysql,半天查出来一个结果,耗时 600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办?
|
||||
|
||||
缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql 折腾 600ms 了,直接从缓存里,通过一个 key 查出来一个 value,2ms 搞定。性能提升 300 倍。
|
||||
@ -24,6 +28,7 @@
|
||||
就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。
|
||||
|
||||
#### 高并发
|
||||
|
||||
mysql 这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。mysql 单机支撑到 `2000QPS` 也开始容易报警了。
|
||||
|
||||
所以要是你有个系统,高峰期一秒钟过来的请求有 1万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单,说白了就是 `key-value` 式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 mysql 单机的几十倍。
|
||||
@ -31,9 +36,11 @@ mysql 这么重的数据库,压根儿设计不是让你玩儿高并发的,
|
||||
> 缓存是走内存的,内存天然就支撑高并发。
|
||||
|
||||
### 用了缓存之后会有什么不良后果?
|
||||
常见的缓存问题有以下几个:
|
||||
- [缓存与数据库双写不一致](/docs/high-concurrency/redis-consistence.md)
|
||||
- [缓存雪崩、缓存穿透](/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md)
|
||||
- [缓存并发竞争](/docs/high-concurrency/redis-cas.md)
|
||||
|
||||
后面再详细说明。
|
||||
常见的缓存问题有以下几个:
|
||||
|
||||
* [缓存与数据库双写不一致](/docs/high-concurrency/redis-consistence.md)
|
||||
* [缓存雪崩、缓存穿透、缓存击穿](/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md)
|
||||
* [缓存并发竞争](/docs/high-concurrency/redis-cas.md)
|
||||
|
||||
点击超链接,可直接查看缓存相关问题及解决方案。
|
||||
|
@ -1,23 +1,29 @@
|
||||
## 面试题
|
||||
- 为什么使用消息队列?
|
||||
- 消息队列有什么优点和缺点?
|
||||
- Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别,以及适合哪些场景?
|
||||
|
||||
* 为什么使用消息队列?
|
||||
* 消息队列有什么优点和缺点?
|
||||
* Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别,以及适合哪些场景?
|
||||
|
||||
## 面试官心理分析
|
||||
|
||||
其实面试官主要是想看看:
|
||||
|
||||
- **第一**,你知不知道你们系统里为什么要用消息队列这个东西?<br>
|
||||
* **第一**,你知不知道你们系统里为什么要用消息队列这个东西?<br>
|
||||
|
||||
不少候选人,说自己项目里用了 Redis、MQ,但是其实他并不知道自己为什么要用这个东西。其实说白了,就是为了用而用,或者是别人设计的架构,他从头到尾都没思考过。<br>
|
||||
没有对自己的架构问过为什么的人,一定是平时没有思考的人,面试官对这类候选人印象通常很不好。因为面试官担心你进了团队之后只会木头木脑的干呆活儿,不会自己思考。
|
||||
|
||||
- **第二**,你既然用了消息队列这个东西,你知不知道用了有什么好处&坏处?<br>
|
||||
* **第二**,你既然用了消息队列这个东西,你知不知道用了有什么好处&坏处?<br>
|
||||
|
||||
你要是没考虑过这个,那你盲目弄个 MQ 进系统里,后面出了问题你是不是就自己溜了给公司留坑?你要是没考虑过引入一个技术可能存在的弊端和风险,面试官把这类候选人招进来了,基本可能就是挖坑型选手。就怕你干 1 年挖一堆坑,自己跳槽了,给公司留下无穷后患。
|
||||
|
||||
- **第三**,既然你用了 MQ,可能是某一种 MQ,那么你当时做没做过调研?<br>
|
||||
* **第三**,既然你用了 MQ,可能是某一种 MQ,那么你当时做没做过调研?<br>
|
||||
|
||||
你别傻乎乎的自己拍脑袋看个人喜好就瞎用了一个 MQ,比如 Kafka,甚至都从没调研过业界流行的 MQ 到底有哪几种。每一个 MQ 的优点和缺点是什么。每一个 MQ **没有绝对的好坏**,但是就是看用在哪个场景可以**扬长避短,利用其优势,规避其劣势**。<br>
|
||||
如果是一个不考虑技术选型的候选人招进了团队,leader 交给他一个任务,去设计个什么系统,他在里面用一些技术,可能都没考虑过选型,最后选的技术可能并不一定合适,一样是留坑。
|
||||
|
||||
## 面试题剖析
|
||||
|
||||
### 为什么使用消息队列
|
||||
其实就是问问你消息队列都有哪些使用场景,然后你项目里具体是什么场景,说说你在这个场景里用消息队列是什么?
|
||||
|
||||
@ -26,58 +32,65 @@
|
||||
先说一下消息队列常见的使用场景吧,其实场景有很多,但是比较核心的有 3 个:**解耦**、**异步**、**削峰**。
|
||||
|
||||
#### 解耦
|
||||
|
||||
看这么个场景。A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃......
|
||||
|
||||

|
||||

|
||||
|
||||
在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊!
|
||||
|
||||
如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。
|
||||
|
||||

|
||||

|
||||
|
||||
**总结**:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。
|
||||
|
||||
**面试技巧**:你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的,你就需要去考虑在你的项目里,是不是可以运用这个 MQ 去进行系统的解耦。在简历中体现出来这块东西,用 MQ 作解耦。
|
||||
|
||||
#### 异步
|
||||
|
||||
再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。
|
||||
|
||||

|
||||

|
||||
|
||||
一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。
|
||||
|
||||
如果**使用 MQ**,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快!
|
||||
|
||||

|
||||

|
||||
|
||||
#### 削峰
|
||||
|
||||
每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。
|
||||
|
||||
一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。
|
||||
|
||||
但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。
|
||||
|
||||

|
||||

|
||||
|
||||
如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。
|
||||
|
||||

|
||||

|
||||
|
||||
这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。
|
||||
|
||||
### 消息队列有什么优缺点
|
||||
|
||||
优点上面已经说了,就是**在特殊场景下有其对应的好处**,**解耦**、**异步**、**削峰**。
|
||||
|
||||
缺点有以下几个:
|
||||
|
||||
- 系统可用性降低<br>
|
||||
系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用,可以[点击这里查看](/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)。
|
||||
* 系统可用性降低<br>
|
||||
|
||||
系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,ABCD 四个系统还好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整?MQ 一挂,整套系统崩溃,你不就完了?如何保证消息队列的高可用,可以[点击这里查看](/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)。
|
||||
|
||||
* 系统复杂度提高<br>
|
||||
|
||||
- 系统复杂度提高<br>
|
||||
硬生生加个 MQ 进来,你怎么[保证消息没有重复消费](/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)?怎么[处理消息丢失的情况](/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。
|
||||
|
||||
- 一致性问题<br>
|
||||
* 一致性问题<br>
|
||||
|
||||
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
|
||||
|
||||
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。
|
||||
@ -93,7 +106,6 @@ A 系统处理完了直接返回成功了,人都以为你这个请求就成功
|
||||
| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ |
|
||||
| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 |
|
||||
|
||||
|
||||
综上,各种对比之后,有如下建议:
|
||||
|
||||
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;
|
||||
|