1. 程式人生 > >.Net Core使用分散式快取Redis:資料結構

.Net Core使用分散式快取Redis:資料結構

一、前言

本篇主要使用StackExchangeRedis在.Net Core中使用Redis,使用基礎見:點選此處。

二、五種基礎資料結構

1.字串型別String

字串型別是Redis中最基本的資料型別,它能儲存任何形式的字串,包括二進位制資料。你可以用其儲存使用者的郵箱、JSON化的物件甚至是一張圖片。一個字串型別鍵允許儲存地得資料的最大容量是512MB。

字串型別是其他4種資料型別的基礎,其他資料型別和字串型別的差別從某種角度來說只是組織字串的形式不同。例如,列表型別是以列表的形式組織字串,二集合型別是以集合的形式組織字串。

以下為StackExchangeRedis中字串常用方法及其命令:

(1)賦值與取值

//方法
redisConnection.GetDatabase().StringSetAsync(key, value);
redisConnection.GetDatabase().StringGetAsync(key); 
//命令
127.0.0.1:6379> set stringKey stringValue
OK
127.0.0.1:6379> get stringKey
"stringValue"

(2)返回 key 中字串值的子字元

//方法
redisConnection.GetDatabase().StringGetRangeAsync(key, start, end);
//命令
127.0.0.1:6379> getrange stringKey 6 10
"Value"

(3)將給定 key 的值設為 value ,並返回 key 的舊值(old value)

//方法
var oldvalue = await redisConnection.GetDatabase().StringGetSetAsync(key, oldkey);
//命令
127.0.0.1:6379> getset stringKey newValue
"stringValue"

(4)如果 key 已經存在並且是一個字串, APPEND 命令將指定的 value 追加到該 key 原來值(value)的末尾

//方法
redisConnection.GetDatabase().StringAppendAsync(key, appendValue);
//命令
127.0.0.1:6379> append stringKey append
(integer) 14

2.雜湊型別Hash

Redis是採用字典結構以鍵值對的形式儲存資料的,而Hash的鍵值也是一種字典結構,其儲存了欄位(field)和欄位值的對映,但欄位值只能是字串,不支援其他資料型別,不能巢狀其他資料型別(其他資料型別同理)。

雜湊型別適合儲存物件:使用物件類別和ID構成鍵名,使用欄位表示物件的屬性,而欄位值則儲存屬性值。例如要儲存ID為2的汽車物件,可以分別使用名為color、name和price的三個欄位來儲存該輛汽車的顏色、名稱和價格。

以下為StackExchangeRedis中雜湊型別常用方法及其命令:

(1)賦值

//方法
HashEntry[] hashEntry = new HashEntry[] {
  new HashEntry("id",2),
  new HashEntry("color","red"),
  new HashEntry("price",200),
};
redisConnection.GetDatabase().HashSetAsync("youCar", hashEntry);
//命令
127.0.0.1:6379> hset myCar price 200
(integer) 1
127.0.0.1:6379> hset mCar color blue
(integer) 1
127.0.0.1:6379> hset mCar name tractor
(integer) 1

(2)取值

//方法
redisConnection.GetDatabase().HashGetAsync("youCar", "color");
redisConnection.GetDatabase().HashGetAllAsync("youCar");
//命令
127.0.0.1:6379> hget youCar color
"red"
127.0.0.1:6379> hgetall youCar
1) "id"
2) "2"
3) "color"
4) "red"
5) "price"
6) "200"

(3)獲取所有雜湊型別中的欄位

//方法
var keys = redisConnection.GetDatabase().HashKeys("youCar");
//命令
127.0.0.1:6379> hkeys youCar
1) "id"
2) "color"
3) "price"

3.列表List

列表型別(List)可以儲存一個有序的字串列表,常用的操作是向列表兩端新增元素,或者獲得列表的一個片段。

列表型別內部是使用雙向連結串列(double linked list)實現的,所以向列表兩段新增元素的時間複雜都為O(1),獲取越接近兩端的元素速度就越快。這意味著即使是一個由幾千萬個元素的列表,獲取頭部或者尾部的10條記錄也是極快的。

不過使用連結串列的代價是通過索引訪問元素比較慢。於是,列表型別很適合於如社交網站的新鮮事、記錄日誌、訊息佇列等,類似獲取最前幾條的資料、從列表尾部插入等場景。

