Java大厂面试:谢飞机的电商系统架构面试实录

Java大厂面试:谢飞机的电商系统架构面试实录

面试场景

面试官 :某头部互联网电商公司架构师张工,严肃专业 求职者:谢飞机,自称3年Java经验的水货程序员

面试室内,张工推了推眼镜,看着对面穿着格子衫、表情紧张的谢飞机...


第一轮提问:基础架构与缓存设计

张工:谢飞机是吧?看你简历说做过电商项目,我来问几个基础问题。你在电商系统中是如何设计商品缓存的?

谢飞机:呃...缓存...我们用的是Redis,就是把商品信息放到Redis里,这样就快了!

张工:嗯,思路不错。那具体是如何解决缓存穿透、缓存击穿、缓存雪崩这三大问题的?

谢飞机:缓存穿透...就是...就是没数据的时候不让它查数据库!我们用了个空值缓存!缓存击穿是热点数据...那个...我们加了互斥锁!雪崩就是批量过期...我们设置了随机过期时间!

张工:(点点头)回答得还可以,能想到解决方案。那你给我写个Redis分布式锁的实现代码吧。

谢飞机:好的好的!(开始敲代码)

java 复制代码
@Component
public class RedisLock {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public boolean tryLock(String key, String value, long expireTime) {
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }
    
    public void unlock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(key), value);
    }
}

张工:代码写得还行,能想到用Lua脚本保证原子性。那分布式锁的可重入性怎么实现?

谢飞机:可重入...这个...就是同一个线程可以多次获取锁?呃...我们可以用计数器...具体实现我...

张工:(打断)行,下一个问题。你们电商系统的库存扣减是怎么做的?


第二轮提问:高并发与消息队列

张工:说到库存,秒杀场景下10万用户同时抢1000件商品,你的架构设计是怎样的?

谢飞机:秒杀啊!这个我知道!我们用了Redis预减库存,Kafka异步处理订单,限流+缓存+异步!

张工:具体说说Redis预减库存的实现细节。

谢飞机:就是把库存数量存到Redis里,用decrement操作,如果结果大于0就说明抢到了,小于0就回滚!

张工:那Redis和数据库的库存如何保证一致性?

谢飞机:这个...就是...用消息队列异步同步...Kafka消费者去更新数据库库存...

张工:如果消息队列宕机了怎么办?消息丢失了怎么办?重复消费了怎么办?

谢飞机:宕机了...我们有集群部署!消息丢失用持久化!重复消费用幂等性处理!就是...

张工:(皱眉)具体怎么实现幂等性?

谢飞机:幂等性...就是...用唯一ID!对,用订单号做唯一键,数据库设唯一约束!

张工:那分布式事务呢?下单涉及订单库、库存库、积分库,如何保证数据一致性?

谢飞机:分布式事务...这个...我们用的是...TCC?Try-Confirm-Cancel?呃...还有Saga模式...

张工:你说TCC,那TCC的空回滚和幂等性处理你是怎么做的?

谢飞机:空回滚...就是...Try阶段没执行就直接Cancel...我们需要记录一下执行状态...具体代码我...


第三轮提问:微服务架构与监控

张工:看你的项目用了微服务架构,服务间调用是怎么做的?

谢飞机:我们用Spring Cloud!Feign调用!Nacos做注册中心!

张工:那服务降级、熔断、限流是怎么配置的?

谢飞机:我们用Hystrix!哦不,是Sentinel!设置规则,超过阈值就熔断!

张工:给我写个Sentinel的限流配置示例。

谢飞机:好的!(再次敲代码)

java 复制代码
@RestController
public class OrderController {
    
    @GetMapping("/create")
    @SentinelResource(value = "createOrder", 
            blockHandler = "createOrderBlockHandler",
            fallback = "createOrderFallback")
    public Result createOrder(@RequestParam String userId, 
                            @RequestParam String productId) {
        // 业务逻辑
        return orderService.createOrder(userId, productId);
    }
    
