我要先承认一件事:这篇文章里的每个事故,都是我自己造的孽。
过去一年,我从"AI编程真香"的狂热者,变成了"AI代码必须review"的谨慎派。不是因为AI不好用——它确实帮我省了大量时间——而是因为我太信任它了,把它生成的代码没仔细看就提交了。
结果就是:5次线上事故,3次半夜被叫起来修bug,1次差点丢了绩效。
下面是5个真实案例,代码都脱敏了,但问题本身原样保留。
翻车1:N+1查询——AI不知道JPA的懒加载
场景
一个订单列表接口,要返回订单信息和关联的用户信息。我让Copilot生成代码:
// AI生成的代码——看起来没毛病
@GetMapping("/orders")
public List<OrderVO> listOrders() {
List<Order> orders = orderRepository.findAll();
return orders.stream()
.map(order -> {
OrderVO vo = new OrderVO();
vo.setOrderId(order.getId());
vo.setUserName(order.getUser().getName()); // ← 这里
vo.setAmount(order.getAmount());
return vo;
})
.toList();
}
代码很干净,逻辑也对。本地测试完全没问题——因为本地数据库就3条数据。
上了生产,订单表有40万条记录。这个接口跑起来后,数据库连接池瞬间被耗尽。
为什么炸了
order.getUser() 是JPA的懒加载关联。每遍历一个order,就会触发一次独立的SQL查询去取user。40万条订单 = 40万次SQL查询。
这就是经典的N+1问题。Copilot不知道你用的是懒加载还是急加载,它只是按"最直观的方式"写了代码。
怎么修
// 方案1: 用JOIN FETCH一次性查出
@Query("SELECT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUsers();
// 方案2: 批量查询
List<Order> orders = orderRepository.findAll();
Set<Long> userIds = orders.stream().map(Order::getUserId).collect(toSet());
Map<Long, User> userMap = userRepository.findAllById(userIds).stream()
.collect(toMap(User::getId, identity()));
// 然后在map里用userMap.get(order.getUserId())
教训
AI生成的代码在"单个对象"的场景下几乎没问题。但一旦涉及集合遍历 + 关联查询,N+1几乎是必然的。AI不理解你的ORM配置,它只按语法生成最直接的写法。
JetBrains在2026年3月发了一篇文章专门讲这个问题,标题就叫"AI-Assisted Java Application Development with Agent Skills",里面用这个Spring Data JPA的例子说明:没有显式约束,AI默认生成N+1查询。
翻车2:事务泄漏——AI不知道@Transactional的边界
场景
一个批量导入接口,每100条数据提交一次事务。我让AI写:
// AI生成的代码
@Service
public class ImportService {
@Transactional
public void importData(List<DataItem> items) {
for (int i = 0; i < items.size(); i++) {
repository.save(items.get(i));
if (i % 100 == 0) {
entityManager.flush();
entityManager.clear();
}
}
}
}
看起来没问题——每100条flush一次,避免内存堆积。AI还挺聪明,知道加clear防内存泄漏。
但线上跑了三天后,有个导入任务卡住了——数据库行锁没释放,后续所有对这张表的写操作全部阻塞。
为什么炸了
@Transactional标注在整个方法上,意味着整个导入过程在一个事务里。flush()只是把SQL发到数据库,并没有提交事务。如果导入10万条数据,整个10万条都在一个事务里,期间数据库会持有行锁,其他写操作全等着。
AI知道flush()和clear(),但它不理解事务边界和锁的关系。
怎么修
@Service
public class ImportService {
private final TransactionTemplate transactionTemplate;
public void importData(List<DataItem> items) {
// 每100条一个独立事务
Iterable<List<DataItem>> chunks = Iterables.partition(items, 100);
for (List<DataItem> chunk : chunks) {
transactionTemplate.execute(status -> {
chunk.forEach(repository::save);
return null;
});
}
}
}
教训
AI对Spring事务的理解停留在"加个注解就行"的层面。 但事务的传播行为、隔离级别、锁的范围这些,AI默认不会考虑。凡是涉及批量操作的代码,一定要人工检查事务边界。
翻车3:缓存击穿——AI的"优化"反而成了炸弹
场景
有个商品详情接口,QPS比较高。我让AI"优化一下性能",AI很贴心地加了缓存:
// AI"优化"后的代码
@GetMapping("/products/{id}")
public Product getProduct(@PathVariable Long id) {
String key = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
// 缓存没有,查数据库
product = productRepository.findById(id).orElseThrow();
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
}
return product;
}
看着很标准对吧?先查缓存,没有就查数据库,然后写入缓存。
某天大促,运营配错了缓存过期时间,导致大量商品缓存同时过期。瞬间数万请求全部打到数据库,数据库CPU飙到100%,整个服务雪崩。
为什么炸了
这就是经典的缓存击穿——大量请求同时发现缓存失效,全部穿透到数据库。AI知道加缓存,但不知道缓存失效时的保护机制。
怎么修
@GetMapping("/products/{id}")
public Product getProduct(@PathVariable Long id) {
String key = "product:" + id;
// 用双重检查锁 + 互斥锁防止击穿
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 缓存没有,用分布式锁防止并发重建
String lockKey = "lock:product:" + id;
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 拿到锁,再查一次缓存(可能其他线程已经重建了)
product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 查数据库
product = productRepository.findById(id).orElseThrow();
// 随机过期时间,防止同时失效
int ttl = 25 + ThreadLocalRandom.current().nextInt(10);
redisTemplate.opsForValue().set(key, product, ttl, TimeUnit.MINUTES);
return product;
} else {
// 没拿到锁,短暂等待后重试
Thread.sleep(50);
return getProduct(id);
}
} finally {
redisTemplate.delete(lockKey);
}
}
教训
AI加缓存是天经地义的"优化",但缓存的所有坑——击穿、穿透、雪崩——AI都不会主动帮你防。 缓存相关的代码,一定、一定要人工review。
翻车4:异常吞掉——AI觉得"catch住就行了"
场景
一个支付回调接口,AI生成的异常处理:
// AI生成的代码
@PostMapping("/payment/callback")
public ResponseEntity<String> handleCallback(@RequestBody String payload) {
try {
PaymentResult result = paymentService.processCallback(payload);
return ResponseEntity.ok("SUCCESS");
} catch (Exception e) {
log.error("处理回调失败", e);
return ResponseEntity.ok("SUCCESS"); // ← 先返回成功,防止重试
}
}
AI的逻辑是:支付回调失败也别报错,返回SUCCESS让支付平台别重试,自己内部处理。
看起来有点道理?但实际翻车了——支付服务的processCallback里有个空指针bug,在特定条件下抛NPE。这个异常被catch住了,返回了SUCCESS,支付平台以为回调成功就不再重试了。
结果:用户的钱扣了,订单状态没更新。客服那边收到一堆投诉。
为什么炸了
AI不理解业务语义——支付回调返回SUCCESS意味着"我处理好了",但实际上异常被吞了,什么都没处理。正确的做法是返回FAIL让支付平台重试。
怎么修
@PostMapping("/payment/callback")
public ResponseEntity<String> handleCallback(@RequestBody String payload) {
try {
paymentService.processCallback(payload);
return ResponseEntity.ok("SUCCESS");
} catch (BusinessException e) {
// 业务异常(如订单已处理过),返回成功
log.warn("业务异常: {}", e.getMessage());
return ResponseEntity.ok("SUCCESS");
} catch (Exception e) {
// 系统异常,返回失败让支付平台重试
log.error("系统异常,需要重试", e);
return ResponseEntity.status(500).body("FAIL");
}
}
教训
AI的异常处理策略默认是"catch住、记日志、继续跑"。 这在大部分场景没问题,但在金融、支付、订单这些有严格业务语义的场景里,异常处理策略直接影响数据一致性。这类代码必须人工把关。
翻车5:线程安全——AI不懂并发
场景
一个限流器,用AI生成:
// AI生成的代码——看起来是个不错的限流器
public class RateLimiter {
private int count = 0;
private long windowStart = System.currentTimeMillis();
private final int maxRequests;
private final long windowMillis;
public RateLimiter(int maxRequests, long windowMillis) {
this.maxRequests = maxRequests;
this.windowMillis = windowMillis;
}
public boolean allow() {
long now = System.currentTimeMillis();
if (now - windowStart > windowMillis) {
count = 0;
windowStart = now;
}
if (count < maxRequests) {
count++;
return true;
}
return false;
}
}
单线程跑完全正确。但这个限流器被用在一个网关过滤器里,多线程并发调用。
线上效果:限流完全失效。配置的是100 QPS,实际跑到了300+ QPS。
为什么炸了
count++不是原子操作。多线程同时执行到if (count < maxRequests)时,可能多个线程同时读到count == 99,然后都执行count++,返回true。限流就形同虚设了。
AI生成的代码里,没有用synchronized、没有用AtomicInteger、没有用ReentrantLock。它根本没考虑线程安全。
怎么修
// 最简单的修法:用AtomicInteger
public class RateLimiter {
private final AtomicInteger count = new AtomicInteger(0);
private volatile long windowStart = System.currentTimeMillis();
public boolean allow() {
long now = System.currentTimeMillis();
if (now - windowStart > windowMillis) {
count.set(0);
windowStart = now;
}
return count.incrementAndGet() <= maxRequests;
}
}
不过说实话,如果你真的需要限流,别自己造轮子。用Guava的RateLimiter或者Sentinel,别人早就把这些坑都填好了。
教训
AI对并发编程的理解非常浅。 它知道synchronized和AtomicInteger的语法,但不会主动判断"这段代码在多线程环境下是否安全"。涉及到共享可变状态的代码,必须人工检查线程安全。
我现在怎么用AI写代码
经历这5次翻车后,我形成了一套自己的AI编程纪律:
放心让AI做的:
- CRUD模板代码
- DTO/VO/Converter
- 单元测试(但会人工补充边界case)
- 文档和注释
- 正则表达式
必须人工review的:
- 任何涉及数据库查询的代码(N+1、事务边界)
- 任何涉及缓存的代码(击穿、穿透、雪崩)
- 任何涉及并发的代码(线程安全)
- 任何涉及钱/支付的代码(异常处理、数据一致性)
- 任何涉及安全的代码(认证、授权、加密)
不让AI碰的:
- 核心业务逻辑设计
- 架构决策
- 安全相关代码(加密算法、认证流程)
说白了就是一句话:AI是副驾驶,你才是主驾驶。方向盘不能交出去。
说了这么多翻车的事,不是为了劝你别用AI编程。恰恰相反,我每天都在用,省了大量时间。但"用"和"信"是两回事——你可以用AI生成代码,但你不能信任它生成的每一行。
工具越好用,用工具的人越要保持清醒。