蔡不菜丶

V1

2022/06/05阅读:26主题:丘比特忙

读书笔记之《软件架构设计: 大型网站技术架构与业务架构融合之道》

大家好呀,我是小菜~

帅哥美女,知道你们时间宝贵,那么就由小菜为你读好一本书,读一本好书,取其精华,与你共享~!

本文主要分享 《软件架构设计:大型网站技术架构与业务架构融合之道》

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,菜农曰,没关注的同学们记得关注哦!

今天带来的是 《软件架构设计:大型网站技术架构与业务架构融合之道》 的读书笔记

(文中使用到的例子贴图均出于原书)

在正式进入分享之前,我们想看下这本树的目录架构

软件架构设计:大型网站技术架构与业务架构融合之道

这本书总共分为 个部分,共计 17 章 ,总体来说内容还是挺多的。内容相对全面,但并没有面面俱到,还是比较推荐阅读的一本书,话不多说,进入正文!

第一部分:什么是架构

第一部分由两个章节组成,简单的介绍了下什么是架构

第一章:五花八门的架构师职业

1.1 架构师职业分类

现在随便找一个招聘网站或猎头发布的招聘广告,我们都能看到各式各样的架构师头衔,比如有:Java 架构师,前端架构师,后端架构师,数据架构师,中间件架构师... 等等,而且年限的要求也各不一,3~5年,8~10年。

但是从这些岗位的需求我们可以看出,“架构师”中的架构是一个很虚的词,不同领域和行业对员工要求的能力和工作经验差异很大。

现在问起很多开发者的发展路线都不约而同的是要成为一名架构师,那么对架构师的定义是怎么样的?架构师在项目体系和团队结构中应当着一个怎么样的角色?如何成为一名架构师?这些你是否都有一个明确的答案,是否也为之目标而努力前行着!

1.2 架构的分类

单纯以技术的角度来看,软件系统自底向上可以分为三层

第一层:基础架构

基础架构是指云平台、操作系统、网络、存储这些构成,一些中小公司大多会选择使用大公司研发的云计算平台,研发成本低,稳定有保障

第二层:中间件与大数据层

中间件属于公司中必有的,类似消息中间件,数据库中间件,缓存中间件,而大数据层对于中小公司来说比较少有沉淀,类似开源的 Hadoop 生态体系,Hive、Spark、Storm、Fink等

第三层:业务系统架构

对于第三层的划分并不是绝对,图中体现了三种架构类型:通用软件架构离线业务系统架构在线业务系统架构,但由于现实中软件的种类过多,比如还存在嵌入式系统。这里简单描述下图中第三种具备的类种:

  • 通用软件架构:常用的办公软件、浏览器、播放器等
  • 离线业务系统: 基于大数据的 BI(商业智能) 分析、数据挖掘、报表与可视化等
  • 在线业务系统架构: 搜索、推荐、即时通信、电商、游戏、广告、企业ERP或CRM等

第二章:架构的道与术

2.1 何为道,何为术

不禁感叹这年头聊架构,这可以上道与术的层面了。

这张图是大多数项目的基本架构图,可以将每层映射到你们的项目中,是不是不会觉得很陌生。

那么实际中这张图能够反映出架构抉择吗,架构师的任务是否就是简单的划分层级结构,然后就可以埋头进行开发了?

我们依赖这张图将问题进行扩展:

  1. 如何拆分微服务?
  2. 如何组织服务与服务之间的层级关系?
  3. 如何设计接口?
  4. 如何保证高可用?如何分库分表?如何保证数据一致性?...

想要表达的问题实在是太多了,由此可见架构师的任务并不简单。

2.2 道与术的辩证关系

问题那么复杂,我们就以道与术来理解。假如你要成为一名武林高手,那么花里胡哨的招式对于某些人来说很重要,因为要追求好看,所谓的花架子,而招式我们便可理解为术,那么追求高手的层面,我们是否要修炼内功心法,底子扎实,才能成为顶级高手。

那么重要还是重要,这是个公说公有理婆说婆有理的问题,段誉的内功厉害,但使不出招式可能也有些徒然,招式好看,却没有内功支撑,也只能成为花架子的笑谈,而道术兼备,方能顶级。

第二部分:计算机功底

这部分的内容颇多,重在道的修炼

第三章:语言

语言是在是太多了,忍不住吐槽~ 尽管语言如此之多,市面上还是不断地推陈出新,我们面对语言的不断迭代要追求潮流还是岿然不动?在我看来,我们要追求道,底层掌握结实,管它日转星移,我亦坦然相对。

语言再多再繁杂,都具备共同的典型特性,无外乎一些语法糖使用熟练与否

第四章:操作系统

I/O是绕不过去的一个基本问题。从文件I/O到网络I/O,存在着各式各样的概念和I/O模型

4.1 缓存I/O 和 直接I/O

在了解两个原理之前,我们先清楚几个概念:

  • 应用程序内存: 通常写代码用 malloc/free、new/delete 等分配出来的内存
  • 用户缓冲区: 位于用户空间中缓冲区,如 C语言FILE 结构体里面的 Buffer
  • 内核缓冲区: Linux 操作系统的 Page Cache。一个 Page 的大小一般为 4K

以上三个概念了解后,我们继续看 I/O 操作

  • 缓冲I/O

:磁盘 -> 内核缓冲区 -> 用户缓冲区 -> 应用程序

写: 应用程序 -> 用户缓冲区 -> 内核缓冲区 -> 磁盘

对于缓冲I/O,一个读操作会有3次数据拷贝,一个写操作会有反向的3次数据拷贝

  • 直接I/O

读: 磁盘 -> 内核缓冲区 -> 应用程序

写: 应用程序 -> 内核缓冲区 -> 磁盘

对于直接I/O,一个读操作会有3次数据拷贝,一个写操作会有返现的2次数据拷贝

总结:直接I/O 并不是没有缓冲,而是没有用户级的缓冲,对于操作系统本身的缓冲还是有的

4.2 内存映射文件与零拷贝

1)内存映射文件

从缓冲I/O到直接I/O,读写操作从3次的数据拷贝缩减到2次数据拷贝。而到了内存映射文件,读写操作再次缩减到了1次数据拷贝,也就是:

  • 读:磁盘 -> 内核缓冲区
  • 写: 内核缓冲区 -> 磁盘

应用程序虽然读写的是自己的内存,但这个内存只是一个 "逻辑地址",实际读写的是 内核缓冲区

