深度认识 Sharding-JDBC:做最轻量级的数据库中间层

转自:

https://juejin.im/entry/5905ac37a22b9d0065e1199c

基于关系型数据库的水平扩展方案有很多开源的解决方案,但成熟稳定的产品凤毛麟角。当当自研的数据库中间层 Sharding-JDBC 在公司内部已广泛使用,并在开源社区推广且初见成果。目前的 Sharding-JDBC 已经历从初出茅庐到稳定运行,再到变革的关键点。

Sharding-JDBC 采用在 JDBC 协议层扩展分库分表,是一个以 jar 形式提供服务的轻量级组件,其核心思路是小而美地完成最核心的事情。

对于这么优秀的一个项目, 在高手问答第 144 期中,我们策划了 “ 轻量级数据库中间层 Sharding-JDBC 深度解析 ” 的主题,并邀请了 @当当_亮(张亮)作为高手嘉宾。

本文整理了此次高手问答中一些精彩的问答。

对于这样一个项目,想必大家都会关心它的开发初衷和适用场景等相关问题。下面先来看看张亮老师对于这些问题的解答。

Q:Sharding-JDBC 的设计初衷是什么?旨在解决什么场景的问题?

Sharding-JDBC 的设计初衷是想提供一个数据库中间层,用于透明的处理分库分表,而无需业务开发人员在业务代码中根据分片键生成 SQL。

第一版的分库分表并不是现有的 Sharding-JDBC,而是当当的一个内部框架 ddframe 的数据库模块,dd-rdb 的其中一项功能就是分库,没有分表功能,当时只是做了简单的 SQL 解析。后来随着 ddframe 被各个团队采用,只分库的需求渐渐不够用了,而 dd-rdb 里面有大量的数据库 ORM 相关的东西,为了使分库分表这一核心需求更加纯粹,我们才将其中的分片的部分单独提炼出来并命名为 Sharding-JDBC,用于在 Java 的 JDBC 层面提供一层驱动,无缝的处理这方面的需求。

Q:Sharding-JDBC 适用于哪些场景,不适用于哪些场景?是否有性能评估?

对于关系型数据库数据量很大的情况,需要进行水平拆库和拆表,这种场景很适合使用 Sharding-JDBC。

举例说明:假设有一亿数据的用户库,放在 MySQL 数据库里查询性能会比较低,而采用水平拆库,将其分为 10 个库,根据用户的 ID 模 10,这样数据就能比较平均的分在 10 个库中,每个库只有 1000w 记录,查询性能会大大提升。分片策略类型非常多,大致分为 Hash + Mod、Range、Tag 等。

Sharding-JDBC 还提供了读写分离的能力,用于减轻写库的压力。

此外,Sharding-JDBC 可以用在 JPA 场景中,如 JPA、Hibernate、Mybatis,Spring JDBC Template 等任何 Java 的 ORM 框架。

Java 的 ORM 框架也都是采用 JDBC 与数据库交互。这也是我们选择在 JDBC 层,而非选择一个 ORM 框架进行开发的原因。我们希望 Sharding-JDBC 可以尽量的兼容所有的 Java 数据库访问层,并且无缝的接入业务应用。

不合适的场景主要是两方面:

  1. 不适合 OLAP 的场景。虽然 Sharding-JDBC 也能做聚合分组查询,但大量的 OLAP 场景,仍然会比较慢,而且复杂的 SQL(如子查询等)目前还没有支持。这种查询不太适合大数据和高并发的互联网 online 数据库,建议使用合理的 OLTP 查询。
  2. 不适合事务强一致的要求。目前 Sharding-JDBC 的事务支持两种,一种是弱 XA,另一种是柔性事务(BASE)。因为 XA 的两阶段或三阶段提交其性能较低,因此互联网公司基本不会采用。而无论是弱 XA 还是柔性事务,都无法保证事务在任意时间段完全保证一致,其中柔性事务能保证数据的最终一致性,但达到最终一致性的时间仍然不可控。因此对于对跨库事务强一致要求很高的场景,需要从设计方面去考虑数据库 schema 的合理性。

对于 JTA 事务,目前 Shariding-JDBC 没有实现 JTA 的标准。而且由于在互联网场景下使用 JTA 比较少见,因此暂时不支持 JIA 事务。

