字节跳动自研强一致在线 KV &表格存储实践 - 下篇

  • 平均数据量分别加、减一个 alpha 值得到高水位值和低水位值,alpha 可以取 Savg 的 10%或 20%,它决定了均衡的松紧程度,根据水位值画出高水位线和低水位线

  • 根据节点数据量与三条线的关系,将它们划分为四个区:

    • 高负载区/主动迁出区:节点数据量高于高水位值
  • 高均衡区/被动迁出区:节点数据量低于高水位值且高于平均值

  • 低均衡区/被动迁入区:节点数据量高于低水位值且低于平均值

  • 低负载区/主动迁入区:节点数据量低于低水位值

  • 当节点位于高负载区时,需要主动迁出副本,目标节点位于迁入区;当节点位于低负载区时,需要主动迁入副本,来源节点是迁出区

  • 当所有节点都位于两个均衡区时,集群达到“足够均衡”状态,下面这个图就是一种“足够均衡”状态

多维度调度策略

以前面的单维度调度为基础,多维度调度的目标是使集群在多个维度上同时或尽量多地达到足够均衡的状态。

我们先想象一下,每个维度都有前面提到的示意图表示它的均衡状态,N 个维度就存在 N 个图。当一个副本发生迁移的时候,会同时改变所有维度的均衡状态,也就是说所有的示意图都会发生改变。如果所有维度都变得更加均衡(均衡区的节点数变多了),或者一部分维度更均衡而另一部分维度不变(均衡区的节点数不变),那么这个迁移是一个好的调度;反正,如果所有维度都变得更加不均衡(均衡区的节点数变少了),或者一部分维度更不均衡而另一部分维度不变,那么这个迁移是一个不好的调度。还有第三种情况,一部分维度更均衡同时也有一部分维度更不均衡了,这是一个中性的调度,往往这种中性的调度是不可避免的,例如集群中只有 A、B 两个节点,A 的流量更高而 B 的数据量更高,由 A 向 B 迁移副本会使流量更均衡而数据量更不均衡,由 B 向 A 迁移副本则相反。

为了判断这种中性的调度能否被允许,我们引入了优先级的概念,为每个维度赋予一个唯一的优先级,牺牲低优维度的均衡度换来高优维度更加均衡是可被接受的,牺牲高优维度的均衡度换来低优维度更加均衡则不可被接受。

仍然考虑前面的例子,因为流量过高会影响读写响应时间进而影响服务质量,我们认为流量的优先级高于数据量优先级,因此由 A 向 B 迁移可被接受。但是也存在一个例外,假设 B 节点的剩余磁盘空间已经接近 0,并且连集群中最小的副本都无法容纳时,即使流量的优先级更好,也不应该允许向 B 迁移任何副本了。为了直观表达这种资源饱和状态,我们在示意图上增加一条硬限线:

配合这个示意图,多维度的负载均衡策略如下:

  • 将多个维度按照优先级排序,从高优维度到低优维度依次执行上文描述的单维度调度策略,仅对流程做少量修改:

  • 源节点上最接近Sbest但小于Sbest的副本为候选迁移对象,如果它导致任一下列情况出现,则将它排除,选择下一个副本作为候选对象,直到找到合适的副本为止:

    • 迁移之后,目标机器在更高优维度上将处于高水位线以上
  • 迁移之后,目标机器在更低优维度上将处于硬限线以上

  • 如果对于某一目标节点,源节点上无法选出迁移对象,将排在目标节点前一位的节点作为新的目标节点,重复上述过程

  • 如果对于所有目标节点,源节点上仍然无法选出迁移对象,将该源节点从排序列表中剔除,重复上述过程

异构机型调度策略

对于同构机型,一个单位的负载在每个节点上都会使用同样比例的资源,我们可以仅根据负载值进行调度,而不必这些负载使用了多少机器资源,但在异构机型上这是不成立的。举个例子,同样是从磁盘上读取 1MB 的数据,在高性能服务器上可能只占用 1%的 IO 带宽和 1%的 CPU cycle,而在虚拟机上可能会占用 5%的 IO 带宽和 3%的 CPU cycle。在不同性能的节点上,同样的负载将产生不同的资源利用率。

