Spring Boot + MyBatis + Redis 整合实战 ------ 项目源码深度解析
本文基于一个完整的 Spring Boot + MyBatis + Redis 整合示例项目,逐层剖析项目结构、核心代码与缓存策略,帮助你快速理解三者如何协同工作。
一、项目概览
本项目是一个城市信息 CRUD 管理系统,核心技术栈:
| 技术 | 版本 | 作用 |
|---|---|---|
| Spring Boot | 2.1.3.RELEASE | 应用框架 |
| MyBatis | 1.2.0 | ORM 持久层 |
| Redis | - | 缓存层 |
| MySQL | 8.0.33 | 关系型数据库 |
二、项目结构
springboot-mybatis-redis/
├── pom.xml # Maven 依赖配置
├── src/
│ ├── main/
│ │ ├── java/org/spring/springboot/
│ │ │ ├── Application.java # 启动入口类
│ │ │ ├── common/
│ │ │ │ ── Result.java # 统一响应封装
│ │ │ ├── controller/
│ │ │ │ └── CityRestController.java # REST 接口层
│ │ │ ├── dao/
│ │ │ │ └── CityDao.java # MyBatis DAO 接口
│ │ │ ├── domain/
│ │ │ │ └── City.java # 城市实体类
│ │ │ └── service/
│ │ │ ├── CityService.java # 业务接口
│ │ │ └── impl/
│ │ │ ├── CityServiceImpl.java # 业务实现(含 Redis 缓存逻辑)
│ │ │ └── RedisConfig.java # Redis 序列化配置
│ │ └── resources/
│ │ ├── application.properties # 配置文件(DB/Redis/MyBatis)
│ │ └── mapper/
│ │ ── CityMapper.xml # MyBatis SQL 映射文件
│ └── test/resources/
│ └── city_post.json # 测试数据
└── target/ # 编译输出目录
三、依赖配置(pom.xml)
xml
<!-- Spring Boot Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot MyBatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
<!-- MySQL 连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
核心依赖只有四个:Redis 、Web 、MyBatis 、MySQL 驱动,简洁清晰。
四、配置文件(application.properties)
properties
## 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdb?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## MyBatis 配置
mybatis.typeAliasesPackage=org.spring.springboot.domain
mybatis.mapperLocations=classpath:mapper/*.xml
## Redis 配置
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.timeout=5000
配置分为三部分:
- 数据源 :连接本地 MySQL 的
springbootdb数据库 - MyBatis:指定实体类包路径和 Mapper XML 文件位置
- Redis:连接本地 6379 端口,无密码,使用默认数据库索引 0
五、启动类(Application.java)
java
@SpringBootApplication
@MapperScan("org.spring.springboot.dao")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
两个关键注解:
@SpringBootApplication:标记为 Spring Boot 应用,自动配置 + 组件扫描@MapperScan("org.spring.springboot.dao"):扫描 DAO 包,自动注册 MyBatis Mapper 接口
六、实体类(City.java)
java
public class City implements Serializable {
private static final long serialVersionUID = -1L;
private Long id; // 城市编号
private Long provinceId; // 省份编号
private String cityName; // 城市名称
private String description; // 描述
}
注意 :实体类实现了
Serializable接口。这是因为对象需要被序列化后存入 Redis 缓存,不实现该接口会导致序列化异常。
七、DAO 层(CityDao.java + CityMapper.xml)
DAO 接口
java
public interface CityDao {
List<City> findAllCity();
City findById(@Param("id") Long id);
Long saveCity(City city);
Long updateCity(City city);
Long deleteCity(Long id);
}
MyBatis SQL 映射
xml
<mapper namespace="org.spring.springboot.dao.CityDao">
<resultMap id="BaseResultMap" type="org.spring.springboot.domain.City">
<result column="id" property="id" />
<result column="province_id" property="provinceId" />
<result column="city_name" property="cityName" />
<result column="description" property="description" />
</resultMap>
<select id="findById" resultMap="BaseResultMap" parameterType="java.lang.Long">
SELECT id, province_id, city_name, description FROM city WHERE id = #{id}
</select>
<insert id="saveCity" parameterType="City" useGeneratedKeys="true" keyProperty="id">
INSERT INTO city(id, province_id, city_name, description)
VALUES(#{id}, #{provinceId}, #{cityName}, #{description})
</insert>
<update id="updateCity" parameterType="City">
UPDATE city
SET
<if test="provinceId!=null">province_id = #{provinceId},</if>
<if test="cityName!=null">city_name = #{cityName},</if>
<if test="description!=null">description = #{description}</if>
WHERE id = #{id}
</update>
<delete id="deleteCity" parameterType="java.lang.Long">
DELETE FROM city WHERE id = #{id}
</delete>
</mapper>
亮点 :updateCity 使用了 <if> 动态 SQL,只更新传入的非空字段,避免将字段误置为 NULL。
八、Redis 序列化配置(RedisConfig.java)
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashKeySerializer(stringSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
为什么需要这个配置?
Spring Data Redis 默认使用 JDK 序列化,存入 Redis 的数据是二进制乱码,无法通过 redis-cli 直接阅读。此配置将 Value 的序列化方式改为 Jackson JSON,使得缓存中的数据以可读的 JSON 格式存储。
| 部分 | 序列化方式 | 效果 |
|---|---|---|
| Key | String | city_1 清晰可读 |
| Value | Jackson JSON | {"id":1,"cityName":"杭州",...} 而非二进制乱码 |
九、业务层 ------ 核心缓存逻辑(CityServiceImpl.java)
这是整个项目最核心的文件 ,实现了经典的 Cache-Aside(旁路缓存)策略:
9.1 查询逻辑 ------ 先缓存后数据库
java
public City findCityById(Long id) {
String key = "city_" + id;
ValueOperations<String, City> operations = redisTemplate.opsForValue();
// 1. 先查缓存
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {
City city = operations.get(key);
LOGGER.info("从缓存中获取了城市 >> " + city);
return city;
}
// 2. 缓存未命中,查数据库
City city = cityDao.findById(id);
if (city == null) {
LOGGER.warn("数据库未找到城市,id={}", id);
return null;
}
// 3. 写入缓存,TTL 500 秒
operations.set(key, city, 500, TimeUnit.SECONDS);
LOGGER.info("城市插入缓存 >> " + city);
return city;
}
流程图:
查询请求 → Redis 缓存命中?
├─ 是 → 直接返回缓存数据 ✅
└─ 否 → 查询 MySQL DB
├─ 找到 → 写入 Redis(TTL 500s)→ 返回
└─ 未找到 → 返回 null(不缓存空值)
9.2 更新逻辑 ------ 先更新数据库,再删除缓存
java
public Long updateCity(City city) {
Long ret = cityDao.updateCity(city);
// 删除缓存,保证下次查询走数据库获取最新数据
String key = "city_" + city.getId();
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {
redisTemplate.delete(key);
LOGGER.info("从缓存中删除城市 >> " + city);
}
return ret;
}
9.3 删除逻辑 ------ 先删除数据库,再删除缓存
java
public Long deleteCity(Long id) {
Long ret = cityDao.deleteCity(id);
String key = "city_" + id;
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {
redisTemplate.delete(key);
LOGGER.info("从缓存中删除城市 ID >> " + id);
}
return ret;
}
缓存策略总结
| 操作 | 步骤 | 原因 |
|---|---|---|
| 查询 | 查缓存 → 未命中查 DB → 写缓存 | 减少数据库压力 |
| 更新 | 更新 DB → 删除缓存 | 保证数据一致性,下次查询自动加载新数据 |
| 删除 | 删除 DB → 删除缓存 | 防止缓存中存在已删除的脏数据 |
这种策略被称为 Cache-Aside Pattern(旁路缓存模式),是业界最常用的缓存方案。
十、REST 接口层(CityRestController.java)
java
@RestController
public class CityRestController {
@Autowired
private CityService cityService;
@RequestMapping(value = "/api/city/{id}", method = RequestMethod.GET)
public Result<Object> findOneCity(@PathVariable("id") Long id) { ... }
@RequestMapping(value = "/api/city", method = RequestMethod.POST)
public Result<City> createCity(@RequestBody City city) { ... }
@RequestMapping(value = "/api/city", method = RequestMethod.PUT)
public Result<City> modifyCity(@RequestBody City city) { ... }
@RequestMapping(value = "/api/city/{id}", method = RequestMethod.DELETE)
public Result<Long> deleteCity(@PathVariable("id") Long id) { ... }
}
API 接口一览
| 方法 | 路径 | 功能 | 请求示例 |
|---|---|---|---|
| GET | /api/city/{id} |
查询城市 | GET /api/city/1 |
| POST | /api/city |
新增城市 | POST /api/city + JSON Body |
| PUT | /api/city |
更新城市 | PUT /api/city + JSON Body |
| DELETE | /api/city/{id} |
删除城市 | DELETE /api/city/1 |
十一、统一响应封装(Result.java)
java
public class Result<T> {
private int code; // 200 = 成功
private String message; // 响应提示
private T data; // 业务数据
public static <T> Result<T> success(T data) { ... }
public static <T> Result<T> error(int code, String message) { ... }
}
所有接口统一返回 Result<T> 格式:
json
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"provinceId": 1,
"cityName": "杭州",
"description": "人间天堂"
}
}
十二、整体数据流转
┌──────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────┐
│ 客户端 │────▶│ Controller │────▶│ Service │────▶│ Redis │
│ 请求 │────│ 接口层 │◀────│ 业务层 │◀────│ 缓存 │
└──────────┘ └──────────────┘ └───────┬───────┘ └──────────┘
│
▼
┌──────────────┐
│ MySQL │
│ 数据库 │
└──────────────┘
完整请求流程:
- 客户端发起
GET /api/city/1请求 CityRestController接收请求,调用CityService.findCityById(1)CityServiceImpl先查询 Redis 缓存city_1- 缓存命中 → 直接返回;缓存未命中 → 通过
CityDao查询 MySQL - 查询结果写入 Redis(TTL 500 秒),然后返回给客户端
Result封装为统一 JSON 格式响应
十三、总结
本项目虽然代码量不大,但完整展示了 Spring Boot + MyBatis + Redis 三者整合的核心要点:
| 要点 | 说明 |
|---|---|
| 分层架构 | Controller → Service → DAO,职责清晰 |
| MyBatis 集成 | 接口 + XML 映射,动态 SQL 支持 |
| Redis 缓存 | Cache-Aside 策略,查询走缓存,写操作删缓存 |
| 序列化方案 | Jackson JSON 替代 JDK 序列化,数据可读 |
| 统一响应 | Result 封装,前后端交互规范 |
| 空值保护 | DB 未查到不缓存 null,避免 NPE |
这是一个非常适合作为 Spring Boot 缓存入门 的学习项目,掌握了这个项目的思路,就可以在实际业务中灵活运用 Redis 缓存来提升系统性能。