Redis 总结

Redis是什么

RedisRemote Dictionary Service远程字典服务)是一个开源的使用C语言编写、支持网络、基于内存、可持久化的日志型、key-value型的非关系数据库。

Redis使用场景

  1. 缓存

减轻数据库(如MySQL)的查询压力,提升系统性能。

  1. 排行榜

利用RedisSorted Set(有序集合)实现。

  1. 好友关系

利用RedisSet(集合)的操作命令实现,比如交集、并集、差集等。可以方便实现共同好友、共同爱好等功能。

  1. 计数器/限速器

利用Redis中原子性的递增操作,可以统计用户点赞数、用户访问数等。
限速器的典型使用场景是限制访问某个API的频率,比如抢购。

  1. 消息队列

除了Redis自身支持的发布/订阅模式,还可以利用List数据类型实现一个队列,达到异步解耦的作用。

  1. Session共享

Session是保存在服务端的数据,在分布式环境下,同一用户的多次请求可能落在不同的机器上,会造成用户多次登录,使用Redis统一保存Session后,无论在哪个服务器上处理用户请求,都可以获取对应的Session信息。

Redis的数据类型

Redis不是一个普通的键值存储,实际上是一个数据结构服务器,支持不同类型的值。下面介绍一下常用的五种数据结构:

  • 字符串
  • 列表:根据插入顺序排序的字符串元素的集合
  • 集合:唯一的、无序的字符串元素的集合
  • 有序集合:类似于集合,但其中每个字符串元素都与一个浮点值相关联,称为score。元素总是按照它们的score排序,所以与集合不同。它可以检索一系列元素(比如前10名,或后10名)
  • 哈希:它是由与值相关联的字段组成的映射。字段和值都是字符串

下面在介绍值的数据类型的同时,还会介绍key的一些知识点

Redis的key(键)

Rediskey是二进制安全的,这意味着可以使用任何二进制序列作为key,从像‘foo’这样的字符串到JPEG文件的内容。空字符串也是一个有效的key

key的其它规则:

  • 不推荐很长的key。例如,1024字节的key不仅在内存上有很大的消耗,而且在数据集中查找key可能需要多次key比较造成不必要的开销。实在太长的key,可以尝试对它进行hash运算(例如SHA1
  • 不推荐很短的key。比如为了减小key占的内存,将“user:1000:flowers”改为“u1000flw”就没有什么意义了,前者更具可读性,而且与键和值本身所占空间相比,这点大小微不足道。虽然更短的key会消耗更少的内存,但应该在使用时找到一个平衡点
  • 尝试使用分层模式(stick with schema)。例如“object-type:id”,实际使用时如“user:1000”。点或破折号通常用于多词字段。如“comment:12345:reply.to”或“comment:12345:reply-to
  • key的最大大小为512MB

Strings(字符串)

字符串类型是Redis最基础的数据结构。键和值都为字符串。

> set mykey somevalue
OK
> get mykey
"somevalue"

如上,使用setget命令是设置和获取String类型的主要方式,值可以是各种字符串(包括二进制数据),例如可以在值中存储JPEG图像。值不能大于512MB

Stringset命令还可以带有一些选项,作为附加参数。比如,如果键已存在,要求set失败;或者相反,只有当键存在时set才成功

> set mykey newval nx
(nil)
> set mykey newval xx
OK

即使字符串是String类型存值的基本类型,也可以进行一些其它操作,比如:原子增量

> set counter 100
OK
> incr counter
(integer) 101
> incr counter
(integer) 102
> incrby counter 50
(integer) 152

incr命令将一个String类型的value解析为一个整数类型,并获取其增量,最终将得到的值作为新的值设置。类似的命令还有incrbydecrdecrby

incr的原子性意味着什么呢?即使多个客户端针对同一个key发出incr也永远不会进入竞争条件。例如,永远不会发生客户端1读取到值为10客户端2同时读取到值为10,两者都进行incr命令操作,最终值将为12

还有一些其它操作字符串的命令。例如,getset将键设置为新值,并返回旧值作为结果。

在单个命令中设置或获取多个键的值可以用来减少延迟。即mgetmset命令:

> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"

当使用mget时,Redis返回一个值数组。

更改和查询key

Redis有些命令没有在特定数据类型上定义,但在进行key的操作时很实用,因此可以与任何类型的key一起使用。
例如,exists命令返回1或0表示数据库中是否存在指定的key,而del命令删除key和其关联的value

> set mykey hello
OK
> exists mykey
(integer) 1
> del mykey
(integer) 1
> exists mykey
(integer) 0

从上面的命令中,还可以看出del命令也可以返回10,这取决于key是否已经被删除。与key相关的命令有很多,但existsdel是必不可少的,还有type命令,返回与key关联的value的类型:

> set mykey x
OK
> type mykey
string
> del mykey
(integer) 1
> type mykey
none

Redis expires(过期):有生存时间的key

另一个无论value是任何类型都有效的特性,就是Redis expires(过期)。即可以为key设置超时时间,或者说有限的生存时间。当生存时间结束时,key会自动销毁,就像执行del命令一样。

关于Redis expires的一些规则:

  • 可以使用秒或者毫秒进行精度设置
  • 但是过期时间的计算始终为毫秒
  • 有关过期的信息被复制并保存在磁盘上,当Redis保持停止状态时,过期时间已经结束了(这意味着Redis会保存key的过期时间)
> set key some-value
OK
> expire key 5
(integer) 1
> get key (immediately)
"some-value"
> get key (after some time)
(nil)

如上,key在两次get调用之间消失了,因为第二次调用延迟了5秒以上。在上面的命令中,使用expire来设置过期时间(expire还可以用来为已经有过期时间的key设置不同的过期时间,还可以使用persist命令来删除过期时间让key持久化)。也可以创建带有过期时间的key,例如:

> set key 100 ex 10
OK
> ttl key
(integer) 9

使用ttl命令来检查key的过期时间。

Lists(列表)

Redis的列表是通过链表实现的。这意味着即使列表中有数百万个元素,在列表的头部或尾部添加新的元素的操作也是在常数时间内进行的。用lpush命令在10个元素的列表头添加一个新元素与在一个有10000个元素的列表头添加一个元素的速度是一样的。
链表实现的列表有什么缺点吗?在使用数组实现的列表中通过索引访问元素非常快,而在由链表实现的列表中则没有那么快。

前面提到的lpush命令将一个元素添加到列表左侧(头部),而rpush命令可以将一个元素添加到列表右侧(尾部)。lrange命令可以从列表中通过索引范围提取元素:

> rpush mylist A
(integer) 1
> rpush mylist B
(integer) 2
> lpush mylist first
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"

请注意,lrange命令需要两个索引,即需要返回的范围的第一个和最后一个元素。两个索引都可以是负数,表示从末尾开始计数:所以-1表示最后一个元素,-2表示倒数第二个元素,以此类推。

lpushrpush都可以在一次调用中将多个元素推送到列表中:

> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "foo bar"

Redis的列表有一个重要的操作是弹出元素。弹出元素是同时从列表中检索元素和从列表中删除元素的操作。可以从左侧和右侧弹出元素,类似于在两侧推送元素:

> rpush mylist a b c
(integer) 3
> rpop mylist
"c"
> rpop mylist
"b"
> rpop mylist
"a"

上面的命令添加了三个元素,同时弹出了三个元素,所以现在这个列表是空的,此时如果尝试从列表中弹出元素时,就返回空表示列表中没有元素:

> rpop mylist
(nil)

列表的常见用例

列表可以完成很多操作,下面两个是比较有代表性的:

  • 记住用户发布到社交网络的最新更新
  • 进程之间的通信,即消费者-生产者模式

Twitter社交网络将用户发布的最新推文放在列表中。描述一个常见的栗子:假设用户的社交网络主页显示了发布的最新照片,此时需要加快访问速度时,可以这样操作:

  • 每次用户发布新照片时,都将其ID添加到列表中
  • 当用户访问主页时,使用lrange 0 9来获取最新发布的10个照片

上限列表

Redis可以使用列表来存储最新的项目,无论具体是什么:发布的新照片或其它东西。
可以使用列表作为上限集合,即只保存最新的N项并使用ltrim命令丢弃最旧的项。
ltrim表示删除给定范围之外的所有元素:

> rpush mylist 1 2 3 4 5
(integer) 5
> ltrim mylist 0 2
OK
> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"

列表的阻塞操作

列表有一个特殊的特性,使它适合实现队列,并且通常作为进程间通信:阻塞操作。
比如一个简单的生产者-消费者模式,可以通过以下方式实现:

  • 生产者调用lpush将消息推送到列表中
  • 消费者调用rpop获取并消费消息

然而,列表有时候是空的,所以rpop只能返回null。在这种情况下,消费者需要不断的尝试获取直到有返回值,这并不是一个好办法。

因此,Redis提供了brpopblpop命令,它们是rpoplpop的阻塞版本,只有在列表中添加了新元素或达到指定的获取超时时间,阻塞命令才会返回值。

> brpop tasks 5
1) "tasks"
2) "do_something"

