锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

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

时间:2023-02-22 01:30:00 型板端连接器

大家好,我是二哥!所有关注我一段时间的朋友都知道,我最近的业余时间都花在编程喵这个实战项目上,应该用到 Redis,所以我想,干脆出一期 Redis 入门教程-主要集成 Redis 实现缓存功能,希望对大家有所帮助。

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

Redis 以其超高的性能、完美的文档和简单的源代码,国内外许多大型互联网公司,如阿里巴巴、腾讯等GitHub、Stack Overflow 等等。当然,中小企业也在使用。

Redis 作者是意大利人,原名 Salvatore Sanfilippo,网名 Antirez。不幸的是,互联网上没有他的维基百科全书,甚至他自己的博客网站都跪在边缘(没有 HTTPS,一些 js 加载失败)。

但是,如果是轻人造的, Redis 这么酷的产品,早已成功退休。

一、安装 Redis

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

官方地址:https://redis.io/docs/getting-started/

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

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

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

二、整合 Redis

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

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

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

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

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

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

Spring Cache 注释(上述四个)将在调用方法后返回缓存方法的最终结果;或者在调用方法之前取缓存结果,当然,以及删除缓存结果。

这些读写操作不需要手动写代码,直接交给我们 Spring Cache 来打理就 OK 是不是很贴心?

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

     org.springframework.boot     spring-boot-starter-data-redis  

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

spring:     redis:         host: 118.xx.xx.xxx # Redis服务器地址         database: 0 # Redis数据库索引(默认为0)         port: 6379 # Redis连接端口的服务器         password: xx # Redis服务器连接密码(默认为空)         timeout: 1000ms # 连接超时间(m秒) 

第三步,新增 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;
    }

}

通过 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);
}

第四步,在标签更新接口中添加 @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) ? "修改成功" : "修改失败");
    }
}

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

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

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

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

三、使用 Redis 连接池

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

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

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

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

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

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

1)Lettuce

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

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

第二步,在 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

添加 commons-pool2 依赖:


    org.apache.commons
    commons-pool2
    2.6.2
    jar
    compile

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

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

2)Jedis

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


    org.springframework.boot
    spring-boot-starter-data-redis
    
        
            io.lettuce
            lettuce-core
        
    



    redis.clients
    jedis

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

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

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

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

四、自由操作 Redis

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

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

第一步,增加 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

}

第二步,增加 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
}

第三步,在标签 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);
    }

}

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

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

五、小结

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

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

https://github.com/itwanger/coding-more

我们下期见~


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

https://github.com/itwanger/toBeBetterJavaer

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

https://tobebetterjavaer.com/

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

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章