要将前面的调度策略应用到异构机型的场景中,首先要将按负载值进行调度修改为按资源利用率进行调度。对于数据量来说,要改为磁盘空间利用率;对于流量来说,要改为 CPU 利用率、IO 利用率等等。为了简化策略,我们将内存、磁盘 IO、网络 IO 等使用情况全部纳入到 CPU 利用率中。解释一下为什么这么做:

  • 对内存来说,我们的进程内存使用量的上限是通过配置项控制的,在部署时,我们会保证内存使用量一定不会超过物理内存大小,剩余物理内存全部用于操作系统的 buffer/cache,实际上也能够被我们利用。内存大小会通过影响诸如 MemTable、BlockCache 的大小而影响节点性能,而这种影响最终会通过 CPU 和 IO 的使用量反映出来,所以我们考察 CPU 和 IO 的利用率就能把内存的使用情况纳入进来。

  • 对于磁盘 IO 来说,IO 利用率最终也会反映在 CPU 利用率上(同步 IO 体现在 wa 上,异步 IO 体现在 sys 上),因此我们考察 CPU 利用率就能把磁盘 IO 的使用情况纳入进来。

  • CPU 中有三级 cache,也有寄存器,在考虑 CPU 利用率时,会把它当作一个整体,不会单独分析 cache 或是寄存器的使用情况。内存和磁盘可以想象成 CPU 的第四、五级 cache,内存越小、磁盘 IO 越慢,CPU 的利用率越高,可以将它们视为一个整体。

异构调度要解决的第二个问题是,资源利用率和负载值之间的转换关系。举个例子,A、B 两个节点的 CPU 利用率分别是 50%和 30%,节点上每个副本的读写请求也是已知的,如何从 A 节点选择最佳的副本迁移到 B 节点,使 A、B 的 CPU 利用率差距最小,要求我们必须计算出每个副本在 A、B 节点上分别会产生多少 CPU 利用率。为了做到这一点,我们尽可能多的收集了每个副本的读写请求信息,例如:

  • 读写请求的 key、value 大小

  • 读的 cache 命中率

  • 更新的随机化程度、删除的比例

根据这些信息,将每个读写请求转换成 N 个标准流量。例如,一个 1KB 以内的请求是一个标准流量,一个 1~2KB 的请求是 2 个标准流量;命中 cache 的请求是一个标准流量,未命中 cache 的请求是 2 个标准流量。知道节点上总的标准流量值,就能根据 CPU 利用率算出这个节点上一个标准流量对应的 CPU 利用率,进而能够算出每个副本在每个节点上对应的 CPU 利用率了。

综上,异构机型调度策略只需要在多维度调度策略的基础上做出如下修改:

  • 节点按照资源利用率排序,而不是负载值

  • 每个副本的负载值要分别转换成源节点的资源利用率和目标节点的资源利用率,在异构机型上,同一个副本的资源利用率会有较大的不同

其它调度策略

KVMaster 中,有一个定时任务执行上述的负载均衡策略,叫做“负载均衡调度器”,这里不再赘述;同时,还有另一个定时任务,用来执行另一类调度,叫做“副本放置调度器”,除了副本安全级别(datacenter/rack/server)、节点异常检测等基本策略之外,它还实现了下面几种调度策略:

  • 业务隔离策略:不同 namespace/table 可以存放在不同的节点上。每个 namespace/table 可指定一个字符串类型的 tag,每个节点可指定一个或多个 tag,副本所在 namespace/table 的 tag 与某节点 tag 相同时,才可放置在该节点上。调度器会对不满足 tag 要求的副本进行调度。

  • 热点检测:当某个数据分片的数据量达到一定阈值时会发生分裂,除此之外,当它的读写流量超过平均值的某个倍数后,也会发生分裂。当分裂发生后,其中一个新产生的分片(左边或右边)的所有副本都会迁移至其他节点,避免节点成为访问热点。

  • 碎片检测:当某个数据分片的数据量和读写流量都小于平均值的一定比例时,会与它所相邻的分片进行合并。合并前会将小分片的所有副本迁移至相邻分片所在的节点上。

表格层

===

前面提到,KV 数据模型过于简单,很难满足一些复杂业务场景的需求。比如:

  • 字段数量和类型比较多

  • 需要在不同的字段维度上进行复杂条件的查询

  • 字段或查询维度经常随着需求而变化。

