案例分析:分库分表后,我的应用崩溃了

今天我们主要分析一个案例,那就是分库分表后,我的应用崩溃了。

前面介绍了一种由于数据库查询语句拼接问题,而引起的一类内存溢出。下面将详细介绍一下这个过程。

假设我们有一个用户表,想要通过用户名来查询某个用户,一句简单的 SQL 语句即可:

select * from user where fullname = "xxx" and other="other";

为了达到动态拼接的效果,这句 SQL 语句被一位同事进行了如下修改。他的本意是,当 fullname 或者 other 传入为空的时候,动态去掉这些查询条件。这种写法,在 MyBaits 的配置文件中,也非常常见。

List<User> query(String fullname, String other) {StringBuilder sb = new StringBuilder("select * from user where 1=1 ");if (!StringUtils.isEmpty(fullname)) {sb.append(" and fullname=");sb.append(" \" + fullname + "\");}if (!StringUtils.isEmpty(other)) {sb.append(" and other=");sb.append(" \" + other + "\");}String sql = sb.toString();...}

大多数情况下,这种写法是没有问题的,因为结果集合是可以控制的。但随着系统的运行,用户表的记录越来越多,当传入的 fullname 和 other 全部为空时,悲剧的事情发生了,SQL 被拼接成了如下的语句:

select * from user where 1=1

数据库中的所有记录,都会被查询出来,载入到 JVM 的内存中。由于数据库记录实在太多,直接把内存给撑爆了。

在工作中,由于这种原因引起的内存溢出,发生的频率非常高。通常的解决方式是强行加入分页功能,或者对一些必填的参数进行校验,但不总是有效。因为上面的示例仅展示了一个非常简单的 SQL 语句,而在实际工作中,这个 SQL 语句会非常长,每个条件对结果集的影响也会非常大,在进行数据筛选的时候,一定要小心。

内存使用问题

拿一个最简单的 Spring Boot 应用来说,请求会通过 Controller 层来接收数据,然后 Service 层会进行一些逻辑的封装,数据通过 Dao 层的 ORM 比如 JPA 或者 MyBatis 等,来调用底层的 JDBC 接口进行实际的数据获取。通常情况下,JVM 对这种数据获取方式,表现都是非常温和的。我们挨个看一下每一层可能出现的一些不正常的内存使用问题(仅限 JVM 相关问题),以便对平常工作中的性能分析和性能优化有一个整体的思路。

首先,我们提到一种可能,那就是类似于 Fastjson 工具所产生的 bug,这类问题只能通过升级依赖的包来解决,属于一种极端案例。具体可参考这里

Controller 层

Controller 层用于接收前端查询参数,然后构造查询结果。现在很多项目都采用前后端分离架构,所以 Controller 层的方法,一般使用 @ResponseBody 注解,把查询的结果,解析成 JSON 数据返回。

这在数据集非常大的情况下,会占用很多内存资源。假如结果集在解析成 JSON 之前,占用的内存是 10MB,那么在解析过程中,有可能会使用 20M 或者更多的内存去做这个工作。如果结果集有非常深的嵌套层次,或者引用了另外一个占用内存很大,且对于本次请求无意义的对象(比如非常大的 byte[] 对象),那这些序列化工具会让问题变得更加严重。

因此,对于一般的服务,保持结果集的精简,是非常有必要的,这也是 DTO(Data Transfer Object)存在的必要。如果你的项目,返回的结果结构比较复杂,对结果集进行一次转换是非常有必要的。互联网环境不怕小结果集的高并发请求,却非常恐惧大结果集的耗时请求,这是其中一方面的原因。

Service 层

Service 层用于处理具体的业务,更加贴合业务的功能需求。一个 Service,可能会被多个 Controller 层所使用,也可能会使用多个 dao 结构的查询结果进行计算、拼装。

Service 的问题主要是对底层资源的不合理使用。举个例子,有一回在一次代码 review 中,发现了下面让人无语的逻辑:

//错误代码示例

int getUserSize() {List<User> users = dao.getAllUser();return null == users ? 0 : users.size();
}

这种代码,其实在一些现存的项目里大量存在,只不过由于项目规模和工期的原因,被隐藏了起来,成为内存问题的定时炸弹。

Service 层的另外一个问题就是,职责不清、代码混乱,以至于在发生故障的时候,让人无从下手。这种情况就更加常见了,比如使用了 Map 作为函数的入参,或者把多个接口的请求返回放在一个 Java 类中。

//错误代码示例

Object exec(Map<String,Object> params){String q = getString(params,"q");if(q.equals("insertToa")){String q1 = getString(params,"q1");String q2 = getString(params,"q2");//do A}else if(q.equals("getResources")){String q3 = getString(params,"q3");//do B}...return null;
}

这种代码使用了万能参数和万能返回值,exec 函数会被几十个上百个接口调用,进行逻辑的分发。这种将逻辑揉在一起的代码块,当发生问题时,即使使用了 Jstack,也无法发现具体的调用关系,在平常的开发中,应该严格禁止。

ORM 层

ORM 层可能是发生内存问题最多的地方,除了本课时开始提到的 SQL 拼接问题,大多数是由于对这些 ORM 工具使用不当而引起的。

举个例子,在 JPA 中,如果加了一对多或者多对多的映射关系,而又没有开启懒加载、级联查询的时候就容易造成深层次的检索,内存的开销就超出了我们的期望,造成过度使用。

另外,JPA 可以通过使用缓存来减少 SQL 的查询,它默认开启了一级缓存,也就是 EntityManager 层的缓存(会话或事务缓存),如果你的事务非常的大,它会缓存很多不需要的数据;JPA 还可以通过一定的配置来完成二级缓存,也就是全局缓存,造成更多的内存占用。

一般,项目中用到缓存的地方,要特别小心。除了容易造成数据不一致之外,对堆内内存的使用也要格外关注。如果使用量过多,很容易造成频繁 GC,甚至内存溢出。

JPA 比起 MyBatis 等 ORM 拥有更多的特性,看起来容易使用,但精通门槛却比较高。

这并不代表 MyBatis 就没有内存问题,在这些 ORM 框架之中,存在着非常多的类型转换、数据拷贝。

举个例子,有一个批量导入服务,在 MyBatis 执行批量插入的时候,竟然产生了内存溢出,按道理这种插入操作是不会引起额外内存占用的,最后通过源码追踪到了问题。

这是因为 MyBatis 循环处理 batch 的时候,操作对象是数组,而我们在接口定义的时候,使用的是 List;当传入一个非常大的 List 时,它需要调用 List 的 toArray 方法将列表转换成数组(浅拷贝);在最后的拼装阶段,使用了 StringBuilder 来拼接最终的 SQL,所以实际使用的内存要比 List 多很多。

事实证明,不论是插入操作还是查询动作,只要涉及的数据集非常大,就容易出现问题。由于项目中众多框架的引入,想要分析这些具体的内存占用,就变得非常困难。保持小批量操作和结果集的干净,是一个非常好的习惯。

分库分表内存溢出

分库分表组件

如果数据库的记录非常多,达到千万或者亿级别,对于一个传统的 RDBMS 来说,最通用的解决方式就是分库分表。这也是海量数据的互联网公司必须面临的一个问题。

根据切入的层次,数据库中间件一般分为编码层、框架层、驱动层、代理层、实现层 5 大类。典型的框架有驱动层的 sharding-jdbc 和代理层的 MyCat。

MyCat 是一个独立部署的 Java 服务,它模拟了一个 MySQL 进行请求的处理,对于应用来说使用是透明的。而 sharding-jdbc 实际上是一个数据库驱动,或者说是一个 DataSource,它是作为 jar 包直接嵌入在客户端应用的,所以它的行为会直接影响到主应用。

这里所要说的分库分表组件,就是 sharding-jdbc。不管是普通 Spring 环境,还是 Spring Boot 环境,经过一系列配置之后,我们都可以像下面这种方式来使用 sharding-jdbc,应用层并不知晓底层实现的细节:

@Autowired
private DataSource dataSource;

我们有一个线上订单应用,由于数据量过多的原因,进行了分库分表。但是在某些条件下,却经常发生内存溢出。

分库分表的内存溢出

一个最典型的内存溢出场景,就是在订单查询中使用了深分页,并且在查询的时候没有使用“切分键”。使用前面介绍的一些工具,比如 MAT、Jstack,最终追踪到是由于 sharding-jdbc 内部实现所引起的。

这个过程也是比较好理解的,如图所示,订单数据被存放在两个库中。如果没有提供切分键,查询语句就会被分发到所有的数据库中,这里的查询语句是 limit 10、offset 1000,最终结果只需要返回 10 条记录,但是数据库中间件要完成这种计算,则需要 (1000+10)*2=2020 条记录来完成这个计算过程。如果 offset 的值过大,使用的内存就会暴涨。虽然 sharding-jdbc 使用归并算法进行了一些优化,但在实际场景中,深分页仍然引起了内存和性能问题。

下面这一句简单的 SQL 语句,会产生严重的后果:

select * from order order by updateTime desc limit 10 offset 10000

这种在中间节点进行归并聚合的操作,在分布式框架中非常常见。比如在 ElasticSearch 中,就存在相似的数据获取逻辑,不加限制的深分页,同样会造成 ES 的内存问题。

另外一种情况,就是我们在进行一些复杂查询的时候,发现分页失效了,每次都是取出全部的数据。最后根据 Jstack,定位到具体的执行逻辑,发现分页被重写了。


private void appendLimitRowCount(final SQLBuilder sqlBuilder, final RowCountToken rowCountToken, final int count, final List<SQLToken> sqlTokens, final boolean isRewrite) {SelectStatement selectStatement = (SelectStatement) sqlStatement;Limit limit = selectStatement.getLimit();if (!isRewrite) {sqlBuilder.appendLiterals(String.valueOf(rowCountToken.getRowCount()));} else if ((!selectStatement.getGroupByItems().isEmpty() || !selectStatement.getAggregationSelectItems().isEmpty()) && !selectStatement.isSameGroupByAndOrderByItems()) {sqlBuilder.appendLiterals(String.valueOf(Integer.MAX_VALUE));} else {sqlBuilder.appendLiterals(String.valueOf(limit.isNeedRewriteRowCount() ? rowCountToken.getRowCount() + limit.getOffsetValue() : rowCountToken.getRowCount()));}int beginPosition = rowCountToken.getBeginPosition() + String.valueOf(rowCountToken.getRowCount()).length();appendRest(sqlBuilder, count, sqlTokens, beginPosition);}

如上代码,在进入一些复杂的条件判断时(参照 SQLRewriteEngine.java),分页被重置为 Integer.MAX_VALUE。

总结

今天以 Spring Boot 项目常见的分层结构,介绍了每一层可能会引起的内存问题,我们把结论归结为一点,那就是保持输入集或者结果集的简洁。一次性获取非常多的数据,会让中间过程变得非常不可控。最后,我们分析了一个驱动层的数据库中间件,以及对内存使用的一些问题。

很多时候我们把这些耗时又耗内存的操作,写了非常复杂的 SQL 语句,然后扔给最底层的数据库去解决,这种情况大多数认为换汤不换药,不过是把具体的问题冲突,转移到另一个场景而已。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/538138.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C goto 语句

C 语言中的 goto 语句允许把控制无条件转移到同一函数内的被标记的语句。 注意&#xff1a;在任何编程语言中&#xff0c;都不建议使用 goto 语句。因为它使得程序的控制流难以跟踪&#xff0c;使程序难以理解和难以修改。任何使用 goto 语句的程序可以改写成不需要使用 goto 语…

Python数据分析毕业设计选题30个及框架大全

当涉及到Python数据分析毕业设计选题时&#xff0c;以下是30个选题建议&#xff1a; 1. 分析社交媒体数据&#xff0c;预测用户行为模式。 2. 使用机器学习算法分析电影评分数据&#xff0c;预测电影票房。 3. 分析股票数据&#xff0c;预测股票的涨跌趋势。 4. 分析用户购…

Kubernetes弃用Dockershim,转向Containerd:影响及如何应对

Kubernetes1.24版本发布时&#xff0c;正式宣布弃用Dockershim&#xff0c;转向Containerd作为默认的容器运行环境。Kubernetes以CRI(Container Runtime Interface)容器运行时接口制定接入准则&#xff0c;用户可以使用Containerd、CRI-O、CRI- Dockerd及其他容器运行时作为Kub…

打破数据孤岛,TDengine 与 Tapdata 实现兼容性互认证

当前&#xff0c;传统行业正面临着数字化升级的紧迫需求&#xff0c;但海量时序数据的处理以及数据孤岛问题却日益突出。越来越多的传统企业选择引入时序数据库&#xff08;Time Series Database&#xff0c;TSDB&#xff09;升级数据架构&#xff0c;同时&#xff0c;为了克服…

一个注解搞定 SpringBoot 接口防刷,还有谁不会?

boolean login accessLimit.needLogin(); String key request.getRequestURI(); //如果需要登录 if(login){ //获取登录的session进行判断 //… key“”“1”; //这里假设用户是1,项目中是动态获取的userId } //从redis中获取用户访问的次数 AccessKey ak AccessK…

同态滤波算法详解

同态滤波是一种用于增强图像的方法&#xff0c;特别适用于去除图像中的照明不均和阴影。该算法基于照射反射模型&#xff0c;将图像分解为两个分量&#xff1a;照射分量&#xff08;illumination component&#xff09;和反射分量&#xff08;reflection component&#xff09;…

基于单片机的指纹采集识别系统设计

目 录 摘 要 I Abstract II 引 言 3 1 硬件选择与设计 5 1.1 总体设计及方案选择 5 1.1.1主控单片机选择 5 1.1.2传感器模块选择 6 1.1.3显示器模块选择 6 1.2 系统总体设计 7 2 系统硬件电路设计 8 2.1 系统主电路设计 8 2.1.1 主体电路设计 8 2.1.2 单片机最小系统设计 8 2.…

嵌入(embedding)概念

嵌入&#xff08;embedding&#xff09;在数学和相关领域中的确是指将一个数学对象在保持其某些关键性质不变的前提下&#xff0c;注入到一个更大或更高维的空间中。这个过程不仅仅是简单的映射&#xff0c;而是要求注入的对象在新空间中的表现形式能够完整反映原有对象的内在结…

MySQL--索引类型详解

索引的类型 主键索引&#xff1a; PRIMARY KEY&#xff0c;当一张表的某个列是主键的时候&#xff0c;该列就是主键索引&#xff0c;一张表只允许有一个主键索引&#xff0c;主键所在的列不能为空。 创建主键索引的SQL语法&#xff1a; # 给user表中的id字段创建名为id_ind…

安科瑞微电网智慧能源平台【能源规划、协调控制、经济运行】

背景 1“双碳”目标 重视能源消费侧节能减碳 碳排放权、碳资产、碳交易 2市场、行业发展 光伏建设成本降低、储能设备成本大幅下降、新能源汽车快速发展&#xff0c;大量充电桩建设 3用户需求 降低用能成本、增加能源收益、避免“能耗双控”&#xff0c;提高用电可靠性、…

最适合Oracle数据库的Linux操作系统?

先声明&#xff1a;以下观点仅供参考。我只引用网上的一些观点&#xff0c;包括官网等。 Oracle数据库认证的Linux操作系统 最近老被问到Oracle Linux免费吗&#xff1f;因为用户需要安装Oracle数据库&#xff0c;面临选择操作系统的问题。 首先&#xff0c;Oracle数据库 19…

Transformer模型引领NLP革新之路

在不到4 年的时间里&#xff0c;Transformer 模型以其强大的性能和创新的思想&#xff0c;迅速在NLP 社区崭露头角&#xff0c;打破了过去30 年的记录。BERT、T5 和GPT 等模型现在已成为计算机视觉、语音识别、翻译、蛋白质测序、编码等各个领域中新应用的基础构件。因此&#…