2)零拷贝

零拷贝(Zero Copy)又是提升 I/O 效率的一大利器,在平时有问到 Kafka 是如何做到读写那么快的时候,其中一个很大的原因便是 Kafka 用到了零拷贝技术。

  1. 这是一个利用直接I/O进行收发文件的过程
磁盘→内核缓冲区→应用程序内存→Socket缓冲区→网络
磁盘→内核缓冲区→应用程序内存→Socket缓冲区→网络
  1. 这是一个利用内存映射文件进行收发文件的过程

整个过程从4次的数据拷贝降低到了3次,不再经过应用程序内存,直接在内核空间中从内核缓冲区拷贝到 Socket 缓冲区

  1. 这是一个利用零拷贝进行收发文件的过程

利用零拷贝的话,连内存缓冲区到Socket缓冲区的数据拷贝步骤都可以省略。在内核缓冲区和 Socket 缓冲区之间并没有做数据拷贝,只是一个地址的映射,底层的网卡驱动程序要读取数据并发送到网络的时候,看似读的是Socket缓冲区中的数据,实际上读得是内核缓冲区的数据。

总结:为什么称之为零拷贝呢,因为从内存的角度上看,数据在内存中没有发生数据拷贝,只在内存与I/O之间传输。利用的还是 内核缓冲区 与 Socket缓冲区之间的映射,数据本身只有一份

4.3 网络 I/O 模型

网络 I/O 模型也是一个极易混淆的概念,至今为止我们听过几种网络I/O模型呢

  1. 网络阻塞 I/O
  2. 网络非阻塞 I/O
  3. I/O 多路复用
  4. 异步 I/O

很多时候我们容易混淆的概念是 非阻塞异步

1)网络模型

1. 网络阻塞 I/O

这种模型很好理解,就是调用的时候会被阻塞,直到数据读取完成或写入成功

2. 网络非阻塞 I/O

和上述相反,但没有数据的时候会立即返回,不会阻塞,然后通过轮询的方式不断查询直到获取到数据

如果只有几十乃至上百个连接的时候,上面两种 I/O 模型处理的方式问题都不大,当连接数达到几十万乃至上百万时,那问题就很严重了

3. I/O 多路复用

该方式也是阻塞调用,一次性将所有的连接请求传进来,当某个连接请求具备条件后,会立即将结果放回,告知应用程序有哪些连接可读或可写。常用的 I/O 多路复用的方法有:select、poll、epoll、Java的NIO,其中 epoll 的效率最高,也是目前最主流的。

epoll

整个 epoll 分为三个步骤

  1. 事件注册
  2. 轮询查询是否就绪
  3. 事件就绪后进行读写

其中又可分为两种模式:LT(水平触发/条件触发)ET(边缘触发/状态触发)

  1. LT 水平触发:只要读缓冲区不空就会一直触发读事件;写缓冲区不满,就会一直触发写事件
  2. ET 边缘触发:读缓冲区的状态从空转为非空的时候触发一次;写缓冲区的状态从满转为非满的时候触发一次

总结:实际的开发中,大家一般倾向于 LT(默认模式)。但使用的时候需要避免 "写死循环"的问题,因为写缓冲区为满的概率很小,会一直触发写事件

4. 异步 I/O

异步 I/O 是指所有的读写操作都由操作系统完成,当处理结束后,将结果通过指定的回调函数或其他机制告知应用程序

总结: 阻塞和非阻塞是从函数调用的角度来说,而同步和非同步是从 "读写是由谁来完成" 的角度来说

2)设计模式

除了上面几种网络I/O模型,我们还经常听到 Reactor 模式与 Proactor 模型,这两种并不是网络I/O模型。而是网络框架中的两种设计模式,无论操作系统的网络 I/O 模型的设计,还是上层网络框架的网络 I/O 模型的设计,用的都是这两中设计模型之一

1. Reactor 模式

这是一种主动模式。应用程序会不断地轮询,询问操作系统或网络框架、I/O 是否就绪。select、poll、epoll、Java中的NIO 就属于这种主动模式。

2. Proactor 模式

这是一种被动模式。应用程序会将所有的读写操作都交给操作系统完成,完成后再将结果通过一定的通知机制告知应用程序

4.4 进程、线程和协程

不同语言有不同的使用习惯。如 Java 一般是写 单进程多线程 ,C++ 一般是 单进程多线程多进程单线程

1)为什么要使用多线程?
  1. 提高 CPU 使用率
  2. 提高 I/O 吞吐
2)多线程会带来的问题?
  1. 锁(悲观锁、乐观锁、互斥锁、读写锁、自旋锁、公平/非公平锁等)
  2. Wait 与 Signal 操作
  3. Condition
3)为什么需要多进程?
  1. 线程间锁的存在,会导致并发效率下降,同时增加编码难度
  2. 线程上下文切换需要时间,过多的切换会导致效率低下
  3. 多进程相互独立,其中一个崩溃后,其他进程可以继续运行,提高可靠性

不要通过共享内存来实现通信,而应通过通信实现共享内存

通俗理解:尽可能通过消息通信, 而不是共享内存来实现进程或线程之间的同步

4)为什么需要协程?
  1. 更好地利用CPU,协程可以由应用程序自己调度,而线程不行
  2. 更好地利用内存,协程的堆栈大小不是固定的,用多少申请多少

4.5 无锁(内存屏障与CAS)

1)内存屏障

读可以多线程、写必须单线程,如果多线程写,则做不到无锁

基于内存屏障(防止代码重排序),有了Java中的volatile关键字,再加上单线程写的原则,就有了Java中的无锁并发框架

2)CAS

如果是多线程写,内存屏障并不适用,这是就需要用到 CAS。CAS是在CPU层面提供的一个硬件原子指令,实现对同一个值的Compare和Set 两个操作的原子化。

第五章:网络

网络的具体认知可以空降:掌握《网络》,见微才能知著

第六章:数据库

6.1 范式与反范式

  • 第一范式:每个字段都是原子的,不能再分解。(反例:某个字段是 JSON 串,或数组)
  • 第二范式:表必须有主键,主键可以是单个属性或几个属性的组合,非主属性必须完全依赖,而不能部分依赖(反例:有张好友关系表,主键是 关注人ID+被关注人ID,但该表中还存储了名字、头像等字段,这些字段只依赖组合主键中其中一个字段(关注人ID),而不是完全依赖主键)
  • 第三范式:没有传递依赖,非主属性必须直接依赖主键,而不能间接依赖主键(反例:有张员工表,有个字段是部门ID,还有其他部门字段,比如部门名称,部门描述等,这些字段直接依赖部门ID,而不是员工ID,不应该在员工表中存在)

