字节跳动国际支付-后端开发-三面面经 用 java 写一个秒杀系统这一面只有手撕环节没有问答环节。虽然这并不是一个难题但是当时我也没有手撕出来主要是平时写代码做项目太依赖 AI 了很多 Java 的 API 我都没有认真去记。除了力扣刷题经常用到的那些 API其他的我只能看懂但完全不知道如何去使用。所以我打算用技术博客记录的方式来补齐这一块短板。题目描述设计一个秒杀系统让 100 个线程去秒杀库存为 30 的商品需要保证线程安全。如果一个线程在调用秒杀接口时抢锁超过 3s那么判定该次秒杀超时放弃秒杀如果一个线程抢到了秒杀的机会但是在支付环节超过 10s 没有进行支付操作那么判定这次秒杀无效需要对库存数量进行回滚。给定如下框架可自行修改输入输出可以自行设置也就是说不一定要走Scanner来输入。java代码解读复制代码public class Redeem { // 初始化商品 ID 和对应的数量 public void init(int goodId, int stock) {} // 秒杀入口若秒杀成功则返回订单 ID这里我直接返回线程名称 public String redeemGood(int goodId) {} // 减少库存 public boolean decrementStock(int goodId) {} // 回滚库存 public void undoDecrement(int goodId) {} // 检查当前库存数量 public int checkStock(int goodId) {} // 主函数 }当时我的想法非常乱先是一上来就给redeemGood加了synchronized然后用ConcurrentHashMapInteger, AtomicInteger来存(goodId, stock)。我不知道如何在synchronized块判断超时当时也没想到ReentrantLock索性跳过这些接口的实现先写主函数里多线程调用的东西。我当时是打算用线程池来对 100 个Callable来invokeAll尽管在面试前一天我也对二面没有做出来的多线程手撕题进行了严肃的学习但是我忘了newFixedThreadPool应该怎么声明也忘了普通的线程池中的拒绝策略应该怎么写最后就是拉了一地。我看过一些小红书的面经分享如果没有手撕出来有的面试官会给你讲手撕题的思路的但是我遇到的这个面试官并没有给我讲解思路不知道是因为太忙还是因为他们组用的技术栈是 Go还是其他原因。最后面面试官让我自己讲解思路面试官也并没有任何打断我的想法然后就到了反问环节过了一周多问 HR 才知道面试挂了。思路我面试完之后想到可以用ReentrantLock里的tryLock来控制抢锁的超时限制典中典之考完试才想到压轴大题的解题思路。最后我用 CodeBuddy DeepSeek 来复盘让 AI 来引导我进行学习每写一步就让 AI 评价我的写法再给出改进建议。init的设计其实这个接口的设计很简单虽然说这里只需要跑通一个测试案例但是为了体现对线程安全的思考我们可以用ConcurrentHashMap来存(goodId, stock)。而接下来的问题是(goodId, stock)分别对应哪些数据结构goodId可以认为是不变的不存在竞态问题直接用Integer就行。stock是被多个线程进行修改的那么必须是一个线程安全的数据结构。AI 给我的提示是使用semaphore。之所以不使用AtomicInteger可以通过以下两个示例来说明示例一假设当前库存为1线程 A 和线程 B 同时执行java代码解读复制代码// ❌ 错误写法先检查再扣减 if (stock.get() 0) { // 时刻 t1A 读到 1 // 时刻 t2B 读到 1A 还没扣呢 stock.decrementAndGet(); // t3A 把 1 → 0 ✅ // t4B 把 0 → -1 ❌ 超卖了 return true; }问题在于get()decrementAndGet()是两个独立操作。虽然各自都是原子的但合在一起不是。线程 B 在 A 扣减之前就已经通过了 0的检查。示例二麻烦的正确写法java代码解读复制代码// ✅ 正确写法CAS 自旋 int current; do { current stock.get(); // 读当前值 if (current 0) return false; // 卖光了快速返回 } while (!stock.compareAndSet(current, current - 1)); // CAS期望值没变才扣 return true;如果用semaphore那么semaphore.tryAcquire本身就是 CAS 的操作无需手动写自旋尝试。所以最后应该用ConcurrentHashMapInteger, Semaphore不完全对应该是ConcurrentHashMapInteger, SemaphoreAndStocksjava代码解读复制代码public SemaphoreAndStocks(Semaphore semaphore, int stock) { this.semaphore semaphore; this.initialStock stock; }后续需要用到stock的初始值来避免semaphore.release()带来的问题。semaphore.release()调用没有限制如果不加判断地进行调用那么可能会造成semaphore.availablePermits()的数量大于初始设定的 30。redeemGood的设计业务逻辑在decrementStock这里只作为秒杀接口的入口如果成功获得秒杀资格那么就返回当前线程名称否则为null。java代码解读复制代码public String redeemGood(int goodId) { if (decrementStock(goodId)) { return Thread.currentThread().getName(); } return null; }decrementStock的设计其实也很简单考虑避免 NPE 的情况然后就是tryAcquire。每成功tryAcquire一次semaphore.availablePermits()就少一个。当semaphore.availablePermits()为 0 时tryAcquire可以快速失效。这里面并没有插入回滚库存的策略因为这里只是抢到了秒杀的资格并没有到达支付环节真正的支付环节在主函数中模拟。之所以没有在此处模拟支付环节是因为计算支付耗时这项功能不应该在扣减库存里面进行操作AI 给我的解释是支付环节会在另一个线程中进行。java代码解读复制代码public boolean decrementStock(int goodId) { SemaphoreAndStocks sas map.get(goodId); if (sas null) { return false; } Semaphore semaphore sas.getSemaphore(); try { if (!semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)) { return false; } } catch (InterruptedException e) { throw new RuntimeException(e); } return true; }undoDecrement的设计没有特别难的地方。java代码解读复制代码public void undoDecrement(int goodId) { SemaphoreAndStocks sas map.get(goodId); if (sas null) { return; } Semaphore semaphore sas.getSemaphore(); if (semaphore.availablePermits() sas.getInitialStock()) { semaphore.release(); } }最终代码java代码解读复制代码import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; /** * 字节跳动国际支付 - 后端开发 - 三面br * 题目描述设计一个简易的秒杀系统给定一个商品 ID 及其数量假设数量为 30此时有 100 个线程调用秒杀接口 */ public class Redeem { public static Random rand new Random(2026); private static class SemaphoreAndStocks { private final Semaphore semaphore; private final int initialStock; public SemaphoreAndStocks(Semaphore semaphore, int stock) { this.semaphore semaphore; this.initialStock stock; } public Semaphore getSemaphore() { return semaphore; } public int getInitialStock() { return initialStock; } } private final ConcurrentHashMapInteger, SemaphoreAndStocks map new ConcurrentHashMap(); private static final long TIMEOUT 3000; private static final int MAX_THREAD 100; /** * 初始化商品 ID 及其对应的初始数量 * param goodId 商品 ID * param stock 商品初始数量 */ public void init(int goodId, int stock) { this.map.putIfAbsent(goodId, new SemaphoreAndStocks(new Semaphore(stock), stock)); } /** * 秒杀接口需要在 3s 内返回结果如果接口响应时间超过 3s当前线程放弃继续秒杀商品br * 如果商品数量小于等于 0说明当前商品已经卖光该接口应该快速响应 * param goodId 商品 ID * return 成功则返回线程名称失败则返回字符串 null */ public String redeemGood(int goodId) { if (decrementStock(goodId)) { return Thread.currentThread().getName(); } return null; } /** * 扣减商品数量 * param goodId 商品 ID * return 扣减成功则返回 {code true}否则返回 {code false} */ public boolean decrementStock(int goodId) { SemaphoreAndStocks sas map.get(goodId); if (sas null) { return false; } Semaphore semaphore sas.getSemaphore(); try { if (!semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)) { return false; } // 支付模拟 } catch (InterruptedException e) { throw new RuntimeException(e); } return true; } /** * 回滚商品数量。此场景需要进行回滚操作用户点击了秒杀按钮但是超过 10s 没有进行支付 * param goodId 商品 ID * return */ public void undoDecrement(int goodId) { SemaphoreAndStocks sas map.get(goodId); if (sas null) { return; } Semaphore semaphore sas.getSemaphore(); if (semaphore.availablePermits() sas.getInitialStock()) { semaphore.release(); } } /** * 返回商品剩余数量 * param goodId 商品 ID * return 商品剩余数量 */ public int checkStock(int goodId) { SemaphoreAndStocks sas map.get(goodId); if (sas null) { return 0; } return sas.getSemaphore().availablePermits(); } /** * 自行设定输入 * param args */ public static void main(String[] args) { int goodId 1248; long payTimeout 10_000; Redeem redeem new Redeem(); redeem.init(goodId, 30); ExecutorService fixedThreadPool Executors.newFixedThreadPool(100); ListCallableString callableList new ArrayList(MAX_THREAD); AtomicInteger undo new AtomicInteger(); for (int i 0; i MAX_THREAD; i) { callableList.add(() - { String res redeem.redeemGood(goodId); if (null.equals(res)) { return res; } // 模拟支付环节 long t0 System.currentTimeMillis(); Thread.sleep(rand.nextLong(0, 11_000)); long t1 System.currentTimeMillis(); return t1 - t0 payTimeout ? res : undo; }); } MapString, Integer threadAndRedeemTimes new LinkedHashMap(); int counter 0; while (redeem.checkStock(goodId) 0) { try { // 逐轮秒杀 var futureList fixedThreadPool.invokeAll(callableList); // 遍历当前轮秒杀的各个线程的结果 for (FutureString future: futureList) { try { String s future.get(); // 支付超时需要发生回滚 if (undo.equals(s)) { redeem.undoDecrement(goodId); undo.incrementAndGet(); } else if (!null.equals(s)) { counter; threadAndRedeemTimes.merge(s, 1, Integer::sum); } } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } } catch (InterruptedException e) { throw new RuntimeException(e); } } for (var entry: threadAndRedeemTimes.entrySet()) { System.out.println(entry.getKey() : entry.getValue()); } System.out.println(); System.out.println( 最终结果 ); System.out.println(剩余库存 redeem.checkStock(goodId)); System.out.println(undo 数 undo.get()); System.out.println(成功数 counter); fixedThreadPool.shutdown(); } }感悟在当下这个时代虽然大家都在调侃手敲代码是古法编程往后是自然语言编程的时代但是我个人感觉手敲代码还是不可缺失的。对系统架构里每一个细节的理解和把控都必须亲自去设计一遍踩过各种各样的坑才知道真实的情况是怎么样的。我也喜欢用 AI 去写代码我也喜欢亲自手搓一个复杂的系统这两种都能给我带来乐趣但一个是短期的乐趣一个是能够不断回味的乐趣。