Redis

IntYou (^_^)

Redis 概述


NoSQL


非关系型数据库

类型

  1. 键值存储数据库

    key-value

  2. 列存储数据库

    关系型数据库是典型的行存储数据库(MySQL),按行存储的数据在物理层面占用的时连续的存储空间,不适合海量数据存储。而按列存储则可实现分布式存储,适合海量存储(HBase)

  3. 文档型数据库

    NoSQL 与 关系型数据库的结合(MangoDB)

  4. 图像(Graph)数据库

    用于存放一个节点关系的数据库(Neo4J)

Redis的用途


数据缓存

减小 RT,而且降低 DBMS 的压力

缓存分类
实时同步缓存

指DBMS中数据更新后,Redis缓存中存放的相关数据会被立即清除,以促使再有对该数据的访问到来时,必须先从DBMS中查询获取到的最新数据,然后再写入到Redis

阶段性同步缓存

指Redis缓存中的数据允许在一段时间内与DBMS中的数据不完全一致,而这个时间段就是这个缓存数据的过期时间

Redis 的 IO 模型


Redis 处理客户端请求所采用的处理架构,称为Redis 的 IO 模型

单线程模型

对于 Redis 3.0 及其以前版本,Redis 的 IO模型采用的是纯粹的单线程模型,即所有客户端的请求全部由一个线程处理,Redis 的单线程模型采用了多路复用技术

优点

可维护性高,性能高,不存在并发读写情况,所以也就不存在执行顺序的不确定性,不存在线程切换开销,不存在死锁问题,不存在为了数据安全而进行的枷锁/解锁开销问题

缺点

性能会受到影响,且由于单线程只使用一个处理器,所以会形成处理器浪费

多线程模型

从 Redis 6.0 版本开始。处理客户端请求的仍是单线程模型,但对于一些比较耗时担又不影响对客户端的响应的操作,就由后台其他线程来处理。

优点

其结合了多线程与单线程的优点,避开了他们的不足

缺点

该模型并非是一个真正意义上的多线程,因为真正处理任务的线程仍是单线程。所以,其对性能也是有些影响的

Redis 的配置


INCLUDE


指定要在当前配置文件中包含的配置文件。这样做的目的主要是便于配置信息管理,可以将不同场景的配置都进行单独定义,然后在当前核心配置文件中根据不同场景选择包含进不同的配置文件。

想要使用外部配置文件覆盖,可以在Redis配置文件的文件末尾 include

例:include /xxx/xxx.conf

MODULES


加载不同的第三方模块,来增强,扩展 Redis 的功能

例:loadmodule /xxx/xxx/xxx/xxx.so

NETWORK


网络配置

tcp-backlog

tcp-backlog 是一个 TCP 连接队列,其主要用于解决高并发场景下客户端慢连接问题。配置中设置的值就是这个队列的长度。该队列与 TCP 连接的三次握手有关。不同的 Linux 内核,backlog 队列中存放的元素(客户端连接)类型是不同的

  • Linux 内核 2.2 版本之前

    该队列中存放的是已完成了第一次握手的所有客户端连接,其中就包含已完成三次握手的客户端连接。当然,此时的 backlog 队列中的连接也具有两种状态:未完成三次握手的连接状态为 SYN_RECEIVED ,已完成三次握手的连接状态为ESTABLISHED 。只有 ESTABLISHED 状态的连接才会被 Redis 处理。

  • Linux 内核 2.2 版本之后

    TCP 系统中维护了两个队列:SYN_RECEIVED 队列与 ESTABLISHED 队列。SYN_RECEIVED 队列中存放的是未完成的三次握手的连接,ESTABLISHED 队列中存放的是已完成三次握手的连接。此时的 backlog 就是 ESTABLISHED 队列

  • TCP 中的 backlog 队列的长度在 Linux 中由内核参数 somaxconn 来决定。在 Redis 中该队列的长度由 Redis 配置文件设置 与 somaxconn 共同决定:取它们中的最小值。

    1
    2
    # 查看 Linux 内核中 somaxconn 的值
    cat /proc/sys/net/core/somaxconn

    生产环境下(特别是高并发场景下),backlog 的值最好要大一些,否则可能会影响系统性能

    1
    2
    3
    4
    5
    6
    7
    8
    # 修改somaxconn的值
    vim /etc/sysctl.conf

    # 添加
    net.core.somaxconn=2048

    # 动态重新加载配置
    sysctl -p

GENERAL


通用配置

daemonize

控制 Redis 启动是否采用守护进程方式,即是否是后台启动

pidfile

指定 Redis 允许是 pid 写入的文件,无论 Redis 是否采用守护进程方式启动,pid 都会写入到该配置文件

注意

  • 如果没有配置 pid 文件,不同的启动方式,pid 文件的产生效果是不同的

    • 采用守护进程方式启动(后台启动,daemonize 为 yes)——pid 文件为/var/run/redis.pid

    • 采用前台启动——不生产 pid 文件

loglevel

日志等级

logfile

指定日志文件。如果设置为空串,则强制将日志记录到标准输出设备(显示器)。如果使用的是前台启动方式,设置为空串,则意味着会将日志发送到设备/dev/null(空设备)

databases

设置数据库数量

security

用户设置 ACL 权限,Redis 访问密码相关配置。该模块中最常用的就是 requirepass 属性

CLIENTS


设置与客户端相关的属性

maxclients

设置Redis可并发处理的客户端连接数量,默认为10000,如果达到该最大连接数,则会拒绝再来的新连接,并返回一个异常消息:已达到最大连接数

注意

  • 该值不能超过 Linux 系统支持的可打开的文件描述符最大数量阈值

MEMORY MANAGEMENT


