文档章节

悠然乱弹:一段SQL引发的性能危机及其背后隐藏的设计缺陷

悠悠然然
 悠悠然然
发布于 2015/08/12 14:43
字数 1808
阅读 2357
收藏 33

有个同学,说是系统中出现性能问题了,说是让我帮助诊断一下。本来是不想花这时间的,结果耐不住对方的死缠乱打,只要答应帮看看。

故事发生的背景是,在文件上传的时候,有时间会有人上传了文件,但是最后没有使用上传的文件,这样就会产生一些垃圾文件。

原来软件作者就想写一个后台定时任务程序,来清除这些垃圾文件?

由于作者坚定的不让我发她的SQL语句(这个我也理解,这么丑陋的SQL),所以这里就不发源代码了,发伪代码。

void deleteMissLinkFile{
  List fileList=getFileList();
  List deleteFileList=new ArrayList();
  for(file:fileList){
      int count1=execute(select count(*) from ...);
      int count2=execute(select count(*) from ...);
      int count3=execute(select count(*) from ...);
      int count4=execute(select count(*) from ...);
      int count5=execute(select count(*) from ...);
      if(count1==0&&count2==0&&count3==0&&count4==0&&count5==0){
          deleteFileList.add(file);
      }
  }
  delete(deleteFileList);
}
当然,这里我已经给进行了一定的加工,使得看起一漂亮了许多,实际上,嗯嗯,实在是丑。

这个时候的性能情况是怎么样的呢?说是表里的数据只有500多条,但是执行时间要100多秒,但是实际上实际的应用场景都远不止这个数量级,而且随着数据的增加,性能会呈指数级下降。

我说你去加10万条记录测试一下,保证你一晚上算不出来。

好吧,废话少说,接下来看看怎么优化这段程序。

在开始之前,我们可以假设有N个文件,有M个文件引用表,而且假设所有的文件引用表中的记录条数都一样。

很显然,原来的实现方法中执行了:1次文件数查询+N*M次统计操作

最笨的优化方法

先用成本最低的方式来优化一把:

void deleteMissLinkFile{
  List fileList=getFileList();
  List deleteFileList=new ArrayList();
  for(file:fileList){
      int count1=execute(select count(*) from ...);
      if(count1>0)continue;
      int count2=execute(select count(*) from ...);
      if(count2>0)continue;
      int count3=execute(select count(*) from ...);
      if(count3>0)continue;
      int count4=execute(select count(*) from ...);
      if(count4>0)continue;
      int count5=execute(select count(*) from ...);
      if(count1>0)continue;
      deleteFileList.add(file);
  }
  delete(deleteFileList);
}
嗯嗯,通过上面的重构,性能马上就可以提升一倍。难看是难看了一点,但是1倍也是不小的提升哦。

原因,原来是要把所有的统计值都算出来,再进行判断,通过上面的重构,平均只要查一半就可以退出了,所以性能会有1倍的提升。

1次文件数查询+N*M/2次统计操作

一般的优化方法

偶当时提醒她说,你可以把内外换换,性能就会提升许多,结果死活听不懂,

实际上逻辑是这样的,由于统计操作的执行效率是非常低的,而带主键的查询速度是非常快的,也就是把逻辑从:遍历所有的文件看看引用次数是多少,改变成从所有文件列表中删除所有已经引用的文件,其余就是要删除的垃圾文件。

void deleteMissLinkFile{
  List fileList=getFileList();
  List refList1=execute(select file from tb1…)
  for(ref:refList1){
	  fileList.remove(ref)
  }
  List refList2=execute(select file from tb2…)
  for(ref:refList2){
	  fileList.remove(ref)
  }
  ……
  delete(deleteFileList);
}
通过上面的优化,需要执行的SQL语句是:

1+m 条SQL语句,其它都是大量的内存数据比对,相对来说,性能会高太多,通过一定的技巧进行一些优化,会有更大的提升。

这种方式,我毛估估比原始的方式,可以提高两个数量级以上。