我们需要更加丰富的数据模型来满足这些场景的需求。在 KV 层之上,我们构建了表格层 ByteSQL,由前面提到的 SQLProxy 实现。ByteSQL 支持通过结构化查询语言(SQL)来写入和读取,并基于 ByteKV 的批量写入(WriteBatch)和快照读接口实现了支持读写混合操作的交互式事务。

表格模型


在表格存储模型中,数据按照数据库(database), 表(table)两个逻辑层级来组织和存放。同一个物理集群中可以创建多个数据库,而每个数据库内也可以创建多个表。表的 Schema 定义中包含以下元素:

  • 表的基本属性,包括数据库名称,表名称,数据副本数等。

  • 字段定义:包含字段的名字,类型,是否允许空值,默认值等属性。一个表中须至少包含一个字段。

  • 索引定义:包含索引名字,索引包含的字段列表,索引类型(Primary Key,Unique Key,Key 等)。一个表中有且仅有一个主键索引(Primary Key),用户也可以加入二级索引(Key 或 Unique Key 类型)来提高 SQL 执行性能。每个索引都可以是单字段索引或多字段联合索引。

表中的每一行都按照索引被编码成多个 KV 记录保存在 ByteKV 中,每种索引类型的编码方式各不相同。Primary Key 的行中包含表中的所有字段的值,而二级索引的行中仅仅包含定义该索引和 Primary Key 的字段。具体每种索引的编码方式如下:

Primary Key: pk_field1, pk_field2,… => non_pk_field1, non_pk_field2…

Unique Key: key_field1, key_field2,…=> pk_field1, pk_field2…

NonUnique Key: key_field1, key_field2,…, pk_field1, pk_field2…=>

其中 pk_field 是定义 Primary Key 的字段,non_pk_field 是表中非 Primary Key 的字段,key_field 是定义某个二级索引的字段。=> 前后的内容分别对应 KV 层的 Key 和 Value 部分。Key 部分的编码依然采用了上述提到的内存可比较编码,从而保证了字段的自然顺序与编码之后的字节顺序相同。而 Value 部分采用了与 protobuf 类似的变长编码方式,尽量减少编码后的数据大小。每个字段的编码中使用 1 byte 来标识该值是否为空值。

全局二级索引


用户经常有使用非主键字段做查询条件的需求,这就需要在这些字段上创建二级索引。在传统的 Sharding 架构中(如 MySQL Shard 集群),选取表中的某个字段做 Sharding Key,将整个表 Hash 到不同的 Shard 中。由于不同 Shard 之间没有高效的分布式事务机制,二级索引需要在每个 Shard 内创建(即局部二级索引)。这种方案的问题在于如果查询条件不包含 Sharding Key,则需要扫描所有 Shard 做结果归并,同时也无法实现全局唯一性约束。

为解决这种问题,ByteSQL 实现了全局二级索引,将主键的数据和二级索引的数据分布在 ByteKV 的不同的分片中,只根据二级索引上的查询条件即可定位到该索引的记录,进一步定位到对应的主键记录。这种方式避免了扫描所有 Shard 做结果归并的开销,也可以通过创建 Unique Key 支持全局唯一性约束,具有很强的水平扩展性。

交互式事务


ByteSQL 基于 ByteKV 的多版本特性和多条记录的原子性写入(WriteBatch),实现了支持快照隔离级别(Snapshot Isolation)的读写事务,其基本实现思路如下:

  1. 用户发起 Start Transaction 命令时,ByteSQL 从 ByteKV Master 获取全局唯一的时间戳作为事务的开始时间戳(Start Timestamp),Start Timestamp 既用作事务内的一致性快照读版本,也用作事务提交时的冲突判断。

  2. 事务内的所有写操作缓存在 ByteSQL 本地的 Write Buffer 中,每个事务都有自己的 Write Buffer 实例。如果是删除操作,也要在 Write Buffer 中写入一个 Tombstone 标记。

  3. 事务内的所有读操作首先读 Write Buffer,如果 Write Buffer 中存在记录则直接返回(若 Write Buffer 中存在 Tombstone 返回记录不存在);否则尝试读取 ByteKV 中版本号小于 Start Timestamp 的记录。

  4. 用户发起 Commit Transaction 命令时,ByteSQL 调用 ByteKV 的 WriteBatch 接口将 Write Buffer 中缓存的记录提交,此时提交是有条件的:对于 Write Buffer 中的每个 Key,都必须保证提交时不能存在比 Start Timestamp 更大的版本存在。如果条件不成立,则必须 Abort 当前事务。这个条件是通过 ByteKV 的 CAS 接口来实现的。