控制最大可用内存及相关移除问题

maxmemory

将内存使用限制设置为指定的字节数,当达到内存限制时,REdis 将根据选择的逐出策略 maxmemory-poilcy 尝试删除符合条件的key

如果不能按照逐出策略移除key,则会给写操作命令返回error,但对于只读的命令是没有影响的

Threaded I/O

配置 Redis 对多线程 IO 模型的支持

Redis 命令


Redis 基本命令


  1. dbsize 查询当前数据库内容长度

  2. flushdb 删除当前数据库中所有数据

  3. flushall 删除所有数据库中所有数据

  4. ping 心跳命令,检测客户端与 Redis 的连接是否正常

  5. set <key> <value> 将指定key-value 写入到当前DB

  6. get <key> 读取指定key的value值

  7. select 5 选择数据库

  8. quit/exit 退出Redis客户端

  9. redis-cli shutdown 关闭Redis服务

Key操作命令


keys

  • 格式 KEYS pattern

查找所有符合给定模式 pattern 的 key, pattern 为正则表达式

KEYS 的速度非常快,但在一个大的数据库中使用它可能会阻塞当前服务器的服务。所以生产环境中一般不使用该命令,而使用 scan 命令代替

exists

  • 格式 EXISTS key

检查给定 key 是否存在

若 key 存在,返回 1,反之 0

del

  • 格式 DEL key [key ...]

删除给定的一个或多个 key。不存在的 key 会被忽略

返回被删除 key 的数量

rename

  • 格式 RENAME key newKey

将 key 改为 newKey

当 key 与 newKey 相同,或者 key 不存在时,返回一个错误。当 newkey 已经存在时,RENAME 命令将覆盖旧值。改名成功后提示 OK,失败时候返回一个错误

move

  • 格式 MOVE key db

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

如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果,移动成功返回 1,反之 0

type

  • 格式 TYPE key

返回 key 所储存的值的类型

返回值有以下六种

  • none(key 不存在)

  • string

  • list

  • set

  • zset

  • hash

expire 与 pexpire

  • 格式 EXPIRE key seconds

为给定 key 设置生存时间。当 key 过期时(生存时间为 0),它会被自动删除。expire 的时间单位为秒,pexpire 的时间单位为毫秒。在 Redis 中,带有生存时间的 key 被称为’易失的’(volatile)

ttl 与 pttl

  • 格式 TTL key

TTL,time to live,返回给定 key 的剩余生存时间

其返回值存在三种可能

  • 当 key 不存在时,返回 -2

  • 当 key 存在但没有设置剩余生存时间时,返回 -1

  • 否则,返回 key 的剩余生存时间,ttl 命令返回的时间单位为秒,而 pttl 命令返回的时间单位为毫秒

persist

  • 格式 persist key

去除给定 key 的生存时间,将这个 key 从’易失的’转换成’持久的’

当生存时间移除成功时,返回 1;若 key 不存在或 key 没有设置生存时间,则返回 0

randomkey

  • 格式 RANDOMKEY

从当前数据库中随机返回(不删除)一个key

当前数据库不为空时,返回一个 key。当数据库为空时,返回nil

scan

  • 格式 SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

用于迭代数据库中的 key

  • cursor:本次迭代开始的游标

  • pattern:本次迭代要匹配的 key 的模式

  • count:本次迭代要从数据集里返回多少元素,默认值为 10

  • type:本次迭代要返回的 value 的类型,默认为所有类型

使用间断的,负数,超出范围的或者其他非正常的游标来执行增量式迭代

不会造成服务器崩溃。

当数据量很大时,count 的数据量的指定可能会不起作用,Redis 会自动调整每次的遍历数目。由于 scan 命令每次执行都只会返回少量元素,所以该命令可以用于生产环境

String 型 Value 操作命令


set

  • 格式 SET key value [EX seconds | PX milliseconds] [NX|XX]

SET 除了可以直接将 key 的值设为 value 外,还可以指定一些参数

  • EX seconds:为当前 key 设置过期时间,单位秒。等价于 SETEX 命令

  • PX milliseconds:为当前 key 设置过期时间,单位毫秒。等价于 PSETEX 命令

  • NX:指定的 key 不存在才会设置成功,用于添加指定的 key。等价于 SETNX 命令

  • XX:指定的 key 必须存在才会设置成功,用于更新指定 key 的 value

getset

  • 格式 GETSET key value

将给定 key 的值设置为value,并返回 key 的旧值

当 key 存在但不是字符串类型时,返回一个错误;当 key 不存在时,返回nil

mset 与 msetnx

  • 格式 MSET/MSETNX key value [key value ...]

同时设置一个或多个 key-value 对

如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX 命令;他只会在所有给定 key 都不存在的情况下进行操作操作,某些给定 key 被更新而另一些给定 key 没有改变的情况不可发生

mget

  • 格式 MGET key [key ...]

返回(一个或多个)给定 key 的值

如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil,因此,该命令永不失败

append

  • 格式 APPEND key value

如果 key 已经存在并且是一个字符串,APPEND 命令将 value 追加到 key 原来的值的末尾。如果 key 不存在,append 就简单的将给定的 key 设为 value,就像执行 SET key value 一样

追加之后,返回 key 中字符串的长度

incr 与 decr

  • 格式 incr keydecr key

increment 自动递增,将 key 中存储的数字值增一

decrement 自动递减,将 key 中存储的数字值减一

如果 key 不存在,那么 key 的值会先被初始化为 0,然后在执行增一/减一操作。如果值不能表示为数字,那么返回一个错误提示。如果执行正确,则返回增一/减一后的值

incrby 与 decrby

  • 格式 incrby key incrementdecrby key decrement