以下為StackExchangeRedis中列表型別常用方法及其命令:

(1)向列表兩端增加元素

//方法
redisConnection.GetDatabase().ListLeftPushAsync("myList","head1");
redisConnection.GetDatabase().ListRightPush("myList","bottom1");
//命令
127.0.0.1:6379> lpush myList head1
(integer) 1127.0.0.1:6379> rpush myList bottom1
(integer) 2

(2)從列表兩端移除並獲取一個元素

//方法
var value = redisConnection.GetDatabase().ListLeftPopAsync("myList");
var value = redisConnection.GetDatabase().ListRightPopAsync("myList");
//命令
127.0.0.1:6379> lpop myList
"head1"
127.0.0.1:6379> rpop myList
"bottom1"
127.0.0.1:6379> rpop myList
(nil)

(3)獲取列表中的元素片段(不刪除只獲取)

//方法
var value = redisConnection.GetDatabase().ListRangeAsync("myList",0,2);
//命令
127.0.0.1:6379> lrange myList 0 2
1) "1"
2) "2"
3) "3"

4.集合Set

Redis 的 Set 是 String 型別的無序集合。集合成員是唯一的,這就意味著集合中不能出現重複的資料。常用的操作是向集合加入或者刪除元素、判斷某個元素是否存在等,由於Redis 中集合是通過雜湊表實現的,所以新增,刪除,查詢的複雜度都是 O(1)。

最方便的是多個集合型別鍵之間還可以進行並集、交集和差集運算。

可以運用在共同好友、喜好和好友推薦(交集超過閾值)等可以運用交差並集操作的場景。

以下為StackExchangeRedis中集合型別常用方法及其命令:

(1)增加/刪除元素(可多個,相同元素自動忽略)

//方法
redisConnection.GetDatabase().SetAddAsync("mySet", new RedisValue[] { "a", "b" });
redisConnection.GetDatabase().SetRemoveAsync("mySet", "a");
//命令
127.0.0.1:6379> sadd mySet a
(integer) 1
127.0.0.1:6379> sadd mySet a b c
(integer) 2
127.0.0.1:6379> srem mySet c
(integer) 1
127.0.0.1:6379> srem mySet b a
(integer) 2

(2)獲取交集

//方法
var value = redisConnection.GetDatabase().SetCombine(SetOperation.Intersect, "mySet1", "mySet2");
//命令
127.0.0.1:6379> sadd mySet1 a b c
(integer) 3
127.0.0.1:6379> sadd mySet2 b c d
(integer) 3
127.0.0.1:6379> sinter mySet1 mySet2
1) "c"
2) "b"

5.有序集合Sort Set

從有序集合的名字就可以看出它和集合的區別就是”有序“二字。

在集合型別的基礎尚有序集合型別為集合中的每個元素都關聯了一個分數,按分數大小進行從小到大的排序,這使得我們不僅可以完成插入、刪除和判斷元素是否在等集合型別支援的操作,還能夠獲得分數最高(或最低)的前N個元素、獲得指定分數範圍內的元素等與分數有關的操作。雖然集合中每個元素都是不同的,但是它們的分數卻可以相同。

有序集合型別在某方面和列表型別有些相似

  • 二者都是有序。

  • 二者都可以獲取某一範圍的元素

但是二者由這很大的區別,這使得他們的應用場景有所不同。

  • 列表是通過連結串列實現的,獲取靠近兩端的資料速度極快,而當元素增多後,訪問中間元素的速度會較慢,所以它更加適合實現如“新鮮事”或“日誌”這樣很少訪問中間元素的應用。

  • 有序集合是使用散列表和跳錶(skip list)實現的,所以即使讀取文娛中間部分的資料速度也快(時間複雜度是O(log(N)))。

  • 列表中不能簡單地調整元素的位置,但有序集合可以(通過更改元素的分數),所以可以用於排行榜、權重應用(有權重的訊息佇列)。

  • 有序集合要比類別更耗記憶體。

以下為StackExchangeRedis中有序集合型別常用方法及其命令:

(1)增加元素

