亿级大表分库分表实战总结(万字干货,实战复盘)


分库分表的文章网上很是多,可是大多内容比较零散,以讲解知识点为主,没有完整地说明一个大表的切分、新架构设计、上线的完整过程。mysql

所以,我结合去年作的一个大型分库分表项目,来复盘一下完整的分库分表从架构设计 到 发布上线的实战总结。算法


1.前言

为何须要作分库分表。这个相信你们多少都有所了解。sql

海量数据的存储和访问成为了MySQL数据库的瓶颈问题,日益增加的业务数据,无疑对MySQL数据库形成了至关大的负载,同时对于系统的稳定性和扩展性提出很高的要求。数据库

并且单台服务器的资源(CPU、磁盘、内存等)老是有限的,最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。api

目前来讲通常有两种方案。缓存

1)一种是更换存储,不使用MySQL,好比可使用HBase、polarDB、TiDB等分布式存储。服务器

2)若是出于各类缘由考虑,仍是想继续使用MySQL,通常会采用第二种方式,那就是分库分表。微信


文章开头就说了,网上分库分表文章不少,对知识点讲解比较多,所以,本文将再也不过多赘述分库分表方案的范式处理。架构

而是专一于梳理分库分表从架构设计 到 发布上线的完整过程,同时总结其中的注意事项和最佳实践。包括五个部分:app

  • 业务重构

  • 存储架构设计

  • 改造和上线

  • 稳定性保障

  • 项目管理

尤为是各个阶段的最佳实践,都是血与泪凝聚的经验教训。

2.第一阶段:业务重构(可选)

对于微服务划分比较合理的分库分表行为,通常只须要关注存储架构的变化,或者只须要在个别应用上进行业务改造便可,通常不须要着重考虑“业务重构” 这一阶段,所以,这一阶段属于“可选”。

本次项目的第一大难点,在于业务重构。

而本次拆分项目涉及到的两张大表A和B,单表将近八千万的数据,是从单体应用时代遗留下来的,从一开始就没有很好的领域驱动/MSA架构设计,逻辑发散很是严重,到如今已经涉及50+个在线服务和20+个离线业务的的直接读写。

所以,如何保证业务改造的完全性、全面性是重中之重,不能出现有遗漏的状况。

另外,表A 和 表B 各自有2、三十个字段,两表的主键存在一一对应关系,所以,本次分库分表项目中,还须要将两个表进行重构融合,将多余/无用的字段剔除。

2.1 查询统计

在线业务经过分布式链路追踪系统进行查询,按照表名做为查询条件,而后按照服务维度进行聚合,找到全部相关服务,写一个文档记录相关团队和服务。

这里特别注意下,不少表不是只有在线应用在使用,不少离线算法和数据分析的业务也在使用,这里须要一并的梳理好,作好线下跨团队的沟通和调研工做,以避免切换后影响正常的数据分析。

2.2 查询拆分与迁移

建立一个jar包,根据2.1的统计结果,与服务owner合做将服务中的相关查询都迁移到这个jar包中(本项目的jar包叫projected)。

此处为1.0.0-SNAPSHOT版本。

而后将本来服务内的xxxMapper.xxxMethod( ) 所有改为projectdb.xxxMethod( )进行调用。

这样作有两个好处:

  • 方便作后续的查询拆分分析。

  • 方便后续直接将jar包中的查询替换为改造后 中台服务 的rpc调用,业务方只需升级jar包版本,便可快速从sql调用改成rpc查询。

这一步花了几个月的实际,务必梳理各个服务作全面的迁移,不能遗漏,不然可能会致使拆分分析不全面,遗漏了相关字段。

查询的迁移主要因为本次拆分项目涉及到的服务太多,须要收拢到一个jar包,更方便后期的改造。若是实际分库分表项目中仅仅涉及一两个服务的,这一步是能够不作的。

2.3 联合查询的拆分分析

根据2.2收拢的jar包中的查询,结合实际状况将查询进行分类和判断,把一些历史遗留的问题,和已经废弃的字段作一些整理。

如下举一些思考点。

1)哪些查询是没法拆分的?例如分页(尽量地改造,实在改不了只能以冗余列的形式)

2)哪些查询是能够业务上join拆分的?

3)哪些表/字段是能够融合的?

4)哪些字段须要冗余?

