今天差点被气笑了,真想把那位的键盘给扬了。上午十点多,业务部门突然炸锅,说用户访问速度慢的像蜗牛爬,卡的让人怀疑人生。第一反应肯定是网络问题,这年头,网络就是背锅侠,有问题先甩锅网络。
赶紧登录监控平台,一看,CPU、内存、磁盘IO,一切正常,K8S集群里的Pod也都活蹦乱跳的。网络带宽使用率也平稳的很,根本没啥异常流量。 心里嘀咕,这网络这次真是躺枪了。
然后开始抓包,tcpdump抓起来,Wireshark 分析走起。 盯着屏幕看了半天,发现大量的TCP Retransmission,重传率高的吓人。这下有点意思了,网络虽然没拥塞,但数据包丢的厉害,导致TCP疯狂重传,用户体验能好才怪。
顺着IP地址,一路追踪,发现问题集中在某一个特定的微服务上。 登录服务器,查看日志,满屏的Exception。定睛一看,尼玛, NullPointerException, 空指针异常!
代码大致如下:
public class OrderService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Order getOrder(String orderId) {
// 从Redis获取订单信息
Order order = (Order) redisTemplate.opsForValue().get("order:" + orderId);
// 如果Redis中没有,则从数据库获取
if (order == null) {
order = orderRepository.findById(orderId).orElse(null);
// 尼玛,这里没判断就直接用了!
redisTemplate.opsForValue().set("order:" + orderId, order, 60, TimeUnit.SECONDS);
}
return order;
}
}
我瞬间血压就上来了。 Redis做缓存,数据库做持久化没问题。 从数据库查出来的数据,如果是空,你倒是判断一下啊! 直接把一个 null 值塞到 Redis 里面,下次再来查,永远都是 null,然后每次都去数据库查,每次都是 null,然后又往 Redis 里面塞 null,死循环了! 这不是自己给自己挖坑吗?
更可气的是,这家伙为了所谓的“性能优化”,给 Redis 设置了 60 秒的过期时间。 这下好了,每隔60秒,缓存失效,大量的请求涌向数据库,数据库扛不住了,连接数被打满,开始丢包。 TCP 重传率飙升,最终用户体验直线下降。
这已经不是初级程序员才会犯的错误了,这种连最基本的空指针判断都忘记的,我真怀疑他是怎么混进来的。 这就好比开车忘记系安全带,还猛踩油门,出事是必然的。
最后,我把代码改成了下面这样:
public class OrderService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Order getOrder(String orderId) {
// 从Redis获取订单信息
Order order = (Order) redisTemplate.opsForValue().get("order:" + orderId);
// 如果Redis中没有,则从数据库获取
if (order == null) {
order = orderRepository.findById(orderId).orElse(null);
// 尼玛,这里做一下判断!
if (order != null) {
redisTemplate.opsForValue().set("order:" + orderId, order, 60, TimeUnit.SECONDS);
}
}
return order;
}
}
简单粗暴,加个判断就完事了。 然后重启服务, 清空Redis缓存。 一切恢复正常,用户又可以愉快的剁手了。
技术结论:
这次事故再次证明了,基础知识的重要性。 空指针异常这种低级错误,绝对不应该出现在生产环境中。 同时,缓存的使用一定要谨慎,避免缓存穿透、缓存击穿等问题。 对于不存在的数据,可以考虑在缓存中设置一个特殊的标记(例如一个特殊的字符串或者一个特定的对象),避免大量的请求穿透到数据库。 另外,完善的监控体系是必不可少的,能够帮助我们及时发现问题,避免损失扩大。
哎,希望以后能少碰到这种让人哭笑不得的事情。 运维不易,且行且珍惜啊!