news 2026/9/10 7:47:36

PHP事务实战:用mysqli+银行转账吃透ACID与并发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP事务实战:用mysqli+银行转账吃透ACID与并发

写 PHP 这么多年,我第一次真正意识到“事务”是干嘛的,是在一个支付项目线上出了 bug 之后。表面现象是用户支付成功、订单状态没更新,更深层看,是代码里扣款、写订单、记流水三个 SQL 操作分散在不同地方,中间某个环节抛了异常,数据就对不上了。那种状态下,你查日志、对数据、找差异,可能半天都说不清金额为什么凭空少了一笔。从那以后,我再看到 PHP 里的数据库操作,第一反应永远是:这段逻辑是不是该放进一个事务里?

事务在 PHP 开发里并不神秘,它本质上就是数据库提供的一套机制,让你可以把多个 SQL 操作打包成一个“要么全部成功、要么全部失败”的整体。这篇文章用最常见的 mysqli 扩展,结合经典的银行转账案例,把 ACID 四个特性、事务语句、隔离级别、行锁以及实战中容易踩的坑完整过一遍。读完之后你不仅能写出带事务的转账代码,遇到并发扣款、超时回滚这类问题时,也知道从哪里下手查。

1. 事务是什么:先用银行转账把 ACID 拆明白

很多新手看到 ACID 四个字母,第一反应是背概念:原子性、一致性、隔离性、持久性。背完就忘,因为概念没和具体场景挂钩。其实这四个特性完全可以用一笔转账讲清楚。

1.1 从转账逻辑反推 ACID 的真实含义

假设你要实现一个最简单的转账功能:从 A 账户扣 100 元,往 B 账户加 100 元。在数据库层面,这至少是两条 UPDATE:

UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2;

原子性(Atomicity)要求的就是这两条 UPDATE 要么都成功,要么都失败。如果第一条执行完、第二条执行到一半数据库突然宕机,那 A 的钱没了,B 的钱没到,这就是典型的“钱凭空消失”。有了事务,系统会在第二条失败时把第一条也撤销,回到转账前的状态。

一致性(Consistency)关注的是业务规则不被破坏。银行系统的老会计会告诉你,转账前后所有账户余额总和必须不变。事务在这里起的作用是保证你在事务内做的所有操作都符合预设约束,比如余额不能为负数、流水表必须插入记录。如果某一步违反了规则,整个事务照样回滚。

隔离性(Isolation)解决的是并发问题。两个用户同时操作同一条数据,比如同一个账户被两笔请求同时扣款,如果没有隔离机制,就可能出现“两个事务都读到余额是 100,各自扣了 50,最后余额变成 50 而不是 0”的情况。隔离性就是让事务之间互不干扰,或者说在可控范围内互不干扰。

持久性(Durability)最容易理解:只要事务提交成功,数据就必须永久保存,即使系统马上断电,重启后数据依然在。数据库通过 redo log 这类机制来保证这一点。

这四个特性不是四个独立选项,而是一个组合拳。原子性保证单个事务内部的完整性,一致性保证业务规则不被破坏,隔离性保证并发环境下的正确性,持久性保证提交结果不会丢。任何一个环节出问题,转账逻辑都不可信。

1.2 为什么 PHP 项目里事务总被写歪

PHP 的特点是请求生命周期短、无状态,一个典型的 PHP 请求从接收到返回可能只有几十毫秒。这种模型让很多开发者习惯于“顺序写 SQL、出了问题靠日志猜”,对事务的重视程度远不如 Java、Go 这类后端技术栈。

再加上不少项目用框架封装了数据库操作,新手可能只见过DB::beginTransaction()这样的方法,却不清楚底层到底发生了什么。常见的写歪方式有三种:

  • 把事务放到了循环外面,导致某一条数据失败时,前面已提交的数据无法回滚。
  • 在事务中间执行了CREATE TABLEALTER TABLE或者mysqli::commit()之外的隐式提交操作,导致事务被提前打断。
  • 异常捕获写得不完全,有些错误路径没走到rollback(),事务就一直挂着,连接长时间持有锁。

在写代码之前,先理解这些反面案例,比直接抄一段事务代码更有价值。事务不是“包一层 begin 就完事”,而是需要你清楚每条 SQL 的执行顺序、每个分支的退出路径。

