天天看點

Redis與Java - 資料結構 Redis與Java

标簽 : Java與NoSQL

Redis(REmote DIctionary Server) is an open source (BSD licensed), in-memory data structure store, used as database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs and geospatial indexes with radius queries. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.
<a href="http://www.infoq.com/cn/articles/tq-why-choose-redis">為什麼使用 Redis及其産品定位</a>
Redis沒有其他外部依賴, 編譯安裝過程非常簡單.

編譯安裝

<code>wget http://download.redis.io/releases/redis-3.0.5.tar.gz</code>

<code>make</code>(32位機器:<code>make 32bit</code>)

<code>make test</code>

<code>make PREFIX=${redis-path} install</code>

安裝完成後,在<code>${redis-path}/bin/</code>下生成如下二進制檔案:

工具

描述

redis-server

服務端

redis-cli

用戶端

redis-benchmark

Redis性能測試工具

redis-check-aof

AOF檔案修複工具

redis-check-dump

RDB檔案檢測工具

redis-sentinel

Sentinel伺服器(僅在2.8之後)

配置

<code>cp ${redis-3.0.5}/redis.conf ${redis-path}</code>

注: 使Redis以背景程序的形式運作: 編輯redis.conf配置檔案,設定<code>daemonize yes</code>.

啟動

<code>${redis-path}/bin/redis-server ./redis.conf</code>

連接配接

<code>${redis-path}/bin/redis-cli</code>連接配接伺服器

<code>- h</code>: 指定server位址

<code>- p</code>: 指定server端口

<code>KEYS pattern</code> 查詢key

Redis支援通配符格式: <code>*, ? ,[]</code>:

<code>*</code>

通配任意多個字元

<code>?</code>

通配單個字元

<code>[]</code>

通配括号内的某1個字元

<code>\x</code>

轉意符

<code>RANDOMKEY</code> 傳回一個随機存在的key

<code>EXISTS key</code> 判斷key是否存在

<code>TYPE key</code> 傳回key存儲類型

<code>SET key value</code> 設定一對key-value

<code>DEL key [key...]</code> 删除key

注: 傳回真正删除的key數量, 且<code>DEL</code>并不支援通配符.

<code>RENAME[NX] key new_key</code> 重命名

NX: not exists <code>new_key</code>不存在才對key重命名.

<code>move key DB</code> 移動<code>key</code>到另外一個DB

一個Redis程序預設打開16個DB,編号0~15(可在redis.conf中配置,預設為0),使用<code>SELECT n</code>可在多個DB間跳轉.

<code>TTL/PTTL key</code> 查詢key有效期(以秒/毫秒為機關,預設-1永久有效)

對于不存在的key,傳回-2; 對于已過期/永久有效的key,都傳回-1

<code>EXPIRE/PEXPIRE key n</code> 設定key有效期

<code>PERSIST key</code> 指定永久有效

字元串<code>Strings</code>是Redis最基本的資料類型,它能存儲任何形式的字元串,如使用者郵箱/JSON化的對象甚至是一張圖檔(二進制資料).一個字元串允許存儲的最大容量為512MB. 字元串類型也是其他4種資料類型的基礎,其他資料類型和字元串的差別從某種角度來說隻是組織字元串的形式不同.

我們使用**Jedis**用戶端連接配接Redis并存儲文章資料(關于本篇部落格實踐部分的詳細場景講解,可以參考[Redis入門指南][5]一書,在此就不再贅述,下同).

使用Jedis需要在pom.xml中添加如下依賴:

applicationContext.xml

使用Spring來管理Reids的連接配接.

DO: Articles文章

DAO

上面代碼使用了Spring與MessagePack的部分功能,是以需要在pom.xml中添加如下依賴:

功能

關鍵詞

增/減指定整數

<code>INCREBY/DECY key number</code>

增加指定浮點數

<code>INCREBYFLOAT key number</code>

尾部追加

<code>APPEND key value</code>

擷取字元串長度

<code>STRLEN key</code>

同時設定多個鍵值

<code>MSET key value [key value ...]</code>

同時獲得多個鍵值

