3、Redis 应用

作者: Brinnatt 分类: ARM64 Linux 补充知识 发布时间: 2023-09-24 23:34

官方网站:https://redis.io/

中文网站: http://www.redis.cn

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

用途:缓存(StackOverFlow)、数据库(微博)、消息中间件(微博)。

3.1、安装

安装 redis 可以参考官网 https://redis.io/docs/getting-started/installation/

yum install tcl systemd-devel -y
wget https://download.redis.io/redis-stable.tar.gz
tar -xzvf redis-stable.tar.gz
cd redis-stable
make USE_SYSTEMD=yes
make install

源码目录下有一个工具目录 redis-stable/utils,里面的脚本可以供我们使用。CentOS 7 以后的操作系统都使用 systemd 作为服务管理器,可以把 redis-stable/utils/systemd-redis_server.service 样本改一下,启动 redis 服务。

另外,redis 源码编译安装没有提供配置文件,可以借用官方的配置文件,参见 https://redis.io/docs/management/config-file/

[root@rocky utils]# pwd
/root/redis-stable/utils
[root@rocky utils]# cp systemd-redis_server.service /usr/lib/systemd/system/redis.server
[root@rocky utils]# vim /usr/lib/systemd/system/redis.server
[Unit]
Description=Redis data structure server
Documentation=https://redis.io/documentation
Wants=network-online.target
After=network-online.target

[Service]
ExecStart=/usr/local/bin/redis-server /etc/redis.conf
LimitNOFILE=10032
NoNewPrivileges=yes
Type=forking
TimeoutStartSec=infinity
TimeoutStopSec=infinity
UMask=0077

[Install]
WantedBy=multi-user.target

配置选项很多,暂时只修改小部分:

[root@rocky utils]# vim /etc/redis.conf
# 地址监听
bind 192.168.136.131 127.0.0.1

# 开启保护模式时,如果不使用bind绑定IP地址,或不使用密码,那么只能本地地址访问或使用Unix socket文件
protected-mode yes
# 远程连接还是要设置密码
requirepass xxx

# 后台服务
daemonize yes
port 6379

# 缺省DB是0,设置DB的数目
databases 16

# 2种持久化方式
# RDB: Redis DB 默认开启
# 下面是执行快照的条件
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb

# AOF 默认关闭
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec

# 用于测试,或者内存小可以设置最大内存,但是生产环境一定尽量使用内存
# maxmemory <bytes>
[root@rocky ~]# systemctl start redis

3.2、Redis 数据模型

3.2.1、键 Key

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

Key 取值原则:

  • 键值不需要太长,消耗内存,而且查找这类键值的计算成本较高。
  • 键值不宜过短,可读性较差。
  • 习惯上 key 采用 user:123:password 形式,表示用户 id 为 123 的用户和密码。

3.2.2、字符串

字符串是一种最基本简单的 Redis 值类型。Redis 字符串是二进制安全的,这意味着一个 Redis 字符串能包含任意类型的数据,例如:一张 JPEG 格式的图片或者一个序列化的 Ruby 对象。

一个字符串类型的值最多能存储 512M 字节的内容。

3.2.2.1、Python3 Redis

安装 redis 库:

$ pip install redis
import redis

db = redis.Redis(host='192.168.229.132', password='WelC0me168!')  # 默认本地6379的0号库

db.set('testbin', 0b01100010)  # 0x62
print(1, '-->', db.get('testbin'))

db.set(0b11, 0x63)
print(2, '-->', db.get(0b11))
print(3, '-->', db.get(3))
print(4, '-->', db.get('3'))

print(db.keys('*'))

注意:上例中 0x62 实际上发生了类型变化,因为返回的 bytes 类型 98,实际上对应 ASCII 的 98,已经是 2 字节了。

数值会先转换成 10 进制 64 位有符号数后,再转成 ASCII 编码字符串,存入 redis 中。

3.2.2.2、字符串设置

设置字符串值 SET key value [EX seconds][PX milliseconds] [NX|XX]

  • EX 设置过期时间,秒,等同于 SETEX key seconds value

  • PX 设置过期时间,毫秒,等同于 PSETEX key milliseconds value

  • NX 键不存在,才能设置,等同于 SETNX key value

  • XX 键存在时,才能设置。

设置多个键的字符串值 MSET key value [key value ...]

  • key 存在则覆盖,key 不存在则增加。

  • 多键值操作具有原子性。

MSETNX key value [key value ...]

  • key 不存在则设置,key 存在则失败。nx 指不存在。
  • 多键值操作具有原子性。
127.0.0.1:6379> set s1 abc
OK
127.0.0.1:6379> set s2 xyz
OK
127.0.0.1:6379> set s3 abcd ex 15
OK
127.0.0.1:6379> mset s3 3 s4 4 s5 5
OK
127.0.0.1:6379> msetnx s5 a5 s6 6
(integer) 0

3.2.2.3、过期操作和生存时间

Redis 中可以给每个 Key 设置一个生存时间(秒或毫秒),当达到这个时长后,这些键值将会被自动删除。

设置多少秒或毫秒后过期:

  • EXPIRE key seconds

  • PEXPIRE key milliseconds

