百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

SpringBoot 整合 MyBatis 组合 Redis 作为数据源缓存

mhr18 2024-11-19 06:58 21 浏览 0 评论

写在最前

MyBatis 是常见的 Java 数据库访问层框架。在日常工作中,开发人员多数情况下是使用 MyBatis 的默认缓存配置,但是 MyBatis 缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患。

本文介绍的是 Redis 组合 MyBatis 作为数据源缓存。**并不是用 Redis 作为 Mybatis 的二级缓存类型!,也不是使用 Mybatis 一级缓存或二级缓存作为数据源缓存 **。

MyBatis 一级缓存与二级缓存

虽然本文不是使用 Mybatis 一级缓存或二级缓存作为数据源缓存,但还是要简单介绍一下 MyBatis 一级缓存与二级缓存,以及为什么不使用其一级缓存或二级缓存,而是推荐使用 Redis 组合 Mybatis 的可控制的缓存代替二级缓存!

推荐阅读文章:tech.meituan.com/2018/01/19/…

一级缓存

在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。

一级缓存小结:

  1. MyBatis 一级缓存的生命周期和 SqlSession 一致。
  2. MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有所欠缺。
  3. MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为 Statement。

二级缓存

在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。

二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库

二级缓存小结:

  1. MyBatis 的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强。
  2. MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  3. 在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。

SpringBoot MyBatis 整合 Redis Cache

本工程基于 mingyue-springboot-mybatis 改造

1. 增加 Redis 依赖

xml复制代码<!--redis-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 增加 Redis 修改配置

yaml复制代码spring:
  redis:
    host: 127.0.0.1
    password: 123456
    port: 6379

3. 添加 Redis 配置类

java复制代码import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * Redis 配置
 *
 * @author Strive
 * @date 2023/4/18 18:53
 */
@Configuration
public class RedisConfig {

    @Bean(name = "redisTemplate")
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(keySerializer());
        redisTemplate.setHashKeySerializer(keySerializer());
        redisTemplate.setValueSerializer(valueSerializer());
        redisTemplate.setHashValueSerializer(valueSerializer());
        return redisTemplate;
    }

    @Primary
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
        //缓存配置对象
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();

        redisCacheConfiguration = redisCacheConfiguration
                //设置缓存的默认超时时间:30分钟
                .entryTtl(Duration.ofMinutes(30L))
                //如果是空值,不缓存
                .disableCachingNullValues()
                //设置key序列化器
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                //设置value序列化器
           .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer((valueSerializer())));

        return RedisCacheManager
                .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                .cacheDefaults(redisCacheConfiguration).build();
    }

    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }

    private RedisSerializer<Object> valueSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

4. 改造 queryUserById 方法

java复制代码/**
   * 根据用户ID查询用户信息
   *
   * @param userId 用户ID
   * @return 用户信息
   */
  @Cacheable(cacheNames = "userInfo",key = "#userId")
  public MingYueUser queryUserById(Long userId) {
    return sysUserMapper.queryUserById(userId);
  }

5. 开启 @EnableCaching 注解

java复制代码@SpringBootApplication
@EnableCaching
public class MingYueSpringbootMybatisRedisCacheApplication {
  public static void main(String[] args) {
    SpringApplication.run(MingYueSpringbootMybatisRedisCacheApplication.class, args);
  }
}

6. 启动项目,测试接口

1.调用接口 http://127.0.0.1:8080/user/1,可以看到控制台有如下打印:

bash复制代码JDBC Connection [HikariProxyConnection@1552674017 wrapping com.mysql.cj.jdbc.ConnectionImpl@315ae5d4] will not be managed by Spring
==>  Preparing: select * from sys_user where user_id = ?
==> Parameters: 1(Long)
<==    Columns: user_id, username
<==        Row: 1, mingyue
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@71d19455]

2.再次调用 http://127.0.0.1:8080/user/1,控制台无打印,查看 Redis 数据库

Redis Cache 增删改

1. 增加用户

编写添加用户方法

java复制代码@CachePut(cacheNames = "mybatis_cache_userInfo", key = "#user.userId")
public MingYueUser addUser(MingYueUser user) {
    boolean flag = sysUserMapper.addUser(user);

  	return flag ? user : null;
}

执行添加接口

bash复制代码curl --location --request POST 'http://127.0.0.1:8080/user' \
--header 'User-Agent: Apifox/1.0.0 (https://www.apifox.cn)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: 127.0.0.1:8080' \
--header 'Connection: keep-alive' \
--data-raw '{"userId":5,"username":"Strive5"}'