在 osgit 上有性能测试文档。单库的场景下,由于需要进行 SQL 解析以及路由,器性能损失是 0.02%。双库的场景下,采用了分布式的方式存取数据,性能提升越 94%。

那么对于同类的项目,老师又是如何看待的呢?

Q:Sharding-JDBC 和其他同类产品有什么区别?能不能集成到 SparkSQL 或者 Hive?

目前和 Sharding-JDBC 这种基于 JDBC 层架构类似的,据我所知只有 TDDL,而 TDDL 并未将分库分表这块开源。基于 JDBC 层进行分片的好处是轻量、简单、兼容性好以及无需额外的运维工作。缺点是无法跨语言,目前仅支持 Java。

现在暂时未考虑集成 SparkSQL 或者 Hive。因为 Sharding-JDBC 的定位还是关系型数据库中间层,为了稳定性的考虑,不会改变数据库的存储引擎。未来我们会做基于 Proxy 版本的 Sharding-JDBC-Server,会渐渐的考虑将 Spark 等大数据的查询方式引入进来。

Q:Sharding-JDBC 与 Mycat 有一定的相似性,区别点在于对于 SQL 语句的自解析上,是否可以这么理解?

从设计理念上看确实有一定的相似性。主要流程都是 SQL 解析 -> SQL 改写 -> SQL 路由 -> SQL 执行 -> 结果归并。但架构设计上是不同的。Mycat 是基于 Proxy,它复写了 MySQL 协议,将 Mycat Server 伪装成一个 MySQL 数据库,而 Sharding-JDBC 是基于 JDBC 接口的扩展,是以 jar 包的形式提供轻量级服务的。

SQL 解析这块,现在的 Shariding-JDBC 和 Mycat 也比较相似,都是使用 Druid 作为 SQL 解析的基础类库。但 Sharding-JDBC 正在重写 SQL 解析这块,是去掉 Duird 的完全自研版本。不可否认 Druid 是一个优秀的连接池,而且 SQL 解析这块做得也很强,但它毕竟不是一个专门为了 Sharding 而做的 SQL 解析器,它的大致解析流程是 Lexer -> Parser -> AST -> Vistor,使用者需要实现它的 Vistor 接口,将自己的业务逻辑在 Vistor 中实现,因此需要通过 Vistor 再生成 SharidingContext,而抽象语法树 AST,也需要对 SQL 完全理解。Sharding-JDBC 自研的 SQL 解析器,对于 Sharding 不相关的关键词采用跳过的方法,整体解析流程简化为 Lexer -> Parser -> SharidingContext,在性能以及实现复杂度上都有所突破。

接下来老师和大家分享了一些关于 Sharding-JDBC 功能的问题

Q:Sharding-JDBC 是否支持读写分离?

Sharding-JDBC 从 1.3.0 开始支持读写分离。其功能包括:

  1. 根据配置区分写库和多个读库,目前暂时只有轮训策略选取读库,可以配合分库分表使用。
  2. 通过 Hint 强制指定某次查询走写库。
  3. 如果在同一线程且同一数据库连接中有发现 DML 语句,则该 DML 之后的查询都从写库查询,DML 之前的 DQL 语句不受影响,仍然查询读库。其目的是保持同一用户线程的数据一致性。

但限于 Sharding-JDBC 本身设计的考虑,数据库层面的主从切换以及主从数据同步,Sharding-JDBC 并不负责。Sharding-JDBC 定位仍然是轻量级的增强版数据库驱动。因此由于主库和从库同步延迟导致的数据不一致,并不是 Sharding-JDBC 的处理范畴。

另外由于 Sharding-JDBC 本身是分库分表中间件,读写分离也是后加入的功能,因此可以支持分库分表+读写分离,但是仅读写分离目前还不容易配置,未来也会将读写分离提炼出来作为独立的 API 使用。

Q:在现有的系统架构的基础上,Sharding-JDBC 能否与第三方数据库连接池(如:C3P0,Druid 等)集成,实现分库分表+读写分离?

是的,可以支持。Shariding-JDBC 本意就是只做分片 + 读写分离,连接池应该交由连接池去处理,各做各的互不影响。

