1. 简介

Redis 全程是 REmote Dictionary Server,是一个基于键值对(key - value)的 NoSQL 数据库。它将所有数据存放在内存中,所以它的读写性能非常惊人。

Redis 官网:Redis

Redis 源码:redis/redis

Redis 中的值包含 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)五种基础的数据结构,另外还支持 Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等数据结构。此外,Redis 还提供了键过期、发布订阅、事务、流水线、Lua 脚本等功能。

Redis 官方宣称读写性能可以达到 10 万/秒,执行读写命令的速度非常快,主要是因为以下几点:

  • 所有的数据都存放在内存中,访问速度比硬盘快很多倍
  • 使用 C 语言实现,代码简介高效
  • 使用单线程结构,避免了线程切换和竞态产生的消耗
  • 使用 epoll 实现非阻塞 I/O 多路复用

Redis 提供了 RDB 和 AOF 两种持久化方式,将内存的数据保存到硬盘中,避免断电和重启导致的数据丢失。

Redis 提供了复制功能,实现了多个相同数据的副本,提升了可用性。

Redis 从 3.0 版本开始提供分布式实现的 Redis Cluster,提供了高可用,扩展了读写性能和容量。

Redis 典型的使用场景:

  • 缓存:加快数据的访问速度,降低数据库的压力,通过键过期时间和内存淘汰策略更合理地对数据进行缓存
  • 排行榜系统:列表和有序集合数据结构可以用于构建排行榜系统
  • 计数器:如视频播放数、电商网页浏览数的统计
  • 消息队列:提供了基础的发布订阅功能和阻塞队列功能

由于 Redis 的数据是存放在内存中的,成本相较于硬盘更高,所以不适合用于存储大规模的数据。数据根据是否频繁操作,分为热数据与冷数据,Redis 更适合存储热数据而非冷数据。

2. 数据结构与命令

2.1 全局命令

2.1.1 状态

Redis 实例状态,包含服务器、客户端、CPU、内存等信息。

info

2.1.2 键

列出键,参数支持匹配规则,* 表示任意数量字符,? 表示任意一个字符,[] 表示匹配部分字符,\ 表示转义字符。线上环境可能保存了大量的键,不要直接查看所有键。

# 符合匹配规则的键
keys <format>

# 所有键
keys *

# 列出部分键
# cursor 是游标,从 0 开始,每次会返回当前游标,到 0 表示遍历结束
# count 是每次遍历的键数量,默认值为 10
scan <cursor> [<format>] [<count>]

# 针对哈希、集合、有序集合的键扫描命令有:hscan、sscan、zscan,用法类似

当前键的总数量。

dbsize

检查键是否存在,不支持匹配规则。

exists <key>

删除一到多个键,返回成功删除的键数量。

del <keys>

重命名键。

# 如果新名字存在键,则覆盖
rename <key> <newkey>

# 如果新名字存在键,则会失败
renamenx <key> <newkey>

随机返回一个键。

randomkey

对一个键设置过期时间,过期后键会自动删除,过期时间为负会被立刻删除。

# 指定有效时长,单位为秒
expire <key> <second>

# 指定过期秒级时间戳
expireat <key> <timestamp>

# 指定有效时长,单位为毫秒
pexpire <key> <millisecond>

# 指定过期毫秒级时间戳
pexpireat <key> <timestamp>

# 清除过期时间,字符串用 set 也会去除过期时间,其他类型不会
persist <key>

查看键的剩余过期时间。正整数表示剩余的过期时间,-1 表示未设置过期时间,-2 表示键不存在。

# 秒级精度
ttl <key>

# 毫秒级精度
pttl <key>

查看键的数据结构类型,键不存在则返回 none。

type <key>

查看键的内部编码实现。

object encoding <key>

2.1.3 数据库

Redis 默认配置有 16 个数据库,各个数据库之间的数据是分隔无关联的,键名称不会冲突。默认数据库为 0 号。

从 Redis 3.0 开始将逐渐弱化该功能,如果需要部署多个 Redis 实例,可以用端口号来进行区分。

切换数据库。

select <dbindex>

清除数据库。

# 清除当前数据库
flushdb

# 清除所有数据库
flushall

2.1.4 配置

