• Spring Boot 集成 Redis 配置 MyBatis 二级缓存


    写在前面

    文中项目基于从0到1项目搭建-框架搭建,如果你是新手,可以跟着上期内容先动手把项目框架搭建起来,然后在结合本期内容继续深入学习,这样会有更好的效果。

    接下来正式介绍本文,本文讲的是在 Spring Boot 项目中集成使用 Redis,并使用 Redis 实现 MyBatis 的二级缓存。使用场景就是在高并发的环境下,大量的查询直接落入DB,会导致数据库宕机,从而导致服务雪崩的情况。我们使用Redis作为MyBatis二级缓存,可以充分的缓解数据库的压力,从而达到服务的高可用。

    源码获取

    源码在 GitCodeGitHub 以及 码云,持续更新中,别忘了 star 喔~

    GitCode

    https://gitcode.net/qq_41779565/my-project.git
    
    • 1

    GitHub

    https://github.com/micromaples/my-project
    
    • 1

    码云Gitee

    https://gitee.com/micromaple/my-project
    
    • 1

    如果不会使用 Git 的小伙伴,我已经上传到了CSDN,资源下载传送门,有会员的小伙伴直接下载即可,没有会员的小伙伴私聊我Mybatis二级缓存可直接获取

    一、MyBatis缓存机制

    Mybatis 提供了查询缓存来缓存数据,以提高查询效率。缓存级别分为一级缓存二级缓存

    1.1、一级缓存

    一级缓存为 SqlSession 级别的缓存,也就是会话级缓存,是基于HashMap的本地缓存,当同一个SqlSession执行两次相同的SQL语句时,第一次执行完后会将数据库中查询到的结果写到缓存,第二次查询时直接从缓存中读取,不经过数据库了。一级缓存默认是开启的。

    1.2、二级缓存

    二级缓存为mapper级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 去操作数据库得到数据会存在二级缓存区域,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。其作用域是 mapper 的同一个 namespace,不同的 sqlSession 两次执行相同 namespace下的 sql 语句且向 sql 中传递参数也相同即最终执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。Mybatis 默认没有开启二级缓存需要在 setting 全局参数中配置开启二级缓存。

    二、集成Redis

    2.1、安装Redis

    使用Docker Compose 安装Redis。docker-compose.yml内容如下:

    version: '3.1'
    services:
      redis:
        image: redis:6.2.4
        container_name: redis
        restart: always
        command: redis-server --requirepass 123456
        ports:
          - '6379:6379'
        volumes:
          - ./data:/data
        environment:
          TZ: Asia/Shanghai
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    安装启动完成后,可使用Redis连接工具测试

    在这里插入图片描述

    2.2、项目引入Redis

    2.2.1、Maven依赖
    <dependency>
    	<groupId>org.springframework.bootgroupId>
    	<artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    <dependency>
    	<groupId>org.apache.commonsgroupId>
    	<artifactId>commons-pool2artifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    额外引入commons-pool2是因为data-redis底层Redis连接池基于apache commons-pool2开 发,不加入依赖会报ClassNotFoundException

    2.2.2、配置application.yml
    spring:
      redis:
        host: 192.168.110.158
        port: 6379
        password: 123456
        lettuce:
          pool:
            #最大允许连接数
            max-active: 100
            #最小空闲连接数,最少准备5个可用连接在连接池候着
            min-idle: 5
            #最大空闲连接数,空闲连接超过10个后自动释放
            max-idle: 10
            #当连接池到达上限后,最多等待30秒尝试获取连接,超时报错
            max-wait: 30000
        timeout: 2000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    2.2.3、配置序列化规则

    RedisTemplateConfiguration配置类如下:

    package com.micromaple.my.project.server.config;
    
    import com.fasterxml.jackson.annotation.JsonInclude;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * RedisTemplate配置
     * Title: RedisTemplateConfiguration
     * Description:
     *
     * @author Micromaple
     */
    @Configuration
    public class RedisTemplateConfiguration {
        
        /**
         * redisTemplate
         *
         * @param redisConnectionFactory
         * @return
         */
        @Bean
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory
                                                                   redisConnectionFactory) {
            RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            // 使用Jackson2JsonRedisSerialize 替换默认序列化
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper objectMapper = new ObjectMapper();
            //对于Null值不输出
            objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
            // 设置key和value的序列化规则
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
            // 设置hashKey和hashValue的序列化规则
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
            //afterPropertiesSet和init-method之间的执行顺序是afterPropertiesSet 先执行,init - method 后执行。
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    三、配置二级缓存

    配置实现MyBatis二级缓存的方式有多种,比如:EhCacheJBossCacheRedis,其核心原理就是客户端实现 MyBatis 提供的Cache 接口,并重写其中的方法,达到二级缓存的效果。

    本文以 Redis 为例。

    2.1、开启二级缓存

    application.yml 中增加如下配置:

    # 开启MyBatis二级缓存
    mybatis:
      configuration:
        cache-enabled: true
    
    • 1
    • 2
    • 3
    • 4

    如果使用的是 MyBatis-Plus ,则使用如下配置:

    # MyBatis-Plus开启二级缓存
    mybatis-plus:
      configuration:
        cache-enabled: true
    
    • 1
    • 2
    • 3
    • 4

    2.2、自定义缓存类

    MybatisRedisCache 缓存工具类如下:

    package com.micromaple.my.project.server.utils;
    
    import com.micromaple.my.project.server.config.ApplicationContextHolder;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.ibatis.cache.Cache;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * MybatisRedisCache 缓存工具类
     * Title: MybatisRedisCache
     * Description:
     *
     * @author Micromaple
     */
    public class MybatisRedisCache implements Cache {
        private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
    
        private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        private final String id; // cache instance id
        private RedisTemplate redisTemplate;
    
        private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间
    
        public MybatisRedisCache(String id) {
            if (id == null) {
                throw new IllegalArgumentException("Cache instances require an ID");
            }
            this.id = id;
        }
    
        @Override
        public String getId() {
            return id;
        }
    
        /**
         * Put query result to redis
         *
         * @param key
         * @param value
         */
        @Override
        public void putObject(Object key, Object value) {
            try {
                redisTemplate = getRedisTemplate();
                if (value != null) {
                    redisTemplate.opsForValue().set(key.toString(), value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
                }
                logger.debug("Put query result to redis");
            } catch (Throwable t) {
                logger.error("Redis put failed", t);
            }
    
    
        }
    
        /**
         * Get cached query result from redis
         *
         * @param key
         * @return
         */
        @Override
        public Object getObject(Object key) {
            try {
                redisTemplate = getRedisTemplate();
                logger.debug("Get cached query result from redis");
                return redisTemplate.opsForValue().get(key.toString());
            } catch (Throwable t) {
                logger.error("Redis get failed, fail over to db", t);
                return null;
            }
        }
    
        /**
         * Remove cached query result from redis
         *
         * @param key
         * @return
         */
        @Override
        @SuppressWarnings("unchecked")
        public Object removeObject(Object key) {
            try {
                redisTemplate = getRedisTemplate();
                redisTemplate.delete(key.toString());
                logger.debug("Remove cached query result from redis");
            } catch (Throwable t) {
                logger.error("Redis remove failed", t);
            }
            return null;
        }
    
        /**
         * Clears this cache instance
         */
        @Override
        public void clear() {
            redisTemplate = getRedisTemplate();
            Set<String> keys = redisTemplate.keys("*:" + this.id + "*");
            if (!CollectionUtils.isEmpty(keys)) {
                redisTemplate.delete(keys);
            }
            logger.debug("Clear all the cached query result from redis");
        }
    
        /**
         * This method is not used
         *
         * @return
         */
        @Override
        public int getSize() {
            return 0;
        }
    
        @Override
        public ReadWriteLock getReadWriteLock() {
            return readWriteLock;
        }
    
        private RedisTemplate getRedisTemplate() {
            if (redisTemplate == null) {
                redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
            }
            return redisTemplate;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135

    ApplicationContextHolder如下:

    package com.micromaple.my.project.server.config;
    
    import org.apache.commons.lang3.Validate;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.DisposableBean;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.stereotype.Component;
    
    /**
     * Spring bean的工具类
     * Title: ApplicationContextHolder
     * Description:
     *
     * @author Micromaple
     */
    @Component
    public class ApplicationContextHolder implements ApplicationContextAware, DisposableBean {
    
        private static final Logger logger = LoggerFactory.getLogger(ApplicationContextHolder.class);
    
        private static ApplicationContext applicationContext;
    
        /**
         * 获取存储在静态变量中的 ApplicationContext
         *
         * @return
         */
        public static ApplicationContext getApplicationContext() {
            assertContextInjected();
            return applicationContext;
        }
    
        /**
         * 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
         *
         * @param name
         * @param 
         * @return
         */
        public static <T> T getBean(String name) {
            assertContextInjected();
            return (T) applicationContext.getBean(name);
        }
    
        /**
         * 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
         *
         * @param clazz
         * @param 
         * @return
         */
        public static <T> T getBean(Class<T> clazz) {
            assertContextInjected();
            return applicationContext.getBean(clazz);
        }
    
        /**
         * 实现 DisposableBean 接口,在 Context 关闭时清理静态变量
         *
         * @throws Exception
         */
        public void destroy() throws Exception {
            logger.debug("清除 SpringContext 中的 ApplicationContext: {}", applicationContext);
            applicationContext = null;
        }
    
        /**
         * 实现 ApplicationContextAware 接口,注入 Context 到静态变量中
         *
         * @param applicationContext
         * @throws BeansException
         */
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            ApplicationContextHolder.applicationContext = applicationContext;
        }
    
        /**
         * 断言 Context 已经注入
         */
        private static void assertContextInjected() {
            Validate.validState(applicationContext != null, "applicationContext 属性未注入,请在 spring-context.xml 配置中定义 SpringContext");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    2.3、增加注解

    在 Mapper 接口中增加 @CacheNamespace(implementation = MybatisRedisCache.class) 注解,声明需要使用二级缓存。

    package com.micromaple.my.project.server.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.micromaple.my.project.server.domain.SysUser;
    import com.micromaple.my.project.server.utils.MybatisRedisCache;
    import org.apache.ibatis.annotations.CacheNamespace;
    
    /**
     * 

    * 用户表 Mapper 接口 *

    * * @author Micromaple * @since 2022-09-21 21:51:15 */
    @CacheNamespace(implementation = MybatisRedisCache.class) public interface SysUserMapper extends BaseMapper<SysUser> { }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.4、测试验证

    访问查询所有用户接口http://localhost:8899/sys-user/get/all

    访问完成后,我们打开Redis查询工具,可以看到已经将我们查询出来的数据缓存起来了。效果图如下:
    在这里插入图片描述

    接着,我们再次访问查询所有用户接口,我们可以在控制台日志中看到,第二次查询并没有走数据库,而是直接在Redis中取出来了

    在这里插入图片描述

  • 相关阅读:
    全球化智能组网以中国联通云联网为核心
    动态列排序
    【MySQL】函数
    学完了Hadoop,我总结了这些重点
    typeinfo类型支持库学习
    Nginx 安装配置
    数字孪生可视化在石油生产管理中的具体应用
    Mybatis关联(嵌套)查询与延迟加载
    Codeforces Global Round 23 E CF1746E Joking (Hard Version)
    Navigation 组件使用入门
  • 原文地址:https://blog.csdn.net/qq_41779565/article/details/127133675