32.最长有效括号【hard】

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号 子串 的长度。

左右括号匹配,即每个左括号都有对应的右括号将其闭合的字符串是格式正确的,比如 "(()())"

要点:1.栈   2.push下标  3.哨兵(初始push(-1))

class Solution {
    public int longestValidParentheses(String s) {
        //栈
        //要保留栈的前一个索引
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(-1);
        int ans = 0;
        for(int i = 0; i < s.length(); i++){
            char c = s.charAt(i);
            if(c == '('){
                stack.push(i);
            }else{
                stack.pop();
                if(stack.isEmpty()){
                    stack.push(i);
                }else{
                    ans = Math.max(ans, i - stack.peek());
                }
            }

        }

        return ans;
        
    }
}

69.x的平方根

给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

要点:1.二分查找  2.记得转换long

class Solution {
    public int mySqrt(int x) {
        if(x== 0 || x == 1){
            return x;
        }

        int left = 0;
        int right = x;
        int ans = 0;
        while(left <= right){
            int mid = left + (right - left)/2;
            //记得转换
            long bijiao = (long) mid * mid;

            //
            if(bijiao == x){
                return mid;
            }else if(bijiao < x){
                ans = mid;
                left = mid +1;
            }else{
                right = mid -1;
            }

        }
        
        return ans;
    }
}

148.排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

要点:1.分治算法【sort,merge】 2.mid【取中间点,注意pre断连结】 3.merger两个链表

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
        //分治

        if(head == null || head.next ==null){
            return head;

        }

        ListNode head2 = middle(head);

        head = sortList(head);
        head2 = sortList(head2);
        

        return merge(head, head2);

        
    }


    public ListNode middle(ListNode head){
        //断开前一个
        ListNode pre = head;
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            pre  = slow;
            fast = fast.next.next;
            slow = slow.next;
        }

        pre.next = null;
        return slow;

    }


    public ListNode merge(ListNode list1, ListNode list2){
        ListNode dummy = new ListNode();
        ListNode curr = dummy;
        while(list1 != null && list2 != null){
            if(list1.val < list2.val){
                curr.next = list1;
                list1 = list1.next;
            }else{
                curr.next = list2;
                list2 = list2.next;
            }

            curr = curr.next;
        }

        curr.next = (list1 == null ) ? list2 : list1;

        return dummy.next;
    }
}

随机刷新的面经整理

说明:小红书上的博主更新的我只是整理

4.10美团后端一面 模块一:项目一(高并发优惠券系统) 1. 异常流推演(数据库): 抢完之后,如果后面数据库操作失败了怎么办? 2. 异常流推演(MQ发送): 你说的 Try-Catch 捕获异常,是在发送 MQ 的时候做的吗? 3. 异常流推演(MQ消费): 发送 MQ 失败存数据库,那有没有可能是消费的时候出现异常呢? 4. 死信队列追问: 你说的消费异常重复 16 次,是因为有库存但是消费逻辑有问题导致没扣减成功,还是什么原因? 5. 极端兜底场景: 有没有可能扣减就扣减不到呢?比如模板被删了,给它重复消费 4 个小时,最终用户那边收到抢券失败吗? 模块二:高并发场景题与数据库设计 1. 超发控制机制: MySQL 扣减库存是用“库存大于0”这个逻辑来判断的,这个逻辑你觉得有没有什么问题? 2. 深挖 ABA(技术向): 你用“库存大于0”判断,会不会有 ABA 的问题? 3. 缓存与数据库一致性: 数据库和缓存同时有数据,更新时是先更新缓存还是先更新数据库?如果是你,你选择哪种方案? 4. 一致性追问(失败场景): 如果你选择先操作缓存,再操作数据库,那如果操作数据库失败了怎么办?5. 一致性追问(并发场景): 高并发场景下,直接操作缓存再去操作数据库,会不会有一些并发安全的问题?模块三:Java 基础(并发与线程池) 1. 线程池运行原理: 线程池里面的源码逻辑有了解过吗?它大概是什么样的? 2. 线程回收机制: 已经有一些临时线程了,它什么时候会被回收? 3. 核心线程追问: 核心线程为什么不会被回收? 模块四:项目二(Agent 智能助手与 RAG) 1. 架构阐述: Agent 项目重点说一到两个觉得比较重点的内容。 2. 认知考察: 你对 Agent 是怎么理解的?你觉得它是一个什么样的东西? 3. 细节追问(动态获取): 你刚才在对比 Prompt 时提到的“动态获取”是什么意思? 4. Function Call 落地: 你这里面是一个独立的 Agent 负责知识库检索,有具体实现什么 Function Call 吗? 模块五:Java 集合底层 1. HashMap 并发缺陷: HashMap 有线程安全的问题吗?举一个具体的并发情况的例子,什么操作会导致线程安全问题? 2. 线程一改成3,线程二覆盖,是覆盖它的 value 吗?如果顺序执行 put 3 再 put 2 最后是 2,那并发情况下最后也是置为 2,这为什么叫线程安全问题? 3. 头插法为什么会有循环的问题?是怎么产生的? 4. 尾插法并发时,丢失的是 value 还是什么? 手撕代码 :反转链表,每个题目给出面试官满意的回答