5)哪些字段能够直接废弃了?

6)根据业务具体场景和sql总体统计,识别关键的分表键。其他查询走搜索平台。

思考后获得一个查询改造整体思路和方案。

同时在本项目中须要将两张表融合为一张表,废弃冗余字段和无效字段。

2.4 新表设计

这一步基于2.3对于查询的拆分分析,得出旧表融合、冗余、废弃字段的结果,设计新表的字段。

产出新表设计结构后,必须发给各个相关业务方进行review,并保证全部业务方都经过该表的设计。有必要的话能够进行一次线下review。

若是新表的过程当中,对部分字段进行了废弃,必须通知全部业务方进行确认

对于新表的设计,除了字段的梳理,也须要根据具体查询,从新设计、优化索引。

2.5 第一次升级

新表设计完成后,先作一次jar包内sql查询的改造,将旧的字段所有更新为新表的字段。

此处为2.0.0-SNAPSHOT版本。

而后让全部服务升级jar包版本,以此来保证这些废弃字段确实是不使用了,新的表结构字段可以彻底覆盖过去的业务场景。

特别注意的是,因为涉及服务众多,能够将服务按照 非核心 与 核心 区分,而后分批次上线,避免出现问题致使严重故障或者大范围回滚。


2.6 最佳实践

2.6.1 尽可能不改变原表的字段名称

在作新表融合的时候,一开始只是简单归并表A 和 表B的表,所以不少字段名相同的字段作了重命名。

后来字段精简过程当中,删除了不少重复字段,可是没有将重命名的字段改回来。

致使后期上线的过程当中,不可避免地须要业务方进行重构字段名。

所以,新表设计的时候,除非必不得已,不要修改原表的字段名称!

2.6.2 新表的索引须要仔细斟酌

新表的索引不能简单照搬旧表,而是须要根据查询拆分分析后,从新设计。

尤为是一些字段的融合后,可能能够归并一些索引,或者设计一些更高性能的索引。

2.6 本章小结

至此,分库分表的第一阶段告一段落。这一阶段所需时间,彻底取决于具体业务,若是是一个历史包袱沉重的业务,那可能须要花费几个月甚至半年的时间才能完成。

这一阶段的完成质量很是重要,不然可能致使项目后期须要重建表结构、从新全量数据。

这里再次说明,对于微服务划分比较合理的服务,分库分表行为通常只须要关注存储架构的变化,或者只须要在个别应用上进行业务改造便可,通常不须要着重考虑“业务重构” 这一阶段。


3.第二阶段:存储架构设计(核心)

对于任何分库分表的项目,存储架构的设计都是最核心的部分!

3.1 总体架构

根据第一阶段整理的查询梳理结果,咱们总结了这样的查询规律。

  • 80%以上的查询都是经过或者带有字段pk一、字段pk二、字段pk3这三个维度进行查询的,其中pk1和pk2因为历史缘由存在一一对应的关系

  • 20%的查询千奇百怪,包括模糊查询、其余字段查询等等

所以,咱们设计了以下的总体架构,引入了数据库中间件、数据同步工具、搜索引擎(阿里云opensearch/ES)等。

下文的论述都是围绕这个架构来展开的。


3.1.1 mysql分表存储

Mysql分表的维度是根据查询拆分分析的结果肯定的。

咱们发现pk1\pk2\pk3能够覆盖80%以上的主要查询。让这些查询根据分表键直接走mysql数据库便可。

原则上通常最多维护一个分表的全量数据,由于过多的全量数据会形成存储的浪费、数据同步的额外开销、更多的不稳定性、不易扩展等问题。

可是因为本项目pk1和pk3的查询语句都对实时性有比较高的要求,所以,维护了pk1和pk3做为分表键的两份全量数据。

而pk2和pk1因为历史缘由,存在一一对应关系,能够仅保留一份映射表便可,只存储pk1和pk2两个字段。

3.1.2 搜索平台索引存储

搜索平台索引,能够覆盖剩余20%的零散查询。

这些查询每每不是根据分表键进行的,或者是带有模糊查询的要求。

对于搜索平台来讲,通常不存储全量数据(尤为是一些大varchar字段),只存储主键和查询须要的索引字段,搜索获得结果后,根据主键去mysql存储中拿到须要的记录。

