1. Redis的数据结构有哪些?各自适用场景是什么?
回答思路 Redis数据结构是美团后端必考题,美团业务(外卖调度/优惠券/排行榜)大量依赖Redis,结合业务场景回答更加分。
- 列出5种基础结构:String/List/Hash/Set/ZSet。
- 每种说清楚:底层实现 + 适用场景 + 美团业务案例(如能结合)。
- 加分项:提及HyperLogLog(UV统计)、Bitmap(签到)、Geo(配送距离)。
回答示例 Redis有5种核心数据结构:
- String(字符串):底层是SDS(简单动态字符串)。适用场景:缓存对象(用户信息JSON序列化后存储)、计数器(点击量、库存扣减,利用INCR原子性)、分布式锁(SET NX EX)。美团外卖中的优惠券库存扣减就是典型的String+INCR应用。
- List(列表):底层是quicklist。适用场景:消息队列(LPUSH + BRPOP阻塞消费)、最近N条数据(LRANGE + LTRIM控制长度)。
- Hash(哈希):底层是listpack(小数据量)或hashtable。适用场景:存储对象字段(用户信息,避免JSON频繁序列化反序列化)、购物车(HSET user:cart item_id quantity)。
- Set(集合):底层是listpack或hashtable,支持交集/并集操作。适用场景:去重(已消费用户ID集合)、共同好友(SINTER)、抽奖(SRANDMEMBER随机取)。
- ZSet(有序集合):底层是listpack或skiplist+hashtable。适用场景:排行榜(ZADD + ZREVRANGE)、延迟队列(score=执行时间戳,定时ZRANGEBYSCORE轮询)。美团的商家评分榜、用户积分排行榜都是ZSet的典型应用。
加分项
- Bitmap:用于签到打卡,1亿用户的全年签到数据只需约4.5MB。
- HyperLogLog:UV(独立访客)统计,误差率约0.81%,内存占用极低。
- Geo:存储地理坐标,支持GEODIST计算两点距离,用于外卖骑手配送范围计算。
2. 线程池的核心参数有哪些?如何合理配置?
回答思路 美团高并发业务场景下线程池是重要知识点,要说清楚参数含义 + 拒绝策略 + 配置方法论。
- 7个核心参数:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。
- 执行流程:核心线程 → 队列 → 最大线程 → 拒绝策略。
- 配置原则:CPU密集型 vs IO密集型。
回答示例 线程池的7个核心参数:
- corePoolSize:核心线程数,长期保留不销毁
- maximumPoolSize:最大线程数
- keepAliveTime:非核心线程的空闲存活时间
- unit:keepAliveTime的时间单位
- workQueue:任务等待队列(ArrayBlockingQueue/LinkedBlockingQueue/SynchronousQueue)
- threadFactory:线程工厂,可自定义线程名(便于排查问题)
- handler:拒绝策略(AbortPolicy/CallerRunsPolicy/DiscardPolicy/DiscardOldestPolicy)
执行流程:提交任务 → 核心线程未满则新建核心线程 → 核心线程满了则入队列 → 队列满了则新建非核心线程(不超过max)→ 超过max则触发拒绝策略。
配置原则:
- CPU密集型(计算、加密):核心线程数 = CPU核心数 + 1,避免过多线程上下文切换。
- IO密集型(数据库、HTTP调用):核心线程数 = CPU核心数 × 2,或根据线程数 = CPU核心数 / (1 - IO等待时间占比)计算。
美团外卖订单推送业务属于IO密集型,大量时间等待HTTP响应,因此线程池配置较大。
3. 如何设计一个高并发下的优惠券秒杀系统?
回答思路 这是美团最高频的系统设计题,直接关联业务。要从限流 → 库存设计 → 防超卖三个层面展开。
- 前端限流:按钮置灰、验证码、排队队列。
- 后端限流:网关层限流(令牌桶)、接口幂等性。
- 库存设计:Redis预扣库存,异步落库。
- 防超卖:Redis Lua脚本原子扣减,避免并发竞争。
- 最终一致性:MQ异步处理订单,失败补偿机制。
回答示例 整体设计分三层:
- 第一层:流量控制
- 前端按钮点击后置灰(防重复点击)、滑块验证码(防机器人)。
- Nginx限制单IP请求频率(令牌桶算法,如每秒最多5次)。
- 网关层对接口进行QPS限流(Sentinel/Hystrix)。
- 第二层:库存扣减(核心)
- 活动开始前,将优惠券库存量预加载到Redis:SET coupon:1001:stock 1000。
- 用户点击领券时,使用Lua脚本原子执行:
-- 先判断库存,再扣减,保证原子性
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) > 0 then
redis.call('DECR', KEYS[1])
return 1
else
return 0
end
- 扣减成功后,将用户ID和优惠券ID写入MQ(如RocketMQ),异步落库。
- 第三层:最终一致性
- 消费MQ消息,将优惠券发放记录写入MySQL。
- 如果落库失败,通过定时任务补偿(扫描Redis中已扣减但MySQL未落库的记录)。
4. 如何实现一个LRU缓存?
回答思路 美团面试高频手撕题,考察数据结构设计能力。核心是HashMap + 双向链表,保证get和put操作时间复杂度O(1)。
- 数据结构:HashMap<Key, Node> + 双向链表(表头=最近使用,表尾=最久未使用)。
- get操作:存在则移到表头,返回value;不存在返回-1。
- put操作:存在则更新value并移到表头;不存在则新建节点,插入表头,若容量超限则删除表尾节点。
回答示例
public class LRUCache {
private HashMap<Integer, Node> cache = new HashMap<>();
private DoubleLinkedList list = new DoubleLinkedList();
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
if (!cache.containsKey(key)) return -1;
Node node = cache.get(key);
list.moveToHead(node); // 访问后移到表头
return node.value;
}
public void put(int key, int value) {
if (cache.containsKey(key)) {
Node node = cache.get(key);
node.value = value;
list.moveToHead(node);
} else {
if (cache.size() >= capacity) {
Node tail = list.removeTail(); // 淘汰最久未使用的(表尾)
cache.remove(tail.key);
}
Node newNode = new Node(key, value);
cache.put(key, newNode);
list.addToHead(newNode);
}
}
// 内部类:双向链表
private static class Node {
int key, value;
Node prev, next;
Node(int k, int v) { this.key = k; this.value = v; }
}
private static class DoubleLinkedList {
Node head, tail; // 哑头和哑尾,表头=最新,表尾=最旧
DoubleLinkedList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
void addToHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
Node removeTail() {
Node node = tail.prev;
remove(node);
return node;
}
void moveToHead(Node node) {
remove(node);
addToHead(node);
}
}
}
5. MySQL主从同步原理是什么?如何解决主从延迟问题?
回答思路 美团数据库高可用必备知识,主从延迟是大规模读多写少场景的核心问题。
- 主从同步原理:Binlog → Dump线程 → IO线程 → RelayLog → SQL线程回放。
- 主从延迟原因:从库单线程回放慢、从库机器负载高、大事务导致从库延迟。
- 解决方案:并行复制(GTID/MTS)、读写分离+延迟路由、应用层判断延迟。
回答示例 MySQL主从同步原理(基于Binlog):
- 主库所有写操作记录为Binlog(statement/row/mixed格式);
- 主库的Dump线程将Binlog内容发送给从库的IO线程;
- 从库IO线程将接收到的Binlog写入本地RelayLog(中继日志);
- 从库SQL线程读取RelayLog,在本地重放执行SQL语句,完成数据同步。
主从延迟的原因:
- 从库回放慢:从库单线程顺序执行Binlog,遇到大事务(如批量更新10万行)时,从库需要很长时间才能追平主库;
- 从库机器负载高:IO/CPU竞争导致回放速度下降;
- 大事务:如果主库上一个事务执行了5分钟,从库至少也要5分钟才能追平。
解决方案:
- 并行复制(MySQL 5.7+):从库开启slave_parallel_workers和slave_parallel_type=LOGICAL_CLOCK,多个Worker线程并行回放Binlog,将延迟降低到毫秒级;
- 读写分离+延迟判断:对于强一致性要求的读(查订单状态),强制走主库;允许弱一致性的读(查商品详情)走从库,并在应用层判断主从延迟(如show slave status的Seconds_Behind_Master字段)决定路由策略;
- 避免大事务:将大事务拆分为小批次提交。
6. 如何处理分布式系统的超时和重试?
9. 你对美团优选(社区电商)有什么了解?技术挑战有哪些?
回答思路:这是美团特色题,考察你对美团核心业务的了解深度,以及能否将技术与业务场景结合。
- 业务理解:美团优选是社区团购模式,次日达,以低价生鲜为核心。
- 技术挑战:供应链库存管理、需求预测、损耗控制、物流调度。
- 个人结合点:如果你是技术岗,说明你能贡献的具体方向。
回答示例:美团优选采用的是预售+次日自提的社区电商模式:用户在当天23:59前下单,供应商次日送货到团长自提点,用户自提。核心价值是低价(预售降低库存损耗)和便利(下沉市场的家门口提货点)。
技术挑战我认为有三个方面:
- 需求预测:由于是预售模式,次日就要供货,供应商需要提前备货。如果预测不准——多备了卖不掉就是损耗(生鲜保质期短),少备了用户买不到就是缺货。要精准预测次日某个社区某个SKU的销量,需要结合历史数据、天气、节假日、促销计划等多维特征,是典型的时序预测问题。
- 库存精细化管理:SKU数量庞大(数千个),每个SKU在每个网格仓都有库存,需要做到单品级的库存控制。如果用简单的人工设置安全库存,每个SKU每天都要人工调整,根本不可行,需要建设智能补货系统。
- 配送调度优化:次日达意味着从供应商到网格仓再到团长的链路只有12-18小时可用,配送路径优化和时间窗口管理(团长什么时候方便接货)是核心挑战。
10. 如何保证MySQL和Redis的数据一致性?
回答思路:这是美团高频题,也是分布式系统中最经典的一致性问题之一。要给出多个方案的对比分析。
- Cache Aside(最常用):读时先Cache后DB,写时先DB后删Cache。
- 问题点:并发情况下可能产生脏数据,延迟双删/设置TTL可以缓解。
- Read Through / Write Through:旁白介绍,不常用。
- 延迟双删:写DB后延迟一段时间再删除Cache,缓解并发导致的脏读。
回答示例:Cache Aside(旁路缓存)是最常用的模式:
读操作:Cache命中则直接返回,未命中则查DB并写入Cache。
read(key):
value = redis.get(key)
if value == null:
value = mysql.get(key)
redis.set(key, value)
return value
写操作:先写DB,删除Cache(注意是删除不是更新)。
write(key, value):
mysql.set(key, value)
redis.del(key) // 删除而非更新,避免脏数据
为什么删除而不是更新?因为更新Cache时,如果DB写入成功但Cache更新失败,就会出现Cache和DB不一致;而删除Cache后,下次读请求会从DB读取到最新数据再写入Cache,天然自愈。
并发问题:并发场景下可能发生:
- 线程A读Cache未命中,查DB(得到旧值V1);
- 线程B更新DB为新值V2,删除Cache;
- 线程A把旧值V1写回Cache → Cache出现脏数据。
解决方案:延迟双删
write(key, value):
mysql.set(key, value)
redis.del(key) // 第一次删除
sleep(100ms) // 延迟100-300ms(覆盖读请求的完成时间)
redis.del(key) // 第二次删除
最终方案:业务对一致性要求极高的场景(如余额、库存),不建议用Cache Aside,建议直接读DB或用分布式锁串行读写。对于一致性要求不那么高的场景(用户头像、商品描述),Cache Aside + TTL即可。