设置在指定 Unix 时间戳过期:

  • EXPIREAT key timestamp

  • PEXPIREAT key milliseconds-timestamp

持久化 key,即取消过期:

  • PERSIST key

适用场景:

  • 多少秒过期,例如一个缓存数据失效。

  • PEXPIREAT key milliseconds-timestamp,比如现在开始缓存数据,到 0 点失效。

Time To Live,Key 的剩余生存时间

  • TTL key:返回键的剩余生存时间,以秒为单位。

  • PTTL key:返回键的剩余生存时间,以毫秒为单位。

key 存在,但没有设置 TTL,返回 -1

key 存在,但还在生存期内,返回剩余的秒或者毫秒。

key 曾经存在,但已经消亡,返回 -2(2.8 版本之前返回 -1)。

127.0.0.1:6379> set s5 abc ex 20
OK
127.0.0.1:6379> ttl s5
(integer) 15
127.0.0.1:6379> ttl s5
(integer) -2
127.0.0.1:6379> setnx s6 6
(integer) 1
127.0.0.1:6379> expire s6 60
(integer) 1
127.0.0.1:6379> pttl s6
(integer) 50627
127.0.0.1:6379> ttl s6
(integer) 38
127.0.0.1:6379> persist s6
(integer) 1
127.0.0.1:6379> ttl s6
(integer) -1
127.0.0.1:6379> EXPIREAT cache 1692529460

3.2.2.4、key 操作

keys pattern,pattern 可以取如下值:

  1. * 任意长度字符。
  2. ? 任意一个字符。
  3. [] 字符集合,表示一个字符。
keys *
keys s?
keys s[13]
keys s*
keys ??

TYPE key 返回 key 类型。

EXISTS key key 是否存在。

RENAME key newkeyRENAMENX key newkey 键重命名。

DEL key [key ...] 键删除。

3.2.2.5、字符串获取

GET key 获取值。

MGET key [key ...] 获取多个给定键的值。

GETSET key value 返回旧值并设置新值,如果键不存在,就创建并赋值。

STRLEN key 字符串长度。

127.0.0.1:6379> get s4
"4"
127.0.0.1:6379> mget s1 s3 s5 s7
1) "abc"
2) "3"
3) (nil)
4) (nil)
127.0.0.1:6379> strlen s3
(integer) 1
127.0.0.1:6379> getset s3 100
"3"
127.0.0.1:6379> getset s8 900
(nil)

3.2.2.6、字符串操作

APPEND key value 追加字符串。如果键存在就追加;如果不存在就等同于 SET key value

GETRANGE key start end 获取子字符串,索引值从 0 开始,支持负索引,-1 表示最后一个字符。范围是 [start, end],start 必须在 end 的左边,否则返回空串。

SETRANGE key offset value 从指定索引处开始覆盖字符串,返回覆盖后字符串长度。key 不存在会创建新的。

127.0.0.1:6379> APPEND s2 abc
(integer) 6
127.0.0.1:6379> get s2
"xyzabc"
127.0.0.1:6379> GETRANGE s2 1 3
"yza"
127.0.0.1:6379> GETRANGE s2 0 -1
"xyzabc"
127.0.0.1:6379> SETRANGE s2 3 12
(integer) 6
127.0.0.1:6379> get s2
"xyz12c"
127.0.0.1:6379> SETRANGE s2 3 12345
(integer) 8
127.0.0.1:6379> get s2
"xyz12345"
127.0.0.1:6379> SETRANGE s7 3 abc
(integer) 6
127.0.0.1:6379> get s7
"\x00\x00\x00abc"
127.0.0.1:6379>

3.2.2.7、自增、自减

INCR keyDECR key 步长为 1 的增减。

INCRBY key decrementDECRBY key decrement 步长增减。

字符串值会被解释成 64 位有符号的十进制整数来操作,结果依然转成字符串。

127.0.0.1:6379> GET s3
"100"
127.0.0.1:6379> INCRBY s3 4
(integer) 104
127.0.0.1:6379> DECR s3
(integer) 103
127.0.0.1:6379> DECRBY s3 2
(integer) 101

3.2.3、库操作

redis-cli -n 2,登录到 2 号库。

FLUSHDB,清除当前库数据。

FLUSHALL,清除所有库中的数据。

3.2.4、位图 bitmap

位图不是真正的数据类型,它是定义在字符串类型上,只不过把字符串按位操作。

一个字符串类型的值最多能存储 512M 字节的内容,可以表示 2^32 位。

# 计算位上限
512 = 2^9
1M = 1024*1024 = 2^(10+10)
1Byte = 8bit = 2^3bit
2^(9+10+10+3) = 2^32bit =  4294967296bit,接近43亿个位

SETBIT key offset value 设置某一位上的值:

  • offset 偏移量,从 0 开始。
  • value 不写,默认是 0。

GETBIT key offset 获取某一位上的值。

BITPOS key bit [start [end [BYTE|BIT]]] 返回指定值 0 或者 1 在指定区间上第一次出现的位置。

BITCOUNT key [start end [BYTE|BIT]] 统计指定区间上值为 1 的个数。

从左向右从 0 开始,从右向左从 -1 开始,注意 start、end 可以是位也可以是字节

