项目视角面试复盘:苍穹外卖套餐新增功能踩坑与核心技术考点

项目视角面试复盘:苍穹外卖套餐新增功能踩坑与核心技术考点

引言

Java后端面试中,面试官最看重「项目实战经验」—— 不是死记硬背技术原理,而是能结合项目场景讲清“遇到什么问题、怎么解决、为什么这么选”。本文以苍穹外卖项目的「套餐新增功能」为核心,从“项目开发实战→问题排查→面试答题逻辑”三层展开,既还原真实开发场景,又提炼面试高频考点,内容可直接复制用于面试复习,让你在回答时既有项目细节,又有技术深度。

一、项目背景与功能需求(面试开篇必讲)

功能描述

开发外卖平台的「套餐新增」功能,核心流程:

  1. 接收前端传入的套餐信息(名称、分类ID、价格、包含的菜品列表等);
  2. 校验套餐名称唯一性、参数合法性;
  3. 插入套餐主表(setmeal)和套餐-菜品中间表(setmeal_dish);
  4. 缓存新增套餐,清理旧缓存,提升查询性能。

技术栈

Spring Boot + MyBatis + MySQL + Redis(缓存)

二、项目开发踩坑实录(面试核心:讲清问题与解决方案)

坑点1:缓存key拼接失败(自增ID未回写)

项目场景

插入套餐主表后,需要用套餐ID拼接缓存key(如ADMINSETMEAL_KEY+1001),但setmeal.getId()始终为null,导致缓存key无效(ADMINSETMEAL_KEY+null)。

问题排查
  • 数据库中setmeal表的id是自增主键(AUTO_INCREMENT),插入时未传入ID,数据库已成功生成ID;
  • 但MyBatis默认不会把数据库生成的ID同步到Java实体对象,导致setmealid属性还是初始值null。
项目解决方案(代码+操作)

修改SetmealMapper.xmlinsert标签,添加useGeneratedKeyskeyProperty配置:

<insert id="insert" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO sky_take_out.setmeal (name, category_id, price, status, description, image, create_time, update_time, create_user, update_user)
    VALUES (#{name}, #{categoryId}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
</insert>
项目效果

插入后setmeal.getId()能直接获取数据库生成的ID(如1001),缓存key拼接成功。

面试提炼考点

MyBatis自增ID回写机制,核心配置与原理。

坑点2:中间表数据无效(多对多关联断裂)

项目场景

插入setmeal_dish后,查询套餐关联的菜品时无结果,查看数据库发现setmeal_dishsetmeal_id字段全为null。

问题排查
  • 套餐与菜品是多对多关系,中间表setmeal_dish需通过setmeal_iddish_id建立关联;
  • 开发时遗漏了给SetmealDish对象设置setmeal_id,导致关联断裂。
项目解决方案(代码+逻辑)

先插入套餐主表(获取回写的ID),再循环给中间表对象设置setmealId

// 1. 先插套餐主表(获取自增ID)
setmealMapper.insert(setmeal);
Long setmealId = setmeal.getId(); // 回写后的套餐ID

// 2. 给中间表设置套餐ID,再插入
for (SetmealDish setmealDish : setmealDishes) {
    setmealDish.setSetmealId(setmealId); // 关键:建立套餐与菜品的关联
    setmealDishMapper.insert(setmealDish);
}
项目效果

setmeal_dish表的setmeal_id字段正常赋值,后续查询套餐关联菜品功能正常。

面试提炼考点

多对多关系的表设计与插入顺序(先主表后中间表)。

坑点3:数据一致性风险(多表插入无事务)

项目场景

测试时发现:套餐主表插入成功,但中间表插入时因菜品ID不存在抛出异常,导致主表有数据、中间表无数据,出现“孤儿套餐”(无关联菜品的无效套餐)。

问题排查
  • 多表操作(主表+中间表)未配置事务,某一步失败时无法回滚之前的操作;
  • 违背事务原子性(ACID原则):多表操作要么全成功,要么全失败。
项目解决方案(代码+配置)

给Service方法添加@Transactional注解,指定回滚异常类型:

@Override
@Transactional(rollbackFor = BaseException.class) // 所有业务异常都回滚
public void addSetmeal(SetmealDTO setmealDTO) {
    // 校验+插入主表+插入中间表+缓存操作
}
项目效果

中间表插入失败时,主表插入操作自动回滚,数据库无无效数据。

面试提炼考点

Spring事务管理(原子性、回滚条件配置)。

坑点4:接口响应慢(同步清理缓存)

项目场景

新增套餐后,同步清理Redis中的套餐缓存,接口响应时间从50ms增至200ms,高并发场景下性能瓶颈明显。

问题排查
  • 同步清理缓存时,主线程需等待缓存操作完成才能返回,阻塞了请求;
  • 缓存清理是非核心流程,无需同步执行。
项目解决方案(代码优化)

CompletableFuture异步清理缓存,不阻塞主线程:

// 异步清理缓存,提升接口响应速度
CompletableFuture.runAsync(() -> setmealCacheManager.cleanSetmealCache());
项目效果

接口响应时间恢复至50ms左右,高并发下吞吐量提升3倍。

面试提炼考点

异步编程(CompletableFuture)在项目中的应用,性能优化思路。

三、项目优化后完整代码(面试可直接讲的实战代码)

import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.concurrent.CompletableFuture;

@Override
@Transactional(rollbackFor = BaseException.class) // 事务保证原子性
public void addSetmeal(SetmealDTO setmealDTO) {
    // 1. 业务校验:套餐名称唯一性(项目中实际遇到重复名称问题,所以加此校验)
    Setmeal oldSetmeal = setmealMapper.searchByName(setmealDTO.getName());
    if (oldSetmeal != null) {
        throw new SetmealException(ErrorCode.SETMEALDISH_DATA_FORMAT_ABNORMAL, "套餐名称重复,请更换");
    }

    // 2. 参数合法性校验(避免无效数据插入,项目中踩过参数为空导致的SQL异常)
    setmealValidator.validateSetmealDTO(setmealDTO);
    List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
    for (SetmealDish dish : setmealDishes) {
        setmealDishValidator.validateSetmealDish(dish);
    }

    // 3. 实体赋值(项目中统一维护创建时间、操作人等基础字段)
    Setmeal setmeal = new Setmeal();
    BeanUtils.copyProperties(setmealDTO, setmeal);
    setmeal.setStatus(StatusConstant.DISABLE); // 默认为停售状态,符合业务逻辑
    setmeal.setCreateTime(LocalDateTime.now());
    setmeal.setUpdateTime(LocalDateTime.now());
    setmeal.setCreateUser(BaseContext.getCurrentId()); // 从上下文获取登录用户ID
    setmeal.setUpdateUser(BaseContext.getCurrentId());

    // 4. 先插主表:获取自增ID(解决中间表关联问题)
    setmealMapper.insert(setmeal);
    Long setmealId = setmeal.getId(); // MyBatis回写后的ID

    // 5. 再插中间表:建立套餐与菜品关联(项目中踩过关联字段为空的坑)
    for (SetmealDish dish : setmealDishes) {
        dish.setSetmealId(setmealId); // 必传外键
        setmealDishMapper.insert(dish);
    }

    // 6. 缓存新增套餐:数据库插入成功后再缓存(避免缓存脏数据)
    String cacheKey = CacheConstant.ADMINSETMEAL_KEY + setmealId;
    setmealCacheManager.set(
        cacheKey,
        setmealDTO,
        CacheConstant.CACHE_EXPIRE_MINUTES,
        TimeUnit.MINUTES,
        CacheConstant.CACHE_RANDOM_OFFSET // 项目中添加随机过期时间,避免缓存雪崩
    );

    // 7. 异步清理旧缓存:提升接口响应速度(项目中优化后的关键步骤)
    CompletableFuture.runAsync(() -> setmealCacheManager.cleanSetmealCache());
}

四、面试答题逻辑(项目视角版)

面试官问:“你在项目中做过套餐新增功能吗?遇到过什么问题?怎么解决的?”

标准回答(按以下逻辑讲,既有细节又有深度)
  1. 先讲功能背景:“我在苍穹外卖项目中负责过套餐新增功能,核心是把套餐主信息和关联的菜品信息存入数据库,同时更新缓存,支撑前端套餐列表和详情查询。”
  2. 再讲遇到的核心问题:“开发时遇到了4个关键问题:一是自增ID未回写导致缓存key无效,二是中间表关联字段缺失导致数据无效,三是多表插入无事务导致数据不一致,四是同步清理缓存导致接口响应慢。”
  3. 重点讲解决方案(结合代码):“针对ID回写问题,我在MyBatis的insert标签加了useGeneratedKeys和keyProperty配置,让数据库生成的ID自动回写到实体类;针对中间表关联问题,我调整了插入顺序,先插主表拿ID,再给中间表设置关联字段;针对事务问题,加了@Transactional注解保证原子性;针对响应慢,用CompletableFuture异步清理缓存。”
  4. 最后讲优化效果:“优化后,接口响应时间从200ms降到50ms,高并发下无数据不一致问题,缓存也没出现脏数据,功能稳定上线。”

五、面试高频追问(项目延伸版)

追问1:你们项目中ID生成策略后来有调整吗?为什么?

项目视角回答:

“初期项目是单体应用,用的数据库自增ID,简单高效;后来项目扩容成分布式架构,多个数据库节点导致自增ID重复,所以改成了雪花ID。具体改法是:用Java代码生成雪花ID后主动设置到Setmeal实体类,不需要MyBatis回写,同时中间表的setmealId直接用生成的雪花ID,避免了分布式ID冲突问题。”

追问2:为什么事务注解要指定rollbackFor=BaseException.class?

项目视角回答:

“项目中我们自定义的业务异常(如SetmealException)都继承了BaseException,而Spring事务默认只对RuntimeException回滚。如果不指定rollbackFor,当抛出业务异常(如套餐名称重复)时,事务不会回滚,可能导致部分数据插入成功,所以必须明确指定回滚的异常类型。”

追问3:缓存为什么要在数据库插入成功后才操作?如果缓存设置失败怎么办?

项目视角回答:

“这是为了避免缓存脏数据。如果先更缓存再插数据库,数据库插入失败但缓存已经更新,会导致缓存中存在无效数据。项目中我们用了两个兜底方案:一是缓存设置失败时会重试3次,二是给缓存设置了过期时间(比如30分钟),即使重试失败,过期后也会自动加载最新数据。”

追问4:多表操作中,插入顺序为什么是“先主表后中间表”?反过来行不行?

项目视角回答:

“不行。因为中间表需要主表的ID作为关联字段,反过来先插中间表的话,主表ID还没生成,中间表的关联字段只能为空,导致关联断裂。项目中一开始我误把顺序搞反了,测试时查询套餐菜品为空,排查后才调整了插入顺序。”

六、项目实战核心考点清单(面试快速记忆)

  1. MyBatis自增ID回写:useGeneratedKeys="true"+keyProperty="id"(项目必备配置);
  2. 多对多关联插入顺序:先主表(获取ID)→ 再中间表(关联ID)(项目踩坑点);
  3. 事务配置:@Transactional(rollbackFor=自定义异常.class)(项目数据一致性保障);
  4. 缓存操作原则:数据库操作成功后再缓存,异步清理缓存(项目性能优化点);
  5. ID生成策略:单体用自增ID,分布式用雪花ID(项目架构演进点)。

总结

本文以真实项目为载体,把MyBatis、事务、缓存、多表关联等面试高频考点,融入“问题-排查-解决-优化”的项目流程中。面试中,面试官不缺理论知识扎实的候选人,缺的是能把技术落地到项目、能解决实际问题的人。

按本文的思路准备,你在回答时不会显得空洞,而是能结合项目细节讲清技术的应用场景和底层逻辑,大幅提升面试通过率。代码和回答逻辑均可直接复用,建议收藏备用!

posted @ 2025-11-18 20:54  WILK  阅读(1)  评论(0)    收藏  举报  来源