首頁  >  文章  >  資料庫  >  redis應用程式場景詳細介紹

redis應用程式場景詳細介紹

尚
轉載
2019-11-26 14:53:121909瀏覽

redis應用程式場景詳細介紹

Redis開創了一種新的資料儲存思路,使用Redis,我們不用在面對功能單調的資料庫時,把精力放在如何把大象放進冰箱這樣的問題上,而是利用Redis靈活多變的資料結構和資料操作,為不同的大象建造不同的冰箱。希望你喜歡這個比喻。

一、Redis常用資料型態(建議:redis影片教學

Redis最常用的資料型別主要有五種:

String 、Hash、List、Set、Sorted set

在具體描述這幾種資料型別之前,我們先透過一張圖片來了解下Redis內部記憶體管理中是如何描述這些不同資料類型的:

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不同資料型別提供一個統一的管理接口,實際作者也提供了多種方法幫助我們盡量節省記憶體使用,我們接著會具體討論。

二、各種資料型別應用與實作方式

下面我們先來逐一的分析下這五種資料型別的使用和內部實作方式:

#1、 String

String 資料結構是簡單的key-value類型,value其實不只String,也可以是數字。

常用指令:get、set、incr、decr、mget等。

應用程式場景:String是最常用的資料類型,普通的key/ value 儲存都可以歸為此類,即可以完全實現目前 Memcached 的功能,並且效率更高。還可以享受Redis的定時持久化,操作日誌及 Replication等功能。除了提供與Memcached 一樣的get、set、incr、decr 等操作外,Redis還提供了下面一些操作: 

  • 取得字串長度

  • 往字串append內容

  • 設定和取得字串的某一段內容

  • 設定及取得字串的某一位(bit)

  • 批次設定一系列字串的內容

#使用場景:常規key-value快取應用程式。常規計數: 微博數, 粉絲數。

實作方式:String在redis內部儲存預設就是一個字串,被redisObject所引用,當遇到incr,decr等操作時會轉成數值類型進行計算,此時redisObject的encoding欄位為int 。

2、Hash

常用指令:hget,hset,hgetall 等。

應用程式場景:

我們簡單舉個實例來描述下Hash的應用場景,例如我們要儲存一個使用者資訊物件數據,包含以下資訊:

使用者ID為查找的key,儲存的value用戶物件包含姓名,年齡,生日等信息,如果用普通的key/value結構來存儲,主要有以下2種存儲方式:

redis應用程式場景詳細介紹

第一種方式將使用者ID作為查找key,把其他資訊封裝成一個物件以序列化的方式存儲,這種方式的缺點是,增加了序列化/反序列化的開銷,並且在需要修改其中一項資訊時,需要把整個物件取回,並且修改作業需要對並發進行保護,引入CAS等複雜問題。

redis應用程式場景詳細介紹

 第二種方法是這個使用者資訊物件有多少成員就存成多少個key-value對兒,用使用者ID 對應屬性的名稱作為唯一識別來取得對應屬性的值,雖然省去了序列化開銷和並發問題,但是用戶ID為重複存儲,如果存在大量這樣的數據,內存浪費還是非常可觀的。

那麼Redis提供的Hash很好的解決了這個問題,Redis的Hash實際上是內部存儲的Value為一個HashMap,並提供了直接訪問這個Map成員的接口,如下圖:

redis應用程式場景詳細介紹

#

 也就是說,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。

3、List

常用指令:lpush,rpush,lpop,rpop,lrange等。

應用程式場景:

Redis list的應用程式場景非常多,也是Redis最重要的資料結構之一,例如twitter的關注列表,粉絲列表等都可以用Redis的list結構來實現。

List 就是鍊錶,相信略有資料結構知識的人都應該能理解其結構。使用List結構,我們可以輕鬆實現最新訊息排行等功能。 List的另一個應用程式就是訊息佇列,
可以利用List的PUSH操作,將任務存在List中,然後工作執行緒再用POP操作將任務取出執行。 Redis也提供了操作List中某一段的api,你可以直接查詢,刪除List中某一段的元素。

實作方式:

Redis list的實作為雙向鍊錶,即可以支援反向查找和遍歷,更方便操作,不過帶來了部分額外的記憶體開銷,Redis內部的很多實現,包括發送緩衝隊列等也都是用的這個資料結構。

Redis的list是每個子元素都是String類型的雙向鍊錶,可以透過push和pop操作從列表的頭部或尾部添加或刪除元素,這樣List即可以作為棧,也可以作為隊列。 

使用場景:

訊息佇列系統

使用list可以建立佇列系統,使用sorted set甚至可以建立有優先權的佇列系統。

例如:將Redis用作日誌收集器

實際上還是一個佇列,多個端點將日誌資訊寫入Redis,然後一個worker統一將所有日誌寫入磁碟。

取最新N個資料的操作

記錄前N個最新登陸的用戶Id列表,超出的範圍可以從資料庫中取得。

//把当前登录人添加到链表里
ret = r.lpush("login:last_login_times", uid)

//保持链表只有N位
ret = redis.ltrim("login:last_login_times", 0, N-1)

//获得前N个最新登陆的用户Id列表
last_login_list = r.lrange("login:last_login_times", 0, N-1)

例如sina微博:

在Redis中我們的最新微博ID使用了常駐緩存,這是一直更新的。但是我們做了限制不能超過5000個ID,因此我們的取得ID函數會一直詢問Redis。只有在start/count參數超出了這個範圍的時候,才需要去存取資料庫。

我們的系統不會像傳統方式那樣「刷新」緩存,Redis實例中的資訊永遠是一致的。 SQL資料庫(或硬碟上的其他類型資料庫)只是在使用者需要取得「很遠」的資料時才會被觸發,而主頁或第一個評論頁是不會麻煩到硬碟上的資料庫了。

4、Set

常用指令:

sadd,spop,smembers,sunion 等。

應用場景:

Redis set對外提供的功能與list類似是一個列表的功能,特殊之處在於set是可以自動排重的,當你需要存儲一個列表數據,又不希望出現重複資料時,set是一個很好的選擇,而set提供了判斷某個成員是否在一個set集合內的重要接口,這個也是list所不能提供的。

Set 就是一個集合,集合的概念就是一堆不重複值的組合。利用Redis提供的Set資料結構,可以儲存一些集合性的資料。

案例:

在微博應用程式中,可以將一個使用者所有的​​追蹤人存在一個集合中,將其所有粉絲存在一個集合。 Redis也為集合提供了求交集、並集、差集等操作,可以非常方便的實現如共同關注、共同喜好、二度好友等功能,對上面的所有集合操作,你還可以使用不同的命令選擇將結果傳回給客戶端還是存集到一個新的集合。

Set是集合,是String類型的無序集合,set是透過hashtable實現的,概念和數學中個的集合基本上類似,可以交集,並集,差集等等,set中的元素是沒有順序的。

實作方式: 

set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

使用场景:

交集,并集,差集:(Set)

//book表存储book名称

set book:1:name    ”The Ruby Programming Language”

set book:2:name     ”Ruby on rail”

set book:3:name     ”Programming Erlang”

//tag表使用集合来存储数据,因为集合擅长求交集、并集

sadd tag:ruby 1

sadd tag:ruby 2

sadd tag:web 2

sadd tag:erlang 3

//即属于ruby又属于web的书?

 inter_list = redis.sinter("tag.web", "tag:ruby") 

//即属于ruby,但不属于web的书?

 inter_list = redis.sdiff("tag.ruby", "tag:web") 

//属于ruby和属于web的书的合集?

 inter_list = redis.sunion("tag.ruby", "tag:web")

获取某段时间所有数据去重值

这个使用Redis的set数据结构最合适了,只需要不断地将数据往set中扔就行了,set意为集合,所以会自动排重。

5、Sorted Set

常用命令:

zadd,zrange,zrem,zcard等

使用场景:

Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

和Set相比,Sorted Set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列,比如一个存储全班同学成绩的Sorted Set,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。

另外还可以用Sorted Set来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

实现方式:

Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

三、Redis实际应用场景

1、显示最新的项目列表

下面这个语句常用来显示最新项目,随着数据多了,查询毫无疑问会越来越慢。

SELECT * FROM foo WHERE ... ORDER BY time DESC LIMIT 10

在Web应用中,“列出最新的回复”之类的查询非常普遍,这通常会带来可扩展性问题。这令人沮丧,因为项目本来就是按这个顺序被创建的,但要输出这个顺序却不得不进行排序操作。类似的问题就可以用Redis来解决。比如说,我们的一个Web应用想要列出用户贴出的最新20条评论。

在最新的评论边上我们有一个“显示全部”的链接,点击后就可以获得更多的评论。我们假设数据库中的每条评论都有一个唯一的递增的ID字段。我们可以使用分页来制作主页和评论页,使用Redis的模板,每次新评论发表时,我们会将它的ID添加到一个Redis列表:

LPUSH latest.comments <id></id>

我们将列表裁剪为指定长度,因此Redis只需要保存最新的5000条评论:

LTRIM latest.comments 0 5000

每次我们需要获取最新评论的项目范围时,我们调用一个函数来完成(使用伪代码):

FUNCTION get_latest_comments(start, num_items):  
    id_list = redis.lrange("latest.comments",start,start+num_items - 1)  
    IF id_list.length <p>这里我们做的很简单。在Redis中我们的最新ID使用了常驻缓存,这是一直更新的。但是我们做了限制不能超过5000个ID,因此我们的获取ID函数会一直询问Redis。只有在start/count参数超出了这个范围的时候,才需要去访问数据库。</p><p>我们的系统不会像传统方式那样“刷新”缓存,Redis实例中的信息永远是一致的。SQL数据库(或是硬盘上的其他类型数据库)只是在用户需要获取“很远”的数据时才会被触发,而主页或第一个评论页是不会麻烦到硬盘上的数据库了。</p><p>2、排行榜应用,取TOP N操作</p><p>这个需求与上面需求的不同之处在于,取最新N个数据的操作以时间为权重,这个是以某个条件为权重,比如按顶的次数排序,这时候就需要我们的sorted set出马了,将你要排序的值设置成sorted set的score,将具体的数据设置成相应的value,每次只需要执行一条ZADD命令即可。</p><p>热门,排行榜应用:</p><pre class="brush:php;toolbar:false">//将登录次数和用户统一存储在一个sorted set里
zadd login:login_times 5 1
zadd login:login_times 1 2
zadd login:login_times 2 3
//当用户登录时,对该用户的登录次数自增1
ret = r.zincrby("login:login_times", 1, uid)
//那么如何获得登录次数最多的用户呢,逆序排列取得排名前N的用户
ret = r.zrevrange("login:login_times", 0, N-1)

另一个很普遍的需求是各种数据库的数据并非存储在内存中,因此在按得分排序以及实时更新这些几乎每秒钟都需要更新的功能上数据库的性能不够理想。典型的比如那些在线游戏的排行榜,比如一个Facebook的游戏,根据得分你通常想要:

- 列出前100名高分选手

- 列出某用户当前的全球排名

这些操作对于Redis来说小菜一碟,即使你有几百万个用户,每分钟都会有几百万个新的得分。模式是这样的,每次获得新得分时,我们用这样的代码:

ZADD leaderboard    

你可能用userID来取代username,这取决于你是怎么设计的。得到前100名高分用户很简单:

ZREVRANGE leaderboard 0 99

 用户的全球排名也相似,只需要:

ZRANK leaderboard

3、删除与过滤

我们可以使用LREM来删除评论。如果删除操作非常少,另一个选择是直接跳过评论条目的入口,报告说该评论已经不存在。 有些时候你想要给不同的列表附加上不同的过滤器。如果过滤器的数量受到限制,你可以简单的为每个不同的过滤器使用不同的Redis列表。毕竟每个列表只有5000条项目,但Redis却能够使用非常少的内存来处理几百万条项目。

4、按照用户投票和时间排序

排行榜的一种常见变体模式就像Reddit或Hacker News用的那样,新闻按照类似下面的公式根据得分来排序:score = points / time^alpha 因此用户的投票会相应的把新闻挖出来,但时间会按照一定的指数将新闻埋下去。下面是我们的模式,当然算法由你决定。

模式是这样的,开始时先观察那些可能是最新的项目,例如首页上的1000条新闻都是候选者,因此我们先忽视掉其他的,这实现起来很简单。每次新的新闻贴上来后,我们将ID添加到列表中,使用LPUSH + LTRIM,确保只取出最新的1000条项目。

有一项后台任务获取这个列表,并且持续的计算这1000条新闻中每条新闻的最终得分。计算结果由ZADD命令按照新的顺序填充生成列表,老新闻则被清除。这里的关键思路是排序工作是由后台任务来完成的。

5、处理过期项目

另一种常用的项目排序是按照时间排序。我们使用unix时间作为得分即可。 模式如下:

- 每次有新项目添加到我们的非Redis数据库时,我们把它加入到排序集合中。这时我们用的是时间属性,current_time和time_to_live。

- 另一项后台任务使用ZRANGE…SCORES查询排序集合,取出最新的10个项目。如果发现unix时间已经过期,则在数据库中删除条目。

6、计数

Redis是一个很好的计数器,这要感谢INCRBY和其他相似命令。我相信你曾许多次想要给数据库加上新的计数器,用来获取统计或显示新信息,但是最后却由于写入敏感而不得不放弃它们。

好了,现在使用Redis就不需要再担心了。有了原子递增(atomic increment),你可以放心的加上各种计数,用GETSET重置,或者是让它们过期。例如这样操作:

INCR user:<id> EXPIRE</id>

你可以计算出最近用户在页面间停顿不超过60秒的页面浏览量,当计数达到比如20时,就可以显示出某些条幅提示,或是其它你想显示的东西。

7、特定时间内的特定项目

另一项对于其他数据库很难,但Redis做起来却轻而易举的事就是统计在某段特点时间里有多少特定用户访问了某个特定资源。比如我想要知道某些特定的注册用户或IP地址,他们到底有多少访问了某篇文章。每次我获得一次新的页面浏览时我只需要这样做:

SADD page:day1:<page_id> <user_id></user_id></page_id>

当然你可能想用unix时间替换day1,比如time()-(time()%3600*24)等等。 想知道特定用户的数量吗?只需要使用

SCARD page:day1:<page_id></page_id>

需要测试某个特定用户是否访问了这个页面?

SISMEMBER page:day1:<page_id></page_id>

8、查找某个值所在的区间(区间无重合) :(Sorted Set)

例如有下面两个范围,10-20和30-40

A_start 10, A_end 20

B_start 30, B_end 40

我们将这两个范围的起始位置存在Redis的Sorted Sets数据结构中,基本范围起始值作为score,范围名加start和end为其value值:

redis 127.0.0.1:6379> zadd ranges 10 A_start
(integer) 1
redis 127.0.0.1:6379> zadd ranges 20 A_end
(integer) 1
redis 127.0.0.1:6379> zadd ranges 30 B_start
(integer) 1
redis 127.0.0.1:6379> zadd ranges 40 B_end
(integer) 1

这样数据在插入Sorted Sets后,相当于是将这些起始位置按顺序排列好了。现在我需要查找15这个值在哪一个范围中,只需要进行如下的zrangbyscore查找:

redis 127.0.0.1:6379> zrangebyscore ranges (15 +inf LIMIT 0 1
1) "A_end"

这个命令的意思是在Sorted Sets中查找大于15的第一个值。(+inf在Redis中表示正无穷大,15前面的括号表示>15而非>=15)查找的结果是A_end,由于所有值是按顺序排列的,所以可以判定15是在A_start到A_end区间上,也就是说15是在A这个范围里。至此大功告成。

9、交集,并集,差集:(Set)

//book表存储book名称
set book:1:name    ”The Ruby Programming Language”
set book:2:name     ”Ruby on rail”
set book:3:name     ”Programming Erlang”

//tag表使用集合来存储数据,因为集合擅长求交集、并集
sadd tag:ruby 1
sadd tag:ruby 2
sadd tag:web 2
sadd tag:erlang 3

//即属于ruby又属于web的书?
 inter_list = redis.sinter("tag.web", "tag:ruby") 
//即属于ruby,但不属于web的书?
 inter_list = redis.sdiff("tag.ruby", "tag:web") 
//属于ruby和属于web的书的合集?
 inter_list = redis.sunion("tag.ruby", "tag:web")

更多redis相关文章请关注redis数据库教程栏目。

以上是redis應用程式場景詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除