BITCOUNT testkey 0 0 表示从索引为 0 个字节到索引为 0 个字节,就是第一个字节的统计(默认是字节)

BITCOUNT testkey 0 -1 等同于 BITCOUNT testkey

最常用的就是 BITCOUNT testkey

127.0.0.1:6379> SET s4 7
OK
127.0.0.1:6379> get s4 # 不是数字7,是ASCII编码字符"7",对应十进制55,二进制00110111
"7"
127.0.0.1:6379> BITCOUNT s4
(integer) 5
127.0.0.1:6379> GETBIT s4 0
(integer) 0
127.0.0.1:6379> GETBIT s4 1
(integer) 0
127.0.0.1:6379> GETBIT s4 2
(integer) 1
127.0.0.1:6379> GETBIT s4 3
(integer) 1
127.0.0.1:6379> GETBIT s4 4
(integer) 0
127.0.0.1:6379> GETBIT s4 5
(integer) 1
127.0.0.1:6379> GETBIT s4 6
(integer) 1
127.0.0.1:6379> GETBIT s4 7
(integer) 1

127.0.0.1:6379> BITPOS s4 1
(integer) 2
127.0.0.1:6379> BITPOS s4 0
(integer) 0

127.0.0.1:6379> BITCOUNT s4
(integer) 5
127.0.0.1:6379> BITCOUNT s4 0 0
(integer) 5
127.0.0.1:6379> BITCOUNT s4 0 0 BYTE
(integer) 5
127.0.0.1:6379> BITCOUNT s4 0 0 BIT
(integer) 0

127.0.0.1:6379> SET str1 abc
OK
127.0.0.1:6379> SETBIT str1 6 1
(integer) 0
127.0.0.1:6379> SETBIT str1 7 0
(integer) 1
127.0.0.1:6379> GET str1
"bbc"

3.2.4.1、位操作

对一个或多个保存二进制位的字符串 key 进行位操作,并将结果保存到 destkey 上。

operation 可以是 AND、OR、NOT、XOR 这四种操作中的任意一种。

BITOP AND destkey key [key ...],对一个或多个 key 求逻辑与,并将结果保存到 destkey。

BITOP OR destkey key [key ...],对一个或多个 key 求逻辑或,并将结果保存到 destkey。

BITOP XOR destkey key [key ...],对一个或多个 key 求逻辑异或,并将结果保存到 destkey。

BITOP NOT destkey key,对给定 key 求逻辑非,并将结果保存到 destkey。

除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。

当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0。

空的 key 也被看作是包含 0 的字符串序列。

思考:a 位或 b 是什么?

127.0.0.1:6379> SET s1 ab
OK
127.0.0.1:6379> BITCOUNT s1
(integer) 6
127.0.0.1:6379> BITCOUNT s1 0 0
(integer) 3
127.0.0.1:6379> BITCOUNT s1 1 1
(integer) 3
127.0.0.1:6379> set s2 a
OK
127.0.0.1:6379> set s3 b
OK
127.0.0.1:6379> BITOP or s8 s2 s3
(integer) 1
127.0.0.1:6379> 
127.0.0.1:6379> set cn 中
OK
127.0.0.1:6379> get cn
"\xe4\xb8\xad"
127.0.0.1:6379> BITCOUNT cn
(integer) 13

python3_redis_bitop

3.2.4.2、习题

1、如何统计网站用户的上线次数(活跃用户)?

用户 ID 为 key,天作为 offset,上线置为 1。ID 为 500 的用户,今年的第 1 天上线、第 30 天上线。

SETBIT u500 1 1

SETBIT u500 30 1

BITCOUNT u500

KYES u*

import redis

db = redis.Redis(host='192.168.229.132', password='WelC0me168!', db=2)  # 默认本地6379的0号库

# u1
db.setbit('u1', 1, 1)
db.setbit('u1', 30, 1)

# u2
db.setbit('u2', 110, 1)
db.setbit('u2', 300, 1)

# u101
for i in range(3, 365, 3):
    db.setbit('u101', i, 1)

# u105
for i in range(4, 365, 2):
    db.setbit('u105', i, 1)

userList = db.keys('u*')
print(userList)

Aus = []
Naus = []
for u in userList:
    loginCount = db.bitcount(u)
    if loginCount > 100:
        Aus.append((u, loginCount))
    else:
        Naus.append((u, loginCount))

for Au in Aus:
    print(str(Au[0]) + ' is a Active User --> ' + str(Au[1]))

print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")

for Nau in Naus:
    print(str(Nau[0]) + ' is not a Active User --> ' + str(Nau[1]))

2、怎样按天统计网站活跃用户?

把天当作 key,用户 ID 为 offset,上线置为 1。求一段时间内活跃用户数:

SETBIT 20160602 15 1

SETBIT 20160601 123 1

SETBIT 20160606 123 1

求 6 月 1 日到 6 月 10 日的活跃用户:

BITOP OR 20160601-10 20160601 20160602 20160603 20160610

BITCOUNT 20160601-10

结果为 2。