<code>MGET key [key ...]</code>

傳回舊值并設定新值

<code>GETSET key value</code>

位操作

<code>GETBIT</code>/<code>SETBIT</code>/<code>BITCOUNT</code>/<code>BITOP</code>

散列<code>Hash</code>類型的鍵值是一種字典結構, 其存儲了字段(filed)和字段值(value)的映射. 但value隻能是字元串,不支援其他資料類型, 且一個<code>Hash</code>類型Key鍵可以包含至多232-1個字段.
<code>HSET</code>不區分插入還是更新,當key不存在時,<code>HSET</code>會自動建立并插入.插入傳回1, 更新傳回0.

前面使用String存儲整篇文章實際上有一個弊端, 如隻需要更新文章标題,需要将篇文章都做更新然後存入Redis,費時費力.是以我們更推薦使用<code>Hash</code>來存儲文章資料:

這樣即使需要為文章新添加字段, 也隻需為該<code>Hash</code>再添加一新key即可, 比如<code>&lt;slug, 文章縮略名&gt;</code>.

值擷取字段名

<code>HKEYS key</code>

隻擷取字段值

<code>HVALS key</code>

擷取字段數量

<code>HLEN key</code>

注: 除了<code>Hash</code>, Redis的其他資料類型同樣不支援類型嵌套, 如集合類型的每個元素隻能是字元串, 不能是另一個集合或<code>Hash</code>等.
清單<code>List</code>可以存儲一個有序的字元串清單, 其内部使用雙向連結清單實作, 是以向清單兩端插入/删除元素的時間複雜度為<code>O(1)</code>,而且越接近兩端的元素速度就越快.
LLEN指令的時間複雜度為O(1): Reids會儲存連結清單長度, 不必每次周遊統計.

考慮到評論時需要存儲評論的全部資料(姓名/聯系方式/内容/時間等),是以适合将一條評論的各個元素序列化為String之後作為清單的元素存儲:

DO: Comment

獲得指定索引元素值

<code>LINDEX key index</code>

設定指定索引元素值

<code>LSET key index value</code>

插入元素

<code>LINSERT key BEFORE|AFTER pivoit value</code>

将元素從一個清單轉入另一個清單

<code>RPOPLPUSH source destination</code>

等待[彈出/轉移][頭/尾]元素

<code>BLPOP</code>/<code>BRPOP</code>/<code>BRPOPLPUSH</code>

<code>RPOPLPUSH</code>是一個很有意思的指令: 先執行<code>RPOP</code>, 再執行<code>LPUSH</code>, 先從source清單右邊中彈出一個元素, 然後将其加入destination左邊, 并傳回這個元素值, 整個過程是原子的.

根據這一特性可将List作為循環隊列使用:source與destination相同,<code>RPOPLPUSH</code>不斷地将隊尾的元素移到隊首.好處在于在執行過程中仍可不斷向隊列中加入新元素,且允許多個用戶端同時處理隊列.
集合<code>Set</code>内的元素是無序且唯一的,一個集合最多可以存儲232-1個字元串.集合類型的常用操作是插入/删除/判斷是否存在, 由于集合在Redis内部是使用值為空的HashTable實作, 是以這些操作的時間複雜度為<code>O(1)</code>, 另外, Set最友善的還是多個集合之間還可以進行并/交/差的運算.

考慮到一個文章的所有标簽都是互不相同的, 且對标簽的儲存順序并沒有特殊的要求, 是以<code>Set</code>比較适用:

在提出這樣的需求之後, 前面的<code>posts:[ID]:tags</code> 文章次元的存儲結構就不适用了, 是以借鑒索引倒排的思想, 我們使用<code>tags:[tag]:posts</code>這種标簽次元的資料結構:

在這種結構下, 根據标簽搜尋文章就變得不費吹灰之力, 而<code>Set</code>自帶交/并/補的支援, 使得多标簽文章搜尋有也變得十分簡單:

獲得集合中元素數

<code>SCARD key</code>

集合運算并将結果存儲

<code>SDIFFSTORE/SINTERSTORE/SUNIONSTORE destination key [key ...]</code>

