某意大利小哥,竟靠一個緩存中間件直接封神?

大家好,我是二哥呀!關注我有一段時間的小夥伴都知道了,我最近的業餘時間都花在了編程喵🐱這個實戰項目上,其中要用到 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。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

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

spring:
    redis:
        host: 118.xx.xx.xxx # Redis服務器地址
        database: 0 # Redis數據庫索引(默認爲0)
        port: 6379 # Redis服務器連接端口
        password: xx # Redis服務器連接密碼(默認爲空)
        timeout: 1000ms # 連接超時時間(毫秒)

第三步,新增 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 依賴:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.2</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

重新啓動服務,在 RedisConfig 類的 redisTemplate 方法裏對 redisTemplate 打上斷點,debug 模式下可以看到連接池的配置信息(redisConnectionFactory→clientConfiguration→poolConfig)。如下圖所示。

如果在 application-dev.yml 文件中沒有添加 Lettuce 連接池配置的話,是不會看到

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>

第二步,修改 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/

沒有什麼使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣,我是不繫之舟

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章