3.2.5、List 列表

  • 列表是基于双向链表实现,列表头尾增删快,中间增删慢。

  • 元素是字符串类型。

  • 元素可以重复出现。

  • 索引支持正索引和负索引,从左至右从 0 开始,从右至左从 -1 开始。

命令说明:

字母 说明
B Block 阻塞
L Left 左起
R Right 右起
X exist 存在

LLEN key 返回列表元素个数。

LPUSH key value [value ...] 从左边向队列中压入元素。

LPUSHX key value 从左边向队列加入元素,要求 key 必须存在。

RPUSH key value [value ...] 从右边向队列中压入数据。

RPUSHX key value 从右边向队列中压入数据,要求 key 存在。

LPOP key 从左边弹出列表中一个元素。

RPOP key 从右边弹出列表中一个元素。

RPOPLPUSH source destination 从源列表中右边 pop 一个元素,从左边加入到目标列表。

LRANGE key start stop 返回列表中指定访问的元素,例如 LRANGE user 0 -1

LINDEX key index 返回列表中指定索引的元素。

LSET key index value 设置列表中指定索引位置的元素值,index 不能超界。

LREM key count value 从左边删除列表中与 value 相等的元素。

  • count > 0 从左至右搜索,移除与 value 相等的元素,数量至多为 count 次。

  • count < 0 从右至左搜索,移除与 value 相等的元素,数量至多为 -count 次。

  • count = 0 移除列表中所有 value 值。

LTRIM key start stop 去除指定范围外的元素。

RPUSH listkey c abc c ab 123 ab bj ab redis list

LTRIM listkey 0 -1 # 什么都没有去除

LTRIM listkey 1 -1 # 去掉左边头

LTRIM listkey 1 10000

LINSERT key BEFORE|AFTER pivot value 在列表中某个存在的值(pivot)前或后插入元素一次,key 或 pivot 不存在,不进行任何操作。

RPUSH lst 1 2 3 4 2 8

LINSERT lst AFTER 2 Python

LINSERT lst BEFORE 2 Ruby

3.2.5.1、阻塞

如果弹出的列表不存在或者为空,就会阻塞。

超时时间设置为 0,就是永久阻塞,直到有数据可以弹出。

如果多个客户端阻塞在同一个列表上,使用 First In First Service 原则,先到先服务。

BLPOP key [key ...] timeout 列表左边阻塞弹出元素。timeout 是超时秒数,为 0 为永久阻塞。

BRPOP key [key ...] timeout 列表左边阻塞弹出元素。

BRPOPLPUSH source destination timeout 从一个列表尾部阻塞弹出元素压入到另一个列表的头部。

# 阻塞式消息队列
BLPOP MyQueue 0
RPUSH MyQueue hello

3.2.5.2、习题

微博某贴最后评论的 50 条。

LPUSH u1234:forumid:comments "这是第1条评论"
LPUSH u1234:forumid:comments "这是第2条评论"
LPUSH u1234:forumid:comments "这是第3条评论"
LTRIM u1234:forumid:comments 0 499

3.2.6、hash 散列

值是由 field 和 value 组成的 map 键值对。field 和 value 都是字符串类型。

python3_redis_hash

HSET key field value 设置单个字段。field 不存在则创建,存在则覆盖 value。

HSETNX key field value 设置单个字段,要求 field 不存在。如果 key 不存在,相当于 field 也不存在。

HMSET key field value [field value ...] 设置多个字段。

HLEN key 返回字段个数。

HEXISTS key field 判断字段是否存在。key 或者 field 不存在,返回 0。

HGET key field 返回字段值。

HMGET key field [field ...] 返回多个字段值。

HGETALL key 返回所有的键值对。

HKEYS key 返回所有字段名。

HVALS key 返回所有值。

HINCRBY key field increment 在字段对应的值上进行整数的增量计算。

HINCRBYFLOAT key field increment 在字段对应的值上进行浮点数的增量计算。

HDEL key field [field ...] 删除指定的字段。

HINCRBY numbers x 100
HINCRBY numbers x -50
HINCRBYFLOAT numbers x 3.14
HDEL numbers x

3.2.6.1、hash 用途

节约内存空间。

每创建一个键,它都会为这个键储存一些附加的管理信息(比如这个键的类型,这个键最后一次被访问的时间等等)。

所以数据库里面的键越多,redis 数据库服务器在储存附加管理信息方面耗费的内存就越多,花在管理数据库键上
的 CPU 时间也会越多。

python3_redis_hash1

不适合 hash 的情况

使用二进制位操作命令:因为 Redis 目前支持对字符串键进行 SETBIT、GETBIT、BITOP 等操作,如果你想使用这些操作,那么只能使用字符串键,虽然散列也能保存二进制数据。

使用过期键功能:Redis 的键过期功能目前只能对键进行过期操作,而不能对散列的字段进行过期操作,因此如果你要对键值对数据使用过期功能的话,那么只能把键值对储存在字符串里面。

3.2.6.2、习题

用户维度统计。

统计数包括:关注数、粉丝数、喜欢商品数、发帖数。

用户为 Key,不同维度为 Field,Value 为统计数。

比如关注了 5 人。

HSET user:100000 follow 5

HINCRBY user:100000 follow 1

商品维度统计。

统计值包括喜欢数,评论数,购买数,浏览数等。

