最近临时处理一批 Redis 数据,需要根据数据库查出的客户 ID,检查 Redis 中的 last_ipaddr 是否命中指定 IP。
几个月前 C家AI 已经帮我写过一个 Redis 查询脚本,原来的使用场景只是偶尔查一个客户。代码能正常运行,逻辑看起来也比较清晰,所以这次有批量查询需求时,我几乎没有多想,直接把原来的查询函数拿过来循环调用。
结果很反直觉:慢得离谱。
刚开始我怀疑的是一些很常见的问题。
是不是 print 太多?
是不是 Python 循环本身太慢?
是不是某些日志输出拖慢了执行?
于是我把大量打印和一些不必要的循环逻辑注释掉,再重新跑。
速度几乎没有改善。
这时才开始真正往查询函数内部看。
问题根本不在 Python 循环
继续检查 query_key() 后,我发现了真正的问题:
每查询一个客户,函数内部都会重新执行一次 RedisCluster(...)。
也就是说,代码大致相当于:
for customer_id in customer_ids:
redis_client = RedisCluster(...)
query_key(redis_client, customer_id)
而不是:
redis_client = RedisCluster(...)
for customer_id in customer_ids:
query_key(redis_client, customer_id)
看起来只差了一个位置,性能上的意义却完全不同。
每次重新初始化 RedisCluster 客户端,都可能涉及连接池、集群拓扑、槽位信息等初始化工作。
对于偶尔查询一个客户,这些固定成本几乎感觉不到。
但如果要查询几万甚至十万个客户,就相当于把原本只应该发生一次的初始化动作,重复执行了几万、十万次。
相比真正读取 Redis 数据的时间,这些初始化成本反而成了主要开销。
所以即使把 print 全部关闭,速度也不会有本质改善。
真正慢的不是“输出太多”,而是整个 Redis 客户端的生命周期设计错了。
单次查询–做了太多事情
继续看原来的函数,还发现了第二层问题。
之前为了把它写成一个比较完整、通用的 Redis 查询工具,单次查询大致会依次执行:
EXISTS
→ TYPE
→ HGETALL
先判断 Key 是否存在,再判断类型,最后把整个 Hash 全部读取出来。
在手工排查某一个客户的时候,这种代码其实很好用。
因为你不知道 Key 是不是存在,也不知道里面具体有什么内容,完整读取出来更方便观察。
但这次业务需求其实非常明确:
我只需要知道
last_ipaddr是什么。
那就完全没有必要把整个 Hash 取回来。
直接:
HGET key last_ipaddr
就够了。
这也是一个很典型的问题:
单次查询时,“多做一点”几乎没有成本;到了批量场景,每一个多余操作都会乘以数据量。
一次多两三个 Redis 请求,看起来无所谓。
十万条数据时,就是几十万次额外操作。
最后的修改
确定问题后,修改方案反而很简单。
第一,把 Redis Cluster 客户端调整成按集群只初始化一次,后续所有查询持续复用同一个客户端及其连接池。
第二,针对当前业务,不再调用原来完整的 query_key(),而是直接读取:
HGET key last_ipaddr
第三,对于更大规模的批量查询,再使用 Pipeline 分批执行,进一步减少大量独立网络往返。
调整以后,性能提升非常明显。
最终处理 10 万个客户 ID 的 Redis 查询不到 1 分钟。
问题解决以后,再回头看,其实这些优化都不是什么高深技术。
Redis 客户端要复用、只取需要的字段、大批量操作考虑 Pipeline,这些都是很常规的工程实践。
真正让我觉得值得记录的,反而是另外一个问题。
AI 写代码以后,我越来越容易把代码当成黑盒
以前自己写代码时,一个函数为什么这么设计,客户端在哪里初始化,循环里做了哪些事情,通常都是自己一点点写出来的。
所以即使代码写得不漂亮,至少对它的运行过程是有感觉的。
现在不一样了。
很多代码是直接描述需求,让 AI 生成。
代码出来以后,我首先验证的是:
输入对不对?
输出对不对?
功能能不能跑?
如果三者都没有问题,很容易就继续往下用了。
因为 AI 的价值本来就是减少执行层面的工作。
如果每一段 AI 生成的代码,我都重新逐行理解、重新推导一遍,那么效率优势也会被抵消很多。
而且随着 AI 写出来的代码越来越多,不同时间生成的代码结构、变量命名和实现风格也不完全一致。
理解和维护整个代码库的成本,会随着规模增加得非常快。
所以一种很自然的使用习惯开始形成:
我只要结果,过程只要暂时没有出问题,就先不管。
这其实很合理。
但这次 Redis 的问题让我意识到,这种习惯有一个风险:
AI 很容易给出“局部正确”的代码,而局部正确不等于放大以后仍然正确。
原来的代码其实没有错
现在回头看,我甚至不能简单地说 C家AI 当时生成的代码是“错的”。
原来的需求就是查一个 Redis Key。
在单次查询场景里:
- 每次初始化一个 Redis 客户端,能运行;
- 先
EXISTS; - 再
TYPE; - 最后
HGETALL;
这些设计虽然不够高效,但完全可以接受。
如果一天只是手工查几个客户,根本不会有什么实际影响。
问题出现在场景发生了变化。
从:
帮我查一个客户的 Redis 数据。
变成了:
我要查10万个客户。
这两句话从业务逻辑上看,好像只是把“一次”变成了“很多次”。
但从工程实现上,它们已经不是同一个需求了。
连接生命周期、网络往返次数、内存占用、批处理方式,全部开始变得重要。
所以这次真正的问题,与其说是:
AI 没把代码写好。
不如说是:
我只告诉了 AI“要做什么”,没有告诉它“这个代码会怎么被使用”。
而数据规模,本身就是需求的一部分。
“让 AI 把代码写得更通用”可能也不是正确答案
一开始我的反思是:
以后让 AI 写代码的时候,应该尽可能要求它考虑通用性。
但再想一下,这个结论其实也不太对。
因为“通用”本身是有代价的。
如果我只是临时查一个 Redis Key,却要求 AI 一开始就设计:
- 客户端生命周期管理;
- Pipeline;
- 批量接口;
- 重试;
- 超时;
- 集群异常处理;
- 日志;
- 并发;
- 性能统计;
很可能又走向另一个极端:过度设计。
一个只用两次的小脚本,最后变成一个小型 Redis SDK。
AI 尤其容易做这种事情。
你让它“考虑全面一点”,它真的可以考虑得非常全面。
结果可能不是更好维护,而是代码越来越重。
所以我现在更倾向于另外一种做法:
不是要求 AI 永远写得通用,而是在提需求时,把使用规模和边界告诉它。
例如,不只是说:
写一个函数,根据 customer_id 查询 Redis 中的 last_ipaddr。
而是说:
这个函数后续需要对数据库查出的约 10 万个 customer_id 执行查询,Redis 是 Cluster。请避免在循环中重复初始化客户端,并考虑减少网络往返;当前只需要读取
last_ipaddr字段。
这时候 AI 得到的已经不是单纯的“功能需求”,而是一个带运行环境和规模约束的工程需求。
最后生成出来的代码,大概率从一开始就会完全不同。
AI Coding 之后,可能需要换一种代码审查方式
这次经历以后,我并没有得出一个结论:
以后 AI 写的代码,我一定要逐行审核。
我觉得这也不现实。
如果最终还是所有东西都自己逐行检查,AI Coding 的意义会下降很多。
但我会开始重点关注一些放大以后特别容易出问题的地方。
比如看到:
RedisCluster(...)
我会问一句:
这个对象是在循环外创建,还是循环内创建?
看到数据库连接:
每条数据重新连接数据库了吗?
看到 HTTP 请求:
Session有没有复用?
看到文件操作:
是不是每处理一条数据都重新打开一次大文件?
看到模型调用:
一百条数据是不是就调用一百次 API?能不能批量?
看到完整对象读取:
我真的需要整个对象,还是只需要其中一个字段?
我不会重新研究每一行代码。
但对于连接、网络、数据库、文件 IO、线程池、模型 API 这类有明显资源成本和生命周期的对象,会额外多看一眼。
还有一个问题也值得固定问一下:
如果数据量从 1 条变成 10 万条,这段代码还成立吗?
这个问题,可能比“代码有没有 Bug”更适合 AI Coding 时代。
最后
AI 的确让我越来越少关注代码是怎么一行一行写出来的。
以前自己写代码时,很多过程其实不需要刻意理解。变量为什么放这里、连接在哪里建立、一个函数会调用几次,因为代码就是自己一点点写出来的,这些东西天然存在于脑子里。
现在这个过程被 AI 接管了。
我描述需求,AI 给出代码,我运行,检查结果。如果输入正确、输出正确,很多时候这段代码就会继续被使用。
从效率上来说,这其实没什么问题。
如果用了 AI 以后,我还要求自己把所有生成代码重新逐行推导一遍,那某种程度上又把 AI 带来的效率还回去了。
所以我并不觉得“只关心结果”本身是一件坏事。
真正的变化可能是:过去我们通过亲自写代码,自然而然地理解过程;现在过程被 AI 隐藏以后,这种理解不再自动发生了。
这次 Redis 查询只是一个很小的例子。
代码一直能运行,结果也一直是对的。直到使用场景从查一个客户变成查十万个客户,之前被隐藏起来的实现方式才突然变成问题。
它让我意识到,AI Coding 之后,“能跑”和“我理解它为什么这样跑”正在逐渐变成两件不同的事情。
而工程师可能也需要接受这种变化。
我们未必还需要像以前一样掌握每一行实现,但至少要知道:哪些地方自己已经交给了 AI,哪些假设自己其实从来没有验证过。
AI 写得越多,代码本身可能越像黑盒。
人的工作也会越来越从“亲手完成过程”,转向“定义问题、判断结果,以及决定什么时候必须打开这个黑盒”。
我现在依然更愿意让 AI 去写代码。
只是下一次当一段代码从“能用”变成“要真正拿来用”的时候,我大概会更有意识地提醒自己:
结果正确,不代表过程已经不重要。只是以前过程是自己写出来的,现在需要自己决定,什么时候值得重新看进去。