Redis 学习笔记

最后更新:
阅读次数:

Redis 是一个读取和写入速度超高的非关系型数据库,即是一种 NoSQL 数据库。它可以存储键值对,可以将存储在内存的键值对数据持久化到本地硬盘,可以使用复制特性来扩展读写性能,还可以使用客户端分片来扩展性能,并且它还提供了多种语言的API。此外,它可以作为主数据库使用,又可以作为其他存储系统的辅助数据库。

安装

# 安装 redis
brew install redis

# 查看 Redis 安装包信息
brew info redis

# Redis 配置文件默认路径
/usr/local/etc/redis.conf

# 更新 redis
brew upgrade redis

# 启动 redis 服务
redis-server # 默认端口 6379
redis-server --port 6380 # 指定端口 6380

# 查看 redis 版本
redis-server -v

# 进入 redis 命令行
# 默认连接本地 Redis 服务,127.0.0.1:6379
redis-cli

# 连接远程服务器
# redis-cli -h host -p port -a password
redis-cli -h 149.28.49.218 -p 6699 -a "mypasswww"

# 读操作显示原始字符串,避免被转码
redis-cli --raw

# 清除 redis 中所有的数据
redis-cli flushall

基本特点

  • 性能极高:因为 Redis 以内存作为数据存储介质
    • 以设置和获取一个 256 字节字符串为例,它的读取速度可高达 110000次/s,写速度高达 81000次/s
  • 支持数据持久化
    • 也可以通过配置文件,关闭持久化功能
  • 支持多种数据结构:字符串、列表、集合、有序集合、哈希表等
  • 支持多种编程语言:为大部分编程语言都提供了相应的调用库
  • 支持主从复制模式:具有高可用性,可以配置集群,可以支撑起大型的项目

支持的数据结构

字符串

  • 简介

String 类型是 Redis 最基本的数据类型,它是二进制安全的。意思是 redis 的字符串可以包含任何数据。比如 jpg 图片或者序列化的对象。String 类型的值最大能存储 512MB。

二进制安全,一种计算机编程术语,简单点讲就是用于描述字符串能不能安全地存储二进制数据。

举例:C 语言的字符串就不是二进制安全的,因为 C 语言的字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符(\0),否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得 C 语言的字符串只能保存普通的文本数据,而不能保存图片、音频、视频、压缩文件这样的二进制数据。

  • 常用命令
# 获取指定键的值
GET key

# 设置指定键的值
# EX 指定生存时间,单位秒
# PX 指定生存时间,单位毫秒
# NX 表示只有键不存在时,才可进行设置操作,用于添加
# XX 表示只有键存在时,才可进行设置操作,用于更新
SET key value [EX seconds] [PX milliseconds] [NX|XX]

# 获取一个或多个指定键的值
MGET key1 key2 ...
# 实例
GET str # (nil)
SET str 123 # OK
GET str # "123"

SET str 123 EX 10 # 10s 的生存时间

SET a 123 # OK
SET a 666 NX # (nil)
SET a 444 XX # OK

列表

  • 简介

Redis 的列表是简单的字符串列表,即每个元素都是字符串,按照插入顺序排序。列表最多可存储 2^32 - 1 个元素 (4294967295, 每个列表可存储大约40多亿个元素)。

  • 常用命令
# 将指定的一个或多个值依次插入列表左端,返回列表的长度
LPUSH key value1 value2 ...

# 将指定的一个或多个值依次插入列表右端,返回列表的长度
RPUSH key value1 value2 ...

# 删除列表左端的第一个值,返回这个删除的值
LPOP key

# 删除列表右端的第一个值,返回这个删除的值
RPOP key

# 获取列表指定位置的值,返回这个值
LINDEX key index

# 获取指定位置范围的值
# 列表索引范围从 0 开始,到 -1 结束
LRANGE key start stop
# 实例
LPUSH list 11 22 33 44 55 # (integer) 5
RPUSH list 0 # (integer) 6

LPOP list # "55"
RPOP list # "0"

LINDEX list 0 # "44"

LRANGE list 0 2 # ["44", "33", "22"]

# 取出列表全部的值
LRANGE list 0 -1 # ["44", "33", "22", "11"]

集合

  • 简介

Redis 的集合是每个元素都是唯一的字符串(不重复)的无序集合。因为集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

  • 常用命令
# 将一个或多个值添加到集合,返回成功添加的值的个数
SADD key value1 value2 ...

# 返回指定键的集合的所有值
SMEMBERS key

# 检查指定值是否在指定集合中
SISMEMBER key value

