Spring Boot整合Redis

目錄

1.spring-data-redis項目簡介

1.1 spring-data-redis項目的設計

1.2 RedisTemplate

1.3 Spring對Redis數據類型操作的封裝

1.4 SessionCallback和RedisCallback

2.Spring Boot中配置和使用Redis

2.1 配置Redis

2.2 操作Redis數據類型

2.2.1 字符串和散列的操作

2.2.2 列表操作

2.2.3 集合操作

2.3 Redis的特殊用法

2.3.1 Redis事務

2.3.2 Redis流水線

2.3.3 Redis發佈訂閱

2.3.4 使用Lua腳本


在現今互聯網應用中,NoSql已經廣泛應用,在互聯網中起到加速系統的作用。有兩種NoSQL使用最爲廣泛,那就是Redis和MongoDB。

Redis是一種運行在內存的數據庫,支持7種數據類型的存儲。Redis的運行速度很快,大約是關係數據庫幾倍到幾十倍的速度。如果我們將常用的數據存儲在Redis中,用來代替關係數據庫的查詢訪問,網站性能將可以得到大幅提高。在現實中,查詢數據要遠遠大於更新數據,一般一個正常的網站查詢和更新的比例大約是1:9到3:7,在查詢比例較大的網站使用Redis可以數倍地提升網站的性能。

Redis自身數據類型比較少,命令功能也比較有限,運算能力一直不強,所以在Redis2.6版本之後,開始增加Lua語言的支持,這樣Redis的運算能力就大大提高了,而且在Redis中Lua語言的執行是原子性的,也就是在Redis執行Lua時,不會被其他命令所打斷,這就能夠保證在高併發場景下的一致性。

Spring是通過spring-data-redis項目對Redis開發進行支持的,在討論Spring Boot如何使用Redis之前,有必要簡單地介紹一下這個項目。

1.spring-data-redis項目簡介

1.1 spring-data-redis項目的設計

在Java中與Redis連接的驅動存在很多種,目前比較廣泛使用的是Jedis,其他的還有Lettuce、Jredis和Srp。Lettuce目前使用的比較少,Jredis和Srp則已經不再推薦使用,所以我們只討論Spring推薦使用的類庫Jedis的使用。

Spring中是通過RedisConnection接口操作Redis的,而RedisConnection則對原生的Jedis進行封裝。要獲取RedisConnection接口對象,是通過RedisConnectionFactory接口去生成的,所以第一步要配置的便是這個工廠,而配置工廠主要是配置Redis的連接池,對於連接池可以設定其最大連接數、超時時間等屬性。創建RedisConnectionFactory對象的實現的代碼如下:

package com.martin.config.chapter7;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @author: martin
 * @date: 2019/11/16 19:35
 * @description:
 */
@Configuration
public class RedisConfig {
    private RedisConnectionFactory connectionFactory = null;
    @Bean("redisConnectionFactory")
    public RedisConnectionFactory initRedisConnectionFactory(){
        if (this.connectionFactory != null){
            return this.connectionFactory;
        }

        JedisPoolConfig poolConfig = new JedisPoolConfig();
        //最大空閒數
        poolConfig.setMaxIdle(30);
        //最大連接數
        poolConfig.setMaxTotal(50);
        //最大等待毫秒數
        poolConfig.setMaxWaitMillis(2000);
        //創建Jedis連接工廠
        JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
        //獲取單機Redis配置
        RedisStandaloneConfiguration rsCfg = connectionFactory.getStandaloneConfiguration();
        rsCfg.setHostName("192.168.0.1");
        rsCfg.setPort(6379);
        RedisPassword redisPassword = RedisPassword.of("12345678");
        rsCfg.setPassword(redisPassword);
        return connectionFactory;
    }
}

這裏我們通過一個連接池的配置創建了RedisConnectionFactory,通過它就能夠創建RedisConnection接口對象。但是我們在使用一條連接時,要先從RedisConnectionFactory工廠獲取,然後在使用完成後還要自己關閉它。Spring爲了進一步簡化開發,提供了RedisTemplate。

1.2 RedisTemplate

