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. 秒杀系统架构
核心设计要点
- Redis预减库存:将库存加载到Redis,使用Lua脚本原子性操作
- 请求限流:使用Nginx限流+Guava令牌桶+Sentinel限流
- 异步下单:Kafka削峰填谷,异步处理订单
- 数据一致性:最终一致性,定时任务对账
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频繁排查思路
- 分析GC日志:查看Full GC触发原因
- 内存泄漏排查:使用MAT分析堆dump文件
- 代码层面:检查是否有大对象、长时间持有的对象引用
- 参数调优:调整堆大小、GC算法参数
总结
通过谢飞机的面试经历,我们可以看到Java面试中的核心考点主要集中在:
- 并发编程:分布式锁、线程安全、并发工具
- 缓存设计:Redis高级应用、缓存策略
- 分布式系统:消息队列、分布式事务、微服务
- 性能调优:JVM调优、监控告警
- 架构设计:高并发、高可用、可扩展性
对于准备Java面试的同学来说,不仅要知其然,更要知其所以然。多写代码,多思考原理,才能在面试中脱颖而出!
本文纯属虚构,如有雷同,纯属巧合。祝各位读者面试顺利,offer多多!