为什么提高了两个左右数量级还是说比较笨的方法呢?

因为这种方法虽然比原始的方法有了显著的提升,但是还是存在严重的设计问题的。

首先,当数据量比较小的时候(这里的小是指与互联网应用中的数据相比),做完全遍历是没有问题的,但是当数据量比较大的时候,用一条SQL来遍历所有的数据,就是有非常大的问题的。这个时候就要引入一系列的复杂问题来解决,比如:把单机计算变成集群计算,把整个计算变成分段时间,不管怎么样,都是非常复杂的处理过程。

无为而治的方法

下面就要推出最快的、最省事的、效率最高的方法。

其实一般来说,只要是算法都是有优化空间和余地的,因此一般来说本人很少把话说满的。这次本人使用了“最”字,那就是用来表明未来已经没有优化的空间了,那什么样的算法才能没有优化的空间呢?答案就是:啥也不做。

当然了,实际上也不可能啥也不做,问题就在哪里,你不做怎么可能好呢?

实际上就是把任务进行一定的分解。通过把架构进行合理的分析与设计,把所有的文件上传、删除都做成公共的方法(或服务),在需要与文件打交道的地方,凡是与文件打交道的时候,做如下处理:

  1. 文件上传:在文件上传数据中加一条数据,比如:文件相关信息,唯一标识,引用次数为0
  2. 文件关联:当数据与文件关联的时候,修改引用次数为+1
  3. 文件取消关联:当数据与文件取消关联的时候(一般来说是删除或编辑的时候置为空或者换成另外一个的时候),修改引用次数为-1

自次,当要清理垃圾的时候,就非常简单的了,只要:

  select ... from ... where ref_times=0

然后进行相应的清理工作就好。

这个时候就优化了处理模式,并且把文件引用数据的维护分解到业务工作的过程当中,可以极大幅度的提升清理垃圾的处理效率。当然有的人说了:如果这么做,会使得我的业务处理过程变慢,那怎么办?其实也没有关系了,你可以把这个变成异步消息的方式,通知文件引用处理去做这件事情就行了,这样就不会影响到你的业务处理效率了。

总结

通过上面的分析,我们对文件上传过程中的垃圾清理过程进行优化,并分析了原来的问题之所在,及后面3种优化方式及其优缺点对比。

当然,实际上许多朋友也会有更好的办法来解决,欢迎大家参与讨论,并批评指正。

如果,你喜欢我的博文,请关注我,以便收到我的最新动态。

如果对我的开源框架感兴趣,可以从这里获取到最新的代码,也可以访问Tiny官网获取更多的消息,或到Tiny社区进行即时交流。

© 著作权归作者所有

悠悠然然

悠悠然然

粉丝 2475
博文 185
码字总数 363071
作品 14
杭州
架构师
私信 提问
加载中

评论(18)

悠悠然然
悠悠然然 博主

引用来自“让您贱笑了”的评论

项目初期有时为了验收等原因,更多的是注重功能的实现,随着数据的增长、业务的变更原来的代码看起来就成了一坨一坨的,后面的人维护时想要优化或者重构来填好这些坑就难了,基本达不到理想水准

所以重构是从一开始要持续做到项目生命周期结束的
让您贱笑了
让您贱笑了
项目初期有时为了验收等原因,更多的是注重功能的实现,随着数据的增长、业务的变更原来的代码看起来就成了一坨一坨的,后面的人维护时想要优化或者重构来填好这些坑就难了,基本达不到理想水准
悠悠然然
悠悠然然 博主

引用来自“胖太久想瘦了”的评论

看了以后才知道自己写的代码是有多挫
亲,意识到问题距离解决问题只有一步之遥。
胖太久想瘦了
看了以后才知道自己写的代码是有多挫
ZmmFly
ZmmFly
她……
悠悠然然
悠悠然然 博主

引用来自“会爬树的蜗牛”的评论