上面的命令表示等待tasks列表中的元素,如果5秒后没有可用元素则返回。
请注意:可以使用0作为超时时间来永远等待列表中的元素,也可以指定多个列表,以同时等待多个列表,直到有一个列表收到元素时得到通知。

Hashes(哈希)

Redishashes数据类型看起来与用户期望的“hash”完全一样,具有字段值对:

> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"

hashes可以很方便的表示对象,而且字段数量没有限制。
hmset命令可以设置hashes的多个字段,hget命令用来检索单个字段。hmget命令类似于hget,返回一组值:

> hmget user:1000 username birthyear no-such-field
1) "antirez"
2) "1977"
3) (nil)

有些命令可以对单个字段进行操作,例如hincrby

> hincrby user:1000 birthyear 10
(integer) 1987
> hincrby user:1000 birthyear 10
(integer) 1997

Sets(集合)

Sets是无序的字符串集合。sadd命令可以添加一个或一组元素到集合中。还可以对集合执行其它许多操作,比如检查给定元素是否存在(sismember),执行多个集合之间的交集(sinter)、并集(sunion)、差集(sdiff)等。

> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2

上面命令表示在集合中添加了三个元素,并使用smembers命令返回所有集合中的所有元素。正如上面显示的,这些元素没有排序,在每次调用时可能以任何顺序返回元素。

还可以使用sismember命令检查元素是否存在:

> sismember myset 3
(integer) 1
> sismember myset 30
(integer) 0

集合有利于表达对象之间的关系。例如,可以使用集合来实现标签。具体做法是:
为想要标记的对象设置一个集合,该集合包含与对象关联的标签的ID。比如标记新闻文章,如果文章ID 1000被标记为标签1、2、5、77,则集合可以将这些标签ID与新闻文章相关联:

> sadd news:1000:tags 1 2 5 77
(integer) 4

并获取新闻文章的所有标签:

> smembers news:1000:tags
1. 5
2. 1
3. 77
4. 2

集合中提取元素的命令为spop(随机弹出一个并从Redis中删除),可以很方便的对某些问题进行建模。例如,为了实现基于Web的扑克游戏,需要一组元素来代表一套扑克牌。想象一下,我们对(C)lubs、(D)iamonds、(H)earts、(S)pades使用第一个字符作为前缀:

>  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
   (integer) 52

现在为每位玩家提供5张牌。spop命令随机弹出并删除一个元素,所以完美适用于这种情况。
然而,如果直接对这组牌调用这个命令,那么在下一次游戏中需要重新填充一组牌,这可能并不是最好的办法。因此,我们可以先把存在key=deck中的集合复制到key=game:1:deck的集合中;
可以使用命令sunionstore来完成复制操作,它通常执行多个集合之间的并集,并将结果存储在另一个集合中。由于单个集合的并集就是其本身,所以可以实现复制:

> sunionstore game:1:deck deck
(integer) 52

现在准备为第一位玩家提供5张牌:

> spop game:1:deck
"C6"
> spop game:1:deck
"CQ"
> spop game:1:deck
"D1"
> spop game:1:deck
"CJ"
> spop game:1:deck
"SJ"

还可以使用命令scard获取集合中元素的数量:

> scard game:1:deck
(integer) 47

如果需要获取元素而不将它们从集合中删除时,可以使用命令srandmember(随机返回一个值),它还具有返回重复和非重复元素的功能。

Sorted sets(有序集合)

有序集合是一种类似于集合和哈希混合的数据类型。与集合一样,有序集合由唯一的、不重复的字符串元素组成,因此有序集合也算是一种特殊的集合。
然而,集合中的元素没有排序,有序集合中的每个元素都与一个浮点数值相关联,称为score(这就是为什么说有序集合类似于哈希,因为每个元素都映射到一个值)。
此外,有序集合中的元素是按顺序获取的,排序规则如下:

  • 如果AB是具有不同score的元素,且A.score > B.score,则A > B
  • 如果ABscore相同,则按照字符串排序,如A的字符串顺序大于B的字符串顺序,则A > BAB字符串不能相同,因为有序集合中的元素是唯一的

举个栗子来介绍有序集合的操作命令,添加一些用户名称作为有序集合的元素,将他们的出生年份作为score

> zadd hackers 1940 "Alan Kay"
(integer) 1
> zadd hackers 1957 "Sophie Wilson"
(integer) 1
> zadd hackers 1953 "Richard Stallman"
(integer) 1
> zadd hackers 1949 "Anita Borg"
(integer) 1
> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
> zadd hackers 1916 "Claude Shannon"
(integer) 1
> zadd hackers 1969 "Linus Torvalds"
(integer) 1
> zadd hackers 1912 "Alan Turing"
(integer) 1

如上,命令zadd类似于sadd添加元素到有序集合中,但需要一个额外的参数(放置在要添加的元素之前),即scorezadd命令也可以一次添加多个元素。
在上面的栗子中,使用有序集合返回一个按照出生年份排序的列表是非常简单的,因为实际上元素已经排好序了。

Sorted sets(有序集合)是通过一个双端数据结构实现的,包含一个skip list(跳表)和一个hash table(hash数组)。

使用命令zrange获取所有排序元素:

> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"

注意:0-1表示从索引为0的元素到最后一个元素(-1的作用和列表中命令lrange的情况一样)。
如果想以相反的排序方式获取元素时,可以使用命令zrevrange

> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"

在使用命令zrangezrevrange时,可以利用参数withscoresscore一起返回:

> zrange hackers 0 -1 withscores
1) "Alan Turing"
2) "1912"
3) "Hedy Lamarr"
4) "1914"
5) "Claude Shannon"
6) "1916"
7) "Alan Kay"
8) "1940"
9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"

在范围内操作

有序集合还有更加强大的功能,可以在范围(score)内操作。利用上面的栗子获取所有出生年份早于1950(包含)的元素。使用zrangebyscore命令:

> zrangebyscore hackers -inf 1950
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"

也可以删除范围内的元素,命令为zremrangebyscore

> zremrangebyscore hackers 1940 1960
(integer) 4

Redis为什么这么快

  1. 基于内存,可以快速响应
  2. 采用单线程,避免了上下文切换和竞争条件,不必考虑多线程问题
  3. 使用多路复用IO模型

什么是缓存穿透?怎么解决?

