Redis

什么是Redis

  1. Redis是高性能的基于Key-Value的结构存储的开源数据库

  2. 目前市面上绝大部分公司都使用Redis来实现分布式缓存,从而提高数据检索效率

  3. Redis之所以这么流行主要有以下几个特点

    1. 基于内存存储,在进行数据I/O操作时候能达到一个非常高的QPS(Query Per Second)每秒查询率,官方提供的指标是10万
    2. 它提供了非常丰富的数据存储结构,比如:strings、lists、sets、sorted sets、hashes、bit arrays等数据类型
    3. redis低层使用的是单线程实现的数据I/O,在数据算法层面并不需要考虑并发的安全性,从而导致低层算法它的时间复杂度基本属于常量复杂度
    4. 虽然redis是基于内存存储,但是它也支持持久化,避免因为服务器故障导致数据丢失的问题

    基于这些特点,reids一般是用来实现分布式缓存从而降低应用程序对关系型数据库检索带来性能的影响,除此之外,redis还可以实现分布式锁,分布式队列,排行榜,查找附近的人等功能,为复杂应用提供非常方便成熟的解决方案

使用

Redis keys

Redis key值是二进制安全的,这意味着可以用任何二进制序列作为key值,从形如”foo”的简单字符串到一个JPEG文件的内容都可以。空字符串也是有效key值。

关于key的几条规则:

  • 太长的键值不是个好主意,例如1024字节的键值就不是个好主意,不仅因为消耗内存,而且在数据中查找这类键值的计算成本很高。
  • 太短的键值通常也不是好主意,如果你要用”u:1000:pwd”来代替”user:1000:password”,这没有什么问题,但后者更易阅读,并且由此增加的空间消耗相对于key object和value object本身来说很小。当然,没人阻止您一定要用更短的键值节省一丁点儿空间。
  • 最好坚持一种模式。例如:”object-type🆔field”就是个不错的注意,像这样”user:1000:password”。我喜欢对多单词的字段名中加上一个点,就像这样:”comment🔢reply.to”。

Redis Strings

这是最简单Redis类型。如果你只用这种类型,Redis就像一个可以持久化的memcached服务器(注:memcache的数据仅保存在内存中,服务器重启后,数据将丢失)。

set get

redis 127.0.0.1:6379> set user:1:name zhangsan
OK
redis 127.0.0.1:6379> get user:1:name
"zhangsan"
redis 127.0.0.1:6379>

上面提供了简单的字符操作,下面是自增操作:

incr incrby

redis 127.0.0.1:6379> incr user:1:age
(integer) 1
redis 127.0.0.1:6379> get user:1:age
"1"
redis 127.0.0.1:6379> incrby user:1:age 5
(integer) 6
redis 127.0.0.1:6379> get user:1:age
"6"

以及自减操作:

decr decrby

redis 127.0.0.1:6379> get user:1:age
"6"
redis 127.0.0.1:6379> decr user:1:age
(integer) 5
redis 127.0.0.1:6379> get user:1:age
"5"
redis 127.0.0.1:6379> decrby user:1:age 2
(integer) 3
redis 127.0.0.1:6379> get user:1:age
"3"

getset 行如其名:他为key设置新值并且返回原值。这有什么用处呢?例如:你的系统每当有新用户访问时就用INCR命令操作一个Redis key。你希望每小时对这个信息收集一次。你就可以GETSET这个key并给其赋值0并读取原值。

以下是:自增或自减后,返回之前的旧值,并置零

redis 127.0.0.1:6379> set user:1:age 27
OK
redis 127.0.0.1:6379> incr user:1:age
(integer) 28
redis 127.0.0.1:6379> getset user:1:age 0
"28"
redis 127.0.0.1:6379> get user:1:age
"0"

为减少等待时间,也可以一次存储或获取多个key对应的值,使用MSETMGET命令:

mset mget

redis 127.0.0.1:6379> get user:1:age
"0"
redis 127.0.0.1:6379> mset user:2:age 23 user:3:age 29
OK
redis 127.0.0.1:6379> mget user:1:age user:2:age user:3:age
1) "0"
2) "23"
3) "29"

使用EXISTS命令返回1或0标识给定key的值是否存在:exists

redis 127.0.0.1:6379> exists user:1:age
(integer) 1

使用DEL命令可以删除key对应的值,DEL命令返回1或0标识值是被删除(值存在)或者没被删除(key对应的值不存在):del

redis 127.0.0.1:6379> del user:1:age
(integer) 1
redis 127.0.0.1:6379> exists user:1:age
(integer) 0

TYPE命令可以返回key对应的值的存储类型:type

redis 127.0.0.1:6379> exists user:1:name
(integer) 0
redis 127.0.0.1:6379> type user:1:name
none
redis 127.0.0.1:6379> set user:1:name zhangsan
OK
redis 127.0.0.1:6379> type user:1:name
string
redis 127.0.0.1:6379> del user:1:name
(integer) 1
redis 127.0.0.1:6379> type user:1:name
none

Redis超时:数据在限定时间内存活

在介绍复杂类型前我们先介绍一个与值类型无关的Redis特性:超时。你可以对key设置一个超时时间,当这个时间到达后会被删除。精度可以使用毫秒或秒。

两行代码设置过期时间:expire key

redis 127.0.0.1:6379> set user:1:gf liuyifei
OK
redis 127.0.0.1:6379> expire user:1:gf 5
(integer) 1
redis 127.0.0.1:6379> get user:1:gf
"liuyifei"
redis 127.0.0.1:6379> get user:1:gf
(nil)

一行代码设置过期时间:set key 100 ex 10

redis 127.0.0.1:6379> set user:1:gf liuyifei ex 10
OK
redis 127.0.0.1:6379> get user:1:gf
"liuyifei"

TTL命令用来查看key对应的值剩余存活时间:ttl

redis 127.0.0.1:6379> set user:1:gf liuyifei ex 10
OK
redis 127.0.0.1:6379> ttl user:1:gf
(integer) 6
redis 127.0.0.1:6379> ttl user:1:gf
(integer) -1
redis 127.0.0.1:6379> get user:1:gf
(nil)

Redis Lists

Redis lists基于Linked Lists实现。

这意味着即使在一个list中有数百万个元素,在头部或尾部添加一个元素的操作,其时间复杂度也是常数级别的。用LPUSH 命令在十个元素的list头部添加新元素,和在千万元素list头部添加新元素的速度相同。

那么,坏消息是什么?

在数组实现的list中利用索引访问元素的速度极快,而同样的操作在linked list实现的list上没有那么快。

Redis Lists用linked list实现的原因:

对于数据库系统来说,至关重要的特性是:能非常快的在很大的列表上添加元素。另一个重要因素是,正如你将要看到的:Redis lists能在常数时间取得常数长度。

如果快速访问集合元素很重要,建议使用可排序集合(sorted sets)。

Redis lists 入门

LPUSH 命令可向list的左边(头部)添加一个新元素,而RPUSH命令可向list的右边(尾部)添加一个新元素。最后LRANGE 命令可从list中取出一定范围的元素:lpush rpush lrange

redis 127.0.0.1:6379> rpush gfList liuyifei
(integer) 1
redis 127.0.0.1:6379> rpush gfList anglebaby
(integer) 2
redis 127.0.0.1:6379> lpush gfList gulinazha
(integer) 3
redis 127.0.0.1:6379> lrange gfList 0 -1
1) "gulinazha"
2) "liuyifei"
3) "anglebaby"

注意:LRANGE 带有两个索引,一定范围的第一个和最后一个元素。这两个索引都可以为负来告知Redis从尾部开始计数,因此-1表示最后一个元素,-2表示list中的倒数第二个元素,以此类推。

上面的所有命令的参数都可变,方便你一次向list存入多个值。

使用rpush批量存入数据:

redis 127.0.0.1:6379> rpush gfList liuyifei gulinazha anjilababby
(integer) 3
redis 127.0.0.1:6379> lrange gfList 0 -1
1) "liuyifei"
2) "gulinazha"
3) "anjilababby"

还有一个重要的命令是pop,它从list中删除元素并同时返回删除的值。可以在左边或右边操作。

lpop从左边开始删除数据 rpop从右边开始删除数据

redis 127.0.0.1:6379> rpush list 0 1 2 3 4 5 0 1 2 3 4 5
(integer) 12
redis 127.0.0.1:6379> lrange list 0 -1
 1) "0"
 2) "1"
 3) "2"
 4) "3"
 5) "4"
 6) "5"
 7) "0"
 8) "1"
 9) "2"
10) "3"
11) "4"
12) "5"
redis 127.0.0.1:6379> rpop list
"5"
redis 127.0.0.1:6379> rpop list
"4"
redis 127.0.0.1:6379> lpop list
"0"
redis 127.0.0.1:6379> lpop list
"1"

List的常用案例

正如你可以从上面的例子中猜到的,list可被用来实现聊天系统。还可以作为不同进程间传递消息的队列。关键是,你可以每次都以原先添加的顺序访问数据。这不需要任何SQL ORDER BY 操作,将会非常快,也会很容易扩展到百万级别元素的规模。

例如在评级系统中,比如社会化新闻网站 reddit.com,你可以把每个新提交的链接添加到一个list,用LRANGE可简单的对结果分页。

在博客引擎实现中,你可为每篇日志设置一个list,在该list中推入博客评论,等等。

Capped lists

可以使用LTRIM把list从左边截取指定长度。

redis 127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "3"
3) "4"
4) "5"
5) "0"
6) "1"
7) "2"
8) "3"
redis 127.0.0.1:6379> ltrim list 0 2
OK
redis 127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "3"
3) "4"

List上的阻塞操作

BRPOP 是一个阻塞的列表弹出原语。 它是 RPOP 的阻塞版本,因为这个命令会在给定list无法弹出任何元素的时候阻塞连接。 该命令会按照给出的 key 顺序查看 list,并在找到的第一个非空 list 的尾部弹出一个元素。

redis 127.0.0.1:6379> rpush list1 1 2 3
(integer) 3
redis 127.0.0.1:6379> lpush list2 1 2 3
(integer) 3
redis 127.0.0.1:6379> brpop list1 list2 0
1) "list1"
2) "3"
redis 127.0.0.1:6379> blpop list1 list2 0
1) "list1"
2) "1"
redis 127.0.0.1:6379> brpop list1 list2 0
1) "list1"
2) "2"
redis 127.0.0.1:6379> brpop list1 list2 0
1) "list2"
2) "1"
redis 127.0.0.1:6379> brpop list1 list2 0
1) "list2"
2) "2"
redis 127.0.0.1:6379> brpop list1 list2 0
1) "list2"
2) "3"

简单点说就是上面先提供了两个list,通过brpop来删除list的值的话 会先根据你提供的list1和list2,先去删除list1中的内容之后再去删除list2中的内容

key 的自动创建和删除

目前为止,在我们的例子中,我们没有在推入元素之前创建空的 list,或者在 list 没有元素时删除它。在 list 为空时删除 key,并在用户试图添加元素(比如通过 LPUSH)而键不存在时创建空 list,是 Redis 的职责。

这不光适用于 lists,还适用于所有包括多个元素的 Redis 数据类型 – Sets, Sorted Sets 和 Hashes。