好的,这是一个非常典型且高质量的后端面试题集合,涵盖了高并发系统设计、数据库、缓存、Java并发、集合源码等多个核心领域。用户希望获得每个题目的“面试官满意的回答”。

我需要为每个问题提供一个深入、准确且结构清晰的回答。回答要体现原理理解、实战经验和问题解决思路。

整体思路:

  1. 模块一(优惠券系统):聚焦异常流处理,从本地事务、MQ可靠性、死信队列机制到极端兜底,层层递进。

  2. 模块二(高并发与数据库):重点分析库存>0判断在ABA、超发、一致性方面的缺陷,并讨论缓存与数据库的更新顺序及并发问题。

  3. 模块三(Java并发与线程池):讲解线程池核心参数、执行流程、线程回收机制(keepAliveTimeallowCoreThreadTimeOut)。

  4. 模块四(Agent与RAG):阐述Agent的核心能力(规划、记忆、工具使用),以及Function Call的具体实现和动态Prompt设计。

  5. 模块五(HashMap):重点说明并发put导致死循环(头插法)或数据覆盖(尾插法)的原理。

  6. 手撕代码:给出递归和迭代两种反转链表的解法,并分析时空复杂度。

回答风格要专业、自信、有深度,多用“首先...其次...另外...”等逻辑词串联,避免过于口语化。现在开始组织每个问题的详细回答。

这是一份非常经典且全面的后端面试复盘。针对你记录的每一个问题,我整理了面试官期望听到的、具备深度和实战经验的回答思路

模块一:项目一(高并发优惠券系统)

1. 异常流推演(数据库): 抢完之后,如果后面数据库操作失败了怎么办?

  • 核心点:本地事务 + 最终一致性。

  • 回答思路: “我会使用数据库本地事务来保证‘扣减库存’和‘创建订单’(或记录领券记录)的原子性。如果扣减库存成功后,后续的插入操作失败,整个事务会回滚,库存也会随之恢复。但如果是网络超时等异常,事务会回滚,此时需要通过日志(如binlog)或定时任务扫描未完成状态的记录进行重试或补偿。核心原则是:先扣库存,后生成凭证,保证资源不超卖。”

2. 异常流推演(MQ发送): 你说的 Try-Catch 捕获异常,是在发送 MQ 的时候做的吗?

  • 核心点:事务消息 vs 本地消息表。

  • 回答思路: “单纯的Try-Catch在发送MQ时存在漏洞(本地事务提交成功但MQ发送失败)。我的做法是:先扣库存并写入本地消息表(同事务),然后由独立的后台线程轮询消息表发送MQ。或者使用RocketMQ的事务消息:发送半消息 → 执行本地事务 → 提交/回滚消息。这是为了保证本地事务和消息发送的原子性。”

