IT七剑客 IT七剑客
首页
wresource
郭霖
孤寒者
IT邦德
沉默王二
老麦
stackoverflow
GitHub (opens new window)
首页
wresource
郭霖
孤寒者
IT邦德
沉默王二
老麦
stackoverflow
GitHub (opens new window)
  • Java基础语法

  • 程序人生

  • 实用工具

  • Java重要知识点

  • Java工具

    • currentTimeMillis 统计耗时了,太 Low,试试 Spring Boot 源码在用的 StopWatch吧,够优雅!
    • WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了!
    • 在IDEA里下个五子棋不过分吧?
    • 好用到爆!GitHub 星标 32.5k+的命令行软件管理神器,功能真心强大!
    • 我们公司使用了6年的Spring Boot项目部署方案,打包 + 一键部署,稳的一批
    • 再见 Spring Task,这款老而弥坚的开源任务调度框架,用起来够优雅!
    • 解放双手!推荐一款 GitHub 星标 8.2k+的命令行软件管理器,非常酷炫!
    • 再见收费的TeamViewer,推荐一款不限速的国产远程控制软件
    • 几行代码,网站图片访问速度 100ms 飙升到 20ms!
    • 某意大利小哥,竟靠一个缓存中间件直接封神?
      • WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了!
      • 厉害!我带的实习生仅用四步就整合好SpringSecurity+JWT实现登录认证!
      • 保姆级SpringBoot+Vue图片上传到阿里云OSS教程
      • 两天两夜,1M图片优化到100kb!
      • 干掉Session?这个跨域认证解决方案真的优雅!
      • 前后端分离项目,如何解决跨域问题?
      • Spring Boot AOP 扫盲,实现接口访问的统一日志记录
      • 再见收费的Navicat!操作所有数据库就靠它了!
      • 取代 Mybatis Generator,这款代码生成神器配置更简单,开发效率更高!
      • 再见丑陋的 SwaggerUI,这款开源的API文档生成神器界面更炫酷,逼格更高!
    • 数组与字符串

    • 沉默王二 Java
    • Java工具
    沉默王二
    2022-09-01
    目录

    某意大利小哥,竟靠一个缓存中间件直接封神?

    大家好,我是二哥呀!关注我有一段时间的小伙伴都知道了,我最近的业余时间都花在了编程喵🐱这个实战项目上,其中要用到 Redis,于是我就想,索性出一期 Redis 的入门教程吧——主要是整合 Redis 来实现缓存功能,希望能帮助到大家。

    作为开发者,相信大家都知道 Redis 的重要性。Redis 是使用 C 语言开发的一个高性能键值对数据库,是互联网技术领域使用最为广泛的存储中间件,它是「Remote Dictionary Service」的首字母缩写,也就是「远程字典服务」。

    Redis 以超高的性能、完美的文档、简洁的源码著称,国内外很多大型互联网公司都在用,比如说阿里、腾讯、GitHub、Stack Overflow 等等。当然了,中小型公司也都在用。

    img

    Redis 的作者是一名意大利人,原名 Salvatore Sanfilippo,网名 Antirez。不过,很遗憾的是,网上竟然没有他的维基百科,甚至他自己的博客网站,都在跪的边缘(没有 HTTPS,一些 js 也加载失败了)。

    img

    不过,如果是鄙人造出 Redis 这么酷炫的产品,早就功成身退了。

    # 一、安装 Redis

    Redis 的官网提供了各种平台的安装包,Linux、macOS、Windows 的都有。

    img

    官方地址:redis.io/docs/gettin… (opens new window)

    我目前用的是 macOS,直接执行 brew install redis 就可以完成安装了。

    img

    完成安装后执行 redis-server 就可以启动 Redis 服务了。

    img

    不过,实际的开发当中,我们通常会选择 Linux 服务器来作为生产环境。我的服务器上安装了宝塔面板,可以直接在软件商店里搜「Redis」关键字,然后直接安装(我的已经安装过了)。

    img

    # 二、整合 Redis

    编程喵是一个 Spring Boot + Vue 的前后端分离项目,要整合 Redis 的话,最好的方式是使用 Spring Cache,仅仅通过 @Cacheable、@CachePut、@CacheEvict、@EnableCaching 等注解就可以轻松使用 Redis 做缓存了。

    img

    1)@EnableCaching,开启缓存功能。

    2)@Cacheable,调用方法前,去缓存中找,找到就返回,找不到就执行方法,并将返回值放到缓存中。

    3)@CachePut,方法调用前不会去缓存中找,无论如何都会执行方法,执行完将返回值放到缓存中。

    4)@CacheEvict,清理缓存中的一个或多个记录。

    Spring Cache 是 Spring 提供的一套完整的缓存解决方案,虽然它本身没有提供缓存的实现,但它提供的一整套接口、规范、配置、注解等,可以让我们无缝衔接 Redis、Ehcache 等缓存实现。

    Spring Cache 的注解(前面提到的四个)会在调用方法之后,去缓存方法返回的最终结果;或者在方法调用之前拿缓存中的结果,当然还有删除缓存中的结果。

    这些读写操作不用我们手动再去写代码实现了,直接交给 Spring Cache 来打理就 OK 了,是不是非常贴心?

    img

    第一步,在 pom.xml 文件中追加 Redis 的 starter。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    复制代码
    
    1
    2
    3
    4
    5

    第二步,在 application.yml 文件中添加 Redis 链接配置。

    spring:
        redis:
            host: 118.xx.xx.xxx # Redis服务器地址
            database: 0 # Redis数据库索引(默认为0)
            port: 6379 # Redis服务器连接端口
            password: xx # Redis服务器连接密码(默认为空)
            timeout: 1000ms # 连接超时时间(毫秒)
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8

    第三步,新增 RedisConfig.java 类,通过 RedisTemplate 设置 JSON 格式的序列化器,这样的话存储到 Redis 里的数据将是有类型的 JSON 数据,例如:

    @EnableCaching
    @Configuration
    public class RedisConfig extends CachingConfigurerSupport {
    
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
    
            // 通过 Jackson 组件进行序列化
            RedisSerializer<Object> serializer = redisSerializer();
    
            // key 和 value
            // 一般来说, redis-key采用字符串序列化;
            // redis-value采用json序列化, json的体积小,可读性高,不需要实现serializer接口。
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(serializer);
    
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashValueSerializer(serializer);
    
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    
        @Bean
        public RedisSerializer<Object> redisSerializer() {
            //创建JSON序列化器
            Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            // https://www.cnblogs.com/shanheyongmu/p/15157378.html
            // objectMapper.enableDefaultTyping()被弃用
            objectMapper.activateDefaultTyping(
                    LaissezFaireSubTypeValidator.instance,
                    ObjectMapper.DefaultTyping.NON_FINAL,
                    JsonTypeInfo.As.WRAPPER_ARRAY);
            serializer.setObjectMapper(objectMapper);
            return serializer;
        }
    
    }
    复制代码
    
    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

    通过 RedisCacheConfiguration 设置超时时间,来避免产生很多不必要的缓存数据。

    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        //设置Redis缓存有效期为1天
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1));
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    第四步,在标签更新接口中添加 @CachePut 注解,也就是说方法执行前不会去缓存中找,但方法执行完会将返回值放入缓存中。

    @Controller
    @Api(tags = "标签")
    @RequestMapping("/postTag")
    public class PostTagController {
    
        @Autowired
        private IPostTagService postTagService;
        @Autowired
        private IPostTagRelationService postTagRelationService;
    
        @RequestMapping(value = "/update", method = RequestMethod.POST)
        @ResponseBody
        @ApiOperation("修改标签")
        @CachePut(value = "codingmore", key = "'codingmore:postags:'+#postAddTagParam.postTagId")
        public ResultObject<String> update(@Valid PostTagParam postAddTagParam) {
            if (postAddTagParam.getPostTagId() == null) {
                return ResultObject.failed("标签id不能为空");
            }
            PostTag postTag = postTagService.getById(postAddTagParam.getPostTagId());
            if (postTag == null) {
                return ResultObject.failed("标签不存在");
            }
            QueryWrapper<PostTag> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("description", postAddTagParam.getDescription());
            int count = postTagService.count(queryWrapper);
            if (count > 0) {
                return ResultObject.failed("标签名称已存在");
            }
            BeanUtils.copyProperties(postAddTagParam, postTag);
            return ResultObject.success(postTagService.updateById(postTag) ? "修改成功" : "修改失败");
        }
    }
    复制代码
    
    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

    注意看 @CachePut 注解这行代码:

    @CachePut(value = "codingmore", key = "'codingmore:postags:'+#postAddTagParam.postTagId")
    复制代码
    
    1
    2
    • value:缓存名称,也就是缓存的命名空间,value 这里应该换成 namespace 更好一点;
    • key:用于在命名空间中缓存的 key 值,可以使用 SpEL 表达式,比如说 'codingmore:postags:'+#postAddTagParam.postTagId;
    • 还有两个属性 unless 和 condition 暂时没用到,分别表示条件符合则不缓存,条件符合则缓存。

    第五步,启动服务器端,启动客户端,修改标签进行测试。

    img

    通过 Red 客户端(一款 macOS 版的 Redis 桌面工具),可以看到刚刚更新的返回值已经添加到 Redis 中了。

    img

    # 三、使用 Redis 连接池

    Redis 是基于内存的数据库,本来是为了提高程序性能的,但如果不使用 Redis 连接池的话,建立连接、断开连接就需要消耗大量的时间。

    用了连接池,就可以实现在客户端建立多个连接,需要的时候从连接池拿,用完了再放回去,这样就节省了连接建立、断开的时间。

    要使用连接池,我们得先了解 Redis 的客户端,常用的有两种:Jedis 和 Lettuce。

    • Jedis:Spring Boot 1.5.x 版本时默认的 Redis 客户端,实现上是直接连接 Redis Server,如果在多线程环境下是非线程安全的,这时候要使用连接池为每个 jedis 实例增加物理连接;
    • Lettuce:Spring Boot 2.x 版本后默认的 Redis 客户端,基于 Netty 实现,连接实例可以在多个线程间并发访问,一个连接实例不够的情况下也可以按需要增加连接实例。

    它俩在 GitHub 上都挺受欢迎的,大家可以按需选用。

    img

    我这里把两种客户端的情况都演示一下,方便小伙伴们参考。

    1)Lettuce

    第一步,修改 application-dev.yml,添加 Lettuce 连接池配置(pool 节点)。

    spring:
        redis:
            lettuce:
              pool:
                max-active: 8 # 连接池最大连接数
                max-idle: 8 # 连接池最大空闲连接数
                min-idle: 0 # 连接池最小空闲连接数
                max-wait: -1ms # 连接池最大阻塞等待时间,负值表示没有限制
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    第二步,在 pom.xml 文件中添加 commons-pool2 依赖,否则会在启动的时候报 ClassNotFoundException 的错。这是因为 Spring Boot 2.x 里默认没启用连接池。

    Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig
    	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    	... 153 common frames omitted
    复制代码
    
    1
    2
    3
    4
    5
    6
    7

    添加 commons-pool2 依赖:

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.6.2</version>
        <type>jar</type>
        <scope>compile</scope>
    </dependency>
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8

    重新启动服务,在 RedisConfig 类的 redisTemplate 方法里对 redisTemplate 打上断点,debug 模式下可以看到连接池的配置信息(redisConnectionFactory→clientConfiguration→poolConfig)。如下图所示。

    img

    如果在 application-dev.yml 文件中没有添加 Lettuce 连接池配置的话,是不会看到

    img

    2)Jedis

    第一步,在 pom.xml 文件中添加 Jedis 依赖,去除 Lettuce 默认依赖。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    第二步,修改 application-dev.yml,添加 Jedis 连接池配置。

    spring:
        redis:
            jedis:
              pool:
                max-active: 8 # 连接池最大连接数
                max-idle: 8 # 连接池最大空闲连接数
                min-idle: 0 # 连接池最小空闲连接数
                max-wait: -1ms # 连接池最大阻塞等待时间,负值表示没有限制
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    启动服务后,观察 redisTemplate 的 clientConfiguration 节点,可以看到它的值已经变成 DefaultJedisClientConfiguration 对象了。

    img

    当然了,也可以不配置 Jedis 客户端的连接池,走默认的连接池配置。因为 Jedis 客户端默认增加了连接池的依赖包,在 pom.xml 文件中点开 Jedis 客户端依赖可以查看到。

    img

    # 四、自由操作 Redis

    Spring Cache 虽然提供了操作 Redis 的便捷方法,比如我们前面演示的 @CachePut 注解,但注解提供的操作非常有限,比如说它只能保存返回值到缓存中,而返回值并不一定是我们想要保存的结果。

    img

    与其保存这个返回给客户端的 JSON 信息,我们更想保存的是更新后的标签。那该怎么自由地操作 Redis 呢?

    img

    第一步,增加 RedisService 接口:

    public interface RedisService {
    
        /**
         * 保存属性
         */
        void set(String key, Object value);
    
        /**
         * 获取属性
         */
        Object get(String key);
    
        /**
         * 删除属性
         */
        Boolean del(String key);
    
        ...
    
        // 更多方法见:https://github.com/itwanger/coding-more/blob/main/codingmore-mbg/src/main/java/com/codingmore/service/RedisService.java
    
    }
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    第二步,增加 RedisServiceImpl 实现类:

    @Service
    public class RedisServiceImpl implements RedisService {
        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
    
        @Override
        public void set(String key, Object value) {
            redisTemplate.opsForValue().set(key, value);
        }
    
        @Override
        public Object get(String key) {
            return redisTemplate.opsForValue().get(key);
        }
    
        @Override
        public Boolean del(String key) {
            return redisTemplate.delete(key);
        }
    
        // 更多代码参考:https://github.com/itwanger/coding-more/blob/main/codingmore-mbg/src/main/java/com/codingmore/service/impl/RedisServiceImpl.java
    }
    复制代码
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    第三步,在标签 PostTagController 中增加 Redis 测试用接口 simpleTest :

    @Controller
    @Api(tags = "标签")
    @RequestMapping("/postTag")
    public class PostTagController {
        @Autowired
        private IPostTagService postTagService;
        @Autowired
        private IPostTagRelationService postTagRelationService;
    
        @Autowired
        private RedisService redisService;
    
        @RequestMapping(value = "/simpleTest", method = RequestMethod.POST)
        @ResponseBody
        @ApiOperation("修改标签/Redis 测试用")
        public ResultObject<PostTag> simpleTest(@Valid PostTagParam postAddTagParam) {
            if (postAddTagParam.getPostTagId() == null) {
                return ResultObject.failed("标签id不能为空");
            }
            PostTag postTag = postTagService.getById(postAddTagParam.getPostTagId());
            if (postTag == null) {
                return ResultObject.failed("标签不存在");
            }
            QueryWrapper<PostTag> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("description", postAddTagParam.getDescription());
            int count = postTagService.count(queryWrapper);
            if (count > 0) {
                return ResultObject.failed("标签名称已存在");
            }
            BeanUtils.copyProperties(postAddTagParam, postTag);
    
            boolean successFlag = postTagService.updateById(postTag);
    
            String key = "redis:simple:" + postTag.getPostTagId();
            redisService.set(key, postTag);
    
            PostTag cachePostTag = (PostTag) redisService.get(key);
            return ResultObject.success(cachePostTag);
        }
    
    }
    复制代码
    
    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

    第四步,重启服务,使用 Knife4j 测试该接口 :

    img

    然后通过 Red 查看该缓存,OK,确认我们的代码是可以完美执行的。

    img

    # 五、小结

    赞美 Redis 的彩虹屁我就不再吹了,总之,如果我是 Redis 的作者 Antirez,我就自封为神!

    img

    编程喵实战项目的源码地址我贴下面了,大家可以下载下来搞一波了:

    github.com/itwanger/co… (opens new window)

    我们下期见~


    本文已收录到 GitHub 上星标 2k+ star 的开源专栏《Java 程序员进阶之路》,据说每一个优秀的 Java 程序员都喜欢她,风趣幽默、通俗易懂。内容包括 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。学 Java,就认准 Java 程序员进阶之路😄。

    github.com/itwanger/to… (opens new window)

    star 了这个仓库就等于你拥有了成为了一名优秀 Java 工程师的潜力。也可以戳下面的链接跳转到《Java 程序员进阶之路》的官网网址,开始愉快的学习之旅吧。

    tobebetterjavaer.com/ (opens new window)

    img

    没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。

    作者:沉默王二 链接:https://juejin.cn/post/7091810926608777246 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    编辑 (opens new window)
    #Java#沉默王二
    上次更新: 2023/01/30, 02:04:53
    几行代码,网站图片访问速度 100ms 飙升到 20ms!
    WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了!

    ← 几行代码,网站图片访问速度 100ms 飙升到 20ms! WindTerm:新一代开源免费的终端工具,GitHub星标6.6k+,太酷了!→

    最近更新
    01
    Coding 102 Writing code other people can read
    02-26
    02
    Kotlin Flow响应式编程,StateFlow和SharedFlow
    02-05
    03
    Kotlin Flow响应式编程,操作符函数进阶
    02-05
    更多文章>
    Theme by Vdoing | Copyright © 2022-2023 IT七剑客 | MIT License
  • 闽ICP备2021006579号-4
  • 闽公网安备 35012102500470号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式