• 《持续交付:发布可靠软件的系统方法》- 读书笔记(二)


    第 2 章 配置管理

    2.1 引言

    配置管理是一个被广泛使用的名词,往往作为版本控制的同义词。为了陈述清晰起见,在这里我们给出本书中对配置管理的定义:

    配置管理是指一个过程,通过该过程,所有与项目相关的产物,以及它们之间的关系都被唯一定义、修改、存储和检索。

    配置管理策略将决定如何管理项目中发生的一切变化。因此,它记录了你的系统以及应用程序的演进过程。另外,它也是对团队成员协作方式的管理。作为配置管理策略的一个结果,虽然第二点至关重要,但常常被忽视。

    虽然版本控制系统是配置管理中最显而易见的工具(团队规模再小,也应该使用版本控制系统),但决定使用一个版本控制工具仅仅是制定配置管理策略的第一步而已。

    假如项目中有良好的配置管理策略,那么你对下列所有问题的回答都应该是“YES”。

    • 你能否完全再现你所需要的任何环境(这里的环境包括操作系统的版本及其补丁级别、网络配置、软件组合,以及部署在其上的软件应用及其配置)?
    • 你能很轻松地对上述内容进行增量式修改,并将修改部署到任意一种或所有环境中吗?
    • 你能否很容易地看到已被部署到某个具体环境中的某次修改,并能追溯到修改源,知道是谁做的修改,什么时候做的修改吗?
    • 你能满足所有必须遵守的规程章则吗?
    • 是否每个团队成员都能很容易地得到他们所需要的信息,并进行必要的修改呢?这个配置管理策略是否会妨碍高效交付,导致周期时间增加,反馈减少呢?

    最后这一点非常重要。因为我们常常遇到这样的情况:配置管理策略完全满足前面四个要点,但这恰恰成了团队间协作的一个巨大障碍。事实上,如果我们能够给予配置管理策略足够的重视,那么最后一点与其他四点之间是可以不对立的。我们不可能在本章中解决所有这些问题,但当你读完这本书后,问题的答案就显而易见了。在本章中,我们将讨论三个问题。

    • (1)为管理应用程序的构建、部署、测试和发布过程做好准备。我们从两个方面解决这个问题:对所有内容进行版本控制;管理依赖关系
    • (2)管理应用软件的配置信息。
    • (3)整个环境的配置管理,这包括应用程序所依赖的软件、硬件和基础设施。另外还有环境管理背后的原则,包括操作系统、应用服务器、数据库和其他COTS(商业现货)软件

    2.2 使用版本控制

    版本控制系统(也称为源代码控制管理系统或修订控制系统)是保存文件多个版本的一种机制。当修改某个文件后,你仍旧可以访问该文件之前的任意一个修订版本。它也是我们共同合作交付软件时所使用的一种机制。

    一般来说,包括SubversionMercurialGit在内的开源工具就可以满足绝大多数团队的需求。我们会花更多的时间来探讨版本控制系统和它们的使用模式包括分支与合并

    本质上来讲,版本控制系统的目的有两个。

    • 首先,它要保留每个文件的所有版本的历史信息,并使之易于查找。这种系统还提供一种基于元数据(这些元数据用于描述数据的存储信息)的访问方式,使元数据与某个单个文件或文件集合相链接。
    • 其次,它让分布式团队(无论是空间上不在一起,还是不同的时区)可以愉快地协作。

    那么,为什么要这样做呢?理由可能很多,但最关键的是它能回答下面这些问题。

    • 对于我们开发的应用软件,某个特定的版本是由哪些文件和配置组成的?如何再现一份与生产环境一模一样的软硬件环境?
    • 什么时候修改了什么内容,是谁修改的,以及为什么要修改?因此,我们很容易知道应用软件在何时出了错,出错的过程,甚至出错的原因。

    这是版本控制的基本原理和根本目的。现在,大多数项目都使用版本控制系统。

    下面是我们对高效使用版本控制系统的几点建议

    2.2.1 对所有内容进行版本控制

    我们所讨论的有关加快发布周期和提高软件质量的所有实践,从持续集成、自动化测试,到一键式部署,都依赖于下面这个前提:与项目相关的所有东西都在版本控制库中。除了存储源代码和配置信息,很多项目还将其应用服务器、编译器、虚拟机以及其他相关工具的二进制镜像也放在版本控制库中。只要能从版本控制库中取出所需要的一切,就能保证为开发、测试,甚至生产环境提供一个稳定的平台。

    这种策略在控制和行为保障方面建立了基础。对于在这种严格配置管理策略约束下的系统来说,根本不存在整个流程的后期还会出错的可能性。

    2.2.2 频繁提交代码到主干

    使用版本控制时,有两点需要牢记在心。

    • 首先,只有频繁提交代码,你才能享受版本控制所带来的众多好处,比如能够轻松地回滚到最近某个无错误的版本。
    • 其次,一旦将变更提交到版本控制中,那么团队的所有人都能看到这些变更,也能签出它。而且,如果使用了持续集成(像我们推荐的那样),你所做的修改还会触发一次构建,本次构建很有可能会最终进入验收测试,甚至被部署到生产环境。

    在一些团队中,这种限制很可能导致开发人员需要几天甚至几个星期才能提交一次代码。这种很长时间才提交的做法是有问题的。除非是第14章提到的那三种例外情况。在这一点上有一些争议,尤其是在使用ClearCase以及相似工具的用户中。我们认为,这种方法存在以下几个问题。

    • 它违背了持续集成的宗旨,因为创建分支的做法推迟了新功能的整合,只有当该分支被合并时才可能发现集成问题。
    • 如果多个开发者同时分别创建了多个分支,问题会成指数增加,而合并过程也会极其复杂。
    • 尽管有一些好用的工具有自动合并功能,但它们无法解决语义冲突。例如,某人在一个分支上重命名了一个方法,而另一个人在另一分支上对该方法增加了一次调用。
    • 它让重构代码库变得非常困难,因为分支往往涉及多个文件,会让合并变得更加困难

    一个更好的解决方案是尽量使用增量方式开发新功能,并频繁且有规律地向版本控制系统提交代码。这会让软件能一直保持在集成以后的可工作状态。

    为了确保提交代码时不破坏已有的应用程序,有两个实践非常有效。

    • 一是在提交代码之前运行测试套件。
    • 二是增量式引入变化。我们建议每完成一个小功能或一次重构之后就提交代码。
    2.2.3 使用意义明显的提交注释

    每个版本管理工具都提供“写注释功能”。但这些注释很容易被忽视,而且很多人习惯于忽略它。写描述性提交注释的最重要原因在于:当构建失败以后,你知道是谁破坏了构建,以及他为什么破坏了构建。

    我们喜欢的一种注释风格是这样的:第一段是简短的总结性描述,接下来的几段描述更多的细节。简短的总结性描述怎么写呢?它就像是报纸的标题一样,要给读者足够的信息,以便让读者知道是否还需要继续读下去。

    这个注释中还应该包括一个链接,可以链接到项目管理工具中的一个功能或缺陷,从而知道为什么要修改这段代码。

    2.3 依赖管理

    在软件项目中,最常见的外部依赖就是其使用的第三方库文件,以及该软件需要用到的正由其他团队开发的模块或组件间的关系。库一般是以二进制文件的形式部署,不会被你自己的团队修改,而且也不经常更新。然而,组件和模块会被其他团队频繁修改。在这里,我们只讨论依赖管理中的几个关键点,因为它会影响配置管理。

    2.3.1 外部库文件管理

    外部库文件通常是以二进制形式存在,除非你使用的是解释型语言。即使是解释型语言,外部库文件也通常会安装在全局系统路径中,并由包管理系统来管理,比如Ruby的Gems和Perl的modules。

    对于“是否将这些库文件放到版本控制库中”这个问题,业界还有一些争议。例如,Maven(Java的一种构建工具)允许指定应用程序所依赖的jar文件,并会从因特网上的代码库下载(如果有本地缓存库的话,也可以从本地缓存中取得)。

    是否一定要把外部依赖库文件放在版本控制库中呢?其实,放与不放,各有利弊。如果放了,那我们更容易将软件的版本与正确的库文件版本相关联,但它也可能使源代码库的体积更大,并且签出时间也会变长。

    2.3.2 组件管理

    将整个应用软件分成一系列的组件进行开发(小型应用除外)是个不错的实践。这能让某些变更的影响范围比较小,从而减少回归缺陷。另外,它还有利于重用,使大项目的开发过程更加高效。

    2.4 软件配置管理

    作为关键部件之一,配置信息与产品代码及其数据共同组成了应用程序。软件在构建、部署和运行时,我们可以通过配置信息来改变它的行为。交付团队需要认真考虑设置哪些配置项,在应用的整个生命周期中如何管理它们,以及如何确保这些配置项在多个应用、多个组件以及多项技术中的管理保持一致性。我们认为,应该以对待代码的方式来对待你的系统配置,使其受到正确的管理和测试。

    2.4.1 配置与灵活性

    每个人都希望使用的软件非常灵活。为什么不呢?可是,灵活性也是有代价的。

    对于软件灵活性的期望常常导致一种反模式,即“终极配置”,而这种反模式常被表述为对一个软件项目的需求。如果做得好,它没有什么坏处,但是如果搞不好的话,它会毁了一个项目。

    现代计算机语言已经采用各种各样的特性和技术来帮助减少错误。在大多数情况下,配置信息却无法使用它们,甚至这些配置的正确性在测试环境和生产环境中也根本无法得到验证。我们认为,对部署活动的冒烟测试(参见5.3.3节)就是一种缓解配置验证问题的方法,我们应始终使用它。

    2.4.2 配置的分类

    我们可以在构建、部署、测试和发布过程中的任何一点进行配置信息的设置。而且,我们也的确会在多个时间点对应用软件进行相关的配置,如下所示。

    • 在生成二进制文件时,构建脚本可以在构建时引入相关的配置,并将其写入新生成的二进制文件。
    • 打包时将配置信息一同打包到软件中,比如在创建程序集,以及打包ear或gem时。
    • 安装部署软件程序时,部署脚本或安装程序可以获取必要的配置信息,或者直接要求用户输入这些配置信息。
    • 软件在启动或运行时可获取配置

    一般来说,我们并不赞同在构建或打包时就将配置信息植入的做法,而是应使用相同二进制安装包向所有的环境中部署,以确保这个发布的软件就是那个被测试过的软件。根据这一个原则,我们可以推出:在相临的两次部署之间,任何变更都应该作为配置项被捕获和记录,而不应该在编译或打包时植入。

    2.4.3 应用程序的配置管理

    在管理应用程序的配置这个问题上,需要回答三个问题。

    • (1) 如何描述配置信息?
    • (2) 部署脚本如何存取这些配置信息?
    • (3) 在环境、应用程序,以及应用程序各版本之间,每个配置信息有什么不同?

    通常配置信息以键值对的形式来表示。有时可使用系统提供的配置类型来有层次地组织这些配置项。比如Windows 属性文件的键值字符串就是以不同的heading来组织的,而YAML文件在Ruby领域非常流行,Java中的属性文件虽然在格式上相对简单,但在大多数情况下还是能够提供足够灵活性的。将配置信息以XML文件的形式来保存可以对其复杂性起到较好的限制效果。

    将应用软件的配置信息保存在哪里呢?显而易见的选择包括数据库、版本控制库、文件目录或注册表等。版本控制库可能是最容易的,只要将配置文件签入就可以了,而且你可以随时拿到任意时间点上的历史配置信息。像源代码一样,将配置选项列表也保存在同一个代码库中是非常值得的。

    将那些特定于测试环境或生产环境的实际配置信息存放于与源代码分离的单独代码库中通常是非常必要的。因为这些信息与源代码的变更频率是不同的。不过,当使用这种方法时,需要注意:配置信息的版本一定要与相应的应用软件的版本相匹配。这种分离方式特别有利于重要信息的安全性,对于这些重要信息(如密码和数字证书等)的存取需要施加限制。

    数据库、文件目录和注册表是比较方便存储配置信息的场所,它们可以被远程访问。但是,为了审计性和可回滚性,一定要将配置项的修改历史保留下来。你可以通过某种系统自动地实现这一功能,也可以让版本控制系统充当这一角色,写一个脚本,根据需要将适当版本的配置信息加载到数据库或文件目录中

    1. 获取配置信息
      无论配置信息是什么样的存储形态,我们建议使用一个简单的Facade类,让它提供与下面类似的接口:

      getThisProperty() 
      getThatProperty() 
      
      • 1
      • 2

      将应用的技术细节与外界相隔离,这样就可以在测试代码中模拟它,并在需要时改变其存储机制。

    2. 为配置项建模
      每个配置都是一个元组,所以应用程序的配置信息由一系列的元组构成。然而,这些元组及其值取决于三方面,即应用程序、该应用程序的版本、该版本所运行的环境。无论你使用哪种方式来存储配置信息,放在源代码控制中的XML文件也好,或REST式Web服务中也好,都要能够满足不同的要求。下面列举了一些在对配置信息建模时需要考虑的用例。

      • 新增一个环境(比如一个新的开发工作站,或性能测试环境)。在这种情况下你要能为这个配置应用的新环境指定一套新的配置信息。
      • 创建应用程序的一个新版本,通常需要添加一些配置设置,删除一些过时的配置设置。此时应该确保在部署新版本时,可以使用新的配置设置,但是一旦需要回滚时,还能够使用旧版本的配置设置。
      • 将新版本从一个环境迁移到另一个环境,比如从测试环境挪到试运行环境。此时应该确保新环境上的新配置项都有效,而且为其设置了正确的值。
      • 重定向到一个数据库服务器。应该只需要简单地修改所有配置设置,就能让它指向新的数据库服务器。
      • 通过虚拟化技术管理环境。应该能够使用虚拟技术管理工具创建某种指定的环境,并且配置好所有的虚拟机。你也许需要将这种虚拟环境中的配置信息作为某特定版本的应用软件在虚拟环境中的标准配置信息。

      在不同环境之间管理配置信息的一种方法是,把预期的生产环境中的配置信息作为默认配置,而在其他环境中,通过适当的方式覆盖这些默认值(确保你有预防措施,以防生产环境受到配置失误的影响)。

    3. 系统配置的测试
      与应用程序和构建脚本一样,配置设置也需要测试。对于系统配置测试来说,包括以下两部分。

      • 一是要保证配置设置中对外部服务的引用是良好的。比如,作为部署脚本的一部分,我们要确保消息总线(messaging bus)在配置信息中所指定的地址已启动并运行,并确保应用程序所用的模拟订单执行服务在功能测试环境中能够正常工作。最起码,要保证能够与所有的外部服务相连通。如果应用程序所依赖的任何部分没有准备好,部署或安装脚本都应该报错,这相当于配置设置的冒烟测试。
      • 二是当应用程序一旦安装好,就要在其上运行一些冒烟测试,以验证它运行正常。对于系统配置的测试,我们只要测试与配置有关的功能就可以了。在理想情况下,一旦测试结果与预期不符,这些测试应该能够自动停止软件的运行,并显示安装或部署失败。
    2.4.4 跨应用的配置管理

    在大中型组织中,通常会同时管理很多应用程序,而软件配置管理的复杂性也会大大增加。这类组织中一般都会有遗留系统,而且很可能某个遗留系统的配置项让人很难搞得清楚明白。这种情况下,最重要的任务之一就是,要为每个应用程序维护一份所有配置选项的索引表,记录这些配置保存在什么地方,它们的生命周期是多长,以及如何修改它们

    如果可能的话,运行每个应用程序的构建脚本时应该自动生成一份这类信息。即使无法做到这一点,也要把它记录在Wiki上,或其他文档管理系统中。我们的目的是:系统运维团队可以通过生产系统的监控平台了解每个软件应用的配置信息,并能看到每种环境中所运行的软件到底是哪一个版本。像Nagios、OpenNMS和惠普的OpenView都提供了记录这类信息的功能。

    如果应用程序之间有依赖关系,部署有先后次序的话,实时存取配置信息的能力就特别重要。很容易因配置信息设置不当而浪费很多时间,甚至导致整套服务无法正常运行,而这类问题是极难诊断的。

    每个应用程序的配置项管理都应该作为项目启动阶段的一个议题,纳入计划当中。需要分析当前的运维环境中其他应用程序是如何管理配置信息的,考虑在新开发的应用中是否能够使用相同的配置管理方法。

    2.4.5 管理配置信息的原则

    我们要把应用程序的配置信息当做代码一样看待,恰当地管理它,并对它进行测试。当创建应用程序的配置信息时,应考虑以下几个方面

    • 在应用程序的生命周期中,我们应该在什么时候注入哪类配置信息。是在打包的时候,还是在部署或安装的时候?是在软件启动时,还是在运行时?要与系统运维和支持团队一同讨论,看看他们有什么样的需求。
    • 将应用程序的配置项与源代码保存在同一个存储库中,但要把配置项的保存在别处。另外,配置设置与代码的生命周期完全不同,而像用户密码这类的敏感信息就不应该放到版本控制库中。
    • 应该总是通过自动化的过程将配置项从保存配置信息的存储库中取出并设置好,这样就能很容易地掌握不同环境中的配置信息了。
    • 配置系统应该能依据应用、应用软件的版本、将要部署的环境,为打包、安装以及部署脚本提供不同的配置值。每个人都应该能够非常容易地看到当前软件的某个特定版本部署到各种环境上的具体配置信息。
    • 对每个配置项都应用明确的命名习惯,避免使用晦涩难懂的名称,使其他人不需要说明手册就能明白这些配置项的含义。
    • 确保配置信息是模块化且封闭的,使得对某处配置项的修改不会影响到那些与其无关的配置项。
    • DRY(Don’t Repeat Yourself )原则。定义好配置中的每个元素,使每个配置元素在整个系统中都是唯一的,其含义绝不与其他元素重叠。
    • 最少化,即配置信息应尽可能简单且集中。除非有要求或必须使用,否则不要新增配置项。
    • 避免对配置信息的过分设计,应尽可能简单。
    • 确保测试已覆盖到部署或安装时的配置操作。检查应用程序所依赖的其他服务是否有效,使用冒烟测试来诊断依赖于配置项的相关功能是否都能正常工作

    2.5 环境管理

    没有哪个应用程序是孤岛。每个应用程序都依赖于硬件、软件、基础设施以及外部系统才能正常工作。

    在做应用程序的环境管理时,我们需要记住的原则是:

    • 环境的配置和应用程序的配置同样重要。
    • 操作系统的配置也同样重要。

    这里把不良环境管理可能带来的问题总结如下。

    • 配置信息的集合非常大;
    • 一丁点变化就能让整个应用坏掉,或者严重降低它的性能。
    • 一旦系统出现问题,需要资深人员花费不确定的时间来找到问题根源并修复它。
    • 很难准确地再现那些手工配置的环境,因此给测试验证带来很大困难。
    • 很难维护一个不使用配置信息的环境,因此维护这种环境下的行为也很难,尤其是不同的节点有不同的配置时。

    环境管理的关键在于通过一个全自动过程来创建环境,使创建全新的环境总是要比修复已受损的旧环境容易得多。重现环境的能力是非常必要的,原因如下。

    • 可以避免知识遗失问题。比如某人离职且无法与他联系上,但只有他明白某个配置项所代表的意思。一旦这类配置项不能正常工作,通常都意味着较长的停机时间。这是一个很大却不必要的风险。
    • 修复某个环境可能需要花费数小时的时间。所以,我们最好能在可预见的时间里重建环境,并将它恢复到某个已知的正常状态下。
    • 创建一个和生产环境相同的测试环境是非常必要的。对于软件配置而言,测试环境应该和生产环境一模一样。这样,配置问题更容易被在早期发现。

    需要考虑的环境配置信息如下:

    • 环境中各种各样的操作系统,包括其版本、补丁级别以及配置设置;
    • 应用程序所依赖的需要安装到每个环境中的软件包,以及这些软件包的具体版本和配置;
    • 应用程序正常工作所必需的网络拓扑结构;
    • 应用程序所依赖的所有外部服务,以及这些服务的版本和配置信息;
    • 现有的数据以及其他相关信息(比如生产数据库)。

    其实高效配置管理策略的两个基本原则是:

    • (1) 将二进制文件与配置信息分离;
    • (2) 将所有的配置信息保存在一处。

    如果应用了这两个基本原则,你就能将“在系统不停机的情况下,创建新环境、升级系统部分功能或增加新的配置项”等工作变成一个
    简单的自动化过程。

    所有这些都需要考虑。尽管把操作系统也提交到版本控制库中的做法显然不合理,但这并不意味着将它的配置信息提交到版本控制库中不合理。远程安装系统与环境管理工具(如Puppet 、CfEngine)的结合使用让我们可以直接对操作系统进行集中管理和配置。

    对于大多数应用来说,将这些原则应用于其所依赖的第三方软件更为重要。好的软件应该有一个能通过命令行执行的安装程序且不需要任何用户干预。应用程序的配置可以通过版本控制系统来管理,而且不需要手工干预。如果第三方软件依赖无法满足这样的要求,你就要设法找到替代品。使用第三方软件时,这应该是一个重要的评估依据。当评估第三方产品或服务时,应该问自己如下问题。

    • 我们可以自行部署它吗?
    • 我们能对它的配置做有效的版本控制吗?
    • 如何使它适应我们的自动化部署策略?

    我们要将处于某个正确部署状态的环境作为配置管理中的一个基线。自动化环境准备系统应该能够从项目部署的历史中找到任一特定基线进行重建。只要对应用程序所在环境的任何配置做修改,就应该把这个修改保存起来,并创建一个新的基线版本,将此时的应用程序版本与这个基线版本关联在一起。这样就可以保证下次部署应用程序或创建新环境时,这些修改也会被包含在内。

    实际上,你应该像对待源代码一样对待环境,增量式地修改,并将修改提交到版本控制库中。对每个修改都要进行测试,以确保它不会破坏在这个新版本的环境中运行的应用程序。

    2.5.1 环境管理的工具

    在以自动化方式管理操作系统配置的工具中,Puppet和CfEngine是两个代表。使用这些工具,你能以声明方式来定义一些事情,如哪些用户可以登录你的服务器,应该安装什么软件,而这些定义可以保存在版本控制库中。运行在系统中的代理(agent)会从版本控制库中取出最新的配置,更新操作系统以及安装在其之上的软件。对于应用了这些工具的系统来说,根本没必要登录到服务器上去操作,所有的修改都可能通过版本控制系统来发起,因而你也能够得到每次变化的完整记录,即谁在什么时候做了什么样的修改。

    虚拟化技术也可以提高环境管理过程的效率。不必利用自动化过程从无到有地创建一个新环境,你可以轻易地得到一份环境副本,并把它作为一个基线保存起来。这样一来,创建新环境也就是小事一桩,点一下按钮就可以搞定。虚拟化技术还有其他好处,比如它可以整合硬件,使硬件平台标准化,即使你的应用程序需要一些不同的环境也没有问题

    2.5.2 变更过程管理

    最后要强调的是,对环境的变更过程进行管理是必要的。应该严格控制生产环境,未经组织内部正式的变更管理过程,任何人不得对其进行修改。这么做的原因很简单:即便很微小的变化也可能把环境破坏掉。任何变更在上线之前都必须经过测试,因而要将其编成脚本,放在版本控制系统中。这样,一旦该修改被认可,就可以通过自动化的方式将其放在生产环境中。

    这样,对于环境的修改和对软件的修改就没什么分别了。它也和应用程序的代码一样,需要经历构建、部署、测试和发布整个过程。

    在这方面,应该像对待生产环境一样对待测试环境。测试环境所需的核准流程通常会简单一些,应该由管理测试环境的人来控制。但在其他方面,其配置管理应该与生产环境中的配置管理没什么不同。

    2.6 小结

    配置管理是本书其他内容的基础。没有配置管理,根本谈不上持续集成、发布管理以及部署流水线。它对交付团队内部的协作也会起到巨大的促进作用。我们希望读者清楚地认识到,这不只是选择和使用什么样工具的问题,尽管这非常重要,但更重要的是,如何正确地使用最佳实践

    如果配置管理流程比较好的话,对于下面的问题,你的回答都应该是肯定的

    • 是否仅依靠保存于版本控制系统中的数据(除了生产数据),就可以从无到有重建生产系统?
    • 是否可以将应用程序回滚到以前某个正确的状态下?
    • 是否能确保在测试、试运行和正式上线时以同样的方式创建部署环境?

    如果回答是否定的,那么你的组织正处于风险之中。我们建议为下面的内容制定出一个保存基线和控制变更的策略:

    • 应用程序的源代码、构建脚本、测试、文档、需求、数据库脚本、代码库以及配置文件;
    • 用于开发、测试和运维的工具集;
    • 用于开发、测试和生产运行的所有环境;
    • 与应用程序相关的整个软件栈,包括二进制代码及相关配置;
    • 在应用程序的整个生产周期(包括构建、部署、测试以及运维)的任意一种环境上,与该应用程序相关联的配置。
  • 相关阅读:
    【Selenium】提高测试&爬虫效率:Selenium与多线程的完美结合
    Matlab数组操作进阶:扩维与构造
    Java笔记七(封装,继承与多态)
    Document类型【2】
    串口控制小车电机转动+蓝牙长按控制
    D00242疫情防控
    网络原理
    【附源码】计算机毕业设计SSM商品推荐系统
    多版本并发控制 MVCC
    SELECT DISTINCT not in 改为使用 JOIN 操作
  • 原文地址:https://blog.csdn.net/u010230019/article/details/133788807