从查询到缓存:MyBatis-Plus、MySQL 与 Redis 的协同工作流程详解

在 Java 后端开发中,数据层的设计直接影响系统性能和用户体验。MyBatis-Plus、MySQL 和 Redis 三者的组合,已经成为高性能业务系统的标配技术栈。它们并非孤立存在,而是通过明确的分工和协作,共同解决 “数据存储”、“操作效率” 和 “访问速度” 三大核心问题。本文将从实际开发场景出发,详解这三者如何协同工作,并给出可落地的实践方案。

一、技术定位:为什么需要这三个技术?

在开始讨论工作流程前,我们首先要明确这三个技术在系统中的角色定位。就像一个高效的仓库系统需要 “存储区”、“快速取货区” 和 “搬运工具”,这三个技术也分别承担着类似的职责。

MySQL:数据的 “最终存储中心”

MySQL 作为关系型数据库,是系统数据的 “唯一可信源”。它就像一个大型仓库的主存储区,负责:

  • 持久化存储所有核心业务数据(用户信息、订单记录、商品详情等)

  • 通过事务保证数据一致性(比如订单创建和库存扣减必须同时成功或失败)

  • 支持复杂查询和关联分析(通过 JOIN、索引、聚合函数等)

但 MySQL 也有明显短板:基于磁盘 IO 操作,在高并发查询场景下响应速度较慢,频繁访问容易成为系统瓶颈。这也是我们需要 Redis 的核心原因。

Redis:数据的 “高速缓存区”

Redis 作为内存数据库,相当于仓库的 “快速取货区”,专门存储高频访问的数据。它的核心价值在于:

  • 基于内存操作,读写速度可达 10 万级 QPS(是 MySQL 的 10-100 倍)

  • 支持多种数据结构(String、Hash、List 等),满足不同缓存需求

  • 可设置过期时间,自动清理不再需要的缓存数据

需要注意的是,Redis 中的数据是 “临时副本”,并非最终存储。它的存在是为了减少对 MySQL 的直接访问,就像我们会把常用工具放在桌面而不是仓库深处。

MyBatis-Plus:数据操作的 “高效工具”

MyBatis-Plus 是基于 MyBatis 的增强工具,相当于连接应用与 MySQL 的 “智能搬运车”。它并不直接参与缓存逻辑,而是通过简化数据访问层代码,让开发者更专注于业务逻辑:

  • 自动生成 CRUD 基础操作,无需编写重复 SQL

  • 提供条件构造器,动态构建查询条件

  • 内置分页、逻辑删除等常用功能

简单说,MyBatis-Plus 让我们操作 MySQL 变得更简单,而 Redis 让我们查询数据变得更快,三者各司其职又紧密配合。

二、核心工作流程:以商品查询为例

理解三者协同机制的最佳方式是通过实际业务场景。我们以 “电商系统商品详情查询” 为例,看看从用户发起请求到获取结果的完整过程中,这三个技术如何配合。

正常查询流程:缓存优先策略

商品详情是典型的 “读多写少” 场景,非常适合使用缓存。

正常查询流程图
用户发起查询请求

    │

    ▼

到达Service层

    │

    ▼

检查Redis缓存

    ├── 有缓存 → 直接返回结果给用户

    │

    └── 无缓存 → 调用MyBatis-Plus查询MySQL

                    │

                    ▼

              获取MySQL查询结果

                    │

                    ▼

              将结果写入Redis(设置过期时间)

                    │

                    ▼

              返回结果给用户

完整流程如下:

  1. 用户发起查询请求:用户在 APP 中点击商品,请求到达 Service 层

  2. 先查 Redis 缓存

public ProductDTO getProductDetail(Long productId) {

    // 1. 定义缓存key(建议使用业务前缀+唯一标识)

    String cacheKey = "product:detail:" + productId;

    // 2. 尝试从Redis获取缓存

    ProductDTO cachedProduct = redisTemplate.opsForValue().get(cacheKey);

    if (cachedProduct != null) {

        log.info("缓存命中,直接返回商品:{}", productId);

        return cachedProduct; // 缓存命中,直接返回

    }

    // 后续步骤:缓存未命中时的处理

}
  1. 缓存未命中,查询 MySQL:当 Redis 中没有对应数据时,通过 MyBatis-Plus 查询 MySQL
// 3. 缓存未命中,查询数据库