固然,从后期实践结果来看,这里仍是须要作一些权衡的:

1)有些非索引字段,若是不是很大,也能够冗余进来,相似覆盖索引,避免多一次sql查询;

2)若是表结构比较简单,字段不大,甚至能够考虑全量存储,提升查询性能,下降mysql数据库的压力。

这里特别提示,搜索引擎和数据库之间同步是必然存在延迟的。因此对于根据分表id查询的语句,尽可能保证直接查询数据库,这样不会带来一致性问题的隐患。

3.1.3 数据同步

通常新表和旧表直接能够采用 数据同步 或者 双写的方式进行处理,两种方式有各自的优缺点。


通常根据具体状况选择一种方式就行。

本次项目的具体同步关系见总体存储架构,包括了四个部分:

1)旧表到新表全量主表的同步

一开始为了减小代码入侵、方便扩展,采用了数据同步的方式。并且因为业务过多,担忧有未统计到的服务没有及时改造,因此数据同步能避免这些状况致使数据丢失。

可是在上线过程当中发现,当延迟存在时,不少新写入的记录没法读到,对具体业务场景形成了比较严重的影响。(具体缘由参考4.5.1的说明)

所以,为了知足应用对于实时性的要求,咱们在数据同步的基础上,从新在3.0.0-SNAPSHOT版本中改形成了双写的形式。

2)新表全量主表到全量副表的同步

3)新表全量主表到映射表到同步

4)新表全量主表到搜索引擎数据源的同步

2)、3)、4)都是重新表全量主表到其余数据源的数据同步,由于没有强实时性的要求,所以,为了方便扩展,所有采用了数据同步的方式,没有进行更多的多写操做。

3.2 容量评估

在申请mysql存储和搜索平台索引资源前,须要进行容量评估,包括存储容量和性能指标。

具体线上流量评估能够经过监控系统查看qps,存储容量能够简单认为是线上各个表存储容量的和。

可是在全量同步过程当中,咱们发现须要的实际容量的需求会大于预估,具体能够看3.4.6的说明。

具体性能压测过程就再也不赘述。

3.3 数据校验

从上文能够看到,在本次项目中,存在大量的业务改造,属于异构迁移。

从过去的一些分库分表项目来讲,大可能是同构/对等拆分,所以不会存在不少复杂逻辑,因此对于数据迁移的校验每每比较忽视。

在彻底对等迁移的状况下,通常确实比较少出现问题。

可是,相似这样有比较多改造的异构迁移,校验绝对是重中之重!!

所以,必须对数据同步的结果作校验,保证业务逻辑改造正确数据同步一致性正确。这一点很是很是重要。

在本次项目中,存在大量业务逻辑优化以及字段变更,因此咱们单独作了一个校验服务,对数据的全量、增量进行校验。

过程当中提早发现了许多数据同步、业务逻辑的不一致问题,给咱们本次项目平稳上线提供了最重要的前提保障!!

3.4 最佳实践

3.4.1 分库分表引发的流量放大问题

在作容量评估的时候,须要关注一个重要问题。就是分表带来的查询流量放大。

这个流量放大有两方面的缘由:

  • 索引表的二次查询。好比根据pk2查询的,须要先经过pk2查询pk1,而后根据pk1查询返回结果。

  • in的分批查询。若是一个select...in...的查询,数据库中间件会根据分表键,将查询拆分落到对应的物理分表上,至关于本来的一次查询,放大为屡次查询。(固然,数据库会将落在同一个分表的id做为一次批量查询,而这是不稳定的合并)

所以,咱们须要注意:

  • 业务层面尽可能限制in查询数量,避免流量过于放大;

  • 容量评估时,须要考虑这部分放大因素,作适当冗余,另外,后续会提到业务改造上线分批进行,保证能够及时扩容;

  • 分6四、128仍是256张表有个合理预估,拆得越多,理论上会放大越多,所以不要无谓地分过多的表,根据业务规模作适当估计;

  • 对于映射表的查询,因为存在明显的冷热数据,因此咱们又在中间加了一层缓存,减小数据库的压力

3.4.2 分表键的变动方案

本项目中,存在一种业务状况会变动字段pk3,可是pk3做为分表键,在数据库中间件中是不能修改的,所以,只能在中台中修改对pk3的更新逻辑,采用先删除、后添加的方式。

