Spring Boot + MyBatis + Redis 整合实战 —— 项目源码深度解析

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      │
                                       │    数据库      │
                                       └──────────────┘

完整请求流程:

  1. 客户端发起 GET /api/city/1 请求
  2. CityRestController 接收请求,调用 CityService.findCityById(1)
  3. CityServiceImpl 先查询 Redis 缓存 city_1
  4. 缓存命中 → 直接返回;缓存未命中 → 通过 CityDao 查询 MySQL
  5. 查询结果写入 Redis(TTL 500 秒),然后返回给客户端
  6. 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 缓存来提升系统性能。

相关推荐
今年下半年4 小时前
【springboot】对接海康威视视频监控技术文档
spring boot·视频监控·安防·海康
小蒜学长5 小时前
基于SpringBoot+Vue的小学数学智能出题系统(代码+数据库+LW)
java·数据库·spring boot·后端·智能出题系统
FYKJ_20106 小时前
springboot贺州地方新闻网站87812-计算机课程设计、毕业设计
java·vue.js·spring boot·python·mysql·django·课程设计
MZ_ZXD0017 小时前
【关注可白嫖源码】--课程设计--毕业设计-- 42557基于springboot的大学生体质健康管理系统
spring boot·课程设计·毕设·大作业·程序开发·程序定制·毕设代做
w***48828 小时前
SpringBoot整合easy-es
spring boot·后端·elasticsearch
摇滚侠9 小时前
《Spring Boot 3:高级与架构设计》第 3 章 Bean 的全生命周期原理 Bean 的全生命周期概览 阅读笔记 9
spring boot·笔记·后端
imDwAaY9 小时前
Redis String 为什么不直接用 C 字符串?从 SDS 到三种编码
redis
imDwAaY9 小时前
Redis 的数据到底怎么存?一篇理清数据类型与底层结构
redis
CarIise10 小时前
Spring Boot图片展示与统一返回格式:从资源映射到泛型封装实战
mybatis
谢亮_vipxieliang10 小时前
一套 Compose 搭起完整开发环境:MySQL + Redis + Nginx
redis·mysql·nginx·docker