《Redis + mybatis-plus》mybatis-plus缓存机制

mybatis-plus二级缓存扩展

  • 理解
  • 核心要点1
  • 核心要点2 pom
  • mybatis-plus代码生成器
  • 一级缓存
    • 步骤如下:
  • 配置二级缓存(redis方式)
  • 主启动类:
  • redis序列化和CacheManager
    • service层使用
  • 参考

理解

  • 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。 一级缓存是默认开启的不用配置。

  • 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。二级缓存的开启(实体类必须序列化),然后在配置文件里面配置。

  • mybatis-plus是基于mybatis进行的增强,所以mybatis-plus缓存机制的了解,我们可以拿mybatis进行举例。

核心要点1

mybatis-plus 在springboot 中的核心配置如下

# mybatis配置
mybatis-plus:
  # 全局配置
  global-config:
    db-config:
      # 逻辑删除全局字段 (默认无 设置会自动扫描实体字段)
      logic-delete-field: delFlag
      # 逻辑删除全局值(默认 1、表示已删除)
      logic-delete-value: 1
      # 逻辑未删除全局值(默认 0、表示未删除)
      logic-not-delete-value: 0
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句
    #设置当查询结果值为null时,同样映射该查询字段给map。
    call-setters-on-nulls: true
    cache-enabled: true #开启二级缓存
  mapper-locations: classpath*:/mapper/*.xml
  type-aliases-package: com.isoftstone.manage.entity

核心要点2 pom

 
 <dependency>
     <groupId>org.springframework.bootgroupId>
     <artifactId>spring-boot-starter-data-redisartifactId>
 dependency>
 
 <dependency>
     <groupId>com.baomidougroupId>
     <artifactId>mybatis-plus-boot-starterartifactId>
     <optional>trueoptional>
 dependency>

 <dependency>
     <groupId>com.baomidougroupId>
     <artifactId>mybatis-plus-generatorartifactId>
     <optional>trueoptional>
 dependency>

 <dependency>
     <groupId>org.springframework.bootgroupId>
     <artifactId>spring-boot-starter-cacheartifactId>
     <version>2.2.2.RELEASEversion>
 dependency>
      

mybatis-plus代码生成器

package com.isoftstone.manage.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;


import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * @author chaoyangzhang
 * @version 1.0
 * @date 2021/6/7 18:36
 * mybatis-plus 代码生成器
 */
public class CodeGenerator {

    private static String projectPath = System.getProperty("user.dir");
    private static String DriverName ="com.mysql.cj.jdbc.Driver";
    private static String Url = "jdbc:mysql://127.0.0.1:3306/transcribe?useUnicode=true&useSSL=false&characterEncoding=utf8";
    private static String username = "root";
    private static String password = "root";
    private static String MICROSERVICE = "";