Redis 从配置文件启动客户端后,可以通过命令获取配置以及修改配置。

获取配置。

config get <config>

修改配置。

config set <config> <value>

将配置改动持久化到配置文件。

config rewrite

2.2 基础数据结构

Redis 中有五种基础的数据结构类型,分别是 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)。

每种数据结构都有多种内部编码实现,Redis 根据实际的数据自动选择使用哪种内部编码实现。

2.3 字符串

字符串(string)类型的值实际可以是字符串、数字、或是二进制的图片视频,最大长度为 512 MB。

字符串的内部编码有 3 种:

  • int:8 字节的长整形
  • embstr:小于等于 39 字节的字符串
  • raw:大于 39 字节的字符串

命令

设置值。

可选选项:

  • ex seconds:设置秒级过期时间
  • px milliseconds:设置毫秒级过期时间
  • nx:键不存在时才设置
  • xx:键存在时才设置
set <key> <value> [ex seconds] [px milliseconds] [nx|xx]

# 设置键
set key value

# 设置键 10 秒过期
set key value ex 10

# 键不存在时才设置
set key value nx

这两个命令和 ex nx 选项等同。

setex <key> <seconds> <value>
setnx <key> <value>

获取值,如果键不存在,则返回 nil(空)。

get <key>

# 设置值并返回之前的值
getset <key> <value>

批量设置和获取值。相比多个单次设置和获取值,批量操作只有一次网络时间。

mset <key> <value> [<key> <value>...]
mget <keys>

自增和自减。如果键不存在则从 0 开始计算。

# 对整数自增
incr <key>
incrby <key> <increment>

# 对整数自减
decr <key>
decrby <key> <increment>

# 对浮点数自增
incrbyfloat <key> <increment>

字符串操作。

# 字符串尾部追加
append <key> <value>

# 字符串长度
strlen <key>

# 获取部分字符串
getrange <key> <start> <end>

# 设置指定位置的字符
setrange <key> <offset> <value>

使用场景

字符串比较常用的使用场景是用作缓存,通过将一些数据以字符串的形式保存在 Redis,可以提高读写速度,同时降低读写底层存储的数据库的压力。如分布式 Web 服务将用户的 Session 信息存储在 Redis 中集中管理。

还可以作为计数的基础工具,实现快速计数功能。

使用键过期的功能,实现限制用户操作频率的功能。如限制一个手机号每分钟接收验证码次数,限制短信接口不被频繁调用,或者网站限制同一个 IP 地址每秒访问次数。

2.4 哈希

哈希(hash)类型又叫字典、关联数组,是一个键值对(field - value)的结构。

哈希的内部编码有 2 种:

  • ziplist:压缩列表,当元素个数和所有的值都小于配置值(默认为 512 和 64 字节)时使用该实现,结构更紧凑,节省内存
  • hashtable:哈希表,不满足使用压缩列表的条件时,读写效率会变差,使用该实现,内存占用更大但读写效率更优

命令

设置哈希的键值对。

hset <key> <field> <value>
hmset <key> <field> <value> [<field> <value>...]

获取哈希的对应 field 的 value 值。

hget <key> <field>
hmget <key> <fields>

删除 field,返回成功删除的个数。

hdel <key> <fields>

计算 field 的个数。

hlen <key>

判断 field 是否存在。

hexists <key> <fields>

获取哈希的内容所有 field。

# 获取所有 field
hkeys <key>

# 获取所有 value
hvals <key>

# 获取所有 field - value
hgetall <key>

对 value 自增。

# 对整数自增
hincrby <key> <field> <increment>

# 对浮点数自增
hincrbyfloat <key> <field> <increment>

计算 value 字符串长度。

hstrlen <key> <field>

使用场景

对于关系型数据库保存的数据,如果很多字段为空时,保存的数据较为稀疏,可以通过哈希的结构来保存必要的 field - value 内容。无法实现关系型数据库的复杂查询,但是对于简单的读写效率更优。

2.5 列表

列表(list)类型是一个双向链表,用于存储多个有序的字符串,列表中的每个字符串成为元素(element),列表存储元素上限为 2^32-1 个。可以对列表两段进行插入和弹出,获取指定下标或指定范围的元素。

