必须用 last_insert_id() 获取,因其会话级隔离、协程安全且不可逆;不能用 max(id) 或 @@identity,前者并发不安全,后者受触发器污染。

Hyperf 里用 MySQL 自增主键,必须靠 LAST_INSERT_ID(),不能查 MAX(id),也不能依赖事务回滚后 ID 还在——ID 一旦生成就不可逆。
Hyperf 中 insert 后怎么拿到自增 ID
Hyperf 默认用 PDO 驱动,insert 方法本身只返回影响行数(int),不返回 ID。要拿 ID,得手动执行 LAST_INSERT_ID() 查询,且必须紧接在 insert 之后、同一连接中执行。
- 不能跨协程调用:Hyperf 的 MySQL 连接池是协程安全的,但
LAST_INSERT_ID()是会话级变量,必须在同一个Connection实例上调用 - 推荐写法:用
$connection->insert(...)插入,再立刻调用$connection->query('SELECT LAST_INSERT_ID()')->fetchColumn() - 避免封装成“先 insert 再 select max(id)”——并发下会错,尤其在高 QPS 场景
- 如果用了事务,也要确保
LAST_INSERT_ID()在commit前调用;事务回滚不影响已生成的 ID,但该 ID 已被消耗
Hyperf + MyBatis 风格写法(useGeneratedKeys)
Hyperf 官方组件 hyperf/database 不直接支持 useGeneratedKeys=true 这类 JDBC 风格配置,但可以通过 QueryBuilder 或原生 SQL 模拟类似行为:
- 用
insertGetId()方法:它内部就是先执行 insert,再查LAST_INSERT_ID(),并自动 cast 类型 - 示例:
$id = $this->db->table('users')->insertGetId(['name' => 'alice']); - 注意:该方法只对单条插入有效;批量插入时
insertGetId()返回的是第一条记录的 ID - 底层仍依赖
LAST_INSERT_ID(),所以同样要求不能有中间语句干扰(比如触发器里又插了另一张自增表)
触发器里想用新 ID?别查 LAST_INSERT_ID(),用 NEW.id
如果业务逻辑放在 MySQL 触发器里(比如 AFTER INSERT),直接读 NEW.id 就行,这是最准、最轻量的方式。
-
BEFORE INSERT中NEW.id是只读的,且此时还没真正生成 ID(除非显式赋值),不建议在此阶段依赖 -
AFTER INSERT中NEW.id已确定,可安全用于插入日志表、更新关联计数等 - 错误写法:
SELECT LAST_INSERT_ID()放在触发器里——它返回的是当前连接最近一次 insert 的 ID,不一定是本条 - 字段名不是
id?那就写NEW.user_id或对应列名,别硬编码
为什么 @@IDENTITY 和 SELECT MAX(id) 在 Hyperf 里更危险
Hyperf 的连接池会让多个协程复用同一条物理连接(按需释放),而 @@IDENTITY 受触发器隐式插入污染,MAX(id) 在并发下必然不准——这两者在协程环境里出错概率比传统 FPM 更高。
-
@@IDENTITY:只要触发器里插了任何带自增的表,它就变成那个表的 ID,和你当前 insert 完全无关 -
SELECT MAX(id):两个协程几乎同时 insert,都查到同一个最大值,后续逻辑全乱 -
LAST_INSERT_ID()是连接内线程/协程隔离的,Hyperf 的Connection对象生命周期可控,只要不手动切连接,就绝对可靠 - 容易被忽略的点:如果 insert 语句用了
INSERT ... ON DUPLICATE KEY UPDATE,只有实际插入时才更新LAST_INSERT_ID();更新操作不会改变它