缓存穿透是指查询一个一定不存在的数据,这样缓存的作用就不存在了,就像穿透了一样。

解决方案:

  1. 缓存空值

如果一次查询返回的数据为空,仍然将空结构进行缓存,但是给它设置一个较短的过期时间。

缓存空值带来的问题:

  • 占用了更多的内存,针对此问题的方法是对空值设置较短的过期时间
  1. 布隆过滤器

将所有可能存在的数据使用hash算法放到一个足够大的bitmap中,一个一定不存在的数据肯定会被bitmap拦截掉,避免缓存穿透对底层数据库造成较大的查询压力。

什么是缓存雪崩?怎么解决?

如果缓存在某个时间段内集中过期,所有的查询都落在数据库上,导致缓存雪崩。

解决方案:

  1. 加锁排队:

在缓存失效后,通过加锁或者队列来控制查询数据库的线程。

  1. 随机过期:

在设置过期时间时设置一个随机值,避免大量缓存在同一时间段一起过期。

Redis的持久化

持久化就是把内存中的数据保存到磁盘上,防止服务宕机造成内存数据丢失。Redis提供两种持久化机制:RDB(默认)和AOF

RDB

RDBRedis DataBase)是按照一定的时间周期把内存中的数据以快照的方式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照保存的周期。核心函数:rdbSave(生成RDB文件)和rdbLoad(从文件加载到内存)两个函数。

image

AOF

Append-only fileRedis将每一个操作命令都通过write函数追加到文件最后,类似于MySQL中的binlogRedis重启时通过重新执行AOF文件中的命令在内存中重建数据库数据。

RDB和AOF的区别

  1. AOF文件比RDB文件更新频率高
  2. AOFRDB更安全

Redis如何实现分布式锁

Redis为单线程模型,所有的操作命令都是串行执行,故而有作为锁的可行性。使用命令setnxset if not exsits)加锁,del命令释放锁。

Redis内存淘汰策略

  1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰;
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰;
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中随机淘汰数据;
  4. allkeys-lru:从所有数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰;
  5. allkeys-random:从所有数据集(server.db[i].dict)中随机淘汰数据;
  6. no-enviction:禁止淘汰数据(将无法插入新数据,不建议)

Redis过期键的删除策略

  1. 惰性删除:

只有访问一个key时,才会判断是否过期并删除。该策略最大化节省CPU资源,但对内存非常不友好。可能导致大量过期key没有被删除,占用大量内存。

  1. 定期删除:

每隔一段时间,就扫描一定量的数据库中的设置了过期时间的一定量的key,并清除已过期的key

Redis中同时使用了惰性删除和定期删除两种策略。

Redis常见性能问题和解决方案

  1. Master最好不要做持久化工作,可以设置某个Slave开启AOF备份数据,设置策略为每秒一次,最多损失一秒的数据;
  2. 为了主从复制的速度和连接的稳定性,MasterSlave最好在同一个局域网内;
  3. 主从复制不要使用网状结构,用链表结构更为稳定,即Master <- Slave1 <- Slave2 <- Slave3

哨兵集群原理

哨兵(sentinel)是一种运行模式,它专注于对Redis实例(主节点,从节点...)运行状态的监控,并能够在主节点发生故障时通过一系列机制实现选举主节点及主从切换,确保整个Redis集群的可用性。

image

哨兵的功能:

  • 监控:持续监控主节点、从节点是否处于正常工作状态
  • 自动切换主库:当主节点运行故障,自动选举新的主节点
  • 通知:让从节点执行replicaof,与新的主节点同步,并且通知客户端与新主节点建立连接

Cluster集群

一种分布式数据库方案,集群通过分片(sharding)来进行数据管理,主要解决大数据量存储导致的性能慢的问题。

posted @ 2021-08-24 16:03  超级鲨鱼辣椒  阅读(88)  评论(0编辑  收藏  举报