对于一个长度为 n 的列表,它的索引下标从左到右为 0 到 n-1,也可以从右到左用 -1 到 -n 来表示。

列表的内部编码有 3 种:

  • ziplist:压缩列表,当元素个数和所有的值都小于配置值(默认为 512 和 64 字节)时使用该实现,结构更紧凑,节省内存
  • linkedlist:链表,不满足使用压缩列表的条件时,读写效率会变差,使用该实现,内存占用更大但读写效率更优
  • quicklist:以 ziplist 为节点的 linkedlist,结合了二者的优势

命令

插入元素。

# 在右边插入元素
rpush <key> <values>

# 在左边插入元素
lpush <key> <values>

# 在某个元素前或后插入元素,只在从左向右查找到第一个时插入一次
linsert <key> before|after <oldvalue> <value>

获取元素。

# 获取指定范围元素,这里的下标范围包含 start 和 end
lrange <key> <start> <end>

# 获取指定下标元素
lindex <key> <index>

获取列表长度。

llen <key>

修改元素。

lset <key> <index> <value>

删除元素。

# 从左侧弹出元素
lpop <key>

# 从右侧弹出元素
rpop <key>

# 删除指定元素,匹配等于 value 的元素
# count > 0 从左向右查找并删除,最多删除 count 个
# count < 0 从右向左查找并删除,最多删除 count 个
# count = 0 删除所有匹配的元素
lrem <key> <count> <value>

# 修剪列表,只保留索引范围内的元素
ltrim <key> <start> <end>

阻塞等待的方式弹出元素。

等待弹出的键可以是一到多个。

timeout 指阻塞时间,单位为秒。如果列表有元素则立刻返回,如果列表为空,则会最多阻塞指定时间,当插入了数据会立刻返回。timeout = 0 表示永远阻塞直到有值返回。

# 从左侧弹出元素
blpop <keys> timeout

# 从右侧弹出元素
brpop <keys> timeout

使用场景

列表可以用作消息队列,使用 lpush 和 brpop 命令组合实现阻塞队列,使用 lpush 和 rpop 命令组合实现队列。

队列也可以用于实现栈,使用 lpush 和 lpop 命令组合实现。

2.6 集合

集合(set)类型用来保存多个字符串元素的集合,集合中的元素是去重的和无序的。一个集合最多可以存储 2^32-1 个元素。

集合的内部编码有 2 种:

  • intset:整数集合,所有元素都是整数切元素个数小于配置值(默认 512)时使用,可以减少内存使用
  • hashtable:哈希表

命令

添加元素,返回成功添加的元素个数。

sadd <key> <elements>

判断元素是否属于集合。

sismember <key> <element>

获取集合内容。

# 元素数量
scard <key>

# 获取所有元素
smembers <key>

删除元素,返回成功删除的元素个数。

srem <key> <elements>

随机获取元素。

# 获取元素,不删除,count 指元素数量,不传默认为 1
srandmember <key> [<count>]

# 获取元素,并从集合删除,count 指元素数量,不传默认为 1
spop <key> [<count>]

集合操作。

# 交集
sinter <keys>

# 并集
sunion <keys>

# 差集,第一个集合减去后面集合的重复元素
sdiff <keys>

# 将集合操作结果保存到目标集合
sinterstore <dstkey> <keys>
sunionstore <dstkey> <keys>
sdiffstore <dstkey> <keys>

使用场景

集合的典型使用场景是保存标签,对于不同用户的多个标签分别用集合来保存,方便计算用户间标签的交集、并集和差集。

2.7 有序集合

有序集合(zset)也是集合,用于保存一系列去重的元素,但是元素是有序的,对于每个元素要设置一个分数(score)作为排序依据,分数可以是整型或浮点数。

集合的内部编码有 2 种:

  • ziplist:压缩列表,元素个数和分数较小时的实现,可以减少内存使用
  • skiplist:跳跃表

命令

添加元素,返回成功添加的元素个数。

有序集合为了保持有序,添加元素的时间复杂度为 O(logn),而集合的时间复杂度为 O(1)。

可选选项:

  • nx:元素不存在时才设置
  • xx:元素存在时才设置
  • ch:命令返回元素和分数发生变化的数量
  • incr:对 score 做增加