//方法
SortedSetEntry[] sortedSetEntry = new SortedSetEntry[] {
  new SortedSetEntry("tom",10),
  new SortedSetEntry("peter",20),
  new SortedSetEntry("david",30),
};
redisConnection.GetDatabase().SortedSetAddAsync("mySs", sortedSetEntry);
//命令
127.0.0.1:6379> zadd mySs 10 tom 20 peter 30 david
(integer) 3

(2)獲取排名在某個分數範圍的元素列表

//方法
var value = redisConnection.GetDatabase().SortedSetRemoveRangeByScoreAsync("mySs", 10, 20);
//命令
127.0.0.1:6379> zrangebyscore mySs 10 20
1) "tom"
2) "peter"

(3)增加某個元素的分數

//方法
var sorce = redisConnection.GetDatabase().SortedSetIncrementAsync("mySs", "top", 3);
//命令
127.0.0.1:6379> zincrby mySs 3 tom
"13"

6.小結

上面各型別所舉得例子有限,基本所有的Redis命令在StackExchangeRedis中都有相應的非同步和同步的方法,大家可以參考https://redis.io/commands。

三、其他資料結構

1.HyperLogLog

Redis 在 2.8.9 版本添加了 HyperLogLog 結構(其本身也是一種演算法)。

Redis HyperLogLog 是用來做基數統計的演算法,HyperLogLog 的優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的空間總是固定 的、並且是很小的。

在 Redis 裡面,每個 HyperLogLog 鍵只需要花費 12 KB 記憶體,就可以計算接近 2^64 個不同元素的基 數。這和計算基數時,元素越多耗費記憶體就越多的集合形成鮮明對比。

但是,因為 HyperLogLog 只會根據輸入元素來計算基數,而不會儲存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個元素。

所以主要應用於:

  • 統計訪問IP數量;

  • 統計搜尋關鍵詞數量;

  • 統計使用者線上數;

  • 等等大數量統計。

以下為StackExchangeRedis中常用方法及其命令:

(1)新增指定元素到 HyperLogLog 中

//方法
RedisValue[] redisValue = new RedisValue[] {
    110,120,130,130
};
redisConnection.GetDatabase().HyperLogLogAddAsync("key", redisValue);
//命令
127.0.0.1:6379> pfadd hyperlogKy 110 120 130 130
(integer) 1

(2)返回給定 HyperLogLog 的基數估算值。

//方法
var count = redisConnection.GetDatabase().HyperLogLogLength("key");
//命令
127.0.0.1:6379> pfcount hyperlogKy
(integer) 3

2.geo

Redis在3.2版本之後新增了一個geo(地理位置),其資料結構為有序集合sort set。

geo可以將地理位置資訊(經緯度)儲存起來,並計算兩個地理座標之間的位置、返回以指定位置為圓心指定半徑內的所有地理位置資訊等。

像是我們平時的打車、租房地圖等功能中就可以用到。

既然前面說到geo是有序集合那它的score怎麼來?是我們自己輸入嘛?其實是通過我們儲存的經緯度通過geohash計算後的base32編碼字串。geohash原理點選此處。

以下為StackExchangeRedis中常用方法及其命令:

(1)新增地理位置

//方法
GeoEntry[] geoEntry = new GeoEntry[] {
  new GeoEntry(120.20000, 30.26667, "hangzhou"),
  new GeoEntry(116.41667, 39.91667, "beijing"),
  new GeoEntry(121.47, 31.23, "shanghai"),
};
redisConnection.GetDatabase().GeoAdd("city", geoEntry);
//命令
127.0.0.1:6379> geoadd city 120.20000 30.26667 hangzhou  116.41667 39.91667 beijing 121.47 31.23 shanghai
(integer) 3

(2)獲取geohash

//方法
var geohash = redisConnection.GetDatabase().GeoHash("city", "hanghzou");
//命令
127.0.0.1:6379> geohash city hangzhou
1) "wtmkpjyuph0"

(3)獲取指定元素範圍的地理資訊位置集合

//方法
var geo = redisConnection.GetDatabase().GeoRadius("city", "hanghzou", 300, GeoUnit.Kilometers, 10, Order.Ascending, GeoRadiusOptions.WithGeoHash);
//命令
127.0.0.1:6379> georadiusbymember city hangzhou 300 km withcoord withdist withhash asc count 10
1) 1) "hangzhou"
   2) "0.0000"
   3) (integer) 4054134205316048
   4) 1) "120.20000249147415"
      2) "30.266670658987586"