RedisTemplate是一個強大的類,首先它會自動從RedisConnectionFactory工廠中獲取連接,然後執行對應的Redis命令,在最後還會關閉Redis連接。這些在RedisTemplate中都被封裝了,所以並不需要開發者關注Redis連接的閉合問題。 創建RedisTemplate的示例代碼如下:

    @Bean("redisTemplate")
    public RedisTemplate<Object, Object> initRedisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(initRedisConnectionFactory());
        return redisTemplate;
    }

然後測試它,測試代碼如下:

package com.martin.config.chapter7;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author: martin
 * @date: 2019/11/16 20:49
 * @description:
 */
public class RedisTemplateTest {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);
        redisTemplate.opsForValue().set("key1", "value1");
        redisTemplate.opsForHash().put("hash", "field", "hvalue");
    }
}

這裏使用了Java配置文件RedisConfig來創建Spring IOC容器,然後從中獲取RedisTemplate對象,接着設置鍵值。代碼運行完成之後,我們在Redis中查詢存入的數據,查詢結果如下:

MyRedis:0>keys *key1
1) \xAC\xED\x00\x05t\x00\x04key1

我們發現,Redis存入的並不是key1這樣的字符串,這是怎麼回事呢?

Redis是一種基於字符串存儲的NoSql,而Java是基於對象的語言,對象是無法存儲到Redis中的,不過Java提供了序列化機制,只要類實現了java.io.Serializable接口,就代表類的對象能夠進行序列化,通過將類對象進行序列化就能夠得到二進制字符串,這樣Redis就可以將這些類對象以字符串進行存儲。Java也可以將那些二進制字符串通過反序列化轉爲Java對象,通過這個原理,Spring提供了序列化器接口RedisSerializer:

package org.springframework.data.redis.serializer;

import org.springframework.lang.Nullable;

public interface RedisSerializer<T> {
    @Nullable
    byte[] serialize(@Nullable T var1) throws SerializationException;

    @Nullable
    T deserialize(@Nullable byte[] var1) throws SerializationException;
}

該接口有兩個方法,這兩個方法一個是serialize,它能夠把那些可以序列化的對象轉換爲二進制字符串;另一個是deserialize,它能夠通過反序列化把二進制字符串轉換爲Java對象。StringRedisSerializer和JdkSerializationRedisSerializer是我們比較常用的兩個實現類,其中JdkSerializationRedisSerializer是RedisTemplate默認的序列化器,“key1”這個字符串就是被它序列化變爲一個比較奇怪的字符串。

spring-data-redis序列化器原理示意圖

RedisTemplate提供瞭如下表所示的幾個可以配置的屬性:

由於我們什麼都沒有配置,因此它會默認使用JdkSerializationRedisSerializer對對象進行序列化和反序列化。這就是我們看到複雜字符串的原因,爲了解決這個問題,我們可以修改代碼如下:

    @Bean("redisTemplate")
    public RedisTemplate<Object, Object> initRedisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        //RedisTemplate會自動初始化StringRedisSerializer
        RedisSerializer stringRedisSerializer = redisTemplate.getStringSerializer();
        //設置字符串序列化器爲默認序列化器
        redisTemplate.setDefaultSerializer(stringRedisSerializer);
        redisTemplate.setConnectionFactory(initRedisConnectionFactory());
        return redisTemplate;
    }

這裏,我們將Redis的默認序列化器設置爲字符串序列化器,這樣把他們轉換出來就會採用字符串了。

MyRedis:0>get key1
value1

MyRedis:0>hget hash field
hvalue

1.3 Spring對Redis數據類型操作的封裝

Redis能夠支持7種類型的數據結構,這7種數據類型是字符串、散列、列表、集合、有序結合、計數和地理位置。爲此,Spring針對每一種數據結構的操作都提供了對應的操作接口。如下表所示:

它們都可以通過RedisTemplate得到,得到的方法非常簡單,代碼清單如下所示:

        GeoOperations geoOperations = redisTemplate.opsForGeo();
        HashOperations hashOperations = redisTemplate.opsForHash();
        HyperLogLogOperations logLogOperations = redisTemplate.opsForHyperLogLog();
        ListOperations listOperations = redisTemplate.opsForList();
        SetOperations setOperations = redisTemplate.opsForSet();
        ValueOperations valueOperations = redisTemplate.opsForValue();
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

這樣就可以通過各類的操作接口來操作不同的數據類型了。有時候我們需要對某一個鍵值對做連續的操作,例如有時需要連續操作一個散列數據類型或者列表多次,這時Spring爲我們提供了對應的BoundXXXOperations接口,接口如下表所示:

同樣地,RedisTemplate也對獲取它們提供了對應的方法,實例代碼如下:

        BoundGeoOperations geoOperations = redisTemplate.boundGeoOps("geo");
        BoundHashOperations hashOperations = redisTemplate.boundHashOps("hash");
        BoundListOperations listOperations = redisTemplate.boundListOps("list");
        BoundSetOperations setOperations = redisTemplate.boundSetOps("set");
        BoundValueOperations valueOperations = redisTemplate.boundValueOps("string");
        BoundZSetOperations zSetOperations = redisTemplate.boundZSetOps("zset");

獲取其中的操作接口後,我們就可以對某個鍵的數據進行多次操作,這樣我們就知道如何有效地通過Spring操作Redis的各種數據類型了。

1.4 SessionCallback和RedisCallback

如果我們希望在同一條Redis鏈接中,執行兩條或者多條命令,可以使用Spring爲我們提供的RedisCallback和SessionCallback兩個接口。這樣就可以避免一條一條的執行命令,對資源的浪費。

SessionCallback接口和RedisCallback接口的主要作用是讓RedisTemplate進行回調,通過它們可以在同一條連接下執行多個Redis命令。其中,SessionCallback提供了良好的封裝,因此在實際開發中應該優先使用;相對而言,RedisCallback接口比較底層,需要處理的內容也比較多,可讀性差,一般不考慮使用。使用SessionCallback的實例代碼如下:

    public static void useRedisCallback(RedisTemplate redisTemplate) {
        redisTemplate.execute((RedisCallback) (redisConnection) -> {
            redisConnection.set("key1".getBytes(), "value1".getBytes());
            redisConnection.hSet("hash".getBytes(), "field".getBytes(), "hvalue".getBytes());
            return null;
        });
    }

執行日誌如下:

16:32:13.941 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source
16:32:13.944 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'redisTemplate'
16:32:18.317 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Opening RedisConnection
16:32:18.742 [main] DEBUG org.springframework.data.redis.core.RedisConnectionUtils - Closing Redis Connection

從日誌中我們看出,使用RedisCallback的方式能夠使得RedisTemplate使用同一條Redis連接進行回調,從而可以在同一條Redis連接下執行多個方法,避免RedisTemplate多次獲取不同的連接。

2.Spring Boot中配置和使用Redis

2.1 配置Redis

在Spring Boot中集成Redis更爲簡單,我們只需要在配置文件application.properties中加入如下代碼清單:

#配置連接池屬性
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-wait=2000
#配置Redis服務器屬性
spring.redis.port=6379
spring.redis.host=192.168.0.1
spring.redis.password=12345678
#連接超時時間,單位毫秒
spring.redis.timeout=1000

這裏我們配置了連接池和服務器的屬性,用以連接Redis服務器,這樣Spring Boot的自動裝配機制就會讀取這些配置來生成有關Redis的操作對象,這裏它會自動生成RedisConnectionFactory、RedisTemplate、StringRedisTemplate等常用的Redis對象。同時爲了修改RedisTemplate默認的序列化器,我們定義如下的代碼:

package com.martin.config.chapter7;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

/**
 * @author: martin
 * @date: 2019/11/16 19:35
 * @description:
 */
@Component
public class RedisSerializationConfig implements InitializingBean {
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void afterPropertiesSet() throws Exception {
        RedisSerializer<String> redisSerializer = redisTemplate.getStringSerializer();
        redisTemplate.setDefaultSerializer(redisSerializer);
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setValueSerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        redisTemplate.setHashValueSerializer(redisSerializer);
    }
}

這樣我們存儲到Redis的鍵值對就是String類型了。

2.2 操作Redis數據類型

這一節主要演示常用的Redis數據類型(字符串、散列、列表、集合和有序集合)的操作。

2.2.1 字符串和散列的操作

首先開始操作字符串和散列,這是Redis最爲常用的數據類型。實例代碼如下:

package com.martin.config.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.Map;

/**
 * @author: martin
 * @date: 2019/11/17 18:22
 * @description:
 */