zadd <key> <score> <element> [<score> <element>...]

元素数量。

zcard <key>

获得某个元素的分数。

zscore <key> <element>

增加元素的分数。

zincrby <key> <increment> <element>

计算元素排名,排名从 0 开始算。

# 分数从低到高排名
zrank <key> <element>

# 分数从高到低排名
zrevrank <key> <element>

返回指定分数范围成员个数。

zcount <key> <min> <max>

返回指定排名范围的元素。withscores 可选,表示同时返回元素的分数。

# 排名从低到高
zrange <key> <start> <end> [withscores]

# 排名从高到低
zrevrange <key> <start> <end> [withscores]

返回指定分数范围的元素。可以用 limit 限制返回的下标范围。min 和 max 还支持开区间和闭区间,如 (100 和 [100,-inf 和 +inf 表示无限小和无限大。

# 分数从低到高
zrangebyscore <key> <min> <max> [withscores] [limit <offset> <count>]

# 分数从高到低
zrevrangebyscore <key> <max> <min> [withscores] [limit <offset> <count>]

删除元素,返回成功删除的元素个数。

# 删除指定元素
zrem <key> <elements>

# 删除指定排名的升序元素
zremrangebyrank <key> <start> <end>

# 删除指定分数范围元素
zremrangebyscore <key> <min> <max>

集合操作。keynumber 指明接下来有几个有序集合,weights 列出每个有序集合的权重,aggregate 表示成员计算求和、最小值或最大值。

# 交集
zinterstore <dstkey> <keynumber> <keys> [weights <weights>] [aggregate sum|min|max]

# 交集
zunionstore <dstkey> <keynumber> <keys> [weights <weights>] [aggregate sum|min|max]

使用场景

有序集合的典型使用场景是排行榜系统,如视频网站对每个视频按照播放数、点赞数等维度的排行榜。

2.8 Bitmaps

Bitmaps 是一个以二进制位为单位的数组,实际上保存在一个字符串中,根据使用到的位的下标来分配字符串的长度。数组的下标在 Bitmaps 中叫做偏移量,支持按偏移量设置某一位的值为 0 或 1。

也可以将一个字符串类型的键看作 Bitmaps,使用它的命令。

命令

设置值,可以设置为 0 或 1。

setbit <key> <offset> <value>

获取某一位的值。

getbit <key> <offset>

获取指定范围值为 1 的数量,可以指定查找范围。

bitcount <key> [<start> <end>]

位运算,将多个的运算结果保存到目标键中。op 运算可以是 and(交集)、or(并集)、not(非)、xor(异或)。

bitop <op> <dstkey> <keys>

返回第一个值为 0 或 1 的偏移量。bit 可以是 0 或 1,可以指定查找范围。

bitpos <key> <bit> [<start> <end>]

使用场景

Bitmaps 适用于保存大量用户的状态,相比每个用户用一个字节来保存,Bitmaps 对于每个用户只用一个字节来保存状态,只需要使用 1/8 大小的内存。

但当设置为 1 的位较少时,会有大量的内存占用浪费,这时候使用集合反而比 Bitmaps 更节省内存。

2.9 HyperLogLog

HyperLogLog 是一种基于概率的统计算法,使用相对少的内存空间来完成总数的统计,但是它的统计结果是不完全准确存在误差的。

这个算法是由 Philippe Flajolet 提出的,因此它的命令都以 pf 为开头。

命令

添加元素。

pfadd <key> <elements>

计算元素数量,如果有多个键则求它们的并集。

pfcount <keys>

合并,将多个 HyperLogLog 的并集复制给一个键。

pfmerge <dstkey> <keys>

使用场景

使用 HyperLogLog 可以减少内存的占用率,但是需要满足以下几点要求:

  • 只计算独立的元素总数,不需要获取单个元素
  • 元素只能增加不能删减
  • 容忍一定的误差率

HyperLogLog 可以用于统计页面的总访问量、视频的总播放量,容忍一定的误差率,但是可以大大减少内存消耗。

2.10 GEO

GEO 用于表示地理信息定位,存储地理位置的信息,用来实现附近位置的功能。

GEO 是将信息存放在 zset 中来实现它的功能的。

命令

增加地理位置,指定经纬度和成员。

geoadd <key> <longitude> <latitude> <member>

获取成员位置。

geopos <key> <members>

获取两个成员间距离。unit 表示单位,可以是m(米)、km(千米)、mi(英里)、ft(尺)。

geodist <key> <member> <member> [<unit>]

找出一个位置一定半径内的成员集合。

georadius <key> <longitude> <latitude> <unit>
georadiusbymember <key> <member> <unit>

删除成员,需要借助 zset 的命令来实现。

zrem <key> <member>

3. 功能

3.1 慢查询分析

一条命令的执行包含了发送命令、命令在队列中排队等待、命令执行、返回结果四部分,慢查询只针对命令执行统计时间。

慢查询相关的配置参数为:

  • slowlog-log-slower-than:慢查询判断的执行时间阈值,单位为微秒
  • slowlog-max-len:保存的慢查询日志数量,超过时会讲最旧的删除

获取慢查询日志。可以指定条数。

slowlog get [<count>]

获取慢查询日志数量。

slowlog len

清理慢查询日志。

slowlog reset

3.2 Pipeline

一条命令的执行包含了发送命令、命令在队列中排队等待、命令执行、返回结果四部分。Redis 提供了一些批量操作命令如 mget、mset 等,将多个命令合并为一次命令,但对于其他命令,每次请求都需要经历命令的发送和返回。

Pipeline(流水线)可以将一组 Redis 命令进行组装,一次过传输给 Redis,最后将他们的执行结果按顺序返回给客户端。多条命令只需要一次发送和返回,节省了网络传输时间。

原生的批量命令是原子操作,而 Pipeline 是非原子操作。可以支持多个命令一起执行。

当封装进 Pipeline 的请求过多时,可能会增加网络阻塞和客户端等待时间,因此需要适当控制 Pipeline 的大小。

3.3 事务

在 Redis 客户端中可以通过命令来实现事务功能,实现多条命令全部执行或全部不执行。

multi 命令表示事务开始,然后每个命令返回结构都是 QUEUED,表示命令被添加到待执行队列中,exec 命令表示事务结束。

multi
set a 1
set b 2
exec

discard 命令表示事务停止执行。

对于语法错误,整个事务都会无法执行,而对于运行时错误,将会有部分命令执行了,而部分命令执行错误。Redis 针对事务不支持回滚,需要开发者自己修复该错误。

Redis 支持执行 Lua 语言脚本,对于 Lua 脚本的执行是原子执行的,开发人员可以通过 Lua 实现复杂的操作,定制自己的命令。整个 Lua 脚本会一次性发送到服务器,减少了网络消耗。

通过客户端执行 Lua 脚本文件。

redis-cli --eval a.lua <参数列表>

通过命令执行 Lua 脚本。

eval <lua脚本> <参数数量> <参数列表>

evalsha 命令则是将 Lua 脚本加载到 Redis 服务器,并得到该脚本的 SHA1 校验值,

在 Lua 脚本中,KEYS[1]、KEYS[2] 等表示传递的参数列表。

Lua 脚本中也可以使用 redis.call 函数实现对 Redis 的访问。

redis.call("get", "a")

Redis 支持将 Lua 脚本加载到服务器,会返回脚本的 SHA1 校验值,然后通过校验值来执行已加载的脚本。

# 加载脚本到服务器
script load <script>

# 启动客户端加载脚本到服务器
redis-cli script load "$(cat a.lua)"

# 执行已加载脚本
evalsha <lua脚本SHA1值> <参数数量> <参数列表>

# 判断脚本是否加载
script exists <SHA1>

# 清除已加载脚本
script flush

# 杀掉执行中的脚本
script kill

3.4 发布订阅

Redis 提供了基于发布订阅模式的消息机制,生产者客户端向指定频道(channel)发布消息,订阅该频道的每个客户端都可以收到消息。

命令

发布消息。

publish <channel> <message>

订阅消息。

subscribe <channels>

# 按照模式订阅
psubscribe <channels>

取消订阅。

unsubscribe <channels>

# 按照模式取消订阅
punsubscribe <channels>

查看活跃的频道,即至少有一个订阅者的频道,可以指定频道名称的模式。

pubsub channels [<format>]

查看频道订阅数。

pubsub numsub <channels>

查看按模式订阅数。

pubsub numpat

使用场景

发布订阅模式通常应用于聊天室、公告牌,以及服务之间的消息传递解耦。

4. 通信协议

Redis 服务器和客户端之间通过 TCP 协议通信,制定了它的通信协议 RESP(REdis Serialization Protocal,Redis 序列化协议)。这种协议以明文传输,简单高效,人可以直接读明白。

发送一条命令的格式如下,CRLF 表示 “\r\n” 换行符:

*<参数数量> CRLF
$<参数1字节数> CRLF
<参数1> CRLF
$<参数2字节数> CRLF
<参数2> CRLF
...
$<参数n字节数> CRLF
<参数n> CRLF

例如以下命令:

set a hello

转化为 RESP 协议发送的格式为:

*3
$3
SET
$1
a
$5
hello

返回结果的格式有五种情况:

  • 状态回复:第一个字节为 +
  • 错误回复:第一个字节为 -
  • 整数回复:第一个字节为 :
  • 字符串回复:第一个字节为 $
  • 多条字符串回复:第一个字节为 *

4. 持久化

Redis 支持 RDB 和 AOF 两种持久化机制,避免因服务器进程退出造成的数据丢失问题,下次重启时可以利用之前持久化的文件实现数据恢复。

4.1 RDB

RDB 持久化是把当前进程的数据生成快照保存到硬盘。

RDB 文件将会保存在配置 dir 指定的目录下,文件名则由配置 dbfilename 指定,Redis 默认采用 LZF 算法对生成的 RDB 文件做压缩处理。

RDB 是在某个时间点的全量数据快照,适用于数据备份、全量复制的场景,且加载 RDB 文件回复数据远快于 AOF 方式。但 RDB 方式执行成本过高,没办法做到实时或秒级持久化,并且存在不同版本的 Redis 格式兼容的问题。

可以通过命令手动触发 RDB 持久化。

# 阻塞服务器执行持久化,直到完成,线上环境应避免使用
save

# fork子进程进行持久化,完成后结束子进程
bgsave

Redis 还支持自动触发持久化的机制,需要在配置中配置。

以下配置表示在 m 秒内数据集存在 n 次修改时,自动触发 bgsave。

save <m> <n>

服务器关闭时,如果没有开启 AOF 持久化功能,也会自动触发 bgsave。

4.2 AOF

AOF(append only file)持久化是以独立日志的方式记录每条写命令,重启时再依次重新执行命令来恢复数据,是目前主流的持久化方式。

开启 AOF 需要配置为 appendonly yes,默认是不开启的。AOF 持久化文件名由配置 appendfilename 指定。AOF 命令写入的内容是依照 RESP 协议来转化的。

AOF 的工作流程为:将所有写入命令追加到缓冲区 aof_buf,根据对应策略从缓冲区写入硬盘。

AOF 缓冲区写入硬盘策略由配置 appendfsync 决定:

  • always:每条命令都会触发 fsync 写入操作
  • everysec:每秒触发 fsync 写入操作
  • no:不对 AOF 文件做 fsync 同步,同步操作由操作系统负责

随着 AOF 文件越来越大,Redis 会定期对 AOF 文件进行重写,方式是直接把进程中的数据传化为写命令,同步到 AOF 文件中。

5. 复制

Redis 为了解决单点问题,将实例分为主节点(master)和从节点(slave),每个主节点可以有 0 到多个从节点,将数据从主节点复制到多个从节点作为副本,满足故障恢复和负载均衡的需求。数据复制是从主节点到从节点单向流动的。

当主节点的读写压力过大时,可以对其配置多个从节点。将读写分离,写命令都对主节点操作,而读命令可以分摊至各个从节点,从而降低 Redis 压力。

配置方法有:

# 配置文件
slaveof <masterhost> <masterport>

# 服务器启动参数
redis-server --slaveof <masterhost> <masterport>

# 执行命令
slaveof <masterhost> <masterport>

从节点也可以断开复制,断开与主节点的复制关系后,从节点晋升为主节点。之前同步过来的数据仍然保留,只是不会再同步新的数据过来了。

需要执行以下命令:

slaveof no one

也可以配置为另一个主节点的从节点,称为切主。

6. 参考