看了三大范式不禁有些汗颜,实际开发中为了性能或便于开发,违背范式的设计比比皆是,但也无可厚非,虽然范式不一定要遵守,但还是需要仔细权衡。

6.2 分库分表

分库分表使分布式系统设计中一个非常普遍的问题。

1)分库分表的目的
  1. 业务拆分。通过业务拆分我们可以把一个大的复杂系统拆成多个业务子系统,系统与系统之间可以通过 RPC 或消息中间件的方式通信。

  2. 应对高并发。高并发我们可以具体分为是读多写少还是读少写多的并发场景。读多我们可以利用缓存中间件减少压力,而写多我们就需要考虑是否要进行分库分表

  3. 数据隔离。核心业务区分开来,区别对待,投入的开发和运维成本也可以侧重点偏移

2)拆分维度的选择
  1. 按照 Id 维度拆分:根据 Id % 64 取模拆成 0~63 的64张表
  2. 固定位拆分:取 Id 指定二位,例如倒数 2 ,3位组成 00~99 张表
  3. hash值拆分:将 Id 取 hash 值,然后 % 表数
  4. range 拆分:按照 userId 指定范围拆分,0 - 1千万一张表,这种用的比较少,容易产生热点数据问题
  5. 业务域拆分:把不同业务域的表拆分到不同库中,例如订单相关的表,用户信息相关的表,营销相关的表分开在不同库
  6. 把不常用的字段单独拿出来存储到一张表中
3)面对 JOIN 问题
  1. 拆分成多个单表查询,在代码层做逻辑拼装
  2. 做宽表(JOIN 好的表),重写轻读
  3. 利用搜索引擎,例如 Elasticsearch

6.3 B+树

B+ 树具备了哪些查询特性:

  1. 范围查询
  2. 前缀匹配模糊查询
  3. 排序和分页
1)逻辑结构
B+ 树结构
B+ 树结构

这个是一个 B+ 树结构,相对来说比较抽象,我们提取下其中的几个关键特征:

  1. 叶子节点之间所有记录的主键,并按照从小到大的顺序排列,形成一个双向链表。叶子节点的每一个key都指向一条记录
  2. 非叶子节点取的是叶子节点里面key的最小值。同层的非叶子节点也相互串联,形成一个双向链表

为什么只支持前缀匹配模糊查询 - like abc%

前缀匹配模糊查询可以转换为范围查询,例如 abc% 可以转换为 key in [abc, abcz],而如果是全模糊查询是没有办法转换的

2)物理结构

上面描述的树只是一个逻辑结构,而不是实际上的物理结构,因为数据最终都是要存储到磁盘上的。

磁盘都是以 为单位

在 InnoDB 引擎中默认的块大小是 16 KB(可通过 innodb_page_size参数指定),这里的块,指的是逻辑单位,而不是磁盘扇区的物理块,块是 InnoDB 读写磁盘的基本单位,InnoDB 每次进行磁盘I/O读取的都是 16 KB的整数倍,无论是叶子节点还是非叶子节点都是装在 Page 里面

三层的磁盘B+树结构
三层的磁盘B+树结构

一个Page大概可以装1000个key(意味着B+树有1000个分叉),每个Page大概可以装200条记录(叶子节点),那么三层结构可以装多少?1000 * 1000 * 200 = 2亿条,约16GB的数据,这就是 B+ 数的强大之处

3)非主键索引

每一个非主键索引都对应一个 B+ 树,与主键索引不同的是非主键索引每个叶子节点存储的是主键的值而不是记录的指针。也就是说对于非主键索引的查询,会先查到主键的值,再拿主键的值去查询主键的B+数,这就需要两次 B+ 树的查询操作,也就我们常说的 回表查询

6.4 事务与锁

1)事务的隔离级别

什么事务?事务就是一个"代码块"(一条船的蚂蚱),要么都不执行,要么都执行。多个事务之间,为了想完成任务,那么它们之间就很容易发生冲突,冲突产生就容易带来问题,比如:有两个事务分别是 小王 和 小李

  • 脏读:小王 读取了 小李 不想要的东西,给 小王 带来了脏数据
  • 不可重复读: 小王两次读取同一个记录,发现两次都不一样,原来是小李在搞鬼,一直在更新数据
  • 幻读: 小王两次读取数据,发现读出来的条数都不一样,原来是小李在搞鬼,一直在增加/删除数据
  • 丢失更新:小王正在将一条数据的值修改为 5,没想到小李也在修改这条数据修改为4,在小李修改结束之前,小王先修改完成了,小李才结束修改,这是小王发现数据怎么变成了 4

看了上面的 4 个问题,我们都觉得小李实在是太坏了,那有没有什么方法可以帮助到小王?

  1. RU(Read Uncommited):徒有其名,什么都没做,什么问题都没解决
  2. RC(Read Commited):可以解决 脏读 问题
  3. RR(Repeatable Read):可以解决 脏读、不可重复读、幻读 问题
  4. Serialization(串行化):解决所有问题

尽管串行化可以解决所有问题,但所有操作都是串行的,性能无法结局,所以常用的隔离级别是 RC 和 RR(默认)。RR 可以解决 脏读、不可重复读、幻读 问题,那最后一个 丢失更新 我们就要另外想方法解决了

2)悲观锁

悲观心态,认为数据发生冲突的概率很大,在读之前就直接上锁,可以利用 select xxx for update 语句。但会存在拿到锁之后一直没释放的问题,在高并发场景下会造成大量请求阻塞

3)乐观锁

乐观心态,认为数据发生冲突的概率很小,读之前不上锁,写的时候才判断原有的数据是否被其他事务修改了,也就是常说的 CAS。

CAS的核心思想是:数据读出来的时候有一个版本v1,然后在内存里面修改,当再写回去的时候,如果发现数据库中的版本不是v1(比v1大),说明在修改的期间内别的事务也在修改,则放弃更新,把数据重新读出来,重新计算逻辑,再重新写回去,如此不断地重试。

6.5 事务实现原理之1:Redo Log