    // 限流处理
    public Result createOrderBlockHandler(String userId, String productId, BlockException ex) {
        return Result.error("系统繁忙,请稍后再试");
    }
    
    // 降级处理
    public Result createOrderFallback(String userId, String productId, Throwable ex) {
        return Result.error("服务降级,请稍后再试");
    }
}

张工:配置代码写得可以。那你们的链路追踪是怎么做的?

谢飞机:链路追踪!我们用的是Sleuth+Zipkin!每个请求都有traceId和spanId!

张工:那如果跨多个微服务的调用链路很长,如何快速定位问题?

谢飞机:这个...就是看日志!用ELK收集日志,根据traceId查询!还有监控...

张工:监控指标都关注哪些?如何设置告警阈值?

谢飞机:CPU、内存、QPS、响应时间!设置阈值,超过就发告警!钉钉通知!

张工:那JVM的GC监控你是怎么做的?出现Full GC频繁如何排查?

谢飞机:GC监控...用jstat!还有jvisualvm!Full GC频繁就是...内存泄漏?或者堆太小?

张工:具体的排查思路和命令能说一下吗?

谢飞机:呃...jstat -gcutil看GC情况,jmap dump堆内存,用MAT分析...具体参数我...


面试结束

张工:(合上面试记录)好了,今天的面试就到这里。你的基础知识还可以,但在一些深层次的原理和实践经验上还需要加强。你先回去等通知吧。

谢飞机:(松了口气)好的好的!谢谢张工!我会继续学习的!

谢飞机走出面试室,擦了擦额头的汗,心里想着:"还好没露馅,但感觉自己就是个水货啊..."


技术知识点详解

1. Redis缓存三大问题

缓存穿透

场景:大量请求查询不存在的数据,绕过缓存直接访问数据库。

解决方案

  • 空值缓存:将null值也缓存起来,设置较短的过期时间
  • 布隆过滤器:在缓存前加一层布隆过滤器,快速判断key是否存在
java 复制代码
public Product getProductById(Long productId) {
    // 先查缓存
    Product product = redisTemplate.opsForValue().get("product:" + productId);
    if (product != null) {
        return product;
    }
    
    // 布隆过滤器检查
    if (!bloomFilter.mightContain(productId)) {
        return null;
    }
    
    // 查数据库
    product = productMapper.selectById(productId);
    if (product != null) {
        redisTemplate.opsForValue().set("product:" + productId, product, 30, TimeUnit.MINUTES);
    } else {
        // 缓存空值,防止穿透
        redisTemplate.opsForValue().set("product:" + productId, "NULL", 5, TimeUnit.MINUTES);
    }
    return product;
}
缓存击穿

场景:热点key过期瞬间,大量并发请求直接打到数据库。

解决方案

  • 互斥锁:只允许一个线程重建缓存
  • 逻辑过期:永不过期,后台线程定期更新
缓存雪崩

场景:大量key同时过期,或Redis宕机导致数据库压力骤增。

解决方案

  • 随机过期时间:避免同时过期
  • 多级缓存:本地缓存+Redis缓存
  • 高可用部署:Redis集群+哨兵模式

2. Redis分布式锁

可重入分布式锁实现
java 复制代码
public class ReentrantRedisLock {
    private static final String LOCK_PREFIX = "lock:";
    private static final long DEFAULT_EXPIRE = 30L;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private final ThreadLocal<Map<String, Integer>> lockCounts = ThreadLocal.withInitial(HashMap::new);
    
    public boolean lock(String key) {
        return lock(key, DEFAULT_EXPIRE);
    }
    
