写点什么

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

作者:Java你猿哥
  • 2023-05-27
    湖南
  • 本文字数:5576 字

    阅读完需:约 18 分钟

写在最前

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 依赖

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


2. 增加 Redis 修改配置

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


3. 添加 Redis 配置类

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 */@Configurationpublic 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 方法

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

5. 开启 @EnableCaching 注解

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

6. 启动项目,测试接口

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

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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@71d19455]
复制代码

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


Redis Cache 增删改

1. 增加用户

编写添加用户方法

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

执行添加接口

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"}'
复制代码

控制台打印如下:

Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@149aa2d7] was not registered for synchronization because synchronization is not activeJDBC 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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@149aa2d7]
复制代码

查看缓存


2. 修改用户

编写修改用户方法

@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;}
复制代码

执行修改接口

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"}'
复制代码

控制台打印如下:

Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6751deb0] was not registered for synchronization because synchronization is not activeJDBC 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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6751deb0]
复制代码

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


3. 删除用户

编写删除用户方法

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

执行用户接口

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'
复制代码

控制台打印如下:

Creating a new SqlSessionSqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@94198eb] was not registered for synchronization because synchronization is not activeJDBC 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: 1Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@94198eb]
复制代码

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

4. 删除所有用户

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

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

5. 最佳实践

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

  • 添加用户不需要放入缓存的,放入缓存的数据一般是查询能用到的,添加的用户可能也不会查询,直接放入缓存只会占用缓存空间;

  • 更新用户也是一样,更新的数据也有可能不会查询。推荐直接删除对应缓存,查询重新放入即可

/** * 用户缓存增删改查 (推荐) * @author Strive  */@Service@RequiredArgsConstructorpublic 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); }
}
复制代码

总结

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

用户头像

Java你猿哥

关注

一只在编程路上渐行渐远的程序猿 2023-03-09 加入

关注我,了解更多Java、架构、Spring等知识

评论

发布
暂无评论
SpringBoot 整合 MyBatis 组合 Redis 作为数据源缓存_Java_Java你猿哥_InfoQ写作社区