坦白讲,我刚工作那两年也是这样,接口写了几百个,感觉自己什么都会,又什么都不精。直到有一次线上出了个死锁问题,盯着SHOW ENGINE INNODB STATUS的输出发呆,才意识到自己对数据库的理解有多浅。

后端的技术壁垒其实挺多,我按照自己踩坑的顺序来聊聊。


壁垒一:数据库——从会写SQL到真懂原理

大部分人对MySQL的理解停留在:建表、写CRUD、加索引。

但线上真出问题的时候,比如:

  • 两个事务为什么会死锁?

  • 明明加了索引为什么还是慢?

  • 读已提交和可重复读到底差在哪?

这些问题不懂原理根本答不上来。

几个关键知识点:

事务隔离级别不是背概念,而是理解MVCC怎么实现的。脏读、不可重复读、幻读,这三个词面试都会说,但Read View是什么?Undo Log怎么保证一致性读?快照读和当前读的区别?能讲清楚的人不多。

锁机制更是重灾区。InnoDB的行锁有三种:记录锁、间隙锁、临键锁。什么时候加什么锁,取决于索引类型和查询条件。最坑的是——没索引的UPDATE会锁全表,很多人不知道。

-- 这条SQL如果age没索引,会锁全表
UPDATE users SET status = 1 WHERE age = 25;

还有死锁排查,能在线上快速定位死锁原因的,基本都是有真功夫的:

-- 查看最近的死锁信息
SHOW ENGINE INNODB STATUS\G

-- 关键是看LATEST DETECTED DEADLOCK部分
-- 看两个事务分别持有什么锁、等待什么锁

怎么突破:找一个复杂的业务场景,把事务隔离级别、锁机制、索引原理都过一遍。推荐《MySQL技术内幕:InnoDB存储引擎》,虽然老但是经典。


壁垒二:缓存——从会用到用好

Redis谁都会用,但用好是另一回事。

面试喜欢问的三大问题:穿透、雪崩、击穿。能答上概念的一堆,真正在生产环境处理过的没几个。

穿透:请求的数据数据库里根本没有,缓存也没有,每次都打到DB。常见于恶意攻击或者业务漏洞。

方案不是只有布隆过滤器,很多场景缓存空值更实用:

public User getUser(Long id) {
    String key = "user:" + id;
    String cached = redis.get(key);
    
    // 空值也是一种缓存
    if ("NULL".equals(cached)) {
        return null;
    }
    if (cached != null) {
        return JSON.parseObject(cached, User.class);
    }
    
    User user = userDao.findById(id);
    if (user == null) {
        // 缓存空值,短过期时间
        redis.setex(key, 60, "NULL");
        return null;
    }
    redis.setex(key, 3600, JSON.toJSONString(user));
    return user;
}

缓存与数据库一致性这个问题更复杂。先更新DB还是先删缓存?延迟双删到底有没有用?最终一致性怎么保证?

说实话,在高并发场景下,没有完美方案。Cache Aside Pattern是基础,但极端情况还是会有问题。追求强一致就要上分布式锁或者订阅binlog,但这又带来新的复杂度。

怎么突破:自己搭一个场景,模拟并发更新,观察不同策略下的一致性问题。


壁垒三:消息队列——异步解耦的坑

Kafka、RocketMQ、RabbitMQ,选型会选,配置会配,但真正的难点在于:

  • 消息丢失怎么排查?

  • 消息重复消费怎么处理?

  • 消费堆积怎么解决?

  • 顺序消息在分区场景下怎么保证?

消息丢失这个问题,Kafka涉及到producer的acks配置、broker的min.insync.replicas、consumer的手动提交,任何一个环节配错都可能丢消息。

消费幂等性,说起来简单做起来难。数据库唯一键约束是最简单的方案,但不是所有业务都能加唯一键。Redis去重、状态机判断、版本号校验,各有各的适用场景。

// 简单的幂等处理
public void handleMessage(OrderMessage msg) {
    String dedupeKey = "msg:" + msg.getMessageId();
    
    // 用Redis的SETNX实现幂等
    if (!redis.setIfAbsent(dedupeKey, "1", 24, TimeUnit.HOURS)) {
        log.info("消息已处理: {}", msg.getMessageId());
        return;
    }
    
    // 业务处理...
}

怎么突破:把消息从生产到消费的完整链路画出来,每个环节可能出什么问题、怎么保证,都想清楚。


壁垒四:性能问题排查

这是区分"会写代码"和"能兜底"的分水岭。

慢SQL排查

先开慢查询日志:

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;  -- 1秒以上算慢

然后EXPLAIN分析执行计划,重点看type、key、rows、Extra这几列:

EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 1;

type从好到差:const > eq_ref > ref > range > index > ALL

Extra里出现Using filesort、Using temporary基本都要优化。

OOM排查

Java的OOM有好几种类型,最常见的是heap space。

# JVM参数,OOM时自动dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

拿到dump文件用MAT分析,看Dominator Tree和Histogram,找到占内存最多的对象。

常见原因:

  • 大集合没清理

  • 流没关闭

  • 缓存没有过期策略

  • 大查询一次性加载

CPU飙高排查

# 找到占CPU高的线程
top -Hp <pid>

# 把线程ID转成16进制
printf '%x\n' <tid>

# jstack找到对应线程
jstack <pid> | grep -A 30 <tid_hex>

看堆栈信息,一般是死循环、正则回溯、频繁Full GC这些问题。

怎么突破:这类问题只能靠实战。自己写点问题代码,模拟OOM、死锁、CPU飙高,然后练习排查。


壁垒五:分布式系统设计

这是最高的一道坎。

前面说的数据库、缓存、消息队列都是点,分布式系统设计是把这些点连成面。

几个绕不开的问题:

  • 分布式事务怎么保证?(TCC、Saga、最终一致性)

  • 分布式锁怎么实现?(Redis、ZK、数据库)

  • 限流熔断怎么做?(Sentinel、Hystrix)

  • 链路追踪怎么接入?

这些没有实际业务场景很难理解深刻。光看文章只能知道概念,真正遇到问题时还是会懵。

我的建议:找开源项目的源码看,比如Seata的AT模式怎么实现的,Redisson的分布式锁怎么处理主从切换的。比看十篇博客有用。


怎么突破这些壁垒

说几点个人体会:

1. 带着问题学,不要漫无目的

"我要学Redis"——这种想法太泛了。不如换成"我要搞清楚为什么我们的热点数据会击穿"。有具体问题驱动,学得快、记得牢。

2. 线上问题是最好的老师

每次线上出问题,不管是不是你负责的,都去看看。看别人怎么排查、怎么解决。这种实战经验比看书有用多了。

3. 输出倒逼输入

写博客、做分享。当你试图给别人讲清楚一个概念的时候,你会发现自己很多地方其实没想透。

4. 不要只看Java

后端不只是Java。MySQL、Redis、Kafka、Linux命令、网络协议,这些基础才是真正的护城河。语言只是工具,解决问题的能力才是核心竞争力。


最后

"缺乏对系统复杂性的认知"。

这个认知不是看书能获得的,是被坑出来的。

被死锁坑过,你才会认真学锁机制;被OOM坑过,你才会关注内存管理;被缓存击穿坑过,你才会设计更健壮的方案。

所以别焦虑,继续写业务代码也行,但遇到问题的时候多往深处挖一挖。壁垒不是一天突破的,但每次深挖都是在积累。

更多推荐