将 key 中存储的数字值增加/减少指定的数值,这个数值只能是整数,可以是负数,但不能是小数

如果 key 不存在,那么 key 的值会先被初始化 0,然后再执行操作,同 incr/decr

incrbyfloat

  • 格式 incrbyfloat key increment

为 key 中所储存的值加上浮点数增量,没有 decrbyfloat

strlen

  • 格式 strlen key

查看 key 中字符串长度

getrange

  • 格式 getrange key start end

返回 key 中字符串值的子字符串,字符串的截取范围由 start 和 end 两个偏移量决定,包括 start 和 end 在内

end 必须要比 start 大,支持负数偏移量,表示从字符串最后开始计数,-1 表示最后一个字符,以此类推

setrange

  • 格式 setrange key offset value

用 offset 参数替换给定 key 所储存的字符串值 str,从偏移量 offset 开始

当 offset 值大于 str 长度时,中间使用零字节\x00填充,对于不存在的 key 当作空串处理

应用场景


数据缓存

Redis 作为数据缓存层,MySQL 作为数据存储层。应用服务器首先从 Redis 中获取数据,如果缓存层中没有,则从 MySQL 中获取后先存入缓存层再返回给应用服务器

共享 Session

对于一个分布式应用系统,如果将类似用户登陆信息这样的 Session 数据保存在提供登陆服务的服务器中,那么如果用户再次提交像收藏,支付等请求时可能会出现的问题:在提供收藏,支付等服务的服务器中并没有该用户的 Session 数据,从而导致该用户需要重新登录。对于用户来说这是不能接受的。

此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从 Redis 中查找相应的 Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。这样就不会引发’重新登录问题’

限制器

Redis 可以结合 key 的过期时间与 incr 命令来完成限速,防止 DoS 攻击

Hash 型 Value 操作命令


Hash 表就是一个映射表 Map,也是由键-值对构成的,为了与整体的 key 进行区分,这里的键称为 field,值称为 value。注意,Redis 的 Hash 表中的 field-value 对均为 String 类型

hset

  • 格式 HSET key field value

将 hash 表 key 中的域 field 的值设为 value

如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作,返回1。已存在则覆盖,返回0

hget

  • 格式 hget key <field>

同 String

hmset

  • 格式 hmset key field value [field value ...]

同 String

hmget

  • 格式 hmget key field [field ...]

同 String

hgetall

  • 格式 hgetall key

获取 key 中所有 field 和 value 值

hkeys

  • 格式 hkeys key

获取 key 中所有 field 值

hlen

  • 格式 hlen key

获取 key 的 长度

略(大多与String命令同)

应用场景

存储对象数据,对对象属性的修改再=在 Redis 中就可以直接完成,其不像 String 型 Value 存储对象,那个对象是序列化过的,例如序列化为 JSON 串,对对象属性值的修改需要先反序列化为对象后再修改,修改后再序列化为 JSON 串后写入到 Redis

List 型 Value 操作命令


lpush/rpush

  • 格式 LPUSH key value [value ...]RPUSH key value [value ...]

表头/表尾添加元素

linsert

  • 格式 LINSERT key BEFORE|AFTER pivot value

将值 value 插入到列表 ley 当中,位于元素 pivot 之前或之后

lpop/rpop

  • 格式 LPOP key [count]RPOP key [count]

从列表 key 的表头/表尾移除 count 个元素,并返回移除的元素,count 默认值 1

当 key 不存在时,返回 nil

blpop/brpop

  • 格式 Blpop key [key ...] timeoutbrpop key [key ..] timeout

列表的阻塞式(blocking)弹出,命令。它们是 LPOP/RPOP 的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLpop/Brpop 命令阻塞,知道等待 timeout 超时或发现可弹出元素为止

rpoplpush

  • 格式 rpoplpush source destnation

将source 列表的尾元素,放到 destnation 列表的表头

brpoplpush

rpoplpush 的阻塞版本

lrem

  • 格式 lrem key count value

根据参数 count 的值,移除列表中与参数 value 相等的元素

count值为下列几种

  • count > 0:从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count

  • count < 0:从表尾开始向表头搜索

  • count = 0:移除表中所有与 value 相等的值

返回被移除元素的数量,当 key 不存在时,返回 0

ltrim

  • 格式 LTRIM key start stop

对一个列表进行修剪,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除

下表参数 start 和 stop 都以 0 为底,如果 start > stop,返回空列表,如果 stop < end,Redis 将 stop 的值设置为 end

应用场景

通过构建不同的数据结构来实现相应的业务功能

  • 列(lpush + lpop)

  • 队列(lpush + rpop)

  • 阻塞式消息队列(lpush + brpop)

  • 动态有限集合(lpush + ltrim)

Set 型 Value 操作命令


Set 型的底层是 Hash 表,Set 中的元素具有无序性与不可重复性

sadd

  • 格式 SADD key member [member ...]

将一个或多个 member 元素加入到集合 key 中,已经存在于集合的 member 元素将被忽略

key 不存在,则创建。当 key 不是集合类型时,返回一个错误

smembers

  • 格式 SMEMBERS key

查看 key 中的所有值

scard

  • 格式 SCARD key

返回 Set 集合的长度

sismember

  • 格式 SISMEMBER key member

判断 member 元素是否是集合 key 的成员

smove

  • 格式 SMOVE source destination member

将 member 元素从 source 集合移动到 destination 集合

srem

  • 格式 SREM key member [member ...]

移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略

srandmember

  • 格式 srandmember key [count]

在 key 中随机 count 个元素,count 默认为 1