2. 动手前的准备:mysqli 事务基础与连接配置

既然要用 mysqli 实现事务,环境检查这步别跳过。很多事务问题源自扩展没开、表引擎不对、或者 PHP 版本太老,代码写得再对也没用。

2.1 环境要求与连接方式

建议至少满足这些条件:

  • PHP 7.4 及以上版本,PHP 8.0 之后对 mysqli 的支持依然很好。
  • 数据库使用 MySQL 5.7 或 8.0,事务依赖 InnoDB 引擎,请务必确认表引擎是 InnoDB。
  • mysqli 扩展已开启,php -m | grep mysqli能看到 mysqli 字样。

连接数据库时,我习惯显式设置字符集,避免中文数据出现乱码影响后续判断。基础连接代码:

<?php $host = '127.0.0.1'; $port = 3306; $user = 'root'; $pass = 'your_password'; $db = 'bank_demo'; $mysqli = new mysqli($host, $user, $pass, $db, $port); if ($mysqli->connect_errno) { // 连接失败时 mysqli->connect_error 里是具体错误信息 throw new RuntimeException('数据库连接失败: ' . $mysqli->connect_error); } $mysqli->set_charset('utf8mb4');

注意,new mysqli()失败时并不会自动抛异常,只是设置connect_errno,所以必须手动判断。后续如果希望所有 mysqli 操作出错时直接抛异常,可以这样设置:

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

开启之后,SQL 执行出错会直接抛mysqli_sql_exception,配合事务回滚写起来会舒服很多,不用在每个query()调用的返回值里反复判断。

2.2 autocommit 与事务函数梳理

MySQL 默认是自动提交模式,也就是每执行一条 SQL,只要没语法错误,立刻写入磁盘。这个模式在不需要事务的普通查询里很方便,但在多个操作需要捆绑时就非常危险。

mysqli 扩展里,和事务直接相关的方法有三个:

  • begin_transaction():关闭自动提交,开启一个新事务。
  • commit():提交事务,把事务内的所有变更确认落库。
  • rollback():回滚事务,把事务内的所有变更撤销。

有些人会看到老代码里用mysqli->autocommit(false)加上mysqli->commit()的组合,效果和begin_transaction()类似。区别在于begin_transaction()更语义化,从 PHP 5.5 开始推荐使用,而且它支持传入标志位,比如只读事务、快照事务等。日常开发我优先用begin_transaction()

一个容易忽略的细节:begin_transaction()之前如果已经有未提交的事务,直接调用可能会报错或者隐式提交。稳妥的做法是在事务代码入口检查$mysqli->errno,或者统一规划好事务边界,避免多层嵌套。mysqli 本身不支持真正的事务嵌套,第二个begin_transaction()默认会返回错误,遇到这种情况需要自己设计“事务嵌套层级”或者干脆禁止嵌套。

3. 银行转账案例完整实现

这一节是全文核心。我直接给出一个可运行的转账函数,然后逐段解释为什么这么写。

3.1 表结构与余额设计

先建两张表:账户表和流水表。企业级的账户体系远比这复杂,但演示事务足够。

CREATE TABLE `account` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_name` VARCHAR(50) NOT NULL DEFAULT '', `balance` DECIMAL(12,2) NOT NULL DEFAULT '0.00', `version` INT UNSIGNED NOT NULL DEFAULT '0', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `account_flow` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `account_id` INT UNSIGNED NOT NULL, `change_amount` DECIMAL(12,2) NOT NULL DEFAULT '0.00', `balance_after` DECIMAL(12,2) NOT NULL DEFAULT '0.00', `remark` VARCHAR(255) NOT NULL DEFAULT '', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_account_id` (`account_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

两个细节要强调:

第一,余额字段必须用DECIMAL,不能用FLOATDOUBLE。浮点数在计算机里是近似值,做金额加减会产生不可预期的误差。DECIMAL(12,2)可以精确到分,大多数业务场景够用了。

第二,流水表里记录balance_after,意思是这笔操作之后账户的余额。这个字段在对账、排查数据问题时非常有用,能在不锁表的情况下快速确认金额链路是否完整。

3.2 完整转账代码

转账函数需要做的事包括:校验参数、开启事务、扣减转出账户余额、增加转入账户余额、写入流水、提交事务,任何一步出错都回滚。

<?php /** * 转账 * * @param mysqli $mysqli * @param int $fromAccountId 转出账户ID * @param int $toAccountId 转入账户ID * @param float $amount 转账金额 * @return bool * @throws RuntimeException */ function transferMoney(mysqli $mysqli, int $fromAccountId, int $toAccountId, float $amount): bool { if ($fromAccountId === $toAccountId) { throw new InvalidArgumentException('不能给自己转账'); } if ($amount <= 0) { throw new InvalidArgumentException('转账金额必须大于0'); } // 统一使用 DECIMAL,避免浮点误差,金额转成字符串传入 SQL $amount = number_format($amount, 2, '.', ''); $mysqli->begin_transaction(); try { // 1. 查询转出账户,并加上行锁 $stmt = $mysqli->prepare('SELECT id, balance FROM account WHERE id = ? FOR UPDATE'); $stmt->bind_param('i', $fromAccountId); $stmt->execute(); $fromResult = $stmt->get_result()->fetch_assoc(); if (!$fromResult) { throw new RuntimeException('转出账户不存在'); } // 2. 检查余额是否充足,这一步必须在事务内完成 if (bccomp($fromResult['balance'], $amount, 2) < 0) { throw new RuntimeException('余额不足'); } // 3. 扣减转出账户余额 $stmt = $mysqli->prepare('UPDATE account SET balance = balance - ? WHERE id = ?'); $stmt->bind_param('si', $amount, $fromAccountId); $stmt->execute(); // 4. 查询转入账户,并加上行锁 $stmt = $mysqli->prepare('SELECT id, balance FROM account WHERE id = ? FOR UPDATE'); $stmt->bind_param('i', $toAccountId); $stmt->execute(); $toResult = $stmt->get_result()->fetch_assoc(); if (!$toResult) { throw new RuntimeException('转入账户不存在'); } // 5. 增加转入账户余额 $stmt = $mysqli->prepare('UPDATE account SET balance = balance + ? WHERE id = ?'); $stmt->bind_param('si', $amount, $toAccountId); $stmt->execute(); // 6. 写入转出流水 $afterFromBalance = bcsub($fromResult['balance'], $amount, 2); $stmt = $mysqli->prepare( 'INSERT INTO account_flow (account_id, change_amount, balance_after, remark) VALUES (?, ?, ?, ?)' ); $remark = '转出至账户' . $toAccountId; $stmt->bind_param('isds', $fromAccountId, $amount, $afterFromBalance, $remark); $stmt->execute(); // 7. 写入转入流水 $afterToBalance = bcadd($toResult['balance'], $amount, 2); $stmt = $mysqli->prepare( 'INSERT INTO account_flow (account_id, change_amount, balance_after, remark) VALUES (?, ?, ?, ?)' ); $remark = '从账户' . $fromAccountId . '转入'; $stmt->bind_param('isds', $toAccountId, $amount, $afterToBalance, $remark); $stmt->execute(); // 8. 全部成功,提交事务 $mysqli->commit(); return true; } catch (Throwable $e) { // 任何异常都回滚,避免扣款成功但流水没写 $mysqli->rollback(); throw $e; } }

代码用到了bcsubbcadd这两个 BC Math 函数,它们是 PHP 处理高精度数值的利器,和DECIMAL字段配合起来,金额计算基本不会因为浮点问题翻车。如果你的环境没装bcmath扩展,也可以把金额当作字符串自己写加减,或者直接让 MySQL 用balance = balance - ?这种表达式计算,获取最终余额时再查一次。

3.3 代码走读与关键决策

这段代码有几个决策点,值得单独说清楚。

为什么要用SELECT ... FOR UPDATE锁转出账户?因为并发场景下,不锁行的话,两个请求同时读到余额是 100,各自判断余额足够,然后各扣 50,最终余额变 50 而不是 0。加了FOR UPDATE后,第二个事务的SELECT ... FOR UPDATE会等待第一个事务提交或回滚,从而避免并发扣款超扣。

为什么不直接用UPDATE account SET balance = balance - ? WHERE id = ? AND balance >= ?做条件更新,然后再检查影响行数?这也是常见做法,而且性能更高,因为它减少了一次 SELECT。但问题在于,条件更新确认的是“账户存在且余额足够”,却无法在线程中继续写“扣款后的余额是多少”的流水,你还得再查一次。对于演示来说,先 SELECT 后 UPDATE 更直观,也方便后面扩展到更复杂的业务校验。实际项目中,我会根据性能需求选择。

为什么要写流水表,而且放进同一个事务?因为资金类操作只改余额是不够的。如果没有流水,后续排查一笔异常转账时,你根本无法还原当时的操作过程。流水和余额变更必须在同一个事务里,要么一起成功,要么一起失败,否则就会出现“余额变了但查不到流水”的尴尬境地。

事务代码写完后,建议在测试环境直接模拟一次断线或异常场景,验证rollback()是否真的把余额变回去了。这一步很多人忽略,结果代码逻辑看着对,真出问题时才发现异常被吞了或者事务根本没开启。

4. 并发、锁与隔离级别实战

转账案例能跑通只是第一步,线上的真实挑战永远是并发。这一节把隔离级别和锁这两个重头戏说透。

4.1 三个并发读问题的直观理解

谈到隔离级别,绕不开三个经典问题:脏读、不可重复读、幻读。

脏读是指一个事务读到了另一个事务还没提交的数据。事务 A 改了余额但没提交,事务 B 读到了这个改过的余额,万一 A 回滚,B 就基于一个不存在的值做了判断。隔离级别为READ UNCOMMITTED时会出现这种现象。

不可重复读是指同一事务内,两次相同的 SELECT 查出的结果不一样。比如事务 A 先查询余额是 100,事务 B 提交了余额改为 90,事务 A 再查就变成 90。隔离级别为READ COMMITTED及以下时可能发生。

幻读是指同一事务内,两次查询返回的行数不一样。比如事务 A 查询所有余额大于 0 的账户,事务 B 插入了一个新账户并提交,事务 A 再查多了一行。这是因为范围查询的间隙可以被其他事务插入。REPEATABLE READSERIALIZABLE之间的取舍,往往就是在讨论怎么解决幻读。

用表格概括就是:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不会可能可能
REPEATABLE READ不会不会可能(InnoDB 下通过间隙锁多数情况可避免)
SERIALIZABLE不会不会不会

MySQL 默认隔离级别是REPEATABLE READ。很多从其他数据库转过来的开发者会惊讶,因为 Oracle 默认是READ COMMITTED。MySQL 之所以敢在REPEATABLE READ下应对大多数业务场景,是因为 InnoDB 实现了间隙锁(Next-Key Lock),在特定条件下能规避幻读。

4.2 隔离级别的设置方式

会话级设置非常简单:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

也可以在 PHP 代码里执行:

$mysqli->query('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED');

但要注意,这条语句必须在begin_transaction()之前执行,否则对本事务不生效。全局设置要谨慎,会影响所有连接,一般在配置文件my.cnf里改:

[mysqld] transaction-isolation = READ-COMMITTED

实际项目中,我建议优先保持 MySQL 默认的REPEATABLE READ。因为READ COMMITTED虽然在部分场景下能减少锁冲突,但会让“同一事务内两次查询结果不一致”的问题暴露出来,代码里如果没做好兜底,反而更难排查。只有在你明确知道某个业务不需要可重复读、且希望降低锁等待时,才去调整隔离级别。

4.3 行锁 SELECT ... FOR UPDATE 的正确用法

回到转账案例,我加了FOR UPDATE,这是一种悲观锁。它假设并发冲突很常见,所以直接把人家的行锁住,让别人等我。好处是逻辑简单、不会超扣;坏处是并发量上来之后,锁等待会成为瓶颈。

使用FOR UPDATE有几个注意事项:

  • 必须在事务内执行才有意义。如果没开启事务,SELECT ... FOR UPDATE锁住的行会立刻释放,相当于白锁。
  • 锁的是索引记录。如果 WHERE 条件没走索引,InnoDB 可能升级为锁表,影响范围瞬间变大。所以FOR UPDATE的查询条件一定要走主键或唯一索引。
  • 查询不存在的行时,FOR UPDATE同样会加间隙锁,可能阻塞其他事务插入数据。转账场景里转入账户不存在时,反而会因为这个间隙锁影响同范围内的插入操作,所以代码里要在锁内检查结果集。

在转账逻辑里,正确的加锁顺序很重要。如果事务 A 先锁账户 1 再锁账户 2,事务 B 先锁账户 2 再锁账户 1,就可能出现死锁。解决方式就是统一加锁顺序,比如按账户 ID 排序后再锁。老练的开发者写转账相关逻辑时,会先把两个账户 ID 做排序,按固定顺序加锁,从根上规避死锁。

5. 常见问题与排查技巧实录

这一节写的都是我实际遇到过、或者在帮别人排查代码时见过的问题,不是网上抄来的理论,每一条都有具体的现场特征。

5.1 事务不生效的典型原因

事务代码写得没问题,但实际跑起来数据还是乱的,先按下面这个清单查:

  • 表引擎不是 InnoDB。MyISAM 不支持事务,begin_transaction()不会报错,但操作根本没有回滚能力。执行SHOW TABLE STATUS WHERE Name = 'account',确认 Engine 字段是 InnoDB。
  • 事务中间执行了隐式提交操作。MySQL 中ALTER TABLECREATE TABLEDROP TABLETRUNCATE TABLEGRANTLOCK TABLES等语句都会导致当前事务隐式提交,前面的未提交操作立刻落库。如果事务代码里混入了这类语句,回滚就失灵了。
  • 异常被吞掉了。PHP 代码里如果把query()包在try { } catch {}里不往外抛,或者用了@抑制错误,那rollback()可能根本没走到,事务会一直挂着。开启mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)能少踩这个坑。
  • 事务连接对象不一致。常见于框架里用了连接池,或者手动 new 了多个 mysqli 对象,一个对象开启事务,另一个对象执行 SQL,事务自然管不到后面的操作。
  • 嵌套事务处理不当。mysqli 不支持原生嵌套事务,第二个begin_transaction()要么报错要么被忽略,内层想回滚根本做不到。要么设计成单层事务,要么自己维护事务计数器模拟保存点。

5.2 死锁、超时与重试

死锁在转账场景里很常见,尤其是两个账户互相转账的并发请求。MySQL 检测到死锁后,会回滚其中一个事务,并返回错误码 1213(ER_LOCK_DEADLOCK)。锁等待超时则返回错误码 1205(ER_LOCK_WAIT_TIMEOUT)。

遇到死锁,正确做法不是改 SQL,而是让业务层重试。伪代码如下:

$maxRetry = 3; $retry = 0; while ($retry < $maxRetry) { try { transferMoney($mysqli, $fromId, $toId, $amount); break; } catch (mysqli_sql_exception $e) { if ($e->getCode() === 1213 || $e->getCode() === 1205) { $retry++; usleep(100000 * $retry); // 简单退避 continue; } throw $e; } }

重试前要注意,连接的事务状态已经不可控。如果transferMoney()内部已经调用过rollback(),这个连接可以继续使用;如果没回滚,就必须先手动rollback()或重新连接,否则后续 SQL 会一直报“Commands out of sync”之类的错误。

5.3 排查实录:从日志到锁监控

我在定位线上事务问题时,习惯按三步走。

第一步,看应用日志里有没有 SQL 异常,尤其是死锁和锁等待超时。很多项目只在异常时记录 message,不记录错误码,这会导致你无法区分死锁和普通语法错误。建议日志中至少记录异常类、错误码、SQL 语句、请求参数。

第二步,连上 MySQL 执行:

SELECT * FROM information_schema.innodb_trx\G

这张表会列出当前所有正在执行的事务,重点关注trx_startedtrx_statetrx_rows_locked字段。如果有事务长时间处于RUNNING状态且trx_rows_locked很大,十有八九是锁等待或事务没提交。

第三步,查锁等待关系:

SELECT * FROM information_schema.innodb_lock_waits\G

配合performance_schema.data_lock_waits,能定位到具体是哪个事务阻塞了哪个事务。加上sys.innodb_lock_waits视图(MySQL 5.7+ 自带 sys 库)可以直接看到阻塞者和被阻塞者的连接 ID、SQL 片段,排查效率会高很多。

遇到怀疑是大事务的情况,还可以执行:

SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration FROM information_schema.innodb_trx;

事务持续时间超过几十秒,基本可以断定代码里有外部调用、慢 SQL 或者忘记提交的问题。

6. 写在最后的实操心得

6.1 我踩过的几个坑

第一个坑,是在事务里做了远程 HTTP 调用。当时为了通知下游系统,我把请求接口放在了事务提交之前,结果下游响应超时,数据库连接一直持有锁,整个表的更新操作被堵了几分钟。后来改成先提交事务,再发异步通知,才彻底解决。记住一个原则:事务里只做数据库操作,外部 IO 一律挪到事务外。

第二个坑,是在大事务里批量更新。有一次做定时任务,循环了几千条数据,每条数据都放在同一个事务里执行更新。事务越积越大,最后主从延迟飙高,还出现了锁等待超时。优化方案很简单:分批提交,每 200 条一个事务,既保证局部一致性,又不至于把事务撑爆。

第三个坑,是测试时候偷懒,没用真实并发压测。单线程跑事务代码一直正常,上线后第一波双十一流量就把余额扣成了负数。后来用两个终端开两个 mysql 会话,模拟同时转账,才真正验证了锁的作用。写事务代码的验证标准,必须是并发场景下依然正确,而不是单线程能跑通。

6.2 除了事务,你还需要想清楚这些

事务解决的是数据库层面的原子性和隔离性,但一个完整的资金类系统,光靠事务远远不够。

比如幂等性。如果客户端超时重试,同一笔转账请求可能被发送两次,事务本身就是“都成功或都失败”,但无法判断这是不是同一笔业务。这时候需要业务幂等表,用唯一键约束保证同一笔单子只处理一次。

比如对账。即使有了事务,线上还是可能出现数据库本身无法发现的逻辑错误,比如程序 bug 把金额算错了。定期跑对账任务,把账户余额、流水、第三方支付账单三方核对一遍,才能兜住最后一层。

再比如性能。事务保证了正确性,却也拉长了锁持有时间。对于高并发扣款类业务,可以在事务外先做预扣、再用消息队列异步更新账单,不过这已经超出本文范围了,属于架构层面的取舍。

写事务代码这几年,我的体会是:数据库事务不是银弹,但它是一张安全网。你得先会正确使用这张网,再讨论怎么网眼更细、性能更好。希望这篇文章能帮你在 PHP 的 mysqli 事务路上少走几步弯路,遇到问题时,能自信地说一句“这是并发问题,我见过”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 7:46:01

大模型Checkpoint恢复基准:AWS存储方案实测与优化指南

如果你的训练任务在AWS上跑了三天&#xff0c;好不容易推进到第2000步&#xff0c;结果一个Spot实例回收通知下来&#xff0c;节点全没了。重新拉起之后&#xff0c;最想做的事情不是骂人&#xff0c;而是赶紧把Checkpoint读回来&#xff0c;继续跑。但等你真的开始做这件事&am…

作者头像 李华
网站建设 2026/9/10 7:45:40

AI Agent跨会话记忆系统:从Redis存储到RippleMem认知重建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 7:45:30

STM32G4驱动IHM08M1电机模块实战指南

简介&#xff1a;本资源是一套基于STM32G431微控制器的电动窗帘电机控制完整工程&#xff0c;面向嵌入式开发工程师及智能硬件爱好者&#xff0c;解决直流电机精准启停、正反转与速度调节等核心控制问题&#xff0c;适用于智能家居场景下的窗帘自动化集成。压缩包含2000个文件&…

作者头像 李华
网站建设 2026/9/10 7:44:59

CANN/ge ACL算子形状推断API

aclopInferShape 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlo…

作者头像 李华
网站建设 2026/9/10 7:42:39

深入解析hermes-agent:从架构到生产落地的Agent框架实战指南

前阵子有个有意思的项目叫hermes-agent&#xff0c;名字起得很妙。Hermes 在希腊神话里是信使之神&#xff0c;干的就是传递消息、接引灵魂、协调众神之间事务的活。而这个项目做的事&#xff0c;跟这位神的职责高度重合&#xff1a;它是大模型与外部工具之间的中间层&#xff…

作者头像 李华