Java大厂面试实录:从秒杀系统设计到微服务架构,谢飞机的逆袭之路
Java大厂面试实录:从秒杀系统设计到微服务架构,谢飞机的逆袭之路
本文通过一场真实的互联网大厂Java面试,带你深入理解电商秒杀系统的技术实现,涵盖Spring Boot、Redis、Kafka、分布式事务等核心技术点。
前言
这是一个阳光明媚的上午,在北京某知名互联网公司的会议室里,面试官李工正襟危坐,对面坐着一位略显紧张的求职者——谢飞机。谢飞机,自称"Java全栈工程师",实则刚转行半年的菜鸟,今天要面对的是一场关于电商秒杀系统的技术面试。
第一轮提问:基础架构与核心技术
面试官李工:谢飞机,我看你简历上写熟悉Spring Boot和微服务架构。我先问你几个基础问题。首先,Spring Boot的自动装配原理是什么?
谢飞机:(挠挠头)呃,这个我知道!Spring Boot启动的时候,会扫描classpath下的META-INF/spring.factories文件,里面配置了很多自动配置类,然后根据条件注解@Conditional来决定是否加载这些配置。对吧?
面试官李工:(点头)嗯,说得还可以。那Spring Boot的启动流程呢?
谢飞机:(眼睛一亮)这个我也知道!首先创建SpringApplication对象,然后调用run()方法,run()方法里面会准备环境、创建ApplicationContext、刷新上下文,最后启动应用。主要步骤就是prepareEnvironment()、createApplicationContext()、refreshContext()这些。
面试官李工:(微笑)不错,基础还可以。那我再问你,Redis的持久化机制有哪些?RDB和AOF有什么区别?
谢飞机:(自信满满)RDB是快照持久化,定时把内存数据保存到磁盘文件里;AOF是追加文件持久化,记录每个写操作命令。RDB恢复快但可能丢失数据,AOF数据更安全但文件大、恢复慢。一般可以配合使用,RDB做备份,AOF做主持久化。
面试官李工:(赞许地)回答得很好!那Redis的缓存穿透、缓存击穿、缓存雪崩怎么解决?
谢飞机:(开始紧张)呃...这个...缓存穿透是查不存在的数据,可以用布隆过滤器或者缓存空值解决。缓存击穿是热点key过期,可以用互斥锁或者热点key永不过期。缓存雪崩是大量key同时过期,可以设置随机过期时间...后面...后面我有点记不清了...
面试官李工:(温和地)没关系,继续思考一下。那分布式锁的实现方式有哪些?
谢飞机:(擦汗)分布式锁...可以用Redis的SET NX EX命令实现,也可以用Zookeeper的临时顺序节点,还有数据库的乐观锁...具体实现细节...我需要再看看文档...
第二轮提问:秒杀系统设计与高并发处理
面试官李工:好的,我们进入正题。假设我们要设计一个电商秒杀系统,你会怎么设计整体架构?
谢飞机:(深吸一口气)秒杀系统...嗯...首先需要前后端分离,前端用Nginx做静态资源服务。后端用Spring Cloud微服务架构,包括用户服务、商品服务、订单服务、库存服务。用Redis做缓存,Kafka做消息队列,MySQL做持久化存储。还需要考虑限流、降级、熔断...
面试官李工:(点头)思路是对的。那秒杀系统如何防止超卖?
谢飞机:(犹豫)超卖...可以用Redis的原子操作扣减库存,比如decr命令。然后在数据库层面用乐观锁,更新库存时检查version字段。还有就是用消息队列异步处理订单,避免数据库压力过大...具体怎么实现...我想想...
面试官李工:(引导)Redis的decr是原子的,但是如何保证Redis和MySQL的数据一致性?
谢飞机:(挠头)这个...数据一致性...可以用最终一致性,先扣Redis库存,然后发消息到Kafka,消费者异步扣MySQL库存。如果失败了就重试...或者用分布式事务...分布式事务...这个我了解不多...
面试官李工:那秒杀系统如何限流?有哪些限流算法?
谢飞机:(有点慌)限流...可以用令牌桶算法、漏桶算法、固定窗口、滑动窗口...令牌桶是匀速放令牌,请求来了拿令牌,没令牌就拒绝。漏桶是匀速处理请求...具体代码实现...我没写过...
面试官李工:(继续追问)那如何实现热点数据的自动发现和缓存预热?
谢飞机:(彻底慌了)热点数据...自动发现...呃...可以统计访问频率,超过阈值的就自动缓存...缓存预热就是系统启动时提前加载热点数据到Redis...具体怎么做...我...我需要回去再学习一下...
第三轮提问:分布式事务与系统优化
面试官李工:好的,谢飞机。我再问你几个深入的问题。分布式事务有哪些解决方案?
谢飞机:(满头大汗)分布式事务...有2PC两阶段提交、3PC三阶段提交、TCC补偿事务、本地消息表、事务消息、Saga模式...2PC是准备阶段和提交阶段,有阻塞和单点故障的问题...TCC需要实现Try、Confirm、Cancel三个接口...具体怎么选型...这个我了解得不够深入...
面试官李工:那Spring Cloud Alibaba的Seata框架了解吗?
谢飞机:(小声)Seata...听说过...好像是阿里开源的分布式事务框架...支持AT模式、TCC模式、Saga模式、XA模式...AT模式是对业务无侵入的...具体原理...我没研究过...
面试官李工:(严肃地)MySQL的MVCC机制了解吗?它是如何实现可重复读的?
谢飞机:(结巴)MVCC...多版本并发控制...通过Undo Log保存历史版本,Read View判断可见性...可重复读...就是在事务开始时生成一个Read View,整个事务期间都用这个Read View...具体实现细节...我...我记不清了...
面试官李工:最后一个问题,如何对秒杀系统进行性能监控和故障排查?
谢飞机:(放弃抵抗)监控...可以用Prometheus+Grafana监控指标,ELK收集日志,SkyWalking做链路追踪...故障排查就是看日志、看监控、查慢SQL...具体怎么分析...我经验不足...
面试结束
面试官李工:(合上简历,微笑着)谢飞机,感谢你今天来参加面试。你的基础还可以,但在分布式系统和高并发方面的经验还需要加强。我们今天先到这里,你回去等通知吧。
谢飞机:(如释重负)好的,谢谢李工!我会继续学习的!
面试题详解
第一轮:基础架构与核心技术
1. Spring Boot自动装配原理
业务场景:在快速开发中,我们希望引入一个starter就能自动配置好相关组件,减少繁琐的手动配置。
技术点:
- Spring Boot启动时会扫描classpath下所有jar包的
META-INF/spring.factories文件 - 该文件中配置了自动配置类的全限定名,如
EnableAutoConfiguration - Spring Boot根据条件注解(@Conditional)判断是否加载对应的自动配置类
- 常用条件注解包括:
@ConditionalOnClass:classpath存在指定类时生效@ConditionalOnMissingBean:容器中没有指定Bean时生效@ConditionalOnProperty:配置文件中指定属性满足条件时生效
代码示例:
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
2. Redis持久化机制
业务场景:电商系统中商品信息、用户会话等数据需要持久化,防止Redis重启后数据丢失。
技术点:
RDB(Redis Database):
- 工作原理:fork子进程,将内存数据快照写入临时文件,完成后替换原RDB文件
- 配置参数:save 900 1(900秒内至少1个key变化则触发)
- 优点:文件紧凑、恢复速度快、适合备份
- 缺点:fork时阻塞主线程、可能丢失最后一次快照后的数据
AOF(Append Only File):
- 工作原理:记录每个写操作命令,追加到文件末尾
- 配置参数:appendonly yes、appendfsync everysec
- 优点:数据安全性高、可读性好
- 缺点:文件体积大、恢复速度慢
混合持久化(Redis 4.0+):
- RDB做基础、AOF做增量,兼顾性能和数据安全
3. 缓存三击问题及解决方案
缓存穿透:查询不存在的数据,请求直达数据库
解决方案:
// 方案1:布隆过滤器
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000,
0.01
);
filter.put("product:123");
if (!filter.mightContain("product:" + id)) {
return null; // 直接返回,不查数据库
}
// 方案2:缓存空值
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value.equals("NULL") ? null : value;
}
Product product = productMapper.selectById(id);
if (product == null) {
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
缓存击穿:热点key过期,大量请求直达数据库
解决方案:
// 方案1:互斥锁
public Product getProductWithLock(Long id) {
String key = "product:" + id;
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return JSON.parseObject(value, Product.class);
}
String lockKey = "lock:product:" + id;
try {
// 获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
Product product = productMapper.selectById(id);
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
return product;
} else {
// 等待后重试
Thread.sleep(100);
return getProductWithLock(id);
}
} finally {
redisTemplate.delete(lockKey);
}
}
// 方案2:逻辑过期
public Product getProductWithLogicalExpire(Long id) {
String key = "product:" + id;
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
ProductCache cache = JSON.parseObject(value, ProductCache.class);
if (cache.getExpireTime() > System.currentTimeMillis()) {
return cache.getProduct();
}
// 异步刷新缓存
CompletableFuture.runAsync(() -> refreshCache(id));
return cache.getProduct(); // 返回旧数据
}
return null;
}
缓存雪崩:大量key同时过期,数据库压力骤增
解决方案:
// 随机过期时间
public void setProductCache(Product product) {
String key = "product:" + product.getId();
// 基础过期时间30分钟 + 随机0-10分钟
long expireTime = 30 + (long)(Math.random() * 10);
redisTemplate.opsForValue().set(
key,
JSON.toJSONString(product),
expireTime,
TimeUnit.MINUTES
);
}
4. 分布式锁实现
Redis实现:
public class RedisDistributedLock {
private final RedisTemplate<String, String> redisTemplate;
private final String lockKey;
private final String lockValue;
private final long expireTime;
public boolean tryLock() {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
expireTime,
TimeUnit.MILLISECONDS
)
);
}
public boolean unlock() {
// Lua脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue);
return result != null && result == 1;
}
}
Zookeeper实现:
public class ZkDistributedLock {
private final CuratorFramework client;
private final String lockPath;
private InterProcessMutex lock;
public boolean tryLock(long timeout, TimeUnit unit) {
lock = new InterProcessMutex(client, lockPath);
try {
return lock.acquire(timeout, unit);
} catch (Exception e) {
return false;
}
}
public void unlock() {
try {
if (lock != null) {
lock.release();
}
} catch (Exception e) {
// log error
}
}
}
第二轮:秒杀系统设计与高并发处理
1. 秒杀系统整体架构
业务场景:双11秒杀活动,千万级用户同时抢购限量商品,要求高并发、高可用、数据一致性。
架构设计:
┌─────────────────────────────────────────────────────────┐
│ 用户层 │
│ Web端 │ H5端 │ 小程序 │ App端 │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ CDN层 │
│ 静态资源加速(图片、JS、CSS) │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ 负载均衡层 │
│ Nginx + LVS + F5 │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ 网关层 │
│ Spring Cloud Gateway │ 限流 │ 鉴权 │ 路由 │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ 应用服务层 │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │用户服务│ │商品服务│ │订单服务│ │库存服务│ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ 缓存层 │
│ Redis Cluster │
│ 商品信息 │ 库存 │ 用户Token │ 限流计数器 │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐│ 消息队列层 │
│ Kafka Cluster │
│ 订单创建 │ 库存扣减 │ 支付通知 │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ 数据层 │
│ MySQL Cluster │ MongoDB │ Elasticsearch │
└─────────────────────────────────────────────────────────┘
核心服务设计:
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private RateLimiter rateLimiter; // 限流器
@PostMapping("/{productId}")
public Result seckill(@PathVariable Long productId,
@RequestHeader("userId") Long userId) {
// 1. 限流
if (!rateLimiter.tryAcquire(userId)) {
return Result.error("请求过于频繁");
}
// 2. 校验用户资格
if (!seckillService.checkUserQualification(userId, productId)) {
return Result.error("无购买资格");
}
// 3. 执行秒杀
String orderNo = seckillService.doSeckill(userId, productId);
return Result.success(orderNo);
}
}
2. 防止超卖方案
方案一:Redis原子扣减 + 异步落库
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public String doSeckill(Long userId, Long productId) {
String stockKey = "seckill:stock:" + productId;
String userKey = "seckill:user:" + productId + ":" + userId;
// 1. 检查用户是否已购买
Boolean hasBought = redisTemplate.hasKey(userKey);
if (Boolean.TRUE.equals(hasBought)) {
throw new BusinessException("每人限购一件");
}
// 2. Lua脚本原子操作:检查库存 + 扣减库存 + 标记用户
String luaScript = """
local stock = redis.call('get', KEYS[1])
if not stock then
return -1
end
if tonumber(stock) <= 0 then
return 0
end
redis.call('decr', KEYS[1])
redis.call('set', KEYS[2], '1', 'EX', 3600)
return 1
""";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Arrays.asList(stockKey, userKey)
);
if (result == -1) {
throw new BusinessException("商品不存在");
}
if (result == 0) {
throw new BusinessException("库存不足");
}
// 3. 发送订单创建消息
String orderNo = generateOrderNo();
SeckillOrderMessage message = new SeckillOrderMessage(
orderNo, userId, productId
);
kafkaTemplate.send("seckill-order", JSON.toJSONString(message));
return orderNo;
}
}
方案二:数据库乐观锁
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockMapper stockMapper;
@Transactional
public void createOrder(SeckillOrderMessage message) {
// 1. 扣减库存(乐观锁)
int updated = stockMapper.decreaseStock(
message.getProductId(),
1
);
if (updated == 0) {
throw new BusinessException("库存不足");
}
// 2. 创建订单
Order order = new Order();
order.setOrderNo(message.getOrderNo());
order.setUserId(message.getUserId());
order.setProductId(message.getProductId());
order.setStatus(OrderStatus.PENDING_PAYMENT);
orderMapper.insert(order);
}
}
<!-- StockMapper.xml -->
<update id="decreaseStock">
UPDATE seckill_stock
SET stock = stock - 1,
version = version + 1
WHERE product_id = #{productId}
AND stock > 0
AND version = #{version}
</update>
3. Redis与MySQL数据一致性
方案一:延迟双删
public void updateProduct(Product product) {
// 1. 删除缓存
redisTemplate.delete("product:" + product.getId());
// 2. 更新数据库
productMapper.updateById(product);
// 3. 延迟删除缓存(防止读请求读到旧数据)
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.schedule(() -> {
redisTemplate.delete("product:" + product.getId());
}, 500, TimeUnit.MILLISECONDS);
}
方案二:Canal监听MySQL binlog
@Component
public class CanalClient {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@KafkaListener(topics = "canal-product")
public void handleCanalMessage(String message) {
CanalEntry.Entry entry = JSON.parseObject(message, CanalEntry.Entry.class);
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {
// 更新缓存
Product product = parseFromRowData(rowData.getAfterColumnsList());
redisTemplate.opsForValue().set(
"product:" + product.getId(),
product,
30,
TimeUnit.MINUTES
);
}
}
}
}
}
4. 限流算法实现
令牌桶算法:
@Component
public class RateLimiter {
private final ConcurrentHashMap<Long, TokenBucket> buckets = new ConcurrentHashMap<>();
public boolean tryAcquire(Long userId) {
TokenBucket bucket = buckets.computeIfAbsent(
userId,
k -> new TokenBucket(100, 10) // 容量100,每秒放入10个令牌
);
return bucket.tryConsume(1);
}
static class TokenBucket {
private final long capacity; // 桶容量
private final long rate; // 令牌生成速率(个/秒)
private long tokens; // 当前令牌数
private long lastRefillTime; // 上次填充时间
public TokenBucket(long capacity, long rate) {
this.capacity = capacity;
this.rate = rate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
}
public synchronized boolean tryConsume(long tokens) {
refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long elapsed = (now - lastRefillTime) / 1000;
if (elapsed > 0) {
long newTokens = elapsed * rate;
this.tokens = Math.min(capacity, this.tokens + newTokens);
this.lastRefillTime = now;
}
}
}
}
滑动窗口算法:
@Component
public class SlidingWindowRateLimiter {
private final ConcurrentHashMap<Long, LinkedList<Long>> windows = new ConcurrentHashMap<>();
private final int limit = 100; // 限制100次
private final long windowSize = 60000; // 窗口大小60秒
public boolean tryAcquire(Long userId) {
LinkedList<Long> window = windows.computeIfAbsent(userId, k -> new LinkedList<>());
long now = System.currentTimeMillis();
// 移除窗口外的记录
while (!window.isEmpty() && now - window.getFirst() > windowSize) {
window.removeFirst();
}
// 检查是否超限
if (window.size() >= limit) {
return false;
}
window.addLast(now);
return true;
}
}
5. 热点数据自动发现与缓存预热
热点数据发现:
@Component
public class HotDataDetector {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final ConcurrentHashMap<String, AtomicLong> accessCount = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 60000) // 每分钟统计一次
public void detectHotData() {
long threshold = 1000; // 访问阈值
accessCount.forEach((key, count) -> {
if (count.get() > threshold) {
// 标记为热点数据
redisTemplate.opsForSet().add("hot:keys", key);
// 设置更长的过期时间
redisTemplate.expire(key, 2, TimeUnit.HOURS);
}
count.set(0); // 重置计数
});
}
public void recordAccess(String key) {
accessCount.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet();
}
}
缓存预热:
@Component
public class CacheWarmup implements ApplicationRunner {
@Autowired
private ProductMapper productMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void run(ApplicationArguments args) {
// 预热秒杀商品
List<Product> seckillProducts = productMapper.selectSeckillProducts();
seckillProducts.forEach(product -> {
String key = "product:" + product.getId();
redisTemplate.opsForValue().set(key, product, 2, TimeUnit.HOURS);
});
// 预热库存
List<Stock> stocks = stockMapper.selectAll();
stocks.forEach(stock -> {
String key = "stock:" + stock.getProductId();
redisTemplate.opsForValue().set(key, stock.getQuantity());
});
}
}
第三轮:分布式事务与系统优化
1. 分布式事务解决方案
2PC(两阶段提交):
准备阶段:
- 协调者向所有参与者发送准备请求
- 参与者执行事务但不提交,返回准备结果
提交阶段:
- 如果所有参与者都返回成功,协调者发送提交请求
- 如果有参与者返回失败,协调者发送回滚请求
缺点:
- 同步阻塞:所有参与者必须等待
- 单点故障:协调者故障导致事务阻塞
- 数据不一致:网络分区导致脑裂
TCC(Try-Confirm-Cancel):
@Service
public class OrderTccService {
// Try阶段:预留资源
@Transactional
public void tryCreateOrder(Order order) {
// 检查订单是否已存在
Order existOrder = orderMapper.selectByOrderNo(order.getOrderNo());
if (existOrder != null) {
throw new BusinessException("订单已存在");
}
// 冻结库存
stockService.freezeStock(order.getProductId(), order.getQuantity());
// 创建待确认订单
order.setStatus(OrderStatus.TRYING);
orderMapper.insert(order);
}
// Confirm阶段:确认提交
@Transactional
public void confirmCreateOrder(String orderNo) {
Order order = orderMapper.selectByOrderNo(orderNo);
order.setStatus(OrderStatus.CONFIRMED);
orderMapper.updateById(order);
// 扣减实际库存
stockService.decreaseStock(order.getProductId(), order.getQuantity());
}
// Cancel阶段:取消回滚
@Transactional
public void cancelCreateOrder(String orderNo) {
Order order = orderMapper.selectByOrderNo(orderNo);
if (order != null && order.getStatus() == OrderStatus.TRYING) {
order.setStatus(OrderStatus.CANCELED);
orderMapper.updateById(order);
// 释放冻结库存
stockService.unfreezeStock(order.getProductId(), order.getQuantity());
}
}
}
本地消息表:
@Service
public class OrderService {
@Autowired
private LocalMessageMapper localMessageMapper;
@Transactional
public void createOrder(Order order) {
// 1. 创建订单
orderMapper.insert(order);
// 2. 保存本地消息
LocalMessage message = new LocalMessage();
message.setTopic("order-created");
message.setContent(JSON.toJSONString(order));
message.setStatus(MessageStatus.PENDING);
localMessageMapper.insert(message);
}
}
@Component
public class MessageSender {
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<LocalMessage> messages = localMessageMapper.selectPendingMessages(100);
for (LocalMessage message : messages) {
try {
kafkaTemplate.send(message.getTopic(), message.getContent());
message.setStatus(MessageStatus.SENT);
localMessageMapper.updateById(message);
} catch (Exception e) {
// 发送失败,下次重试
message.setRetryCount(message.getRetryCount() + 1);
localMessageMapper.updateById(message);
}
}
}
}
2. Seata AT模式原理
AT模式工作流程:
一阶段:
1. 解析SQL语义,找到要更新的业务数据
2. 查询前镜像(修改前的数据)
3. 执行业务SQL
4. 查询后镜像(修改后的数据)
5. 生成Undo Log(前后镜像)
6. 提交本地事务
7. 上报分支状态
二阶段:
- 提交:异步删除Undo Log
- 回滚:根据Undo Log反向生成SQL并执行
代码示例:
@Service
public class OrderService {
@Autowired
private ProductMapper productMapper;
@Autowired
private OrderMapper orderMapper;
// Seata全局事务注解
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(Order order) {
// 扣减库存(自动生成Undo Log)
productMapper.decreaseStock(order.getProductId(), order.getQuantity());
// 创建订单
orderMapper.insert(order);
// 模拟异常,触发回滚
if (order.getAmount() > 10000) {
throw new BusinessException("金额超限");
}
}
}
3. MySQL MVCC原理
MVCC核心概念:
- Read View(读视图):事务开始时创建的快照,包含活跃事务ID列表
- Undo Log:记录数据修改历史,用于构建旧版本数据
- 隐藏字段:
DB_TRX_ID:最近修改事务IDDB_ROLL_PTR:回滚指针,指向Undo LogDB_ROW_ID:隐藏主键
可重复读实现:
// Read View结构
class ReadView {
long m_ids[]; // 活跃事务ID列表
long m_low_limit_id; // 下一个要分配的事务ID(大于所有活跃事务)
long m_up_limit_id; // 最小活跃事务ID
}
// 可见性判断算法
boolean isVisible(ReadView view, long trxId) {
if (trxId < view.m_up_limit_id) {
// 事务ID小于最小活跃事务,可见
return true;
}
if (trxId >= view.m_low_limit_id) {
// 事务ID大于等于下一个分配ID,不可见
return false;
}
// 在活跃事务列表中,不可见
return !contains(view.m_ids, trxId);
}
RR与RC的区别:
| 隔离级别 | Read View生成时机 | |---------|------------------| | RC | 每次SELECT都生成新Read View | | RR | 第一次SELECT生成Read View,后续复用 |
4. 性能监控与故障排查
Prometheus监控配置:
# prometheus.yml
scrape_configs:
- job_name: 'spring-boot'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
自定义监控指标:
@Component
public class SeckillMetrics {
private final Counter orderCounter;
private final Histogram orderLatency;
private final Gauge stockGauge;
public SeckillMetrics(MeterRegistry registry) {
orderCounter = Counter.builder("seckill.order.total")
.description("Total seckill orders")
.tag("status", "success")
.register(registry);
orderLatency = Histogram.builder("seckill.order.latency")
.description("Seckill order processing time")
.register(registry);
stockGauge = Gauge.builder("seckill.stock.remaining", this::getRemainingStock)
.description("Remaining seckill stock")
.register(registry);
}
public void recordOrder(long productId, long latency) {
orderCounter.increment();
orderLatency.record(latency);
}
private double getRemainingStock() {
// 查询剩余库存
return stockService.getRemainingStock();
}
}
链路追踪:
@RestController
public class SeckillController {
@Autowired
private Tracer tracer;
@PostMapping("/seckill/{productId}")
public Result seckill(@PathVariable Long productId) {
// 创建新的Span
Span span = tracer.nextSpan()
.name("seckill-process")
.tag("product.id", String.valueOf(productId));
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span.start())) {
// 业务逻辑
String orderNo = seckillService.doSeckill(productId);
return Result.success(orderNo);
} finally {
span.end();
}
}
}
故障排查工具链:
1. 日志分析(ELK)
- Kibana查询异常日志
- Logstash过滤关键字
2. 性能分析
- Arthas在线诊断
- JProfiler内存分析
- JStack线程dump
3. 数据库分析
- Slow Query Log
- Explain执行计划
- Show Processlist
4. 网络分析
- Tcpdump抓包
- Wireshark分析
总结
通过这场面试,我们可以看到:
- 基础扎实:Spring Boot自动装配、Redis持久化等基础概念需要牢固掌握
- 场景理解:秒杀系统需要考虑高并发、数据一致性、可用性等多个维度
- 深入原理:分布式事务、MVCC等底层原理是区分初级和高级工程师的关键
- 实战经验:限流、缓存、监控等都需要在实际项目中积累经验
对于求职者来说,不仅要知其然,更要知其所以然。持续学习,深入实践,才能在技术道路上越走越远!
本文所有代码示例均为教学用途,实际生产环境需要根据具体场景调整优化。
参考资料:
- Spring Boot官方文档
- Redis设计与实现
- Designing Data-Intensive Applications
- 分布式系统原理与范型
更多推荐
所有评论(0)