# 如果指定的一个或多个值存在于指定集合中,则从集合中删除这些值,返回删除的值的个数
SREM key value1 value2 ...
# 实例
SADD set 11 22 33 44 55 # (integer) 5
SADD set 11 111 # (integer) 1

SMEMBERS set # ["11", "22", "33", "44", "55", "111"]

SISMEMBER set 11 # (integer) 1
SISMEMBER set 66 # (integer) 0

SREM set 11 22 333 # (integer) 2

哈希表

  • 简介

哈希表,也叫散列表。Redis 的哈希表可以存储多个键值对,键和值都是以字符串的形式进行存储的。一个哈希表最多可以包含 2^32-1 个 key-value 键值对(超过40亿)。

  • 常用命令
# 向哈希表中添加键值对
HSET key field value

# 获取哈希表指定字段的值
HGET key field

# 获取整个哈希表
HGETALL key

# 如果指定的一个或多个字段存在于指定哈希表,则从哈希表中删除这些字段,返回删除的字段的个数
HDEL key field1 field2 ...

# 判断某个字段是否存在于指定哈希表
HEXISTS key field

# 获取指定哈希表的所有字段
HKEYS key

# 获取指定哈希表的所有值
HVALS key
# 实例
HSET hash name percy # (integer) 1
HSET hash age 22 # (integer) 1
HSET hash a 123 # (integer) 1
HSET hash b 111 # (integer) 1
HSET hash c 222 # (integer) 1

HGET hash age # "22"

HGETALL hash # {"name":"percy", "age":"22", "a":"123", "b":"111", "c":"222"}

HDEL hash a b aaa # (integer) 2

HEXISTS hash c # (integer) 1
HEXISTS hash a # (integer) 0

HKEYS hash # ["name", "age", "c"]
HVALS hash # ["percy", "22", "222"]

有序集合

  • 简介

Redis 的有序集合和无序集合类似,也是每个元素都是唯一字符串的集合。不同的是,有序集合的每个元素都会关联一个浮点数类型的分数,并且 Redis 会通过这个分数来为集合中的成员进行从小到大的排序。

有序集合的每个元素是唯一的,但分数却可以重复。

  • 常用命令
# 将一个或多个绑定分数的值添加到有序集合,返回成功添加的个数
ZADD key [NX|XX] [CH] [INCR] score1 value1 score2 value2 ...

# 获取指定值在有序集合中关联的分数
ZSCORE key value

# 获取指定位置范围的值
# 列表索引范围从 0 开始,到 -1 结束
ZRANGE key start stop [WITHSCORES]

# 获取指定分值范围内的所有值的个数
ZCOUNT key min max

# 获取指定分值范围内的所有值
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

# 如果指定的一个或多个值存在于指定有序集合中,则从有序集合中删除这些值,返回删除的值的个数
ZREM key value1 value2 ...
# 实例
ZADD zset 9 aaa 4 bbb 6 ccc # (integer) 3

ZSCORE zset aaa # "9"

ZRANGE zset 0 1 # ["aaa", "bbb"]
ZRANGE zset 0 -1 # ["aaa", "bbb", "ccc"]

ZCOUNT zset 1 7 # (integer) 2
ZRANGEBYSCORE zset 1 7 # ["bbb", "ccc"]

ZREM zset aaa 123 bbb # (integer) 2

位图

  • 简介

位图(Bitmap,或 Bit Array)不是特殊的数据结构,它的内容其实就是普通的字符串,也就是字节数组。我们可以使用普通的 GET/SET 命令直接获取和设置整个位图的内容,也可以使用位图操作 GETBIT/SETBIT 等将 字节数组 看成 位数组 来处理。(1字节 = 8位,即 1 byte = 8 bit,每个 bit 上只能存储 0 或 1)

  • 常用命令
# 设置指定位置上位的值
SETBIT key offset value

# 获取指定位置上位的值
GETBIT key offset

# 获取指定范围内 bit 位为一的个数
# start 和 end 都是字节索引,也就是说指定的位范围必须是 8 的倍数,不能任意指定
BITCOUNT key [start end]

# 对两个或多个值进行位运算,并将结果存到 destkey 中
# operation:AND, OR, XOR, NOT
# NOT 取反运算只需要一个 key
BITOP operation destkey key1 key2 ...

# 返回第一个符合指定位的值的位置
# start,end 也均为字节索引
BITPOS key bit [start] [end]

