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

理解

  • 一级缓存是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

 <!--springboot-配置redis-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>
 <!-- 起步依赖 -->
 <dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-boot-starter</artifactId>
     <optional>true</optional>
 </dependency>

 <dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-generator</artifactId>
     <optional>true</optional>
 </dependency>

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-cache</artifactId>
     <version>2.2.2.RELEASE</version>
 </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 = "";

    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    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层使用

/**
 * <p>
 *  服务类
 * </p>
 *
 * @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

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值