事务的四大核心属性:

  1. 原子性: 事务要么不执行,要么完全执行。如果执行一半,宕机重启,已执行的一半要回滚回去
  2. 一致性:事务的执行使得数据库从一种正确状态转换成另外一种正确状态
  3. 隔离性:在事务正确提交之前,不允许把该事务对数据的任何改变提供给其他事务
  4. 持久性:一旦事务提交,数据就不能丢
1)Write-Ahead

一个事务存在修改多张表的多条记录,而多条记录又可分布在不同的 Page 里面,对应着磁盘的不同文职。如果每个事务都直接写磁盘,性能势必达不到要求。

解决的方式就是在内存中进行事务提交,然后通过后台线程异步地把内存中的数据写入到磁盘中。但这个时候又会有个问题,那就是如果发生宕机,内存中的数据没来得及刷盘就丢失了。

而这个时候 Redo Log 就是用来解决这种问题

一样是先在内存中提交事务,然后写日志(Redo Log),然后后台任务把内存中的数据异步刷到磁盘中。日志是顺序的记录在尾部,这样就可以避免一个事务发生多次磁盘随机I/O 问题。

从图中我们可以看到,在事务提交之后,Redo Log先写入到内存中的 Redo Log Buffer 中,然后异步地刷到磁盘的 Redo Log。因此不光光事务修改的操作是异步刷盘的,Redo Log 的写入也是异步刷盘的。

既然都是先写到内存中,那么发生宕机还是会出现丢失数据的问题,因此 InnoDB 有个参数 innodb_flush_log_at_trx_commit 可以控制刷盘策略:

  • 0: 每秒刷一次,默认的策略
  • 1: 每提交一个事务,就刷一次(最安全)
  • 2: 不刷盘。然后根据参数innodb_flush_log_at_timeout设置的值决定刷盘频率。

总结:0 和 2 都可能丢失数据,1 是最安全的,但是性能是最差的

2)日志结构

从物理结构上来看,日志是一个永不结束的字节流,但从逻辑结构上看,日志不可能是一个永不结束的字节流

物理结构
物理结构
逻辑结构
逻辑结构

因此在 Redo Log 中存在一个 LSN(Log Sequence Number)的编号(按照时间顺序),在一定时间后之前的历史日志就会归档,并从头开始循环使用

在 Redo Log 中会采用逻辑和物理的方式总和记录,先以 Page 为单位记录日志,然后每个 Page 中在采用逻辑记法(记录 Page 里面的哪一行被修改了),这种记法也称为 Physiological Logging

3)崩溃后恢复

不同事务的日志在Redo Log 中是交叉存在的,也就意味着未提交的事务也在 Redo Log 中。而崩溃后恢复就会用到一个名为 ARIES 算法,不管事务有没有提交,日志都会记录到 Redo Log 中,当崩溃再恢复的时候就会把 Redo Log 全部重放一遍,提交和未提交的事务都会重放,从而让数据库回到宕机之前的状态,称之为 Repeating History 。重放结束后再把宕机之前未完成的事务找出来,然后逐一利用 Undo Log 进行回滚。

4)总结
  1. 一个事务对应多条 Redo Log,并且是不连续存储的
  2. Redo Log 只保证事务的持久性,而无关原子性
  3. 未提交的事务回滚是通过 Checkpoint 记录的 “活跃事务表” + 每个事务日志的开始/结束标识 + Undo Log实现的
  4. Redo Log 具有幂等性,通过 LSN 实现
  5. 无论是提交的、还是未提交的事务,其对应的 Page 数据都可能被刷到了磁盘中。未提交的事务对应的Page数据,在宕机重启后会回滚。

6.6 事务实现原理值2: Undo Log

上面说到进行 Redo Log 宕机回滚的时候,如果 Redo Log 中存在未提交的事务,那么就需要借助 Undo Log进行辅助,换言之,如果 Redo Log 里面记录的都是已经提交的事务,那么回滚的时候也就不需要 Undo Log 的帮助

那么 Undo Log 除了在宕机恢复时对未提交的事务进行回滚,还具备以下两个核心作用:

  • 实现 隔离性
  • 高并发

在多线程的场景中应对并发问题的策略通常有三种:

  1. 互斥锁: 一个数据对象上面只有一个锁,先到先得。(写写互斥,读写互斥,读读互斥)
  2. 读写锁: 一个数据对象一个锁,两个视图。(写写互斥,读写互斥,读读并发)
  3. CopyOnWrite: 写时复制,写完之后再把数据对象的指针一次性赋值回去(写写并发,读写并发,读读并发)

Undo Log 的作用就是在 CopyOnWrite 部分。每个事务修改记录之前,都会先把记录拷贝一份出来,拷贝出来的那个备份就是存在Undo Log 里面。每个事务都有唯一的编号,ID从小到大递增,每一次修改就是一个版本,因此Undo Log负责的就是维护数据从旧到新的每个版本,各个版本之间的记录通过链表串联

为了不能让事务读取到正在修改的数据,只能读取历史版本,这就实现了隔离性

Undo Log 不是 log 而是数据,因为 Undo Log 只是临时记录,当事务提交之后,对应的 Undo Log 文件就可以删除了,因此 Undo Log 成为记录的备份数据更为准确

正是有了 MVCC 这种特性,通常的 select 语句都是不加锁的,读取的全部是数据的历史版本,从而支撑高并发的查询,也就是所谓的 快照读,与之相对应的是 当前读

快照读/当前读

读取历史数据的方式就叫做快照读,而读取数据库最新版本数据的方式叫做 当前读

  • 快照读

当执行 select 操作时,InnoDB 默认会执行快照读,会记录下这次 select 后的结果,之后 select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前 select 的数据,这就实现了可重复读

快照的生成当在第一次执行 select 的时候,也就是说假设当 A 开启了事务,然后没有执行任何操作,这时候 B insert 了一条数据然后 commit,这时 A 执行 select,那么返回的数据中就会有 B 添加的那条数据。之后无论再有其他事务 commit 都没有关系,因为快照已经生成了,后面的 select 都是根据快照来的。

  • 当前读

对于会对数据修改的操作(update、insert、delete)都是采用 当前读 的模式。在执行这几个操作时会读取最新的版本号记录,写操作后会把版本号改为当前事务的版本号,所以即使是别的事务提交的数据也可以查询到。

假设要 update 一条记录,但是在另一个事务中已经 delete 掉这条数据并且 commit 了,如果 update 就会产生冲突,也正是因为这样所以会产生幻读,所以在 update 的时候需要知道最新的记录。