2) 1) "shanghai"
   2) "161.9183"
   3) (integer) 4054803462927619
   4) 1) "121.47000163793564"
      2) "31.229999039757836"

在給定以下可選項時, 命令會返回額外的資訊:

  • WITHDIST : 在返回位置元素的同時, 將位置元素與中心之間的距離也一併返回。 距離的單位和使用者給定的範圍單位保持一致。

  • WITHCOORD : 將位置元素的經度和維度也一併返回。

  • WITHHASH : 以 52 位有符號整數的形式, 返回位置元素經過原始 geohash 編碼的有序集合分值。

  • ASC : 根據中心的位置, 按照從近到遠的方式返回位置元素。DESC : 根據中心的位置, 按照從遠到近的方式返回位置元素。

它還有個類似的georadius命令,區別是由給定的經緯度為圓心。

(4)計算兩個位置之間的距離
//方法
var geo = redisConnection.GetDatabase().GeoDistance("city", "hanghzou","beijing");
//命令
127.0.0.1:6379> geodist city hangzhou beijing km
"1126.8937"

3.釋出訂閱pub/sub 

"釋出/訂閱"模式中包含兩種角色,分別是釋出者和訂閱者。訂閱者可以訂閱一個或者若干個頻道(channel),而釋出者可以向指定的頻道傳送訊息,所有訂閱此頻道的訂閱者都會收到此訊息。

釋出者通過publish命令傳送訊息:

//命令
127.0.0.1:6379> publish channel_1 hi
(integer) 0
//方法
var count = redisConnection.GetSubscriber().Publish("channel_1", "hi");

訊息是傳送出去了,publish命令的返回值表示接收到這條訊息的訂閱者數量。因為當前沒有訂閱這訂閱這個頻道,所以返回0。

注意,傳送出去的訊息是不會持久化的,訂閱者只能收到訂閱之後釋出者傳送的訊息。

訂閱者通過subscribe命令訂閱一個或者多個頻道:

//命令
127.0.0.1:6379> subscribe channel_1 channel_2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel_1"
3) (integer) 1
1) "subscribe"
2) "channel_2"
3) (integer) 2
1) "message"
2) "channel_1"
3) "hi"
//方法
while (true)
{
  redisConnection.GetSubscriber().Subscribe("channel_1", (channel, message) =>
  {
    var msg = message;//收到的訊息
    var chan = channel;//頻道名稱
  });
}

執行subscribe命令之後進入訂閱狀態,可能收到三種類型的回覆。每種型別的回覆都包含三個值,第一個值是訊息的型別,根據訊息型別的不同,第二、三個值得含義也不同。訊息型別可能得取值有以下三個:

  • subscribe:表示訂閱成功得反饋資訊。第二個值是訂閱成功得頻道名稱,第三個值是當前客戶端訂閱得頻道數量。

  • message:這個型別得回覆是我們最關心得,它表示接收到得訊息。第二個值表示禪師訊息的頻道名稱,第三個值是訊息的內容。

  • unsubscribe:表示成功取消訂閱某個頻道。第二個值是對應頻道的名稱,第三個值是當前客戶端訂閱的頻道數量。

StackExchangeRedis中用到message和unsubscribe這兩種。

 除了通過頻道名之外,還可以使用psubscribe命令通過規則訂閱頻道。如約定規則為channel_?*則可以匹配channel_為開頭的頻道,如channel_1、channel_2等。

//命令
127.0.0.1:6379> psubscribe cahnnel_?*
Reading messages... (press Ctrl-C to quit)
(integer) 1
1) "psubscribe"
2) "cahnnel_?*"
3) (integer) 1
//方法
while (true)
{
  redisConnection.GetSubscriber().Subscribe("channel_?*", (channel, message) =>
  {
    var msg = message;//收到的訊息
    var chan = channel;//頻道名稱
  });
}

解除訂閱通過命令unsubscribe,如果不指定頻道則取消全部訂閱。在StackExchangeRedis中分別時方法Unsubscribe()和UnsubscribeAll()。