大型网站需要处理海量的用户访问,需要解决高并发、高可用带来的数据一致性问题。
解决思路通常有两种:
- 利用分布式系统的特性不断地分拆,把大系统拆小
- 快速迭代
但还需要解决系统的可重用、可扩展、可维护性
所以对于一个系统如何解决面临的高并发、高可用的技术问题,又面临复杂的业务问题
如何解决两者的关系,是架构的重点。
什么是架构
架构的分类
第一层:基础结构
第二层:中间件和大数据平台
第三层:业务系统架构
通用业务中间件 权限控制 工作流引擎 规则引擎
架构的道与术
道:是理论基础(内功)
术:是具体实践(招式)
计算机功底
熟悉原理
1.做上层开发时知道 底层系统帮我们做了 那些不支持,需要我们去实现。
2.更容易理解如何实现的,有哪些潜在的问题, 遇到问题更容易解决。
3.借鉴思路
语言
精通一门语言
操作系统
缓冲I/O和直接I/O
内核缓冲区:
为了加快磁盘的I/O Linux系统会把磁盘上的数据以页为单位 缓存在操作系统中 页一般是4k
对于直接I/O ,没有用户级的缓存
读:磁盘->内核缓冲区->应用程序内存
写:应用程序内存->内核缓冲区->磁盘

关于缓冲I/O和直接I/O,有几点需要特别说明
(1)fflush和fsync 区别: fflush是缓冲I/O的一个API,把数据从用户缓冲区刷到内核缓冲区,fsync是吧数据从内核缓冲区刷到磁盘里
如果断电时 不适用fsync数据会丢失
(2)pread/pwrite在多线程写同一个文件的时候很有用
内存映射文件和零拷贝
内存映射文件
相比于直接I/O更近了一步, 直接那应用程序的逻辑内存地址映射到Linux的系统内核缓冲区/
读:磁盘->内核缓冲区
写:内核缓冲区->磁盘

在Java中 使用 MappedByteBuffer
零拷贝
零拷贝是提高I/O效率的利器
用户需要把文件中的数据发送到网络的时候,不用零拷贝
直接I/O 伪代码如下:

会有4次拷贝
硬盘->内核缓冲区->应用程序内存->Socket缓冲区->网络

内存映射文件会有3次I/O


零拷贝:
在内核缓冲区 到 Socket缓冲区 之间并没有做数据拷贝,只是一个地址映射,底层网卡的网络驱动读取数据并发到网络的时候,
实际直接读的是内存缓冲区的数据

零拷贝 实际上有两次拷贝 之所以叫零拷贝是从内存的角度考虑。 内存之间没有拷贝,只在内存和I/O之间传输。
在linux 零拷贝API为sendfile函数
Java中 FileChannel.transferTo函数
网络I/O模型
实现层面的网络I/O模型
linux下的I/O模型
第一种模式:同步阻塞I/O
Linux系统的read write 在调用的时候会被阻塞 ,直到数据读取完成,或者写入成功。
第二种模式:同步非阻塞I/O
和同步阻塞I/O的API一样,当fd带有O_NOBLOCK参数时。当调用read whrite函数时
如果没有准备好数据 会返回 但不会阻塞吗,然后让应用程序不多地去轮询。
第三种模式:I/O多路复用
就是同时监控多个IO,有一个准备好了,就不需要继续等待其他IO,可以开始处理,提交了同时处理IO的能力
属于 同步阻塞
linux种有: select、 poll 、epoll
select:阻塞调用 一次性把所有的fd传过去,当fd可读或者可写之后,该函数返回。
第四种模式:异步I/O
读写丢失由操作系统完成,然后通过回调函数或者其它消息机制通知应用程序