由上述过程可知,ByteSQL 实现了乐观模式的事务冲突检测。这种模式在写入冲突率不高的场景下非常高效。如果冲突率很高,会导致事务被频繁 Abort。

执行流程优化


ByteSQL 提供了更加丰富的 SQL 查询语义,但比起 KV 模型中简单的 Put,Get 和 Delete 等操作却增加了额外的开销。SQL 中的 Insert,Update 和 Delete 操作实际都是一个先读后写的流程。以 Update 为例,先使用 Get 操作从 ByteKV 读取旧值,在旧值上根据 SQL 的 Set 子句更新某些字段生成新值,然后用 Put 操作写入新值到 ByteKV。

在一些场景下,某些字段的值可能是 ByteSQL 内自动生成的(如自动主键,以及具有 DEFAULT/ON UPDATE CURRENT_TIMESTAMP 属性的时间字段)或根据依赖关系计算出来的(如 SET a = a+1),用户需要在 Insert,Update 或 Delete 操作之后立即获取实际变更的数据,需要在写入之后执行一次 Select 操作。总共需要两次 Get 操作和一次 Put 操作。为了优化执行效率,ByteSQL 中实现了 PostgreSQL/Oracle 语法中的 Returning 语义:在同一个 Query 请求中将 Insert/Update 的新值或 Delete 的旧值返回,节省了一次 Get 开销。

UPDATE table1 SET count = count + 1 WHERE id >= 10 RETURNING id, count;

在线 schema 变更


业务需求的不断演进和变化导致 Schema 变更成为无法逃避的工作,传统数据库内置的 Schema 变更方案一般需要阻塞整表的读写操作,这是线上应用所无法接受的。ByteSQL 使用了 Google F1 的在线 Schema 变更方案[3],变更过程中不会阻塞线上读写请求。

ByteSQL Schema 元数据包含了库和表的定义,这些元数据都保存在 ByteKV 中。SQLProxy 实例是无状态的,每个实例定期从 ByteKV 同步 Schema 到本地,用来解析并执行 Query 请求。同时集群中有一个专门的 Schema Change Worker 实例负责监听并执行用户提交的 Schema 变更任务。Schema Change Worker 一旦监听到用户提交的 Schema 变更请求,就将其放到一个请求队列中并按序执行。本节从数据一致性异常的产生和解决角度,阐述了引入 Schema 中间状态的原因。详细的正确性证明可以参考原论文。

由于不同的 SQLProxy 实例加载 Schema 的时机并不相同,整个集群在同一时刻大概率会有多个版本的 Schema 在使用。如果 Schema 变更过程处理不当,会造成表中数据的不一致。以创建二级索引为例,考虑如下的执行流程:

  1. Schema Change Worker 执行了一个 Create Index 变更任务,包括向 ByteKV 中填充索引记录和写入元数据。

  2. SQLProxy 实例 1 加载了包含新索引的 Schema 元数据。

  3. SQLProxy 实例 2 执行 Insert 请求。由于实例 2 尚未加载索引元数据,Insert 操作不包含新索引记录的写入。

  4. SQLProxy 实例 2 执行 Delete 请求。由于实例 2 尚未加载索引元数据,Delete 操作不包含新索引记录的删除。

  5. SQLProxy 实例 2 加载了包含新索引的 Schema 元数据。

第 3 步和第 4 步都会导致二级索引和主键索引数据的不一致的异常:第 3 步导致二级索引记录的缺失(Lost Write),第 4 步导致二级索引记录的遗留(Lost Delete)。这些异常的成因在于不同 SQLProxy 实例加载 Schema 的时间不同,导致有些实例认为索引已经存在,而另外一些实例认为索引不存在。具体而言,第 2 步 Insert 的异常是由于索引已经存在,而写入方认为其不存在;第 3 步的 Delete 异常是由于写入方感知到了索引的存在,而删除方未感知到。实际上,Update 操作可能会同时导致上述两种异常。