Q:分库分表使用 like 查询,是否能查询出来?性能如何?会去查询所有的库和表吗?

  • 分库分表使用 like 查询是有限制的。目前 Shariding-JDBC 不支持 like 语句中包含分片键,但不包含分片键的 like 语句可以正确执行。
  • 至于 like 性能问题,是与数据库相关的,Shariding-JDBC 仅仅是解析 SQL 以及路由至正确的数据源而已。
  • 是否会查询所有的库和表是根据分片键决定的,如果 SQL 中不包括分片键,就会查询所有库和表,这个和是否有 like 没有关系。

Q:Sharding-JDBC 是否有完善的管理配置界面?

目前 Sharding-JDBC 还没来得及做配置界面。目前主要集中于以 jar 包的形式提供服务,和业务应用一起发布,旨在简化开发,对 DBA 无影响,因此 DBA 看到的还是分库后的零散的数据库。

未来会做配置中心,用于动态的修改分片数据源,也会配套的提供管理界面。未来还会将数据库的 Metadata 统一管理起来,为 DBA 提供更加友好的服务。

Q:Sharding-JDBC 如何强制查询走主库?

读写分离的文档地址:dangdangdotcom.github.io/sharding-jd…

大致使用方式如下:

HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
// 继续JDBC操作

还有一些与 Sharding-JDBC 相关的问题,张亮老师也进行了详尽的解答

Q:Sharding-JDBC 是如何解决系统鲁棒性的问题的?我们的后台对服务的可靠性要求比较高,目前还在考虑异地灾备的情况。如使用 Sharding-JDBC 的话,碎片化的库表结构是否会增加运维难度?

因为 Sharding-JDBC 是一个 jar,它与业务应用的生命周期是一致的,同生同死。因此只要解决好使用 Sharding-JDBC 的业务系统的鲁棒性就可以了。

碎片化库表的问题,即使不用 Shariding-JDBC 分库分表,也会同样存在的,Shariding-JDBC 确实没处理这块,不过也不会更加恶化。

等核心稳定后,未来会考虑为 DBA 做运维工具。

Q:请问 Sharding-JDBC 自研的 SQL 解析器开源否?性能能否有大的提升?另外,现在当当的业务中,数据库的分库分表迁移可否自动化?

Sharding-JDBC 自研的 parser 是开源的,目前在 parser 这个分支,但是还未做完,仍然在快速的迭代中。

SQL 解析原理和 Druid 基本相同,但是简化了一些流程。Duird 的初衷是对 SQL 进行监控、分析以及规范化,因此它的 SQL 解析场景需要对 SQL 进行完全的理解。采用 Druid 的作为基础解析器的 Sharding-JDBC 解析流程为:(Lexer -> Parser -> AST -> Vistor) -> SharidingContext,其中括号内是 Duird 框架的流程,无法修改。而 Sharding-JDBC 仅需理解与 Sharding 相关的关键字,无关内容则采取跳过的方式,因此将直接生成分片上下文,无需再通过抽象语法树的访问器再获取。

New Parser 的解析流程简化为:Lexer -> Parser -> SharidingContext。因此在性能以及实现复杂度上都有所突破。具体的性能测试报告目前还未做,会随着 New Parser 分支完全一起发布。

关于分库分表自动迁移的事,当当还没有做到自动化。由于数据迁移更加贴近于数据库运维工具,和 JDBC关系不大,因此 Shariding-JDBC 也暂时未将数据迁移纳入范围。

关于 Sharding-JDBC 未来的规划,张亮老师也和我们分享了很多干货

Q:Sharding-JDBC 下一步规划是什么?

Sharding-JDBC 目前正在进行 SQL Parser 部分的重写。之前的 Sharding-JDBC 使用 Duird 作为 SQL 解析的基础工具,但基于各方面的考虑,我们决定采用自研的方式解析 SQL,能进一步的提升性能和 Sharding 的准确性以及兼容性。

New SQL Parser 完成之后,我们会着重处理之前没有完成的柔性事务 TCC 部分,更多类型 SQL 的支持以及配置动态化。

这些做完之后会考虑将 Sharding-JDBC 分为 Sharding-Driver 以及 Sharding-JDBC-Server 两个版本,用于满足不同用户的需要。Sharding-JDBC-Driver 会更加轻量级,而 Sharding-JDBC-Server 会以 Proxy 的形式提供服务,能更好的处理数据迁移,事务以及 OLAP 等需求。