怎么区分阻塞 非阻塞 同步 异步?
阻塞和非阻塞 是从调用函数 返回结果的角度 同步和异步 是谁来完成的角度
Reactor模式和Preactor模式
它们是网络框架的设计模式
(1)Reactor模式:主动模式。所谓主动是指应用程序不断地轮询,询问操作系统或网络框架。I/O是否准备就绪。
Linux下的select poll epoll就是主动模式 需要一个循环一直轮询
NIO也是这种模式 这种模式 I/O操作还是由应用程序执行
(2)Preactor模式: 被动模式 实际I/O操作是由操作系统或者网络框架完成,之后再回调应用程序
异步I/O 属于Preactor模式
select、epoll
select poll 都需要把fd数组传递过去
epoll的过程:
(1)事件注册 通过函数epoll_ctl实现 对于服务器而言 是accept read write 三个事件 对于客户端来说
是connect read write三个事件
(2)轮询三个事件 是否就绪 通过epoll_wait实现 有事件发生 该函数返回
(3)事件就绪 执行I/O操作 通过函数accept/read/write实现
epoll里的LT ET模式
LT 水平触发 又叫条件触发
比卖你写的死循环 ,注册完写事件,却没有数据要写,它会一直触发,写完数据要取消事件
EF 边缘触发 又叫状态触发
避免 short read 问题 读缓冲区的数据一次性读完
倾向使用LT
服务器编程的1+N+M模型
一个监听程序 N个I/O多线程 M个Worker线程 N通常等于CPU核心数 M的个数由上层决定 通常几百个

监听线程:负责accept事件的注册和处理。 和每一个新进来的客户端建立socket连接 然后将Socket移交给I/O 线程
I/O线程 负责每个socket链接上面的read/write事件的注册和 socket的·读写 把request方法request队列 交由worker线程处理
worker线程 : 没有socket的读写操作 对Request队列处理 生成response队列 ,然后写入Response队列
Tomcat6的NIO模型

进程、线程和协程
多线程
多线程主要为了应对I/O密集型应用。
1.提高CPU的利用率 不能让cpu空闲着 发生I/O时 其它线程调用上去继续计算
2.提高I/O吞吐。
Redis MySql 连接池
多进程
为什么要有多进程
线程存在两个问题
1.线程内存共享 要加线程锁(效率降低 难度变大)
2.多线程导致上下文切换 导致效率降低
并发一个重要的设计原则:
尽可能使用消息通信 而不是共享内存 来实现进程或者线程之间的同步
进程是资源分配的最小单位,进程间不共享资源,通过管道或者socket方式通信 天生符合并发设计的原则
Nginx是一个多进程的程序
Redis是一个单进程 单线程的模型 (单线程 是指处理客户端请求的线程只有一个)
请求接受的时候用的epoll的I/O多路复用
对于I/O密集型,提高效率
1.异步I/O
2.多线程
3.多协程
多协程
由于多线程 锁 和切换开销比较大 以tomcat为例 最多处理几百个并发
但是协程可以开几万个
相对于线程:
1.协程是应用程序自己调度
2.更好的利用内存,协程堆栈大小不固定 用多少申请多少 内存利用率高


无锁(内存屏障与CAS)
无锁
Linux内核的kfifo.c 源码 RingBuffer 允许一个线程写 一个线程读 整个代码没有锁 确是线程安全的
两个核心:都可以多线程 写必须单线程
多核CPU 每个CPU都有自己的缓存 使用内存屏障 就是把缓存刷到内存中
Java中的volatile 是基于内存屏障
CAS
cas是cpu层面的一个硬件原子指令(实现对一个值compare和swap两个操作的原子化)
基于cas 上层可以实现乐观锁 无锁队列 无所链表
Java JUC中 无锁队列 基于单向链表 维护了一头一尾的引用 head 和 tail 入队出队 都是用cas互斥操作
网络
HTTP1.0
基本特点 一来一回 请求完,连接关闭
协议存在的问题
-
性能问题 连接创建 关闭 耗时的操作
-
服务器推送的问题 无法在客户没有请求的情况下 推送消息
keep_Alive 和 Content_length
使用keep_Alive实现连接的复用 请求和返回 头部添加Connection: keep_Alive 这样就不会关闭
但这样一直不关闭 keep_Alive timeout一段时间自动关闭
客户端怎么知道连接处理结束
Response头部Content_length告诉body有多少个字节
HTTP1.1
连接复用变成默认属性 处理完后不关闭连接
Content_length存在的问题
动态语言生成的内容 需要服务器渲染整个页面 在计算长度,太耗时
引入chunk机制 (http straming)
Transfer-Encoding: chunked
所有快的结尾有特殊标识
连接复用并发不足
引入Pipeline机制
Pipeline存在的问题
队头阻塞 如果头部的请求延迟,接下来的请求也会被阻塞
无论HTTP1.0还是1.1都无法解决服务器主动推送
1.客户端定期轮询
低效,增大压力 很少采用
2.WebSocket
TCP基于tcp 有局限性
3.HTTP长轮询
发送请求 保持连接 客户端等待请求
过了约定时间 没有新消息 发送空消息 客户端重新建立请求
4.HTTP Straming
发送没完没了的chunk流
没有长轮询简单直接
断点续传
HTTP/2
HTTP/2和HTTP1.1并不是平级关系
相当于在HTTP1.1和TCP之间多了个转换层
二进制分帧
解决队头阻塞
对于一个域名 ,只维护一个tcp连接 tcp是全双工

多余tcp层面是串行 对HTTP层面是并发
是不是解决了头部阻塞 只是把头部阻塞 从HTTP Reques粒度细化到帧 降低了 发生的概率
头部压缩
报文体是压缩的 但头部没压缩
SSL/TLS
SSL 安全套接层 TLS 传输层安全协议
SSL/TLS tcp协议上面
对称加密 密钥无法传输
双向非对称加密
公钥 私钥

单向非对称加密
只需要客户端到服务器 是安全的 返回时明文的 这就是SSL/TSL的原型
公钥如何安全传输
如过 公钥被劫持 就会错误认为 和对方通信
数字证书和证书认证中心


根证书和CA信任链
怎么证明CA不是假的

SSL/TLS 四次握手

HTTPS
HTTPS = HTTP/TLS


TCP/UDP
TCP可靠 UDP不可靠
甚么是不可靠?
1.丢包
2.时序错乱
3.重复
TCP是怎么可靠的
1.解决不丢 ACK + 重发
2.解决重复 消息顺序编号 ACK顺序确认 如果重复ack已经发送过了,就会抛弃
3.解决时序 消息顺序编号 ack按顺序确认 前面未到 后面等待 其他暂存 超时重发
消息顺序编号 ack按顺序确认 重发 保证了可靠性
分布式消息中间件类似
三次握手
四次挥手
QUIC
UDP协议的多路并发协议
1.不丢包(Raid5和Raid6协议)
Raid5 每发送5个包 多发送一个冗余包 丢的话可以计算出来
Raid6 每发送5个包 多发送二个冗余包 丢的话可以计算出来
目前采用Raid5 10个有一个冗余包
减少重传的概率
2.更少的握手
7次变为0次
3.连接迁移
移动端 ip 不断变化 如何连接维持?
客户端生成64位标识
数据库
范式与反范式

分库分表
为什么要分?
1.业务拆分 便于职责分工,系统扩展
2.应对高并发
读多写少 从库,缓存 读少写多,或者写入QPS达到瓶颈
3.数据隔离
面临的问题
####### 分布式ID
雪花算法
####### 拆分的维度
如何查询其它维度
1.建立一个映射表
2.业务双写
3.异步双写
4.两个维度统一到一个维度
5.join问题
1.代码层拼接
2.做宽表,重写轻读
3.利用搜索引擎
####### 分布式事务
尽量优化逻辑 避免跨库的事务
B+树
事务与锁
隔离级别
事务与事务存在并发冲突

数据库事务的隔离级别有4个

悲观锁和乐观锁
丢失更新在业务场景很常见 但是靠数据库本身没有解决
1.利用单条语句的原子性
2.悲观锁
认为发生冲突的可能性比较大,所以读之前就加锁 利用select xxx for update语句
潜在问题: 如果commit之前 出现问题 锁不释放 数据库死锁
另外高并发场景用户请求阻塞 为此,有了乐观锁
3.乐观锁
对于乐观锁,认为并发冲突的概率比较小,所以读之前不上锁。等到写回去的时候再判断数据是否被它事务更改了。 即CAS原理
分布式锁
乐观锁 这只能解决同一张表的同一条记录 复杂业务场景就需要分布式锁
死锁检测
锁使用不当会发生死锁


事务的实现原理 redo log
事务就是一组原子性的SQL操作或者一个独立的工作单元,事务内的语句要么全部执行成功,要么全部执行失败。
事务是指对系统进行的一组操作,为了保证系统的完整性,事务需要具有ACID特性,具体如下: \1. 原子性(Atomic) 一个事务包含多个操作,这些操作要么全部执行,要么全都不执行。实现事务的原子性,要支持回滚操作,在某个操作失败后,回滚到事务执行之前的状态。 回滚实际上是一个比较高层抽象的概念,大多数DB在实现事务时,是在事务操作的数据快照上进行的(比如,MVCC),并不修改实际的数据,如果有错并不会提交,所以很自然的支持回滚。 而在其他支持简单事务的系统中,不会在快照上更新,而直接操作实际数据。可以先预演一边所有要执行的操作,如果失败则这些操作不会被执行,通过这种方式很简单的实现了原子性。
\2. 一致性(Consistency) 一致性是指事务使得系统从一个一致的状态转换到另一个一致状态。事务的一致性决定了一个系统设计和实现的复杂度,也导致了事务的不同隔离级别。
事务可以不同程度的一致性:
强一致性:读操作可以立即读到提交的更新操作。
弱一致性:提交的更新操作,不一定立即会被读操作读到,此种情况会存在一个不一致窗口,指的是读操作可以读到最新值的一段时间。
最终一致性:是弱一致性的特例。事务更新一份数据,最终一致性保证在没有其他事务更新同样的值的话,最终所有的事务都会读到之前事务更新的最新值。如果没有错误发生,不一致窗口的大小依赖于:通信延迟,系统负载等。
其他一致性变体还有:
单调一致性:如果一个进程已经读到一个值,那么后续不会读到更早的值。
会话一致性:保证客户端和服务器交互的会话过程中,读操作可以读到更新操作后的最新值。
\3. 隔离性(Isolation) 并发事务之间互相影响的程度,比如一个事务会不会读取到另一个未提交的事务修改的数据。在事务并发操作时,可能出现的问题有: 脏读:事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。 不可重复读:在同一个事务中,对于同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果,和提交后读到的结果可能不同。不可重复读出现的原因就是事务并发修改记录,要避免这种情况,最简单的方法就是对要修改的记录加锁,这回导致锁竞争加剧,影响性能。另一种方法是通过MVCC可以在无锁的情况下,避免不可重复读。 幻读:在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读是由于并发事务增加记录导致的,这个不能像不可重复读通过记录加锁解决,因为对于新增的记录根本无法加锁。需要将事务串行化,才能避免幻读。 事务的隔离级别从低到高有: Read Uncommitted:最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。 Read Committed:只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。 Repeated Read:在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。 Serialization:事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。 通常,在工程实践中,为了性能的考虑会对隔离性进行折中。
- 持久性(Durability)
事务提交后,对系统的影响是永久的。
write_ahead log
如果每个事务都直接写磁盘,一次事务提交就要多次磁盘的随机I/O,性能达不到要求
如果使用内存 异步把内存中的数据写入磁盘 机器怠机 可能导致数据丢失
为此有了Write-ahead Log的思路
先内存提交事务 然后写日志 再后台任务把内存中的数据异步刷到磁盘
LSM树 和 多备份一致性 复制状态机 模型也是基于此
InnoDb write_ahead log是redo log
redo log的也是异步 先写到redo log buffer 然后异步刷到磁盘上

redo log的逻辑和物理结构

physiological logging
采用逻辑 和物理的结合体
先以page 为单位记录日志 每个Page里面采用 逻辑记法
I/O写入的原子性
事务 LSN 与Log Block的关系


TXID是INnoDB为每个事务分配 一个唯一的ID
Binlog与主从复制
BinLog 和 Redo Log的差异
Redo log 和 Undo log是InnoDB里的工具
Binlog是MySQL层面的东西 Binlog主要作用是主从复制
在互联网应用中第二个作用 :
一个应用程序把自己伪装成Slave,来监听Master的Binlog,然后把数据库的变更以消息的形式抛出来
业务系统消费消息 执行业务逻辑操作。
阿里开源的有Canal 国外有DataBus
同Redo log一样 Binlog也有刷盘策略