count 为正数时,且小于集合长度,那么返回一个包含 count 个元素的数组,数组中的元素各不相同。如果 count 大于等于集合长度,返回整个集合。如果 count 为负数,那么返回一个包含 count 绝对值个元素的数组,但数组中的元素可能会出现重复

spop

  • 格式 SPOP key [count]

移除并返回集合中的 count 个随机元素,count 必须为正数,且默认值为 1

sdiff

  • 格式 SDIFF key key2

差集,返回 key 相对于 key2 不同的元素

sdiffstore

  • 格式 SDIFFSTORE destination key key2

差集存储,将 key 相对于 key2 的差集存储在 destation 集合中

sinter

  • 格式 SINTER key key2

交集,返回 key 与 key2 的交集

sinterstore

  • 格式 SINTERSTROE destination key key2

交集存储,将 key 与 key2 的交集存储在 destination 集合中

sunion

并集,同上

sunionstore

并集存储,同上

应用场景

  • 动态黑白名单

  • 有限随机数(spop 或 srandmember)

  • 用户画像(sadd/sinter/sinterstore)

有序 Set 型 Value 操作命令(ZSet)

Redis 存储数据的 Value 可以是一个有序的 Set,这个有序的 Set 中的每个元素均 String 类型,有序 Set 与 Set 的不同之处是,有序 Set 中的每个元素都有一个分值 scor,Redis 会根据 score 的值对集合进行由小到大的排序。其与 Set 集合要求相同,元素不能重复,但元素的 score 可以重复,由于该类型的所有命令均是 z 开头,所有该 Set 也称为 ZSet

zadd

  • 格式 ZADD key score member [[score member] ...]

将一个或多个 member 元素以及其 score 值加入到有序 key 中的适当位置

zrange

  • 格式 zrange key start end [withscores]

将集合 key 中从 start 到 end 的元素切片输出,添加 withscores 显示 key 内元素的 score 值

zrangebyscore 与 zrevrangebyscore

  • 格式 zrangebyscore key min max [withscores] [LIMIT offset count]

  • 格式 zrangebyscore key max min [withscores] [LIMIT offset count]

将集合 key 中 score > min,score <= max 的 元素输出,limit 按照前部分查询到的结果 从下标为 offset 开始,切片 count 数量元素并返回

zrevrangebyscore 是 zrangebyscore 的反向

zcard

查询 key 集合的长度,同Set

zcount

  • 格式 zcount key start end

计数 key 集合中下标 > start,下标 <= end 的元素个数

zscore

  • 格式 zscore key field

查询 key 集合中 field 元素的 score 值

zrank 与 zrevrank

  • 格式 zrank key fieldzrevrank key field

查询 key 中 field 元素的排名,zrevrank 倒序排名

zrem 与 zremrangerank

删除 key 中元素,zremrangerank 删除 key 中一个范围内的元素

zremrangebyscore

删除 key 中 score 在一个范围内的元素