3. 异常流推演(MQ消费): 发送 MQ 失败存数据库,那有没有可能是消费的时候出现异常呢?

  • 核心点:消费幂等性 + 重试机制。

  • 回答思路: “会的,消费异常很常见(如依赖服务宕机、数据冲突)。解决方案:1. 重试机制:Spring Kafka/RabbitMQ默认会重试,我们可以配置重试次数(如3次)和间隔。2. 死信队列:超过重试次数后,消息进入死信队列,人工介入或定时补偿。3. 消费幂等:这是核心前提,下游服务必须能处理重复消息(通过唯一ID+状态机判断),避免重复发券。”

4. 死信队列追问: 你说的消费异常重复 16 次,是因为有库存但是消费逻辑有问题导致没扣减成功,还是什么原因?

  • 核心点:区分业务异常与系统异常。

  • 回答思路: “这取决于异常类型。如果是下游服务超时、网络抖动等临时性问题,重试16次很可能成功。但如果是业务逻辑错误(如参数校验失败、模板状态为‘已删除’),重试一万次也不会成功。因此,死信队列的设计应该区分:业务异常直接进死信;系统异常先重试,仍失败再进死信。实际操作中,我们还会监控死信队列,及时告警人工处理。”

5. 极端兜底场景: 有没有可能扣减就扣减不到呢?比如模板被删了,给它重复消费 4 个小时,最终用户那边收到抢券失败吗?

  • 核心点:逻辑删除 vs 物理删除,最终一致性。

  • 回答思路: “会的。如果模板是逻辑删除(deleted=1),消费时检查不通过,应该直接失败并进死信,同时更新本地消息表状态为‘失败’,并通知用户‘抢券失败,活动已结束’。如果模板是物理删除(数据都没了),这属于数据灾难,需要从备份恢复。兜底方案是:消费端最后会记录一条明确的失败日志,通过后台管理界面,运营人员可以主动给用户补发优惠券作为补偿。”

模块二:高并发场景题与数据库设计

1. 超发控制机制: MySQL 扣减库存是用“库存大于0”这个逻辑来判断的,这个逻辑你觉得有没有什么问题?

  • 核心点:ABA问题、性能问题。

  • 回答思路: “单纯用UPDATE table SET stock = stock - 1 WHERE id = ? AND stock > 0能防止超卖,但有局限:1. 性能瓶颈:所有请求都落到MySQL,扛不住非常高并发(如万级QPS)。2. ABA不是主要问题:在高并发扣减场景下,ABA(A线程读库存=5,B线程扣到4,A线程基于旧值5判断)不会造成超卖,因为SQL的stock > 0在数据库行锁下判断的是当前最新值。3. 真正的风险:如果业务是先SELECT stock,代码里判断if(stock>0),再UPDATE,就会有超卖风险。必须用一条原子SQL。”

2. 深挖 ABA(技术向): 你用“库存大于0”判断,会不会有 ABA 的问题?

  • 核心点:理解ABA发生的条件。

  • 回答思路: “如果只是判断库存大于0,没有ABA问题。ABA通常发生在‘比较并交换’时值先变A再变回A,但中间业务状态发生了变化。在库存扣减中,如果库存从5减到4再回滚到5,另一个线程在回滚期间扣减,即使看到库存=5,它扣减后库存=4,依然正确,不会超卖。真正需要注意ABA的是无锁数据结构版本号乐观锁场景。库存扣减用SQL的stock > 0配合行锁是安全的。”