6.7 Binlog 与主从复制

Binlog 称之为记录日志,它与 Redo Log 和 Undo Log 不同之处在于,后两者是 InnoDB 引擎层面的,而 Binlog 是Mysql 层面的,它的主要作用是用来做主从复制,它同样具有刷盘机制:

  • 0: 事务提交之后不主动刷盘,依靠操作系统自身的刷盘机制
  • 1: 每提交一个事务,刷一次磁盘
  • n: 每提交 n 个事务,刷一次磁盘

总结:0 和 n 都是不安全的,为了不丢失数据,一般都是建议双 1 保证,即 sync_binlog 和 innodb_flush_log_at_trx_commit 的值都是 1

1)Binlog 与 Redo Log的区别
  1. Redo Log 和 Binlog 的产生方式不同。redo log是在物理存储引擎产生,而Binlog是在 mysql 数据库的 server 层产生。并且 Binlog不仅针对 InnDB 存储引擎,MySQL 数据库中的任何存储引擎对数据库的更改都会产生 Binlog
  2. Redo Log 和 binlog 记录的方式不同。Binlog 记录的是一种逻辑日志,即通过 sql 语句的方式来记录数据库的修改;而 InnoDB层产生的Redo Log 是一种物理格式的日志,记录磁盘中每一个数据页的修改
  3. Redo Log 和 Binlog 记录的时间点不同。Binlog只是在事务提交完成后进行一次写入,而 Redo Log 则是在事务进行中不断写入,Redo Log 并不是随着事务提交的顺序进行写入的,这也就是说在 Redo Log 中针对一个事务会有多个不连续的记录日志
2)主从复制

Mysql 有三种主从复制的方式

  • 同步复制: 所有的 Slave 都接受完 Binlog 才认为事务提交成功,便返回成功的结果
  • 异步复制: 只要 Master 事务提交成功,就对客户端返回成功,然后通过后台线程的方式把 Binlog 同步给 Slave(可能会丢数据)
  • 半同步复制: Master 事务提交,同时把 Binlog 同步给 Slave,只要部分 Slave 接收到了 Binlog(数量可设置),就认为事务提交成功,返回成功结果

总结:无论异步复制,还是半异步复制(可能退化为异步复制),都可能在主从切换的时候丢数据。业务一般的做法是牺牲一致性来换取高可用性,即在Master宕机后切换到Slave,忍受少量的数据丢失,后续再人工修复

3)并行复制

原生的 MySQL 主从复制都是单线程的,将 Master 的 Binlog 发送到 Slave 上后生成 RelayLog 文件,Slave 再对 RelayLog 文件进行重放

串行复制
串行复制

而所谓的并行复制实际上是并行回放,传输还是单线程,但是回放是使多线程

并行复制
并行复制

第七章:框架、软件与中间件

开源运行的兴起,最不缺的便是开发框架,现市面上有各种各样的轮子

常用软件与中间件
常用软件与中间件

第三部分:技术架构之道

第八章:高并发问题

任何问题都是速途同归,到最后只能通过两种操作:读和写。

8.1 高并发读

1. 加缓存

缓存可分为 本地缓存集中式缓存 。使用缓存的同时我们需要思考缓存的更新策略:

  • 主动更新: 当数据库中的数据发生变更的时候,主动删除或更新缓存中的数据
  • 被动更新: 当用户查询请求到来时,再对缓存进行更新

同样使用缓存可能会面临的几个问题:

  • 缓存雪崩: 即缓存的高可用问题。如果缓存宕机/过期,所有请求会瞬间压垮数据库
  • 缓存穿透: 查询缓存中不存在的数据,导致短时间内大量请求写入并压垮数据库
  • 缓存击穿: 缓存中热点数据过期,直接访问数据库,导致数据库被压垮

那么缓存的使用无外乎都是对数据进行冗余,达到空间换时间的效果

2. 并发读

单线程不行,通常就会使用多线程。这种明显治标不指标,容易达到性能瓶颈

3. 重写轻读

当微博这种大流量的平台,查看关注人和自己发布的微博列表看似很简单需求,通常只需要两张表,一个是 关注关系表 ,一个是 微博发布表。但是对于高并发查询的时候很容易将数据库打崩。

那我们就需要改成 重写轻读 的方式,不是查询的时候才聚合,,而是提前为每个 userId 准备一个 收件箱

当某个被关注的用户发布微博时,只需要将这条微博发送给所有关注自己每个用户的收件箱中,这样用户查询的时候只需要查看自己的收件箱即可。

但通过使用 重写轻读 容易带来一个问题,那就是如果一个人拥有了 500 万粉丝,那就意味着他需要往 500 万个收件箱中推送,这对系统来说同样是个不小的挑战,那这个时候就需要采用 推拉结合 的方式

对于粉丝量少的用户(设个阈值),发送微博后可以直接推送到用户的收件箱,对于粉丝较多的用户,只推送给在线的用户,对于读的一端,用户有些可以通过收件箱获取,有些需要自己手动去拉,这种就是推拉结合的方式

8.2 高并发写

1. 数据分片

常见的有: 分库分表Java的ConcurrentHashMapKafka的partition

2. 任务分片

数据分片是对要处理的数据(或请求)进行分片,任务分片是对处理程序本身进行分片。

常见的有:CPU 的指令流水线Map/ReduceTomcat 的1+N+M网络模型

1+N+M 模型
1+N+M 模型
3. 异步化

通过消息中间件,分流处理

4. 批量处理

不管是Mysql、Redis、Kafka 通常上都不会将数据一条一条的进行处理,而是多条合并成一条,一次性写入

8.3 容量规划

高并发读写是一种定性分析,而压力测试和容量规划就是一种定量分析

1)吞吐量、响应时间与并发数

这三个概念都是比较常见的

  • 吞吐量:单位时间内处理的请求数,例如 QPS、TPS 等指标
  • 响应时间:处理每个请求需要的事件
  • 并发数:服务器同时并行处理的请求个数

三者关系:吞吐量 * 响应时间 = 并发数

关键点说明:谈论吞吐量(QPS)的时候,一定需要谈对应的响应时间是多少,随着QPS的增加,响应时间也在增加,虽然 QPS 提上来了,但用户端的响应时间却变长了,客户端的超时率增加,用户体验变差,所以这两者需要权衡,不能一昧地提升 QPS,而不顾及响应时间

