天天看點

深入淺出springboot--redis的整合(2)

本章主要講述了redis在SpringBoot中的內建

上面一章節主要講解了spring和redis的開發

SpringBoot中的redis的常用操作

  • application.properties

我們首先在application.properties中直接配置redis的相關資訊,包括redis的連接配接池屬性和redis的伺服器的相關屬性

# 配置redis的連接配接池屬性
spring.redis.pool.min-idle=5
spring.redis.pool.max-active=10
spring.redis.pool.max-wait=2000
# 端口号
spring.redis.port=6379
# 主機位址
spring.redis.host=127.0.0.1
# redis的連接配接逾時時間,機關是ms
spring.redis.timeout=1000

server.port=8081
           
配置上述的資訊後,SpringBoot會預設裝配有關redis的操作對象,包括RedisConnectionFactory,RedisTemplate,StringRedisTemplate(該stringRedisTemplate是直接将序列化器設定為String類型的,但是這個隻能夠存儲字元串,并不能支援java對象的存儲)
  • 配置類

為了能夠可以存儲java對象的存儲,且存儲的值應該是一個我們能夠看懂的字元串,是以我們需要将預設的JDK的序列化器替換為StringSerilizer的序列化器

package com.xiyou.redis.conf;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

import javax.annotation.PostConstruct;

@Configuration
public class RedisConfig {

    @Autowired
    private RedisTemplate redisTemplate;

    // spring的自定義初始化注解
    @PostConstruct
    public void init(){
        initRedisTemplate();
    }

    private void initRedisTemplate() {
        // 直接從RedisTemplate中擷取字元串序列化器
        RedisSerializer redisSerializer = redisTemplate.getStringSerializer();
        // 将redis的key的序列化器進行改變 ,隻改變其key的序列化
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
    }

}

           

(1)redisTemplate是springboot自動注入進來的

(2)我們這裡隻對其key進行了序列化器的設定,其value值并沒有對其進行設定

(3)@PostConstruct是定義的後初始化方法,通常我們把需要初始化的自定義的東西放到postConstruct中去執行(具體可以參考spring的生命周期,有詳細的介紹)

  • 常用的五種資料類型的操作

(1)操作字元串和散列類型

@GetMapping("/stringAndHash")
    public Map<String, Object> testStringAndHash(){
        // 此時的redisTemplate隻對其key和value進行了序列化
        redisTemplate.opsForValue().set("key1", "value1");
        // 隻對其key進行了序列化,value沒有,是以其value存到redis的時候并不是1,而是一個jdk預設序列化器轉碼出來的不知名的東西
        redisTemplate.opsForValue().set("int_key", "1");
        // 預設用的是string的序列化器,是以其key和value都是string的
        stringRedisTemplate.opsForValue().set("int", "1");
        // 對其進行運算, 對其key是1的進行運算
        Long anInt = stringRedisTemplate.opsForValue().increment("int", 1);
        System.out.println(anInt);
        // 擷取底層的jedis連接配接,因為有些操作redis并不支援
        Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
        // 減1操作,這個操作redisTemplate不支援,是以得擷取底層連接配接再進行操作
        jedis.decr("int");
        Map<String, String> hash = new HashMap<String, String>();
        hash.put("field1", "value1");
        hash.put("field2", "value2");
        // 存入一個散列的資料類型
        // 這裡用的是putAll方法,同樣也可以使用put(key,field,value)
        stringRedisTemplate.opsForHash().putAll("hash", hash);
        // 新增一個字段
        stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
        // 綁定散列的key,這樣連續對同一個散列資料類型進行操作
        // 綁定一個key為hash的redisTemplate
        BoundHashOperations hashOperations = stringRedisTemplate.boundHashOps("hash");
        // 删除兩個field
        hashOperations.delete("field1", "field2");
        // 新增一個field
        hashOperations.put("field3", "value3");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("success", true);
        return map;
    }
           

(2)操作清單

@GetMapping("/list")
    public Map<String, Object> testList(){
        // 插入兩個清單,注意順序
        // 連結清單從左到右順序為V10,V8,v6,v4,v2
        // leftpush就是說從左到右進行插入
        stringRedisTemplate.opsForList().leftPushAll("list1", "v2", "v4", "v6", "v8", "v10");
        // 連結清單從左到右的順序是v1,v2,v3,v4,v5
        // rightPush是從右到左進行插入
        stringRedisTemplate.opsForList().rightPushAll("list2", "v1", "v2", "v3", "v4", "v5");
        // 綁定list2對其進行操作
        BoundListOperations listOps = stringRedisTemplate.boundListOps("list2");
        // 從右邊彈出一個元素
        Object result1 = listOps.rightPop();
        System.out.println(result1);
        // 擷取定位元素,redis從0開始,這裡值是v2
        Object result2 = listOps.index(1);
        System.out.println(result2);
        // 從左邊插傳入連結表
        listOps.leftPush("v0");
        // 求連結清單的長度
        Long size = listOps.size();
        // 求連結清單的下标區間成員,整個連結清單下标範圍是0到size-1,這裡不取最後一個元素
        // 取左取右
        listOps.range(0, size-2);
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("success", true);
        return map;
    }
           