zrangebylex

  • 格式 zrangebylex key (min [max

按照第一个字母排序切片,min 和 max 前必须添加符号

zlexcount

根据字母返回计数

简单动态字符串 SDS(String类型底层)


无论是 Redis 的 Key 还是 Value,其基础数据类型都是字符串。例如,Hash 型 Value 的 field 与 value 类型,List,Set,ZSet 型 Value 的元素的类型等都是字符串。Redis 虽然是使用标准 C 语言开发,但并没有直接使用 C语言中传统的字符串表示,而是自定义了一种字符串。这种字符串本身的结构比较简单,但功能却非常强大,称为简单动态字符串 SDS

注意,Redis 中的所有字符串并不都是 SDS,也会出现 C 字符串,C 字符串只会出现在字符串’字面常量’中,并且该字符串不可能发生变化

SDS 结构

buf[] 字节数组 + len 数组中已使用字节数量 + free 数组中未使用的字节数量

SDS 的优势

  1. 防止’字符串长度获取’性能瓶颈

  2. 保证二进制安全(‘\0’)

  3. 减少内存再分配次数

    SDS 采用了 空间预分配策略惰性空间释放策略 来避免内存再分配问题

    空间预分配策略是指,每次 SDS 进行空间扩展时,程序不但为其分配所需空间,还会为其分配额外的未使用空间,以减少内存再分配次数,而额外分配的未使用空间大小取决于空间扩展后 SDS 的 len 属性

    • 如果 len 属性值小于 1M,那么分配的未使用空间 free 的大小与 len 属性值相同

    • 如果 len 属性值大于等于 1M,那么分配的未使用空间 free 的大小固定是 1M

    SDS 对于空间释放采用的是惰性空间释放策略,该策略是指,SDS 字符串长度如果缩短,那么多出的未使用空间将暂时不释放,而是增加到 free 中,以使后期扩展 SDS 时减少内存再分配次数

    如果要释放 SDS 的未使用空间,则可通过 sdsREmoveFreeSpace() 函数来释放

  4. 兼容 C 函数

    SDS 底层数组 buf[] 中的字符串仍以空字符’\0’结尾

集合的底层实现原理


两种实现的选择

对于 Hash 与 ZSet 集合,其底层的实现实际有两种:压缩列表 zipList 与 跳跃列表 skipList,这两种实现对于用户来说是透明的,但用户写入不同的数据,系统会自动使用不同的实现。只有同时满足以配置文件 redis.conf 中相关集合元素数量阈值两个条件,使用的就是压缩列表 zipList,只要有一个条件不满足使用的就是跳跃列表 skipList

例如,对于 ZSet 集合中这两个条件如下

  • 集合元素个数小于 redis.conf 中 zset-max-ziplist-entries 属性的值,其默认值为 128

  • 每个集合元素大小都小于 redis.conf 中 zset-max-ziplist-value 属性的值,其默认值为 64字节

zipList

  1. 什么是 zipList

    通常称为压缩列表,是一个经过 特殊编码 的用于存储字符串或整数的双向链表,其底层数据结构由三部分构成:head,entries 与 end,这三部分在内存上是连续存放的

  2. head

    head 分为三部分

    1. zlbytes:占4个字节,用于存放 zipList 列表整体数据结构所占的字节数,包括 zlbytes 本身的长度

    2. zltail:占4个字节,用于存放 zipList 中最后一个 entry 在整个数据结构中的偏移量(字节)。该数据的存在可以快速定位列表的尾 entry 位置,以方便操作

    3. zllen:占2个字节,用于存放列表包含的 entry 个数。由于其只有 16 位,所以 zipList 最多可以含有的 entry 个数为 2^16 -1 = 65535 个

  3. entries

    entries 是真正的列表,有很多的列表元素 entry 构成。由于不同的元素类型,数值的不同,从而导致每个 entry 的长度不同

    每个 entry 由三部分组成

    1. prevlength:该部分用于记录上一个 entry 的长度,以实现逆序遍历,默认长度为 1 字节,只要上一个 entry 的长度 < 254 字节,prevlength 就占 1 字节,否则其会自动扩展为 3 字节长度

    2. encoding:该部分用于标志后面的 data 的具体类型。如果 data 为整数类型,encoding 固定长度为 1 字节。如果 data 为字符串类型,则 encoding 长度可能会是 1 字节,2 字节或5 字节,data 字符串不同的长度,对应着不同的 encoding 长度

    3. data:真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字节长度不同

  4. end

    end只包含一部分,称为zlend,占 1字节,值固定为255,表示一个 zipList 列表的结束

listPack

对于 zipList,实现复杂,为了逆序遍历,每个 entry 中包含前一个 entry 的长度,这样会导致在 zipList 中间修改或者插入 entry 时需要进行级联更新。在高并发的写操作场景下会极度降低 Redis 的性能。为了实现更紧凑,更快的解析,更简单的实现,重写实现了 zipList,并命名为 listPack

在 Redis 7.0 中,已经将 zipList 全部替换为了 listPack,但为了兼容性,在配置中也保留了 zipList 的相关属性

  1. 什么是 listPack

    listPack 与 zipList 的区别在 head 与每个 entry 的结构上。表示列表结束的end 与 zipList 的 zlend 是相同的,占一个字节,且8位全为1

  2. head

    head 由两部分构成

    1. totalBytes:占 4个字节,用于存放 listPack 列表整体数据结构所占的字节数,包括 totalBytes 本身长度

    2. elemNum:占 2字节,用于存放列表包含的 entry 个数,其意义与 zipList 中 zllen 的相同

    与 zipList 的 head 相比,没有了记录最后一个 entry 偏移量的 zltail。

  3. entries

    与 zipList 不同的是,没有了记录前一个 entry 长度的 prevlength,而增加了记录当前 entry 长度的 element-total-len。而这个改变仍然可以实现逆序遍历,但却避免了由于在列表中间修改或插入 entry 时引发的级联更新

    每个 entry 仍由三部分构成

    1. encoding:该部分用于标志后面的 data 的具体类型,如果 data 位整数类型,encoding 长度可能会是1,2,3,4,5 或 9字节,不同的字节长度,其标识位不同。如果 data 为字符串类型,则 encoding 长度可能会是 1,2 或 5 字节,data 字符串不同的长度,对应着不同的 encoding 长度

    2. data

    3. element-total-len:该部分用于记录当前 entry 的长度,用于实现逆序遍历。由于其特殊的记录方式,使其本身占有的字节数据可能会是1,2,3,4 或 5字节

skipList

  1. 什么时 skipList

    skipList 简称跳表,是一种 随机化 的数据结构,基于 并联 的链表,实现简单,查找效率较高,简单来说跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,也正是这个功能,使得在查找元素时,能够提供较高的效率

  2. skipList 原理

    每个节点随机分层,每多一层多一层链表,减少比较的次数

quickList

  1. 什么是 quickList

    quickList 快速列表,quickList 本身是一个双向无循环链表,他的每一个节点都是一个 zipList,从 Redis 3.2 版本开始,对于 List 的底层实现,使用 quickList 替代了 zipList 和 linkedList

    quickList 本质上是 zipList 和 linkedList 的混合体,其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来,当然,对于每个 zipList 中最多可存放多大容量的数据元素,在配置文件中通过 list-max-ziplist-size 属性可以指定

  2. 检索操作

    对于 List 元素的检索,都是以其索引 index 为依据的。quickList 由一个个的 zipList 构成,每个 zipList 的 zllen 中记录的就是当前 zipList 中包含的 entry 的个数,即包含的真正数据元素的个数。根据要检索的 index,从 quickList 的头节点开始,逐个对 zipList 的 zllen 做 sum 求和,直到找到第一个求和后 sum 大于 index 的 zipList,那么要检索的这个元素就在这个 zipList 中

  3. 插入操作

    假设要插入的元素的大小为 indexBytes

    1. 若 indexBytes + zlBytes <= zipListMaxSize,则直接将元素插入到当前找到的 zipList 中即可

    2. 若 indexBytes + zlbytes > zipListMaxSize,且插入的位置在该 zipList 的首部位置,此时需要再查看该 zipList 的前一个 zipList 的大小 prev_zlBytes

      • 若 indexBytes + prev_zlBytes <= zipListMaxSize,则直接将元素插入到前一个 zipList 的尾部即可

      • 若 indexBytes + prev_zlBytes > zipListMaxSize,则将元素自己构建为一个 zipList,连接到 quickList

    3. 若 indexBytes + zlbytes > zipListMaxSize,且插入的位置在该 zipList 的尾部位置,此时需要再查看该 zipList 的下一个 zipList 的大小 next_zlBytes

      • 若 indexBytes + next_zlBytes <= zipListMaxSize,则直接将元素插入到下一个 zipList 的首部即可

      • 若 indexBytes + next_zlBytes > zipListMaxSize,则将元素自己构建为一个 zipList,连接到 quickList

    4. 若 indexBytes + zlbytes > zipListMaxSize,且插入的位置在该 zipList 的中间位置,此时需要将该 zipList 分割为两个 zipList 并连接到 quickList,然后再将元素直接插入到分割出的两个 zipList 中的前一个 zipList 的尾部位置

  4. 删除操作

    直接将节点删除即可

    注意,如果 entry 只有一个元素,需要将 head 和 end 一起删除

key 与 value 中元素的数量

  • Redis 最多可以处理 2^32 个 key,每个 Redis 实例至少可以处理 2.5 亿个 key

  • 每个 Hash,List,Set,ZSet 集合都可以包含 2^32 个元素

Redis 事务


Redis 的事务的本质是一组命令的批处理。这组命令在执行过程中会被顺序的,一次性全部执行完毕,只要没有出现语法错误,这组命令在执行期间是不会被中断

Redis 事务特性

redis 的事务仅保证了数据的一致性,不具有像 DBMS 一样的 ACID 特性

  • 这组命令中的某些命令的执行失败不会影响其他命令的执行,不会引发 回滚 。即不具备原子性

  • 这组命令通过乐观锁机制实现了简单的隔离性。没有复杂的隔离级别

  • 这组命令的执行结果是被写入到内存的,是否持久取决于 Redis 的持久化策略,与事务无关

Redis 事务实现

  1. 三个命令

    • multi:开启事务

    • exec:执行事务

    • discard:取消事务

    exec 与 discard 不可共存

Redis 持久化


Redis 具有持久化功能,其会按照设置以 快照操作日志 的形式将数据持久化到磁盘

根据持久化使用技术不同,分为两种:RDB 与 AOF

持久化(钝化)基本原理


RDB 与 AOF 可以共存,RDB 是默认持久化技术,而如果开启 AOF,则 AOF 的优先级更高

RDB 持久化


是指将内存中某一时刻的数据快照 全量 写入到指定的 rdb 文件的持久化技术。RDB 持久化是默认开启的。当 Redis 启动时会自动读取 RDB 快照文件,将数据从硬盘载入到内存,以恢复 Redis 关机前的数据库状态

持久化的执行

RDB 持久化的执行有三种方式:手动 save 命令,手动 bgsave 命令,与自动条件触发

  1. 手动 save 命令

    通过在 redis-cli 客户端中执行 save 命令可立即进行一次持久化保存。save 命令在执行期间会阻塞 redis-server 进程,直至持久化过程完毕。而在 redis-server 进程阻塞期间,redis 不能处理任何读写请求,无法对外提供服务

  2. 手动 bgsave 命令

    通过在 redis-cli 客户端中执行 bgsave 命令可立即进行一次持久化保存。不同于 save 的是,可以后台允许 save。bgsave 命令会使服务器进程 redis-server 生成一个子进程,由该子进程负责完成保存过程。在子进程进行保存过程中,不会阻塞 redis-server 进程对客户端读写请求的处理

  3. 自动条件触发

    本质仍是 bgsave 命令的执行。只不过是用户通过在配置文件中做相应的设置后,Redis 会根据设置信息自动调用 bgsave 命令执行。

  4. 查看上次持久化的时间

    • 格式:lastsave

RDB 优化配置

参考配置文件 redis.conf 中的 SNAPSHOTTING 字段内容

RDB 文件结构

RDB 持久化文件 dump.rdb 分为五部分

  1. SOF

    SOF 是一个常量,一个字符串 REDIS,仅包含这五个字符,其长度为 5。用于 标识 RDB 文件的开始,以便在加载 RDB 文件时可以迅速判断出文件是否是 RDB 文件

  2. rdb_version

    一个整数,长度为 4 字节,标识 RDB 文件的版本号

  3. databases

    内部包含任意多个非空数据库。而每个 database 由三部分组成

    • SODB:常量,占1字节,用于标识一个数据库的开始

    • db_number:数据库编号

    • key_value_pairs:当前数据库中的键值对数据

    每个 key_value_pairs 又由很多个用于描述键值对的数据构成

    • VALUE_TYPE:常量,占1字节,用于标识该键值对中 value 的类型

    • EXPIRETIME_UNIT:常量,占1字节,用于标识过期时间的单位是秒还是毫秒

    • time:当前 key-value 的过期时间

  4. EOF

    一个常量,长度为 1,用于标识 RDB 数据的结束,校验和的开始

  5. check_sum

    校验和 check_sum 用于判断 RDB 文件中的内容是否出现数据异常,其采用的是 CRC 校验算法

RDB 持久化过程

对于 Redis 默认的 Redis 持久化,在进行 bgsave 持久化时,redis-server 进程会 fork 出一个 bgsave 子进程,由该子进程以异步方式负责完成持久化。而在持久化过程中,redis-server 进程不会阻塞,其会继续接收并处理用户的读写请求。

bgsave 子进程的详细工作原理如下

  • 由于子进程可以继承父进程的所有资源,且父进程不能拒绝子进程的继承权。所以,bgsave 子进程有权读取到 redis-server 进程写入到内存中的用户数据,使得将内存数据持久化到 dump.rdb 成为可能

  • bgsave 子进程在持久化时首先会将内存中得全量数据 copy 到磁盘中的一个 RDB 临时文件,copy 结束后,在将该文件 rename 为 dump.rdb,替换掉原来的同名文件

  • 不过,在进行持久化过程中,如果 redis-server 进程接收到了用户写请求,则系统会将内存中发生数据修改的物理块 copy 出一个副本。等内存中的全量数据 copy 结束后,会再将副本中的数据 copy 到 RDB 临时文件。这个副本的生成是由于 Linux 系统的写时复制技术(Copy-On-Write)实现的

AOF 持久化


AOF,是指 Redis 将每一次的写操作都以日志的形式记录到一个 AOF 文件中的持久性技术。当需要恢复内存数据时,将这些写操作重新执行一次,便会恢复到之前的内存数据状态

AOF 基础配置

  1. AOF 的开启

    在 redis.conf 配置文件内将 appendonly 字段设置为 yes

  2. 文件名配置

    包括三类多个文件

    • 基本文件:可以是 RDF 格式也可以是 AOF 格式。其存放的内容是由 RDB 转为 AOF 当时内存的快照数据。该文件可以有多个

    • 增量文件:以操作日志形式记录转为 AOF 后的写入操作。该文件可以有多个

    • 清单文件:用于维护 AOF 文件的创建顺序,保障激活时的应用顺序。该文件只有一个

  3. 混合式持久化开启

    对于基本文件可以是 RDB 格式也可以是 AOF 格式。通过 aof-use-rdb-preamble 属性可以选择。其默认值是 yes,即默认 AOF 持久化的基本文件为 rdb 格式文件,也就是默认采用混合式持久化

AOF 文件格式

  1. Redis 协议

    增量文件扩展名为.aof,采用 AOF 格式。AOF 格式其实就是 Redis 通讯协议格式,AOF 持久化文件的本质就是基于 Redis 通讯协议的文本,将命令以纯文本的方式写入到文件中

    Redis 协议规定,Redis 文本是以行来划分,每行以\r\n 行结束。每一行都有一个我消息头,以表示消息类型。消息头由六种不同的符号表示

    • (+):表示一个正确的状态信息

    • (-):表示一个错误信息

    • (*):表示消息体总共有多少行,不包括当前行

    • ($):表示下一行消息数据的长度,不包括换行符长度\r\n

    • (空):表示一个消息数据

    • (:):表示返回一个数值

Rewrite 机制

为了防止 AOF 文件由于太大而占用大量的磁盘空间,降低性能,Redis 引入了 Rewrite 机制来对 AOF 文件进行压缩

  1. 何为 rewrite

    所谓 Rewrite 其实就是对 AOF 文件进行重写整理。当 Rewrite 开启后,主进程 redis-server 创建出一个子进程 bgrewriteaof,由该子进程完成 rewrite 过程。其首先对现有 aof 文件进行 rewrite 计算,将计算结果写入到一个临时文件,写入完毕后,再 rename 该临时文件为原 aof 文件名,覆盖原有文件

  2. rewrite 计算

    其遵循以下策略

    • 读操作命令不写入文件

    • 无效命令不写入文件

    • 过期数据不写入文件

    • 多条命令合并写入文件

  3. 手动开启 rewrite

    • 格式 bgrewriteaof

AOF 优化配置

  1. appendfsync

    当客户端提交写操作命令后,该命令就会写入到 aof_buf 中,而 aof_buf 中的数据持久化到磁盘 AOF 文件的过程称为数据同步。

    可以采用以下三种不同的数据同步策略

    • always:写操作命令写入 aof_buf 后会立即调用 fsync() 系统函数,将其追加到 AOF 文件。该策略效率较低,但相对比较安全,不会丢失太多的数据。(生产情况不要使用)

    • no:写操作命令写入 aof_buf 后什么都不做,不会调用 fsync() 函数。而将 aof_buf 中的数据同步磁盘的操作由操作系统负责。Linux 系统默认同步周期为 30 秒,效率较高。

    • everysec:默认策略。写操作命令写入 aof_buf 后并不直接调用 fsync(),而是每秒调用一次 fsync() 系统函数来完成同步。该策略兼顾了性能与安全,是一种折中方案

  2. no-appendfsync-on-rewrite

    前提是 appendfsync 设置为 always 或 everysec

    该属性用于指定,当主进程创建了子进程正在执行 bgsave 或 bgrewriteaof 时,主进程是否不调用 fsync() 来做数据同步

    如果调用 fsync(),在需要同步的数据量非常大时,会阻塞主进程对外提供服务,可以将此命令设置为 yes,关闭 rewrite 操作,但这样也就和 appendfsync no 一样了

  3. aof-rewrite-incremental-fsync

    当 bgrewriteaof 在执行过程也是先将 rewrite 计算的结果写入到了 aof_rewrite_buf 缓存中,然后当缓存中数据达到一定量后就会调用 fsync() 进行刷盘操作,即数据同步,将数据写入到临时文件。该属性用于控制 fsync() 每次刷盘的数据量最大不超过 4MB。这样可以避免由于单次刷盘量过大而引发长时间堵塞

  4. aof-timestamp-enabled

    该属性设置为 yes 则会开启在 AOF 文件中增加时间戳的显示功能,可方便按照时间对数据进行恢复。但该方法可能会与 AOF 解析器不兼容,所以默认值为 no,不开启

  5. aof-load-truncated

    AOF 文件可能会在加载到内存时被截断(当操作系统崩溃时,极有可能发生。但是当 Redis 崩溃,但系统没有崩溃时,不会发生这个情况),如果此命令设置为 yes,会重新加载 AOF 文件

AOF 持久化过程

RDB 与 AOF 对比


RDB 优势与不足

优势
  • RDB 文件较小(快照)

  • 数据恢复快

不足
  • 数据安全性较差

  • 写时复制会降低性能

  • RDB 文件可读性较差

AOF 优势与不足

优势
  • 数据安全性高

  • AOF 文件可读性高

不足
  • AOF 文件比较大(日志)

  • 写操作会影响性能

  • 数据恢复较慢(重新执行命令)

持久化技术的选型

官方推荐使用 RDB 与 AOF 混合式持久化。

若对数据安全性要求不高,则推荐使用纯 RDB 持久化方式

不推荐使用纯 AOF 持久化方式。

若 Redis 仅用于缓存,无需使用任何持久化技术

Redis 主从集群


为了避免 Redis 的单点故障问题,我们可以搭建一个 Redis 集群,将数据备份到集群中的其他节点上。若一个 Redis 节点宕机,则由集群中的其他节点顶上

主从集群搭建


Redis 的主从集群是一个”一主多从”的读写分离集群。集群中的 Master 节点负责处理客户端的读写请求,而 Slave 节点仅能处理客户端的读请求。之所以要将集群搭建为读写分离模式,主要原因是对于数据库集群,写操作压力一般较小,压力大多来源于读操作请求。所以,只有一个接待你负责处理写操作请求即可

伪集群搭建与配置

在采用单线程 IO 模型时,为了提高处理器的利用率,一般会在一个主机中安装多台 Redis,构建一个 Redis 主从伪集群。

演示搭建配置:一个 Master 与 两个 Slave

  1. 复制 redis.conf

  2. 修改 redis.conf

    • 设置 masterauth

      主从 Redis 数据库的密码需要一致

    • 设置 repl-disable-tcp-nodelay

      该属性用于设置是否禁用 TCP 特性 tcp-nodelay。设置为 yes 则禁用,此时 Master 与 Slave 间的通信会产生延迟,但使用的 TCP 包数量会较少,占用的网络带宽会较小。反之,则网络延时变小,TCP 包数量较多,占用网络带宽较大

      tcp-nodelay

      为了充分复用网络带宽,TCP 总是希望发送尽可能大的数据块。为了达到该目的,TCP 中使用了一个叫 Nagle 的算法。

      网络在接收到要发送的数据后,不直接发送,而是等待数据量足够大时在一次性发送出去(延迟发送)

      此命令是 TCP 协议中 Nagle 算法的开关

  3. 新建配置文件

    将 redis.conf 中的共有属性,写入新的配置文件中,并引用源 redis.conf 文件

    • include redis.conf 引用源文件

    • pidfile /var/run/redis_xxx.pid <redis_xxx> 不能和源 redis.conf 中的相同

    • port <端口号> 自定义端口号

    • dbfilename dumpxxxx.rdb <dumpxxxx.rdb> 不能和源 redis.conf 中的相同

    • appendfilename appendonlyxxxx.aof <appendonlyxxxx.aof> 不能相同 AOF文件配置

    • replice-priority xx <xx> 为优先级数值,优先级越小,选举时优先为主 redis

  4. 设置主从关系

    当主从库宕机后重新启动,需要重新设置主从关系

    • 格式 slaveof <ip> <port>

      设置本机的主 Redis

分级管理

若 Redis 主从集群中的 Slave 较多时,它们的数据同步过程会对 Master 形成较大的性能压力。此时可以对 Slave 进行分级管理

使用 slaveof 低级 Slave 指向高级 Slave 即可

容灾冷处理

在 Master/Slave 的 Redis 集群中,如果 Master 宕机有两种处理方式

  • 通过手动角色调整,使 Slave 晋升为 Master 的 冷处理

  • 使用哨兵模式,实现 Redis 集群的高可用 HA,即 热处理

主从复制原理


当一个 Redis 节点(slave 节点)接收到类似 slaveof ip port 的命令后直至其可以从 master 持续复制数据

主从复制过程

  1. 保存 master 地址

    当 slave 接收到 slaveof 指令后,slave 会立即将新的 master 的地址保存下来

  2. 建立连接

    slave 中维护着一个定时任务,该定时任务会尝试着发送请求,与该 master 建立 socket 连接。

    • 如果连接成功,slave会发送 ping 命令进行首次通信。

    • 如果连接失败,重新让定时任务发出连接请求。

    • 如果在接收请求时接收到 slaveof no one 命令,slave 转 master 命令,连接立即停止

  3. 身份验证

    master 在接收到 slave 的 ping 命令后对 slave 进行身份验证

    • 如果验证通过,master 向 slave 发送连接成功响应

    • 如果验证失败,拒绝连接

  4. 数据同步

    slave 接收到 master 的连接成功响应后,slave 向 master 发出数据同步请求。master 在接收到数据同步请求后 fork 出一个子进程进行数据持久化。持久化完毕后 master 再 fork 出一个子进程将 master 的持久化文件发送给 slave。slave 接收到 master 的数据并写入到本地持久化文件

    • 如果数据同步过程中 master 主进程又有写操作发生,master 将数据写入到本地内存的同时又将数据写入到同步缓存。当 master 的持久化文件数据发送完毕后,master 将同步缓存中的数据发送给 slave(数据补偿),slave 将数据追加到本地持久化文件中,数据同步完毕
  5. 恢复数据

    数据同步完毕后 slave 读取本地持久化文件,恢复内存数据,对外服务。对外服务中 master 持续接收到写操作,会以增量方式发送给 slave

  • 标题: Redis
  • 作者: IntYou
  • 创建于: 2023-04-17 09:28:36
  • 更新于: 2023-04-23 21:19:45
  • 链接: https://intyou.netlify.app/2023/04/17/Redis/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。