2)压力测试与容量评估

容量评估的基本思路:

机器数 = 预估总流量/单机流量

其中分子是一个预估的值(通过历史数据预估),分母通过压力测试得到

在计算的时候需要使用峰值测算,而不能使用均值。尽管有时候峰值持续的时间很短,但不容忽视。

压力测试方法:

  1. 线上压力测试对比测试环境压力测试
  2. 读接口压力测试对比写接口压力测试
  3. 单机压力测试对比全链路压力测试

第九章:高可用与稳定性

高并发使系统更有效率,高可用使系统更可靠

9.1 多副本

不要把所有鸡蛋放到一个篮子里

1)本地缓存多副本

利用消息中间(发布/订阅机制),一条消息发出,多台机器收到后更新自己的本地缓存

2)Redis多副本

Redis Cluster 提供了 Master - Slave 之间的复制机制,当 Master 宕机后可以切换到 Slave。

3)MySQL 多副本

MySQL 之间可以用到异步复制或半异步复制,同步复制性能较差,比较少用

4)消息中间件多副本

对于Kafka类的消息中间件,一个Partition通常至少会指定三个副本,为此Kafka专门设计了一种称为ISR的算法,在多个副本之间做消息的同步

9.2 隔离、限流、熔断和降级

1)隔离

隔离是指将系统或资源分割开,在系统发生故障时能限定传播范围和影响范围,即发生故障后不会出现滚雪球的效应

  1. 数据隔离
  2. 机器隔离
  3. 线程池隔离:核心业务的线程池需要和非核心业务的线程池隔离开
  4. 信号量隔离

信号量隔离是 Hystrix 提出的一种隔离方式,比线程池隔离更要轻量,由于线程池太多会导致线程过多从而导致切换的开销大,而使用信号量隔离不会额外增加线程池,只在调用线程内部执行。信号量本质上是一个数字,记录当前访问某个资源的并发线程数,在线程访问资源之前获取信号量,访问结束时释放信号量,一旦信号量达到阈值,便申请不到信号量,会直接 丢弃请求,而不是阻塞等待

2)限流

限流可以分为技术层面的限流和业务层面的限流。技术层面的限流比较通用,各种业务场景都可以用到;业务层面的限流需要根据具体的业务场景做开发。

具体操作可以空降:《餐厅小故事》| 服务限流的实施

3)熔断
  1. 根据请求失败率做熔断
  2. 根据请求响应做熔断

注意点: 限流是服务端,根据其能力上限设置一个过载保护;而熔断是调用方对自己的一个保护。能熔断的服务肯定不是核心链路上的必选服务,如果是的话,则服务超时或者宕机,前端就不能用了,而不是熔断。熔断其实也是降级的一种方式

4)降级

降级是一种兜底方案,是在系统出故障之后的一个尽力而为的措施,比较偏向业务层面

9.3 灰度发布与回滚

频繁进行系统变更是个风险较高的操作。灰度与回滚可以使该操作变的相对可靠稳定

1)新功能上线的灰度

当一个新的功能上线时,可以将一部分流量导入到这个新的功能,如果验证功能没有问题,再一点点增加流量,最终让所有流量都切换到这个新功能上。

  1. 按 userId 进行流量划分
  2. 固定位数进行流量划分
  3. 属性或标签进行流量划分
2)旧系统重构的灰度

如果旧的系统被重构了,我们不可能在一瞬间把所有旧的系统下线,完全变成新的系统,一般会持续一段时间,新旧系统同时共存,就需要增加流量分配机制。

3)回滚

回滚的方式:

  1. 安装包回滚: 这种方式最简单,不需要开发额外的代码,发现线上有问题直接重新部署之前的安装版本
  2. 功能回滚: 在开发新功能的时候,可以配置相应的配置开关,一旦发现新功能有问题,则关闭开关,让所有流量进入老系统

第十章:事务一致性

分布式解决方案

1)2 PC 理论

2 PC 中有两个角色:事务协调者事务参与者

每一个数据库就是一个参与者,调用方也就是协调者,2 PC 将事务的提交分为两个阶段:

  1. 阶段一:协调者向所有参与者询问是否可以提交事务,并等待回复,各参与者执行事务操作,将 undo 和 redo 日志计入事务日志中,执行成功后给协调者反馈 ack
  2. 阶段二:如果阶段一成功,则通知参与者提交事务,否则利用 undo 日志进行回滚

这种方式也存在了许多问题:

  1. 性能问题:所有参与者在事务比较阶段处于同步阻塞状态,容易导致性能瓶颈
  2. 可靠性问题:如果协调者出现问题,那么会一直处于锁定状态
  3. 数据一致性问题:在阶段2中如果协调者和参与者都挂了,有可能导致数据不一致
2)3PC 理论

解决了 2PC 同时挂掉的问题,将 2PC 的准备阶段再次一分为二

  • 阶段一:协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务
  • 阶段二:如果阶段一成功,协调者会再次发出 preCommit 请求,进入准备阶段,参与者将 undo 和redo 日志计入事务日志中。如果阶段一失败,协调者则发出 abort 请求,参与者便会中断事务
  • 阶段三:如果阶段二成功,协调者发出 doCommit 请求,参与者便会真正提交事务。如果失败,便会发出 rollback 请求,参与者会利用 undo 事务进行回滚,并结束事务

该方式依然会造成数据不一致问题:如果 preCommit 阶段存在部分节点返回 nack,那么协调者刚要中断事务便挂掉了,一定时间后参与者便会继续提交事务,造成数据不一致问题

3)补偿事务 TCC

TCC(try-confirm-cancel)是服务化的二阶段编程模型,核心思想是:针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)。他同样也是分为三个步骤

  • try 阶段:主要是对业务系统做检测及资源预留
  • confirm 阶段:主要是对业务系统做确认提交。try 阶段执行成功并开始执行 confirm 阶段,默认情况下 try 成功,confirm 一定会成功
  • cancel 阶段:主要是业务执行错误,执行回滚,将预留的资源释放

例子:转账操作,第一步在 try 阶段,首先调用远程接口把自己和对方的钱冻结起来,第二步在 confirm 阶段,执行转账操作,如果成功则进行解冻,否则执行 cancel

它解决了数据最终一致性的问题,通过 confirm 和 cancel 的幂等性,保证数据一致性

4)最终一致性(消息中间件)