Shariding-JDBC 的核心应该就是 JDBC 驱动的增强,以 jar 形式提供轻量级的服务。大体上看,Sharding-JDBC 的生态应该大致分为 3 个环路:

  • 一环是 JDBC 相关的核心功能,包括分库分表、读写分离、分布式主键等。这个是小而美的核心部分。
  • 二环是和数据库相关,但不属于 JDBC 范畴的,将以插件的形式提供,包括柔性事务、数据库的 HA、数据库 Metadata 管理、配置动态化等。
  • 三环是业务或使用友好度相关的,包括多租户、账户安全、Spring 自定义命名空间、Yaml 配置等。

有很多朋友提到关于对其他语言的支持,因为 Shariding-JDBC 是基于 Java 提供的 JDBC 规范的接口所开发,因此目前暂时不支持 Python、Node.js 等。

但其核心的解析、路由、结果归并等功能模块和基于 Proxy 版本开发几乎是一致的。因此,未来我们有打算提供 Shariding-JDBC-Server 版本,将会支持全语言。

有朋友看到我们在考虑开发 Sharding-JDBC-Server,以为目前的 Sharding-JDBC 模式遇到了某些不可解决的问题。其实 Shariding-JDBC 以当前的定位来说,没有遇到不可解决的问题,但如果想做的更多(前面提到的数据迁移,分布式事务,元数据管理等),则需要向 Proxy 的方式靠拢。Shariding-JDBC 想提供两种不同的使用方式,给使用者更自由的选择。

还有就是对 SQL 语句的支持。由于时间和精力有限,目前无法做到全 SQL 的兼容。我们现阶段的目标是尽量支持 OLTP 最常用的 80% 的 SQL。目前支持聚合、分组、排序等查询。暂时不支持 distinct,对 or 的支持也不是特别完善,但是 distinct 和 group by 可以互换,or 也可以用 in 代替。因此绝大部分 SQL 经过修改是可以使用的。

有朋友提到,既然 distinct 和 group by 可以互换,or 可以用 in 代替,那么是不是可以考虑直接在 SQL 解析的时候自动切换?

对于这个问题,虽然这样做在技术上是可行的,但是设计上来说还有待商榷。

如果已经做了 distinct 和 or 的解析,其实完全没有必要改写 SQL,直接支持就可以了。而改写 SQL,对于 DBA 的调试就比较痛苦,因此我们希望做到尽量不修改 SQL,仅修改必要的部分,如:分表的名称、avg 转化为 count 和 sum、limit 的 offset 和 rowCount。

对于过于复杂的 SQL,如子查询等,不一定适合在大数据量的分片数据库中使用,也许需要重新梳理。

未来我们也会对 SQL 兼容性这块持续进行提升。

目前 Sharding-JDBC 仅支持 MySQL,对其他数据库的支持(比如 Oracle)也正在进行。现在 New SQL Parser 正在进行中。计划在这个版本发布对 Oracle、PG 以及 SQLServer 的支持。对于 Oracle 以及 SQLServer 主要的支持在于特殊关键词识别和分页。

New SQL Parser 确实工作量比较大,虽然目前整体代码已经梳理得差不多了,但想稳定的提供服务还需要一些时间。预计 4 月份提供 snapshot,6 月份提供稳定版。

New SQL Parser 在 Git 的 parser 分支持续更新中,欢迎继续关注。

还和大家分享了很多数据库相关的使用经验和心得

Q:现在用着自己写的 Sharding,不过在解析语句这块比较尴尬,有些语句解析不来,所以自己封装 jsqlparser,Druid,自写正则三个方法来取表名,老师有什么建议?

jsqlparser 用的是 JavaCC 的方式解析 SQL,相对于 Druid 来说,性能比较低。正则解析的话,性能应该会更低,而且这两种方式都比较难调优。

Druid 采用的词法和语法分析的方式解析 SQL,编码工作量大,但性能会提升很多。Druid 的 SQL 解析器对于开发者而言稍微有些不易上手。Shariding-JDBC 采用与 Druid 相同的 SQL 解析方式,但为 Sharding 做了优化。

只是获取表名的话,jsqlparser 和 Duird 都没问题,如果考虑性能问题,还是建议用 Druid 或者参考下 Sharing-JDBC 的做法。

