计算复杂度与并发冲突对事务处理性能的耦合影响分析
本文所讨论的方案，来源于笔者在 UrnaDB 数据库设计过程中的问题抽象与实践总结。事务抽象事务 Transaction 是数据库系统中的基础机制，Transaction 用于保证数据操作时的正确性与可靠性而提出的一组核心约束。事务必须具备的 ACID 四个核心特性，ACID 的缩写缩写源自于：原子性 Atomicity、一致性 Consistency、隔离性 Isolation 和持久性 Durability。ACID 约束确保了一组数据库操作，即使在运算故障时也能保持数据的一致性和可靠性，实现数据从一个合法状态向另一个合法状态的转换，使其执行运算过程中没有异常状态的产生。目前工业级主流数据库产品 MySQL、PostgreSQL、Redis、MongoDB、DynamoDB 都有对 Transaction 特性的支持，但具体实现细节有着天壤之别。同样按照数据库类型分类：关系型数据库如 PostgreSQL 的事务实现通常基于 SQL 标准并完整支持 ACID 特性；而 NoSQL 数据库如 Redis、DynamoDB 的事务机制则没有统一标准，通常根据系统设计在原子性、隔离性等方面提供不同级别的支持，部分系统甚至仅支持单操作原子性。关系型数据库的 SQL 语言表达能力强大，SQL 可以通过复杂的 JOIN、子查询、聚合函数、窗口函数等表达复杂的业务逻辑，一条 SQL 语句就能完成多表关联、分组统计、排序分页等操作。使用者就需要花费大量的时间来学习和掌握使用 SQL 语法包括复杂的子查询、窗口函数、索引原理、执行计划分析、事务隔离级别、锁机制、MVCC 原理、查询优化技巧、性能调优、高可用架构等。[scode type="blue"]关系型 SQL 数据库在技术实现层面同样存在较高门槛，其核心组件如 SQL 解析器、查询优化器和执行引擎的设计与实现复杂度极高，不仅需要长期的工程投入，还伴随着持续的维护成本，这种复杂性需要多年经验和多学科知识才能胜任，对于小规模团队或独立开发者而言，通常是不现实的选择。[/scode]NoSQL 相较于关系型 SQL 数据库在实现层面通常更加简化，通过放弃通用 SQL 标准，转而采用自定义 API 来定义数据访问与事务语义。NoSQL 系统无需实现 SQL 解析器、查询优化器以及执行引擎等复杂核心组件，从而显著降低了系统实现的复杂度，这使得 NoSQL 数据库对于小规模团队或独立开发者而言更加可行。[scode type="blue"]NoSQL 也有相应的学习成本，每换一款新的 NoSQL 数据库，使用者就得重新学习对应的新 API 来操作数据库。例如 Redis 和 MongoDB 数据库产品的设计还需要使用者去学习 Lua、JavaScript 语言，引入特定语言的设计会为使用者增加额外负担和学习成本。[/scode]🛡️ 安全性本节将以数据库 “安全性” 为切入点，对比 UrnaDB 与其他竞品数据库在相同安全问题下的设计思路与解决方案。传统 SQL 关系型数据库在生产环境中，安全层面的风险是不可忽略的要素，其中 SQL 注入作为一种典型的应用层安全漏洞，长期以来是开发者需要重点防范的问题之一，由于缺乏内建防护机制，使用者需要在应用层额外处理，例如在 Java 中通过 Prepared Statement 对 SQL 语句进行预编译。同样 NoSQL 数据库也存在安全层面的风险，例如 Redis 通过内置 Lua VM 虚拟机来实现事务逻辑或扩展能力如脚本执行、存储过程等，虽然增强了灵活性，但也带来了额外的复杂性和潜在的安全风险。[scode type="blue"]Redis 在运行期间支持通过 SDK 或 API 动态加载并执行用户自定义的 Lua 脚本，这依赖于内置 Lua VM 提供脚本执行能力。该设计在增强可编程性的同时，也对执行环境的隔离性与资源控制提出了更高的要求，如果宿主机（这里是指 Redis 数据库）缺乏严格的沙箱机制与执行限制，可能引发诸如沙箱逃逸、长时间阻塞、如死循环，以及内存滥用等问题。[/scode]下文将通过具体恶意代码示例，分析这些潜在的安全风险，如下：-- 死循环攻击 while true do redis.call('GET', 'key') end -- 内存炸弹 local t = {} for i=1,10000000 do t[i] = string.rep("x", 1000000) end -- DoS 攻击 for i=1,1000000 do redis.call('KEYS', '*') end上述任意代码，如果被恶意用户提交到 Redis 的 Lua VM 中执行，一定会导致系统行为不可预测，从而影响整体稳定性与调试难度，例如脚本中的异常逻辑如死循环或大量内存分配会耗尽数据库硬件资源，使 Lua VM 长时间被占用，进而阻塞其他脚本或相关操作的正常执行。例如在 Redis 配置文件中的参数，可以设置 Lua VM 执行自定义脚本的超时时间：lua-time-limit 5000 # 默认 5 秒[scode type="yellow"]Redis 并未提供真正的执行隔离，而是借助单线程模型、执行时间限制和受控 API，将 Lua 脚本带来的风险由不可控的系统级崩溃，转化为可干预的阻塞行为。Redis 受限于早期架构，引入了内置 Lua VM 以增强事务处理的灵活性；而 UrnaDB 参考 Redis 这些问题之后，在设计之初即放弃此类执行模型，直接放弃内置 VM 执行方案，从源头减少可受攻击面，将相关风险降至最低。[/scode]另外一个问题是 Redis Lua 原子事务无回滚机制，在一组事务计算过程中出现意外中断，无法正常回滚到这组数据原始状态，一个实际案例如下：-- 场景：转账操作 EVAL " redis.call('DECRBY', 'account:A', 100) -- ✅ 成功：A 扣款 -- 这里发生错误（比如 B 账户不存在） redis.call('INCRBY', 'account:B', 100) -- ❌ 失败 return 'done' " 2 account:A account:B[scode type="yellow"]事务计算过程中异常中断之后，数据出现不一致性：[/scode]A 账户：-100. ✅ 已执行 => B 账户：失败. ❌ 未执行[scode type="yellow"]上面案例最终结果是数据不一致，A 账户的钱凭空消失了，B 账户却没有增加金额！按正常的业务逻辑 SQL 事务设计，事务计算是要符合 ACID 原则的，一组数据操作要么成功要么失败，不会出现其他状态；Redis Lua 只适合 可以容忍部分失败 的场景，不适合金融转账这种严格要求 ACID 的场景。[/scode]下面是一组 UrnaDB 与 Redis 事务完备性的数据对比表：操作类型是否幂等Redis LuaUrnaDBSET✅ 是✅ 安全✅ 安全DEL✅ 是✅ 安全✅ 安全Lua❌ 否❌ 危险✅ 安全INCR❌ 否❌ 危险✅ 安全LPUSH❌ 否❌ 危险✅ 安全通过上述数据对比可以看出，UrnaDB 在事务设计上优于 Redis 的设计。⏱️ 高性能首先需要明确这里的 “高性能” 并非泛指 UrnaDB 的整体性能，而是聚焦于其在高并发场景下处理海量事务时的吞吐能力。数据库事务的处理性能不仅受磁盘 I/O、CPU 和 RAM 等硬件因素影响，还与数据库系统本身内部的设计密切相关，本节将忽略外部环境因素，重点从 UrnaDB 的数据内结构设计以及事务管理器、和锁机制出发，探讨其对 UrnaDB 事务吞吐能力的影响。[scode type="blue"]事务的执行时间具有不可预测性，而多个事务在并发执行过程中，可能对同一热点数据产生竞争访问。由于执行顺序的不确定性，系统无法依赖物理时间来保证结果的一致性。因此数据库必须引入并发控制机制，在异步执行环境下对数据访问进行协调，以确保最终结果满足可串行化等一致性约束。[/scode]数据库通常通过内置的锁管理器来协调并发事务对共享数据的访问，以避免数据竞争并保证一致性，UrnaDB 数据库系统中多个事务在并发数据竞争示意图：[scode type="yellow"]数据库的事务锁的粒度如行级、页级或表级是影响事务处理性能的关键因素，它在并发度与锁管理开销之间形成权衡：粒度越细，并发性越高但管理成本越大；粒度越粗，管理成本较低但冲突概率更高。所以影响数据库的事务并发吞吐量关键要素是锁的持有时间，锁的持有时间与其访问的数据规模和执行复杂度密切相关，长事务会延长锁的占用周期，导致其他事务在竞争相同资源时发生阻塞，在高并发负载下，锁竞争会形成放大效应，使系统吞吐量下降，并成为限制事务处理能力的关键瓶颈。[/scode]UrnaDB 中的批量原子事务操作是针对的 Table 命名空间中的数据记录所设计。在之前的 API 交互 文档中有提到 Table 命名空间在初始化完成之后会自动分配一个全局锁。在默认单个原子事务请求下，UrnaDB 内部的事务处理器会使用本次事务请求中所涉及到的 Table 对应锁进行并发数据竞争的保护，单个事务处理的示意图如下：[scode type="blue"] 在该数据结构与锁管理策略下，即使 Table 命名空间规模达到百万级，系统仍具备良好的并发扩展能力，由于事务仅对访问的 Table 加锁，而活跃事务数通常远小于 Table 空间规模，因此锁竞争概率较低，从而提升整体吞吐性能。但是在当事务涉及多个 Table 命名空间的交集运算时，基于 Table 的细粒度锁机制不再具备设计之初的并发特性和高事务吞吐量。[/scode]但在涉及到多个 Table 批量事务处理时候上述的设计就不适用了，跨多 Table 事务在执行过程中，需要同时获取并持有多个 Table 命名空间对应的锁，从而扩大锁作用域，锁范围的扩展会提高锁冲突概率，降低并发执行效率，并最终影响系统整体吞吐性能，多事务锁冲突示意图如下：[scode type="yellow"] 并发冲突的本质在于事务之间访问数据集的重叠程度，由此可以推导出数据库系统的并发性能在很大程度上取决于事务访问数据集的重叠程度，而减少访问范围能够有效降低冲突概率。基于这一原则，主流数据库系统在并发控制策略上进行了大量优化，例如 MySQL 和 PostgreSQL 采用 MVCC 以降低读写之间的互斥，而 Redis 则通过 Compare-And-Swap 乐观并发机制减少锁竞争，从而提升整体吞吐量。[/scode]UrnaDB 的批量事务操作也参考了传统 SQL 数据库的 MVCC 的设计，在默认批量事务请求处理下也是使用的多个数据版本快照的方式供事务执行运算操作。多个并行处理的事务之间操作的数据都是 Snapshot 副本，每个事务的运算都是基于 Snapshot 快照副本，从而实现了无事务锁干预来提升数据库整体事务吞吐量，多个事务 MVCC 处理示意图如下：[scode type="green"]UrnaDB 的事务处理器基于 MVCC 管理并发事务访问的数据版本，对于同时执行的多个事务，当其访问的数据存在重叠时，系统会在提交阶段根据事务的提交顺序进行冲突检测，并决定是合并变更还是回滚冲突事务。并发优化的核心在于降低事务之间的数据访问重叠，让多个事务之间 “尽量少碰同一批数据” ，MVCC 通过为每个事务维护独立的数据版本，使事务能够在各自的版本视图上执行操作，从而实现读写解耦，减少锁竞争并提升系统整体并发性能。当数据规模 N 远大于并发事务数量 T 时，即数学公式为 𝑁 ≫ 𝑇 成立时，事务访问集合之间的交集概率显著降低，系统冲突率趋于较低水平，在此条件下 MVCC 模型能够有效提升系统并发能力，从而提高整体事务吞吐量。[/scode]需要注意的是在 MVCC 模式下，事务处理并不能保证每个事务都能够成功提交，当多个事务对同一数据记录产生写冲突时，系统会在提交阶段进行冲突检测并回滚冲突事务。在需要强一致性的批量事务场景下，UrnaDB 提供类似传统关系型数据库 Serialization 模式中两阶段锁 2PL 的事务处理模式，事务串行化示意图如下：[scode type="yellow"]Serialization 适用于对批量原子事务具有强一致性要求的场景，然而由于其执行过程中会放大锁粒度，不建议在单个事务中操作过多 Table 命名空间，实践中通常将数量控制在 5 个以内，否则将显著增加锁竞争与阻塞时间，降低系统整体并发性能。[/scode]⚛️ 原子事务前文从安全性与性能角度对不同类型数据库的设计优缺点进行了分析，在此基础上本节将进一步介绍 UrnaDB 的 Transaction 事务功能及其使用方法。对于用户而言，只需掌握相关 API 的使用方式，无需关注底层事务机制的实现细节，与其他事务相关 API 一致，UrnaDB 的 Transaction 事务操作同样基于 PQL 协议 实现。[scode type="green"]在 UrnaDB 中批量原子事务操作被抽象为一组 Mutations 结构，由多个 Mutation 组成。每个 Mutations 用于定义本次事务需要操作的数据集合，其操作类型包括 INSERT、UPDATE 和 REMOVE 类型，这些类型分别对应着单个原子事务的操作类型。[/scode]例如下面展示的是一组 Mutations 结构请求的 JSON Body 抽象表示：{ "mutations": [ { "name": "users", "operation": "INSERT", "values": { "id": 1, "name": "Leon Ding", "age": 25 } }, { "name": "users", "operation": "UPDATE", "where": { "id": 1 }, "values": { "age": 26 } } ], "serialization": false }[scode type="blue"]Mutations 通过对多个原子操作进行封装与组合，对外提供批量原子事务的抽象，使多个操作在逻辑上表现为一个整体事务。[/scode]使用 UrnaDB 批量原子事务时，客户端只需向 https://.../txns 端点发送 HTTP 协议的 POST 请求，服务器端收到事务请求之后即可执行批量事务操作，需要注意的是事务中涉及的 Table 命名空间必须已提前创建；下面是使用 HTTP 客户端软件发送批量原子事务请求的示例：curl -X POST "http://192.168.101.252:2668/txns" \ -H "Auth-Token: yv2PH82JXfm2UpScAQK37iJVI" \ -H "Content-Type: application/json" \ -d '{ "mutations": [ { "name": "users", "operation": "INSERT", "values": { "id": 1, "name": "Leon Ding", "age": 25 } }, { "name": "users", "operation": "UPDATE", "where": { "id": 1 }, "values": { "age": 26 } } ], "serialization": false }'[scode type="yellow"]上述请求会对名为 users 的 Table 命名空间执行批量原子操作，类似于关系数据库中的 INSERT 和 UPDATE 组合事务语句，需要注意的是 serialization 字段的值，如果不传入 serialization 字段，UrnaDB 事务处理器默认会以值为 false 进行处理。此处的 serialization 对应前文提到的关系型数据库中的事务串行化模式，但在使用方式上更加灵活，关系型数据库通常需要在配置中统一设置事务隔离级别，而 UrnaDB 则通过在每次事务请求中指定 serialization 字段，动态决定是否启用串行化模式。[/scode]批量原子事务操作 JSON Body 请求中的 operation 字段用于标识当前 Mutation 的操作类型，不同类型对应不同的数据结构变更需求，客户端必须根据操作类型提供相应字段，否则事务请求将被拒绝；不同 Operation 类型说明表格如下：Operation描述必填字段说明INSERT插入数据name, values向指定 Table 写入一条或多条记录UPDATE更新数据name, where, values根据条件更新符合条件的数据REMOVE删除数据name, where根据条件删除数据目前批量原子事务仅支持上述表格中列出的 Operation 类型，后续将逐步扩展更多复杂条件数据筛选能力，例如范围查询以及基于 <= 、>= 、!= 的条件过滤。🆚 产品差异性功能维度传统 SQL 数据库UrnaDBSQL 注入⚠️ 高风险✅ 无风险性能可预测❌ 不可预测✅ 可预测查询优化⚠️ 复杂✅ 无需优化，一级索引和可选择二级索引事务隔离⚠️ 4 种级别，复杂✅ MVCC 简单，基于版本的冲突检测解析开销⚠️ 10-70ms✅ ≈ 0.3ms，简单的 JSON 解析器锁竞争⚠️ 严重✅ MVCC 无锁读，可 2PL 锁升级串行化Schema 变更⚠️ 锁表✅ 无 Schema，动态修改 Schema 结构维护成本⚠️ 需要 DBA 团队✅ 低维护量，一个人就能胜任扩展性⚠️ 困难✅ 简单，按需通过配置启用插件功能学习曲线⚠️ 陡峭✅ 平缓，导入对应语言 SDK 即可使用功能维度UrnaDBRedisACID 支持✅ 完整支持❌ 不支持，Lua 事务有被攻击风险原子性✅ 真正的原子性❌ 无回滚机制隔离性✅ MVCC + 可选 2PL 模式❌ 单线程串行事务持久性✅ WAL + .txn 文件⚠️ 依赖 AOF + RDB事务回滚机制✅ 自动回滚❌ 无回滚事务并发控制✅ MVCC 乐观锁❌ 单线程无并发事务版本检测✅ 自动版本检测和回滚⚠️ 手动 WATCH 丢弃执行事务快照隔离✅ 自动快照隔离❌ 需手动读取跨多 Key 事务✅ 支持⚠️ Lua 脚本异常会导致数据不一致事务实现✅ 真正的事务⚠️ 串行执行数据压缩✅ 支持⚠️ 需配置数据加密✅ 静态加密❌ 需第三方垃圾回收✅ 自动磁盘 GC⚠️ 内存淘汰策略分布式锁✅ 原生支持⚠️ 需 Redlock 算法强一致性✅ ACID 保证❌ 不适合订单系统✅ 事务支持❌ 不适合持久化方式✅ 基于 LogStructuredFS 实时⚠️ RDB + AOF默认持久化✅ 始终持久化❌ 默认仅内存数据安全性✅ 高，每次写入持久化⚠️ 中，可能丢失数据
NoSQL 的抽象数据库系统的核心作用在于高效地存储和管理数据，并支持从海量信息中快速检索所需记录。传统的 SQL 关系型数据库在数据规模持续增长的情况下，容易遭遇单机性能瓶颈。目前提升性能的主流做法多依赖垂直扩展增强单机硬件能力，而非水平扩展增加节点以线性扩容。由于其架构限制，关系型数据库在弹性扩展性方面存在障碍，难以充分适应多节点分布式环境。同时为满足 ACID 事务特性，这类系统在支持分布式事务处理时也面临一定的技术挑战。相比之下，NoSQL 数据库更适用于大规模数据存储和高吞吐、低延迟的应用场景。由于 NoSQL 不受固定数据结构和关系的限制，具有更灵活的扩展能力，并且支持节点的动态扩展，从而更好地适应分布式环境。无论是关系型 SQL 数据库还是 NoSQL 数据库，它们都有一个共同的核心组件存储引擎，这是数据库设计与实现的关键核心部分。目前数据库存储引擎可以分为两大类，一类是基于内存的 （In-Memory）实现，例如 Redis 和 Memcached 这类的将数据全部存储在内存中；另外一种则是类似于 MySQL 的 InnoDB 和 LevelDB 这种将数据存储在磁盘中。两者都有各自的优势和劣势，基于内存的存储引擎通常存储的数据量较少，但其访问速度往往比基于磁盘的存储引擎快好几个数量级，而基于磁盘的存储引擎访问速度较慢，但存储数据量要比内存引擎多几个数量级。通常内存的价格要比磁盘的贵很多，这也是在选择存储硬件时要考虑的成本问题。存储引擎的开发者需要权衡读写性能和硬件成本的同时，重点在于如何高效存储数据和检索数据。数据库的存储引擎相当于在操作系统文件系统之上，建立的一套用于数据组织、存储与查询的逻辑管理层，最终实现是依赖于操作系统的文件系统 API 接口的，最终数据是被持久化到磁盘文件中保存，这个就对应着 ACID 中的 Durability 数据持久性。持久性（Durability）是存储引擎最为至关重要的功能实现，目前很多基于内存版本的 NoSQL 数据库例如 Memcached 和 Redis 在这方面做的就比较差，在 NoSQL 服务器运行过程中突然崩溃断电就会导致数据没有被持久化存储的情况，从而导致数据丢失影响到上层的业务程序。[scode type="yellow"]所谓的持久性就是如何将计算机运算的结果持久化存储在一些物理介质中；持久性是指系统能够将状态变更的结果可靠保存，使其在系统重启或故障后仍然能够恢复。 [/scode]存储是现代计算机最核心的功能之一，计算机中的程序和文件以数据块的形式保存在各种存储介质中，主要可分为持久化存储和非持久化存储两类。非持久化存储通常指内存（如 RAM），其特点是在断电或关机后，数据会被清除。尽管容量通常小于持久化存储，但其高速读写性能使其广泛应用于程序运行过程中的数据处理和临时缓存。相比之下，持久化存储如硬盘、固态硬盘、DVD、磁带等，能够在断电后保留数据，适用于长期保存程序和文件信息。鉴于数据可能存储在多种物理介质（如磁盘、SSD、光盘等）上，主流的操作系统需要提供一致的抽象接口，以屏蔽底层硬件差异，使用户能够高效、统一地进行文件的读写、创建与删除等操作。当前主流操作系统均实现了完整的文件系统，开发者可以通过如 POSIX 这样的标准 API 接口来访问和管理存储设备，而无需关心具体设备的物理实现方式。尽管操作系统的文件子系统通过标准如 POSIX 提供了对存储设备的访问能力但仍存在一些限制；例如存储性能受限于设备的物理读写速度，同时文件系统通常采用页缓存 Page Cache 或写缓冲机制，这意味着数据写入操作并不会立即持久化到磁盘，一旦系统发生故障或意外断电，可能会导致数据丢失，影响系统的可靠性。基础架构综上所述，针对这些场景的问题 UrnaDB 采用了基于 Log-Structured Megre Tree 日志结构化文件系统的存储引擎实现，存储引擎会以 Append-Only Log 的方式将所有的数据操作写入到数据文件中。同时 UrnaDB 为了高速查询检索数据记录，存储引擎会将数据记录索引信息全部保存在内存中，从而实现高效快速的查询目标数据记录。这样的设计的好处是能以磁盘最大写入性能进行写入数据，并且还能减少读取磁盘索引所需要的时间，通过一次索引定位来读取数据记录，写入和查询流程图：其核心持久化机制基于预写日志 Write-Ahead Logging 简称 WAL ，在对数据执行任何操作前，都会先将操作记录写入 WAL 日志文件。WAL 文件不仅承担持久化的角色，也作为主要的数据存储载体。在数据库进程崩溃后，只需从 WAL 日志中顺序读取各条 Segment 记录，即可高效恢复内存中的索引结构。这是一种 Log-Structured Megre Tree 的变体实现，LSM-Tree 在正常写入情况下会将最近一段时间的热点数据存储在内存中有序数据结构实现的 MemTable 里。这样设计的好处是使用内存作为存储能提升写入速度，但会占用大量内存。并且还需保障在突然断电的情况下 MemTable 数据全丢的情况，业界统一的解决方案是对 MemTable 结构进行数据写入之前，执行 Write-Ahead Logging 操作，以便崩溃能从 WAL 重新构建出 MemTable 的数据。 在 UrnaDB 中对这些 WAL 数据文件有一个统称为叫 Region 文件，这些文件有单个固定大小限制，当一个数据文件写满之后就会被关闭，会重新创建一个新的 Active 活跃的 Region 文件进行数据记录的进行写入。被关闭的 Region 文件会被视为冷数据文件，随着数据库不间断长时间运行 Region 文件会不断递增逐渐占用磁盘空间。此时数据库进程就需要对旧的 Region 文件执行压缩 Compaction 和定期清理，以降低存储压力并提高查崩溃数据恢复时的启动效率，压缩流程原理图：目前工业级的存储系统中，RaikDB 是一个采用类似模型实现的数据库产品，RaikDB 是基于 Amazon Dynamo 论文的设计理念构建而成。其 RaikDB 底层也是采用的顺序写入的日志式存储引擎，并将其命名为 Bitcask 存储模型，以提升写入性能和读写效率。该模型与 UrnaDB 所采用的存储引擎在设计理念上颇为相似。Bitcask 存储引擎在执行数据压缩 Compaction 时会生成 Hint 文件，其作用是在存储引擎重启时，辅助快速重建内存索引，从而避免每次启动都需全量扫描数据文件，显著提升系统的启动效率。然而 Hint 文件仅在 Compaction 过程中生成，所记录的只是当时内存索引的快照，无法反映实时状态。针对这一问题 UrnaDB 在存储引擎中进行了改进，将 Hint 文件机制替换为 Checkpoint 功能，系统会在固定时间间隔内，定期将当前内存索引持久化为 checkpoint 文件写入磁盘。当服务器进程发生异常崩溃，也可通过读取 checkpoint 快照快速恢复内存索引状态。在实际恢复流程中，UrnaDB 会优先加载最近一次的 checkpoint 快照，并从该时间点之后的 Region 文件中继续回放增量数据。通过这种方式，有效提升了系统启动恢复的速度，减少了启动耗时，同时兼顾了数据一致性与运行效率。在该存储引擎设计的基础上，UrnaDB 构建了内置的 HTTP 服务器，并引入了多种抽象化的数据结构层。整体架构如下图所示：系统采用模块化设计，各模块以包结构组织并协同工作，覆盖底层存储、核心逻辑与服务接口，构成一个完整的单机版 NoSQL 数据库，实现了分层解耦与一体化集成。检查点技术由于 UrnaDB 数据库存储引擎采用将所有索引数据存储在内存中的设计，一旦数据库进程意外崩溃，内存中的索引信息将会丢失，未能持久化到磁盘。这样在下一次启动时，系统需要重新扫描所有数据文件，以重建内存中的索引结构，如果数据文件体量较大，将导致启动过程变得缓慢，启动耗时较长。Checkpoint 功能机制会通过定期将内存中的索引状态保存到磁盘，在发生异常重启时即可快速加载 checkpoint 文件，从而大幅缩短数据库的启动时间。开启 checkpoint 功能可以在数据库崩溃后显著加快启动恢复的速度。然而 checkpoint 文件的生成时间间隔设置同样需要谨慎权衡。如果生成间隔过短，将导致 checkpoint 文件过于频繁地写入磁盘，占用大量 I/O 资源，可能影响数据库的整体性能；而如果间隔设置过长，崩溃恢复时仍需扫描大量 Region 数据记录，反而无法发挥 checkpoint 的优势，导致启动时间较长。因此合理配置 checkpoint 的生成频率，需在性能开销与数据恢复效率之间做出平衡，建议根据实际业务的数据写入频率和系统负载，动态调整该参数以获得最佳效果。磁盘垃圾回收器由于 UrnaDB 存储引擎采用顺序日志写入结构，随着系统运行时间的增长，每个 Region 区域对应的磁盘数据文件会不断增大。默认情况下 Region 文件中的每条数据记录未经过压缩，导致磁盘空间占用随数据量增加而持续上升。为了解决这一问题 UrnaDB 存储引擎内置了数据压缩算法，启用该压缩器功能后，可以显著减少磁盘文件的存储体积，从而有效降低存储成本。在配置 Region 区域的垃圾回收功能时，需要综合考虑多种因素。若不启用该功能，随着系统长时间运行，磁盘空间占用将持续增长，甚至可能过高，影响系统稳定性。若启用垃圾回收，则必须合理设定其执行周期：周期过长：会导致空间回收不及时，磁盘占用上升，同时在压缩 Region 时耗时较长。周期过短：会导致垃圾回收过于频繁，增加系统资源开销，进而影响存储引擎的整体性能。在 UrnaDB 中垃圾回收器的执行周期同样依赖于 Time 触发，并且其执行过程会占用一定的系统资源。如果采用并行执行机制，这段时间对外部应用基本无感，系统可持续对外提供服务。这种设计理念与许多现代编程语言的垃圾回收器类似，UrnaDB 的 Region 区域划分为 Active Region 和 Read Region ，其架构与 Java 虚拟机（JVM）中的垃圾回收机制具有相似性。当垃圾回收开始运行时，它不会对 Active Region 中的活跃数据进行操作，而是仅针对旧的 Read Region 区域执行垃圾分析与清理，从而避免对当前读写路径造成干扰，不会出现类似于编程语言虚拟机中的 Stop-The-World 世界时钟停止问题，减少压缩磁盘空间的抖动，保障数据库稳定性。时间就是一个问题在整个 NoSQL 数据库的设计过程中，许多复杂问题的根源其实都与物理世界中的 Time 有关。当前的 UrnaDB 实现为单机版本，因此主要面临的是本地磁盘 I/O 和线程调度带来的延迟问题。然而一旦在未来引入多个存储节点，发展为分布式架构，就必须应对网络延迟、时钟同步、事务一致性等与时间密切相关的挑战。因此无论是当前的 NoSQL 单机实现，还是面向未来的分布式扩展，如何合理建模和管理时间 Time 维度，始终是数据库架构设计中的核心研究问题之一。在单机版本中还面临另一个挑战如何为数据记录提供过期时间（Time-To-Live）支持，尽管 TTL 是常见的需求，但其实现往往较为复杂，目前主流 NoSQL 数据库普遍采用惰性删除机制，即仅在数据被访问时才判断其是否过期并删除，这种方式依赖于外部读取请求的触发，可能导致已过期数据长期滞留在存储中。数据文件采用 WAL（Write-Ahead Logging）机制实现持久化，但 WAL 本身仍需通过操作系统文件系统 API 写入磁盘。现代操作系统通常基于 Page Cache 和写缓冲进行 I/O 加速，因此普通的 write 操作并不会立即落盘，而是先进入内核缓存，若在此期间发生掉电，仍可能丢失尚未刷新的数据。UrnaDB 运行在用户态无法从根本上绕过操作系统的 Page Cache 机制，只能通过 fsync/O_DSYNC 等接口请求内核将数据强制写入物理持久介质中。在通过一些代码测试中不同持久化策略下写入性能存在很大差异性，差异对比表格：写入模式性能 (ops/sec)性能下降数据安全性刷盘依赖普通写入~100,000基准依赖 OS 缓存OS 自动定期 sync~50,00050%中等应用控制O_DSYNC~1,00099%最高每次写入mmap~100,000基准依赖 OS 缓存OS 自动mmap+msync~1,00099%最高应用控制所以无论是 write() 还是 mmap 内存文件映射，只要不主动调用同步操作sync/msync 都是依赖 OS 的页面缓存刷盘策略。对于 UrnaDB 用户而言，更推荐的优化方式是从操作系统层面调整相关参数，这才是更为根本和可靠的解决方案。通过调整 Page Cache 的刷写策略，可降低数据在内核缓存中滞留的时间，从而减少断电或崩溃场景下的数据丢失风险，以下为关键的操作系统可调优的参数示例：# 减少脏页比例，更频繁地触发后台刷盘 echo 5 > /proc/sys/vm/dirty_background_ratio # 默认10%，改为5% echo 10 > /proc/sys/vm/dirty_ratio # 默认20%，改为10% # 缩短脏页存活时间 echo 500 > /proc/sys/vm/dirty_expire_centisecs # 默认3000(30秒)，改为5秒 echo 100 > /proc/sys/vm/dirty_writeback_centisecs # 默认500(5秒)，改为1秒[scode type="yellow"]通过调整 Linux Page Cache 相关参数，可缩短脏页滞留时间并提升后台刷写频率，从而减小 WAL 数据因掉电而丢失的时间窗口。核心思路是：降低触发阈值、加快刷写节奏，使数据尽可能提前落盘，在断电时能使用备用电源和其他机制来完成数据持久化。[/scode]在 UrnaDB 的运行过程中，如果发生异常宕机，可能会导致部分数据处于未完成写入的状态，从而出现数据记录的不一致性。在某些极端情况下会出现检查点和索引文件同时损毁情况，在这种情况下如果从这些半损毁文件中恢复数据，可能导致记录不一致甚至造成二次损坏。索引文件和检查点文件损毁的原因较为多样，其中一种典型场景出现在 Docker 容器化部署 UrnaDB 时，Docker 在正常关闭容器时会启用默认的超时机制，当 UrnaDB 的内存索引规模较大、持久化耗时较长时，进程可能无法在超时时间内完成数据落盘。此时 Docker 会强制终止容器，导致内存中的索引与检查点数据未能完整写入磁盘，进而引发文件损毁或数据不一致。[scode type="blue"]出现元文件损毁的情况下，更安全可靠的恢复流程是使用 UrnaDB 所提供的 fix-dbfs 工具在启动之前对数据目录中文件进行损坏数据整理，经过 fix-dbfs 修复后的数据文件才被视为安全且一致的有效数据文件，再让 UrnaDB 正常加载启动即可恢复宕机之前的数据状态。[/scode]如何合理权衡 Checkpoint 的执行频率与垃圾回收的触发时机，是 UrnaDB 数据库系统优化中的关键问题。若在数据高频读写期间执行垃圾回收（如磁盘空间压缩），可能引入短暂的读写延迟；同时在数据迁移与压缩过程中，存储引擎还可能导致磁盘空间的临时占用上升，从而影响整体性能与资源利用率。为此 UrnaDB 在未来版本中将引入一种主动回收机制：结合运行时监控指标与 Unix 的 Signal 机制，通过 kill 命令向 UrnaDB 进程发送信号，实时唤醒后台垃圾回收线程执行过期数据清理任务。该方案能够更精准地控制存储空间占用，提升系统的可预测性与运行稳定性。
在 MySQL 8.x 带来了许多新特性和改进包括 JSON 数据类型和 Java Script 作为 JavaScript Shell 支持，可以直接使用 JavaScript（JS）作为数据库查询语言或存储过程的语言，更好的性能和安全性，例如连接过程中出现的 Public Key Retrieval is not allowed 错误提示，这些都是新版本特性。本文将记录完整的基于 Ubuntu 最新版本中安装 MySQL8.x 版本的过程。安装和配置在安装 MySQL8.x 之前可以尝试先更新一下操作系统和软件包，执行下面命令可以更新：sudo apt update && apt upgrade -y更新系统和软件包版本需要一点时间，这个过程取决于你网络和所使用的 apt 软件包数据源地址，数据源配置文件路径 /etc/apt/sources.list。[scode type="yellow"]如果不确定系统中是否安装其他版本的 MySQL 版本，可以先执行清理命令。清空完成之后，提供 apt search mysql 包管理工具搜索最新的 MySQL8.x 软件安装包，最后通过 apt install 执行进行安装。[/scode]sudo apt remove mysql* sudo apt autoremove sudo apt autoclean清空完成之后提供 apt 所提供的 MySQL 软件包进行安装：sudo apt search mysql sudo apt install mysql-*安装完成之后，可以使用 sudo mysql_secure_installation 来初始化 root 用户的密码，这是 MySQL 新版本所提供的初始化数据库程序：sudo mysql_secure_installation[scode type="yellow"]如果需要让 MySQL 服务器能被远程的用户提供客户端访问，需要配置修改服务器所绑定监听的 ipv4 地址，默认是本地环路地址，在这个 /etc/mysql/mysql.conf.d/mysqld.cnf 中找到 bind-address 的值修改它为 0.0.0.0 即可。[/scode]# changed. Ref https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_tmpdir # tmpdir = /tmp # # Instead of skip-networking the default is now to listen only on # localhost which is more compatible and is not less secure. bind-address = 0.0.0.0 #这一行 ✅ mysqlx-bind-address = 127.0.0.1通过上面的配置目前 root 还是只能通过本机的客户端进行连接，有远程登录需求的可以配置一个远程用户并且允许远程登录，并且设置初始化密码，我这里开放的是 root 用户，执行下面命令：GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED WITH mysql_native_password BY '新密码' WITH GRANT OPTION;解决安全连接问题在 MySQL8.x 版本中如果要使用正常的密码连接到 Server 中连接过程中出现的 Public Key Retrieval is not allowed 错误提示，这个是新特别特意到安全功能增强，如果要使用这种方式连接必须要配置 SSL/TLS 密钥对进行连接。[scode type="yellow"]指定用户使用的密码验证方法 mysql_native_password ，这让 MySQL 使用的默认身份验证插件，它支持基于用户名和密码的常规身份验证机制，执行下面命令使用默认密码验证方式进行连接：[/scode]ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'new password';执行上面的 SQL 之后，刷新一下 MySQL 授权表：flush privileges;最后记得在服务器上允许开放 3306 端口数据包的连接。
现在注册一个域名和购买一台服务器部署一个 Web 网站非常容易，现在流行的 Serverless 架构更是连服务器都不需要购买，直接在各个云厂商的后台点点鼠标就可以部署一个在线的 Web 程序，虽然相比传统建站的方式简单很多，但是也有考虑一些很基础因素和配置，这篇博文我讲以博客系统为例子讲解如何维护一个 Web 服务程序。现在有很多流行的博客系统，例如静态的 Jekyll、Hugo、Hexo，动态以 PHP 为首的 WordPress 系统，WordPress 有着完善的生态系统是世界上目前二次开发使用最多的 Web 系统，静态的博客可以部署在 Github Pages 和 Vercel 等，直接将编写好的网站源代码上传到免费的空间即可。而对于动态博客系统还说，需要 LNMP 环境的支持才能正常运行，现在也流行直接购买已经部署好的数据库环境和运行环境来跑博客，对于动态博客系统还是数据才是最重要的，这篇博客将介绍一些方法一名合格的站长该如何运维自己博客。基础防御[scode type="yellow"]博客系统存放在主机之上通过互联网提供世界各地访问，这些访客里面有普通人也有一些黑客，黑客可以利用博客程序的漏洞例如 XSS 和 SQL 注入漏洞来破坏程序，这对于博客系统安全有了新的要求。普通的静态博客完全可以不用管这些，但是对于 PHP 这类的动态脚本语言来说更需要关注，因为 PHP 是一门动态语言如果被黑客注入了恶意代码会导致程序数据泄露，严重的会破坏程序的运行，目前场景的作为就是在博客核心代码里面对用户输入的数据进行过滤或者做合法性校验，或者在 PHP 使用 disable_functions 配置项来禁用一些敏感函数，来保证程序安全性。[/scode]如果考虑不是在应用程序嵌入代码来做防御，可以使用一些带有代码注入攻击检查防御的网关产品，博客作为一种 Web 程序提供的服务是要经过 HTTP 网络请求的，可以考虑采用带有防御型的网关产品。例如 Cloudflare 它可以提供基于 SASE 和 SSE 平台，将网站的域名的 DNS 接入到 Cloudflare 并且使用它来解析服务网站，可以改善安全和韧性减少攻击参数传入到主程序中，国内也有长亭科技的 SafeLine 的 WAF 产品。Cloudflare 并且提供 CDN 服务，可以隐藏真实的主机 IP 地址，也能确保域名和 DNS 设置的安全，防止域名劫持和 DNS 污染攻击。对于 HTTP 协议来说才有基于 TLS 安全层的 HTTP 协议，启用 SSL/TLS 证书，确保所有数据在传输过程中都是加密的，防止 Cookie 和 Session 信息被截取或篡改。对于基于使用 LNMP 作为基础设施运行的系统，要考虑是不是使用的长期支持版本的软件，如果使用的不是长期支持的版本的软件，如果出现了漏洞，对于这些已经不维护的基础软件会很麻烦，不能得到相应的补丁更新。操作系统环境还是建议使用基于 RHEL 系列的 Linux 发行版本，现在 CentOS 已经是最优的，我推荐使用 Rocky Linux 这个系统是 CentOS 创始人创建的一个克隆 RHEL 的开源操作系统，是在 RHEL 的基础上进行重新编译的，该系统在很大程度上兼容 RHEL 版本的，并且可以通过相同的方式安装 RHEL 软件包，还能得到长期的更新支持。当整个基础环境搭建完成之后，强烈不建议使用 root 账号去运行网站的基础设施，更应该创建一个专用的用户组和用户来运行对于的软件，这样即使黑客绕过前面的基层防护，也没有更高的权限去访问其他的数据和程序。服务器防火墙软件还是推荐使用目前主流的 firewalld ，这是 REHL 企业级的服务器管理软件，相比一些老的 iptables 来管理出入站流量复杂的操作更容易操作。不要开放不必要的端口，一个 Web 服务器最多开发基础的端口 80 、 443 、22 端口，其他端口可以一律禁止外部访问使用，最好的方式使用白名单 IP 的方式进行访问控制，通过跳板机进行管理。对于 root 用户来说应该不使用 root 密码进登录，而使用基于 SSH 的非对称密钥进行登录，可以将登录电脑和被管理的服务器进行绑定，将公钥上传到远程服务器来进行登录管理。此外此种方式还要考虑私钥的安全性，不要将私钥公开，避免在页面或日志中输出敏感信息，如数据库凭据、API 密钥等。数据备份在线系统的核心是数据，这些数据一般会被存储在 MySQL 这些关系型数据库中，目前主流的动态博客系统数据都是存储在关系数据库中，前面提到黑客通过一些特殊的手段获取到了服务器执行权限，从而可能会导致数据库被拖库导致数据泄漏。对于数据库系统来说管理也是不推荐开放 root 权限给远程用户登陆，对于 Web 程序所依赖的数据库表所在数据库提供对于数据库账户来管理，并且取该账户的一些敏感权限例如删除数据表，添加新字段等，最后 Web 程序的 IP 和能被访问数据库 IP 白名单进行绑定，这样以免泄露了账户和密码也能阻挡恶意的用户远程连接到数据库。建议在数据库中的敏感字段进行加密存储，并且定期对数据库的数据进行周期性备份。对于普通博客系统建议每天执行一次数据库备份计划，例如使用 crontab 进行周期性执行备份脚本，这个备份可以使用 mysqldump 这类工具部分，对其备份的数据可以考虑加密存储。前面所说的是传统备份，部分云厂商提供了主机快照功能，可以对整个主机的数据进行快照备份储存，当然要额外支付一些费用，不管以何种方式进行数据备份，最重要的是养成备份数据习惯，即使数据丢失也能通过备份数据恢复。
关系型数据库目前有很多产品共同特点是分为商业版本和社区开源免费版本，目前社区开源版本的关系型数据库用的最多的数据库是 MySQL Community Server Edition ，最大特点为免费商业使用也不用顾虑任何法律相关的问题，MySQL 被 Oracle 公司收购之后也推出了商业服务，原有的 MySQL 开发人员为了能摆脱 Oracle 相关的控制也在 MySQL 原有代码的分支上开发出来了 MariaDB 开源社区驱动开发的版本。[scode type="lbule"]对于一个想要学习数据库技术的新手来说要解决的问题当然是如何拥有数据库服务器端，数据库也是一种 C/S 架构的程序，对于多数开发者来说用的最多的是客户端，服务器管理大部分由 dba 操作，这篇博客将介绍如何通过 tar 包的方式在 RHEL9 上安装 MySQL 服务端程序。[/scode]准备工作首先需要一台已经安装 RHEL9 的操作系统，目前 rhel 官方的仓库已经支持直接使用 dnf 命令安装 MySQL 服务器，这种针对是完全没有经验用户傻瓜安装方式。如果需要自定义安装路径和用户组还是推荐使用 tar 包的方式安装，首先安装一些依赖包和工具包：yum -y groupinstall "Development Tools" yum -y intsall vim wget libaio[scode type="yellow"]最后去 MySQL 官方网站找对应的 Linux Generic Archive 安装包下载到本地：[/scode]wget https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.32-linux-glibc2.12-x86_64.tar.xz[scode type="yellow"]创建一个用于存储 MySQL 服务器程序的目录，通过命令 mkdir -p /usr/local/mysql ，将其解压到这个目录中：[/scode]# 解压到指定目录中 tar -xvf mysql-8.0.32-linux-glibc2.12-x86_64.tar.xz -C /usr/local # 重新命名目录 mv mysql-8.0.32-linux-glibc2.12-x86_64 mysql解压完成之后接下来需要创建两个目录，一个用来存放 mysql 运行时产生的日志文件，另外一个为存放 mysql 生成的数据文件目录，需要创建两个目录命令如下：# 创建 log 目录 和 创建数据目录 sudo mkdir -p /usr/local/mysql/data /usr/local/mysql/logs将 mysql 的支持文件拷贝到 /etc/systemd/system/ 目录中，该目录存放都是一些开机自动化启动程序，能够使用 systemctl 程序的子命令 start 、 stop 、restart 、reload 来管理某个具体的应用，例如这里的 mysql 程序。# 复制支持文件，并且重新命名为 mysqld sudo cp /usr/local/mysql/support-files/mysql.server /etc/systemd/system/ # 所有者应有执行权限 sudo chmod +x /etc/systemd/system/mysql.server[scode type="blue"]接下来要创建用来运行 mysqld 的用户，和将上面的文件夹所有者和所属组全部设置为新创建的 mysql 用户和组：[/scode]# 创建用户 useradd -M -s /sbin/nologin mysql # 更改文件所属组和所有者 chown -R mysql:mysql /usr/local/mysql[scode type="yellow"]最后将 mysql 的二进制命令工具目录加到环境变量中，以方便在任何地方都能使用 mysql 提供的命令工具：[/scode]# 在 /etc/profile 最后一行添加 echo "PATH=$PATH:/usr/local/mysql/bin" >> /etc/profile # 刷新环境变量 source /etc/profile到此整个安装前置条件已经满足，剩下就是对数据库进行安装和初始化了。初始化安装当准备工作完成之后就需要对数据库进行初始化，这里初始化使用 mysqld 命令，要提供 --initialize 参数，--user 指定运行程序用户，--basedir 指定数据库生成的数据文件存放的目录，--defaults-file 指定配置文件路径，执行命令会自动初始化数据库并且设置默认的 root 登录密码：[root@rhel9 ~]# sudo mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data 2023-03-22T12:08:36.476715Z 0 [System] [MY-013169] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.32) initializing of server in progress as process 23676 2023-03-22T12:08:36.486664Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. 2023-03-22T12:08:36.983513Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. 2023-03-22T12:08:38.468726Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: %ajsN=2G_)nf[scode type="red"]如果是比较新的系统可能会出现 libtinfo.so.6.1 文件版本过高，导致 mysql 客户端链接的 libtinfo.so.5 ，所以创建一个软链接：[/scode]ln -s /usr/lib64/libtinfo.so.6 /usr/lib64/libtinfo.so.5[scode type="green"]最后使用 mysql 客户端登录连接到服务器，第一步为重置 mysql 到密码，一定要重新设置 root 的密码，否则不允许任何操作：[/scode]# 重置密码 alter user 'root'@'localhost' identified by 'xxxxx'; # 开启远程登录 RENAME USER 'root'@'localhost' TO 'root'@'%'; # 或者直接创建一个新的用户 CREATE USER 'root'@'%' IDENTIFIED BY '12345678'; # 再给予权限操作 GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;最后如果要自定义配置 mysqld 话，需要在 /etc 目录中创建一个 my.conf 文件，这个文件就为 mysqld 启动时所需要的配置文件：[mysql] # 设置mysql客户端默认字符集 default-character-set=utf8mb4 [mysqld] skip-name-resolve #设置3306端口 port = 3306 # 设置mysql的安装目录 basedir=/usr/local/mysql # 设置mysql数据库的数据的存放目录 datadir=/usr/local/mysql/data # 允许最大连接数 max_connections=200 # 服务端使用的字符集默认为8比特编码的latin1字符集 character_set_server=utf8mb4 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB lower_case_table_names=1 max_allowed_packet=16M #配置免密登录 skip-grant-tables [mysqld_safe] # 配置日志文件和进程文件 error-log=/usr/local/mysql/logs/error.log pid-file=/usr/local/mysql/logs/mysql.pid最后如果想方便管理，使用系统 systmctl 命令管理 mysqld 程序，此时就的使用 chkconfig 命令添加一个服务管理项目：# 添加 mysqld chkconfig --add /etc/systemd/system/mysql.server # 这样就可以启动一个 mysql 服务 systemctl start mysql.server配置成功之后管理 mysql 就可以使用 systemctl 命令进行管理了。其他资料RHEL9 数据库指南Systemd 入门教程：实战篇
Oracle 数据库目前是世界最为领先的数据库，也是很多有钱的企业使用的数据库，与开源数据库不一样的是 Oracle 数据库针对企业用户是收费，而且费用也不是一些小公司能承受的起的，和 SQL Server 一样针对是企业级应用市场。我第一次接触 Oracle 数据库是 2017 年的时候，培训使用过 Oracle 课程，但是目前我用的最多还是 MySQL 数据库，因为它是开源和免费的，商业场景使用没有法律顾虑，而 Oracle 针对教育使用是免费的，如果商业使用那么就要付费。最近在闲逛的时候发现 Oracle 有一款 Oracle Database Express Edition ，有核心数 2 颗和内存大小 2G 限制和磁盘数据大小 12G 限制，适用于小型应用程序和我这种想体验 Oracle 最新特性的开发人员。准备工作Oracle XE RPM 安装包去 Oracle 官方网站下载 Oracle DataBase Express Edition 选择对应版本，我使用的 Linux 版本和 AMD 架构 CPU ，也可以在通过 SFTP 上传下载好的 rpm 包到服务器中，如果服务器在没有连接到外网的情况下。我目前使用的 DigitalOcean 所提供的 IaaS 服务，他们家没有提供 rhel8 的 ISO 文件需要自己去 rhel 控制中创建 .vmdk 文件上传到 DO 中，目前 Oracle XE 是原生支持 rhel8 和 oel8 的系统，其他发行版本 Linux 安装过程中会提示 yum repo 仓库密钥错误或者一些莫名其妙的错误。[scode type="yellow"]由于 Oracle XE 要求硬件最低配置是 1G 内存和 12GB 磁盘，如果未达到要求安装过程中会提示安装失败，最好使用 2G 内存机器。[/scode][scode type="yellow"]如果使用的 RHEL 系统初始化完成之后，必须对系统进行注册和检查极好状态，可以运行下面的命令：[/scode]# 查看系统当其系统发行版本 cat /etc/redhat-release # 检查是否注册 subscription-manager identity # 注册系统 subscription-manager register # 检查订阅列表 subscription-manager list --available # 检查订阅状态 subscription-manager status没有特殊情况一般都会显示注册成功。安装 Oracle XE[scode type="lbule"]在安装 Oracle DataBase Express Edition 之前会需要检查依赖 libaio 软件包，因为 Oracle XE 版本采用了 AIO 模型，这个库 libaio 为异步 I/O 提供了 Linux 原生 API 被 Oracle XE 程序所采用。从这里就可以看出来 Oracle 数据库在这方面技术领先性，目前大部分网络和磁盘 IO 程序还是用的 NIO 或者 BIO 的方式。[/scode]# 检查依赖是否安装 rpm -qa | grep libaio # 如果没有就安装，并且安装一些依赖软件 yum install -y libaio vim wget # 安装一些开发工具依赖 yum -y groupinstall "Development Tools" 安装过程中可能会提示没有安装 compat-openssl10 依赖包，这是需要手动下载并且安装到系统中：# 下载依赖 wget https://repo.almalinux.org/almalinux/8/AppStream/x86_64/os/Packages/compat-openssl10-1.0.2o-4.el8_6.x86_64.rpm # 安装依赖 yum install -y compat-openssl10-1.0.2o-4.el8_6.x86_64.rpm[scode type="yellow"]Oracle XE 相比其他免费的数据库安装起来要复杂，其他开源数据库大部分不用考虑主机名和主机 IP 映射关系，在安装之前一定要保持主机名和 IP 地址是对应的，否则后面安装会提示 No valid IP Address returned for the host，需要自己手动更改 hostname 和 hosts 文件中的记录。[/scode]接下来下载对应的 Oracle EX 版本的软件安装包，分别为 oracle-database-preinstall-21c 和 oracle-database-xe-21c 软件包，这是 Oracle Database Express Edition 所依赖的包，安装之下命令：# 下载软件包，必须提前安装 wget https://yum.oracle.com/repo/OracleLinux/OL8/appstream/x86_64/getPackage/oracle-database-preinstall-21c-1.0-1.el8.x86_64.rpm # 这里连接会超时，不是永久外链 wget https://download.oracle.com/otn-pub/otn_software/db-express/oracle-database-xe-21c-1.0-1.ol8.x86_64.rpm[scode type="yellow"]下载速度完全取决于网络带宽和本地磁盘写入速度，下载完整执行 yum -y localinstall oracle-database-xe* 命令进行安装，安装过程中会进行事务测试时间可能需要 10 分钟左右，如果没有依赖包缺失问题都会安装成功。[/scode][root@orcale-xe ~]# yum -y localinstall oracle-database-xe-21c-1.0-1.ol8.x86_64.rpm Failed to set locale, defaulting to C.UTF-8 Last metadata expiration check: 0:04:28 ago on Mon Mar 20 12:25:20 2023. Dependencies resolved. ================================================================================================ Package Architecture Version Repository Size ================================================================================================ Installing: oracle-database-xe-21c x86_64 1.0-1 @commandline 2.2 G Transaction Summary ================================================================================================ Install 1 Package Total size: 2.2 G Installed size: 5.8 G Downloading Packages: Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Running scriptlet: oracle-database-xe-21c-1.0-1.x86_64 1/1 Installing : oracle-database-xe-21c-1.0-1.x86_64 1/1 Running scriptlet: oracle-database-xe-21c-1.0-1.x86_64 1/1 [INFO] Executing post installation scripts... [INFO] Oracle home installed successfully and ready to be configured. To configure Oracle Database XE, optionally modify the parameters in '/etc/sysconfig/oracle-xe-21c.conf' and then execute '/etc/init.d/oracle-xe-21c configure' as root. Verifying : oracle-database-xe-21c-1.0-1.x86_64 1/1 Installed: oracle-database-xe-21c-1.0-1.x86_64 Complete![scode type="green"]当整个安装过程中执行完成之后，在尾部提示 Oracle XE 的配置文件路 /etc/sysconfig/oracle-xe-21c.conf ，最关键一步为设置 Oracle XE 管理员账号密码，执行 /etc/init.d/oracle-xe-18c configure 命令，也可以通过下面命令一键配置：[/scode](echo "password"; echo "password";) | /etc/init.d/oracle-xe-21c configure >> XEsilentinstall.log 2>&1至此初步的 Oracle XE 软件包安装和初始化完成。配置环境[scode type="yellow"]安装完成之后要配置整个 Oracle 数据库的用户环境，让在系统中随时随地生效和能被正确的使用，先切换到 oracle 用户，在用户根目录中的 ~/.bash_profile 文件中添加 Oracle 的安装目录：[/scode]# vim ~/.bash_profile # Oracle 安装目录 export ORACLE_HOME=/opt/oracle/product/21c/dbhomeXE # Oracle SID 是 System IDentifier 的缩写，启动之后实例名 export ORACLE_SID=XE # Oracle 的官方 bin 目录 export PATH=$PATH:/opt/oracle/product/21c/dbhomeXE/bin/ # 刷新环境变量 source ~/.bash_profile至此可以从 Linux CLI 执行 Oracle RDBMS 命令，执行 lsnrctl 命令检查 Oracle Listener 的状态，执行 sqlplus / as sysdba 即可连接到 Oracle 数据库 XE 实例，管理 Oracle XE 服务进程的命令如下：# 查看数据库状态 /etc/init.d/oracle-xe-21c status # 关闭数据库 /etc/init.d/oracle-xe-21c stop # 开启数据库 /etc/init.d/oracle-xe-21c start[scode type="yellow"]当配置设置完成之后如果需要远程网络访问必须允许数据库端口能被外部访问到，在防火墙中添加对应的端口，数据库连接为 1521 ，另外是 Oracle EM Express 端口 5500 ，Oracle EM Express 允许通过浏览器访问和管理 Oracle XE 数据库。[/scode]# 先安装 firewalld 管理工具 yum install -y firewalld # 设置 1521 5500 端口 systemctl start firewalld firewall-cmd --permanent --zone=public --add-port={1521,5500}/tcp firewall-cmd --reload[scode type="yellow"]如果需要远程通过 SQLDeveloper 连接到数据库，必须在服务器命令行 sqlplus 中执行 EXEC DBMS_XDB.SETLISTENERLOCALACCESS(FALSE); 允许远程登录功能，也可以通过浏览器查看 Oracle EM Express 界面。[/scode]总的来说在裸机操作系统之上通过 rpm 软件包安装 Oracle 要解决很多依赖和环境问题，如果图方便推荐使用 Docker 安装 Oracle 数据库，现在大环境好像不用 Docker 就很 LowB 一样，没有必要使用就不要强制使用，但具体取决于你的具体需求和应用场景，如果需要更高的性能或更大的控制权，则原生安装 Oracle 数据库可能更适合。其他资料Linux中的异步I/O模型New Droplet from Custom ImageInstall Oracle Database 21c XE on Rocky Linux 8Oracle Linux 9 java.lang.nullpointerexceptionINSTALLING ORACLE DATABASE 21C ON ORACLE LINUX 7
PostgreSQL 是一个功能强大的开源对象关系数据库系统，经过超过 35 年的积极开发，在可靠性、功能稳健性和性能方面赢得了良好的声誉，该数据库产品在开源 BSD 协议下发行，这意味着如果你有能力在此基础之上进行二次开发的，可以基于它来构建你商业型产品，作为一名开源软件爱好者一个多多使用的开源软件。 MySQL 在被 Oracle 收购之后出现了 MariaDB 分支这些都是兼容 SQL 标准关系数据库，在数据库引擎排行榜上显示 PostgreSQL 正在慢慢上升趋势，作为一款开源数据库能和一些企业级专有数据库软件排名不分上下，已经表示出它的优势所在了。PostgreSQL 和传统的关系数据库相比，还有一个特别特性是对象关系型数据库，写过面向对象编程语言 Java 会熟悉多个对象继承关系，类似的 Postgres 它弥补了关系型编程和面向对象编码之间的差距，这使得对象的定义以及表的继承成为可能，从而导致创建更复杂的数据库系统；此外 PostgreSQL 除了传统关系数据库支持的数据类型以外还有一些特殊自定义数据类型，例如 Array 、Points 、 Inet 、JSON 、XML 等类型。安装 PostgreSQL这里我采用的通过 Homebrew 的方式进行安装 PostgreSQL ，通过 Homebrew 安装的软件包可以方便管理也是一种比较即可的方式；在使用 Homebrew 安装任何东西之前，你应该始终确保它是最新的并且是健康的：brew update & brew doctor这两条命令会更新你电脑上的 Homebrew 并且自动检测健康状态和自动修复一些常见的问题，并且也会报告一些无法自行修复的错误。接下来就可以使用 Homebrew 进行安装 PostgreSQL 使用命令如下：brew reinstall postgresql@14这个安装过程可能比较缓慢，取决于当前电脑所在地区的网络情况，可以切换到国内的软件源进行安装，使用下面命令就可以切换阿里云的软件源，如下命令：# 替换brew.git cd "$(brew --repo)" git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git # 替换homebrew-core.git cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core" git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git # 刷新源 brew update安装完成之后可以使用 brew services list 查看当前支持管理的软件服务，启动 Postgres 也很简单，只需要使用下面的命令来启动 Postgres 服务：brew services start postgresql@14当启动成功之后，在最新的 MacOS 系统设置中的登录项可以查看到此数据库的进程信息，如下图所示：可以通过 psql --version 命令来查看是否成功安装，停止 Postgres 只需要使用下面命令停止进程：brew services stop postgresql@14如果是使用 Linux 环境可以在官网查看具体安装步骤文档，但是要注意的是在 Linux 系统环境下 Postgres 采用的系统用户的方式进行管理，默认超级用户是一个名为 postgres 用户，需要使用下面的命令切换：# 切换换默认超级管理员用户登录到数据库中 sudo -u postgres psql # 切换用户 sudo -i -u postgres默认使用端口 5432 ，默认的 Postgres 用户是 postgres ，不需要密码进行身份验证因此如果需要添加密码，我们必须首先以 postgres 用户身份登录并连接。修改密码需要注意的如果要修改默认用户的密码必须有足够的权限，如果这里出现了默认权限不够情况，可以在 /opt/homebrew/var/postgresql@14/pg_hba.conf 配置文件中进行修改，身份验证配置文件是身份验证规则列表：# "local" is for Unix domain socket connections only local all all md5 # IPv4 local connections: host all all 127.0.0.1/32 md5 # IPv6 local connections: host all all ::1/128 md5 # Allow replication connections from localhost, by a user with the # replication privilege. local replication all md5 host replication all 127.0.0.1/32 md5 host replication all ::1/128 md5上方的配置项是要求每次进行连接的时必须要进行鉴权，只要鉴权成功之后才能连接，默认的 trust 代表可信的连接，意思是没有密码也行为没有校验过程，这里全部修改为 md5 的方式了，意思是密码会以 md5 形式传送给数据库较安全，现在就可以在 psql 命令行提示符下建立了到 Postgres 的连接，并且输入一下命令来更改默认用户的密码：postgres=# ALTER USER dings PASSWORD 'myPassword'; ALTER ROLEPostgres 还有另外一种方式可以修改默认的账号密码，在命令行提示符下使用 \password postgres 来给 postgres 用户设置密码，效果是一样的。另外如果是生产环境数据库部署在网络云主机环境中，这时还需要设置允许远程连接，在配置文件 postgresql.conf 中修改配置如下：listen_addresses = '*'重启 Postgres 后生效，这时候任何远程 IP 都可以连接。使用 Homebrew 默认安装的软件会开机自动启动，如果要禁止自动启动需要执行下面这条命令，如果不知道对应软件位置可以使用 brew services list 命令进行查看，然后使用如下命令禁止开机自启动：launchctl unload -w ~/Library/LaunchAgents/homebrew.xxxx.xxx.plist其 launchctl unload -w <路径> 的作用为停止并卸载和禁用任务，该任务将不会在下次登录时重新启动。操作命令作为一名开发人员最常也是操作数据库里面的数据信息，这时就需要掌握一些基本 DBA 命令，下面是我列出的一些常用的运维操作命令：# 创建一个用户名为 postgres 用户并且设置密码 createuser postgres -P # 为 postgres 用户创建一个名为 test_db 的数据库编码为utf8 createdb -Opostgres -Eutf8 test_db # 删除一个数据库 dropdb test_db; # 创建新表 CREATE TABLE user(name VARCHAR(20), signup_date DATE); # 插入数据 INSERT INTO user_info(name, signup_date) VALUES('张三', '2023-01-01'); # 查询记录 SELECT * FROM user_info; # 更新数据 UPDATE user_info set name = '李四' WHERE name = '张三'; # 删除记录 DELETE FROM user_info WHERE name = '李四' ; # 添加字段 ALTER TABLE user_info ADD email VARCHAR(40); # 更改字段类型 ALTER TABLE user_info ALTER COLUMN signup_date SET NOT NULL; # 设置字段默认值（注意字符串使用单引号） ALTER TABLE user_info ALTER COLUMN email SET DEFAULT 'x@example.com'; # 去除字段默认值 ALTER TABLE user_info ALTER email DROP DEFAULT; # 重命名字段 ALTER TABLE user_info RENAME COLUMN signup_date TO signup; # 删除字段 ALTER TABLE user_info DROP COLUMN email; # 表重命名 ALTER TABLE user_info RENAME TO backup_tbl; # 删除表 DROP TABLE IF EXISTS backup_tbl; # 删除库 DROP DATABASE IF EXISTS hello;还有一些常见命令行提示命令说明：\l 列出所有数据库 \c 选择切换数据库 \password 设置密码 \q 退出 \h 查看SQL命令的解释，比如\h select \? 查看psql命令列表 \c [database_name] 连接其他数据库 \d 列出当前数据库的所有表格 \d [table_name] 列出某一张表格的结构 \du 列出所有用户 \e 打开文本编辑器 \conninfo 列出当前数据库和连接的信息其他资料Install PostgreSQL LinuxHow to Set the Default User Password in PostgreSQLPostgreSQL与MySQL的比较PostgreSQL Data Types
在前面的分析 MySQL 的存储引擎相关的文章中，已经明确知道了在数据记录在以 16KB 的页进存储的，当操作 SQL 时数据库会针对数据所在的页面进行增删改查操作，读页面、写页面、创建新页面。在数据库执行事务的过程中会将所需要的数据页读取到内存中的 Buffer Pool 中进行操作，这样就使得内存中对事务进行隔离运算，当完成了数据更新操作就会进行刷盘持久化，这是一个完整的事务生命周期。当在内存中 Buffer Pool 的修改页面时，突发发生断电情况，那么内存中的数据就会丢失，如果数据未提交持久化就发生丢失情况，如何保证数据不丢失？最简单的办法记录日志，将在内存中对数据产生修改的页面信息记录到一个日志中，通过日志的方式好处是不需要对整个页面进行刷盘操作产生的随机 IO 性能问题，日志文件系统都是以 Append Only 方式进行的。REDO LOG为了防止在执行事务过程中程序突然崩溃导致内存数据丢失，MySQL 的设计者在引入 Redo 日志的设计，即使程序崩溃也能通过 Redo 日志里面信息恢复数据。Redo Log 相比传统数据页占用的内存空间非常小，只会存储表空间的 ID 和 页面号、和具体修改的数据偏移量，和被修改数据的新值，Redo Log 和之前博客中介绍的 Bitcask 存储引擎很相似。一条通用的 Redo Log 日志结构如下，Type 日志类型、Space ID 表空间、Page Number 页号、Data 真实的数据，至于 Len 和 offset 针对的是一些特殊类型的日志记录的附加信息：在 Redo Log 中的 Type 字段的类型有 53 种，日志数据的类型是根据页面中写入的数据类型进行划分的，整理一个常见的类型表格：类 型作用说明MLOG_1BYTE对应十进制数字为 1 表示在偏移量处写 1 字节MLOG_2BYTE对应十进制数字为 2 表示在偏移量处写 2 字节MLOG_4BYTE对应十进制数字为 4 表示在偏移量处写 4 字节MLOG_8BYTE对应十进制数字为 8 表示在偏移量处写 8 字节MLOG_WRITE_STRING对应十进制数字为 30 表示在偏移量处写入一个字节序列设计这么多的 Type 类型是为了节省空间，能劲量不要使用 offset 和 len 字段时就劲量不要使用，提高空间利用率积少成多，其 Type 字段类型远远不止本文中列出来的，能节省一个字节是一个字节。在 Redo Log 日志里面不仅仅有物理日志，还有一部分的逻辑日志，逻辑日志针对事务操作的类型日志，Type 类型是数据操作的动作，具体其他类型可以查看其他资料。在 MySQL 默认的存储引擎 Redo Log 大小可以通过命令进行查看，默认是 16MB 被存储引擎提前申请好存储在 /var/lib/mysql 目录中：SHOW VARIABLES LIKE 'innodb_log_buffer_size';当一个事务执行时 Redo Log 的工作原理图为下图，先是解析 SQL 语句，生成执行器然后去执行器里面执行加载需要的数据页，然后开始记录事务操作逻辑日志信息：正如上图 Redo Log 作为文件存储在磁盘上，那么就需要将内存中的日志操作记录刷入到磁盘文件里面，因为数据库是用户态下的程序所以他文件写入依赖于操作系统的文件缓存的刷入频率，Innodb 提供多种刷盘策略，默认 MySQL 的刷盘策略可以通过下面命令查看：SHOW VARIABLES LINK 'innodb_flush_log_at_trx_commit'分有 3 种策略，默认值为 1 下图：目前已 3 种值类型操作，分别为下面列表中：值类型作用说明0每次事务提交时不进行强行执行刷入磁盘，而是根据后台的线程进行的1每次事务提交时都会执行一次刷入磁盘的操作2每次事务提交时只把内存缓冲区的数据刷入到系统的 Page Cache 中依赖于系统在日志中还有一种特别的类型为 Mini-Transaction 类型，它是指一次事务对 Innodb 底层数据页的一次原子访问的过程。也就是说在一张有索引表中插入一条记录，那么聚族索引 B+ 树的数据页也会插入这条记录的索引信息，这个过程就称为对底层页的一次原子访问，如果这个过程中运行到一半就宕机了，如果 Redo Log 日志只记录一部分日志不是完整的，下次恢复数据就不能恢复完整的数据记录，所以这里的存储引擎设计者设计一组 Redo Log 要么把全部的日志都恢复掉，要么就一条也不恢复，结构如下图：在 Redo Log Buffer 中的多个 Mini-Transaction 可以组成一个 Log Block 块，多而个 Log Block 又可以组成 Redo Log Buffer，而负责存储 Mini-Transaction 数据的是 Log Body，Log Body 可以存储多条 Mini-Transaction，每块 Log Block 中都有 buf_free 指向这里该位置之后就是空闲的区域，每当执行一个 MTR 过程中会产生多个 Redo 日志都不可以分割的组，属于同一组的日志才能归属为同一组的 MTR 所有，运行过程中 MTR 只能在内存中，直到 MTR 执行结束复制到对应的 Log Block 中持久化。在 Log Block 分为 3 个部分，分别为 Header 、 Body 、和 Trailer，其中 Header 最为数据源信息部分存储每个 Log Block 一些基本信息，而 Trailer 为防止数据丢失或者损坏查错使用的校验码。当 Log Buffer 刷入到磁盘上会有多个日志文件组保存，而这些文件组的数量是固定的，并且文件组的总体占用大小限制为 innodb_log_file_size * innodb_log_files_in_group ，当要写入日志时如果发现大小不够就会从头开始覆盖文件，多个文件会形成一个环路，这个稍作了解即可。Rode 磁盘日志文件格式对应着内存中的 Log Buffer 本质上也是一片连续的存储空间， 被划分成了多个 512 字节大小的 Block，当持久化时也就是将内存的缓冲区的 Block 刷写到磁盘上的日志文件中，整个磁盘上的日志文件可以看成是内存中的多个 512 字节的大小组成一个链表结构。多个日志文件组成一个日志文件组，每个大小和数据组织格式方式也是相同的，前 2048 也就是前 4 个 Block 对应存储着每个日志文件管理信息，从 2048 以后为真实的 Block 数据信息，多个文件日志文件形成了一个环，下图：日志文件最核心功能即为保证系统崩溃或者断电情况下，在数据库恢复正常时还能恢复数据，如果一个事务在执行完成之后内存中的 Buffer Pool 脏页面已经被刷入到磁盘中，则 Redo Log 中对应的 Block 数据块已经毫无存在的意义，那么其所占用空间可以被其他日志覆盖重用，多个日志文件组会形成一个环形以此达成存储空间复用。图上的 2 个日志文件组的分割线在代码实现层面是 2 个变量，分为 checkpoint 和 write_pos，checkpoint 之前的数据页已经被刷写到磁盘中， write_pos 之后的数据页为当前需要刷写的，checkpoint 和 write_pos 之间的数据页新数据页可以插入的位置。UNDO LOG上面的 Redo Log 是属于帮助数据库的事务提供 持久化 的保证一种方案，而 Undo Log 要为数据提供一种数据在某个时间段的数据 一致性 方案，Undo Log 的数据信息属于逻辑级别的，会记录一条数据在被修改前的数据方便事务进行回滚操作，数据库有 Undo Log 的支持才能在执行过程中可以有退回之前数据版本的选项。当像已经存在的某条数据进行修改操作时 Undo Log 就会针对该条数据进行记录相反的操作，例如 执行 DELETE 一条数据的事务，而 Undo Log 会生成一条对应的 INSERT 事务操作，方便后面回滚使用，核心规则有 4 条：在插入一条记录时，至少要吧这条记录的主键值记录下来，如果发生回滚操作可以删除对应值的主键记录。在删除一条数据记录时会记录当前记录数据，备份当前记录的数据到日志中方便后面回滚恢复数据，重新插入到表中。在修改一条记录时，至少要把被更新记录的值记录下来，方便后面回滚到旧值的版本。在执行 SELECT 事务如果是只读事务则不需要进行任何操作，但是 MVCC 模式下可以通过旧值提高读取。在 MySQL 中实现可重复读级别隔离的事务时，大部分数据库采用的 MVCC 多版本并发控制，而实现 MVCC 最主要的基础就是 Undo Log 来实现的，当一个事务读取一行记录时若改记录已经被其他事务占用，则当前事务可以读存储在 Undo Log 中的之前的行记录版本，从而事务非锁定并行多事务读取行记录。BIN LOGBinLog 相比上面的 Redo Log 和 Undo Log 两种日志完全不同，BinLog 是 MySQL Sever 层维护的一种二进制日志，可以帮助多台主从架构的 MySQL Server 能正常复制表数据记录，其主要是用来记录对 MySQL 数据更新或潜在发生更新的 SQL 语句，并以事务的形式保存在磁盘中。它记录了所有的 DDL 和 DML 语句，除了数据查询语句 SELETE 、SHOW 等语句，以事件形式记录，还包含语句所执行的消耗的时间，MySQL 的二进制日志是事务安全型的，主从架构下可以使用 Master 把它的二进制日志传递给 Slave 并回放来达到 Master 节点和 Slave 节点数据一致的目的。默认情况下数据库服务器会自动开启 BinLog 功能，相关的设置可以在配置文件中编写，可以配置参数有很多，一般默认情况下的配置：[mysqld] # 二进制日志的存储目录 log-bin=/home/mysql/binlog/ # 高版本MySQL需要server-id这个参数，提供一个集群中不重复的id值即可 server-id=1 # mysql-bin.*日志文件最大字节（单位：字节） # 设置最大100MB max_binlog_size=104857600 # 设置了只保留7天BINLOG（单位：天） expire_logs_days = 7 # binlog日志只记录指定库的更新 # binlog-do-db=db_name # binlog日志不记录指定库的更新 # binlog-ignore-db=db_name # 写缓冲多少次，刷一次磁盘，默认0 sync_binlog=0查看 MySQL 有没有开启使用下面命令即可查询相关的参数信息：SHOW VARIABLES LINK `%log_bin%`;如果开启 BinLog 后相关的日志文件会在数据目录中，可以使用下面命令查看数据目录文件列表：SHOW BINARY LOGS; 如果开启的主从架构相关的可以使用下面命令查看日志的状态信息：SHOW MASTER STATUS;默认 MySQL 生成的 BinLog 是采用的 ROW 模式，这里的复制模式和之前我技术文章中的分布式数据复制中讲解的概念很类似，在 MySQL 中提供了 3 中模式：STATEMENT：基于SQL语句的复制（statement-based replication, SBR）ROW：基于行的复制（row-based replication, RBR）MIXED：混合模式复制（mixed-based replication, MBR）在 MySQL 5.7.7 版本之后的默认都是采用 ROW 模式进行的，如果单纯基于 SQL 语句的复制会出现一些问题，不能使用一些特殊的 SQL 内置函数，这个问题之前的文章中我也讲过，日志格式通过 binlog-format 进行设置。其他资料Redo Log 的刷盘策略与过程剖析InnoDB 启动选项和系统变量列表InnoDB 的 Redo Log 分析『浅入浅出』MySQL 和 InnoDBMySQL 是怎么运行的Undo Log 的概述与写入过程MySQL · 引擎特性 · 庖丁解InnoDB之UNDO LOG
在数据库事务中如何保证数据一致性这也是给开发者带来许多的挑战，我们将现实生活中的东西抽象成数据库的关系模型，存放在数据表中，但是现实生活中某些东西有一些约束的，例如付款不能出现负数，人民币最大的面值是 100 元不可能出现比 100 面值还要大的面值，那么这些如何在数据库中保证这些约束？在做某件事情之前查看是否满足某个条件，这就是本篇文章笔者要写的数据库触发器，如何使用 Trigger 来保证数据正确性。应用场景例如下面我用 DDL 写一段 SQL 语句创建表结构，在这段 DDL 语句中最后有一句 CHECK (balance >= 0)，如下：CREATE TBALE account_info { uid INT NOT NULL AUTO_INCREMENT, name VARCHAR(10), balance INT, PRIMARY KEY(id), CHECK (balance >= 0), }这是用SQL语句里面的 CHECK 关键字来规定 balance 列的值不能小于 0 的约束，这就是保证现实世界银行账户不可能出现负数情况，但是这个 CHECK 关键字在 MySQL 中是不起作用的插入数据值时不会检测条件是否成立，而在 Oracle 和 SQLServer 中是可以起到作用的，如果某个用 CHECK 语句来判断条件的列不满足条件就会出现错误。触发器像是上面的这种场景在日常业务需求经常遇到这些问题，例如一个用户在注册系统的时候会查看当前用户名是否占用？如果被使用了就不允许使用，这时大部分做法是通过业务层的代码进行检查的，通过业务层通过 SELECT 语句条件判断，但是这种情况会出现一个问题如果系统并发量很多如何保证多个用户输入一个相同的用户名情况下还能被正常满足数据正确性，也就是多个用户的条件查询如何保证？一般操作将几个语句包装成一个事务去操作。上面这种只是一个场景在某个操作之前先要查看是否能满足某个条件才能操作，这种场景很多，在数据库里面可以通过触发器去做，触发器是数据库中提供一个非常实用的功能，它可以在操作者对表进行「增删改」 之前（或之后）被触发，自动执行一段事先写好的 SQL 代码，目前触发器支持的模式有：名称作用域BEFORE INSERT在插入数据前检查某个条件，如不满足返回错误信息。AFTER INSERT在插入数据之后执行某个预定义好的SQL语句。BEFORE UPDATE在更新某个条记录之前检查某个条件，如果不满足返回错误信息。AFTER UDPATE在更新某条记录之后执行某个预定义SQL语句，例如记录日志信息。BEFORE DELETE在删除数据前，检查是否有关联数据，如有，停止删除操作。AFTER DLELTE删除表 A 信息后，自动删除表 B 中与表 A 相关联的信息。有了触发器在某些场景下就不需要再业务层做额外的处理了，有时候也不需要让业务方知道这些事情，例如每条更新操作都会被记录到日志表中，触发器也有自己的特定的语法的，类似于 Linux 下的 Shell 脚本，触发器的基本语法如下：DELIMITER // CREATE TRIGGER [触发器的名字] [触发器执行时机] [触发器监测的对象] ON [表名] FOR EACH ROW [触发器主体代码]// DELIMITER ;在头部和结尾的 // 符号表示触发器开始与结束，下面的中括号里面都已经其作用，触发器执行时机可以是 BEFORE 和 AFTER ，而触发器监测的对象则为具体对应操作动作类型可以是 INSERT 、 UPDATE 、 DELETE 这些，表名则为具体监测对象表，触发器主体代码则为具体触发器代码，下面是一个实例：DELIMITER // CREATE TRIGGER validate_sales_amount BEFORE INSERT ON account_info FOR EACH ROW IF NEW.balance<=0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = "你输入Balance金额不合法！"; END IF// DELIMITER ;上面这段代码中，我们使用 IF...THEN...END IF 来创建一个监测 INSERT 语句写入的值是否在限定的范围内的触发器，这个触发器的作用就是为了检测在向 account_info 表添加新数据时会检测 balance 列的值是否合法，从而达到前面类似于 CHECK 的效果。触发器缺点触发器不是没有缺点的，虽然能解决一部分业务场景下的问题，但是需要注意合理使用。例如一些变态的需求，假设一个系统有张用户表里面存储用户账户和余额信息，需求要让每个用户在完成每笔交易之后都会对其让整个所有用户余额减去用户支出金额，然后看整体系统的金额是否相同，以此来检查整体系统金额完整性。这个需求就不适合触发器，因为每笔交易都要对所有的数据记录进行操作，如果数据量过大，会导致整体每笔交易异常缓慢，所以要分业务场景合理使用触发器。其他资料MySQL / MariaDB 触发器应用场景
数据库系统核心就是存储引擎，前面的文章我写过日志类型的LSM-Tree存储引擎，本篇文章将介绍基于B+树的 InnoDB 存储引擎实现细节，存储引擎一般都是有不同专业领域人员开发的，目前在MySQL5.7之后使用的默认存储引擎就为 InnoDB ，下面为笔者整理的 InnoDB 引擎相关的实现细节笔记总结。配置设计MySQL 是如何对配置选项进行设计的？设置这个可能大部分软件都有手机上大部分是 GUI 图形化设置界面。而大部分的基础软件或者运行在 Linux 平台上的软件都是通过命令行或者指定的配置文件进行设置的，MySQL 作为一个数据库基础软件也提供很多种配置参数的方式，分别为命令行参数、环境变量、配置文件。权限最高的就是命令行方式，如果在启动数据库的时候再命令行上传入参数那么可以覆盖其他方式配置的环境参数，例如下面的：mysqld --skip-networking # 等价于 mysqld --skip_networking上面这种方式通过做参数标记，这种没有具体的参数值，而另外一种命令行的方式就是有默认值可以通过命令行修改其值的，方式如下：# 修改默认的存储引擎 mysqld --default-storage-engine=MyISAM如果是传入参数值时不容许有空格必须用等号划分，多个参数值可以通过空格分割传递起来；上面这些都为长参数配置选项，还有一种就是 MySQL 客户端所使用的默认短参数，主要短参数是区分大小写的，例如 -p 代表密码，而 -P 表示的是端口号，如下图：mysql -h127.0.01 -uroot -p Enter password:上面的方式每次都要通过命令行参数传入其配置参数值，如果要配置很多的话需要写多个参数集合，这使得很多的参数需要一起写到命令行中并且每次都要这么传入，非常麻烦，但是命令行在所有配置方式中命令行上传入的参数优先级最高。另外一种方式是通过配置文件的方式设置其参数值，采用的是 ini 的配置文件格式，例如上面的命令行方式配置的参数可以编写配置文件的格式：[client] port=3306 [mysql] default-character-set=utf-8 [mysqld] # mysql服务端默认监听(listen on)的TCP/IP端口 port=3306 # mysql数据库文件所在目录 datadir="xxxx/Data" # 服务端使用的字符集,默认为Latin，安装时一般选择utf-8 character-set-server=utf-8 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB 像上面这种方式称之为配置组的方式，每个组的都有自己对应的配置值，而组名大部分是对应的bin下面的可执行二进制名称，同一个配置文件中的配置出现多次，将以出现在最后一组的参数值为准；如果是命令行传入参数那么最终使用的是命令行参数，命令行参数级别最高；当然部分配置可以在使用 DDL 语言进行修改操作，例如数据库的字符集和表使用的默认存储引擎。MySQL的可配置信息远不止这么多，这些配置参数一部分是系统变量的成员，可以使用 SHOW VARIABLES 来查看相关的系统变量信息，设置系统环境变量的方式就是上面我所整理的命令行的方式和配置文件的方式，当数据库启动的时候这些被设置的参数就会被设置为其对应的值，如果没有配置则有默认的值。对应某些环境变量在程序运行的是无法动态修改的，且部分变量例如存储引擎都有各自作用域，例如字符串和字符串排序规则有数据级别、表级别、行级别，这时设计是因为可以合理的使用存储空间提高数据库性能。另外一些变量分为 GLOBAL 和 SESSION 级别的变量，服务器启动的时候会为分配初始化一些全局的变量，而会话级别则是针对于当前连接到服务器的某个客户端的环境变量设置，例如 max_connections 为全局变量，而 insert_id 客户端表示自增 ID 的初始化启始值，还有 default_storage_engine 则为服务器端和会话端共有的值。存储单位在 InnoDB 中数据表中的每条数据记录采用页存储，以页作为存储磁盘上的基本单位，默认的页大小为 16KB ，如果需要处理数据记录也就是要把页从磁盘中加载到内存中再操作，一次性批量操作 16KB 大小的页。当然这个页面大小页可以通过 innodb_page_size 系统环境来设置，当数据库启动之后这个值就不能被再次修改，运行过程中是不可变的系统变量。InnoDB 一些设计细节运用一些巧妙的设计来提高空间利用率，官方将存储数据的记录的页称之为 INDEX PAGE ，下面就为页布局图：这里主要关注的是 User Records 部分，数据库里面的记录都是存储在 User Records 里面，而 User Records 是从Free Space 里面申请的。Free Space 会在页面初始化时创建完成其实并没有 User Records 部分，当数据库开始工作的时候会从 Free Space 申请，当把整个 Free Space 使用完成之后等于整个页面被使用完，当使用完成之后就会申请新的页面。数据记录平时我们使用 INSERT 语句传入的数据记录在 InnoDB 底层存储也是一条数据记录，也称之为行格式， InnoDB 的作者在设计的时候提供了4种数据记录存储格式分别为：CompactRedundantDynamicCompressed其中 Redundant 是以前使用的旧格式，为了兼容性还一直保留；自 MySQL 5.1 开始，默认的行记录格式为 Compact，而从 MySQL 5.7 开始，默认的行记录格式为 Dynamic，使用 SHOW TABLE STATUS LIKE 'table_name' 可以查看指定表的行格式。目前最常用的还是 Compact 和 Dynamic 的方式进行存储的，这取决于所使用的数据库版本。在创建表或者创建列记录时可以使用 ROW_FORMAT=COMPACT 关键字来指定对应格式，如果默认没有指定存储格式会使用 Compact 格式，Compact 格式如下图：页中的数据是按行进行存放的，每页中存放的行记录最少为 2 行，最多为 16 KB / 2 - 200 行，也就是 7992 行。所谓的行就是上面的 Compact 数据格式，Compact 行记录格式是在 MySQL 5.0 引入的，设计目标是高效的存储数据，一个页中存放的行数据越多，其性能就越高。变长字段长度列表里有 3 个重要的参数为 M、W、L ，这3个参数分别代表着：M 最大占用字符数， W 一个字符集里面一个字符占用的字节数，L 可变长字段实际存储的数据字节长度。变长字段长度列表是存储一些不定长的字符串类型，例如 VARCHAR(M)、VARBINARY(M) 各种 TEXT 类型，例如 BLOB 类型的二进制格式，因为是这些类型不定长所以在存储记录会将其字节数也存起来。其中的 M 表示某一个字符集表示一个字符最多需要的字节数，例如 UTF-8mb3 中 W 就等于 3，例如 VARCHAR(M) 来说 M 则表示最多能存储 M 个字符，可以通过 SHOW CHARSET 命令查看对应的 Maxlen 的值；如果变长字节数占用的比较多，可能就要使用 2 个字节来描述其占用大小，至于用多少有一套默认的算法：如果 M * W <= 255 ，那么使用 1 个字节来表示长度如果 M * W > 255，并且 L <= 127，那么使用 1 个字节来表示长度如果 M * W > 255，并且 L > 127，那么使用 2 个字节来表示长度在 Compact 格式下的变长字段只允许存储非 NULL 的列的值，如果一个列中有 NULL 值都会放到一个统一的地方管理，首先会统计行记录中允许存储为 NULL 的列表有哪些（主键列以及使用 NOT NULL 修饰的列都不满足条件），则其他满足 NULL 条件的列被映射成一个二进制位图，如果二进制位上为 1 时代表该列的值为 NULL，存放顺序为小端逆序排列。其记录头信息都是一些特殊的标记二进制位，这里不经常使用并且维护者是存储引擎自己，主要看列值存储部分，且列值的头3个值为每条记录一些附加信息，分别为 ROW_ID、TRX_ID、ROLL_POINTER，这3个字段是存储引擎自动生成的，ROW_ID 为一个隐藏的主键，如果在定义列的时候没有声明主键或者 UNIQUE 键就会默认添加一个 ROW_ID作为主键，而 TRX_ID 为事务处理的唯一标识，ROLL_POINTER 则为回滚的指针。Redundant 格式是MySQL 5.0之前使用的格式，其最大差异就是没有变长字段长度，这就意味着会通过占用字节数来通过偏移量访问列值，主要的特点就是通过两个相邻的偏移量的差值来计算各个列的长度；也没有 NULL 专属列，而是将每个列的第一个二进制位来判断，如果是 1 则该列为空，如果真是数据长度大于 127 就采用 2 个字节来表示一个列对应的偏移量。 Dynamic 和 Compressed 行格式是在MySQL 5.7之后所使用的行格式，和 Compact 格式很相近，只是在出来溢出数据记录的时候方式不一样。Compact 和 Redundant 在处理溢出数据记录的时候就会有所差异，例如如果定义一个列为 VARCHAR(65532) 此时就会发数据记录溢出情况，上面说过 InnoDB 采用的页存储数据记录一个页大小为 16KB 也就是 16384 字节，而 65532 字节显然超过这个范围就会发生溢出情况。在 Compact 和 Redundant 中如果发生这种情况会，就会只存储 768 字节的数据，而另外 20 字节存储指向其他页的地址信息。Dynamic 和 Compressed 行格式不会存储 768 字节的数据，而是直接将其溢出的数据存储在多个溢出页中，方便通过压缩算法压缩空间。其他资料MySQL configuration documentThe physical structure of InnoDB index pagesInnoDB 行记录格式