Oracle 9i & 10g编程艺术

lianyedie

贡献于2013-08-21

字数:0 关键词: Oracle 数据库服务器

Oracle 9i & 10g 编程艺术 本书是一本有关 Oracle 9i 和 10g 体系结构数据库的权威书籍,涵盖了所有最重要的 Oracle 体 系结构特性,包括文件、内存结构和进程,锁和锁存,事务、并发和多版本,表和索引,数据类 型,以及分区和并行,并充分利用具体的例子来介绍每个特性,不仅讨论了各个特性是什么,还 说明了它是如何工作的,如何使用这个特性来实现软件,以及有关的常见陷阱。 本书面向从事 Oracle 数据库应用的所有开发人员或 DBA。 本书能够帮助你发挥 Oracle 技术的最大潜力。……毋庸置疑,这是最重要的 Oracle 书籍之一, 绝对值得拥有。 — Ken Jacobs, 产品战略部(服务器技术)副总裁,Oracle 公司 无论你是程序员还是 DBA,要创建和管理稳定、高质量的 Oracle 系统,归根结底都需要理解 Oracle 数据库的体系结构。 本书是讲述 Oracle 数据库毋庸置疑的权威指南,凝聚了世界顶尖的 Oracle 专家 Thomas Kyte 数十年的宝贵经验和大量真知灼见。书中深入地分析了 Oracle 数据库体系结构,包括文件、内 存结构以及构成 Oracle 数据库和实例的底层进程,然后讨论了一些重要的数据库主题,如锁定、 并发控制、事务、重做和撤销,还解释了这些内容的重要性。最后,分析了数据库中的物理结构, 如表、索引和数据类型,并介绍哪些技术能最优地使用这些物理结构。 在介绍每个特性时,作者都充分利用具体的例子来说明,不仅讨论了各个特性是什么,还说明了 它如何工作,如何使用它来实现软件,并涵盖了相关的常见陷阱。 作者简介 Thomas Kyte 是 Oracle 公司核心技术集团的副总裁,从 Oracle 7.0.9 版本开始就一直任职于 Oracle 公司,不过,其实他从 5.1.5c 版本就开始使用 Oracle 了。 在 Oracle 公司,Kyte 专门 负责 Oracle 数据库,他的任务是帮助使用 Oracle 数据库的客户,并与他们共同设计和构建系统, 或者对系统进行重构和调优。在进入 Oracle 公司之前,Kyte 是一名系统集成人员,主要为美国 军方和政府部门的客户构建大规模、异构数据库。 Thomas Kyte就是主持Oracle Magazine Ask Tom专栏和Oracle公司同名在线论坛的那个Tom, 他通过这一方式热心地回答困扰着 Oracle 开发人员和 DBA 的各种问题。 目 录 第 1 章开发成功的 Oracle 应用 1 1.1 我的方法 2 1.2 黑盒方法 4 1.3 开发数据库应用的正确(和不正确)方法 8 1.3.1 了解 Oracle 体系结构 8 1.3.2 理解并发控制 14 1.3.3 多版本 19 1.3.4 数据库独立性?25 1.3.5“怎么能让应用运行得更快?”41 1.3.6DBA 与开发人员的关系 45 1.4 小结 46 第 2 章体系结构概述 47 2.1 定义数据库和实例 48 2.2SGA 和后台进程 53 2.3 连接 Oracle56 2.3.1 专用服务器 56 2.3.2 共享服务器 57 2.3.3TCP/IP 连接的基本原理 58 2.4 小结 61 第 3 章文件 63 3.1 参数文件 64 3.1.1 什么是参数?65 3.1.2 遗留的 init.ora 参数文件 67 3.1.3 服务器参数文件 69 3.1.4 参数文件小结 75 3.2 跟踪文件 76 3.2.1 请求的跟踪文件 77 3.2.2 针对内部错误生成的跟踪文件 80 3.2.3 跟踪文件小结 83 3.3 警告文件 83 3.4 数据文件 86 3.4.1 简要回顾文件系统机制 86 3.4.2Oracle 数据库中的存储层次体系 87 3.4.3 字典管理和本地管理的表空间 91 3.5 临时文件 93 3.6 控制文件 95 3.7 重做日志文件 95 3.7.1 在线重做日志 96 3.7.2 归档重做日志 98 3.8 密码文件 100 3.9 修改跟踪文件 103 3.10 闪回日志文件 104 3.10.1 闪回数据库 104 3.10.2 闪回恢复区 105 3.11DMP 文件(EXP/IMP 文件)106 3.12 数据泵文件 107 3.13 平面文件 110 3.14 小结 111 第 4 章内存结构 113 4.1 进程全局区和用户全局区 113 4.1.1 手动 PGA 内存管理 114 4.1.2 自动 PGA 内存管理 121 4.1.3 手动和自动内存管理的选择 131 4.1.4PGA 和 UGA 小结 132 4.2 系统全局区 133 4.2.1 固定 SGA137 4.2.2 重做缓冲区 137 4.2.3 块缓冲区缓存 138 4.2.4 共享池 145 4.2.5 大池 148 4.2.6Java 池 149 4.2.7 流池 150 4.2.8 自动 SGA 内存管理 150 4.3 小结 151 第 5 章 Oracle 进程 153 5.1 服务器进程 153 5.1.1 专用服务器连接 154 5.1.2 共享服务器连接 156 5.1.3 连接与会话 157 5.1.4 专用服务器与共享服务器 163 5.1.5 专用/共享服务器小结 166 5.2 后台进程 167 5.2.1 中心后台进程 168 5.2.2 工具后台进程 175 5.3 从属进程 178 5.3.1I/O 从属进程 178 5.3.2 并行查询从属进程 179 5.4 小结 179 第 6 章锁 181 6.1 什么是锁?181 6.2 锁定问题 184 6.2.1 丢失更新 184 6.2.2 悲观锁定 185 6.2.3 乐观锁定 187 6.2.4 乐观锁定还是悲观锁定?197 6.2.5 阻塞 198 6.2.6 死锁 201 6.2.7 锁升级 206 6.3 锁类型 206 6.3.1DML 锁 207 6.3.2DDL 锁 215 6.3.3 闩 218 6.3.4 手动锁定和用户定义锁 226 6.4 小结 227 第 7 章并发与多版本 229 7.1 什么是并发控制?229 7.2 事务隔离级别 230 7.2.1READUNCOMMITTED232 7.2.2READCOMMITTED233 7.2.3REPEATABLEREAD235 7.2.4SERIALIZABLE237 7.2.5READONLY239 7.3 多版本读一致性的含义 240 7.3.1 一种会失败的常用数据仓库技术 240 7.3.2 解释热表上超出期望的 I/O241 7.4 写一致性 244 7.4.1 一致读和当前读 244 7.4.2 查看重启动 247 7.4.3 为什么重启动对我们很重要?250 7.5 小结 251 第 8 章事务 253 8.1 事务控制语句 254 8.2 原子性 255 8.2.1 语句级原子性 255 8.2.2 过程级原子性 257 8.2.3 事务级原子性 260 8.3 完整性约束和事务 260 8.3.1IMMEDIATE 约束 260 8.3.2DEFERRABLE 约束和级联更新 261 8.4 不好的事务习惯 263 8.4.1 在循环中提交?264 8.4.2 使用自动提交?270 8.5 分布式事务 271 8.6 自治事务 273 8.6.1 自治事务如何工作?273 8.6.2 何时使用自治事务?276 8.7 小结 279 第 9 章 redo 与 undo281 9.1 什么是 redo?281 9.2 什么是 undo?282 9.3redo 和 undo 如何协作?285 9.4 提交和回滚处理 289 9.4.1COMMIT 做什么?289 9.4.2ROLLBACK 做什么?296 9.5 分析 redo297 9.5.1 测量 redo298 9.5.2redo 生成和 BEFORE/AFTER 触发器 300 9.5.3 我能关掉重做日志生成吗?306 9.5.4 为什么不能分配一个新日志?310 9.5.5 块清除 312 9.5.6 日志竞争 315 9.5.7 临时表和 redo/undo317 9.6 分析 undo321 9.6.1 什么操作会生成最多和最少的 undo?321 9.6.2ORA-01555:snapshottooold 错误 323 9.7 小结 334 第 10 章数据库表 335 10.1 表类型 335 10.2 术语 337 10.2.1 段 337 10.2.2 段空间管理 339 10.2.3 高水位线 340 10.2.4freelists342 10.2.5PCTFREE 和 PCTUSED345 10.2.6LOGGING 和 NOLOGGING348 10.2.7INITRANS 和 MAXTRANS349 10.3 堆组织表 349 10.4 索引组织表 352 10.5 索引聚簇表 368 10.6 散列聚簇表 376 10.7 有序散列聚簇表 386 10.8 嵌套表 390 10.8.1 嵌套表语法 390 10.8.2 嵌套表存储 399 10.8.3 嵌套表小结 402 10.9 临时表 402 10.10 对象表 410 10.11 小结 418 第 11 章索引 421 11.1Oracle 索引概述 422 11.2B*树索引 423 11.2.1 索引键压缩 426 11.2.2 反向键索引 429 11.2.3 降序索引 435 11.2.4 什么情况下应该使用 B*树索引?437 11.2.5B*树小结 448 11.3 位图索引 448 11.3.1 什么情况下应该使用位图索引?449 11.3.2 位图联结索引 453 11.3.3 位图索引小结 455 11.4 基于函数的索引 456 11.4.1 重要的实现细节 456 11.4.2 一个简单的基于函数的索引例子 457 11.4.3 只对部分行建立索引 465 11.4.4 实现有选择的惟一性 467 11.4.5 关于 CASE 的警告 467 11.4.6 关于 ORA-01743 的警告 469 11.4.7 基于函数的索引小结 470 11.5 应用域索引 470 11.6 关于索引的常见问题和神话 472 11.6.1 视图能使用索引吗?472 11.6.2Null 和索引能协作吗?472 11.6.3 外键是否应该加索引?475 11.6.4 为什么没有使用我的索引?476 11.6.5 神话:索引中从不重用空间 483 11.6.6 神话:最有差别的元素应该在最前面 486 11.7 小结 490 第 12 章数据类型 491 12.1Oracle 数据类型概述 491 12.2 字符和二进制串类型 494 12.2.1NLS 概述 494 12.2.2 字符串 497 12.3 二进制串:RAW 类型 504 12.4 数值类型 506 12.4.1NUMBER 类型的语法和用法 509 12.4.2BINARY_FLOAT/BINARY_DOUBLE 类型的语法和用法 513 12.4.3 非固有数值类型 513 12.4.4 性能考虑 514 12.5LONG 类型 515 12.5.1LONG 和 LONGRAW 类型的限制 516 12.5.2 处理遗留的 LONG 类型 517 12.6DATE.TIMESTAMP 和 INTERVAL 类型 523 12.6.1 格式 523 12.6.2DATE 类型 525 12.6.3TIMESTAMP 类型 533 12.6.4INTERVAL 类型 541 12.7LOB 类型 544 12.7.1 内部 LOB545 12.7.2BFILE557 12.8ROWID/UROWID 类型 559 12.9 小结 560 第 13 章分区 561 13.1 分区概述 561 13.1.1 提高可用性 562 13.1.2 减少管理负担 564 13.1.3 改善语句性能 569 13.2 表分区机制 571 13.2.1 区间分区 571 13.2.2 散列分区 574 13.2.3 列表分区 579 13.2.4 组合分区 581 13.2.5 行移动 583 13.2.6 表分区机制小结 585 13.3 索引分区 586 13.3.1 局部索引与全局索引 587 13.3.2 局部索引 587 13.3.3 全局索引 594 13.4 再论分区和性能 610 13.5 审计和段空间压缩 617 13.6 小结 618 第 14 章并行执行 619 14.1 何时使用并行执行 620 14.2 并行查询 622 14.3 并行 DML628 14.4 并行 DDL631 14.4.1 并行 DDL 和使用外部表的数据加载 632 14.4.2 并行 DDL 和区段截断 634 14.5 并行恢复 643 14.6 过程并行化 643 14.6.1 并行管道函数 644 14.6.2DIY 并行化 648 14.7 小结 652 第 15 章数据加载和卸载 655 15.1SQL*Loader655 15.1.1 用 SQLLDR 加载数据的 FAQ660 15.1.2SQLLDR 警告 686 15.1.3SQLLDR 小结 686 15.2 外部表 687 15.2.1 建立外部表 688 15.2.2 处理错误 693 15.2.3 使用外部表加载不同的文件 697 15.2.4 多用户问题 697 15.2.5 外部表小结 698 15.3 平面文件卸载 698 15.4 数据泵卸载 708 15.5 小结 710 索引 711 第 1 章开发成功的 ORACLE 应用 我花了大量时间使用 Oracle 数据库软件,更确切地讲,一直在与使用 Oracle 数据库软 件的人打交道。在过去的 18 年间,我参与过许多项目,有的相当成功,有的却彻底失败, 如果把这些经验用几句话来概括,可以总结如下: ‰ 基于数据库(或依赖于数据库)构建的应用是否成功,这取决于如何使用数据库。另外, 从我的经验看,所有应用的构建都围绕着数据库。如果一个应用未在任何地方持久地存储数 据,很难想象这个应用真的有用。 ‰ 应用总是在“来来去去”,而数据不同,它们会永远存在。从长远来讲,我们的目标并 不是构建应用,而应该是如何使用这些应用底层的数据。 ‰ 开发小组的核心必须有一些精通数据库的开发人员,他们要负责确保数据库逻辑是可靠 的,系统能够顺利构建。如果已成事实(应用已经部署)之后再去调优,这通常表明,在开 发期间你没有认真考虑这些问题。 这些话看上去再显然不过了。然而,我发现太多的人都把数据库当成是一个黑 盒(black box),好像不需要对它有深入了解。他们可能有一个 SQL 生成器,认 为有了这个工具,就不需要再费工夫去学 SQL 语言。也可能认为使用数据库就像 使用平面文件一样,只需要根据索引读数据就行。不管他们怎么想,有一点可以 告诉你,如果按这种思路来考虑,往往会被误导:不了解数据库,你将寸步难行。 这一章将讨论为什么需要了解数据库,具体地讲,就是为什么需要理解以下内容: ‰ 数据库的体系结构,数据库如何工作,以及有怎样的表现。 ‰ 并发控制是什么,并发控制对你意味着什么。 ‰ 性能、可扩缩性和安全性都是开发时就应该考虑的需求,必须适当地做出设计,不要指 望能碰巧满足这些需求。 ‰ 数据库的特性如何实现。某个特定数据库特性的实际实现方式可能与你想象的不一样。 你必须根据数据库实际上如何工作(而不是认为它应该如何工作)来进行设计。 ‰ 数据库已经提供了哪些特性,为什么使用数据库已提供的特性要优于自行构建自己的 特性。 ‰ 为什么粗略地了解 SQL 还不够,还需要更深入地学习 SQL。 ‰ DBA 和开发人员都在为同一个目标努力;他们不是敌对的两个阵营,不是想在每个回合 中比试谁更聪明。 初看上去,好像要学的东西还不少。不过可以做个对照,请考虑这样一个问 题:如果你在一个全新的操作系统(operating system,OS)上开发一个高度可 扩缩的企业应用,首先要做什么?希望你的答案是“了解这个新操作系统如何工 作,应用在它上面怎样运行,等等”。如果不是这样,你的开发努力就会付诸东 流。 例如,可以考虑一下 Windows 与 Linux。它们都是操作系统,能为开发人员 提供大致相同的一组服务,如文件管理、内存管理、进程管理、安全性等。不过, 这两个操作系统的体系结构却大相径庭。因此,如果你一直是 Windows 程序员, 现在给你一个任务,让你在Linux 平台上开发新应用,那么很多东西都得从头学 起。内存管理的处理就完全不同。建立服务器进程的方式也有很大差异。在 Win dows 下,你会开发一个进程、一个可执行程序,但有许多线程。在 Linux 下则 不同,不会开发单个独立的可执行程序;相反,会有多个进程协作。总之,你在 Windows 环境下学到的许多知识到了 Linux 上并不适用(公平地讲,反之亦然)。 要想在新平台上也取得成功,你必须把原来的一些习惯丢掉。 在不同操作系统上运行的应用存在上述问题,基于不同数据库运行的应用也 存在同样的问题:你要懂得,数据库对于成功至关重要。如果不了解你的数据库 做什么,或者不清楚它怎么做,那你的应用很可能会失败。如果你认为应用在 S QL Server 上能很好地运行,那它在 Oracle 上也肯定能很好地工作,你的应用 往往会失败。另外,公平地讲,反过来也一样:一个 Oracle 应用可能开发得很 好,可扩缩性很好,但是如果不对体系结构做重大改变,它在 SQL Server 上不 一定能正常运行。Windows 和 Linux 都是操作系统,但二者截然不同;同样地, Oracle 和 SQL Server(甚至可以是任何其他数据库)尽管都是数据库,但二者 完全不同。 1.1 我的方法 在阅读下面的内容之前, 我觉得有必要先解释一下我的开发方法。针对问 题,我喜欢采用一种以数据库为中心的方法。如果能在数据库中完成,我肯定就 会让数据库来做,而不是自行实现。对此有几个原因。首先,也是最重要的一点, 我知道如果让数据库来实现功能,应用就可以部署在任何环境中。据我所知,没 有哪个流行的服务器操作系统不支持 Oracle;从 Windows 到一系列 UNIX/Linux 系统,再到 OS/390 大型机系统,都支持 Oracle 软件和诸多选项。我经常在我的 笔记本电脑上构建和测试解决方案,其中在 Linux 或 Windows XP 上(或者使用 VMware 来模拟这些环境)运行 Oracle9i/Oracle 10g。这样一来,我就能把这些 解决方案部署在运行相同数据库软件但有不同操作系统的多种服务器上。我发 现,如果某个特性不是在数据库中实现,要想在希望的任何环境中部署这个特性 将极其困难。Java 语言之所以让人趋之若鹜,一个主要原因就是 Java 程序总在 同样的虚拟环境[即 Java 虚拟机(Java Virtual Machine,JVM)]中编译,这 使得这些程序的可移植性很好。有意思的是,也正是这个特性让我对数据库着迷 不已。数据库就是我的“虚拟机”,它也是我的“虚拟操作系统”。 前面已经提到,我采用的方法是尽可能在数据库中实现功能。如果数据库环 境无法满足我的需求,我也会在数据库之外使用 Java 或 C 来实现。采用这种方 式,操作系统的复杂细节对我来说几乎是隐藏的。我要了解我的“虚拟机”如何 工作(也就是 Oracle 如何工作,有时可能还需要用到 JVM),毕竟,起码要了 解自己使用的工具才行。不过,至于在一个给定的操作系统上怎么才能最好地工 作,这些都由“虚拟机”来负责。 所以,只需知道这个“虚拟操作系统”的细节,我构建的应用就能在多种操 作系统上很好地工作和扩缩。我并不是暗示你可以完全忽略底层的操作系统。不 过,作为一个构建数据库应用的软件开发人员,还是尽量避开它比较好,你不必 处理操作系统的诸多细微之处。应该让你的 DBA(负责运行 Oracle 软件)来考 虑如何适应操作系统(如果他或她做不到,你就该换个新的 DBA 了!)。如果你 在开发客户/服务器软件,而且大量代码都是在数据库和虚拟机(VM;JVM 可能 是最流行的虚拟机了)之外实现,那你还得再次考虑你的操作系统。 对于开发数据库软件,我有一套很简单的哲学,这是我多年以来一直信守的 思想: ‰ 如果可能,尽量利用一条 SQL 语句完成工作。 ‰ 如果无法用一条 SQL 语句完成,就通过 PL/SQL 实现(不过,尽可能少用 PL/SQL!)。 ‰ 如果在 PL/SQL 中也无法做到(因为它缺少一些特性,如列出目录中的文件),可以试 试使用 Java 存储过程来实现。不过,有了 Oracle9i 及以上版本后,如今需要这样做的可能 性极小。 ‰ 如果用 Java 还办不到,那就在 C 外部过程中实现。如果速度要求很高,或者要使用采用 C 编写的一个第三方 API,就常常使用这种做法。 ‰ 如果在 C 外部例程中还无法实现,你就该好好想想有没有必要做这个工作了。 在这本书中,你会看到我是怎样将上述思想付诸实现的。我会尽可能使用 S QL,充分利用它强大的新功能,如使用分析函数来解决相当复杂的问题,而不是 求助于过程性代码。如果需要,我会使用PL/SQL 和 PL/SQL 中的对象类型来完成 SQL 本身办不到的事情。PL/SQL 发展至今已经有很长时间了,它得到了长达 18 年的调整和优化。实际上,Oracle 10g 编译器本身就首次重写为一个优化编译 器。你会发现,没有哪种语言能像 PL/SQL 这样与 SQL 如此紧密地耦合,也没有 哪种语言得到如此优化,可以与 SQL 更好地交互。在 PL/SQL 中使用 SQL 是一件 相当自然的事情,而在几乎所有其他语言(从 Visual Basic 到 Java)中,使用 SQL 感觉上都很麻烦。对于这些语言来说,使用 SQL 绝对没有“自然”的感觉; 它不是这些语言本身的扩缩。如果 PL/SQL 还无法做到(这在 Oracle 9i 或10g 中可能相当少见),我们会使用 Java。有时,如果 C 是惟一的选择,或者需要 C 才能提供的高速度,我们也会用 C 来完成工作,不过这往往是最后一道防线。随 着本地 Java 编译(native Java compilation)的闪亮登场(可以把 Java 字节 码转换为具体平台上特定于操作系统的对象码),你会发现,在许多情况下,J ava 与 C 的运行速度相差无几。所以,需要用到 C 的情况越来越少。 1.2 黑盒方法 根据我个人的第一手经验(这表示,在学习软件开发时我自己也曾犯过错误),我对基于数 据库的软件开发为什么如此频繁地遭遇失败有一些看法。先来澄清一下,这里提到的这些项 目可能一般不算失败,但是启用和部署所需的时间比原计划多出许多,原因是需要大幅重写, 重新建立体系结构,或者需要充分调优。我个人把这些延迟的项目称为“失败”,因为它们 原本可以按时完成(甚至可以更快完成)。 数据库项目失败的最常见的一个原因是对数据库的实际认识不足,缺乏对所 用基本工具的了解。黑盒方法是指有意让开发人员对数据库退避三舍,甚至鼓励 开发人员根本不要学习数据库!在很多情况下,开发人员没有充分利用数据库。 这种方法的出现,原因可以归结为 FUD[恐惧(fear)、不确定(uncertainty) 和怀疑(doubt)]。一般都认为数据库“很难”,SQL、事务和数据完整性都“很 难”。所以“解决方法”就是:不要卷入难题中,要知难而退。他们把数据库当 成一个黑盒,利用一些软件工具来生成所有代码。他们试图利用重重保护与数据 库完全隔离,以避免接触这么“难”的数据库。 我一直很难理解这种数据库开发方法,原因有二。一个原因是对我来说,学 习 Java 和 C 比学习数据库基本概念要难多了。现在我对 Java 和 C 已经很精通, 但是在能熟练使用 Java 和 C 之前我经受了许多磨炼,而掌握数据库则没有这么 费劲。对于数据库,你要知道它是怎么工作的,但无需了解每一个细节。用 C 或 Java 编程时,则确实需要掌握每一个细枝末节,而这些语言实在是很“庞大”。 让我无法理解这种方法的另一个原因是,构建数据库应用时,最重要的软件 就是数据库。成功的开发小组都会认识到这一点,而且每个开发人员都要了解数 据库,并把重点放在数据库上。但我接触到的许多项目中,情况却几乎恰恰相反。 例如,下面就是一种典型的情况: ‰ 在构建前端所用的 GUI 工具或语言(如 Java)方面,开发人员得到了充分的培训。在很 多情况下,他们会有数周甚至数月的培训。 ‰ 开发人员没有进行过 Oracle 培训,也没有任何 Oracle 经验。大多数人都没有数据库经验, 所以并未理解如何使用核心的数据库构造(如各种可用的索引和表结构)。 ‰ 开发人员力图谨守“数据库独立性”这一原则,但是出于许多原因,他们可能做不到。 最明显的一个原因是:他们对于数据库没有足够的了解,也不清楚这些数据库可能有什么区 别。这样一个开发小组无法知道要避开数据库的哪些特性才能保证数据库独立性。 ‰ 开发人员遇到大量性能问题、数据完整性问题、挂起问题等(但这些应用的界面往往很 漂亮)。 因为出现了无法避免的性能问题,他们把我找来,要求帮助解决这些难题。 我最早就是从构建数据库独立的应用做起的(从某种程度上讲,在ODBC 问世之 前,我就已经编写了自己的 ODBC 驱动程序),我知道哪些地方可能会犯错误, 因为我以前就曾犯过这些错误。我总会查看是否存在下面这些问题:存在效率 低下的 SQL;有大量过程性代码,但这些工作原本用一条 SQL 语句就足够了; 为了保持数据库独立性,没有用到新特性(1995 年以后的新特性都不敢用), 等等。 我还记得这样一个特例,有个项目找我来帮忙。当时要用到一个新命令,但 我记不清那个新命令的语法。所以我让他们给我拿一本 SQL Reference 手册,谁 知他们给了我一本 Oracle 6.0 文档。那个项目开发用的是7.3 版本,要知道,6. 0 版本和 7.3 版本之间整整相差 5 年!7.3 才是所有开发人员使用的版本,但似 乎谁都不关心这一点。不用说他们需要了解的跟踪和调优工具在6.0 版本中甚至 不存在。更不用说在这5 年间又增加了诸如触发器、存储过程和数百个其他特性, 这些都是编写 6.0 版文档(也就是他们现在参考的文档)时根本没有的特性。由 此很容易看出他们为什么需要帮助,但解决起来就是另一码事了。 注意 甚至时至今日,已经到了 2005 年,我还是经常发现有些数据库应用开发人员根本 不花些时间来看文档。我的网站(http://asktom.oracle.com)上经常会有:“……的 语法是什么”这样的问题,并解释说“我们拿不到文档,所以请告诉我们”。对于 许多这样的问题,我拒绝直接做出回答,而是会把在线文档的地址告诉他们。无论 你身处何地,都能免费得到这些文档。在过去 10 年中,“我们没有文档”或“我 们无法访问资源”之类的借口已经站不住脚了。如今已经有了诸如 http://otn.oracle. com(Oracle 技术网络)和 http://groups. google.com (Google Groups Usenet 论坛) 等网站,它们都提供了丰富的资源,如果你手边还没有一套完整的文档,那就太说 不过去了! 构建数据库应用的开发人员要避开数据库的主张实在让我震惊,不过这种做 法还顽固不化地存在着。许多人还认为开发人员没办法花那么多时间来进行数据 库培训,而且他们根本不需要了解数据库。为什么?我不止一次地听到这样的说 法:“Oracle 是世界上最可扩缩的数据库,所以我们不用了解它,它自然会按 部就班地把事情做好的。”Oracle 是世界上最可扩缩的数据库,这一点没错。 不过,用 Oracle 不仅能写出好的、可扩缩的代码,也同样能很容易地写出不好 的、不可扩缩的代码(这可能更容易)。把这句话里的“Oracle”替换为其他任 何一种技术的名字,这句话仍然正确。事实是:编写表现不佳的应用往往比编写 表现优秀的应用更容易。如果你不清楚自己在做什么,可能会发现你打算用世界 上最可扩缩的数据库建立一个单用户系统! 数据库是一个工具;不论是什么工具,如果使用不当都会带来灾难。举个例 子,你想用胡桃钳弄碎胡桃,会不会把胡桃钳当锤子一样用呢?当然这也是可以 的,不过这样用胡桃钳很不合适,而且后果可能很严重,没准会重重地伤到你的 手指。如果还是对你的数据库一无所知,你也会有类似的结局。 例如,最近我参与了一个项目。开发人员正饱受性能问题之苦,看上去他们 的系统中许多事务在串行进行。他们的做法不是大家并发地工作,而是每个人都 要排一个长长的队,苦苦等着前面的人完成后才能继续。应用架构师向我展示了 系统的体系结构,这是经典的三层方法。他们想让Web 浏览器与一个运行 JSP(J avaServer Pages)的中间层应用服务器通信。JSP 再使用另一个 EJB(Enterpr ise JavaBeans)层,在这一层执行所有 SQL。EJB 中的 SQL 由某个第三方工具生 成,这是采用一种数据库独立的方式完成的。 现在看来,对这个系统很难做任何诊断,因为没有可测量或可跟踪的代码。 测量代码(instrumenting code)堪称一门艺术,可以把开发的每行代码变成调 试代码,这样就能跟踪应用的执行,遇到性能、容量甚至逻辑问题时就能跟踪到 问题出在哪里。在这里,我们只能肯定地说问题出在“浏览器和数据库之间的某 个地方”。换句话说,整个系统都是怀疑对象。对此有好消息也有坏消息。一方 面,Oracle 数据库完全可测量;另一方面,应用必须能够在适当的位置打开和 关闭测量,遗憾的是,这个应用不具备这种能力。 所以,我们面对的困难是,要在没有太多细节的情况下诊断出导致性能问题 的原因,我们只能依靠从数据库本身收集的信息。一般地,要分析应用的性能问 题,采用应用级跟踪更合适。不过,幸运的是,这里的解决方案很简单。通过查 看一些 Oracle V$表(V$ 表是 Oracle 提供其测量结果或统计信息的一种方 法),可以看出,竞争主要都围绕着一个表,这是一种排队表。结论是根据 V$L OCK 视图和 V$SQL 做出的,V$LOCK 视图可以显示阻塞的会话,V$SQL 会显示这些 阻塞会话试图执行的 SQL。应用想在这个表中放记录,而另外一组进程要从表中 取出记录并进行处理。通过更深入地“挖掘”,我们发现这个表的 PROCESSED_F LAG 列上有一个位图索引。 注意 第 12 章会详细介绍位图索引,并讨论为什么位图索引只适用于低基数值,但是对 频繁更新的列不适用。 原因在于,PROCESSED_FLAG 列只有两个值:Y 和 N。对于插入到表中的记录, 该列值为 N(表示未处理)。其他进程读取和处理这个记录时,就会把该列值从 N 更新为 Y。这些进程要很快地找出PROCESSED_FLAG 列值为 N 的记录,所以开发 人员知道,应该对这个列建立索引。他们在别处了解到,位图索引适用于低基数 (low-cardinality)列,所谓低基数列就是指这个列只有很少的可取值,所以 看上去位图索引是一个很自然的选择。 不过,所有问题的根由正是这个位图索引。采用位图索引,一个键指向多行, 可能数以百计甚至更多。如果更新一个位图索引键,那么这个键指向的数百条记 录会与你实际更新的那一行一同被有效地锁定。 所以,如果有人插入一条新记录(PROCESSED_FLAG 列值为 N),就会锁定位 图索引中的 N 键,而这会有效地同时锁定另外数百条 PROCESSED_FLAG 列值为 N 的记录(以下记作 N 记录)。此时,想要读这个表并处理记录的进程就无法将 N 记录修改为 Y 记录(已处理的记录)。原因是,要想把这个列从 N 更新为 Y,需 要锁定同一个位图索引键。实际上,想在这个表中插入新记录的其他会话也会阻 塞,因为它们同样想对这个位图索引键锁定。简单地讲,开发人员实现了这样一 组结构,它一次最多只允许一个人插入或更新! 可以用一个简单的例子说明这种情况。在此,我使用两个会话来展示阻塞很 容易发生: 现在,如果我在另一个 SQL*Plus 会话中执行以下命令: 这条语句就会“挂起”,直到在第一个阻塞会话中发出 COMMIT 为止。 这里的问题就是缺乏足够的了解造成的;由于不了解数据库特性(位图索 引),不清楚它做些什么以及怎么做,就导致这个数据库从一开始可扩缩性就很 差。一旦找出了问题,修正起来就很容易了。处理标志列上确实要有一个索引, 但不能是位图索引。这里需要一个传统的 B*Tree 索引。说服开发人员接受这个 方案很是费了一番功夫,因为这个列只有两个不同的可取值,却需要使用一个传 统的索引,对此没有人表示赞同。不过,通过仿真(我很热衷于仿真、测试和试 验),我们证明了这确实是正确的选择。对这个列加索引有两种方法: ‰ 在处理标志列上创建一个索引。 ‰ 只在处理标志为 N 时在处理标志列上创建一个索引,也就是说,只对感兴趣的值加索引。 通常,如果处理标志为 Y,我们可能不想使用索引,因为表中大多数记录处理标志的值都可 能是 Y。注意这里的用辞,我没有说“我们绝对不想使用索引”,如果出于某种原因需要频 繁地统计已处理记录的数目,对已处理记录加索引可能也很有用。 最后,我们只在处理标志为 N 的记录上创建了一个非常小的索引,由此可以 快速地访问感兴趣的记录。 到此就结束了吗?没有,绝对没有结束。开发人员的解决方案还是不太理想。 由于他们对所用工具缺乏足够的了解,我们只是修正了由此导致的主要问题,而 且经过大量研究后才发现系统不能很好地测量。我们还没有解决以下问题: ‰ 构建应用时根本没有考虑过可扩缩性。可扩缩性必须在设计中加以考虑。 ‰ 应用本身无法调优,甚至无法修改。经验证明,80%~90%的调优都是在应用级完成的, 而不是在数据库级。 ‰ 应用完成的功能(排队表)实际上在数据库中已经提供了,而且数据库是以一种高度并 发和可扩缩的方式提供的。我指的就是数据库已有的高级排队(Advanced Queuing,AQ) 软件,开发人员没有直接利用这个功能,而是在埋头重新实现。 ‰ 开发人员不清楚 bean 在数据库中做了什么,也不知道出了问题要到哪里去查。 这个项目的问题大致如此,所以我们需要解决以下方面的问题: ‰ 如何对 SQL 调优而不修改 SQL。这看起来很神奇,不过在 Oracle 10g 中确实可以办得到, 从很大程度上讲堪称首创。 ‰ 如何测量性能。 ‰ 如何查看哪里出现了瓶颈。 ‰ 如何建立索引,对什么建立索引。 ‰ 如此等等。 一周结束后,原本对数据库敬而远之的开发人员惊讶地发现,数据库居然能 提供如此之多的功能,而且了解这些信息是如此容易。最重要的是,这使得应用 的性能发生了翻天覆地的变化。最终他们的项目还是成功了,只是比预期的要晚 几个星期。 这个例子不是批评诸如 EJB 和容器托管持久存储之类的工具或技术。我们 要批评的是故意不去了解数据库,不去学习数据库如何工作以及怎样使用数据 库这种做法。这个案例中使用的技术本来能很好地工作,但要求开发人员对数 据库本身有一些深入的了解。 关键是:数据库通常是应用的基石。如果它不能很好地工作,那其他的都没 有什么意义了。如果你手上有一个黑盒,它不能正常工作,你能做些什么呢?可 能只能做一件事,那就是盯着这个黑盒子发愣,搞不明白为什么它不能正常工作。 你没办法修理它,也没办法进行调整。你根本不知道它是怎样工作的,最后的决 定只能是保持现状。我提倡的方法则是:了解你的数据库,掌握它是怎么工作的, 弄清楚它能为你做什么,并且最大限度地加以利用。 1.3 开发数据库应用的正确和不正确方法 到目前为止,我一直是通过闲聊来强调理解数据库的重要性。本章后面则会 靠实验说话,明确地讨论为什么了解数据库及其工作原理对于成功的实现大有帮 助(而无需把应用写两次!)。有些问题修改起来很简单,只要你知道怎么发现 这些问题即可。还有一些问题则不同,必须大动干戈地重写应用方能更正。这本 书的目标之一就是首先帮助避免问题的发生。 注意 在下面的几节中,我会谈到一些核心的 Oracle 特性,但并不深入地讨论这些特性 到底是什么,也不会全面地介绍使用这些特性的全部细节。如果想了解更多的信息, 建议你接着阅读本书后面的章节,或者参考相关的 Oracle 文档。 1.3.1 了解 Oracle 体系结构 最近,我参与了一个客户的项目,他运行着一个大型的生产应用。这个应用已经从 S QL Server“移植到”Oracle。之所以把“移植”一词用引号括起来,原因是我看到的大多 数移植都只是“怎么能只对 SQL Server 代码做最少的改动,就让它们在 Oracle 上编译和 执行”。要把应用从一个数据库真正移植到另一个数据库,这绝对是一项大工程。必须仔 细检查算法,看看算法在目标数据库上能否正确地工作;诸如并发控制和锁定机制等特性 在不同的数据库中实现不同,这会直接影响应用在不同数据库上如何工作。另外还要深入 地分析算法,看看在目标数据库中实现这些算法是否合理。坦率地讲,我看到的大多数应 用通常都是根据“最小移植”得来的,因为这些应用最需要帮助。当然,反过来也一样: 把一个 Oracle 应用简单地“移植”到 SQL Server,而且尽可能地避免改动,这也会得到 一个很成问题、表现极差的应用。 无论如何,这种“移植”的目标都是扩缩应用,要支持更大的用户群。不过, “移植”应用的开发人员一方面想达到这个目的,另一方面又想尽量少出力。所 以,这些开发人员往往保持客户和数据库层的体系结构基本不变,简单地将数据 从 SQL Server 移到 Oracle,而且尽可能不改动代码。如果决定将原来 SQL Ser ver 上的应用设计原封不动地用在 Oracle 上,就会导致严重的后果。这种决定 最糟糕的两个后果是: ‰ Oracle 中采用与 SQL Server 同样的数据库连接体系结构。 ‰ 开发人员在 SQL 中使用直接量(而不是绑定变量)。 这两个结果导致系统无法支持所需的用户负载(数据库服务器的可用内存会 耗尽),即使用户能登录和使用应用,应用的性能也极差。 1. 在 Oracle 中使用一个连接 目前 SQL Server 中有一种很普遍的做法,就是对想要执行的每条并发语句 都打开一个数据库连接。如果想执行 5 个查询,可能就会在 SQL Server 中看到 5 个连接。SQL Server就是这样设计的,就好像Windows 是针对多线程而不是多 进程设计的一样。在 Oracle 中,不论你想执行 5 个查询还是 500 个查询,都希 望最多打开一个连接。Oracle 就是本着这样的理念设计的。所以,SQL Server 中常用的做法在 Oracle 中却不提倡;你可能并不想维护多个数据库连接。 不过,他们确实这么做了。一个简单的Web 应用对于每个网页可能打开 5 个、 10 个、15 个甚至更多连接,这意味着,相对于服务器原本能支持的并发用户数, 现在服务器只能支持其 1/5、1/10、1/15 甚至更少的并发用户数。另外,开发人 员只是在 Windows 平台本身上运行数据库,这是一个平常的Windows XP 服务器, 而没有使用 Datacenter 版本的 Windows。这说明,Windows单进程体系结构会限 制 Oracle 数据库服务器总共只有大约 1.75 GB 的 RAM。由于每个 Oracle 连接要 同时处理多条语句,所以Oracle 连接通常比 SQL Server 连接占用更多的 RAM(不 过 Oracle 连接比 SQL Server 连接能干多了)。开发人员能否很好地扩缩应用, 很大程度上受这个硬件的限制。尽管服务器上有 8 GB 的 RAM,但是真正能用的 只有 2 GB 左右。 注意 Windows 环境中还能通过其他办法得到更多的 RAM,如利用/AWE 开关选项,但 是只有诸如Windows Server Datacenter Edition等版本的操作系统才支持这个选项, 而在这个项目中并没有使用这种版本。 针对这个问题,可能的解决方案有 3 种,无论哪一种解决方案都需要做大量 工作(另外要记住,这可是在原先以为“移植”已经结束的情况下补充的工作!)。 具体如下: ‰ 重建应用,充分考虑到这样一个事实:应用是在 Oracle 上运行,而不是在另外某个数据 库上;另外生成一个页面只使用一个连接,而不是 5~15 个连接。这是从根本上解决这个问 题的惟一方法。 ‰ 升级操作系统(这可不是小事情),使用 Windows Datacenter 版本中更大的内存模型(这 本身就非区区小事,而且还会带来相当复杂的数据库安装,需要一些间接的数据缓冲区和其 他非标准的设置)。 ‰ 把数据库从 Windows 系列操作系统移植到另外某个使用多进程的操作系统,以便数据库 使用所安装的全部 RAM(重申一遍,这个任务也不简单)。 可以看到,以上都不是轻轻松松就能办到的。不论哪种方法,你都不会毫无 芥蒂地一口应允“好的,我们下午就来办”。每种方案都相当复杂,所要解决的 问题原本在数据库“移植”阶段修正才最为容易,那是你查看和修改代码的第一 个机会。另外,如果交付生产系统之前先对“可扩缩性”做一个简单的测试,就 能在最终用户遭遇苦痛之前及时地捕捉到这些问题。 2. 使用绑定变量 如果我要写一本书谈谈如何构建不可扩缩的 Oracle 应用,肯定会把“不要使 用绑定变量”作为第一章和最后一章的标题重点强调。这是导致性能问题的一个 主要原因,也是阻碍可扩缩性的一个重要因素。Oracle 将已解析、已编译的 SQL 连同其他内容存储在共享池(shared pool)中,这是系统全局区(System Glob al Area ,SGA)中一个非常重要的共享内存结构。第 4 章将详细讨论共享池。这 个结构能完成“平滑”操作,但有一个前提,要求开发人员在大多数情况下都会 使用绑定变量。如果你确实想让 Oracle 缓慢地运行,甚至几近停顿,只要根本不 使用绑定变量就可以办到。 绑定变量(bind variable)是查询中的一个占位符。例如,要获取员工 12 3 的相应记录,可以使用以下查询: 或者,也可以将绑定变量:empno 设置为 123,并执行以下查询: 在典型的系统中,你可能只查询一次员工 123,然后不再查询这个员工。之 后,你可能会查询员工 456,然后是员工 789,如此等等。如果在查询中使用直 接量(常量),那么每个查询都将是一个全新的查询,在数据库看来以前从未见 过,必须对查询进行解析、限定(命名解析)、安全性检查、优化等。简单地讲, 就是你执行的每条不同的语句都要在执行时进行编译。 第二个查询使用了一个绑定变量:empno,变量值在查询执行时提供。这个查 询只编译一次,随后会把查询计划存储在一个共享池(库缓存)中,以便以后获 取和重用这个查询计划。以上两个查询在性能和可扩缩性方面有很大差别,甚至 可以说有天壤之别。 从前面的描述应该能清楚地看到,与重用已解析的查询计划(称为软解析, soft parse)相比,解析包含有硬编码变量的语句(称为硬解析,hard parse) 需要的时间更长,而且要消耗更多的资源。硬解析会减少系统能支持的用户数, 但程度如何不太明显。这部分取决于多耗费了多少资源,但更重要的因素是库缓 存所用的闩定(latching)机制。硬解析一个查询时,数据库会更长时间地占用 一种低级串行化设备,这称为闩(latch),有关的详细内容请参见第 6 章。这 些闩能保护 Oracle 共享内存中的数据结构不会同时被两个进程修改(否则,Or acle 最后会得到遭到破坏的数据结构),而且如果有人正在修改数据结构,则 不允许另外的人再来读取。对这些数据结构加闩的时间越长、越频繁,排队等待 闩的进程就越多,等待队列也越长。你可能开始独占珍贵的资源。有时你的计算 机显然利用不足,但是数据库中的所有应用都运行得非常慢。造成这种现象的原 因可能是有人占据着某种串行化设备,而其他等待串行化设备的人开始排队,因 此你无法全速运行。数据库中只要有一个应用表现不佳,就会严重地影响所有其 他应用的性能。如果只有一个小应用没有使用绑定变量,那么即使其他应用原本 设计得很好,能适当地将已解析的SQL 放在共享池中以备重用,但因为这个小应 用的存在,过一段时间就会从共享池中删除已存储的 SQL。这就使得这些设计得 当的应用也必须再次硬解析 SQL。真是一粒老鼠屎就能毁了一锅汤。 如果使用绑定变量,无论是谁,只要提交引用同一对象的同一个查询,都会 使用共享池中已编译的查询计划。这样你的子例程只编译一次就可以反复使用。 这样做效率很高,这也正是数据库期望你采用的做法。你使用的资源会更少(软 解析耗费的资源相当少),不仅如此,占用闩的时间也更短,而且不再那么频繁 地需要闩。这些都会改善应用的性能和可扩缩性。 要想知道使用绑定变量在性能方面会带来多大的差别,只需要运行一个非常 小的测试来看看。在这个测试中,将在一个表中插入一些记录行。我使用如下所 示的一个简单的表: 下面再创建两个非常简单的存储过程。它们都向这个表中插入数字1到10 000;不过,第一个过程使用了一条带绑定变量的 SQL 语句: 第二个过程则分别为要插入的每一行构造一条独特的 SQL 语句: 现在看来,二者之间惟一的差别,是一个过程使用了绑定变量,而另一个没 有使用。它们都使用了动态 SQL(所谓动态 SQL 是指直到运行时才确定的 SQL), 而且过程中的逻辑也是相同的。不同之处只在于是否使用了绑定变量。 下面用我开发的一个简单工具 runstats 对这两个方法详细地进行比较: 注意 关于安装 runstats 和其他工具的有关细节,请参见本书开头的“配置环境”一节。 结果清楚地显示出,从墙上时钟来看,proc2(没有使用绑定变量)插入 10 000 行记录的时间比 proc1(使用了绑定变量)要多出很多。实际上,proc2 需 要的时间是 proc1 的 3 倍多,这说明,在这种情况下,对于每个“无绑定变量” 的 INSERT,执行语句所需时间中有 2/3 仅用于解析语句! 注意 如果愿意,也可以不用 runstats,而是在 SQL*Plus 中执行命令 SET TIMING ON, 然后运行 proc1 和 proc2,这样也能进行比较。 不过,对于 proc2,还有更糟糕的呢!runstats 工具生成了一个报告,显示 出这两种方法在闩利用率方面的差别,另外还提供了诸如解析次数之类的统计结 果。这里我要求 runstats 打印出差距在 1 000 以上的比较结果(这正是 rs_sto p 调用中 1000 的含义)。查看这个信息时,可以看到各方法使用的资源存在显 著的差别: 注意 你自己测试时可能会得到稍微不同的值。如果你得到的数值和上面的一样,特别 是如果闩数都与我的测试结果完全相同,那倒是很奇怪。不过,假设你也像我一样, 也是在 Linux 平台上使用 Oracle9i Release 2,应该能看到类似的结果。不论哪个版 本,可以想见,硬解析处理每个插入所用的闩数总是要高于软解析(对于软解析, 更确切的说法应该是,只解析一次插入,然后反复执行)。还在同一台机器上,但 是如果使用 Oracle 10g Release 1 执行前面的测试,会得到以下结果:与未使用绑 定变量的方法相比,绑定变量方法执行的耗用时间是前者的 1/10,而所用的闩总数 是前者的 17%。这有两个原因,首先,10g 是一个新的版本,有一些内部算法有所 调整;另一个原因是在 10g 中,PL/SQL 采用了一种改进的方法来处理动态 SQL。 可以看到,如果使用了绑定变量(后面称为绑定变量方法),则只有 4 次硬 解析;没有使用绑定变量时(后面称为无绑定变量方法),却有不下 10 000 次 的硬解析(每次插入都会带来一次硬解析)。还可以看到,无绑定变量方法所用 的闩数是绑定变量方法的两倍之多。这是因为,要想修改这个共享结构,Oracl e 必须当心,一次只能让一个进程处理(如果两个进程或线程试图同时更新同一 个内存中的数据结构,将非常糟糕,可能会导致大量破坏)。因此,Oracle 采 用了一种闩定(latching)机制来完成串行化访问,闩(latch)是一种轻量级 锁定设备。不要被“轻量级”这个词蒙住了,作为一种串行化设备,闩一次只允 许一个进程短期地访问数据结构。闩往往被硬解析实现滥用,而遗憾的是,这正 是闩最常见的用法之一。共享池的闩和库缓存的闩就是不折不扣的闩;它们成为 人们频繁争抢的目标。这说明,想要同时硬解析语句的用户越多,性能问题就会 变得越来越严重。人们执行的解析越多,对共享池的闩竞争就越厉害,队列会排 得越长,等待的时间也越久。 注意 如果机器的处理器不止一个,在 9i 和以上版本中,共享池还可以划分为多个子池, 每个子池都由其自己的闩保护。这样即使应用没有使用绑定变量,也可以提高可扩 缩性,但是这并没有从根本上克服闩定问题。 执行无绑定变量的 SQL 语句,很像是在每个方法调用前都要编译子例程。假 设把 Java 源代码交付给客户,在调用类中的方法之前,客户必须调用Java 编译 器,编译这个类,再运行方法,然后丢掉字节码。下一次想要执行同样的方法时, 他们还要把这个过程再来一遍:先编译,再运行,然后丢掉字节码。你肯定不希 望在应用中这样做。数据库里也应该一样,绝对不要这样做。 对于这个特定的项目,可以把现有的代码改写为使用绑定变量,这是最好的 做法。改写后的代码与原先比起来,速度上有呈数量级的增长,而且系统能支持 的并发用户数也增加了几倍。不过,在时间和精力投入方面却要付出很大的代价。 并不是说使用绑定变量有多难,也不是说使用绑定变量容易出错,而只是因为开 发人员最初没有使用绑定变量的意识,所以必须回过头去,几乎把所有代码都检 查和修改一遍。如果他们从第一天起就很清楚在应用中使用绑定变量至关重要, 就不用费这么大的功夫了。 1.3.2 理解并发控制 并发控制在不同的数据库中各不相同。正是因为并发控制,才使得数据库不 同于文件系统,也使得不同的数据库彼此有所区别。你的数据库应用要在并发访 问条件下正常地工作,这一点很重要,但这也是人们时常疏于测试的一个方面。 有些技术在一切都顺序执行的情况下可能工作得很好,但是如果任务要同时进 行,这些技术的表现可能就差强人意了。如果对你的特定数据库如何实现并发控 制了解不够,就会遭遇以下结果: ‰ 破坏数据的完整性。 ‰ 随着用户数的增多,应用的运行速度减慢。 ‰ 不能很好地扩缩应用来支持大量用户。 注意我没有说“你可能……”或者“有……的风险”,而是直截了当地说:如 果没有适当的并发控制,甚至如果未能充分了解并发控制,你肯定会遇到这些情 况。如果没有正确的并发控制,就会破坏数据库的完整性,因为有些技术单独能 工作,但是在多用户情况下就不能像你预期的那样正常工作了。你的应用会比预 想的运行得慢,因为它总在等待资源。另外因为存在锁定和竞争问题,你将不能 很好地扩缩应用。随着访问资源的等待队列变得越来越长,等待的时间也会越来 越久。 这里可以打个比方,考虑一下发生在收费站的阻塞情况。如果所有汽车都以顺 序的、可预料的方式到来,一辆接着一辆,就不会出现阻塞。但是,如果多辆车 同时到达,就要排队。另外,等待时间并不是按照到达收费站的车辆数量线性增 长。达到某个临界点后,一方面要花费大量额外的时间来“管理”排队等待的人, 另一方面还要为他们提供服务(在数据库中这称为上下文切换)。 并发问题最难跟踪,就像调试多线程程序一样。在调试工具控制的人工环 境下,程序可能工作得很好,但到实际中却可怕地崩溃了。例如,在竞争条件 下,你会发现两个线程力图同时修改同一个数据结构。这种 bug 跟踪起来非常 困难,也很难修正。如果你只是独立地测试你的应用,然后部署,并交给数十 个并发用户使用,就很有可能痛苦地遭遇原先未能检测到的并发问题。 在下面两节中,我会通过两个小例子来谈谈对并发控制缺乏了解可能会破坏 你的数据,或者会影响应用的性能和可扩缩性。 1. 实现锁定 数据库使用锁(lock)来保证任何给定时刻最多只有一个事务在修改给定的 一段数据。实质上讲,正是锁机制才使并发控制成为可能。例如,如果没有某种 锁定模型来阻止对同一行的并发更新,数据库就不可能提供多用户访问。不过, 如果滥用或者使用不当,锁反倒会阻碍并发性。如果你或数据库本身不必要地对 数据锁定,能并发地完成操作的人数就会减少。因此,要理解什么是锁定,你的 数据库中锁定是怎样工作的,这对于开发可扩缩的、正确的应用至关重要。 还有一点很重要,你要知道每个数据库会以不同的方式实现锁定。有些数据库 可能有页级锁,另外一些则有行级锁;有些实现会把行级锁升级为页级锁,另外 一些则不然;有些使用读锁,另外一些不使用;有些通过锁定实现串行化事务, 另外一些则通过数据的读一致视图来实现(没有锁)。如果你不清楚这些微小的 差别,它们就会逐步膨胀为严重的性能问题,甚至演变成致命的 bug。 以下是对 Oracle 锁定策略的总结: ‰ Oracle 只在修改时才对数据加行级锁。正常情况下不会升级到块级锁或表级锁(不过两 段提交期间的一段很短的时间内除外,这是一个不常见的操作)。 ‰ 如果只是读数据,Oracle 绝不会对数据锁定。不会因为简单的读操作在数据行上锁定。 ‰ 写入器(writer)不会阻塞读取器(reader)。换种说法:读(read)不会被写(write)阻 塞。这一点几乎与其他所有数据库都不一样。在其他数据库中,读往往会被写阻塞。尽管听 上去这个特性似乎很不错(一般情况下确实如此),但是,如果你没有充分理解这个思想, 而且想通过应用逻辑对应用施加完整性约束,就极有可能做得不对。第 7 章介绍并发控制时 还会更详细地讨论这个内容。 ‰ 写入器想写某行数据,但另一个写入器已经锁定了这行数据,此时该写入器才会被阻塞。 读取器绝对不会阻塞写入器。 开发应用时必须考虑到这些因素,而且还要认识到这个策略是 Oracle 所独有 的,每个数据库实现锁定的方法都存在细微的差别。即使你在应用中使用最通用 的 SQL,由于各数据库开发商采用的锁定和并发控制模型不同,你的应用也可能 有不同的表现。倘若开发人员不了解自己的数据库如何处理并发性,肯定会遇到 数据完整性问题。(开发人员从另外某种数据库转向 Oracle,或者从 Oracle 转 向其他数据库时,如果没有考虑在应用中采用不同的并发机制,这种情况就尤为 常见。) 2. 防止丢失更新 Oracle 的无阻塞方法有一个副作用,如果确实想保证一次最多只有一个用户 访问一行数据,开发人员就得自己做些工作。 考虑下面这个例子。一位开发人员向我展示了他刚开发的一个资源调度程 序(可以用来调度会议室、投影仪等资源),这个程序正在部署当中。应用中 实现了这样一个业务规则:在给定的任何时间段都不能将一种资源分配给多个 人。也就是说,应用中包含了实现这个业务规则的代码,它会明确地检查此前 这个时间片没有分配给其他用户(至少,这个开发人员认为是这样)。这段代 码先查询 SCHEDULES 表,如果不存在与该时间片重叠的记录行(该时间片尚未 分配),则插入新行。所以,开发人员主要考虑两个表: 在分配资源(如预订房间)之前,应用将查询: 看上去很简单,也很安全(在开发人员看来):如果得到的计数为 0,这个 房间就是你的了。如果返回的数非 0,那在此期间你就不能预订这个房间。了解 他的逻辑后,我建立了一个非常简单的测试,来展示这个应用运行时可能出现的 一个错误,这个错误极难跟踪,而且事后也很难诊断。有人甚至以为这必定是一 个数据库 bug。 我所做的其实很简单,就是让另外一个人使用这个开发人员旁边的一台机 器,两个人都浏览同一个屏幕,然后一起数到 3 时,两人都单击 Go 按钮,尽量 同时预订同一个房间,一个人想预订下午3:00 到下午 4:00 这个时段,另一个人 要预订下午 3:30 到下午 4:00 这个时段。结果两个人都预订成功。这个逻辑独立 执行时原本能很好地工作,但到多用户环境中就不行了。为什么会出现这个问 题?部分原因就在于 Oracle 的非阻塞读。这两个会话都不会阻塞对方,它们只 是运行查询,然后完成调度房间的逻辑。两个会话都通过运行查询来查找是否已 经有预订,尽管另一个会话可能已经开始修改SCHEDULES 表,但查询看不到这些 修改(所做的修改在提交之前对其他会话来说是不可见的,而等到提交时已为时 过晚)。由于这两个会话并没有试图修改 SCHEDULES 表中的同一行,所以它们不 会相互阻塞。由此说来,这个应用不能像预期的那样保证前面提到的业务规则。 开发人员需要一种方法使得这个业务规则在多用户环境下也能得到保证,也 就是要确保一次只有一个人预订一种给定的资源。在这种情况下,解决方案就是 加入他自己的一些串行化机制。他的做法是,在对 SCHEDULES 表进行修改之前, 先对 RESOURCES 表中的父行锁定。这样一来,SCHEDULES 表中针对给定 RESOURC E_NAME 值的所有修改都必须依次按顺序进行,一次只能执行一个修改。也就是 说,要预订资源 X 一段时间,就要锁定 RESOURCES 表中对应 X 的那一行,然后修 改 SCHEDULES 表。所以,除了前面的 count(*)外,开发人员首先需要完成以下 查询: 这里,他在调度资源之前先锁定了资源(这里指房间),换句话说,就是在 SCHEDULES 表中查询该资源的预订情况之前先锁定资源。通过锁定所要调度的资 源,开发人员可以确保别人不会同时修改对这个资源的调度。其他人都必须等待, 直到他提交了事务为止,此时就能看到他所做的调度。这样就杜绝了调度重叠的 可能性。 开发人员必须了解到,在多用户环境中,他们必须不时地采用多线程编程中 使用的一些技术。在这里,FOR UPDATE 子句的作用就像是一个信号量(semapho re),只允许串行访问 RESOURCES 表中特定的行,这样就能确保不会出现两个人 同时调度的情况。我建议把这个逻辑实现为一个事务 API,也就是说,把所有逻 辑都打包进一个存储过程中,只允许应用通过这个 API 修改数据。代码如下: 首先在 RESOURCES 表中锁定我们想调度的那个资源的相应行。如果别人已经 锁定了这一行,我们就会阻塞并等待 : 既然我们已经有了锁,那么只有我们能在这个 SCHEDULES 表中插入对应此资 源名的调度,所以如下查看这个表是安全的: 如果能运行到这里而没有发生错误,就可以安全地在 SCHEDULES 表中插入预 订资源的相应记录行,而不用担心出现重叠: 这个解决方案仍是高度并发的,因为可能有数以千计要预订的资源。这里的 做法是,确保任何时刻只能有一个人修改资源。这是一种很少见的情况,在此要 对并不会真正更新的数据手动锁定。我们要知道哪些情况下需要这样做,还要知 道哪些情况下不需要这样做(稍后会给出这样一个例子),这同样很重要。另外, 如果别人只是读取数据,就不会锁定资源不让他们读(但在其他数据库中可能不 是这样),所以这种解决方案可以很好地扩缩。 如果你想把应用从一个数据库移植到另一个数据库,这一节讨论的问题就有很大 的影响(本章后面还会再谈这个内容),而且可能会一再地把人“绊倒”。例如, 如果你有使用另外某些数据库的经验,其中写入器会阻塞读取器,而且读取器也会 阻塞写入器,你可能就会有成见,过分依赖这一点来避免数据完整性问题。一种解 决办法是干脆不要并发,在许多非 Oracle 数据库中就是这样做的。但在 Oracle 中 则是并发规则至上,因此,你必须知道可能会发生不同的情况(或者遭受不同的后 果)。 注意 第 7 章还会再次谈到这个例子。以上代码有一个前提,即假设事务隔离级别是 RE AD COMMITTED。如果事务隔离级别是 SERIALIZABLE,这个逻辑将无法正常工 作。倘若现在就详细介绍这两种模式的区别,这一章就会变得过于复杂,所以这个 内容以后再讨论。 99%的情况下,锁定是完全透明的,无需你来操心。但还有另外的 1%,你必 须清楚哪些情况下需要自己考虑锁定。对于这个问题,并没有非黑即白的直接结 论,无法简单地罗列出“如果你要这样做,就应该这样做”之类的条条框框。关 键是要了解应用在多用户环境中有何表现,另外在你的数据库中表现如何。 第 7 章会更深入地讨论这个内容,你会进一步了解本节介绍的这种完整性约 束,有些情况下,一个表中的多行必须遵循某个规则,或者两个表或多个表之间 必须保证某个规则(如引用完整性约束),一定要特别注意这些情况,而且这些 情况也最有可能需要采用手动锁定或者另外某种技术来确保多用户环境下的完 整性。 1.3.3 多版本 这个主题与并发控制的关系非常紧密,因为这正是 Oracle 并发控制机制的 基础,Oracle 采用了一种多版本、读一致(read-consistent)的并发模型。再 次说明,我们将在第 7 章更详细地介绍有关的技术。不过,实质上讲,Oracle 利用这种机制提供了以下特性: ‰ 读一致查询:对于一个时间点(point in time),查询会产生一致的结果。 ‰ 非阻塞查询:查询不会被写入器阻塞,但在其他数据库中可能不是这样。 Oracle 数据库中有两个非常重要的概念。多版本(multi-versioning)一词 实质上指 Oracle 能够从数据库同时物化多个版本的数据。如果你理解了多版本 如何工作,就会知道能从数据库得到什么。在进一步深入讨论 Oracle 如何实现 多版本之前,下面用我认为最简单的一个方法来演示 Oracle 中的多版本: 在前面的例子中,我创建了一个测试表 T,并把 ALL_USERS 表的一些数据加 载到这个表中。然后在这个表上打开一个游标。在此没有从该游标获取数据,只 是打开游标而已。 注意 要记住,Oracle 并不“回答”这个查询。打开游标时,Oracle 不复制任何数据,你 可以想想看,即使一个表有十亿条记录,是不是也能很快就打开游标?没错,游标 会立即打开,它会边行进边回答查询。换句话说,只是在你获取数据时它才从表中 读数据。 在同一个会话中(或者也可以在另一个会话中;这同样能很好地工作),再 从该表删除所有数据。甚至用COMMIT 提交了删除所做的工作。记录行都没有了, 但是真的没有了吗?实际上,还是可以通过游标获取到数据。OPEN 命令返回的 结果集在打开的那一刻(时间点)就已经确定。打开时,我们根本没有碰过表中 的任何数据块,但答案已经是铁板钉钉的了。获取数据之前,我们无法知道答案 会是什么;不过,从游标角度看,结果则是固定不变的。打开游标时,并非 Ora cle 将所有数据复制到另外某个位置;实际上是 DELETE 命令为我们把数据保留 下来,把它放在一个称为 undo 段(undo segment)的数据区,这个数据区也称 为回滚段(rollback segment)。 读一致性(read-consistency)和多版本就是这么回事。如果你不了解 Ora cle 的多版本机制是怎样工作的,不清楚这意味着什么,你就不可能充分利用 O racle,也不可能在 Oracle 上开发出正确的应用(也就是说,能确保数据完整性 的应用)。 1. 多版本和闪回 过去,Oracle 总是基于查询的某个时间点来做决定(从这个时间点开始查询 是一致的)。也就是说,Oracle 会保证打开的结果集肯定是以下两个时间点之 一的当前结果集: ‰ 游标打开时的时间点。这是 READ COMMITTED 隔离模式的默认行为,该模式是默 认的事务模式(第 7 章将介绍 READ COMMITTED、READ ONLY 和 SERIALIZABLE 事务级别之间的差别)。 ‰ 查询所属事务开始的时间点。这是 READ ONLY 和 SERIALIZABLE 隔离级别中的默认 行为。 不过,从 Oracle9i 开始,情况要灵活得多。实际上,我们可以指示 Oracle 提供任何指定时间的查询结果(对于回放的时间长度有一些合理的限制;当然, 这要由你的 DBA 来控制),这里使用了一种称为闪回查询(flashback query) 的特性。 请考虑以下例子。首先得到一个 SCN,这是指系统修改号(System Change Number)或系统提交号(System Commit Number);这两个术语可互换使用。S CN 是 Oracle 的内部时钟:每次发生提交时,这个时钟就会向上滴答(递增)。 实际上也可以使用日期或时间戳,不过这里 SCN 很容易得到,而且相当准确: 现在可以让 Oracle 提供 SCN 值所表示时间点的数据。以后再查询Oracle 时, 就能看看这一时刻表中的内容。首先来看 EMP 表中现在有什么: 下面把这些信息都删除,并验证数据是否确实“没有了”: 此外,使用闪回查询(即 AS OF SCN 或 AS OF TIMESTAMP 子句),可以让 O racle 告诉我们 SCN 值为 33295399 的时间点上表中有什么: 不仅如此,这个功能还能跨事务边界。我们甚至可以在同一个查询中得到同 一个对象在“两个时间点”上的结果!因此可以做些有意思的事情: 如果你使用的是 Oracle 10g 及以上版本,就有一个“闪回”(flashback) 命令,它使用了这种底层多版本技术,可以把对象返回到以前某个时间点的状态。 在这个例子中,可以将 EMP 表放回到删除信息前的那个时间点: 注意 如果你得到一个错误“ORA-08189: cannot flashback the table because row move ment is not enabled using the FLASHBACK command”(ORA-08189:无法闪回 表,因为不支持使用 FLASHBACK 命令完成行移动),就必须先执行一个命令: A LTER TABLE EMP ENABLE ROW MOVEMENT。这个命令的作用是,允许 Ora cle 修改分配给行的 rowid。在 Oracle 中,插入一行时就会为它分配一个 rowid,而 且这一行永远拥有这个 rowid。闪回表处理会对 EMP 完成 DELETE,并且重新插入 行,这样就会为这些行分配一个新的 rowid。要支持闪回就必须允许 Oracle 执行这 个操作。 2. 读一致性和非阻塞读 下面来看多版本、读一致查询以及非阻塞读的含义。如果你不熟悉多版本, 下面的代码看起来可能有些奇怪。为简单起见,这里假设我们读取的表在每个数 据库块(数据库中最小的存储单元)中只存放一行,而且这个例子要全面扫描这 个表。 我们查询的表是一个简单的 ACCOUNTS 表。其中包含了一家银行的账户余额。 其结构很简单: 在实际中,ACCOUNTS 表中可能有上百万行记录,但是为了力求简单,这里只 考虑一个仅有 4 行的表(第 7 章还会更详细地分析这个例子),如表 1-1 所示。 表 1-1 ACCOUNTS 表的内容 行 账 号 账户余额 1 123 $500.00 2 234 $250.00 3 345 $400.00 4 456 $100.00 我们可能想运行一个日报表,了解银行里有多少钱。这是一个非常简单的查 询: 当然,这个例子的答案很明显:$1 250.00。不过,如果我们现在读了第 1 行,准备读第 2 行和第 3 行时,一台自动柜员机(ATM)针对这个表发生了一个 事务,将$400.00 从账户 123 转到了账户 456,又会怎么样呢?查询会计算出第 4 行的余额为$500.00,最后就得到了$1 650.00,是这样吗?当然,应该避免这 种情况,因为这是不对的,任何时刻账户余额列中的实际总额都不是这个数。读 一致性就是 Oracle 为避免发生这种情况所采用的办法,你要了解,与几乎所有 的其他数据库相比,Oracle 采用的方法有什么不同。 在几乎所有的其他数据库中,如果想得到“一致”和“正确”的查询答案, 就必须在计算总额时锁定整个表,或者在读取记录行时对其锁定。这样一来,获 取结果时就可以防止别人再做修改。如果提前锁定表,就会得到查询开始时数据 库中的结果。如果在读取数据时锁定(这通常称为共享读锁(shared read loc k),可以防止更新,但不妨碍读取器访问数据),就会得到查询结束时数据库 中的结果。这两种方法都会大大影响并发性。由于存在表锁,查询期间会阻止对 整个表进行更新(对于一个仅有 4 行的表,这可能只是很短的一段时间,但是对 于有上百万行记录的表,可能就是几分钟之多)。“边读边锁定”的办法也有问 题,不允许对已经读取和已经处理过的数据再做更新,实际上这会导致查询与其 他更新之间产生死锁。 我曾经说过,如果你没有理解多版本的概念,就无法充分利用 Oracle。下面告 诉你一个原因。Oracle会利用多版本来得到结果,也就是查询开始时那个时间点的 结果,然后完成查询,而不做任何锁定(转账事务更新第 1 行和第 4 行时,这些行 会对其他写入器锁定,但不会对读取器锁定,如这里的 SELECT SUM...查询)。实 际上,Oracle 根本没有“共享读”锁(这是其他数据库中一种常用的锁),因为这 里不需要。对于可能妨碍并发性的一切因素,只要能去掉的,Oracle 都已经去掉了。 我见过这样一些实际案例,开发人员没有很好地理解 Oracle 的多版本功能, 他编写的查询报告将整个系统紧紧地锁起来。之所以会这样,主要是因为开发人 员想从查询得到读一致的(即正确的)结果。这个开发人员以前用过其他一些数 据库,在这些数据库中,要做到这一点都需要对表锁定,或者使用一个SELECT ... WITH HOLDLOCK(这是 SQL Server 中的一种锁定机制,可以边读取边以共享模 式对行锁定)。所以开发人员想在运行报告前先对表锁定,或者使用SELECT ... FOR UPDATE(这是 Oracle 中与 holdlock 最接近的命令)。这就导致系统实质 上会停止处理事务,而这完全没有必要。 那么,如果 Oracle 读取时不对任何数据锁定,那它又怎么能得到正确、一致的 答案($1 250.00)呢?换句话说,如何保证得到正确的答案同时又不降低并发性? 秘密就在于 Oracle 使用的事务机制。只要你修改数据,Oracle 就会创建撤销(un do)条目。这些 undo 条目写至 undo 段(撤销段,undo segment)。如果事务失败, 需要撤销,Oracle 就会从这个回滚段读取“之前”的映像,并恢复数据。除了使用 回滚段数据撤销事务外,Oracle 还会用它撤销读取块时对块所做的修改,使之恢复 到查询开始前的时间点。这样就能摆脱锁来得到一致、正确的答案,而无需你自己 对任何数据锁定。 所以,对我们这个例子来说,Oracle 得到的答案如表 1-2 所示。 表 1-2 实际的多版本例子 时 间 查 询 转账事务 T1 T2 读第 1 行;到目前为止 sum = $500 更新第 1 行;对第 1 行加一个排他锁(也称独占锁, exclusive lock),阻止其他更新 第 1 行现在有$100 T3 读第 2 行;到目前为止 sum = $750 T4 读第 3 行;到目前为止 sum = $1 150 T5 更新第 4 行;对第 4 行加一个排他锁,阻止其他更 新(但不阻止读操作)。第 4 行现在有$500 T6 读第 4 行,发现第 4 行已修改。这会 将块回滚到 T1 时刻的状态。查询从这个 块读到值$100 T7 得到答案$1 250 在 T6 时,Oracle 有效地“摆脱”了事务加在第 4 行上的锁。非阻塞读是这 样实现的:Oracle 只看数据是否改变,它并不关心数据当前是否锁定(锁定意 味着数据已经改变)。Oracle 只是从回滚段中取回原来的值,并继续处理下一 个数据块。 下一个例子也能很好地展示多版本。在数据库中,可以得到同一个信息处于 不同时间点的多个版本。Oracle 能充分使用不同时间点的数据快照来提供读一 致查询和非阻塞查询。 数据的读一致视图总是在 SQL 语句级执行。SQL 语句的结果对于查询开始的 时间点来说是一致的。正是因为这一点,所以下面的语句可以插入可预知的数据 集: SELECT * FROM T 的结果在查询开始执行时就已经确定了。这个SELECT 并不 看 INSERT 生成的任何新数据。倘若真的能看到新插入的数据,这条语句就会陷 入一个无限循环。如果INSERT 在 T 中生成了更多的记录行,而SELECT 也随之能 “看到”这些新插入的行,前面的代码就会建立数目未知的记录行。如果表 T 刚开始有 10 行,等结束时 T 中可能就会有 20、21、23 或无限行记录。这完全不 可预测。Oracle 为所有语句都提供了这种读一致性,所以如下的 INSERT 也是可 预知的: 这个 INSERT 语句得到了 T 的一个读一致视图。它看不到自己刚刚插入的行, 而只是插入 INSERT 操作刚开始时表中已有的记录行。许多数据库甚至不允许前 面的这种递归语句,因为它们不知道到底可能插入多少行。 所以,如果你用惯了其他数据库,只熟悉这些数据库中处理查询一致性和并发 性的方法,或者你根本没有接触过这些概念(也就是说,你根本没有使用数据库 的经验),现在应该知道,理解 Oracle 的做法对你来说有何等重要的意义。要想 最大限度地发挥 Oracle 的潜能,以及为了实现正确的代码,你必须了解 Oracle 中的这些问题是怎么解决的(而不是其他数据库中是如何实现的)。 1.3.4 数据库独立性 至此,你可能想到这一节要讲什么了。我提到了其他的数据库,也谈到各个 数据库中会以不同的方式实现特性。除了一些只读应用外,我的观点是:要构建 一个完全数据库独立的应用,而且是高度可扩缩的应用,是极其困难的。实际上, 这几乎不可能,除非你真正了解每个数据库具体如何工作。另外,如果你清楚每 个数据库工作的具体细节,就会知道,数据库独立性可能并不是你真正想要的(这 个说法有点绕!)。 例如,再来看最早提到的资源调度例子(增加 FOR UPDATE 子句之前)。假 设在另一个数据库上开发这个应用,这个数据库有着与Oracle 完全不同的锁定/ 并发模型。我想说的是,如果把应用从一个数据库移植到另一个数据库,就必须 验证它在完全不同的环境下还能正常地工作,而且为此我们要做大幅修改! 假设把这个资源调度应用部署在这样一个数据库上,它采用了阻塞读机制 (读会被写阻塞)。现在业务规则通过一个数据库触发器实现(在 INSERT 之后, 但在事务提交之前,我们要验证表中对应特定时间片的记录只有一行,也就是刚 插入的记录)。在阻塞读系统中,由于有这种新插入的数据,所以表的插入要串 行完成。第一个人插入他(她)的请求,要在星期五的下午 2:00 到下午 3:00 预订“房间 A”,然后运行一个查询查看有没有重叠的预订。下一个人想插入一 个重叠的请求,查找重叠情况时,这个请求会被阻塞(它发现有新插入的数据, 但要等待直到这些数据确实可以读取)。在这个采用阻塞读机制的数据库中,我 们的应用显然可以正常工作(不过如果两个人都插入自己的行,然后试图读对方 的数据,就有可能得到一个死锁,这个概念将在第6 章讨论),但不能并发工作, 因为我们是一个接一个地检查是否存在重叠的资源分配。 如果把这个应用移植到 Oracle,并简单地认为它也能同样地工作,结果可能 让人震惊。由于 Oracle 会在行级锁定,并提供了非阻塞读,所以看上去一切都 乱七八糟。如前所示,必须使用 FOR UPDATE 子句来完成串行访问。如果没有这 个子句,两个用户就可能同时调度同一个资源。如果不了解所用数据库在多用户 环境中如何工作,就会导致这样的直接后果。 将应用从数据库 A 移植到数据库 B 时,我时常遇到这种问题:应用在数据库 A 上原本无懈可击,到了数据库 B 上却不能工作,或者表现得很离奇。看到这种 情况,我们的第一个想法往往是,数据库 B 是一个“不好的”数据库。而真正的 原因其实是数据库 B 的工作方式完全不同。没有哪个数据库是错的或“不好的”, 它们只是有所不同而已。应当了解并理解它们如何工作,这对于处理这些问题有 很大的帮助。将应用从 Oracle 移植到 SQL Server 时,也会暴露 SQL Server 的 阻塞读和死锁问题,换句话说,不论从哪个方向移植都可能存在问题。 例如,有人请我帮忙将一些 Transact-SQL(T-SQL,SQL Server 的存储过程语 言)转换为 PL/SQL。做这个转换的开发人员一直在抱怨 Oracle 中 SQL 查询返回 的结果是“错的”。查询如下所示: 这个查询的目标是:在 T 表中,如果不满足某个条件,则找出 x 为 NULL 的 所有行;如果满足某个条件,就找出 x 等于某个特定值的所有行。 开发人员抱怨说,在 Oracle 中,如果 L_SOME_VARIABLE 未设置为一个特定 的值(仍为 NULL),这个查询居然不返回任何数据。但是在 Sybase 或 SQL Ser ver 中不是这样的,查询会找到将x 设置为 NULL 值的所有行。从Sybase 或 SQL Server 到 Oracle 的转换中,几乎都能发现这个问题。SQL 采用一种三值逻辑来 操作,Oracle 则是按 ANSI SQL 的要求来实现 NULL 值。基于这些规则的要求,x 与 NULL 的比较结果既不为 true 也不为 false,也就是说,实际上,它是未知的 (unknown)。从以下代码可以看出我的意思: 第一次看到这些结果可能会被搞糊涂。这说明,在 Oracle 中,NULL 与 NULL 既不相等,也不完全不相等。默认情况下,SQL Server 则不是这样处理;在 SQ L Server 和 Sybase 中,NULL 就等于 NULL。不能说 Oracle 的 SQL 处理是错的, 也不能说 Sybase 或 SQL Server 的处理不对,它们只是方式不同罢了。实际上, 所有这些数据库都符合 ANSI,但是它们的具体做法还是有差异。有许多二义性、 向后兼容性等问题需要解决。例如, SQL Server也支持 ANSI 方法的 NULL 比较, 但这不是默认的方式(如果改成 ANSI 方法的 NULL 比较,基于 SQL Server 构建 的数千个遗留应用就会出问题)。 在这种情况下,一种解决方案是编写以下查询: 不过,这又会带来另一个问题。在 SQL Server 中,这个查询会使用 x 上的 索引。Oracle中却不会这样,因为B*树索引不会对一个完全为 NULL 的项加索引 (索引技术将在第 12 章介绍)。因此,如果需要查找 NULL 值,B*树索引就没有 什么用处。 这里,为了尽量减少对代码的影响,我们的做法是赋给 x 某个值,不过这个 值并没有实际意义。在此,根据定义可知,x 的正常值是正数,所以可以选择 –1。这样一来,查询就变成: 由此创建一个基于函数的索引: 只需做最少的修改,就能在 Oracle 中得到与 SQL Server 同样的结果。从这 个例子可以总结出以下几个要点: ‰ 数据库是不同的。在一个数据库上取得的经验也许可以部分应用于另一个数据库,但是 你必须有心理准备,二者之间可能存在一些基本差别,可能还有一些细微的差别。 ‰ 细微的差别(如对 NULL 的处理)与基本差别(如并发控制机制)可能有同样显著的 影响。 ‰ 应当了解数据库,知道它是如何工作的,它的特性如何实现,这是解决这些问题的惟一 途径。 常有开发人员问我如何在数据库中做某件特定的事情(通常这样的问题一天 不止一个),例如“如何在一个存储过程中创建临时表?”对于这些问题,我并 不直接回答,而是反过来问他们“你为什么想那么做?”给我的回答常常是:“我 们在 SQL Server 中就是用存储过程创建临时表,所以在Oracle 中也要这么做。” 这不出我所料,所以我的回答很简单:“你根本不是想在 Oracle 中用存储过程 创建临时表,你只是以为自己想那么做。”实际上,在 Oracle 中这样做是很不 好的。在 Oracle 中,如果在存储过程中创建表,你会发现存在以下问题: ‰ DDL 操作会阻碍可扩缩性。 ‰ DDL 操作的速度往往不快。 ‰ DDL 操作会提交事务。 ‰ 必须在所有存储过程中使用动态 SQL 而不是静态 SQL 来访问这个表。 ‰ PL/SQL 的动态 SQL 没有静态 SQL 速度快,或者说没有静态 SQL 优化。 关键是,即使真的需要在 Oracle 中创建临时表,你也不愿意像在 SQL Ser ver 中那样在过程中创建临时表。你希望在 Oracle 中能以最佳方式工作。反过 来也一样,在 Oracle 中,你会为所有用户创建一个表来共享临时数据;但是从 Oracle 移植到 SQL Server 时,可能不希望这样做,这会影响 SQL Server 的可 扩缩性和并发性。所有数据库创建得都不一样,它们存在很大的差异。 1. 标准的影响 如果所有数据库都符合 SQL99,那它们肯定一样。至少我们经常做这个假设。 在这一节中,我将揭开它的神秘面纱。 SQL99 是数据库的一个 ANSI/ISO 标准。这个标准的前身是 SQL92 ANSI/ISO 标准,而 SQL92 之前还有一个 SQL89 ANSI/ISO 标准。它定义了一种语言(SQL) 以及数据库的行为(事务、隔离级别等)。你知道许多商业数据库至少在某种程 度上是符合 SQL99 的吗?不过,这对于查询和应用的可移植性没有多大的意义, 这一点你也清楚吗? SQL92 标准有 4 个层次: ‰ 入门级(Entry level)。这是大多数开发商符合的级别。这一级只是对前一个标准 SQL 89 稍做修改。所有数据库开发商都不会有更高的级别,实际上,美国国家标准和技术协会 NIST(National Institute of Standards and Technology,这是一家专门检验 SQL 合规性的 机构)除了验证入门级外,甚至不做其他的验证。Oracle 7.0 于 1993 年通过了 NIST 的 S QL92 入门级合规性验证,那时我也是小组中的一个成员。如果一个数据库符合入门级, 它的特性集则是 Oracle 7.0 的一个功能子集。 ‰ 过渡级。这一级在特性集方面大致介于入门级和中间级之间。 ‰ 中间级。这一级增加了许多特性,包括(以下所列并不完整): „ 动态 SQL „ 级联 DELETE 以保证引用完整性 „ DATE 和 TIME 数据类型 „ 域 „ 变长字符串 „ CASE 表达式 „ 数据类型之间的 CAST 函数 ‰ 完备级。增加了以下特性(同样,这个列表也不完整): „ 连接管理 „ BIT 串数据类型 „ 可延迟的完整性约束 „ FROM 子句中的导出表 „ CHECK 子句中的子查询 „ 临时表 入门级标准不包括诸如外联结(outer join)、新的内联结(inner join) 语法等特性。过渡级则指定了外联结语法和内联结语法。中间级增加了更多的特 性,当然,完备级就是SQL92 全部。有关SQL92 的大多数书都没有区别这些级别, 这就会带来混淆。这些书只是说明了一个完整实现SQL92 的理论数据库会是什么 样子。所以无论你拿起哪一本书,都无法将书中所学直接应用到任何SQL92 数据 库上。关键是,SQL92 最多只达到入门级,如果你使用了中间级或更高级里的特 性,就存在无法“移植”应用的风险。 SQL99 只定义了两级一致性:核心(core)一致性和增强(enhanced)一致 性。SQL99 力图远远超越传统的“SQL”,并引入了一些对象—关系构造(数组、 集合等)。它包括 SQL MM(多媒体,multimedia)类型、对象—关系类型等。 还没有哪个开发商的数据库经认证符合 SQL99 核心级或增强级,实际上,据我 所知,甚至没有哪个开发商声称他们的产品完全达到了某级一致性。 对于不同的数据库来说,SQL 语法可能存在差异,实现有所不同,同一个查 询在不同数据库中的性能也不一样,不仅如此,还存在并发控制、隔离级别、查 询一致性等问题。我们将在第7 章详细讨论这些问题,并介绍不同数据库的差异 对你会有什么影响。 SQL92/SQL99 试图对事务应如何工作以及隔离级别如何实现给出一个明确的 定义,但最终,不同的数据库还是有不同的结果。这都是具体实现所致。在一个 数据库中,某个应用可能会死锁并完全阻塞。但在另一个数据库中,同样是这个 应用,这些问题却有可能不会发生,应用能平稳地运行。在一个数据库中,你可 能利用了阻塞(物理串行化),但在另一个数据库上部署时,由于这个数据库不 会阻塞,你就会得到错误的答案。要将一个应用部署在另一个数据库上,需要花 费大量的精力,付出艰辛的劳动,即使你 100%地遵循标准也不例外。 关键是,不要害怕使用开发商特有的特性,毕竟,你为这些特性花了钱。每 个数据库都有自己的一套“技巧”,在每个数据库中总能找到一种完成操作的好 办法。要使用最适合当前数据库的做法,移植到其他数据库时再重新实现。要使 用合适的编程技术,从而与这些修改隔离,我把这称为防御式编程(defensive programming)。 2. 防御式编程 我推崇采用防御式编程技术来构建真正可移植的数据库应用,实际上,编写 操作系统可移植的应用时也采用了这种技术。防御式编程的目标是充分利用可用 的工具,但是确保能够根据具体情况逐一修改实现。 可以对照来看,Oracle 是一个可移植的应用。它能在许多操作系统上运行。 不过,在 Windows 上,它就以 Windows 方式运行,使用线程和其他 Windows 特有 的工具。在 UNIX 上,Oracle 则作为一个多进程服务器运行,使用进程来完成 W indows 上线程完成的工作,也就是采用UNIX 的方式运行。两个平台都提供了“核 心 Oracle”功能,但是在底层却以完全不同的方式来实现。如果你的数据库应 用要在多个数据库上运行,道理也是一样的。 例如,许多数据库应用都有一个功能,即为每一行生成一个惟一的键。插入 行时,系统应自动生成一个键。为此,Oracle 实现了一个名为 SEQUENCE 的数据 库对象。Informix 有一个 SERIAL 数据类型。Sybase 和 SQL Server 有一个 IDEN TITY 类型。每个数据库都有一个解决办法。不过,不论从做法上讲,还是从输 出来看,各个数据库的方法都有所不同。所以,有见识的开发人员有两条路可走: ‰ 开发一个完全独立于数据库的方法来生成惟一的键。 ‰ 在各个数据库中实现键时,提供不同的实现,并使用不同的技术。 从理论上讲,第一种方法的好处是从一个数据库转向另一个数据库时无需执 行任何修改。我把它称为“理论上” 的好处,这是因为这种实现实在太庞大了, 所以这种方案根本不可行。要开发一个完全独立于数据库的进程,你必须创建如 下所示的一个表: 然后,为了得到一个新的键,必须执行以下代码: 看上去很简单,但是有以下结果(注意结果不止一项): ‰ 一次只能有一个用户处理事务行。需要更新这一行来递增计数器,这会导致程序必须串 行完成这个操作。在最好的情况下,一次只有一个人生成一个新的键值。 ‰ 在 Oracle 中(其他数据库中的行为可能有所不同),倘若隔离级别为 SERIALIZABLE,除 第一个用户外,试图并发完成此操作的其他用户都会接到这样一个错误:“ORA-08177: can't serialize access for this transaction”(ORA-08177:无法串行访问这个事务)。 例如,使用一个可串行化的事务(在 J2EE 环境中比较常见,其中许多工具 都自动将 SERIALIZABLE 用作默认的隔离模式,但开发人员通常并不知道),你 会观察到以下行为。注意 SQL 提示符(使用 SET SQLPROMPT SQL*Plus 命令)包 含了活动会话的有关信息: 下面,再到另一个 SQL*Plus 会话完成同样的操作,并发地请求惟一的 ID: 此时它会阻塞,因为一次只有一个事务可以更新这一行。这展示了第一种可 能的结果,即这个会话会阻塞,并等待该行提交。但是由于我们使用的是 Oracl e,而且隔离级别是SERIALIZABLE,提交第一个会话的事务时会观察到以下行为: 第二个会话会立即显示以下错误: 所以,尽管这个逻辑原本想做到独立于数据库,但它根本不是数据库独立的。 取决于隔离级别,这个逻辑甚至在单个数据库中都无法可靠地完成,更不用说跨 数据库了!有时我们会阻塞并等待,但有时却会得到一条错误消息。说得简单些, 无论是哪种情况(等待很长时间,或者等待很长时间后得到一个错误),都至少 会让最终用户不高兴。 实际上,我们的事务比上面所列的要大得多,所以问题也更为复杂。实际的 事务中包含多条语句,上例中的UPDATE 和 SELECT 只是其中的两条而已。我们还 要用刚生成的这个键向表中插入行,并完成这个事务所需的其他工作。这种串行 化对于应用的扩缩是一个很大的制约因素。如果把这个技术用在处理订单的网站 上,而且使用这种方式来生成订单号,可以想想看可能带来的后果。这样一来, 多用户并发性就会成为泡影,我们不得不按顺序做所有事情。 对于这个问题,正确的解决方法是针对各个数据库使用最合适的代码。在 O racle 中,代码应该如下(假设表 T 需要所生成的主键): 其效果是为所插入的每一行自动地(而且透明地)指定一个惟一键。还有 一种性能更优的方法: 也就是说,完全没有触发器的开销(这是我的首选方法)。 在第一个例子中,我们特意使用了各个数据库的特性来生成一个非阻塞、高 度并发的惟一键,而且未对应用代码带来任何真正的改动,因为在这个例子中所 有逻辑都包含在DDL中。 提示 在其他数据库中也可以使用其内置的特性或者生成惟一的数来达到同样的效果。C REATE TABLE 语法可能不同,但是最终结果是一样的。 理解了每个数据库会以不同的方式实现特性,再来看一个支持可移植性的防御 式编程的例子,这就是必要时将数据库访问分层。例如,假设你在使用 JDBC 进行 编程,如果你用的都是直接的 SQL(SELECT、INSERT、UPDATE 和 DELETE),可能 不需要抽象层。你完全可以在应用程序中直接编写 SQL,前提是只能用各个数据 库都支持的构造,而且经验证,这些构造在不同数据库上会以同样的方式工作(还 记得关于 NULL=NULL 的讨论吧!)。另一种方法的可移植性更好,而且可以提供 更好的性能,就是使用存储过程来返回结果集。你会发现,每个开发商的数据库 都可以从存储过程返回结果集,但是返回的方式不同。针对不同的数据库,要编 写的具体源代码会有所不同。 这里有两个选择,一种做法是不使用存储过程返回结果集,另一种做法是针 对不同的数据库实现不同的代码。我就坚持第二种做法,即针对不同的开发商编 写不同的代码,而且大量使用存储过程。初看上去,另换一个数据库实现时这好 像会增加开发时间。不过你会发现,在多个数据库上实现时,采用这种方法实际 上容易得多。你不用寻找适用于所有数据库的最佳 SQL(也许在某些数据库上表 现好一些,但在另外一些数据库上可能并不理想),而只需实现最适合该数据库 的 SQL。这些工作可以在应用之外完成,这样对应用调优时就有了更大的灵活性。 你可以在数据库自身中修正一个表现很差的查询,并立即部署所做的改动,而无 需修改应用。另外,采用这种方法,还可以充分利用开发商提供的 SQL 扩缩。例 如,Oracle 在其 SQL 中提供了 CONNECT BY 操作,能支持层次查询。这个独有的 特性对于处理递归查询很有意义。在 Oracle 中,你可以自由地使用这个 SQL 扩 缩,因为它在应用“之外”(也就是说,隐藏在数据库中)。在其他数据库中, 则可能需要使用一个临时表,并通过存储过程中的过程性代码才能得到同样的结 果。既然你花钱购买了这些特性,自然可以充分地加以使用。 应用要在哪个数据库上部署,就针对这个数据库开发一个专用的代码层,这 种技术与实现多平台代码所用的开发技术是一样的。例如,Oracle公司在开发 O racle 数据库时就使用了这些技术。这一层代码量很大(但相对于数据库的全部 代码来讲,还只是很少的一部分),称为操作系统相关(operating system-de pendent,OSD)代码,是专门针对各个平台实现的。使用这层抽象,Oracle 就 能利用许多本地 OS 特性来提高性能和支持集成,而无需重写数据库本身的很大 一部分代码。Oracle 能作为一个多线程应用在 Windows 上运行,也能作为一个 多进程应用在 UNIX 上运行,这就反映出Oracle 利用了这种 OSD 代码。它将进程 间通信的机制抽象到这样一个代码层上,可以根据不同的操作系统重新实现,所 以允许有完全不同的实现,它们的表现与直接(专门)为各平台编写的应用相差 无几。 采用这个方法还有一个原因,要想找到一个样样精通的开发人员,要求他熟 知 Oracle、SQL Server 和 DB2 之间的细微差别(这里只讨论这 3 个数据库)几 乎是不可能的,更别说找到这样一个开发小组了。我在过去 11 年间一直在用 Or acle(大体如此,但不排除其他软件)。每一天使用 Oracle,都会让我学到一 些新的东西。但我还是不敢说同时精通这 3 种数据库,知道它们之间的差别,并 且清楚这些差别会对要构建的“泛型代码”层有什么影响。我觉得自己无法准确 或高效地实现这样一个“泛型代码”层。再说了,我们指的是一般的开发人员, 有多少开发人员能真正理解或充分使用了手上的数据库呢?更别说掌握这 3 种 数据库了!要寻找这样一个“全才”,他能开发安全、可扩缩而且独立于数据库 的程序,就像是大海捞针一样。而希望由这样的人员组建一支开发队伍更是绝无 可能。反过来,如果去找一个 Oracle 专家、一个 DB2 专家和一个 SQL Server 专家,告诉他们“我们需要事务完成 X、Y 和 Z”,这倒是很容易。只需告诉他 们“这是你的输入,这些是我们需要的输出,这是业务过程要做的事情”,根据 这些来生成满足要求的事务性 API(存储过程)就很简单了。针对特定的数据库, 按照数据库特有的一组功能,可以采用最适于该数据库的方式来实现。开发人员 可以自由地使用底层数据库平台的强大能力(也可能底层数据库缺乏某种能力, 而需要另辟蹊径)。 3. 特性和功能 你不必努力争取数据库独立性,这还有一个很自然的理由:你应当准确地知道 特定数据库必须提供什么,并充分加以利用。这一节不会列出 Oracle 10g 提供的 所有特性,光是这些特性本身就需要一本很厚的书才能讲完。Oracle 9i Releas e 1、9i Release 2 和 10g Release 1 本身的新特性在 Oracle 文档中已做介绍。 Oracle 为此提供了大约 10 000 页的文档,涵盖了每一个有意义的特性和功能。 你起码要对数据库提供的特性和功能有一个大致的了解,这一节只是讨论大致了 解有什么好处。 前面提到过,我总在http://asktom.oracle.com 上回答有关 Oracle 的问题。 我说过,我的答案中 80%都只是给出相关文档的 URL(这是指我公开提出的那些 问题,其中许多答案都只是指向文档,另外还会有几个问题我没有公开提出,因 为这些问题的答案几乎都是“读读这本书”)。人们问我怎么在数据库中编写一 些复杂的功能(或者在数据库之外编写),我就会告诉他们在文档的哪个地方可 以了解到 Oracle 已经实现了这个功能,并且还说明了应该如何使用这个功能。 我时常会遇到一些有关复制的问题。可能有这样一个问题:“我想在每个地方都 留有数据的一个副本。我希望这是一个只读的副本,而且每天只在半夜更新一次。 我该怎么编写代码来做到呢?”答案很简单,只是一个 CREATE MATERIALIZED V IEW 命令而已。这是数据库中的一个内置功能。实际上,实现复制还有许多方法, 从只读的物化视图到可更新的物化视图,再到对等复制以及基于流的复制,等等。 你当然可以编写你自己的复制,这么做可能很有意思,但是从最后看来,自 己编写可能不是最明智的做法。数据库做了很多工作。一般来说,数据库会比我 们自己做得更好。例如,Oracle 中复制是用 C 编写的,充分考虑到了国际化。 不仅速度快、相当容易,而且很健壮。它允许跨版本和跨平台,并且提供了强大 的技术支持,所以倘若你遇到问题,Oracle Support 会很乐意提供帮助。如果 你要升级,也会同步地提供复制支持,可能还会增加一些新的特性。下面考虑一 下如果由你自己来开发会怎么样。你必须为每一个版本都提供支持。老版本和新 版本之间的互操作性谁来负责?这个任务会落在你的头上。如果出了“问题”, 你没有办法寻求支持,至少在得到一个足够小的测试用例(但足以展示你的主要 问题)之前,没有人来帮助你。当新版本的 Oracle 推出时,也要由你自己将你 的复制代码移植到这个新版本。 如果没有充分地了解数据库已经提供了哪些功能,从长远看,其坏影响还会 几次三番地出现。我曾经与一些有多年数据库应用开发经验的人共事,不过他们 原先是在其他数据库上开发应用。这一次他们在 Oracle 上构建了一个分析软件 (趋势分析、报告和可视化软件),要用于分析临床医学数据(与保健相关)。 这些开发人员不知道 SQL 的一些语法特性,如内联视图、分析功能和标量子查询。 他们遇到的一个主要问题是需要分析一个父表及两个子表的数据。相应的实体— 关系图(entity-relationship diagram,ERD)如图 1-1 所示。 图 1-1 简单的ERD 他们想生成父记录的报告,并提供子表中相应子记录的聚集统计。他们原来 使用的数据库不支持子查询分解(WITH 子句),也不支持内联视图(所谓内联 视图,就是 “查询一个查询”,而不是查询一个表)。由于不知道有这些特性, 开发人员们在中间层编写了他们自己的一个数据库。他们的做法是先查询父表, 对应返回的每一行,再对各个子表分别运行聚集查询。这样做的后果是:对于最 终用户想要运行的每一个查询,他们都要运行数千个查询才能得到所需的结果。 或者,他们的另一种做法是在中间层获取完整的聚集子表,再放入内存中的散列 表,并完成一个散列联结(hash join)。 简而言之,他们重新开发了一个数据库,自行完成了与嵌套循环联结或散列 联结相当的功能,而没有充分利用临时表空间、复杂的查询优化器等所提供的好 处。这些开发人员把大量时间都花费在这个软件的开发、设计、调优和改进上, 而这个软件只是要做数据库已经做了的事情,要知道他们原本已经花钱买了这些 功能!与此同时,最终用户还在要求增加新特性,但是一直没有如愿,因为开发 人员总忙于开发报告“引擎”,没有更多的时间来考虑这些新特性,实际上这个 报告引擎就是一个伪装的数据库引擎。 我告诉他们,完全可以联结两个聚集来比较用不同方法以不同详细程度存储 的数据(见代码清单 1-1~代码清单 1-3)。 代码清单 1-1 内联视图:对“查询”的查询 代码清单 1-2 标量子查询:每行运行另一个查询 代码清单 1-3 WITH 子查询分解 更何况他们还可以使用 LAG、LEAD、ROW_NUMBER 之类的分析函数、分级函数 等。我们没有再花时间去考虑如何对他们的中间层数据库引擎进行调优,而是把 余下的时间都用来学习 SQL Reference Guide,我们把它投影在屏幕上,另外还 打开一个 SQL*Plus 实际演示到底如何工作。最终目标不是对中间层调优,而是 尽快地把中间层去掉。 我曾经见过许多人在 Oracle 数据库中建立后台进程从管道(一种数据库 IP C 机制)读消息。这些后台进程执行管道消息中包含的 SQL,并提交工作。这样 做是为了在事务中执行审计,即使更大的事务(父事务)回滚了,这个事务(子 事务)也不会回滚。通常,如果使用触发器之类的工具来审计对某数据的访问, 但是后来有一条语句失败,那么所有工作都会回滚。所以,通过向另一个进程发 送消息,就可以有一个单独的事务来完成审计工作并提交。即使父事务回滚,审 计记录仍然保留。在 Oracle8i 以前的版本中,这是实现此功能的一个合适的方 法(可能也是惟一的方法)。我告诉他们,数据库还有一个称为自治事务(aut onomous transaction)的特性,他们听后很是郁闷。自治事务的实现只需一行 代码,就完全可以做到他们一直在做的事情。好的一面是,这说明他们可以丢掉 原来的大量代码,不用再维护了。另外,系统总的来讲运行得更快,而且更容易 理解。不过,他们还在为“重新发明”浪费了那么多时间而懊恼不已。特别是那 个写后台进程的开发人员更是沮丧,因为他写了一大堆的代码。 还是我反复重申的那句话:针对某个问题,开发人员力图提供复杂的大型解 决方案,但数据库本身早已解决了这个问题。在这个方面,我自己也有些心虚。 我还记得,有一天我的 Oracle 销售顾问走进我的办公室(那时我还只是一个客 户),看见我被成堆的 Oracle 文档包围着。我抬起头,问他“这是真的吗?” 接下来的几天我一直在深入研究这些文档。此前我落入一个陷阱,自以为“完全 了解数据库”,因为我用过 SQL/DS、DB2、Ingress、Sybase、Informix、SQLBa se、Oracle,还有其他一些数据库。我没有花时间去了解每个数据库提供了什么, 而只是把从其他数据库学到的经验简单地应用到当时正在使用的数据库上(移植 到 Sybase/SQL Server 时对我的触动最大,它与其他数据库的工作根本不一样)。 等到我真正发现 Oracle(以及其他数据库)能做什么之后,我才开始充分利用 它,不仅能更快地开发,而且写的代码更少。我认识到这一点的时候是 1993 年。 请仔细想想你能用手头的软件做些什么,不过与我相比,你已经晚了十多年了。 除非你花些时间来了解已经有些什么,否则你肯定会在某个时候犯同样的 错误。在这本书中,我们会深入地分析数据库提供的一些功能。我选择的是人 们经常使用的特性和功能,或者是本应更多地使用但事实上没有得到充分利用 的功能。不过,这里涵盖的内容只是冰山一角。Oracle 的知识太多了,单用一 本书来讲清楚是做不到的。 重申一遍:每天我都会学到 Oracle 的一些新知识。这需要“与时俱进”, 时刻跟踪最新动态。我自己就常常阅读文档(不错,我还在看文档)。即使如此, 每天还是会有人指出一些我不知道的知识。 4. 简单地解决问题 通常解决问题的途径有两种:容易的方法和困难的方法。我总是看到人们在 选择后者。这并不一定是故意的,更多的情况下,这么做只是出于无知。他们没 想到数据库能“做那个工作”。而我则相反,我总是希望数据库什么都能做,只 有当我发现它确实做不了某件事时才会选择困难的办法(自己来编写)。 例如,人们经常问我,“怎么确保最终用户在数据库中只有一个会话?”(其 实类似这样的例子还有很多,我只是随便选了一个)。可能许多应用都有这个需 求,但是我参与的应用都没有这样做,我不知道有什么必要以这种方式限制用户。 不过,如果确实想这样做,人们往往选择困难的方法来实现。例如,他们可能建 立一个由操作系统运行的批作业,这个批作业将查看V$SESSION 表;如果用户有 多个会话,就坚决地关闭这些会话。还有一种办法,他们可能会创建自己的表, 用户登录时由应用在这个表中插入一行,用户注销时删除相应行。这种实现无疑 会带来许多问题,于是咨询台的铃声大作,因为应用“崩溃”时不会将该行删除。 为了解决这个问题,我见过许多“有创意的”方法,不过哪一个也没有下面这种 方法简单: 仅此而已。现在有 ONE_SESSION 配置文件的所有用户都只能登录一次。每次 我提出这个解决方案时,人们总是拍着自己的脑门,不无惊羡地说:“我不知道 居然还能这么做!”正所谓磨刀不误砍柴工,花些时间好好熟悉一下你所用的工 具,了解它能做些什么,在开发时这会为你节省大量的时间和精力。 还是这句“力求简单”,它也同样适用于更宽泛的体系结构层。我总是鼓励 人们在采用非常复杂的实现之前先要再三思量。系统中不固定的部分越多,出问 题的地方就越多。在一个相当复杂的体系结构中,要想准确地跟踪到错误出在哪 里不是一件容易的事。实现一个有“无数”层的应用可能看起来很“酷”,但是 既然用一个简单的存储过程就能更好、更快地完成任务,而且只利用更少的资源, 实现为多层的做法就不是正确的选择。 我见过许多项目的应用开发持续数月之久,好像没有尽头。开发人员都在使 用最新、最好的技术和语言,但是开发速度还是不快。应用本身的规模并不大, 也许这正是问题所在。如果你在建一个狗窝(这是一个很小的木工活),就不会 用到重型机器。你只需要几样小工具就行了,大玩艺是用不上的。另一方面,如 果你在建一套公寓楼,就要下大功夫,可能要用到大型机器。与建狗窝相比,解 决这个问题所用的工具完全不同。应用开发也是如此。没有一种“万全的体系结 构”,没有一种“完美的语言”,也没有一个“无懈可击的方法”。 例如,我就使用了 HTML DB 来建我的网站。这是一个很小的应用,只有一个 (或两个)开发人员参与。它有大约 20 个界面。这个实现使用 PL/SQL 和 HTML DB 是合适的,这里不需要用 Java 编写大量的代码,不需要建立 EJB,等等。这 是一个简单的问题,所以应该用简单的方式解决。确实有一些大型应用很复杂、 规模很大(如今这些应用大多会直接购买,如人力资源 HR 系统、ERP 系统等), 但是小应用更多。我们要选用适当的方法和工具来完成任务。 不论什么时候,我总是提倡用最简单的体系结构来解决问题,而不要采用复 杂的体系结构。这样做可能有显著的回报。每种技术都有自己合适的位置。不要 把每个问题都当成钉子,高举铁锤随处便砸,我们的工具箱里并非只有铁锤。 5. 开放性 我经常看到,人们选择艰难的道路还有一个原因。这还是与那种观点有关, 我们总认为要不遗余力地追求开放性和数据库独立性。开发人员希望避免使用封 闭的专有数据库特性,即使像存储过程或序列这样简单的特性也不敢用,因为使 用这些专有特性会把他们锁定到某个数据库系统。这么说吧,我的看法是只要你 开发一个涉及读/写的应用,就已经在某种程度上被锁定了。一旦开始运行查询 和修改,你就会发现数据库间存在着一些微小的差别(有时还可能存在显著差 异)。例如,在一个数据库中,你可能发现 SELECT COUNT(*) FROM T 查询与两 行记录的更新发生了死锁。在 Oracle 中,却发现 SELECT COUNT(*)绝对不会阻 塞写入器。你可能见过这样的情况,一个数据库看上去能保证某种业务规则,这 是由于该数据库锁定模型的副作用造成的,但另一个数据库则不能保证这个业务 规则。给定完全相同的事务,在不同数据库中却有可能报告全然不同的答案,原 因就在于数据库的实现存在一些基本的差别。你会发现,要想把一个应用轻轻松 松地从一个数据库移植到另一个数据库,这种应用少之又少。不同数据库中对于 如何解释 SQL(例如,NULL=NULL 这个例子)以及如何处理 SQL 往往有不同的做 法。 在我最近参与的一个项目中,开发人员在使用Visual Basic、ActiveX控件、 IIS 服务器和 Oracle 构建一个基于 Web 的产品。他们不无担心地告诉我,由于 业务逻辑是用 PL/SQL 编写的,这个产品已经依赖于数据库了。他们问我:“怎 么修正这个问题?” 先不谈这个问题,退一步说,针对他们所选的技术,我实在看不出依赖于数据 库有什么“不好”: ‰ 开发人员选择的语言已经把他们与一个开发商提供的一个操作系统锁定(要想独立于操 作系统,其实他们更应选择 Java)。 ‰ 他们选择的组件技术已经把他们与一个操作系统和一个开发商锁定(选择 J2EE 更合适)。 ‰ 他们选择的 Web 服务器已经将他们与一个开发商和一个平台锁定(为什么不用 Apache 呢?)。 所选择的每一项技术都已经把他们锁定到一个非常特定的配置,实际上,就操作系统而言, 惟一能让他们有所选择的技术就是数据库。 暂且不管这些(选择这些技术可能有他们自己的原因),这些开发人员还刻意不去用 体系结构中一个重要部件的功能,而美其名曰是为了开放性。在我看来,既然精心地选择 了技术,就应该最大限度地加以利用。购买这些技术你已经花了不少钱,难道你想白白地 花冤枉钱吗?我认为,他们一直想尽力发挥其他技术的潜能,那么为什么要把数据库另眼 相看呢?再者,数据库对于他们的成功至关重要,单凭这一点也说明,不充分利用数据库 是说不过去的。 如果从开放性的角度来考虑,可以稍稍换个思路。你把所有数据都放在数据 库中。数据库是一个很开放的数据池。它支持通过大量开放的系统协议和访问机 制来访问数据。这听起来好像很不错,简直就是世界上最开放的事物。 不过接下来,你把所有应用逻辑还有(更重要的)安全都放在数据库之外。 可能放在访问数据的 bean 中;也可能放在访问数据的 JSP 中;或者置于在 Micr osoft 事务服务器(Microsoft Transaction Server,MTS)管理之下运行的 Vi sual Basic 代码中。最终结果就是,你的数据库被封闭起来,这么一来,数据 库已经被你弄得“不开放”了。人们无法再采用现有技术使用这些数据;他们必 须使用你的访问方法(或者干脆绕过你的安全防护)。尽管现在看上去还不错, 但是你要记住,今天响当当的技术(比如说,EJB)也会成为昨日黄花,到了明 天可能就是一个让人厌倦的技术了。在关系领域中(以及大多数对象实现中), 过去 25 年来只有数据库自己傲然屹立。数据前台技术几乎每年一变,如果应用 把安全放在内部实现,而不是在数据库中实现,随着前台技术的变革,这些应用 就会成为前进道路上的绊脚石。 Oracle 数据库提供了一个称为细粒度访问控制(fine-grained access con trol,FGAC)的特性。简而言之,这种技术允许开发人员把过程嵌入数据库中, 向数据库提交查询时可以修改查询。这种查询修改可用于限制客户只能接收或修 改某些行。过程在运行查询时能查看是谁在运行查询,他们从哪个终端运行查询, 等等,然后能适当地约束对数据的访问。利用 FGAC,可以保证以下安全性,例 如: ‰ 某类用户在正常工作时间之外执行的查询将返回 0 条记录。 ‰ 如果终端在一个安全范围内,可以向其返回所有数据,但是远程客户终端只能得到不敏 感的信息。 实质上讲,FGAC 允许我们把访问控制放在数据库中,与数据“如影随形” 。 不论用户从 bean、JSP、使用 ODBC 的 Visual Basic 应用,还是通过 SQL*Plus 访问数据,都会执行同样的安全协议。这样你就能很好地应对即将到来的下一 种新技术。 现在我再来问你,你想让所有数据访问都通过调用 Visual Basic 代码和 Ac tiveX 控件来完成(如果愿意,也可以把 Visual Basic 换成 Java,把 ActiveX 换成 EJB,我并不是推崇哪一种技术,这里只是泛指这种实现);还是希望能从 任何地方访问数据(只要能与数据库通信),而不论协议是 SSL、HTTP、Oracle Net,还是其他协议,也不论使用的是 ODBC、JDBC、OCI,还是其他 API,这两 种实现中哪一种更“开放”? 我还没见过哪个报告工具能“查询”Visual Ba sic 代码,但是能查询 SQL 的工具却有不少。 人们总是不遗余力地去争取数据库独立性和完全的开放性,但我认为这是一个 错误的决定。不管你使用的是什么数据库,都应该充分地加以利用,把它的每一 个功能都“挤出来”。不论怎样,等到调优阶段你也会这样做的(不过,往往在 部署之后才会调优)。如果通过充分利用软件的功能,会让你的应用快上 5 倍, 你会惊讶地发现,居然这么快就把数据库独立性需求抛在脑后了。 1.3.5 怎么能让应用运行得更快 总是有人问我这个问题:“怎么能让应用运行得更快?”所有人都希望有一 个“fast = true”开关,认为“数据库调优”就意味着让你调整数据库。实际 上,根据我的经验,80%以上(甚至经常是 100%)的性能问题都出现在设计和实 现级,而不是数据库级。通过修改应用,我常常能让性能呈数量级地增长。但是, 如果只是在数据库级做修改,就不太可能得到这么大幅度的提高。在对数据库上 运行的应用进行调优之前,先不要对数据库进行调优。 随着时间的推移,数据库级也有了一些开关,有助于减轻编程错 误带来的影响。例如,Oracle 8.1.6增加了一个新参数 CURSOR_SHARING=FORCE。 如果你愿意,这个特性会实现一个自动绑定器(auto-binder)。如果有一个查 询编写为 SELECT * FROM EMP WHERE EMPNO = 1234,自动绑定器会悄无声息地 把它改写成 SELECT * FROM EMP WHERE EMPNO = :x。这确实能动态地大大减少 硬解析数,并减少前面讨论的库闩等待时间——但是(凡事总有个“但是”), 它可能有一些副作用。游标共享的一个常见副作用如下所示: 这里到底发生了什么?为什么到第二个查询时 SQL*Plus 报告的列突然变得 这么大?要知道,这还是同一个查询呀!如果查看一下游标共享设置为我们做了 些什么,原因就会很清楚了(还会明白其他一些问题): 游标共享会删除查询中的信息。它找到每一个直接量(literal),包括内 置求子串函数(substr)的参数,直接量就是我们使用的常量。它把这些直接量 从查询中删除,并代之以绑定变量。SQL 引擎再也不知道这个列是长度为 1 的子 串,它的长度是不确定的。另外,可以看到 where rownum = 1 现在也已经绑定。 看上去似乎不错;不过,优化器把一个重要的信息也一并删除了。它不知道“这 个查询将获取一行”;现在只认为“这个查询将返回前N 行,而N 可能是任何值”。 实际上,如果加上 SQL_TRACE=TRUE 后再运行这些查询,你会发现每个查询使用 的查询计划都不同,它们完成的工作量也大相径庭。考虑以下查询: 查询计划有一些微小的差别(有时甚至完全不同);另外它们的工作量也 有很大差异。所以,打开游标共享确实需要特别谨慎(而且需要进行充分测试)。 游标共享可能会改变应用的行为(例如,列宽发生变化),而且由于它删除了 SQL 中的所有直接量,甚至包括那些绝对不会变化的直接量,所以可能会对查 询计划带来负面影响。 另外,与解析和优化大量各不相同的查询相比,尽管使用 CURSOR_SHARING = FORCE 会让运行速度更快,但同时我也发现,倘若开发人员确实在查询中使用 了绑定变量,查询的速度就比使用游标共享要快。这不是因为游标共享代码的效 率不高,而是因为程序本身的效率低下。在许多情况下,如果应用没有使用绑定 变量,也不会高效地解析和重用游标。因为应用认为每个查询都是惟一的(并把 查询分别建立为不同的语句),所以绝对不会多次使用一个游标。事实上,如果 程序员刚开始就使用了绑定变量,他(或她)就能只解析一次查询,然后多次重 用它。正是这种解析开销降低了总体性能。 实质上讲,一定要记住重要的一点,只打开 CURSOR_SHARING = FORCE 并不 一定能解决你的问题。而且游标共享还可能带来新的问题:在有些情况下 CURSO R_SHARING 是一个非常有用的工具,但它不是银弹。开发得很好的应用从不需要 游标共享。从长远来看,要尽可能地使用绑定变量,而在需要时才使用常量,这 才是正确的做法。 注意 世上没有银弹——要记住,根本没有。如果有的话,自然就会默认地采用那种做法, 这样也就无所谓银弹了。 就算是确实能在数据库级放几个开关(这种开关真的很少),但是有些问题 与并发控制和执行不佳的查询(可能是因为查询写得不好,也可能是因为数据的 结构性差)有关,这些问题用开关是解决不了的。这些情况往往需要重写(而且 时常需要重建)。移动数据文件、修改多块读计数(multiblock read count) 和其他数据库级开关对应用的总体性能通常影响很小。你想让用户接受你的应 用,可能需要让性能提升 2 倍、3 倍、……、n 倍才行。你的应用是不是只慢了 10%,这种情况多不多?如果只是慢 10%,没有人会有太多抱怨。但是如果慢了 5 倍,就会让人很不高兴。再说一遍,如果只是移动数据文件,性能不会提升 5 倍。要想达到这个目的,只能通过调整应用才能办到,可能要让它大幅减少 I/O 操作。 在整个开发阶段,你都要把性能作为一个目标精心地设计,合理地构建,并 且不断地测试。绝对不能把它当作马后炮,事后才想起来。我真是很奇怪,为什 么那么多人根本不对应用调优,就草率地把应用交付到客户手里,匆匆上马,并 运行起来。我见过一些应用除了主键索引外,居然没有其他的任何索引。从来没 有对查询执行过调优,也没有执行过压力测试。应用的用户数很少,从未让更多 的用户试用过。这些应用总是把调优当成产品安装的一部分。对我来说,这种做 法绝对不可接受。最终用户应该第一天就拿到一个响应迅速、充分优化的系统。 肯定还有许多“产品问题”需要处理,但不能让用户从一开始就领教糟糕的性能。 对用户来说,一个新应用里有几个bug 尚能容忍,但你别指望他们能耐心地在屏 幕前等待漫长的时间。 1.3.6DBA 与开发人员的关系 有一点很肯定,要建立最成功的信息系统,前提是 DBA 与应用开发人员之间 要有一种“共生关系”。在这一节里,我想从开发人员的角度谈谈开发人员与 D BA 之间的分工(假设所有正式开发都有 DBA 小组的参与)。 作为一名开发人员,你不必知道如何安装和配置软件。这应该是 DBA 或者 系统管理员(system administrator,SA)的任务。安装 Oracle Net、配置监 听器、配置共享服务器、建立连接池、安装数据库、创建数据库等,这些事情 我都会交给 DBA/SA 来做。 一般来讲,开发人员不必知道如何对操作系统调优。我个人通常会让系统的 SA 负责这个任务。作为数据库应用的软件开发人员,应该能熟练地使用你选择 的操作系统,但是不要求你能对它调优。 DBA 最重大的职责是数据库恢复。注意,我说的可不是“备份”,而是“恢 复”。而且,我认为这也是 DBA 惟一重要的职责。DBA 要知道回滚(rollback) 和重做(redo)怎么工作,不错,这也是开发人员要了解的。DBA 还要知道如何 完成表空间时间点恢复,这一点开发人员不必介入。如果你能有所了解,也许以 后会用得上,但是作为开发人员目前不必亲力而为。 在数据库实例级调优,并得出最优的 PGA_AGGREGATE_TARGET 是什么,这一般 是 DBA 的任务(数据库往往能帮助他们得出正确的答案)。也有一些例外情况, 有时开发人员可能需要修改会话的某个设置,但是如果在数据库级修改设置,就 要由 DBA 来负责。一般数据库并不是只支持一位开发人员的应用,而是运行着多 个应用,因此只有支持所有应用的 DBA 才能做出正确的决定。 分配空间和管理文件也是 DBA 的工作。开发人员可以对分配的空间做出估计 (他们觉得需要多少空间),但是余下的都要由 DBA/SA 决定。 实质上讲,开发人员不必知道如何运行数据库,他们只需要知道如何在数据 库中运行。开发人员和 DBA 要协同解决问题,但各有分工。假设你是一位开发人 员,如果你的查询用的资源太多,DBA 就会来找你;如果你不知道怎么让系统跑 得更快,可以去找 DBA(如果应用已经得到充分调优,此时就可以完成实例级调 优)。 这些任务因环境而异,不过我还是认为存在着分工。好的开发人员往往是很 糟糕的 DBA,反之亦然。在我看来,他们的能力不同、思路不同,而且个性也不 同。很自然地,人们都爱做自己最喜欢的工作,而且能越做越好,形成良性循环。 如果一个人比较喜欢某项工作,他会做得更好,但是这并不是说其他工作就一定 做得很糟。就我而言,我觉得我更应算是一位开发人员,但兼有DBA 的许多观点。 我不仅喜欢开发,也很喜欢“服务器方面”的工作(这大大提高了我的应用调优 水平,而且总会有很多收获)。 1.4 小结 这一章好像一直在东拉西扯地闲聊,我是想用这种方式让你认识到为什么需 要了解数据库。这里提到的例子并不是个别现象,这些情况每天都在出现。我注 意到,诸如此类的问题总在连续不断地发生。 下面把要点再重述一遍。如果你要用 Oracle 开发,应该做到: ‰ 需要理解 Oracle 体系结构。不要求你精通到能自行重写服务器的程度,不过确实需要有 足够的了解,知道使用某个特定特性的含义。 ‰ 需要理解锁定和并发控制特性,而且知道每个数据库都以不同的方式实现这些特性。如 果不清楚这一点,你的数据库就可能给出“错误”的答案,而且应用会遭遇严重的竞争问题, 以至于性能低下。 ‰ 不要把数据库当作黑盒,也就是说,不要以为无需了解数据库。在大多数应用中,数据 库都是最为重要的部分。如果忽略它,后果是致命的。 ‰ 用尽可能简单的方法解决问题,要尽量使用 Oracle 提供的内置功能。这可是你花大价钱 买来的。 ‰ 软件项目、编程语言以及框架总是如走马灯似地在变。作为开发人员,我们希望几周(可 能几个月)内就把系统建立并运行起来,然后再去解决下一个问题。如果总是从头开始重新 “创造”,就永远也追不上开发的脚步。你肯定不会用 Java 建立你自己的散列表,因为 Ja va 已经提供了一个散列表,同样,你也应该使用手头可用的数据库功能。当然,为此第一 步是要了解有哪些数据库功能可用。我曾经见过不止一个开发小组遇到麻烦,不光技术上有 困难,人员也很紧张,而造成这种结果的原因只是不清楚 Oracle 已经免费提供了哪些功能。 ‰ 还是上面这一条(软件项目和编程语言总是像走马灯似的),但数据是永远存在的。我 们构建了使用数据的应用,从长远看,这些数据会由多个应用使用。所以重点不是应用,而 是数据。应该采用允许使用和重用数据的技术和实现。如果把数据库当成一个桶,所有数据 访问都必须通过你的应用,这就错了。这样一来,你将无法自主地查询应用,也无法在老应 用之上构建新应用。但是,如果充分地使用数据库,你就会发现,无论是增加新应用、新报 告,还是其他任何功能,都会容易得多 牢记以上这几点,再接着看下面的内容。 第 2 章体系结构概述 2.1 定义数据库和实例 Oracle 被设计为一个相当可移植的数据库;在当前所有平台上都能运行, 从 Windows 到 UNIX 再到大型机都支持 Oracle。出于这个原因,在不同的操作系 统上,Oracle的物理体系结构也有所不同。例如,在UNIX 操作系统上可以看到, Oracle 实现为多个不同的操作系统进程,实际上每个主要功能分别由一个进程 负责。这种实现对于UNIX 来说是正确的,因为UNIX 就是以多进程为基础。不过, 如果放到 Windows 上就不合适了,这种体系结构将不能很好地工作(速度会很慢, 而且不可扩缩)。在 Windows 平台上,Oracle 实现为一个多线程的进程。如果 是一个运行 OS/390 和 z/OS 的 IBM 大型机系统,针对这种操作系统的 Oracle 体 系结构则充分利用了多个 OS/390 地址空间,它们都作为一个Oracle 实例进行操 作。一个数据库实例可以配置多达 255 个地址空间。另外,Oracle 还能与 OS/3 90 工作负载管理器(Workload Manager,WLM)协作,建立特定 Oracle 工作负 载相互之间的相对执行优先级,还能建立相对于 OS/390 系统中所有其他工作的 执行优先级。尽管不同平台上实现 Oracle 所用的物理机制存在变化,但 Oracle 体系结构还是很有一般性,所以你能很好地了解Oracle 在所有平台上如何工作。 这一章会从全局角度概要介绍这个体系结构。我们会分析 Oracle 服务器, 并给出“数据库”和“实例”等术语的定义(这些术语通常很容易混淆)。这里 还会介绍“连接”到 Oracle 时会发生什么,另外将从高层分析服务器如何管理 内存。在后续 3 章中,我们还会详细介绍 Oracle 体系结构中的 3 大部分: ‰ 第 3 章将介绍文件,其中涵盖构成数据库的 5 大类文件:参数文件、数据文件、临时文 件、控制文件和重做日志文件。我们还会介绍另外几类文件,包括跟踪文件、警告文件、转 储文件(DMP)、数据泵文件(data pump)和简单的平面文件。这一章将谈到 Oracle 10g 新增的一个文件区,称为闪回恢复区(Flashback Recovery Area),另外我们还会讨论自动 存储管理(Automatic Storage Management,ASM)对文件存储的影响。 ‰ 第 4 章介绍 Oracle 的一些内存结构,分别称为系统全局区(System Global Area,SGA)、 进程全局区(Process Global Area,PGA)和用户全局区(User Global Area,UGA)。我 们会分析这些结构之间的关系,并讨论共享池(shared pool)、大池(big pool)、Java 池 (Java pool)以及 SGA 中的其他一些组件。 ‰ 第 5 章介绍 Oracle 的物理进程或线程。我们会讨论数据库上运行的 3 类不同的进程:服 务器进程(server process)、后台进程(background process)和从属进程(slave process)。 先介绍哪一部分实在很难定夺。由于进程使用了 SGA,所以如果在进程之前 先介绍 SGA 可能不太合适。另一方面,讨论进程及其工作时,又会引用 SGA。另 外两部分的关系也很紧密:文件由进程处理,如果不先了解进程做什么,将很难 把文件搞清楚。 正因如此,我会在这一章定义一些术语,对 Oracle 是什么提供一个一般性 的概述(也许你会把它画出来)。有了这些准备,你就能深入探访各个部分的具 体细节了。 2.1 定义数据库和实例 在 Oracle 领域中有两个词很容易混淆,这就是“实例”(instance)和“数 据库”(database)。作为 Oracle 术语,这两个词的定义如下: ‰ 数据库(database):物理操作系统文件或磁盘(disk)的集合。使用 Oracle 10g 的自动 存储管理(Automatic Storage Management,ASM)或 RAW 分区时,数据库可能不作为操 作系统中单独的文件,但定义仍然不变。 ‰ 实例(instance):一组 Oracle 后台进程/线程以及一个共享内存区,这些内存由同一个 计算机上运行的线程/进程所共享。这里可以维护易失的、非持久性内容(有些可以刷新输 出到磁盘)。就算没有磁盘存储,数据库实例也能存在。也许实例不能算是世界上最有用 的事物,不过你完全可以把它想成是最有用的事物,这有助于对实例和数据库划清界线。 这两个词有时可互换使用,不过二者的概念完全不同。实例和数据库之间的 关系是:数据库可以由多个实例装载和打开,而实例可以在任何时间点装载和打 开一个数据库。实际上,准确地讲,实例在其整个生存期中最多能装载和打开一 个数据库!稍后就会介绍这样的一个例子。 是不是更糊涂了?我们还会做进一步的解释,应该能帮助你搞清楚这些概 念。实例就是一组操作系统进程(或者是一个多线程的进程)以及一些内存。这 些进程可以操作数据库;而数据库只是一个文件集合(包括数据文件、临时文件、 重做日志文件和控制文件)。在任何时刻,一个实例只能有一组相关的文件(与 一个数据库关联)。大多数情况下,反过来也成立:一个数据库上只有一个实例 对其进行操作。不过,Oracle 的真正应用集群(Real Application Clusters, RAC)是一个例外,这是 Oracle 提供的一个选项,允许在集群环境中的多台计算 机上操作,这样就可以有多台实例同时装载并打开一个数据库(位于一组共享物 理磁盘上)。由此,我们可以同时从多台不同的计算机访问这个数据库。Oracl e RAC 能支持高度可用的系统,可用于构建可扩缩性极好的解决方案。 下面来看一个简单的例子。假设我们刚安装了 Oracle 10g 10.1.0.3。我们 执行一个纯软件安装,不包括初始的“启动”数据库,除了软件以外什么都没 有。 通过 pwd 命令可以知道当前的工作目录(这个例子使用一个 Linux 平台的计 算机)。我们的当前目录是 dbs(如果在Windows 平台上,则是database 目录)。 执行 ls–l 命令显示出这个目录为“空”。其中没有 init.ora 文件,也没有任 何存储参数文件(stored parameter file,SPFILE);存储参数文件将在第 3 章详细讨论。 使用 ps(进程状态)命令,可以看到用户 ora10g 运行的所有进程,这里假 设 ora10g 是 Oracle 软件的所有者。此时还没有任何 Oracle 数据库进程。 然后使用 ipcs 命令,这个 UNIX 命令可用于显示进程间的通信设备,如共享 内存、信号量等。目前系统中没有使用任何通信设备。 然后启动 SQL*Plus(Oracle 的命令行界面),并作为 SYSDBA 连接(SYSDBA 账户可以在数据库中做任何事情)。连接成功后,SQL*Plus 报告称我们连上了 一个空闲的实例: 我们的“实例”现在只包括一个 Oracle 服务器进程,见以下输出中粗体显 示的部分。此时还没有分配共享内存,也没有其他进程。 现在来启动实例: 这里提示的文件就是启动实例时必须要有的一个文件,我们需要有一个参数 文件(一种简单的平面文件,后面还会详细说明),或者要有一个存储参数文件。 现在就来创建参数文件,并放入启动数据库实例所需的最少信息(通常还会指定 更多的参数,如数据库块大小、控制文件位置,等等)。 然后再回到 SQL*Plus: 这里对 startup 命令加了 nomount 选项,因为我们现在还不想真 正“装载”数据库(要了解启动和关闭的所有选项,请参见 SQL*Plus 文档)。 注意 在 Windows 上运行 startup 命令之前,还需要使用 oradim.exe 实用程序执行一条服 务创建语句。 现在就有了所谓的“实例”。运行数据库所需的后台进程都有了,如进程监 视器(process monitor,PMON)、日志写入器(log writer,LGWR)等,这些 进程将在第 5 章详细介绍。 再使用 ipcs 命令,它会首次报告指出使用了共享内存和信号量,这是 UNIX 上的两个重要的进程间通信设备: 注意,我们还没有“数据库”呢!此时,只有数据库之名(在所创建的参数 文件中),而没有数据库之实。如果试图“装载”这个数据库,就会失败,因为 数据库根本就不存在。下面就来创建数据库。有人说创建一个 Oracle 数据库步 骤很繁琐,真是这样吗?我们来看看: 这里创建数据库就是这么简单。但在实际中,也许要使用一个稍有些复杂的 CREATE DATABASE 命令,因为可能需要告诉 Oracle 把日志文件、数据文件、控 制文件等放在哪里。不过,我们现在已经有了一个完全可操作的数据库了。可能 还需要运行$ORACLE_HOME/rdbms/admin/ catalog.sql 脚本和其他编录脚本(ca talog script)来建立我们每天使用的数据字典(这个数据库中还没有我们使用 的某些视图,如 ALL_OBJECTS),但不管怎么说,数据库已经有了。可以简单地 查询一些 Oracle V$视图(具体就是V$DATAFILE、V$LOGFILE和 V$CONTROLFILE), 列出构成这个数据库的文件: Oracle 使用默认设置,把所有内容都放在一起,并把数据库创建为一组持久 的文件。如果关闭这个数据库,再试图打开,就会发现数据库无法打开: 一个实例在其生存期中最多只能装载和打开一个数据库。要想再打开这个 (或其他)数据库,必须先丢弃这个实例,并创建一个新的实例。 重申一遍: ‰ 实例是一组后台进程和共享内存。 ‰ 数据库是磁盘上存储的数据集合。 ‰ 实例“一生”只能装载并打开一个数据库。 ‰ 数据库可以由一个或多个实例(使用 RAC)装载和打开。 前面提到过,大多数情况下,实例和数据库之间存在一种一对一的关系。 可能正因如此,才导致人们很容易将二者混淆。从大多数人的经验看来,数 据库就是实例,实例就是数据库。 不过,在许多测试环境中,情况并非如此。在我的磁盘上,可以有 5 个不同 的数据库。测试主机上任意时间点只会运行一个 Oracle 实例,但是它访问的数 据库每天都可能不同(甚至每小时都不同),这取决于我的需求。只需有不同的 配置文件,我就能装载并打开其中任意一个数据库。在这种情况下,任何时刻我 都只有一个“实例”,但有多个数据库,在任意时间点上只能访问其中的一个数 据库。 所以,你现在应该知道,如果有人谈到实例,他指的就是 Oracle 的进程和 内存。提到数据库时,则是说保存数据的物理文件。可以从多个实例访问一个数 据库,但是一个实例一次只能访问一个数据库。 2.2SGA 和后台进程 你可能已经想到了 Oracle 实例和数据库的抽象图是个什么样子(见图2-1)。 图 2-1 以最简单的形式展示了 Oracle 实例和数据库。Oracle 有一个很大的 内存块,称为系统全局区(SGA),在这里它会做以下工作: ‰ 维护所有进程需要访问的多种内部数据结构; ‰ 缓存磁盘上的数据,另外重做数据写至磁盘之前先在这里缓存; ‰ 保存已解析的 SQL 计划; ‰ 等等。 图 2-1 Oracle 实例和数据库 Oracle 有一组“附加到”SGA 的进程,附加机制因操作系统而异。在 UNIX 环境中,这些进程会物理地附加到一个很大的共享内存段,这是操作系统中分配 的一个内存块,可以由多个进程并发地访问(通常要使用shmget()和 shmat())。 在 Windows 中,这些进程只是使用 C 调用(malloc())来分配内存,因为它 们实际上是一个大进程中的线程,所以会共享相同的虚拟内存空间。Oracle 还 有一组供数据库进程/线程读写的文件(只允许 Oracle 进程读写这些文件)。这 些文件保存了所有的表数据、索引、临时空间、重做日志等。 如果在一个 UNIX 系统上启动 Oracle,并执行 ps 命令,会看到运行着许多物 理进程,还会显示出这些进程的名字。在前面的例子中,我们已经观察到了 pmo n、smon 以及其他一些进程。我会在第 5 章逐一介绍这些进程,现在只要知道它 们通称为 Oracle 后台进程(background process)就足够了。这些后台进程是 构成实例的持久性进程,从启动实例开始,这些进程会一直运行,直至实例关闭。 有一点要注意,这些都是进程,而不是单个的程序。UNIX 上只有一个 Oracl e 二进制可执行文件;根据启动时所提供的选项,这个可执行文件会有多种不同 的“个性”。执行 ora_pmon_ora10g 进程要运行这个二进制可执行文件,执行 o ra_ckpt_ora10g 进程时运行的可执行文件仍是它。二进制可执行文件只有一个, 就是 oracle,只是会以不同的名字执行多次。 在 Windows 上,使用 pslist 工具(http://www.sysinternals.com/ntw2k/f reeware/ pslist.shtml)只会看到一个进程 oracle.exe。同样,Windows 上也 只有一个二进制可执行文件(oracle.exe)。在这个进程中,可以看到表示 Ora cle 后台进程的多个线程。 使用 pslist(或另外的某个工具),可以看到以下线程: 从中可以看出,这个 Oracle 进程里有 19 个线程(以上所示的 Thd 列)。这 些线程就对应于 UNIX 上的进程(pmon、arch、lgwr 等 Oracle 进程)。还可以 用 pslist 查看各线程的更多详细信息: 不同于 UNIX,这里看不到线程的“名字”(UNIX 上则会显示 ora_pmon_ora 10g 等进程名),不过,我们可以看到线程 ID(Tid),优先级(Pri)以及有关 的其他操作系统审计信息。 2.3 连接 Oracle 这一节将介绍 Oracle 服务器处理请求的两种最常见的方式,并分析它们的 基本原理,这两种方式分别是专用服务器(dedicated server)连接和共享服务 器(shared server)连接。要想登录数据库并在数据库中真正做事情,必须先 连接,我们会说明建立连接时客户端和服务器端会发生什么。最后会简要地介绍 如何建立 TCP/IP 连接(TCP/IP 是网络上连接 Oracle 所用的主要网络协议), 并说明对于专用服务器连接和共享服务器连接,服务器上的监听器(listener) 进程会以不同的方式工作,这些监听器进程负责建立与服务器的物理连接。 2.3.1 专用服务器 从图 2-1 和 pslist 输出可以看出启动 Oracle 之后是什么样子。如果现在使 用一个专用服务器登录数据库,则会创建一个新的进程,提供专门的服务: 现在可以看到,线程有 20 个而不是 19 个,多加的这个线程就是我们的专用 服务器进程(稍后会介绍专用服务器进程的更多内容)。注销时,这个额外的线 程也没有了。在UNIX 上,可以看到正在运行的Oracle 进程列表上会多加一个进 程,这就是我们的专用服务器。 再回过来看前面的那个图。现在如果按最常用的配置连接 Oracle,则如图 2 -2 所示。 图 2-2 典型的专用服务器配置 如前所述,在我登录时,Oracle 总会为我创建一个新的进程。这通常称为专 用服务器配置,因为这个服务器进程会在我的会话生存期中专门为我服务。对于 每个会话,都会出现一个新的专用服务器,会话与专用服务器之间存在一对一的 映射。按照定义,这个专用服务器不是实例的一部分。我的客户进程(也就是想 要连接数据库的程序)会通过某种网络通道(如 TCP/IP socket)与这个专用服 务器直接通信,并由这个服务器进程接收和执行我的 SQL。如果必要,它会读取 数据文件,并在数据库的缓存中查找我要的数据。也许它会完成我的更新语句, 也可能会运行我的 PL/SQL 代码。这个服务器进程的主要目标就是对我提交的 SQ L 调用做出响应。 2.3.2 共享服务器 Oracle 还可以接受另一种方式的连接,这称为共享服务器(shared server), 正式的说法是多线程服务器(Multi-Threaded Server)或 MTS。如果采用这种 方式,就不会对每条用户连接创建另外的线程或新的 UNIX 进程。在共享服务器 中,Oracle 使用一个“共享进程”池为大量用户提供服务。共享服务器实际上 就是一种连接池机制。利用共享服务器,我们不必为 10 000 个数据库会话创建 10 000 个专用服务器(这样进程或线程就太多了),而只需建立很少的一部分 进程/线程,顾名思义,这些进程/线程将由所有会话共享。这样 Oracle 就能让 更多的用户与数据库连接,否则很难连接更多用户。如果让我的机器管理 10 00 0 个进程,这个负载肯定会把它压垮,但是管理 100 个或者 1 000 个进程还是可 以的。采用共享服务器模式,共享进程通常与数据库一同启动,使用 ps 命令可 以看到这个进程。 共享服务器连接和专用服务器连接之间有一个重大区别,与数据库连接的客 户进程不会与共享服务器直接通信,但专用服务器则不然,客户进程会与专用服 务器直接通信。之所以不能与共享服务器直接对话,原因就在于这个服务器进程 是共享的。为了共享这些进程,还需要另外一种机制,通过这种机制才能与服务 器进程“对话”。为此,Oracle 使用了一个或一组称为调度器(dispatcher, 也称分派器)的进程。客户进程通过网络与一个调度器进程通信。这个调度器进 程将客户的请求放入 SGA 中的请求队列(这也是 SGA 的用途之一)。第一个空闲 的共享服务器会得到这个请求,并进行处理(例如,请求可能是 UPDATE T SET X = X+5 WHERE Y = 2)。完成这个命令后,共享服务器会把响应放在原调度器 (即接收请求的调度器)的响应队列中。调度器进程一直在监听这个队列,发现 有结果后,就会把结果传给客户。从概念上讲,共享服务器请求的流程如图 2-3 所示。 图 2-3 共享服务器请求的流程步骤 如图 2-3 所示,客户连接向调度器发送一个请求。调度器首先将这个请求放在 SGA 中的请求队列中①。第一个可用的共享服务器从请求队列中取出这个请求② 并处理。共享服务器的处理结束后,再把响应(返回码、数据等)放到响应队列 中③,接下来调度器拿到这个响应④,传回给客户。 在开发人员看来,共享服务器连接和专用服务器连接之间并没有什么区别。 既然已经了解了专用服务器连接和共享服务器连接是什么,你可能会有一些 疑问: ‰ 首先怎么才能连接呢? ‰ 谁来启动这个专用服务器? ‰ 怎么与调度器联系? 这些问题的答案取决于你的特定平台,不过下一节会概括介绍一般的过程。 2.3.3 TCP/IP 连接的基本原理 这里将分析网络上最常见的一种情形:在 TCP/IP 连接上建立一个基于网络 的连接请求。在这种情况下,客户在一台机器上,而服务器驻留在另一台机器上, 这两台机器通过一个 TCP/IP 网络连接。客户率先行动,使用Oracle 客户软件(O racle 提供的一组应用程序接口,或 API)建立一个请求,力图连接数据库。例 如,客户可以发出以下命令: 这里,客户是程序SQL*Plus,scott/tiger为用户名/密码,ora10g.localdo main是一个TNS服务名。TNS代表透明网络底层(Transparent Network Substra te),这是Oracle客户中处理远程连接的“基础”软件,有了它才有可能建立对 等通信。TNS连接串告诉Oracle软件如何与远程数据库连接。一般地,你的机器 上运行的客户软件会读取一个tnsnames.ora文件。这是一个纯文本的配置文件, 通常放在 [ORACLE_HOME]\network\admin 目录下([ORACLE_HOME] 表示Oracle 安装目录的完整路径)。如果有以下配置: 根据这个配置信息,Oracle 客户软件可以把我们使用的 TNS 连接串 ora10g. localdomain 映射到某些有用的信息,也就是主机名、该主机上“监听器”进程 接受(监听)连接的端口、该主机上所连接数据库的服务名,等等。服务名表示 具有公共属性、服务级阈值和优先级的应用组。提供服务的实例数量对应用是透 明的,每个数据库实例可以向监听器注册,表示要提供多个服务。所以,服务就 映射到物理的数据库实例,并允许 DBA 为之关联阈值和优先级。 这个串(ora10g.localdomain)还可以用其他方式来解析。例如,可以使用 Oracle Internet 目录(Oracle Internet Directory,OID),这是一个分布式 轻量级目录访问协议(Lightweight Directory Access Protocol,LDAP)服务 器,其作用就相当于解析主机名的 DNS。不过,tnsnames.ora 文件通常只适用于 大多数小到中型安装,在这些情况下,这个配置文件的副本不算太多,尚可管理。 既然客户软件知道要连接到哪里,它会与主机名为 localhost.localdomain 的服务器在端口 1521 上打开一条 TCP/IP socket 连接。如果服务器 DBA 安装并 配置了 Oracle Net,并且有一个监听器在端口 1521 上监听连接请求,就会收到 这个连接。在网络环境中,我们会在服务器上运行一个称为 TNS 监听器的进程。 就是这个监听器进程能让我们与数据库物理连接。当它收到入站连接请求时,它 会使用自己的配置文件检查这个请求,可能会拒绝请求(例如,因为没有这样的 数据库,或者可能我们的 IP 地址受到限制,不允许连接这个主机),也可能会 接受请求,并真正建立连接。 如果建立一条专用服务器连接,监听器进程就会为我们创建一个专用服务 器。在 UNIX 上,这是通过 fork()和 exec()系统调用做到的(在 UNIX 中,要在 初始化之后创建新进程,惟一的办法就是通过 fork())。这个新的专用服务器 进程继承了监听器建立的连接,现在就与数据库物理地连接上了。在 Windows 上,监听器进程请求数据库进程为连接创建一个新线程。一旦创建了这个线程, 客户就会“重定向”到该线程,相应地就能建立物理连接。图 2-4 显示了 UNIX 上的监听器进程和专用服务器连接。 图 2-4 监听器进程和专用服务器连接 另一方面,如果我们发出共享服务器连接请求,监听器的表现则会有所不同。 监听器进程知道实例中运行了哪些调度器。接收到连接请求后,监听器会从可用 的调度器池中选择一个调度器进程。监听器会向客户返回连接信息,其中说明了 客户如何与调度器进程连接;如果可能的话,还可以把连接“转发”给调度器进 程(这依赖于不同的操作系统和数据库版本,不过实际效果是一样的)。监听器 发回连接信息后,它的工作就结束了,因为监听器一直在特定主机的特定端口上 运行(主机名和端口号大家都知道),而调度器会在服务器上随意指派的端口上 接受连接。监听器要知道调度器指定的这些随机端口号,并为我们选择一个调度 器。客户再与监听器断开连接,并与调度器直接连接。现在就与数据库有了一个 物理连接。这个过程如图 2-5 所示。 图 2-5 监听器进程和共享服务器连接 2.4 小结 以上就是 Oracle 体系结构的概述。在这一章中,我们给出了“实例”和“数据库”的 定义,并且了解了如何通过专用服务器连接或共享服务器连接来连接数据库。图2-6 对本章 的所有内容做了一个总结,展示了使用共享服务器连接的客户和使用专用服务器连接的客户 之间的交互方式。由此还显示出,一个 Oracle 实例可以同时使用这两类连接(实际上,即 使配置为使用共享服务器连接,Oracle 数据库也总是支持专用服务器连接)。 图 2-6 连接概述 有了以上的介绍,下面就能更深入地了解服务器底层的进程,这些进程做什 么,以及进程间如何交互。你也可以看看 SGA 的内部包含什么,它有什么用途。 下一章先来介绍 Oracle 管理数据所用的不同文件类型,并讨论各种文件类型的 作用。 第 3 章文件 3.1 参数文件 这一章,我们将分析构成数据库和实例的8 种文件类型。与实例相关的文件只有: ‰ 参数文件(parameter file):这些文件告诉 Oracle 实例在哪里可以找到控制文件,并且 指定某些初始化参数,这些参数定义了某种内存结构有多大等设置。我们还会介绍存储数据 库参数文件的两种选择。 ‰ 跟踪文件(trace file):这通常是一个服务器进程对某种异常错误条件做出响应时创建的 诊断文件。 ‰ 警告文件(alert file):与跟踪文件类似,但是包含“期望”事件的有关信息,并且通过 一个集中式文件(其中包括多个数据库事件)警告 DBA。 构成数据库的文件包括: ‰ 数据文件(data file):这些文件是数据库的主要文件;其中包括数据表、索引和所有其 他的段。 ‰ 临时文件(temp file):这些文件用于完成基于磁盘的排序和临时存储。 ‰ 控制文件(control file):这些文件能告诉你数据文件、临时文件和重做日志文件在哪里, 还会指出与文件状态有关的其他元数据。 ‰ 重做日志文件(redo log file):这些就是事务日志。 ‰ 密码文件(password file):这些文件用于对通过网络完成管理活动的用户进行认证。我 们不打算详细讨论这些文件。 从 Oracle 10g 开始,又增加了两种新的可选文件类型,可以帮助 Oracle 实 现更快的备份和更快的恢复操作。这两类新文件是: ‰ 修改跟踪文件(change tracking file):这个文件有利于对 Oracle 数据建立真正的增量备 份。修改跟踪文件不一定非得放在闪回恢复区(Flash Recovery Area),不过它只与数据库 备份和恢复有关,所以我们将在介绍闪回恢复区时再讨论这个文件。 ‰ 闪回日志文件(flashback log file):这些文件存储数据库块的“前映像”,以便完成新 增加的 FLASHBACK DATABASE 命令。 我们还会讨论通常与数据库有关的其他类型的文件,如: ‰ 转储文件(dump file ,DMP file):这些文件由 Export(导出)数据库实用程序生成, 并由 Import(导入)数据库实用程序使用。 ‰ 数据泵文件(Data Pump file):这些文件由 Oracle 10g 新增的数据泵导出(Data Pum p Export)进程生成,并由数据泵导入(Data Pump Import)进程使用。外部表也可以创建 和使用这种文件格式。 ‰ 平面文件(flat file):这些无格式文件可以在文本编辑器中查看。通常会使用这些文件 向数据库中加载数据。 以上文件中,最重要的是数据文件和重做日志文件,因为其中包含了你辛辛 苦苦才积累起来的数据。只要有这两个文件,就算是其他文件都没有了,我也能 得到我的数据。如果把重做日志文件弄丢失了,可能会丢失一些数据。如果把数 据文件和所有备份都丢失了,那么这些数据就永远也找不回来了。 下面将分别介绍上述各类文件,并分析这些文件中会有哪些内容。 与 Oracle 数据库有关的参数文件有很多,从客户工作站上的 tnsnames.ora 文件(用于“查找”网络上的一个服务器)到服务器上的 listener.ora 文件(用 于启动网络监听器),还有 sqlnet.ora、cman.ora 和 ldap.ora 等文件。不过, 最重要的参数文件是数据库的参数文件,如果没有这个参数文件,甚至无法启动 数据库。其他文件也很重要;它们涉及网络通信以及与数据库连接的各个方面。 不过,这些参数文件超出了我们的范围,这里不做讨论。要了解如何配置和建立 这些参数文件,建议你参考 Net Services Administrator’s Guide。不过,作 为开发人员,这些文件应该已经为你设置好了,不需要你来设置。 数据库的参数文件通常称为初始文件(init file),或 init.ora 文件。这 是因为历史上它的默认名就是 init.ora。之所以称之为“历史上” 的默认名,原因是从 Oracle9i Release 1 以来,对于存储数据库的参数设置, 引入了一个有很大改进的新方法:服务器参数文件(server parameter file), 或简称为 SPFILE。这个文件的默认名为 spfile.ora。接下来分别 介绍这两种参数文件。 注意 如果你还不熟悉术语 SID 或 ORACLE_SID,下面给出一个完整的定义。 SID 是站点标识符(site identifie)。在 UNIX 中,SID 和 ORACLE_HOME(Oracl e 软件的安装目录)一同进行散列运算,创建一个惟一的键名从而附加到 SGA。如 果 ORACLE_SID 或 ORACLE_HOME 设置不当,就会得到 ORACLE NOT AVAIL ABLE (ORACLE 不可用)错误,因为无法附加到这个惟一键所标识的共享内存段。 在 Windows 上,使用共享内存的方式与 UNIX 中有所不同,不过, SID 还是很重要。 同一个 ORACLE_HOME 上可以有多个数据库,所以需要有办法惟一地标识各个数 据库及相应的配置文件。 如果没有参数文件,就无法启动一个 Oracle 数据库。所以参数文件相当重 要,到了 Oracle9i Release 2(9.2 及以上版本),备份和恢复工具——恢复管 理器(Recovery Manager,RMAN)认识到了这个文件的重要性,允许把服务器参 数文件包括在备份集中(而不是遗留的 init.ora 参数文件类型)。不过,由于 init.ora 参数文件只是一个纯文本文件,可以用任何文本编辑器创建,所以这 个文件不需要你花大力气去“保卫”。只要知道文件中的内容,完全可以重新创 建(例如,如果你能访问数据库的警告日志,就可以从中获得参数文件的信息)。 下面依次介绍这两类参数文件(init.ora 和 SPFILE),不过,在此之前, 先来看看数据库参数文件是什么样子。 3.1.1 什么是参数? 简单地说,可以把数据库参数想成是一个“键”/“值”对。在上一章你已 经看到过一个很重要的参数,即 DB_NAME。这个 DB_NAME 参数简单地存储为 db_ name = ora10g。这里的“键”是 DB_NAME,“值”是 ora10g,这就是我们的键/ 值对。要得到一个实例参数的当前值,可以查询 V$视图 V$PARAMETER。另外,还 可以在 SQL*Plus 中使用 SHOW PARAMETER 命令来查看,如: 无论采用哪种方法,输出的信息基本上都一样,不过从 V$PARAMETER 能得到 更多信息(这个例子中只选择了一列,实际上还可以选择更多的列)。但是,我 还是比较倾向于使用 SHOW PARAMETER,因为这个命令使用更简单,而且它会自 动完成“通配”。注意我只键入了 pga_agg;SHOW PARAMETER 会自动在前面和后 面添加%。 注意 所有 V$视图和所有字典视图在 Oracle Database Reference 手册中都有充分的说 明。要想了解给定视图里有什么,这个手册可以作为一个权威资源。 对于 Oracle 9.0.1、9.2.0 和 10.1.0 版本,如果对可以设置的有记录的(d ocumented)参数做一个统计,可能会分别得到参数个数为 251、258 和 255(我 相信,在不同的操作系统上可能还会增加另外的参数)。换句话说,参数个数(和 参数名)因版本而异。大多数参数(如 DB_BLOCK_SIZE)留存已久(它们不会因 版本变化而消失),不过,随着时间的推移,其他的很多参数会随着实现的改变 而过时。 例如,在 Oracle 9.0.1 中有一个 DISTRIBUTED_TRANSACTIONS 参数,这个参 数可以设置为某个正整数,它能控制数据库可以执行的并发分布式事务的个数。 以前的版本中都有这个参数,但是在9.0.1 以后,这个参数就被去掉了。实际上, 如果在以后的版本中还想使用这个参数,将产生一个错误: 如果你想查看这些参数,了解有哪些参数,以及各个参数能做什么,请参考 Oracle Database Reference 手册。这个手册的第 1 章就详细地分析了每一个有 记录的参数。需要指出,一般来讲,这些参数的默认值对于大多数系统都已经足 够了(如果某些参数是从其他参数得到默认设置,则完全可以使用所得到的值)。 一般而言,要为各个数据库分别设置不同的参数值,如 CONTROL_FILES 参数(指 定系统上控制文件的位置)、DB_BLOCK_SIZE(数据库块大小)以及与内存相关 的各个参数。 注意,在上一段中我用了“有记录的”(documented)一词。还有一些无记 录的(undocumented)参数。如果参数名用下划线(_)开头,就说明这个参数 在文档中未做说明,即所谓的“无记录”。关于这些参数有很多推测。因为文档 中没有这些参数,有些人以为它们肯定是“神奇的”,许多人都认为大家都知道 这些参数,它们是 Oracle“内部人士”用的。不过在我看来,实际上恰恰相反。 这些参数并不是大家都知道的,而且也很少用到。其中大多数参数实际上令人厌 烦,因为它们表示的只是过时的功能以及为保证向后兼容性而设置的标志。还有 一些参数有助于数据的恢复,而不是数据库本身的恢复;例如,有些无记录的参 数允许数据库在某些极端环境中启动,但是时间不长,只足以把数据取出来。取 出数据后还是得重新构建。 除非 Oracle Support 明确要求,否则没有理由在你的配置中使用这种无记 录的参数。其中很多参数都有副作用,而且可能是破坏性的。在我的开发数据库 中,即使有无记录的参数,也只会设置一个这样的参数: 有了这个参数,所有人都可以读取跟踪文件,而不仅限于 DBA 小组。在我的 开发数据库上,我希望开发人员经常使用 SQL_TRACE、TIMED_STATISTICS 和 TKP ROF 实用程序(真的,我强烈建议使用它们);所以他们必须能读取跟踪文件。 不过,由于 Oracle 9.0.1 及以上版本增加了外部表,可以看到,即便是要允许 别人访问跟踪文件,也不再需要使用这个参数了。 我的生产数据库则没有设置任何无记录的参数。实际上,前面提到的看似“安 全”的无记录参数可能会在实际系统中产生不好的副作用。想想看跟踪文件中的 敏感信息,如 SQL 甚至数据值(见后面的“跟踪文件”一节),问问自己,“我 真的想让所有最终用户读取这个数据吗?”大多数情况下答案都是否定的。 警告 只有在 Oracle Support 要求的情况下才使用无记录的参数。使用这些参数可能对数 据库有害,而且这些参数在不同版本中的实现可能有变化(而且将会改变)。 可以用两种方式来设置各个参数值:只设置当前实例的参数值,或者永久性 地设置。你要确保参数文件包含你期望的值。使用遗留的init.ora 参数文件时, 这是一个手动过程。如果使用 init.ora 文件,要永久地修改一个参数值(即使 服务器重启这个新设置也有效),就必须手动地编辑和修改init.ora 参数文件。 如果是服务器参数文件,则只需一条命令就能轻松完成,这多少有些全自动的味 道。 3.1.2 遗留的 init.ora 参数文件 遗留的 Oracle init.ora 文件从结构来讲是一个相当简单的文件。这是一系 列可变的键/值对。以下是一个 init.ora 文件示例: 实际上,这与你在实际生活中可能遇到的最基本的 init.ora 文件很接近。 如果块大小正是平台上的默认块大小(默认块大小随平台不同会有所不同),可 以不要 db_block_size 参数。使用这个参数文件只是要得到数据库名和控制文件 的位置。控制文件告诉 Oracle 其他的各个文件的位置,所以它们对于启动实例 的“自启”过程(也称自举)非常重要。 既然已经知道了这些遗留的数据库参数文件是什么,也知道了在哪里能更详 细地了解可设置的有效参数,最后还需要知道这些参数文件在磁盘上的什么位 置。这个文件的命名约定默认为: 而且,默认地把它放在以下目录中: 有意思的是,许多情况下,你会发现这个参数文件中只有一行内容: IFILE 指令与 C 中的#include 很类似。它会在当前文件中包含指定文件的内 容。前面的指令就会包含一个非默认位置上的 init.ora 文件。 需要注意,参数文件不必放在特定的位置上。启动一个实例时,可以在启动 命令上使用 pfile=filename 选项。如果你想在数据库上尝试不同的 init.ora 参数,来看看不同设置带来的影响,这就非常有用。 遗留的参数文件可以利用任何纯文本编辑器来维护。例如,在 UNIX/Linux 上,我会用 vi;在很多版本的 Windows 操作系统上,我会使用记事本;在大型 机上,可能会使用 Xedit。重要的是,你要全盘负责这个文件的编辑和维护。Or acle 数据库本身没有命令可以用来维护 init.ora 文件中包含的值。例如,如果 使用 init.ora 参数文件,发出ALTER SYSTEM 命令来改变 SGA 组件的大小时,这 并不会作为一个永久修改反映到 init.ora 文件中。如果希望这个修改是永久的, 换句话说,如果希望这成为以后数据库重启时的默认值,你就要负责确保可能用 于启动数据库的所有 init.ora 参数文件都得到手动地更新。 最后要注意,有意思的是,遗留的参数文件不一定位于数据库服务器上。之 所以会引入存储参数(稍后将介绍),原因之一就是为了补救这种情况。试图启 动数据库的客户机上必须有遗留的参数文件,这说明,如果你运行一台 UNIX 服 务器,但是通过网络使用一台 Windows 台式机上安装的 SQL*Plus 来管理,这台 计算机上就需要有数据库参数文件。 我还记得,发现参数文件没有存放在服务器上时我是多么的沮丧!那是几年 前的事了,当时推出了一个全新的工具,名叫 SQL*DBA。利用这个工具,可以完 成远程操作(具体地讲,可以完成远程管理操作)。从我的服务器(那时运行的 是 SunOS),我能远程地连接一个大型机数据库服务器。而且我还能发出“关机” 命令。不过,此时我意识到遇到了麻烦,启动实例时,SQL*DBA 会“抱怨”无法 找到参数文件。我发现这些参数文件(init.ora 纯文本文件)放在客户机上, 而不是在服务器上。SQL*DBA 则是在启动大型机数据库的本地系统上查找参数。 我不仅没有这样一个文件,也不知道要在这个文件中放什么内容才能让系统再次 启动!我不知道db_name 或控制文件位置(光是得到这些大型机文件的正确的命 名约定都很困难),而且我无法访问大型机系统本身的日志。从那以后,我再也 没有犯过同样的错误;这个教训实在太惨痛了。 当 DBA 认识到 init.ora 参数文件必须放在启动数据库的客户机上时,这会 导致这些参数文件大面积“繁殖”。每个 DBA 都想从自己的桌面运行管理工具, 所以每个 DBA 都需要在自己的台式机上留有参数文件的一个副本。Oracle 企业 管理器(Oracle Enterprise Manager,OEM)之类的工具还会再增加一个参数文 件,这会使情况更加混乱。这些工具试图将所有数据库的管理都集中到一台机器 上,有时称之为“管理服务器”(management server)。这台机器会运行一个 软件,所有 DBA 均使用这个软件来启动、关闭、备份和管理数据库。听上去是一 个很不错的解决方案:把所有参数文件都集中在一个位置上,并使用GUI 工具来 完成所有操作。但事实是,完成某个管理任务时,有时直接从数据库服务器主机 上的 SQL*Plus 发出启动命令会方便得多,这样又会有多个参数文件:一个参数 文件在管理服务器上,另一个参数文件在数据库服务器上。而且这些参数文件彼 此不同步,人们可能会奇怪,为什么他们上个月做的参数修改“不见了”,不过 这些修改有可能会随机地再次出现。 所以引入了服务器参数文件(server parameter file,SPFILE),如今这 可以作为得到数据库参数设置的惟一“信息来源”。 3.1.3 服务器参数文件 在访问和维护实例参数设置方面,SPFILE 是 Oracle 做出的一个重要改变。 有了 SPFILE,可以消除传统参数文件存在的两个严重问题: ‰ 可以杜绝参数文件的繁殖。SPFILE 总是存储在数据库服务器上;必须存在于服务器主机 本身,不能放在客户机上。对参数设置来说,这样就可以只有一个“信息来源”。 ‰ 无需在数据库之外使用文本编辑器手动地维护参数文件(实际上,更确切的说法是不能 手动地维护)。利用 ALTER SYSTEM 命令,完全可以直接将值写入 SPFILE。管理员不必 再手动地查找和维护所有参数文件。 这个文件的命名约定默认为: 我强烈建议使用默认位置;否则会影响 SPFILE 的简单性。如果 SPFILE 在其 默认位置,几乎一切都会为你做好。如果将 SPFILE 移到一个非默认的位置,你 就必须告诉 Oracle 到哪里去找 SPFILE,这又会导致遗留参数文件的一大堆问题 卷土重来! 1. 转换为 SPFILE 假设有一个数据库,它使用了前面所述的遗留参数文件。转换为 SPFILE 非 常简单;这里使用了 CREATE SPFILE 命令。 注意 还可以使用一个“逆”命令从 SPFILE 创建参数文件(parameter file,PFILE), 稍后我们会解释为什么希望这样做。 所以,假设使用一个 init.ora 参数文件,而且这个 init.ora 参数文件确实 在服务器的默认位置上,那么只需发出CREATE SPFILE 命令,并重启服务器实例 就行了: 这里使用 SHOW PARAMETER 命令显示出原先没有使用 SPFILE,但是创建 SPFI LE 并重启实例后,确实使用了这个 SPFILE,而且它采用了默认名。 注意 在集群环境中,通过使用 Oracle RAC,所有实例共享同一个 SPFILE,所以要以一 种受控的方式完成这个转换过程(从 PFILE 转换为 SPFILE)。这个 SPFILE 可以包 含所有参数设置,甚至各个实例特有的设置都可以放在这一个 SPFILE 中 ,但是必 须把所有必须的参数文件合并为一个有以下格式的 PFILE。 在集群环境中,为了从使用各个 PFILE 转换为所有实例都共享一个公共的 S PFILE,需要把各个 PFILE 合并为如下一个文件: 也就是说,集群中所有实例共享的参数设置都以*.开头。单个实例特有的参 数设置(如 INSTANCE_NUMBER 和所用的重做 THREAD)都以实例名(Oracle SID) 为前缀。在前面的例子中, ‰ PFILE 对应包含两个节点的集群,其中的实例分别为 O10G1 和 O10G2。 ‰ *.db_name = 'O10G'这个赋值指示,使用这个 SPFILE 的所有实例会装载一个名为 O10G 的数据库。 ‰ O10G1.undo_tablespace='UNDOTBS1'指示,名为 O10G1 的实例会使用这个特定的撤销(u ndo)表空间,等等。 2. 设置 SPFILE 中的参数值 一旦根据 SPFILE 启动并运行数据库,下一个问题就是如何设置和修改其中 的值。要记住,SPFILE 是二进制文件,它们可不能用文本编辑器来编辑。这个 问题的答案就是使用 ALTER SYSTEM 命令,语法如下(< > 中的部分是可选的, 其中的管道符号(|)表示“取候选列表中的一个选项”): 默认情况下,ALTER SYSTEM SET 命令会更新当前运行的实例,并且会为你修 改 SPFILE,这就大大简化了管理;原先使用 init.ora 参数文件时,通过 ALTER SYSTEM 命令设置参数后,如果忘记更新 init.ora 参数文件,或者把 init.ora 参数文件丢失了,就会产生问题,使用 SPFILE 则会消除这些问题。 记住这一点,下面来详细分析这个命令中的各个元素: ‰ parameter=value 这个赋值提供了参数名以及参数的新值。例如,pga_aggregate_target = 1024m 会把 PGA_AGGREGATE_TARGET 参数值设置为 1 024 MB(1 GB)。 ‰ comment='text'是一个与此参数设置相关的可选注释。这个注释会出现在 V$PARAMETE R 视图的 UPDATE_COMMENT 字段中。如果使用了相应选项允许同时保存对 SPFILE 的修 改,注释会写入 SPFILE,而且即便服务器重启也依然保留,所以将来重启数据库时会看到 这个注释。 ‰ deferred 指定系统修改是否只对以后的会话生效(对当前建立的会话无效,包括执行此修 改的会话)。默认情况下,ALTER SYSTEM 命令会立即生效,但是有些参数不能“立即” 修改,只能为新建立的会话修改这些参数。可以使用以下查询来看看哪些参数要求必须使用 deferred: 上面的代码表明,SORT_AREA_SIZE 可以在系统级修改,但是必须以延迟方式 修改。以下代码显示了有deferred 选项和没有 deferred 选项时修改这个参数的 值会有什么结果: ‰ SCOPE=MEMORY|SPFILE|BOTH 指示了这个参数设置的“作用域”。设置参数值时作用 域有以下选择: „ SCOPE=MEMORY 只在实例中修改;数据库重启后将不再保存。下一次重启数 据库时,设置还是修改前的样子。 „ SCOPE=SPFILE 只修改 SPFILE 中的值。数据库重启并再次处理 SPFILE 之前, 这个修改不会生效。有些参数只能使用这个选项来修改,例如,processes 参数 就必须使用 SCOPE=SPFILE,因为我们无法修改活动实例的 processes 值。 „ SCOPE=BOTH 是指,内存和 SPFILE 中都会完成参数修改。这个修改将反映在 当前实例中,下一次重启时,这个修改也会生效。这是使用 SPFILE 时默认的作 用域值。如果使用 init.ora 参数文件,默认值则为 SCOPE=MEMORY,这也是此 时惟一合法的值。 ‰ sid='sid|*'主要用于集群环境;默认值为 sid='*'。这样可以为集群中任何给定的实例惟一 地指定参数设置。除非你使用 Oracle RAC,否则一般不需要指定 sid=设置。 这个命令的典型用法很简单: 或者,更好的做法是,还可以指定 COMMENT=赋值来说明何时以及为什么执行某 个修改: 3. 取消 SPFILE 中的值设置 下一个问题又来了,“好吧,这样就设置了一个值,但是现在我们又想‘取 消这个设置’,换句话说,我们根本不希望 SPFILE 有这个参数设置,想把它删 掉。但是既然不能使用文本编辑器来编辑这个文件,那我们该怎么办呢?”同样 地,这也要通过 ALTER SYSTEM 命令来完成,但是要使用 RESET 子句: 在这里,SCOPE/SID 设置的含义与前面一样,但是 SID=部分不再是可选的。 Oracle SQL Reference 手册在介绍这个命令时有点误导,因为从手册来看,好 像这只对 RAC(集群)数据库有效。实际上,手册中有下面的说明: alter_system_reset_clause(ALTER SYSTEM 命令的 RESET 子句)用于“真 正应用集群”(RAC)环境。 接下来,它又说: 在非 RAC 环境中,可以为这个子句指定 SID='*'。 这就有点让人糊涂了。不过,要从 SPFILE“删除”参数设置,也就是仍然采 用参数原来的默认值,就要使用这个命令。所以,举例来说,如果我们想删除 S ORT_AREA_SIZE,以允许使用此前指定的默认值,可以这样做: 这样会从 SPFILE 中删除 SORT_AREA_SIZE,通过执行以下命令可以验证这一点: 然后可以查看/tmp/pfile.tst 的内容,这个文件将在数据库服务器上生成。 可以看到,参数文件中不再有 SORT_AREA_SIZE 参数了。 4. 从 SPFILE 创建 PFILE 上一节用到的 CREATE PFILE...FROM SPFILE 命令刚好与 CREATE SPFILE 相 反。这个命令根据二进制 SPFILE 创建一个纯文本文件,生成的这个文件可以在 任何文本编辑器中编辑,并且以后可以用来启动数据库。正常情况下,使用这个 命令至少有两个原因: ‰ 创建一个“一次性的”参数文件,用于启动数据库来完成维护,其中有一些特殊的设置。 所以,可以执行 CREATE PFILE...FROM SPFILE 命令,并编辑得到的文本 PFILE,修改所 需的设置。然后启动数据库,使用 PFILE=选项指定要使用这个 PFILE 而不是 SPFILE。完成后,可以正常地启动,数据库又会使用 SPFILE。 ‰ 维护修改历史,在注释中记录修改。过去,许多 DBA 会在参数文件中加大量的注释,来 记录修改历史。例如,如果一年内把缓冲区缓存大小修改过 20 次,在 db_cache_size init.or a 参数设置前就会有 20 条注释,这些注释会指出修改的日期,以及修改的原因。SPFILE 不 支持这样做,但是如果习惯了以下用法,也可以达到同样的效果: 通过这种方式,修改历史就会在一系列参数文件中长久保存。 5. 修正被破坏的 SPFILE 关于 SPFILE 还有最后一个问题,“SPFILE是二进制文件,如果 SPFILE 被破 坏了,数据库无法启动,那该怎么办?还是 init.ora 文件更好一些,至少它是 文本文件,我们可以直接编辑和修正”。嗯,这么说吧,SPFILE 不会像数据文 件、重做日志文件、控制文件等那样被破坏,但是,倘若真的发生了这种情况, 还是有几种选择的。 首先,SPFILE 中的二进制数据量很小。如果在 UNIX 平台上,只需一个简单 的 strings 命令就能提取出所有设置: 在 Windows 上,则要用 write.exe(WordPad,即写字板)打开这个文件。Word Pad 会显示出文件中的所有文本,只需将其剪切并粘贴到 init.ora 中,就能创建启动实例的 PFILE。 万一 SPFILE 丢失了(不论是什么原因,反正我没有见过SPFILE 消失的情况), 还可以从数据库的警告日志恢复参数文件的信息(稍后将介绍警告日志的更多内 容)。每次启动数据库时,警告日志都会包含如下一部分内容: 通过这一部分内容,可以很容易地创建一个 PFILE,再用 CREATE SPFILE 命 令将其转换为一个新的 SPFILE。 3.1.4 参数文件小结 在这一节中,我介绍了管理 Oracle 初始化参数和参数文件的所有基础知识。 我们了解了如何设置参数、查看参数值,以及如何让这些设置在数据库重启时依 然保留。我们分析了两类数据库参数文件:传统的 PFILE(简单的文本文件)和 SPFILE(服务器参数文件)。对于所有现有的数据库,都推荐使用 SPFILE,因 为这更易于管理,而且也更为简洁。由于数据库的参数只有一个“信息来源”, 而且可以使用 ALTER SYSTEM 命令持久地保存参数值,这使得SPFILE 相当引人注 目。自从有了 SPFILE,我就一直在使用 SPFILE,而且从来没有想过再回头去使 用 PFILE。 3.2 跟踪文件 跟踪文件(Trace file)能提供调试信息。服务器遇到问题时,它会生成一个包含大量诊断 信息的跟踪文件。如果开发人员设置了 SQL_TRACE=TRUE,服务器就会生成一个包含性 能相关信息的跟踪文件。我们之所以可以使用这些跟踪文件,是因为 Oracle 是一个允许充 分测量的软件。我所说的“可测量”(instrumented)是指,编写数据库内核的程序员在内 核中放入了调试代码,而且调试代码相当多。这些调试代码仍然被程序员有意留在内核中。 我见过许多开发人员都认为调试代码会带来开销,认为系统投入生产阶段之 前必须把这些调试代码去掉,希望从代码中“挤出”点滴的性能。当然,随后他 们可能又会发现代码中有一个“bug”,或者“运行得没有应有的那么快”(最 终用户也把这称为“bug”。对于最终用户来说,性能差就是 bug!)。此时, 他们多么希望调试代码还在原处未被删掉(或者,如果原来没有加过调试代码, 他们可能很后悔当初为什么没有添加)。特别是,他们无法再向生产系统中添加 调试代码,在生产环境中,新代码必须先经过测试,这可不是说添加就添加那么 轻松。 Oracle 数据库(以及应用服务器和 Oracle 应用)都是可以充分测量的。数 据库中这种测量性反映在以下几方面: ‰ V$视图:大多数 V$视图都包含“调试”信息。V$WAITSTAT、V$SESSION_EVENT 还 有其他许多 V$视图之所以存在,就是为了让我们知道内核内部到底发生了什么。 ‰ 审计命令:利用这个命令,你能指定数据库要记录哪些事件以便日后分析。 ‰ 资源管理器(DBMS_RESOURCE_MANAGER):这个特性允许你对数据库中的资源(C PU、I/O 等)实现微管理。正是因为数据库能访问描述资源使用情况的所有运行时统计信息, 所以才可能有资源管理器。 ‰ Oracle“事件”:基于 Oracle 事件,能让 Oracle 生成所需的跟踪或诊断信息。 ‰ DBMS_TRACE:这是 PL/SQL 引擎中的一个工具,它会全面地记录存储过程的调用树、 所产生的异常,以及遇到的错误。 ‰ 数据库事件触发器:这些触发器(如 ON SERVERERROR)允许你监控和记录你觉得“意 外”或非正常的情况。例如,可以记录发生“临时空间用尽”错误时正在运行的 SQL。 ‰ SQL_TRACE:这个 SQL 跟踪工具还可以采用一种扩展方式使用,即通过 10046 Oracle 事件。 还不止这些。在应用设计和开发中,测量至关重要,每一版 Oracle 数据库 的测量性都越来越好。实际上,Oracle9i Release 2 和 Oracle 10g Release 1 这两个版本之间增加的测量代码量就相当显著。Oracle 10g 将内核中的代码测 量发展到一个全新的层次。 在这一节中,我们将重点讨论各种跟踪文件中的信息。这里会分析有哪些跟 踪文件,这些跟踪文件存放在哪里,以及对这些跟踪文件能做些什么。 通常有两类跟踪文件,对这两类跟踪文件的处理完全不同: ‰ 你想要的跟踪文件:例如,启用 SQL_TRACE=TRUE 选项的结果,其中包含有关会话的 诊断信息,有助于你调整应用,优化应用的性能,并诊断出遭遇的瓶颈。 ‰ 你不想要的跟踪文件,但是由于出现了以下错误,服务器会自动生成这些跟踪文件。这 些错误包括 ORA-00600“Internal Error”(内部错误)、ORA-03113“End of file on communi cation channel”(通信通道上文件结束)或 ORA-07445“Exception Encountered”(遇到异常)。 这些跟踪文件包含一些诊断信息,它们主要对 Oracle Support 的分析人员有用,但对我们来 说,除了能看出应用中哪里出现了内部错误之外,用处不大。 3.2.1 请求的跟踪文件 你想要的跟踪文件通常都是因为设置了 SQL_TRACE=TRUE 生成的结果,或者 是通过 10046 事件使用扩展的跟踪工具生成的,如下所示: 1. 文件位置 不论是使用 SQL_TRACE 还是扩展的跟踪工具,Oracle都会在数据库服务器主 机的以下两个位置生成一个跟踪文件: ‰ 如果使用专用服务器连接,会在 USER_DUMP_DEST 参数指定的目录中生成跟踪文件。 ‰ 如果使用共享服务器连接,则在 BACKGROUND_DUMP_DEST 参数指定的目录中 生成跟踪文件。 要想知道跟踪文件放在哪里,可以从 SQL*Plus 执行 SHOW PARAMETER DUMP_ DEST 命令来查看,也可以直接查询 V$PARAMETER 视图: 这里显示了 3 个转储(跟踪)目标。后台转储(background dump)目标由 所有“服务器”进程使用(第 5 章会全面介绍 Oracle 后台进程及其作用)。 如果使用 Oracle 的共享服务器连接,就会使用一个后台进程;因此,跟踪文件 的位置由 BACKGROUND_DUMP_DEST 确定。如果使用的是专用服务器连接,则会使用 一个用户或前台进程与 Oracle 交互;所以跟踪文件会放在 USER_DUMP_DEST 参数指 定的目录中。如果出现严重的 Oracle 内部错误(如 UNIX 上的“segmentation fau lt”错误),或者如果 Oracle Support 要求你生成一个跟踪文件来得到额外的调 试信息,CORE_DUMP_DEST 参数则定义了此时这个“内核”文件应该放在哪里。一般 而言,我们只对后台和用户转储目标感兴趣。需要说明,除非特别指出,这本书里 都使用专用服务器连接。 如果你无法访问 V$PARAMETER 视图,那么可以使用 DBMS_UTILITY 来访问大多数 (但不是全部)参数的值。从下面的例子可以看出,要看到这个信息(还不止这些), 只需要 CREATE SESSION 权限: 2. 命名约定 Oracle 中跟踪文件的命名约定总在变化,不过,如果把你的系统上的跟踪文 件名作为示例,应该能很容易地看出这些命名有一个模板。例如,在我的各台服 务器上,跟踪文件名如表 3-1 所示。 表 3-1 跟踪文件名示例 跟踪文件名 平 台 数据库版本 ora10g_ora_24574.trc Linux 10g Release 1 ora9ir2_ora_24628.trc Linux 9i Release 2 ora_10583.trc Linux 9i Release 1 ora9ir2w_ora_688.trc Windows 9i Release 2 ora10g_ora_1256.trc Windows 10g Release 1 在我的服务器上,跟踪文件名可以分为以下几部分: ‰ 文件名的第一部分是 ORACLE_SID(但 Oracle9i Release 1 例外,在这一版本中,Oracl e 决定去掉这一部分)。 ‰ 文件名的下一部分只有一个 ora。 ‰ 跟踪文件名中的数字是专用服务器的进程 ID, 可以从 V$PROCESS 视图得到。 因此,在实际中(假设使用专用服务器模式),需要访问 4 个视图: ‰ V$PARAMETER:找到 USER_DUMP_DEST 指定的跟踪文件位置。 ‰ V$PROCESS:查找进程 ID。 ‰ V$SESSION:正确地标识其他视图中的会话信息。 ‰ V$INSTANCE:得到 ORACLE_SID。 前面提到过,可以使用 DBMS_UTILITY 来找到位置,而且通常你“知道”ORAC LE_SID,所以从理论上讲只需要访问 V$SESSION 和 V$PROCESS,但是,为了便于 使用,这 4 个视图你可能都想访问。 以下查询可以生成跟踪文件名: 显然,在 Windows 平台上要把 / 换成 \。如果使用 9i Release 1,只需发 出以下查询,不用在跟踪文件名中增加实例名: 3. 对跟踪文件加标记 有一种办法可以对跟踪文件“加标记”,这样即使你无权访问 V$PROCESS 和 V$SESSION,也能找到跟踪文件。假设你能读取 USER_DUMP_DEST 目录,就可以使 用会话参数 TRACEFILE_IDENTIFIER。采用这种方法,可以为跟踪文件名增加一 个可以惟一标识的串,例如: 可以看到,跟踪文件还是采用标准的_ora_格式 命名,但是这里还有我们为它指定的一个惟一的串,这样就能很容易地找到“我 们的”跟踪文件名。 3.2.2 针对内部错误生成的跟踪文件 这一节最后我再来谈谈另一类跟踪文件,这些跟踪文件不是我们想要的,只 是由于 ORA-00600 或另外某个内部错误而自动生成。对这些跟踪文件我们能做些 什么吗? 答案很简单,一般来讲,这些跟踪文件不是给你我用的。它们只对 Oracle Support 有用。不过,我们向 Oracle Support 提交 iTAR 时,这些跟踪文件会很 有用。有一点很重要:如果得到内部错误,修改这个错误的惟一办法就是提交一 个 iTAR。如果你只是将错误忽略,除非出现意外,否则它们不会自行修正。 例如,在 Oracle 10g Release 1 中,如果创建下表,并运行以下查询,就 会得到一个内部错误(也可能不会得到错误,因为这个错误已经作为一个 bug 提交,并在后来的补丁版本中得到修正): 如果你是一名 DBA,会发现用户转储目标中突然冒出这个跟踪文件。 或者, 如果你是一名开发人员,你的应用将产生一个ORA-00600 错误,你肯定想知道到 底发生了什么。跟踪文件中信息很多(实际上,另外还有 35 000 行),但一般 来讲,这些信息对你我来说都没有用。我们只是想压缩这个跟踪文件,并将其上 传来完成 iTAR 处理。 不过,确实有些信息能帮助你跟踪到“谁”造成了错误,错误是“什么”, 以及错误在“哪里”,另外,利用 http://metalink.oracle.com,你还能发现 这些问题是不是别人已经遇到过(许多次),以及为什么会出现这些错误。快速 检查一下跟踪文件的最前面,你会得到一些有用的信息,如: 你在 http://metalink.oracle.com 上提交 iTAR 时,数据库信息当然很重要, 不仅如此,如果在 http://metalink.oracle.com 上查看是否以前已经提出过这 个问题,这些数据库信息也很有用。另外,可以看出错误出现在哪个 Oracle 实 例上。并发地运行多个实例是很常见的,所以把问题隔离到一个实例上会很有用。 跟踪文件中的这一部分是 Oracle 10g 新增的,Oracle9i 里没有。它显示了 V$SESSION 的 ACTION 和 MODULE 列中的会话信息。这里可以看到,是一个 SQL*P lus 会话导致了错误(开发人员应该设置 ACTION 和 MODULE 信息;有些环境已经 为你做了这项工作,如 Oracle Forms 和 HTML DB)。 另外还可以得到 SERVICE NAME。这就是连接数据库所用的服务名(这里就是 SYS$USERS),由此看出没有通过TNS 服务来连接。如果使用user/pass@ora10g. localdomain 登录,可以看到: 其中 ora10g 是服务名(而不是 TNS 连接串;这是所连接 TNS 监听器中注册 的最终服务)。这对于跟踪哪个进程/模块受此错误影响很有用。 最后,在查看具体的错误之前,可以看到会话 ID 和相关的日期/时间等进一 步的标识信息(所有版本都提供了这些信息): 现在可以深入到内部看看错误本身了: 这里也有一些重要的信息。首先,可以看到产生内部错误时正在执行的 SQL 语句,这有助于跟踪哪个(哪些)应用会受到影响。同时,由于这里能看到 SQL, 所以可以研究采用哪些“迂回路线”,用不同的方法编写 SQL,看看能不能很快 绕过问题解决 bug。另外,也可以把出问题的 SQL 剪切并粘贴到 SQL*Plus 中, 看看能不能为 Oracle Support 提供一个可再生的测试用例(当然,这些是最棒 的测试用例)。 另一个重要信息是错误码(通常是 600、3113 或 7445)以及与错误码相关的 其他参数。使用这些信息,再加上一些栈跟踪信息(显示按顺序调用的一组 Ora cle 内部子例程),可能会发现这个 bug 已经报告过(还能找到解决方法、补丁 等)。例如,使用以下查询串: 利用 MetaLink 的高级搜索(全文搜索 bug 数据库),很快就能发现 bug 38006 14, “ORA-600 [12410] ON SIMPLE QUERY WITH ANALYTIC FUNCTION”。如果访问 http://metalink.oracle.com,并使用这个文本进行搜索,可以找到这个 bug,了 解到下一版中已经修正了这个 bug,并注意到已经有相应的补丁,所有这些信息我 们都能得到。很多次我都发现,所遇到的错误以前已经出现过,而且事实上已经有 了修正和解决的办法。 3.2.3 跟踪文件小结 现在你知道有两种一般的跟踪文件,它们分别放在什么位置,以及如何找到 这些跟踪文件。希望你使用跟踪文件主要是为了调整和改善应用的性能,而不只 是提交 iTAR。最后再说一句,Oracle Support 确实会利用文档中没有记录的一 些“事件”,如果数据库遭遇错误,可以利用这些事件得到大量的诊断信息。例 如,如果得到一个 ORA-01555,但你自认为不该有这个错误,此时 Oracle Supp ort 就会教你设置这种诊断事件,每次遇到错误时都会创建一个跟踪文件,由此 可以帮助你准确地跟踪到为什么会产生错误。 3.3 警告文件 这一章中,我们将分析构成数据库和实例的8 种文件类型。与实例相关的文件只 有: 3.4 数据文件 数据文件和重做日志文件是数据库中最重要的文件。你的数据最终就是要存 储在数据文件中。每个数据库都至少有一个相关的数据文件,通常还不止一个。 最简单的“测试”数据库只有一个数据文件。实际上,在第 2 章中我们已经见过 一个例子,其中用最简单的CREATE DATABASE 命令根据默认设置创建了一个数据 库,这个数据库中有两个数据文件,其中一个对应 SYSTEM 表空间(真正的 Orac le 数据字典),另一个对应 SYSAUX 表空间(在 10g 及以上版本中,非字典对象 都存储在这个表空间中)。不过,所有实际的数据库都至少有 3 个数据文件;一 个存储 SYSTEM 数据,一个存储 SYSAUX 数据,还有一个存储 USER 数据。 简要回顾文件系统类型之后,我们将讨论如何组织这些文件,以及文件中如 何组织数据。要了解这些内容,需要知道什么是表空间(tablespace)、什么是 段(segment)、什么是区段(extent),以及什么是块(block)。这些都是 O racle 在数据库中存储对象所用的分配单位,稍后将详细介绍。 3.4.1 简要回顾文件系统机制 在 Oracle 中,可以用 4 种文件系统机制存储你的数据。这里强调了“你的 数据”,是指你的数据字典、redo 记录、undo 记录、表、索引、LOB 等,也就 是你自己每天关心的数据。简单地讲,这包括: ‰ “Cooked”操作系统(OS)文件系统:这些文件就像字处理文档一样放在文件系统中。 在 Windows 资源管理器中可以看到这些文件,在 UNIX 上,可以通过 ls 命令看到这些文件。 可以使用简单的 OS 工具(如 Windows 上的 xcopy 或 UNIX 上的 cp)来移动文件。从历史 上看,Cooked OS 文件一直是 Oracle 中存储数据的“最流行”的方法,不过我个人认为, 随着 ASM(稍后再详细说明)的引入,这种情况会有所改观。Cooked 文件系统(“加工” 文件系统或“熟”文件系统)通常也会缓存,这说明在你读写磁盘时,OS 会为你缓存信息。 ‰ 原始分区(raw partitions,也称裸分区):这不是文件,而是原始磁盘。不能用 ls 来查 看;不能在 Windows 资源管理器中查看其内容。它们就是磁盘上的一些大扇区,上面没有 任何文件系统。对 Oracle 来说,整个原始分区就是一个大文件。这与 cooked 文件系统不同, cooked 文件系统上可能有几十个甚至数百个数据库数据文件。目前,只有极少数 Oracle 安 装使用原始分区,因为原始分区的管理开销很大。原始分区不是缓冲设备,所完成的所有 I /O 都是直接 I/O,对数据没有任何 OS 缓冲(不过,对于数据库来说,这通常是一个优点)。 ‰ 自动存储管理(Automatic Storage Management,ASM):这是 Oracle 10g Release 1 的一个新特性(标准版和企业版都提供了这个特性)。ASM 是专门为数据库设计的文件系 统。可以简单地把它看作一个数据库文件系统。在这个文件系统上,不是把购物清单存储在 文本文件中;这里只能存储与数据库相关的信息:你的表、索引、备份、控制文件、参数文 件、重做日志、归档文件等。不过,即使是 ASM,也同样存在着相应的数据文件;从概念 上讲,数据库仍存储在文件中,不过现在的文件系统是 ASM。ASM 设计成可以在单机环境 或者集群环境中工作。 ‰ 集群文件系统:这个文件系统专用于 RAC(集群)环境,看上去有些像由集群环境中多 个节点(计算机)共享的 cooked 文件系统。传统的 cooked 文件系统只能由集群环境中的一 台计算机使用。所以,尽管可以在集群中的多个节点之间使用 NFS 装载或 Samba 共享一个 cooked 文件系统(Samba 与 NFS 类似,可以在 Windows/UNIX 环境之间共享磁盘),但这 会导致一损俱损。如果安装有文件系统并提供共享的节点失败,这个文件系统都将不可用。 Oracle 集群文件系统(Oracle Cluster File System,OCFS)是 Oracle 在这个领域推出的一 个新的文件系统,目前只能在 Windows 和 Linux 上使用。其他第三方开发商也提供了一些 经认证的集群文件系统,也可以用于 Oracle。集群文件系统让 cooked 文件系统的优点延伸 到了集群环境中。 有意思的是,数据库可能包含来自上述所有文件系统中的文件,你不必只选 其中的一个。在你的数据库中,可能部分数据存储在一个传统的 cooked 文件系 统中,有些在原始分区上,有一些在 ASM 中,还有一些在集群文件系统中。这样 就能很容易地切换技术,或者只是涉及一个新的文件系统,而不必把整个数据库 都搬到这个文件系统中。现在,因为完整地讨论文件系统及其详细的属性超出了 本书的范围,所以我们还是回过头来深入探讨 Oracle 文件类型。不论文件是存 储在 cooked 文件系统、原始分区、ASM 中,还是存储在集群文件系统中,以下 概念都适用。 3.4.2 Oracle 数据库中的存储层次体系 数据库由一个或多个表空间构成。表空间(tablespace)是 Oracle 中的一个 逻辑存储容器,位于存储层次体系的顶层,包括一个或多个数据文件。这些文件可 能是文件系统中的 cooked 文件、原始分区、ASM 管理的数据库文件,或者是集群文 件系统上的文件。表空间包含段,请看下面的介绍。 1. 段 现在开始分析存储层次体系,首先讨论段,这是表空间中主要的组织结构。 段(segment)就是占用存储空间的数据库对象,如表、索引、回滚段等。创建 表时,会创建一个表段。创建分区表时,则每个分区会创建一个段。创建索引时, 就会创建一个索引段,依此类推。占用存储空间的每一个对象最后都会存储在一 个段中,此外还有回滚段(rollback segment)、临时段(temporary segment)、 聚簇段(cluster segment)、索引段(index segment)等。 注意 上面有这样一句话:“占用存储空间的每一个对象最后都会存储在一个段中”, 这可能会把你搞糊涂。你会发现许多 CREATE 语句能创建多段的对象。之所以会产 生困惑,原因是一条 CREATE 语句最后创建的对象可能包含 0 个、1 个或多个段! 例如,CREATE TABLE T ( x int primary key, y clob)就会创建 4 个段:一个是 T ABLE T 的段,还有一个段对应索引(这个索引是为支持主键而创建的),另外还 有两个 CLOB 段(一个 CLOB 段是 LOB 索引,另一个段是 LOB 数据本身)。与之 不同,CREATE TABLE T ( x int, y date ) cluster MY_CLUSTER 则不会创建任 何段。第 10 章还会更深入地讨论这个概念。 2. 区段 段本身又由一个或多个区段组成。区段(extent)是文件中一个逻辑上连续 分配的空间(一般来讲,文件本身在磁盘上并不是连续的;否则,根本就不需要 消除磁盘碎片的工具了!)。另外,利用诸如独立磁盘冗余阵列(Redundant A rray of Independent Disks,RAID)之类的磁盘技术,你可能会发现,一个文 件不仅在一个磁盘上不连续,还有可能跨多个物理磁盘。每个段都至少有一个区 段,有些对象可能还需要至少两个区段(回滚段就至少需要两个区段)。如果一 个对象超出了其初始区段,就会请求再为它分配另一个区段。第二个区段不一定 就在磁盘上第一个区段旁边,甚至有可能不在第一个区段所在的文件中分配。第 二个区段可能与第一个区段相距甚远,但是区段内的空间总是文件中的一个逻辑 连续空间。区段的大小可能不同,可以是一个Oracle 数据块,也可以大到2 GB。 3. 块 区段又进一步由块组成。块(block)是 Oracle 中最小的空间分配单位。数据 行、索引条目或临时排序结果就存储在块中。通常 Oracle 从磁盘读写的就是块。O racle 中块的常见大小有 4 种:2 KB、4 KB、8 KB 或 16 KB(尽管在某些情况下 32 KB 也是允许的;但是操作系统可能对最大大小有限制)。 注意 有一点可能很多人都不知道:数据库的默认块大小不必是 2 的幂。2 的幂只是一个 常用的惯例。实际上,你完全可以创建块大小为 5 KB、7 KB 或 n KB 的数据库, 这里 n 介于 2~32 KB 之间。不过,我还是建议你在实际中不要考虑这样做,块大 小还是用 2 KB、4 KB、8 KB 或 16 KB 比较好。 段、区段和数据块之间的关系如图 3-1 所示。 一个段由一个或多个区段组成,区段则由连续分配的一些块组成。从 Oracl e9i Release 1 起,数据库中最多可以有 6 种不同的块大小(block size)。 图3-1 段、区段和数据块 注意 之所以引入这个特性,即一个数据库中允许有多种块大小,目的是为了可以在更 多的情况下使用可传输的表空间。如果能传输表空间,DBA 就能从一个数据库移动 或复制格式化的数据文件,把它放在另一个数据库中,例如,可以从一个联机事务 处理(Online Transaction Processing,OLTP)数据库中把所有表和索引复制到一个 数据仓库(Data Warehouse,DW)中。不过,在许多情况下,OLTP 数据库使用的 块大小可能很小,如 2 KB 或 4 KB,而 DW 使用的块大小可能很大(8 KB 或 16 KB)。如果一个数据库中不支持多种块大小,就无法传输这些信息。有多种块大小 的表空间主要用于传输表空间,一般没有其他用途。 数据库还有一个默认的块大小,即执行 CREATE DATABASE 命令时初始化文件 中指定的大小。SYSTEM 表空间总是使用这个默认块大小,不过你完全可以按非 默认块大小(2 KB、4 KB、8 KB 或 16 KB)创建其他表空间,如果操作系统允许, 还可以使用 32 KB 的块大小。当且仅当创建数据库时指定了一个非标准的块大小 (不是 2 的幂)时,才会有 6 种不同的块大小。因此,在实际中,数据库最多有 5 种不同的块大小:默认大小和另外 4 种非默认的块大小。 在所有给定的表空间内部,块大小都是一致的,这说明,一个表空间中的所 有块大小都相同。对于一个多段对象,如一个包含 LOB 列的表,可能每个段在不 同的表空间中,而这些表空间分别有不同的块大小,但是任何给定段(包含在表 空间中)都由相同大小的块组成。无论大小如何,所有块格式都一样,如图 3-2 所示。 图3-2 块结构 块首部(block header)包含块类型的有关信息(表块、索引块等)、块上 发生的活动事务和过去事务的相关信息(仅事务管理的块有此信息,例如临时排 序块就没有事务信息),以及块在磁盘上的地址(位置)。块中接下来两部分是 表目录和行目录,最常见的数据库块中(即堆组织表的数据块)都有这两部分。 第 10 章将更详细地介绍数据库表类型,不过,现在知道大多数表都是这种类型 就足够了。如果有表目录(table directory),则其中会包含把行存储在这个 块上的表的有关信息(可能一个块上存储了多个表的数据)。行目录(row dir ectory)包含块中行的描述信息。这是一个指针数组,指向块中数据部分中的行。 块中的这 3 部分统称为块开销(block overhead),这部分空间并不用于存放数 据,而是由 Oracle 用来管理块本身。块中余下的两部分就很清楚了:块上可能 有一个空闲空间(free space),通常还会有一个目前已经存放数据的已用空间 (used space)。 从以上介绍可以知道,段由区段组成,区段由块组成,对段有了大致的了解 后,下面再来更深入地分析表空间,然后说明文件在这个存储层次体系中的位置。 4. 表空间 前面已经提到,表空间是一个容器,其中包含有段。每个段都只属于一个表 空间。一个表空间中可能有多个段。一个给定段的所有区段都在与段相关联的表 空间中。段绝对不会跨越表空间边界。表空间本身可以有一个或多个相关的数据 文件。表空间中给定段的一个区段完全包含在一个数据文件中。不过,段可以有 来自多个不同数据文件的区段。表空间如图 3-3 所示。 图 3-3 这个表空间包含两个数据文件、3 个段和 4 个区段 图 3-3 显示了一个名为 USER_DATA 的表空间。其中包括两个数据文件:user _data01 和 user_data02。并分配了 3 个段:T1、T2 和 I1(可能是两个表和一个 索引)。这个表空间中分配了 4 个区段,每个区段表示为逻辑上连续分配的一组 数据库块。段 T1 包括两个区段,分别在不同的文件中。段 T2 和 I1 都各有一个 区段。如果这个表空间需要更多的空间,可以调整已经分配给表空间的数据文件 的大小,或者可以再增加第三个数据文件。 表空间是 Oracle 中的逻辑存储容器。作为开发人员,我们会在表空间中创 建段,而绝对不会深入到原始的“文件级”。我们可不希望在一个特定的文件中 分配区段(当然这也是可以的,但我们一般都不会这么做)。相反,我们会在表 空间中创建对象,余下的工作都由 Oracle 负责。如果将来某个时刻 DBA 决定在 磁盘上移动数据文件,从而使 I/O 分布得更均匀,这对我们来说没有任何关系, 它根本不会影响我们的处理。 5. 存储层次体系小结 总结一下,Oracle 中的存储层次体系如下: (1) 数据库由一个或多个表空间组成。 (2) 表空间由一个或多个数据文件组成。这些文件可以是文件系统中的 coo ked 文件、原始分区、ASM 管理的数据库文件,或集群文件系统上的文件。表空 间包含段。 (3) 段(TABLE、INDEX 等)由一个或多个区段组成。段在表空间中,但是可 以包含这个表空间中多个数据文件中的数据。 (4) 区段是磁盘上一组逻辑连续的块。区段只在一个表空间中,而且总是在 该表空间内的一个文件中。 (5) 块是数据库中最小的分配单位,也是数据库使用的最小 I/O 单位。 3.4.3 字典管理和本地管理的表空间 在继续讨论之前,我们再来看看关于表空间的一个问题:在表空间中如何管理 区段。在 Oracle 8.1.5 之前,表空间中管理区段的分配只有一种方法:字典管理 的表空间(dictionary-managed tablespace)。也就是说,表空间中的空间在数 据字典表中管理,这与管理账户数据(利用 DEBIT 和 CREDIT 表)的方法是一样的。 借方有已经分配给对象的所有区段。贷方是所有可用的自由区段。如果一个对象 需要另一个区段,就会向系统“申请”。然后 Oracle 访问其数据字典表,运行一 些查询,查找到空间(也许找不到),然后更新一个表中的一行(或者从表中将 这一行删除),再向另一个表插入一行。Oracle 管理空间与你编写应用可谓异曲 同工:同样是要修改数据以及移动数据。 为了得到额外的空间而在后台代表你执行的 SQL 称为递归 SQL(recursive SQL)。你的 SQL INSERT 语句会导致执行其他递归 SQL 来得到更多空间。如果 频繁地执行这种递归 SQL,开销可能相当大。对数据字典的这种更新必须是串行 的;它们不能同时进行,所以要尽量避免。 在 Oracle 的早期版本中,可以看到,这种空间管理问题(递归 SQL 开销) 在“临时表空间”中最常见(这还不是“真正的”临时表空间,真正的临时表空 间是通过 CREATE TEMPORARY TABLESPACE 命令创建的)。空间会频繁地分配(从 字典表删除,而插入到另一个表)和撤销(把刚移动的行再移回原来的位置)。 这些操作必须串行执行,这就大大削弱了并发性,而增加了等待时间。在 7.3 版本中,Oracle 引入了一个真正的临时表空间(true temporary tablespace) 概念,这是一个新的表空间类型,专门用于存储临时数据,从而帮助缓解这个问 题。在引入这个特殊的表空间类型之前,临时数据与永久数据在同样的表空间中 管理,处理方式也与永久数据一样。 而临时表空间则不同,你不能在其中创建自己的永久对象。实际上根本的区 别只有这一条;空间还是在数据字典表中管理。不过,一旦在临时表空间中分配 了一个区段,系统就会一直持有(也就是说,不会把空间交回)。下一次有人出 于某种目的在临时表空间中请求空间时,Oracle 会在其内部的已分配区段列表 中查找已经分配的区段。如果找到,就会直接重用,否则还是用老办法来分配一 个区段。采用这种方式,一旦数据库启动,并运行一段时间,临时段看上去就好 像满了,但是实际上只是“已分配”。里面都是空闲区段,它们的管理完全不同。 当有人需要临时空间时,Oracle 会在内存中的数据结构里查找空间,而不是执 行代价昂贵的递归 SQL。 在 Oracle 8.1.5 及以后版本中,Oracle 在减少这种空间管理开销方面又前 进了一步。它引入了一个本地管理表空间(locally-managed tablespace )概 念,而不是字典管理表空间。与 Oracle 7.3 中对临时表空间的管理一样,本地 空间管理采用了同样的办法来管理所有表空间:这样就无需使用数据字典来管理 表空间中的空间。对于本地管理表空间,会使用每个数据文件中存储的一个位图 来管理区段。现在要得到一个区段,系统所做的只是在位图中将某一位设置为1。 要释放空间,系统再把这一位设置为 0。与使用字典管理的表空间相比,这样分 配和释放空间就相当快。为了处理跨所有表空间的空间请求,我们不再需要在数 据库级串行完成这些耗时的操作,相反,只需在表空间级串行执行一个速度相当 快的操作。本地管理的表空间还有另外一些很好的特点,如可以保证区段的大小 统一,不过这一点 DBA 更关心。 再往后,则只应使用本地管理的表空间作为存储管理方法。实际上,在 Ora cle9i 及以上版本中,如果使用数据库配置助手(database configuration ass istant,DBCA)创建一个数据库,它就会创建一个 SYSTEM 作为本地管理的表空 间,如果 SYSTEM 是本地管理的,那么该数据库中所有其他表空间也会是本地管 理的,而且遗留的字典管理方法将无法工作。如果数据库中的 SYSTEM 是本地管 理的表空间,并不是说这样的数据库中不支持字典管理的表空间,而是说其中根 本无法创建字典管理的表空间: 这是一个正面的副作用,因为这样可以杜绝你使用遗留的存储机制,要知道 它的效率相对较低,而且很可能导致碎片。本地管理的表空间除了在空间分配和 撤销方面效率更高以外,还可以避免出现表空间碎片,这正是以本地管理表空间 的方式分配和撤销空间的一个副作用。有关内容将在第 10 章更深入地讨论。 3.5 临时文件 Oracle 中的临时数据文件(Temporary data files)即临时文件(temp fi les)是一种特殊类型的数据文件。Oracle 使用临时文件来存储大规模排序操作 和散列操作的中间结果,如果RAM 中没有足够的空间,还会用临时文件存储全局 临时表数据,或结果集数据。永久数据对象(如表或索引)不会存储在临时文件 中,但是临时表及其索引的内容要存储在临时文件中。所以,你不可能在临时文 件中创建表,但是使用临时表时完全可以在其中存储数据。 Oracle 以一种特殊的方式处理临时文件。一般而言,你对对象所做的每一个 修改都会存储在重做日志中;这些事务日志会在以后某个时间重放以“重做事 务”,例如,失败后进行恢复时就可能需要“重做事务”。临时文件不包括在这 个重放过程内。对临时文件并不生成 redo 日志,不过可以生成 undo 日志。由于 UNDO 总是受 redo 的“保护”,因此,这就会生成使用临时表的 redo 日志,有 关详细内容见第 9 章。为全局临时表生成 undo 日志的目的是为了回滚在会话中 所做的一些工作,这可能是因为处理数据时遇到一个错误,也可能因为某个一般 性的事务失败。DBA 不需要备份临时数据文件,实际上,备份临时数据文件只会 浪费时间,因为你无法恢复临时数据文件。 建议将数据库配置为使用本地管理的临时表空间。作为 DBA,要确保使用 CR EATE TEMPORARY TABLESPACE 命令。你肯定不想把一个永久表空间改成临时表空 间,因为这样得不到临时文件的任何好处。 关于真正的临时文件,有一个细节需要注意,如果操作系统允许创建临时文 件,则会稀疏(sparse)地创建,也就是说,在需要之前它们不会真正占用磁盘 存储空间。通过下面这个例子能很容易看出这一点(这里的平台是 Red Hat Lin ux): 注意 df 是显示“磁盘空闲空间”的 Unix 命令。这个命令显示出,向数据库中添加一个 2 GB 的临时文件之前,包含/d01/temp 的文件系统中有 29 008 368 KB 的空闲空间。 添加了这个文件之后,文件系统中有 29 008 240 KB 的空闲空间。 显然,这个文件只占了 128 KB 的存储空间,但是,如果用 ls 将其列出,可 以得到: 看上去是一个正常的 2 GB 文件,但它实际上只用了128 KB 的存储空间。之所以 要指出这一点,原因是我们实际上可能创建了数百个 2 GB 的临时文件,尽管空 闲的磁盘空间只有大约 29 GB。听起来不错,空闲空间那么多!问题是,真正开 始使用这些临时文件时,它们就会膨胀,很快我们就会得到“没有更多空间”的 错误。由于空间会按操作系统的需要来分配或者物理地分配文件,所以我们肯定 会用光空间(特别是这样一种情况,我们创建了临时文件后,有人又用其他内容 把文件系统填满了,此时临时文件实际上根本没有可用的空间)。 这个问题的解决因操作系统而异。在 Linux 上,可以使用 dd 在文件中填入 数据,这样,操作系统就会物理地为文件分配磁盘空间,或者使用 cp 创建一个 非稀疏的文件,例如: 将稀疏的 2 GB 文件复制到/d01/temp/temp_huge2 中,并使用 REUSE 选项利 用该临时文件创建临时表空间,这样就能肯定这个临时文件已经分配了所有文件 系统空间,而且数据库确实有了 2 GB 的临时空间可以使用。 注意 根据我的经验,Windows NTFS 不支持稀疏文件,以上讨论只适用于 UNIX/Linux 平台。好的一面是,如果必须在 UNIX/Linux 上创建一个 15 GB 的临时表空间,而 且支持临时文件,你会发现创建过程相当快(几乎立即完成),但是要保证确实有 15 GB 的空闲空间,而且一定要记住保留这些空间。 3.6 控制文件 控制文件(control file)是一个相当小的文件(最多能增长到64 MB 左右), 其中包含 Oracle 需要的其他文件的一个目录。参数文件告知实例控制文件的位 置,控制文件则告知实例数据库和在线重做日志文件的位置。 控制文件还告知了 Oracle 其他一些事情,如已发生检查点的有关信息、数 据库名(必须与 DB_NAME 参数匹配)、创建数据库的时间戳、归档重做日志的历 史(有时这会让控制文件变大)、RMAN 信息等。 控制文件应该通过硬件(RAID)多路保存,如果不支持镜像,则要通过 Ora cle 多路保存。应该有不止一个副本,而且它们应该保存在不同的磁盘上,以防 止万一出现磁盘故障而丢失控制文件。丢失控制文件并不是致命的,但会使恢复 变得困难得多。 开发人员实际上可能不会接触到控制文件。对于 DBA 来说,控制文件是数据 库中一个非常重要的部分,但是对于软件开发人员,它们并不是太重要。 3.7 重做日志文件 重做日志文件(redo log file)对于 Oracle 数据库至关重要。它们是数据 库的事务日志。通常只用于恢复,不过也可以用于以下工作: ‰ 系统崩溃后的实例恢复 ‰ 通过备份恢复数据文件之后恢复介质 ‰ 备用(standby)数据库处理 ‰ 输入到流中,这是一个重做日志挖掘过程,用于实现信息共享(这也是一种奇特的复制) 重做日志文件的主要目的是,万一实例或介质失败,重做日志文件就能派上 用场,或者可以作为一种维护备用数据库(standby database)的方法来完成故 障恢复。如果数据库所在主机掉电,导致实例失败,Oracle 会使用在线重做日 志将系统恢复到掉电前的那个时刻。如果包含数据文件的磁盘驱动器出现了永久 性故障,Oracle 会使用归档重做日志以及在线重做日志,将磁盘驱动器的备份 恢复到适当的时间点。另外,如果你“无意地”删除了一个表,或者删掉了一些 重要的信息,而且提交了操作,则可以恢复一个备份,并让 Oracle 使用这些在 线和归档重做日志文件将其恢复到意外发生前的那个时刻。 你在 Oracle 中完成的每个操作几乎都会生成一定的 redo 信息,并写入在线 重做日志文件。向表中插入一行时,插入的最终结果会写入重做日志。删除一行 时,则会在重做日志中写入你删除了这一行这一事实。删除一个表时,删除的效 果会写入重做日志。从表中删除的数据不会写入;不过,Oracle 删除表时执行 的递归 SQL 确实会生成 redo。例如,Oracle 从 SYS.OBJ$表(和其他内部字典对 象)中删除一行时,这就会生成 redo,另外如果支持不同模式的补充日志(sup plemental logging ),还会把具体的 DROP TABLE 语句写入重做日志流。 有些操作可能会以尽量少生成 redo 的模式完成。例如,可以使用 NOLOGGIN G 属性创建一个索引。这说明,最初创建索引数据的操作不会记入日志,但是 O racle 完成的所有递归 SQL 会写入日志。例如,创建索引后,将向 SYS.OBJ$表中 插入一行表示索引存在,这个插入会记入日志,以后使用 SQL 插入、更新和删除 等操作完成的修改也会记入日志。但是,最初向磁盘写索引结构的操作不会记入 日志。 前面我提到了两种类型的重做日志文件:在线(online)和归档(archived)。 下面几节将详细介绍这两类重做日志文件。在第9 章中,我们还会结合回滚段来 讨论 redo,看看它们对开发人员有什么影响。现在,我们只关注这些重做日志 文件是什么,它们有什么用途。 3.7.1 在线重做日志 每个 Oracle 数据库都至少有两个在线重做日志文件组。每个重做日志组都 包含一个或多个重做日志成员(redo 按成员组来管理)。这些组的单个重做日 志文件成员之间实际上形成彼此真正的镜像。这些在线重做日志文件的大小是固 定的,并以循环方式使用。Oracle先写日志文件组 1,当到达这组文件的最后时, 会切换至日志文件组 2,从头到尾重写这些文件的内容。日志文件组 2 填满时, 再切换回到日志文件组 1(假设只有两个重做日志文件组;如果有 3 个重做日志 文件组,当然会继续写第 3 个组)。如图 3-4 所示。 图3-4 日志文件组 从一个日志文件组切换到另一个日志文件组的动作称为日志切换(log swit ch)。重要的是注意到,如果数据库配置得不好,日志切换可能会导致临时性“暂 停”。由于重做日志的目的是在失败时恢复事务,所以我们自己必须保证一点: 在重用重做日志之前,失败时应该不需要重做日志文件的内容。如果 Oracle 不 能肯定这一点,也就是说,它不清楚是否真的不需要日志文件的内容,就会暂时 挂起数据库中的操作,确保将缓存中的数据(即 redo“保护”的数据)安全地 写入磁盘本身(建立检查点)。一旦 Oracle 能肯定这一点,再恢复处理,并重 用重做文件。 我们刚刚提到一个重要的数据库概念:检查点(checkpointing)。要理解 在线重做日志如何使用,就需要了解检查点,知道数据库缓冲区缓存如何工作, 还要知道一个称为数据块写入器(data block writer,DBWn)的进程会做什么。 数据库缓冲区缓存和 DBWn 将在后面详细讨论,但是我们先提前说两句,不过点 到为止。 数据库缓冲区缓存(database buffer cache)就是临时存储数据库块的地 方。这是 Oracle SGA 中的一个结构。读取块时,会存储在这个缓存中,这样以 后就不必再物理地重新读取它们。缓冲区缓存首先是一个性能调优设备,其目的 只是让非常慢的物理 I/O 过程看上去快一些。修改块(更新块上的一行)时,这 些修改会在内存中完成,写至缓冲区缓存中的块。另外,会把重做这些修改所需 的足够信息保存在重做日志缓冲区(redo log buffer)中,这是另一个 SGA 数 据结构。提交(COMMIT)修改时,会使这些修改成为永久的。Oracle 并不是访 问 SGA 中修改的所有块,并把它们写到磁盘上。相反,它只是把重做日志缓冲区 的内容写到在线重做日志中。只要修改的块还在缓冲区缓存中,而不在磁盘上, 数据库失败时我们就会需要该在线重做日志的内容。如果提交过后,突然掉电, 数据库缓冲区缓存就会彻底清空。 如果发生这种情况,则只有重做日志文件中有修改记录。重启数据库时,Or acle 实际上会重放我们的事务,再用同样的方式修改块,并提交。所以,只要 修改的块被缓存而未写入磁盘,就不能重用重做日志文件。 在这里 DBWn 就能起作用了。这是 Oracle 的一个后台进程,负责在缓冲区缓 存填满时请求空间,更重要的是,它会建立检查点。建立检查点就是把脏块(已 修改的块)从缓冲区缓存写至磁盘。Oracle 会在后台为我们做这个工作。有很 多情况都会导致建立检查点,最常见的事件就是重做日志切换。 在填满日志文件 1 并切换到日志文件 2 时,Oracle 就会启动一个检查点。此 时,DBWn 开始将日志文件组 1 所保护的所有脏块写至磁盘。在 DBWn 把该日志文 件保护的所有块刷新输出之前,Oracle 不能重用这个日志文件。如果 DBWn 在完 成其检查点之前就想使用日志文件,就会在数据库的 ALERT 日志中得到以下消 息: 所以,出现这个消息时,数据库中的处理会挂起,因为 DBWn 正忙于完成它 的检查点。此时,Oracle 会尽可能地把所有处理能力都交给 DBWn,希望它能更 快地完成。 如果数据库实例得到了妥善地调优,是不会看到这个消息的。如果你确实看 到了这个消息,就应该知道肯定让最终用户陷入了不必要的等待,而这是可以避 免的。我们的目标是分配足够的在线重做日志文件(这是对 DBA 而言,对开发人 员则不一定),这样就不会在检查点完成之前试图重用日志。如果经常看到这个 消息,这说明 DBA 未能为应用分配足够多的在线重做日志文件,或者要对 DBWn 进行调优才能更高效地工作。 不同的应用会生成不同数量的重做日志。很自然地,决策支持系统(Decis ion Support System,DSS,仅查询)或数据仓库(DW)系统生成的在线重做日 志总是比 OLTP(事务处理)系统生成的在线重做日志少得多。如果一个系统在 数据库中对二进制大对象(Binary Large Object,BLOB)完成了大量图像处理, 相对于简单的订单输入系统来说,则会生成更多的 redo。有 100 位用户的订单 输入系统与有 1 000位用户的系统相比,生成的 redo可能只是后者的十分之一。 至于重做日志多大才合适,这没有“正确”的答案,不过你肯定希望重做日志 足够大,能适应你的工作负载。 在设置在线重做日志的大小和数目时,还有一些问题需要考虑。其中很多问 题都超出了这本书的范围,不过在此把它们都列出来,以便你有一个大致的认识: ‰ 高峰负载(peak workload):你可能希望系统不必等待对未完成的消息建立检查点,不 要在高峰处理期间遭遇瓶颈。你不能针对“平均”的小时吞吐量来确定重做日志的大小,而 要针对高峰处理来确定。如果每天生成 24 GB 的日志,但是其中 10 GB 的日志都是在 9:00 am 到 11:00 am 这一时段生成的,就要把重做日志的大小调整到足以放下那两小时高峰期间 生成的日志。如果只是针对每小时 1 GB 来确定日志大小可能是不够的。 ‰ 大量用户修改相同的块:如果大量用户都要修改相同的块,你可能希望重做日志文件很 大。因为每个人都在修改同样的块,最好尽可能多地更新之后才将其写出到磁盘。每个日志 切换都会导致一个检查点,所以你可能不希望频繁地切换日志。不过,这样一来又会影响恢 复时间。 ‰ 平均恢复时间:如果必须确保恢复尽可能快地完成,即便是大量用户要修改相同的块,也 可能倾向于使用较小的重做日志文件。如果只是处理一两个小的重做日志文件,而不是一个 巨大的日志文件,则所需的恢复时间会比较短。由于重做日志文件小,往往会过多地建立检 查点,时间长了,整个系统会越来越慢(本不该如此),但是恢复所花的时间确实会更短。 要减少恢复时间,除了使用小的重做日志文件外,还可以使用其他的数据库参数。 3.7.2 归档重做日志 Oracle 数据库可以采用两种模式运行:ARCHIVELOG 模式和 NOARCHIVELOG 模 式。这两种模式的区别只有一点,即 Oracle 重用重做日志文件时会发生什么情 况。“会保留redo 的一个副本吗?还是 Oracle 会将其重写,而永远失去原来的 日志?”这是一个很重要的问题,下面就来回答。除非你保留了这个文件,否则 无法从备份将数据恢复到当前的时间点。 假设你每周的星期六做一次备份。现在是星期五下午,已经生成了这一周的 数百个重做日志,突然你的磁盘出问题了。如果没有以 ARCHIVELOG 模式运行, 那么现在的选择只有: ‰ 删除与失败磁盘相关的表空间。只要一个表空间有该磁盘上的文件,就要删除这个表空 间(包括表空间的内容)。如果影响到 SYSTEM 表空间(Oracle 的数据字典),就不能用这 个办法。 ‰ 恢复上周六的数据,这一周的工作就白做了。 不论是哪种选择都不太好。这两种做法都意味着你会丢失数据。不过另一方 面,如果之前以 ARCHIVELOG 模式运行,那么只需再找一个磁盘就行了。你要根 据上周六的备份将受影响的文件恢复到这个磁盘上。最后,再对这些文件应用归 档重做日志和(最终的)在线重做日志,实际上是以一种快进的方式重放整个星 期的事务。这样一来,什么也不会丢失。数据会恢复到发生失败的那个时间点。 人们经常告诉我,他们的生产系统不需要 ARCHIVELOG 模式。在我的印象里, 这样说的人没有一个说对的。我认为,如果系统不以 ARCHIVELOG 模式运行,那 它根本就不能算是生产系统。未以 ARCHIVELOG 模式运行的数据库总有一天会丢 失数据。这是在所难免的;如果你的数据库不以 ARCHIVELOG 模式运行,你肯定 会丢失数据。 “我们在使用 RAID-5,所以可以得到完全的保护”,这是一种很常见的托 辞。我曾见过,由于制造方面的错误,RAID 中的所有磁盘都“冻结”了,而且 几乎是同时发生的。我也见过,有时硬件控制器会对数据文件带来破坏,所以 他们只是在用 RAID 设备安全地保护已经被破坏的数据。另外,对于避免操作员 错误(这也是丢失数据的一个最常见的原因),RAID 也无能为力。 “在出现硬件或操作员错误之前,而且归档尚未受到影响,如果此时建立了备 份,就能很好地恢复”。关键是,既然系统上的数据是有价值的,有什么理由不 采用 ARCHIVELOG 模式呢?性能不能作为理由;适当配置的归档只会增加极少的开 销甚至根本不增加开销。由于这一点,再加上另外一条:如果一个系统会“丢失 数据”,那它再快也是没有用的,所以退一万步说,即使归档会增加 100%的开销, 我们也不得不做。如果可以把一个特性删除而没有任何重大损失,这个特性就叫 做开销(overhead);开销就像是蛋糕上的糖霜,可以不要而不会影响蛋糕的美 味。但归档不同,利用归档可以保住你的数据,确保数据不会丢失,这不是开销, 而且正是 DBA 的主要任务! 只有测试或开发系统才应当采用 NOARCHIVELOG 模式执行。不要受人蛊惑在 非 ARCHIVELOG 模式下运行。你花了很长时间开发你的应用,肯定希望人们相信 你。如果把他们的数据丢失了,也会让他们对你的系统失去信心。 注意 有些情况下,大型的 DW(数据仓库)以 NOARCHIVELOG 模式运行也是合适的,因 为它可能适当地使用了 READ ONLY(只读)表空间,而且会通过重新加载数据来完 全重建受失败影响的所有 READ WRITE(读写)表空间。 3.8 密码文件 密码文件(password file)是一个可选的文件,允许远程 SYSDBA 或管理员 访问数据库。 启动 Oracle 时,还没有数据库可以用来验证密码。在“本地”系统上启 动 Oracle 时(也就是说,不在网络上,而是从数据库实例所在的机器启动), Oracle 会利用操作系统来执行这种认证。 安装 Oracle 时,会要求完成安装的人指定管理员“组”。在 UNIX/Linux 上, 这个组一般默认为 DBA,在 Windows 上则默认为 OSDBA。不过,也可以是平台上 任何合法的组名。这个组很“特殊”,因为这个组中的任何用户都可以作为 SYS DBA 连接 Oracle ,而无需指定用户名或密码。例如,在安装 Oracle 10g Relea se 1 时,我指定了一个 ora10g 组。ora10g 组中的任何用户都无需用户名/密码 就能连接: 这是可以的—我就成功地连接了 Oracle,现在我能启动这个数据库,将其关 闭,或者完成我想做的任何管理工作。不过,假设我想从另外一台机器通过网络 完成这些操作,会怎么样呢?在这种情况下,我试图使用@tns-connect-string 来连接。不过这会失败: 在网络上,对于 SYSDBA 的操作系统认证不再奏效,即使把很不安全的 REMO TE_ OS_AUTHENT 参数设置为 TRUE 也不例外。所以,操作系统认证不可行。如前 所述,如果你想启动一个实例进行装载,并打开一个数据库,根据定义,在连接 的另一端实际上“还没有数据库”,也无法从中查找认证的详细信息。这就是一 个鸡生蛋还是蛋生鸡的问题。因此密码文件“应运而生”。密码文件保存了一个 用户名和密码列表,这些用户名和密码分别对应于可以通过网络远程认证为 SYS DBA 的用户。Oracle必须使用这个文件来认证用户,而不是数据库中存储的正常 密码列表。 下面校正这种情况。首先,我们要本地启动数据库,以便设置 REMOTE_LOGI N_PASSWORDFILE。其默认值为 NONE,这意味着没有密码文件;不存在“远程 SY SDBA 登录”。这个参数还有另外两个设置:SHARED(多个数据库可以使用同样 的密码文件)和 EXCLUSIVE(只有一个数据库使用一个给定的密码文件)。这里 设置为 EXCLUSIVE,因为我们只想对一个数据库使用这个密码文件(这也是一般 用法): 实例启动和运行时,这个设置不能动态改变,所以要想让它生效必须重启实 例。下一步是使用命令行工具(UNIX 和 Windows 平台上)orapwd 创建和填写这 个初始的密码文件: 在此: file——密码文件名(必要)。 password——SYS 的密码(必要)。 entries——DBA 和操作员的最大数目(可选)。 force——是否重写现有的文件(可选)。 等号(=)两边没有空格。 我们使用的命令为: 对我来说,这样会创建一个名为 orapwora10g 的密码文件(我的 ORACLE_SI D 是 ora10g)。 这是大多数 UNIX 平台上密码文件的命名约定(有关各平台上密码文件的命 名,详细内容请参见你的安装/操作系统管理员指南),这个文件位于$ORACLE_ HOME/dbs 目录中。在 Windows 上,文件名为 PW%ORACLE_SID%.ora,在%ORACLE_ HOME%\database 目录中。 目前该文件中只有一个用户,也就是用户 SYS,尽管数据库上还有其他 SYSD BA 账户,但它们还不在密码文件中。不过,基于以上设置,我们可以第一次作 为 SYSDBA 通过网络连接 Oracle: 我们通过了认证,所以登录成功,现在可以使用 SYSDBA 账户成功地启动、 关闭和远程管理这个数据库了。下面,再看另一个用户 OPS$TKYTE,它已经是一 个 SYSDBA 账户(已经授予 SYSDBA),但是还不能远程连接: 原因是,OPS$TKYTE 还不在密码文件中。要把 OPS$TKYTE 放到密码文件中, 需要重新对该账户授予 SYSDBA: 这样会在密码文件中创建一个条目,Oracle 现在会保持密码“同步”。如果 OPS$TKYTE 修改了他的密码,原来的密码将无法完成远程 SYSDBA 连接,新密码 才能启动 SYSDBA 连接: 对于其他不在密码文件中的 SYSDBA 用户,再重复同样的过程。 3.9 修改跟踪文件 修改跟踪文件(change tracking file)是一个可选的文件,这是 Oracle 10g 企业版中新增的。这个文件惟一的目的是跟踪自上一个增量备份以来哪些 块已经修改。采用这种方式,恢复管理器(Recovery Manager,RMAN)工具就 能只备份确实有变化的数据库块,而不必读取整个数据库。 在 Oracle 10g 之前的版本中,要完成增量备份,必须读取整个数据库文件, 查找自上一次增量备份以来修改的块。所以,如果有一个 1 TB 的数据库,只在 其中增加了 500 MB 的新数据(例如,数据仓库负载),增量备份就必须读取 1 TB 的数据,在其中找出要备份的 500 MB 新信息。所以,尽管增量备份存储的数 据确实少得多,但它还是要读取整个数据库。 在 Oracle 10g 企业版中,就不是这样了。Oracle 运行时,如果块被修改,O racle 可能会维护一个文件,告诉 RMAN 哪些块已经修改。创建这个修改跟踪文 件的过程相当简单,只需通过 ALTER DATABASE 命令就可以完成: 警告 我在这本书里再三强调一点:要记住,不要轻易执行设置参数、修改数据库和产 生重大改变的命令, 在你的“实际”系统上用这些命令之前一定要先进行测试。 实际上,前面这个命令会导致数据库做更多工作。它会消耗资源。 要关闭和删除块修改跟踪文件,还要再用一次 ALTER DATABASE 命令: 注意,这个命令实际上会清除块修改跟踪文件。它不只是禁用这个特性,而 是连文件也一并删除了。可以采用ARCHIVELOG 或 NOARCHIVELOG 模式再次启用这 个新的块修改跟踪特性。不过,要记住,NOARCHIVELOG 模式的数据库中并不保 留每天生成的重做日志,所以一旦介质(磁盘/设备)出现故障,所有修改都将 无法恢复!NOARCHIVELOG 模式的数据库总有一天会丢失数据。我们将在第 9 章 更详细地讨论这两种数据库模式。 3.10 闪回日志文件 闪回日志文件(flashback log file)简称为闪回日志(flashback log), 这是 Oracle 10g 中为支持 FLASHBACK DATABASE 命令而引入的,也是 Oracle 1 0g 企业版的一个新特性。闪回日志包含已修改数据库块的“前映像”,可用于 将数据库返回(恢复)到该时间点之前的状态。 3.10.1 闪回数据库 引入 FLASHBACK DATABASE 命令是为了加快原本很慢的时间点数据库恢复(p oint in time database recovery)过程。闪回可以取代完整的数据库恢复和使 用归档日志完成的前滚,主要目的是加快从“意外状态”中恢复。例如,下面来 看这样一种情况,如果 DBA“意外地”删除了模式(schema),该如何恢复?他 在本来要在测试环境中删除的数据库中删除了正确的模式。DBA 立即意识到做错 了,并且随即关闭了数据库。现在该怎么办? 在引入闪回数据库功能之前,可能只能这样做: (1) DBA 要关闭数据库。 (2) DBA(一般)要从磁带机恢复上一个完整的数据库备份。这通常是一 个很长的过程。 (3) DBA 要恢复所生成的全部归档重做日志,因为系统上没有备份。 (4) DBA 要前滚数据库,并在出错的 DROP USER 命令之前的时间点停止。 (5) 要以 RESETLOGS 选项打开数据库。 这个过程很麻烦,步骤很多,通常要花费很长的时间(当然,这个期间任何 人都无法访问数据库)。导致这种时间点恢复的原因有很多:如升级脚本错误, 升级失败,有权限的某个人无意地发出了某个命令而导致时间点恢复(无意的错 误,这可能是最常见的原因),或者是某个进程对一个大型数据库带来了数据完 整性问题(同样,这可能也是意外;也许是进程运行了两次而不是一次,也可能 是因为存在 bug)。不论是什么原因,最终的结果都是很长时间的宕机。 Oracle 10g 企业版的恢复步骤如下,这里假设已经配置了闪回数据库功能: (1) DBA 关闭数据库。 (2) DBA 启动并装载数据库,可以使用 SCN、Oracle 时钟或时间戳(墙上时 钟时间)发出闪回数据库命令,时间可以精确到一两秒钟。 (3) DBA 以 RESETLOGS 选项打开数据库。 要使用这个特性,数据库必须采用 ARCHIVELOG 模式,而且必须配置为支持 F LASHBACK DATABASE 命令。我的意思是,在你使用这个功能之前,必须先行配置。 等到真正发生了破坏,再想启用这个功能就为时已晚了;使用时必须早做打算。 3.10.2 闪回恢复区 闪回恢复区(Flash Recovery Area)也是 Oracle 10g 中的一个新概念。这 么多年来(不止 25 年),Oracle 中数据库备份的基本概念第一次有了变化。过 去,数据库中备份和恢复的设计都围绕着一种顺序介质(如磁带设备)的概念。 也就是说,总是认为随机存取设备(磁盘设备)太过昂贵,只是用来完成备份有 些浪费,而应该使用相对廉价但存储量大的磁带设备。 不过,现如今完全可以用很少的价钱买到容量达 TB 的磁盘。实际上,到 20 07 年,HP还打算推出磁盘容器达 TB 级的台式机。我还记得我的个人计算机上的 第一块硬盘:在当时它的容量可是大得惊人:40 MB。实际上,我不得不把它分 为两个逻辑盘,因为我所用的操作系统(当时是 MS-DOS)无法识别超过 32 MB 的硬盘。在过去的 20 年间,情况已经发生了翻天覆地的变化。 Oracle 10g 中的闪回恢复区(Flash Recovery Area)是一个新位置,Orac le 会在这里管理与数据库备份和恢复相关的多个文件。在这个区(area)中(这 里“区”表示用于此目的的一个预留的磁盘区;例如一个目录),其中可以找到: ‰ 磁盘上数据文件的副本。 ‰ 数据库的增量备份。 ‰ 重做日志(归档重做日志)。 ‰ 控制文件和控制文件的备份。 ‰ 闪回日志。 Oracle 利用这个新的闪回恢复区来管理这些文件,这样服务器就能知道磁盘 上有什么,以及磁盘上没有什么(可能在别处的磁带上)。使用这些信息,数据 库可以对被破坏的数据文件完成磁盘到磁盘的恢复操作,或者对数据库完成闪回 (这是一种“倒带”操作),从而撤销一个不该发生的操作。例如,可以使用闪 回数据库命令,将数据库放回到5 分钟之前的状态(而不需要完整的数据库恢复 和时间点恢复)。这样你就能“找回”无意删除的用户账户。 闪回恢复区更应算是一个“逻辑”概念。这是为本章讨论的各种文件类型所 预留的一个区。使用闪回恢复区是可选的,没有必要非得使用,不过,如果你想 使用诸如闪回数据库之类的高级特性,就必须用闪回恢复区存储信息。 3.11DMP 文件(EXP/IMP 文件) 导出工具(Export)和导入工具(Import)是年头已久的 Oracle 数据抽取 和加载工具,很多个版本中都有这些工具。导出工具的任务是创建一个平台独立 的 DMP 文件(转储文件),其中包含所有必要的元数据(CREATE 和 ALTER 语句 形式),可能还有数据本身,可以用于重新创建表、模式甚至整个数据库。导入 工具的惟一作用就是读取这些 DMP 文件,执行其DDL 语句,并加载它找到的所有 数据。 DMP 文件设计为向后兼容,这说明新版本可以读取老版本的 DMP,并成功地 处理。我听说有人导出过一个 Oracle 5 的数据库,并将其成功地导入到 Oracle 10g 中(只是一个测试!)。所以导入工具可以读取老版本的 DMP 文件,并处 理其中的数据。不过,大多数情况下反过来不成立:Oracle9i Release 1 的导 入工具进程不能(也不会)成功地读取 Oracle9i Release 2 或 Oracle 10g Re lease 1 创建的 DMP。例如,我曾经从 Oracle 10g Release 1 和 Oracle9i Rel ease 2 导出过一个简单的表。我试图在 Oracle9i Release 1 中使用这些 DMP 文 件时,很快发现 Oracle9i Release 1 导入工具甚至不打算处理 Oracle 10g Rel ease 1 的 DMP 文件: 处理 Oracle9i Release 2 文件时,情况也好不到哪儿去: 9i Release 1 试图读取文件,但它无法处理其中包含的 DDL。Oracle9i Relea se 2 中增加了一个新特性,称为表压缩(table compression)。因此,这个版本 的导出工具开始对每条 CREATE TABLE 语句增加一个 NOCOMPRESS 或 COMPRESS 关键 字。Oracle9i Release 2 的 DDL 在 Oracle9i Release 1 中无法执行。 不过,如果对 Oracle9i Release 2 或 Oracle 10g Release 1 使用 Oracle9 i Release 1 导出工具,总会得到一个有效的 DMP 文件,并可以成功地导入到 O racle9i Release 1 中。所以,对于 DMP 文件的规则是:创建 DMP 文件的 Expor t 版本必须小于或等于使用该 DMP 文件的 Import 的版本。要将数据导入 Oracle 9i Release 1 中,必须使用 Oracle9i Release 1 的导出工具(或者也可以使用 一个 8i 的 Export 进程;创建 DMP 文件的 Export 版本必须小于或等于 Oracle9i Release 1)。 这些 DMP 文件是平台独立的,所以可以安全地用任何平台的导出工具创建 D MP 文件,然后转换到另一个平台,再导入这个DMP 文件(只要Oracle 版本允许)。 不过,对于 Windows 和文件的 FTP 传输有一点警告,Windows 会默认地把 DMP 文 件当成是一个“文本”文件,并把换行符(UNIX 上为行末标记)转换为回车/换 行对,这就会完全破坏 DMP 文件。在 Windows 中通过 FTP 传输 DMP 文件时,要确 保所执行的是二进制传输。如果导入不成功,请检查源文件大小和目标文件大小 是否一样。这种问题常常导致令人痛苦的异常中止,而不得不重传文件,这种情 况发生过多少次我简直都记不清了。 DMP 文件是二进制文件,这说明你不能编辑这些文件来进行修改。可以从中 抽取大量信息(CREATE DDL),但是不能在文本编辑器(或者实际上任何类型的 编辑器)中编辑它们。在第一版的 Expert One-on-One Oracle 中(你手上的是 第二版,本书配套光盘提供了第一版的电子文档),我花了大量篇幅讨论导入和 导出工具,并介绍了如何使用 DMP 文件。随着这些工具越来越失宠,取而代之的 是更为灵活的数据泵工具,所以要想全面地了解如何管理导入和导出工具、如何 从中抽取数据以及如何使用这些工具,请参考第一版的电子文档。 3.12 数据泵文件 Oracle 10g 中至少有两个工具使用数据泵(data pump)文件格式。外部表 (external table)可以加载和卸载数据泵格式的数据,新的导入/导出工具 IM PDP 和 EXPDP 使用这种文件格式的方式与 IMP 和 EXP 使用 DMP 文件格式的方式完 全一样。 注意 数据泵格式只在 Oracle 10g Release 1 及以后版本中可用,Oracle9i release 中没有也 不能使用它。 前面提到过对 DMP 文件的警告,这些警告同样适用于数据泵文件。它们都是 跨平台(可移植)的二进制文件,包含有元数据(并非存储为 CREATE/ALTER 语 句,而是作为 XML 存储),可能还包含数据。数据泵文件使用 XML 作为元数据表 示结构,这一点对你和我这些最终用户来说非常重要。IMPDP 和 EXPDP 有一些复 杂的过滤和转换功能,这些在老版本的 IMP/EXP 工具中是没有的。从某种程度 上讲,这就归功于使用了 XML,另外还因为 CREATE TABLE 语句并非存储为 CREA TE TABLE,而是存储为一个有标记的文档。这样就能很容易地实现一些请求,如 “请把表空间 FOO 的所有引用替换为表空间 BAR”。DMP 中元数据存储为 CREATE /ALTER 语句,导入工具在执行 SQL 语句之前实际上必须解析每一条 SQL 语句, 才能完成这个工作(做得并不漂亮)。与之不同,IMPDP 只需应用一个简单的 X ML 转换就能达到同样的目的,FOO(指一个 TABLESPACE)会转换为FOO标记或另外某种表示。 由于使用了 XML,这使得 EXPDP 和 IMPDP 工具的功能相对于原来的 EXP 和 I MP 工具来说有了大幅的提升。在第 15 章,我们将更深入地介绍这些工具。不 过,在此之前,先来看看如何使用数据泵格式快速地从数据库 A 抽取数据,并 移至数据库 B。这里我们将使用一个“反过来的外部表”。 外部表(external table)最早在 Oracle9i Release 1 中引入,利用外部 表,我们能像读取数据库表一样读取平面文件(无格式的文本文件),完全可以 用 SQL 来处理外部表。外部表是只读的,设计为从外部向 Oracle 提供数据。Or acle 10g Release 1 及以上版本中的外部表还可以走另外一条路:用于以数据 泵格式从数据库获取数据,以便将数据移至另一台机器(另一个平台)。要完成 这个练习,首先需要一个 DIRECTORY 对象,告诉 Oracle 卸载的位置: 接下来,从 ALL_OBJECTS 视图卸载数据。数据可以来自任意查询,涉及我们 想要的所有表或 SQL 构造: 从字面上可以很清楚地看出其含义:在/tmp 中有一个名为 allobjects.dat 的文件,其中包含查询select * from all_objects 的内容。可以看一下这个信 息: 这只是文件的开头,即最前面的部分;二进制数据表示为……(如果查看这 个数据时你的终端发出“嘟嘟”声,不要奇怪)。下面使用二进制 FTP 传输(D MP 文件的警告同样适用!),将这个 allobject.dat 文件移至一个 Windows XP 服务器,并创建一个目录对象与之对应: 然后创建一个表指向这个外部表: 现在就能查询从另一个数据库卸载的数据了: 这就是数据泵文件格式的强大之处:如果需要,它能立即通过一个“隐秘的 网”将数据从一个系统传输到另一个系统。想想看,有了数据泵,下一次测试时, 周末你就能把一部分数据带回家去工作了。 有一点不太明显:这两个数据库的字符集不同。如果你注意以上输出的开头 部分,可以发现 Linux 数据库 WE8ISO8859P1 的字符集已经编码写入到文件中。 我的 Windows 服务器则有: 归功于数据泵文件格式,Oracle 现在能识别不同的字符集,并能加以处理。 字符集转换会根据需要动态地完成,使得各个数据库表示中的数据“正确”。 我们还是会在第 15 章再详细讨论数据泵文件格式,不过通过这一节的介绍, 你应该对数据泵文件格式是什么以及这个文件中可能包含什么有一定的认识 了。 3.13 平面文件 自从有了电子数据处理,就有了平面文件(flat file)。我们每天都会看 到平面文件。前面讨论的警告日志就是一个平面文件。 我在 Web 上看到有关“平面文件”的以下定义,觉得这些定义实在太绕了: 平面文件是去除了所有特定应用(程序)格式的电子记录,从而使数据元素 可以迁移到其他的应用上进行处理。这种去除电子数据格式的模式可以避免因为 硬件和专有软件的过时而导致数据丢失。 平面文件是一种计算机文件,所有信息都在一个信号字符串中。 实际上,平面文件只是这样一个文件,其中每一“行”都是一个“记录”, 而且每行都有一些定界的文本,通常用逗号或管道符号(竖线)分隔。通过使 用遗留的数据加载工具 SQLLDR 或外部表,Oracle 可以很容易地读取平面文件, 实际上,我会在第 15 章详细讨论这个内容(还会在第 10 章谈到外部表)。不 过,Oracle 生成平面文件可就不那么容易了,不管由于什么原因,确实没有一 个简单的命令行工具能把信息导出到一个平面文件中。诸如 HTML DB 和企业管 理器之类的工具有助于完成这个过程,但是并没有一个官方的命令行工具可以 轻松地在脚本中用来完成这个操作。 正是出于这个原因,所以我决定在这一章对平面文件说两句,我提议能有一 些生成简单平面文件的工具。多年来,为此我开发过 3 种方法,每种方法都各有 特点。第一种方法是使用PL/SQL和UTL_FILE(利用动态SQL)来完成任务。如果 数据量不大(几百或几千行),这个工具则有足够的灵活性,速度也不错。不过, 它必须在数据库服务器主机上创建文件,但有时我们并不想在数据库服务器上创 建文件。因此,我又开发了一个 SQL*Plus实用程序,可以在运行SQL*Plus的机 器上创建平面文件。由于SQL*Plus可以连接网络上任何位置的Oracle服务器,所 以能从网络上的任何数据库把任何数据卸载到一个平面文件中。最后,如果速度 要求很高,那么非C莫属。为此,我还开发了一个Pro*C命令行卸载工具来生成平 面文件。这些工具都可以从http://asktom.oracle.com/~tkyte/flat/index.ht ml免费得到,另外我还会在这里提供为了把数据卸载到平面文件而开发的新工 具。 3.14 小结 在这一章中,我们分析了 Oracle 数据库使用的各种重要的文件类型,从底层的参数文件(如 果没有参数文件,甚至无法启动数据库)到所有重要的重做日志和数据文件。我们分析了 O racle 的存储结构,从表空间到段,再到区段,最后是数据库块(这是最小的存储单位)。 我们还简要介绍了检查点在数据库中如何工作,并提前了解了 Oracle 的一些物理进程或线 程的工作。 第 4 章内存结构 4.1 进程全局区和用户全局区 这一章将讨论 Oracle 的 3 个主要的内存结构: ‰ 系统全局区(System Global Area,SGA):这是一个很大的共享内存段,几乎 所有 Oracle 进程都要访问这个区中的某一点。 ‰ 进程全局区(Process Global Area,PGA):这是一个进程或线程专用的内存, 其他进程/线程不能访问。 ‰ 用户全局区(User Global Area,UGA):这个内存区与特定的会话相关联。它 可能在 SGA 中分配,也可能在 PGA 中分配,这取决于是用共享服务器还是用专用 服务器来连接数据库。如果使用共享服务器,UGA 就在 SGA 中分配;如果使用专 用服务器,UGA 就会在 PGA(即进程内存区)中。 注意 在 Oracle 的较早版本中,共享服务器称为多线程服务器(Multi-Threaded Server)或 MTS。这本书中我们会一直用“共享服务器”的说法。 下面首先讨论 PGA 和 UGA,然后再来介绍SGA,SGA确实是一个很庞大的结构。 4.1 进程全局区和用户全局区 进程全局区(PGA)是特定于进程的一段内存。换句话说,这是一个操作系 统进程或线程专用的内存,不允许系统中的其他进程或线程访问。PGA 一般通过 C 语言的运行时调用 malloc()或 memmap()来分配,而且可以在运行时动态扩大 (甚至可以收缩)。PGA 绝对不会在 Oracle 的 SGA 中分配,而总是由进程或线 程在本地分配。 实际上,对你来说,用户全局区(UGA)就是你的会话的状态。你的会话总 能访问这部分内存。UGA的位置完全取决于你如何连接 Oracle。如果通过一个共 享服务器连接,UGA 肯定存储在每个共享服务器进程都能访问的一个内存结构 中,也就是 SGA 中。如果是这样,你的会话可以使用任何共享服务器,因为任何 一个共享服务器都能读写你的会话的数据。另一方面,如果使用一个专用服务器 连接,则不再需要大家都能访问你的会话状态,UGA 几乎成了 PGA 的同义词;实 际上,UGA 就包含在专用服务器的 PGA 中。查看系统统计信息时可以看到,采用 专用服务器模式时,总是会报告UGA 在 PGA 中(PGA大于或等于所用的 UGA 内存; 而且 PGA 内存的大小会包括 UGA 的大小)。 所以,PGA 包含进程内存,还可能包含 UGA。PGA 内存中的其他区通常用于完 成内存中的排序、位图合并以及散列。可以肯定地说,除了 UGA 内存,这些区在 PGA 中的比重最大。 从 Oracle9i Release 1 起,有两种办法来管理 PGA 中的这些非 UGA 内存: ‰ 手动 PGA 内存管理, 采用这种方法时,你要告诉 Oracle:如果一个特定进程中需 要排序或散列,允许使用多少内存来完成这些排序或散列。 ‰ 自动 PGA 内存管理 ,这要求你告诉 Oracle:在系统范围内可以使用多少内存。 分配和使用内存的方式因情况不同而有很大的差异,因此,我们将分别进行 讨论。需要说明,在 Oracle9i 中,如果采用共享服务器连接,就只能使用手动 PGA 内存管理。这个限制到 Oracle 10g Release 1(及以上版本)中就没有了。 在 Oracle 10g Release 1 中,对于共享服务器连接,既可以使用手动 PGA 内存 管理,也可以使用自动 PGA 内存管理。 PGA 内存管理受数据库初始化参数 WORKAREA_SIZE_POLICY 的控制,而且可以 在会话级修改。在 Oracle9i Release 2 及以上版本中,这个初始化参数默认为 AUTO,表示自动 PGA 内存管理。而在 Oracle9i Release 1 中,这个参数的默认 设置为 MANUAL。 下面几节将分别讨论这两种方法。 4.1.1 手动 PGA 内存管理 如果采用手动 PGA 内存管理,有些参数对 PGA 大小的影响最大,这是指 PGA 中除了会话为 PL/SQL 表和其他变量分配的内存以外的部分,这些参数如下: ‰ SORT_AREA_SIZE:在信息换出到磁盘之前,用于对信息排序的 RAM 总量。 ‰ SORT_AREA_RETAINED_SIZE:排序完成后用于保存已排序数据的内存总量。 也就是说,如果 SORT_AREA_SIZE 是 512 KB,SORT_AREA_RETAINED_SIZE 是 256 KB,那么服务器进程最初处理查询时会用 512 KB 的内存对数据排序。等到 排序完成时,排序区会“收缩”为 256 KB,这 256 KB 内存中放不下的已排序数据 会写出到临时表空间中。 ‰ HASH_AREA_SIZE:服务器进程在内存中存储散列表所用的内存量。散列联结时 会使用这些散列表结构,通常把一个大集合与另一个集合联结时就会用到这些结构。 两个集合中较小的一个会散列到内存中,散列区中放不下的部分都会通过联结键存储 在临时表空间中。 Oracle 将数据写至磁盘(或换出到磁盘)之前,数据排序或散列所用的空间 量就由这些参数控制,这些参数还控制着排序完成后会保留多少内存段。SORT_ AREA_SIZE~SORT_AREA_ RETAINED_SIZE 这部分内存一般从 PGA 分配,SORT_ARE A_RETAINED_SIZE 这部分内存会在 UGA 中分配。通过查询一些特殊的 Oracle V$ 视图,可以看到 PGA 和 UGA 内存的当前使用情况,并监视其大小的变化,这些特 殊的 V$视图也称为动态性能视图(dynamic performance view)。 例如,下面来运行一个小测试,这里会在一个会话中对大量数据排序,在第 二个会话中,我们将监视第一个会话中UGA/PGA 内存的使用。为了以一种可预测 的方式完成这个工作,我们建立了 ALL_OBJECTS 表的一个副本,其中有大约 45 000 行,而且没有任何索引(这样就能知道肯定会发生排序): 为了消除最初硬解析查询所带来的副作用,我们将运行以下脚本,不过现在 先不管它的输出。后面还会在一个新会话中再次运行这个脚本,查看受控环境中 对内存使用的影响。我们会依次使用大小为 64 KB、1 MB 和 1 GB 的排序区: 注意 数据库中处理 SQL 时,首先必须“解析”SQL 语句,有两种类型的解析。 第一种是硬解析(hard parse),数据库实例第一次解析查询时完成的就是 硬解析,其中包括查询计划的生成和优化。第二种解析是软解析(soft pars e),在此会跳过硬解析的许多步骤。由于对前面的查询完成了硬解析,后 面再查询时就可以避免硬解析,所以在下面的操作中,我们不必测量与硬解 析相关的开销(因为后面都只是软解析)。 现在,建议你注销刚才的 SQL*Plus 会话,紧接着再登录,这样能得到一个 一致的环境;也就是说,相对于刚才的环境来讲,还没有做任何其他的工作。 为了确保使用手动内存管理,我们要专门设置,并指定一个很小的排序区大 小(64 KB)。另外,还要标识会话 ID(SID),以便监视该会话的内存使用情 况。 下面需要在第二个单独的会话中测量第一个会话(SID 151)使用的内存。 如果使用同一个会话测量自身的内存使用情况,在查询排序所用的内存时,这个 查询本身可能会影响我们查看的结果。为了在第二个会话中测量内存,要使用我 为此开发的一个 SQL*Plus 小脚本。实际上这是一对脚本,其中一个脚本名为 re set_stat.sql,用于重置一个小表,并将一个 SQL*Plus 变量设置为 SID,这个 脚本如下: 注意 使用这个脚本(或任何脚本)之前,先要确保你了解脚本会做什么。这个 脚本会删除一个 SESS_STATS 表,然后重新创建。如果你的模式中已经有 这样一个表,你可能得换个名字! 另一个脚本是 watch_stat.sql,对于这个案例研究,脚本中使用了 MERGE SQ L 语句,这样就能首先插入(INSERT)一个会话的统计值,以后再回过来对其进 行更新,而无需单独的 INSERT/UPDATE 脚本: 这里我强调了“对于这个案例研究”,因为上面粗体显示的行(我们感兴趣 的统计名)在不同的示例中会有所不同。在这个例子中,我们感兴趣的是名字里 包括 ga 的统计结果(pga 和 uga),或者名字里有 direct temp 的统计结果(在 Oracle 10g 中,这会显示对临时空间的直接读写,也就是读写临时空间所执行 的 I/O 次数)。 注意 在 Oracle9i 中,对临时空间的直接 I/O 不是这样表示的。我们要使用一个 WHERE 子句,其中应包括 and (a.name like '%ga %'or a.name like '%phys ical % direct%')。 从 SQL*Plus 命令行运行这个 watch_stat.sql 脚本时,可以看到会话的 PGA 和 UGA 内存统计信息列表,而且列出了对临时空间执行的I/O。在对会话151(也 就是使用手动 PGA 内存管理的会话)做任何工作之前,下面使用以上脚本来看看 这个会话当前使用了多少内存,以及对临时空间执行了多少次 I/O: 可以看出,开始查询之前,UGA 中大约有 149 KB(152 176/1 024)的数据,PGA 中大约有 487 KB 的数据。第一个问题是:“在 PGA 和 UGA 之间使用了多少内存?” 也就是说,用了 149 KB + 487 KB 的内存吗?还是另外的某个数?这是一个很棘手的 问题,除非你了解所监视的会话(SID 为 151)通过专用服务器还是共享服务器连接 数据库,否则这个问题无法回答,而且就算你知道使用的是专用服务器连接还是共 享服务器连接,可能也很难得出答案。如果采用专用服务器模式,UGA 完全包含在 P GA 中,在这种情况下,进程或线程就使用 487 KB 的内存。如果使用共享服务器,U GA 将从 SGA 中分配,PGA 则在共享服务器中。所以,在共享服务器模式下,从前面 的查询得到最后一行时,共享服务器进程可能会由其他人使用。这个PGA 不再是“我 们的” 了,所以,从技术上讲,我们使用了 149 KB 的内存(除非正在运行查询, 此时还使用了 PGA 和 UGA 之间的 487 KB 内存)。下面在会话 151 中运行第一个大查 询,这个会话采用专用服务器模式,并使用手动 PGA 内存管理。需要说明,这里还 是使用前面的脚本,SQL 文本完全一样,因此可以避免硬解析: 注意 由于我们还没有设置 SORT_AREA_RETAINED_SIZE,所以报告的 SORT _AREA_RETAINED_SIZE 值将是 0,但是排序区实际保留的大小等于 SOR T_AREA_SIZE。 现在,如果在第二个会话中再次运行脚本,会得到下面的结果。注意,这一 次 session xxx memory 和 session xxx memory max 值并不一样。session xxx memory 值表示我们现在使用了多少内存。session xxx memory max 值表示会话 处理查询时某个时刻所使用内存的峰值。 可以看到,使用的内存增加了,这里对数据做了某种排序。在处理查询期间, UGA 临时从 149 KB 增加到 213 KB(增加了 64 KB),然后再收缩回原来的大小。 这是因为,为了完成查询和排序,Oracle 为会话分配了一个排序区。另外,PGA 内存从 487 KB 增加到 551 KB,增加了 64 KB。另外可以看到,我们对临时空间 执行了 2 906 次读和写。 如以上结果所示,完成查询并得到结果集之前,UGA 内存又退回到原来的大 小(从 UGA 释放了排序区),PGA 也会有某种程度的收缩(注意,在 Oracle8i 和以前的版本中,可能根本看不到 PGA 收缩;这是 Oracle9i 及以上版本中新增 的特性)。 下面再来完成这个操作,不过这一次 SORT_AREA_SIZE 增加到 1 MB。注销所 监视的会话,再登录,然后使用 reset_stat.sql 脚本从头开始。因为开始的统 计结果都是一样的,所以这里不再显示,我只给出最后的结果: 下面再在另一个会话中测量内存的使用情况: 可以看到,这一次处理查询期间,PGA 大幅增长。它临时增加了大约 1 728 KB,但是进行数据排序所必须执行的物理I/O 次数则显著下降(使用更多的内存, 就会减少与磁盘的交换)。而且,我们还可以避免一种多趟排序(multipass s ort),如果有太多很小的排序数据集合要合并(或归并),Oracle 最后就要多 次将数据写至临时空间,这种情况下就会发生多趟排序。现在,再来看一个极端 情况: 从另一个会话进行测量,可以看到迄今为止所使用的内存: 可以观察到,尽管允许 SORT_AREA_SIZE 有 1 GB,但实际上只用了大约 6.6 MB。这说明 SORT_AREA_SIZE 设置只是一个上界,而不是默认的分配大小。还要 注意,这里也做了排序,但是这一次完全在内存中进行;而没有使用磁盘上的临 时空间,从物理 I/O 次数为 0 可以看出这一点。 如果在不同版本的 Oracle 上运行这个测试,甚至在不同操作系统上运行这 个测试,都可能会看到不同的行为,相信你得到的数值肯定和我得到的结果稍有 差异。但是一般的行为应该是一样的。换句话说,增加允许的排序区大小并完成 大规模排序时,会话使用的内存量会增加。你可能注意到PGA 内存上上下下地变 化,或者可能一段时间总保持不变(前面已经介绍过这种情况)。例如,如果你 在 Oracle8i 上执行前面的测试,肯定会注意到 PGA 内存大小根本没有收缩(也 就是说,无论什么情况,SESSION PGA MEMORY都等于 SESSION PGA MEMORY MAX)。 这是可以想见的,因为在 8i 中,PGA 作为堆来管理,并通过 malloc()分配内存 来创建。在 9i 和10g 中,则使用了新的方法,会根据需要使用操作系统特有的 内存分配调用来分配和释放工作区。 在使用*_AREA_SIZE 参数时,需要记住以下重要的几点: ‰ 这些参数控制着 SORT、HASH 和/或 BITMAP MERGE 操作所用的最大内存量。 ‰ 一个查询可能有多个操作,这些操作可能都要使用这个内存,这样会创建多个排 序/散列区。要记住,可以同时打开多个游标,每个游标都有自己的 SORT_AREA_ RETAINED 需求。所以,如果把排序区大小设置为 10 MB,在会话中实际上可以 使用 10 MB、100 MB、1 000 MB 或更多 RAM。这些设置并非对会话的限制; 它们只是对一个操作的限制。你的会话中,一个查询可以有多个排序,或者多个 查询需要一个排序。 ‰ 这些内存区都是根据需要来分配的。如果像我们一样,将排序区大小设置为 1 G B,这并不是说你要分配 1 GB 的 RAM,而只是说,你允许 Oracle 进程为一个排序 /散列操作最多分配 1 GB 的内存。 4.1.2 自动 PGA 内存管理 从 Oracle9i Release 1 起,又引入了一种新的方法来管理 PGA 内存,即自 动 PGA 内存管理。这种方法中不再使用 SORT_AREA_SIZE、BITMAP_MERGE_AREA_S IZE 和 HASH_AREA_SIZE 这些参数。引入自动 PGA 内存管理是为了解决以下问题: ‰ 易用性:很多人并不清楚如何设置适当的*_AREA_SIZE 参数。另外这些参数具 体如何工作,内存究竟如何分配,这些问题都很让人困惑。 ‰ 手动分配是一种“以一概全”的方法: 一般地,随着在一个数据库上运行类似应 用的用户数的增加,排序/散列所用的内存量也会线性增长。如果有 10 个并发用户, 排序区大小为 1 MB,这就会使用 10 MB 的内存,100 个并发用户可能使用 100 M B,1 000 个并发用户则可能使用 1 000 MB,依此类推。除非 DBA 一直坐在控制 台前不断地调整排序/散列区大小设置,否则每个人每天可能都会使用同样的设置 值。考虑一下前面的例子,你自己可以清楚地看到, 随着允许使用的 RAM 量的增 加,对临时空间执行的物理 I/O 在减少。如果你自己运行这个例子,肯定会注意到, 随着排序可用 RAM 的增加,响应时间会减少。手动分配会把排序所用的内存量固 定为某个常量值,而不论实际有多少内存。利用自动内存管理的话,只有当内存真 正可用时我们才会使用;自动内存管理会根据工作负载动态地调整实际使用的内存 量。 ‰ 内存控制: 根据上一条,手动分配很难保证 Oracle 实例“合法”地使用内存,甚 至不可能保证。你不能控制实例要用的内存量,因为你根本无从控制会发生多少并 发的排序/散列。很有可能要使用的实际内存(真正的物理空闲内存)过多,而机器 上并没有这么多可用内存。 下面来看自动 PGA 内存管理。首先简单地建立 SGA 并确定其大小。SGA 是一 段大小固定的内存,所以你可以准确地看到它有多大,这将是 SGA 的总大小(除 非你改变了这个大小)。得到了 SGA 的大小后,再告诉 Oracle:“你就要在这 么多内存中分配所有工件区,所谓工作区(work area)只是排序区和散列区的 另一种通用说法。”现在,理论上讲,如果一台机器有 2 GB 的物理内存,可以 分配 768 MB 内存给 SGA,768 MB 内存分配给 PGA,余下的 512 MB 内存留给操作 系统和其他进程。我提到了“理论上”,这是因为实际情况不会毫厘不差,但是 会很接近。为什么会这样呢?在做进一步的解释之前,先来看一下如何建立和打 开自动 PGA 内存管理。 建立自动 PGA 内存管理时,需要为两个实例初始化参数确定适当的值,这两 个参数是: ‰ WORKAREA_SIZE_POLICY:这个参数可以设置为 MANUAL 或 AUTO,如 果 是 MANUAL,会使用排序区和散列区大小参数来控制分配的内存量;如果是 AUTO, 分配的内存量会根据数据库中的当前工作负载而变化。默认值是 AUTO,这也是推 荐的设置。 ‰ PGA_AGGREGATE_TARGET:这个参数会控制实例为完成数据排序/散列的所有 工作区(即排序区和散列区)总共应分配多少内存。在不同的版本中,这个参数的 默认值有所不同,可以用多种工具来设置,如 DBCA。一般来讲,如果使用自动 P GA 内存管理,就应该显式地设置这个参数。 所以,假设 WORKAREA_SIZE_POLICY 设置为 AUTO,PGA_AGGREGATE_TARGET 有 一个非 0 值,就会使用这种新引入的自动 PGA 内存管理。可以在会话中通过 ALT ER SESSION 命令“打开”自动 PGA 内存管理,也可以在系统级通过 ALTER SYST EM 命令打开。 注意 要记住前面的警告,在 Oracle9i 中,共享服务器连接不会使用自动 PGA 内 存管理;而是使用 SORT_AREA_SIZE 和 HASH_AREA_SIZE 参数来确定为 各个操作分配多少 RAM。在 Oracle 10g 及以上版本中,无论哪种连接(专 用服务器连接或共享服务器连接)都能使用自动 PGA 内存管理。对于 Oracl e9i,使用共享服务器连接时,要适当地设置 SORT_AREA_SIZE 和 HASH_ AREA_SIZE 参数,这很重要。 所以,自动 PGA 内存管理的总目标就是尽可能充分地使用 RAM,而且不会超 出可用的 RAM。倘若采用手动内存管理,这个目标几乎无法实现。如果将 SORT_ AREA_SIZE 设置为 10 MB,一个用户完成一个排序操作时,该用户会用掉排序工 作区的 10 MB 内存。如果 100 个用户执行同样的操作,就会用掉 1 000 MB 的内 存。如果你只有 500 MB 的空闲内存,那么无论对于单独的 1 个用户还是对于 10 0 个用户,这种设置都是不合适的。对于单独一个完成排序的用户来说,他本来 可以使用更多的内存(而不只是 10 MB),而对于 100 个想同时完成排序的用户 来说,应该少用一些内存才行(应该少于 10 MB)。自动 PGA 内存管理就是要解 决这种问题。如果工作负载小,随着系统上负载的增加,会最大限度地使用内存; 随着更多的用户完成排序或散列操作,分配给他们的内存量会减少,这样就能达 到我们的目标,一方面使用所有可用的 RAM,另一方面不要超额使用物理上不存 在的内存。 1. 确定如何分配内存 有几个问题经常被问到:“内存是怎么分配的?”以及“我的会话使用了多 少 RAM?”这些问题都很难回答,原因只有一个,文档中没有说明采用自动模式 时分配内存的算法,而且在不同版本中这个算法还可能(而且将会)改变。只要 技术以 A 开头(表示自动,automatic),你就会丧失一定的控制权,而由底层 算法确定做什么以及如何进行控制。 我们可以根据 MetaLink 147806.1 中的一些信息来做一些观察: ‰ PGA_AGGREGATE_TARGET 是一个上限目标,而不是启动数据库时预分配的内 存大小。可以把 PGA_AGGREGATE_TARGET 设置为一个超大的值(远远大于服务 器上实际可用的物理内存量),你会看到,并不会因此分配很大的内存。 ‰ 串行(非并行查询)会话会使用 PGA_AGGREGATE_TARGET 中的很少一部分, 大约 5%或者更少。所以,如果把 PGA_AGGREGATE_TARGET 设置为 100 MB, 可能每个工作区(例如,排序或散列工作区)只会使用大约不到 5 MB。你的会话 中可能为多个查询分配有多个工作区,或者一个查询中就有多个排序/散列操作,但 是不论怎样,每个工作区只会用 PGA_AGGREGATE_TARGET 中不到 5%的内存。 ‰ 随着服务器上工作负载的增加(可能有更多的并发查询和更多的并发用户),分 配给各个工作区的 PGA 内存量会减少。数据库会努力保证所有 PGA 分配的总和不 超过 PGA_AGGREGATE_TARGET 设置的阈值。这就像有一位 DBA 整天坐在控制 台前,不断地根据数据库中完成的工作量来设置 SORT_AREA_SIZE 和 HASH_ARE A_SIZE 参数。稍后会通过一个测试来观察这种行为。 ‰ 一个并行查询最多可以使用 PGA_AGGREGATE_TARGET 的 30%,每个并行进 程会在这 30%中得到自己的那一份。也就是说,每个并行进程能使用的内存量大约 是 0.3*PGA_ AGGREGATE_TARGET / (并行进程数)。 那么,怎么观察分配给会话的不同工作区的大小呢?在介绍手动内存管理那 一节中,我们介绍过一种技术,现在可以采用同样的技术来观察会话所用的内存 以及对临时空间执行的 I/O。以下测试在 Red Hat Advanced Server 3.0 Linux 主机上完成,使用了 Oracle 10.1.0.3 和专用服务器连接。这是一个双 CPU 的 D ell PowerEdge,支持超线程(hyperthreading),所以就好像有 4 个 CPU 一样。 这里又使用了 reset_stat.sql,并对前面的 watch_stat.sql 稍做修改,不仅要 得到一个会话的会话统计信息,还将得到实例总的统计信息。这个稍做修改的 w atch_stat.sql 脚本通过 MERGE 语句来得到这些信息: 除了单个会话的统计信息外,我只增加了 UNION ALL 部分,通过将所有会话 的统计结果累加,从而得到总的PGA/UGA 使用情况和排序写次数。然后在这个会 话中运行以下 SQL*Plus 脚本。在此之前已经创建了 BIG_TABLE 表,而且填入了 50 000 行记录。我删除了这个表的主键,这样余下的只是表本身(从而确保肯 定会执行一个排序过程): 注意 BIG_TABLE 表创建为 ALL_OBJECTS 的一个副本,并增加了主键,行数 由你来定。big_table.sql 脚本在本书开头的“配置环境”一节中介绍过。 下面,对一个数据库运行这个小查询脚本,该数据库的 PGA_AGGREGATE_TARGE T 设置为 256 MB,这说明我希望 Oracle 使用最多约 256 MB 的 PGA 内存来完成排 序。我还建立了另一个脚本,可以在其他会话中运行,它会在机器上生成很大的 排序负载。这个脚本有一个循环,并使用一个内置的包(DBMS_ALERT)查看是否 继续处理。如果是,则再一次运行这个大查询,对整个 BIG_TABLE 表排序。仿真 结束后,再由一个会话通知所有排序进程(也就是负载生成器)“停止”并退出。 执行排序的脚本如下: 以下脚本会让这些进程停止运行: 为了观察对所测量的会话分配的 RAM 量有什么不同,首先独立地运行 SELEC T 查询,这样就只有一个会话。我得到了与前面相同的 6 个统计结果,并把这些统 计结果连同活动会话数保存在另一个表中。然后,再向系统增加 25 个会话(也就 是说,在 25 个新会话中运行以上带循环的基准测试脚本)。接下来等待很短时间 (1 分钟),让系统能针对这个新负载做出调整。然后我创建了一个新会话,用 re set_stat.sql 得到其统计结果,再运行执行排序的查询,接下来运行 watch_stat. sql 得到排序前后的统计结果之差。然后重复地做了这个工作,直至并发用户数达 到 500。 需要说明,这里实际上在要求数据库实例做它根本做不了的事情。前面已经 提到,第一次运行watch_stat.sql 时,每个Oracle 连接在完成排序之前会使用 大约 0.5 MB 的 RAM。如果有 500 个并发用户全部登录,单单是他们登录所用的 内存就已经非常接近所设置的 PGA_AGGREGATE_TARGET(PGA_AGGREGATE_TARGET 设置为 256 MB),更不用说具体做工作了!由此再一次表明,PGA_AGGREGATE_T ARGET 只是一个目标,而不是明确地指定要分配多少空间。出于很多原因,实际 分配的空间还可能超过这个值。 表 4-1 总结了每次增加大约 25 个用户时得到的统计结果。 表 4-1 随着活动会话数的增加,PGA 内存分配行为的变化(PGA_AGGREGATE_ TARGET 设置为 256 MB) 活动会话 一个会话使用的 PGA 系统使用的 PGA 一个会话的临时写 一个会话的临时读 1 7.5 2 0 0 27 7.5 189 0 0 51 4.0 330 728 728 76 4.0 341 728 728 101 3.2 266 728 728 126 1.5 214 728 728 151 1.7 226 728 728 177 1.4 213 728 728 201 1.3 218 728 728 226 1.3 211 728 728 251 1.3 237 728 728 276 1.3 251 728 728 301 1.3 281 728 728 326 1.3 302 728 728 351 1.3 324 728 728 376 1.3 350 728 728 402 1.3 367 728 728 426 1.3 392 728 728 452 1.3 417 728 728 476 1.3 439 728 728 501 1.3 467 728 728 注意 你可能会奇怪,有 1 个活动用户时,为什么系统使用的 RAM 只报告为 2 MB。这与我的测量方法有关。这个仿真会对所测会话中的统计结果建立快 照。接下来,我会在所测的这个会话中运行上述大查询,然后再次记录该会 话统计结果的快照。最后再来测量系统使用了多少 PGA。在我测量所用的 P GA 时,这个会话已经结束,并交回了它用于排序的一些 PGA。所以,这里 系统使用的 PGA 只是对测量时系统所用 PGA 内存的准确度量。 可以看到,如果活动会话不多,排序则完全在内存中执行。活动会话数在1~ 50 之间时,我就可以完全在内存中执行排序。不过,等到有 50 个用户登录并执 行排序时,数据库就会开始控制一次能使用的内存量。所用的PGA 量要退回到可 接受的限值(256 MB)以内,在此之前需要几分钟的时间来调整,不过最后总是 会落回到阈值范围内。分配给会话的 PGA 内存量从 7.5 MB 降到 4 MB,随后又降 到 3.2 MB,最后降至 1.7~1.3 MB 之间(要记住,PGA 中有一部分不用于排序, 也不用于其他操作,光是登录这个动作就要创建 0.5 MB 的 PGA)。系统使用的 总 PGA 量仍保持在可以接受的限值内,直至用户数达到 300~351 之间。从这里 开始,系统使用的 PGA 开始有规律地超过 PGA_AGGREGATE_TARGET,并延续至测 试结束。在这个例子中,我交给数据库实例一个不可能完成的任务,光是支持 3 50 个用户(大多数都执行一个 PL/SQL,再加上他们都要请求排序),我设定的 这个目标(256 MB 的 RAM)就无法胜任。每个会话只能使用尽可能少的内存,但 另一方面又必须分配所需的足够内存,所以这根本就办不到。等我完成这个测试 时,500 个活动会话已经使用了总共 467 MB 的 PGA 内存,大大超出了我所设定 的目标(256 MB),但对每个会话来说使用的内存已经够少的了。 不过,再想想在手动内存管理情况下表 4-1 会是什么样子。假设 SORT_AREA_S IZE 设置为 5 MB。计算很简单:每个会话都能在 RAM 中执行排序(如果实际 RAM 用完了,还可以使用虚拟内存),这样每个会话会使用 6~7 MB 的 RAM(与前面 只有一个用户而且不在磁盘上排序时使用的内存量相当)。再运行前面的测试, 将 SORT_AREA_SIZE 设置为 5 MB,从 1 个用户开始,每次增加 25 个用户,得到的 结果保持一致,如表 4-2 所示。 表 4-2 随着活动会话数的增加,PGA内存分配行为的变化(手动内存管理,SORT _AREA_SIZE设置为 5 MB) 活动会话 一个会话使用的 PGA 系统使用的 PGA 一个会话的临时写 一个会话的临时读 1 6.4 5 728 728 26 6.4 137 728 728 51 6.4 283 728 728 76 6.4 391 728 728 102 6.4 574 728 728 126 6.4 674 728 728 151 6.4 758 728 728 176 6.4 987 728 728 202 6.4 995 728 728 226 6.4 1227 728 728 251 6.4 1383 728 728 277 6.4 1475 728 728 302 6.4 1548 728 728 如果我能完成这个测试(这个服务器上有 2 GB 的实际内存,我的 SGA 是 60 0 MB;等到用户数达到325 时,机器换页和交换开始过于频繁,而无法继续工作), 有 500 个并发用户时,我就要分配大约 2 750 MB 的 RAM!所以,在这个系统上 D BA 可能不会将 SORT_AREA_SIZE 设置为 5 MB,而是设置为 0.5 MB,力图使高峰 期的最大 PGA 使用量在可以忍受的范围内。现在如果并发用户数为 500,就要分 配大约 500 MB 的 PGA,这可能与采用自动内存管理时所观察到的结果类似,但 是即使用户不太多,还是会写临时空间,而不是在内存中执行排序。实际上,如 果 SORT_AREA_SIZE 设置为 0.5 MB,再运行以上测试,会观察到表 4-3 所示的数 据。 表 4-3 随着活动会话数的增加,PGA 内存分配行为的变化(手 动内存管理,SORT_AREA_SIZE 设置为 0.5 MB) 活动会话 一个会话使用的 PGA 系统使用的 PGA 一个会话的临时写 一个会话的临时读 1 1.2 1 728 728 26 1.2 29 728 728 51 1.2 57 728 728 76 1.2 84 728 728 101 1.2 112 728 728 126 1.2 140 728 728 151 1.2 167 728 728 176 1.2 194 728 728 201 1.2 222 728 728 226 1.2 250 728 728 工作负载随着时间的推移而增加或减少时,这种内存的使用完全可以预计, 但是并不理想。自动 PGA 内存管理正是为此设计的。在有足够的内存时,自动内 存管理会让少量的用户尽可能多地使用 RAM,而过一段时间负载增加时,可以减 少分配,再过一段时间,随着负载的减少,为每个操作分配的 RAM 量又能增加。 2. 使用 PGA_AGGREGATE_TARGET 控制内存分配 之前我曾说过,“理论上”可以使用 PGA_AGGREGATE_TARGET 来控制实例使用 的 PGA 内存的总量。不过,从上一个例子中可以看到,这并不是一个硬性限制。 实例会尽力保持在 PGA_AGGREGATE_TARGET 限制以内,但是如果实在无法保证,它 也不会停止处理;只是要求超过这个阈值。 这个限制只是一个“理论上”的限制,对此还有一个原因:尽管工作区在 PGA 内存中所占的比重很大,但 PGA 内存中并非只有工作区。PGA 内存分配涉及很多 方面,其中只有工作区在数据库实例的控制之下。如果创建并执行一个 PL/SQL 代 码块将数据填入一个很大的数组,这里采用专用服务器模式,因此 UGA 在 PGA 中, 倘若是这样,Oracle 只能任由你这样做,而无法干涉。 考虑下面这个小例子。我们将创建一个包,其中可以保存服务器中的一些持 久(全局)数据: 下面,测量这个会话当前使用的 PGA/UGA 内存量(这个例子使用了专用服务 器,所以 UGA 在 PGA 内存中,是 PGA 的一个子集): 所以,最初会话中使用了大约1.5 MB 的 PGA 内存(因为还要编译 PL/SQL包, 运行这个查询,等等)。现在,再对 BIG_TABLE 运行这个查询,这里 PGA_AGGRE GATE_TARGET 同样是 256 MB(这一回是在一个空闲的实例上执行查询;现在我们 是惟一需要内存的会话): 可以看到,排序完全在内存中完成,实际上,如果看一下这个会话的 PGA/U GA 使用情况,就能看出我们用了多少 PGA/UGA 内存: 还是前面观察到的 7.5 MB 的 RAM。现在,再填入包中的 CHAR 数组(CHAR 数 据类型用空格填充,这样每个数组元素的长度都正好是 2 000 个字符): 在此之后,测量会话当前使用的 PGA,可以看到下面的结果: 现在,数据库本身无法控制 PGA 中分配的这些内存。我们已经超过了 PGA_A GGREGATE_TARGET,但数据库对此无计可施,如果它能干预的话,肯定会拒绝我 们的请求,不过只有当操作系统报告称再没有更多可用内存时我们的请求才会失 败。如果想试试看,你可以在数组中分配更多的空间,再向其中放入更多的数据, 等到操作系统报告再无内存可用时,数据库就会“忍无可忍”地拒绝内存分配请 求。 不过,数据库很清楚我们做了什么。尽管有些内存无法控制,但它不会忽略 这部分内存;而是会识别已经使用的内存,并相应地减少为工作区分配的内存大 小。所以,如果再运行同样的排序查询,可以看到,这一次会在磁盘上排序,如 果要在内存中排序,这需要 7 MB 左右的 RAM,但是数据库没有提供这么多 RAM, 原因是分配的内存已经超过了 PGA_AGGREGATE_TARGET: 因此,由于一些 PGA 内存不在 Oracle 的控制之下,所以如果在 PL/SQL 代码 中分配了大量很大的数据结构,就很容易超出 PGA_AGGREGATE_TARGET。在此并 不是建议你绝对不要这样做,我只是想说明 PGA_AGGREGATE_TARGET 不能算是一 个硬性限制,而更应该算是一个请求。 4.1.3 手动和自动内存管理的选择 那么,你要用哪种方法呢?手动还是自动?默认情况下,我倾向于自动 PGA 内存管理。 警告 在这本书里我一而再、再而三地提醒你:不要对生产系统(实际系统)做 任何修改,除非先测试修改有没有副作用。例如,先别阅读这一章,检查你 的系统,看看是不是在使用手动内存管理,然后再打开自动内存管理。查询 计划可能改变,而且也许还会影响性能。可能会发生以下 3 种情况之一: ‰ 运行得完全一样。 ‰ 比以前运行得好。 ‰ 比以前运行得差。 在做出修改之前一定要谨慎,应当先对要做的修改进行测试。 最让 DBA 头疼的一件事可能就是设置各个参数,特别是像 SORT|HASH_AREA_ SIZE 之类的参数。系统运行时这些参数的值可能设置得相当小,而且实在是太 小了,以至于性能受到了负面影响,这种情况我已经屡见不鲜。造成这种情况的 原因可能是默认值本身就非常小:排序区的默认大小为 64 KB,散列区的默认大 小也只是 128 KB。这些值应该多大才合适呢?对此人们总是很困惑。不仅如此, 在一天中的不同时段,你可能还想使用不同的值。早上 8:00 只有两个用户,此 时登录的每个用户使用 50 MB 大小的排序区可能很合适。不过,到中午 12:00, 已经有 500 个用户,再让每个用户使用50 MB 的排序区就不合适了。针对这种情 况,WORKAREA_SIZE_POLICY = AUTO 和相应的 PGA_AGGREGATE_TARGET 就能派上 用场了。要设置 PGA_AGGREGATE_TARGET,也就是你希望 Oracle 能自由使用多大 的内存来完成排序和散列,从概念上讲这比得出最佳的 SORT|HASH_AREA_SIZE 要容易得多,特别是,SORT|HASH_AREA_SIZE 之类的参数并没有一个最佳的值; “最佳值”会随工作负载而变化。 从历史上看,DBA 都是通过设置 SGA(缓冲区缓存、日志缓冲区、共享池、 大池和 Java 池)的大小来配置 Oracle 使用的内存量。机器上余下的内存则由 P GA 区中的专用或共享服务器使用。对于会使用(或不会使用)其中的多少内存, DBA 无从控制。不错,DBA 确实能设置 SORT_AREA_SIZE,但是如果有 10 个并发 的排序,Oracle就会使用 10 * SORT_AREA_SIZE 字节的 RAM。如果有100 个并发 的排序,Oracle 将使用 100 * SORT_AREA_SIZE 字节;倘若是 1 000 个并发的排 序,则会使用 1 000 *SORT_AREA_SIZE;依此类推。不仅如此,再加上 PGA 中还 有其他内容,你根本不能很好地控制系统上最多能使用的 PGA 内存量。 你所希望的可能是:随着系统上内存需求的增加和减少,会使用不同大小的 内存。用户越多,每个用户使用的 RAM 就越少。用户越少,每个用户能使用的 R AM 则越多。设置 WORKAREA_SIZE_POLICY = AUTO 就是要达到这个目的。现在 DB A 只指定一个大小值,即PGA_AGGREGATE_TARGET,也就是数据库应当努力使用的 最大 PGA 内存量。Oracle 会根据情况将这个内存适当地分配给活动会话。另外, 在 Oracle9i Release 2 及以上版本中,甚至还有一个PGA 顾问(PGA advisory), 这是 Statspack 的一部分,可以通过一个 V$动态性能视图得到,也可以在企业 管理器(EM)中看到,PGA 顾问与缓冲区缓存顾问很相似。它会一直告诉你,为 了尽量减少对临时表空间执行的物理 I/O,系统最优的 PGA_AGGREGATE_TARGET 是什么。可以使用这个信息动态修改 PGA 大小(如果你有足够多的 RAM),或者 确定是否需要服务器上的更多 RAM 来得到最优性能。 不过,有没有可能不想使用自动 PGA 内存管理的情况呢?当然有,好在不想 用的情况只是例外,而不是一般现象。自动内存管理力图对多个用户做到“公平”。 由于预见到可能有另外的用户加入系统,因此自动内存管理会限制分配的内存量 只是 PGA_AGGREGATE_TARGET 的一部分。但是假如你不想要公平,确实知道应该 得到所有可用的内存(而不是其中的一部分),这该怎么办?倘若如此,就应该 使用 ALTER SESSION 命令在你的会话中禁用自动内存管理(而不影响其他会话), 并且根据需要,手动地设置你的 SORT|HASH_AREA_SIZE。例如,凌晨 2:00 要做 一个大型的批处理,它要完成大规模的散列联结、建立索引等工作,对于这样一 个批处理作业,你要怎么做?它应该可以使用机器上的所有资源。在内存使用方 面,它不想“公平”,而是全部都想要,因为它知道,现在数据库中除了它以外 再没有别的任务了。当然这个批处理作业可以发出ALTER SESSION 命令,充分使 用所有可用的资源。 所以,简单地讲,对于成天在数据库上运行的应用,我倾向于对最终用户会 话使用自动 PGA 内存管理。手动内存管理则适用于大型批处理作业(它们在特殊 的时段运行,此时它们是数据库中惟一的活动)。 4.1.4PGA 和 UGA 小结 以上讨论了两种内存结构:PGA 和 UGA。你现在应该了解到,PGA 是进程专用 的内存区。这是 Oracle 专用或共享服务器需要的一组独立于会话的变量。PGA 是一个内存“堆”,其中还可以分配其他结构。UGA 也是一个内存堆,其中定义 不同会话特有的结构。如果使用专用服务器来连接 Oracle,UGA 会从 PGA 分配, 如果使用共享服务器连接,UGA 则从 SGA 分配。这说明,使用共享服务器时,必 须适当地设置 SGA 中大池(large pool)的大小,以便有足够的空间来适应可能 并发地连接数据库的每一个用户。所以,如果数据库支持共享服务器连接,与有 类似配置但只使用专用服务器模式的数据库相比,前者的 SGA 通常比后者大得 多。下面将更详细地讨论 SGA。 4.2 系统全局区 每个 Oracle 实例都有一个很大的内存结构,称为系统全局区(System Glob al Area,SGA)。这是一个庞大的共享内存结构,每个 Oracle 进程都会访问其 中的某一点。SGA 的大小不一,在小的测试系统上只有几 MB,在中到大型系统上 可能有几百 MB,对于非常大的系统,甚至多达几 GB。 在 UNIX 操作系统上,SGA 是一个物理实体,从操作系统命令行上能“看到” 它。它物理地实现为一个共享内存段,进程可以附加到这段独立的内存上。系统 上也可以只有 SGA 而没有任何 Oracle 进程;只有内存而已。不过,需要说明, 如果有一个 SGA 而没有任何 Oracle 进程,这就说明数据库以某种方式崩溃了。 这是一种很罕见的情况,但是确实有可能发生。以下是 Red Hat Linux 上 SGA 的“样子”: 这里表示了 3 个 SGA:一个属于操作系统用户 ora10g,另一个属于操作系统 用户 ora9ir2,第 3 个属于操作系统用户 ora9ir1,大小分别约 512 MB、112 MB 和 124 MB。 在 Windows 上,则无法像 UNIX/Linux 上那样把 SGA 看作一个实体。由于在 W indows 平台上, Oracle 会作为有一个地址空间的单个进程来执行,所以 SGA 将作为专用(私有)内存分配给 oracle.exe 进程。如果使用 Windows Task Man ager(任务管理器)或其他性能工具,则可以看到 oracle.exe 总共分配了多少 空间,但是 SGA 和其他已分配的内存无法看到。 在 Oracle 自身内,则完全可以看到 SGA,而不论平台是什么。为此,只需使 用另一个神奇的 V$视图,名为 V$SGASTAT。它可能如下所示(注意,这个代码并 非来自前面的系统;而是来自一个已经适当地配置了相应特性的系统,从而可以 查看所有可用的池): SGA 分为不同的池(pool): ‰ Java 池(Java pool):Java 池是为数据库中运行的 JVM 分配的一段固定大小的 内存。在 Oracle10g 中,Java 池可以在数据库启动并运行时在线调整大小。 ‰ 大池(Large pool):共享服务器连接使用大池作为会话内存,并行执行特性使 用大池作为消息缓冲区,另外 RMAN 备份可能使用大池作为磁盘 I/O 缓冲区。在 O racle 10g 和 9i Release 2 中,大池都可以在线调整大小。 ‰ 共享池(Shared pool):共享池包含共享游标(cursor)、存储过程、状态对象、 字典缓存和诸如此类的大量其他数据。在 Oracle 10g 和 9i 中,共享池都可以在线调 整大小。 ‰ 流池(Stream pool):这是 Oracle 流(Stream)专用的一个内存池,Oracle 流是 数据库中的一个数据共享工具。这个工具是 Oracle 10g 中新增的,可以在线调整大 小。如果未配置流池,但是使用了流功能,Oracle 会使用共享池中至多 10%的空间 作为流内存。 ‰ “空”池(“Null”pool):这个池其实没有名字。这是块缓冲区(缓存的数据 库块)、重做日志缓冲区和“固定 SGA”区专用的内存。 典型的 SGA 可能如图 4-1 所示。 图 4-1 典型的 SGA 对 SGA 整体大小影响最大的参数如下: ‰ JAVA_POOL_SIZE:控制 Java 池的大小。 ‰ SHARED_POOL_SIZE:在某种程度上控制共享池的大小。 ‰ LARGE_POOL_SIZE:控制大池的大小。 ‰ DB_*_CACHE_SIZE:共有 8 个 CACHE_SIZE 参数,控制各个可用的缓冲区缓存 的大小。 ‰ LOG_BUFFER:在某种程度上控制重做缓冲区的大小。 ‰ SGA_TARGET:Oracle 10g 及以上版本中用于自动 SGA 内存管理。 ‰ SGA_MAX_SIZE:用于控制数据库启动并运行时 SGA 可以达到的最大大小。 在 Oracle9i 中,各个 SGA 组件必须由 DBA 手动地设置大小,但是从 Oracle 10g 开始,又有了一个新的选择:自动 SGA 内存管理。如果采用自动 SGA 内存管 理,数据库实例会根据工作负载条件在运行时分配和撤销(释放)各个 SGA 组件。 在 Oracle 10g 中使用自动 SGA 内存管理时,只需把SGA_TARGET 参数设置为所需 的 SGA 大小,其他与 SGA 相关的参数都不用管。只要设置了 SGA_TARGET 参数, 数据库实例就会接管工作,根据需要为各个池分配内存,随着时间的推移,甚至 还会从一个池取出内存交给另一个池。 不论是使用自动内存管理还是手动内存管理,都会发现各个池的内存以一种 称为颗粒(granule,也称区组)的单位来分配。一个颗粒是大小为 4 MB、8 MB 或 16 MB 的内存区。颗粒是最小的分配单位,所以如果想要一个 5 MB 的 Java 池,而且颗粒大小为 4 MB,Oracle 实际上会为这个 Java 池分配 8 MB(在 4 的 倍数中,8 是大于或等于 5 的最小的数)。颗粒的大小由 SGA 的大小确定(听上 去好像又转回来了,因为 SGA 的大小取决于颗粒的大小)。通过查询 V$SGA_DYN AMIC_ COMPONENTS,可以查看各个池所用的颗粒大小。实际上,还可以使用这个 视图来查看 SGA 的总大小如何影响颗粒的大小: 在这个例子中,我使用了自动 SGA 内存管理,并通过一个参数(SGA_TARGET) 来控制 SGA 的大小。SGA 小于 1 GB 时,颗粒为 4 MB。当 SGA 大小增加到超过阈 值 1 GB 时(对于不同的操作系统,甚至对于不同的版本,这个阈值可能稍有变 化),可以看到颗粒大小有所增加: 可以看到,SGA 为 1.5 GB 时,会以 16 MB 的颗粒为池分配空间,所以池大小 都将是 16 MB 的某个倍数。 记住这一点,下面逐一分析各个主要的 SGA 组件。 4.2.1 固定 SGA 固定 SGA(fixed SGA)是 SGA 的一个组件,其大小因平台和版本而异。安装 时,固定 SGA 会“编译到”Oracle 二进制可执行文件本身当中(所以它的名字 里有“固定”一词)。在固定 SGA 中,有一组指向 SGA 中其他组件的变量,还有 一些变量中包含了各个参数的值。我们无法控制固定 SGA 的大小,不过固定 SGA 通常都很小。可以把这个区想成是 SGA 中的“自启”区,Oracle 在内部要使用 这个区来找到 SGA 的其他区。 4.2.2 重做缓冲区 如果数据需要写到在线重做日志中,则在写至磁盘之前要在重做缓冲区(re do buffer)中临时缓存这些数据。由于内存到内存的传输比内存到磁盘的传输 快得多,因此使用重做日志缓冲区可以加快数据库的操作。数据在重做缓冲区里 停留的时间不会太长。实际上,LGWR 会在以下某个情况发生时启动对这个区的 刷新输出(flush): ‰ 每 3 秒一次 ‰ 无论何时有人提交请求 ‰ 要求 LGWR 切换日志文件 ‰ 重做缓冲区 1/3 满,或者包含了 1 MB 的缓存重做日志数据 由于这些原因,如果重做缓冲区的大小超过几 MB,通常对系统就没有什么意 义了,实际上,能从这么大的重做缓冲区得到好处的系统极为少见。如果是一个 有大量并发事务的大型系统,也许大的重做日志缓冲区会对它有利,因为 LGWR (这个进程负责将重做日志缓冲区刷新输出到磁盘)写日志缓冲区的一部分时, 其他会话可能会在缓冲区中填入新的数据。一般而言,如果一个事务长时间运行, 就会生成大量重做日志,倘若采用更大的日志缓冲区而不是正常的日志缓冲区, 对这种事务最有好处。因为在 LGWR 忙于将部分重做日志写出到磁盘时,重做日 志缓冲区还会继续填入日志。事务越大、越长,大日志缓冲区的好处就越显著。 重做缓冲区的默认大小由 LOG_BUFFER 参数控制,取值为 512 KB 和(128 * C PU 个数)KB 中的较大者。这个区的最小大小取决于操作系统。如果想知道到底是 多少,只需将 LOG_BUFFER 设置为 1 字节,再重启数据库。例如,在我的 Red H at Linux 实例上,可以看到以下输出: 在这个系统上,可能的最小日志缓冲区就是 256 KB(而不论我设置的是多 少)。 4.2.3 块缓冲区缓存 到目前为止,我们已经介绍了 SGA 中一些相对较小的组件。下面再来看看可 能比较大的组件。Oracle 将数据库块写至磁盘之前,另外从磁盘读取数据库块 之后,就会把这些数据库块存储在块缓冲区缓存(block buffer cache)中。对 我们来说,这是 SGA 中一个很重要的区。如果太小,我们的查询就会永远也运行 不完。如果太大,又会让其他进程饥饿(例如,没有为专用服务器留下足够的空 间来创建其 PGA,甚至无法启动)。 在 Oracle 的较早版本中,只有一个块缓冲区缓存,所有段的所有块都放在 这个区中。从 Oracle 8.0 开始,可以把 SGA 中各个段的已缓存块放在 3 个位置 上: ‰ 默认池(default pool):所有段块一般都在这个池中缓存。这就是原先的缓冲区 池(原来也只有一个缓冲区池)。 ‰ 保持池(keep pool):按惯例,访问相当频繁的段会放在这个候选的缓冲区池中, 如果把这些段放在默认缓冲区池中,尽管会频繁访问,但仍有可能因为其他段需要 空间而老化(aging)。 ‰ 回收池(recycle pool):按惯例,访问很随机的大段可以放在这个候选的缓冲区 池中,这些块会导致过量的缓冲区刷新输出,而且不会带来任何好处,因为等你想 要再用这个块时,它可能已经老化退出了缓存。要把这些段与默认池和保持池中的 段分开,这样就不会导致默认池和保持池中的块老化而退出缓存。 需要注意,在保持池和回收池的描述中,我用了一个说法“按惯例”。因为 你完全有可能不按上面描述的方式使用保持池或回收池,这是无法保证的。实际 上,这3 个池会以大体相同的方式管理块;将块老化或缓存的算法并没有根本的 差异。这样做的目标是让 DBA 能把段聚集到“热”区 (hot)、“温”区 (warm) 和“不适合缓存”区 (do not care to cache)。理论上讲,默认池中的对象应 该足够热(也就是说,用得足够多),可以保证一直呆在缓存中。缓存会把它们 一直留在内存中,因为它们是非常热门的块。可能还有一些段相当热门,但是并 不太热;这些块就作为温块。这些段的块可以从缓存刷新输出,为不常用的一些 块(“不适合缓存”块)腾出空间。为了保持这些温段的块得到缓存,可以采取 下面的某种做法: ‰ 将这些段分配到保持池,力图让温块在缓冲区缓存中停留得更久。 ‰ 将“不适合缓存”段分配到回收池,让回收池相当小,以便块能快速地进入缓存 和离开缓存(减少管理的开销)。 这样会增加 DBA 所要执行的管理工作,因为要考虑 3 个缓存,要确定它们的 大小,还要为这些缓存分配对象。还要记住,这些池之间没有共享,所以,如果 保持池有大量未用的空间,即使默认池或回收池空间不够用了,保持池也不会把 未用空间交出来。总之,这些池一般被视为一种非常精细的低级调优设备,只有 所有其他调优手段大多用过之后才应考虑使用(如果可以重写查询,将I/O 减少 为原来的 1/10,而不是建立多个缓冲区池,我肯定会选择前者!)。 从 Oracle9i 开始,除了默认池、保持池和回收池外,DBA 还要考虑第 4 种可 选的缓存:db_Nk_caches。增加这些缓存是为了支持数据库中多种不同的块大小。 在 Oracle9i 之前,数据库中只有一种块大小(一般是 2 KB、4 KB、8 KB、16 K B 或 32 KB)。从 Oracle9i 开始,数据库可以有一个默认的块大小,也就是默认 池、保持池或回收池中存储的块的大小,还可以有最多 4 种非默认的块大小,请 见第 3 章的解释。 与原来默认池中的块一样,这些缓冲区缓存中的块会以同样的方式管理,没 有针对不同的池采用任何特殊的算法。下面来看在这些池中如何管理块。 1. 在缓冲区缓存中管理块 为简单起见,这里假设只有一个默认池。由于其他池都以同样的方式管理, 所以我们只需要讨论其中一个池。 缓冲区缓存中的块实质上在一个位置上管理,但有两个不同的列表指向这些 块: ‰ 脏(dirty)块列表,其中的块需要由数据库块写入器(DBWn;稍后将介绍这个 进程)写入磁盘。 ‰ 非脏(nondirty)块列表。 在 Oracle 8.0 及以前版本中,非脏块列表就是最近最少使用(Least Rece ntly Used,LRU)列表。块按使用的顺序列出。在 Oracle8i 及以后版本中,算 法稍有修改。不再按物理顺序来维护块列表,Oracle 采用了一种接触计数(to uch count,也称使用计数)算法,如果命中缓存中的一个块,则会增加与之相 关联的计数器。不是说每次命中这个块都会增加计数,而是大约每 3 秒一次(如 果你连续命中的话)。有一组相当神奇的 X$表,利用其中的某个表就可以看出 这个算法是怎样工作的。在 Oracle 的文档中完全没有提到 X$表,但是有关的 信息还是时不时地会漏出来一些。 X$BH 表显示了块缓冲区缓存中块的有关信息(文档中有记录的 V$BH 视图也 能提供块的有关信息,不过 X$BH 表提供的信息更多)。在这个表中可以看到, 我们命中块时,接触计数会增加。可以对这个表运行以下查询,得到 5 个“当 前最热的块”,并把这个信息与 DBA_OBJECTS 视图联结,得出这些块属于哪些 段。这个查询按 TCH(接触计数)列对 X$BH 中的行排序,并保留前 5 行。然后 按 X$BH.OBJ 等于 DBA_OBJECTS.DATA_OBJECT_ID 为条件将 X$BH 信息与 DBA_OBJ ECTS 联结: 注意 (2^32 – 1) 或 4 294 967 295 是一个神奇的数,常用来指示“特殊”的块。 如果想了解块关联的信息,可以使用查询 select * from dba_extents where file_id = FILE# and block_id <= = D BABLK。 你可能会问,'maybe!'是什么意思,前面的标量子查询中为什么使用 MAX()。 这是因为,DATA_OBJECT_ID 不是 DBA_OBJECTS 视图中的“主键”,通过下面的 例子可以说明这一点: 这是因为存在聚簇(cluster),有关内容见第 10 章的讨论,其中可能包含 多个表。因此,从 X$BH 联结 DBA_OBJECTS 来打印一个段名时,从技术上讲,我 们必须列出聚簇中所有对象的所有名字,因为数据库块并不一直属于一个表。 甚至对于重复查询的块,我们也可以观察 Oracle 如何递增这个块的接触计 数。在这个例子中我们使用了一个神奇的表 DUAL,可以知道这是一个只有一行 一列的表。我们想得出这一行的块信息。内置的 DBMS_ROWID 包就很适合得到这 个信息。另外,由于我们要从 DUAL 查询 ROWID,所以 Oracle 会从缓冲区缓存读 取真正的 DUAL 表,而不是 Oracle 10g 增加的“虚拟”DUAL 表。 注意 在 Oracle 10g 以前,查询 DUAL 就会导致对数据字典中存储的一个实际 D UAL 表进行全表扫描。如果打开 autotrace(自动跟踪),并查询 SELECT DUMMY FROM DUAL,不论是哪个 Oracle 版本,你都会观察到存在一些 I/O(一致获取,consistent get)。在 9i 及以前的版本中,如果在 PL/SQL 中 查询 SELECT SYSDATE FROM DUAL 或 variable := SYSDATE,也会看 到出现实际的 I/O。不过,在 Oracle 10g 中,会把 SELECT SYSDATE 识别 为不需要真正查询 DUAL 表(因为你没有从该表中请求列或 rowid),而会 使用另一种方法,就像是调用一个函数。因此,不会对 DUAL 做全表扫描, 而只是向应用返回 SYSDATE。对于大量使用 DUAL 的系统来说,仅仅这样 一个很小的改动,就能大大减少所要执行的一致获取操作的次数。 所以,每次运行以下查询时,都会命中真正的 DUAL 表: 在不同的 Oracle 版本上,输出可能不同,你可能还会看到返回不止两行。 也许你观察到 TCH 并没有每次都递增。在一个多用户系统上,结果可能更难预料。 Oracle 试图每 3 秒将 TCH 递增一次(还有一个 TIM 列,它会显示对 TCH 列最后 一次更新的时间),但是这个数是否 100%正确并不重要,只要接近就行。另外, Oracle 会有意地“冷却”块,过一段时间会让 TCH 计数递减。所以,如果你在 自己的系统上运行这个查询,可能会看到完全不同的结果。 因此,在 Oracle8i 及以上版本中,块缓冲区不再像以前那样移到块列表的 最前面;而是原地留在块列表中,只是递增它的接触计数。不过,过一段时间后, 块会很自然地在列表中“移动”。这里把“移动”一词用引号括起来,这是因为 块并不是物理地移动;只是因为维护了多个指向块的列表,所以块会在这些列表 间“移动”。例如,已修改的块由脏列表指示(要由 DBWn 写至磁盘)。过一段 时间要重用块时,如果缓冲区缓存满了,就要将接触计数较小的某个块释放,将 其“放回到”新数据块列表的接近于中间的位置。 管理这些列表的整个算法相当复杂,而且随着 Oracle 版本的变化也在变化, 并不断改进。作为开发人员,我们并不需要关心所有细节,只要知道频繁使用的 块会被缓存,不常使用的块不会缓存太久,这就够了。 2. 多个块大小 从 Oracle9i 开始,同一个数据库中可以有多个不同的数据库块大小。此前, 一个数据库中的所有块大小都相同,要想使用一个不同的块大小,必须重新建立 整个数据库。现在就不同了,你可以有一个“默认的”块大小(最初创建数据库 时使用的块大小;即 SYSTEM 和所有 TEMPORARY 表空间的块大小),以及最多 4 个其他的块大小。每个不同的块大小都必须有其自己的缓冲区缓存。默认池、保 持池和回收池只缓存具有默认大小的块。为了在数据库中使用非默认的块大小, 需要配置一个缓冲区池来保存这些块。 在这个例子中,我的默认块大小是 8 KB。我想创建一个块大小为 16 KB 的表 空间: 现在,由于我还没有配置一个16 KB 的缓存,所以无法创建这样一个表空间。 要解决这个问题,可以在以下方法中选择一种。我可以设置 DB_16K_CACHE_SIZE 参数,并重启数据库。也可以缩小另外的某个 SGA 组件,从而在现有的 SGA 中腾 出空间来建立一个 16 KB 的缓存。或者,如果 SGA_MAX_SIZE 参数大于当前的 SG A 大小,我还可以直接分配一个 16 KB 的缓存。 注意 从 Oracle9i 开始,即使数据库已经启动并且正在运行,你也能重新设置 各个 SGA 组件的大小。如果你想拥有这个能力,能够“扩大”SGA 的大 小(超过初始分配的大小),就必须把 SGA_MAX_SIZE 参数设置为大于 已分配 SGA 的某个值。例如,如果启动之后,你的 SGA 大小为 128 MB, 你想再为缓冲区缓存增加另外的 64 MB,就必须把 SGA_MAX_SIZE 设置 为 192 MB 或更大,以便扩展。 在这个例子中,我采用收缩的办法,即缩小我的 DB_CACHE_SIZE,因为目前 这个参数设置得太大了: 这样一来,我就建立了另外一个缓冲区缓存,要用来缓存 16 KB 大小的块。 默认池(由 db_cache_size 参数控制)大小为 768 MB,16 KB 缓存(由 db_16k_ cache_size 参数控制)大小为 256 MB。这两个缓存是互斥的,如果一个“填满” 了,也无法使用另一个缓存中的空间。这样 DBA 就能很精细地控制内存的使用, 但是这也是有代价的。代价之一就是复杂性和管理。使用多个块大小的目的并不 是为了性能或作为一个调优特性,而是为了支持可传输的表空间,也就是可以把 格式化的数据文件从一个数据库传输或附加到另一个数据库。比如说,通过实现 多个块大小,可以取得一个使用 8 KB 块大小的事务系统中的数据文件,并将此 信息传输到使用 16 KB 或 32 KB 块大小的数据仓库。 不过,对于测试来说,有多个块大小很有好处。如果你想看看你的数据库如 何处理另一个块大小,例如,如果使用 4 KB 的块而不是 8 KB 的块,一个表会占 用多大的空间。现在由于可以支持多个块大小,你就能很轻松地进行测试,而不 必创建一个全新的数据库实例。 还可以把多个块大小用作一种精细调优工具,对一组特定的段进行调优,也就 是为这些段分配各自的私有缓冲区池。或者,在一个具有事务用户的混合系统中, 事务用户可能使用一组数据,而报告/仓库用户查询另外一组单独的数据。如果块 比较小,这对事务数据很有好处,因为这样会减少块上的竞争(每个块上的数据/ 行越少,就意味着同时访问同一个块的人越少),另外还可以更好地利用缓冲区 缓存(用户只向缓存读入他们感兴趣的数据,可能只有一行或者很少的几行)。 报告/仓库数据(可能以事务数据为基础)则不同,块更大一些会更好,其部分原 因在于这样块开销会较少(所占的总存储空间较小),而且逻辑 I/O 处理的数据 更多。由于报告/仓库数据不存在事务数据那样的更新竞争问题,所以如果每个块 上有更多的行,这并不是问题,反而是一个优点。另外,事务用户实际上会得到 自己的缓冲区缓存;他们并不担心报告查询会过分占用缓存。 但是一般来讲,默认池、保持池和回收池对于块缓冲区缓存精细调优来说应 该已经足够了,多个块大小主要用于从一个数据库向另一个数据库传输数据,可 能在混合的报告/事务系统中也会用到这种机制。 4.2.4 共享池 共享池是 SGA 中最重要的内存段之一,特别是对于性能和可扩缩性来说。共 享池如果太小,会严重影响性能,甚至导致系统看上去好像中止了一样。如果共 享池太大,也会有同样的效果。共享池使用不当会导致灾难性的后果。 那么,到底什么是共享池?共享池就是 Oracle 缓存一些“程序”数据的地 方。在解析一个查询时,解析得到的表示(representation)就缓存在那里。在 完成解析整个查询的任务之前, Oracle 会搜索共享池,看看这个工作是否已经 完成。你运行的 PL/SQL 代码就在共享池中缓存,所以下一次运行时,Oracle 不 会再次从磁盘重新读取。PL/SQL 代码不仅在这里缓存,还会在这里共享。如果 有 1 000 个会话都在执行同样的代码,那么只会加载这个代码的一个副本,并由 所有会话共享。Oracle 把系统参数存储在共享池中。数据字典缓存(关于数据 库对象的已缓存信息)也存储在这里。简单地讲,就像是厨房的水池一样,什么 东西都往共享池里放。 共享池的特点是有大量小的内存块(chunk),一般为 4 KB 或更小。要记住, 4 KB 并不是一个硬性限制,可能有的内存分配会超过这个大小,但是一般来讲, 我们的目标是使用小块的内存来避免碎片问题。如果分配的内存块大小显著不同 (有的很小,有的却相当大),就可能出现碎片问题。共享池中的内存根据 LRU (最近最少使用)的原则来管理。在这方面,它类似于缓冲区缓存,如果你不用 某个对象,它就会丢掉。为此提供了一个包,名叫 DBMS_SHARED_POOL,这个包 可用于改变这种行为,强制性地“钉住”共享池中的对象。可以使用这个过程在 数据库启动时加载频繁使用的过程和包,并使它们不至于老化。不过,通常如果 过一段时间共享池中的一段内存没有得到重用,它就会老化。甚至 PL/SQL 代码 (可能相当大)也以一种分页机制来管理,这样当你执行一个非常大的包中的代 码时,只有所需的代码会加载到共享池的小块中。如果你很长时间都没有用它, 而且共享池已经填满,需要为其他对象留出空间,它就会老化。 如果你真的想破坏 Oracle 的共享池,最容易的办法是不使用绑定变量。在 第 1 章已经看到,如果不使用绑定变量,可能会让系统陷于瘫痪,这有两个原因: ‰ 系统要花大量 CPU 时间解析查询。 ‰ 系统使用大量资源来管理共享池中的对象,因为从来不重用查询。 如果提交到 Oracle 的每个查询都是具有硬编码值的惟一查询,则共享池的 概念就一点用都没有。设计共享池是为了反复使用查询计划。如果每个查询都 是全新的,都是以前从来没有见过的查询,那么缓存只会增加开销。共享池反 而会损害性能。为了解决这个问题,很多人都会用一种看似合理的常用技术, 也就是向共享池增加更多的空间,但是这种做法一般只会使问题变得比以前更 糟糕。由于共享池不可避免地会再次填满,比起原来较小的共享池来说,开销 甚至更大,原因很简单,与管理一个较小的满共享池相比,管理一个大的满共 享池需要做更多的工作。 对于这个问题,真正的解决方案只有一个,这就是使用共享 SQL,也就是重 用查询。在前面(第 1 章),我们简要介绍了参数 CURSOR_SHARING,在这方面, 游标共享可以作为一种短期的解决方案。不过,真正要解决这个问题,首当其冲 地还是要使用可重用的 SQL。即使在最大的系统上,我发现一般也最多有 10 00 0~20 000 条不同的 SQL 语句。大多数系统只执行数百个不同的查询。 下面是一个真实的示例,从这个例子可以看出,如果共享池使用不当后果 会有多严重。我曾参与过这样一个系统,它的标准操作过程是每天晚上关闭数 据库,清空 SGA,再重启。之所以这样做只是因为,系统白天运行时有问题, 会完全占用 CPU,所以如果数据库运行的时间超过一天,性能就开始严重下降。 他们原来在一个 1.1 GB 的 SGA 中使用了 1 GB 的共享池。确实如此:0.1 GB 由 块缓冲区缓存和其他元素专用,另外 1 GB 则完全用于缓存不同的查询,但这些 查询从来都不会再次执行。必须冷启动的原因是,如果让系统运行一天以上的 时间,就会用光共享池中的空闲内存。此时,结构老化的开销就太大了(特别 是对于一个如此大的结构),系统会为此疲于奔命,而性能也会大幅恶化(不 过原来的性能也好不到哪里去,因为他们管理的是一个 1 GB 的共享池)。另外, 使用这个系统的人一直想向机器增加越来越多的 CPU,因为硬解析 SQL 太耗费 C PU。通过对应用进行修正,让它使用绑定变量,不仅消除了物理的硬件需求(现 在的 CPU 能力比他们实际需要的已经高出几倍),而且对各个池的内存分配也 反过来了。现在不是使用 1 GB 的共享池,而只为共享池分配了不到 100 MB 的 空间,即使是经过数周连续地运行,也不会用光共享池的内存。 关于共享池和参数 SHARED_POOL_SIZE 还有一点要说明。在 Oracle9i 及以前 的版本中,查询的结果与 SHARED_POOL_SIZE 参数之间没有直接的关系,查询结 果是: SHARED_POOL_SIZE 参数是: 如果实在要谈谈它们的关系,只能说 SUM(BYTES) FROM V$SGASTAT 总是大于 SHARED_ POOL_SIZE。共享池还保存了另外的许多结构,它们不在相应参数的作 用域内。SHARED_POOL_SIZE 通常占了共享池(SUM(BYTES)报告的结果)中最大 的一部分,但这不是共享池中惟一的一部分。例如,参数 CONTROL_FILES 就为共 享池“混合”部分做出了贡献,每个文件有 264 字节。遗憾的是,V$SGASTAT 中 的“共享池”与参数 SHARED_POOL_SIZE 的命名让人很容易混淆,这个参数对共 享池大小贡献最大,但是它并不是惟一有贡献的参数。 不过,在 Oracle 10g 及以上版本中,应该能看到二者之间存在一对一的对 应关系,假设你使用手动的 SGA 内存管理(也就是说,自己设置 SHARED_POOL_S IZE 参数): 如果你从 Oracle9i 或之前的版本转向 10g,这是一个相当重要的改变。在 Oracle10g 中,SHARED_POOL_SIZE 参数控制了共享池的大小,而在 Oracle9i 及 之前的版本中,它只是共享池中贡献最大的部分。你可能想查看 9i(或之前版 本)中实际的共享池大小(根据 V$SGASTAT),并使用这个数字来设置 Oracle 10g(及以上版本)中的 SHARED_POOL_SIZE 参数。用于增加共享池大小的多种其 他组件现在期望由你来分配内存。 4.2.5 大池 大池(large pool)并不是因为它是一个“大”结构才这样取名(不过,它 可能确实很大)。之所以称之为大池,是因为它用于大块内存的分配,共享池不 会处理这么大的内存块。 在 Oracle 8.0 引入大池之前,所有内存分配都在共享池中进行。如果你使 用的特性要利用“大块的”内存分配(如共享服务器 UGA 内存分配),倘若都在 共享池中分配就不太好。另外,与共享池管理内存的方式相比,处理(需要大量 内存分配)会以不同的方式使用内存,所以这个问题变得更加复杂。共享池根据 LRU 来管理内存,这对于缓存和重用数据很合适。不过,大块内存分配则是得到 一块内存后加以使用,然后就到此为止,没有必要缓存这个内存。 Oracle 需要的应该是像为块缓冲区缓存实现的回收和保持缓冲区池之类的 组件。这正是现在的大池和共享池。大池就是一个回收型的内存空间,共享池则 更像是保持缓冲区池。如果对象可能会被频繁地使用,就将其缓存起来。 大池中分配的内存在堆上管理,这与 C 语言通过 malloc()和 free()管理内 存很相似。一旦“释放”了一块内存,它就能由其他进程使用。在共享池中,实 际上没有释放内存块的概念。你只是分配内存,然后使用,再停止使用而已。过 一段时间,如果需要重用那个内存,Oracle 会让你的内存块老化。如果只使用 共享池,问题在于:一种大小不一定全局适用。 大池专门用于以下情况: ‰ 共享服务器连接,用于在 SGA 中分配 UGA 区 ‰ 语句的并行执行,允许分配进程间的消息缓冲区,这些缓冲区用于协调并行查 询服务器。 ‰ 备份,在某些情况下用于 RMAN 磁盘 I/O 缓冲区。 可以看到,这些内存分配都不应该在 LRU 缓冲区池中管理,因为 LRU 缓冲区 池的目标是管理小块的内存。例如,对于共享服务器连接内存,一旦会话注销, 这个内存就不会再重用,所以应该立即返回到池中。另外,共享服务器 UGA 内存 分配往往“很大”。如果查看前面使用 SORT_AREA_RETAINED_SIZE 或 PGA_AGGRE GATE_TARGET 的例子,可以看到,UGA可能扩张得很大,成为绝对大于4 KB 的块。 把 MTS 内存放在共享池中,这会导致把它分片成很小的内存,不仅如此,你还会 发现从不重用的大段内存会导致可能重用的内存老化。这就要求数据库以后多做 更多的工作来重建内存结构。 对于并行查询消息缓冲区也是如此,因为它们不能根据 LRU 原则来管理。并 行查询消息缓冲区可以分配,但是在使用完之前不能释放。一旦发送了缓冲区中 的消息,就不再需要这个缓冲区,应该立即释放。对于备份缓冲区更是如此,备 份缓冲区很大,而且一旦 Oracle 用完了这些缓冲区,它们就应该“消失”。 使用共享服务器连接时,并不是一定得使用大池,但是强烈建议你使用大池。 如果没有大池,而且使用了一个共享服务器连接,就会像 Oracle 7.3 及以前版 本中一样从共享池分配空间。过一段时间后,这会导致性能恶化,一定要避免这 种情况。如果 DBWR_IO_SLAVES 或者 PARALLEL_MAX_SERVERS 参数设置为某个正值, 大池会默认为某个大小。如果你使用了一个用到大池的特性,建议你手动设置大 池的大小。默认机制一般并不适合你的具体情况。 4.2.6Java 池 Java 池(Java pool)是 Oracle 8.1.5 版本中增加的,目的是支持在数据库 中运行 Java。如果用 Java 编写一个存储过程,Oracle 会在处理代码时使用 Jav a 池的内存。参数 JAVA_POOL_SIZE 用于确定为会话特有的所有 Java 代码和数据 分配多大的 Java 池内存量。 Java 池有多种用法,这取决于 Oracle 服务器运行的模式。如果采用专用服 务器模式,Java 池包括每个 Java 类的共享部分,由每个会话使用。这些实质上 是只读部分(执行向量、方法等),每个类的共享部分大约 4~8 KB。 因此,采用专用服务器模式时(应用使用纯Java存储过程时往往就会出现这 种情况),Java池所需的总内存相当少,可以根据要用的Java类的个数来确定。 应该知道,如果采用专用服务器模式,每个会话的状态不会存储在SGA中,因为 这个信息要存储在UGA中,你应该记得,使用专用服务器模式时,UGA包括在PGA 中。 使用共享服务器连接来连接 Oracle 时,Java 池包括以下部分: ‰ 每个 Java 类的共享部分。 ‰ UGA 中用于各会话状态的部分,这是从 SGA 中的 JAVA_POOL 分配的。UGA 中余下的部分会正常地在共享池中分配,或者如果配置了大池,就会在大池中分配。 由于 Oracle9i 及以前版本中 Java 池的总大小是固定的,应用开发人员需要 估计应用的总需求,再把估计的需求量乘以所需支持的并发会话数,所得到的结 果能指示出 Java 池的总大小。每个Java UGA 会根据需要扩大或收缩,但是要记 住,池的大小必须合适,所有 UGA 加在一起必须能同时放在里面。在 Oracle10 g 及以上版本中,这个参数可以修改,Java 池可以随着时间的推移而扩大和收 缩,而无需重启数据库。 4.2.7 流池 流池(stream pool)是一个新的 SGA 结构,从 Oracle 10g 开始才增加的。 流(Stream)本身就是一个新的数据库特性,Oracle9i Release 2 及以上版本 中才有。它设计为一个数据库共享/复制工具,这是Oracle 在数据复制方面发展 的方向。 注意 上面提到了流“是 Oracle 在数据复制方面发展的方向”,这句话不能解释 为高级复制(Advanced Replication,这是 Oracle 现有的复制特性)会很快 过时。相反,将来几个版本中还会支持高级复制。要了解流本身的更多内容, 请参考 Streams Concepts Guide (在 http://otn.oracle.com 的 Documentation 部分)。 流池(或者如果没有配置流池,则是共享池中至多 10%的空间)会用于缓存 流进程在数据库间移动/复制数据时使用的队列消息。这里并不是使用持久的基 于磁盘的队列(这些队列有一些附加的开销),流使用的是内存中的队列。如果 这些队列满了,最终还是会写出到磁盘。如果使用内存队列的 Oracle 实例由于 某种原因失败了,比如说因为实例错误(软件瘫痪)、掉电或其他原因,就会从 重做日志重建这些内存中的队列。 因此,流池只对使用了流数据库特性的系统是重要的。在这些环境中,必须 设置流池,以避免因为这个特性从共享池“窃取”10%的空间。 4.2.8 自动 SGA 内存管理 与管理 PGA 内存有两种方法一样,从 Oracle 10g 开始,管理 SGA 内存也有 两种方法:手动管理和自动管理。手动管理需要设置所有必要的池和缓存参数, 自动管理则只需设置少数几个内存参数和一个 SGA_TARGET 参数。通过设置 SGA_ TARGET 参数,实例就能设置各个 SGA 组件的大小以及调整它们的大小。 注意 在 Oracle9i 及以前版本中,只能用手动 SGA 内存管理,不存在参数 SGA_ TARGET,而且参数 SGA_MAX_SIZE 只是一个上限,而不是动态目标。 在 Oracle 10g 中,与内存相关的参数可以归为两类: ‰ 自动调优的 SGA 参数:目前这些参数包括 DB_CACHE_SIZE、SHARED_POOL_ SIZE、LARGE_POOL_SIZE 和 JAVA_POOL_SIZE。 ‰ 手动 SGA 参数:这些参数包括 LOG_BUFFER、STREAMS_POOL、DB_NK_CA CHE_SIZE、DB_KEEP_CACHE_SIZE 和 DB_RECYCLE_CACHE_SIZE。 在 Oracle 10g 中,任何时候你都能查询V$SGAINFO,来查看 SGA 的哪些组 件的大小可以调整。 注意 要使用自动 SGA 内存管理,参数 STATISTICS_LEVEL 必须设置为 TYPI CAL 或 ALL。如果不支持统计集合,数据库就没有必要的历史信息来确定 大小。 采用自动 SGA 内存管理时,确定自动调整组件大小的主要参数是 SGA_TARGE T,这个参数可以在数据库启动并运行时动态调整,最大可以达到 SGA_MAX_SIZE 参数设置的值(默认等于 SGA_TARGET,所以如果想增加 SGA_TARGET,就必须在 启动数据库实例之前先把 SGA_MAX_SIZE 设置得大一些)。数据库会使用 SGA_TA RGET 值,再减去其他手动设置组件的大小(如 DB_KEEP_CACHE_SIZE、DB_RECYC LE_CACHE_SIZE 等),并使用计算得到的内存量来设置默认缓冲区池、共享池、 大池和 Java 池的大小。在运行时,实例会根据需要动态地对这 4 个内存区分配 和撤销内存。如果共享池内存用光了,实例不会向用户返回一个 ORA-04031“Un able to allocate N bytes of shared memory”(无法分配N 字节的共享内存) 错误,而是会把缓冲区缓存缩小几 MB(一个颗粒的大小),再相应地增加共享 池的大小。 随着时间的推移,当实例的内存需求越来越确定时,各个 SGA 组件的大 小也越来越固定。即便数据库关闭后又启动,数据库还能记得组件的大小, 因此不必每次都从头再来确定实例的正确大小。这是通过 4 个带双下划线的 参数做到的:__DB_CACHE_SIZE、__JAVA_POOL_SIZE、__LARGE_POOL_SIZE 和 __SHARED_POOL_SIZE。如果正常或立即关闭数据库,则数据库会把这些值记 录到存储参数文件(SPFILE)中,并在启动时再使用这些值来设置各个区的 默认大小。 另外,如果知道 4 个区中某个区的最小值,那么除了设置 SGA_TARGET 外, 还可以设置这个参数。实例会使用你的设置作为下界(即这个区可能的最小大 小)。 4.3 小结 这一章介绍了 Oracle 内存结构。首先从进程和会话级开始,我们分析了 P GA 和 UGA 以及它们的关系。还了解到连接 Oracle 的模式可以指示内存组织的 方式。相对于共享服务器连接来说,专用服务器连接表示服务器进程中会使用 更多的内存,但是使用共享服务器连接的话,则说明需要的 SGA 大得多。接下 来,我们讨论了 SGA 本身的主要结构,揭示了共享池和大池之间的区别,并说 明为什么希望有一个大池来“节省”我们的共享池。我们还介绍了 Java 池,以 及在各种情况下如何使用 Java 池。此外还分析了块缓冲区缓存,以及如何将块 缓冲区缓存划分为更小、更“专业”的池。 下面可以转向 Oracle 实例的余下一部分,即构成 Oracle 实例的物理进程。 第 5 章 Oracle 进程 5.1 服务器进程 现在要谈到 Oracle 体系结构的最后一部分了。我们已经研究了数据库以及构成 数据库的物理文件集。讨论Oracle 使用的内存时,我们分析了实例的前半部分。 Oracle 体系结构中还有一个问题没有讲到,这就是构成实例另一半的进程(pro cess)集。 Oracle 中的各个进程要完成某个特定的任务或一组任务,每个进程都会分配 内部内存(PGA 内存)来完成它的任务。Oracle 实例主要有 3 类进程: ‰ 服务器进程(server process):这些进程根据客户的请求来完成工作。我们已经 对专用服务器和共享服务器有了一定的了解。它们就是服务器进程。 ‰ 后台进程(background process):这些进程随数据库而启动,用于完成各种维护 任务,如将块写至磁盘、维护在线重做日志、清理异常中止的进程等。 ‰ 从属进程(slave process):这些进程类似于后台进程,不过它们要代表后台进 程或服务器进程完成一些额外的工作。 其中一些进程(如数据库块写入器(DBWn)和日志写入器(LGWR))在前面 已经提到过,不过现在我们要更详细地介绍这些进程的功能,说明这些进程会做 什么,并解释为什么。 注意 这一章谈到“进程”时,实际上要把它理解为两层含义,在某些操作系统 (如 Windows)上,Oracle 使用线程实现,所以在这种操作系统上,就要把 我们所说的“进程”理解为“线程”的同义词。在这一章中,“进程”一词 既表示进程,也涵盖线程。如果你使用的是一个多进程的 Oracle 实现,比如 说 UNIX 上的 Oracle 实现,“进程”就很贴切。如果你使用的是单进程的 O racle 实现,如 Windows 上的 Oracle 实现,“进程”实际是指“Oracle 进程 中的线程”。所以,举例来说,当我谈到 DBWn 进程时,在 Windows 上就 对应为 Oracle 进程中的 DBWn 线程 。 5.1 服务器进程 服务器进程就是代表客户会话完成工作的进程。应用向数据库发送的 SQL 语 句最后就要由这些进程接收并执行。 在第 2 章中,我们简要介绍了两种 Oracle 连接,包括: ‰ 专用服务器(dedicated server)连接,采用专用服务器连接时,会在服务器上得 到针对这个连接的一个专用进程。数据库连接与服务器上的一个进程或线程之间 存在一对一的映射。 ‰ 共享服务器(shared server)连接,采用共享服务器连接时,多个会话可以共享 一个服务器进程池,其中的进程由 Oracle 实例生成和管理。你所连接的是一个数据 库调度器(dispatcher),而不是特意为连接创建的一个专用服务器进程。 注意 有一点很重要,要知道 Oracle 术语中连接和会话之间的区别。连接(conn ection)就是客户进程与 Oracle 实例之间的一条物理路径(例如,客户与实 例之间的一个网络连接)。会话(session)则不同,这是数据库中的一个逻 辑实体,客户进程可以在会话上执行 SQL 等。多个独立的会话可以与一个 连接相关联,这些会话甚至可以独立于连接存在。稍后将进一步讨论这个问 题。 专用服务器进程和共享服务器进程的任务是一样的:要处理你提交的所有 S QL。当你向数据库提交一个SELECT * FROM EMP 查询时,会有一个Oracle 专用/ 共享服务器进程解析这个查询,并把它放在共享池中(或者最好能发现这个查询 已经在共享池中)。这个进程要提出查询计划,如果必要,还要执行这个查询计 划,可能在缓冲区缓存中找到必要的数据,或者将数据从磁盘读入缓冲区缓存中。 这些服务器进程是干重活的进程。在很多情况下,你都会发现这些进程占用 的系统 CPU 时间最多,因为正是这些进程来执行排序、汇总、联结等等工作,几 乎所有工作都是这些进程做的。 5.1.1 专用服务器连接 在专用服务器模式下,客户连接和服务器进程(或者有可能是线程)之间会 有一个一对一的映射。如果一台 UNIX 主机上有 100 条专用服务器连接,就会有 相应的 100 个进程在执行。可以用图来说明,如图 5-1 所示。 客户应用中链接着 Oracle 库,这些库提供了与数据库通信所需的 API。这些 API 知道如何向数据库提交查询,并处理返回的游标。它们知道如何把你的请求 打包为网络调用,专用服务器则知道如何将这些网络调用解开。这部分软件称为 Oracle Net,不过在以前的版本中可能称之为 SQL*Net 或 Net8。这是一个网络 软件/协议,Oracle 利用这个软件来支持客户/服务器处理(即使在一个 n 层体 系结构中也会“潜伏”着客户/服务器程序)。不过,即使从技术上讲没有涉及 Oracle Net,Oracle 也采用了同样的体系结构。也就是说,即使客户和服务器 在同一台机器上,也会采用这种两进程(也称为两任务)体系结构。这个体系结 构有两个好处: 图 5-1 典型的专用服务器连接 ‰ 远程执行(remote execution):客户应用可能在另一台机器上执行(而不是数据 库所在的机器),这是很自然的。 ‰ 地址空间隔离(address space isolation):服务器进程可以读写 SGA。如果客户进 程和服务器进程物理地链接在一起,客户进程中一个错误的指针就能轻松地破坏 SGA 中的数据结构。 我们在第 2 章中了解了这些专用服务器如何“产生”,也就是如何由 Oracl e 监听器进程创建它们。这里不再介绍这个过程;不过,我们会简要地说明如果 不涉及监听器会是什么情况。这种机制与使用监听器的机制基本上是一样的,但 不是由监听器通过 fork()/exec()调用(UNIX)或进程间通信 IPC 调用(Window s)来创建专用服务器,而是由客户进程自己来创建。 注意 有多种 fork()和 exec()调用,如 vfork()、execve()等。Oracle 所用的调用可 能根据操作系统和实现的不同而有所不同,但是最后的结果是一样的。 for k()创建一个新进程,这是父进程的一个克隆,而且在 UNIX 上这也是创建 新进程的惟一途径。exec()在内存中现有的程序映像上加载一个新的程序映 像,这就启动了一个新程序。所以 SQL*Plus 可以先“fork”(即复制自身), 然后“ exec”Oracle 二进制可执行程序,用这个新程序覆盖它自己的副本。 在 UNIX 上,可以在同一台机器上运行客户和服务器,就能很清楚地看出这种父 /子进程的创建: 在此,我使用了一个查询来发现与专用服务器相关联的进程 ID(PID),从 V$PROCESS 得到的 SPID 是执行该查询时所用进程的操作系统 PID。 5.1.2 共享服务器连接 下面更详细地介绍共享服务器进程。共享服务器连接强制要求必须使用 Oracle Net,即使客户和服务器都在同一台机器上也不例外。如果不使用 Oracle TNS 监 听器,就无法使用共享服务器。如前所述,客户应用会连接到 Oracle TNS 监听器, 并重定向或转交给一个调度器。调度器充当客户应用和共享服务器进程之间的“导 管”。图 5-2 显示了与数据库建立共享服务器连接时的体系结构。 图 5-2 典型的共享服务器连接 在此可以看到,客户应用(其中链接了 Oracle 库)会与一个调度器进程物 理连接。对于给定的实例,可以配置多个调度器,但是对应数百个(甚至数千个) 用户只有一个调度器的情况并不鲜见。调度器只负责从客户应用接收入站请求, 并把它们放入 SGA 中的一个请求队列。第一个可用的共享服务器进程(与专用服 务器进程实质上一样)从队列中选择请求,并附加相关会话的 UGA(图 5-2 中标 有“S”的方框)。共享服务器处理这个请求,把得到的输出放在响应队列中。 调 度器一直监视着响应队列来得到结果,并把结果传回给客户应用。就客户而 言,它分不清到底是通过一条专用服务器连接还是通过一条共享服务器连接进行 连接,看上去二者都一样,只是在数据库级二者的区别才会明显。 5.1.3 连接与会话 连接并不是会话的同义词,发现这一点时很多人都很诧异。在大多数人眼里, 它们都是一样的,但事实上并不一定如此。在一条连接上可以建立 0 个、一个或 多个会话。各个会话是单独而且独立的,即使它们共享同一条数据库物理连接也 是如此。一个会话中的提交不会影响该连接上的任何其他会话。实际上,一条连 接上的各个会话可以使用不同的用户身份! 在 Oracle 中,连接只是客户进程和数据库实例之间的一条特殊线路,最常见 的就是网络连接。这条连接可能连接到一个专用服务器进程,也可能连接到调度 器。如前所述,连接上可以有 0 个或多个会话,这说明可以有连接而无相应的会 话。另外,一个会话可以有连接也可以没有连接。使用高级 Oracle Net 特性(如 连接池)时,客户可以删除一条物理连接,而会话依然保留(但是会话会空闲)。 客户在这个会话上执行某个操作时,它会重新建立物理连接。下面更详细地定义 这些术语: ‰ 连接(connection):连接是从客户到 Oracle 实例的一条物理路径。连接可以在 网络上建立,或者通过 IPC 机制建立。通常会在客户进程与一个专用服务器或一个 调度器之间建立连接。不过,如果使用 Oracle 的连接管理器(Connection Manager , CMAN),还可以在客户和 CMAN 之间以及 CMAN 和数据库之间建立连接。CMA N 的介绍超出了本书的范围,不过 Oracle Net Services Administrator’s Guide(可以 从 http://otn.oracle.com 免费得到)对 CMAN 有详细的说明。 ‰ 会话(session):会话是实例中存在的一个逻辑实体。这就是你的会话状态(ses sion state),也就是表示特定会话的一组内存中的数据结构。提到“数据库连接” 时,大多数人首先想到的就是“会话”。你要在服务器中的会话上执行 SQL、提交 事务和运行存储过程。 可以使用 SQL*Plus 来看一看实际的连接和会话是什么样子,从中还可以了 解到,实际上一条连接有多个会话的情况相当常见。这里使用了AUTOTRACE 命令, 并发现有两个会话。我们在一条连接上使用一个进程创建了两个会话。以下是其 中的第一个会话: 这说明现在有一个会话:这是一个与单一专用服务器连接的会话。以上 PAD DR 列是这个专用服务器进程的地址。下面,只需打开 AUTOTRACE 来查看 SQL*Pl us 中所执行语句的统计结果: 这样一来,我们就有了两个会话,但是这两个会话都使用同一个专用服务器 进程,从它们都有同样的PADDR 值就能看出这一点。从操作系统也可以得到确认, 因为没有创建新的进程,对这两个会话只使用了一个进程(一条连接)。需要注 意,其中一个会话(原来的会话)是 ACTIVE(活动的)。这是有道理的:它正 在运行查询来显示这个信息,所以它当然是活动的。但是那个 INACTIVE(不活 动的)会话呢?那个会话要做什么?这就是 AUTOTRACE 会话,它的任务是“监 视”我们的实际会话,并报告它做了什么。 在 SQL*Plus 中启用(打开)AUTOTRACE 时,如果我们执行 DML 操作(INSER T、UPDATE、DELETE、SELECT 和 MERGE),SQL*Plus 会完成以下动作: (1) 如果还不存在辅助会话,它会使用当前连接创建一个新会话。 (2) 要求这个新会话查询 V$SESSTAT 视图来记住实际会话(即运行 DML 的会 话)的初始统计值。这与第 4 章中 watch_stat.sql 脚本完成的功能非常相似。 (3) 在原会话中运行 DML 操作。 (4) DML 语句执行结束后,SQL*Plus 会请求另外那个会话(即“监视”会话) 再次查询 V$SESSTAT,并生成前面所示的报告,显示出原会话(执行DML 的会话) 的统计结果之差。 如果关闭 AUTOTRACE,SQL*Plus 会终止这个额外的会话,在 V$SESSION 中将 无法看到这个会话。你可能会问:“SQL*Plus 为什么要这样做,为什么要另建 一个额外的会话?”答案很简单。我们在第 4 章中曾经使用第二个 SQL*Plus 会 话来监视内存和临时空间的使用情况,原因是:如果使用同一个会话来监视内存 使用,那执行监视本身也要使用内存。SQL*Plus 之所以会另外创建一个会话来 执行监视,原因也是一样的。如果在同一个会话中观察统计结果,就会对统计结 果造成影响(导致对统计结果的修改)。倘若 SQL*Plus 使用一个会话来报告所 执行的 I/O 次数,网络上传输了多少字节,以及执行了多少次排序,那么查看这 些详细信息的查询本身也会影响统计结果。这些查询可能自己也要排序、执行 I /O 以及在网络上传输数据等(一般来说都会如此!)。因此,我们需要使用另 一个会话来正确地测量。 到目前为止,我们已经看到一条连接可以有一个或两个会话。现在,我们想 使用 SQL*Plus 来查看一条没有任何会话的连接。这很容易。在上例所用的同一 个 SQL*Plus 窗口中,只需键入一个“很容易误解”的命令即 DISCONNECT: 从技术上讲,这个命令应该叫 DESTROY_ALL_SESSIONS 更合适,而不是 DISC ONNECT,因为我们并没有真正物理地断开连接。 注意 在 SQL*Plus 中要真正地断开连接,应该执行“exit”命令,因为你必须退 出才能完全撤销连接。 不过,我们已经关闭了所有会话。如果使用另一个用户账户打开另一个会话, 并查询(当然要用你原来的账户名代替 OPS$TKYTE)。 可以看到,这个账户名下没有会话,但是仍有一个进程,相应地有一条物理 连接(使用前面的 ADDR 值): 所以,这就有了一条没有相关会话的“连接”。可以使用 SQL*Plus 的 CONN ECT 命令(这个命令的名字也起得不恰当),在这个现有的进程中创建一个新会 话(CONNECT 命令叫 CREATE_SESSION 更合适): 可以注意到,PADDR 还是一样的,所以我们还是在使用同一条物理连接,但 是(可能)有一个不同的 SID。我说“可能有”,是因为也许还会分配同样的 S ID,这取决于在我们注销时是否有别人登录,以及我们原来的 SID 是否可用。 到此为止,这些测试都是用一条专用服务器连接执行的,所以 PADDR 正是专 用服务器进程的进程地址。如果使用共享服务器会怎么样呢? 注意 要想通过共享服务器连接,你的数据库实例必须先做必要的设置才能启 动。有关如何配置共享服务器,这超出了本书的范围,不过这个主题在 Ora cle Net Services Administrator’s Guide 中有详细说明。 那好,下面使用共享服务器登录,并在这个会话中查询: 这个共享服务器连接与一个进程关联,利用 PADDR 可以联结到 V$PROCESS 来 得出进程名。在这个例子中,可以看到这确实是一个共享服务器,由文本 S000 标识。 不过,如果使用另一个 SQL*Plus 窗口来查询这些信息,而保持我们的共享 服务器会话空闲,就会看到下面的信息: 可以注意到,PADDR 不一样了,与连接关联的进程名也改变了。空闲的共享 服务器连接现在与一个调度器 D000 关联。这样一来,我们又有了一种方法来观 察指向一个进程的多个会话。可以有数百个(甚至数千个)会话指向一个调度器。 共享服务器连接有一个很有意思的性质:我们使用的共享服务器进程可能随 调用而改变。如果只有我一个人在使用这个系统(因为我在执行这些测试),作 为 OPS$TKYTE 反复运行这个查询,会反复生成同样的 PADDR:AE4CF118。不过, 如果打开多条共享服务器连接,并开始在其他会话中使用这个共享服务器,可能 就会注意到使用的共享服务器有变化。 考虑下面的例子。这里要查询我当前的会话信息,显示所用的共享服务器。 然后在另一个共享服务器会话中完成一个运行时间很长的操作(也就是说,我要 独占这个共享服务器)。再次询问数据库我所用的共享服务器时,很有可能看到 一个不同的共享服务器(如果原来的共享服务器已经开始为另一个会话提供服 务)。在下面的例子中,粗体显示的代码表示通过共享服务器连接的第二个 SQL *Plus 会话: 注意第一次是怎么查询的,我使用了 S000 作为共享服务器。然后在另一个 会话中执行了一个长时间运行的语句,它会独占这个共享服务器,而此时所独占 的这个共享服务器恰好是 S000。要执行这个长时间运行的操作,这个工作会交 给第一个空闲的共享服务器来完成,由于此时没有人要求使用S000 共享服务器, 所以 DBMS_LOCK 命令就选择了 S000。现在,再次在第一个 SQL*Plus 会话中查询 时,将分配另一个共享服务器进程,因为 S000 共享服务器正在忙。 有一点很有意思,解析查询(尚未返回任何行)可能由共享服务器 S000 处 理,第一行的获取可能由 S001 处理,第二行的获取可能由 S002 处理,而游标的 关闭可能由 S003 负责。也就是说,一条语句可能由多个共享服务器一点一点地 处理。 总之,从这一节可以了解到,一条连接(从客户到数据库实例的一条物理路 径)上可以建立 0 个、1 个或多个会话。我们看到了这样一个用例,其中使用了 SQL*Plus 的 AUTOTRACE 工具。还有许多其他的工具也利用了这一点。例如,Ora cle Forms 就使用一条连接上的多个会话来实现其调度功能。Oracle 的 n 层代 理认证特性可用于提供从浏览器到数据库的端到端用户鉴别,这个特性也大量使 用了有多个会话的单连接概念,但是每个会话中可能会使用一个不同的用户账 户。我们已经看到,随着时间的推移,会话可能会使用多个进程,特别是在共享 服务器环境中这种情况更常见。另外,如果使用 Oracle Net 的连接池,会话可 能根本不与任何进程关联;连接空闲一段时间后,客户会将其删除,然后再根据 检测活动(是否需要连接)透明地重建连接。 简单地说,连接和会话之间有一种多对多的关系。不过,最常见的是专用服 务器与单一会话之间的一对一关系,这也是大多数人每天所看到的情况。 5.1.4 专用服务器与共享服务器 在继续介绍其他进程之前,下面先来讨论为什么会有两种连接模式,以及各 个模式在哪些情况下更适用。 1. 什么时候使用专用服务器 前面提到过,在专用服务器模式中,客户连接与服务器进程之间存在一种一 对一的映射。对于目前所有基于 SQL 的应用来说,这是应用连接 Oracle 数据库 的最常用的方法。设置专用服务器最简单,而且采用这种方法建立连接也最容易, 基本上不需要什么配置。 因为存在一对一的映射,所以不必担心长时间运行的事务会阻塞其他事务。 其他事务只通过其自己的专用进程来处理。因此,在非 OLTP 环境中,也就是可 能有长时间运行事务的情况下,应该只考虑使用这种模式。专用服务器是 Oracl e 的推荐配置,它能很好地扩缩。只要服务器有足够的硬件(CPU 和 RAM)来应 对系统所需的专用服务器进程个数,专用服务器甚至可以用于数千条并发连接。 某些操作必须在专用服务器模式下执行,如数据库启动和关闭,所以每个数 据库中可能同时有专用服务器和共享服务器,也可能只设置一个专用服务器。 2. 什么时候使用共享服务器 共享服务器的设置和配置尽管并不困难,但是比设置专用服务器要多一步。不 过,二者之间的主要区别还不在于其设置;而是操作的模式有所不同。对于专用 服务器,客户连接和服务器进程之间存在一对一的映射。对于共享服务器,则有 一种多对一的关系:多个客户对应一个共享服务器。 顾名思义,共享服务器是一种共享资源,而专用服务器不是。使用共享资源时, 必须当心,不要太长时间独占这个资源。如前所示,在会话中使用一个简单的 DB MS_LOCK.SLEEP(20)就会独占共享服务器进程 20 秒的时间。如果独占了共享服务 器资源,会导致系统看上去好像挂起了一样。 图 5-2 中有两个共享服务器。如果我有 3 个客户,这些客户都试图同时运行 一个 45 秒左右的进程,那么其中两个会在45 秒内得到响应,第3 个进程则会在 90 秒内才得到响应。这就是共享服务器的首要原则:要确保事务的持续时间尽 量短。事务可以频繁地执行,但必须在短时间内执行完(这正是 OLTP 系统的特 点)。如果事务持续时间很长,看上去整个系统都会慢下来,因为共享资源被少 数进程独占着。在极端情况下,如果所有共享服务器都很忙,除了少数几个独占 着共享服务器的幸运者以外,对其他用户来说,系统就好像挂起了。 使用共享服务器时,你可能还会观察到另一个有意思的现象,这就是人工死 锁(artificial deadlock)。对于共享服务器,多个服务器进程被多个用户所 “共享”,用户数量可能相当大。考虑下面这种情况,你有 5 个共享服务器,并 建立了 100 个用户会话。现在,一个时间点上最多可以有5 个用户会话是活动的。 假设其中一个用户会话更新了某一行,但没有提交。正当这个用户呆坐在那里对 是否修改还有些迟疑时,可能又有另外 5 个用户会话力图锁住这一行。当然,这 5 个会话会被阻塞,只能耐心地等待这一行可用。现在,原来的用户会话(它持 有这一行的锁)试图提交事务,相应地释放行上的锁。这个用户会话发现所有共 享服务器都已经被那 5 个等待的会话所垄断。这就出现了一个人工死锁的情况: 锁的持有者永远也拿不到共享服务器来完成提交,除非某个等待的会话放弃其共 享服务器。但是,除非等待的会话所等待的是一个有超时时间的锁,否则它们绝 对不会放弃其共享服务器(当然,你也可以让管理员通过一个专用服务器“杀死” (撤销)等待的会话来摆脱这种困境)。 因此,由于这些原因,共享服务器只适用于 OLTP 系统,这种系统的特点是 事务短而且频繁。在一个 OLTP 系统中,事务以毫秒为单位执行,任何事务的执 行都会在 1 秒以内的片刻时间内完成。共享服务器对数据仓库很不适用,因为在 数据仓库中,可能会执行耗时 1 分钟、2 分钟、5 分钟甚至更长时间的查询。如 果采用共享服务器模式,其后果则是致命的。如果你的系统中 90%都是 OLTP,只 有 10%“不那么 OLTP”,那么可以在同一个实例上适当地混合使用专用服务器和 共享服务器。采用这种方式,可以大大减少机器上针对 OLTP 用户的服务器进程 个数,并使得“不那么 OLTP”的用户不会独占共享服务器。另外,DBA 可以使用 内置的资源管理器(Resource Manager)进一步控制资源利用率。 当然,使用共享服务器还有一个很重要的原因,这就是有时你别无选择。许多 高级连接特性都要求使用共享服务器。如果你想使用 Oracle Net 连接池,就必须 使用共享服务器。如果你想在数据库之间使用数据库链接集合(database link concentration),也必须对这些连接使用共享服务器。 注意 如果你的应用中已经使用了一个连接池特性(例如,你在使用 J2EE 连接池), 而且适当地确定了连接池的大小,再使用共享服务器只会成为性能“杀手”, 导致性能下降。你已经确定了连接池的大小,以适应任何时间点可能的并发 连接数,所以你希望这些连接都是直接的专用服务器连接。否则,在你应用 的连接池特性中,只是“嵌套”了另一个连接池特性。 3. 共享服务器的潜在好处 前面说明了要当心哪些事务类型能使用共享服务器,那么,如果记住了这些 原则,共享服务器能带来什么好处呢?共享服务器主要为我们做3 件事:减少操 作系统进程/线程数,刻意地限制并发度,以及减少系统所需的内存。在后面几 节中将更详细地讨论这几点。 z 减少操作系统进程/线程数 在一个有数千个用户的系统上,如果操作系统力图管理数千个进程,可能很 快就会疲于奔命。在一个典型的系统中,尽管有数千个用户,但任何时间点上只 有其中很少一部分用户同时是活动的。例如,我最近参与开发了一个有 5 000 个并发用户的系统。在任何时间点上,最多只有 50 个用户是活动的。这个系统 利用 50 个共享服务器进程就能有效地工作,这使得操作系统必须管理的进程数 下降了两个数量级(100 倍)。从很大程度上讲,这样一来,操作系统可以避免 上下文切换。 z 刻意地限制并发度 由于我曾经参加过大量的基准测试,所以在这方面我应该很有发言权。运行 基准测试时,人们经常要求支持尽可能多的用户,直至系统崩溃。这些基准测试 的输出之一往往是一张图表,显示出并发用户数和事务数之间的关系(见图 5- 3)。 图 5-3 并发用户数与每秒事务数 最初,增加并发用户数时,事务数会增加。不过到达某一点时,即使再增加 额外的用户,也不会增加每秒完成的事务数,图中的曲线开始下降。吞吐量有一 个峰值,从这个峰值开始,响应时间开始增加(你每秒完成的事务数还是一样, 但是最终用户观察到响应时间变慢)。随着用户数继续增加,会发现吞吐量实际 上开始下降。这个下降点之前的并发用户数就是系统上允许的最大并发度。从这 一点开始,系统开始出现拥塞,为了完成工作,用户将开始排队。就像收费站出 现的阻塞一样,系统将无法支撑。此时不仅响应时间开始大幅增加,系统的吞吐 量也可能开始下跌,另外上下文切换要占用资源,而且在这么多消费者之间共享 资源就存在着开销,这本身也会占用额外的资源。如果把最大并发度限制为这个 下降点之前的某一点,就可以保持最大吞吐量,并尽可能减少大多数用户的响应 时间。利用共享服务器,我们就能把系统上的最大并发度限制为这个合适的数。 可以把这个过程比作一扇简单的门。门有宽度,人也有宽度,这就限制了最 大吞吐量,即每分钟最多可以有多少人通过这扇门。如果“负载”低,就没有什 么问题;不过,随着越来越多的人到来,就会要求某些人等待(CPU 时间片)。 如果很多人都想通过这扇门,反而会“退步”,假如有太多的人在你身后排队, 吞吐量就开始下降。每个人通过时都会有延迟。使用队列意味着吞吐量增加,有 些人会很快地通过这扇门,就好像没有队列一样,而另外一些人(排在队尾的人) 却要忍受最大的延迟,焦燥地认为“这是一个不好的做法”。但实际上,在测量 每个人(包括最后一个人)通过门的速度时,排队模型(共享服务器)确实比“各 行其是”(free-for-all)的方法要好。如果是各行其是,即使大家都很文明, 也不乏有这样的情形:商店大降价时,开门后,每个人都争先恐后地涌入大门, 结果却是大家都挤不动。 z 减少系统所需的内存 这是使用共享服务器最主要的原因之一:它能减少所需的内存量。共享服务 器确实能减少内存需求,但是没有你想象中的那么显著,特别是假设采用了第 4 章介绍的自动 PGA 内存管理,那么共享服务器在减少内存需求方面更没有太大意 义。自动 PGA 内存管理是指,为进程分配工作区后,使用完毕就释放,而且所分 配工作区的大小会根据并发工作负载而变化。所以,这个理由在较早版本的 Ora cle 中还算合理,但对当前来讲意义不大。另外要记住,使用共享服务器时,UG A 在 SGA 中分配。这说明,转变为共享服务器时,必须能准确地确定需要多少 U GA 内存,并适当地在 SGA 中分配(通过 LARGE_POOL_SIZE 参数)。所以,共享 服务器配置中对 SGA 的需求通常很大。这个内存一般要预分配,从而只能由数据 库实例使用。 注意 对于大小可以调整的 SGA,随着时间的推移确实可以扩大或收缩这个内 存,但是在大多数情况下,它会由数据库实例所“拥有”,其他进程不能使 用。 专用服务器与此正好相反,任何人都可以使用未分配给 SGA 的任何内存。那 么,既然 UGA 在 SGA 中分配而使得 SGA 相当大,又怎么能节省内存呢? 之所以 能节省内存,这是因为共享服务器会分配更少的 PGA。每个专用/共享服务器都 有一个 PGA,即进程信息。PGA 是排序区、散列区以及其他与进程相关的结构。 采用共享服务器,就可以从系统去除这部分内存的需求。如果从使用5 000 个专 用服务器发展到使用 100 个共享服务器,那么通过使用共享服务器,累计起来就 能节省 4 900 个 PGA 的内存(但不包括其 UGA)。 5.1.5 专用/共享服务器 除非你的系统负载过重,或者需要为一个特定的特性使用共享服务器,否则 专用服务器可能最合适。专用服务器设置起来很简单(实际上,根本没有设置!), 而且调优也更容易。 注意 对于共享服务器连接,会话的跟踪信息(SQL_TRACE=TRUE 时的输出) 可能分布在多个独立的跟踪文件中,重建会话可能更困难。 如果用户群很大,而且你知道要部署共享服务器,强烈建议你先开发并测试 这个共享服务器。如果只是在一个专用服务器下开发,而且从来没有对共享服务 器进行测试,出现失败的可能性会更大。要对系统进行压力测试,建立基准测试, 并确保应用使用共享服务器时也能很好地工作。也就是说,要确保它不会独占共 享服务器太长时间。如果在开发时发现它会长时间地独占共享服务器,与等到部 署时才发现相比,修正会容易得多。你可以使用诸如高级排队(Advanced Queu ing,AQ)之类的特性使长时间运行的进程变得很短,但是必须在应用中做相应 的设计。这种工作最好在开发时完成。另外,共享服务器连接和专用服务器连接 可用的特性集之间还有一些历史上的差别。例如,我们已经讨论过 Oracle 9i 中没有自动 PGA 内存管理,但是过去有的一些基本特性(如两个表之间的散列联 结)在共享服务器连接中反倒没有了。 5.2 后台进程 Oracle 实例包括两部分:SGA 和一组后台进程。后台进程执行保证数据库运 行所需的实际维护任务。例如,有一个进程为我们维护块缓冲区缓存,根据需要 将块写出到数据文件。另一个进程负责当在线重做日志文件写满时将它复制到一 个归档目标。另外还有一个进程负责在异常中止进程后完成清理,等等。每个进 程都专注于自己的任务,但是会与所有其他进程协同工作。例如,负责写日志文 件的进程填满一个日志后转向下一个日志时,它会通知负责对填满的日志文件进 行归档的进程,告诉它有活干了。 可以使用一个 V$视图查看所有可能的 Oracle 后台进程,确定你的系统中正 在使用哪些后台进程: 这个视图中 PADDR 不是 00 的行都是系统上配置和运行的进程(线程)。 有两类后台进程:有一个中心(focused)任务的进程(如前所述)以及完成各 种其他任务的进程(即工具进程)。例如,内部作业队列(job queue)有一个工 具后台进程,可以通过 DBMS_JOB 包使用它。这个进程会监视作业队列,并运行其 中的作业。在很多方面,这就像一个专用服务器进程,但是没有客户连接。下面会 分析各种后台进程,先来看有中心任务的进程,然后再介绍工具进程。 5.2.1 中心后台进程 图 5-4 展示了有一个中心(focused)用途的 Oracle 后台进程。 图 5-4 中心后台进程 启动实例时也许不会看到所有这些进程,但是其中一些主要的进程肯定存 在。如果在 ARCHIVELOG 模式下,你可能只会看到 ARCn(归档进程),并启用自 动归档。如果运行了 Oracle RAC,这种 Oracle 配置允许一个集群中不同机器上 的多个实例装载并打开相同的物理数据库,就只会看到 LMD0、LCKn、LMON 和 LM Sn(稍后会更详细地介绍这些进程)。 注意 为简洁起见,图 5-4 中没有画出共享服务器调度器(Dnnn)和共享服务器 (Snnn)进程。 因此,图 5-4 大致展示了启动 Oracle 实例并装载和打开一个数据库时可能 看到哪些进程。例如,在我的 Linux 系统上,启动实例后,有以下进程: 这些进程所用的命名约定很有意思。进程名都以ora_开头。后面是4 个字符, 表示进程的具体名字,再后面是_ora10g。因为我的 ORACLE_SID(站点标识符) 是 ora10g。在 UNIX 上,可以很容易地标识出 Oracle 后台进程,并将其与一个 特定的实例关联(在 Windows 上则没有这么容易,因为在 Windows 上这些后台 进程实际上只是一个更大进程中的线程)。最有意思的是(但从前面的代码可能 不太容易看出来),这些进程实际上都是同一个二进制可执行程序,对于每个“程 序”,并没有一个单独的可执行文件。你可以尽可能地查找一下,但是不论在磁 盘的哪个位置上肯定都找不到一个 arc0 二进制可执行程序,同样也找不到 LGWR 或 DBW0。这些进程实际上都是 oracle(也就是所运行的二进制可执行程序的名 字)。它们只是在启动时对自己建立别名,以便更容易地标识各个进程。这样就 能在 UNIX 平台上高效地共享大量对象代码。Windows 上就没有什么特别的了, 因为它们只是进程中的线程,因此,当然只是一个大的二进制文件。 下面来介绍各个进程完成的功能,先从主要的 Oracle 后台进程开始。 1. PMON:进程监视器(Process Monitor) 这个进程负责在出现异常中止的连接之后完成清理。例如,如果你的专用服 务器“失败”或者出于某种原因被撤销,就要由 PMON 进程负责修正(恢复或撤 销工作),并释放你的资源。PMON 会回滚未提交的工作,并释放为失败进程分 配的 SGA 资源。 除了出现异常连接后完成清理外,PMON还负责监视其他的 Oracle 后台进程, 并在必要时(如果可能的话)重启这些后台进程。如果一个共享服务器或调度器 失败(崩溃),PMON 会介入,并重启另一个共享服务器或调度器(对失败进程 完成清理之后)。PMON 会查看所有 Oracle 进程,可能重启这些进程,也可能适 当地终止实例。例如,如果数据库日志写入器进程(LGWR)失败,就最好让实例 失败。这是一个严重的错误,最安全的做法就是立即终止实例,并根据正常的恢 复来修正数据(注意,这是一种很罕见的情况,要立即报告给Oracle Support)。 PMON 还会为实例做另一件事,这就是向 Oracle TNS 监听器注册这个实例。 实例启动时,PMON 进程会询问公认的端口地址(除非直接指定),来查看是否 启动并运行了一个监听器。Oracle使用的公认/默认端口是 1521。如果此时监听 器在另外某个端口上启动会怎么样呢?在这种情况下,原理是一样的,只不过需 要设置 LOCAL_LISTENER 参数来显式地指定监听器地址。如果数据库实例启动时 有监听器在运行,PMON 会与这个监听器通信,并向它传递相关的参数,如服务 名和实例的负载度量等。如果监听器未启动,PMON 则会定期地试图与之联系来 注册实例。 2. SMON:系统监视器(System Monitor) SMON 进程要完成所有“系统级”任务。PMON 感兴趣的是单个的进程,而 SM ON 与之不同,它以系统级为出发点,这是一种数据库“垃圾收集器”。SMON 所 做的工作包括: ‰ 清理临时空间: 原先清理临时空间这样的杂事都要由我们来完成,随着引入了“真 正” 的临时表空间,这个负担已经减轻,但并不是说完全不需要清理临时空间。例 如,建立一个索引时,创建时为索引分配的区段标记为 TEMPORARY。如果出于某 种原因 CREATE INDEX 会话中止了,SMON 就要负责清理。其他操作创建的临时 区段也要由 SMON 负责清理。 ‰ 合并空闲空间:如果你在使用字典管理的表空间,SMON 要负责取得表空间中相 互连续的空闲区段,并把它们合并为一个更大的空闲区段。只有当字典管理的表空 间有一个默认的存储子句,而且 pctincrease 设置为一个非 0 值时,才会出现空闲空 间的合并。 ‰ 针对原来不可用的文件恢复活动的事务:这类似于数据库启动时 SMON 的作用。 在实例/崩溃恢复时由于某个文件(或某些文件)不可用,可能会跳过一些失败的事 务(即无法恢复),这些失败事务将由 SMON 来恢复。例如,磁盘上的文件可能不 可用或者未装载,当文件确实可用时,SMON 就会由此恢复事务。 ‰ 执行 RAC 中失败节点的实例恢复:在一个 Oracle RAC 配置中,集群中的一个数 据库实例失败时(例如,实例所在的主机失败),集群中的另外某个节点会打开该 失败实例的重做日志文件,并为该失败实例完成所有数据的恢复。 ‰ 清理 OBJ$:OBJ$是一个低级数据字典表,其中几乎对每个对象(表、索引、触 发器、视图等)都包含一个条目。很多情况下,有些条目表示的可能是已经删除的 对象,或者表示“not there”(不在那里)对象(“not there”对象是 Oracle 依赖 机制中使用的一种对象)。要由 SMON 进程来删除这些不再需要的行。 ‰ 收缩回滚段:如果有设置,SMON 会自动将回滚段收缩为所设置的最佳大小。 ‰ “离线”回滚段:DBA 有可能让一个有活动事务的回滚段离线(offline),或置为 不可用。也有可能活动事务会使用离线的回滚段。在这种情况下,回滚段并没有真 正离线;它只是标记为“将要离线”。在后台,SMON 会定期尝试真正将其置为离 线,直至成功为止。 从以上介绍你应该对 SMON 做些什么有所认识了。除此之外,它还会做许多 其他的事情,如将 DBA_TAB_MONITORING 视图中的监视统计信息刷新输出,将 SM ON_SCN_TIME 表中的 SCN-时间戳映射信息刷新输出,等等。随着时间的推移,S MON 进程可能会累积地占用大量 CPU 时间,这应该是正常的。SMON会定期地醒来 (或者被其他后台进程唤醒),来执行这些维护工作。 3. RECO:分布式数据库恢复(Distributed Database Recovery) RECO 有一个很中心的任务:由于两段提交(two-phase commit,2PC)期间 的崩溃或连接丢失等原因,有些事务可能会保持准备状态,这个进程就是要恢复 这些事务。2PC 是一种分布式协议,允许影响多个不同数据库的修改实现原子提 交。它力图在提交之前尽可能地关闭分布式失败窗口。如果在N 个数据库之间采 用 2PC,其中一个数据库(通常是客户最初登录的那个数据库,但也不一定)将 成为协调器(coordinator)。这个站点会询问其他 N−1 个站点是否准备提交。 实际上,这个站点会转向另外这 N−1 个站点,问它们是否准备好提交。这 N−1 个站点都会返回其“准备就绪状态”,报告为 YES 或 NO。如果任何一个站点投 票(报告)NO,整个事务都要回滚。如果所有站点都投票 YES,站点协调器就会 广播一条消息,使这 N−1 个站点真正完成提交(提交得到持久地存储)。 如果某个站点投票 YES,称其准备好要提交,但是在此之后,并且在得到协 调器的指令真正提交之前,网络失败了,或者出现了另外某个错误,事务就会成 为一个可疑的分布式事务(in-doubt distributed transaction)。2PC 力图限 制出现这种情况的时间窗口,但是无法根除这种情况。如果正好在那时(这个时 间窗口内)出现一个失败,处理事务的工作就要由 RECO 负责。RECO 会试图联系 事务的协调器来发现协调的结果。在此之前,事务会保持未提交状态。当再次到 达事务协调器时,RECO 可能会提交事务,也可能将事务回滚。 需要说明,如果失败(outrage)持续很长一段时间,而且你有一些很重要 的事务,可以自行手动地提交或回滚。有时你可能想要这样做,因为可疑的分 布式事务可能导致写入器阻塞读取器(Oracle 中只有此时会发生“写阻塞读” 的情况)。你的 DBA 可以通知另一个数据库的 DBA,要求他查询那些可疑事务 的状态。然后你的 DBA 再提交或回滚,而不再由 RECO 完成这个任务。 4. CKPT:检查点进程(Checkpoint Process) 检查点进程并不像它的名字所暗示的那样真的建立检查点(检查点在第 3 章 介绍重做日志一节中已经讨论过),建立检查点主要是 DBWn 的任务。CKPT 只是 更新数据文件的文件首部,以辅助真正建立检查点的进程(DBWn)。以前 CKPT 是一个可选的进程,但从 8.0 版本开始,这个进程总会启动,所以,如果你在 U NIX 上执行一个 ps 命令,就肯定能看到这个进程。原先,用检查点信息更新数 据文件首部的工作是 LGWR 的任务;不过,一段时间后,随着数据库大小的增加 以及文件个数的增加,对 LGWR 来说,这个额外的工作负担太重了。如果 LGWR 必须更新数十个、数百个甚至数千个文件,等待提交事务的会话就可能必须等待 太长的时间。有了 CKPT,这个任务就不用 LGWR 操心了。 5. DBWn:数据库块写入器(Database Block Writer) 数据库块写入器(DBWn)是负责将脏块写入磁盘的后台进程。DBWn 会写出缓 冲区缓存中的脏块,通常是为了在缓存中腾出更多的空间(释放缓冲区来读入其 他数据),或者是为了推进检查点(将在线重做日志文件中的位置前移,如果出 现失败,Oracle 会从这个位置开始读取来恢复实例)。如第 3 章所述,Oracle 切换日志文件时就会标记(建立)一个检查点。Oracle 需要推进检查点,推进 检查点后,就不再需要它刚填满的在线重做日志文件了。如果需要重用这个重做 日志文件,而此时它还依赖于原来的重做日志文件,我们就会得到一个“检查点 未完成”消息,而必须等待。 注意 推进日志文件只是导致检查点活动的途径之一。有一些增量检查点由诸如 FAST_START_ MTTR_TARGET 之类的参数以及导致脏块刷新输出到磁盘 的其他触发器控制。 可以看到,DBWn 的性能可能很重要。如果它写出块的速度不够快,不能很快 地释放缓冲区(可以重用来缓存其他块),就会看到 Free Buffer Waits 和 Wri te Complete Waits 的等待数和等待时间开始增长。 可以配置多个 DBWn;实际上,可以配置多达20 个 DBWn(DBW0…DBW9、DBWa… DBWj)。大多数系统都只有一个数据库块写入器,但是更大的多 CPU 系统可以利 用多个块写入器。通常为了保持 SGA 中的一个大缓冲区缓存“干净” ,将脏的 (修改过的)块刷新输出到磁盘,就可以利用多个 DBWn 来分布工作负载。 最好的情况下,DBWn 使用异步 I/O 将块写至磁盘。采用异步 I/O,DBWn 会收 集一批要写的块,并把它们交给操作系统。DBWn 并不等待操作系统真正将块写 出;而是立即返回,并收集下一批要写的块。当操作系统完成写操作时,它会异 步地通知 DBWn 写操作已经完成。这样,与所有工作都串行进行相比,DBWn 可以 更快地工作。在后面的“从属进程”一节中,我们还将介绍如何使用 I/O 从属进 程在不支持异步 I/O 的平台或配置上模拟异步 I/O。 关于 DBWn,还有最后一点需要说明。根据定义,块写入器进程会把块写出到 所有磁盘,即分散在各个磁盘上;也就是说,DBWn 会做大量的分散写(scattere d write)。执行一个更新时,你会修改多处存储的索引块,还可能修改随机地分 布在磁盘上的数据块。另一方面,LGWR 则是向重做日志完成大量的顺序写(sequ ential write)。这是一个很重要的区别,Oracle 之所以不仅有一个重做日志和 LGWR 进程,还有 DBWn 进程,其原因就在于此。分散写比顺序写慢多了。通过在 S GA 中缓存脏块,并由 LGWR 进程完成大规模顺序写(可能重建这些脏缓冲区), 这样可以提升性能。DBWn 在后台完成它的任务(很慢),而 LGWR 在用户等待时 完成自己的任务(这个任务比较快),这样我们就能得到更好的整体性能。尽管 从技术上讲这样会使 Oracle 执行更多不必要的 I/O(写日志以及写数据文件), 但整体性能还是会提高。从理论上讲,如果提交期间 Oracle 已经将已修改的块物 理地写出到磁盘,就可以跳过写在线重做日志文件。但在实际中并不是这样:LG WR 还是会把每个事务的重做信息写至在线重做日志,DBWn 则在后台将数据库块刷 新输出到磁盘。 6. LGWR:日志写入器(Log Writer) LGWR 进程负责将 SGA 中重做日志缓冲区的内容刷新输出到磁盘。如果满足以 下某个条件,就会做这个工作: ‰ 每 3 秒会刷新输出一次 ‰ 任何事务发出一个提交时 ‰ 重做日志缓冲区 1/3 满,或者已经包含 1 MB 的缓冲数据 由于这些原因,分配超大的(数百 MB)重做日志缓冲区并不实际,Oracle 根本不可能完全使用这个缓冲区。日志会通过顺序写来写至磁盘,而不像 DBWn 那样必须执行分散 I/O。与向文件的各个部分执行多个分散写相比,像这样大批 的写会高效得多。这也是使用 LGWR 和重做日志的主要原因。通过使用顺序 I/O, 只写出有变化的字节,这会提高效率;尽管可能带来额外的 I/O,但相对来讲所 提高的效率更为突出。提交时,Oracle 可以直接将数据库块写至磁盘,但是这 需要对已满的块执行大量分散 I/O,而让 LGWR 顺序地写出所做的修改要比这快 得多。 7. ARCn:归档进程(Archive Process) ARCn 进程的任务是:当 LGWR 将在线重做日志文件填满时,就将其复制到另 一个位置。此后这些归档的重做日志文件可以用于完成介质恢复。在线重做日 志用于在出现电源故障(实例终止)时“修正”数据文件,而归档重做日志则 不同,它是在出现硬盘故障时用于“修正”数据文件。如果丢失了包含数据文 件/d01/oradata/ora10g/system.dbf 的磁盘,可以去找上一周的备份,恢复旧 的文件副本,并要求在数据库上应用自这次备份之后生成的所有归档和在线重 做日志。这样就能使这个数据文件“赶上”数据库中的其他数据文件,所以我 们可以继续处理而不会丢失数据。 ARCn 通常将在线重做日志文件复制到至少两个位置(冗余正是不丢失数据的 关键所在!)。这些位置可能是本地机器上的磁盘,或者更确切地讲,至少有一 个位置在另一台机器上,以应付灾难性的失败。在许多情况下,归档重做日志文 件会由另外某个进程复制到一个第三辅存设备上,如磁带。也可以将这些归档重 做日志文件发送到另一台机器上,应用于“备用数据库”(standby database), 这是 Oracle 提供的一个故障转移选项。稍后将讨论其中涉及的进程。 8. 其他中心进程 取决于所用的 Oracle 特性,可能还会看到其他一些中心进程。这里只是简 单地列出这些进程,并提供其功能的简要描述。前面介绍的进程都是必不可少的, 如果运行一个 Oracle 实例,就肯定会有这些进程。以下进程则是可选的,只有 在利用了某个特定的特性时才会出现。下面的进程是使用ASM 的数据库实例所特 有的,见第 3 章的讨论: ‰ 自动存储管理后台(Automatic Storage Management Background,ASMB)进程: ASMB 进程在使用了 ASM 的数据库实例中运行。它负责与管理存储的 ASM 实例通 信、向 ASM 实例提供更新的统计信息,并向 ASM 实例提供一个“心跳”,让 AS M 实例知道它还活着,而且仍在运行。 ‰ 重新平衡(Rebalance,RBAL)进程:RBAL 进程也在使用了 ASM 的数据库实例 中运行。向 ASM 磁盘组增加或去除磁盘时,RBAL 进程负责处理重新平衡请求(即 重新分布负载的请求)。 以下进程出现在 Oracle RAC 实例中。RAC 是一种 Oracle 配置,即集群中的 多个实例可以装载和打开一个数据库,其中每个实例在一个单独的节点上运行 (通常节点是一个单独的物理计算机)。这样,你就能有多个实例访问(以一种 全读写方式)同样的一组数据库文件。RAC 的主要目标有两个: ‰ 高度可用性:利用 Oracle RAC,如果集群中的一个节点/计算机由于软件、硬件 或人为错误而失败,其他节点可以继续工作,还可以通过其他节点访问数据库。你 也许会丧失一些计算能力,但是不会因此而无法访问数据库。 ‰ 可扩缩性:无需购买更大的机器来处理越来越大的工作负载(这称为垂直扩缩), RAC 允许以另一种方式增加资源,即在集群中增加更多的机器(称为水平扩缩)。 举例来说,不必把你的 4 CPU 机器扩缩为有 8 个或 16 个 CPU,通过利用 RAC, 你可以选择增加另外一个相对廉价的 4 CPU 机器(或多台这样的机器)。 以下进程是 RAC 环境所特有的,如果不是 RAC 环境,则看不到这些进程。 ‰ 锁监视器(Lock monitor,LMON)进程:LMON 监视集群中的所有实例,检测 是否有实例失败。这有利于恢复失败实例持有的全局锁。它还负责在实例离开或加 入集群时重新配置锁和其他资源(实例失败时会离开集群,恢复为在线时又会加入 集群,或者可能有新实例实时地增加到集群中)。 ‰ 锁管理器守护(Lock manager daemon,LMD)进程:LMD 进程为全局缓存服务 (保持块缓冲区在实例间一致)处理锁管理器服务请求。它主要作为代理(broker) 向一个队列发出资源请求,这个队列由 LMSn 进程处理。LMD 会处理全局死锁的检 测/解析,并监视全局环境中的锁超时。 ‰ 锁管理器服务器(Lock manager server,LMSn)进程:前面已经提到,在一个 R AC 环境中,各个 Oracle 实例在集群中的不同机器上运行,它们都以一种读写方式 访问同样的一组数据库文件。为了达到这个目的,SGA 块缓冲区缓存相互之间必须 保持一致。这也是 LMSn 进程的主要目标之一。在以前版本的 Oracle 并行服务器(O racle Parallel Server,OPS)中,这是通过 ping 实现的。也就是说,如果集群中的 一个节点需要块的一个读一致视图,而这个块以一种独占模式被另一个节点锁定, 数据的交换就要通过磁盘刷新输出来完成(块被 ping)。如果本来只是要读取数据, 这个操作(ping)的代价就太昂贵了。现在则不同,利用 LMSn,可以在集群的高速 连接上通过非常快速的缓存到缓存交换来完成数据交换。每个实例可以有多达 10 个 LMSn 进程。 ‰ 锁(Lock,LCK0)进程:这个进程的功能与前面所述的 LMD 进程非常相似,但 是它处理所有全局资源的请求,而不只是数据库块缓冲区的请求。 ‰ 可诊断性守护(Diagnosability daemon,DIAG)进程:DIAG 只能用于 RAC 环境 中。它负责监视实例的总体“健康情况”,并捕获处理实例失败时所需的信息。 5.2.2 工具后台进程 这些后台进程全都是可选的,可以根据你的需要来选用。它们提供了一些工 具,不过这些工具并不是每天运行数据库所必需的,除非你自己要使用(如作业 队列),或者你要利用使用了这些工具的特性(如新增的Oracle 10g 诊断功能)。 在 UNIX 中,这些进程可以像其他后台进程一样可见,如果你执行 ps 命令, 就能看到这些进程。在介绍中心后台进程那一节的开始,我列出了 ps 命令的执 行结果(这里列出其中一部分),可以看到,我有以下进程: ‰ 配置了作业队列。CJQ0 进程是作业队列协调器(job queue coordinator)。 ‰ 配置了 Oracle AQ,从 Q000(AQ 队列进程,AQ queue process)和 QMNC(AQ 监视器进程,AQ monitor process)可以看出。 ‰ 启用了自动设置 SGA 大小,由内存管理器(memory manager ,MMAN)进程 可以看出。 ‰ 启用了 Oracle 10g 可管理性/诊断特性,由可管理性监视器(manageability monit or,MMON)和可管理性监视器灯(manageability monitor light,MMNL)进程可 以看出。 下面来看看根据所用的特性可能会看到哪些进程。 1. CJQ0 和 Jnnn 进程:作业队列 在第一个 7.0 版本中,Oracle 通过一种称为快照(snapshot)的数据库对 象来提供复制特性。作业队列就是刷新快照(或将快照置为当前快照)时使用的 内部机制。 作业队列进程监视一个作业表,这个作业表告诉它何时需要刷新系统中的 各个快照。在 Oracle 7.1 中,Oracle 公司通过一个名为 DBMS_JOB 的数据库 包来提供这个功能。所以,原先 7.0 中与快照相关的进程到了 7.1 及以后版 本中变成了“作业队列”。后来,控制作业队列行为的参数(检查的频度, 以及应该有多少个队列进程)的名字也发生了变化,从 SNAPSHOT_REFRESH_I NTERVAL 和 SNAPSHOT_REFRESH_PROCESSES 变成了 JOB_QUEUE_INTERVAL 和 JOB _QUEUE_PROCESSES。在当前的版本中,只有 JOB_QUEUE_PROCESSES 参数的设 置是用户可调的。 最多可以有 1 000 个作业队列进程。名字分别是 J000,J001,…,J999。这 些进程在复制中大量使用,并作为物化视图刷新进程的一部分。基于流的复制(O racle9i Release 2 中新增的特性)使用 AQ 来完成复制,因此不使用作业队列 进程。开发人员还经常使用作业队列来调度一次性(后台)作业或反复出现的作 业,例如,在后台发送一封电子邮件,或者在后台完成一个长时间运行的批处理。 通过在后台做这些工作,就能达到这样一种效果:尽管一个任务耗时很长,但在 性急的最终用户看来所花费的时间并不多(他会认为任务运行得快多了,但事实 上可能并非如此)。这与 Oracle 用 LGWR 和 DBWn 进程所做的工作类似,他们在 后台做大量工作,所以你不必实时地等待它们完成所有任务。 Jnnn 进程与共享服务器很相似,但是也有专用服务器中的某些方面。它们处 理完一个作业之后再处理下一个作业,从这个意义上讲是共享的,但是它们管理 内存的方式更像是一个专用服务器(其 UGA 内存在 PGA 中,而不是在 SGA 中)。 每个作业队列进程一次只运行一个作业,一个接一个地运行,直至完成。正因为 如此,如果我们想同时运行多个作业,就需要多个进程。这里不存在多线程或作 业的抢占。一旦运行一个作业,就会一直运行到完成(或失败)。 你会注意到,经过一段时间,Jnnn 进程会不断地来来去去,也就是说,如果 配置了最多 1 000 个 Jnnn 进程,并不会看到真的有1 000 个进程随数据库启动。 相反,开始时只会启动一个进程,即作业队列协调器(CJQ0),它在作业队列表 中看到需要运行的作业时,会启动 Jnnn 进程。如果 Jnnn 进程完成其工作,并发 现没有要处理的新作业,此时 Jnnn 进程就会退出,也就是说,会消失。因此, 如果将大多数作业都调度为在凌晨 2:00 运行(没有人在场),你可能永远也看 不到这些 Jnnn 进程。 2. QMNC 和 Qnnn:高级队列 QMNC 进程对于 AQ 表来说就相当于 CJQ0 进程之于作业表。QMNC 进程会监视 高级队列,并警告从队列中删除等待消息的“出队进程”(dequeuer):已经有 一个消息变为可用。QMNC 和 Qnnn 还要负责队列传播(propagation),也就是 说,能够将在一个数据库中入队(增加)的消息移到另一个数据库的队列中,从 而实现出队(dequeueing)。 Qnnn 进程对于 QMNC 进程就相当于 Jnnn 进程与 CJQ0 进程的关系。QMNC 进程 要通知 Qnnn 进程需要完成什么工作,Qnnn 进程则会处理这些工作。 QMNC 和 Qnnn 进程是可选的后台进程。参数 AQ_TM_PROCESSES 可以指定最多 创建 10 个这样的进程(分别名为 Q000,…,Q009),以及一个 QMNC 进程。如 果 AQ_TM_PROCESSES 设置为 0,就没有 QMNC 或 Qnnn 进程。不同于作业队列所用 的 Jnnn 进程,Qnnn 进程是持久的。如果将 AQ_TM_PROCESSES 设置为 10,数据 库启动时可以看到 10 个 Qnnn 进程和一个 QMNC 进程,而且在实例的整个生存期 中这些进程都存在。 3. EMNn:事件监视器进程(Event Monitor Process) EMNn 进程是 AQ 体系结构的一部分,用于通知对某些消息感兴趣的队列订购 者。通知会异步地完成。可以用一些 Oracle 调用接口(Oracle Call Interfac e,OCI)函数来注册消息通知的回调。回调是 OCI 程序中的一个函数,只要队列 中有了订购者感兴趣的消息,就会自动地调用这个函数。EMNn 后台进程用于通 知订购者,第一次向实例发出通知时会自动启动 EMNn 进程。然后应用可以发出 一个显式的 message_receive(dequeue)来获取消息。 4. MMAN:内存管理器(Memory Manager) 这个进程是 Oracle 10g 中新增的,自动设置SGA 大小特性会使用这个进程。 MMAN 进程用于协调共享内存中各组件(默认缓冲区池、共享池、Java 池和大池) 的大小设置和大小调整。 5. MMON、MMNL 和 Mnnn:可管理性监视器(Manageability Monitor) 这些进程用于填充自动工作负载存储库(Automatic Workload Repository, AWR),这是 Oracle 10g 中新增的一个特性。MMNL 进程会根据调度从 SGA 将统 计结果刷新输出至数据库表。MMON 进程用于“自动检测”数据库性能问题,并 实现新增的自调整特性。Mnnn 进程类似于作业队列的 Jnnn 或 Qnnn 进程;MMON 进程会请求这些从属进程代表它完成工作。Mnnn 进程本质上是临时性的,它们 将根据需要来来去去。 6. CTWR:修改跟踪进程(Change Tracking Process) 这是 Oracle 10g 数据库中新增的一个可选进程。CTWR 进程负责维护新的修 改跟踪文件,有关内容见第 3 章的介绍。 7. RVWR:恢复写入器(Recovery Writer) 这个进程也是 Oracle 10g 数据库中新增的一个可选进程,负责维护闪回恢 复区中块的“前”映像(见第 3 章的介绍),要与 FLASHBACK DATABASE 命令一 起使用。 8. 其他工具后台进程 这就是完整的列表吗?不,还有另外一些工具进程没有列出。例如,Oracle Data Guard 有一组与之相关的进程,有利于将重做信息从一个数据库移送到另 一个数据库,并应用这些重做信息(详细内容请见 Oracle 的 Data Guard Conce pts and Administration Guide)。还有一些进程与 Oracle 10g 新增的数据泵 工具有关,在某些数据泵操作中会看到这些进程。另外还有一些流申请和捕获进 程。不过,以上所列已经基本涵盖了你可能遇到的大多数常用的后台进程。 5.3 从属进程 下面来看最后一类Oracle进程:从属进程(slave process)。Oracle中有 两类从属进程:I/O从属进程和并行查询从属进程。 5.3.1I/O 从属进程 I/O 从属进程用于为不支持异步 I/O 的系统或设备模拟异步 I/O。例如,磁 带设备(相当慢)就不支持异步 I/O。通过使用 I/O 从属进程,可以让磁带机模 仿通常只为磁盘驱动器提供的功能。就好像支持真正的异步I/O 一样,写设备的 进程(调用者)会收集大量数据,并交由写入器写出。数据成功地写出时,写入 器(此时写入器是 I/O 从属进程,而不是操作系统)会通知原来的调用者,调用 者则会从要写的数据列表中删除这批数据。采用这种方式,可以得到更高的吞吐 量,这是因为会由I/O 从属进程来等待慢速的设备,而原来的调用进程得以脱身, 可以做其他重要的工作来收集下一次要写的数据。 I/O 从属进程在 Oracle 中有两个用途。DBWn 和 LGWR 可以利用 I/O 从属进程 来模拟异步 I/O,另外 RMAN 写磁带时也可能利用 I/O 从属进程。 有两个参数控制着 I/O 从属进程的使用: ‰ BACKUP_TAPE_IO_SLAVES:这个参数指定 RMAN 是否使用 I/O 从属进程将 数据备份、复制或恢复到磁带上。由于这个参数是围绕着磁带设备设计的,而且 磁带设备一次只能由一个进程访问,所以这个参数是一个布尔值,而不是所用从 属进程的个数(这可能出乎你的意料)。RMAN 会为所用的物理设备启动多个必 要的从属进程。BACKUP_TAPE_IO_SLAVES = TRUE 时,则使用一个 I/O 从属 进程从磁带设备读写。如果这个参数为 FALSE(默认值),就不会使用 I/O 从属 进程完成备份。相反,完成备份的专用服务器进程会直接访问磁带设备。 ‰ DBWR_IO_SLAVES:这个参数指定了 DBW0 进程所用 I/O 从属进程的个数。D BW0 进程及其从属进程总是将缓冲区缓存中的脏块写至磁盘。这个值默认为 0,表 示不使用 I/O 从属进程。注意,如果将这个参数设置为一个非 0 的值,LGWR 和 A RCH 也会使用其自己的 I/O 从属进程,LGWR 和 ARCH 最多允许 4 个 I/O 从属进程。 DBWR I/O 从属进程的名字是 I1nn,LGWR I/O 从属进程的名字是 I2nn,这里 nn 是一个数。 5.3.2 并行查询从属进程 Oracle 7.1.6 引入了并行查询功能。这个功能是指:对于 SELECT、CREATE TABLE、CREATE INDEX、UPDATE 等 SQL 语句,创建一个执行计划,其中包含可以 同时完成的多个(子)执行计划。将每个执行计划的输出合并在一起构成一个更 大的结果。其目标是仅用少量的时间来完成操作,这只是串行完成同一操作所需 时间的一小部分。例如,假设有一个相当大的表,分布在 10 个不同的文件上。 你配置有 16 个 CPU,并且需要在这个表上执行一个即席查询。另一种方法是: 可以将这个查询计划分解为 32 个小部分,并充分地利用机器;而不是只使用一 个进程串行地读取和处理所有数据。相比之下,前一种做法要好得多。 使用并行查询时,会看到名为 Pnnn 的进程,这些就是并行查询从属进程。 处理一条并行语句时,服务器进程则称为并行查询协调器(parallel query co ordinator)。操作系统上服务器进程的名字并不会改变,但是阅读有关并行查 询的文档时,如果提到了协调器进程,你应该知道这就是原来的服务器进程。 5.4 小结 我们已经介绍了 Oracle 使用的文件,涵盖了从低级但很重要的参数文件到 数据文件、重做日志文件等等。此外深入分析了 Oracle 使用的内存结构,包括 服务器进程中的内存(PGA)和 SGA,还了解了不同的服务器配置(如共享服务 器模式和专用服务器模式的连接)对于系统如何使用内存有着怎样显著的影响。 最后,我们介绍了进程(或线程,这取决于操作系统),Oracle 正是通过这些 进程来完成功能。下面可以具体看看 Oracle 另外一些特性的实现,如锁定、并 发控制和事务。 第 6 章锁 开发多用户、数据库驱动的应用时,最大的难点之一是:一方面要力争取得 最大限度的并发访问,与此同时还要确保每个用户能以一致的方式读取和修改数 据。为此就有了锁定(locking)机制,这也是所有数据库都具有的一个关键特 性,Oracle 在这方面更是技高一筹。不过,Oracle 的这些特性的实现是 Oracle 所特有的,就像SQL Server 的实现只是 SQL Server 特有的一样,应用执行数据 处理时,要正确地使用这些机制,而这一点要由你(应用的开发人员)来保证。 如果你做不到,你的应用可能会表现得出人意料,而且不可避免地会危及数据的 完整性(见第 1 章的说明)。 在这一章中,我们将详细介绍 Oracle 如何对数据(例如,表中的行)和共 享数据结构(如 SGA 中的内存结构)锁定。这里会分析 Oracle 以怎样的粒度锁 定数据,并指出这对你来说意味着什么。在适当的时候,我会把 Oracle 的锁定 机制与其他流行的锁实现(即其他数据库中的锁定机制)进行对照比较,主要是 为了消除关于行级锁的一个“神话”:人们认为行级锁总会增加开销;而实际上, 在不同的实现中情况有所不同,只有当实现本身会增加开销时,行级锁才会增加 开销。在下一章中,我们还会继续讨论这个内容,进一步研究 Oracle 的多版本 技术,并说明锁定策略与多版本技术有什么关系。 6.1 什么是锁? 锁(lock)机制用于管理对共享资源的并发访问。注意,我说的是“共享资 源”而不是“数据库行”。Oracle 会在行级对表数据锁定,这固然不错,不过 O racle 也会在其他多个级别上使用锁,从而对多种不同的资源提供并发访问。例 如,执行一个存储过程时,过程本身会以某种模式锁定,以允许其他用户执行这 个过程,但是不允许另外的用户以任何方式修改这个过程。数据库中使用锁是为 了支持对共享资源进行并发访问,与此同时还能提供数据完整性和一致性。 在单用户数据库中,并不需要锁。根据定义,只有一个用户修改信息。不过, 如果有多个用户访问和修改数据或数据结构,就要有一种机制来防止对同一份信 息的并发修改,这一点至关重要。这正是锁定所要做的全部工作。 需要了解的重要一点是:有多少种数据库,其中就可能有多少种实现锁定的方 法。你可能对某个特定的关系数据库管理系统(relational database manageme nt system,RDBMS)的锁定模型有一定的经验,但凭此并不意味着你通晓锁定的 一切。例如,在我“投身”Oracle 之前,曾经使用过许多其他的数据库,如 Syb ase、Microsoft SQL Server 和 Informix。这 3 个数据库都为并发控制提供了锁 定机制,但是每个数据库中实现锁定的方式都大相径庭。为了说明这一点,下面 简要地概括一下我的“行进路线”,告诉你我是怎样从 SQL Server 开发人员发展 为 Informix 用户,最后又怎样成为 Oracle 开发人员。那是好多年前的事情了, 可能有些 SQL Server 支持者会说:“但是我们现在也有行级锁了!”没错, SQ L Server 现在确实可以使用行级锁,但是其实现方式与 Oracle 中的实现方式完 全不同。它们就像是苹果和桔子,是截然不同的两个物体,这正是关键所在。 作为 SQL Server 程序员,我很少考虑多个用户并发地向表中插入数据的可 能性。在 SQL Server 数据库中,这种情况极少发生。那时,SQL Server 只提供 页级锁,对于非聚簇表,由于所有数据都会插入到表的最后一页,所以两个用户 并发插入的情况根本不可能发生。 注意 从某种程度上讲, SQL Server 聚簇表(有一个聚簇索引的表)与 Oracle 聚簇有点相似,但二者存在很大的差别。 SQL Server 以前只支持页(块)级 锁,如果所插入的每一行都会插入到表的“末尾”,那么这个数据库中绝对 不会有并发插入和并发事务。利用 SQL Server 中的聚簇索引,就能按聚簇 键的顺序在整个表中插入行(而不是只在表的末尾插入),这就能改善 SQ L Server 数据库的并发性。 并发更新也存在同样的问题(因为 UPDATE 实际上就是 DELETE 再加上一个 I NSERT)。可能正是由于这个原因,默认情况下,SQL Server 每执行一条语句后 就会立即提交或回滚。这样做的目的是为了得到更大的并发性,但是会破坏事务 完整性。 因此,大多数情况下,如果采用页级锁,多个用户就不能同时修改同一个表。 另外,如果正在修改一个表,这会有效地阻塞对这个表的多个查询。如果我想查 询一个表,而且所需的页被一个更新锁住了,那我就必须等待(等待,再等 待……)。这种锁定机制太糟糕了,要想支持耗时超过 1 秒的事务,结果可能是 致命的;倘若真的这样做了,整个数据库看上去可能就像是“冻住”了一样。我 从这里学到了很多坏习惯。我认识到:事务很“不好”,应该尽快地提交,而且 永远不要持有数据的锁。并发性要以一致性为代价。要么保证正确,要么保证速 度,在我看来,鱼和熊掌不可兼得。 等我转而使用 Informix 之后,情况好了一些,但也不是太好。只要创建表 时记得启用行级锁,就能允许两个人同时向这个表中插入数据。遗憾的是,这种 并发性的代价很高。Informix 实现中的行级锁开销很大,不论从时间上讲还是 从内存上讲都是如此。它要花时间获得和“不要”(或释放)这些行级锁,而且 每个锁都要占用实际内存。另外,在启动数据库之前,必须计算系统可用的锁的 总数。如果超过这个数,那你可就要倒霉了。由于这些原因,大多数表都采用页 级锁创建,而且与 SQL Server 一样,Informix 中的行级锁和页级锁都会阻塞查 询。所以,我再一次发现需要尽快地提交。在 SQL Server 中学到的坏习惯在此 得到了巩固,而且,我还学会了一条:要把锁当成一种很稀有的资源,一种可望 而难求的事物。我了解到,应该手动地将行级锁升级为表级锁,从而尽量避免需 要太多的锁而导致系统崩溃,我就曾经因此多次使系统崩溃。 等开始使用 Oracle 时,我没有费心去读手册,看看这个特定的数据库中锁 定是怎么工作的。毕竟,我用数据库已经不是一年半载了,而且也算得上是这个 领域的专家(除了 Sybase、SQL Server 和 Informix,我还用过 Ingres、DB2、G upta SQLBase 和许多其他的数据库)。我落入了过于自信的陷阱,自以为知道 事情应该怎么做,所以想当然地认为事情肯定就会这样做。这一次我可大错特错 了。 直到一次基准测试时,我才认识到犯了多大的错误。在这些数据库的早期阶 段(大约1992/1993 年),开发商常常对确实很大的数据库产品进行“基准测试”, 想看看哪一个数据库能最快、最容易地完成工作,而且能提供最丰富的特性。 这个基准测试在 Informix、Sybase、SQL Server 和 Oracle 之间进行。最先 测试的是 Oracle。他们的技术人员来到现场,读过基准测试规范后,他们开始 进行设置。我首先注意到的是, Oracle 技术人员只使用一个数据库表来记录他 们的计时信息,尽管我们要建立数十条连接来执行测试,而且每条连接都需要频 繁地向这个日志表中插入和更新数据。不仅如此,他们还打算在基准测试期间读 这个日志表!出于好心,我把一位 Oracle 技术人员叫到一边,问他这样做是不 是疯了,为什么还要故意往系统里引入竞争呢?基准测试进程难道不是对这个表 串行地执行操作吗?别人正在对表做大量的修改,而此时他们还要读这个表,基 准测试不会因此阻塞吗?为什么他们想引入所有这些额外的锁,要知道这些锁都 需要他们来管理呀!我有一大堆“为什么你会那么想?”之类的问题。那时,我 认为 Oracle 的技术人员有点傻。也就是说,直到我摆脱 SQL Server 或 Informi x 的阴影,显示了让两个人同时插入一个表会有什么结果时;或者有人试图查询 一个表,而其他人正在向这个表中插入行,此时会有什么结果(查询将每秒返回 0 行),我的观念才有所转变。Oracle 的做法与几乎所有其他数据库的做法有显 著的差别,简直是天壤之别。 不用说,无论是 Informix 还是 SQL Server 技术人员,都对这种数据库日志 表方法不太热心。他们更倾向于把计时信息记录到操作系统上的平面文件中。O racle 人员对于如何胜出 SQL Server 和 Informix 很有自己的一套:他们只是问 测试人员:“如果数据已经锁定,你当前的数据库每秒返回多少行?”并以此为 出发点“展开攻势”。 从这个故事得到的教训是两方面的。首先,所有数据库本质上都不同。其次, 为一个新的数据库平台设计应用时,对于数据库如何工作不能做任何假设。学习 每一个新数据库时,应该假设自己从未使用过数据库。在一个数据库中能做的事 情在另一个数据库中可能没有必要做,或者根本不能做。 在 Oracle 中,你会了解到: ‰ 事务是每个数据库的核心,它们是“好东西”。 ‰ 应该延迟到适当的时刻才提交。不要太快提交,以避免对系统带来压力。这是因 为,如果事务很长或很大,一般不会对系统有压力。相应的原则是:在必要时才提 交,但是此前不要提交。事务的大小只应该根据业务逻辑来定。 ‰ 只要需要,就应该尽可能长时间地保持对数据所加的锁。这些锁是你能利用的工 具,而不是让你退避三舍的东西。锁不是稀有资源。恰恰相反,只要需要,你就应 该长期地保持数据上的锁。锁可能并不稀少,而且它们可以防止其他会话修改信息。 ‰ 在 Oracle 中,行级锁没有相关的开销,根本没有。不论你是有 1 个行锁,还是 1 000 000 个行锁,专用于锁定这个信息的“资源”数都是一样的。当然,与修改 1 行相比,修改 1 000 000 行要做的工作肯定多得多,但是对 1 000 000 行锁定所 需的资源数与对 1 行锁定所需的资源数完全相同,这是一个固定的常量。 ‰ 不要以为锁升级“对系统更好”(例如,使用表锁而不是行锁)。在 Oracle 中, 锁升级(lock escalate)对系统没有任何好处,不会节省任何资源。也许有时会使用 表锁,如批处理中,此时你很清楚会更新整个表,而且不希望其他会话锁定表中的 行。但是使用表锁绝对不是为了避免分配行锁,想以此来方便系统。 ‰ 可以同时得到并发性和一致性。每次你都能快速而准确地得到数据。数据读取器 不会被数据写入器阻塞。数据写入器也不会被数据读取器阻塞。这是 Oracle 与大多 数其他关系数据库之间的根本区别之一。 接下来在这一章和下一章的介绍中,我还会强调这几点。 6.2 锁定问题 讨论 Oracle 使用的各种类型的锁之前,先了解一些锁定问题会很有好处, 其中很多问题都是因为应用设计不当,没有正确地使用(或者根本没有使用)数 据库锁定机制产生的。 6.2.1 丢失更新 丢失更新(lost update)是一个经典的数据库问题。实际上,所有多用户 计算机环境都存在这个问题。简单地说,出现下面的情况时(按以下所列的顺序), 就会发生丢失更新: (1) 会话 Session1 中的一个事务获取(查询)一行数据,放入本地内存, 并显示给一个最终用户 User1。 (2) 会话 Session2 中的另一个事务也获取这一行,但是将数据显示给另一 个最终用户 User2。 (3) User1 使用应用修改了这一行,让应用更新数据库并提交。会话 Sessi on1 的事务现在已经执行。 (4) User2 也修改这一行,让应用更新数据库并提交。会话 Session2 的事 务现在已经执行。 这个过程称为“丢失更新”,因为第(3)步所做的所有修改都会丢失。例如, 请考虑一个员工更新屏幕,这里允许用户修改地址、工作电话号码等信息。应用 本身非常简单:只有一个很小的搜索屏幕要生成一个员工列表,然后可以搜索各 位员工的详细信息。这应该只是小菜一碟。所以,编写应用程序时没有考虑锁定, 只是简单的 SELECT 和 UPDATE 命令。 然后最终用户(User1)转向详细信息屏幕,在屏幕上修改一个地址,单击 S ave(保存)按钮,得到提示信息称更新成功。还不错,但是等到 User1 第二天 要发出一个税表时,再来检查记录,会发现所列的还是原先的地址。到底出了什 么问题?很遗憾,发生这种情况太容易了。在这种情况下,User1 查询记录后, 紧接着另一位最终用户(User2)也查询了同一条记录;也就是说,在 User1 读 取数据之后,但在她修改数据之前,User2 也读取了这个数据。然后,在 User2 查询数据之后,User1 执行了更新,接到成功信息,甚至还可能再次查询看看是 否已经修改。不过,接下来 User2 更新了工作电话号码字段,并单击 Save(保 存)按钮,完全不知道他已经用旧数据重写(覆盖)了User1 对地址字段的修改! 之所以会造成这种情况,这是因为应用开发人员编写的程序是这样的:更新一个 特定的字段时,该记录的所有字段都会“刷新”(只是因为更新所有列更容易, 这样就不用先得出哪些列已经修改,并且只更新那些修改过的列)。 可以注意到,要想发生这种情况,User1 和 User2 甚至不用同时处理记录。 他们只要在大致同一时间处理这个记录就会造成丢失更新。 我发现,如果 GUI 程序员在数据库方面的培训很少(或者没有),编写数据 库应用程序时就时常会冒出这个数据库问题。这些程序员了解了如何使用 SELEC T、INSERT、UPDATE 和 DELETE 等语句后,就着手开始编写应用程序。如果开发 出来的应用程序有上述表现,就会让用户完全失去对它的信心,特别是这种现象 只是随机地、零星地出现,而且在受控环境中完全不可再生(这就导致开发人员 误以为是用户的错误)。 许多工具可以保护你避免这种情况,如 Oracle Forms 和 HTML DB,这些工具 能确保:从查询记录的那个时刻开始,这个记录没有改变,而且对它执行任何修 改时都会将其锁定,但是其他程序做不到这一点(如手写的 Visual Basic 或 Ja va 程序)。为了保护你不丢失更新,这些工具在后台做了哪些工作呢?或者说 开发人员必须自己做哪些工作呢?实际上就是要使用某种锁定策略,共有两种锁 定策略:悲观锁定或乐观锁定。 6.2.2 悲观锁定 用户在屏幕上修改值之前,这个锁定方法就要起作用。例如,用户一旦有意 对他选择的某个特定行(屏幕上可见)执行更新,如单击屏幕上的一个按钮,就 会放上一个行锁。 悲观锁定(pessimistic locking)仅用于有状态(stateful)或有连接(c onnected)环境,也就是说,你的应用与数据库有一条连续的连接,而且至少在 事务生存期中只有你一个人使用这条连接。这是 20 世纪 90 年代中期客户/服务 器应用中的一种流行做法。每个应用都得到数据库的一条直接连接,这条连接只 能由该应用实例使用。这种采用有状态方式的连接方法已经不太常见了(不过并 没有完全消失),特别是随着 20 世纪 90 年代中后期应用服务器的出现,有状态 连接更是少见。 假设你在使用一条有状态连接,应用可以查询数据而不做任何锁定: 最后,用户选择他想更新的一行。在这个例子中,假设用户选择更新 MILLE R 行。在这个时间点上(即用户还没有在屏幕上做任何修改,但是行已经从数据 库中读出一段时间了),应用会绑定用户选择的值,从而查询数据库,并确保数 据尚未修改。在 SQL*Plus 中,为了模拟应用可能执行的绑定调用,可以发出以 下命令: 下面,除了简单地查询值并验证数据尚未修改外,我们要使用 FOR UPDATE NOWAIT 锁定这一行。应用要执行以下查询: 根据屏幕上输入的数据,应用将提供绑定变量的值(在这里就是 7934、MIL LER 和 1300),然后重新从数据库查询这一行,这一次会锁定这一行,不允许其 他会话更新;因此,这种方法称为悲观锁定(pessimistic locking)。在试图 更新之前我们就把行锁住了,因为我们很悲观,对于这一行能不能保持未改变很 是怀疑。 所有表都应该有一个主键(前面的 SELECT 最多会获取一个记录,因为它包 括主键 EMPNO),而且主键应该是不可变的(不应更新主键),从这句话可以得 出三个结论: ‰ 如果底层数据没有改变,就会再次得到 MILLER 行,而且这一行会被锁定,不允 许其他会话更新(但是允许其他会话读)。 ‰ 如果另一个用户正在更新这一行,我们就会得到一个 ORA-00054:resource busy (ORA-00054:资源忙)错误。相应地,必须等待更新这一行的用户执行工作。 ‰ 在选择数据和指定有意更新之间,如果有人已经修改了这一行,我们就会得到 0 行。这说明,屏幕上的数据是过时的。为了避免前面所述的丢失更新情况,应用需 要重新查询(requery),并在允许在最终用户修改之前锁定数据。有了悲观锁定, User2 试图更新电话号码字段时,应用现在会识别出地址字段已经修改,所以会重 新查询数据。因此,User2 不会用这个字段的旧数据覆盖 User1 的修改。 一旦成功地锁定了这一行,应用就会绑定新值,发出更新命令后,提交所做 的修改: 现在可以非常安全地修改这一行。我们不可能覆盖其他人所做的修改,因为 已经验证了在最初读出数据之后以及对数据锁定之前数据没有改变。 6.2.3 乐观锁定 第二种方法称为乐观锁定(optimistic locking),即把所有锁定都延迟到 即将执行更新之前才做。换句话说,我们会修改屏幕上的信息而不要锁。我们很 乐观,认为数据不会被其他用户修改;因此,会等到最后一刻才去看我们的想法 对不对。 这种锁定方法在所有环境下都行得通,但是采用这种方法的话,执行更新的用 户“失败”的可能性会加大。这说明,这个用户要更新他的数据行时,发现数据 已经修改过,所以他必须从头再来。 可以在应用中同时保留旧值和新值,然后在更新数据时使用如下的更新语 句,这是乐观锁定的一种流行实现: 在此,我们乐观地认为数据没有修改。在这种情况下,如果更新语句更新 了一行,那我们很幸运;这说明,在读数据和提交更新之间,数据没有改变。 但是如果更新了零行,我们就会失败;另外一个人已经修改了数据,现在我们 必须确定应用中下一步要做什么。是让最终用户查询这一行现在的新值,然后 再重新开始事务呢(这可能会让用户很受打击,因为这一行有可能又被修改 了)?还是应该根据业务规则解决更新冲突,试图合并两个更新的值(这需要 大量的代码)? 实际上,前面的 UPDATE 能避免丢失更新,但是确实有可能被阻塞,在等待另 一个会话执行对这一行的 UPDATE 时,它会挂起。如果所有应用(会话)都使用乐 观锁定,那么使用直接的 UPDATE 一般没什么问题,因为执行更新并提交时,行只 会被锁定很短的时间。不过,如果某些应用使用了悲观锁定,它会在一段相对较 长的时间内持有行上的锁,你可能就会考虑使用 SELECT FOR UPDATE NOWAIT,以 此来验证行是否未被修改,并在即将 UPDATE 之前锁定来避免被另一个会话阻塞。 实现乐观并发控制的方法有很多种。我们已经讨论了这样的一种方法,即应 用本身会存储行的所有“前”(before)映像。在后几节中,我们将介绍另外三 种方法,分别是: ‰ 使用一个特殊的列,这个列由一个数据库触发器或应用程序代码维护,可以告诉 我们记录的“版本” ‰ 使用一个校验和或散列值,这是使用原来的数据计算得出的 ‰ 使用新增的 Oracle 10g 特性 ORA_ROWSCN。 1. 使用版本列的乐观锁定 这是一个简单的实现,如果你想保护数据库表不出现丢失更新问题,应对每 个要保护的表增加一列。这一列一般是NUMBER 或 DATE/TIMESTAMP 列,通常通过 表上的一个行触发器来维护。每次修改行时,这个触发器要负责递增 NUMBER 列 中的值,或者更新 DATE/TIMESTAMP 列。 如果应用要实现乐观并发控制,只需要保存这个附加列的值,而不需要保存 其他列的所有“前”映像。应用只需验证请求更新那一刻,数据库中这一列的值 与最初读出的值是否匹配。如果两个值相等,就说明这一行未被更新过。 下面使用 SCOTT.DEPT 表的一个副本来看看乐观锁定的实现。我们可以使用 以下数据定义语言(Data Definition Language,DDL)来创建这个表: 然后向这个表 INSERT(插入)DEPT 数据的一个副本: 以上代码会重建 DEPT 表,但是将有一个附加的 LAST_MOD 列,这个列使用 T IMESTAMP WITH TIME ZONE 数据类型(Oracle9i 及以上版本中才有这个数据类 型)。我们将这个列定义为 NOT NULL,以保证这个列必须填有数据,其默认值 是当前的系统时间。 这个 TIMESTAMP 数据类型在 Oracle 中精度最高,通常可以精确到微秒(百 万分之一秒)。如果应用要考虑到用户的思考时间,这种 TIMESTAMP 级的精度 实在是绰绰有余,而且数据库获取一行后,人看到这一行,然后修改,再向数 据库发回更新,一般不太可能在不到 1 秒钟的片刻时间内执行整个过程。两个 人在同样短的时间内(不到 1 秒钟)读取和修改同一行的几率实在太小了。 接下来,需要一种方法来维护这个值。我们有两种选择:可以由应用维护 这一列,更新记录时将 LAST_MOD 列的值设置为 SYSTIMESTAMP;也可以由触发 器/存储过程来维护。如果让应用维护 LAST_MOD,这比基于触发器的方法表现 更好,因为触发器会代表 Oracle 对修改增加额外的处理。不过这并不是说: 无论什么情况,你都要依赖所有应用在表中经过修改的所有位置上一致地维 护 LAST_MOD。所以,如果要由各个应用负责维护这个字段,就需要一致地验 证 LAST_MOD 列未被修改,并把 LAST_MOD 列设置为当前的 SYSTIMESTAMP。例 如,如果应用查询 DEPTNO=10 这一行: 目前我们看到的是: 再使用下面的更新语句来修改信息。最后一行执行了一个非常重要的检查,以确 保时间戳没有改变,并使用内置函数 TO_TIMESTAMP_TZ(TZ 是 TimeZone 的缩写, 即时区)将以上 select(选择)得到的串转换为适当的数据类型。另外,如果 发现行已经更新,以下更新语句中的第 3 行会把 LAST_MOD 列更新为当前时间: 可以看到,这里更新了一行,也就是我们关心的那一行。在此按主键(DEPTN O)更新了这一行,并验证从最初读取记录到执行更新这段时间,LAST_MOD 列未 被其他会话修改。如果我们想尝试再更新这个记录,仍然使用同样的逻辑,不过 没有获取新的 LAST_MOD 值,就会观察到以下情况: 注意到这一次报告称“0 rows updated”(更新了 0 行),因为关于 LAST_ MOD 的谓词条件不能满足。尽管 DEPTNO 10 还存在,但是想要执行更新的那个时 刻的 LAST_MOD 值与查询行时的时间戳值不再匹配。所以,应用知道,既然未能 修改行,就说明数据库中的数据已经(被别人)改变,现在它必须得出下一步要 对此做什么。 不能总是依赖各个应用来维护这个字段,原因是多方面的。例如,这样会增 加应用程序代码,而且只要是表中需要修改的地方,都必须重复这些代码,并正 确地实现。在一个大型应用中,这样的地方可能很多。另外,将来开发的每个应 用也必须遵循这些规则。应用程序代码中很可能会“遗漏”某一处,未能适当地 使用这个字段。因此,如果应用程序代码本身不负责维护这个 LAST_MOD 字段, 我相信应用也不应负责检查这个 LAST_MOD 字段(如果它确实能执行检查,当然 也能执行更新!)。所以在这种情况下,我建议把更新逻辑封装到一个存储过程 中,而不要让应用直接更新表。如果无法相信应用能维护这个字段的值,那么也 无法相信它能正确地检查这个字段。存储过程可以取以上更新中使用的绑定变量 作为输入,执行同样的更新。当检测到更新了 0 行时,存储过程会向客户返回一 个异常,让客户知道更新实际上失败了。 还有一种实现是使用一个触发器来维护这个 LAST_MOD 字段,但是对于这么 简单的工作,我建议还是避免使用触发器,而让 DML 来负责。触发器会引入大量 开销,而且在这种情况下没有必要使用它们。 2. 使用校验和的乐观锁定 这与前面的版本列方法很相似,不过在此要使用基数据本身来计算一个“虚 拟的”版本列。为了帮助解释有关校验和或散列函数的目标和概念,以下引用了 Oracle 10g PL/SQL Supplied Packages Guide 中的一段话(尽管现在还没有介 绍如何使用 Oracle 提供的任何一个包!): 单向散列函数取一个变长输入串(即数据),并把它转换为一个定长的输出 串(通常更小),这个输出称为散列值(hash value)。散列值充当输入数据的 一个惟一标识符(就像指纹一样)。可以使用散列值来验证数据是否被修改。 需要注意,单向散列函数只能在一个方向上应用。从输入数据计算散列值很容易, 但是要生成能散列为某个特定值的数据却很难。 散列值或校验和并非真正惟一。只能说,通过适当地设计,能使出现冲突的 可能性相当小,也就是说,两个随机的串有相同校验和或散列值的可能性极小, 足以忽略不计。 与使用版本列的做法一样,我们可以采用同样的方法使用这些散列值或校验 和,只需把从数据库读出数据时得到的散列或校验和值与修改数据前得到的散列 或校验和值进行比较。在我们读出数据之后,但是在修改数据之前,如果有人在 这段时间内修改了这一行的值,散列值或校验和值往往会大不相同。 有很多方法来计算散列或校验和。这里列出其中的 3 种方法,分别在以下 3 个小节中介绍。所有这些方法都利用了 Oracle 提供的数据库包: ‰ OWA_OPT_LOCK.CHECKSUM:这个方法在 Oracle8i 8.1.5 及以上版本中提供。 给定一个串,其中一个函数会返回一个 16 位的校验和。给定 ROWID 时,另一个函 数会计算该行的 16 位校验和,而且同时将这一行锁定。出现冲突的可能性是 65 53 6 分之一(65 536 个串中有一个冲突,这是假警报的最大几率)。 ‰ DBMS_OBFUSCATION_TOOLKIT.MD5:这个方法在 Oracle8i 8.1.7 及以上版本 中提供。它会计算一个 128 位的消息摘要。冲突的可能性是 3.4028E+38 分之一(非 常小)。 ‰ DBMS_CRYPTO.HASH:这个方法在 Oracle 10g Release 1 及以上版本中提供。 它能计算一个 SHA-1(安全散列算法 1,Secure Hash Algorithm 1)或 MD4/MD5 消息摘要。建议你使用 SHA-1 算法。 注意 很多编程语言中都提供了一些散列和校验和函数,所以还可以使用数据库 之外的散列和校验和函数。 下面的例子显示了如何使用 Oracle 10g 中的 DBMS_CRYPTO 内置包来计算这 些散列/校验和。这个技术也适用于以上所列的另外两个包;逻辑上差别不大, 但是调用的 API 可能不同。 下面在某个应用中查询并显示部门 10 的信息。查询信息之后,紧接着我们 使用 DBMS_CRYPTO 包计算散列。这是应用中要保留的“版本”信息: 可以看到,散列值就是一个很大的 16 进制位串。DBMS_CRYPTO 的返回值是一 个 RAW 变量,显示时,它会隐式地转换为 HEX。这个值会在更新前使用。为了执 行更新,需要在数据库中获取这一行,并按其现在的样子锁定,然后计算所获取 的行的散列值,将这个新散列值与从数据库读出数据时计算的散列值进行比较。 上述逻辑表示如下(当然,在实际中,可能使用绑定变量而不是散列值直接量): 更新后,重新查询数据,并再次计算散列值,此时可以看到散列值大不相同。 如果有人抢在我们前面先修改了这一行,我们的散列值比较就不会成功: 这个例子显示了如何利用散列或校验和来实现乐观锁定。要记住,计算散列 或校验和是一个 CPU 密集型操作(相当占用 CPU),其计算代价很昂贵。如果系 统上 CPU 是稀有资源,在这种系统上就必须充分考虑到这一点。不过,如果从“网 络友好性”角度看,这种方法会比较好,因为只需在网络上传输相当小的散列值, 而不是行的完整的前映像和后映像(以便逐列地进行比较),所以消耗的资源会 少得多。下面最后一个例子会使用一个新的 Oracle 10g 函数 ORA_ROWSCN,它不 仅很小(类似于散列),而且计算时不是 CPU 密集的(不会过多占用 CPU)。 3. 使用 ORA_ROWSCN 的乐观锁定 从 Oracle 10g Release 1 开始,你还可以使用内置的 ORA_ROWSCN 函数。它 的工作与前面所述的版本列技术很相似,但是可以由 Oracle 自动执行,而不需 要在表中增加额外的列,也不需要额外的更新/维护代码来更新这个值。 ORA_ROWSCN 建立在内部 Oracle 系统时钟(SCN)基础上。在 Oracle 中,每 次提交时,SCN 都会推进(其他情况也可能导致 SCN 推进,要注意,SCN 只会推 进,绝对不会后退)。这个概念与前面在获取数据时得到 ORA_ROWSCN 的方法是 一样的,更新数据时要验证 SCN 未修改过。之所以我会强调这一点(而不是草草 带过),原因是除非你创建表时支持在行级维护 ORA_ROWSCN,否则 Oracle 会在 块级维护。也就是说,默认情况下,一个块上的多行会共享相同的 ORA_ROWSCN 值。如果更新一个块上的某一行,而且这个块上还有另外 50 行,那么这些行的 ORA_ROWSCN 也会推进。这往往会导致许多假警报,你认为某一行已经修改,但 实际上它并没有改动。因此,需要注意这一点,并了解如何改变这种行为。 我们想查看这种行为,然后进行修改,为此还要使用前面的小表 DEPT: 现在可以观察到每一行分别在哪个块上(在这种情况下,可以假设它们都在 同一个文件中,所以如果块号相同,就说明它们在同一个块上)。我使用的块大 小是 8 KB,一行的宽度大约 3 550 字节,所以我预料这个例子中每块上有两行: 不错,我们观察的结果也是这样,每块有两行。所以,下面来更新块 20972 上 DEPTNO = 10 的那一行: 接下来观察到,ORA_ROWSCN 的结果在块级维护。我们只修改了一行,也只提 交了这一行的修改,但是块 20972 上两行的 ORA_ROWSCN 值都推进了: 如果有人读取 DEPTNO=20 这一行,看起来这一行已经修改了,但实际上并非 如此。块 20973 上的行是“安全”的,我们没有修改这些行,所以它们没有推进。 不过,如果更新其中任何一行,两行都将推进。所以现在的问题是:如何修改这 种默认行为。遗憾的是,我们必须启用 ROWDEPENDENCIES 再重新创建这个段。 Oracle9i 为数据库增加了行依赖性跟踪,可以支持推进复制,以便更好地并 行传播修改。在 Oracle 10g 之前,这个特性只能在复制环境中使用;但是从 Or acle 10g 开始,还可以利用这个特性用 ORA_ROWSCN 来实现一种有效的乐观锁定 技术。它会为每行增加 6 字节的开销(所以与自己增加版本列的方法(即 DIY 版本列方法)相比,并不会节省空间),而实际上,也正是因为这个原因,所以 需要重新创建表,而不只是简单地 ALTER TABLE:必须修改物理块结构来适应这 个特性。 下面重新建立我们的表,启用 ROWDEPENDENCIES。可以使用 DBMS_REDEFINIT ION 中(Oracle 提供的另一个包)的在线重建功能来执行,但是对于一个这么小 的任务,我们还是从头开始更好一些: 又回到前面:两个块上有 4 行,它们都有相同的 ORA_ROWSCN 值。现在,更 新 DEPTNO=10 的那一行时: 查询 DEPT 表时应该能观察到以下结果: 此时,只有 DEPTNO = 10 这一行的 ORA_ROWSCN 改变,这正是我们所希望的。 现在可以依靠 ORA_ROWSCN 来为我们检测行级修改了。 将 SCN 转换为墙上时钟时间 使用透明的 ORA_ROWSCN 列还有一个好处:可以把 SCN 转换为近似的墙上时 钟时间(有+/–3 秒的偏差),从而发现行最后一次修改发生在什么时间。例如,可 以执行以下查询: 在此可以看到,在表的最初创建和更新 DEPTNO = 10 行之间,我等了大约 3 分钟。不过,从 SCN 到墙上时钟时间的这种转换有一些限制:数据库的正常运行时 间只有 5 天左右。例如,如果查看一个“旧”表,查找其中最旧的 ORA_ROWSCN (注意,在此我作为 SCOTT 登录;没有使用前面的新表): 如果我试图把这个 SCN 转换为一个时间戳,可能看到以下结果(取决于 DEPT 表有多旧!): 所以从长远看不能依赖这种转换。 6.2.4 乐观锁定还是悲观锁定 那么哪种方法最好呢?根据我的经验,悲观锁定在 Oracle 中工作得非常好 (但是在其他数据库中可能不是这样),而且与乐观锁定相比,悲观锁定有很多 优点。不过,它需要与数据库有一条有状态的连接,如客户/服务器连接,因为 无法跨连接持有锁。正是因为这一点,在当前的许多情况下,悲观锁定不太现实。 过去,客户/服务器应用可能只有数十个或数百个用户,对于这些应用,悲观锁 定是我的不二选择。不过,如今对大多数应用来说,我都建议采用乐观并发控制。 要在整个事务期间保持连接,这个代价太大了,一般无法承受。 在这些可用的方法中,我使用哪一种呢?我喜欢使用版本列方法,并增加一 个时间戳列(而不只是一个 NUMBER)。从长远看,这样能为我提供一个额外的 信息:“这一行最后一次更新发生在什么时间?”所以意义更大。而且与散列或 校验和方法相比,计算的代价不那么昂贵,在处理 LONG、LONG RAW、CLOB、BLO B 和其他非常大的列时,散列或校验和方法可能会遇到一些问题,而版本列方法 则没有这些问题。 如果必须向一个表增加乐观并发控制,而此时还在利用悲观锁定机制使用这 个表(例如,客户/服务器应用都在访问这个表,而且还在通过 Web 访问),我 则倾向于选择 ORA_ROWSCN 方法。这是因为,在现有的遗留应用中,可能不希望 出现一个新列,或者即使我们另外增加一步把这个额外的列隐藏起来,为了维护 这个列,可能需要一个必要的触发器,而这个触发器的开销非常大,这是我们无 法承受的。ORA_ROWSCN 技术没有干扰性,而且在这个方面是轻量级的(当然, 这是指我们执行表的重建之后)。 散列/校验和方法在数据库独立性方面很不错,特别是如果我们在数据库之 外计算散列或校验和,则更是如此。不过,如果在中间层而不是在数据库中执行 计算,从 CPU 使用和网络传输方面来看,就会带来更大的资源使用开销。 6.2.5 阻塞 如果一个会话持有某个资源的锁,而另一个会话在请求这个资源,就会出现 阻塞(blocking)。这样一来,请求的会话会被阻塞,它会“挂起”,直至持有 锁的会话放弃锁定的资源。几乎在所有情况下,阻塞都是可以避免的。实际上, 如果你真的发现会话在一个交互式应用中被阻塞,就说明很有可能同时存在着另 一个 bug,即丢失更新,只不过你可能没有意识到这一点。也就是说,你的应用 逻辑有问题,这才是阻塞的根源。 数据库中有 5 条常见的 DML 语句可能会阻塞,具体是:INSERT、UPDATE、DE LETE、MERGE 和 SELECT FOR UPDATE。对于一个阻塞的 SELECT FOR UPDATE,解 决方案很简单:只需增加 NOWAIT 子句,它就不会阻塞了。这样一来, 你的应用 会向最终用户报告,这一行已经锁定。另外 4 条 DML 语句才有意思。我们会分别 分析这些 DML 语句,看看它们为什么不应阻塞,如果真的阻塞了又该如何修正。 1. 阻塞的 INSERT INSERT 阻塞的情况不多见。最常见的情况是,你有一个带主键的表,或者表 上有惟一的约束,但有两个会话试图用同样的值插入一行。如果是这样,其中一 个会话就会阻塞,直到另一个会话提交或者回滚为止:如果另一个会话提交,那 么阻塞的会话会收到一个错误,指出存在一个重复值;倘若另一个会话回滚,在 这种情况下,阻塞的会话则会成功。还有一种情况,可能多个表通过引用完整性 约束相互链接。对子表的插入可能会阻塞,因为它所依赖的父表正在创建或删除。 如果应用允许最终用户生成主键/惟一列值,往往就会发生 INSERT 阻塞。为 避免这种情况,最容易的做法是使用一个序列来生成主键/惟一列值。序列(se quence)设计为一种高度并发的方法,用在多用户环境中生成惟一键。如果无法 使用序列,那你可以使用以下技术,也就是使用手工锁来避免这个问题,这里的 手工锁通过内置的 DBMS_LOCK 包来实现。 注意 会话可能因为主键或惟一约束而遭遇插入阻塞,下面的例子展示了如何避 免这种情况。需要强调一点,这里所示的“修正方法”只能算是一个短期的 解决方案,因为这个应用的体系结构本身就存在问题。这种方法显然会增加 开销,而且不能轻量级地实现。如果应用设计得好,就不会遇到这个问题。 只能把这当作最后一道防线,千万不要因为“以防万一”而对应用中的每个 表都采用这种技术。 对于插入,不会选择现有的行,也不会对现有的行锁定。没有办法避免其他人 插入值相同的行,如果别人真的插入了具有相同值的行,这会阻塞我们的会话, 而导致我们无休止地等待。此时,DBMS_LOCK 就能派上用场了。为了介绍这个技 术,下面创建一个带主键的表,还有一个触发器,它会防止两个(或更多)会话 同时插入相同的值。这个触发器使用 DBMS_UTILITY.GET_ HASH_VALUE 来计算主键 的散列值,得到一个 0~1 073 741 823 之间的数(这也是 Oracle 允许我们使用 的锁 ID 号的范围)。在这个例子中,我选择了一个大小为 1 024 的散列表,这说 明我们会把主键散列到 1 024 个不同的锁 ID。然后使用 DBMS_LOCK.REQUEST 根据 这个 ID 分配一个排他锁(也称独占锁,exclusive lock)。一次只有一个会话能 做这个工作,所以,如果有人想用相同的主键值向表中插入一条记录,这个人的 锁请求就会失败(并且会产生 resource busy(资源忙)错误): 注意 为了成功地编译这个触发器,必须直接给你的模式授予 DBMS_LOCK 的 执行权限。执行 DBMS_LOCK 的权限不能从角色得来: 现在,如果在两个单独的会话中执行下面的插入: 第一个会话会成功,但是紧接着第二个会话中会得出以下错误: 这里的思想是:为表提供的主键值要受触发器的保护,并把它放入一个字符 串中。然后可以使用DBMS_UTILITY.GET_HASH_VALUE 为这个串得出一个“几乎惟 一”的散列值。只要使用小于 1 073 741 823 的散列表,就可以使用 DBMS_LOCK 独占地“锁住”这个值。 计算散列之后,取得这个散列值,并使用 DBMS_LOCK 来请求将这个锁 ID 独 占地锁住(超时时间为 ZERO,这说明如果已经有人锁住了这个值,它会立即返 回)。如果超时或者由于某种原因失败了,将产生 ORA-54 Resource Busy(资 源忙)错误。否则什么也不做,完全可以顺利地插入,我们不会阻塞。 当然,如果表的主键是一个 INTEGER,而你不希望这个主键超过 1 000 000 000,那么可以跳过散列,直接使用这个数作为锁 ID。要适当地设置散列表的大 小(在这个例子中,散列表的大小是 1 024),以避免因为不同的串散列为同一 个数(这称为散列冲突)而人工地导致资源忙消息。散列表的大小与特定的应用 (数据)有关,并发插入的数量也会影响散列表的大小。最后,还要记住,尽管 Oracle 有无限多个行级锁,但是 enqueue 锁(这是一种队列锁)的个数则是有 限的。如果在会话中插入大量行,而没有提交,可能就会发现创建了太多的 enq ueue 队列锁,而耗尽了系统的队列资源(超出了 ENQUEUE_RESOURCES 系统参数 设置的最大值),因为每行都会创建另一个 enqueue 锁。如果确实发生了这种情 况,就需要增大 ENQUEUE_RESOURCES 参数的值。还可以向触发器增加一个标志, 允许打开或关闭这种检查。例如,如果我准备插入数百条或数千条记录,可能就 不希望启用这个检查。 2. 阻塞的 Merge、Update 和 Delete 在一个交互式应用中,可以从数据库查询某个数据,允许最终用户处理这个 数据,再把它“放回”到数据库中,此时如果 UPDATE 或 DELETE 阻塞,就说明你 的代码中可能存在一个丢失更新问题(如果真是这样,按我的说法,就是你的代 码中存在 bug)。你试图 UPDATE(更新)其他人正在更新的行(换句话说,有人 已经锁住了这一行)。通过使用 SELECT FOR UPDATE NOWAIT 查询可以避免这个 问题,这个查询能做到: ‰ 验证自从你查询数据之后数据未被修改(防止丢失更新)。 ‰ 锁住行(防止 UPDATE 或 DELETE 被阻塞)。 如前所述,不论采用哪一种锁定方法都可以这样做。不论是悲观锁定还是乐 观锁定都可以利用 SELECT FOR UPDATE NOWAIT 查询来验证行未被修改。悲观锁 定会在用户有意修改数据那一刻使用这条语句。乐观锁定则在即将在数据库中更 新数据时使用这条语句。这样不仅能解决应用中的阻塞问题,还可以修正数据完 整性问题。 由于 MERGE 只是 INSERT 和 UPDATE(如果在 10g 中采用改进的 MERGE 语法, 还可以是 DELETE),所以可以同时使用这两种技术。 6.2.6 死锁 如果你有两个会话,每个会话都持有另一个会话想要的资源,此时就会出现死锁(deadloc k)。例如,如果我的数据库中有两个表 A 和 B,每个表中都只有一行,就可以很容易地展示什 么是死锁。我要做的只是打开两个会话(例如,两个 SQL*Plus 会话)。在会话 A 中更新表 A, 并在会话 B 中更新表 B。现在,如果我想在会话 B 中更新表 A,就会阻塞。会话 A 已经锁定了 这一行。这不是死锁;只是阻塞而已。我还没有遇到过死锁,因为会话 A 还有机会提交或回滚, 这样会话 B 就能继续。 如果我再回到会话 A,试图更新表 B,这就会导致一个死锁。要在这两个会话中选择一 个作为“牺牲品”,让它的语句回滚。例如,会话 B 中对表 A 的更新可能回滚,得到以下错 误: 想要更新表 B 的会话 A 还阻塞着,Oracle 不会回滚整个事务。只会回滚与死锁有关的 某条语句。会话 B 仍然锁定着表 B 中的行,而会话 A 还在耐心地等待这一行可用。收到死 锁消息后,会话 B 必须决定将表 B 上未执行的工作提交还是回滚,或者继续走另一条路, 以后再提交。一旦这个会话执行提交或回滚,另一个阻塞的会话就会继续,好像什么也没有 发生过一样。 Oracle 认为死锁很少见,而且由于如此少见,所以每次出现死锁时它都会在服务器上创 建一个跟踪文件。这个跟踪文件的内容如下: 显然,Oracle 认为这些应用死锁是应用自己导致的错误,而且在大多数情况下,Oracle 的这种看法都是正确的。不同于许多其他的 RDBMS,Oracle 中极少出现死锁,甚至可以认 为几乎不存在。通常情况下,必须人为地提供条件才会产生死锁。 根据我的经验,导致死锁的头号原因是外键未加索引(第二号原因是表上的位图索引遭 到并发更新,这个内容将在第 11 章讨论)。在以下两种情况下,Oracle 在修改父表后会对子 表加一个全表锁: ‰ 如果更新了父表的主键(倘若遵循关系数据库的原则,即主键应当是不可变的, 这种情况就很少见),由于外键上没有索引,所以子表会被锁住。 ‰ 如果删除了父表中的一行,整个子表也会被锁住(由于外键上没有索引)。 在 Oracle9i 及以上版本中,这些全表锁都是短期的,这意味着它们仅在 DML 操作期间 存在,而不是在整个事务期间都存在。即便如此,这些全表锁还是可能(而且确实会)导致 很严重的锁定问题。下面说明第二点,如果用以下命令建立了两个表: 然后执行以下语句: 到目前为止,还没有什么问题。但是如果再到另一个会话中,试图删除第一条父记 录: 此时就会发现,这个会话立即被阻塞了。它在执行删除之前试图对表 C 加一个全表锁。现 在,别的会话都不能对 C 中的任何行执行 DELETE、INSERT 或 UPDATE(已经开始的会话 可以继续,但是新会话将无法修改 C)。 更新主键值也会发生这种阻塞。因为在关系数据库中,更新主键是一个很大的禁忌,所 以更新在这方面一般没有什么问题。在我看来,如果开发人员使用能生成 SQL 的工具,而 且这些工具会更新每一列,而不论最终用户是否确实修改了那些列,此时更新主键就会成为 一个严重的问题。例如,假设我们使用了 Oracle Forms,并为表创建了一个默认布局。默认 情况下,Oracle Forms 会生成一个更新,对我们选择要显示的表中的每一列进行修改。如果 在 DEPT 表中建立一个默认布局,包括 3 个字段,只要我们修改了 DEPT 表中的任何列,O racle Forms 都会执行以下命令: 在这种情况下,如果 EMP 表有 DEPT 的一个外键,而且在 EMP 表的 DEPTNO 列上没 有任何索引,那么更新 DEPT 时整个 EMP 表都会被锁定。如果你使用了能生成 SQL 的工具, 就一定要当心这一点。即便主键值没有改变,执行前面的 SQL 语句后,子表 EMP 也会被锁 定。如果使用 Oracle Forms,解决方案是把这个表的 UPDATE CHANGED COLUMNS ON LY 属性设置为 YES。这样一来,Oracle Forms 会生成一条 UPDATE 语句,其中只包含修改 过的列(而不包括主键)。 删除父表中的一行可能导致子表被锁住,由此产生的问题更多。我已经说过,如果删除 表 P 中的一行,那么在 DML 操作期间,子表 C 就会锁定,这样能避免事务期间对 C 执行 其他更新(当然,这有一个前提,即没有人在修改 C;如果确实已经有人在修改 C,删除会 等待)。此时就会出现阻塞和死锁问题。通过锁定整个表 C,数据库的并发性就会大幅下降, 以至于没有人能够修改 C 中的任何内容。另外,出现死锁的可能性则增加了,因为我的会 话现在“拥有”大量数据,直到提交时才会交出。其他会话因为 C 而阻塞的可能性也更大; 只要会话试图修改 C 就会被阻塞。因此,我开始注意到,数据库中大量会话被阻塞,这些 会话持有另外一些资源的锁。实际上,如果其中任何阻塞的会话锁住了我的会话需要的资源, 就会出现一个死锁。在这种情况下,造成死锁的原因是:我的会话不允许别人访问超出其所 需的更多资源(在这里就是一个表中的所有行)。如果有人抱怨说数据库中存在死锁,我会 让他们运行一个脚本,查看是不是存在未加索引的外键,而且在 99%的情况下都会发现表 中确实存在这个问题。只需对外键加索引,死锁(以及大量其他的竞争问题)都会烟消云散。 下面的例子展示了如何使用这个脚本来找出表 C 中未加索引的外键: 这个脚本将处理外键约束,其中最多可以有 8 列(如果你的外键有更多的列,可能就得 重新考虑一下你的设计了)。首先,它在前面的查询中建立一个名为 CONS 的内联视图(inl ine view)。这个内联视图将约束中适当的列名从行转置到列,其结果是每个约束有一行, 最多有 8 列,这些列分别取值为约束中的列名。另外,这个视图中还有一个列 COL_CNT, 其中包含外键约束本身的列数。对于这个内联视图中返回的每一行,我们要执行一个关联子 查询(correlated subquery),检查当前所处理表上的所有索引。它会统计出索引中与外键约 束中的列相匹配的列数,然后按索引名分组。这样,就能生成一组数,每个数都是该表某个 索引中匹配列的总计。如果原来的 COL_CNT 大于所有这些数,那么表中就没有支持这个约 束的索引。如果 COL_CNT 小于所有这些数,就至少有一个索引支持这个约束。注意,这里 使用了 NVL2 函数,我们用这个函数把列名列表“粘到”一个用逗号分隔的列表中。这个 函数有 3 个参数:A、B 和 C。如果参数 A 非空,则返回 B;否则返回参数 C。这个查询有 一个前提,假设约束的所有者也是表和索引的所有者。如果另一位用户对表加索引,或者表 在另一个模式中(这两种情况都很少见),就不能正确地工作。 所以,这个脚本展示出,表 C 在列 X 上有一个外键,但是没有索引。通过对 X 加索 引,就可以完全消除这个锁定问题。除了全表锁外,在以下情况下,未加索引的外键也 可能带来问题: ‰ 如果有 ON DELETE CASCADE,而且没有对子表加索引: 例如,EMP 是 DEPT 的子表,DELETE DEPTNO = 10 应该 CASCADE(级联)至 EMP。如果 EMP 中 的 DEPTNO 没有索引,那么删除 DEPT 表中的每一行时都会对 EMP 做一个全表扫 描。这个全表扫描可能是不必要的,而且如果从父表删除多行,父表中每删除一行 就要扫描一次子表。 ‰ 从父表查询子表: 再次考虑 EMP/DEPT 例子。利用 DEPTNO 查询 EMP 表是相当 常见的。如果频繁地运行以下查询(例如,生成一个报告),你会发现没有索引会 使查询速度变慢: „ select * from dept, emp „ where emp.deptno = dept.deptno and dept.deptno = :X; 那么,什么时候不需要对外键加索引呢?答案是,一般来说,当满足以下条件 时不需要加索引: ‰ 没有从父表删除行。 ‰ 没有更新父表的惟一键/主键值(当心工具有时会无意地更新主键!)。 ‰ 没有从父表联结子表(如 DEPT 联结到 EMP)。 如果满足上述全部 3 个条件,那你完全可以跳过索引,不需要对外键加索引。如果 满足以上的某个条件,就要当心加索引的后果。这是一种少有的情况,即 Oracle“过分 地锁定了”数据。 6.2.7 锁升级 如果你有两个会话,每个会话都持有另一个会话想要的资源,此时就会出现死锁(deadloc k)。例如,如果我的数据库中有两个表 A 和 B,每个表中都只有一行,就可以很容易地展示什 么是死锁。我要做的只是打开两个会话(例如,两个 SQL*Plus 会话)。在会话 A 中更新表 A, 并在会话 B 中更新表 B。现在,如果我想在会话 B 中更新表 A,就会阻塞。会话 A 已经锁定了 这一行。这不是死锁;只是阻塞而已。我还没有遇到过死锁,因为会话 A 还有机会提交或回滚, 这样会话 B 就能继续。 如果我再回到会话 A,试图更新表 B,这就会导致一个死锁。要在这两个会话中选择一 个作为“牺牲品”,让它的语句回滚。例如,会话 B 中对表 A 的更新可能回滚,得到以下错 误: 想要更新表 B 的会话 A 还阻塞着,Oracle 不会回滚整个事务。只会回滚与死锁有关的 某条语句。会话 B 仍然锁定着表 B 中的行,而会话 A 还在耐心地等待这一行可用。收到死 锁消息后,会话 B 必须决定将表 B 上未执行的工作提交还是回滚,或者继续走另一条路, 以后再提交。一旦这个会话执行提交或回滚,另一个阻塞的会话就会继续,好像什么也没有 发生过一样。 Oracle 认为死锁很少见,而且由于如此少见,所以每次出现死锁时它都会在服务器上创 建一个跟踪文件。这个跟踪文件的内容如下: 显然,Oracle 认为这些应用死锁是应用自己导致的错误,而且在大多数情况下,Oracle 的这种看法都是正确的。不同于许多其他的 RDBMS,Oracle 中极少出现死锁,甚至可以认 为几乎不存在。通常情况下,必须人为地提供条件才会产生死锁。 根据我的经验,导致死锁的头号原因是外键未加索引(第二号原因是表上的位图索引遭 到并发更新,这个内容将在第 11 章讨论)。在以下两种情况下,Oracle 在修改父表后会对子 表加一个全表锁: ‰ 如果更新了父表的主键(倘若遵循关系数据库的原则,即主键应当是不可变的, 这种情况就很少见),由于外键上没有索引,所以子表会被锁住。 ‰ 如果删除了父表中的一行,整个子表也会被锁住(由于外键上没有索引)。 在 Oracle9i 及以上版本中,这些全表锁都是短期的,这意味着它们仅在 DML 操作期间 存在,而不是在整个事务期间都存在。即便如此,这些全表锁还是可能(而且确实会)导致 很严重的锁定问题。下面说明第二点,如果用以下命令建立了两个表: 然后执行以下语句: 到目前为止,还没有什么问题。但是如果再到另一个会话中,试图删除第一条父记 录: 此时就会发现,这个会话立即被阻塞了。它在执行删除之前试图对表 C 加一个全表锁。现 在,别的会话都不能对 C 中的任何行执行 DELETE、INSERT 或 UPDATE(已经开始的会话 可以继续,但是新会话将无法修改 C)。 更新主键值也会发生这种阻塞。因为在关系数据库中,更新主键是一个很大的禁忌,所 以更新在这方面一般没有什么问题。在我看来,如果开发人员使用能生成 SQL 的工具,而 且这些工具会更新每一列,而不论最终用户是否确实修改了那些列,此时更新主键就会成为 一个严重的问题。例如,假设我们使用了 Oracle Forms,并为表创建了一个默认布局。默认 情况下,Oracle Forms 会生成一个更新,对我们选择要显示的表中的每一列进行修改。如果 在 DEPT 表中建立一个默认布局,包括 3 个字段,只要我们修改了 DEPT 表中的任何列,O racle Forms 都会执行以下命令: 在这种情况下,如果 EMP 表有 DEPT 的一个外键,而且在 EMP 表的 DEPTNO 列上没 有任何索引,那么更新 DEPT 时整个 EMP 表都会被锁定。如果你使用了能生成 SQL 的工具, 就一定要当心这一点。即便主键值没有改变,执行前面的 SQL 语句后,子表 EMP 也会被锁 定。如果使用 Oracle Forms,解决方案是把这个表的 UPDATE CHANGED COLUMNS ON LY 属性设置为 YES。这样一来,Oracle Forms 会生成一条 UPDATE 语句,其中只包含修改 过的列(而不包括主键)。 删除父表中的一行可能导致子表被锁住,由此产生的问题更多。我已经说过,如果删除 表 P 中的一行,那么在 DML 操作期间,子表 C 就会锁定,这样能避免事务期间对 C 执行 其他更新(当然,这有一个前提,即没有人在修改 C;如果确实已经有人在修改 C,删除会 等待)。此时就会出现阻塞和死锁问题。通过锁定整个表 C,数据库的并发性就会大幅下降, 以至于没有人能够修改 C 中的任何内容。另外,出现死锁的可能性则增加了,因为我的会 话现在“拥有”大量数据,直到提交时才会交出。其他会话因为 C 而阻塞的可能性也更大; 只要会话试图修改 C 就会被阻塞。因此,我开始注意到,数据库中大量会话被阻塞,这些 会话持有另外一些资源的锁。实际上,如果其中任何阻塞的会话锁住了我的会话需要的资源, 就会出现一个死锁。在这种情况下,造成死锁的原因是:我的会话不允许别人访问超出其所 需的更多资源(在这里就是一个表中的所有行)。如果有人抱怨说数据库中存在死锁,我会 让他们运行一个脚本,查看是不是存在未加索引的外键,而且在 99%的情况下都会发现表 中确实存在这个问题。只需对外键加索引,死锁(以及大量其他的竞争问题)都会烟消云散。 下面的例子展示了如何使用这个脚本来找出表 C 中未加索引的外键: 这个脚本将处理外键约束,其中最多可以有 8 列(如果你的外键有更多的列,可能就得 重新考虑一下你的设计了)。首先,它在前面的查询中建立一个名为 CONS 的内联视图(inl ine view)。这个内联视图将约束中适当的列名从行转置到列,其结果是每个约束有一行, 最多有 8 列,这些列分别取值为约束中的列名。另外,这个视图中还有一个列 COL_CNT, 其中包含外键约束本身的列数。对于这个内联视图中返回的每一行,我们要执行一个关联子 查询(correlated subquery),检查当前所处理表上的所有索引。它会统计出索引中与外键约 束中的列相匹配的列数,然后按索引名分组。这样,就能生成一组数,每个数都是该表某个 索引中匹配列的总计。如果原来的 COL_CNT 大于所有这些数,那么表中就没有支持这个约 束的索引。如果 COL_CNT 小于所有这些数,就至少有一个索引支持这个约束。注意,这里 使用了 NVL2 函数,我们用这个函数把列名列表“粘到”一个用逗号分隔的列表中。这个 函数有 3 个参数:A、B 和 C。如果参数 A 非空,则返回 B;否则返回参数 C。这个查询有 一个前提,假设约束的所有者也是表和索引的所有者。如果另一位用户对表加索引,或者表 在另一个模式中(这两种情况都很少见),就不能正确地工作。 所以,这个脚本展示出,表 C 在列 X 上有一个外键,但是没有索引。通过对 X 加索 引,就可以完全消除这个锁定问题。除了全表锁外,在以下情况下,未加索引的外键也 可能带来问题: ‰ 如果有 ON DELETE CASCADE,而且没有对子表加索引: 例如,EMP 是 DEPT 的子表,DELETE DEPTNO = 10 应该 CASCADE(级联)至 EMP。如果 EMP 中 的 DEPTNO 没有索引,那么删除 DEPT 表中的每一行时都会对 EMP 做一个全表扫 描。这个全表扫描可能是不必要的,而且如果从父表删除多行,父表中每删除一行 就要扫描一次子表。 ‰ 从父表查询子表: 再次考虑 EMP/DEPT 例子。利用 DEPTNO 查询 EMP 表是相当 常见的。如果频繁地运行以下查询(例如,生成一个报告),你会发现没有索引会 使查询速度变慢: „ select * from dept, emp „ where emp.deptno = dept.deptno and dept.deptno = :X; 那么,什么时候不需要对外键加索引呢?答案是,一般来说,当满足以下条件 时不需要加索引: ‰ 没有从父表删除行。 ‰ 没有更新父表的惟一键/主键值(当心工具有时会无意地更新主键!)。 ‰ 没有从父表联结子表(如 DEPT 联结到 EMP)。 如果满足上述全部 3 个条件,那你完全可以跳过索引,不需要对外键加索引。如果 满足以上的某个条件,就要当心加索引的后果。这是一种少有的情况,即 Oracle“过分 地锁定了”数据。 6.3 锁类型 Oracle 中主要有 3 类锁,具体是: ‰ DML 锁(DML lock):DML 代表数据操纵语言(Data Manipulation Language)。一般 来讲,这表示 SELECT、INSERT、UPDATE、MERGE 和 DELETE 语句。DML 锁机制允许 并发执行数据修改。例如,DML 锁可能是特定数据行上的锁,或者是锁定表中所有行的表 级锁。 ‰ DDL 锁(DDL lock):DDL 代表数据定义语言(Data Definition Language),如 CRE ATE 和 ALTER 语句等。DDL 锁可以保护对象结构定义。 ‰ 内部锁和闩:Oracle 使用这些锁来保护其内部数据结构。例如,Oracle 解析一个查询并 生成优化的查询计划时,它会把库缓存“临时闩”,将计划放在那里,以供其他会话使用。 闩(latch)是 Oracle 采用的一种轻量级的低级串行化设备,功能上类似于锁。不要被“轻 量级”这个词搞糊涂或蒙骗了,你会看到,闩是数据库中导致竞争的一个常见原因。轻量级 指的是闩的实现,而不是闩的作用。 下面将更详细地讨论上述各个特定类型的锁,并介绍使用这些锁有什么影 响。除了我在这里介绍的锁之外,还有另外一些锁类型。这一节以及下一节介绍 的锁是最常见的,而且会保持很长时间。其他类型的锁往往只保持很短的一段时 间。 6.3.1DML 锁 DML 锁(DML Lock)用于确保一次只有一个人能修改某一行,而且你正在处 理一个表时别人不能删除这个表。在你工作时,Oracle 会透明程度不一地为你 加这些锁。 1. TX 锁(事务锁) 事务发起第一个修改时会得到 TX 锁(事务锁),而且会一直持有这个锁, 直至事务执行提交(COMMIT)或回滚(ROLLBACK)。TX 锁用作一种排队机制, 使得其他会话可以等待这个事务执行。事务中修改或通过 SELECT FOR UPDATE 选择的每一行都会“指向”该事务的一个相关 TX 锁。听上去好像开销很大,但 实际上并非如此。要想知道这是为什么,需要从概念上对锁“居住”在哪里以及 如何管理锁有所认识。在 Oracle 中,闩为数据的一个属性(第 10 章会给出 Ora cle 块格式的一个概述)。Oracle 并没有一个传统的锁管理器,不会用锁管理器 为系统中锁定的每一行维护一个长长的列表。不过,其他的许多数据库却是这样 做的,因为对于这些数据库来说,锁是一种稀有资源,需要对锁的使用进行监视。 使用的锁越多,系统要管理的方面就越多,所以在这些系统中,如果使用了“太 多的”锁就会有问题。 如果数据库中有一个传统的基于内存的锁管理器,在这样一个数据库中,对 一行锁定的过程一般如下: (1) 找到想锁定的那一行的地址。 (2) 在锁管理器中排队(锁管理器必须是串行化的,因为这是一个常见的内 存中的结构)。 (3) 锁定列表。 (4) 搜索列表,查看别人是否已经锁定了这一行。 (5) 在列表中创建一个新的条目,表明你已经锁定了这一行。 (6) 对列表解锁。 既然已经锁定了这一行,接下来就可以修改它了。之后,在你提交修改时, 必须继续这个过程,如下: (7) 再次排队。 (8) 锁住锁的列表。 (9) 在这个列表中搜索,并释放所有的锁。 (10) 对列表解锁。 可以看到,得到的锁越多,这个操作所花的时间就越多,修改数据前和修改 数据之后耗费的时间都会增加。Oracle 不是这样做的。Oracle 中的锁定过程如 下: (1) 找到想锁定的那一行的地址。 (2) 到达那一行。 (3) 锁定这一行(如果这一行已经锁定,则等待锁住它的事务结束,除非使 用了 NOWAIT 选项)。 仅此而已。由于闩为数据的一个属性,Oracle 不需要传统的锁管理器。事务 只是找到数据,如果数据还没有被锁定,则对其锁定。有意思的是,找到数据时, 它可能看上去被锁住了,但实际上并非如此。在 Oracle 中对数据行锁定时,行指 向事务 ID 的一个副本,事务 ID 存储在包含数据的块中,释放锁时,事务 ID 却会 保留下来。这个事务 ID 是事务所独有的,表示了回滚段号、槽和序列号。事务 I D 留在包含数据行的块上,可以告诉其他会话:你“拥有”这个数据(并非块上 的所有数据都是你的,只是你修改的那一行“归你所有”)。另一个会话到来时, 它会看到锁 ID,由于锁 ID 表示一个事务,所以可以很快地查看持有这个锁的事 务是否还是活动的。如果锁不活动,则允许会话访问这个数据。如果锁还是活动 的,会话就会要求一旦释放锁就得到通知。因此,这就有了一个排队机制:请求 锁的会话会排队,等待目前拥有锁的事务执行,然后得到数据。 以下是一个小例子,展示了这到底是怎么回事,这里使用了 3 个 V$ 表: ‰ V$TRANSACTION,对应每个活动事务都包含一个条目。 ‰ V$SESSION,显示已经登录的会话。 ‰ V$LOCK,对应持有所有 enqueue 队列锁以及正在等待锁的会话,都分别包含一个 条目。这并不是说,对于表中被会话锁定的每一行,这个视图中就有相应的一行。你 不会看到这种情况。如前所述,不存在行级锁的一个主列表。如果某个会话将 EMP 表中的一行锁定,V$LOCK 视图中就有对应这个会话的一行来指示这一事实。如果一 个会话锁定了 EMP 表中的数百万行,V$LOCK 视图中对应这个会话还是只有一行。 这个视图显示了各个会话有哪些队列锁。 首先启动一个事务(如果你没有 DEPT 表的一个副本,只需使用 CREATE TAB LE AS SELECT 来建立一个副本): 下面来看看此时系统的状态。这个例子假设是一个单用户系统;否则,在 V $TRANS ACTION 中可以看到多行。即使在一个单用户的系统中,如果看到 V$TRA NSACTION 中有多行也不要奇怪,因为许多后台 Oracle 进程可能也会执行事务。 这里有几点很有意思: ‰ $LOCK 表中的 LMODE 为 6,REQUEST 为 0。如果在 Oracle Server Reference 手册中查看 V$LOCK 表的定义,会发现 LMODE=6 是一个排他锁。请求(REQUE ST)值为 0 则意味着你没有发出请求;也就是说,你拥有这个锁。 ‰ 这个表中只有一行。V$LOCK 表更应算是一个队列表而不是一个锁表。许多人都 认为 V$LOCK 中会有 4 行,因为我们锁定了 4 行。不过,你要记住,Oracle 不会在 任何地方存储行级锁的列表(也就是说,不会为每一个被锁定的行维护一个主列表)。 要查看某一行是否被锁定,必须直接找到这一行。 ‰ 我选择了 ID1 和 ID2 列,并对它们执行了一些处理。Oracle 需要保存 3 个 16 位的 数,但是对此只有两个列。所以,第一个列 ID1 保存着其中两个数。通过用 trunc(i d1/power (2,16))rbs 除以 2^16,并用 bitand(id1,to_number('ffff','xxxx'))+0 slot 把高位 屏蔽,就能从这个数中找回隐藏的两个数。 ‰ RBS、SLOT 和 SEQ 值与 V$TRANSACTION 信息匹配。这就是我的事务 ID。 下面使用同样的用户名启动另一个会话,更新 EMP 中的某些行,并希望试图 更新 DEPT: 现在这个会话会阻塞。如果再次运行 V$查询,可以看到下面的结果: 这里可以看到开始了一个新的事务,事务 ID 是(5,34,1759)。这一次,这个 新会话(SID=144)在V$LOCK 中有两行。其中一行表示它所拥有的锁(LMODE=6)。 另外还有一行,显示了一个值为 6 的 REQUEST。这是一个对排他锁的请求。有意 思的是,这个请求行的 RBS/SLOT/SEQ 值正是锁持有者的事务 ID。SID=145 的事 务阻塞了 SID=144 的事务。只需执行 V$LOCK 的一个自联结,就可以更明确地看 出这一点: 现在,如果提交原来的事务(SID=145),并重新运行锁查询,可以看到请 求行不见了: 另一个会话一旦放弃锁,请求行就会消失。这个请求行就是排队机制。一旦 事务执行,数据库会唤醒被阻塞的会话。当然,利用各种 GUI 工具肯定能得到更 “好看”的显示,但是,必要时对你要查看的表有所了解还是非常有用的。 不过,我们还不能说自己已经很好地掌握了 Oracle 中行锁定是如何工作的,因 为还有最后一个主题需要说明:如何用数据本身来管理锁定和事务信息。这是块开 销的一部分。在第 9 章中,我们会详细分析块的格式,但是现在只需知道数据库块 的最前面有一个“开销”空间,这里会存放该块的一个事务表,了解这一点就足够 了。对于锁定了该块中某些数据的各个“实际”事务,在这个事务表中都有一个相 应的条目。这个结构的大小由创建对象时CREATE 语句上的两个物理属性参数决定: ‰ INITRANS:这个结构初始的预分配大小。对于索引和表,这个大小默认为 2(不 过我已经提出,Oracle SQL Reference 手册中与此有关的说明有问题)。 ‰ MAXTRANS:这个结构可以扩缩到的最大大小。它默认为 255,在实际中,最小 值为 2。在 Oracle 10g 中,这个设置已经废弃了 ,所以不再使用。这个版本中的 M AXTRANS 总是 255。 默认情况下,每个块最开始都有两个事务槽。一个块上同时的活动事务数受 MAXTRANS 值的约束,另外也受块上空间可用性的限制。如果没有足够的空间来 扩大这个结构,块上就无法得到 255 个并发事务。 我们可以创建一个具有受限 MAXTRANS 的表,来专门展示这是如何工作的。 为此,需要使用Oracle9i 或以前的版本,因为Oracle 10g 中会忽略 MAXTRANS。 在 Oracle 10g 中,只要块上的空间允许,即使设置了 MAXTRANS,Oracle 也会不 受约束地扩大事务表。在 Oracle9i 及以前的版本中,一旦块达到了 MAXTRANS 值,事务表就不会再扩大了,例如: 因此,我们有 24 行,而且经验证,它们都在同一个数据库块上。现在,在 一个会话中发出以下命令: 在另一个会话中,发出下面的命令: 最后,在第三个会话中,发出如下命令: 现在,由于这 3 行在同一个数据库块上,而且我们将 MAXTRANS(该块的最大 并发度)设置为 2,所以第 3 个会话会被阻塞。 注意 要记住,在 Oracle 10g 中,不会发生上例中出现的阻塞,不管怎样, MA XTRANS 都会设置为 255。在这个版本中,只有当块上没有足够的空间来扩 大事务表时,才会看到这种阻塞。 从这个例子可以看出,如果多个 MAXTRANS 事务试图同时访问同一个块时会 发生什么情况。类似地,如果 INITRANS 设置得很低,而且块上没有足够的空间 来动态地扩缩事务,也会出现阻塞。大多数情况下,INITRANS 的默认值 2 就足 够了,因为事务表会动态扩大(只要空间允许)。但是在某些环境中,可能需 要加大这个设置来提高并发性,并减少等待。比如,在频繁修改的表上就可能 要增加 INITRANS 设置,或者更常见的是,对于频繁修改的索引也可能需要这么 做,因为索引块中的行一般比表中的行多。你可能需要增加 PCTFREE(见第 10 章的讨论)或 INITRANS,从而在块上提前预留足够的空间以应付可能的并发事 务数。尤其是,如果你预料到块开始时几乎是满的(这说明块上没有空间来动 态扩缩事务结构),则更需要增加 PCTFREE 或 INITRANS。 2. TM (DML Enqueue)锁 TM 锁(TM lock)用于确保在修改表的内容时,表的结构不会改变。例如, 如果你已经更新了一个表,会得到这个表的一个 TM 锁。这会防止另一个用户在 该表上执行 DROP 或 ALTER 命令。如果你有表的一个TM 锁,而另一位用户试图在 这个表上执行 DDL,他就会得到以下错误消息: 初看上去,这是一条让人摸不着头脑的消息,因为根本没有办法在 DROP TA BLE 上指定 NOWAIT 或 WAIT。如果你要执行的操作将要阻塞,但是这个操作不允 许阻塞,总是会得到这样一条一般性的消息。前面已经看到,如果在一个锁定的 行上发出 SELECT FOR UPDATE NOWAIT 命令,也会得到同样的消息。 以下显示了这些锁在 V$LOCK 表中是什么样子: 尽管每个事务只能得到一个 TX 锁,但是 TM 锁则不同,修改了多少个对象, 就能得到多少个 TM 锁。在此,有意思的是,TM 锁的 ID1 列就是 DML 锁定对象的 对象 ID,所以,很容易发现哪个对象持有这个锁。 关于 TM 锁还有另外一个有意思的地方:系统中允许的 TM 锁总数可以由你来 配置(有关细节请见 Oracle Database Reference 手册中的 DML_LOCKS 参数定 义)。实际上,这个数可能设置为 0。但这并不是说你的数据库变成了一个只读 数据库(没有锁),而是说不允许 DDL。在非常专业的应用(如 RAC 实现)中, 这一点就很有用,可以减少实例内可能发生的协调次数。通过使用 ALTER TABLE TABLENAME DISABLE TABLE LOCK 命令,还可以逐对象地禁用 TM 锁。这是一种 快捷方法,可以使意外删除表的“难度更大”,因为在删除表之前,你必须重新 启用表锁。还能用它来检测由于外键未加索引而导致的全表锁(前面已经讨论 过)。 6.3.2DDL 锁 在 DDL 操作中会自动为对象加 DDL 锁(DDL Lock),从而保护这些对象不会 被其他会话所修改。例如,如果我执行一个 DDL 操作 ALTERTABLE T,表 T 上就 会加一个排他 DDL 锁,以防止其他会话得到这个表的 DDL 锁和 TM 锁。在 DDL 语 句执行期间会一直持有 DDL 锁,一旦操作执行就立即释放 DDL 锁。实际上,通常 会把 DDL 语句包装在隐式提交(或提交/回滚对)中来执行这些工作。由于这个 原因,在 Oracle 中 DDL 一定会提交。每条 CREATE、ALTER 等语句实际上都如下 执行(这里用伪代码来展示): 因此,DDL 总会提交(即使提交不成功也会如此)。DDL 一开始就提交,一 定要知道这一点。它首先提交,因此如果必须回滚,它不会回滚你的事务。如果 你执行了 DDL,它会使你所执行的所有未执行的工作成为永久性的,即使 DDL 不 成功也会如此。如果你需要执行 DDL,但是不想让它提交你现有的事务,就可以 使用一个自治事务(autonomous transaction)。 有 3 种类型的 DDL 锁: ‰ 排他 DDL 锁(Exclusive DDL lock):这会防止其他会话得到它们自己的 DDL 锁 或 TM(DML)锁。这说明,在 DDL 操作期间你可以查询一个表,但是无法以任何 方式修改这个表。 ‰ 共享 DDL 锁(Share DDL lock):这些锁会保护所引用对象的结构,使之不会 被其他会话修改,但是允许修改数据。 ‰ 可中断解析锁(Breakable parse locks):这些锁允许一个对象(如共享池中缓存 的一个查询计划)向另外某个对象注册其依赖性。如果在被依赖的对象上执行 DDL, Oracle 会查看已经对该对象注册了依赖性的对象列表,并使这些对象无效。因此, 这些锁是“可中断的”,它们不能防止 DDL 出现。 大多数 DDL 都带有一个排他 DDL 锁。如果发出如下一条语句: 在执行这条语句时,表 T 不能被别人修改。在此期间,可以使用 SELECT 查 询这个表,但是大多数其他操作都不允许执行,包括所有 DDL 语句。在 Oracle 中,现在有些 DDL 操作没有 DDL 锁也可以发生。例如,可以发出以下语句: ONLINE 关键字会改变具体建立索引的方法。Oracle 并不是加一个排他 DDL 锁 来防止数据修改,而只会试图得到表上的一个低级(mode 2)TM 锁。这会有效地 防止其他 DDL 发生,同时还允许 DML 正常进行。Oracle 执行这一“壮举”的做法 是,为 DDL 语句执行期间对表所做的修改维护一个记录,执行 CREATE 时再把这些 修改应用至新的索引。这样能大大增加数据的可用性。 另外一类 DDL 会获得共享 DDL 锁。在创建存储的编译对象(如过程和视图) 时,会对依赖的对象加这种共享 DDL 锁。例如,如果执行以下语句: 表 EMP 和 DEPT 上都会加共享 DDL 锁,而 CREATE VIEW 命令仍在处理。可以修改 这些表的内容,但是不能修改它们的结构。 最后一类 DDL 锁是可中断解析锁。你的会话解析一条语句时,对于该语句引 用的每一个对象都会加一个解析锁。加这些锁的目的是:如果以某种方式删除或 修改了一个被引用的对象,可以将共享池中已解析的缓存语句置为无效(刷新输 出)。 有一个意义非凡的视图可用于查看这个信息,即 DBA_DDL_LOCKS 视图。对此 没有相应的 V$视图。DBA_DDL_LOCKS 视图建立在更神秘的 X$表基础上,而且默 认情况下,你的数据库上不会安装这个视图。可以运行[ORACLE_HOME]/rdbms/a dmin 目录下的 catblock.sql 脚本来安装这个视图以及其他锁视图。必须作为用 户 SYS 来执行这个脚本才能成功。一旦执行了这个脚本,可以对视图运行一个查 询。例如,在一个单用户数据库中,我看到以下结果: 这些就是我的会话“锁定”的所有对象。我对一组 DBMS_*包加了可中断解析 锁。这是使用 SQL*Plus 的副作用;例如,它会调用 DBMS_APPLICATION_INFO。 可以看到不同的对象可能有不止一个副本,这是正常的,这只是表明,共享池中 有多个“事物”引用了这些对象。需要指出有意思的一点,在这个视图中,OWN ER 列不是锁的所有者;而是所锁定对象的所有者。正是由于这个原因,所以你 会看到多个 SYS 行。SYS 拥有这些包,但是它们都属于我的会话。 要看到一个实际的可中断解析锁,下面先创建并运行存储过程 P: 过程 P 现在会出现在 DBA_DDL_LOCKS 视图中。我们有这个过程的一个解析锁: 然后重新编译这个过程,并再次查询视图: 可以看到,现在这个视图中没有 P 了。我们的解析锁被中断了。 这个视图对开发人员很有用,发现测试或开发系统中某段代码无法编译时, 将会挂起并最终超时。这说明,有人正在使用这段代码(实际上在运行这段代码), 你可以使用这个视图来查看这个人是谁。对于 GRANTS 和对象的其他类型的 DDL 也是一样。例如,无法对正在运行的过程授予 EXECUTE 权限。可以使用同样的方 法来发现潜在的阻塞者和等待者。 6.3.3 闩 闩(latch)是轻量级的串行化设备,用于协调对共享数据结构、对象和文 件的多用户访问。 闩就是一种锁,设计为只保持极短的一段时间(例如,修改一个内存中数据结 构所需的时间)。闩用于保护某些内存结构,如数据库块缓冲区缓存或共享池中 的库缓存。一般会在内部以一种“愿意等待”(willing to wait)模式请求闩。 这说明,如果闩不可用,请求会话会睡眠很短的一段时间,并在以后再次尝试这 个操作。还可以采用一种“立即”(immediate)模式请求其他闩,这与 SELECT FOR UPDATE NOWAIT 的思想很相似,说明这个进程会做其他事情(如获取另一个 与之相当的空闲闩),而不只是坐而等待这个闩直到它可用。由于许多请求者可 能会同时等待一个闩,你会看到一些进程等待的时间比其他进程要长一些。闩的 分配相当随机,这要看运气好坏了。闩释放后,紧接着不论哪个会话请求闩都会 得到它。等待闩的会话不会排队,只是一大堆会话在不断地重试。 Oracle 使用诸如“测试和设置”(test and set)以及“比较和交换”(c ompare and swap)之类的原子指令来处理闩。由于设置和释放闩的指令是原子 性的,尽管可能有多个进程在同时请求它,但操作系统本身可以保证只有一个进 程能测试和设置闩。指令仅仅是一个指令而已,它执行得可能非常快。闩只保持 很短的时间,而且提供了一种清理机制,万一某个闩持有者在持有闩时异常地“死 掉了”,就能执行清理。这个清理过程由 PMON 执行。 队列锁(enqueue)在前面已经讨论过,这也是一种更复杂的串行化设备, 例如,在更新数据库表中的行时就会使用队列锁。与闩的区别在于,队列锁允许 请求者“排队”等待资源。对于闩请求,请求者会话会立即得到通知是否得到了 闩。而对于队列锁,请求者会话会阻塞,直至真正得到锁。 注意 使用 SELECT FOR UPDATE NOWAIT 或 WAIT [n],你还可以决定倘若 会话被阻塞,则并不等待一个队列锁,但是如果确实阻塞并等待,就会在一 个队列中等待。 因此,队列锁没有闩快,但是它确实能提供闩所没有的一些功能。可以在不 同级别上得到队列锁,因此可以有多个共享锁以及有不同程度共享性的锁。 1. 闩“自旋” 关于闩还要了解一点:闩是一种锁,锁是串行化设备,而串行化设备会妨碍 可扩缩性。如果你的目标是构建一个能在 Oracle 环境中很好地扩缩的应用,就 必须寻找合适的方法和解决方案,尽量减少所需执行的闩定的量。 有些活动尽管看上去很简单(如解析一条 SQL 语句),也会为共享池中的库 缓存和相关结构得到并释放数百个或数千个闩。如果我们有一个闩,可能会有另 外某个人在等待这个闩。而当我们想要得到一个闩时,也许我们自己也必须等待 (因为别人拥有着这个闩)。 等待闩可能是一个代价很高的操作。如果闩不是立即可用的,我们就得等待 (大多数情况下都是如此),在一台多 CPU 机器上,我们的会话就会自旋(spi n),也就是说,在循环中反复地尝试来得到闩。出现自旋的原因是,上下文切 换(context switching)的开销很大(上下文切换是指被“踢出”CPU,然后又 必须调度回 CPU)。所以,如果进程不能立即得到闩,我们就会一直呆在 CPU 上, 并立即再次尝试,而不是先睡眠,放弃CPU,等到必须调度回CPU 时才再次尝试。 之所以呆在 CPU 上,是因为我们指望闩的持有者正在另一个CPU 上忙于处理(由 于闩设计为只保持很短的时间,所以一般是这样),而且会很快放弃闩。如果出 现自旋并不断地尝试想得到闩,但是之后还是得不到闩,此时我们的进程才会睡 眠,或者让开 CPU,而让其他工作进行。得到闩的伪代码如下所示: 其逻辑是,尝试得到闩,如果失败,则递增未命中计数(miss count),这 个统计结果可以在 Statspack 报告中看到,或者直接查询V$LATCH 视图也可以看 到。一旦进程未命中,它就会循环一定的次数(有一个参数能控制这个次数,通 常设置为 2 000,但是这个参数在文档中未做说明),反复地试图得到闩。如果 某次尝试成功,它就会返回,我们能继续处理。如果所有尝试都失败了,这个进 程就会将该闩的睡眠计数(sleep count)递增,然后睡眠很短的一段时间。醒 来时,整个过程会再重复一遍。这说明,得到一个闩的开销不只是“测试和设置” 操作这么简单,我们尝试得到闩时,可能会耗费大量的 CPU 时间。系统看上去非 常忙(因为消耗了很多 CPU 时间),但是并没有做多少实际的工作。 2. 测量闩定共享资源的开销 举个例子,我们来研究闩定共享池的开销。我们会把一个编写得很好的程序 和一个编写得不太好的程序进行比较,前者使用了绑定变量,而在编写得不好的 程序中,每条语句使用了 SQL 直接量或各不相同的 SQL。为此,我们使用了一个 很小的 Java 程序,它只是登录 Oracle,关掉自动提交(所有 Java 程序在连接 数据库后紧接着都应这么做),并通过一个循环执行 25 000 条不同的 INSERT 语句。我们会执行两组测试:在第一组测试中,程序不使用绑定变量;在第二组 测试中,程序会使用绑定变量。 要评估这些程序以及它们在多用户环境中的行为,我喜欢用 Statspack 来收集 度量信息,如下: (1) 执行一个 Statspack 快照来收集系统的当前状态。 (2) 运行程序的 N 个副本,每个程序向其自己的数据库表中插入(INSERT), 以避免所有程序都试图向一个表中插入而产生的竞争。 (3) 在最后一个程序副本执行后,紧接着取另一个快照。 然后只需打印出 Statspack 报告,并查看完成 N 个程序副本需要多长时间, 使用了多少 CPU 时间,主要的等待事件是什么,等等。 这些测试在一台双 CPU 机器上执行,并启用了超线程(看上去就好像有 4 个 CPU)。给定两个物理 CPU,你可能以为能线性扩缩,也就是说,如果一个用户 使用了一个 CPU 单位来处理其插入,那么两个客户可能需要两个CPU 单位。你会 发现,这个假设尽管听上去好像是正确的,但可能并不正确(随后将会看到,不 正确的程度取决于你的编程水平)。如果所要执行的处理不需要共享资源,这么 说可能是正确的,但是我们的进程确实会使用一个共享资源,即共享池(Share d pool)。我们需要闩定共享池来解析 SQL 语句,为什么要闩定共享池呢?因为 这是一个共享数据结构,别人在读取这个共享资源时,我们不能对其进行修改, 另外如果别人正在修改它,我们就不能读取。 注意 我分别使用 Java、PL/SQL、Pro*C 和其他语言执行过这些测试。每一次 的最终结果基本上都一样。这里所展示和讨论的内容适用于所有语言和所 有数据库接口。这个例子之所以选择 Java,是因为我发现处理 Oracle 数据 库时,Java 和 Visual Basic 应用最有可能不使用绑定变量。 z 不使用绑定变量 在第一个实例中,我们的程序不使用绑定变量,而是使用串连接来插入数据: 我以“单用户”模式运行这个测试,Statspack 报告返回了以下信息: 这里加入了 SGA 配置以供参考,不过其中最重要的统计信息是: ‰ 耗用时间大约是 30 秒 ‰ 每秒有 807 次硬解析 ‰ 使用了 26 秒的 CPU 时间 现在,如果要同时运行这样的两个程序,你可能会认为硬解析会跃升至每秒 1 600 个(毕竟,我们有两个可用的 CPU),并认为 CPU 时间会加倍为大约 52 秒。下面看一下: 可以发现,硬解析数比预计的稍微多了一点,但是 CPU 时间是原来的 3 倍而 不是两倍!怎么会这样呢?答案就在于 Oracle 的闩定实现。在这台多 CPU 机器 上,无法立即得到一个闩时,我们就会“自旋”。自旋行为本身会消耗 CPU 时间。 进程 1 多次尝试想要得到共享池的闩,但最终只是一再地发现进程2 持有着这个 闩,所以进程 1 必须自旋并等待(这会消耗 CPU 时间)。反过来对进程 2 也是一 样,通过多次尝试,它发现进程 1 正持有着所需资源的闩。所以,很多处理时间 都没有花在正事上,只是在等待某个资源可用。如果把Statspack 报告向下翻页 到“Latch Sleep Breakdown”报告部分,可以发现: 注意到这里 SLEEPS 列怎么出现了一个 406 呢?这个 406 对应于前面“Top 5 Timed Events”报告中报告的等待数。这个报告显示了自旋循环中尝试得到闩 并且失败的次数。这说明,“Top 5”报告只是显示了闩定问题的冰山一角,而 没有给出共有 229 537 次未命中这一信息(这说明我们尝试得到闩时陷入了自 旋)。尽管这里存在一个严重的硬解析问题,但是分析“Top 5”报告后,我们 可能想不到:“这里有一个硬解析问题”。为了完成两个单位的工作,这里需要 使用 3 个单位的 CPU 时间。其原因就在于:我们需要一个共享资源(即共享池), 这正是闩定的本质所在。不过,除非我们知道闩定实现的原理,否则可能很难诊 断与闩定相关的问题。简单地看一下 Statspack 报告,从“Top 5”部分我们可 能会漏掉这样一个事实:此时存在很糟糕的扩缩问题。只有更深入地研究 Stats pack 报告的闩定部分才会发现这个问题。 另外,由于存在这种自旋,通常不可能确定系统使用多少 CPU 时间,从这 个两用户的测试所能知道的只是:我们使用了 74 秒的 CPU 时间,而且力图得 到共享池闩时未命中次数共有 229 537 次。我们不知道每次得不到闩时要尝 试多少次 ,所以没有具体的办法来度量有多少 CPU 时间花在自旋上,而有多 少 CPU 时间用于处理。要得到这个信息,我们需要多个数据点。 在我们的测试中,由于有一个单用户例子可以对照比较,因此可以得出结论: 大约 22 秒的 CPU 时间花费在闩的自旋上,这些CPU 时间白白浪费在等待资源上。 z 使用了绑定变量 现在来看与上一节相同的情况,不过,这一次使用的程序在处理时使用的闩 要少得多。还是用原来的 Java 程序,但把它重写为要使用绑定变量。为此,把 Statement 改为 PreparedStatement,解析一条INSERT 语句,然后在循环中反复 绑定并执行这个 PreparedStatement: 与前面“不使用绑定变量”的例子一样,下面来看所生成的单用户情况和两 个用户情况下的 Statspack 报告。可以看到这里有显著的差别。以下是单用户情 况下的报告: 差别确实很大,不使用绑定变量的例子中需要 26 秒的 CPU 时间,现在只需 4 秒。原来每秒钟有 807 次硬解析,现在仅为每秒 0.14 次。甚至耗用时间也从 45 秒大幅下降到 8 秒。没有使用绑定变量时,我们的CPU 时间中有 5/6 的时间都用 于解析 SQL。这并非都是闩导致的,因为没有使用绑定变量时,解析和优化 SQL 也需要许多 CPU 时间。解析 SQL 是 CPU 密集型操作(需要耗费大量 CPU 时间), 不过如果大幅增加 CPU 时间,但其中5/6 的 CPU 时间都只是用来执行我们并不需 要的解析,而不是做对我们有用的事情,这个代价实在太昂贵了。 再来看两个用户情况下的测试,结果看上去更好: CPU 时间大约是单用户测试用例所报告 CPU 时间的 2~2.5 倍。 注意 由于取整,4 秒的 CPU 时间实际上是指 3.5~4.49 秒之间,11 实际上表示 10.5~11.49 秒。 另外,使用绑定变量时,与不使用绑定变量的一个用户所需的CPU 时间相比, 两个用户使用的 CPU 时间还不到前者的一半!查看这个Statspack 报告中的闩部 分时,我发现,如果使用了绑定变量,则根本没有闩等待,对共享池和库缓存的 竞争太少了,所以甚至没有相关的报告。实际上,再进一步挖掘还可以发现,使 用绑定变量时,两用户测试中请求共享池闩的次数是 50 367 次,而在前面不使 用绑定变量的两用户测试中,请求次数超过 1 000 000 次。 z 性能/可扩缩性比较 表 6-1 总结了随着用户数的增加(超过 2 个),各个实现所用的 CPU 时间以 及相应的闩定结果。可以看到,随着用户负载的增加,使用较少闩的方案能更好 地扩缩。 表 6-1 使用和不使用绑定变量时 CPU 使用情况的比较 用户数 CPU 时间(秒) /耗用时间(分钟) 共享池闩请求 对闩等待的度量 (等待数/等待时间(秒)) 不使用绑定变量 使用绑定变量 不使用绑定变量 使用绑定变量 不使用绑定变量 使用绑定变量 1 26/0.52 4/0.10 563 883 25 232 0/0 2 74/0.78 11/0.20 1 126 006 50 367 406/1 3 155/1.13 29/0.37 1 712 280 75 541 2 830/4 4 272/1.50 44/0.45 2 298 179 100 682 9 400/5 5 370/2.03 64/0.62 2 920 219 125 933 13 800/20 6 466/2.58 74/0.72 3 526 704 150 957 30 800/80 17/0 7 564/3.15 95/0.92 4 172 492 176 085 40 800/154 8 664/3.57 106/1.00 4 734 793 201 351 56 300/240 120/1 9 747/4.05 117/1.15 5 360 188 230 516 74 600/374 230/1 10 822/4.42 137/1.30 5 901 981 251 434 60 000/450 354/1 对我来说,我观察到很有意思的一点,如果 10 个用户使用绑定变量(所以 闩请求很少),使用的硬件资源与不使用绑定变量的 2~2.5 个用户(也就是说, 过量使用了闩,或者执行了本不需要的处理)所需的硬件资源相当。检查 10 个 用户的结果时,可以看到,倘若未使用绑定变量,与使用了绑定变量的方案相比, 所需的 CPU 时间是后者的 6 倍,执行时间也是后者的3.4 倍。随着增加更多的用 户,每个用户等待闩所花费的时间就更长。当有 5 个用户时,对闩的等待时间平 均为 4 秒/会话,等到有 10 个用户时,平均等待时间就是 45 秒/会话。不过,如 果采用了能避免过量使用闩的实现,则用户规模的扩大不会带来不好的影响。 6.3.4 手动锁定和用户定义锁 到此为止,前面主要了解了 Oracle 为我们所加的锁,这些锁定工作对我们 来说都是透明的。更新一个表时,Oracle 会为它加一个 TM 锁,以防止其他会话 删除这个表(实际上,也会防止其他会话对这个表执行大多数 DDL)。在我们修 改的各个块上会加上 TX 锁,这样就能告诉别人哪些数据是“我们的”。数据库 采用 DDL 锁来保护对象,这样当我们正在修改这些对象时,别人不会同时对它们 进行修改。数据库在内部使用了闩和锁(lock)来保护自己的结构。 接下来,我们来看看如何介入这种锁定活动。有以下选择: ‰ 通过一条 SQL 语句手动地锁定数据。 ‰ 通过 DBMS_LOCK 包创建我们自己的锁。 在后面的小节中,我们将简要地讨论这样做的目的。 1. 手动锁定 我们可能想使用手动锁定(manual locking),实际上,前面已经见过这样 的几种情况了。SELECT...FOR UPDATE 语句就是手动锁定数据的一种主要方法。 在前面的例子中,曾经用过这个语句来避免丢失更新问题(也就是一个会话可能 覆盖另一个会话所做的修改)。我们已经看到,可以用这种方法来串行访问详细 记录,从而执行业务规则(例如,第 1 章中的资源调度程序示例)。 还可以使用 LOCK TABLE 语句手动地锁定数据。这个语句实际上很少使用, 因为锁的粒度太大。它只是锁定表,而不是对表中的行锁定。如果你开始修改行, 它们也会被正常地“锁定”。所以,这种方法不能节省资源(但在其他 RDBMS 中可以用这个方法节省资源)。如果你在编写一个大批量的更新,它会影响给定 表中的大多数行,而且你希望保证没有人能“阻塞”你,就可以使用 LOCK TABL E IN EXCLUSIVE MODE 语句。通过以这种方式锁定表,就能确保你的更新能够执 行所有工作,而不会被其他事务所阻塞。不过,有 LOCK TABLE 语句的应用确实 很少见。 2. 创建你自己的锁 通过 DBMS_LOCK 包,Oracle实际上向开发人员公开了它在内部使用的队列锁 (enqueue lock)机制。你可能会奇怪,为什么想创建你自己的锁呢?答案通常 与应用有关。例如,你可能要使用这个包对 Oracle 外部的一些资源进行串行访 问。假设你在使用UTL_FILE 例程,它允许你写至服务器文件系统上的一个文件。 你可能已经开发了一个通用的消息例程,每个应用都能调用这个例程来记录消 息。由于这个文件是外部的,Oracle 不会对试图同时修改这个文件的多个用户 进行协调。现在,由于有了 DBMS_LOCK 包,在你打开、写入和关闭文件之前,可 以采用排他模式请求一个锁(以文件命名),一次只能有一个人向这个文件写消 息。这样所有人都会排队。通过利用 DBMS_LOCK 包,等你用完了锁之后能手动地 释放这个锁,或者在你提交时自动放弃这个锁,甚至也可以在你登录期间一 直保持这个锁。 6.4 小结 到此为止,前面主要了解了 Oracle 为我们所加的锁,这些锁定工作对我们 来说都是透明的。更新一个表时,Oracle 会为它加一个 TM 锁,以防止其他会话 删除这个表(实际上,也会防止其他会话对这个表执行大多数 DDL)。在我们修 改的各个块上会加上 TX 锁,这样就能告诉别人哪些数据是“我们的”。数据库 采用 DDL 锁来保护对象,这样当我们正在修改这些对象时,别人不会同时对它们 进行修改。数据库在内部使用了闩和锁(lock)来保护自己的结构。

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档

相关文档