打家劫舍 III
要点:节点是两个状态
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { //节点是两个状态 public int rob(TreeNode root) { int[] res = dfs(root); return Math.max(res[0], res[1]); } private int[] dfs(TreeNode node){ if(node == null){ return new int[]{0,0}; } int[] left = dfs(node.left); int[] right = dfs(node.right); int rob = left[1]+right[1]+node.val; int norob = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); return new int[]{rob, norob}; } }监控二叉树
要点:分三种情况讨论,int[]
class Solution { public int minCameraCover(TreeNode root) { int[] res = dfs(root); return Math.min(res[0], res[2]); } private int[] dfs(TreeNode node) { if (node == null) { return new int[]{Integer.MAX_VALUE / 2, 0, 0}; // 除 2 防止加法溢出 } int[] left = dfs(node.left); int[] right = dfs(node.right); int choose = Math.min(left[0], left[1]) + Math.min(right[0], right[1]) + 1; int byFa = Math.min(left[0], left[2]) + Math.min(right[0], right[2]); int byChildren = Math.min(Math.min(left[0] + right[2], left[2] + right[0]), left[0] + right[0]); return new int[]{choose, byFa, byChildren}; } }随机知识
核心题**:什么是死信队列?怎么利用它实现延迟队列?
面试官为什么这么问?
死信队列是 RabbitMQ 实现可靠服务的兜底方案,延迟队列则是定时任务的异步实现。我问这个是想看你会不会用 RabbitMQ 的特性实现业务需求,比如下单未支付自动取消。
希望听到怎样的回答:
- 死信(Dead Letter):消息在以下情况会变成死信:
- 被消费者 basicReject 或 basicNack 且 requeue=false。
- 消息 TTL 过期。
- 队列已满,无法入队。
- 死信交换机(DLX):在声明队列时可以指定
x-dead-letter-exchange,消息成为死信后会被重新路由到指定的死信交换机,再进入绑定队列,供专门消费者处理(记录日志、重试、人工介入)。 - 延迟队列实现:RabbitMQ 本身没有延迟队列,但可以通过TTL + DLX组合实现:
- 声明一个普通队列,设置消息 TTL(或者设置队列 TTL)。
- 该队列不设消费者,消息过期后自动成为死信。
- 死信被路由到真正处理的队列,消费者从那里消费,就实现了“延迟”消费。
- 结合项目:“我们面经 Agent 中,用户请求生成一份模拟面试,如果大模型服务暂时不可用,我们会把请求放入一个带 TTL 的消息,如果 30 秒内没被成功消费,就自动转入死信,通知用户稍后重试。”
候选人:
好的。这个问题分为两部分:死信队列的概念和生成条件,以及如何巧妙地用 TTL + 死信队列实现延迟队列。我从死信的基本概念讲起,然后说明延迟队列的实现原理,最后结合项目场景举例。
第一,消息怎么变成死信。
死信其实就是"正常队列里待不下去"的消息。RabbitMQ 定义了三种情况,消息会被转成死信:
一是消费者拒绝且不让消息重回队列。消费者收到消息后发现处理不了、格式错误或者重试次数已经耗尽,调basicNack或basicReject返回,同时把requeue设为false。消息被标记为"不回去了",变成死信。
二是消息 TTL 过期。发送消息时设了存活时间,或者队列设了 TTL 属性,消息在队列里等待超时还没被消费,RabbitMQ 自动判定它过期,成为死信。
三是队列满了。队列设置了最大长度,新消息过来时队列已经满了,最老的消息会被"挤出去",也变成死信。
这三种情况下的死信会被重新交给一个特殊的交换机处理,就是死信交换机(DLX)。声明队列时加参数x-dead-letter-exchange,指定死信的"回收站",消息成了死信后,原来的队列把消息原封不动(还会带上一些死信相关的 header)重新路由到死信交换机,再进入绑定的死信队列,等待专门消费者做善后处理——记录异常、发送告警、人工补偿。
第二,怎么用死信队列实现延迟队列。
RabbitMQ 自身没有直接支持"等 30 秒再投递"这种延迟队列,但可以通过TTL + DLX这个组合巧妙实现。
做法是创建两个队列:
一个是"延时队列"。这个队列本身不配消费者,只设两个关键属性:x-message-ttl指定消息存活时间(或发消息时设expiration),以及x-dead-letter-exchange指向真正的业务交换机。消息进入这个队列后,就静静等待 TTL 到期。
一个是"业务处理队列"。这个队列正常绑定消费者。当延时队列里的消息 TTL 过期、变成死信后,RabbitMQ 自动把这条死信转发到业务交换机,再路由到业务处理队列。消费者从这个业务处理队列拿到消息时,TTL 时间已经过去,达到了延迟消费的效果。
举个例子:订单 30 分钟未支付自动取消。下单后发消息到延时队列,设 TTL 30 分钟。这 30 分钟内消息搁在延时队列里等过期,时间一到自动转死信,路由到业务处理队列,消费者收到消息后检查订单状态,如果还未支付就执行取消。
需要特别注意的是,RabbitMQ 对消息 TTL 的处理是在"队列头部检查"而非主动轮询每条消息。如果队列是多条不同过期时间的消息混合排列,第一条消息 TTL 未到,后面即使有消息已经过期也不会被处理,因为检查只发生在队列头部。所以建议每个延迟时间独立一个队列,保证同一队列中的所有消息具有统一的 TTL,避免过期消息被头部阻塞。
另外,RabbitMQ 3.8 之后也提供了官方的延迟交换机插件rabbitmq-delayed-message-exchange,本质上类似逻辑,只是内部把 TTL+DLX 的链路封装好了,使用更简洁。但无论哪种实现,底层都是基于"存活时间到期后重新路由"的机制。
第三,项目中的实际应用。
在面试系统里,我们有一个场景:用户请求生成模拟面试,此时大模型服务可能正在高负载中,直接返回失败用户体验很差。用延迟队列做如下处理:
接到生成请求后,先尝试调大模型服务。如果成功,直接返回结果。如果大模型返回"繁忙",把请求消息发到延时队列,设 TTL 30 秒。消费者监听的是延时队列对应的业务处理队列。30 秒后,消费者收到消息,再次尝试调用大模型——如果这次成功,正常生成面试结果;如果仍然失败,记录异常信息到专门的死信队列,人工介入或通知用户稍后重试。
这样的好处是:用户请求不会直接丢失,系统会自动在合理时间窗口内重试,同时对大模型形成一定程度的压力缓冲。
总结一句话:死信队列是消息被拒绝、过期或队列满时的兜底路由机制;延迟队列通过 TTL + DLX 组合把消息在延时队列里"搁"一段时间,到期后自动转为死信并路由到真正的处理队列,适用于订单超时、失败重试等需要异步延时的场景。
碎碎念:后续会更新每天学习的八股和算法 题,开始准备秋招的第32天。努力连续更新100天!以后每天就按,秋招项目【java+agent】,科研,必做项目,算法,八股,锻炼身体来总结。
总结:得多动脑子
1.算法要系统过一遍【灵神】24/27【早上】1h
2.秋招项目,【java】开始实际看业务,1/6;无
【agent】还在学,整理cc,无,
3.科研要跑一下,【下午】3h
4.检测项目也得总结文档,【晚上】5h
6.背八股,无
7.锻炼身体,无
杂活2h
反思:做完东西要总结