俗话说:天下大势,分久必合,合久必分。
最早的软件系统开发,讲究的就是一个全栈——在最早期的桌面软件时代,数据、用户界面和业务逻辑是完全混在一起的,讲究的就是一个一体化……那个年代也诞生了大量的码农界的传奇人物,比如:
后面嘛,开始讲究专业化,所以把数据和业务逻辑分开了,还搞了一个专门的名称,叫做CS架构:
好吧,上面那个不算,下面这个才是:
主要就是把数据和业务逻辑分离了,服务器专门管数据的存储(以及有限的部分对数据依赖度比较高的计算),而在客户端上复杂业务逻辑。
之后,又进化出来了BS架构与MVC模式:
包括CS架构,也在在借鉴BS架构的MVC模式下,也进化出了QML这一类的专门的做桌面APP界面的描述性脚本语言,基本上完成了UI与业务逻辑的分离。
后面更是进化出了微服务这种能分多细分多细的架构……
至于后面还要分细碎,我就不好预测,不过近些年来,有出现了一阵反潮流,什么概念呢?
叫做:
code push down:直接翻译过来就是代码下沉,下沉到哪里呢?就是下沉到数据库层面。(这个概念最早来自SPA的HANA数据库架构提出来的)
通俗来说,就是一句话:
在最贴近数据的地方做运算。
为什么说是复古风,因为最早的代码下沉,不就是存储过程么……下面我们就存储过程来聊聊今天的内容:
先说为什么在互联网时代,存储过程失宠了:
这是福报厂的Java开发手册:
很明确的规定不能使用存储过程,一般来说,理由如下:
首先是存储过程本身的缺点,主要有以下六个:
缺点
1. 兼容性和移植性差
如果你的业务逻辑复杂到需要写存储过程的地步,总会不可避免地用到数据库独有的特性和语法,不同虽然SQL有标准,但是不同数据库之间的PLSQL都是有一些不同地地方:
例如:
- Oracle的叫做PL/SQL
- SQL Server叫做T_SQL
- postgresql叫做PL/pgSQL
- DB2叫做inceptor-plsql……
(看到这里我已经有点晕了) 虽然他们都遵循SQL 99标准,但是别说不通库,就算同库不同版本之间,也有一些特性是不相同的。
所以,一旦发生了需要更换数据库时,这部分代码基本上就得需要重写。
如果只是简单地替换函数名和参数规则(如日期转换等),那成本还不高;如果用到了新数据库不支持的某种特性,那还要重新设计算法来编写计算逻辑;如果还要再兼顾性能因素,那成本就非常感人了。
反之,如果用高级语言来处理这些业务逻辑,对于数据库的依赖就降到最低了,特别是类似与ORM一类的技术,让数据库底层与业务逻辑几乎完全隔离,换了数据库之后,只需要换个连接字符串就行,其他代码几乎完全兼容。
2. 调试困难
所谓工欲善其事,必先利其器,对于码农来说,一个好的编辑调试工具是工作效率的前提。
而存储过程的开发一直缺少有效的 IDE 环境。(目前有明确具备调试能力的,只有Oracle的PLSQL DEVELOPER工具)。
构成存储过程的SQL本身经常很长,调试势必要把句子拆开分别独立执行,非常麻烦。即使是分步的运算,因为没有好的 IDE 支持,要看哪一步出错,也要把中间结果输出才行,仍然是非常麻烦。
存储过程的调试功能近年来略有起色,但离流畅实用始终差距甚远,数据库厂商似乎也无心解决。这点和Java、Python等高级语言拥有的成熟IDE(集成开发环境)的几乎完全不可同日而语。哪怕是备受诟病的C/C++在VIM下面开发,也有GDB这样的调试工具。而存储过程开发中这让码农们焦头烂额的调试自然会导致低下的开发效率。
3. 体系封闭
说到封闭性,其实是数据库本身的问题。数据库既然叫做“库”,那么外部数据只有入到库中才能计算和使用。
而现代应用数据源众多,先不说半结构化和非结构化这种东西,就算是标准的结构化数据,要临时转入到库中,其效率很低且开销也极大(因为数据库的 IO 成本高)。很可能跟不上访问需求,定时批量转入又很难获得最新的数据,同样影响计算结果的实时性。同时,很多ETL业务,往往有时间窗口(如当天夜里到第二天凌晨),赶上业务繁忙的时候还可能因为时间窗口不足无法完成 ETL工作而影响第二天的业务。
不仅如此,把外部数据存储在数据库中,又会形成众多中间表,面临中间表的各种问题。而且互联网上最通用的通常是多层的 json 或 XML 格式,在关系数据库中还要建立多个关联的表来存储,会进一步加剧中间表的问题,占用过多宝贵的数据库空间。
存储过程在数据库中运算自然继承了封闭性的特点,想混合计算外部数据很不方便。更别说如果还想在存储过程中,调用一下其他的API,来实现一些复杂的算法,更是几乎不可能完成的任务。
4. 耦合性高
高耦合是系统升级的噩梦,高耦合代表系统内各个部分高度的关联在一起,无法独立的维护和处理,这样出现任何问题都会造成牵一发动全身。
虽然从理论上来看,管用存储过程还是高级语言编写的后台逻辑功能,通常是为前台用户应用提供服务的,而用户只能理论上只能通过UI界面才能使用系统,所以这些东西本质上应该在一起,从而有机的组成完整的业务功能点。这就是早期所谓的一体化全栈模式。但哪怕这种模式下,也要尽量把各个部分做成独立的,以防止这种高耦合的产生。
但存储过程必然与数据库紧密耦合,所以实践中存储过程与前端应用是物理分离的,且无法使用统一的技术路线。对于同一个功能点的存储过程和前端应用,维护其中一处,通常就要维护另一处,但两者物理上分离,维护因此变得很困难,而不统一的技术路线则加剧了这种困难。
存储过程与数据库紧密耦合,反而与前端应用分离,这就容易使同一个存储过程被多个前端应用共享。时间一长,哪个存储过程到底被哪些应用调用就变成了谜团。如果某个应用的计算发生变化,面对谜团一般的共享调用关系,管理员只能新建存储过程而不敢修改原存储过程。这样恶性循环下去,存储过程越来越多,屎山也就越来越大,终将变得不可收拾。
5. 管理困难
在数据库中,存储过程的目录是扁平的,而不是软件工程或者文件系统那样的树形结构,要是脚本少的时候还好办,一旦多起来,整个系统就会陷入一团乱麻。
可以想象,多个项目的存储过程、同一个项目不同模块的存储过程、同一模块不同年份或不同版本的存储过程,这些如果混杂在同一个目录下,区分起来会非常困难,除非加大管理幅度,比如按项目、模块、年代、版本命名,这显然又会影响开发效率。有些项目管理较弱的团队,在开发周期紧张时,常常就顾不得这些规矩了,先赶着让项目上线再说,而上线之后这些遗留问题又容易被忘掉,结果常常一直存在很久。
最后导致的问题就是数据库中的存储过程会变成一个谁也不敢乱动的巨大屎山。
6. 开发、升级、维护困难
绝大部分的存储过程都是为查询分析服务的,但是这类业务的最大特点就是经常性会被前端需求所绑架,也就是需求一边,功能就得跟着改变。
由于存储过程与数据库紧密结合,所以这就带来了两个方面的问题:
- 存储过程编码的语法与实现依托于数据本身,不理解数据的情况下,对于存储过程的实现也几乎完全无法理解,这就给维护人员带来了额外的学习、理解和使用成本。
存储过程的编写需要一定的专业技能。对于一些中小型公司来说,缺乏这方面的人才,开发难度比较大。而且从软件工程上来说,存储过程不利于代码版本控制和管理。存储过程通常集中在数据库中,也不易管理和维护。 - 代码的编译和发布的流程破坏了常规的安全规范。
常见的项目安全规范就是程序员不应该具备数据库的管理权限,而数据库管理员一般不负责编译和部署。
但是存储过程必须是发布在数据库上的,这样,要么程序员每次修改存储过程代码之后,提交给数据库管理员,由管理员编译并发布,但是这样会大大增加管理员的工作负担。
要么给程序员赋予高级权限,至少也是创建存储过程的权限,这样就不用频繁打扰管理员了。而这么做虽然方便,但存在严重的安全隐患。
本来做报表查询只需要对数据库有“select”权限就行,而可以编译存储过程的权限就太大了,几乎可以做一切。这时候程序员如果失误(不管是主观的还是客观的),很可能删除或修改了数据、甚至破坏整个数据库环境,造成严重的安全事故。
7. 加大了数据库系统的压力
在高并发的互联网场景下,并没有太多复杂的数据业务,反而对于常规简单数据服务要求很多,这样数据库就成为了一个系统里面压力最大的部分,在 常见高并发系统里面,数据库是最容易被攻破的环节,是因为相对于微服务架构下,天生就具备了容易横向扩展和负载均衡的应用系统而言,数据库的横向扩展的难度和成本都是指数级的,所以大部分高并发架构设计中,都使用了各种手段来保护数据库,例如Redis缓存、队列、读写分离等,希望把数据库的压力都分配出去。
而存储过程是直接运行在数据库上的,运算的任务是直接放在数据库引擎上的,这样就会使用一部分数据库的计算资源,加重了数据库的负担,这是与高并发架构设计思想是相悖的。
优点
任何一件事,既然客观存在,必然有他存在的理由,存储过程有这么多缺点,但是依然还活得好好的,所以必然有存在的理由,所以说完了问题,下面我们来说说存储过程的优点:
1. 高性能与事务性
存储过程可以将一组SQL语句封装在一起,作为一个单元来执行。这样可以减少数据库服务器与客户端之间的通信量,并且在最贴近数据的地方进行运算,最大限度的减少了各种IO开销。
在一些复杂业务逻辑中,存储过程可以将大量的复杂的业务逻辑封装起来,使得客户端代码更加简洁明了。
例如,在银行业务中进行转账时,可能需要检查账户余额、账户是否被冻结,同时还需要进行相应的日志记录等操作。如果将这些操作分散在多个客户端请求中完成,就会增加数据库服务器的负载,降低系统性能。而使用存储过程将所有的操作封装在一个单元中,可以显著提高系统性能。 另外它还提高SQL执行效率:存储过程可以预编译SQL语句,避免了每次执行SQL时都要重新解析的时间开销。
2. 安全性
存储过程的业务逻辑本身就是以SQL语句写成,所以常见的SQL注入这种安全问题基本上就直接可以避免了,另外通过存储过程,可以实现对数据访问权限的控制。例如,在银行行业中,只有具有特定权限的用户才能够进行某些敏感操作。
而这两项,也恰恰是银行业和金融业依旧在大范围的使用存储过程的主要原因。
随着互联网、云计算和大数据技术的兴起,存储过程开始逐渐失去它原有的优势。现在的互联网应用更加注重系统的可伸缩性和灵活性,银行业和金融业的新业务系统,也开始慢慢的放弃存储过程,转移到新的架构上来。
不过这些新的架构,是否能够完美支撑银行业,还有待时间的检验,要知道,互联网业务出了问题,杀码农祭天是个玩笑,但是银行业务出了问题,是真的有人要去坐牢的。
数据库扩展函数对于存储过程的改进
前面我们看到了存储过程的缺点,如果你是互联网派系的IT人员,可能会得出存储过程已经走向穷途末路的感觉,但是如果突然发现,世界上居然还有一种能够快速编写数据库扩展函数的能力,无疑是山穷水尽疑无路,柳暗花明又一村。
因为在(快捷、方便、有效的编写)扩展函数的加持下,存储过程很多的缺点都迎刃而解了。
例如我们就用Rust的PGRX框架来说吧:
- 兼容性和移植性这个问题,不同的数据库之间大概率还是无解,但是同一数据库的话,因为Rust的扩展函数讲究的是一个single-binary,既无依赖模式,这样就算不同数据库版本新增或者修改了什么特性,一般与扩展函数也无关。
- 调试困难这种事,在Rust这种高级语言下面,根本就不是个事,一大堆的IDE、工具、测试框架都在摩拳擦掌,饥渴难耐……
- 体系封版这种事情,Rust不但能够直接读写数据库本身,而且还能够直接读取外部数据、读取外部接口,所以这种事,基本上就是直接pass了。
- 耦合度高的问题,能够解决一部分,代码的逻辑可以通过library功能的方式,写在功能函数里面,可以独立维护与更新,但是最终到数据库里面,毕竟是贴在数据上面的,所以也无法全部解决。
- 管理困难这个问题,解决方式同4,代码逻辑可以按照正常软件工程的管理模式,但是数据函数部分,依然无法完全解决。
- 开发、升级、维护,同上,大部分问题在Rust开发中已经不是问题,但是与数据的问题依然存在。
- 此问题无解……
所以,我们发现,如果使用rust的pgrx一类框架来做数据库扩展,以求解决存储过程带来的问题,又回到了软件工程里面的经典论断,既:
没有银弹
不过,能解决60%以上的问题,我觉得就已经很不错了,真有可能是存储过程的续命良药也说不定。
打完收工。