Redis
是開源,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。它支持多種類型的數據結構,如字符串 strings,散列 hashes,列表 lists,集合 sets,有序集合 sorted sets 與範圍查詢, bitmaps,hyperloglogs 和 地理空間(geospatial)索引半徑查詢。
Redis 還內置了複製(replication),LUA腳本(Lua>
緩存 + 拆分
隨著訪問量的上升,一個數據庫已經不能滿足我們的需求了。為了更高的性能,我們在中間加上了一個緩存層並且將數據庫做了集群、結構優化和讀寫分離。
這裡的緩存就是 NoSQL,當然做緩存也只是 NoSQL 的一種功能,就像 Redis 並不僅僅有緩存這一種功能。比如它還能實現 簡單的消息隊列,解決Session共享,計數器,排行榜,好友關係處理 等等功能。
Redis 通用命令
keys
172.0.0.1:6379> set test1 hello
127.0.0.1:6379> set test2 world
127.0.0.1:6379> keys *test*
1) "test2"
2) "test1"
dbsize計算key的總數,這是redis內置的一個計算器,時間複雜度為O(1)。
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 k4 v4
OK
127.0.0.1:6379> dbsize
(integer) 4
127.0.0.1:6379> sadd myset a b c d e f g
(integer) 7
127.0.0.1:6379> dbsize
(integer) 5
exists檢查key是否存在,存在返回1,不存在返回0
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> exists a
(integer) 1
127.0.0.1:6379> del a
(integer) 1
127.0.0.1:6379> exists a
(integer) 0
del刪除指定key-value,可以同時刪除多個,刪除成功返回刪除的key個數,如果不存在返回0,時間複雜度為O(1)。
127.0.0.1:6379> mset a b test1 hello test2 world
OK
127.0.0.1:6379> del a test1 test2
(integer) 3
127.0.0.1:6379> del a test1 test2
(integer) 0
expireexpire key seconds 設置key在seconds秒後過期,時間複雜度為O(1)。
127.0.0.1:6379> set test1 hello
OK
127.0.0.1:6379> expire test1 20
(integer) 1
ttlttl key 查看key剩餘的過期時間,時間複雜度為O(1)。
127.0.0.1:6379> ttl test1
(integer) 13
127.0.0.1:6379> ttl test1
(integer) 8
127.0.0.1:6379> ttl test1
(integer) -2
# -2代表key已經不存在了
# 過期後再去查看key值,發現已經不存在了
127.0.0.1:6379> get test1
(nil)
persistpersist key 去掉key的過期時間,時間複雜度為O(1)。
127.0.0.1:6379> set test1 hello
OK
127.0.0.1:6379> expire test1 60
(integer) 1
127.0.0.1:6379> ttl test1
(integer) 56
# 去掉過期時間
127.0.0.1:6379> persist test1
(integer) 1
127.0.0.1:6379> ttl test1
(integer) -1
# -1代表key存在,但是沒有過期時間
# 去掉過期時間後即便過期了也沒有被刪除
127.0.0.1:6379> get test1
"hello"
typetype key 返回key的類型,redis有5種數據結構,所以返回值有string、hash、list、set、zset,如果key不存在的,則返回none,時間複雜度為O(1)。
127.0.0.1:6379> type myset
none
127.0.0.1:6379> sadd myset a b c d e
(integer) 5
127.0.0.1:6379> type myset
set
Redis 的五種基本數據類型
Redis 作為緩存能實現的其他功能,比如計數器,排行榜,好友關係等,這些實現的依據就是靠著 Redis 的數據結構。在整個 Redis 中一共有五種基本的數據結構,他們分別是 字符串strings,散列 hashes,列表 lists,集合 sets,有序集合 sorted sets。
- String(字符串)
string 是 redis 最基本的類型,你可以理解成與 Memcached 一模一樣的類型,一個 key 對應一個 value。
string 類型是二進制安全的。意思是 redis 的 string 可以包含任何數據。比如jpg圖片或者序列化的對象。
string 類型是 Redis 最基本的數據類型,string 類型的值最大能存儲 512MB。
常用命令:set、get、decr、incr、mget等。
注意:一個鍵最大能存儲512MB。
- Hash(哈希)
Redis hash 是一個鍵值(key=>value)對集合;是一個 string 類型的 field 和 value 的映射表,hash 特別適合用於存儲對象。
每個 hash 可以存儲 232 -1 鍵值對(40多億)。
常用命令:hget、hset、hgetall等。
應用場景:存儲一些結構化的數據,比如用戶的暱稱、年齡、性別、積分等,存儲一個用戶信息對象數據。
- List(列表)
Redis 列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素到列表的頭部(左邊)或者尾部(右邊)。
list類型經常會被用於消息隊列的服務,以完成多程序之間的消息交換。
常用命令:lpush、rpush、lpop、rpop、lrange等。
列表最多可存儲 232 - 1 元素 (4294967295, 每個列表可存儲40多億)。
- Set(集合)
Redis的Set是string類型的無序集合。和列表一樣,在執行插入和刪除和判斷是否存在某元素時,效率是很高的。集合最大的優勢在於可以進行交集並集差集操作。Set可包含的最大元素數量是4294967295。集合是通過哈希表實現的,所以添加,刪除,查找的複雜度都是O(1)。
應用場景:
1、利用交集求共同好友。
2、利用唯一性,可以統計訪問網站的所有獨立IP。
3、好友推薦的時候根據tag求交集,大於某個threshold(臨界值的)就可以推薦。
常用命令:sadd、spop、smembers、sunion等。
集合中最大的成員數為 232 - 1(4294967295, 每個集合可存儲40多億個成員)。
- zset(sorted set:有序集合)
Redis zset 和 set 一樣也是string類型元素的集合,且不允許重複的成員。
不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來為集合中的成員進行從小到大的排序。
zset的成員是唯一的,但分數(score)卻可以重複。
- sorted set是插入有序的,即自動排序。
常用命令:zadd、zrange、zrem、zcard等。
當你需要一個有序的並且不重複的集合列表時,那麼可以選擇sorted set數據結構。
應用舉例:
(1)例如存儲全班同學的成績,其集合value可以是同學的學號,而score就可以是成績。
(2)排行榜應用,根據得分列出topN的用戶等。
redis存儲文件格式
使用了兩種文件格式:全量數據和增量請求。
全量數據格式是把內存中的數據寫入磁盤,便於下次讀取文件進行加載;
增量請求文件則是把內存中的數據序列化為操作請求,用於讀取文件進行replay得到數據,序列化的操作包括SET、RPUSH、SADD、ZADD。
redis的存儲分為內存存儲、磁盤存儲和log文件三部分,配置文件中有三個參數對其進行配置。
save seconds updates,save配置,指出在多長時間內,有多少次更新操作,就將數據同步到數據文件。這個可以多個條件配合,比如默認配置文件中的設置,就設置了三個條件。
appendonly yes/no ,appendonly配置,指出是否在每次更新操作後進行日誌記錄,如果不開啟,可能會在斷電時導致一段時間內的數據丟失。因為redis本身同步數據文件是按上面的save條件來同步的,所以有的數據會在一段時間內只存在於內存中。
appendfsync no/always/everysec ,appendfsync配置,no表示等操作系統進行數據緩存同步到磁盤,always表示每次更新操作後手動調用fsync()將數據寫到磁盤,everysec表示每秒同步一次。
Redis分佈式鎖
- 分佈式鎖在很多場景中是非常有用的原語, 不同的進程必須以獨佔資源的方式實現資源共享就是一個典型的例子。
有很多分佈式鎖的庫和描述怎麼實現分佈式鎖管理器(DLM)的博客,但是每個庫的實現方式都不太一樣,很多庫的實現方式為了簡單降低了可靠性,而有的使用了稍微複雜的設計。
這個頁面試圖提供一個使用Redis實現分佈式鎖的規範算法。我們提出一種算法,叫Redlock,我們認為這種實現比普通的單實例實現更安全,請關注redis社區。
獲取鎖的時候,使用set加鎖,使用expire命令為鎖添加一個超時時間,鎖的value值為一個隨機生成的uuid。
獲取鎖的時候還設置一個獲取超時的時間,若超過時間則放棄獲取鎖。
釋放鎖判斷是否是此uuid 如果是則刪除,釋放鎖。
優點:很多緩存服務都是集群部署避免單點問題,提供了很多實現分佈式鎖的方法。性能好實現起來方便。
缺點:通過超時時間來控制鎖的失效時間不是十分靠譜。
Redis 大量數據插入
有些時候,Redis實例需要裝載大量用戶在短時間內產生的數據,數以百萬計的keys需要被快速的創建。我們稱之為大量數據插入(mass insertion)。
使用Luke協議,正常模式的Redis 客戶端執行大量數據插入不是一個好主意:因為一個個的插入會有大量的時間浪費在每一個命令往返時間上。使用管道(pipelining)是一種可行的辦法,但是在大量插入數據的同時又需要執行其他新命令時,這時讀取數據的同時需要確保請可能快的的寫入數據。
只有一小部分的客戶端支持非阻塞輸入/輸出(non-blocking I/O),並且並不是所有客戶端能以最大限度的提高吞吐量的高效的方式來分析答覆。
分區:怎樣將數據分佈到多個redis實例
分區是將你的數據分發到不同redis實例上的一個過程,每個redis實例只是你所有key的一個子集。文檔第一部分將介紹分區概念,第二部分介紹分區的另外一種可選方案。
Redis分區主要有兩個目的:
- 分區可以讓Redis管理更大的內存,Redis將可以使用所有機器的內存。如果沒有分區,你最多隻能使用一臺機器的內存。
- 分區使Redis的計算能力通過簡單地增加計算機得到成倍提升,Redis的網絡帶寬也會隨著計算機和網卡的增加而成倍增長。
分區基本概念
有許多分區標準。假如我們有4個Redis實例R0, R1, R2, R3 ,有一批用戶數據user:1, user:2, … ,那麼有很多存儲方案可以選擇。從另一方面說,有很多different systems to map方案可以決定用戶映射到哪個Redis實例。
一種最簡單的方法就是範圍分區,就是將不同範圍的對象映射到不同Redis實例。比如說,用戶ID從0到10000的都被存儲到R0,用戶ID從10001到20000被存儲到R1,依此類推。
這是一種可行方案並且很多人已經在使用。但是這種方案也有缺點,你需要建一張表存儲數據到redis實例的映射關係。這張表需要非常謹慎地維護並且需要為每一類對象建立映射關係,所以redis範圍分區通常並不像你想象的那樣運行,比另外一種分區方案效率要低很多。
另一種可選的範圍分區方案是散列分區,這種方案要求更低,不需要key必須是object_name:
- 使用散列函數 (如 crc32 )將鍵名稱轉換為一個數字。例:鍵foobar, 使用crc32(foobar)函數將產生散列值93024922。
- 對轉換後的散列值進行取模,以產生一個0到3的數字,以便可以使這個key映射到4個Redis實例當中的一個。93024922 % 4 等於 2, 所以 foobar 會被存儲到第2個Redis實例。 R2 注意: 對一個數字進行取模,在大多數編程語言中是使用運算符%
還有很多分區方法,上面只是給出了兩個簡單示例。有一種比較高級的散列分區方法叫一致性哈希,並且有一些客戶端和代理(proxies)已經實現。
不同的分區實現方案
分區可以在程序的不同層次實現。
- 客戶端分區就是在客戶端就已經決定數據會被存儲到哪個redis節點或者從哪個redis節點讀取。大多數客戶端已經實現了客戶端分區。
- 代理分區 意味著客戶端將請求發送給代理,然後代理決定去哪個節點寫數據或者讀數據。代理根據分區規則決定請求哪些Redis實例,然後根據Redis的響應結果返回給客戶端。redis和memcached的一種代理實現就是Twemproxy
- 查詢路由(Query routing) 的意思是客戶端隨機地請求任意一個redis實例,然後由Redis將請求轉發給正確的Redis節點。Redis Cluster實現了一種混合形式的查詢路由,但並不是直接將請求從一個redis節點轉發到另一個redis節點,而是在客戶端的幫助下直接redirected到正確的redis節點。
訂閱發佈 Pub/Sub
訂閱,取消訂閱和發佈實現了發佈/訂閱消息範式(引自wikipedia),發送者(發佈者)不是計劃發送消息給特定的接收者(訂閱者)。而是發佈的消息分到不同的頻道,不需要知道什麼樣的訂閱者訂閱。訂閱者對一個或多個頻道感興趣,只需接收感興趣的消息,不需要知道什麼樣的發佈者發佈的。這種發佈者和訂閱者的解耦合可以帶來更大的擴展性和更加動態的網絡拓撲。
為了訂閱foo和bar,客戶端發出一個訂閱的頻道名稱:
SUBSCRIBE foo bar
其他客戶端發到這些頻道的消息將會被推送到所有訂閱的客戶端。
客戶端訂閱到一個或多個頻道不必發出命令,儘管他能訂閱和取消訂閱其他頻道。訂閱和取消訂閱的響應被封裝在發送的消息中,以便客戶端只需要讀一個連續的消息流,其中第一個元素表示消息類型。
推送消息的格式-消息是一個有三個元素的多塊響應 。
第一個元素是消息類型:
- subscribe: 表示我們成功訂閱到響應的第二個元素提供的頻道。第三個參數代表我們現在訂閱的頻道的數量。
- unsubscribe:表示我們成功取消訂閱到響應的第二個元素提供的頻道。第三個參數代表我們目前訂閱的頻道的數量。當最後一個參數是0的時候,我們不再訂閱到任何頻道。當我們在Pub/Sub以外狀態,客戶端可以發出任何redis命令。
- message: 這是另外一個客戶端發出的發佈命令的結果。第二個元素是來源頻道的名稱,第三個參數是實際消息的內容。
數據庫與作用域
發佈/訂閱與key所在空間沒有關係,它不會受任何級別的干擾,包括不同數據庫編碼。 發佈在db 10,訂閱可以在db 1。 如果你需要區分某些頻道,可以通過在頻道名稱前面加上所在環境的名稱(例如:測試環境,演示環境,線上環境等)。
模式匹配訂閱
Redis 的Pub/Sub實現支持模式匹配。客戶端可以訂閱全風格的模式以便接收所有來自能匹配到給定模式的頻道的消息。
redis架構
redis使用場景
1 會話緩存
最常用的一種使用Redis的情景是會話緩存(session cache)。用Redis緩存會話比其他存儲(如Memcached)的優勢在於:Redis提供持久化。
2 全頁緩存
除基本的會話token之外,Redis還提供很簡便的FPC平臺。回到一致性問題,即使重啟了Redis實例,因為有磁盤的持久化,用戶也不會看到頁面加載速度的下降,類似PHP本地FPC。
3 隊列
Reids在內存存儲引擎領域的一大優點是提供 list 和 set 操作,這使得Redis能作為一個很好的消息隊列平臺來使用。Redis作為隊列使用的操作,就類似於本地程序語言(Python)對 list 的 push/pop 操作。
4 排行榜/計數器
Redis在內存中對數字進行遞增或遞減的操作實現的非常好。
集合(Set)和有序集合(Sorted Set)也使得我們在執行這些操作的時候變的非常簡單,Redis只是正好提供了這兩種數據結構。
5 發佈/訂閱
Redis的發佈/訂閱功能。發佈/訂閱的使用場景確實非常多。
常考-Redis集群
哨兵模式Sentinel,它是為了解決Redis集群的高可用應運而生的。Redis集群,一般來說只會有一個master服務,當master掛掉之後,之能手動切換master就會造成一段時間內的服務不可用。sentinel就是專門針對這種情況而產生的一個監聽服務。它主要負責監聽我們一個組內所有Redis,當我們的master掛掉之後,它就會根據Raft算法進行選舉一臺新的leader出來,然後將選舉出來的leader當成我們的master。
Redis使用實例
要想在Java中連接Redis,並進行操作,由兩種方式,一種是spring data redis,它是由spring集成的,不支持集群,一種是官方推薦的jedis,支持集群。
- 下載jedis-2.7.3.jar
- 創建redis.properties配置文件:
redis.host=127.0.0.1
redis.port=6379
redis.password=
redis.timeout=100000
redis.maxIdle=100
redis.maxActive=300
redis.maxWait=1000
redis.testOnBorrow=true
- Redis工具類:
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import redis.clients.jedis.Jedis;
/**
* 操作redis緩存的工具類,簡單的demo,方便入門初步理解redis
*
* @author Xuan
*
*/
public final class RedisUtil {
public RedisUtil() {
}
// 靜態代碼塊
static {
// 加載properties配置文件
Properties properties = new Properties();
InputStream is = RedisUtil.class.getClassLoader().getResourceAsStream(
"redis.properties");
try {
properties.load(is);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
host = properties.getProperty("redis.host");
port = properties.getProperty("redis.port");
password = properties.getProperty("redis.password");
timeout = properties.getProperty("redis.timeout");
maxIdle = properties.getProperty("redis.maxIdle");
maxActive = properties.getProperty("redis.maxActive");
maxWait = properties.getProperty("redis.maxWait");
testOnBorrow = properties.getProperty("redis.testOnBorrow");
// 得到Jedis實例並且設置配置
jedisxuan = new Jedis(host, Integer.parseInt(port),
Integer.parseInt(timeout));
}
/**
* 寫入緩存
*
* @param key
* @param value
* @return
*/
public static boolean set(final String key, String value) {
boolean result = false;
try {
jedisxuan.set(key, value);
result = true;
} catch (Exception e) {
System.out.println("set cache error");
}
return result;
}
/**
* 讀取緩存
*
* @param key
* @return
*/
public static Object get(final String key) {
Object result = null;
result = jedisxuan.get(key);
return result;
}
/**
* 刪除key對應的value
*
* @param key
*/
public static void remove(final String key) {
if (key != null && key.length() >= 1 && !key.equals("")
&& jedisxuan.exists(key)) {
jedisxuan.del(key);
}
}
/**
* 判斷緩存中是否有key對應的value
*
* @param key
* @return
*/
public static boolean exists(final String key) {
return jedisxuan.exists(key);
}
/**
* 寫入緩存(規定緩存時間)
*
* @param key
* @param value
* @param expireSecond
* @return
*/
public static boolean set(final String key, String value, Long expireSecond) {
boolean result = false;
try {
// NX代表不存在才set,EX代表秒,NX代表毫秒
jedisxuan.set(key, value, "NX", "EX", expireSecond);
result = true;
} catch (Exception e) {
System.out.println("set cache error");
}
return result;
}
}
- 使用主方法
/**
* 測試類
* @author Xuan
*
*/
public class test {
/**
* @param args
*/
public static void main(String[] args) {
// 寫入一個緩存
boolean flag = RedisUtil.set("x", "007");
if (flag) {
// 讀取緩存
System.out.println("成功寫入緩存");
System.out.println("正在讀取緩存......");
String leo = String.valueOf(RedisUtil.get("x"));
System.out.println("你讀取的緩存為:" + leo);
} else {
System.out.println("寫入緩存失敗");
}
//寫入一個帶時間的緩存 30秒消失
//可以自己去驗證是否正確
boolean flag1 = RedisUtil.set("LEO", "時間緩存測試~",Long.parseLong("30"));
if (flag){
System.out.println("寫入成功");
}
}
}
閱讀更多 天天面試題 的文章