REmote DIctionary Server(Redis) 是一個由Salvatore Sanfilippo寫的key-value存儲系統。
Redis是一個開源的使用ANSI C語言編寫、遵守BSD協議、支持網絡、可基於內存亦可持久化的日志型、Key-Value數據庫,並提供多種語言的API。
它通常被稱為數據結構服務器,因為值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等類型。
Redis開創了一種新的數據存儲思路,使用Redis,我們不用在面對功能單調的數據庫時,把精力放在如何把大象放進冰箱這樣的問題上,而是利用Redis靈活多變的數據結構和數據操作,為不同的大象構建不同的冰箱。
Redis最為常用的數據類型主要有以下五種:
String
Hash
List
Set
Sorted set
在具體描述這幾種數據類型之前,我們先通過一張圖了解下Redis內部內存管理中是如何描述這些不同數據類型的:
首先Redis內部使用一個redisObject對象來表示所有的key和value,redisObject最主要的信息如上圖所示:type代表一個value對象具體是何種數據類型,encoding是不同數據類型在redis內部的存儲方式,比如:type=string代表value存儲的是一個普通字符串,那麼對應的encoding可以是raw或者是int,如果是int則代表實際redis內部是按數值型類存儲和表示這個字符串的,當然前提是這個字符串本身可以用數值表示,比如:"123" "456"這樣的字符串。
這裡需要特殊說明一下vm字段,只有打開了Redis的虛擬內存功能,此字段才會真正的分配內存,該功能默認是關閉狀態的,該功能會在後面具體描述。通過上圖我們可以發現Redis使用redisObject來表示所有的key/value數據是比較浪費內存的,當然這些內存管理成本的付出主要也是為了給Redis不同數據類型提供一個統一的管理接口,實際作者也提供了多種方法幫助我們盡量節省內存使用,我們隨後會具體討論。
下面我們先來逐一的分析下這五種數據類型的使用和內部實現方式:
- String
常用命令:
set,get,decr,incr,mget 等。
應用場景:
String是最常用的一種數據類型,普通的key/value存儲都可以歸為此類,這裡就不所做解釋了。
實現方式:
String在redis內部存儲默認就是一個字符串,被redisObject所引用,當遇到incr,decr等操作時會轉成數值型進行計算,此時redisObject的encoding字段為int。
- Hash
常用命令:
hget,hset,hgetall 等。
應用場景:
我們簡單舉個實例來描述下Hash的應用場景,比如我們要存儲一個用戶信息對象數據,包含以下信息:
用戶ID為查找的key,存儲的value用戶對象包含姓名,年齡,生日等信息,如果用普通的key/value結構來存儲,主要有以下2種存儲方式:
第一種方式將用戶ID作為查找key,把其他信息封裝成一個對象以序列化的方式存儲,這種方式的缺點是,增加了序列化/反序列化的開銷,並且在需要修改其中一項信息時,需要把整個對象取回,並且修改操作需要對並發進行保護,引入CAS等復雜問題。
第二種方法是這個用戶信息對象有多少成員就存成多少個key-value對兒,用用戶ID+對應屬性的名稱作為唯一標識來取得對應屬性的值,雖然省去了序列化開銷和並發問題,但是用戶ID為重復存儲,如果存在大量這樣的數據,內存浪費還是非常可觀的。
那麼Redis提供的Hash很好的解決了這個問題,Redis的Hash實際是內部存儲的Value為一個HashMap,並提供了直接存取這個Map成員的接口,如下圖:
也就是說,Key仍然是用戶ID, value是一個Map,這個Map的key是成員的屬性名,value是屬性值,這樣對數據的修改和存取都可以直接通過其內部Map的Key(Redis裡稱內部Map的key為field), 也就是通過 key(用戶ID) + field(屬性標簽) 就可以操作對應屬性數據了,既不需要重復存儲數據,也不會帶來序列化和並發修改控制的問題。很好的解決了問題。
這裡同時需要注意,Redis提供了接口(hgetall)可以直接取到全部的屬性數據,但是如果內部Map的成員很多,那麼涉及到遍歷整個內部Map的操作,由於Redis單線程模型的緣故,這個遍歷操作可能會比較耗時,而另其它客戶端的請求完全不響應,這點需要格外注意。
實現方式:
上面已經說到Redis Hash對應Value內部實際就是一個HashMap,實際這裡會有2種不同實現,這個Hash的成員比較少時Redis為了節省內存會采用類似一維數組的方式來緊湊存儲,而不會采用真正的HashMap結構,對應的value redisObject的encoding為zipmap,當成員數量增大時會自動轉成真正的HashMap,此時encoding為ht。
- List
常用命令:
lpush,rpush,lpop,rpop,lrange等。
應用場景:
Redis list的應用場景非常多,也是Redis最重要的數據結構之一,比如twitter的關注列表,粉絲列表等都可以用Redis的list結構來實現,比較好理解,這裡不再重復。
實現方式:
Redis list的實現為一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷,Redis內部的很多實現,包括發送緩沖隊列等也都是用的這個數據結構。
- Set
常用命令:
sadd,spop,smembers,sunion 等。
應用場景:
Redis set對外提供的功能與list類似是一個列表的功能,特殊之處在於set是可以自動排重的,當你需要存儲一個列表數據,又不希望出現重復數據時,set是一個很好的選擇,並且set提供了判斷某個成員是否在一個set集合內的重要接口,這個也是list所不能提供的。
實現方式:
set 的內部實現是一個 value永遠為null的HashMap,實際就是通過計算hash的方式來快速排重的,這也是set能提供判斷一個成員是否在集合內的原因。
- Sorted set
常用命令:
zadd,zrange,zrem,zcard等
使用場景:
Redis sorted set的使用場景與set類似,區別是set不是自動有序的,而sorted set可以通過用戶額外提供一個優先級(score)的參數來為成員排 序,並且是插入有序的,即自動排序。當你需要一個有序的並且不重復的集合列表,那麼可以選擇sorted set數據結構,比如twitter 的public timeline可以以發表時間作為score來存儲,這樣獲取時就是自動按時間排好序的。
實現方式:
Redis sorted set的內部使用HashMap和跳躍表(SkipList)來保證數據的存儲和有序,HashMap裡放的是成員到score的映射,而跳躍表裡存放的 是所有的成員,排序依據是HashMap裡存的score,使用跳躍表的結構可以獲得比較高的查找效率,並且在實現上比較簡單。
開始在 Java 中使用 Redis 前, 我們需要確保已經安裝了 redis 服務及 Java redis 驅動,且你的機器上能正常使用 Java。 Java的安裝配置可以參考我們的Java開發環境配置接下來讓我們安裝 Java redis 驅動:
所需jar:jedis-2.1.0.jar和commons-pool-1.5.4.jar
Jedis操作步驟如下:
1->獲取Jedis實例需要從JedisPool中獲取;
2->用完Jedis實例需要返還給JedisPool;
3->如果Jedis在使用過程中出錯,則也需要還給JedisPool;
package redisJava; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class RedisJava { private static JedisPool jedisPool;// Jedis連接池 /** * @功能 構建redis連接池 * @author 皮鋒 * @return * @date 2016年3月29日 */ public static JedisPool getPool() { if (jedisPool == null) { JedisPoolConfig config = new JedisPoolConfig(); // 控制一個pool可分配多少個jedis實例,通過pool.getResource()來獲取; // 如果賦值為-1,則表示不限制;如果pool已經分配了maxActive個jedis實例,則此時pool的狀態為exhausted(耗盡)。 config.setMaxActive(500); // 控制一個pool最多有多少個狀態為idle(空閒的)的jedis實例。 config.setMaxIdle(5); // 表示當borrow(引入)一個jedis實例時,最大的等待時間,如果超過等待時間,則直接拋出JedisConnectionException; config.setMaxWait(1000 * 100); // 在borrow一個jedis實例時,是否提前進行validate操作;如果為true,則得到的jedis實例均是可用的; config.setTestOnBorrow(true); jedisPool = new JedisPool(config, "192.168.10.241", 6379); } return jedisPool; } /** * @功能 返還到連接池 * @author 皮鋒 * @param pool * @param redis * @date 2016年3月29日 */ public static void returnResource(JedisPool pool, Jedis redis) { if (redis != null) { pool.returnResource(redis); } } /** * @功能 獲取數據 * @author 皮鋒 * @param key * @return * @date 2016年3月29日 */ public static String getString(String key) { String value = null; JedisPool pool = null; Jedis jedis = null; try { pool = getPool(); jedis = pool.getResource(); value = jedis.get(key); } catch (Exception e) { // 釋放redis對象 pool.returnBrokenResource(jedis); e.printStackTrace(); } finally { // 返還到連接池 returnResource(pool, jedis); } return value; } /** * @功能 設置數據 * @author 皮鋒 * @param key * @param value * @return * @date 2016年3月29日 */ public static boolean setString(String key, String value) { JedisPool pool = null; Jedis jedis = null; try { pool = getPool(); jedis = pool.getResource(); value = jedis.set(key, value); } catch (Exception e) { // 釋放redis對象 pool.returnBrokenResource(jedis); e.printStackTrace(); return false; } finally { // 返還到連接池 returnResource(pool, jedis); } return true; } /** * @功能 測試 * @author 皮鋒 * @param args * @date 2016年3月29日 */ public static void main(String[] args) { setString("sex", "男"); setString("name", "皮鋒"); setString("age", "24"); System.out.println(getString("sex")); System.out.println(getString("name")); System.out.println(getString("age")); } }
import redis.clients.jedis.Jedis;public class RedisJava { public static void main(String[] args) { //連接本地的 Redis 服務 Jedis jedis= new Jedis("localhost"); System.out.println("Connection to server sucessfully"); //查看服務是否運行 System.out.println("Server is running: "+jedis.ping()); }}
編譯以上 Java 程序,確保驅動包的路徑是正確的。
$javac RedisJava.java $java RedisJavaConnection to server sucessfullyServer is running: PONG Redis Java String Example
import redis.clients.jedis.Jedis;public class RedisStringJava { public static void main(String[] args) { //連接本地的 Redis 服務 Jedis jedis= new Jedis("localhost"); System.out.println("Connection to server sucessfully"); //設置 redis 字符串數據 jedis.set("w3ckey", "Redis tutorial"); // 獲取存儲的數據並輸出 System.out.println("Stored string in redis:: "+ jedis.get("w3ckey")); }}
編譯以上程序。
$javac RedisStringJava.java $java RedisStringJavaConnection to server sucessfullyStored string in redis:: Redis tutorial
import redis.clients.jedis.Jedis;public class RedisListJava { public static void main(String[] args) { //連接本地的 Redis 服務 Jedis jedis= new Jedis("localhost"); System.out.println("Connection to server sucessfully"); //存儲數據到列表中 jedis.lpush("tutorial-list", "Redis"); jedis.lpush("tutorial-list", "Mongodb"); jedis.lpush("tutorial-list", "Mysql"); // 獲取存儲的數據並輸出 Listlist= jedis.lrange("tutorial-list", 0 ,5); for(int i=0; i
編譯以上程序。
$javac RedisListJava.java $java RedisListJavaConnection to server sucessfullyStored string in redis:: RedisStored string in redis:: MongodbStored string in redis:: Mysql六.Redis Java Keys 實例
import redis.clients.jedis.Jedis;public class RedisKeyJava { public static void main(String[] args) { //連接本地的 Redis 服務 Jedis jedis= new Jedis("localhost"); System.out.println("Connection to server sucessfully"); // 獲取數據並輸出 Listlist= jedis.keys("*"); for(int i=0; i 編譯以上程序。
$javac RedisKeyJava.java $java RedisKeyJavaConnection to server sucessfullyList of stored keys:: tutorial-nameList of stored keys:: tutorial-list