# 操作位域,即一条指令操作多个位,参考:https://cloud.tencent.com/developer/section/1374165
# 可以一次执行多个子指令
# GET 以指定的数据格式获取指定位域的值
# SET 设置指定的位域的值并返回其旧值
# INCRBY 递增或递减(如果给定负递增)指定的位域并返回新值
# type: i8 表示有符号8位整数,u16 表示无符号16位整数
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
# 示例
# 字符串 `ab` 的二进制为: 0110 0001 0110 0010
SET bit ab

GETBIT bit 0 # 0
BITCOUNT bit # 6
BITCOUNT bit 0 0 # 3

SETBIT bit 0 1 # 0

GETBIT bit 0 # 1
BITCOUNT bit # 7
BITCOUNT bit 0 0 # 4

# 此时 bit 的二进制为: 1110 0001 0110 0010
BITOP NOT bit2 bit # 2

# 取反后,bit2 二进制为:0001 1110 1001 1101
BITCOUNT bit2 # 9
BITCOUNT bit # 7

# 获取位置
BITPOS bit 0 # 3
BITPOS bit 1 1 # 9

# 98 的二进制: 0110 0010
BITFIELD bit GET i8 8 # 98
BITFIELD bit get i8 8 get i4 8 # 98 6

HyperLogLog

  • 简介

Redis 的 HyperLogLog 是一种用于估算一个集合中元素数量的概率性的高级数据结构。目的是做基数统计,不是集合,不会保存元数据,只记录数量而不是数值。其核心是基数估算算法,即 HyperLogLog 算法,Redis 的 HyperLogLog 数据结构只是对 HyperLogLog 算法的一种实现。

HyperLogLog 算法是一种用于估算基数的概率性算法,最终数值存在一定误差。其特点是在输入元素的数量非常非常大时,计算基数所需的空间总是固定的、并且是很小的。该特性对于大数据的统计而言,其误差是可以容忍的。

Redis 中每个 HyperLogLog 结构占用了 12K 的内存用于标记基数,最大可以统计 2^64(约 1800 千兆) 个数,并且最终结果是一个带有 0.81% 标准错误(standard error)的近似值。值得注意的是,Redis 对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小(小于 12K),仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间。

Redis 中 HyperLogLog 结构的内存占用为什么是 12K 呢?是因为在 Redis 的 HyperLogLog 实现中用到的是 16384 个桶,也就是 2^14,每个桶的 maxbits 需要 6 个 bits 来存储,最大可以表示 maxbits=63,于是总共占用内存就是2^14 * 6 / 8 = 12k字节。(具体请看算法实现)

  • 常用命令
# 添加一个或多个元素到指定的 HyperLogLog 结构中
PFADD key value1 value2 ...

# 获取指定 HyperLogLog 结构的近似基数
# 当有多个key时,返回这多个 HyperLogLog 结构并集的近似基数
# 副作用:PFCOUNT 命令可能会导致 HyperLogLog 结构内部被更改
PFCOUNT key1 key2 ...

# 合并多个 HyperLogLog 结构为一个 HyperLogLog 结构,并存储到 destkey
PFMERGE destkey key1 key2 ...
# 实例
PFADD uv 5 # 1
PFADD uv 1 2 3 4 5 4 3 6 # 1
PFCOUNT uv # 6

PFADD 2018_01_UV 1 2 3 4 5 # 1
PFADD 2018_02_UV 7 6 3 4 5 4 8 # 1
PFCOUNT 2018_01_UV 2018_02_UV # 8

PFMERGE 2018_01-02_UV 2018_01_UV 2018_02_UV # OK
PFCOUNT 2018_01-02_UV # 8

一个思考

  • Redis 虽然支持一系列如字符串、列表、哈希表等数据结构,但是却不支持数值类型。虽然字符串结构可以存储数值类型的数据,但 Redis 却不会记忆数据原始的数据类型
# Redis 记录了数值类型,却返回了字符串类型
SET test 123 # OK
GET test # "123"
  • 针对上面的问题,有两种解决方案:
    1. 使用 JSON 字符串:原始数据在存储前,先转为 JSON 字符串,然后再进行存储
    2. 对键使用阶层式的命名:比如要存储一个数值类型的数据,可以命名键为 age.int,然后执行命令 SET age.int 22