@Controller
@RequestMapping("/redis")
public class RedisController {
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/stringAndHash")
    @ResponseBody
    public Map<String, Object> testStringAndHash() {
        redisTemplate.opsForValue().set("key1", "value1");
        redisTemplate.opsForValue().set("int_key", "1");
        redisTemplate.opsForValue().increment("int_key", 2L);
        //獲取底層Jedis連接
        Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
        //減1操作,這個命令RedisTemplate不支持,所以需要先獲取底層的連接再操作
        jedis.decr("int_key");

        Map<String, String> hash = new HashMap<>();
        hash.put("field1", "value1");
        hash.put("field2", "value2");
        //存入一個散列的數據類型
        redisTemplate.opsForHash().putAll("hash", hash);
        //新增一個字段
        redisTemplate.opsForHash().put("hash", "field3", "value3");
        //綁定散列操作的key,這樣可以連續對同一個散列數據類型進行操作
        BoundHashOperations hashOperations = redisTemplate.boundHashOps("hash");
        //刪除其中的兩個字段
        hashOperations.delete("field1", "field2");
        //新增一個字段
        hashOperations.put("field4", "value4");
        Map<String, Object> result = new HashMap<>();
        result.put("info", "success");
        return result;
    }
}

這裏需要注意的一點是我在做測試的時候,將redis連接池的配置單獨放置到文件application-redis.properties中了。爲了能夠加載到該文件,我需要在SpringBootConfigApplication中導入該文件:

@PropertySource({"classpath:application-redis.properties"})

代碼中的@Autowired注入了Spring Boot爲我們自動初始化的RedisTemplate和StringRedisTemplate對象。有一點需要注意的是,這裏進行的加一操作,因爲RedisTemplate並不能支持底層所有的Redis命令,所以這裏先獲取了原始的Redis連接的Jedis對象。

2.2.2 列表操作

列表在Redis中其實是一種鏈表結構,這就意味着查詢性能不高,而增刪節點的性能高,這是它的特性。操作實例代碼如下:

    @RequestMapping("/list")
    @ResponseBody
    public Map<String, Object> testList() {
        //鏈表從左向右的順序e,d,c,b
        redisTemplate.opsForList().leftPushAll("list1", "b", "c", "d", "e");
        //鏈表從左向右的順序爲f,g,h,i
        redisTemplate.opsForList().rightPushAll("list2", "f", "g", "h", "i");
        //綁定list2
        BoundListOperations listOperations = redisTemplate.boundListOps("list2");
        //從右邊彈出一個成員
        System.out.println(listOperations.rightPop());
        System.out.println(listOperations.index(1));
        System.out.println(listOperations.size());
        List<String> allElement = listOperations.range(0, listOperations.size() - 1);
        System.out.println(allElement);

        return (Map<String, Object>) new HashMap().put("success", true);
    }

2.2.3 集合操作

集合是不允許成員重複的,它在數據結構上是一個散列表的結構,所以對於它而言是無序的。Redis還提供了交集、並集和差集的運算。實例代碼如下;

 @RequestMapping("/set")
    public void testSet() {
        redisTemplate.opsForSet().add("set1", "a", "b", "c", "d", "e", "e");
        redisTemplate.opsForSet().add("set2", "b", "c", "f", "g");
        //綁定集和set1操作
        BoundSetOperations setOperations = redisTemplate.boundSetOps("set1");
        //添加兩個元素
        setOperations.add("h", "i");
        //刪除兩個元素
        setOperations.remove("e", "h");
        //返回所有的元素
        Set<String> set1 = setOperations.members();
        //元素的個數
        setOperations.size();
        //交集
        setOperations.intersect("set2");
        //差集
        setOperations.diff("set2");
        //求交集並保存到inter集和
        setOperations.intersectAndStore("set2", "inter");
        //求並集
        setOperations.union("set2");
    }