    public boolean lock(String key, long expireTime) {
        String lockKey = LOCK_PREFIX + key;
        String threadId = Thread.currentThread().getId() + "";
        
        // 检查是否已持有锁
        Map<String, Integer> counts = lockCounts.get();
        if (counts.containsKey(lockKey)) {
            counts.put(lockKey, counts.get(lockKey) + 1);
            return true;
        }
        
        // 尝试获取锁
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, threadId, expireTime, TimeUnit.SECONDS);
        
        if (Boolean.TRUE.equals(success)) {
            counts.put(lockKey, 1);
            return true;
        }
        return false;
    }
    
    public void unlock(String key) {
        String lockKey = LOCK_PREFIX + key;
        Map<String, Integer> counts = lockCounts.get();
        
        if (!counts.containsKey(lockKey)) {
            throw new IllegalStateException("未持有该锁");
        }
        
        int count = counts.get(lockKey);
        if (count > 1) {
            counts.put(lockKey, count - 1);
            return;
        }
        
        // 释放锁
        String threadId = Thread.currentThread().getId() + "";
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), 
                            Collections.singletonList(lockKey), threadId);
        counts.remove(lockKey);
    }
}

3. 秒杀系统架构

核心设计要点
  1. Redis预减库存:将库存加载到Redis,使用Lua脚本原子性操作
  2. 请求限流:使用Nginx限流+Guava令牌桶+Sentinel限流
  3. 异步下单:Kafka削峰填谷,异步处理订单
  4. 数据一致性:最终一致性,定时任务对账
Redis预减库存实现
java 复制代码
@Service
public class SeckillService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String SECKILL_SCRIPT = """
        local key = KEYS[1]
        local userId = ARGV[1]
        local orderId = ARGV[2]
        
        -- 检查用户是否已购买
        if redis.call('sismember', key .. ':users', userId) == 1 then
            return 0
        end
        
        -- 预减库存
        local stock = redis.call('decr', key .. ':stock')
        if stock < 0 then
            redis.call('incr', key .. ':stock')
            return 0
        end
        
        -- 标记用户已购买
        redis.call('sadd', key .. ':users', userId)
        redis.call('rpush', key .. ':orders', orderId)
        
        return 1
    """;
    
    public boolean seckill(Long productId, String userId, String orderId) {
        String key = "seckill:" + productId;
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(SECKILL_SCRIPT, Long.class);
        
        Long result = redisTemplate.execute(script, 
            Collections.singletonList(key), userId, orderId);
        
        return result != null && result == 1;
    }
}

4. TCC分布式事务

TCC核心概念
  • Try阶段:预留资源,检查业务规则
  • Confirm阶段:确认执行业务操作
  • Cancel阶段:取消操作,释放资源
空回滚处理
java 复制代码
@Component
public class SeckillTccService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // Try阶段
    @Transactional
    public boolean tryCreateOrder(SeckillOrder order) {
        String key = "tcc:order:" + order.getOrderId();
        
        // 记录事务状态
        redisTemplate.opsForValue().set(key + ":status", "TRY", 24, TimeUnit.HOURS);
        
        // 预扣库存
        String stockKey = "seckill:" + order.getProductId() + ":stock";
        Long stock = redisTemplate.opsForValue().decrement(stockKey);
        
        if (stock < 0) {
            // 库存不足,回滚
            redisTemplate.opsForValue().increment(stockKey);
            redisTemplate.opsForValue().set(key + ":status", "CANCELLED", 24, TimeUnit.HOURS);
            return false;
        }
        
        // 记录预扣库存数量
        redisTemplate.opsForValue().set(key + ":stock", "1", 24, TimeUnit.HOURS);
        return true;
    }
    
    // Confirm阶段
    public boolean confirmCreateOrder(String orderId) {
        String key = "tcc:order:" + orderId;
        String status = redisTemplate.opsForValue().get(key + ":status");
        
        // 幂等性检查
        if (!"TRY".equals(status)) {
            return false;
        }
        
        // 确认订单
        // 这里可以插入数据库等操作
        
        // 更新状态
        redisTemplate.opsForValue().set(key + ":status", "CONFIRMED", 24, TimeUnit.HOURS);
        return true;
    }
    
    // Cancel阶段
    public boolean cancelCreateOrder(String orderId) {
        String key = "tcc:order:" + orderId;
        String status = redisTemplate.opsForValue().get(key + ":status");
        
        // 空回滚处理:如果Try阶段未执行,直接返回成功
        if (status == null || "CANCELLED".equals(status) || "CONFIRMED".equals(status)) {
            return true;
        }
        
        // 恢复库存
        String stockKey = "tcc:order:" + orderId + ":stock";
        String stockStr = redisTemplate.opsForValue().get(stockKey);
        if (stockStr != null) {
            String productId = orderId.split(":")[0];
            redisTemplate.opsForValue().increment("seckill:" + productId + ":stock", Integer.parseInt(stockStr));
        }
        
        // 更新状态
        redisTemplate.opsForValue().set(key + ":status", "CANCELLED", 24, TimeUnit.HOURS);
        return true;
    }
}