    /**
     * 

* 读取控制台内容 *

*/
public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } /** * 全局配置 * @return */ public static GlobalConfig getGlobalConfig(){ GlobalConfig gc = new GlobalConfig(); // MICROSERVICE = scanner("微服务名"); // gc.setOutputDir(projectPath + "/" + MICROSERVICE + "/src/main/java"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("Mr.Zhang"); gc.setOpen(false); gc.setFileOverride(false); //如果存在文件是否覆盖 gc.setServiceName("%sService"); //去掉service前面的I前缀 gc.setIdType(IdType.AUTO); //设置主键策略为自增 gc.setDateType(DateType.ONLY_DATE); //设置日期格式 //gc.setSwagger2(true); // 实体属性 Swagger2 注解 return gc; } /** * 数据源配置 * @return */ public static DataSourceConfig getDataSourceConfig(){ DataSourceConfig dsc = new DataSourceConfig(); dsc.setDriverName(DriverName); dsc.setUrl(Url); dsc.setUsername(username); dsc.setPassword(password); dsc.setDbType(DbType.MYSQL); return dsc; } /** * 包配置 * @return */ public static PackageConfig getPackageConfig(){ PackageConfig pc = new PackageConfig(); //将来代码会生成于com.isoftStone.manage.user的目录下 //自定义输入模块名称 // pc.setModuleName(scanner("模块名称")); pc.setParent("com.xxx.manage"); pc.setEntity("entity"); pc.setMapper("mapper"); pc.setController("controller"); pc.setService("service"); return pc; } /** * 策略配置 * @return */ public static StrategyConfig getStrategyConfig(){ StrategyConfig strategy = new StrategyConfig(); //下划线转驼峰命名 strategy.setNaming(NamingStrategy.underline_to_camel); //列名下划线转驼峰命名 strategy.setColumnNaming(NamingStrategy.underline_to_camel); // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!"); //自动支持Lombok注解 strategy.setEntityLombokModel(true); //设置实体类逻辑删除字段 strategy.setLogicDeleteFieldName("deleted"); //设置自动填充字段 TableFill createDate = new TableFill("oper_time", FieldFill.INSERT); // TableFill modifyDate = new TableFill("modify_date", FieldFill.INSERT_UPDATE); List<TableFill> list = new ArrayList<>(); list.add(createDate); // list.add(modifyDate); strategy.setTableFillList(list); //设置乐观锁 strategy.setVersionFieldName("version"); //设置下划线命名 localhost:8080/hello_id_2 strategy.setControllerMappingHyphenStyle(true); //设置restful的驼峰命名 strategy.setRestControllerStyle(true); strategy.setSuperEntityClass(BaseDO.class); // 公共父类 //strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!"); // 写于父类中的公共字段 //strategy.setSuperEntityColumns("id"); //strategy.setInclude("user","product"); 设置只映射生成两张表:user表和product表 //配置生成的表由控制台输入 strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); return strategy; } /*** * 入口 * @param args */ public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 1.全局配置 GlobalConfig gc = CodeGenerator.getGlobalConfig(); mpg.setGlobalConfig(gc); // 2.数据源配置 DataSourceConfig dsc = CodeGenerator.getDataSourceConfig(); mpg.setDataSource(dsc); // 3.包配置 PackageConfig pc = CodeGenerator.getPackageConfig(); mpg.setPackageInfo(pc); // 4.策略配置 StrategyConfig strategy = CodeGenerator.getStrategyConfig(); mpg.setStrategy(strategy); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity // String templatePath = "/templates/mapper.xml.vm"; // 自定义输出配置,自定义配置会被优先输出 List<FileOutConfig> focList = new ArrayList<>(); focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath +"/"+ MICROSERVICE + "/src/main/resources/mapper/" + pc.getModuleName() + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); /* cfg.setFileCreate(new IFileCreate() { @Override public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) { // 判断自定义文件夹是否需要创建 checkDir("调用默认方法创建的目录,自定义目录用"); if (fileType == FileType.MAPPER) { // 已经生成 mapper 文件判断存在,不想重新生成返回 false return !new File(filePath).exists(); } // 允许生成模板文件 return true; } }); */ cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板,不在mapper中新建xml目录并生成xml文件 TemplateConfig templateConfig = new TemplateConfig(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }

一级缓存

一级缓存是默认打开的,但是在测试过程中,发现mybatis的一级缓存没有起作用,失效了。经过调研,发现是由于以下原因引起的:

  • 1.mybatis的一级缓存生效的范围是sqlsession,是为了在sqlsession没有关闭时,业务需要重复查询相同数据使用的。一旦sqlsession关闭,则由这个sqlsession缓存的数据将会被清空。
  • 2.spring对mybatis的sqlsession的使用是由template控制的,sqlSessionTemplate又被spring当作resource放在当前线程的上下文里(threadlocal),spring通过mybatis调用数据库的过程

步骤如下:

  • 我们需要访问数据
  • spring检查到了这种需求,于是去申请一个mybatis的sqlsession(资源池),并将申请到的sqlsession与当前线程绑定,放入threadlocal里面
  • qlSessionTemplate从threadlocal获取到sqlsession,去执行查询
  • 查询结束,清空threadlocal中与当前线程绑定的sqlsession,释放资源
  • 我们又需要访问数据
  • 返回到步骤b

通过以上步骤后发现,同一线程里面两次查询同一数据所使用的sqlsession是不相同的,所以,给人的印象就是结合spring后,mybatis的一级缓存失效了。

在SqlSessionTemplate中执行SQL的session都是通过sqlSessionProxy来,sqlSessionProxy的生成在构造函数中赋值,如下:

this.sqlSessionProxy = (SqlSession) newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class },
    new SqlSessionInterceptor());

sqlSessionProxy通过JDK的动态代理方法生成的一个代理类,主要逻辑在InvocationHandler对执行的方法进行了前后拦截,主要逻辑在invoke中,包好了每次执行对sqlsesstion的创建,commit,关闭

