我相信phper知道Redis是什么。既然如此,为了仪式感,首先不得不说一下什么是Redis。什么是Redis?Redis是一个高性能的键值数据库。它是完全开源和免费的。此外,Redis是一个NOSQL数据库。它是为解决高并发、高扩展、大数据存储等一系列问题而创建的数据库。解决方案是非关系数据库。但它不能替代关系型数据库,只能作为特定环境下的扩展。redis的出现很大程度上弥补了memcached等key/value存储的不足,在某些场合可以对关系型数据库起到很好的补充作用。Redis的特点:1.速度快Redis中的所有数据都存储在内存中,所以将数据放在内存中是Redis速度快的主要原因。Redis是用C语言实现的。一般来说,用C语言实现的程序更“接近”操作系统,执行速度也相对较快。Redis使用单线程架构来防止多线程可能产生的竞争问题。2、Redis提供了丰富的数据结构。它主要提供了五种数据结构:string(字符串)、list(链表)、set(集合)、zset(sortedset——有序集合)和hash(散列类型)。3、丰富的功能除了这五种数据结构,Redis还提供了很多额外的功能:它提供了key过期功能,可以用来实现缓存。提供发布-订阅功能,可用于实现消息系统。支持Lua脚本功能,可以使用Lua创建新的Redis命令。提供简单的交易功能,可以在一定程度上保证交易特性。提供了Pipeline功能,使得客户端可以一次性向Redis发送一批命令,减少网络开销。4、简单稳定Redis的简单性主要表现在三个方面。Redis的源代码非常少。Redis采用单线程模型,不仅简化了Redis服务端的处理模型,也简化了客户端的开发。Redis不需要依赖操作系统中的类库(比如Memcache需要依赖libevent等系统类库),Redis自己实现了事件处理相关的功能。Redis虽然很简单,但并不代表它不稳定。以维护的几千个Redis为例,从来没有出现过因为Redis本身的bug导致Redis宕机的情况。5.多种客户端语言Redis提供了简单的TCP通信协议,很多编程语言都可以很方便的连接到Redis,而且由于Redis被社区和各大公司广泛认可,所以支持Redis的客户端语言也很多,涵盖了几乎主流的编程语言,如Java、PHP、Python、C、C++、Nodejs等。6.持久化一般来说,将数据存储在内存中是不安全的。一旦发生停电或机器故障,重要数据可能会丢失。因此Redis提供了两种持久化方式:RDB和AOF,可以通过两种策略将内存中的数据保存到硬盘中(如图),保证了数据的持久化。7、主从复制Redis提供了复制功能,实现了对同一数据的多个Redis副本(如图)。复制功能是分布式Redis的基础。从图中可以看出Redis的工作流程。当客户端访问服务器时,客户端请求首先到达Nginx,Nginx负责分发数据并上传到多个服务器。当用户访问tomcat1时,会进行登录验证,并将session放入session管理中。使用Redis来管理session的好处是在第二个client登录后才会进行操作,这时候很可能会到达tomcat2服务器。这个时候tomcat2会去Redis找session。这样就避免了session只在一台服务器上查到,第二次读session发现为空的问题。在shiro中,shiro也提供了分布式session管理功能,但是使用Redis可以更加集中管理。Redis数据类型和使用场景前面提到,Redis支持五种数据类型。下面详细介绍这五种数据类型级别的使用场景。1.字符串简介。Strings数据类型是最常用和最简单的键值类型。普通的键/值存储可以归为此类。值不仅可以是字符串,还可以是数字。因为它是二进制安全的,所以您可以将图像文件的内容存储为字符串。Redis的字符串可以完全实现现在memcached的功能,而且效率更高。除了提供与Memcached一样的get、set、incr、decr等操作外,Redis还提供了以下附加操作:获取字符串的长度设置字符串的内容append获取字符串的某个内容和getthestring某个位(bit)批量设置一系列字符串的内容。常用命令:set、get、decr、incr、mget等。适用场景:Memcached、CKV的所有应用场景。字符串和数字直接访问。结构化数据需要先序列化,再设置值;相应的,取值后需要反序列化。可以使用redis的INCR、INCRBY、DECR、DECRBY等指令来实现原子计数的效果。即可以用来实现业务的统计统计需求。它还可以用于实现idmaker,生成全局唯一的id。存储会话密钥,实现分布式会话系统。Rediskey可以方便的设置过期时间,实现sessionkey的自动过期。校验skey时,先根据uid路由到对应的redis。如果获取不到skey,说明skey已经过期,需要重新登录;如果获取到skey并验证通过,则可以更新skey的过期时间。Setnx或SetNx,仅当key不存在时才设置。可用于选举Master或实现分布式锁:所有Client不断尝试用SetNxmastermyName抢占Master,成功者不断使用Expire刷新其过期时间。如果Master挂了,key就会失效,剩下的节点会进行新一轮的抢夺。借助redis2.6支持的lua脚本,可以实现两种更安全的分布式锁:一种适用于各个进程竞争,但始终是单个进程获取并处理锁的场景。除非原处理进程挂掉,锁到期,否则锁会被其他进程获取。无需主动解锁。通过get、expire/pexpire、setnxex|的lua脚本实现像素;适合每个进程竞争获取和处理锁的场景。通过setnxex|获取锁像素。当锁用完后,需要先通过get判断锁,然后通过del释放。否则在锁过期之前无法获取到锁。GetSet,设置新值,返回旧值。例如,要实现一个计数器,您可以使用GetSet获取计数并将其重置为0。GetBit/SetBit/BitOp/BitCount,BitMap的方式,比如在统计今天唯一访问用户数的时候,每个注册用户都有一个偏移量,如果他今天进来了,就把他的bit置1,用BitCount得到今天的总人数。Append/SetRange/GetRange/StrLen,扩展、替换、截取和查找文本的长度,对特定数据格式非常有用。实现方法:String默认以字符串形式存储在redis中,由redisObject引用。当遇到incr、decr等操作时,会转化为数值进行计算。此时redisObject的编码字段为int。2.List简介。List是双向链表,支持双向Pop/Push。江湖的规则一般是左端push,右端pop——LPush/RPop,也有Blocking版本的BLPop/BRPop。客户端可以在That's上阻塞,直到消息到达。还有RPopLPush/BRPopLPush,弹出并返回给客户端,同时将自己push到另一个列表中,LLen获取列表的长度。还有按值操作:LRem(按值删除元素),LInsert(在具有一定值的元素前后插入),复杂度是O(N),N是List的长度,因为List的值是不唯一,所以要遍历所有元素,Set只需要O(log(N))。按下标运算:下标从0开始,队列从左到右计数,下标为负数时,从右到左。LSet,按下标设置元素值。LIndex,按下标返回元素。LRange不像POP直接弹走元素,只是返回列表中的一个下标元素,这是分页的最爱。LTrim,限制List的大小,比如只保留最新的20条消息。复杂度也是O(N),其中LSet的N是List的长度,LIndex的N是下标的值,LRange的N是start的值+列出的元素个数,因为是链表列表而不是数组,所以下标访问实际上需要遍历链表,除非下标恰好是队列的头部和尾部。LTrim的N是移除元素的数量。常用命令:lpush、rpush、lpop、rpop、lrange等应用场景:各种列表,如twitter的followlist、fanlist等,也可以实现最新的新闻排名、每篇文章的评论等Redis的链表结构。消息队列可以使用Lists的PUSH操作将任务存储到Lists中,然后工作线程可以使用POP操作来获取并执行任务。这里的消息队列没有ack机制。如果消费者在将任务发送给Pop后崩溃了怎么办?解决方案之一是增加一个额外的有序集,在分发时同时发送到列表和有序集。分发时间用作分数。用户完成任务后,需要使用ZREM删除sortedset中的job,并周期性地从sortedset中取出超时未完成的任务,放回列表中。另一种方法是为每个worker添加一个额外的列表,在弹出任务时使用RPopLPush代替,同时将作业放入worker自己的列表中,完成后使用LREM将其删除。如果集群管理(比如zookeeper)发现worker挂了,就会把worker的列表内容放回主列表中。使用LRANGE可以轻松实现列表内容的分页功能。取最近N条数据的操作:LPUSH用于插入一个contentID,作为key存储在链表头部。LTRIM用于限制列表中的项目数最多为5000。如果用户需要检索的数据量超过此缓存容量,则需要将请求发送到数据库。实现方式:Redislist实现为双向链表,可以支持反向查找和遍历,操作起来更方便,但是带来了一些额外的内存开销。Redis中的很多实现,包括发送缓冲队列,也是用到了这个数据结构。3.Set简单介绍一下,set是一个无序的集合,集合中的元素没有顺序也没有重复。将重复的元素放入Set会自动对它们进行去重。常用命令:sadd、spop、smembers、sunion等。应用场景:一些需要去重的list,set提供了一个重要的判断成员是否在set集合中的接口,list也是没有提供的。可以存储一些集体数据。例如,在一个微博应用中,可以将用户的所有关注者存储在一个集合中,将其所有粉丝存储在一个集合中。Redis还提供了集合的交集、并集、差集等操作,可以很方便的实现共同关注、共同偏好、二度好友等功能。对于以上所有的集合操作,您还可以使用不同的命令来选择是将结果返回给客户端还是存储在一个新的集合中。又比如QQ有一个社交功能叫“好友标签”。你可以给你的好友打标签,比如“大美女”、“土豪”、“欧巴”等。在这里,你还可以将每个用户的标签存储在一个集合中。如果你想知道有多少特定的注册用户或IP地址访问了某个页面,你可以这样做:SADDpage:day1:。要了解特定用户的数量,请使用SCARDpage:day1:。需要测试特定用户是否访问过该页面?SISMEMBER页面:第1天:。实现方法:set的内部实现是一个HashMap,其值永远为null。实际上,它计算哈希值是为了快速排除重复项。这就是为什么set可以提供一种方法来判断一个成员是否在集合中。4.简单介绍zest。与set相比,有序set在元素放入集合时还提供了元素的得分,可以根据得分自动排序。常用命令:zadd、zrange、zrem、zcard等使用场景:存储一个有序的、不重复的集合列表,比如twitter的publictimeline可以存储发布时间作为分数,这样就可以了获取的时候自动按时间排序。它可以是加权队列。比如普通消息的得分为1,重要消息的得分为2,那么工作线程可以选择按照得分的倒序获取工作任务。优先处理重要任务。排行榜相关:ZADD排行榜。获取前100高分用户很容易:ZREVRANGEleaderboard099。用户的全球排名也类似,只需要执行:ZRANKleaderboard。新闻按用户投票和时间排序。在ZADD中,score=points/time^alpha,这样用户投票会相应地挖掘新闻,而时间会按照一定的指数埋葬新闻。逾期项目处理:使用unix时间作为key,保持列表按时间排序。检索current_time和time_to_live会执行查找过期项目的艰苦工作。另一个后台任务使用ZRANGE...WITHSCORES进行查询,删除过期的条目。实现方法:Redissortedset内部使用HashMap和跳跃列表(SkipList)来保证数据的存储和顺序。HashMap存储的是成员到分数的映射,skiplist存储的是所有成员。排序的依据是对于存储在HashMap中的分数,可以采用跳表的结构来获得比较高的查找效率,实现也比较简单。5.Hash简单介绍一下,Hash存储的是字符串与字符串值之间的映射关系。Hash将对象的每个属性存储在Map中,只能读取/更新对象的某些属性。这样,如果有些属性太长,就让它们留在一边。另外,不同模块只能更新自己关心的属性,不会并发造成覆盖冲突。常用命令:hget、hset、hgetall等应用场景:存储结构化数据,如用户信息。在Memcached或者CKV中,对于用户的昵称、年龄、性别、积分等用户信息,我们需要先序列化,然后存储为字符串值。值取出反序列化后,修改某一项的值,再序列化存回去。这不仅增加了开销,而且不适用于一些可以并发操作的场合(比如两个并发操作需要修改积分)。Redis的Hash结构允许你像更新数据库中的一个属性一样只修改某个属性值。Key是用户ID,value是一个Map,这个Map的key是成员的属性名,value是属性值。数据的修改和访问可以直接通过其内部Map的Key(内部Map的key在Redis中称为field),即可以通过key(用户ID)+field(属性标签)来操作对应的属性数据,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。3.不过这里需要注意的是,Redis提供了一个接口(hgetall)可以直接获取所有的属性数据,但是如果内部Map的成员比较多,则涉及到遍历整个内部Map的操作。由于Redis的单线程模型,这种遍历操作可能比较耗时,但它根本不响应其他客户端的请求,这一点需要特别注意。可用于建立索引。比如User对象,除了id之外,有时候还需要通过name来查询。您可以创建一个Hash,其Key为user:name:id。在插入User对象的时候(setuser:101{"id":101,"name":"calvin"}),顺便在这个hash中插入一个(hsetuser:name:idcalvin101),然后用calvin作为哈希中的键,值为101。按名称查询时,使用hgetuser:name:idcalvin从名为calvin的键中获取id。如果需要使用多个索引来查找一条数据,可以使用一个hashkey来避免使用多个stringkey来存储索引值。HINCRBY也可以用来实现idmaker。与string类型的idmaker相比,每种类型都需要一个key,而hash类型只需要一个key。实现方法:RedisHash其实就是一个Value对应的HashMap。这里有两种不同的实现。当这个Hash的成员比较少的时候,Redis为了节省内存会使用类似一维数组的方式进行紧凑存储,而不是使用真正的HashMap结构,对应值redisObject的编码是zipmap,当成员数量增加,会自动转为真正的HashMap,此时编码为ht。以上就是今天的分享。如果您有任何问题或建议,请留言交流。