5. JVM调优与监控

GC监控命令
bash 复制代码
# 查看GC情况
jstat -gcutil <pid> 1000 10

# 查看堆内存分布
jmap -heap <pid>

# 导出堆内存文件
jmap -dump:format=b,file=heap.hprof <pid>

# 查看线程堆栈
jstack <pid> > thread_dump.txt
GC调优参数
bash 复制代码
# 针对高并发场景的JVM参数
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:+G1UseAdaptiveIHOP
-XX:G1MixedGCCountTarget=8
-XX:InitiatingHeapOccupancyPercent=45
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=10M
Full GC频繁排查思路
  1. 分析GC日志:查看Full GC触发原因
  2. 内存泄漏排查:使用MAT分析堆dump文件
  3. 代码层面:检查是否有大对象、长时间持有的对象引用
  4. 参数调优:调整堆大小、GC算法参数

总结

通过谢飞机的面试经历,我们可以看到Java面试中的核心考点主要集中在:

  1. 并发编程:分布式锁、线程安全、并发工具
  2. 缓存设计:Redis高级应用、缓存策略
  3. 分布式系统:消息队列、分布式事务、微服务
  4. 性能调优:JVM调优、监控告警
  5. 架构设计:高并发、高可用、可扩展性

对于准备Java面试的同学来说,不仅要知其然,更要知其所以然。多写代码,多思考原理,才能在面试中脱颖而出!

本文纯属虚构,如有雷同,纯属巧合。祝各位读者面试顺利,offer多多!

相关推荐
GEM的左耳返4 天前
Java面试实战:从Spring Boot到AI集成的技术深度挑战
spring boot·redis·微服务·kafka·java面试·spring ai·缓存优化
努力发光的程序员6 天前
互联网大厂Java面试:技术点与场景结合详解
java面试·支付安全·微服务监控·电商搜索·技术场景
努力发光的程序员12 天前
互联网大厂Java面试:从Spring Boot到大数据处理的实战场景问题解析
spring boot·微服务·云原生·java面试·大数据处理·技术解析·互联网求职
Java爱好狂.15 天前
2025全年Java面试真题总结!
java·jvm·高并发·多线程·java面试·后端开发·java八股文
无心水20 天前
【分布式利器:RocketMQ】2、RocketMQ消息重复?3种幂等方案,彻底解决重复消费(附代码实操)
网络·数据库·rocketmq·java面试·消息幂等·重复消费·分布式利器
陈果然DeepVersion24 天前
Java大厂面试真题:从Spring Boot到AI微服务的三轮技术拷问
spring boot·redis·微服务·ai·智能客服·java面试·rag
陈果然DeepVersion25 天前
Java大厂面试真题:从Spring Boot到AI微服务的三轮技术拷问(二)
spring boot·redis·spring cloud·微服务·ai·java面试·rag