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

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

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

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

由于作者坚定的不让我发她的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){
	  fileList2.remove(ref)
  }
  List refList2=execute(select file from tb2…)
  for(ref:refList2){
	  fileList2.remove(ref)
  }
  ……
  delete(deleteFileList);
}

通过上面的优化,需要执行的SQL语句是:

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

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

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

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

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

无为而治的方法

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

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

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

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

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

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

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

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

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

总结

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

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

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

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

时间: 2024-08-28 23:17:24

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

《开源框架那些事儿27》悠然乱弹:一段SQL引发的性能危机及其背后隐藏的设计缺

有个同学,说是系统中出现性能问题了,说是让我帮助诊断一下.本来是不想花这时间的,结果耐不住对方的死缠乱打,只要答应帮看看.  故事发生的背景是,在文件上传的时候,有时间会有人上传了文件,但是最后没有使用上传的文件,这样就会产生一些垃圾文件. 原来软件作者就想写一个后台定时任务程序,来清除这些垃圾文件? 由于作者坚定的不让我发她的SQL语句(这个我也理解,这么丑陋的SQL),所以这里就不发源代码了,发伪代码. 123456789101112131415 void deleteMissLinkFil

《开源框架那些事儿26》:“最好的模板引擎”Beetl剖析及与Tiny模板引擎对比

查找最好的模板引擎,发现这个搜索词出来的是beetl,于是就仔细学习了Beetl,试图找寻“最好的”三个字表现在哪里?于是搭建环境,阅读代码,与鄙人所做的TinyTemplate进行了粗略的对比,在征得beetl作者@闲.大赋 的同意后,编写了此对比文章.由于时间关系,对Beetl的认知深度还有不足,分析不当之处在所难免,还请广大同学纠正,定当有错误和不当必改. 点滴悟透设计思想,加入框架设计兴趣小组:http://bbs.tinygroup.org/group-113-1.html Beetl

《开源框架那些事儿12》:框架2.0的设计梳理

前面从各个角度,讲了大概11篇了.言归正传,这里讲解一个完整的框架设计实例.这里不是一个空白的描述,而是基于V1.0之后的建构.因此,整个设计过程,也会尽量少走一些弯路.一起来看看吧! 方法论 方法论决定了可以达到的高度 方法论,就是人们认识世界.改造世界的根本方法. 它是人们用什么样的方式.方法来观察事物和处理问题.概括地说,世界观主要解决世界"是什么"的问题,方法论主要解决"怎么办"的问题. 方法论是一种以解决问题为目标的体系或系统,通常涉及对问题阶段.任务.工

《开源框架那些事儿21》:巧借力与借巧力

同样做前端UI,为什么有人花了一点力气,就可以做好?而有的人费尽全力,仍然错误百出?我们可以先看看几个故事. 故事1:巧借力,乌鸦也可以吃核桃 有一个盛产核桃的村子,每年秋末冬初,成群的乌鸦总会来到这里,到果园里捡拾那些被果农们遗落的核桃. 核桃仁虽然美味,但是外壳那么坚硬,乌鸦怎么才能吃到呢?原来乌鸦先把核桃叼起,然后飞到高高的树枝上,再将核桃摔下去,核桃落到坚硬的地面上,被撞破了,于是,乌鸦就得到了那美味的核桃仁. 可是,让核桃从高空坠落,核桃壳破裂的概率很低,很多时候,乌鸦都是望而兴叹.然

《开源框架那些事儿22》:UI框架设计实战

UI是User Interface的缩写,通常被认为是MVC中View的部分,作用是提供跟人机交互的可视化操作界面.MVC中Model提供内容给UI进行渲染,用户通过UI框架产生响应,一般而言会由控制层调用业务逻辑进行处理,并把处理结果以Model方式返回View,再次渲染.UI框架的大致过程就是如此,按实现方式可以分为RIA和瘦客户端方式,目前基于B/S的瘦客户端方式比较流行.UI框架套路上很简单,但是想要做好可就不容易了.目前基于MVC的框架灿若繁星,不客气的说是个软件公司就有自己的技术框架

《开源框架那点事儿11》:软件开发杂谈

杂谈之中的一个:技术仅仅是成功的一点点基础条件.真正还是得靠做人 话说,有位lianzi同学.水平不错.思想超前,签约阿里如今在百度实习,曾经由于喷我的贴又没有啥理由,因此告诉他离我远一点.可是近期他又回到我群里了.一直伸个大拇指,我说啥他都是大拇指,认为怪怪的.总不是那么个感觉,最终憋了一段时间,又恢复了正常的沟通方式,聊天实录: [传说]杭州-悠然 18:31:13 lianzi本色最终出来了. [传说]杭州-悠然 18:31:30 我学得这样才是你自己.你天天伸个大拇指.我都认为不像你了.

《开源框架那点事儿18》:为什么要先从测试用例编写和文档编写开始?

有一个同学,问我一个问题:加入Tiny是否必须从写单元测试用例和文档作起? 此问题引发我诸多感触,故形成乱弹一篇. 作为一个新加入者,多看.少说,是正点.而这个时候,写写测试用例.文档,就是个不错的选择.这样入手比较容易,也比较容易体现水平. 可以说好的程序员,测试和文档都是写得好的.测试和文档一定写不好的,一定不是好的程序员. 同时,在看代码,写测试用例.写文档的过程中,还可以这样思考: 他为什么要这么设计?换成我,我会怎么设计?然后有相当一部分,会转化成:哦,原来是这个样子的!这个时候你进步

《开源框架那点事儿18》:为什么从开始的第一个测试写入和文档?

有一个同学.问我一个问题:增加Tiny是否必须从写单元測试用例和文档作起? 此问题引发我诸多感触,故形成乱弹一篇. 作为一个新增加者.多看.少说,是正点. 而这个时候.写写測试用例.文档,就是个不错的选择. 这样入手比較easy,也比較easy体现水平. 能够说好的程序猿.測试和文档都是写得好的. 測试和文档一定写不好的,一定不是好的程序猿. 同一时候,在看代码.写測试用例.写文档的过程中.还能够这样思考: 他为什么要这么设计?换成我,我会怎么设计?然后有相当一部分.会转化成:哦,原来是这个样子

《开源框架那点事儿25》:对框架模板引擎实现方式的改造实录

点滴悟透设计思想,Tiny模板引擎优化实录! 增加框架设计兴趣小组:http://bbs.tinygroup.org/group-113-1.html Tiny模板引擎的实现方式原来是採用的编译方式,近期发生了一些问题.因此我认为有必要把编译方式调整为解释方式,为此就開始了此次实现活动. 编译方式存在的问题 当时採用编译方式.主要是考虑到编译方式在执行时不必再去遍历语法树.因此就採用了编译方式. 可是在实际应用其中,出现了例如以下问题: 文件路径冲突的问题 因为採用的是编译方式,这个时候就存在在