代码如下:

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   // 每次执行前都创建一个新的sqlSession
   SqlSession sqlSession = getSqlSession(
     SqlSessionTemplate.this.sqlSessionFactory,
     SqlSessionTemplate.this.executorType,
     SqlSessionTemplate.this.exceptionTranslator);
   try {
   // 执行方法
    Object result = method.invoke(sqlSession, args);
    if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
     // force commit even on non-dirty sessions because some databases require
     // a commit/rollback before calling close()
     sqlSession.commit(true);
    }
    return result;
   } catch (Throwable t) {
    Throwable unwrapped = unwrapThrowable(t);
    if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
     // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
     closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
     sqlSession = null;
     Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
     if (translated != null) {
      unwrapped = translated;
     }
    }
    throw unwrapped;
   } finally {
    if (sqlSession != null) {
     closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
    }
   }
  }
 }

因为每次都进行创建,所以就用不上sqlSession的缓存了.

对于开启了事务为什么可以用上呢, 跟入getSqlSession方法

如下:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
  // 首先从SqlSessionHolder里取出session
  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
   return session;
  }
  if (LOGGER.isDebugEnabled()) {
   LOGGER.debug("Creating a new SqlSession");
  }
  session = sessionFactory.openSession(executorType);
  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
  return session;
 }

在里面维护了个SqlSessionHolder,关联了事务与session,如果存在则直接取出,否则则新建个session,所以在有事务的里,每个session都是同一个,故能用上缓存了。

配置二级缓存(redis方式)

yml配置可以参考上面。

主启动类:

@EnableCaching 注解

@EnableTransactionManagement
@EnableCaching
@MapperScan("com.isoftstone.manage.mapper")
@SpringBootApplication
public class TranscribeApplication {
    public static void main(String[] args) {
        SpringApplication.run(TranscribeApplication.class, args);
    }
}

redis序列化和CacheManager

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
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.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @Author: zhangchaoyang
 * @Date: 2021/3/23 18:01
 * @Version 1.0
 * @Description   redis配置文件,重新编排序列化和兼容mybatis缓存
 */
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * 重写Redis序列化方式,使用Json方式:
     * 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是StringRedisSerializer。
     * Spring Data JPA为我们提供了下面的Serializer:
     * GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
     * 在此我们将自己配置RedisTemplate并定义Serializer。
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

//        JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

        // 设置值(value)的序列化采用FastJsonRedisSerializer。
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//        redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
        // 设置键(key)的序列化采用StringRedisSerializer。
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    //缓存管理器
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory lettuceConnectionFactory) {
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
        // 设置缓存管理器管理的缓存的默认过期时间
        defaultCacheConfig = defaultCacheConfig.entryTtl(Duration.ofMinutes(60))
                // 不缓存空值
                .disableCachingNullValues();

        Set<String> cacheNames = new HashSet<>();
        cacheNames.add("my-redis-cache1");

        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("my-redis-cache1", defaultCacheConfig.entryTtl(Duration.ofMinutes(50)));

        RedisCacheManager cacheManager = RedisCacheManager.builder(lettuceConnectionFactory)
                .cacheDefaults(defaultCacheConfig)
                .initialCacheNames(cacheNames)
                .withInitialCacheConfigurations(configMap)
                .build();
        return cacheManager;
    }
}

关于redis序列化的说明,可以参考https://blog.csdn.net/CSDN_zcy_my/article/details/121820969

service层使用

/**
 * 

* 服务类 *

* * @author chaoyang * @since 2021-10-27 */
@CacheConfig(cacheNames = "user") public interface TblUserService extends IService<TblUser> { @CachePut(key = "#tblUser.id") ActionResult<TblUser> register(TblUser tblUser); @Cacheable(key = "#id") ActionResult<TblUser> info(Long id); TblUser userInfo(Long id); @CacheEvict(key = "#userPasswordVO.id") ActionResult<?> changePassword(UserPasswordVO userPasswordVO); @CacheEvict(key = "#id") ActionResult<?> resetPassword(Long id); @CacheEvict(key = "#tblUser.id") ActionResult<?> updateUser(TblUser tblUser); @CacheEvict(key = "#id") ActionResult<?> updateFrozen(Long id); TblUser getUserInfo(HttpServletRequest request); boolean updateUserCache(TblUser tblUser, String... type); boolean updateUserListCache(TblUser tblUser); ActionResult<?> relationPayonner(String type, String id, HttpServletRequest request); }

二级缓存使用之后,初次会执行数据库,并缓存进redis,再次访问将不再执行sql,直接从redis缓存中读取。

参考

https://www.cnblogs.com/gmhappy/p/11864104.html

你可能感兴趣的:(个人笔记,缓存,redis,java)