随機獲得集合中的元素

<code>SRANDMEMBER key [count]</code>

随機彈出集合中的一個元素

<code>SPOP key</code>

有序集合<code>Sorted-Sets</code>在<code>Set</code>基礎上為每個元素都關聯了一個分數[<code>score</code>],這使得我們不僅可以完成插入/删除和判斷元素是否存在等操作,還能夠獲得與<code>score</code>有關的操作(如<code>score</code>最高/最低的前N個元素、指定<code>score</code>範圍内的元素).<code>Sorted-Sets</code>具有以下特點: 1) 雖然集合中元素唯一, 但<code>score</code>可以相同. 2) 内部基于<code>HashTable</code>與<code>SkipList</code>實作,是以即使讀取中間部分的資料速度也很快(<code>O(log(N))</code>). 3) 可以通過更改元素<code>score</code>值來元素順序(與List不同).

要按照文章的點選量排序, 就必須再額外使用一個<code>Sorted-Set</code>類型來實作, 文章ID為元素,以該文章點選量為元素分數.

獲得集合中的元素數目

<code>ZCARD key</code>

獲得指定分數範圍内的元素個數

<code>ZCOUNT key min max</code>

獲得元素排名

<code>ZRANK/ZREVRANK key member</code>

<code>ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]</code>

<code>ZINTERSTORE</code>用來計算多個<code>Sorted-Set</code>的交集并将結果存儲在<code>destination</code>, 傳回值為<code>destination</code>中的元素個數.

<code>AGGREGATE</code>:

<code>destination</code>中元素的分數由<code>AGGREGATE</code>參數決定:<code>SUM</code>(和/預設), <code>MIN</code>(最小值), <code>MAX</code>(最大值)

<code>WEIGHTS</code>

通過<code>WEIGHTS</code>參數設定每個集合的權重,在參與運算時元素的分數會乘上該集合的權重.

<code>ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]</code>

用法類似

參考以往RDBMS的設計經驗: 1. 将表名轉換為key字首, 如<code>user:</code>. 2. 第2段放置用于區分key的字段, 對應于RDBMS中的主鍵, 如<code>user:[uid]:</code>. 3. 第3段放置要存儲的列名, 如<code>user:[uid]:email</code>.
使用者子產品資料分3個Key存儲: 使用者ID由<code>user:count</code>自增生成(<code>String</code>), 使用者email與id映射關系由<code>user:email.to.id</code>存儲(<code>Hash</code>), 使用者真實資料由<code>user:[id]:data</code>存儲(<code>Hash</code>):

User(domain)

UserDAO

UserService

關系子產品資料由2個Key存儲: 關注由<code>relation:following:[id]</code>存儲(<code>Set</code>), 被關注由<code>relation:follower:[id]</code>存儲(<code>Set</code>): 這樣存的優勢是既可以快速的查詢關注清單, 也可以快速的查詢粉絲清單, 而且還可以基于Redis對<code>Set</code>的支援, 做共同關注功能.

Relation(domain)

RelationDAO

RelationService

發微網誌功能我們采用推模式實作: 為每個使用者建立一個信箱<code>List</code>, 存儲關注的人發的微網誌, 是以每個使用者在發微網誌時都需要擷取自己的粉絲清單, 然後為每個粉絲推送一條微網誌資料(考慮到一個使用者關注的人過多, 是以實際開發中隻存最新1000條即可). 由此微網誌子產品資料由4個Key存儲: 微網誌ID由<code>miblog:count</code>自增生成(<code>String</code>), 微網誌真實資料由<code>miblog:[id]:data</code>存儲(<code>Hash</code>), 自己發的微網誌由<code>miblog:[uid]:my</code>存儲(<code>List</code>), 推送給粉絲的微網誌由<code>miblog:[uid]:flow</code>存儲(<code>List</code>):

MiBlog(domain)

MiBlogDAO

MiBlogService

<dl></dl>

<dt>參考&amp;擴充</dt>

<dd></dd>

<a href="http://www.infoq.com/cn/articles/weibo-relation-service-with-redis/">微網誌關系服務與Redis的故事</a>