在数字化转型中,选择合适的跨平台开发框架不仅能提高效率,还有助于确保数据安全与合规性。
689
2022-11-17
MySQl的内部存储代码
MySQL允许通过触发器、存储过程、函数的形式来存储代码。从MySQL5.1开始,还可以在定时任务中存放代码,这个定时任务也被称为“事件”。存储过程和存储函数都被统称为“存储程序”。
这四种存储代码都使用特殊的SQL语句扩展,它包含了很多过程处理语法,例如循环和条件分支等。不同类型的存储代码的主要区别在于其执行的上下文——也就是其输入和输出。存储过程和存储函数都可以接收参数然后返回值,但是触发器和事件却不行。
一般来说,存储代码是一种很好的共享和复用代码的方法。Giuseppe Maxia和其他一些人也建立了一些通用的存储过程库,在网站Stored Procedure Programming(O’Reilly)应该会有帮助。
有人倡导使用存储代码,也有人反对。这里我们不站在任何一边,只是列举一下在MySQL中使用存储代码的优点和缺点。首先,它有如下优点:
它在服务器内部执行,离数据最近,另外在服务器上执行还可以节省带宽和网络延迟。这是一种代码重用。可以方便地统一业务规则,保证某些行为总是一致,所以也可以为应用提供一定的安全性。它可以简化代码的维护和版本更新。它可以帮助提升安全,比如提供更细粒度的权限控制。一个常见的例子是银行用于转移资金的存储过程:这个存储过程可以在一个事务中完成资金转移和记录用于审计的日志。应用程序也可以通过存储过程的接口访问那些没有权限的表。服务器端可以缓存存储过程的执行计划,这对于需要反复调用的过程,会大大降低消耗。因为是在服务器端部署的,所以备份、维护都可以在服务器端完成。所以存储程序的维护工作会很简单。它没什么外部依赖,例如,不依赖任何Perl包和其他不想在服务器上部署的外部软件。它可以在应用开发和数据库开发人员之间更好地分工。不过最好是由数据库专家来开发存储过程,因为不是每个应用开发人员都能写出高效的SQL査询。
存储代码也有如下缺点:
MySQL本身没有提供好用的开发和调试工具,所以编写MySQL的存储代码比其他的数据库要更难些。较之应用程序的代码,存储代码效率要稍微差些。例如,存储代码中可以使用的函数非常有限,所以使用存储代码很难编写复杂的字符串维护功能,也很难实现太复杂的逻辑。存储代码可能会给应用程序代码的部署带来额外的复杂性。原本只需要部署应用代码和库表结构变更,现在还需要额外地部署MySQL内部的存储代码。因为存储程序都部署在服务器内,所以可能有安全隐患。如果将非标准的加密功能放在存储程序中,那么若数据库被攻破,数据也就泄漏了。但是若将加密函数放在应用程序代码中,那么攻击者必须同时攻破程序和数据库才能获得数据。存储过程会给数据库服务器增加额外的压力,而数据库服务器的扩展性相比应用服务器要差很多。MySQL并没有什么选项可以控制存储程序的资源消耗,所以在存储过程中的一个小错误,可能直接把服务器拖死。存储代码在MySQL中的实现也有很多限制——执行计划缓存是连接级别的,游标的物化和临时表相同,在MySQL5.5版本之前,异常处理也非常困难,等等。简而言之,较之T-SQL或者PL/SQL,MySQL的存储代码功能还非常非常弱。调试MySQL的存储过程是一件很困难的事情。如果慢日志只是给出CALL XYZ(‘A’),通常很难定位到底是什么导致的问题,这时不得不看看存储过程中的SQL语句是如何编写的。(这在Percona Server中可以通过参数控制。)它和基于语句的二进制日志复制合作得并不好。在基于语句的复制中,使用存储代码通常有很多的陷阱,除非你在这方面的经验非常丰富或者非常有耐心排査这类问题,否则需要谨慎使用。
这个缺陷列表很长——那么在真实世界中,这意味着什么?我们来看一个真实世界中弄 巧成拙的案例:在一个实例中,创建了一个存储过程来给应用程序访问数据库中的数据,这使得所有的数据访问都需要通过这个接口,甚至很多根据主键的查询也是如此,这大概使系统的性能降低了五倍左右。
最后,存储代码是一种帮助应用隐藏复杂性,使得应用开发更简单的方法。不过,它的性能可能更低,而且会给MySQL的复制等增加潜在的风险。所以当你打算使用存储过程的时候,需要问问自己,到底希望程序逻辑在哪儿实现:是数据库中还是应用代码中?这两种做法都可以,也都很流行。只是当你编写存储代码的时候,你需要明白这是将程序逻辑放在数据库中。
1.存储过程和函数
MySQL的架构本身和优化器的特性使得存储代码有一些天然的限制,它的性能也一定程度受限于此。5.6之前,有如下的限制:
优化器无法使用关键字DETERMINISTIC来优化单个查询中多次调用存储函数的情况。优化器无法评估存储函数的执行成本。每个连接都有独立的存储过程的执行计划缓存。如果有多个连接需要调用同一个存储过程,将会浪费缓存空间来反复缓存同样的执行计划。(如果使用的是连接池或者是持久化连接,那么执行计划缓存可能会有更长的生命周期。)存储程序和复制是一组诡异组合。如果可以,最好不要复制对存储程序的调用。直接复制由存储程序改变的数据则会更好。MySQL5.1引入的行复制能够改善这个问题。如果在MySQL5.0中开启了二进制日志,那么要么在所有的存储过程中都增加DETERMINISTIC限制或者设置MySQL的选项log_bin_trust_function_creators。
我们通常会希望存储程序越小、越简单越好。希望将更加复杂的处理逻辑交给上层的应用实现,通常这样会使代码更易读、易维护,也会更灵活。这样做也会让你拥有更多的计算资源,潜在的还会让你拥有更多的缓存资源。
不过,对于某些操作,存储过程比其他的实现要快得多——特别是当一个存储过程调用可以代替很多小査询的时候。如果査询很小,相比这个査询执行的成本,解析和网络开销就变得非常明显。为了证明这一点,我们先创建一个简单的存储过程,用来写入一定数量的数据到一个表中,下面是存储过程的代码:
DROP PROCEDURE IF EXISTS insert_many_rows; 2 3 delimiter // 4 5 CREATE PROCEDURE insert_many_rows (IN loops INT) 6 BEGIN 7 DECLARE v1 INT; 8 SET v1=loops; 9 WHILE v1 > 0 DO10 INSERT INTO test_table values(NULL,0,11 'qqqqqqqqqq 'qqqqqqqqqq SET v1 = v1 - 1;14 END WHILE;15 END;16 //1718 delimiter ;
然后对该存储过程执行基准测试,看插入一百万条记录的时间,并和通过客户端程序逐 条插入一百万条记录的时间进行对比。这里表结构和硬件并不重要——重要的是两种方式的相对速度。另外,我们还测试了使用MySQL Proxy连接MySQL来执行客户端程序测试的性能。为了让事情简单,整个测试在一台服务器上完成,包括客户端程序和MySQL Proxy实例。表7-1展示了测试结果。
表7-1:写入一百万数据所花费的总时间 | |
写入方式 | 总消耗时间 |
存储过程 | 101 sec |
客户端程序 | 279 sec |
使用MySQL Proxy的客户端程序 | 307 sec |
可以看到存储过程要快很多,很大程度因为它无须网络通信开销、解析开销和优化器开销等。
2.触发器
触发器可以让你在执行INSERT、UPDATE或者DELETE的时候,执行一些特定的操作。可以在MySQL中指定是在SQL语句执行前触发还是在执行后触发。触发器本身没有返回值,不过它们可以读取或者改变触发SQL语句所影响的数据。所以,可以使用触发器实现一些强制限制,或者某些业务逻辑,否则,就需要在应用程序中实现这些逻辑。
因为使用触发器可以减少客户端和服务器之间的通信,所以触发器可以简化应用逻辑,还可以提髙性能。另外,还可以用于自动更新反范式化数据或者汇总表数据。例如,在示例数据库Sakila中,我们可以使用触发器来维护film_text表。
MySQL触发器的实现非常简单,所以功能也有限。如果你在其他数据库产品中已经重度依赖触发器,那么在使用MySQL的时候需要注意,很多时候MySQL触发器的表现和预想的并不一样。特别需要注意以下几点:
对每一个表的每一个事件,最多只能定义一个触发器(换句话说,不能在AFTER INSERT上定义两个触发器)。MySQL只支持“基于行的触发”——也就是说,触发器始终是针对一条记录的,而不是针对整个SQL语句的。如果变更的数据集非常大的话,效率会很低。
下面这些触发器本身的限制也适用于MySQL
触发器可以掩盖服务器背后的工作,一个简单的SQL语句背后,因为触发器,可能包含了很多看不见的工作。例如,触发器可能会更新另一个相关表,那么这个触发器会让这条SQL影响的记录数翻一倍。触发器的问题也很难排査,如果某个性能问题和触发器相关,会很难分析和定位。触发器可能导致死锁和锁等待。如果触发器失败,那么原来的SQL语句也会失败。如果没有意识到这其中是触发器在搞鬼,那么很难理解服务器抛出的错误代码是什么意思。
如果仅考虑性能,那么MySQL触发器的实现中对服务器限制最大的就是它的“基于行的触发”设计。因为性能的原因,很多时候无法使用触发器来维护汇总和缓存表。使用触发器而不是批量更新的一个重要原因就是,使用触发器可以保证数据总是一致的。
触发器并不能一定保证更新的原子性。例如,一个触发器在更新MyISAM表的时候,如果遇到什么错误,是没有办法做回滚操作的。这时,触发器可以抛出错误。假设你在一个MyISAM表上建立一个AFTER UPDATE的触发器,用来更新另一个MyISAM表。如果触发器在更新第二个表的时候遇到错误导致更新失败,那么第一个表的更新并不会回滚。
在InnoDB表上的触发器是在同一个事务中完成的,所以它们执行的操作是原子的,原操作和触发器操作会同时失败或者成功。不过,如果在InnoDB表上建触发器去检查数据的一致性,需要特别小心MVCC,稍不小心,你可能会获得错误的结果。假设,你想实现外键约束,但是不打算使用InnoDB的外键约束。若打算编写一个BEFORE INSERT触发器来检査写入的数据对应列在另一个表中是存在的,但若你在触发器中没有使用SELECT FOR UPDATE,那么并发的更新语句可能会立刻更新对应记录,导致数据不一致。
我们不是危言耸听,让大家不要使用触发器。相反,触发器非常有用,尤其是实现一些约束、系统维护任务,以及更新反范式化数据的时候。
还可以使用触发器来记录数据变更日志。这对实现一些自定义的复制会非常方便,比如需要先断开连接,然后修改数据,最后再将所有的修改重新合并回去的情况。一个简单的例子是,一组用户各自在自己的个人电脑上工作,但他们的操作都需要同步到一台主数据库上,然后主数据库会将他们所有人的操作都分发给每个人。实现这个系统需要做两次同步操作。触发器就是构建整个系统的一个好办法。每个人的电脑上都可以使用一个触发器来记录每一次数据的修改,并将其发送到主数据库中。然后,再使用MySQL的复制将主数据库上的所有操作都复制一份到本地并应用。这里需要额外注意的是,如果触发器基于有自增主键的记录,并且使用的是基于语句的复制,那么自增长可能会在复制中出现不一致。
有时候可以使用一些技巧绕过触发器是“基于行的触发”这个限制。Roland Bouman发现, 对于BEFORE触发器除了处理的第一条记录,触发器函数R0W_C0UNT()总是会返回1。可以使用这个特点,使得触发器不再是针对每一行都运行,而是针对一条SQL语句运行一次。这和真正意义上的单条SQL语句的触发器并不相同,不过可以使用这个技术来模拟单条SQL语句的BEFORE触发器。这个行为可能是MySQL的一个缺陷,未来版本中可能会被修复,所以在使用这个技巧的时候,需要先验证在你的MySQL版本中是否适用,另外,在升级数据库的时候还需要检査这类触发器是否还能够正常工作。下面是一个使用这个技巧的例子:
CREATE TRIGGER fake_statement_triggerBEFORE INSERT ON sometableFOR EACH ROWBEGIN DECLARE v_row_count INT DEFAULT ROW_COUNT(); IF v_row_count <> 1 THEN -- Your code here END IF;END;
3.事件
事件是MySQL5.1引入的一种新的存储代码的方式。它类似于Linux的定时任务,不过是完全在MySQL内部实现的。你可以创建事件,指定MySQL在某个时候执行一段SQL代码,或者每隔一个时间间隔执行一段SQL代码。通常,我们会把复杂的SQL都封装到一个存储过程中,这样事件在执行的时候只需要做一个简单的CALL调用。
事件在一个独立事件调度线程中被初始化,这个线程和处理连接的线程没有任何关系。它不接收任何参数,也没有任何的返回值。可以在MySQL的日志中看到命令的执行日志,还可以在表INFORMATION_SCHEMA.EVENTS中看到各个事件状态,例如这个事件最后一次被执行的时间等。
类似的,一些适用于存储过程的考虑也同样适用于事件。首先,创建事件意味着给服务器带来额外工作。事件实现机制本身的开销并不大,但是事件需要执行SQL,则可能会对性能有很大的影响。更进一步,事件和其他的存储程序一样,在和基于语句的复制一起工作时,也可能会触发同样的问题。事件的一些典型应用包括定期地维护任务、重建缓存、构建汇总表来模拟物化视图,或者存储用于监控和诊断的状态值。
下面的例子创建了一个事件,它会每周一次针对某个数据库运行一个存储过程(后面我们将展示如何创建这个存储过程):
CREATE EVENT optimize_somedb ON SCHEDULE EVERY 1 WEEKDOCALL optimize_tables('somedb');
你可以指定事件本身是否被复制。根据需要,有时需要被复制,有时则不需要。看前面的例子,你可能会希望在所有的备库上都运行OPTIMIZE TABLE,不过要注意如果所有的备库同时执行,可能会影响服务器的性能(会对表加锁)。
最后,如果一个定时事件执行需要很长的时间,那么有可能会出现这样的情况,即前面一个事件还未执行完成,下一个时间点的事件又开始了。MySQL本身不会防止这种并发,所以需要用户自己编写这种情况下的防并发代码。你可以使用函数GET_L0CK()来确保当前总是只有一个事件在被执行:
CREATE EVENT optimize_somedb ON SCHEDULE EVERY 1 WEEKDOBEGIN DECLARE CONTINUE HANLDER FOR SQLEXCEPTION BEGIN END; IF GET_LOCK('somedb', 0) THEN DO CALL optimize_tables('somedb'); END IF; DO RELEASE_LOCK('somedb');END
这里的“CONTINUE HANLDER”用来确保,即使当事件执行出现了异样,仍然会释放持有的锁。
虽然事件的执行是和连接无关的,但是它仍然是线程级别的。MySQL中有一个事件调度线程,必须在MySQL配置文件中设置,或者使用下面的命令来设置:
mysql> SET GLOBAL event_scheduler := 1;
该选项一旦设置,该线程就会执行各个用户指定的事件中的各段SQL代码。你可以通过 观察MySQL的错误日志来了解事件的执行情况。
虽然事件调度是一个单独的线程,但是事件本身是可以并行执行的。MySQL会创建一个新的进程用于事件执行。在事件的代码中,如果你调用函数CONNECTION_ID(),也会返回一个唯一值,和一般的线程返回值一样一一虽然事件和MySQL的连接线程是无关的(这里的函数 CONNECTION_ID()返回的只是线程ID)。这里的进程和线程生命周期就是事件的执行过程。可以通过SHOW PROCESSLIST中的Command列来查看,这些线程的该列总是显示为“Connect”。
虽然事件处理进程需要创建一个线程来真正地执行事件,但该线程在时间执行结束后会被销毁,而不会放到线程缓存中,并且状态值Threads_created也不会被增加。
4.在存储程序中保留注释
存储过程、存储函数、触发器、事件通常都会包含大量的重要代码,在这些代码中加上注释就非常有必要了。但是这些注释可能不会存储在MySQL服务器中,因为MySQL的命令行客户端会自动过滤注释(命令行客户端的这个“特性”令人生厌,不过这就是生活)。
一个将注释存储到存储程序中的技巧就是使用版本相关的注释,因为这样的注释可能被MySQL服务器执行(例如,只有版本号大于某个值的时候才执行的代码)。服务器和客户端都知道这不是普通的注释,所以也就不会删除这些注释。为了让这样的“版本相关的代码”不被执行,可以指定一个非常大的版本号,例如99 999。我们现在给触发器加上一些注释文档,让它更易读:
CREATE TRIGGER fake_statement_triggerBEFORE INSERT ON sometableFOR EACH ROWBEGIN DECLARE v_row_count INT DEFAULT ROW_COUNT(); /*!99999 ROW_COUNT() is 1 except for the first row, so this executes only once per statement. */ IF v_row_count <> 1 THEN -- Your code here END IF;END;
作者:小家电维修
转世燕还故榻,为你衔来二月的花。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~