这里须要注意,删除和添加操做的事务原子性。固然,简单处理也能够经过日志的方式,进行告警和校准。

3.4.3 数据同步一致性问题

咱们都知道,数据同步中一个关键点就是(消息)数据的顺序性,若是不能保证接受的数据和产生的数据的顺序严格一致,就有可能由于(消息)数据乱序带来数据覆盖,最终带来不一致问题。

咱们自研的数据同步工具底层使用的消息队列是kakfa,,kafka对于消息的存储,只能作到局部有序性(具体来讲是每个partition的有序)。咱们能够把同一主键的消息路由至同一分区,这样一致性通常能够保证。可是,若是存在一对多的关系,就没法保证每一行变动有序,见以下例子。


那么须要经过反查数据源获取最新数据保证一致性。

可是,反查也不是“银弹“,须要考虑两个问题。

1)若是消息变动来源于读写实例,而反查 数据库是查只读实例,那就会存在读写实例延迟致使的数据不一致问题。所以,须要保证 消息变动来源 和 反查数据库 的实例是同一个。

2)反查对数据库会带来额外性能开销,须要仔细评估全量时候的影响。

3.4.4 数据实时性问题

延迟主要须要注意几方面的问题,并根据业务实际状况作评估和衡量。

1)数据同步平台的秒级延迟

2)若是消息订阅和反查数据库都是落在只读实例上,那么除了上述数据同步平台的秒级延迟,还会有数据库主从同步的延迟

3)宽表到搜索平台的秒级延迟

只有可以知足业务场景的方案,才是合适的方案。

3.4.5 分表后存储容量优化

因为数据同步过程当中,对于单表而言,不是严格按照递增插入的,所以会产生不少”存储空洞“,使得同步完后的存储总量远大于预估的容量。

所以,在新库申请的时候,存储容量多申请50%。

具体缘由能够参考个人这篇文章 为何MySQL分库分表后总存储大小变大了?

3.5 本章小结

至此,分库分表的第二阶段告一段落。

这一阶段踩了很是多的坑。

一方面是设计高可用、易扩展的存储架构。在项目进展过程当中,也作了屡次的修改与讨论,包括mysql数据冗余数量、搜索平台的索引设计、流量放大、分表键修改等问题。

另外一方面是“数据同步”自己是一个很是复杂的操做,正如本章最佳实践中说起的实时性、一致性、一对多等问题,须要引发高度重视。

所以,更加依赖于数据校验对最终业务逻辑正确、数据同步正确的检验!

在完成这一阶段后,能够正式进入业务切换的阶段。须要注意的是,数据校验仍然会在下一阶段发挥关键性做用。

4.第三阶段:改造和上线(慎重)

前两个阶段完成后,开始业务切换流程,主要步骤以下:

1)中台服务采用单读 双写 的模式

2)旧表往新表开着数据同步

3) 全部服务升级依赖的projectDB版本,上线RPC,若是出现问题,降版本便可回滚(上线成功后,单读新库,双写新旧库)

4)检查监控确保没有 中台服务 之外的其余服务访问旧库旧表

5)中止数据同步

6)删除旧表

4.1 查询改造

如何验证咱们前两个阶段设计是否合理?可否彻底覆盖查询的修改 是一个前提条件。

当新表设计完毕后,就能够以新表为标准,修改老的查询。

以本项目为例,须要将旧的sql在 新的中台服务中 进行改造。

1)读查询的改造

可能查询会涉及如下几个方面:

a)根据查询条件,须要将pk1和pk2的inner join改成对应分表键的新表表名

b)部分sql的废弃字段处理

c)非分表键查询改成走搜索平台的查询,注意保证语义一致

d)注意写单测避免低级错误,主要是DAO层面。

只有新表结构和存储架构能彻底适应查询改造,才能认为前面的设计暂时没有问题。

固然,这里还有个前提条件,就是相关查询已经所有收拢,没有遗漏。

2) 写查询的改造

除了相关字段的更改之外,更重要的是,须要改造为旧表、新表的双写模式。

这里可能涉及到具体业务写入逻辑,本项目尤其复杂,须要改造过程当中与业务方充分沟通,保证写入逻辑正确。

能够在双写上各加一个配置开关,方便切换。若是双写中发现新库写入有问题,能够快速关闭。

