天天看點

Spring Boot整合Redis1.spring-data-redis項目簡介2.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 Boot整合Redis1.spring-data-redis項目簡介2.Spring Boot中配置和使用Redis

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

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

Spring Boot整合Redis1.spring-data-redis項目簡介2.Spring Boot中配置和使用Redis

由于我們什麼都沒有配置,是以它會預設使用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針對每一種資料結構的操作都提供了對應的操作接口。如下表所示:

Spring Boot整合Redis1.spring-data-redis項目簡介2.Spring Boot中配置和使用Redis

它們都可以通過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接口,接口如下表所示:

Spring Boot整合Redis1.spring-data-redis項目簡介2.Spring Boot中配置和使用Redis

同樣地,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事務下資料一緻性。

Spring Boot整合Redis1.spring-data-redis項目簡介2.Spring Boot中配置和使用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實作類。

繼續閱讀