Q:对于分布式事务这块,有什么实践经验分享吗?

分布式事务这块,我们认为 XA 多阶段提交的方式,虽然对分布式数据的完整性有比较好的保障,但会极大的降影响应用性能,并未考虑采用。我们采用的是两种方式,一种称之为弱 XA,另一种是柔性事务,即 BASE。

弱 XA 就是分库之后的数据库各自负责自己事务的提交和回滚,没有统一的调度器集中处理。这样做的好处是天然就支持,对性能也没有影响。但一旦出问题,比如两个库的数据都需要提交,一个提交成功,另一个提交时断网导致失败,则会发生数据不一致的问题,而且这种数据不一致是永久存在的。

柔性事务是对弱 XA 的有效补充。柔性事务类型很多。

Sharding-JDBC 主要实现的是最大努力送达型。即认为事务经过反复尝试一定能够成功。如果每次事务执行失败,则记录至事务库,并通过异步的手段不断的尝试,直至事务成功(可以设置尝试次数,如果尝试太多仍然失败则入库并需要人工干预)。在尝试的途中,数据会有一定时间的不一致,但最终是一致的。通过这种手段可以在性能不受影响的情况下牺牲强一致性,达到数据的最终一致性。最大努力送达型事务的缺点是假定事务一定是成功的,无法回滚,因此不够灵活。

还有一种柔性事务类型是 TCC,即 Try Confirm Cancel。可以通过事务管理器控制事务的提交或回滚,更加接近原生事务,但仍然是最终一致性。其缺点是需要业务代码自行实现 Try Confirm Cancel 的接口,对现有业务带来一定冲击。未来 Sharding-JDBC 会带来对 TCC 的支持。

还有一些其他的分布式事务,如 Google 提出的 F1 等,由于 Shariding-JDBC 仍然使用数据库的原有存储引擎,并未改变,因此暂时不考虑对此类型事务的支持。

Q:请问何时需要分表?目前我们都是按照业务分库。

分库分表分为水平拆分和垂直拆分。按照业务分库或分表属于垂直拆分。水平拆分是将同样的库或表按照一定的分片规则拆成多个。

分库和分表都可以有效的处理由于数据量大而导致的查询性能下降的问题。分库还可以缓解高并发对数据库带来的压力,但仅分表可以使用本地事务代替分布式事务。因此分库和分表的合理使用是需要根据业务场景来决定的。

Sharding-JDBC 作为开发的基础类库,支持分库和分表,将选择的余地留给业务开发的工程师。

Q:请教一下,根据主键分片,以用户名登录,如何知道用户落在哪个分片上?

如果用户名是主键,则可以直接根据您定义的分片策略计算,算出该用户最终落在哪个库的哪张表上。

如果用户名不是主键,则必须通过全路由查询,一个一个的找,直到找到为止。

张亮老师还和大家畅谈了很多其他关于数据库的问题

Q:我有一个很大的问题,就是如何判断自己需要搞魔改还是换数据库呢?MySQL 各种魔改的成本恐怕未必比买高端数据库便宜吧。特别是现在数据库很多都有云服务可以选择,买个 Oracle 或者微软的云服务数据库,入门成本现在应该可以被中小企业接受了吧。

这个问题比较大,需要从整体的角度来讨论一下。

MySQL + 开源分片中间件是公司将技术核心抓在了自己的手里,互联网公司大多愿意采用。这个不仅是眼前的经济成本问题,还包括了技术问题解决成本、对业务发生变化时对技术的控制能力等。比较成熟的公司都会沉淀出适合自己的中间件架构,以及各种监控、治理等辅助系统。

NoSQL 也是一种选择,但它们的定位是关系型数据库的有益补充,并不是要完全替换掉关系型数据库,因此,关系型数据库 + 分片仍然有其存在价值。

对于 Oracle,我的看法是重要的业务数据可以考虑,而一些周边数据,就没什么必要。当然如果公司不差钱,定位又非技术导向,而是纯业务导向的话,完全依赖 Oracle 也是可以的。只不过 Oracle 不能满足互联网的全部场景。各种云数据库和 Oracle 差不多,公司对自我的定位很重要。如果有核心业务,完全可以把技术全包出去。