控制台打印如下:

bash复制代码Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@149aa2d7] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@509790631 wrapping com.mysql.cj.jdbc.ConnectionImpl@41af2f20] will not be managed by Spring
==>  Preparing: insert sys_user(user_id,username) values(?,?)
==> Parameters: 4(Long), Strive4(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@149aa2d7]

查看缓存

2. 修改用户

编写修改用户方法

java复制代码@CachePut(cacheNames = "mybatis_cache_userInfo", key = "#user.userId")
public MingYueUser updateUser(MingYueUser user) {
    boolean flag = sysUserMapper.updateUser(user);
    System.out.println(JSONUtil.toJsonStr(user));

    return flag ? user : null;
}

执行修改接口

bash复制代码curl --location --request PUT 'http://127.0.0.1:8080/user' \
--header 'User-Agent: Apifox/1.0.0 (https://www.apifox.cn)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: 127.0.0.1:8080' \
--header 'Connection: keep-alive' \
--data-raw '{"userId":2,"username":"Strive Update 2023 33"}'

控制台打印如下:

bash复制代码Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6751deb0] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1839838656 wrapping com.mysql.cj.jdbc.ConnectionImpl@c16c966] will not be managed by Spring
==>  Preparing: update sys_user set username = ? where user_id = ?
==> Parameters: Strive Update 2023 33(String), 2(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6751deb0]

查看缓存,多次请求并刷新缓存查看是否更新缓存数据

3. 删除用户

编写删除用户方法

java复制代码@CacheEvict(cacheNames = "mybatis_cache_userInfo", key = "#userId")
public boolean delUser(Long userId) {
    return sysUserMapper.delUser(userId);
}

执行用户接口

bash复制代码curl --location --request DELETE 'http://127.0.0.1:8080/user/2' \
--header 'User-Agent: Apifox/1.0.0 (https://www.apifox.cn)' \
--header 'Accept: */*' \
--header 'Host: 127.0.0.1:8080' \
--header 'Connection: keep-alive'

控制台打印如下:

bash复制代码Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@94198eb] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@62518171 wrapping com.mysql.cj.jdbc.ConnectionImpl@73034169] will not be managed by Spring
==>  Preparing: delete from sys_user where user_id = ?
==> Parameters: 2(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@94198eb]

查看缓存对应数据是否已经删除了

4. 删除所有用户

主要就是 @CacheEvict(allEntries = true) 注解

java复制代码@CacheEvict(cacheNames = "mybatis_cache_userInfo", allEntries = true)
public void delAllUser(Long userId) {
  // TODO sysUserMapper.delAllUser;
}

5. 最佳实践

上面已经介绍了缓存的增删改查如何实现,但这不是最佳实践!推荐使用下面的缓存用法!!!

  • 添加用户不需要放入缓存的,放入缓存的数据一般是查询能用到的,添加的用户可能也不会查询,直接放入缓存只会占用缓存空间;
  • 更新用户也是一样,更新的数据也有可能不会查询。推荐直接删除对应缓存,查询重新放入即可
java复制代码/**
 * 用户缓存增删改查 (推荐)
 * @author Strive 
 */
@Service
@RequiredArgsConstructor
public class MingYueUserNewService {

  private final SysUserMapper sysUserMapper;

  /**
   * 根据用户ID查询用户信息
   *
   * @param userId 用户ID
   * @return 用户信息
   */
  @Cacheable(cacheNames = "mybatis_cache_userInfo",key = "#userId")
  public MingYueUser queryUserById(Long userId) {
    return sysUserMapper.queryUserById(userId);
  }

  /**
   * 添加用户信息
   */
  public boolean addUser(MingYueUser user) {
    return sysUserMapper.addUser(user);
  }

  /**
   * 修改用户信息
   */
  @CacheEvict(cacheNames = "mybatis_cache_userInfo", key = "#userId")
  public boolean updateUser(MingYueUser user) {
    return sysUserMapper.updateUser(user);
  }

  /**
   * 删除用户信息
   * @param userId 用户ID
   */
  @CacheEvict(cacheNames = "mybatis_cache_userInfo", key = "#userId")
  public boolean delUser(Long userId) {
    return sysUserMapper.delUser(userId);
  }

}

总结

在无法保证数据不出现脏读的情况下,建议在业务层使用可控制的缓存代替二级缓存!


作者:Strive_MY
链接:https://juejin.cn/post/7237425805512835128

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: