Redis可以用来做什么
- 缓存
- 排行榜系统
- 计数器应用(如统计文章阅读次数)
- 社交网络(踩,赞、共同好友等)
- 消息队列系统(利用list结构的lpush()和rbpop()可以很容易的实现一个消息队列)
Redis的8个特性:速度快、基于键值对的数据结构服务器、功能丰富、简单稳定、客户端语言多、持久化、主从复制、支持高可用和分布式。
全局命令
command | 作用 | 返回值 | 说明 |
---|---|---|---|
keys * |
查看所有键 | 键列表 | |
dbsize |
查询键总数 | int | 在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以dbsize命令的时间复杂度是O(1) |
exists key |
检查键是否存在 | 存在则返回1,不存在则返回0 | |
del key |
删除键 | 返回结果为成功删除键的个数,假设删除一个不存在的键,就会返回0 | |
expire key seconds |
设置键的过期时间(秒) | ttl命令会返回键的剩余过期时间,它有3种返回值:大于等于0的整数:键剩余的过期时间。-1:键没设置过期时间。-2:键不存在 | |
type key |
查看键的数据结构类型 | string , list , hash , set , zset , none |
如果键不存在,则返回none |
数据结构和内存编码
可以通过type
命令查看当前键的数据类型,Redis支持string
(字符串)、hash
(哈希)、list
(列表)、set
(集合)、zset
(有序集合),但这些只是Redis对外的数据结构。实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。
Redis3.2提供了quicklist,结合了ziplist和linkedlist两者,的优势,为列表类型提供了一种更为优秀的内部编码实现。
单线程架构
Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务
Redis是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行。所以客户端命令的执行顺序是不确定的(如图2-4所示),但是可以确定不会有两条命令被同时执行。不会产生并发问题,这就是Redis单线程的基本模型。但是像发送命令、返回结果、命令排队肯定不像描述的这么简单,Redis使用了I/O多路复用
技术来解决I/O的问题。
为什么Redis使用单线程速度还这么快
- 第一,纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
- 第二,非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间
- 第三,单线程避免了线程切换和竞态产生的消耗。
但是单线程会有一个问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。
字符串
键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB
。
命令
command | 作用 | 返回值 | 说明 |
---|---|---|---|
set key value |
设置值 | 返回OK表示设置成功 | set命令有几个选项:set key value [ex seconds] [px milliseconds] [nx|xx] ·ex seconds:为键设置秒级过期时间。·px milliseconds:为键设置毫秒级过期时间。·nx:键必须不存在,才可以设置成功,用于添加。·xx:与nx相反,键必须存在,才可以设置成功,用于更新。 |
setex key seconds value |
作用和ex 选项一样 |
||
setnx key value |
作用和nx 选项一样 |
setnx失败,返回结果为0 | 由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value ,根据setnx的特性只有一个客户端能设置成功,setnx可以作为分布式锁的一种实现方案,Redis官方给出了使用setnx实现分布式锁的方法 |
setxx key value |
作用和xx 选项一样 |
setxx 失败,返回结果为0 |
|
get key |
获取值 | 不存在返回nil |
|
mset key value [key value …] |
批量设置值 | 返回OK表示设置成功 | 学会使用批量操作,有助于提高业务处理效率,但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成Redis阻塞或者网络拥塞 |
mget key [key …] |
批量获取值 | 不存在的key返回nil |
批量操作命令可以有效提高开发效率,假如没有mget这样的命令,执行n次get命令: $n 次 get 时间 = n 次网络时间 + n 次命令时间$. 使用mget命令后,要执行n次get命令操作: $n 次 get 时间 = 1 次网络时间 + n 次命令时间$, 大幅节约了网络时间 |
incr key |
计数 | 值不是整数,返回错误。·值是整数,返回自增后的结果。·键不存在,按照值为0自增,返回结果为1 | 很多存储系统和编程语言内部使用CAS机制实现计数功能,会有一定的CPU开销,但在Redis中完全不存在这个问题,因为Redis是单线程架构,任何命令到了Redis服务端都要顺序执行。 |
decr (自减),incrby (自增指定数字),decrby (自减指定数字),incrbyfloat (自增浮点数) |
|||
append key value |
append可以向字符串尾部追加值 | ||
strlen key |
返回值的字符串长度 | ||
getset key value |
设置并返回原值 | ||
setrange key offset value |
设置指定位置的字符 | ||
getrange key start end |
获取部分字符串 | 与java 的substring(start,end) 含首不含尾的特性不同, getrange截取的字符包含start和end位置 |
内部编码
字符串类型的内部编码有3种:
- int:设置的值为8个字节的长整型数字时使用。
- embstr:设置的值为小于等于39个字节的字符串时使用。
- raw:设置的值为大于39个字节的字符串时使用。
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
典型使用场景
缓存功能
Redis作为缓存层,MySQL作为存储层,绝大部分请求的热点数据都从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
计数
使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。视频播放数系统可以使用Redis作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增1
共享session
一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。为了解决这个问题,可以使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
限速
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。此功能可以使用Redis来实现,利用Redis可以设置键过期的功能,在用户首次访问时为其新建一个带有过期时间的key(如60秒),在这个key过期之前,都不允许用户再次申请短信验证码。
哈希
在Redis中,哈希类型是指对应key的value本身又是一个键值对结构,形如value={{field1,value1},…{fieldN,valueN}}。
命令
command | 作用 | 返回值 | 说明 |
---|---|---|---|
hset key field value |
设置值 | 设置成功会返回1,反之会返回0 | Redis提供了hsetnx 命令,它们的关系就像set 和setnx 命令一样,只不过作用域由键变为field |
hget key field |
获取值 | 如果键或field 不存在,会返回nil |
|
hdel key field [field …] |
删除filed | hdel会删除一个或多个field,返回结果为成功删除field的个数 | |
hlen key |
计算field的个数 | ||
hmget key field [field …] |
批量获取field-value | ||
hmset key field value [field value …] |
批量设置field-value | ||
hexists key field |
判断field是否存在 | ||
hkeys key |
获取所有field | ||
hvals key |
获取所有value | ||
hgetall key |
获取所有field-value | 使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果开发人员只需要获取部分field,可以使用hmget,如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型 | |
hincrby hincrybyfloat |
像incrby和incrbyfloat命令一样,但是它们的作用域是filed | ||
hstrlen key field |
计算value的字符串长度 |
内部编码
哈希类型的内部编码有两种:
ziplist
(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries
配置(默认512个)、同时所有值都小于hash-max-ziplist-value
配置(默认64字节)时,Redis会使用ziplist
作为哈希的内部实现,ziplist
使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable
更加优秀。hashtable
(哈希表):当哈希类型无法满足ziplist
的条件时,Redis会使用hashtable
作为哈希的内部实现,因为此时ziplist
的读写效率会下降,而hashtable
的读写时间复杂度为O(1)。
哈希类型和关系型数据库有两点不同之处
- 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为
NULL
)。 - 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高
列表
列表(list
)类型是用来存储多个有序的字符串,列表中的每个字符串称为元素(element
),一个列表最多可以存储$2_32 -1$个元素。在Redis中,可以对列表两端插入(push
)和弹出(pop
),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈
和队列
的角色,在实际开发上有很多应用场景。
列表类型有两个特点:
第一、列表中的元素是有序的,可以通过索引下标获取某个元素或者某个范围内的元素列表
第二、列表中的元素可以是重复的
命令
command | 作用 | 返回值 | 说明 |
---|---|---|---|
添加操作 | |||
rpush key value [value …] |
从左边插入元素 | 返回成功插入元素的个数 | lpush 使用方法与rpush 相同,只不过从左侧插入 |
linsert key before|after pivot value |
linsert命令会从列表中找到等于pivot的元素,在其前(before)或者后 | ||
(after)插入一个新的元素value | ‘linsert listkey before b java’是在元素b前插入java, 返回插入数据的长度 | ||
查找操作 | |||
lrange key start end |
lrange操作会获取列表指定索引范围所有的元素 | lrange listkey 0 -1 可以从左到右获取列表的所有元素 |
索引下标有两个特点:第一,索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。第二,lrange中的end选项包含了自身 |
lindex key index |
获取列表指定索引下标的元素 | lindex listkey -1 获取当前列表的最后一个元素 |
|
llen key |
获取列表长度 | ||
删除操作 | |||
lpop key |
从列表左侧弹出元素 | ||
rpop key |
从列表右侧弹出元素 | ||
lrem key count value |
从列表中找到等于value 的元素进行删除 |
根据count 的不同分为三种情况:·count >0,从左到右,删除最多count 个元素。·count <0,从右到左,删除最多count 绝对值个元素。·count =0,删除所有. |
|
ltrim key start end |
按照索引范围修剪列表 | ||
修改操作 | |||
lset key index newValue |
修改指定索引下标的元素 | ||
阻塞操作 | |||
brpop key [key …] timeout |
rpop 的阻塞版本 |
||
blpop key [key …] timeout |
lpop 的阻塞版本 |
阻塞操作的解释
以brpop
为例,brpop
命令包含两个参数:
key[key...]
:多个列表的键。timeout
:阻塞时间(秒)
1)若列表为空:如果timeout
=3,那么客户端要等到3秒后返回,如果timeout
=0,那么客户端一直阻塞等下去,阻塞期间如果列表添加了新数据导致不为空,则立刻返回列表右边的元素。
2)列表不为空:客户端会立即返回
在使用brpop时,有两点需要注意。
- 第一点,如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回;
- 第二点,如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值。
内部编码
列表类型的内部编码有两种:
ziplist
(压缩列表):当列表的元素个数小于list-max-ziplist-entries
配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value
配置时(默认64字节),Redis会选用ziplist
来作为列表的内部实现来减少内存的使用。linkedlist
(链表):当列表类型无法满足ziplist
的条件时,Redis会使用linkedlist
作为列表的内部实现。
此外Redis3.2版本提供了quicklist
内部编码,简单地说它是以一个ziplist
为节点的linkedlist
,它结合了ziplist
和linkedlist
两者的优势,为列表类型提供了一种更为优秀的内部编码实现。
使用场景
消息队列
Redis的lpush
+brpop
命令组合即可实现阻塞队列,生产者客户端使用lrpush
从列表左侧插入元素,多个消费者客户端使用brpop
命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
- 每篇文章使用哈希结构存储,例如每篇文章有3个属性title、timestamp、content
- 向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键
- 分页获取用户文章列表
使用列表类型保存和获取文章列表会存在两个问题。
- 如果每次分页获取的文章个数较多,需要执行多次
hgetall
操作,此时可以考虑使用Pipeline
批量获取,或者考虑将文章数据序列化为字符串类型,使用mget
批量获取。 - 分页获取文章列表时,
lrange
命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或者使用Redis3.2的quicklist
内部编码实现,它结合ziplist
和linkedlist
的特点,获取列表中间范围的元素时也可以高效完成。
开发提示
实际上列表的使用场景很多,在选择时可以参考以下口诀:
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpsh+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
集合
集合(set
)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
一个集合最多可以存储$2_32 -1$个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。
命令
command | 作用 | 返回值 | 说明 |
---|---|---|---|
集合内操作 | |||
sadd key element [element …] |
添加元素 | 返回结果为添加成功的元素个数 | |
srem key element [element …] |
删除元素 | 返回结果为成功删除元素个数 | |
scard key |
计算元素个数 | scard 的时间复杂度为O(1),它不会遍历集合所有元素,而是直接用Redis内部的变量 |
|
sismember key element |
判断元素是否在集合中 | 如果给定元素element在集合内返回1,反之返回0 | |
srandmember key [count] |
随机从集合中返回指定个数的元素 | [count]是可选参数,如果不写默认为1 | |
spop key |
从集合随机弹出元素 | srandmember 和spop 都是随机从集合选出元素,两者不同的是spop 命令执行后,元素会从集合中删除,而srandmember 不会 |
|
smembers key |
获取所有元素 | smembers 和lrange 、hgetall 都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,这时候可以使用sscan 来完成 |
|
集合间的操作 | |||
sinter key [key …] |
求多个集合的交集 | sinterstore destination key [key …] 是将取交集的结果保存到desination key |
|
sunion key [key …] |
求多个集合的并集 | sunionstore destination key [key …] 是将取并集的结果保存到desination key |
|
sdiff key [key …] |
求多个集合的差集 | sdiffstore destination key [key …] 是将取差集的结果保存到desination key |
内部编码
集合类型的内部编码有两种:
intset
(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries
配置(默认512个)时,Redis会选用intset
来作为集合的内部实现,从而减少内存的使用。hashtable
(哈希表):当集合类型无法满足intset
的条件时,Redis会使用hashtable
作为集合的内部实现。
使用场景
集合类型比较典型的使用场景是标签(tag
)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。
用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成的数据不一致。
开发提示
集合类型的应用场景通常为以下几种:
- sadd=Tagging(标签)
- spop/srandmember=Random item(生成随机数,比如抽奖)
- sadd+sinter=Social Graph(社交需求)
有序集合
有序集合它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。有序集合中的元素不能重复,但是score可以重复。
命令
command | 作用 | 返回值 | 说明 |
---|---|---|---|
集合内操作 | |||
zadd key score member [score member …] |
添加成员 | 返回结果代表成功添加成员的个数。有序集合相比集合提供了排序字段,但是也产生了代价,zadd 的时间 |
|
复杂度为$O(log(n))$,sadd的时间复杂度为$O(1)$ | Redis3.2为zadd命令添加了nx 、xx 、ch 、incr 四个选项:·nx :member 必须不存在,才可以设置成功,用于添加。·xx :member 必须存在,才可以设置成功,用于更新。·ch :返回此次操作后,有序集合元素和分数发生变化的个数·incr :对score 做增加,相当于后面介绍的zincrby |
||
zcard key |
计算成员个数 | ||
zscore key member |
计算某个成员的分数 | 如果成员不存在则返回nil |
|
zrank key member |
分数从低到高计算成员的排名 | ||
zrevrank key member |
分数从高到低计算成员的排名 | ||
zrem key member [member …] |
删除成员 | 返回结果为成功删除的个数 | |
zincrby key increment member |
增加成员的分数 | ||
zrange key start end [withscores] |
从低到高返回指定排名范围的成员 | 如果加上withscores选项,同时会返 | |
回成员的分数 | |||
zrevrange key start end [withscores] |
从高到低返回指定排名范围的成员 | 如果加上withscores选项,同时会返 | |
回成员的分数 | |||
zrangebyscore key min max [withscores] [limit offset count] |
返回指定分数范围的成员 | withscores选项会同时返回每个成员的分数。[limit offset count]选项可以限制输出的起始位置和个数 | |
zrevrangebyscore key min max [withscores] [limit offset count] |
返回指定分数范围的成员 | withscores选项会同时返回每个成员的分数。[limit offset count]选项可以限制输出的起始位置和个数 | |
zcount key min max |
返回指定分数范围成员个数 | ||
zremrangebyrank key start end |
删除指定排名内的升序元素 | 删除第start到第end名的成员 | |
zremrangebyscore key min max |
删除指定分数范围的成员 | ||
集合间操作 |
交集
zinterstore
destination numkeys key [key …] [weights weight [weight …]] [aggregate sum|min|max]
参数说明:
- destination:交集计算结果保存到这个键。
- numkeys:需要做交集计算键的个数。
- key[key…]:需要做交集计算的键。
- weights weight[weight…]:每个键的权重,在做交集计算时,每个键中的每个member会将自己分数乘以这个权重,每个键的权重默认是1。
- aggregate sum|min|max:计算成员交集后,分值可以按照sum(和)、min(最小值)、max(最大值)做汇总,默认值是sum。
并集:
zunionstore
destination numkeys key [key …] [weights weight [weight …]][aggregate sum|min|max]
该命令的所有参数和zinterstore是一致的,只不过是做并集计算。
内部编码
有序集合类型的内部编码有两种:
ziplist
(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries
配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value
配置(默认64字节)时,Redis会用ziplist
来作为有序集合的内部实现,ziplist
可以有效减少内存的使用。skiplist
(跳跃表):当ziplist
条件不满足时,有序集合会使用skiplist
作为内部实现,因为此时ziplist
的读写效率会下降。
使用场景
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。
重点回顾
1)Redis提供5种数据结构,每种数据结构都有多种内部编码实现。
2)纯内存存储、IO多路复用技术、单线程架构是造就Redis高性能的三个因素。
3)由于Redis的单线程架构,所以需要每个命令能被快速执行完,否则会存在阻塞Redis的可能,理解Redis单线程命令处理机制是开发和运维Redis的核心之一。
4)批量操作(例如mget
、mset
、hmset
等)能够有效提高命令执行的效率,但要注意每次批量操作的个数和字节数。
5)了解每个命令的时间复杂度在开发中至关重要,例如在使用keys
、hgetall
、smembers
、zrange
等时间复杂度较高的命令时,需要考虑数据规模对于Redis的影响。
6)persist
命令可以删除任意类型键的过期时间,但是set
命令也会删除字符串类型键的过期时间,这在开发时容易被忽视。
7)move
、dump
+restore
、migrate
是Redis发展过程中三种迁移键的方式,其中move
命令基本废弃,migrate
命令用原子性的方式实现了dump
+restore
,并且支持批量操作,是Redis Cluster实现水平扩容的重要工具。
8)scan
命令可以解决keys命令可能带来的阻塞问题,同时Redis还提供了hscan
、sscan
、zscan
渐进式地遍历hash
、set
、zset
。
大佬加油,拿下自己喜欢的offer
谢谢\~,不过离大佬还差很远哈哈哈哈\~\~\~