Redis入门

123 阅读18分钟

Redis是数百万开发人员用作数据库、缓存、流引擎和消息代理的开源内存数据存储。

The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker.

本文是对文末的参考链接进行学习后整理的笔记(或者说翻译?),练习用的操作系统是macOS,Redis版本为7.0.0。

安装Redis

1. 下载最新版本的Redis。

wget https://download.redis.io/redis-stable.tar.gz

因为我电脑没有wget命令,并且也不想安装这个命令,所以直接把链接粘贴到浏览器中下载了这个压缩包。

其他版本的Redis下载:Download | Redis

2. 编译Redis

解压缩压缩包,然后到解压缩后的文件目录下,执行make命令。

tar -xzvf redis-stable.tar.gz
cd redis-stable
make

编译成功之后,在src文件目录下会有一些二进制文件,包括:

  • redis-server: Redis Server 自身。
  • redis-cli:和Redis交互的命令行接口工具。

3. 安装二进制文件

src文件夹下的二进制文件安装到 /usr/local/bin下。

redis-stable文件目录下,执行:

make install

4. 在前台启动Redis。

执行命令:

redis-server

Ctrl + C退出。

在一个终端执行redis-server启动Redis服务,在另一终端执行redis-cli连接Redis并进入交互模式。

5. 在后台启动Redis。

可以使用Redis配置中的daemonize 参数(默认值为no),以守护进程的方式启动服务,这样这个终端关掉之后,服务也不会被停止。

redis-server --daemonize yes

在解压后的文件夹下的redis.conf文件中,也能看到配置示例。

这时候如果要停止Redis,执行redis-cli命令进入交互模式,输入shutdown命令停止服务。

其他操作系统下的安装方式:安装Redis

基础使用

在我的印象里Redis就是一个字典对象,由一个个键值对组成,只是值的类型更多,键值对的数量和占的内存可以是比较大的范围。通过设置值和取值来对数据进行操作,只是规则和功能比较丰富。

可以使用任何二进制序列作为一个键,键可以是从一个像"foo"那样的字符串,到jpeg文件的内容。空字符串也是一个有效的键。

  • 键太长了不好。比如查找一个1024KB的键时会进行昂贵的键比较。如果必须要处理一个大键,尤其是考虑到内存和带宽方面,对键内容进行哈希处理以减小键的长度会比较合适。
  • 键太短了也不好。把键写成"user:1000:followers"比写成"u1000flw"好,因为"user:1000:followers"的可读性比较好。虽然说短的键占用的内存比较少,但是和键对象本身以及它的值对象使用的内存比起来,节省的内存是很少的。需要找到键长度和内存之间的一种平衡。
  • 坚持使用模式(schema)。"object-type:id"就不错,比如"user:1000"。点号或者破折号用于多个词的字段,比如"comment:4321:reply.to"或者"comment:4321:reply-to"。
  • 允许的键的最大尺寸为512MB。

所谓模式(schema),就是数据库的组织和结构。比如MySQL数据库中的用户表,要设计什么字段,字段的内容是什么,表与表之间的关系如何。对于Redis来说,模式就是要使用什么样的键,这些键中要保存什么样的值,比如"user:1000:followers"键名,可以抽象为user表中存储的id为1000的用户的粉丝,键中存储的值就是的粉丝的数据。(当然,这只是阅读上的模式,Redis中是没有表这种概念的。)

设置和获取键值

现在只简单以设置和获取字符串类型的值为例,后文会对各个类型数据的操作进行介绍。

在终端输入redis-cli进入交互模式。之后的练习都在交互模式下进行。

127.0.0.1:6379> set mykey somevalue
OK
127.0.0.1:6379> get mykey
"somevalue"

通过set命令设置键"mykey"中存储的值为"somevalue"字符串。通过get命令获取键"mykey"中存储的值。

修改和查询键空间

键空间(key space)指的是Redis管理的内部字典,所有的键都存储在其中。

有些命令不是在数据类型上定义的,而是定义在键空间上的,可以对任何类型的键使用这些命令。

exists命令会返回1或者0,标识给定的键是否存在于数据库中。

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

del命令会移除键,返回1表示键已经被删除了,返回0表示数据库不存在给定的键。

type命令返回存储在给定键中的值的类型。

127.0.0.1:6379> type mykey
string

键过期

键过期可以让你为键设置超时,也称为存活时间("time to live","TTL")。当超过存活时间后,键会被自动销毁。

  • 能以秒或者毫秒的精度设置过期时间。
  • 过期时间的解析精度永远是1毫秒。(不论设置的是秒级过期时间还是毫秒级过期时间,内部使用的都是毫秒级过期)
  • 过期相关的信息被复制和保存在磁盘中,即使Redis是在终止状态,时间也会过去。比如设置了键过期时间为1分钟后,但是在这中间Redis服务终止了,不会因为服务终止而影响过期时间。

使用expire命令设置过期时间:

127.0.0.1:6379> set key some-value
OK
127.0.0.1:6379> expire key 5
(integer) 1
127.0.0.1:6379> get key
"some-value"
127.0.0.1:6379> get key
(nil)

设置键"key"的过期时间是5秒,第一次使用get命令的时候键还没有过期,所以拿到值了,第二次使用get命令的时候,数据库中已经不存在键"key"了,拿到的值为空。

还可以使用set命令创建有过期时间的键。

使用persist命令来移除键的到期时间,让键一直保留。

使用ttl检查键的剩余存活时间:

  •  返回的正数表示的就是键的剩余存活时间(单位是秒)。
  • 返回-2表示键不存在。
  • 返回-1表示键存在,但是没有对应的过期时间。
127.0.0.1:6379> set practice:expire:key a ex 1000
OK
127.0.0.1:6379> ttl practice:expire:key
(integer) 995
127.0.0.1:6379> persist practice:expire:key
(integer) 1
127.0.0.1:6379> ttl practice:expire:key
(integer) -1

pexpirepttlexpirettl的用法一样,只是单位为毫秒。

键过期时间被存储为绝对Unix时间戳。

键有两种过期方式:

  • 当有客户端尝试访问一个键,但是发现键已经过期时,就会销毁这个键。

  • Redis每秒钟会执行10次以下操作以销毁过期的键:

    1. 测试包含过期数据的键的集合中随机的20个键。
    2. 删除所有已过期的键。
    3. 如果超过25%的键过期了,重新从步骤1开始执行。

数据类型

字符串(Strings)

可以使用setget命令设置和获取一个字符串值。注意,如果键已经存在,set会覆盖已经存储在键中的值,即使之前存的是非字符串类型的值。

127.0.0.1:6379> set mykey somevalue
OK
127.0.0.1:6379> get mykey
"somevalue"

值可以是任何类型的字符串(包括二进制数据),例如可以存储一个jpeg图片。一个字符串值不可以超过512MB。

set命令有可选的参数可以使用。

  • nx,只有在键不存在的时候才能执行成功。
  • xx,只有在键存在的时候才能执行成功。
127.0.0.1:6379> set mykey newvalue nx
(nil)
127.0.0.1:6379> set mykey newvalue xx
OK

可以对字符串类型进行原子递增/递减操作。

  • incr将字符串值解析为整数,每次以1递增。返回递增后的值。
  • incryby将字符串值解析为整数,每次以给定的数字递增。返回递增后的值。
  • decrdecrby命令与incrincryby用法相同,只不过是进行递减。
127.0.0.1:6379> set counter "100"
OK
127.0.0.1:6379> incr counter
(integer) 101
127.0.0.1:6379> incrby counter 20
(integer) 121
127.0.0.1:6379> decr counter
(integer) 120
127.0.0.1:6379> decrby counter 100
(integer) 20

即使多个客户端对同一个键执行incr命令也不会进入竞争状态,所以称为原子递增。客户端1和客户端2同时读取到数据"10",同时递增,将其设置为11,这种情况永远不会发生。读-递增-写 这个操作只会在其他客户端没有执行相同操作的时候会执行,如果客户端1和客户端2同时执行了incr命令,最终的值总会是12。

getset命令为键设置一个新值,并返回旧的值。

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> getset a 100
"1"

msetmget指令批量设置和获取值。这能够减少客户端和Redis服务的通信,从而减少延迟。

127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> mget a b c
1) "1"
2) "2"
3) "3"

列表(Lists)

简单来说,列表就是排序元素的一个序列。

Redis列表通过链表(Linked Lists)实现。这意味着如果有一个包含数百万个元素的列表,添加一个元素到列表的头部和尾部花费的是常量时间。添加一个元素到包含10个元素的列表的头部的速度和添加一个元素到1000万个元素的列表的头部的速度是一样的。

缺点是访问元素的速度和被访问元素的索引成正比。

  • lpush命令添加一个新的元素到列表的最左边(头)。返回push操作后列表的长度。
  • rpush命令添加一个新的元素到列表的最右边(尾)。返回push操作后列表的长度。
  • lrange命令从列表中取指定范围内的元素。lrange接收两个索引,表示返回从第一个索引到最后一个索引的元素(两个索引对应的元素都包含在内),两个索引都可以是负数,-1表示最后一个元素,-2表示倒数第二个元素,依此类推。
127.0.0.1:6379> rpush mylist A
(integer) 1
127.0.0.1:6379> rpush mylist B
(integer) 2
127.0.0.1:6379> lpush mylist first
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"

lpushrpush都可以同时添加多个元素。

127.0.0.1:6379> rpush mylist 1 2 3
(integer) 6
127.0.0.1:6379> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"

rpop命令和lpop命令获取并删除最右边和最左边的元素。返回被删除的那个元素。当列表中没有元素时,执行pop操作会返回空。

127.0.0.1:6379> lpop mylist
"first"
127.0.0.1:6379> rpop mylist
"3"

在前面的例子中,我们没有在向一个列表中插入元素之前先创建一个键保存一个空列表,也没有在列表中没有元素时将该键删除,Redis在背后做了这些事。(不止列表如此,所有的由多个元素组成的数据类型(流、集合、排序集合和哈希)都是这样)

  1. 当我们添加一个元素到一个聚合数据类型中,如果目标键不存在,会在添加元素之前先创建一个空的聚合数据类型。
  2. 当我们从一个聚合数据类型中移除元素,如果值变为空了,键会自动被销毁。(流数据类型是此规则的唯一例外)
  3. 对于一个空的键(已被移除或者从不存在),执行只读命令比如llen(返回列表的长度),或者执行移除元素的写命令,返回的结果就像是这个键保存着一个该命令期望找到的空的聚合类型一样。(比如键"aaa"为空,执行llen aaa会返回0,就像是"aaa"中保存着一个空列表一样)

列表的常见用法

  • 记录用户在社交网络的最新发布更新。
  • 进程间通信,使用消费者-生产者模式,生产者push项目到列表中,消费者(通常是一个worker)消费这些项目并且执行动作。Redis有特殊的列表命令,使得这些使用场景更加可靠和高效。

有上限的列表

ltrim命令将指定范围的元素设置为新的列表值,移除范围外的元素。

127.0.0.1:6379> rpush alist 1 2 3 4 5
(integer) 5
127.0.0.1:6379> ltrim alist 0 2
OK
127.0.0.1:6379> lrange alist 0 -1
1) "1"
2) "2"
3) "3"

列表上的阻塞操作

想象你需要在一个进程中push项目到列表中,在另一个进程中对这些项目进行处理,可以通过以下方式用队列简单实现:

  • 生产者(producer)执行lpush将项目(items)推送到列表中。
  • 消费者(counsumer)执行rpop获取/处理列表中的项目(items)。

但是有时候队列中可能没有项目了,这时消费者就不得不等待一段时间,然后重新尝试执行rpop命令。这称为轮询(polling),并且有以下缺点:

  1. 强制Redis和客户端处理没必要的命令。(只要列表还是空的,执行rpop就会永远返回空,在列表为空时执行的rpop都是没有意义的)
  2. 对项目的处理会有延时,因为当一个worker接收到空的时候,它会等待一段时间。为了减少延迟,可以减少执行drop的间隔,但是这样又会放大问题1,导致对Redis导致更多无用的调用。

在进程(process)中指派任务给一个worker,然后继续进程。worker会在不同的线程(thread)中对任务进行处理,当完成任务之后,会通过回调进行反馈。

所以Redis实现了brpopblpop命令,与rpoplpop命令对应,但是会在列表为空的时候阻塞(block): 它们会在列表中存在元素,或者达到了用户定义的超时 时才会返回。

可以在一个终端中push元素,在另一个终端中pop元素,查看效果。

127.0.0.1:6379> rpush tasks "do-something"
(integer) 1

brpop tasks 5表示等待列表中的元素,如果超过5秒都还没有元素,就直接返回。

127.0.0.1:6379> brpop tasks 5
(nil)
(5.07s)
127.0.0.1:6379> brpop tasks 5
1) "tasks"
2) "do-something"
(1.92s)

可以设置超时为0,那样就会永远等待元素。也可以指定多个列表,用于同时等待多个列表,当其中有一个列表接收到元素,就返回。

brpop需要注意以下几点:

  1. 客户端以有序的方式被服务:第一个被阻塞以等待列表的客户端,当其他客户端向列表推送元素时,它是第一个被服务的,以此类推。(谁先开始等,有元素时,就首先得到返回值。)
  2. rpop不同,返回的值是2个元素的数组,除了拿到的元素,还包含键的名称,因为brpopblpop能够等待等待多个列表的元素,所以需要区分一下拿到的是哪个列表中的元素。
  3. 如果超时了,会返回空(NULL)。

哈希(Hashes)

Redis中的哈希就和普通的哈希一样,包含字段-值对。

  • hmset设置哈希的多个字段。
  • hmget返回值的列表。
  • hget获取一个单独的字段。
  • hgetall获取存储在哈希中的字段和值。
  • hincrby以指定的数字对字段的值进行递增。
127.0.0.1:6379> hmset user:1000 username xiaoming birthyear 2000 verified 1
OK
127.0.0.1:6379> hmget user:1000 username birthyear verified
1) "xiaoming"
2) "2000"
3) "1"
127.0.0.1:6379> hget user:1000 username
"xiaoming"
127.0.0.1:6379> hgetall user:1000
1) "username"
2) "xiaoming"
3) "birthyear"
4) "2000"
5) "verified"
6) "1"
127.0.0.1:6379> hincrby user:1000 birthyear 22
(integer) 2022

集合(Sets)

Redis的Sets存储未排序的字符串的集合。

  • sadd命令添加新元素(member)到集合中。
  • smembers命令返回集合中的所有元素。
  • sismember命令检查一个元素是否存在。
  • spop命令从集合中删除并返回一个或多个随机元素。
127.0.0.1:6379> sadd myset 1 2 3
(integer) 3
127.0.0.1:6379> smembers myset
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> sismember myset 1
(integer) 1
127.0.0.1:6379> sismember myset 100
(integer) 0
127.0.0.1:6379> spop myset
"3"
127.0.0.1:6379> spop myset 2
1) "1"
2) "2"

sunionstore取多个集合的并集,并将结果存储在另一个集合中。

scard返回集合中元素的数量。

srandmember从集合中随机取出一些元素。

127.0.0.1:6379> sadd set:1 a b c
(integer) 3
127.0.0.1:6379> sadd set:2 c d e f a
(integer) 5
127.0.0.1:6379> sunionstore set:3 set:1 set:2
(integer) 6
127.0.0.1:6379> smembers set:3
1) "e"
2) "f"
3) "b"
4) "d"
5) "c"
6) "a"
127.0.0.1:6379> scard set:3
(integer) 6
127.0.0.1:6379> srandmember set:3
"c"
127.0.0.1:6379> srandmember set:3 3
1) "f"
2) "b"
3) "a"

排序集合(Sorted sets)

排序集合有点像集合和哈希的混合。它包含的元素是唯一的、不重复的字符串,同时每个元素还包含一个对应的浮点数值,称为分数(score),元素根据分数进行排序。

排序集合根据以下规则进行排序:

  • 如果B和A是包含不同分数的两个元素,如果A.score > B.score,则A > B。
  • 如果B和A有相同的分数,那么如果A在字典顺序上大于B,则A > B。因为排序集合只能包含唯一的元素,所以B和A字符串不可能完全一样。

zaddsadd相似,但是有一个额外的参数,即分数。

zrange返回指定范围内的排序元素。zrevrange返回指定范围内倒序排序的元素。可以使用withscores参数同时将分数返回。

127.0.0.1:6379> zadd sortset 1 "orange"
(integer) 1
127.0.0.1:6379> zadd sortset 10 "apple" 5 "watermelon" 200 "strawberry"
(integer) 3
127.0.0.1:6379> zrange sortset 0 -1
1) "orange"
2) "watermelon"
3) "apple"
4) "strawberry"
127.0.0.1:6379> zrevrange sortset 0 -1
1) "strawberry"
2) "apple"
3) "watermelon"
4) "orange"
127.0.0.1:6379> zrevrange sortset 0 1 withscores
1) "strawberry"
2) "200"
3) "apple"
4) "10"

zrangebyscore返回指定分数范围内的元素。

zrangebyscore sortset -inf 5 返回分数位负无穷小(-inf)到5之间(两个边界都包含在内)的水果。

127.0.0.1:6379> zrangebyscore sortset -inf 5
1) "orange"
2) "watermelon"

zremrangebyscore移除分数范围内的元素,并返回移除的元素的数量。

127.0.0.1:6379> zremrangebyscore sortset 1 5
(integer) 2
127.0.0.1:6379> zrange sortset 0 -1 withscores
1) "apple"
2) "10"
3) "strawberry"
4) "200"

zrank查看元素的正序排行。zrevrank查看元素的倒序排行。

127.0.0.1:6379> zrank sortset "apple"
(integer) 0
127.0.0.1:6379> zrevrank sortset "apple"
(integer) 1

注意:排序集合中元素的分数永远不能被更新,只能通过zadd命令进行覆盖。

位图(Bitmaps)

位图不是实际的数据类型,而是在字符串类型上定义的一组面向位的操作。

位图的一个优点是,可以在存储信息时很大地节省空间。比如可以用512MB的内存存储40亿用户是否喜欢玩手机的信息,如果id为123的用户喜欢玩手机,那么位图数据的第123位的值为1,如果不喜欢玩,那么该位的值为0。

setbit 设置或清除字符串的指定偏移位置的位值。清除还是设置取决于值为0还是1。

getbit获取指定偏移位置的位值。

127.0.0.1:6379> setbit key 10 1
(integer) 1
127.0.0.1:6379> getbit key 5
(integer) 0
127.0.0.1:6379> getbit key 10
(integer) 1
127.0.0.1:6379> getbit key 11
(integer) 0

setbit key 10 1 设置键"key"存储的字符串的偏移量为10处的位值为1,10就是位的数量,说明存的是包含10位的字符串。getbit key 10获取在键"key"存储的字符串的偏移量为10处的位值。getbit key 11的偏移量已经超过了存储的位的长度,拿到的值为0。

setbitgetbit都是针对单个位的操作。

以下是针对多个位的操作的3个命令:

  • bitop执行不同字符串之间的位运算。提供的操作有AND(与)、OR(或)、XOR(异或)和NOT(非)。返回存储在目标键中的字符串的尺寸。
  • bitcount命令返回被设置为1的比特位的数量。
  • bitpos找到具有指定值0或1的第一个位。
127.0.0.1:6379> setbit key-a 0 1
(integer) 0
127.0.0.1:6379> setbit key-b 2 1
(integer) 0
127.0.0.1:6379> bitop and key-c key-a key-b
(integer) 1
127.0.0.1:6379> get key-c
"\x00"
127.0.0.1:6379> bitcount key-c
(integer) 0
127.0.0.1:6379> bitcount key-a
(integer) 1
127.0.0.1:6379> setbit key 10 1
(integer) 0
127.0.0.1:6379> setbit key 2 0
(integer) 0
127.0.0.1:6379> setbit key 5 1
(integer) 0
127.0.0.1:6379> bitpos key 1 0 10
(integer) 5
127.0.0.1:6379> bitpos key 0 0 10
(integer) 0

bitpos key 1 0 10从位置0到10之间开始找位值为1的第一个位。

HyperLogLogs

HyperLogLog(HLL)是一种概率数据结构,用于统计唯一的事物。

Redis中的HLL从技术上来说是编码为Redis字符串的一种不同的数据结构。使用HLL的目的是不需要使用与技术的项目数成正比的内存,而是可以使用常量的内存。

  • 遇到新元素时,使用pfadd添加到计数中。
  • 想要获取当前用pfadd添加的唯一元素的近似值时,使用pfcount
127.0.0.1:6379> pfadd hll a b c d e
(integer) 1
127.0.0.1:6379> pfcount hll
(integer) 5

其他

Stream数据类型的内容可以查看Streams了解。

一般我们开发的时候会直接使用已有的包,来和Redis服务进行通信,官网已经给到了各语言对应的和Redis相关的库:Libraries | Redis,可以从中进行选择。如果用的Django框架,可以直接使用Django 缓存

参考网址

Redis官网

Data types tutorial | Redis

EXPIRE | Redis

Commands | Redis

命令: SET 、 GET、 EXISTS、 DEL、 TYPE、 EXPIRE、 PERSIST、 TTL、 PEXPIRE、 PTTL、 INCRINCRBYDECR 、 DECRBYGETSET、 MSET 、 MGET、 LPUSH、 RPUSH、 LRANGE、 LTRIM、 BRPOP 、 BLPOP、 LMOVE、 BLMOVE、 LLEN、 HMSET、 HGET、 SADD、 SPOP、 SUNIONSTORE、 SCARD、 ZADD、 LRANGEZREVRANGE 、 ZRANGEZRANGEBYSCOREZREMRANGEBYSCORE、 ZREVRANK、 SETBITGETBITBITOPBITCOUNTBITPOS、 PFCOUNT