likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 14

写在最前

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

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

本文不再重复介绍 SpringBoot 如何整合 MyBatis。想学习 SpringBoot 整合 MyBatis 的同学可以参考下面文章,希望对您有所帮助:

【数据篇】SpringBoot 整合 MyBatis 操作 MySql

MyBatis 一级缓存与二级缓存

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

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

一级缓存

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

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

一级缓存小结:

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

二级缓存

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

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

二级缓存开启后,同一个 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
 */
@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 方法

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

5. 开启 @EnableCaching 注解

@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,可以看到控制台有如下打印:

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 数据库

【数据篇】SpringBoot 整合 MyBatis 组合 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 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]

查看缓存

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

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 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]

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

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

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 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) 注解

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

5. 最佳实践

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

  • 添加用户不需要放入缓存的,放入缓存的数据一般是查询能用到的,添加的用户可能也不会查询,直接放入缓存只会占用缓存空间;
  • 更新用户也是一样,更新的数据也有可能不会查询。推荐直接删除对应缓存,查询重新放入即可
/**
 * 用户缓存增删改查 (推荐)
 * @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);
  }

}

总结

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

转载自:https://juejin.cn/post/7237425805512835128
评论
请登录