基本上,我们可以用三条规则来概括它的行为:

  1. 当我们向一个聚合数据类型中添加元素时,如果目标键不存在,就在添加元素前创建空的聚合数据类型。

    redis 127.0.0.1:6379> exists list
    (integer) 1
    redis 127.0.0.1:6379> exists nullData
    (integer) 0
    redis 127.0.0.1:6379> rpush nullData 1 2 3
    (integer) 3
    redis 127.0.0.1:6379> lrange nullData 0 -1
    1) "1"
    2) "2"
    3) "3"
    
  2. 当我们从聚合数据类型中移除元素时,如果值仍然是空的,键自动被销毁。

    redis 127.0.0.1:6379> exists nullData
    (integer) 1
    redis 127.0.0.1:6379> lrange nullData 0 -1
    1) "1"
    2) "2"
    3) "3"
    redis 127.0.0.1:6379> lpop nullData
    "1"
    redis 127.0.0.1:6379> lpop nullData
    "2"
    redis 127.0.0.1:6379> lpop nullData
    "3"
    redis 127.0.0.1:6379> lpop nullData
    (nil)
    redis 127.0.0.1:6379> exists nullData
    (integer) 0
    
  3. 对一个空的 key 调用一个只读的命令,比如 LLEN (返回 list 的长度),或者一个删除元素的命令,将总是产生同样的结果。该结果和对一个空的聚合类型做同个操作的结果是一样的。

    redis 127.0.0.1:6379> lrange list 0 -1
     1) "2"
     2) "3"
     3) "4"
     4) "5"
     5) "0"
     6) "1"
     7) "2"
     8) "3"
     9) "a"
    10) "b"
    11) "c"
    redis 127.0.0.1:6379> llen list
    (integer) 11
    

    Redis Hashes

    Redis hash ,由键值对组成:hmset hget hgetall

    redis 127.0.0.1:6379> hmset user:1 name zhangsan age 18 salary 2000
    OK
    redis 127.0.0.1:6379> hget user:1 name
    "zhangsan"
    redis 127.0.0.1:6379> hget user:1 age
    "18"
    redis 127.0.0.1:6379> hget user:1 salary
    "2000"
    redis 127.0.0.1:6379> hgetall user:1
    1) "name"
    2) "zhangsan"
    3) "age"
    4) "18"
    5) "salary"
    6) "2000"
    

    Hash 便于表示 objects,实际上,你可以放入一个 hash 的域数量实际上没有限制(除了可用内存以外)。

    HMSET 指令设置 hash 中的多个域,而 HGET 取回单个域。HMGET 和 HGET 类似,但返回一系列值:

    redis 127.0.0.1:6379> hmget user:1 name age salary nothing
    1) "zhangsan"
    2) "18"
    3) "2000"
    4) (nil)
    

    也有一些指令能够对单独的域执行操作,比如 HINCRBY

    redis 127.0.0.1:6379> hincrby user:1 age 1
    (integer) 19
    redis 127.0.0.1:6379> hincrby user:1 age 1
    (integer) 20
    

    Redis Sets

    Redis Set 是 String 的无序排列。SADD 指令把新的元素添加到 set 中。对 set 也可做一些其他的操作,比如测试一个给定的元素是否存在,对不同 set 取交集,并集或差,等等。

    redis 127.0.0.1:6379> exists set1
    (integer) 0
    redis 127.0.0.1:6379> sadd set1 1 2 3
    (integer) 3
    redis 127.0.0.1:6379> exists set1
    (integer) 1
    redis 127.0.0.1:6379> smembers set1
    1) "1"
    2) "2"
    3) "3"
    

    Redis 有检测成员的指令。检测一个特定的元素是否存在:

    redis 127.0.0.1:6379> sismember set1 1
    (integer) 1
    redis 127.0.0.1:6379> sismember set1 4
    (integer) 0
    

    Sets 适合用于表示对象间的关系。 例如,我们可以轻易使用 set 来表示标记。

    一个简单的建模方式是,对每一个希望标记的对象使用 set。这个 set 包含和对象相关联的标签的 ID。

    假设我们想要给新闻打上标签。 假设新闻 ID 1000 被打上了 1,2,5 和 77 四个标签,我们可以使用一个 set 把 tag ID 和新闻条目关联起来:

    redis 127.0.0.1:6379> sadd news:1000:tags 1 2 4 5 8 9 11 15
    (integer) 8
    redis 127.0.0.1:6379> smembers news:1000:tags
    1) "1"
    2) "2"
    3) "4"
    4) "5"
    5) "8"
    6) "9"
    7) "11"
    8) "15"
    

    但是,有时候我可能也会需要相反的关系:所有被打上相同标签的新闻列表:

    redis 127.0.0.1:6379> sadd tag:1:news 1000
    (integer) 1
    redis 127.0.0.1:6379> sadd tag:2:news 1000
    (integer) 1
    redis 127.0.0.1:6379> sadd tag:4:news 1000
    (integer) 1
    redis 127.0.0.1:6379> sadd tag:5:news 1000
    (integer) 1
    redis 127.0.0.1:6379> sadd tag:8:news 1000
    (integer) 1
    redis 127.0.0.1:6379> sadd tag:9:news 1000
    (integer) 1
    redis 127.0.0.1:6379> sadd tag:11:news 1000
    (integer) 1
    redis 127.0.0.1:6379> sadd tag:15:news 1000
    (integer) 1
    

    获取一个对象的所有 tag 是很方便的:

    redis 127.0.0.1:6379> smembers news:1000:tags
    1) "1"
    2) "2"
    3) "4"
    4) "5"
    5) "8"
    6) "9"
    7) "11"
    8) "15"
    

    注意:在这个例子中,我们假设你有另一个数据结构,比如一个 Redis hash,把标签 ID 对应到标签名称。

    使用 Redis 命令行,我们可以轻易实现其它一些有用的操作。比如,我们可能需要一个含有 1, 2, 10, 和 27 标签的对象的列表。我们可以用 SINTER 命令来完成这件事。它获取不同 set 的交集。我们可以用:

    redis 127.0.0.1:6379> sinter tag:1:news tag:2:news tag:4:news
    1) "1000"
    

    不光可以取交集,还可以并集,差集,获取随时取元素,等等。

    获取元素的命令是SPOP,它很适合针对特定的建模。例如,要实现一个基于网络的系列游戏,你可能会来表示一副牌让我们用一个类似的设置来表示不同的花色。

    sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3 H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 SJ SQ SK
    

    执行完上面的命令便生成了52张牌,

    由于上面的一幅牌打完后就没有了,这边使用sunionstore 将牌复制一份出来,每局过后从新复制一份出来

    redis 127.0.0.1:6379> sunionstore game:1:deck deck
    (integer) 52
    

    现在,我已经准备好给1号玩家发五张牌了:

    redis 127.0.0.1:6379> spop game:1:deck
    "D9"
    redis 127.0.0.1:6379> spop game:1:deck
    "S3"
    redis 127.0.0.1:6379> spop game:1:deck
    "H2"
    redis 127.0.0.1:6379> spop game:1:deck
    "H4"
    redis 127.0.0.1:6379> spop game:1:deck
    "C7"
    

    这次分的牌不好,只有一个2,还有四张散牌。

    现在是介绍 set 命令的好时机,该命令提供集合内元素的数量。这在集合论的上下文中通常被称为集合的基数 ,因此 Redis 命令被称为SCARD

    redis 127.0.0.1:6379> scard game:1:deck
    (integer) 47
    

    我们看到现在没发的牌还有47(52 - 5)张。

    当您只需要获取随机元素而不将它们从集合中删除时,可以使用SRANDMEMBER适合该任务的命令。它还具有返回重复和非重复元素的能力。

    redis 127.0.0.1:6379> scard game:1:deck
    (integer) 47
    redis 127.0.0.1:6379> srandmember game:1:deck
    "D6"
    redis 127.0.0.1:6379> srandmember game:1:deck
    "D1"
    redis 127.0.0.1:6379> scard game:1:deck
    (integer) 47
    

    Redis Sorted sets

    排序集是一种数据类型,类似于 Set 和 Hash 的混合。与集合一样,有序集合由唯一的、不重复的字符串元素组成,因此在某种意义上,有序集合也是一个集合。

    让我们从一个简单的例子开始,添加一些选定的黑客姓名作为排序的集合元素,他们的出生年份作为“分数”。

    redis 127.0.0.1:6379> zadd hackers 1989 datuzi
    (integer) 1
    redis 127.0.0.1:6379> zadd hackers 1987 Fetag
    (integer) 1
    redis 127.0.0.1:6379> zadd hackers 1977 wantao
    (integer) 1
    redis 127.0.0.1:6379> zadd hackers 1973 glacier
    (integer) 1
    redis 127.0.0.1:6379> zadd hackers 1985 xiaorong
    (integer) 1
    redis 127.0.0.1:6379> zadd hackers 1983 Goodwill
    (integer) 1
    redis 127.0.0.1:6379> zadd hackers 1988 gududejianke
    (integer) 1
    redis 127.0.0.1:6379> zadd hackers 1984 leijun
    (integer) 1
    

    如您所见,ZADD它类似于SADD,但需要一个额外的参数(放置在要添加的元素之前),即分数。 ZADD也是可变参数,因此您可以自由指定多个分值对,即使在上面的示例中没有使用它。

    使用排序集,返回按出生年份排序的黑客列表是微不足道的,因为实际上他们已经排序

    实现说明:排序集是通过包含跳过列表和哈希表的双端口数据结构实现的,因此每次添加元素时,Redis 都会执行 O(log(N)) 操作。这很好,但是当我们要求排序的元素时,Redis 根本不需要做任何工作,它已经全部排序了:

    redis 127.0.0.1:6379> zrange hackers 0 -1
    1) "glacier"
    2) "wantao"
    3) "Goodwill"
    4) "leijun"
    5) "xiaorong"
    6) "Fetag"
    7) "gududejianke"
    8) "datuzi"
    

    注意:0 和 -1 表示从元素索引 0 到最后一个元素(-1 在这里的工作方式与LRANGE命令的情况一样)。

    如果我想以相反的顺序排序呢,年龄从小到大排序。使用ZREVRANGE代替ZRANGE

    redis 127.0.0.1:6379> zrevrange hackers 0 -1
    1) "datuzi"
    2) "gududejianke"
    3) "Fetag"
    4) "xiaorong"
    5) "leijun"
    6) "Goodwill"
    7) "wantao"
    8) "glacier"
    

    WITHSCORES也可以使用以下参数返回分数:

    redis 127.0.0.1:6379> zrange hackers 0 -1 withscores
     1) "glacier"
     2) "1973"
     3) "wantao"
     4) "1977"
     5) "Goodwill"
     6) "1983"
     7) "leijun"
     8) "1984"
     9) "xiaorong"
    10) "1985"
    11) "Fetag"
    12) "1987"
    13) "gududejianke"
    14) "1988"
    15) "datuzi"
    16) "1989"
    

    在范围内操作

    排序集比这更强大。他们可以在范围内操作。让我们把所有出生到 1950 年的人都包括在内。我们使用ZRANGEBYSCORE命令来做到这一点:

    redis 127.0.0.1:6379> zrangebyscore hackers -inf 1980
    1) "glacier"
    2) "wantao"
    (1.65s)