详解Redis应⽤场景及应⽤实例
Redis是⼀个开源的使⽤ANSI C语⾔编写、⽀持⽹络、可基于内存亦可持久化的⽇志型、Key-Value数据库,并提供多种语⾔的API。从2010年3⽉15⽇起,Redis的开发⼯作由VMware主持。
1. MySql+Memcached架构的问题
实际MySQL是适合进⾏海量数据存储的,通过Memcached将热点数据加载到cache,加速访问,很多公司都曾经使⽤过这样的架构,但随着业务数据量的不断增加,和访问量的持续增长,我们遇到了很多问题:
1.MySQL需要不断进⾏拆库拆表,Memcached也需不断跟着扩容,扩容和维护⼯作占据⼤量开发时间。
2.Memcached与MySQL数据⼀致性问题。
3.Memcached数据命中率低或down机,⼤量访问直接穿透到DB,MySQL⽆法⽀撑。
4.跨机房cache同步问题。
众多NoSQL百花齐放,如何选择
最近⼏年,业界不断涌现出很多各种各样的NoSQL产品,那么如何才能正确地使⽤好这些产品,最⼤化地发挥其长处,是我们需要深⼊研究和思考的问题,实际归根结底最重要的是了解这些产品的定位,并且了解到每款产品的tradeoffs,在实际应⽤中做到扬长避短,总体上这些NoSQL主要⽤于解决以下⼏种问题
1.少量数据存储,⾼速读写访问。此类产品通过数据全部in-momery 的⽅式来保证⾼速访问,同时提供数据落地的功能,实际这正是Redis 最主要的适⽤场景。
2.海量数据存储,分布式系统⽀持,数据⼀致性保证,⽅便的集节点添加/删除。
3.这⽅⾯最具代表性的是dynamo和bigtable 2篇论⽂所阐述的思路。前者是⼀个完全⽆中⼼的设计,节点之间通过gossip⽅式传递集信息,数据保证最终⼀致性,后者是⼀个中⼼化的⽅案设计,通过类似⼀个分布式锁服务来保证强⼀致性,数据写⼊先写内存和redo log,然后定期compat归并到磁盘上,将随机写优化为顺序写,提⾼写⼊性能。
4.Schema free,auto-sharding等。⽐如⽬前常见的⼀些⽂档数据库都是⽀持schema-free的,直接存储json格式数据,并且⽀持auto-sharding等功能,⽐如mongodb。
⾯对这些不同类型的NoSQL产品,我们需要根据我们的业务场景选择最合适的产品。
Redis最适合所有数据in-momory的场景,虽然Redis也提供持久化功能,但实际更多的是⼀个disk-backed的功能,跟传统意义上的持久化有⽐较⼤的差别,那么可能⼤家就会有疑问,似乎Redis更像⼀个加强版的Memcached,那么何时使⽤Memcached,何时使⽤ Redis呢?
如果简单地⽐较Redis与Memcached的区别,⼤多数都会得到以下观点:
1 、Redis不仅仅⽀持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
2 、Redis⽀持数据的备份,即master-slave模式的数据备份。
3 、Redis⽀持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进⾏使⽤。
2. Redis常⽤数据类型
Redis最为常⽤的数据类型主要有以下:
String
Hash
List
Set
Sorted set
pub/sub
Transactions
在具体描述这⼏种数据类型之前,我们先通过⼀张图了解下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不同数据类型提供⼀个统⼀的管理接⼝,实际作者也提供了多种⽅法帮助我们尽量节省内存使⽤,我们随后会具体讨论。
3. 各种数据类型应⽤和实现⽅式
下⾯我们先来逐⼀的分析下这7种数据类型的使⽤和内部实现⽅式:
String:
Strings 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字.
常⽤命令: set,get,decr,incr,mget 等。
应⽤场景:String是最常⽤的⼀种数据类型,普通的key/ value 存储都可以归为此类.即可以完全实现⽬前 Memcached 的功能,并且效率更⾼。还可以享受Redis的定时持久化,操作⽇志及 Replication等功能。除了提供与 Memcached ⼀样的get、set、incr、decr 等操作
外,Redis还提供了下⾯⼀些操作:
获取字符串长度
往字符串append内容
设置和获取字符串的某⼀段内容
设置及获取字符串的某⼀位(bit)
批量设置⼀系列字符串的内容
实现⽅式:String在redis内部存储默认就是⼀个字符串,被redisObject所引⽤,当遇到incr,decr等操作时会转成数值型进⾏计算,此时redisObject的encoding字段为int。
Hash
常⽤命令:hget,hset,hgetall 等。
应⽤场景:在Memcached中,我们经常将⼀些结构化的信息打包成HashMap,在客户端序列化后存储为⼀个字符串的值,⽐如⽤户的昵称、年龄、性别、积分等,这时候在需要修改其中某⼀项时,通常需要将所有值取出反序列化后,修改某⼀项的值,再序列化存储回去。这样不仅增⼤了开销,也不适⽤于⼀些可能并发操作的场合(⽐如两个并发的操作都需要修改积分)。⽽Redis的Hash结构可以使你像在数据库中Update⼀个属性⼀样只修改某⼀项属性值。
我们简单举个实例来描述下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结构来实现。
Lists 就是链表,相信略有数据结构知识的⼈都应该能理解其结构。使⽤Lists结构,我们可以轻松地实现最新消息排⾏等功能。Lists的另⼀个应⽤就是消息队列,
可以利⽤Lists的PUSH操作,将任务存在Lists中,然后⼯作线程再⽤POP操作将任务取出进⾏执⾏。Redis还提供了操作Lists中某⼀段的api,你可以直接查询,删除Lists中某⼀段的元素。
实现⽅式:
Redis list的实现为⼀个双向链表,即可以⽀持反向查和遍历,更⽅便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是⽤的这个数据结构。
Set
常⽤命令:
sadd,spop,smembers,sunion 等。
应⽤场景:
Redis set对外提供的功能与list类似是⼀个列表的功能,特殊之处在于set是可以⾃动排重的,当你需要存储⼀个列表数据,⼜不希望出现重复数据时,set 是⼀个很好的选择,并且set提供了判断某个成员是否在⼀个set集合内的重要接⼝,这个也是list所不能提供的。
Sets 集合的概念就是⼀堆不重复值的组合。利⽤Redis提供的Sets数据结构,可以存储⼀些集合性的数据,⽐如在微博应⽤中,可以将⼀个⽤户所有的关注⼈存在⼀个集合中,将其所有粉丝存在⼀个集合。Redis还为集合提供了求交集、并集、差集等操作,可以⾮常⽅便的实现如共同关注、共同喜好、
⼆度好友等功能,对上⾯的所有集合操作,你还可以使⽤不同的命令选择将结果返回给客户端还是存集到⼀个新的集合中。
实现⽅式:
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来存储,这样获取时就是⾃动按时间排好序的。
另外还可以⽤Sorted Sets来做带权重的队列,⽐如普通消息的score为1,重要消息的score为2,然后⼯作线程可以选择按score的倒序来获取⼯作任务。让重要的任务优先执⾏。
实现⽅式:
Redis sorted set的内部使⽤HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap⾥放的是成员到score的映射,⽽跳跃表⾥存放的是所有的成员,排序依据是HashMap⾥存的score,使⽤跳跃表的结构可以获得⽐较⾼的查效率,并且在实现上⽐较简单。
Pub/Sub
Pub/Sub 从字⾯上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某⼀个key值进⾏消息发布及消息订阅,当⼀个 key 值上进⾏了消息发布后,所有订阅它的客户端都会收到相应的消息。这⼀功能最明显的⽤法就是⽤作实时消息系统,⽐如普通的即时聊天,聊等功能。
Transactions
谁说NoSQL都不⽀持事务,虽然Redis的Transactions提供的并不是严格的ACID的事务(⽐如⼀串⽤EXEC提交执⾏的命令,在执⾏中服务器宕机,那么会有⼀部分命令执⾏了,剩下的没执⾏),但是这个Transactions还是提供了基本的命令打包执⾏的功能(在服务器不出问题的情况下,可以保证⼀连串
的命令是顺序在⼀起执⾏的,中间有会有其它客户端命令插进来执⾏)。Redis还提供了⼀个Watch功能,你可以对⼀个key进⾏ Watch,然后再执⾏Transactions,在这过程中,如果这个Watched的值进⾏了修改,那么这个Transactions会发现并拒绝执⾏。
4. Redis实际应⽤场景
Redis在很多⽅⾯与其他数据库解决⽅案不同:它使⽤内存提供主存储⽀持,⽽仅使⽤硬盘做持久性的存储;它的数据模型⾮常独特,⽤的是单线程。另⼀个⼤区别在于,你可以在开发环境中使⽤Redis的功能,但却不需要转到Redis。
转向Redis当然也是可取的,许多开发者从⼀开始就把Redis作为⾸选数据库;但设想如果你的开发环境已经搭建好,应⽤已经在上⾯运⾏了,那么更换数据库框架显然不那么容易。另外在⼀些需要⼤容量数据集的应⽤,Redis也并不适合,因为它的数据集不会超过系统可⽤的内存。所以如果你有⼤数据应⽤,⽽且主要是读取访问模式,那么Redis并不是正确的选择。
然⽽我喜欢Redis的⼀点就是你可以把它融⼊到你的系统中来,这就能够解决很多问题,⽐如那些你现有的数据库处理起来感到缓慢的任务。这些你就可以通过 Redis来进⾏优化,或者为应⽤创建些新的功能。在本⽂中,我就想探讨⼀些怎样将Redis加⼊到现有的环境中,并利⽤它的原语命令等功能来解决传统环境中碰到的⼀些。在这些例⼦中,Redis都不是作为⾸选数据库。
1、显⽰最新的项⽬列表
下⾯这个语句常⽤来显⽰最新项⽬,随着数据多了,查询毫⽆疑问会越来越慢。
SELECT * FROM foo WHERE … ORDER BY time DESC LIMIT 10
在Web应⽤中,“列出最新的回复”之类的查询⾮常普遍,这通常会带来可扩展性问题。这令⼈沮丧,因为项⽬本来就是按这个顺序被创建的,但要输出这个顺序却不得不进⾏排序操作。
类似的问题就可以⽤Redis来解决。⽐如说,我们的⼀个Web应⽤想要列出⽤户贴出的最新20条评论。在最新的评论边上我们有⼀个“显⽰全部”的链接,点击后就可以获得更多的评论。
我们假设数据库中的每条评论都有⼀个唯⼀的递增的ID字段。
我们可以使⽤来制作主页和评论页,使⽤Redis的模板,每次新评论发表时,我们会将它的ID添加到⼀个Redis列表:
LPUSH latestments
我们将列表裁剪为指定长度,因此Redis只需要保存最新的5000条评论:
LTRIM latestments 0 5000
每次我们需要获取最新评论的项⽬范围时,我们调⽤⼀个函数来完成(使⽤伪代码):
FUNCTION get_latest_comments(start, num_items):
id_list = redis.lrange(“latestments”,start,start+num_items – 1)
IF id_list.length < num_items
id_list = SQL_DB(“SELECT … ORDER BY time LIMIT …”)
END
RETURN id_list
END
这⾥我们做的很简单。在Redis中我们的最新ID使⽤了常驻缓存,这是⼀直更新的。但是我们做了限制不能超过5000个ID,因此我们的获取ID函数会⼀直询问Redis。只有在start/count参数超出了这个范围的时候,才需要去访问数据库。
我们的系统不会像传统⽅式那样“刷新”缓存,Redis实例中的信息永远是⼀致的。SQL数据库(或是硬盘上的其他类型数据库)只是在⽤户需要获取“很远”的数据时才会被触发,⽽主页或第⼀个评论页是不会⿇烦到硬盘上的数据库了。
mongodb和mysql结合2、删除与过滤
我们可以使⽤LREM来删除评论。如果删除操作⾮常少,另⼀个选择是直接跳过评论条⽬的⼊⼝,报告说该评论已经不存在。
有些时候你想要给不同的列表附加上不同的过滤器。如果过滤器的数量受到限制,你可以简单的为每个不同的过滤器使⽤不同的Redis列表。毕竟每个列表只有5000条项⽬,但Redis却能够使⽤⾮常少的内存来处理⼏百万条项⽬。
3、排⾏榜相关
另⼀个很普遍的需求是各种数据库的数据并⾮存储在内存中,因此在按得分排序以及实时更新这些⼏乎每秒钟都需要更新的功能上数据库的性能不够理想。
典型的⽐如那些在线游戏的排⾏榜,⽐如⼀个Facebook的游戏,根据得分你通常想要:
– 列出前100名⾼分选⼿
– 列出某⽤户当前的全球排名
这些操作对于Redis来说⼩菜⼀碟,即使你有⼏百万个⽤户,每分钟都会有⼏百万个新的得分。
模式是这样的,每次获得新得分时,我们⽤这样的代码:
ZADD leaderboard
你可能⽤userID来取代username,这取决于你是怎么设计的。
得到前100名⾼分⽤户很简单:ZREVRANGE leaderboard 0 99。
⽤户的全球排名也相似,只需要:ZRANK leaderboard 。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论