Product product = productMapper.selectById(productId);

if (product == null) {

    // 处理商品不存在的情况
    return null;
}

// 4. 转换为DTO(领域模型转数据传输对象)

ProductDTO productDTO = convertToDTO(product);
  1. 写入 Redis 缓存:将查询结果存入 Redis,设置合理的过期时间
// 5. 写入缓存(设置30分钟过期,避免缓存永久有效)

redisTemplate.opsForValue().set(

    cacheKey, 

    productDTO, 

    30, 

    TimeUnit.MINUTES
);

// 6. 返回结果

return productDTO;
  1. 结果返回:数据从 Service 层返回给 Controller,最终展示给用户

整个过程中,MyBatis-Plus 负责与 MySQL 交互(productMapper.selectById),Redis 负责缓存数据,而业务代码则协调两者的调用顺序。

数据更新流程:缓存一致性保障

当商品信息发生变更(如价格调整、库存更新)时,需要同步更新缓存,否则会出现 “缓存与数据库数据不一致” 的问题。

数据更新流程图
用户发起更新请求

    │

    ▼

到达Service层

    │

    ▼

调用MyBatis-Plus更新MySQL数据

    │

    ▼

判断更新是否成功(通过受影响行数)

    ├── 失败 → 抛出异常

    │

    └── 成功 → 处理Redis缓存

               ├── 方案A:直接更新Redis缓存(适合更新频率低场景)

               │

               └── 方案B:删除Redis旧缓存(适合更新频繁场景)

                       │

                       ▼

                 返回更新成功结果给用户

更新流程需遵循 “先更数据库,再更缓存” 的原则:

public void updateProduct(ProductUpdateDTO updateDTO) {
    // 1. 转换为实体对象
    Product product = convertToEntity(updateDTO);
    // 2. 先更新数据库(通过MyBatis-Plus)
    int rows = productMapper.updateById(product);
    if (rows <= 0) {
        throw new BusinessException("商品更新失败");
    }
    // 3. 再更新缓存(两种方案可选)
    String cacheKey = "product:detail:" + product.getId();
    // 方案A:直接更新缓存(适合更新频率低的场景)
    ProductDTO newProductDTO = convertToDTO(productMapper.selectById(product.getId()));
    redisTemplate.opsForValue().set(cacheKey, newProductDTO, 30, TimeUnit.MINUTES);
    // 方案B:删除旧缓存(适合更新频繁的场景,下次查询自动重建)
    // redisTemplate.delete(cacheKey);
}

这里的关键是先保证数据库数据正确,再处理缓存。因为数据库有事务支持,而 Redis 是内存操作,先更新数据库可以最大限度保证数据一致性。

三、进阶实践:解决缓存常见问题

在实际应用中,缓存与数据库的配合会遇到各种问题,需要针对性设计解决方案。结合 MyBatis-Plus 和 Redis 的特性,我们可以处理这些典型场景。

缓存穿透:如何应对不存在的数据查询

缓存穿透是指查询不存在的数据(如查询 ID 为 - 1 的商品),导致每次请求都穿透到数据库,给 MySQL 带来压力。

缓存穿透解决方案流程图
用户查询不存在的数据

    │

    ▼

到达Service层查询Redis

    │

    ▼

Redis无对应缓存(因数据不存在从未缓存)

    │

    ▼

调用MyBatis-Plus查询MySQL

    │

    ▼

MySQL返回空结果(数据不存在)

    │

    ▼

将空结果写入Redis(设置较短过期时间)

    │

    ▼

返回空结果给用户

