Repository navigation
数据库连接懒加载、租约管理与作用域归还 - #7819
Open
tw2066 wants to merge 10 commits into
Open
数据库连接懒加载、租约管理与作用域归还#7819tw2066 wants to merge 10 commits into
tw2066 wants to merge 10 commits into
Conversation
- 在 ConnectionResolver 中实现懒加载连接逻辑,通过 databases.{name}.lazy 配置控制
- 新增 LazyConnection 类作为连接池的延迟代理,只在首次实际使用时才获取真实连接
- 懒加载连接支持元数据方法调用,无需占用连接池资源
- 查询构建等操作延迟到执行时才解析真实连接
- 添加相关单元测试验证懒加载行为和连接释放机制
- 新增 databases.{name}.lazy 配置选项支持延迟连接
- 新增 databases.{name}.release_after_use 配置选项支持使用后立即释放连接
- 实现 LazyConnection 代理类处理延迟连接逻辑
- 在 release-after-use 模式下查询完成后立即释放连接回池
- 事务期间保持连接持有确保数据一致性
- 粘性读写连接在写入后保持连接防止读取不一致
- 查询日志启用时阻止连接释放保证日志完整性
- 协程环境下正确管理连接生命周期和清理机制
- 添加了 Db::enableReleaseAfterUse()、Db::disableReleaseAfterUse() 和 Db::resetReleaseAfterUse() 方法用于运行时切换 release_after_use 选项 - 在 ConnectionResolver 中添加了运行时重写机制,支持全局和协程级别的连接释放策略控制 - 更新了 LazyConnection 类以支持运行时动态调整连接释放行为 - 修复了上下文管理逻辑,确保运行时切换能够立即生效 - 添加了协程本地的连接释放标志覆盖功能 - 增强了测试用例以验证运行时控制功能的正确性
- 添加了 databases.{name}.lazy 配置选项说明
- 添加了 databases.{name}.release_after_use 配置选项说明
- 添加了 Db::enableReleaseAfterUse 等运行时控制方法说明
- 补充了懒加载连接代理的工作机制描述
- 更新了相关 Pull Request 链接为实际编号 hyperf#7819
- 在游标流式传输期间添加 streamingCursors 计数器防止连接被意外释放 - 修改 releaseAfterUse 方法增加对 streamingCursors 的检查 - 更新文档中关于连接释放条件的描述,补充游标流式传输场景 - 添加嵌套语句在游标迭代期间的测试用例 - 实现 PDOStatementStubPHP8 中的行数据返回功能用于测试
- 修复了在启用 release_after_use 选项时,粘性读写连接在写操作后的连接持有逻辑 - 更新了文档中关于连接在查询日志启用时仍被持有的说明 - 添加了测试用例验证粘性写入连接在协程间的重置行为 - 确保读取-你的-写入(read-your-write)行为在粘性模式下正常工作 - 修复了游标迭代完成后连接的释放逻辑
- 补充了 release_after_use 选项的详细行为描述 - 添加了 insert 操作不会立即释放连接的说明 - 更新了运行时切换 release_after_use 配置的方法文档
tw2066
marked this pull request as draft
October 5, 2026 09:19
- 添加了 ConnectionOperationInterface 接口支持连接操作处理
- 实现了 LazyConnection 类用于延迟连接池连接获取
- 新增 release_after_use 配置选项实现连接使用后立即归还
- 添加 databases.{name}.lazy 和 release_after_use 配置选项
- 实现 ConnectionLease 管理连接租约生命周期
- 支持查询日志、事务和写入记录的状态管理
- 添加了连接元数据解析器支持轻量级查询构建
- 实现了连接借用和归还的安全机制
- 支持协程本地连接策略配置和临时覆盖功能
- 详细说明了懒加载和使用后释放连接的配置选项 - 解释了粘性连接的行为和查询日志存储机制 - 描述了需要相同连接的命令作用域使用方法 - 介绍了临时和工作进程策略的配置方式 - 说明了协程清理和自定义驱动的实现要求 - 提供了 MySQL 集成测试的运行指南
- 添加了连接租赁激活状态检查方法 isActive() - 实现了 Schema 语法的正确恢复机制,包括克隆和重置逻辑 - 修复了外部释放连接时的状态恢复和异常处理 - 改进了租赁回调注册的安全性检查 - 解决了懒加载连接的延迟清理问题 - 添加了对无 Schema 支持驱动的元数据回退处理 - 优化了查询日志在逻辑会话中的管理 - 修复了事务和连接复用时的状态隔离问题
tw2066
marked this pull request as ready for review
October 6, 2026 06:47
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
数据库连接懒加载、租约管理与作用域归还
概要
当前
Model::query()等仅构建查询的操作就会借用连接池槽位,并持有到协程结束。在消费者、常驻 worker 或包含其他耗时操作的请求中,连接的占用时间可能远长于 SQL 的执行时间。本 PR 为
hyperf/db-connection增加可选的延迟借用和使用后归还:构建查询不占池,执行时才借用连接;开启release_after_use后,在完整数据库操作结束、且没有事务或其他持有条件时归还。默认仍使用原有 eager 行为。配置与使用
两个选项位于
databases.{name}下,与pool同级,默认均为false:lazy:延迟到首次执行时借用,随后持有到协程结束。release_after_use:隐含开启 lazy,在满足生命周期条件时短借短还。多条命令依赖同一个租约时,可以使用
Db::withConnection(),无需额外开启事务。下面是 MySQL 会话变量的示例;读写分离时显式从写 PDO 读取该变量:请求内临时调整策略,可以使用支持嵌套及异常恢复的协程级作用域:
实现原理
实现将编译设置、逻辑连接、借用所有权和 SQL 执行分开。上层 Builder 始终绑定协程内的逻辑连接,物理连接则可以在多次完整操作之间归还、重新借用。
flowchart TD A[Db / Model / Query Builder] --> R[ConnectionResolver:协程 Context] R --> S[LazyConnection:逻辑 Session] S --> M[ConnectionMetadata:无 IO 编译设置] S --> O[操作计数、显式 pin 与归还策略] O --> L[ConnectionLease:所有者与借用代次] L --> W[DbPool 中的 Connection 包装] W --> D[真实 driver Connection] D --> P[写 PDO / 可选读 PDO] D -. 查询观察器 .-> S图中的操作计数、pin 和策略由
LazyConnection内部字段与方法管理。1. 构建查询与借用连接分开
ConnectionResolver::connection()先查找当前协程的 Context。需要 lazy 时,创建并缓存LazyConnection;此时没有调用pool->get()。普通自动归还只清掉代理内部的租约,Context 中的逻辑连接继续存在,因此之前创建的 Builder 下次执行时仍能通过同一个入口借用连接。query()、table()、toSql()依赖ConnectionFactory::makeMetadata()提供的ConnectionMetadata,其中只有配置、grammar、processor、数据库名、表前缀及 Builder 类型信息。内置驱动直接创建这些编译对象,不构造可执行 Connection,因此 SQLite 构造时的 PRAGMA 也不会被触发。SQL Server 的元数据包含专用 Builder 类型,保留其方言行为。执行查询时,
borrow()才通过 resolver 获取租约,并把逻辑连接的编译设置应用到真实 driver。第三方驱动没有无 IO 元数据契约时,明确回退到借用并固定真实连接,使用该驱动自身的编译设置。2. 用租约表达一次借用的所有权
DbPool::get()调用池包装的markBorrowed(),设置借用状态并递增leaseGeneration。ConnectionLease记录当前协程 ID 和这次借用的代次;每次访问都检查协程归属、租约是否已关闭、代次是否仍然匹配。例如 C1 的第一份租约归还后,C1 被另一个使用者借出,代次会从 G1 变为 G2。旧租约无法再操作 C1,即使池包装对象本身仍是同一个 PHP 对象。正常归还会消费租约并清空其引用,重复归还不再入池;池包装也通过借用标记阻止重复入池。
acquireLease()始终从池获取新的借用,不接管 Context 中已有、已由其他对象负责清理的连接,从入口上避免多个清理所有者共享同一次借用。3. 归还发生在完整操作结束时
普通执行方法统一进入
runOperation()。它先校验逻辑连接的状态和协程归属,再增加活动操作计数;执行和回调全部结束后,在finally中减少计数并尝试归还:执行钩子或事件中再次查询时,内层操作只会把计数从 2 减到 1,外层仍在使用租约,因此无法提前归还。异常路径也经过相同的计数恢复过程。
不同操作的完整边界如下:
insertGetId()cursor()transaction()beginTransaction()pretend()withConnection()insertGetId()通过可选的ConnectionOperationInterface包裹 Processor 调用,并在操作内用克隆的 Builder 绑定真实 driver。这样 MySQL/SQLite 的 insert + lastInsertId、PostgreSQL RETURNING、SQL Server 的 ID 获取都使用同一个租约。Processor 内部不会因某条语句返回而触发代理归还,普通insert()则不必为了 ID 获取持续占池。事务和
pretend()的用户回调收到的是逻辑连接,所以通过回调参数、Db 或 Model 再次执行的操作仍进入同一入口。pretend()使用finally恢复之前的预演及日志状态,嵌套预演结束也不会关闭外层预演。4. 游标迭代覆盖整个生成器生命周期
cursor()是生成器。仅创建生成器不会执行函数体;第一次迭代才增加operations并借用连接,计数一直保留到生成器的finally执行。以游标内嵌套
update()为例,假定进入循环前没有活动操作或租约:如果
break后仍保存生成器,它尚未销毁,租约也继续保留。协程清理时若生成器仍处于活动状态,则使底层 driver 失效,避免未读完的结果流进入下一次借用。这里保证的是租约生命周期。MySQL 非缓冲结果未读完时,协议仍禁止在同一个 PDO 上执行另一条 SQL;操作计数不会改变这个数据库限制。
5. 统一判断是否可以自动归还
releaseIfIdle()只在下列条件全部满足时归还:operations == 0、pins == 0,没有作用域外暴露的原始句柄。release_after_use策略为 true。withConnection()增加显式 pin,并把整个回调放在操作范围内,退出时在finally中解除 pin。归还策略只决定空闲时是否归还,不能突破活动操作、事务或 pin 的保护。暴露 PDO、Schema Builder、真实连接或调用未知 API 时,代理保守地记录原始句柄和不透明状态。作用域外访问会持续固定租约,直到显式释放或协程清理;显式作用域内访问则在范围结束后按当前策略判断归还。真正归还时,使底层 driver 失效,避免无法可靠重置的状态被后续使用者继承。
6. 状态分别归逻辑连接和真实 driver 管理
查询日志存放在逻辑连接中。每次借用时给真实 driver 安装查询观察器,由
logQuery()将查询信息送到逻辑连接;归还后再次借用其他物理连接,日志仍然连续。池归还前移除观察器并清理物理日志、预演及修改标记,避免请求状态跨租约残留。编译设置也归逻辑连接所有。setter 更新逻辑元数据并同步当前租约,fluent 调用返回逻辑入口。租约记录借用前的设置,归还前恢复 grammar、processor、表前缀、数据库名及需要恢复的 schema grammar;恢复失败时记录错误并使 driver 失效,再归还池包装,保留原始查询或回调异常。
事务层级和实际写标记继续由真实 driver 管理。sticky 判断读取
hasModifiedRecords(),因此 PostgreSQL Processor 直接修改真实连接的路径也能被识别,无需在代理中额外维护写状态镜像。只读事务不会合成写标记;满足 sticky 写后条件时,继续持有同一个物理租约。7. 协程清理与临时策略
逻辑连接构造时就记录所属协程并注册清理 defer,无需等首次查询才注册。清理先把逻辑连接标记为关闭,再按对象身份移除 Context 中对应的入口并归还租约。之后使用捕获的旧代理会抛出明确异常;晚期 defer 通过 resolver 获取连接时,可以建立新的逻辑入口及其清理责任。
策略默认值保存在 worker 的 resolver 中。
withReleaseAfterUse()在当前逻辑连接上临时保存覆盖值,并用try/finally恢复;嵌套调用恢复父范围的值,当前连接覆盖优先于 worker 默认值和配置。lazy 未开启时,resolver 在创建入口期间使用临时 Context 标记选择逻辑连接,随后恢复该标记,再进入回调范围,避免范围退出时错误读取临时策略。兼容性与边界
ConnectionInterface的必需方法保持兼容。ConnectionOperationInterface是可选扩展能力。enableReleaseAfterUse()、disableReleaseAfterUse()、resetReleaseAfterUse()设置当前 worker 的默认策略,不广播到其他 worker。已有逻辑连接在下一次归还判断时读取策略;已有 eager 句柄保持原生命周期。协程级临时策略须在获取 eager 句柄之前设置,否则抛出明确异常。Connection::resolverFor()的第三个可选参数注册无 IO 元数据工厂,也可覆盖ConnectionFactory::makeMetadata()。缺少元数据契约时回退到固定真实连接,可能在构建期间发生 IO;缺少可观测状态契约的连接采用保守的作用域生命周期。withConnection()固定的是池连接租约;读写分离仍可能包含两个 PDO。普通 SQL 创建的会话变量、临时表或锁,需要显式选择正确 PDO 并在归还前清理。update()不会导致租约提前归还。MySQL 默认缓冲查询支持该写法;非缓冲查询在结果未读完时不能在同一个 PDO 上执行其他 SQL。大表处理可使用独立命名连接分别读写,或使用chunkById()分批处理。验证
本地 PHP 8.4.24、Swoole 环境下,以下多次运行累计 288 个测试、927 个断言通过:
新增 26 个生命周期回归用例,覆盖真实 SQLite 两连接交替、ID 获取、预演不写入、重入、句柄固定、日志、编译设置恢复、策略隔离、defer 顺序、废弃游标、失效租约及清理异常。
真实 MySQL 的 10 个用例在本机服务上执行,验证两连接 ID 获取、保存点、事务异常、协程退出回滚、预演、sticky 路由、作用域会话变量、临时表隔离及非缓冲游标销毁。读写 PDO 使用同一服务器,通过
CONNECTION_ID()验证路由选择;未验证独立主从节点的复制延迟。PostgreSQL / SQL Server 使用真实框架处理链及 PDO stub,尚未进行真实服务集成验证。MySqlConnectionLifecycleTest通过HYPERF_MYSQL_DATABASE显式启用,连接参数来自HYPERF_MYSQL_HOST、HYPERF_MYSQL_PORT、HYPERF_MYSQL_USER和HYPERF_MYSQL_PASSWORD。测试使用随机命名的表并在结束后清理;未配置环境时跳过,凭据不进入仓库。其他检查:
git diff --check通过。array_first()/array_last();临时脚本不属于补丁,依赖版本及锁文件未修改。