Redis基礎和應用

學習5種基本數據結構的指令和java api。以及分佈式鎖,位圖,HyperLogLog,布隆過濾器(BloomFliter)等多種高級數據結構的指令和java api,以及具體應用場景。

數據結構

Redis五種數據結構,分別爲string,list,hash,set,zset。

string

key,value數據結構。

存儲結構類似java的數組。

指令

  • set key value
  • get key
  • exists key
  • del key
  • mset key1 value1 key2 value2 …
  • mget key1 key2 …
  • expire key expire_time_sec :設置過期時間
  • setex key expire_time_sec value : 創建時設置過期時間
  • setnx key value : 不存在key則創建,set會直接覆蓋

測試代碼

private static void testString() throws InterruptedException {

    String key = "string";
    // 1. 先刪除指定的key
    jedis.del(key);
    // 2. 添加字符串,過期時間5s,存在則不添加
    jedis.set(key, "value", new SetParams().ex(5).nx());
    // 3. 查詢符合模式的所有key
    // h?llo will match hello hallo hhllo
    // h*llo will match hllo heeeello
    // h[ae]llo will match hello and hallo, but not hillo
    String pattern = "str*";
    System.out.println("keys, PATTERN : {" + pattern + "}, RETURN : {" + jedis.keys(pattern) + "}");
    // 4. 查詢過期時間
    System.out.println("KEY : {" + key + "}, expire time : {" + jedis.ttl(key) + "}s");
    // 5. 移除過期時間
    jedis.persist(key);
    System.out.println("persist KEY , expire time : {" + jedis.ttl(key) + "}s");
    // 6. 隨機訪問
    String randomKey = jedis.randomKey();
    // 7. 查看類型
    System.out.println("RANDOM KEY : {" + randomKey + "},TYPE : {" + jedis.type(randomKey) + "}");
}

打印如下:
keys, PATTERN : {str*}, RETURN : {[string]}
KEY : {string}, expire time : {5}s
persist KEY , expire time : {-1}s
RANDOM KEY : {hash},TYPE : {hash}

list

key, value1,value2數據結構,類似java的list。

兩種存儲結構:當數據量小時,使用壓縮列表(ziplist);數量大大時,使用快速列表(quicklist)

struct ziplist<T>{
    int32 zlbytes; // 整個壓縮列表佔用字節數
    int32 zltail_offset; // 最後一個元素距離壓縮列表起始位置的偏移量,用於快速定位到最後一個節點
    int16 zllength; // 元素個數
    T[] entries; // 元素內容列表,依次緊湊存儲
    int8 zlend; // 標誌壓縮列表的結束
}

struct entry{
    int<var> prevlen; // 前一個entry的字節長度
    int<var> encoding; // 元素內容編碼
    optional byte[] content; // 元素內容
}

struct quicklist{
    quicklistNode* head;
    quicklistNode* tail;
    long count; // 元素總和
    int nodes; // ziplist節點的個數
    int compressDepth; // LZF 算法壓縮度
}

struct quicklistNode{
    quicklistNode* prev;
    quicklistNode* next;
    ziplist* zl; // 指向壓縮列表
    int32 size; // ziplist 的字節總數
    int16 count; // ziplist 中的元素數量
    int2 encoding; // 存儲形式2bit
}

quicklist 內部默認單個ziplist長度爲8kb,超過這個字節會另起一個ziplist。可以通過參數list-max-ziplist-size決定。

指令

  • push : lpush / rpush

rpush key value1 value2 value3

  • pop : lpop / rpop

lpop key

  • lindex
  • ltrim
  • lrange

程序

private static void testList() {
    // 1. 先刪除指定key
    jedis.del("list");
    // 2. 左添加
    jedis.lpush("list", "listValue3", "listValue2", "listValue1");
    // 右添加
    jedis.rpush("list", "listValue4", "listValue5", "listValue6");
    // 3. 取出所有元素
    System.out.println(jedis.lrange("list", 0, -1));
    // 4. 取出最後一個元素
    System.out.println(jedis.lindex("list", -1));
    // 5. 移除部分數據,保留1-3
    jedis.ltrim("list", 1, 3);
    System.out.println(jedis.lrange("list", 0, -1));
}

打印如下:
[listValue1, listValue2, listValue3, listValue4, listValue5, listValue6]
listValue6
[listValue2, listValue3, listValue4] 