可以基于 RocketMQ 实现最终一致性。为了能通过消息中间件解决该问题,同时又不和业务耦合,RocketMQ提出了“事务消息”的概念

  1. 步骤1:系统A调用Prepare接口,预发送消息。此时消息保存在消息中间件里,但消息中间件不会把消息给消费方消费,消息只是暂存在那。
  2. 步骤2:系统A更新数据库,进行扣钱操作。
  3. 步骤3:系统A调用Comfirm接口,确认发送消息。此时消息中间件才会把消息给消费方进行消费。

RocketMQ会定期(默认是1min)扫描所有的预发送但还没有确认的消息,回调给发送方,询问这条消息是要发出去,还是取消。发送方根据自己的业务数据,判断这条消息是应该发出去(DB更新成功了),还是应该取消(DB更新失败)

第十一章:多副本一致性

无论是 MySQL的 Master/Slave,还是 Redis 的 Master/Slave,或是Kafka的多副本复制,都是通过牺牲一致性来换取高可用性的。

本章主要对 Paxos、Zab、Raft 三种算法进行解析。做出的笔记内容较多,保证本篇篇幅的情况下,考虑单独抽出讲解,有兴趣的小伙伴可以后续关注~!

第十二章:CAP理论

  • 强一致性 Consistency:是指所有节点同时看到相同的数据。
  • 可用性 Availability:任何时候,读写操作都是成功的,保证服务一直可用
  • 分区容错性 Partition tolerance:当部分节点出现消息丢失或分区故障的时候,分布式系统仍然能够运行

CP的系统追求强一致性,比如Zookeeper,但牺牲了一定的性能

AP的系统追求高可用,牺牲了一定的一致性,比如数据库的主从复制、Kafka的主从复制

1)分布式锁

1. 基于 Zookeeper 实现

可以利用 Zookeeper 的 瞬时节点 的特性。每次加锁都是创建一个瞬时节点,释放锁则删除瞬时节点。因为 Zookeeper 和客户端之间通过心跳探测客户端是否宕机,如果宕机,则 Zookeeper 检测到后自动删除瞬时节点,从而释放锁。

2. 基于 Redis 实现

Redis的性能比Zookeeper更好,所以通常用来实现分布式锁。但 Redis 相对 Zookeeper 也存在些许问题

  1. 没有强一致性的Zab协议。如果Master 宕机,Slave会丢失部分数据,造成多个进程拿到同一把锁
  2. 没有心跳检测。在释放锁之前宕机,会导致锁永远不会释放

第四部分:业务架构知道

第十三章:业务意识

1) 产品经理与需求分析师

产品经理从某种意义上来说就称之为需求分析师。作为一个技术人员,不需要像产品经理或需求分析师那样对需求了如指掌,但具有良好的业务意识确是做业务架构的基本条件

那么什么业务意识?

  1. 了解需求来自何处

有时需求来自何处,技术为谁而坐,往往和公司的基因、盈利模式紧密挂钩,公司本身决定了需求从什么地方来

  1. 判断是真需求还是假需求

很多原因都会导致伪需求,比如老板的决定,面向 KPI 的需求。而其中存在一个因素便是:信息传播的递减效应

当发生一个事件时,第一个人 A 看到事件的全过程,掌握 100 的信息量,描述给 B 的时候,受制于记忆力、表达力等因素只能描述出 90 的信息,往下递推,到 D 的时候可能只剩 60 的信息。

所以,作为一个技术人员,当从产品经理接到需求的时候,一定要回溯,明确需求是在什么背景下提出的,究竟要解决用户的什么问题。

  1. 需求的优先级

人力资源和时间资源是有限的。如何合理分配尤为重要

2)业务是什么

一个内容能称为一个业务,往往具备一个特点,就是闭环。

什么是闭环?

  1. 团队闭环:有自己的产品、技术、运营和销售联合作战
  2. 产品闭环:从内容的生成到消费,整条链路把控
  3. 商业闭环:具备自负盈亏的能力
  4. 纵向闭环:某个垂直领域,涵盖从前到后
  5. 横向闭环:平台模式,横向覆盖某个横切面

3)业务架构的双重含义

业务架构既关乎组织架构,也关乎技术架构

  1. 从理论上讲,合理的团队的组织架构应该是根据业务的发展来决定的,不同的公司在不同的发展阶段会根据业务的发展情况,将壮大的业务拆分,萎靡的业务合并
  2. 支持业务的技术架构,业务架构和计数架构会相互作用,相互影响

第十四章:业务架构思维

1)伪分层

不管是业务架构还是技术架构,C端业务还是B端业务,我们都会用到分层技术

经典分层技术
经典分层技术
伪分层的特征
  1. 底层调用上层:设计分层的时候应深入思考 DIP(依赖反转)原则
  2. 同层之间,服务之间各种双向调用: 这个很容易造成循环依赖问题,考虑是否要抽取 Middle 层来作为中间层
  3. 层之间没有隔离,参数层层透传,一直穿透到最低层,导致底层系统经常变动
总结
  1. 越底层的系统越单一、越简单、越固化
  2. 越上层的系统花样越多、越容易变化。要做到这一点,需要层与层之间有很好的隔离和抽象。
  3. 层与层之间的关系应该严格遵守上层调用下层的准则

2)边界思维

1. 对象层面(SOLID 原则)

一个函数、一个类、一个模块只做一件事,不要把不同的职责糅在一起,这就是边界思维的一种体现

2. 接口层面

首先想到的不是如何实现,而是把系统当做一个黑盒,看系统对外提供的接口是什么,接口也就是系统的边界,定义了系统可以支持什么、不支持什么。所以接口的设计往往比接口的实现更重要!

3. 产品层面

内部实现很复杂,用户界面很简单,把复杂留给自己,把简单留给用户

4. 组织结构层面

总结: 边界思维的重点在于约束,是一个 "负方法" 的思维方式。架构强调的不是系统能支持什么,而是系统的“约束”是什么,不管是业务约束,还是技术约束。没有“约束”,就没有架构。一个设计或系统,如果“无所不能”

3)系统化思维

系统化系统不在于头痛医头脚痛医脚,而是追溯源头,关注整体上的影响,把不同的东西串在一起考虑,而不是割裂后分开来看

4)利益相关者分析

当谈到系统的时候,首先要确定的是系统为哪几类人服务,同哪几个外部系统交互,也就确定了系统的边界。