应用场景

  • 缓存:因为 Redis 以内存为存储介质,读写速度快

  • 消息队列系统:一般可以使用列表数据结构来实现,如果是带有权重的消息队列系统,则可以考虑使用有序集合来实现。

  • 微博时间轴:可以使用列表来实现,然后可以使用 LRANGE 命令来获取最新的某一段微博

  • 排行榜:使用有序集合来实现

  • 获取共同好友:则可以利用 Redis 为集合提供的求交集、并集、差集等操作来实现

  • 分布式锁

  • 记录各种状态:比如记录用户签到状态,在线状态等,可以使用 Redis 的位图来实现。此外,还可以利用 Redis 位位图提供的 AND、OR、NOT 等位操作来实现统计的功能,比如使用 AND 操作可以过滤出三个月内的活跃签到用户等。

  • 大数据统计:比如统计注册 IP 数,统计每日访问 IP 数,统计页面实时 UV 数,统计在线用户数,统计用户每天搜索不同词条的个数等。Redis 的 HyperLogLog 结构非常适用于需要统计的数据量较大的应用场景,因为仅占用很小的空间就可统计出可容忍误差的近似值。

其它命令

Redis 提供了很多用来操作数据库的命令,对于不同的数据结构,有一部分命令是通用的,如 DELTYPERENAME 等,但也有一部分命令只能对特定的一种或者两种结构使用。

在使用命令操作数据库时,适当地使用批量操作命令(比如 MGETHMGET 等)可以减少网络请求次数,提供更高的性能。

系统命令

# 打印字符串
ECHO message

# 检查服务是否还在运行
PING # PONG

# 切换到指定数据库,默认使用索引为 0 的数据库
SELECT index

# 查看服务器配置信息及资源使用情况
INFO

# 退出当前连接
QUIT

操作键

# 检查指定的一个或多个键是否存在,返回存在键的个数
EXISTS key1 key2 ...

EXISTS a b list # (integer) 2


# 重命名键,如果新键的名称已存在,则新键原来的值会被覆盖
RENAME key newKey

SET a 123
SET b 111

GET b # "111"
RENAME a b
GET b # "123"

# 重命名键,仅当 newkey 不存在时,才将 key 改名为 newkey
RENAMENX key newKey


# 删除一个或多个键值对,命令在执行成功后会返回被成功删除的键值对的数量
DEL key1 key2 ...

DEL a b # (integer) 2
  • 键的生存时间相关命令
# 为指定键设置生存时间,单位秒,当键过期时(生存时间为0时),它会被自动删除
EXPIRE key seconds

# 返回指定键剩余的生存时间,单位秒
# TTL = Time To Live
TTL key

# 设置指定键的生存时间,单位毫秒
PEXPIRE key milliseconds

# 返回指定键剩余的生存时间,单位毫秒
PTTL key

# 移除指定键的生存时间
PERSIST key


SET a 123 # OK
EXPIRE a 60 # (integer) 1
TTL a # (integer) 54 剩余54秒过期
PERSIST a # (integer) 1
  • 其它命令
# 查找所有匹配正则表达式的键
KEYS pattern

KEYS "*i*" # ["list"]
KEYS "*" # ["a", "list", "set", "zset", "hash"]


# 从数据库中随机返回一个键
RANDOMKEY


# 返回指定键存储的数据的数据结构类型
TYPE key

TYPE a # string


# 将当前数据库的 key 移动到给定的数据库 db 当中
MOVE key db

发布与订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。Redis 客户端可以订阅任意数量的频道(channel)。

  • 相关命令
# 订阅一个或多个指定频道
SUBSCRIBE channel1 channel2 ...

# 订阅所有匹配指定正则表达式的频道
PSUBSCRIBE pattern1 pattern2 ...


# 退订一个或多个指定频道
# 如果一个频道也没有指定,则默认退订当前客户端订阅的所有频道
UNSUBSCRIBE channel1 channel2 ...

# 退订所有匹配指定正则表达式的频道
# 如果一个 pattern 也没有指定,则默认退订当前客户端订阅的所有频道
PUNSUBSCRIBE pattern1 pattern2 ...


# 向指定频道发布消息
PUBLISH channel message
# 查看订阅与发布系统状态
# https://redis.io/commands/pubsub
PUBSUB subcommand [argument [argument ...]]

# 示例,返回当前活跃的频道列表
PUBSUB CHANNELS
  • 实例
### 终端 1,订阅频道
SUBSCRIBE testChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "testChannel"
3) (integer) 1
### 终端 2,发布消息
PUBLISH testChannel "Redis is great~" # (integer) 1
### 终端 1,接收到消息
SUBSCRIBE testChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "testChannel"
3) (integer) 1
1) "message"
2) "testChannel"
3) "Redis is great~"

事务

事务是一个单独的隔离操作,可以一次执行多个命令,事务中的所有命令都会按顺序执行。

事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。并且事务是一个原子操作,即事务中的命令要么全部执行,要么全不执行。

一个完整的事务从开始到执行会经历以下三个阶段:开始事务,命令入队,执行事务。

  • 相关命令
# 标记一个事务块的开始
MULTI

# 执行所有事务块内的命令
EXEC

# 取消当前的事务
DISCARD
### 示例一
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET test1 "This is test"
QUEUED
127.0.0.1:6379> GET test1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "This is test"
# 监视一个或多个键,如果事务执行之前,这些键被其它命令所改动,那么事务将被打断
# WATCH 命令不能出现在事务之中
WATCH key1 key2 ...

# 取消 WATCH 命令对所有键的监视
UNWATCH
### 示例二
127.0.0.1:6379> WATCH test1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET test1 "change value"
QUEUED
127.0.0.1:6379> GET test1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "change value"

持久化

Redis 支持数据持久化,即把内存中的数据以一定的方式存储到传统数据库中,或存储在磁盘文件中。Redis 提供了两种持久化方法,分别是 RDB(Redis DataBase)持久化AOF(Append Only File)持久化

RDB 持久化

RDB 持久化,又称快照持久化,是指以设定好的时间间隔,周期性地将某一时刻的所有数据(快照)都写入硬盘里面的一种操作。

一般,Redis 会按照配置文件中设定的周期定时进行一次 RDB 持久化。但是,我们也可以通过 SAVEBGSAVE 命令来手动进行 RDB 持久化。

# 同步进行 RDB 持久化
# 注意:执行 SAVE 命令,会阻塞其它命令的执行
SAVE

# 异步进行 RDB 持久化
BGSAVE
# RDB 持久化在 Redis 配置文件中的选项
save 60 1000 # 60秒内有1000次写入操作的时候执行快照的创建
stop-writes-on-bgsave-error no # 创建快照失败的时候是否仍然继续执行写命令
rdbcompression yes # 是否对快照文件进行压缩
dbfilename dump.rdb # 如何命名硬盘上的快照文件
dir ./ # 快照所保存的位置
  • 优点
    • 存储的数据非常紧凑,占用硬盘资源小
    • 适合大规模的数据恢复
  • 缺点
    • 数据的完整性和一致性不高

AOF 持久化

AOF 持久化,是指通过追加到文件的末尾的方式记录服务器执行的所有写操作命令,并在服务器启动时,重新执行这些命令来还原数据集的一种操作。

AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 随着数据量的增大,AOF 文件可能会变的很大,Redis 就会自动在后台对 AOF 文件进行重写(rewrite),删除掉冗余的命令,大大减小 AOF 文件的体积。

# AOF 持久化在 Redis 配置文件中的选项
appendonly no # 是否使用AOF持久化
appendfsync everysec # 多久才将写入的内容同步到硬盘
no-appendfsync-on-rewrite no # 在对AOF进行压缩(compaction)的时候能否执行同步操作
auto-aof-rewrite-percentage 100 # 多久执行一次AOF压缩
auto-aof-rewrite-min-size 64mb # 多久执行一次AOF压缩
dir ./ # AOF所保存的位置
  • appendfsync 选项定义同步频率,有三个值

    • always: 每个 Redis 写命令都要同步写入硬盘。这样做会严重降低Redis的速度。不推荐
    • everysec : 每秒执行一次同步,将多个写命令一次同步到硬盘
    • no: 让操作系统来决定应该何时进行同步,不推荐
  • 优点

    • 数据的完整性和一致性更高
  • 缺点
    • AOF 文件记录的内容多,文件会越来越大,数据恢复也会越来越慢

持久化总结

  • Redis 默认开启 RDB 持久化方式,在指定的时间间隔内,执行指定次数的写操作,则将内存中的数据写入到磁盘中。Redis 需要手动开启 AOF 持久化方式,默认是每秒将写操作日志追加到 AOF 文件中。
  • 若只打算用 Redis 做缓存,可以关闭持久化。
  • 若打算使用 Redis 的持久化。建议 RDB 和 AOF 都开启。其实 RDB 更适合做数据的备份,留一后手。AOF 出问题了,还有 RDB。

主从复制

主从复制,是指将一台 Redis 服务器作为主服务器,再将其数据单向地复制或同步到其它服务器(从服务器)的一种模式。

主服务器也叫主节点,从服务器也叫从节点。默认情况下,每台 Redis 服务器都是主节点;且一个主节点可以有多个从节点,但一个从节点只能有一个主节点。

主从复制的复制又分为全量复制部分复制,部分复制是通过设置一个复制偏移量来实现的。

  • 主从复制的作用主要包括:
    • 数据备份:主从复制实现了数据的热备份,是除持久化之外的另一种数据备份方式,均可用于故障恢复
    • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量
    • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础

参考资料