为了解决 Lost Write 异常,我们需要保证对于插入的每行数据,写入实例需要先感知到索引存在,然后再写入;而对于 Lost Delete 异常,需要保证同一行数据的删除实例比写入实例先感知到索引的存在(如果写入实例先感知索引,删除实例后感知,删除时有可能会漏删索引而导致 Lost Delete)。

然而,我们无法直接控制不同 SQLProxy 实例作为写入实例和删除实例的感知顺序,转而使用了间接的方式:给 Schema 定义了两种控制读写的中间状态:DeleteOnly 状态和 WriteOnly 状态,Schema Change Worker 先写入 DeleteOnly 状态的 Schema 元数据,待元数据同步到所有实例后,再写入 WriteOnly 状态的 Schema 元数据。那些感知到 DeleteOnly 状态的实例只能删除索引记录,不能写入索引记录;感知到 WriteOnly 状态的实例既可以删除又可以插入索引记录。这样就解决了 Lost Delete 异常。

而对于 Lost Write 异常,我们无法阻止尚未感知 Schema WriteOnly 状态的实例写入数据(因为整个 Schema 变更过程是在线的),而是将填充索引记录的过程(原论文中称之为 Reorg 操作)推迟到了 WriteOnly 阶段之后执行,从而既填充了表中存量数据对应的索引记录,也填充了那些因为 Lost Write 异常而缺失的索引记录。待填充操作完成后,就可以将 Schema 元数据更新为对外可见的 Public 状态了。

我们通过引入两个中间状态解决了 Schema 变更过程中数据不一致的异常。这两个中间状态均是对 ByteSQL 内部而言的,只有最终 Public 状态的索引才能被用户看到。这里还有一个关键问题:如何在没有全局成员信息的环境中确保将 Schema 状态同步到所有 SQLProxy 实例中?解决方案是在 Schema 中维护一个全局固定的 Lease Time,每个 SQLProxy 在 Lease Time 到期前需要重新从 ByteKV 中加载 Schema 来续约。Schema Change Worker 每次更新 Schema 之后,需要等到所有 SQLProxy 加载成功后才能进行下一次更新。这就需要保证两次更新 Schema 的间隔需要大于一定时间。至于多长的间隔时间是安全的,有兴趣的读者可以详细阅读原论文[3]来得到答案。如果某个 SQLProxy 因为某种原因无法在 Lease Time 周期内加载 Schema,则设置当前 ByteSQL 实例为不可用状态,不再处理读写请求。

未来探讨

====

更多的一致性级别


在跨机房部署的场景里,总有部分请求需要跨机房获取事务时间戳,这会增加响应延迟;同时跨机房的网络环境不及机房内部稳定,跨机房网络的稳定性直接影响到集群的稳定性。实际上,部分业务场景并不需要强一致保证。在这些场景中,我们考虑引入混合逻辑时钟 HLC[4]来替代原有的全局授时服务,将 ByteKV 改造成支持因果一致性的系统。同时,我们可以将写入的时间戳作为同步口令返回给客户端,客户端在后续的请求中携带上同步口令,以传递业务上存在因果关系而存储系统无法识别的事件之间的 happen-before 关系,即会话一致性。

此外,还有部分业务对延迟极其敏感,又有多数据中心访问的需求;而 ByteKV 多机房部署场景下无法避免跨机房延迟。如果这部分业务只需要机房之间保持最终一致即可,我们可以进行机房间数据同步,实现类最终一致性的效果。

Cloud Native


随着 CloudNative 的进一步发展,以无可匹敌之势深刻影响着现有的开发部署模型。ByteKV 也将进一步探索与 CloudNative 的深入结合。探索基于 Kubernetes 的 auto deployment, auto scaling, auto healing。进一步提高资源的利用率,降低运维的成本,增强服务的易用性。提供一个更方便于 CloudNative 用户使用的 ByteKV。

参考文献

====

[1] Ongaro D, Ousterhout J. In search of an understandable consensus algorithm (extended version)[J]. 2013.

[2] https://github.com/facebook/mysql-5.6/wiki/MyRocks-record-format#memcomparable-format

[3] Rae I, Rollins E, Shute J, et al. Online, asynchronous schema change in F1[J]. Proceedings of the VLDB Endowment, 2013, 6(11): 1045-1056.