同时,双写过程当中不关闭 旧库到新库 的数据同步。

为何呢?主要仍是因为咱们项目的特殊性。因为咱们涉及到几十个服务,为了下降风险,必须分批上线。所以,存在比较麻烦的中间态,一部分服务是老逻辑,一部分服务是新逻辑,必须保证中间态的数据正确性,具体见4.5.1的分析。

4.2 服务化改造

为何须要新建一个 服务来 承载改造后的查询呢?

一方面是为了改造可以方便的升级与回滚切换,另外一方面是为了将查询收拢,做为一个中台化的服务来提供相应的查询能力。

将改造后的新的查询放在服务中,而后jar包中的本来查询,所有替换成这个服务的client调用。

同时,升级jar包版本到3.0.0-SNAPSHOT。

4.3 服务分批上线

为了下降风险,须要安排从非核心服务到核心服务的分批上线。

注意,分批上线过程当中,因为写服务每每是核心服务,因此安排在后面。可能出现非核心的读服务上线了,这时候会有读新表、写旧表的中间状态。

1) 全部相关服务使用 重构分支 升级projectdb版本到3.0.0-SNAPSHOT并部署内网环境;

2) 业务服务依赖于 中台服务,须要订阅服务

3) 开重构分支(不要与正常迭代分支合并),部署内网,内网预计测试两周以上

使用一个新的 重构分支 是为了在内网测试两周的时候,不影响业务正常迭代。每周更新的业务分支能够merge到重构分支上部署内网,而后外网使用业务分支merge到master上部署。

固然,若是从线上线下代码分支一致的角度,也能够重构分支和业务分支一块儿测试上线,对开发和测试的压力会较大。

4)分批上线过程当中,若是碰到依赖冲突的问题,须要及时解决并及时更新到该文档中

5)服务上线前,必需要求业务开发或者测试,明确评估具体api和风险点,作好回归。

这里再次提醒,上线完成后,请不要漏掉离线的数据分析业务!请不要漏掉离线的数据分析业务!请不要漏掉离线的数据分析业务!


4.4 旧表下线流程

1)检查监控确保没有中台服务之外的其余服务访问旧库旧表

2)检查数据库上的sql审计,确保没有其余服务仍然读取旧表数据

3)中止数据同步

4)删除旧表

4.5 最佳实践

4.5.1 写完当即读可能读不到

在分批上线过程当中,遇到了写完当即读可能读不到的状况。因为业务众多,咱们采用了分批上线的方式下降风险,存在一部分应用已经升级,一部分应用还没有升级的状况。未升级的服务仍然往旧表写数据,而升级后的应用会重新表读数据,当延迟存在时,不少新写入的记录没法读到,对具体业务场景形成了比较严重的影响。

延迟的缘由主要有两个

1)写服务尚未升级,尚未开始双写,仍是写旧表,这时候会有读新表、写旧表的中间状态,新旧表存在同步延迟。

2)为了不主库压力,新表数据是从旧表获取变动、而后反查旧表只读实例的数据进行同步的,主从库自己存在必定延迟。

解决方案通常有两种

1)数据同步改成双写逻辑。

2)在读接口作补偿,若是新表查不到,到旧表再查一次。


4.5.2 数据库中间件惟一ID替换自增主键(划重点,敲黑板)

因为分表后,继续使用单表的自增主键,会致使全局主键冲突。所以,须要使用分布式惟一ID来代替自增主键。各类算法网上比较多,本项目采用的是数据库自增sequence生成方式。

数据库自增sequence的分布式ID生成器,是一个依赖Mysql的存在, 它的基本原理是在Mysql中存入一个数值, 每有一台机器去获取ID的时候,都会在当前ID上累加必定的数量好比说2000, 而后把当前的值加上2000返回给服务器。这样每一台机器均可以继续重复此操做得到惟一id区间。

可是仅仅有全局惟一ID就大功告成了吗?显然不是,由于这里还会存在新旧表的id冲突问题。

由于服务比较多,为了下降风险须要分批上线。所以,存在一部分服务仍是单写旧表的逻辑,一部分服务是双写的逻辑。

这样的状态中,旧表的id策略使用的是auto_increment。若是只有单向数据来往的话(旧表到新表),只须要给旧表的id预留一个区间段,sequence从一个较大的起始值开始就能避免冲突。

