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:最近修改事务ID
    • DB_ROLL_PTR:回滚指针,指向Undo Log
    • DB_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分析

总结

通过这场面试,我们可以看到:

  1. 基础扎实:Spring Boot自动装配、Redis持久化等基础概念需要牢固掌握
  2. 场景理解:秒杀系统需要考虑高并发、数据一致性、可用性等多个维度
  3. 深入原理:分布式事务、MVCC等底层原理是区分初级和高级工程师的关键
  4. 实战经验:限流、缓存、监控等都需要在实际项目中积累经验

对于求职者来说,不仅要知其然,更要知其所以然。持续学习,深入实践,才能在技术道路上越走越远!

本文所有代码示例均为教学用途,实际生产环境需要根据具体场景调整优化。


参考资料

  • Spring Boot官方文档
  • Redis设计与实现
  • Designing Data-Intensive Applications
  • 分布式系统原理与范型

更多推荐