步骤/目录:
1.Redis简介与安装
2.redis的数据结构
  (1)string
  (2)list
  (3)set
  (4)hash
  (5)zset
  (6)HLL
3.redis的一些命令
4.redis事务
5.TTL(time to live)
6.排序
7.发布/订阅
8.管道
9.redis GEO
10.其他

本文首发于个人博客https://lisper517.top/index.php/archives/53/,转载请注明出处。
本文的目的是初步介绍Redis、为爬虫使用redis打好基础,主要整理自 runoob教程 。这里介绍的内容非常浅显,更深入的内容请自行查询书籍。
本文写作日期为2022年9月15日。运行的平台为win10和树莓派,编辑器为VS Code。

1.Redis简介与安装

Redis是一个由 Salvatore Sanfilippo 在2008年开始创造的 key-value 存储系统,是一种开源的非关系型(NoSql)数据库,它的特点有:基于内存,分布式,键值对,支持多种语言的API(Redis本身是由ANSI C写的)。虽然它主要在内存中运行,但也可以将数据保存在磁盘;它支持主从式的数据备份;它的读写性能极高(所以把Redis开放到公网上时还要注意安全性);它支持事务,多个操作可以打包到一起,要么不执行,要么全部执行(all or nothing);它支持5种数据类型(字符串,列表,hash散列,集合,有序集合);它支持推送,key过期。总的来说Redis就是好。在使用Redis时,你可以把它用作主数据库,也可以用作辅数据库,不论何种方式,都是对关系型数据库功能的极大补充。

由于redis的一些特性,它特别适合做存储标签这种一对多的工作,而存储标签在mysql这样的关系型数据库中比较麻烦;redis可以设置键的过期时间TTL(time to live),到期后自动过期,这是很多服务都需要的(比如登录cookie的过期);redis的读写效率非常高,在轻度使用中很少会遇到redis是瓶颈的情况;redis的命令比sql要简单得多(因为它以键值对形式存储数据)。

下面介绍三种安装redis的方法。需要注意,redis为了效率而牺牲了安全性,所以默认情况下redis几乎没有保护措施,不要把它开放到外网。

在windows上安装redis:window版redis由热心网友维护,版本远落后于linux版或docker版(目前是5.0和7.0的差距),所以不推荐下载。需要的话,建议在windows上用docker安装redis,见 Docker入门(一)
如果嫌麻烦的话,到 windows版redis下载网页 下载zip文件或msi文件,解压或安装即可。解压zip文件,或用msi文件安装后,在cmd中进入redis目录,运行 redis-server.exe redis.windows.conf 即可运行redis服务器端。然后在同样的目录下,运行 redis-cli.exe -h 127.0.0.1 -p 6379 即可连接到redis数据库。但是要注意,目前它的版本只有5.0,笔者在后续将演示redis使用tls双向验证,但这是redis 6.0后才加入的功能。所以有条件的话还是用docker装redis。

在linux上:到 https://download.redis.io/releases/ 下载tar.gz文件,解压、 make 即可。具体过程为:

mkdir /usr/local/redis
cd /usr/local/redis
wget https://download.redis.io/releases/redis-7.0.4.tar.gz #最新版本的redis
tar -xzf redis-7.0.4.tar.gz
cd redis-7.0.4
apt-get install pkg-config make gcc libssl-dev #一些依赖项
make BUILD_TLS=yes

BUILD_TLS=yes 这个选项在redis 6.0以后可用,是用tls在传输层对redis通信加密的一种方式,在后续安全性的部分会提到。

这里介绍一下如何用docker安装redis(安装docker、docker-compose请参考 Docker入门(一)Docker入门(二))。在linux上使用如下命令(windows上也是一样的步骤,只是路径改一下):

mkdir -p /docker/redis/data/redis
vim /docker/redis/docker-compose.yml

写入如下内容:

version: "3.9"

services:
  redis:
    image: redis
    ports:
      - "56379:6379"
    volumes:
      - /docker/redis/data/redis:/data
    logging:
      driver: json-file
    restart: always

然后运行:

cd /docker/redis
docker-compose config
docker-compose up -d

然后你可以使用redis客户端尝试连接,比如本文后面将用的python,但是首先需要 pip install redis 。然后写一个py文件:

import redis, pprint

r = redis.StrictRedis(host='127.0.0.1',
                      port=56379,
                      decode_responses=True,
                      db=0)
pprint.pprint(r.info())

运行,如果成功连接的话它将会打印出redis服务器的一些信息。在python中,更好的连接方式是使用连接池:

import redis

pool = redis.ConnectionPool(host='127.0.0.1',
                                port=56379,
                                decode_responses=True,
                                db=0)
r = redis.Redis(connection_pool=pool)

或者你也可以使用redis-cli连接,这是redis服务器端自带的软件。比如你可以进入docker-redis容器:

cd /docker/redis
docker-compose exec -it redis bash
redis-cli -h localhost -p 6379
SAVE

SAVE 将返回一个 OK ,并且在/docker/redis/data下出现一个dump.rdb文件。实际上, SAVE 会把redis在内存中的数据写到磁盘中,这会在后面提及。更常用的测试连接命令为 ping ,服务器会返回一个 PONG 表示服务器已连接。另外, clear 可以清屏, exit 可以断开连接。最后,如果你想在redis中看到中文,使用 redis-cli --raw 来发起连接。除了redis-server、redis-cli,你还能找到redis-benchmark(性能测试工具,使用方法同redis-cli)、redis-check-aof(AOF文件修复)、redis-check-dump(RDB文件检查)。另外,如果想让redis把内存数据写到硬盘并关闭服务器,可使用 redis-cli SHUTDOWN 命令(由于docker-compose.yml里写了 restart: always ,所以它还会重启)。

最后再提醒一下,目前的redis安装方式,效率极高,但安全性几乎没有,不要把它开放到外网了。后续将介绍如何通过一些方法提升redis的安全性。

安装好redis后,使用 select 数据库编号 来选择一个数据库。默认情况下redis有16个database,从0~15编号,不支持自定义数据库名(你需要自己记住每个库里存了什么),不支持用户对单独一个数据库有权限。

2.redis的数据结构

正如前面提到过,redis中有5种数据结构:string字符串,list列表,hash散列,set集合,zset有序集合,接下来将一一介绍它们。下面介绍的操作是一小部分,更多操作参考 redis官网

(1)string

首先是字符串,它是redis最基本的类型,一般的格式就是一个键对应一个字符串。它还可以存储二进制数据,比如图片等,但它最大只能存储512MB(毕竟redis跑在内存里)。你可以在redis-cli尝试下面3种命令(set,get,del):

set test 123
OK
get test
"123"
del test
(integer) 1
get test
(nil)
del test
(integer) 0

set用于给键赋值,get用于取出键对应的值,del用于删除一个键。在上面的例子中,如果你在python中运行,代码如下:

import redis

r = redis.StrictRedis(host='localhost',
                      port=536379,
                      decode_responses=True,
                      db=0)
a = []
a.append(r.set('test', '123'))
a.append(r.get('test'))
a.append(r.delete('test'))
a.append(r.get('test'))
a.append(r.delete('test'))
for i in a:
    print(i)

这里把每个返回的值都打印了出来,注意观察对应关系。

另外,如果字符串是纯数字的话,redis提供了 INCR ,用于让其自增1,这个函数返回自增后的数字(如果键不存在,会自动创建一个为 "0" 的值并自增为1)。常见的例子是用于存储阅读量,比如键为 page.view ,在里面存储阅读量。注意在redis键的命名中,要力求写详细、直观,一般用 : 或 . 来分隔。在mysql中对列可用指定 AUTO_INCREMENT 来让列值自增,在redis中则可以用 INCR 达到类似的效果。 INCRBY 可达到相同的效果,但是它可以指定增加多少。同系列的命令还有 DECRDECRBY (decrby每太大用,因为incrby可以设置成负值), INCRBYFLOAT (增加浮点数)。

其他关于字符串的命令还有:
APPEND ,用于向字符串的末尾追加字符串。
STRLEN ,获取字符串长度。注意UTF-8编码中一个中文字符的长度为3。
MSETMGET 同时设置、获取多个键值。
位操作,包括 GETBIT (获得字符串的一个bit位,超出范围时返回0)、 SETBIT (返回旧值)、 BITCOUNT (返回为1的bit个数)、 BITOP (进行位运算,包括 AND 、 OR 、XOR 、 NOT)。 使用位操作,可以让存储空间非常紧凑。

最后,下面介绍的其他类型都是字符串衍生出来的,它们的键值只能是字符串,不能嵌套。

(2)list

下面介绍一下列表。列表是有序的,可以存储多个字符串,在redis-cli中它支持的方法有:

lpush 在列表头部添加一个元素
rpush 在列表末尾添加一个元素
lrange 获取从头到尾、一定范围的元素(包含两端;支持负索引)
ltrim 用法和lrange一样,但是它将会删除范围外的元素
lindex 获取从头到尾的第几个元素。支持负索引
lset 给出索引并赋值指定元素
lpop 从列表头部弹出一个或几个元素(弹出,意为返回这个元素的值,并且从列表中删除它)
rpop 从列表末尾弹出一个或几个元素
llen 获取列表中元素个数
lrem 删除列表中的几个元素,这些元素的值需要指定。格式为 LREM key count value,count指定要删除几个,当count为正时从左边开始删,为负时从右边开始删,0时删除所有
linsert 在一个元素的前或后插入元素
rpoplpush 将旧列表转移到新列表
brpop 类似rpop,区别在于列表内无元素时会被阻塞(每当列表加入一个新元素时brpop就会返回这个元素)。常用于实现任务队列(可带优先级)

如果熟悉数据结构的话,会发现这里的列表可以当作栈或队列使用。
在redis-cli中实验如下:

lpush test 123
(integer) 1
lpush test 234
(integer) 2
lpush test 345
(integer) 3
rpush test 456
(integer) 4
lrange test 0 -1
1) "345"
2) "234"
3) "123"
4) "456"
lindex test 0
"345"
lpop test
"345"
rpop test 2
1) "456"
2) "123"
lrange test 0 -1
1) "234"
del test
(integer) 1

除了这些命令,redis对列表还有其他操作,将在后面介绍。另外,一个列表最多可存储 2^32-1 个元素,即 4,294,967,295 。

(3)set

列表中可以出现相同的元素,且有序,但集合中每个元素不能相同,且set是无序的(set里的元素只有键没有值)。它支持的命令有:

sadd 添加一个元素到集合
srem 从集合中移除一个元素
sismember 检查元素是否在集合中
smembers 返回集合的所有元素。这个命令在集合较大时运行很慢
sdiff 差集
sinter 交集
sunion 并集
sdiffstore 存储运算结果
sinterstore 同上
sunionstore 同上
scard 获得集合中元素个数
srandmember 随机获取一个元素。它可以添加count参数,当count为正时取出count个不重复元素,为负时表示可以重复。但是由于redis的底层机制,某些元素出现的概率可能更大
spop 随机弹出一个或多个元素

使用例:

sadd test 123
(integer) 1
sadd test 234
(integer) 1
sadd test 234 #添加重复元素时被忽略了
(integer) 0
sadd test 345
(integer) 1
sadd test 456
(integer) 1
srem test 123
(integer) 1
sismember test "123"
(integer) 0
sismember test "234"
(integer) 1
smembers test
1) "234"
2) "345"
3) "456"
del test
(integer) 1