(后续同ID查询将从Redis获取空结果,不再访问MySQL

解决方案是 “缓存空结果”:

public ProductDTO getProductDetail(Long productId) {

    String cacheKey = "product:detail:" + productId;
    // 先查缓存
    ProductDTO cachedProduct = redisTemplate.opsForValue().get(cacheKey);
    if (cachedProduct != null) {
        // 注意:如果是缓存的空对象,也直接返回
        return cachedProduct;
    }
    // 查数据库
    Product product = productMapper.selectById(productId);
    if (product == null) {
        // 缓存空结果(设置较短过期时间,如5分钟)
        redisTemplate.opsForValue().set(cacheKey, new ProductDTO(), 5, TimeUnit.MINUTES);
        return null;
    }
    // 正常缓存
    ProductDTO productDTO = convertToDTO(product);
    redisTemplate.opsForValue().set(cacheKey, productDTO, 30, TimeUnit.MINUTES);
    return productDTO;
}

缓存击穿:热点商品的缓存保护

缓存击穿指一个热点 key 突然过期时,大量请求同时穿透到数据库。比如某款热销商品的缓存过期瞬间,大量请求同时查询 MySQL。

缓存击穿的解决方案有很多,但互斥锁方案是最通用、最易实现的一种。其核心思想是:在缓存失效时,只允许一个请求去查询数据库并重建缓存,其他请求等待缓存重建完成后再从缓存获取数据

互斥锁方案的工作逻辑

就像多人要喝同一壶水:

  • 正常时:大家直接从水壶(缓存)取水

  • 水壶空了(缓存过期):只允许一个人去打水(查数据库)

  • 其他人:等待打水完成后再取水(等缓存重建)

缓存击穿解决方案流程图
热点商品缓存过期

    │

    ▼

大量请求同时到达Service层查询Redis

    │

    ▼

Redis无缓存(已过期)

    │

    ▼

请求竞争获取Redis互斥锁

    ├── 成功获取锁 → 调用MyBatis-Plus查询MySQL

    │                 │

    │                 ▼

    │           将新数据写入Redis

    │                 │

    │                 ▼

    │           释放互斥锁

    │                 │

    │                 ▼

    │           返回结果

    │

    └── 未获取锁 → 等待一段时间后重新查询Redis

                      │

                      ▼

                从Redis获取到新缓存(由获锁请求写入)

                      │

                      ▼

                返回结果

解决方案是 “互斥锁”:

public ProductDTO getProductDetail(Long productId) {
    String cacheKey = "product:detail:" + productId;
    // 先查缓存
    ProductDTO cachedProduct = redisTemplate.opsForValue().get(cacheKey);
    if (cachedProduct != null) {
        return cachedProduct;
    }
    // 缓存未命中,获取锁
    String lockKey = "lock:product:" + productId;
    try {
        // 尝试获取锁(设置3秒过期,避免死锁)
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);

        if (locked) {
            // 获得锁,查询数据库
            Product product = productMapper.selectById(productId);
            // ... 处理逻辑 ...
            return productDTO;
        } else {
            // 未获得锁,等待后重试(简单自旋)
            Thread.sleep(100);
            return getProductDetail(productId); // 递归重试
        }

    } finally {
        // 释放锁
        redisTemplate.delete(lockKey);
    }

}

缓存雪崩:避免大量缓存同时过期

缓存雪崩指大量缓存 key 在同一时间过期,导致数据库压力骤增。

缓存雪崩解决方案流程图
设置缓存时

    │

    ▼

确定基础过期时间(如30分钟)

    │

    ▼

生成随机偏移量(如-55分钟)

    │

    ▼

计算最终过期时间(基础时间+随机偏移量)

    │

    ▼

将数据写入Redis(使用最终过期时间)

    │

    ▼

不同key的过期时间分散

(避免大量key同时过期)

解决方案是 “过期时间随机化”:

// 设置过期时间时添加随机值(30±5分钟)

int baseExpire = 30;
int random = new Random().nextInt(10) - 5; // -5到5之间的随机数
redisTemplate.opsForValue().set(
    cacheKey, 
    productDTO, 
    baseExpire + random, 
    TimeUnit.MINUTES
);

四、总结:三者协同的核心原则

MyBatis-Plus、MySQL 和 Redis 的组合之所以成为主流,是因为它们完美解决了数据层的三大核心问题:数据存储(MySQL)、操作效率(MyBatis-Plus)和访问速度(Redis)。总结它们的协同原则:

  1. 数据流向清晰:查询时 “先缓存后数据库”,更新时 “先数据库后缓存”

  2. 职责边界明确:MySQL 存核心数据,Redis 存热点数据,MyBatis-Plus 处理数据访问

  3. 缓存策略灵活:根据业务场景选择缓存粒度、过期时间和更新方式

  4. 异常场景覆盖:针对穿透、击穿、雪崩等问题设计防护机制

在实际开发中,我们不需要刻意记忆复杂的理论,只需记住一个核心思想:用 MySQL 保证数据正确性,用 Redis 提升访问速度,用 MyBatis-Plus 简化开发工作。这三个技术的组合,让我们能够在保证系统稳定性的同时,轻松应对高并发场景下的性能挑战。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值