Binlog是串行写的 写之前需要拿到锁 影响效率 5.6的Group Commit 使用pipeline 批量化
内部XA-BinLog与Redo Log一致性问题
Binlog自身写入的原子性: 是通过类似Redo log的Checksum办法 或者 Binlog中有结束标识
把不完整的截掉
Binlog和Redo log的书记一致性问题 即内部XA 使用的是经典的2阶段提交方案(2PC)
三种主从复制方式
异步复制 Master怠机 切换到Slave 此时Slave上没有最先的数据 所以很多时候使用半同步复制
如果超过时间 Slave还没 恢复ACK Master就会切换为异步复制模式

异步复制 和 半同步复制 都可能在主从切换的时候丢数据 牺牲一致性来获取高可用性
但如果主从复制延迟过大 也是无法忍受的 在跨机房情况下 并行复制 尤其重要。
并行复制


并行复制其实就是并行回放
不同表的事务是顺序摆列的 并行执行 就涉及什么事务可以并行执行 什么事务不能并行执行
1.按数据粒度并行
不同库的事务可以并行 不同表的事务可以并行 不同行事务可以执行
2.按事务的提交顺序并行
同样commit_id的事务可以并行
框架 软件与中间件
框架
对于开发者必须熟悉掌握至少一种框架。 框架设计会用到好多设计模式
比如:工厂 模板方法 责任链
熟悉一个框架 最重要关注其缺点
开源软件和中间件

技术架构之道
高并发问题
高并发读
缓存
本地缓存或Memcached/Redis集中式缓存
MySQL的和Master/Slave
添加Slave 来分担数据库读的压力
CDN静态文件加速(动静分离)
对于静态内容 一个常见的处理策略就是CDN
当用户访问的时候,离用户最近的节点没有缓存数据,CDN就去源系统抓取文件缓存到该节点。当第二个用户访问的时候,只需要从这个节点访问即可
Redis MySQL的Slave CND 从策略上都是一种 缓存的形式,通过对数据进行冗余,达到空间换时间的效果。
并发读
对于读还是写,串行改并行都是一个常用的策略。
异步RPC
Google的冗余请求
向多个服务器发生请求,那个返回快 其他的丢弃 这样就会使调用量翻倍
解决方案:
如果客户端在一定的时间内没有收到服务端的响应,则马上给另一台(或多台)服务器发送同样的请求
Google的测试数据,采用这种方法 可以仅用2%的额外请求将系统99%的请求响应时间从1800ms降落到74ms。
重写轻读
微博Feeds流
基本功能 查看关注人的微博列表 和 查看自己发布的微博列表
关联查询模型无法满足高并发的查询请求 改成重写轻读
不是查询的时候再去聚合,而是提前为每个user_id准备feeds流 或者叫收件箱
每个用户都有 一个发件箱和收件箱
假设用户有1000个粉丝 发布微博后 写入发件箱就成功返回 后台异步推送给1000个粉丝的收件箱

收件箱使用内存 假设使用redis的list twitter上限800
粉丝多的 比如大于5000 只推送给在线用户
自己发布的 微博 考虑用mysql但是数据会一直增长 不可能放在一个数据库里面 需要数据库分片
只按用户id分库 数据会随时间不停的增长
只按时间分库 冷热不均
所以按时间 和 用户id分库
多表关联查询:宽表与搜索引擎
分库之后 join查询
准备一张宽表,把关联表的数据算好后保存在宽表 根据实际情况 定时算 也可以数据变化就出发宽表数据计算
或者放到ES类的搜索引擎里 多张表Join结果做成一个文档
总结:读写分离
无论缓存,动静分离,还是重写轻读本质上都是读写分离

读和写的串联
定时任务
消息中间件
Binlog 监听数据库的变化
读比写有延迟,因为左边数据实时变化 读写之间是最终一致性 而不是强一致
高并发写
数据分片
数据库的分库分表
JDK的ConCurrentHashMap的实现
内部被分为多个槽(默认16个槽) 也就是若干个HashMap 这些槽可以并发地读写 槽与槽之间独立 不会数据互斥
Kafka的partition
ES的分布式索引
如果商品很多。建在一个倒排索引里面,则索引很大
如果建立n个小索引 一个查询请求来了以后 并行的n个索引上查询 就可以查询根快
任务分片
任务分片是对程序本身进行分片
归并排序
子序列排序和归并
1+N+M的网络模型
CPU的指令流水线
异步化

