1. 项目概述:苍穹外卖菜品模块开发实录
去年接手公司外卖系统重构时,恰逢"苍穹外卖"这个开源项目在技术社区爆火。其清晰的模块划分和标准的MyBatis实践,为我们提供了绝佳的参考样本。今天要分享的正是其中最典型的业务场景——菜品管理模块的开发实录,重点解析那些官方文档不会告诉你的实战细节。
这个模块看似简单,实则暗藏玄机。从MyBatis的XML映射配置到批量操作优化,从前后端数据对接到异常处理机制,每个环节都需要结合业务特点做针对性设计。尤其在日均订单量超过5万笔的生产环境中,一个不合理的SQL映射就可能成为系统瓶颈。
2. 核心需求拆解与架构设计
2.1 业务需求分析
典型的菜品管理模块需要支持以下核心功能:
- 多维度菜品信息录入(基础信息、分类、规格、图片等)
- 动态库存管理
- 多条件组合查询
- 批量上下架操作
在苍穹外卖的实现中,特别考虑了外卖业务的特殊性:
- 规格组合:同一菜品可能有不同规格(如大/中/小份),需要支持价格动态计算
- 时效控制:早餐类菜品需要在指定时段显示
- 关联营销:菜品需要与优惠活动、套餐进行关联
2.2 技术栈选型
| 技术组件 | 选型理由 | 特别优化点 |
|---|---|---|
| MyBatis | 灵活的SQL控制能力 | 启用二级缓存+本地缓存 |
| XML映射 | 便于复杂SQL维护 | 使用 |
| PageHelper | 简化分页逻辑 | 重写count查询优化性能 |
| Jackson | 完善的对象映射 | 自定义日期序列化格式 |
提示:在日均UV超过10万的系统中,MyBatis的缓存配置需要特别关注。我们最终采用READ_WRITE隔离级别+LRU淘汰策略,缓存命中率提升到78%
3. MyBatis XML映射深度解析
3.1 基础映射配置
菜品模块的核心映射文件DishMapper.xml包含以下关键部分:
xml复制<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="com.cangqiong.model.Dish">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="category_id" property="categoryId" jdbcType="BIGINT"/>
<!-- 省略其他字段... -->
</resultMap>
<!-- 包含分类信息的扩展映射 -->
<resultMap id="WithCategoryResultMap" extends="BaseResultMap"
type="com.cangqiong.model.Dish">
<association property="category" javaType="com.cangqiong.model.Category">
<id column="c_id" property="id"/>
<result column="c_name" property="name"/>
</association>
</resultMap>
3.2 动态SQL实践
菜品查询需要支持多达12种组合条件,这是典型的动态SQL应用场景:
xml复制<select id="selectByCondition" resultMap="WithCategoryResultMap">
SELECT d.*, c.id as c_id, c.name as c_name
FROM dish d LEFT JOIN category c ON d.category_id = c.id
<where>
<if test="name != null and name != ''">
AND d.name LIKE CONCAT('%',#{name},'%')
</if>
<if test="categoryId != null">
AND d.category_id = #{categoryId}
</if>
<if test="status != null">
AND d.status = #{status}
</if>
<!-- 时间范围查询 -->
<if test="beginTime != null and endTime != null">
AND d.create_time BETWEEN #{beginTime} AND #{endTime}
</if>
</where>
ORDER BY d.update_time DESC
</select>
3.3 批量操作优化
外卖系统经常需要批量更新菜品状态(如节假日统一打烊),我们对比了三种实现方案:
- 循环单次更新:简单但性能差(100条数据需1.2s)
- 拼接动态SQL:较快但有SQL注入风险
- 批量UPDATE:最优选择(100条数据仅需0.3s)
最终采用的批量更新方案:
xml复制<update id="batchUpdateStatus">
UPDATE dish
SET status = #{status},
update_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
4. 前后端交互关键实现
4.1 数据格式设计
菜品新增接口的请求体设计需要考虑扩展性:
json复制{
"name": "宫保鸡丁",
"categoryId": 1,
"price": 3800,
"description": "经典川菜微辣口味",
"image": "https://xxx.com/xxx.jpg",
"flavors": [
{
"name": "辣度",
"value": "微辣"
},
{
"name": "分量",
"value": "大份"
}
],
"status": 1
}
4.2 事务控制要点
菜品新增涉及多表操作(dish表+flavor表),必须添加事务控制:
java复制@Transactional
public void saveWithFlavor(DishDTO dishDTO) {
// 1.保存菜品基本信息
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.insert(dish);
// 2.获取菜品ID
Long dishId = dish.getId();
// 3.保存口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && !flavors.isEmpty()){
flavors.forEach(flavor -> {
flavor.setDishId(dishId);
});
dishFlavorMapper.insertBatch(flavors);
}
}
注意:@Transactional默认只对RuntimeException回滚,建议明确指定rollbackFor
5. 性能优化实战记录
5.1 查询优化方案
通过EXPLAIN分析发现菜品列表查询的瓶颈在分类关联查询,优化方案:
- 添加复合索引:
ALTER TABLE dish ADD INDEX idx_category_status (category_id, status) - 重写count查询:
xml复制<select id="countByCondition" resultType="int"> SELECT COUNT(1) FROM dish d <include refid="conditionSql"/> </select> - 启用MyBatis二级缓存(需处理缓存一致性)
5.2 批处理改造
原批量插入采用循环单次插入,改造为MyBatis批量执行器:
java复制public void insertBatch(List<DishFlavor> flavors) {
SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory()
.openSession(ExecutorType.BATCH, false);
try {
DishFlavorMapper mapper = sqlSession.getMapper(DishFlavorMapper.class);
for (DishFlavor flavor : flavors) {
mapper.insert(flavor);
}
sqlSession.commit();
} finally {
sqlSession.close();
}
}
实测性能对比:
- 100条数据:从1200ms → 150ms
- 500条数据:从6500ms → 600ms
6. 典型问题排查手册
6.1 XML映射常见陷阱
问题1:<if test="status != null"> 判断失效
- 原因:MyBatis使用OGNL表达式,0会被视为空
- 解决:改为
<if test="status != null or status == 0">
问题2:批量插入返回的主键ID不正确
- 原因:没有配置useGeneratedKeys
- 解决:添加
useGeneratedKeys="true" keyProperty="id"
6.2 事务失效场景
-
自调用问题:同类中A方法调用B方法(@Transactional)
- 解决:通过代理对象调用或拆分类
-
异常被捕获:try-catch吞掉异常
- 解决:catch中抛出RuntimeException
-
非public方法:Spring代理限制
- 解决:改为public方法
7. 扩展思考与进阶优化
在完成基础功能后,我们进一步实施了这些优化:
-
弹性字段设计:使用JSON类型存储菜品扩展属性
sql复制ALTER TABLE dish ADD COLUMN ext_info JSON; -
历史版本管理:通过触发器记录菜品变更历史
sql复制CREATE TRIGGER dish_history AFTER UPDATE ON dish FOR EACH ROW INSERT INTO dish_history(...) VALUES(...); -
分布式锁控制:使用Redis实现菜品库存扣减的原子操作
java复制String lockKey = "dish_stock:" + dishId; try { Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if(locked) { // 扣减库存操作 } } finally { redisTemplate.delete(lockKey); }
这个模块的开发经历让我深刻体会到:看似简单的CRUD操作,在真实业务场景下需要考虑的细节远超想象。特别是MyBatis的使用,灵活性的背后是对开发者更高的要求。建议大家在日常开发中养成这些习惯:
- 为所有SQL添加EXPLAIN分析
- 统一管理XML中的SQL片段
- 建立标准的异常处理规范
- 重要操作添加操作日志
- 定期ReviewMapper文件中的SQL性能