3. 缓存与数据库一致性: 数据库和缓存同时有数据,更新时是先更新缓存还是先更新数据库?如果是你,你选择哪种方案?

  • 核心点:权衡一致性与性能。

  • 回答思路: “我倾向先更新数据库,再删除缓存(Cache Aside Pattern)。原因:1. 先更新缓存在极端情况下(更新缓存成功但DB失败)会造成永久脏数据。2. 先更新DB再删缓存,即使删缓存失败,也只是短暂不一致,下次读会从DB加载。而先删缓存再更新DB,在并发读时容易把旧数据重新加载进缓存。所以标准方案是:更新DB → 删除缓存。如果对一致性要求极高,可以加延迟双删或订阅binlog异步删除。”

4. 一致性追问(失败场景): 如果你选择先操作缓存,再操作数据库,那如果操作数据库失败了怎么办?

  • 核心点:补偿机制。

  • 回答思路: “如果我先操作缓存成功了,但DB操作失败了,缓存就成了脏数据。我需要在DB操作失败的catch块中,立即回滚缓存(即再次删除缓存)。同时记录日志,必要时触发告警。这种做法复杂且容易出错,这也是为什么业界更推荐‘先DB后缓存’的原因。”

5. 一致性追问(并发场景): 高并发场景下,直接操作缓存再去操作数据库,会不会有一些并发安全的问题?

  • 核心点:并发导致的数据不一致。

  • 回答思路: “会的。比如:线程A先删缓存,准备更新DB;此时线程B读缓存miss,从DB加载旧数据到缓存;然后线程A更新DB。最终缓存中是旧数据。这就是‘先删缓存后更新DB’的经典并发问题。解决方案:1. 延迟双删:更新DB后,sleep几十毫秒再删一次缓存。2. 串行化:对同一个key的写操作加分布式锁。3. 最终一致性:接受短暂不一致,利用binlog异步更新缓存。”

模块三:Java 基础(并发与线程池)

1. 线程池运行原理: 线程池里面的源码逻辑有了解过吗?它大概是什么样的?

  • 核心点:corePoolSize, workQueue, maximumPoolSize的执行流程。

  • 回答思路: “核心逻辑在execute方法中(以JDK ThreadPoolExecutor为例):1. 如果当前工作线程数 < corePoolSize,直接创建新核心线程执行任务。2. 如果 >= corePoolSize,尝试将任务放入阻塞队列。3. 如果队列满了,且线程数 < maximumPoolSize,创建非核心线程执行任务。4. 如果线程数已达maximumPoolSize,执行拒绝策略。注意:非核心线程只在队列满后被创建,如果队列是无界的,永远不会创建非核心线程。”

2. 线程回收机制: 已经有一些临时线程了,它什么时候会被回收?

  • 核心点:keepAliveTime + 阻塞队列的poll超时。

  • 回答思路: “临时线程(非核心线程)在空闲时间超过keepAliveTime后会被回收。源码中,线程执行完任务后,会从工作队列中poll(keepAliveTime, TimeUnit)获取新任务。如果超时还没获取到,且当前线程数 > corePoolSize,这个线程就会结束并被回收。核心线程如果设置了allowCoreThreadTimeOut(true),也会被回收。”

3. 核心线程追问: 核心线程为什么不会被回收?

  • 核心点:对比poll和take。

  • 回答思路: “因为核心线程从队列中取任务时,使用的是take()方法,它会一直阻塞直到拿到任务,不会像poll(timeout)那样超时返回null。除非通过setAllowCoreThreadTimeOut(true)改变了这个行为。所以默认情况下,核心线程会一直存活,避免了频繁创建销毁线程的开销。”

模块四:项目二(Agent 智能助手与 RAG)

1. 架构阐述: Agent 项目重点说一到两个觉得比较重点的内容。

  • 回答思路: “我重点说一下动态检索路由多轮对话记忆管理。1. 动态路由:根据用户问题的意图分类,自动选择走常规RAG还是调用API工具(如查天气、订机票)。2. 记忆槽位:我们维护了一个滑动窗口的对话历史,并利用LLM做关键信息摘要压缩,避免长期对话token溢出,同时保留上下文。”