短信验证码
电商的订单系统
拆单
广告计费系统
LSM(写内存+ Write-Ahead日志)
为了提高磁盘I/O的写性能,可以使用WriteAhead也就是Redo Log
Log Structured Merge Trees(LSM) 用到的核心思想就是异步写
LSM 支持KV存储 当插入时K是无序的;但当在磁盘上又需要按K的大小顺序存储 也就是磁盘上实现的是Sorted HashMap1

写内存 + Write_Ahead日志 这种思路不仅在数据库和 KV存储领域使用 上层业务系统也可以同时使用。
比如高并发的扣减MySQL 余额 或者电商系统中扣库存,如果直接数据库中扣 扛不住
可以在Redis扣 同时落地一条日志(消息中间件 或者数据库)。
Kafka的PipeLine
批量
kafka的百万QPS写入

广告系统的合并扣费

MySQL的小事务合并机制
同样在多机房的数据库多活(跨数据中心的数据库复制)场景中,事务合并也是加速数据库复制的一个重要策略
串行化+多进程单线程+异步I/O
提高并发 使用多线程 但多线程有两大问题 : 锁竞争 线程切换开销大
容量规划
吞吐量 响应时间 与 并发数
吞吐量:单位时间内处理的请求数 QPS(每秒查询率) TPS(每秒处理的事务)
吞吐量 X 响应时间= 并发数

压力测试 和容量评估
机器数 = 预估流量/单机容量
分子是预估的值 分母是通过压力测试得到的
高可用和稳定性
多副本
1.本地缓存多副本
2.Redis多副本
3.Mysql多副本
4.消息中间件多副本
隔离、限流、熔断、降级
隔离
指将系统或资源分隔开,在系统发生故障能限定 传播范围和影响
(1) 数据隔离
(2)机器隔离
成熟的RPC框架往往有隔离功能,根据调用方的标识 把来自某个调用方的请求都发送到固定的机器中
(3)线程池隔离
慢的影响其他接口
(4)信号量隔离
Sentinel 提供了并发线程数模式的限流,其实也是一种隔离手段。
限流
生活中景点限流。
(1)技术层面的限流 限制并发数
限制速率的这种方式 对于接口调用非常有用QPS2000 就可以限流2000QPS 超过直接拒绝服务
(2)业务层面的限流 秒杀
(3)限流算法
漏桶算法

令牌桶算法

对比:
- 一个是流出速率保持恒定 一个是流入速率保持恒定
- 用途:令牌桶限制 平均平均流入速率 而不是瞬时速率
漏桶有点类似消息队列 起到了削峰的作用 平缓突发流入速率
熔断
发生短路,保险丝熔断,切断电路
计算机中熔断有两种策略:
(1) 根据请求失败率做熔断
如果客户端短时间内大量超时或抛错,则客户端直接开启熔断
过段时间打开 还不行 继续熔断

(2)根据响应时间做熔断
和限流对比:限流是服务端,根据其能力上限设置的一个过载保护;而熔断则是调用端对自己的一个保护
注意: 能熔断的服务肯定不是 黑犀牛链路上的必选服务 所以熔断其实也是降级的一种
降级
降级是一种兜底方案
灰度发布和回滚
新功能上线的灰度
先将一部分的流量导入这个新的功能,如果验证没有问题,再一点点地增加流量
旧系统重构的灰度
回滚
监控日志和报警体系
监控体系
1.资源监控
cpu、内存、磁盘、带宽、端口
2.系统监控
最前端的URL访问失败率
RPC调用失败
RPC接口响应时间
DB long SQL
Java JVM的young GC full GC回收频率 回收时间
3.业务监控
日志报警
WARNING 引起注意
ERROR 马上解决
事务一致性
多副本一致性
无论是MYSQL的Master/Slave,还是Redis的Master/Slave,或是Kafka的多副本复制,都是通过牺牲一致性来换取高可用性的。
既满足强一致,又满足高可用的系统。就需要一致性算法或协议——–Paxos,Raft,Zab

高可用和强一致到底有多难?
Kafka的消息丢失问题
Paxos算法(派克索斯)
解决的问题
多个客户端的请求是并发的,没有先后顺序,但需要保证在集群中多台服务器的日志顺序是一致的