redis中的集合使用哈希表实现(只有键的hash),它增、删、查元素的复杂度都是O(1)。除了这些命令,redis对集合还有其他操作,将在后面介绍。另外,一个集合最多也可存储 2^32-1 个元素,即 4,294,967,295 。

在一般的实践中,常用set来存储tag,或者用列表。比如,键名为 tag:标签名:posts ,值为set,存储带有该标签的文章id

(4)hash

散列是存储键值对的集合,它存储的值可以为字符串或数字(不能嵌套字符串以外的类型)。hash的命令有:

hset 在hash中添加或修改一个键值对
hget 获取hash中指定键的值
hmset 添加或修改多个键值对
hmget 获取多个键值
hsetnx 仅当键不存在时新建一个键
hincrby 类似字符串的 incrby 操作
hgetall 获取hash中所有键值对
hexists 判断键是否存在
hdel 如果hash中存在一个或多个键,删除它
hkeys 获得所有键名
hvals 获得所有值
hlen 获得键值对数量

使用例:

hset test 1 111
(integer) 1
hset test 2 222
(integer) 1
hset test 3 333
(integer) 1
hset test 1 123
(integer) 0
hget test 1
"123"
hgetall test
1) "1"
2) "123"
3) "2"
4) "222"
5) "3"
6) "333"
hdel test 1
(integer) 1
hget test 1
(nil)
del test
(integer) 1

一个hash可以看作mysql中的一行,不同之处在于每行包含的列可以不同。hash比较适合存储对象;它同样能存储 2^32-1 个键值对,即 4,294,967,295 。

现在可用比较一下string和hash。比如你想存储一篇文章,需要存储的信息有文章名,文章url,文章内容等。那么你此时有两种选择,一是把文章对象序列化、转换为二进制数据,用string存储;二是用hash,把它当成mysql中的一行。用hash显得更直观,也不需要在读写是进行编解码。

(5)zset

有序集合,它可以看作是介于列表和集合之间,因为zset中的元素是唯一的,但它里面的元素有序;但它又像散列,存储键值对(但是zset中的键称为member,值称为score,值必须为浮点数)。zset支持的操作如下:

zadd 添加键值对到zset( +inf 和 -inf 表示正无穷和负无穷)
zrange 取出范围内的成员值(键值),按score从小到大
zrevrange 同上,但是score从大到小
zrangebyscore 给定score范围,取出所有在范围内的元素(包含极值。如果不想包含,可加上 ( 如 "80 (100")
zrem 如果zset中存在一个或多个成员,删除它
zscore 获取元素的分数
zincrby 增加score
zcard 获取元素个数
zcount 获取分数在范围之间的元素个数
zremrangebyrank 按照元素score从小到大的顺序删除范围内的元素
zremrangebyscore 删除指定分数范围的元素
zrank 获取元素的排名,从小到大排列
zrevrank 同上,从大到小
zinsertstore 计算并导出有序集合的交集。新zset中的分数会使用聚合函数,支持sum、min、max。还能设置权重
zunionstore 计算并导出并集

使用例:

zadd test 123 1
(integer) 1
zadd test 234 2
(integer) 1
zadd test 345 3
(integer) 1
zrange test 0 -1
1) "1"
2) "2"
3) "3"
zrangebyscore test 124 345
1) "2"
2) "3"
zrem test 1
(integer) 1
del test
(integer) 1

它也支持 2^32-1 个键值对。

在应用场景上,一般string用于存储文件、文字;hash用于存储对象(描述对象的各种属性),可以当成mysql中的一行来使用;list用于列表、队列;set可用于去重;zset用于排行、权重等。

(6)HLL

在2.8.9版本中,redis添加了HyperLogLog,这种结构专用于数据量很大的集合,在一定误差内返回集合中元素的个数(不能返回元素本身),它主要用于计算视频点击量(上千万、上亿次)等。它的特点有:占用空间小,一个HLL键在redis中只占12KB,就能统计2^64量级的元素;使用了一些概率算法,标准差在 0.81% 左右(可以设置辅助因子降低,但要付出空间)。
redis中HLL相关命令有:

pfadd 添加元素到HLL中
pfcount 返回个数估算值
pfmerge 将多个HLL合并为一个HLL

3.redis的一些命令

除了前面提到的 pingexitclearsaveshutdownselect ,数据类型相关的命令,还有一些命令:
keys 模式 ,返回所有匹配模式的键。在模式中, ? 匹配一个字符, * 匹配0到任意个字符, [] 用于给出单个字符的范围, \ 用于转义。
DEL ,删除一个或多个键。不能用通配符,但是可在linux命令行借助管道与xargs实现类似的功能: redis-cli KEYS "模式" | xargs redis-cli DEL
DUMP ,把一个键序列化,类似python中decode的功能。
EXISTS ,检查键是否存在。
EXPIRE ,为键设定过期时间(单位为秒)。到过期时间后,键会被删除。
EXPIREAT key timestamp ,和 EXPIRE 功能相同,但是使用UNIX时间戳(从1970年1月1日到现在的秒数)。
PEXPIRE ,和 EXPIRE 功能相同,只是单位为毫秒。
PERSIST ,取消键的过期时间。
TTL (time to live),以秒为单位返回键的剩余时间。
PTTL ,以毫秒为单位返回键的剩余时间。
KEYS ,给出一个pattern,返回符合模式的键。
MOVE ,把键转移到另一个库中(redis中默认有16个库)。
RANDOMKEY ,返回一个随机键。
RENAME ,修改键名。
RENAMENX ,仅当要改成的键名不存在时,将旧键名改为新键名。
SCAN ,返回可迭代的数据库键(数据库编号)。
TYPE ,返回键对应值的类型。

更多命令可参考 redis官网

4.redis事务

事务(transaction)是现在很流行的一个概念。设想我进行一个转账1000¥的操作,银行的数据库系统需要进行两步:从我的账户扣去1000,然后给目标账户增加1000。但是如果中间出了差错,比如我的账户扣钱、对方账户却没有收到钱,这怎么办呢?这种类型的问题可用事务解决,包括在事务内的多条命令要么全部执行,要么全部都不执行,不存在执行一半的状态。

在redis-cli中执行事务如下:

multi
命令1
命令2
...
exec

redis先接受各条命令,当接收到exec时redis会将所有命令执行。如果在客户端输入命令时断线,redis就不会执行命令。另外,事务不会被其他命令打断,redis在执行一个事务时会把它执行完,再执行其他命令或事务;redis中的事务也不支持回滚。另外,如果事务中的某条命令有错误,其他命令也会继续执行。

事务相关的命令除了 multiexec ,还有:

discard 取消事务
watch 监视键
unwatch 放弃监视键

watch 是事务的重要功能之一,它用于监视键,保证在事务执行前相关的一些键不被改动,一般格式为:

watch
multi
exec

如果watch监视到了键被改动,事务的执行将被阻止(watch的作用不是阻止键被改动,而是阻止事务执行)。

5.TTL(time to live)

这里介绍一下redis的键过期功能。
使用 expire 可以为一个键设置过期时间,单位为秒,到时间后该键会自动被删除。
使用 ttl 则可以返回一个键剩余的时间,单位为秒(当键不存在时返回-2,键没有设置过期时间时返回-1)。
使用 persist 可以取消键的过期时间,成功取消时返回1,否则返回0。另外,如果用 setgetset 等命令为键赋值,也会取消过期时间(incrlpushhsetzrem 等修改键值的命令则不会)。
pexpire 以毫秒设置ttl。对应的有 pttl
最后,还有 expireatpexpireat ,它们给出的是UNIX时间,到该时间后删除键。

另外,如果键是被expire相关命令删除,watch不会认为键被改动过。

使用ttl功能,可以方便地为网页提供cookies等服务。

6.排序

redis中使用 sort 排序(升序),用 sort key desc 降序排序,用 sort key alpha 对字符串排序。
在hash中,使用 sort 键名 by 参考键名*->字段名 可以指定排序的字段,redis会用字段值来替换 * ,by只能出现一次; sort by GET 参考键名*->字段名 可以返回指定的字段,get可以出现多次,用 # 可以代替元素本身的值; sort by get STORE 键名 可以把结果保存到新的键中。

7.发布/订阅

redis中提供了发布、订阅的功能。类似youtube,发布者向频道(channel)中发布一个消息,订阅者会收到这个消息。发布消息的命令为 publish ,订阅频道的命令为 subscribe 。另外还有以下命令:
unsubscribe 可以取订一个或所有频道;
psubscribe 按一定的模式订阅频道。支持的匹配符有 ? 、 * 等;
punsubscribe 按模式退订频道。

在分布式的scrapy中,主机发布待爬url就可以用publish,所有订阅url频道的机器就能收到待爬url。

另外,redis 5.0后新增了stream,它主要用于消息队列。publish的一个特点是不会存储之前发布的消息,就是说新订阅的用户不能接受到订阅之前发布的消息。stream就是为了解决这个问题而出现的,它对历史发布的消息进行了持久化

8.管道

在redis的使用中,上面提到的方法都有返回值。日常使用中,redis-cli发出指令,server收到后执行并返回结果,如果client要执行多条语句,有时只需要最后的值,就可以使用管道。管道应用于多条命令,每条命令的执行不依赖于前面的命令,只需要最后的结果时。管道的具体实现和编程语言有关,比如用python:

import redis

pool = redis.ConnectionPool(host='127.0.0.1',
                            port=56379,
                            db=0,
                            decode_responses=True)
r = redis.Redis(connection_pool=pool)
pipe = r.pipeline()
pipe.set('test', '123')
#pipe的其他操作
pipe.execute()

你会发现这和pymysql差不多。
使用管道,即使在局域网中,也可以大大降低延迟。

9.redis GEO

在redis 3.2版本新增了geo功能,主要用于计算实际的地理位置。这里就不过多介绍了。

10.其他

save 命令可将内存中的数据写入磁盘,默认情况下是dump.rdb文件。恢复数据时将这个文件放到redis安装目录(用 config get dir 查看)即可(docker-redis则是放到容器的/data下)。
bgsave 在后台执行save。
client 相关操作,包括 client list (查看所有连接的客户端)、 client setname (设置当前连接的名称)、 client getname (获取连接名称)、 client pause (客户端挂起多少毫秒)、 client kill 关闭客户端连接。
redis支持分区,就是把一个很大的键分成小份,每个redis实例只保存它的一个子集,这个技术稍微有些复杂,不拓展了。
redis命令行使用Lua语言,并且支持编写脚本(如同mysql)。客户端提供一个脚本后,其会永久存储在redis中,便于其他客户端使用。Lua的语法这里也不介绍了。
config 系列命令,可获取server的配置。
redis支持主从模式,这主要是因为一台机器的资源有限。当需要扩展时,你可以在一台机器上运行主redis,其他机器上运行从redis;主redis负责写入的操作,从redis会把数据拷贝到本地、负责读取的操作,也称为读写分离。

最后,要提醒一下:
注意redis的安全性;安装redis的时候提到过 make BUILD_TLS=yes 这个选项,但是redis应该尽量追求效率,只在内网运行,尽量不用tls等安全手段,因为会降低redis的速度。
redis内部使用Lua语言,有时你使用爬虫入库的东西可能被当成脚本执行,比如说给你把redis清空( flushall ),这点要注意(反爬手段之一);
如果想更深入学习redis,可以参考 redis的官方文档 或 《redis实战》 等书籍。

标签: redis, python

添加新评论