2. 认知考察: 你对 Agent 是怎么理解的?你觉得它是一个什么样的东西?

  • 回答思路: “我认为Agent是大语言模型+规划能力+工具使用+记忆的集合体。它不仅仅是一个聊天机器人,而是能够自主理解目标、拆解步骤(如ReAct模式)、调用外部工具(API、数据库、代码解释器)来完成任务,并在过程中根据反馈调整计划的智能体。比如让它‘帮我订下周去北京的机票并安排会议’,它会先查日历,再调机票API,再调日历创建日程。”

3. 细节追问(动态获取): 你刚才在对比 Prompt 时提到的“动态获取”是什么意思?

  • 回答思路: “动态获取指的是,我们不在Prompt中硬编码固定的系统消息,而是在每次请求时,根据用户当前问题用户上下文(如用户ID、历史行为)、以及从知识库检索到的相关信息,动态拼接生成最终的Prompt。这样能保证LLM获知最相关、最新的信息,而不是过时或无关的通用指令。”

4. Function Call 落地: 你这里面是一个独立的 Agent 负责知识库检索,有具体实现什么 Function Call 吗?

  • 回答思路: “有实现。我们定义了几个Function:retrieve_knowledge(query)query_database(sql)send_email(to, subject, body)等。LLM会根据用户意图,返回一个结构化的JSON来调用对应Function。比如用户问‘查一下上季度的销售数据’,LLM会返回{function: ‘query_database’, arguments: {sql: ‘SELECT …’}},我们执行后将结果返回给LLM生成最终回答。”

模块五:Java 集合底层

1. HashMap 并发缺陷: HashMap 有线程安全的问题吗?举一个具体的并发情况的例子,什么操作会导致线程安全问题?

  • 回答思路: “有。最经典的是JDK 1.7及以前,并发put时可能出现死循环(链表成环)和数据丢失。JDK 1.8改用尾插法和红黑树后,死循环问题基本解决,但仍会出现数据覆盖(两个线程同时执行put,后一个覆盖前一个的value,或size计数不准)和扩容时数据错乱。”

2. 线程一改成3,线程二覆盖,是覆盖它的 value 吗?如果顺序执行 put 3 再 put 2 最后是 2,那并发情况下最后也是置为 2,这为什么叫线程安全问题?

  • 核心点:可见性 + 丢失更新。

  • 回答思路: “因为并发场景下,最终结果不一定是2。可能线程一put(3)后,线程二put(2)同时发生,它们都读取到了同一个空的table槽位。线程一计算出hash位置,写入节点;线程二也基于旧状态(空)计算出同一个位置,直接覆盖写入。最终结果是2,但线程一put的3丢失了,这就是数据覆盖。更重要的是,size++不是原子操作,可能只加了1次size却是2,导致size与实际entry数量不符。”

3. 头插法为什么会有循环的问题?是怎么产生的?

  • 核心点:并发扩容 + 头插法导致链表反向。

  • 回答思路: “JDK 1.7扩容时,遍历旧链表的每个节点,用头插法插入新数组。如果两个线程(T1、T2)同时触发扩容,T1被挂起;T2完成扩容后,原链表顺序被反转(如A->B变成B->A)。T1恢复后,继续用头插法处理自己持有的旧头节点,由于链表已经被T2改变指向,T1在新数组中插入时可能形成循环引用(A.next = B, B.next = A),导致get时死循环。”

4. 尾插法并发时,丢失的是 value 还是什么?

  • 核心点:尾插法避免死循环,但丢失数据。

  • 回答思路: “尾插法主要丢失(value)。并发put时,两个线程计算出的索引位置相同,后put的那个会覆盖前一个的value,造成前一个put的值丢失。另外,modCount(快速失败机制)和size的更新都不是原子的,导致size与实际元素个数不一致。尾插法解决了死循环,但没有解决数据可见性、原子性、覆盖等并发问题。”

碎碎念:后续会更新每天学习的八股和算法题,暑假实习找不到了,开始准备秋招的第一天。努力连续更新100天!

更多推荐