hash

key,k1,value1,k2,value2,類似java中的Map。

存儲結構類似java的HashMap,使用數組加鏈表的方式來存儲。rehash採用循環漸進的處理,兩組hashtable,將舊的值一步一步的遷移到新的(hset、hdel操作),如果客戶端沒有操作,將會通過定時器,定時執行

指令

  • hset key k1 v1
  • hgetAll
  • hlen
  • hmget
  • hmset

程序

private static void testHash() {
    String key = "hash";
    // 1. 先刪除指定key
    jedis.del(key);
    Map<String, String> map = new HashMap<>();
    map.put("k1", "v1");
    map.put("k2", "v2");
    // 2. 添加數據結構
    jedis.hset(key, map);
    // 3. 追加數據
    jedis.hset(key, "k3", "v3");
    // 4. 獲取數據和長度
    System.out.println("獲取所有數據:" + jedis.hgetAll(key) + ",長度:" + jedis.hlen(key));
    // 5. 批量獲取
    System.out.println("批量獲取數據:" + jedis.hmget(key, "k1", "k3"));
    // 6. 計數
    jedis.hset(key, "k4", 1 + "");
    jedis.hincrBy(key, "k4", 1);
    System.out.println("獲取數據:" + jedis.hget(key, "k4"));
}

打印日誌

獲取所有數據:{k3=v3, k1=v1, k2=v2},長度:3
批量獲取數據:[v1, v3]
獲取數據:2

set

key,value1,value2,類似java的Set,value不能重複。

特殊的hash,值爲NULL,無序

指令

  • sadd key value1 value2
  • smembers key(無序)
  • sismember key value : 查詢key是否存在value,存在則返回1,否則返回0
  • scard
  • spop

程序

private static void testSet() {
    String key = "set";
    // 1. 先刪除指定key
    jedis.del(key);
    // 2. 添加數據結構
    jedis.sadd(key, "value1", "value2");
    // 3. 獲取數據和長度
    System.out.println("獲取所有數據:" + jedis.smembers(key) + ",長度:" + jedis.scard(key));
    // 4. 是否存在指定value
    System.out.println("是否存在數據:" + jedis.sismember(key, "value2"));
    // 5. 彈出數據
    System.out.println("彈出數據:" + jedis.spop(key));
    // 6.  獲取數據
    System.out.println("獲取數據:" + jedis.smembers(key));
}

zset

有序Set

存儲結構爲:跳躍列表

指令

  • zadd
  • zrange
  • zrevrange : 按score逆序列出
  • zcard
  • zscore : 獲取指定key的分數
  • zrank : 獲取指定key的排名
  • zrangebyscore : 遍歷分值區域
  • zrem : 刪除value

程序

private static void testZSet() {
    String key = "zset";
    // 1. 先刪除指定key
    jedis.del(key);
    // 2. 添加數據結構
    jedis.zadd(key, 100, "value1");
    jedis.zadd(key, 50, "value2");
    jedis.zadd(key, 0, "value3");
    // 3. 獲取數據
    System.out.println("獲取數據:" + jedis.zrange(key, 0, 100));
    // 4. 是否存在指定value
    System.out.println("獲取數據:" + jedis.zrevrange(key, 0, 100));
    // 5. 數據量
    System.out.println("數據量:" + jedis.zcard(key));
    // 6.  獲取數據
    System.out.println("獲取數據:" + jedis.zscore(key, "value2"));
    // 7.  獲取數據
    System.out.println("刪除數據:" + jedis.zrem(key, "value2"));
    System.out.println("獲取數據:" + jedis.zrange(key, 0, 100));
}

打印如下:

獲取數據:[value3, value2, value1]
獲取數據:[value1, value2, value3]
數據量:3
獲取數據:50.0
刪除數據:1
獲取數據:[value3, value1]

應用

分佈式鎖(重要)

set key value ex expire_time_sec nx (原子操作)

設置超時時間(ex),以及存在則不添加(nx)。可以設置一個隨機值作爲value,在刪除鎖時候,判斷value是否一致。

超時問題

如果函數在超時時間內無法結束,則會出現問題。解決方案:redisson插件,原理如下:

加鎖時,會有一個監聽線程,當持鎖線程還在執行則會追加超時時間。