HSET item:58000 fav 500

HINCRBY item:58000 fav 1

缓存用户信息。

登录后,反复需要读取用户的常用信息,最好的方式就是缓存起来。

set user:001 "bob,18,20010101"

mset user:001:name "bob" user:001:age 18 user:001:birthday "20010101"

hmset user:001 name "bob" age 18 birthday "20010101"

3.2.7、Set 集合

集合的元素是无序的、去重的,元素是字符串类型。

SADD key member [member ...] 增加一个或多个元素,元素已存在将忽略。

SREM key member [member ...] 移除一个或多个元素,元素不存在自动忽略。

SCARD key 返回集合中元素的个数。不需要遍历。

SMEMBERS key 返回集合中的所有元素。注意,如果集合中元素过多,应当避免使用该方法。

SISMEMBER key member 元素是否在集合中。

SADD f1 "peter" "jack" "tom" "john" "may" "ben"
SADD f2 "peter" "jack"
SADD f2 "tom" "john"
SADD f2 "may" "ben"
SMEMBERS f1
SMEMBERS f2

元素相同的两个集合,未必有相同的顺序。去重且有序可使用有序集合

SRANDMEMBER key [count] 随机返回集合中指定个数的元素。

  • 如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。

  • 如果 count 大于等于集合基数,那么返回整个集合。

  • 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。

  • 如果 count 为 0,返回空。

  • 如果 count 不指定,随机返回一个元素。

SPOP key 从集合中随机移除一个元素并返回该元素。

SMOVE source destination member 把元素从源集合移动到目标集合。

3.2.7.1、集合运算

差集

SDIFF key [key ...] 从第一个 key 的集合中去除其他集合和自己的交集部分。

SDIFFSTORE destination key [key ...] 将差集结果存储在目标 key 中。

SADD number1 123 456 789
SADD number2 123 456 999
SDIFF number1 number2

交集

SINTER key [key ...] 取所有集合交集部分。

SINTERSTORE destination key [key ...] 将交集结果存储在目标 key 中。

SADD number1 123 456 789
SADD number2 123 456 999
SINTER number1 number2

并集

SUNION key [key ...] 取所有集合并集。

SUNIONSTORE destination key [key ...] 将并集结果存储在目标 key 中。

SADD number1 123 456 789
SADD number2 123 456 999
SUNION number1 number2

3.2.7.2、习题

微博的共同关注。

需求:当用户访问另一个用户的时候,会显示出两个用户共同关注哪些相同的用户。

设计:将每个用户关注的用户放在集合中,求交集即可。

peter={'john','jack','may'}
ben={'john','jack','tom'}

那么peter和ben的共同关注为:
SINTER peter ben 结果为 {'john','jack'}

3.2.8、SortedSet 有序集合

类似 Set 集合,有序的集合。

每一个元素都关联着一个浮点数分值(Score),并按照分值从小到大的顺序排列集合中的元素。分值可以相同。

一个保存了水果价格的有序集合:

分值 2.0 3.2 4.0 6.8 7.0 9.2 12.0
元素 西瓜 香蕉 番石榴 芒果 葡萄 苹果

一个保存了员工薪水的有序集合:

分值 3500.0 4000.0 4000.0 4500.0 25000.0
元素 jack john peter tom david

一个有序集合,保存了正在阅读某些技术书的人数:

分值 251 347 928 1030 3436
元素 编程人生 人月神话 设计模式 深入了解计算机系统 算法导论

ZADD key score member [score member ...] 增加一个或多个元素。如果元素已经存在,则使用新的 score。

ZCARD key 返回集合的元素个数。

ZCOUNT key min max 返回指定 score 范围元素的个数。

ZSCORE key member 显示分值。

ZINCRBY key increment member 增加或减少分值。increment 为负数就是减少。

ZRANGE key start stop [WITHSCORES] 返回指定索引区间元素。

  • 如果 score 相同,则按照字典序 lexicographical order 排列。

  • 默认按照 score 从小到大,如果需要 score 从大到小排列,使用 ZREVRANGE。

ZREVRANGE key start stop [WITHSCORES] 返回指定索引区间元素。

  • 如果 score 相同,则按照字典序 lexicographical order 的逆序排列。

  • 默认按照 score 从大到小,如果需要 score 从小到大排列,使用 ZRANGE。

ZRANK key member 返回元素的排名(索引)。

ZREVRANK key member 返回元素的逆序排名(索引)。

ZADD employees 3500 jack 4000 peter 4000 john 4500 tom 2500 david
ZCOUNT employees 3000 4000
ZADD employees 3.2 david
ZSCORE employees david

ZINCRBY employees 1.5 jack
ZINCRBY employees -500 tom

ZRANGE employees 0 -1 WITHSCORES
ZRANK employees peter
ZREVRANGE employees 0 -1 WITHSCORES # 逆序后的索引0到-1,即返回所有
ZREVRANK employees peter

