元数据
软件架构设计:大型网站技术架构与业务架构融合之道
- 书名: 软件架构设计:大型网站技术架构与业务架构融合之道
- 作者: 余春龙
- 简介: 为什么说是“多想”了呢?因为无论在企业面试还是日常工作中,人们更多谈论的是语言、数据结构、算法、操作系统原理,框架或中间件的使用方式、原理等“硬”性的内容,因为这些“硬”性的内容比较容易表述,其中学问的深浅也容易衡量。而对于软件建模、架构设计等“软”性的内容,就不容易衡量了。人们都知道它们很重要,但又说不清楚里面到底包含了哪些学问,所以谈论这些内容时通常都比较“虚”,最终导致很少从方法论的角度去讲,而是在项目中遇到问题时再具体解决,属于实用主义思维的做法。
- 出版时间 2019-01-01 00:00:00
- ISBN: 9787121356032
- 分类: 计算机-编程设计
- 出版社: 电子工业出版社
高亮划线
4.1 缓冲I/O和直接I/O
-
📌 用户缓冲区:C语言的FILE结构体里面的buffer。 ^25462656-13-956-987
- ⏱ 2022-01-02 20:56:28
-
📌 内核缓冲区:Linux操作系统的Page Cache。为了加快磁盘的I/O,Linux系统会把磁盘上的数据以Page为单位缓存在操作系统的内存里 ^25462656-13-1381-1457
- ⏱ 2022-01-02 20:57:17
-
📌 一个Page一般为4K。 ^25462656-13-1483-1495
- ⏱ 2022-01-02 21:42:12
-
📌 对于缓冲I/O,一个读操作会有3次数据拷贝,一个写操作,有反向的3次数据拷贝:读:磁盘→内核缓冲区→用户缓冲区→应用程序内存;写:应用程序内存→用户缓冲区→内核缓冲区→磁盘。 ^25462656-13-1508-1621
- ⏱ 2022-01-02 21:43:23
-
📌 对于直接I/O,一个读操作,会有2次数据拷贝,一个写操作,有反向的2次数据拷贝:读:磁盘→内核缓冲区→应用程序内存;写:应用程序内存→内核缓冲区→磁盘。所以,所谓的“直接I/O”,其中直接的意思是指没有用户级的缓冲 ^25462656-13-1634-1780
- ⏱ 2022-01-02 21:44:45
-
📌 fflush和fsync的区别:fflush是缓冲I/O中的一个API,它只是把数据从用户缓冲区刷到内核缓冲区而已,fsync则是把数据从内核缓冲区刷到磁盘里。 ^25462656-13-2082-2162
- ⏱ 2022-01-02 21:49:53
4.2 内存映射文件与零拷贝
-
📌 直接拿应用程序的逻辑内存地址映射到 Linux 操作系统的内核缓冲区 ^25462656-14-627-661
- ⏱ 2022-01-02 22:08:34
-
📌 数据拷贝次数从缓冲I/O的3次,到直接I/O的2次,再到内存映射文件,变成了1次。读:磁盘→内核缓冲区;写:内核缓冲区→磁盘。 ^25462656-14-719-808
- ⏱ 2022-01-02 22:09:48
-
📌 但如果用零拷贝,可能连内核缓冲区到Socket缓冲区的拷贝也省略了。如图4-5所示,在内核缓冲区和Socket缓冲区之间并没有做数据拷贝,只是一个地址的映射,底层的网卡驱动程序要读取数据并发送到网络的时候,看似读的是Socket缓冲区的数据,但实际上直接读的是内核缓冲区中的数据。 ^25462656-14-2330-2470
- ⏱ 2022-01-02 22:20:52
-
📌 磁盘→内核缓冲区→应用程序内存→Socket缓冲区→网络。 ^25462656-14-1768-1797
- ⏱ 2022-01-02 22:26:09
-
📌 在这里,我们看到虽然叫零拷贝,实际是 2 次数据拷贝,1 次是从磁盘到内核缓冲区,1次是从内核缓冲区到网络。之所以叫零拷贝,是从内存的角度来看的,数据在内存中没有发生过数据拷贝,只在内存和I/O之间传输。 ^25462656-14-3049-3151
- ⏱ 2022-01-02 22:27:30
-
📌 对于把文件数据发送到网络的这个场景,直接I/O、内存映射文件、零拷贝对应的数据拷贝次数分别是4次、3次、2次,内存拷贝次数分别是2次、1次、0次。 ^25462656-14-3171-3244
- ⏱ 2022-01-02 22:29:03
4.3 网络I/O模型
-
📌 该函数是阻塞调用 ^25462656-15-1924-1932
- ⏱ 2022-01-02 22:52:49
-
📌 阻塞和非阻塞是从函数调用角度来说的,而同步和异步是从“读写是谁完成的”角度来说的。 ^25462656-15-3323-3364
- ⏱ 2022-01-02 23:00:49
-
📌 同步:读写由应用程序完成。异步:读写由操作系统完成,完成之后,回调或者事件通知应用程序。 ^25462656-15-3453-3510
- ⏱ 2022-01-02 23:01:23
-
📌 在本书后续的章节中提到的“异步I/O”,主要指应用层面的语境(底层可能是epoll,也可能是真正的异步I/O)。 ^25462656-15-4169-4225
- ⏱ 2022-01-02 23:10:09
-
📌 Reactor模式:主动模式。所谓主动,是指应用程序不断地轮询,询问操作系统或者网络框架、I/O是否就绪。 ^25462656-15-4541-4594
- ⏱ 2022-01-02 23:12:22
-
📌 Proactor模式:被动模式。应用程序把read和write函数操作全部交给操作系统或者网络框架,实际的 I/O 操作由操作系统或网络框架完成,之后再回调应用程序。 ^25462656-15-4703-4786
- ⏱ 2022-01-02 23:13:01
-
📌 整个服务器有1+N+M个线程,一个监听线程,N个I/O线程,M个Worker线程。N的个数通常等于CPU核数,M的个数根据上层决定,通常有几百个。 ^25462656-15-7562-7677
- ⏱ 2022-01-03 18:01:11
4.4 进程、线程和协程
-
📌 多进程模型的典型例子是Nginx。Nginx有一个Master进程,N个Worker进程,每个Worker进程对应一个CPU核,每个进程都是单线程的。Master进程不接收请求,负责管理功能;各个Worker 进程间相互独立,并行地接收客户端的请求,也不需要像多线程那样在不同的 CPU 核间切换。 ^25462656-16-2240-2396
- ⏱ 2022-01-03 20:16:36
-
📌 比如Redis就是单进程单线程的模型(这里说的单线程模型,不是指整个Redis服务器只有一个线程,而是指接收并处理客户端请求的线程只有一个)。之所以单线程可以支持,是因为在请求接收的地方用的是epoll的I/O多路复用,在请求处理的地方又完全是内存操作,没有磁盘或者网络I/O,所以只需单线程就足够了。要利用多核也很简单,开多个Redis实例就可以了。 ^25462656-16-2450-2626
- ⏱ 2022-01-03 20:23:05
4.5 无锁(内存屏障与CAS)
-
📌 使用了内存屏障。 ^25462656-17-1451-1459
- ⏱ 2022-01-03 20:45:53
-
📌 所谓“重排序”,通俗地讲,就是CPU不会按照开发者写的代码顺序来执行! ^25462656-17-1716-1751
- ⏱ 2022-01-03 20:46:54
-
📌 为什么会出现读不到最新的值?因为在多核CPU体系下,每个CPU有自己的缓存!改过的这个值可能还在CPU的缓存里,没有刷新到内存里。内存屏障就是要强制把这个值刷新到内存里面。 ^25462656-17-2045-2131
- ⏱ 2022-01-03 21:38:55
-
📌 读可以多线程,写必须单线程 ^25462656-17-1372-1385
- ⏱ 2022-01-03 21:55:56
-
📌 如果是多线程写,则内存屏障也不够用了,这时要用到CAS。 ^25462656-17-2470-2498
- ⏱ 2022-01-03 21:56:35
-
📌 基于单向链表,维护一头一尾两个引用:head和tail。入队,就是在队列的尾部追加节点,多个线程通过CAS互斥的操作tail;出队,就是移除队列的头部节点,多个线程通过CAS互斥的操作head。 ^25462656-17-3115-3212
- ⏱ 2022-01-03 22:31:17
-
📌 无锁队列 ^25462656-17-2925-2929
- ⏱ 2022-01-03 22:31:34
5.1 HTTP 1.0
-
📌 服务器会有一个Keep-Alive timeout参数,过一段时间之后,如果该连接上没有新的请求进来,则连接就会关闭。 ^25462656-19-1420-1479
- ⏱ 2022-01-04 10:20:42
-
📌 现在,即使一个请求处理完了,连接也不关闭,那么客户端怎么知道连接处理结束了呢? ^25462656-19-1566-1605
- ⏱ 2022-01-04 10:22:25
-
📌 答案是在HTTP Response的头部,返回了一个Content-Length:xxx的字段,这个字段可以告诉客户端HTTP Response的Body共有多少个字节,客户端接收到这么多个字节之后,就知道响应成功接收完毕。 ^25462656-19-1643-1755
- ⏱ 2022-01-04 10:26:33
5.2 HTTP 1.1
-
📌 但 Content-Length 有个问题,如果服务器返回的数据是动态语言生成的内容,则要计算Content-Length,这点对服务器来说比较困难。即使能够计算,也需要服务器在内存中渲染出整个页面,然后计算长度,非常耗时。 ^25462656-20-808-920
- ⏱ 2022-01-04 10:26:52
-
📌 为此,在HTTP 1.1中引用了Chunk机制(Http Streaming)。具体来说,就是在响应的头部加上Transfer-Encoding:chunked属性,其目的是告诉客户端,响应的Body是分成了一块块的,块与块之间有间隔符,所有块的结尾也有个特殊标记。这样,即使没有 Content-Length 字段,也能方便客户端判断出响应的末尾。 ^25462656-20-933-1108
- ⏱ 2022-01-04 10:27:42
-
📌 但 Pipeline 有个致命问题,就是Head-of-Line Blocking翻译成中文叫作“队头阻塞”。 ^25462656-20-2249-2304
- ⏱ 2022-01-04 10:39:11
-
📌 客户端发送的请求顺序是 1、2、3,虽然服务器是并发处理的,但客户端接收响应的顺序必须是 1、2、3,如此才能把响应和请求成功配对,跟队列一样,先进先出。 ^25462656-20-2323-2400
- ⏱ 2022-01-04 10:39:35
-
📌 有了“连接复用”之后,减少了建立连接、关闭连接的开销。但还存在一个问题,在同一个连接上,请求是串行的,客户端发送一个请求,收到响应,然后发送下一个请求,再收到响应。这种串行的方式,导致并发度不够。为此,HTTP 1.1引入了Pipeline机制。 ^25462656-20-1721-1857
- ⏱ 2022-01-04 10:41:49
-
📌 “一来多回”问题 ^25462656-20-3953-3961
- ⏱ 2022-01-04 10:51:38
-
📌 HTTP长轮询客户端发送一个HTTP请求,如果服务器有新消息,就立即返回;如果没有,则服务器夯住此连接,客户端一直等该请求返回。然后过一个约定的时间之后,如果服务器还没有新消息,服务器就返回一个空消息(客户端和服务器约定好的一个消息)。客户端收到空消息之后关闭连接,再发起一个新的连接,重复此过程。 ^25462656-20-4284-4450
- ⏱ 2022-01-04 10:59:50
-
📌 断点续传 ^25462656-20-4862-4866
- ⏱ 2022-01-04 11:11:23
-
📌 一旦连接中断了,重新建立连接之后,在请求的头部加上Range:first offset-last offset 字段,指定从某个offset下载到某个offset,服务器就可以只返回(first offset,last offset)之间的数据。 ^25462656-20-4949-5072
- ⏱ 2022-01-04 11:11:49
5.3 HTTP/2
-
📌 HTTP 1.1本身是明文的字符格式,所谓的二进制分帧,是指在把这个字符格式的报文给TCP之前转换成二进制,并且分成多个帧(多个数据块)来发送。 ^25462656-21-1985-2057
- ⏱ 2022-01-04 11:19:24
-
📌 有了二进制分帧,是不是就彻底解决了 Pipeline 的“队头阻塞”问题呢?其实还没有。只是把“队头阻塞”问题从HTTP Request粒度细化到了“帧”粒度。 ^25462656-21-3127-3207
- ⏱ 2022-01-04 11:38:31
-
📌 只要用 TCP 协议,就绕不开“队头阻塞”问题,因为 TCP 协议是先进先出的! ^25462656-21-3207-3260
- ⏱ 2022-01-04 11:47:10
-
📌 头部压缩。 ^25462656-21-3994-3999
- ⏱ 2022-01-04 11:57:59
-
📌 HPACK协议 ^25462656-21-4141-4148
- ⏱ 2022-01-04 11:58:13
5.4 SSL/TLS
-
📌 服务器把个人信息+服务器的公钥发给 CA,CA 用自己的私钥为服务器生成一个数字证书。 ^25462656-22-6781-6824
- ⏱ 2022-01-04 14:57:13
-
📌 Root CA机构都是一些世界上公认的机构,在用户的操作系统、浏览器发布的时候,里面就已经嵌入了这些机构的Root证书。你信任这个操作系统,信任这个浏览器,也就信任了这些Root 证书。 ^25462656-22-7784-7877
- ⏱ 2022-01-04 15:00:40
-
📌 SSL/TLS四次握手示意图 ^25462656-22-9153-9167
- ⏱ 2022-01-04 15:04:22
5.6 TCP/UDP
-
📌 三次握手恰好可以保证客户端和服务器对自己的发送、接收能力做了一次确认。 ^25462656-24-6054-6089
- ⏱ 2022-01-04 16:53:01
-
📌 这会导致,之前闲逛的数据包在新连接打开后被当作新的数据包,这样一来,老连接上的数据包会“串”到新连接上面,这是不能接受的。 ^25462656-24-7542-7603
- ⏱ 2022-01-04 17:03:29
-
📌 任何一个IP数据包在网络上逗留的最长时间是MSL,这个值默认是120s。 ^25462656-24-7677-7713
- ⏱ 2022-01-04 17:04:23
-
📌 如果超出了这个时间,中间的路由节点就会把该数据包丢弃。 ^25462656-24-7744-7771
- ⏱ 2022-01-04 17:05:22
-
📌 第四次数据包的传输时间+服务器重新发送第三次数据包的时间,最长是两个MSL,所以要让客户端在TIME_WAIT状态等待2×MSL的时间。 ^25462656-24-8005-8073
- ⏱ 2022-01-04 17:08:14
-
📌 为什么不让服务器也进入TIME_WAIT状态呢?原因是没有必要。任何一个连接都是一个4元组,同时关联了客户端和服务器,客户端处于TIME_WAIT状态后,意味着这个连接要到2×MSL时间之后才能重新启用,服务器端即使想立马使用也无法实现。 ^25462656-24-8158-8277
- ⏱ 2022-01-04 17:10:22
-
📌 不要让服务器主动关闭连接。这样服务器的连接就不会处于TIME_WAIT状态。· 客户端做连接池,复用连接,而不要频繁地创建和关闭 ^25462656-24-8424-8501
- ⏱ 2022-01-04 17:28:16
5.7 QUIC
-
📌 每发送5个数据包,就发送一个冗余包。 ^25462656-25-1319-1337
- ⏱ 2022-01-04 17:30:02
-
📌 每5个数据块生成两个冗余块,这就允许每5个块当中丢失两个。 ^25462656-25-1817-1846
- ⏱ 2022-01-04 17:30:26
-
📌 对于QUIC来说,它采用了RAID5,目前是每发送10个数据包,构建一个冗余包,允许每10个当中丢失一个。如果10个当中丢失了两个呢?那就要回到TCP的老办法——重传。通过合理的设置冗余比,QUIC减小了数据重传的概率。 ^25462656-25-2376-2486
- ⏱ 2022-01-04 17:32:39
-
📌 QUIC 协议也可以创造一个逻辑上的连接。具体做法是,不再以4元组来标识连接,而是让客户端生成一个64位的数字标识连接,虽然客户端的IP和Port在漂移,但64位的数字没有变化,这条连接就会存在。这样,对于上层应用来说,就感觉连接一直存在,没有中断过。 ^25462656-25-3117-3243
- ⏱ 2022-01-04 17:38:43
6.2 分库分表
-
📌 如果是读多写少,可以通过加从库、加缓存解决,不一定要分库分表。如果是读少写多,或者说写入的QPS已经达到了数据库的瓶颈,这时就要考虑分库分表了。 ^25462656-27-786-858
- ⏱ 2022-01-04 17:47:49
-
📌 在分库分表之后,需要一个全局的ID生成服务。 ^25462656-27-1157-1179
- ⏱ 2022-01-04 17:50:09
-
📌 把订单ID和用户ID统一成一个维度,比如把用户ID作为订单ID中的某几位,这样订单ID中就包含了用户ID信息,然后按照用户ID 分库,当按订单ID查询的时候,截取出用户ID,再按用户 ID查询 ^25462656-27-2078-2174
- ⏱ 2022-01-04 18:00:02
-
📌 还是两套表,只是业务单写。然后通过监听Binlog,同步到另外一套表上。 ^25462656-27-1996-2032
- ⏱ 2022-01-04 18:01:07
-
📌 同一份数据,两套分库分表。一套按用户ID切分,一套按商户ID 切分。 ^25462656-27-1904-1938
- ⏱ 2022-01-05 11:51:28
-
📌 建立辅助维度和主维度之间的映射关系(商户 ID 和用户 ID 之间的映射关系)。查询的时候根据商户ID查询映射表,得到用户ID;再根据用户ID查询订单ID。 ^25462656-27-1662-1740
- ⏱ 2022-01-05 11:51:29
-
📌 对于在分库分表之后其他维度的查询,一般有以下几个方法: ^25462656-27-1593-1620
- ⏱ 2022-01-05 11:51:30
6.3 B+树
-
📌 非主键索引的叶子节点存储的不是记录的指针,而是主键的值。所以,对于非主键索引的查询,会查询两棵B+树,先在非主键索引的B+树上定位主键,再用主键去主键索引的B+树上找到最终记录。 ^25462656-28-4105-4194
- ⏱ 2022-01-05 11:51:27
-
📌 块是InnoDB读写磁盘的基本单位,InnoDB每一次磁盘I/O,读取的都是16KB的整数倍的数据。 ^25462656-28-2210-2260
- ⏱ 2022-01-05 11:51:29
-
📌 Page与Page之间组成双向链表,每一个Page头部有两个关键字段:前一个Page的编号,后一个 Page 的编号。Page 里面存储一条条的记录,记录之间用单向链表串联,最终所有的记录形成图6-1所示的双向链表的逻辑结构。 ^25462656-28-2922-3035
- ⏱ 2022-01-05 11:51:31
6.4 事务与锁
-
📌 事务并发导致的几类问题[插图] ^25462656-29-796-812
- ⏱ 2022-01-05 12:05:44
-
📌 既然默认的隔离级别是3(RR),如何解决最后一个问题,丢失更新呢?这涉及下面要讲的悲观锁和乐观锁。 ^25462656-29-1398-1447
- ⏱ 2022-01-05 12:35:53
-
📌 悲观锁,就是认为数据发生并发冲突的概率很大,所以读之前就上锁。利用select xxx for update语句 ^25462656-29-3136-3192
- ⏱ 2022-01-05 13:12:44
-
📌 对于乐视锁,认为数据发生并发冲突的概率比较小,所以读之前不上锁。等到写回去的时候再判断数据是否被其他事务改了,即多线程里面经常会讲的CAS(Comapre And Set)的思路。 ^25462656-29-3928-4018
- ⏱ 2022-01-05 13:14:37
-
📌 在实现层面,就是利用update语句的原子性实现了CAS,当且仅当version=v1时,才能把balance更新成功。在更新balance的同时,version也必须加1。version的比较、version的加1、balance的更新,这三件事情都是在一条update语句里面完成的,这是这个事情的关键所在! ^25462656-29-4838-4994
- ⏱ 2022-01-05 13:22:41
-
📌 乐观锁的方案可以很好地应对上述场景,但有一个限制是select和update的是同一张表的同一条记录 ^25462656-29-5339-5389
- ⏱ 2022-01-05 13:29:08
6.5 事务实现原理之1:Redo Log
-
📌 具体到InnoDB中,Write-Ahead Log是Redo Log。在InnoDB中,不光事务修改的数据库表数据是异步刷盘的,连Redo Log的写入本身也是异步的。如图6-7所示,在事务提交之后,Redo Log先写入到内存中的Redo Log Buffer中,然后异步地刷到磁盘上的Redo Log。 ^25462656-30-1912-2066
- ⏱ 2022-01-05 15:10:50
-
📌 1:每提交一个事务,就刷一次磁盘(这个最安全)。 ^25462656-30-2223-2247
- ⏱ 2022-01-05 15:15:52
-
📌 先在内存中提交事务,然后写日志(所谓的Write-ahead Log),然后后台任务把内存中的数据异步刷到磁盘。 ^25462656-30-1607-1663
- ⏱ 2022-01-05 15:40:44
-
📌 明明是先在内存中提交事务,后写的日志,为什么叫作Write-Ahead呢?这里的Ahead,其实是指相对于真正的数据刷到磁盘,因为是先写的日志,后把内存数据刷到磁盘,所以叫Write-Ahead Log。 ^25462656-30-1707-1809
- ⏱ 2022-01-05 15:41:00
-
📌 物理上面,一个固定的文件大小,每 512 个字节一个 Block,循环使用。 ^25462656-30-3766-3804
- ⏱ 2022-01-05 15:44:26
-
📌 假设底层没有保证512个字节的原子性,可以通过在日志中加入checksum解决。通过checksum能判断出宕机之后重启,一个Log Block是否完整。如果不完整,就可以丢弃这个LogBlock,对日志来说,就是做截断操作。 ^25462656-30-5584-5697
- ⏱ 2022-01-05 16:06:52
-
📌 Double write。把16KB写入到一个临时的磁盘位置,写入成功后再拷贝到目标磁盘位置。 ^25462656-30-6109-6156
- ⏱ 2022-01-05 17:51:29
-
📌 应用层所说的事务都是“逻辑事务”,具体到底层实现,是“物理事务”,也叫作Mini Transaction(Mtr)。 ^25462656-30-7533-7591
- ⏱ 2022-01-05 18:28:43
-
📌 Redo Log采用了哪种记法呢?它采用了逻辑和物理的综合体,就是先以Page为单位记录日志,每个Page里面再采取逻辑记法(记录Page里面的哪一行被修改了)。这种记法有个专业术语,叫Physiological Logging。 ^25462656-30-4565-4680
- ⏱ 2022-01-05 18:35:59
-
📌 记法2。类似Binlog的RAW格式,记录每张表的每条记录的修改前的值、修改后的值,类似(表,行,修改前的值,修改后的值)。 ^25462656-30-4275-4337
- ⏱ 2022-01-05 18:38:20
-
📌 为Redo Log也恢复不了。因为Redo Log是Physiological Logging,里面只是一个对Page的修改的逻辑记录,Redo Log记录了哪个地方修改了,但不知道哪个地方损坏了。 ^25462656-30-5884-5983
- ⏱ 2022-01-05 18:39:16
-
📌 客户端提交了Rollback,数据库并没有更改之前的数据,而是以相反的方向生成了三个新的SQL语句,然后Commit,所以是逻辑层面上的回滚,而不是物理层面的回滚。 ^25462656-30-9510-9592
- ⏱ 2022-01-05 22:04:56
-
📌 同样,如果宕机时一个事务执行了一半,在重启、回滚的时候,也并不是删除之前的部分,而是以相反的操作把这个事务“补齐”,然后Commit ^25462656-30-9855-9921
- ⏱ 2022-01-05 22:05:56
-
📌 这种逆向操作的SQL语句对应到Redo Log里面,叫作Compensation Log Record(CLR), ^25462656-30-10273-10330
- ⏱ 2022-01-05 22:07:11
-
📌 活跃事务表是当前所有未提交事务的集合,每个事务维护了一个关键变量lastLSN,是该事务产生的日志中最后一条日志的LSN。 ^25462656-30-11440-11501
- ⏱ 2022-01-05 22:31:04
-
📌 脏页表是当前所有未刷到磁盘上的Page的集合(包括了已提交的事务和未提交的事务),recoveryLSN是导致该Page为脏页的最早的LSN。 ^25462656-30-11709-11780
- ⏱ 2022-01-05 22:31:48
-
📌 在内存中,维护了两个关键的表 ^25462656-30-11388-11402
- ⏱ 2022-01-05 22:35:29
-
📌 所谓的Fuzzy Checkpoint,就是对这两个关键表做了一个Checkpoint,而不是对数据本身做Checkpoint。 ^25462656-30-12094-12158
- ⏱ 2022-01-05 22:46:20
-
📌 从Checkpoint开始,一直遍历到Redo Log末尾,一旦遇到Redo Log操作的是新的Page,就把它加入脏页集合 ^25462656-30-12951-13013
- ⏱ 2022-01-05 22:51:16
-
📌 从Checkpoint2到Crash,这个集合会只增不减。可能P1、P2在Checkpoint之后已经不是脏页了,但把它认为是脏页也没关系,因为Redo Log是幂等的。 ^25462656-30-13056-13141
- ⏱ 2022-01-05 22:54:24
-
📌 取集合中所有脏页的recoveryLSN的最小值,得到firstLSN。从firstLSN遍历Redo Log到末尾,把每条Redo Log对应的Page全部重刷一次磁盘。 ^25462656-30-13461-13547
- ⏱ 2022-01-05 22:56:25
-
📌 关键是如何做幂等?磁盘上的每个Page有一个关键字段——pageLSN。这个LSN记录的是这个 Page 刷盘时最后一次修改它的日志对应的 LSN。如果重放日志的时候,日志的 LSN <=pageLSN,则不修改日志对应的Page,略过此条日志。 ^25462656-30-13560-13683
- ⏱ 2022-01-05 23:01:31
-
📌 所谓的Undo,是指每遇到一条属于T3、T4、T5的Log,就生成一条逆向的SQL语句来执行,其执行对应的Redo Log是Compensation Log Record(CLR),会在Redo Log尾部继续追加。 ^25462656-30-14671-14779
- ⏱ 2022-01-05 23:34:40
-
📌 在阶段1,我们已经找出了未提交事务集合{T3,T4,T5}。 ^25462656-30-14558-14588
- ⏱ 2022-01-05 23:34:45
-
📌 损坏的Page。 ^25462656-30-5826-5834
- ⏱ 2022-01-05 23:44:30
-
📌 Redo Log其实是一个固定大小的文件,循环使用,写到尾部之后,回到头部覆写 ^25462656-30-3160-3199
- ⏱ 2022-01-05 23:49:14
-
📌 之所以能覆写,因为一旦 Page 数据刷到磁盘上,日志数据就没有存在的必要了。 ^25462656-30-3239-3278
- ⏱ 2022-01-05 23:49:21
-
📌 0:每秒刷一次磁盘, ^25462656-30-2163-2173
- ⏱ 2022-01-05 23:50:37
-
📌 了日志写入有原子性问题,数据写入的原子性问题更大。一个Page有16KB,往磁盘上刷盘,如果刷到一半系统宕机再重启 ^25462656-30-5711-5768
- ⏱ 2022-01-05 23:56:47
-
📌 Redo Log不保证事务的原子性,而是保证了持久性。 ^25462656-30-15852-15879
- ⏱ 2022-01-06 00:00:46
6.6 事务实现原理之2:Undo Log
-
📌 Steal是指未提交的事务也能写入 ^25462656-31-1623-1640
- ⏱ 2022-01-06 00:11:57
-
📌 Force是指已经提交的事务必须强制写入磁盘。 ^25462656-31-1748-1771
- ⏱ 2022-01-06 00:22:17
-
📌 除了在宕机恢复时对未提交的事务进行回滚,Undo Log还有两个核心作用:(1)实现ACID中I(隔离性)。(2)高并发。 ^25462656-31-2615-2702
- ⏱ 2022-01-06 00:27:06
-
📌 表6-11 并发读写的三种策略[插图] ^25462656-31-2912-2932
- ⏱ 2022-01-06 00:40:43
-
📌 InnoDB 用的就是CopyOnWrite思想,是在Undo Log里面实现的。每个事务修改记录之前,都会先把该记录拷贝一份出来,拷贝出来的这个备份存在Undo Log里。 ^25462656-31-3224-3311
- ⏱ 2022-01-06 01:15:40
-
📌 Undo Log维护了数据的从旧到新的每个版本,各个版本之间的记录通过链表串联。 ^25462656-31-3348-3388
- ⏱ 2022-01-06 01:15:54
-
📌 通常的select语句都是不加锁的,读取的全部是数据的历史版本,从而支撑高并发的查询。 ^25462656-31-3528-3571
- ⏱ 2022-01-06 01:24:14
-
📌 快照读就是最常用的select语句,当前读包括了加锁的select语句和insert/update/delete语句。 ^25462656-31-3624-3737
- ⏱ 2022-01-06 01:24:37
-
📌 一旦事务Commit了,就可以删掉Undo Log。 ^25462656-31-4291-4317
- ⏱ 2022-01-06 01:27:22
-
📌 Undo Log其实没有顺序,多个事务是并行地向Undo Log中随机写入的。 ^25462656-31-4171-4210
- ⏱ 2022-01-06 01:28:18
-
📌 Undo Log这个词有很大的迷惑性,它其实不是Log,而是数据。为什么这么说? ^25462656-31-4068-4108
- ⏱ 2022-01-06 01:31:57
-
📌 所以,更应该把Undo Log叫作记录的“备份数据”,即在事务未提交之前的时间里的“备份数据”! ^25462656-31-4520-4568
- ⏱ 2022-01-06 01:32:14
-
📌 Page中的每条记录,除了自身的主键ID和数据外,还有两个隐藏字段:一个是修改该记录的事务ID,一个是rollback_ptr,用来串联所有的历史版本。 ^25462656-31-4636-4712
- ⏱ 2022-01-06 01:33:42
-
📌 修改记录之前先把记录拷贝一份出来,然后拷贝出来的这些历史版本形成一个链表 ^25462656-31-5376-5412
- ⏱ 2022-01-06 01:39:22
-
📌 回滚段 ^25462656-31-5296-5299
- ⏱ 2022-01-06 01:39:40
-
📌 可以把Undo Log也当作数据!在内存中记录Undo Log,异步地刷盘,宕机重启,用Redo Log恢复Undo Log。 ^25462656-31-5709-5772
- ⏱ 2022-01-06 02:35:04
-
📌 在这里,所有Undo Log和Redo Log的写入都可以只在内存中进行,只要保证Commit之后Redo Log落盘即可,Undo Log可以一直保留在内存里,之后异步刷盘。 ^25462656-31-6207-6295
- ⏱ 2022-01-06 02:36:00
-
📌 MVCC解决了快照读和写之间的并发问题,但对于写和写之间、当前读和写之间的并发,MVCC就无能为力了,这时就需要用到锁。 ^25462656-31-6411-6471
- ⏱ 2022-01-06 02:37:07
-
📌 按锁的粒度来分,可分为锁表、锁行、锁一个Gap(一个范围);按锁的模式来分,可分为共享、排他、意向等; ^25462656-31-6936-7000
- ⏱ 2022-01-06 02:39:08
-
📌 共享锁(S)和排他锁(X)是读写锁的另外一种叫法 ^25462656-31-7321-7362
- ⏱ 2022-01-06 10:22:46
-
📌 意向锁就是为了解决这个锁的判断效率问题产生的。意向锁是专门加在表上 ^25462656-31-7794-7827
- ⏱ 2022-01-06 10:31:37
-
📌 一个事务要给某张表的某一行加S锁,必须先获得整张表的IS锁;要给某张表的某一行加X锁,必须先获得整张表的IX锁。 ^25462656-31-7921-7977
- ⏱ 2022-01-06 10:31:47
-
📌 所有的IX锁、IS锁之间都不互斥,IX锁、IS锁只是为了和表共享锁、表排他锁进行互斥。 ^25462656-31-8084-8127
- ⏱ 2022-01-06 10:33:17
-
📌 意向锁实际上是表(共享锁、排他锁)和行(共享锁、排他锁)之间的桥梁,通过意向锁来串起两个不同粒度(表、行)的锁之间如何做互斥判断。 ^25462656-31-8629-8694
- ⏱ 2022-01-06 10:35:53
-
📌 自增锁是一种表级别的锁,专门针对AUTO_INCREMENT的列。 ^25462656-31-8747-8780
- ⏱ 2022-01-06 10:37:59
-
📌 锁Gap是和锁行密切相关的,Gap肯定建立在某一行的基准之上,所以往往又把锁Gap当作锁行的不同算法来看待 ^25462656-31-9362-9415
- ⏱ 2022-01-06 10:40:31
-
📌 间隙锁(Gap Lock)。只是锁一个范围,不包括记录本身,也是一个开区间,目的是避免另外一个事务在这个区间上插入新记录。 ^25462656-31-9432-9493
- ⏱ 2022-01-06 10:43:31
-
📌 临键锁(Next-Key Lock)。Gap Lock与Record Lock的综合不仅锁记录,也锁记录之前的范围。 ^25462656-31-9509-9567
- ⏱ 2022-01-06 10:44:28
-
📌 插入意向锁也是一种Gap锁,专门针对Insert操作。多个事务在同一索引、同一个范围区间内可以并发插入,即插入意向锁之间并不互相阻碍。 ^25462656-31-9612-9679
- ⏱ 2022-01-06 10:47:04
-
📌 锁Gap,一个主要目的是避免幻读。 ^25462656-31-9771-9788
- ⏱ 2022-01-06 10:54:26
-
📌 锁Gap往往针对非唯一索引 ^25462656-31-9830-9843
- ⏱ 2022-01-06 10:54:53
-
📌 通过Undo Log+Redo Log实现事务的A(原子性)和D(持久性);通过“MVCC+锁”实现了事务的I(隔离性)和并发性。 ^25462656-31-10052-10130
- ⏱ 2022-01-06 10:57:43
6.7 Binlog与主从复制
-
📌 MySQL是一个能支持多种存储引擎的数据库,InnoDB只是其中一种(当然,也是最主要的一种)。Redo Log和Undo Log是InnoDB引擎里面的工具,但Binlog是MySQL层面的东西。 ^25462656-32-650-749
- ⏱ 2022-01-06 11:07:43
-
📌 不同于Redo Log和Undo Log用来实现事务,Binlog的主要作用是做主从复制,如果是单机版的,没有主从复制,也可以不写Binlog。 ^25462656-32-762-834
- ⏱ 2022-01-06 11:07:52
-
📌 在互联网应用中,Binlog有了第二个用途:一个应用进程把自己伪装成Slave,监听Master 的Binlog,然后把数据库的变更以消息的形式抛出来,业务系统可以消费消息,执行对应的业务逻辑操作,比如更新缓存。 ^25462656-32-837-943
- ⏱ 2022-01-06 11:10:38
-
📌 为了不丢失数据,一般都建议双 1 保证,即 sync_binlog 和innodb_flush_log_at_trx_commit的值都取为1。 ^25462656-32-1195-1267
- ⏱ 2022-01-06 11:13:10
-
📌 表6-13 Binlog与Redo Log的详细对比[插图] ^25462656-32-1384-1415
- ⏱ 2022-01-06 11:16:21
-
📌 1:每提交一个事务,刷一次磁盘。 ^25462656-32-1123-1139
- ⏱ 2022-01-06 11:19:06
-
📌 Binlog 全局只有一份 ^25462656-32-1724-1737
- ⏱ 2022-01-06 11:24:05
-
📌 虽然 Binlog 只能串行地写入,但不需要提交一个事务刷一次磁盘,而是把事务的提交和刷盘放到不同的线程里,刷盘时可以对多个提交的事务同时刷盘,虽然还是串行,但是批量化了。 ^25462656-32-1939-2025
- ⏱ 2022-01-06 11:24:27
-
📌 Group Commit ^25462656-32-1855-1867
- ⏱ 2022-01-06 11:26:24
-
📌 下Binlog自身写入的原子性问题:Binlog刷盘到一半,出现宕机。 ^25462656-32-2259-2294
- ⏱ 2022-01-06 11:28:15
-
📌 内部分布式事务是Binlog和Redo Log之间的事务,使用的是经典的2阶段提交方案(2PC,2 Phase Commit)。 ^25462656-32-2523-2587
- ⏱ 2022-01-06 11:29:38
-
📌 如何实现Binlog和Redo Log的数据一致性 ^25462656-32-2436-2461
- ⏱ 2022-01-06 11:30:00
-
📌 通过类似于Checksum的办法或者Binlog中有结束标记,来判断出这是部分的、不完整的Binlog,把最后一段截掉。对于客户端来说,此时宕机,事务肯定是没有成功提交的,所以截掉也没有问题。 ^25462656-32-2323-2419
- ⏱ 2022-01-06 12:01:44
-
📌 图6-19展示了一个事务的2阶段提交过程, ^25462656-32-2600-2621
- ⏱ 2022-01-06 12:03:56
-
📌 阶段1:InnoDB的Prepare,是在把事务提交之前,对应的Redo Log和Undo Log全部都写入了。Binlog也已经写入到内存,只等刷盘。 ^25462656-32-2647-2723
- ⏱ 2022-01-06 12:06:11
-
📌 阶段2:收到客户端的Commit指令,先刷盘Binlog,然后让InnoDB执行Commit。 ^25462656-32-2975-3022
- ⏱ 2022-01-06 12:06:18
-
📌 如果发生宕机,如何恢复? ^25462656-32-3114-3126
- ⏱ 2022-01-06 12:10:19
-
📌 以 Binlog 为准,让Redo Log向Binlog“靠齐 ^25462656-32-3174-3205
- ⏱ 2022-01-06 12:14:32
-
📌 表6-14 MySQL的三种主从复制方式 ^25462656-32-4025-4045
- ⏱ 2022-01-06 12:16:50
-
📌 很多时候大家用的是半同步复制。 ^25462656-32-3699-3714
- ⏱ 2022-01-06 13:33:01
-
📌 半同步复制可能退化为异步复制。因为Master不可能无限期地等Slave,当超过某个时间,Slave还没有回复ACK时,Master就会切换为异步复制模式。 ^25462656-32-3746-3824
- ⏱ 2022-01-06 13:33:53
-
📌 无论异步复制,还是半异步复制(可能退化为异步复制),都可能在主从切换的时候丢数据。业务一般的做法是牺牲一致性来换取高可用性,即在Master宕机后切换到Slave,忍受少量的数据丢失,后续再人工修复。 ^25462656-32-4219-4319
- ⏱ 2022-01-06 13:37:57
-
📌 图6-20 原生的MySQL主从复制的原理 ^25462656-32-4943-4964
- ⏱ 2022-01-06 13:46:52
-
📌 所谓的并行复制,准确说是并行回放,因为传输环节还是单线程的。之所以传输环节没有用多线程,主要 ^25462656-32-5270-5316
- ⏱ 2022-01-06 13:48:30
-
📌 所谓并行回放,就是一次性从 RelayLog中拿出多个事务,并行地执行。 ^25462656-32-5478-5514
- ⏱ 2022-01-06 13:55:05
-
📌 不同库的事务可以并行,不同表的事务可以并行,不同行的事务可以并行。 ^25462656-32-5600-5633
- ⏱ 2022-01-06 13:55:13
-
📌 一个事务还没有结束之前,另外一个事务也开始进入提交阶段,说明这两个事务是在并行的,它们操作的是肯定是不同的数据记录。 ^25462656-32-5829-5887
- ⏱ 2022-01-06 13:59:20
第3部分 技术架构之道
- 📌 几大类重要技术问题,如高并发、高可用、稳定性、一致性, ^25462656-37-576-603
- ⏱ 2022-01-06 14:10:44
第8章 高并发问题
-
📌 场景2:支付系统和微信红包 ^25462656-38-3821-3834
- ⏱ 2022-01-06 14:24:17
-
📌 同时侧重于“高并发读”和“高并发写”的系统 ^25462656-38-3266-3287
- ⏱ 2022-01-06 14:24:27
-
📌 场景1:电商的库存系统和秒杀系统 ^25462656-38-3306-3322
- ⏱ 2022-01-06 14:24:34
8.2 高并发读
-
📌 缓存穿透。虽然缓存没有宕机,但是某些Key发生了大量查询,并且这些Key都不在缓存里,导致短时间内大量请求写入并压垮数据库。 ^25462656-39-1109-1171
- ⏱ 2022-01-06 14:36:27
-
📌 缓存雪崩。即缓存的高可用问题。如果缓存宕机,是否会导致所有请求全部写入并压垮数据库呢? ^25462656-39-1032-1075
- ⏱ 2022-01-06 14:39:28
-
📌 大量的热Key过期。和第二个问题类似 ^25462656-39-1187-1205
- ⏱ 2022-01-06 14:42:55
-
📌 这些问题和缓存的回源策略有关:一种是不回源,只查询缓存,缓存没有,直接返回给客户端为空,这种方式肯定是主动更新缓存,并且不设置缓存的过期时间,不会有缓存穿透、大量热Key过期问题;另一种是回源,缓存没有,要再查询数据库更新缓存,这种需要考虑应对上面的问题。 ^25462656-39-1249-1377
- ⏱ 2022-01-06 14:44:01
-
📌 客户端首先给服务端发送一个请求,并等待服务端返回的响应;如果客户端在一定的时间内没有收到服务端的响应,则马上给另一台(或多台)服务器发送同样的请求;客户端等待第一个响应到达之后,终止其他请求的处理。上面“一定的时间”定义为:95%请求的响应时间。 ^25462656-39-3337-3460
- ⏱ 2022-01-06 14:58:04
-
📌 冗余请求。 ^25462656-39-3256-3261
- ⏱ 2022-01-06 14:58:18
-
📌 改成重写轻读,不是查询的时候再去聚合,而是提前为每个user_id准备一个Feeds流,或者叫收件箱。 ^25462656-39-4925-4976
- ⏱ 2022-01-06 17:22:10
-
📌 每个用户都有一个发件箱和收件箱。假设某个用户有1000个粉丝,发布1条微博后,只写入自己的发件箱就返回成功。然后后台异步地把这条微博推送到1000个粉丝的收件箱,也就是“写扩散”。 ^25462656-39-4997-5087
- ⏱ 2022-01-06 19:28:01
-
📌 这里的关键问题是收件箱是如何实现的?因为从理论上来说,这是个无限长的列表。 ^25462656-39-5390-5427
- ⏱ 2022-01-06 19:37:39
-
📌 很显然,这个列表必须在内存里面。 ^25462656-39-5440-5456
- ⏱ 2022-01-06 19:39:25
-
📌 假设设置一个上限为2000。 ^25462656-39-5524-5538
- ⏱ 2022-01-06 19:39:42
-
📌 Redis只能保存最近的2000个,2000个以前的数据如何持久化地存储并且支持分页查询呢? ^25462656-39-5809-5855
- ⏱ 2022-01-06 19:39:50
-
📌 需要同时按 user_id和时间范围进行分片。 ^25462656-39-6168-6191
- ⏱ 2022-01-06 19:43:14
-
📌 但分完之后,如何快速地查看某个user_id 从某个offset开始的微博呢? ^25462656-39-6204-6243
- ⏱ 2022-01-06 19:43:32
-
📌 另外要有一张表,记录<user_id,月份,count>。也就是每个user_id在每个月份发表的微博总数。 ^25462656-39-6330-6384
- ⏱ 2022-01-06 19:44:45
-
📌 假设一个用户的粉丝很多,给每个粉丝的收件箱都复制一份,计算量和延迟都很大。 ^25462656-39-6467-6504
- ⏱ 2022-01-06 19:45:47
-
📌 这就又回到了最初的思路,也就是读的时候实时聚合,或者叫作“拉”。 ^25462656-39-6577-6609
- ⏱ 2022-01-06 19:45:55
-
📌 对于读的一端,一个用户的关注的人当中,有的人是推给他的(粉丝数少于 5000),有的人是需要他去拉的(粉丝数大于5000),需要把两者聚合起来,再按时间排序,然后分页显示,这就是“推拉结合”。 ^25462656-39-6823-6932
- ⏱ 2022-01-06 19:48:53
-
📌 可以另外准备一张宽表:把要关联的表的数据算好后保存在宽表里。依据实际情况,可以定时算,也可能任何一张原始表发生变化之后就触发一次宽表数据的计算。 ^25462656-39-7367-7439
- ⏱ 2022-01-06 19:57:36
-
📌 也可以用ES类的搜索引擎来实现:把多张表的Join结果做成一个个的文档,放在搜索引擎里面,也可以灵活地实现排序和分页查询功能。 ^25462656-39-7452-7608
- ⏱ 2022-01-06 19:59:15
-
📌 多表的关联查询:宽表与搜索引擎 ^25462656-39-6954-6969
- ⏱ 2022-01-06 20:00:17
-
📌 无论加缓存、动静分离,还是重写轻读,其实本质上都是读写分离,这也就是微服务架构里经常提到的CQRS(Command Query Responsibility Separation)。 ^25462656-39-7643-7734
- ⏱ 2022-01-06 20:01:56
-
📌 读和写的串联。定时任务定期把业务数据库中的数据转换成适合高并发读的数据结构;或者是写的一端把数据的变更发送到消息中间件,然后读的一端消费消息;或者直接监听业务数据库中的Binlog,监听数据库的变化来更新读的一端的数据。 ^25462656-39-8281-8391
- ⏱ 2022-01-06 20:09:49
-
📌 读比写有延迟。因为左边写的数据是在实时变化的,右边读的数据肯定会有延迟,读和写之间是最终一致性,而不是强一致性,但这并不影响业务的正常运行。 ^25462656-39-8407-8477
- ⏱ 2022-01-06 20:13:22
-
📌 但等用户下单的一刻,会去实时地扣减数据库里面的库存,也就是左边的写是“实时、完全准确”的,即使右边的读有一定时间延迟也没有影响。 ^25462656-39-8558-8622
- ⏱ 2022-01-06 20:14:11
-
📌 这里需要做一个补充:对于用户自己的数据,自己写自己读(比如账号里面的钱、用户下的订单),在用户体验上肯定要保证自己修改的数据马上能看到。这种在实现上读和写可能是完全同步的(对一致性要求非常高,比如涉及钱的场景);也可能是异步的,但要控制读比写的延迟非常小,用户感知不到。 ^25462656-39-8723-8871
- ⏱ 2022-01-06 20:15:39
-
📌 抽象地来看,数据通道传输的是日志流,消费日志的一端只是一个状态机。 ^25462656-39-8948-8981
- ⏱ 2022-01-06 20:16:09
8.3 高并发写
-
📌 任务分片是对处理程序本身进行分片。 ^25462656-40-1633-1650
- ⏱ 2022-01-06 20:24:32
-
📌 接口的异步有两种实现方式:· 假异步。在接口内部做一个线程池,把异步接口调用转化为同步接口调用。· 真异步。在接口内部通过NIO实现真的异步,不需要开很多的线程。 ^25462656-40-4636-4743
- ⏱ 2022-01-06 20:37:45
-
📌 对于“异步”而言,站在客户端的角度来讲,是请求服务器做一个事情,客户端不等结果返回,就去做其他的事情,回头再去轮询,或者让服务器回调通知。站在服务器角度来看,是接收到一个客户的请求之后不立即处理,也不立马返回结果,而是在“后台慢慢地处理”,稍后返回结果。 ^25462656-40-5092-5219
- ⏱ 2022-01-06 20:42:17
-
📌 写内存+Write-Ahead日志的这种思路 ^25462656-40-9232-9254
- ⏱ 2022-01-07 00:24:29
-
📌 Leader并不会主动给两个Follower同步数据,而是等Follower主动拉取,并且是批量拉取。 ^25462656-40-10298-10349
- ⏱ 2022-01-07 00:30:48
-
📌 只有等两个Follower把消息msg1拖过去后,Leader才会返回客户端说msg1接收成功了。 ^25462656-40-10690-10739
- ⏱ 2022-01-07 00:32:25
-
📌 在Kafka中,一个topic表示一个逻辑上的消息队列,具体到物理上,一个topic被分成了多个partition,每个partition对应磁盘中的一个日志文件。 ^25462656-40-1169-1251
- ⏱ 2022-01-07 00:49:29
8.4 容量规划
-
📌 吞吐量、响应时间与并发数。 ^25462656-41-851-864
- ⏱ 2022-01-07 01:02:08
-
📌 三个指标的数学关系。[插图] ^25462656-41-1000-1041
- ⏱ 2022-01-07 01:03:11
-
📌 图8-21 吞吐量、响应时间随并发用户数变化示意图 ^25462656-41-2303-2328
- ⏱ 2022-01-07 01:16:47
-
📌 需要用峰值测算,而不能用均值。 ^25462656-41-3717-3732
- ⏱ 2022-01-07 01:19:03
-
📌 在线上部署一个与真实数据库一样的“影子数据库”,对测试数据打标签,测试数据不进入线上数据库,而是进入这个“影子数据库”。 ^25462656-41-4460-4520
- ⏱ 2022-01-07 01:23:58
9.2 隔离、限流、熔断和降级
-
📌 当线程池中没有空闲线程,并且线程池内部的队列也已经满了的情况下,线程池会直接抛出异常,拒绝新的请求,从而确保调用线程不会被阻塞。 ^25462656-44-1402-1466
- ⏱ 2022-01-08 21:35:43
-
📌 常用的有漏桶算法和令牌桶算法 ^25462656-44-2831-2845
- ⏱ 2022-01-08 21:35:43
-
📌 线程池隔离,为每个RPC调用单独准备一个线程池(一般2~10个线程) ^25462656-44-1350-1384
- ⏱ 2022-01-08 21:35:44
-
📌 令牌桶限制的是平均流入速率,而不是瞬时速率,因为可能出现一段时间没有请求进来,令牌桶里塞满了令牌,然后短时间内突发流量过来,一瞬间(可以认为是同时)从桶里拿几个令牌出来;漏桶有点类似消息队列,起到了削峰的作用,平滑了突发流入速率。 ^25462656-44-3747-3862
- ⏱ 2022-01-08 21:35:45
-
📌 相比于限流、熔断两个偏技术性的词汇,降级则是一个更加偏向业务的词汇。 ^25462656-44-5158-5192
- ⏱ 2022-01-08 21:35:46
-
📌 能熔断的服务肯定不是核心链路上的必选服务。如果是的话,则服务如果超时或者宕机,前端就不能用了,而不是熔断。所以,说熔断其实也是降级的一种方式。 ^25462656-44-5020-5091
- ⏱ 2022-01-08 21:35:47
-
📌 熔断有两种策略:一种是根据请求失败率,一种是根据请求响应时间。 ^25462656-44-3983-4014
- ⏱ 2022-01-08 21:35:47
-
📌 限制速率的算法 ^25462656-44-2819-2826
- ⏱ 2022-01-08 21:35:48
-
📌 限流是服务端,根据其能力上限设置一个过载保护;而熔断是调用端对自己做的一个保护。 ^25462656-44-4957-4997
- ⏱ 2022-01-08 21:35:49
-
📌 业务层面的限流。比如在秒杀系统中,一个商品的库存只有100件,现在有2万人抢购,没有必要放2万个人进来,只需要放前500个人进来,后面的人直接返回已售完即可。针对这种业务场景,可以做一个限流系统,或者叫售卖的资格系统(票据系统),票据系统里面存放了500张票据,每来一个人,领一张票据。领到票据的人再进入后面的业务系统进行抢购;对于领不到票据的人,则返回已售完。 ^25462656-44-2490-2684
- ⏱ 2022-01-08 21:35:50
10.2 分布式事务解决方案汇总
-
📌 要实现2PC,所有参与者都要实现三个接口:Prepare、Commit、Rollback,这也就是XA协议 ^25462656-49-1557-1610
- ⏱ 2022-01-08 21:35:45
-
📌 阶段 2:提交阶段。 ^25462656-49-1094-1104
- ⏱ 2022-01-08 21:35:49
-
📌 如果有一个参与者回复的是 NO,或者超时了,则事务协调者向所有参与者发起事务回滚操作,所有参与者各自回滚事务,然后发送ACK ^25462656-49-1185-1247
- ⏱ 2022-01-08 21:35:51
-
📌 阶段 1:准备阶段。 ^25462656-49-1030-1040
- ⏱ 2022-01-08 21:35:51
-
📌 2PC是指事务的提交分为两个阶段, ^25462656-49-991-1008
- ⏱ 2022-01-08 21:35:52
-
📌 Try 操作主要是为了“保证业务操作的前置条件都得到满足”,然后在Confirm阶段,因为前置条件都满足了,所以可以不断重试保证成功。 ^25462656-49-8020-8087
- ⏱ 2022-01-09 21:51:18
-
📌 问题1:丢失消费。 ^25462656-49-4226-4235
- ⏱ 2022-01-09 21:51:18
-
📌 当然,系统 A、B、C根据全局的事务ID做幂等操作,所以即使重复调用也没有关系。 ^25462656-49-8849-8889
- ⏱ 2022-01-09 21:51:19
-
📌 电商网站的下单至少需要两个操作:创建订单和扣库存。 ^25462656-49-11244-11269
- ⏱ 2022-01-09 21:51:20
-
📌 如果消费失败了,则可以重试,但还一直失败怎么办?是否要自动回滚整个流程?答案是人工介入。 ^25462656-49-6199-6256
- ⏱ 2022-01-09 21:51:21
-
📌 一次业务操作,要向两个数据库中写入两条数据,如何保证原子性? ^25462656-49-10179-10209
- ⏱ 2022-01-09 21:51:21
-
📌 然后有一个后台任务,扫描状态表,在过了某段时间后 ^25462656-49-8737-8761
- ⏱ 2022-01-09 21:51:22
-
📌 初始是状态1,每调用成功1个服务则更新1次状态 ^25462656-49-8575-8598
- ⏱ 2022-01-09 21:51:22
-
📌 这就涉及RocketMQ的关键点:RocketMQ会定期(默认是1min)扫描所有的预发送但还没有确认的消息,回调给发送方,询问这条消息是要发出去,还是取消。发送方根据自己的业务数据,知道这条消息是应该发出去(DB更新成功了),还是应该取消(DB更新失败)。 ^25462656-49-5746-5875
- ⏱ 2022-01-09 21:51:23
-
📌 状态没有变为最终的状态4,说明这条事务没有执行成功。于是重新调用 ^25462656-49-8783-8815
- ⏱ 2022-01-09 21:51:24
-
📌 可以采用下面这种更加妥协而简单的方法。按方案 1,先扣库存,后创建订单。不做状态补偿,为库存系统提供一个回滚接口。创建订单如果失败了,先重试。如果重试还不成功,则回滚库存的扣减。如回滚也失败,则发报警,进行人工干预修复。 ^25462656-49-13268-13391
- ⏱ 2022-01-09 21:51:24
-
📌 假设消息中间件没有提供“事务消息”功能,比如用的是Kafka。该如何解决这个问题呢? ^25462656-49-3551-3593
- ⏱ 2022-01-09 21:51:25
-
📌 基于RocketMQ事务消息 ^25462656-49-5044-5058
- ⏱ 2022-01-09 21:51:25
-
📌 事务状态表+调用方重试+接收方幂等 ^25462656-49-8187-8204
- ⏱ 2022-01-09 21:51:26
-
📌 系统A增加一张消息表 ^25462656-49-3646-3656
- ⏱ 2022-01-09 21:51:27
-
📌 凡是没有发送ACK的消息,系统B重启之后消息中间件会再次推送。 ^25462656-49-4354-4385
- ⏱ 2022-01-09 21:51:28
-
📌 因为两个库的数据是冗余的,可以先保证一个库的数据是准确的,以该库为基准校对另外一个库。 ^25462656-49-10482-10525
- ⏱ 2022-01-09 21:51:28
-
📌 每次接收到新消息,先通过判重表进行判重,实现业务的幂等。同样,对DB2的加钱操作和消息写入判重表两个操作,要在一个DB的事务里面完成。 ^25462656-49-4713-4780
- ⏱ 2022-01-09 21:51:29
-
📌 但是,库存多扣了,数据不一致,怎么补偿呢?库存每扣一次,都会生成一条流水记录。这条记录的初始状态是“占用”,等订单支付成功后,会把状态改成“释放”。对于那些过了很长时间一直是占用,而不释放的库存,要么是因为前面多扣造成的,要么是因为用户下了单但没有支付。通过比对,得到库存系统的“占用又没有释放的库存流水”与订单系统的未支付的订单,就可以回收这些库存 ^25462656-49-12828-13042
- ⏱ 2022-01-09 21:51:30
-
📌 可以利用业务的特性,采用一种弱一致的方案。对于该需求,有一个关键特性:对于电商的购物来讲,允许少卖,但不能超卖。 ^25462656-49-11821-11890
- ⏱ 2022-01-09 21:51:31
-
📌 对应的“扣钱”的Try操作就是“锁定”,对应的“加钱”的Try操作就是检查账号合法性 ^25462656-49-7835-7877
- ⏱ 2022-01-09 21:51:32
-
📌 对账又分为全量对账和增量对账 ^25462656-49-10538-10552
- ⏱ 2022-01-09 21:51:33
-
📌 TCC的方法通过TCC框架内部来做,下面介绍的方法是业务方自己实现的。 ^25462656-49-8240-8275
- ⏱ 2022-01-09 21:51:34
-
📌 如果把案例 2、案例 3 的问题看作为一个分布式事务的话,可以用最终一致性、TCC、事务状态表去实现,但这些方法都太重,一个简单的方法是“对账”。 ^25462656-49-10396-10469
- ⏱ 2022-01-09 21:51:34
-
📌 通过消息中间件的ACK机制 ^25462656-49-4310-4323
- ⏱ 2022-01-09 21:51:35
-
📌 文介绍了基于订单的状态+库存流水的状态做补偿(或者说叫对账)。 ^25462656-49-13218-13249
- ⏱ 2022-01-09 21:51:36
-
📌 RocketMQ不是提供一个单一的“发送”接口,而是把消息的发送拆成了两个阶段,Prepare阶段(消息预发送)和Confirm阶段(确认发送)。具体使用方法如下:步骤1:系统A调用Prepare接口,预发送消息。此时消息保存在消息中间件里,但消息中间件不会把消息给消费方消费,消息只是暂存在那。步骤2:系统A更新数据库,进行扣钱操作。步骤3:系统A调用Comfirm接口,确认发送消息。此时消息中间件才会把消息给消费方进行消费。 ^25462656-49-5365-5619
- ⏱ 2022-01-09 21:51:36
-
📌 为了解决重复消息的问题,系统B增加一个判重表。判重表记录了处理成功的消息ID 和消息中间件对应的offset(以Kafka为例),系统B宕机重启,可以定位到offset位置,从这之后开始继续消费。 ^25462656-49-4602-4700
- ⏱ 2022-01-09 21:51:37
-
📌 不管是Confirm失败了,还是Cancel失败了,都不断重试。这就要求Confirm和Cancel都必须是幂等操作。注意,这里的重试是由TCC的框架来执行的,而不是让业务方自己去做。 ^25462656-49-7479-7571
- ⏱ 2022-01-09 21:51:38
-
📌 TCC是Try、Confirm、Cancel三个单词的缩写,其实是一个应用层面的2PC协议,Confirm对应2PC中的事务提交操作,Cancel对应2PC中的事务回滚操作 ^25462656-49-6610-6696
- ⏱ 2022-01-09 21:51:38
-
📌 如何保证创建订单+扣库存两个操作的原子性,同时还要能抵抗线上的高并发流量? ^25462656-49-11357-11394
- ⏱ 2022-01-09 21:51:39
-
📌 问题2:重复消费。除了ACK机制,可能会引起重复消费;系统A的后台任务也可能给消息中间件重复发送消息。 ^25462656-49-4538-4589
- ⏱ 2022-01-09 21:51:40
-
📌 调用方维护一张事务状态表 ^25462656-49-8288-8300
- ⏱ 2022-01-09 21:51:40
-
📌 在每次调用之前,落盘一条事务流水,生成一个全局的事务ID。 ^25462656-49-8315-8344
- ⏱ 2022-01-09 21:51:41
11.1 高可用且强一致性到底有多难
-
📌 即使客户端同步发送,服务器端ACK=ALL(或者-1),也就是等Master把消息同步给所有的 Slave 后,再成功返回给客户端,这样如此“可靠”的情况下, ^25462656-51-801-880
- ⏱ 2022-01-09 21:54:10
-
📌 Master宕机,切换到Slave,可能导致消息丢失。 ^25462656-51-937-964
- ⏱ 2022-01-09 21:54:18
-
📌 这个丢失是由Kafka的ISR算法本身的缺陷导致的,而不是系统问题。 ^25462656-51-977-1011
- ⏱ 2022-01-09 21:54:43
-
📌 HW取三个LEO的最小值 ^25462656-51-1723-1735
- ⏱ 2022-01-09 21:57:44
-
📌 Master是等Slave1和Slave2把HW=5之前的日志都复制过去之后,才把HW更新到5的。但它把HW=5传递给Slave1和Slave2,要等到下一个网络来回, ^25462656-51-1913-1997
- ⏱ 2022-01-09 22:05:10
-
📌 但等它把HW=5传递给Slave1和Slave2的时候,自己可能已经更新到HW=7。这意味着,Slave1和Slave2上的HW的值一直会比Master延迟一个网络来回。如果不发送Master/Slave的切换,则没有问题;但发生切换之后,问题就出现了。 ^25462656-51-2055-2195
- ⏱ 2022-01-09 22:07:22
-
📌 发生日志错乱的场景的前提是“异步刷盘”。 ^25462656-51-4186-4206
- ⏱ 2022-01-09 22:53:20
11.2 Paxos算法解析
-
📌 虽然三个客户端是并发的,没有先后顺序,但到了服务器的集群里必须保证三台机器的日志顺序是一样的,这就是所谓的“分布式一致性”。 ^25462656-52-2788-2866
- ⏱ 2022-01-10 18:07:14
-
📌 复制状态机 ^25462656-52-4645-4650
- ⏱ 2022-01-10 18:20:57
-
📌 的原理是:一样的初始状态+一样的输入事件=一样的最终状态。因此,要保证多个 Node 的状态完全一致,只要保证多个Node的日志流是一样的即可! ^25462656-52-4654-4726
- ⏱ 2022-01-10 18:21:19
-
📌 每个Node在存储日志之前先要问一下其他Node,之后再决定把这条日志写到哪个位置。这里有两个阶段:先问,再做决策,也就是Paxos 2PC的原型! ^25462656-52-6005-6079
- ⏱ 2022-01-10 18:26:28
-
📌 基本思路是当一个节点被确认为Leader之后,它先广播一次Prepare,一旦超过半数同意,之后对于收到的每条日志直接执行Accept操作。在这里,Perpare不再是对一条日志的控制了,而是相对于拿到了整个日志的控制权。一旦这个Leader拿到了整个日志的控制权,后面就直接略过Prepare,直接执行Accept。 ^25462656-52-10610-10769
- ⏱ 2022-01-11 10:40:37
-
📌 如果有新的Leader出现怎么办呢?新的Leader肯定会先发起Prepare,导致minProposalId变大。这时旧的 Leader 的广播 Accept 肯定会失败,旧的 Leader 会自己转变成一个普通的Acceptor,新的Leader把旧的顶替掉了。 ^25462656-52-10782-10915
- ⏱ 2022-01-11 11:42:04
-
📌 当一个Acceptor被选为Leader后,对于所有未确认的日志,可以逐个再执行一遍Paxos,来判断该条日志被多数派确认的值是多少。因为Basic Paxos有一个核心特性:一旦一个值被确定后,无论再执行多少遍Paxos,该值都不会改变!因此,再执行1遍Paxos,相当于向集群发起了一次查询! ^25462656-52-12031-12192
- ⏱ 2022-01-11 12:27:57
11.3 Raft算法解析
-
📌 Leader会并发地向所有的Follower发送AppendEntries RPC请求,只要超过半数的Follower复制成功,就返回给客户端日志写入成功。 ^25462656-53-8130-8208
- ⏱ 2022-01-10 14:53:13
-
📌 通过看接收者的处理逻辑会发现,新选出的 Leader 一定拥有最新的日志。因为只有Candidate 的日志和接收者一样新,或者比接收者还要新(反正不比接收者旧),接收者才会返回true。 ^25462656-53-7535-7629
- ⏱ 2022-01-10 15:28:27
-
📌 term有什么用?term的一个关键作用是可以解决Leader的“脑裂”问题。 ^25462656-53-3275-3318
- ⏱ 2022-01-10 16:07:41
-
📌 因为选举的时候是要多数派(超过半数的节点)同意的,意味着在多数派里面一定有一个节点保存了最新的term的值。 ^25462656-53-4513-4567
- ⏱ 2022-01-10 16:08:31
-
📌 两条日志a和b,日志a比日志b新,当且仅当符合下面两个条件之一。· term>b.term.· term=b.term 且 a.index>b.index。 ^25462656-53-7670-7774
- ⏱ 2022-01-10 16:13:27
11.4 Zab算法解析
-
📌 一个持久化的是客户端的请求序列(日志序列),另外一个持久化的是数据的状态变化,前者对应的是 Replicated State Machine,后者对应的是Primary-Backup System。 ^25462656-54-1182-1294
- ⏱ 2022-01-09 23:03:33
-
📌 Replicated State Machine 对比Primary-Backup System。Paxos和Raft用的是前者 ^25462656-54-787-851
- ⏱ 2022-01-09 23:04:00
-
📌 而Zab用的是后者。 ^25462656-54-875-885
- ⏱ 2022-01-09 23:04:06
-
📌 Zab也是单点写入。客户端的写请求都会写入Primary Node,Parimary Node更新自己本地的树,这棵树也就是上面所说的状态机,完全在内存当中,对应的树的变化存储在磁盘上面,称为Transaction日志。Primary节点把Transaction日志复制到多数派的Backup Node上面,Backup Node根据Transaction日志更新各自内存中的这棵树。 ^25462656-54-2406-2598
- ⏱ 2022-01-10 11:23:34
-
📌 Zookeeper中的Transaction指的并不是客户端的请求日志,而是Zookeeper的这棵内存树的变化。每一次客户端的写请求导致的内存树的变化,生成一个对应的 Transaction,每个Transaction有一个唯一的ID,称为zxid。 ^25462656-54-2950-3076
- ⏱ 2022-01-10 11:25:07
-
📌 zxid是一个64位的整数,高32位表示Leader的任期,在Raft里面叫term,这里叫epoch;低32位是任期内日志的顺序编号。对于每一个新的epoch,zxid的低32位的编号都从0开始。这是不同于Raft的一个地方,在Raft里面,日志的编号呈全局的顺序递增。 ^25462656-54-3134-3283
- ⏱ 2022-01-10 11:33:06
-
📌 在Zab里面是双向心跳 ^25462656-54-6944-6955
- ⏱ 2022-01-10 12:06:42
-
📌 Raft选取日志最新的节点作为新的Leader,Zab的FLE(Fast Leader Election)算法也类似 ^25462656-54-7085-7143
- ⏱ 2022-01-10 12:07:38
-
📌 在阶段1,收到多数派的ACK后,就表示返回给客户端成功了。 ^25462656-54-7964-7993
- ⏱ 2022-01-10 12:38:13
12.2 现实世界不存在“强一致性”(PACELC理论)
-
📌 信息的传播需要“时间” ^25462656-58-1458-1469
- ⏱ 2022-01-11 12:42:45
-
📌 信息所反映的“世界”在变化 ^25462656-58-1909-1922
- ⏱ 2022-01-11 12:42:58
-
📌 2将军问题(通道不可靠) ^25462656-58-2118-2130
- ⏱ 2022-01-11 12:43:50
-
📌 当P没有出现(网络正常)的情况下,需要在L和C之间做权衡 ^25462656-58-3185-3213
- ⏱ 2022-01-11 12:50:29
12.3 典型案例:分布式锁
-
📌 最常用的分布式锁是基于Zookeeper来实现的,利用Zookeeper的“瞬时节点”的特性。每次加锁都是创建一个瞬时节点,释放锁则删除瞬时节点。因为 Zookeeper 和客户端之间通过心跳探测客户端是否宕机,如果宕机,则Zookeeper检测到后自动删除瞬时节点,从而释放锁。 ^25462656-59-569-709
- ⏱ 2022-01-11 12:52:38
-
📌 因为用心跳探测客户端是否宕机,当网络超时或客户端发生Full GC的时候会产生误判。本来客户端没有宕机,却误判为宕机了,锁被释放,然后被另外一个进程拿到,从而导致两个进程拿到同一把锁。 ^25462656-59-822-914
- ⏱ 2022-01-11 13:14:49
-
📌 问题1:Redis没有Zookeeper强一致性的Zab协议,Redis的主从之间采用的是异步复制,如果主宕机,则切换到从,会导致部分锁的数据丢失,也就是多个进程会拿到同一把锁。 ^25462656-59-1030-1119
- ⏱ 2022-01-11 13:16:37
-
📌 问题2:客户端和Redis之间没有心跳,如果客户端在拿到锁之后、释放锁之前宕机,锁将永远不能释放。要解决这个问题,是给锁加一个超时时间,过了一段时间之后,锁将无条件释放。但这又带来第三个问题: ^25462656-59-1132-1228
- ⏱ 2022-01-11 13:16:44
-
📌 问题3:如果客户端不是真的宕机,而只是因为Full GC发生了阻塞,或业务逻辑的执行时间超出了锁的超时时间,则锁被无条件释放,也会导致两个进程拿到同一把锁。 ^25462656-59-1241-1319
- ⏱ 2022-01-11 13:16:50
第14章 业务架构思维
- 📌 OOD中的典型办法,DIP(依赖反转)。底层定义接口,上层实现,而不是底层直接调用上层。 ^25462656-65-1305-1349
- ⏱ 2022-01-12 14:47:03
读书笔记
6.2 分库分表
划线评论
- 📌 做宽表,重写轻读 ^3533118-7w4b4SCV5
- 💭 重写轻读:多写些预计算信息,减轻读时压力
- ⏱ 2022-01-04 23:22:37
6.4 事务与锁
划线评论
- 📌 ^3533118-7w50PGeaO
- 💭 脏读的描述错了。应该是事务B修改了一条记录的值,还没提交呢,事务A就读到了修改的值,然后事务B回滚了,导致事务A读到这条记录的一个脏数据。
- ⏱ 2022-01-05 12:32:51
11.2 Paxos算法解析
划线评论
- 📌 取acceptorProposalId最大的acceptValue作为v ^3533118-7we5dxdGc
- 💭 假设只有单点发起proposal,那每次的v值都是上一轮的值,这v值就没什么意义吧?v值是自己的值时才是要存的值。
- ⏱ 2022-01-11 11:39:50
划线评论
- 📌 Proposer广播prepare(n) ^3533118-7we596vKD
- 💭 n相当于term任期,n大的占有写入权,相当于上文比拟的想写入位置
- ⏱ 2022-01-11 11:38:45
划线评论
- 📌 1号位置不能用了,也得把自己的1号位置赋值成X=5 ^3533118-7we51GuBS
- 💭 这个比拟不恰当。看后文的算法实现,当发现位置不可用时直接弃用,尝试下一位置,没有什么把这个位置的日志拷过来的操作。只要保证位置号递增,不需要位置号连续。
- ⏱ 2022-01-11 11:36:55
划线评论
- 📌 acceptorProposalId ^3533118-7we2bcxxq
- 💭 acceptorProposalId相当于节点的当前term
- ⏱ 2022-01-11 10:53:27