最后的解决方案,业务逻辑改动较大,大部分经理都不会同意!
如果原来实现得高内聚,还是非常简单的。
会爬树的蜗牛
会爬树的蜗牛
最后的解决方案,业务逻辑改动较大,大部分经理都不会同意!
悠悠然然
悠悠然然 博主

引用来自“北风刮的不认真了”的评论

根据输入法的习惯,一般“他 “是排在第一位的

好吧,我故意留了一个口子,被大家盯得真紧,我坦诚,确认是女的
北风刮的不认真了
北风刮的不认真了
根据输入法的习惯,一般“他 “是排在第一位的
悠悠然然
悠悠然然 博主

引用来自“路小磊”的评论

她,怪不得悠然这么有耐心~~

哈哈,话说你最后的解决方案很像垃圾回收里的计数回收~~

你们这些人啊,一个她字就有这么多想象......
大道相通
悠然乱弹:聊聊模块化

序言 熟悉了TINY相关开源内容的同学都有一个印象,那就是Tiny框架的目录分得非常细,比如Tiny工程的目录结构是下面的样子的: 比如TinyUiEnterprise项目的目录结构是这样的: 再比如,我们开...

悠悠然然
2016/01/08
3.2K
23
OSChina 开源周刊 40 期 —— 2015 年五大移动端设计趋势

每周技术抢先看,总有你想要的! 移动开发 【博客】Android 4.4 的 init 进程详解 【博客】Android4.4的zygote进程 前端开发 【翻译】在 Microsoft Edge 提供快速的 JavaScript 性能 服务端开...

OSC编辑部
2015/06/28
4.9K
2
阿里开源实时计算平台Blink,能让计算延迟降至毫秒级 | 附技术详解

雷刚 发自 凹非寺 量子位 报道 | 公众号 QbitAI 阿里巴巴这份开源礼物,业内期待已久。 近期,中国科技互联网巨头正式宣布将实时计算平台Blink开源,该技术由开源的Flink改造而来,被广泛应用...

量子位
01/29
0
0
Code Igniter 2.1的PDO驱动中的一个蛋疼BUG

一天工友碰到问题,插入新数据的时候总是会写入两条。他一直怀疑是自己代码有BUG、又或是数据库设置错误,百思不得其解,然后就和我说了这个奇怪现象,我俩就一起走近科学。 一段简单的SQL语...

AaronJan
2014/03/09
131
1
TinyFramework/urlshorter

#urlshorter 首先我要说,开源托管,必须得 @红薯 家的。 上一次本人写过一篇博客《长URL转短连接的简单设计与实现》,由于写得比较仓促,是缺少设计的,因此方案也是不完整的,看到大家非常...

TinyFramework
2018/11/22
0
0

没有更多内容

加载失败,请刷新页面

加载更多

02.日志系统:一条SQL更新语句是如何执行的?

我们还是从一个表的一条更新语句说起,我们创建下面一张表: create table T(ID int primary key, c int); 如果要将ID=2这一行c的值加1,SQL可以这么写: update T set c=c+1 where ID=2; 前...

scgaopan
今天
9
0
【五分钟系列】掌握vscode调试技巧

调试前端js 准备一个前端项目 index.html <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1......

aoping
今天
8
0
PhotoShop 高级应用:USM锐化/S锐化/防抖

、 高反差锐化+混合模式:叠加模式 【将更多的边缘细节添加到图像中】

东方墨天
今天
9
0
Python数据可视化之matplotlib

常用模块导入 import numpy as npimport matplotlibimport matplotlib.mlab as mlabimport matplotlib.pyplot as pltimport matplotlib.font_manager as fmfrom mpl_toolkits.mplot3d i......

松鼠大帝
昨天
7
0
我用Bash编写了一个扫雷游戏

我在编程教学方面不是专家,但当我想更好掌握某一样东西时,会试着找出让自己乐在其中的方法。比方说,当我想在 shell 编程方面更进一步时,我决定用 Bash 编写一个扫雷游戏来加以练习。 我在...

老孟的Linux私房菜
昨天
15
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部