ZRANGEBYSCORE key min max [WITHSCORES][LIMIT offset count] 返回指定分数区间的元素。

  • 返回 score 默认属于 [min,max] 之间的元素,元素按照 score 升序排列,score 相同按照字典升序排列。

  • LIMIT 中 offset 代表跳过多少个元素,count 是返回几个。类似于 Mysql。

  • 使用小括号,修改区间为开区间,例如 (5 或者 (10、5)

  • -inf+inf 表示负无穷和正无穷。

ZREVRANGEBYSCORE key max min [WITHSCORES][LIMIT offset count] 降序返回指定分数区间的元素。

  • 返回 score 默认属于 [min,max] 之间的元素,元素按照 score 降序排列,score 相同按照字典降序排列。
ZRANGEBYSCORE employees 3500 4000
ZRANGEBYSCORE employees (4000 5000
ZRANGEBYSCORE employees 4000 5000 LIMIT 1 5 # 跳过一个,返回至多5个

ZREVRANGEBYSCORE employees +inf -inf

ZREM key member [member ...] 移除一个或多个元素。元素不存在,自动忽略。

ZREMRANGEBYRANK key start stop 移除指定排名范围的元素。

ZREMRANGEBYSCORE key min max 移除指定分值范围的元素。

ZREMRANGEBYRANK employees 0 1
ZREMRANGEBYSCORE employees 4000 5000

3.2.8.1、集合运算

并集

ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

numkeys 指定 key 的数量。

WEIGHTS 选项,与前面设定的 key 对应,对应 key 中每一个 score 都要乘以这个权重。

AGGREGATE 选项,指定并集结果的聚合方式。

SUM:将所有集合中某一个元素的 score 值之和作为结果集中该成员的 score 值,默认。

MIN:将所有集合中某一个元素的 score 值中最小值作为结果集中该成员的 score 值。

MAX:将所有集合中某一个元素的 score 值中最大值作为结果集中该成员的 score 值。

ZADD scores1 70 tom 80 peter 60 john
ZADD scores2 90 peter 60 ben
ZUNIONSTORE scores-all 2 scores1 scores2
ZUNIONSTORE scores-all1 2 scores1 scores2 AGGREGATE SUM
ZUNIONSTORE scores-all2 2 scores1 scores2 WEIGHTS 1 0.5 AGGREGATE SUM

交集

ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

numkeys 指定 key 的数量。

WEIGHTS 选项,与前面设定的 key 对应,对应 key 中每一个 score 都要乘以这个权重。

AGGREGATE 选项,指定并集结果的聚合方式。

SUM:将所有集合中某一个元素的 score 值之和作为结果集中该成员的 score 值。

MIN:将所有集合中某一个元素的 score 值中最小值作为结果集中该成员的 score 值。

MAX:将所有集合中某一个元素的 score 值中最大值作为结果集中该成员的 score 值。

3.2.8.2、习题

音乐排行榜怎样实现?

每首歌的歌名作为元素(先不考虑重复)。

每首歌的播放次数作为分值。

ZREVRANGE 来获取播放次数最多的歌曲。

import redis

db = redis.Redis(host='192.168.229.132', password='WelC0me168!', db=3)

data = {
    'yellow': 1,
    'rolling in the deep': 1,
    'happy': 1,
    'just the way you are': 1
}

db.zadd('mboard', data)
db.zadd('mboard', {'eye of the tiger': 1, 'billie jean': 1, 'say you say me': 1, 'payphone': 1})
db.zadd('mboard', {'my heart will go on': 1, 'when you believe': 1, 'hero': 1})

db.zincrby('mboard', 50, 'yellow')
db.zincrby('mboard', 60, 'rolling in the deep')
db.zincrby('mboard', 68.8, 'my heart will go on')
db.zincrby('mboard', 70, 'when you believe')

# 所有元素
allmusic = db.zrange('mboard', 0, -1, withscores=True)
print(type(allmusic))
for m in allmusic:
    print(m)
print('-' * 30)
# 排行榜
musicboard = db.zrevrange('mboard', 0, 9, True)
print('欧美热曲榜')
for i, m in enumerate(musicboard):
    print(i, *m)

新浪微博翻页?

新闻网站、博客、论坛、搜索引擎,页面列表条目多,都需要分页。

blog 这个 key 中使用时间戳作为 score。

ZADD blog 1407000000 '今天天气不错'

ZADD blog 1450000000 '今天我们学习Redis'

ZADD blog 1560000000 '几个Redis使用示例'

ZREVRANGE blog 10 20

显示所有博客中最后指定的条目。

京东图书畅销榜?

统计单日榜,计算出周榜单、月榜单、年榜单,怎么做?

每天统计一次排行榜
ZADD bk:it:01 1000 'java' 1500 'Redis' 2000 'haoop' 100 'scala' 80 'python'
ZADD bk:it:02 1020 'java' 1500 'Redis' 2100 'haoop' 120 'python' 110 'scala'
ZADD bk:it:03 1620 'java' 1510 'Redis' 3000 'haoop' 150 'storm' 120 'python'

求销售前10名
ZUNIONSTORE bk:it:01-03 3 bk:it:01 bk:it:02 bk:it:03
行吗?

因为上面的单日榜单是累计值,所以不能直接使用并集,要指定聚合运算为 MAX。

ZUNIONSTORE bk:it:01-03 3 bk:it:01 bk:it:02 bk:it:03 AGGREGATE MAX
ZREVRANGE bk:it:01-03 0 9 WITHSCORES

注意:如果参与并集元素的元素太多,会耗费大量内存和计算时间,可能会导致 Redis 服务阻塞,如果非要计算,选在空闲时间或备用服务器上计算。

另一种统计:

ZADD bk:it:01 50 'java' 20 'Redis' 40 'haoop'
ZADD bk:it:02 70 'java' 30 'Redis' 20 'haoop'
ZADD bk:it:03 20 'java' 30 'Redis' 5 'haoop'

每天统计当天销售量,统计IT类图书一段时间的最新销售榜单
ZUNIONSTORE bk:it:01-03 3 bk:it:01 bk:it:02 bk:it:03 AGGREGATE SUM
ZREVRANGE bk:it:01-03 0 9 WITHSCORES

3.3、Redis 持久化

持久化:将数据从掉电易失的内存存放到能够永久存储的设备上。

Redis 服务是使用内存来存储数据,如果掉电、服务崩溃都会导致 Redis 中数据丢失,如有必要,可以持久化数据。

Redis 持久化方式:RDB(Redis DB)、AOF(AppendOnlyFile)。

3.3.1、RDB

在默认情况下,Redis 将某时间点的数据库快照保存在名字为 dump.rdb 的二进制文件中。

策略:

  • 自动:按照配置文件中的条件满足就执行 BGSAVE。

  • 手动:客户端发起 SAVE、BGSAVE 命令。

3.3.1.1、配置

save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis/6379

save 60 1000,Redis 要满足在 60 秒内至少有 1000 个键被改动,会自动保存一次。

只要满足上面 3 个条件之一,就自动执行快照。执行完后,时间计数器和次数计数器都会归零重新计数。这里多个条件不是叠加效果。

SAVE 命令:阻塞式命令,执行期间不响应客户端的请求。

BGSAVE 命令:非阻塞命令,执行期间还可以接收并处理客户端请求,会 folk 一个子进程创建 RDB 文件。

127.0.0.1:6379> save
OK
127.0.0.1:6379> bgsave
Background saving started
127.0.0.1:6379>

优点:

  • 完全备份,不同时间的数据集备份可以做到多版本恢复。

  • 紧凑的单一文件,方便网络传输,适合灾难恢复。

  • 快照文件直接恢复,大数据集速度较 AOF 快。

缺点:

  • 会丢失最近写入、修改的而未能持久化的数据。

  • folk 过程非常耗时,会造成毫秒级不能响应客户端请求。

RDB 备份策略:

  • 创建一个定时任务 cron job,每小时或者每天将 dump.rdb 复制到指定目录。

  • 确保备份文件名称带有日期时间信息,便于管理和还原对应的时间点的快照版本。

  • 定时任务删除过期的备份。

  • 如果有必要,跨物理主机、跨机架、异地备份。

3.3.2、AOF

Append only file,采用追加的方式保存,默认文件 appendonly.aof。

记录所有的写操作命令,在服务启动的时候使用这些命令就可以还原数据库。

3.3.2.1、AOF 写入机制

AOF 方式不能保证绝对不丢失数据

目前常见的操作系统中,执行系统调用 write 函数,将一些内容写入到某个文件里面时,为了提高效率,系统通常不会直接将内容写入硬盘里面,而是先将内容放入一个内存缓冲区(buffer)里面,等到缓冲区被填满,或者用户执行 fsync 调用或 fdatasync 调用时才将储存在缓冲区里的内容真正的写入到硬盘里,未写入磁盘之前,数据可能会丢失。

写入磁盘的策略

appendfsync 选项,这个选项的值可以是 always、everysec 或者 no。

  • Always:服务器每写入一个命令,就调用一次 fdatasync,将缓冲区里面的命令写入到硬盘。这种模式下,服务器出现故障,也不会丢失任何已经成功执行的命令数据。

  • Everysec(默认):服务器每一秒重调用一次 fdatasync,将缓冲区里面的命令写入到硬盘。这种模式下,服务器出现故障,最多只丢失一秒钟内的执行的命令数据。

  • No:服务器不主动调用 fdatasync,由操作系统决定何时将缓冲区里面的命令写入到硬盘。这种模式下,服务器遭遇意外停机时,丢失命令的数量是不确定的。

运行速度:always 的速度慢,everysec 和 no 都很快。

3.3.2.2、AOF 重写机制

写操作越来越多的被记录,AOF 文件会很大。Redis 会合并写操作,以压缩 AOF 文件。

合并重复的写操作,AOF 会使用尽可能少的命令来记录。

重写过程

  1. folk 一个子进程负责重写 AOF 文件。
  2. 子进程会创建一个临时文件写入 AOF 信息。
  3. 父进程会开辟一个内存缓冲区接收新的写命令。
  4. 子进程重写完成后,父进程会获得一个信号,将父进程接收到的新的写操作由子进程写入到临时文件中。
  5. 新文件替代旧文件。

注:如果写入操作的时候出现故障导致命令写半截,可以使用 redis-check-aof 工具修复。

原有AOF文件 重写后的AOF文件
SELECT 0 SELECT 0
SADD fruits "apple" SADD fruits "apple" "banana" "cherry"
SADD fruits "banana" SET msg "hello world again!"
SADD fruits "cherry" RPUSH lst 3 5 7
SADD fruits "apple"
INCR counter
INCR counter
DEL counter
SET msg "hello world"
SET msg "hello world again!"
RPUSH lst 1 3 5
RPUSH lst 7
LPOP lst

AOF重写触发

手动:客户端向服务器发送 BGREWRITEAOF 命令。

自动:配置文件中的选项,自动执行 BGREWRITEAOF 命令。

auto-aof-rewrite-min-size <size>:触发 AOF 重写所需的最小体积。只要在 AOF 文件的体积大于等于 size 时,才会考虑是否需要进行 AOF 重写,这个选项用于避免对体积过小的 AOF 文件进行重写。

auto-aof-rewrite-percentage <percent>:指定触发重写所需的 AOF 文件体积百分比。当 AOF 文件的体积大于 auto-aof-rewrite-min-size 指定的体积,并且超过上一次重写之后的 AOF 文件体积的 percent % 时,就会触发 AOF 重写。(如果服务器刚刚启动不久,还没有进行过 AOF 重写,那么使用服务器启动时载入的 AOF 文件的体积来作为基准值)。将这个值设置为 0 表示关闭自动 AOF 重写。

重写配置举例:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
当AOF文件大于64MB时候,可以考虑重写AOF文件
只有当AOF文件的增量大于起始size的100%时(就是文件大小翻了一倍),启动重写

appendonly yes
默认关闭,请开启

优点:

  • 写入机制,默认 fysnc 每秒执行,性能很好不阻塞服务,最多丢失一秒的数据。

  • 重写机制,可以优化 AOF 文件体积。

  • 如果误操作了(FLUSHALL 等),只要 AOF 未被重写,停止服务移除 AOF 文件尾部 FLUSHALL 命令,重启 Redis,可以将数据集恢复到 FLUSHALL 执行之前的状态。

缺点:

  • 相同数据集,AOF 文件体积较 RDB 大了很多。

  • 恢复数据库速度较 RDB 慢(文本,命令重演)。

3.4、Redis 集群

Redis 集群分为:

  • 主从复制 Replication

  • 高可用 Sentinel

  • 集群 Cluster

3.4.1、主从复制

典型的主从模型,主 Redis 服务称为 Master,从 Redis 服务称为 Slave。

一主可以多从。

Master 会一直将自己的数据更新同步到 Slave,以保持主从同步。

只有 Master 可以执行读写操作,Slave 只能执行读操作。客户端可以连接到任一 Slave 执行读操作,来降低 Master 的读取压力。

创建主从复制:

  1. 命令创建

    redis-server --slaveof <master-ip> <master-port>

    配置当前服务成为某 Redis 服务的 Slave

    redis-server --port 6380 --slaveof 127.0.0.1 6379

  2. 指令创建

    SLAVEOF host port 命令,将当前服务器状态从 Master 修改为别的服务器的 Slave

    redis > SLAVEOF 192.168.1.1 6379,将服务器转换为 Slave

    redis > SLAVEOF NO ONE,将服务器重新恢复到 Master,不会丢弃已同步数据

  3. 配置方式

    启动时,服务器读取配置文件,并自动成为指定服务器的从服务器

    slaveof <masterip> <masterport>

    slaveof 127.0.0.1 6379

3.4.1.1、主从实验

Master:192.168.229.135 6379

Slave:192.168.229.132 6379

master:6379> KEYS *
1) "key1"
2) "key2"
master:6379>
slave:6379> KEYS *
(empty array)
slave:6379> SET k1 v1
OK
slave:6379> KEYS *
1) "k1"
slave:6379>
slave:6379> SLAVEOF 192.168.229.135 6379
OK
slave:6379> KEYS *
1) "key1"
2) "key2"
slave:6379> SET k2 v2
(error) READONLY You can't write against a read only replica.
master:6379> SET key3 hello3
OK
slave:6379> KEYS *
1) "key3"
2) "key1"
3) "key2"
slave:6379> info replication # 查看主从信息
# Replication
role:slave
master_host:192.168.229.135
master_port:6379
master_link_status:up
...
slave:6379> SLAVEOF NO ONE # 解除从关系
OK
slave:6379> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover

注意:我们安装 redis 时,配置文件中指定了 requirepass <password>,所以配置主从时,从服务器配置文件中要指定 masterauth <password> 认证。

采用上面的多种方式都可以实现主从模式,一般来说,主从服务器都是固定的,采用配置文件方式。

3.4.1.2、主从复制问题

一个 Master 可以有多个 Slaves。

如果 Slave 下线,只是读请求的处理能力下降。

但 Master 下线,写请求无法执行。

当 Master 下线,其中一台 Slave 使用 SLAVEOF no one 命令成为 Master,其它 Slaves 执行 SLAVEOF 命令指向这个新的 Master,从它这里同步数据。

这个主从转移的过程是手动的,如果要实现自动故障转移,这就需要 Sentinel 哨兵,实现故障转移 Failover 操作。

3.4.2、高可用和集群

redis 的高可用和集群方案参见官网 https://redis.io/docs/management/sentinel/

标签云