但该项目中,还有新表数据和旧表数据的双写,若是采用上述方案,较大的id写入到旧表,旧表的auto_increment将会被重置到该值,这样单旧表的服务产生的递增id的记录必然会出现冲突。

因此这里交换了双方的区间段,旧库从较大的auto_increment起始值开始,新表选择的id(也就是sequence的范围)从大于旧表的最大记录的id开始递增,小于旧表auto_increment即将设置的起始值,很好的避免了id冲突问题。

1)切换前:

sequence的起始id设置为当前旧表的自增id大小,而后旧表的自增id须要改大,预留一段区间,给旧表的自增id继续使用,防止未升级业务写入旧表的数据同步到新库后产生id冲突;

2)切换后

无需任何改造,断开数据同步便可

3)优势

只用一份代码;

切换可使用开关进行,不用升级改造;

若是万一中途旧表的autoincrement被异常数据变大了,也不会形成什么问题。

4)缺点

若是旧表写失败了,新表写成功了,须要日志辅助处理

4.6 本章小结

完成旧表下线后,整个分库分表的改造就完成了。

在这个过程当中,须要始终保持对线上业务的敬畏,仔细思考每一个可能发生的问题,想好快速回滚方案(在三个阶段提到了projectdb的jar包版本迭代,从1.0.0-SNAPSHOT到3.0.0-SNAPSHOT,包含了每一个阶段不一样的变动,在不一样阶段的分批上线的过程当中,经过jar包版本的方式进行回滚,发挥了巨大做用),避免形成重大故障。

5.稳定性保障

这一章主要再次强调稳定性的保障手段。做为本次项目的重要目标之一,稳定性其实贯穿在整个项目周期内,基本上在上文各个环节都已经都有提到,每个环节都要引发足够的重视,仔细设计和评估方案,作到心中有数,而不是靠天吃饭:

1)新表设计必须跟业务方充分沟通、保证review。

2)对于“数据同步”,必须有数据校验保障数据正确性,可能致使数据不正确的缘由上文已经提到来不少,包括实时性、一致性的问题。保证数据正确是上线的大前提

3)每一阶段的变更,都必须作好快速回滚都预案。

4)上线过程,都以分批上线的形式,从非核心业务开始作试点,避免故障扩大。

5)监控告警要配置全面,出现问题及时收到告警,快速响应。不要忽略,很重要,有几回出现过数据的问题,都是经过告警及时发现和解决的。6)单测,业务功能测试等要充分

6.项目管理之跨团队协做

关于“跨团队协做”,本文专门拎出来做为一章。

由于在这样一个跨团队的大型项目改造过程当中,科学的团队协做是保障总体项目按时、高质量完成的不可缺乏的因素。

下面,分享几点心得与体会。

6.1 一切文档先行

团队协做最忌“空口无凭”。

不管是团队分工、进度安排或是任何须要多人协做的事情,都须要有一个文档记录,用于追踪进度,把控流程。

6.2 业务沟通与确认

全部的表结构改造,必须跟相关业务方沟通,对于可能存在的历史逻辑,进行全面梳理;

全部讨论肯定后的字段改造,必须由每一个服务的Owner进行确认。

6.3 责任到位

对于多团队多人次的合做项目,每一个团队都应该明确一个对接人,由项目总负责人与团队惟一对接人沟通,明确团队完整进度和完成质量。

7.展望

其实,从全文的篇幅就可以看出,本次的分库分表项目因为复杂的业务逻辑改造,费大量的时间和精力,而且很是容易在改造过程当中,引发不稳定的线上问题。

本文复盘了整个分库分表从拆分、设计、上线的总体过程,但愿能对你们有所帮助。


看到这里,咱们会想问一句。因此,有没有更好的方式呢?

也许,将来仍是须要去结合业界新的数据库中间件技术,可以快速实现分库分表。

也许,将来还能够引入新的数据存储技术与方案(polardb、tidb、hbase),根本再也不须要分库分表呢?

继续跟进新技术的发展,我相信会找到答案。



原创:阿丸笔记(微信公众号:aone_note),欢迎 分享,转载请保留出处。

扫描下方二维码能够关注我哦~

                                                                              以为不错,就点个  再看 吧👇



本文分享自微信公众号 - 阿丸笔记(aone_note)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索