5)非功能性需求分析(以终为始)

软件有功能需求和非功能需求,非功能性需求有:

  1. 并发性:关注点在于系统能抵抗多大的流量
  2. 一致性:数据一致性问题
  3. 可用性:是否保证服务一直处于可用状态
  4. 可维护性:关注点在于代码的可理解性
  5. 可扩展性: 系统功能是否能够灵活扩展,而不会遇到一个需求就需要大刀阔斧地修改
  6. 可重用性: 开发新的需求,旧的功能模块可以拿过来直接用

6)抽象

语言只是对现实中我们所注意到的事务特征的一种抽象,每一次命名,都是一个抽象化的过程,这个过程会忽略掉现实事务的许多特征。但是抽象的目的是为了交流提供便利,而不是给交流带来负担,因此我们需要对自己的每一次抽象负责,不能抽象到最后自己都不明白抽象的含义是什么。

抽象的几种特征:

  1. 越抽象的词,在词典中个数越少;越具象的词,在词典中个数越多。

  2. 越抽象的词,本身所表达的特征越少;越具象的词,特征越丰富。

  3. 越抽象的词,意义越容易被多重解读;越具象的词,意义越明确

7)建模

建模的本质:把重要的东西进行显性化,进而把这些显性化的构造块互相串联起来,组成一个体系

8)正交分解

分解是一个很朴素的思维方式,把一个大的东西分成几个部分。比分解更为严谨,更为系统的是 正交分解,需要保证两个原则:

  1. 分清:同一层次的多个部分之间要相互独立,无重叠
  2. 分净:完全穷尽,无遗漏

第十五章:技术架构与业务架构的融合

该章节主要是对 DDD(领域驱动模型) 做出解释,比较泛化,这里推荐一本好书 《实现领域驱动设计》 ,书中对 DDD 解说的相对具体,这本书小菜最近也在啃读中,后续会出相应的读书笔记,请伙伴们点点关注,后续不会迷路!

第十六章:个人素质的替身

1)能力模型

对于程序员来说,我们是干技术,很纯粹,技术很好表示你能力越强。但是当你慢慢职位上涨的时候,会发现技术不能代表你的全部。

1. 格局

打开格局,打开格局,平时常说的一句调侃的话却格外重要。

做技术我们需要开阔视野打开格局,我们才能了解更多的技术栈,更好的运用到项目中。

做产品我们需要开阔视野打开格局,我们才能了解市面上的竞品是什么样子,更好的借鉴到自己的项目中。

2. 历史观

格局 是从 空间 的角度看待问题,而 历史观 则是从 时间的角度看待问题。任何一种技术,都不是凭空想出来的,任何一个需求,都不是凭空捏造的,我们需要进行回溯,了解它诞生的背景,才能知其所以然。

3. 抽象能力

有些人抽象出来的事物可以让别人一眼贯通,有些人抽象出来的事物却连自己的看不懂。这就是抽象能力的表现。

很多写代码的人习惯利用 自底向上 的思维解决问题,讨论需求的时候首先想到的是这个需求如何实现,而不是这个需求本身合不合理,对于很多新人来说 需求的合不合理,依赖于需求好不好实现,这样的方式很容易导致 只见树木,不见森林,最后淹没在各种错综复杂的细节中。

4. 深入思考的能力

深入思考的能力主要考察技术的深度

深度并不表示要在所有领域都很精通,而是专注于某个领域,对于专家和全栈工程师的区别,想想哪个职位的薪资可能会更高

5. 落地能力

落地能力值的就是执行力,有空头画大饼的能力,却无落地去实现的能力,只会阻碍项目的正常前行。这大概就是技术不喜销售的原因吧

2)影响力的塑造

进入职场的前几年尤为关键,有的人平步青云,有的人却止步不前。那就是没能很好的塑造自己的影响力。影响力该如何塑造?

1. 关键时候能顶上

最怕的是 事不关己高高挂起 的心态,如果下次摊上事的是你如何?如果当团队中遇到问题,这个时候能够迎上,绝对可以让人知道还有你这一号人物(当然要斟酌抗下的风险,迎难而上并不意味着逞强

2. 打工思维和老板思维

虽然我们常说自己是打工人,但有的时候何不把自己当成合伙人?

打工的思维,安排的事需要干一件,绝不多一点,只管好自己的一亩三分地

老板的思维,这个产品的价值在哪?这个产品存在哪些问题,需要如何改进?为何用户一直投诉的事,还没及时处理?

3. 空杯心态

术业有专攻,水平再高的人都需要谨记山外有山人外有人,否则就会一直待在自己的舒适圈中,刚愎自用

4. 建言献策

不必害怕自己的回答是否正确,而瞻前顾后不敢发言,充分发挥 圆桌文化, 有建议有想法大胆提出,不然你是想留给自己的蛔虫知道吗

第十七章:团队能力的提升

1)不确定性与风险把控

技术管理的首要任务就是项目管理,通常存在以下几种不确定性

1. 需求的不确定性

由于各种外部条件,导致需求提议的想法不是很成熟(可能只是头脑风暴),处于需要不断优化的阶段,那么这个时候过早的进行开发容易浪费资源。作为技术负责人就需要和产品经理以及相关的业务方进行广泛的头痛,需要达成共识的情况,才能投入。

2. 技术的不确定性

启动新项目的时,最怕的就是一开始技术没有很好的选型,到中间开发阶段时候再进行替换,这种劳民伤财的事情还是尽量避免发生。必须在项目早期的时候就进行过多的调研和测试。

3. 人员的不确定性

现在的大多数职员都是面向金线开发,大多数在职情况并不是那么稳定,而将项目的大多权限与业务集中在一名成员上是个不明智的选择,能够进行 AB岗位开发是个不错的选择,两人之间的业务相互熟悉,哪怕是因为请假的原因也能很快的进行替代补充

4. 组织的不确定性

公司越大,业务越复杂,部门越多。随便做一个项目,都可能与好几个业务部门打交道。这些部门可能还在异地,平时只能即时通信,或者远程电话沟通。对于这种情况,在项目前期必须要做尽可能多的沟通,调研对方提供的业务能力,哪些目前有,哪些还在开发中,哪些还没有开发。在充分沟通的基础上,和对方敲定排期表,不定期地同步进度,保证对方的进度和自己在一个节奏上。

分类:

后端

标签:

后端

作者介绍

蔡不菜丶
V1