private static RedissonClient redissonClient;

public static void main(String[] args) {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://localhost:6379");
    redissonClient = Redisson.create(config);
}

// 非公平鎖
private static void testLock() {
   try {
        System.out.println(Thread.currentThread().getName());
        RLock rLock = redissonClient.getLock("key");
        // 這一步線程阻塞
        System.out.println(Thread.currentThread().getName() + ",準備加鎖。。。。。。");
        rLock.lock(3, TimeUnit.SECONDS);
        System.out.println(Thread.currentThread().getName() + "加鎖成功");
        Thread.sleep(5000);
        rLock.unlock();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

消息隊列

使用list數據結構作爲隊列,lpush爲生產消息,rpop爲消費消息。

可以使用brpop來代替rpop。爲了避免空閒連接自動斷開,所以需要捕獲異常,然後重試。

位圖(加分)

使用位圖getbit/setbit等操作對string類型進行處理。

指令

  • getbit
  • setbit
  • bitfield key get
  • bitfield key set
  • bitfield key incrby

應用場景

用戶簽到記錄

程序

public static void main(String[] args) {
    try {
        jedis = new Jedis("localhost", 6379);

        // 實現用戶的簽到記錄
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-DD");
        long beginTime = simpleDateFormat.parse("2020-01-01").getTime();
        // 位移,當天是當年的第幾天
        Long day = (System.currentTimeMillis() - beginTime) / 1000 * 60 * 60 * 24L;
        jedis.setbit("user1", day.intValue(), true);
        System.out.println(jedis.getbit("user1", day.intValue()));
    } catch (ParseException e) {
        e.printStackTrace();
    } finally {
        jedis.close();
    }
}

HyperLogLog(加分)

高級數據結構HyperLogLog,提供不精確的去重技術方案,標準誤差率在0.81%,佔用內存空間12KB。

指令

  • pfadd
  • pfcount
  • pfmerge

注意:在計數比較小時,採用稀疏矩陣存儲,空間佔用很小,當計數慢慢變大、稀疏矩陣佔用空間漸漸超過了閾值時,纔會一次性轉變爲稠密矩陣,纔會佔用12KB空間。

應用場景

記錄某個連接的UV

程序

public static void main(String[] args) throws InterruptedException {
	// 大致統計用戶數量
    try {
        jedis = new Jedis("localhost", 6379);
        for (int i = 0; i < 1000; i++) {
            jedis.pfadd("hyper1", "user" + i);
        }
        System.out.println(jedis.pfcount("hyper1"));

        for (int i = 0; i < 1000; i++) {
            jedis.pfadd("hyper2", "user" + (1000 + i));
        }
        System.out.println(jedis.pfcount("hyper2"));

        jedis.pfmerge("hyper3", "hyper1", "hyper2");
        System.out.println(jedis.pfcount("hyper3"));
    } finally {
        jedis.close();
    }
}

// 打印如下

1011
997
2002

布隆過濾器(重要)

高級數據結構布隆過濾器(Bloom Filter)。提供去重作用,在空間上能節省90%以上,存在一定的誤判概率。

注意:需要安裝插件,https://github.com/RedisBloom/RedisBloom;docker可以使用鏡像redislabs/rebloom

指令

  • bf.add key value
  • bf.exists key value
  • bf.madd key value1 value2
  • bf.exists key value1 value2
  • bf.reserve key error_rate(錯誤率,默認0.01) initial_size(默認100)

應用場景

數據去重
緩存穿透

程序

// 添加mavan依賴
<!-- https://mvnrepository.com/artifact/com.redislabs/jrebloom -->
<dependency>
    <groupId>com.redislabs</groupId>
    <artifactId>jrebloom</artifactId>
    <version>2.0.0-m1</version>
</dependency>
        
public static void main(String[] args) {
    try {
        jedis = new Jedis("localhost", 6379);

        // 設置布隆過濾器參數
        jedis.sendCommand(io.rebloom.client.Command.RESERVE, "bf", "0.01", "5");

        // 第一種場景:數據去重
        // 當使用過數據,添加到redis,通過exists來判斷是否存在:0爲不存在,1爲存在
        jedis.sendCommand(io.rebloom.client.Command.ADD, "bf", "user1");
        jedis.sendCommand(io.rebloom.client.Command.ADD, "bf", "user2");

        System.out.println(jedis.sendCommand(io.rebloom.client.Command.EXISTS, "bf", "user1"));
        System.out.println(jedis.sendCommand(io.rebloom.client.Command.EXISTS, "bf", "user3"));

        // 第二種場景:緩存穿透
        // 將所有緩存預熱到指定key中,存在則查詢,不存在則直接返回
        Set<String> keys = jedis.keys("*");
        keys.forEach(key->{
            jedis.sendCommand(Command.ADD, "all", key);
        });

        long result = (Long) jedis.sendCommand(io.rebloom.client.Command.EXISTS, "all", "user3");
        if(result == 1){
            System.out.println("執行緩存查詢");
        }else{
            System.out.println("直接返回");
        }


    } finally {
        jedis.close();
    }
}

// 日誌打印
1
0
直接返回

原理

通過錯誤率和初始大小,生成位數組的大小和hash函數數量,公式如下:

hash函數數量 = 0.7 * (位數組的大小 / 初始大小)
錯誤率 = 0.6185^(位數組的大小 / 初始大小)

布隆計算器:https://krisives.github.io/bloom-calculator
在這裏插入圖片描述

通過計算器可知,100個初始大小,錯誤率在1%的情況下,有7個hash函數和959長度的位數組,那麼具體的算法如下:

  1. 7個hash函數,分別爲h1,h2,h3,h4,h5,h6,h7
  2. 當添加一個value時
  3. 第一個位置:location1 = h1(value) % 959,將該位置設置爲1,(setbit key location1 1);
  4. 第二個位置:location2 = h2(value) % 959,將該位置設置爲1,(setbit key location2 1);
  5. 重複以上步驟,直到獲取第七個位置
  6. 在判斷是否存在指定value,也是類似的算法
  7. 第一個位置:location1 = h1(value) % 959,獲取位置1的值,(getbit key location1);如果不爲1則直接返回不存在
  8. 只有當七個位置都爲1的情況下,才表示該value是存在的

錯誤率的原因是因爲hash時,可能存在hash衝突

GeoHash(地理位置算法)

指令

  • geoadd key 經度 緯度 value1
  • 計算兩個元素之間的距離(指定單位):geodist key value1 value2 m/km/ml/ft
  • 獲取指定元素的經緯度:geopos key value1
  • 獲取指定元素的hash:geohash key value1
  • 查詢指定元素相關的元素:georadiusbymember key value1 x m/km/ml/ft [withcoord/withdist\withhash] count y asc/desc
  • 查詢指定經緯度相關的元素:georadius key 經度 緯度 x m/km/ml/ft [withcoord/withdist\withhash] count y asc/desc

withcoord ;withdist:顯示距離;withhash:顯示hash

程序

private static Jedis jedis;

public static void main(String[] args) {
    try {
        Jedis jedis = new Jedis("localhost", 6379);

        String key = "company";
        jedis.geoadd(key, 116.48105, 39.996794, "掘金");

        Map<String, GeoCoordinate> map = new HashMap<>(3);
        map.put("掌閱", new GeoCoordinate(116.514203, 39.905409));
        map.put("美團", new GeoCoordinate(116.489033, 40.007669));
        map.put("京東", new GeoCoordinate(116.562108, 39.9787602));

        jedis.geoadd(key, map);
        System.out.println("美團、京東距離:"+ jedis.geodist(key, "美團","京東", GeoUnit.M) + "米");

        // 範圍20公里內的三家公司
       List<GeoRadiusResponse> list = jedis.georadiusByMember(key, "京東", 20, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().count(3));
       list.forEach(geoRadiusResponse -> {
           System.out.println(geoRadiusResponse.getMemberByString() + geoRadiusResponse.getDistance());
       });

        List<GeoRadiusResponse> list2 = jedis.georadius(key, 116.562108, 39.9787602, 20, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().count(3));
        list2.forEach(geoRadiusResponse -> {
            System.out.println(geoRadiusResponse.getMemberByString() + geoRadiusResponse.getDistance());
        });
    } finally {
        jedis.close();
    }
}

// 日誌打印
美團、京東距離:7008.1463米
京東0.0
美團7.0081
掘金7.1929
京東2.0E-4
美團7.0082
掘金7.193
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章