在一些網站中,經常會有排名,如最熱門的商品或者最大的購買買家,都是常見的場景。對於這類排名,刷新往往需要及時,也涉及到較大的統計,如果使用數據庫會很慢。爲了支持集合的排序,Redis提供了有序集合(zset),它的有序性通過在數據結構中增加一個屬性-score(分數)得以支持。Spring提供了TypedTuple接口以及默認的實現類DefaultTypedTuple來支持有序集合。實例代碼如下:

 @RequestMapping("/zset")
    public void testZSet() {
        Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
        //初始化有序集合
        for (int i = 1; i < 9; i++) {
            double score = i * 0.1;
            ZSetOperations.TypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value" + i, score);
            typedTupleSet.add(typedTuple);
        }
        //往有序集合插入元素
        redisTemplate.opsForZSet().add("zset1",typedTupleSet);
        //綁定zset有序集合操作
        BoundZSetOperations<String,String> zSetOperations = redisTemplate.boundZSetOps("zset1");
        //增加一個元素
        zSetOperations.add("value10",0.26);
        //獲取有序集合
        System.out.println(zSetOperations.range(1,6));
        System.out.println(zSetOperations.rangeByScore(0.2,0.6));
        //獲取大於value3的元素
        RedisZSetCommands.Range range = new RedisZSetCommands.Range();
        range.gt("value3");
        //求分數
        System.out.println(zSetOperations.score("value8"));
    }

2.3 Redis的特殊用法

Redis除了操作數據類型以外,還能支持事務、流水線、發佈訂閱和Lua腳本等功能。

2.3.1 Redis事務

Redis中使用事務通常的命令組合是watch.......multi.......exec,也就是要在一個Redis連接中執行多個命令,其中watch命令監控Redis的一些鍵;multi命令是開始事務,開始事務以後,該客戶端的命令不會馬上執行,而是存放在一個隊列中;exe命令的意義在於執行事務,只是它在隊列命令執行前會判斷被watch監控的Redis的鍵的數據是否發生過變化,如果已經發生了變化,那麼Redis就會取消事務,否則就會執行事務,Redis執行事務時,要麼全部執行,要麼全部不執行,而且不會被其他客戶端打斷,這樣就保證了Redis事務下數據一致性。

Redis事務執行過程

2.3.2 Redis流水線

在默認情況下,Redis客戶端是一條條命令發送給Redis客戶端的,這樣顯然性能不高。在Redis中,使用流水線技術,可以一次性地發送多個命令去執行,大幅度地提升Redis的性能。

2.3.3 Redis發佈訂閱

發佈訂閱是消息的一種常用模式。首先是 Redis 提供一個渠道,讓消息能夠發送到這個渠道上 ,而多個系統可以監聽這個渠道, 如短信、微信和郵件系統都可以監聽這個渠道,當一條消息發送到渠道,渠道就會通知它的監聽者,這樣短信、微信和郵件系統就能夠得到這個渠道給它們的消息了,這些監聽者會根據自己的需要去處理這個消息,於是我們就可以得到各種各樣的通知了 。

Redis消息監聽器代碼如下:
 

package com.martin.config.redis;

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

/**
 * @author: martin
 * @date: 2020/1/21
 */
@Component
public class RedisMsgListener implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
        //消息體
        String body = new String(message.getBody());
        //渠道名稱
        String topic = new String(pattern);
        System.out.println(body);
        System.out.println(topic);
    }
}

2.3.4 使用Lua腳本

Redis中有很多的命令,但是嚴格來說 Redis提供的計算能力還是比較有限的 。爲了增強 Redis的計算能力,Redis在2.6版本後提供了 Lua 腳本的支持,而且執行 Lua 腳本在 Redis 中還具備原子性,所以在需要保證數據一致性的高併發環境中,我們也可以使用Redis的Lua語言來保證數據的一致性,且 Lua 腳本具備更加強大的運算功能,在高併發需要保證數據一致性時,Lua 腳本方案比使用Redis自身提供的事務要更好一些 。

在 Redis 中有兩種運行 Lua 的方法,一種是直接發送Lua到Redis服務器去執行,另一種是先把Lua發送給Redis,Redis會對Lua腳本進行緩存,然後返回一個SHA 1的32位編碼回來,之後只需要發送SHA 1和相關參數給Redis便可以執行了。這裏需要解釋的是爲什麼會存在通過32位編碼執行的方法。如果Lua腳本很長,那麼就需要通過網絡傳遞腳本給Redis去執行了,而現實的情況是網絡的傳遞速度往往跟不上 Redis的執行速度,所以網絡就會成爲Redis執行的瓶頸。如果只是傳遞32位編碼和參數,那麼需要傳遞的消息就少了許多,這樣就可以極大地減少網絡傳輸的內容,從而提高系統的性能。

爲了支持Redis的Lua腳本,Spring提供了RedisScript接口,與此同時也有一個DefaultRedisScript實現類。

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