[4] Kulkarni S, Demirbas M, Madeppa D, et al. Logical physical clocks and consistent snapshots in globally distributed databases[C]//The 18th International Conference on Principles of Distributed Systems. 2014.

[5] Peng D, Dabek F. Large-scale incremental processing using distributed transactions and notifications[J]. 2010.

[6] https://www.cockroachlabs.com/blog/how-cockroachdb-distributes-atomic-transactions/

[7] https://www.cockroachlabs.com/blog/serializable-lockless-distributed-isolation-cockroachdb/

[8] https://www.cockroachlabs.com/blog/living-without-atomic-clocks/

[9] Shute J, Oancea M, Ellner S, et al. F1-the fault-tolerant distributed rdbms supporting google’s ad business[J]. 2012.

[10] Corbett J C, Dean J, Epstein M, et al. Spanner: Google’s globally distributed database[J]. ACM Transactions on Computer Systems (TOCS), 2013, 31(3): 1-22.

[11] Ongaro D. Consensus: Bridging theory and practice[D]. Stanford University, 2014.

[12] Roohitavaf M, Ahn J S, Kang W H, et al. Session guarantees with raft and hybrid logical clocks[C]//Proceedings of the 20th International Conference on Distributed Computing and Networking. 2019: 100-109.

[13] Huang G, Cheng X, Wang J, et al. X-Engine: An optimized storage engine for large-scale E-commerce transaction processing[C]//Proceedings of the 2019 International Conference on Management of Data. 2019: 651-665.

更多分享

====

获取最新《大规模混合部署项目在字节跳动的落地实践》技术沙龙的回放地址和 ppt 链接,请在「字节跳动技术团队」微信公众号后台回复: 架构技术沙龙 。

字节跳动基础架构团队

==========

字节跳动基础架构团队是支撑字节跳动旗下包括抖音、今日头条、西瓜视频、火山小视频在内的多款亿级规模用户产品平稳运行的重要团队,为字节跳动及旗下业务的快速稳定发展提供了保证和推动力。

公司内,基础架构团队主要负责字节跳动私有云建设,管理数以万计服务器规模的集群,负责数万台计算/存储混合部署和在线/离线混合部署,支持若干 EB 海量数据的稳定存储。

文化上,团队积极拥抱开源和创新的软硬件架构。我们长期招聘基础架构方向的同学,具体可参见  job.bytedance.com (文末“阅读原文”),感兴趣可以联系邮箱  guoxinyu.0372@bytedance.com



欢迎关注「字节跳动技术团队」
点击阅读原文,快来加入我们吧!

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

可以看出,笔者的工作学习模式便是由以下 「六个要点」 组成:

❝ 多层次的工作/学习计划 + 番茄工作法 + 定额工作法 + 批处理 + 多任务并行 + 图层工作法❞

希望大家能将这些要点融入自己的工作学习当中,我相信一定会工作与学习地更富有成效。

下面是我学习用到的一些书籍学习导图,以及系统的学习资料。每一个知识点,都有对应的导图,学习的资料,视频,面试题目。

**如:我需要学习 **Flutter的知识。(大家可以参考我的学习方法)

  • Flutter 的思维导图(无论学习什么,有学习路线都会事半功倍)

  • Flutter进阶学习全套手册

  • Flutter进阶学习全套视频

大概就上面这几个步骤,这样学习不仅高效,而且能系统的学习新的知识。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

处理 + 多任务并行 + 图层工作法❞

希望大家能将这些要点融入自己的工作学习当中,我相信一定会工作与学习地更富有成效。

下面是我学习用到的一些书籍学习导图,以及系统的学习资料。每一个知识点,都有对应的导图,学习的资料,视频,面试题目。

**如:我需要学习 **Flutter的知识。(大家可以参考我的学习方法)

  • Flutter 的思维导图(无论学习什么,有学习路线都会事半功倍)

[外链图片转存中…(img-ILovpA3N-1713585533825)]

  • Flutter进阶学习全套手册

[外链图片转存中…(img-ahK6J7dC-1713585533826)]

  • Flutter进阶学习全套视频

[外链图片转存中…(img-zxy6qyPI-1713585533827)]

大概就上面这几个步骤,这样学习不仅高效,而且能系统的学习新的知识。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值