个人理解,当业务量较小或适中的情况下,采用 Oracle 和云数据库是合理的选择。而在公司的业务爆炸式增长的大型互联网公司,这些方案未可行,因为独角兽级别的公司遇到业务场景基本都是独一无二的,其解决方案并不是第三方功能能够直接给予的。成长到一定程度的公司大多会选择自研 + 开源的组合,保证其技术的适应度以及和业务的匹配度。

Q:所以说选型其实是针对 OLTP 业务模型和数据量的变化作为主要考虑指标?在初期未必有足够的技术和资金的时候,选择免费的 MySQL 先用起来。然后等活下来有能力做大,数据也暴涨了,这时候就招人来魔改,或者选择已有魔改方案。传统商业数据库集中在财务等关键地方,或者 OLAP 这种 MySQL “草鸡”的领域(也可能完全不用数据库选择大数据产品)。不知道我理解得是否正确。

是的,正确。

简单说,刚起步用 MySQL;有钱并且业务量适中用 Oracle,核心业务用 Oracle,非核心业务用云数据库;资金不充足业务量较大用 MySQL + 开源中间层;业务量超大只能自研。

大数据和关系型数据库不是一个方向,主要用于存储其他类型数据了,订单、交易等数据一般不会放大数据,更适合日志,浏览记录等。

Q:另外在选型上,我还想提一下 PG 这个数据库。业内选择 MySQL 比较多,PG 比较少。是不是就是因为 PG 在大规模集群魔改方面一直没有成功案例的关系?起码自从谷歌魔改 MySQL 开始,我们听说了太多的案例。PG就好像没听说过了。

用了 MySQL,哪怕有各种糟糕,但是因为有各种成功案例和各种第三方开源魔改集群方案,起码我知道可以做得够大。但是 PG 虽然特性上远远强过 MySQL,但是因为本身不像 MySQL 那么灵活,所以采用的反而少。有钱的买商业,没钱的 MySQL+Hadoop。

Sharding-JDBC 的服务对象是任何关系型数据库,无论是 MySQL 还是 PG。

不过针对这个问题可以再大致聊一下。很多公司的技术选型在于平衡,尤其是数据库选型这块。不一定 MySQL 就是最好的数据库,某些方面 PG 确实要更好,但 MySQL 是在各个方面受认可最多的数据库。比如周边的 MHA 套件、开发和 DBA 对数据库的熟悉程度等。因此,即使是 MySQL 的分支版本 MariaDB 或 Percona,无论功能性能再怎么提升、引擎再怎么变化,其总体的认可度也还是不如 MySQL 的。

Q:有一个问题一直很疑惑,目前分库分表的中间件有两种思想,分别是:

  1. 类似 Sharding-JDBC 及 TDDL 的增强版 JDBC 驱动思想
  2. 类似 Mycat 增加中间层,然后在中间层进行分库分表思想

我想问的是,这两种思想都有什么优势和劣势呢,大公司的主流选型又是哪种?

两种方式各有优缺点。

JDBC 驱动版的优点:

  1. 轻量,范围更加容易界定,只是 JDBC 增强,不包括 HA、事务以及数据库元数据管理
  2. 开发的工作量较小,无需关注 nio,各个数据库协议等
  3. 运维无需改动,无需关注中间件本身的 HA
  4. 性能高,JDBC 直连数据库,无需二次转发
  5. 可支持各种基于 JDBC 协议的数据库,如:MySQL,Oralce,SQLServer

Proxy 版的优点:

  1. 可以负责更多的内容,将数据迁移,分布式事务等纳入 Proxy 的范畴
  2. 更有效的管理数据库的连接
  3. 整合大数据思路,将 OLTP 和 OLAP 分离处理

因此两种方式互相可以互补,建议使用 Java 的团队,且仅 OLTP 的互联网前端操作。有可能会使用多种数据库的情况,可以选择 JDBC 层的中间件;如果需要 OLAP 和 OLTP 混合,加以重量级的操作,如数据迁移,分布式事务等,可以考虑 Proxy 层的中间件。但目前开源的数据迁移和分布式事务的完善解决方案还不常见。NewSQL 这种改变数据库引擎的方式就不在这里讨论了。

    原文作者:noSql
    原文地址: https://www.cnblogs.com/yeahwell/p/7920383.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