(3)操作無序集合

@GetMapping("/set")
    public Map<String, Object> testSet(){
        // set是不允許重複的,即使添加有重複的資料,但是也不會添加進來
        stringRedisTemplate.opsForSet().add("set1", "v1", "v1", "v2", "v2", "v3", "v4", "v5");
        stringRedisTemplate.opsForSet().add("set2", "v2", "v4", "v6", "v8");
        // 綁定set1集合操作
        BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
        // 增加2個元素
        setOps.add("v6", "v7");
        // 删除兩個元素
        setOps.remove("v1", "v7");
        // 傳回所有的元素
        Set set1 = setOps.members();
        // 計算成員數
        int size = set1.size();
        // 求交集
        Set inter = setOps.intersect("set2");
        // 求交集并儲存到新的集合中
        setOps.intersectAndStore("set2", "inter");
        // 求差集
        Set diff = setOps.diff("set2");
        // 求差集并儲存到新的集合中
        setOps.diffAndStore("set2", "diff");
        // 求并集
        Set union = setOps.union("set2");
        // 求并集并儲存到新的集合中
        setOps.unionAndStore("set2", "union");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("success", true);
        return map;
    }
           

集合存儲的是無序無重複的資料,是以即使add了相同多個資料,在redis的集合中隻會存儲一個資料

(4)操作有序集合

@GetMapping("/zset")
    public Map<String, Object> testZset(){
        // TypedTuple就是用來存儲 zset中的value和score的元組
        Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<ZSetOperations.TypedTuple<String>>();
        for(int i = 1; i <= 9; i++){
            // 分數
            double score = i * 0.1;
            // 存入value和score
            DefaultTypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value" + i, score);
            typedTupleSet.add(typedTuple);
        }

        // 向有序集合中插入元素
        stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
        // 綁定有序集合zset1進行操作
        BoundZSetOperations<String, String> zSetOps = stringRedisTemplate.boundZSetOps("zset1");
        // 增加一個元素
        // 添加的是value和score
        zSetOps.add("value10", 0.26);
        // 按範圍取值
        Set<String> setRange = zSetOps.range(1, 6);
        // 按分數排序擷取有序集合
        Set<String> setScore = zSetOps.rangeByScore(0.2, 0.6);
        // 省略部分操作,可以直接檢視相關API
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("success", true);
        return map;
    }
           

這裡強調一點,為了支援value和score,spring提供了接口-----TypedTuple接口,他定義了兩個方法,并且Spring還提供了預設的實作類DefaultTypedTuple

TypedTuple是一個接口,該接口有兩個屬性是score和value,value儲存的是zset中的值,score儲存的是分數,redis就是使用分數來進行集合排序的。

其實學過python的人可以看到這其實就是一個元組 (value, score)

  • zset的存儲格式:
{
	key: [(value,score), (value,score)]
}
           

每一個(key,value)就是一個typedTuple結構

Redis的特殊的用法

redis除了上述的常用操作外還提供了一些特殊的用法,比如redis對事務的支援,流水線,釋出訂閱和Lua語言等功能。

(1)高并發的場景中我們需要保持資料的一緻性,此時我們就可以考慮redis的事務以及利用redis執行Lua的原子性來達到資料的一緻性。

(2)當我們需要大量執行redis指令的時候,我們就可以利用redis的流水線功能,批量執行,提升執行效率

1. 使用redis的事務

redis是支援事務的,redis的事務通常的指令組合是watch…multi…exec,也就是要在一個redis的連接配接中執行多個指令,這時候我們就不應該使用RedisTemplate的對象,因為該對象每一次操作都會開啟一個連接配接,操作完成後再自動關閉,此時我們就應該考慮SessionCallback接口來達到這個目的。

(1)watch:該指令可以監控Redis的一些鍵

(2)multi:該指令是開啟事務,注意一點,這裡的開啟事務并不是開始執行事務,他不會立刻執行,而是将指令存放到一個隊列中,也就是在這時我們執行一些傳回資料的指令的時候,redis不會馬上執行,将其存放在隊列中,是以此時調用redis的get指令的時候,取到的值都是null

(3)exe:該指令的作用是開始正式的執行事務,注意,他在執行之前會判斷被watch監控的redis的鍵是否發生過變化,即使賦予了相同的值,也認為發生了改變,如果認為其發生了變化,redis就會取消事務,否則就會執行事務

redis在執行事務的時候,要麼都執行,要麼都不執行,而且不會被其他的用戶端打斷,這樣保證了redis事務下的資料一緻性。
深入淺出springboot--redis的整合(2)

代碼如下:

  • (1)首先是全部正常的情況下運作
