进了互联网公司,整天也就是搬砖,等到了面试的时候,发现数据库方面,忘得一塌糊涂,抽时间整理了一些数据库方面的题。
进了互联网公司,整天也就是搬砖,等到了面试的时候,发现数据库方面,忘得一塌糊涂,抽时间整理了一些数据库方面的题。
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如 A 正在从一张银行卡中取钱,在 A 取钱的过程结束前,B 不能向这张卡转账。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。 持久性(Durability)
从理论上来说, 事务应该彼此完全隔离, 以避免并发事务所导致的问题,然而, 那样会对性能产生极大的影响, 因为事务必须按顺序运行, 在实际开发中, 为了提升性能, 事务会以较低的隔离级别运行, 事务的隔离级别可以通过隔离事务属性指定。
1、脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果因此本事务先后两次读到的数据结果会不一致。
3、幻读:幻读解决了不重复读,保证了同一个事务里,查询的结果都是事务开始时的状态(一致性)。
例如:事务 T1 对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作 这时事务 T2 又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。 而操作事务 T1 的用户如果再查看刚刚修改的数据,会发现还有跟没有修改一样,其实这行是从事务 T2 中添加的,就好像产生幻觉一样,这就是发生了幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 read-uncommitted | 是 | 是 | 是 |
不可重复读 read-committed | 否 | 是 | 是 |
可重复读 repeatable-read | 否 | 否 | 是 |
串行化 serializable | 否 | 否 | 否 |
读未提交:另一个事务修改了数据,但尚未提交,而本事务中的 SELECT 会读到这些未被提交的数据脏读 | |||
不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果因此本事务先后两次读到的数据结果会不一致。 | |||
可重复读:在同一个事务里,SELECT 的结果是事务开始时时间点的状态,因此,同样的 SELECT 操作读到的结果会是一致的。但是,会有幻读现象 | |||
串行化:最高的隔离级别,在这个隔离级别下,不会产生任何异常。并发的事务,就像事务是在一个个按照顺序执行一样 | |||
MySQL 默认的事务隔离级别为 repeatable-read | |||
MySQL 支持 4 中事务隔离级别. | |||
事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持. | |||
Oracle 支持的 2 种事务隔离级别:READ_COMMITED , SERIALIZABLE | |||
补充: |
|||
SQL 规范所规定的标准,不同的数据库具体的实现可能会有些差异 | |||
MySQL 中默认事务隔离级别是“可重复读”时并不会锁住读取到的行 | |||
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,鱼和熊掌不可兼得啊。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为 Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
MySQL 有多种存储引擎,每种存储引擎有各自的优缺点,可以择优选择使用:MyISAM、InnoDB、MERGE、MEMORY(HEAP)、BDB(BerkeleyDB)、EXAMPLE、FEDERATED、ARCHIVE、CSV、BLACKHOLE
。
虽然 MySQL 里的存储引擎不只是 MyISAM 与 InnoDB 这两个,但常用的就是两个。
两种存储引擎的大致区别表现在:
select count(*) from table
时,InnoDB 需要扫描一遍整个表来计算有多少行,但是 MyISAM 只要简单的读出保存好的行数即可。注意的是,当 count(*)语句包含 where 条件时 MyISAM 也需要扫描整个表。DELETE FROM table
时,InnoDB 不会重新建立表,而是一行一行的 删除,效率非常慢。MyISAM 则会重建表。update table set a=1 where user like '%lee%'
。有人说 MyISAM 只能用于小型应用,其实这只是一种偏见。如果数据量比较大,这是需要通过升级架构来解决,比如分表分库,而不是单纯地依赖存储引擎。
现在一般都是选用 innodb 了,主要是 MyISAM 的全表锁,读写串行问题,并发效率锁表,效率低,MyISAM 对于读写密集型应用一般是不会去选用的。
MEMORY 是 MySQL 中一类特殊的存储引擎。它使用存储在内存中的内容来创建表,而且数据全部放在内存中。这些特性与前面的两个很不同。
每个基于 MEMORY 存储引擎的表实际对应一个磁盘文件。该文件的文件名与表名相同,类型为 frm 类型。该文件中只存储表的结构。而其数据文件,都是存储在内存中,这样有利于数据的快速处理,提高整个表的效率。值得注意的是,服务器需要有足够的内存来维持 MEMORY 存储引擎的表的使用。如果不需要了,可以释放内存,甚至删除不需要的表。
MEMORY 默认使用哈希索引。速度比使用 B 型树索引快。当然如果你想用 B 型树索引,可以在创建索引时指定。
注意,MEMORY 用到的很少,因为它是把数据存到内存中,如果内存出现异常就会影响数据。如果重启或者关机,所有数据都会消失。因此,基于MEMORY 的表的生命周期很短,一般是一次性的。
事务处理上方面
锁级别
select--from--where--group by--having--order by
其中 select 和 from 是必须的,其他关键词是可选的,这六个关键词的执行顺序 与 sql 语句的书写顺序并不是一样的,而是按照下面的顺序来执行
也就是说,在写 SQL 文的时候,尽量把数据量小的表放在最右边来进行关联(用小表去匹配大表),而把能筛选出小量数据的条件放在 where 语句的最左边 (用小表去匹配大表)
时表可以手动删除:
DROP TEMPORARY TABLE IF EXISTS temp_tb;
临时表只在当前连接可见,当关闭连接时,MySQL 会自动删除表并释放所有空间。因此在不同的连接中可以创建同名的临时表,并且操作属于本连接的临时表。
创建临时表的语法与创建表语法类似,不同之处是增加关键字 TEMPORARY,如:
CREATE TEMPORARY TABLE tmp_table ( NAME VARCHAR (10) NOT NULL, time date NOT NULL);select * from tmp_table;
那为什么大家不都用 Hash 索引而还要使用 B+树索引呢?
MySQL 中,只有 HEAP/MEMORY 引擎才显示支持 Hash 索引。
常用的 InnoDB 引擎中默认使用的是 B+树索引,它会实时监控表上索引的使用情况,如果认为建立哈希索引可以提高查询效率,则自动在内存中的“自适应哈希索引缓冲区”建立哈希索引(在 InnoDB 中默认开启自适应哈希索引),通过观察搜索模式,MySQL 会利用 index key 的前缀建立哈希索引,如果一个表几乎大部分都在缓冲池中,那么建立一个哈希索引能够加快等值查询。
如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;当然了,这个前提是,键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;
如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;
同理,哈希索引没办法利用索引完成排序,以及 like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);
哈希索引也不支持多列联合索引的最左匹配规则;
B+树索引的关键字检索效率比较平均,不像 B 树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈希碰撞问题。
在大多数场景下,都会有范围查询、排序、分组等查询特征,用 B+树索引就可以了。
<,<=,=,>,>=,between,in
聚合索引(clustered index) / 非聚合索引(nonclustered index)
根本区别
聚集索引和非聚集索引的根本区别是表记录的排列顺序和与索引的排列顺序是否一致。
聚集索引
聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,只要找到第一个索引值记录,其余就连续性的记录在物理也一样连续存放。聚集索引对应的缺点就是修改慢,因为为了保证表中记录的物理和索引顺序一致,在记录插入的时候,会对数据页重新排序。
聚集索引类似于新华字典中用拼音去查找汉字,拼音检索表于书记顺序都是按照 a~z 排列的,就像相同的逻辑顺序于物理顺序一样,当你需要查找 a,ai 两个读音的字,或是想一次寻找多个傻(sha)的同音字时,也许向后翻几页,或紧接着下一行就得到结果了。
非聚集索引
非聚集索引制定了表中记录的逻辑顺序,但是记录的物理和索引不一定一致,两种索引都采用 B+树结构,非聚集索引的叶子层并不和实际数据页相重叠,而采用叶子层包含一个指向表中的记录在数据页中的指针方式。非聚集索引层次多,不会造成数据重排。
非聚集索引类似在新华字典上通过偏旁部首来查询汉字,检索表也许是按照横、竖、撇来排列的,但是由于正文中是 a~z 的拼音顺序,所以就类似于逻辑地址于物理地址的不对应。同时适用的情况就在于分组,大数目的不同值,频繁更新的列中,这些情况即不适合聚集索引。
悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的 select … for update 操作来实现悲观锁。当数据库执行 select for update 时会获取被 select 中的数据行的行锁,因此其他并发执行的 select for update 如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update 获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。
这里需要注意的一点是不同的数据库对 select for update 的实现和支持都是有所区别的,例如 oracle 支持 select for update no wait,表示如果拿不到锁立刻报错,而不是等待,MySQL 就没有 no wait 这个选项。另外MySQL 还有个问题是 select for update 语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在 MySQL 中用悲观锁务必要确定走了索引,而不是全表扫描。
乐观锁,也叫乐观并发控制,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,那么当前正在提交的事务会进行回滚。
乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。
乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,然后按照如下方式实现:
乐观锁(给表加一个版本号字段) 这个并不是乐观锁的定义,给表加版本号,是数据库实现乐观锁的一种方式。
1\. SELECT data AS old_data, version AS old_version FROM …;2\. 根据获取的数据进行业务操作,得到 new_data 和 new_version3\. UPDATE SET data = new_data, version = new_version WHERE version = old_versionif (updated row > 0) { // 乐观锁获取成功,操作完成} else { // 乐观锁获取失败,回滚并重试}
乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能
乐观锁还适用于一些比较特殊的场景,例如在业务操作过程中无法和数据库保持连接等悲观锁无法适用的地方。
悲观锁和乐观锁是数据库用来保证数据并发安全防止更新丢失的两种方法,例子在select ... for update
前加个事务就可以防止更新丢失。悲观锁和乐观锁大部分场景下差异不大,一些独特场景下有一些差别,一般我们可以从如下几个方面来判断。
响应速度:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。
冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大。
重试代价:如果重试代价大,建议采用悲观锁。
1. 性能
NOSQL 是基于键值对的,可以想象成表中的主键和值的对应关系,而且不需要经过 SQL 层的解析,所以性能非常高。
2. 可扩展性
同样也是因为基于键值对,数据之间没有耦合性,所以非常容易水平扩展。
1. 复杂查询
可以用 SQL 语句方便的在一个表以及多个表之间做非常复杂的数据查询。
2. 事务支持
使得对于安全性能很高的数据访问要求得以实现。
对于这两类数据库,对方的优势就是自己的弱势,反之亦然。
NOSQL 数据库慢慢开始具备 SQL 数据库的一些复杂查询功能,比如 MongoDB。
对于事务的支持也可以用一些系统级的原子操作来实现例如乐观锁之类的方法来曲线救国,比如 Redis set nx。
第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。
第一范式的合理遵循需要根据系统的实际需求来定。比如某些数据库系统中需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字段就行。但是如果系统经常会访问“地址”属性中的“城市”部分,那么就非要将“地址”这个属性重新拆分为省份、城市、详细地址等多个部分进行存储,这样在对地址中某一部分操作的时候将非常方便。这样设计才算满足了数据库的第一范式,如下表所示。
上表所示的用户信息遵循了第一范式的要求,这样在对用户使用城市进行分类的时候就非常方便,也提高了数据库的性能。
第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
比如要设计一个订单信息表,因为订单中可能会有多种商品,所以要将订单编号和商品编号作为数据库表的联合主键。
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。
比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。
同步复制
异步复制
半同步复制
问题 1:master 的写操作,slaves 被动的进行一样的操作,保持数据一致性,那么 slave 是否可以主动的进行写操作?
假设 slave 可以主动的进行写操作,slave 又无法通知 master,这样就导致了 master 和 slave 数据不一致了。因此slave 不应该进行写操作,至少是 slave 上涉及到复制的数据库不可以写。实际上,这里已经揭示了读写分离的概念。
问题 2:主从复制中,可以有 N 个 slave,可是这些 slave 又不能进行写操作,要他们干嘛?
以实现数据备份。
类似于高可用的功能,一旦 master 挂了,可以让 slave 顶上去,同时 slave 提升为 master。
异地容灾,比如 master 在北京,地震挂了,那么在上海的 slave 还可以继续。
主要用于实现 scale out,分担负载,可以将读的任务分散到 slaves 上。
【很可能的情况是,一个系统的读操作远远多于写操作,因此写操作发向 master,读操作发向 slaves 进行操作】
问题 3:主从复制中有 master,slave1,slave2,...等等这么多 MySQL 数据库,那比如一个 JAVA WEB 应用到底应该连接哪个数据库?
当 然,我们在应用程序中可以这样,insert/delete/update
这些更新数据库的操作,用connection(for master)
进行操作,select 用 connection(for slaves)
进行操作。那我们的应用程序还要完成怎么从 slaves 选择一个来执行 select,例如使用简单的轮循算法。
这样的话,相当于应用程序完成了 SQL 语句的路由,而且与 MySQL 的主从复制架构非常关联,一旦 master 挂了,某些 slave 挂了,那么应用程序就要修改了。能不能让应用程序与 MySQL 的主从复制架构没有什么太多关系呢?
找一个组件,application program 只需要与它打交道,用它来完成 MySQL 的代理,实现 SQL 语句的路由。
MySQL proxy 并不负责,怎么从众多的 slaves 挑一个?可以交给另一个组件(比如 haproxy)来完成。
这就是所谓的MySQL READ WRITE SPLITE,MySQL
的读写分离。
问题 4:如果 MySQL proxy , direct , master 他们中的某些挂了怎么办?
总统一般都会弄个副总统,以防不测。同样的,可以给这些关键的节点来个备份。
问题 5:当 master 的二进制日志每产生一个事件,都需要发往 slave,如果我们有 N 个 slave,那是发 N 次,还是只发一次?
如果只发一次,发给了 slave-1,那 slave-2,slave-3,...它们怎么办?
显 然,应该发 N 次。实际上,在 MySQL master 内部,维护 N 个线程,每一个线程负责将二进制日志文件发往对应的 slave。master 既要负责写操作,还的维护 N 个线程,负担会很重。可以这样,slave-1 是 master 的从,slave-1 又是 slave-2,slave-3,...的主,同时 slave-1 不再负责 select。 slave-1 将 master 的复制线程的负担,转移到自己的身上。这就是所谓的多级复制的概念。
问题 6:当一个 select 发往 MySQL proxy,可能这次由 slave-2 响应,下次由 slave-3 响应,这样的话,就无法利用查询缓存了。
应该找一个共享式的缓存,比如 memcache 来解决。将 slave-2,slave-3,...这些查询的结果都缓存至 mamcache 中。
问题 7:随着应用的日益增长,读操作很多,我们可以扩展 slave,但是如果 master 满足不了写操作了,怎么办呢?
scale on ?更好的服务器? 没有最好的,只有更好的,太贵了。。。
scale out ? 主从复制架构已经满足不了。
可以分库【垂直拆分】,分表【水平拆分】。
对于复杂、效率低的 sql 语句,我们通常是使用 explain sql 来分析 sql 语句,这个语句可以打印出,语句的执行。这样方便我们分析,进行优化
const、eq_reg、ref、range、index
和ALL
between ,< ,>
等查询;内连接查询操作列出与连接条件匹配的数据行,它使用比较运算符比较被连接列的 列值。
内连接分三种:
(=)
运算符比较被连接列的列值,其查询结 果中列出被连接表中的所有列,包括其中的重复列。例,下面使用等值连接列出 authors 和 publishers 表中位于同一城市的作者和出版社:
SELECT * FROM authors AS a INNER JOIN publishers AS p ON a.city=p.city
不等连接: 在连接条件使用除等于运算符以外的其它比较运算符比较被连接的 列的列值。这些运算符包括>、>=、<=、<、!>、!<
和<>
。
自然连接:在连接条件中使用等于(=)运算符比较被连接列的列值,但它使用选 择列表指出查询结果集合中所包括的列,并删除连接表中的重复列。
例,在选择列表中删除 authors 和 publishers 表中重复列(city 和 state):
SELECT a.*,p.pub_id,p.pub_name,p.country FROM authors AS a INNER JOIN publishers AS p ON a.city=p.city
外连接,返回到查询结果集合中的不仅包含符合连接条件的行,而且还包括左表(左外连接或左连接)、右表(右外连接或右连接)或两个边接表(全外连接)中的所有数据行。
例如 1:
SELECT a.*,b.* FROM luntan LEFT JOIN usertable as b ON a.username=b.username
例如 2:
SELECT a.*,b.* FROM city as a FULL OUTER JOIN user as b ON a.username=b.username
交叉连接不带 WHERE
子句,它返回被连接的两个表所有数据行的“笛卡尔积”,返回到结果集合中的数据行数等于第一个表中符合查询条件的数据行数乘以第二个表中符合查询条件的数据行数。
例,titles 表中有 6 类图书,而 publishers 表中有 8 家出版社,则下 列交叉连接检索到的记录数将等于 6*8=48 行。
例如:
SELECT type,pub_name FROM titles CROSS JOIN publishers ORDER BY type
笛卡尔积是两个表每一个字段相互匹配,去掉where
或者inner join
的等值 得出的结果就是笛卡尔积。笛卡尔积也等同于交叉连接。
MySQL 有三种锁的级别:页级、表级、行级。
死锁的关键在于:两个(或以上)的 Session 加锁的顺序不一致。
那么对应的解决死锁问题的关键就是:让不同的 session加锁有次序。
SELECT trx_MySQL_thread_id FROM information_schema.INNODB_TRX;
Innodb 行锁的等待时间,单位秒。可在会话级别设置,RDS 实例该参数的默认值为 50(秒)。
生产环境不推荐使用过大的 innodb_lock_wait_timeout
参数值
该参数支持在会话级别修改,方便应用在会话级别单独设置某些特殊操作的行锁等待超时时间,如下:
set innodb_lock_wait_timeout=1000; —设置当前会话 Innodb 行锁等待超时时间,单位秒。
char 的长度是不可变的,而 varchar 的长度是可变的。
定义一个 char[10]和 varchar[10]。
如果存进去的是‘csdn’,那么 char 所占的长度依然为 10,除了字符‘csdn’外,后面跟六个空格,varchar 就立马把长度变为 4 了,取数据的时候,char 类型的要用 trim()去掉多余的空格,而 varchar 是不需要的。
char 的存取数度还是要比 varchar 要快得多,因为其长度固定,方便程序的存储与查找。
char 也为此付出的是空间的代价,因为其长度固定,所以难免会有多余的空格占位符占据空间,可谓是以空间换取时间效率。
varchar 是以空间效率为首位。
char 的存储方式是:对英文字符(ASCII)占用 1 个字节,对一个汉字占用两个字节。
varchar 的存储方式是:对每个英文字符占用 2 个字节,汉字也占用 2 个字节。
两者的存储数据都非 unicode 的字符数据。
MySQL 高并发环境解决方案 分库 分表 分布式 增加二级缓存。。。。。
需求分析:互联网单位 每天大量数据读取,写入,并发性高。
Undo Log 是为了实现事务的原子性,在 MySQL 数据库 InnoDB 存储引擎中,还用了 Undo Log 来实现多版本并发控制(简称:MVCC)。
事务的原子性(Atomicity)事务中的所有操作,要么全部完成,要么不做任何操作,不能只做部分操作。如果在执行的过程中发生了错误,要回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过。
原理Undo Log 的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为 UndoLog)。然后进行数据的修改。如果出现了错误或者用户执行了 ROLLBACK 语句,系统可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。
之所以能同时保证原子性和持久化,是因为以下特点:
缺陷:每个事务提交前将数据和 Undo Log 写入磁盘,这样会导致大量的磁盘 IO,因此性能很低。
如果能够将数据缓存一段时间,就能减少 IO 提高性能。但是这样就会丧失事务的持久性。因此引入了另外一种机制来实现持久化,即 Redo Log。
在以往的面试经历中总结了一些经验,其实当你面试到一个水平相对较高的职位时,往往较量的技术方面已经是其次,大部分公司在与你聊完技术之后,更看重你的沟通能力、解决问题的能力、以及你以往的一些比较成功的经历。
最后,分享一些 Java 架构学习资料和面试资料,有需要的小伙伴可以点击下面的链接免费领取
2019Java 秋招面试复习资料分享(面试专题+学习笔记+架构笔记文档)
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5d9c1d7001491e07e7f6938e
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。