/**
     * redis的事務機制
     */
    @GetMapping("/multi")
    public Map<String, Object> testMulti(){
        // 1
        stringRedisTemplate.opsForValue().set("key1", "1");
        // redisTemplate.opsForValue().set("key1", "value1");
        List list = (List) redisTemplate.execute(new SessionCallback() {

            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                // 監控key1
                redisOperations.watch("key1");
                // 開啟事務,全部操作進入隊列,不立刻執行
                redisOperations.multi();
                redisOperations.opsForValue().set("key2", "value2");
                // 2
                redisOperations.opsForValue().increment("key1", 1);
                // 獲得的值是null,因為此時隻是将其放到隊列中,并沒有真正的執行
                Object value2 = redisOperations.opsForValue().get("key2");
                System.out.println("指令在隊列,是以value是null【" + value2 + "】");
                redisOperations.opsForValue().set("key3", "value3");
                // 執行exec指令,先判斷key1是否被更改,如果更改了就不執行事務,否則就執行業務
                // 3
                return redisOperations.exec();
            }

        });

        System.out.println(list);

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

此時我們可以觀察redis的時候就會發現,redis已經全部正常存儲,沒有報錯資訊,第一步使用stringRedisTemplate是因為後面對key1的操作是increment,如果使用預設的jdk序列化器,得到的不是一個數字,無法進行累加

  • (2)在exec前修改key1的值

    在exec前打上斷點,執行到exec之前,在redis資料庫中手動修改key1的值。這麼做的目的是,我們watch了key1的值,此時在正式送出後,觀察redis是否有值,得到的結果是若watch的key在exec之前被修改了,則不會執行事務,取消執行,是以這裡key2,key3不會存放到redis中

  • (3)在multi中步驟出錯

    步驟出錯的意思是,我們會在exec之前,multi之後寫一個錯誤的redis操作,這裡我們将key1存放一個value1,且對key1進行increment操作。此時發現,即使出錯,key2,key3也依舊會執行,将其存放在redis中,這就是redis的事務和資料庫的事務最大的不同,redis隻有在watch的值被改變的時候才不會執行

/**
     * redis的事務機制
     */
    @GetMapping("/multi")
    public Map<String, Object> testMulti(){
        // 1
        //stringRedisTemplate.opsForValue().set("key1", "1");
        redisTemplate.opsForValue().set("key1", "value1");
        List list = (List) redisTemplate.execute(new SessionCallback() {

            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                // 監控key1
                redisOperations.watch("key1");
                // 開啟事務,全部操作進入隊列,不立刻執行
                redisOperations.multi();
                redisOperations.opsForValue().set("key2", "value2");
                // 2
                redisOperations.opsForValue().increment("key1", 1);
                // 獲得的值是null,因為此時隻是将其放到隊列中,并沒有真正的執行
                Object value2 = redisOperations.opsForValue().get("key2");
                System.out.println("指令在隊列,是以value是null【" + value2 + "】");
                redisOperations.opsForValue().set("key3", "value3");
                // 執行exec指令,先判斷key1是否被更改,如果更改了就不執行事務,否則就執行業務
                // 3
                return redisOperations.exec();
            }

        });

        System.out.println(list);

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

這裡可能會有疑問,為什麼我們在multi之後操作了watch的值key1,依舊回執行,這是因為我們的multi操作隻是将其存放在了隊列中,并沒有真正的執行,exec後才會執行,是以我們并沒有改變我們watch的key1的值

2. 使用redis流水線

預設情況下,Redis用戶端是一條一條指令發送給Redis伺服器的,這樣顯然性能不高。在資料庫中我們有批量執行的操作,同理,在redis中我們也有這種操作,這便是流水線技術,我們在需要執行的時候統一執行,并不是一次一次的發送指令去執行。測試代碼如下:

@GetMapping("/pipeline")
    public Map<String, Object> testPipeline(){
        Long startTime = System.currentTimeMillis();
        // 注意這裡使用的是executePipelined 而不是execute
        List list = (List) redisTemplate.executePipelined(new SessionCallback() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                for(int i = 1; i < 100000; i++){
                    redisOperations.opsForValue().set("pipeline_" + i, "value_" + i);
                    // 此時還是空,為什麼呢?
                    // 因為這裡和事務的操做原理相似,都是将其放到隊列中,真正用到的時候才會統一執行
                    String value = (String) redisOperations.opsForValue().get("pipeline_" + i);
                    if(i == 100000){
                        System.out.println("指令隻是進入了隊列,沒有真正的執行,是以隻為空【" + value + "】");
                    }
                }
                return null;
            }
        });

        Long end = System.currentTimeMillis();
        System.out.println("耗時: " + (end - startTime) + "毫秒");
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("success", true);
        return map;
    }
           

此時我們可以看到這個10萬條資料的執行結果,僅僅用了505ms。

  • 這裡需要注意兩點:

    (1)我們需要考慮空間的消耗,因為我們的executePipelined最終傳回了一個list,所有的結果都會儲存在這裡,是以會造成記憶體消耗過大,造成JVM記憶體異常,這個時候應該考慮疊代的方法執行redis 指令。

    (2)與事務一樣,使用流水線的過程中,所有的指令隻是進入了隊列而沒有真正的執行,是以在執行的時候傳回值也為空,這也是要注意的

繼續閱讀