Redis 连接
-
Redis 连接命令主要是用于连接 redis 服务。
-
通过密码验证连接到 redis 服务,并检测服务是否在运行:
redis 127.0.0.1:6379> AUTH "password" OK redis 127.0.0.1:6379> PING PONG
-
Redis Auth 命令用于检测给定的密码和配置文件中的密码是否相符。密码匹配时返回 OK ,否则返回一个错误。
redis 127.0.0.1:6379> AUTH PASSWORD (error) ERR Client sent AUTH, but no password is set redis 127.0.0.1:6379> CONFIG SET requirepass "mypass" OK redis 127.0.0.1:6379> AUTH mypass Ok
-
Redis Echo 命令用于打印给定的字符串。返回字符串本身。
redis 127.0.0.1:6379> ECHO "Hello World" "Hello World"
-
Redis Ping 命令使用客户端向 Redis 服务器发送一个 PING ,如果服务器运作正常的话,会返回一个 PONG 。通常用于测试与服务器的连接是否仍然生效,或者用于测量延迟值,如果连接正常就返回一个 PONG ,否则返回一个连接错误。
# 客户端和服务器连接正常 redis 127.0.0.1:6379> PING PONG # 客户端和服务器连接不正常(网络不正常或服务器未能正常运行) redis 127.0.0.1:6379> PING Could not connect to Redis at 127.0.0.1:6379: Connection refused
-
Redis Quit 命令用于关闭与当前客户端与redis服务的连接。一旦所有等待中的回复(如果有的话)顺利写入到客户端,连接就会被关闭。总是返回 OK 。
redis 127.0.0.1:6379> QUIT OK
-
Redis Select 命令用于切换到指定的数据库,数据库索引号 index 用数字值指定,以 0 作为起始索引值。总是返回 OK
redis 127.0.0.1:6379> SET db_number 0 # 默认使用 0 号数据库 OK redis 127.0.0.1:6379> SELECT 1 # 使用 1 号数据库 OK redis 127.0.0.1:6379[1]> GET db_number # 已经切换到 1 号数据库,注意 Redis 现在的命令提示符多了个 [1] (nil) redis 127.0.0.1:6379[1]> SET db_number 1 OK redis 127.0.0.1:6379[1]> GET db_number "1" redis 127.0.0.1:6379[1]> SELECT 3 # 再切换到 3 号数据库 OK redis 127.0.0.1:6379[3]> # 提示符从 [1] 改变成了 [3]
Redis 慢查询
-
Redis也有慢查询日志,可用于监视和优化查询
-
慢查询设置:在redis.conf中可以配置和慢查询日志相关的选项:
#执行时间超过多少微秒的命令请求会被记录到日志上 0 :全记录 <0 不记录 slowlog-log-slower-than 10000 #slowlog-max-len 存储慢查询日志条数 slowlog-max-len 128
-
Redis使用列表存储慢查询日志,采用队列方式(FIFO)
config set的方式可以临时设置,redis重启后就无效 config set slowlog-log-slower-than 微秒 config set slowlog-max-len 条数
-
查看日志:
slowlog get [n]
127.0.0.1:6379> config set slowlog-log-slower-than 0 OK 127.0.0.1:6379> config set slowlog-max-len 2 OK 127.0.0.1:6379> set name:001 zhaoyun OK 127.0.0.1:6379> set name:002 zhangfei OK 127.0.0.1:6379> get name:002 "zhangfei" 127.0.0.1:6379> slowlog get 1) 1) (integer) 7 #日志的唯一标识符(uid) 2) (integer) 1589774302 #命令执行时的UNIX时间戳 3) (integer) 65 #命令执行的时长(微秒) 4) 1) "get" #执行命令及参数 2) "name:002" 5) "127.0.0.1:37277" 6) "" 2) 1) (integer) 6 2) (integer) 1589774281 3) (integer) 7 4) 1) "set" 2) "name:002" 3) "zhangfei" 5) "127.0.0.1:37277" 6) "" # set和get都记录,第一条被移除了。
-
慢查询记录的保存:在redisServer中保存和慢查询日志相关的信息
struct redisServer { // ... // 下一条慢查询日志的 ID long long slowlog_entry_id; // 保存了所有慢查询日志的链表 FIFO list *slowlog; // 服务器配置 slowlog-log-slower-than 选项的值 long long slowlog_log_slower_than; // 服务器配置 slowlog-max-len 选项的值 unsigned long slowlog_max_len; // ... };
Redis 监视器
- Redis客户端通过执行MONITOR命令可以将自己变为一个监视器,实时地接受并打印出服务器当前处理的命令请求的相关信息。
- 此时,当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条命令请求的信息发送给所有监视器。
Redis客户端1
127.0.0.1:6379> monitor
OK
1589706136.030138 [0 127.0.0.1:42907] "COMMAND"
1589706145.763523 [0 127.0.0.1:42907] "set" "name:10" "zhaoyun"
1589706163.756312 [0 127.0.0.1:42907] "get" "name:10"
Redis客户端2
127.0.0.1:6379>
127.0.0.1:6379> set name:10 zhaoyun
OK
127.0.0.1:6379> get name:10
"zhaoyun"
Redis持久化-RDB
-
Redis是内存数据库,宕机后数据会消失。Redis重启后快速恢复数据,要提供持久化机制,Redis持久化是为了快速的恢复数据而不是为了存储数据
-
Redis有两种持久化方式:RDB和AOF,注意:Redis持久化不保证数据的完整性。
-
当Redis用作DB时,DB数据要完整,所以一定要有一个完整的数据源(文件、mysql),在系统启动时,从这个完整的数据源中将数据load到Redis中,数据量较小,不易改变,比如:字典库(xml、Table)
-
通过info命令可以查看关于持久化的信息
# Persistence loading:0 rdb_changes_since_last_save:1 rdb_bgsave_in_progress:0 rdb_last_save_time:1589363051 rdb_last_bgsave_status:ok rdb_last_bgsave_time_sec:-1 rdb_current_bgsave_time_sec:-1 rdb_last_cow_size:0 aof_enabled:1 aof_rewrite_in_progress:0 aof_rewrite_scheduled:0 aof_last_rewrite_time_sec:-1 aof_current_rewrite_time_sec:-1 aof_last_bgrewrite_status:ok aof_last_write_status:ok aof_last_cow_size:0 aof_current_size:58 aof_base_size:0 aof_pending_rewrite:0 aof_buffer_length:0 aof_rewrite_buffer_length:0 aof_pending_bio_fsync:0 aof_delayed_fsync:0
-
RDB(Redis DataBase),是redis默认的存储方式,RDB方式是通过快照( snapshotting )完成的。这一刻的数据不关注过程
-
触发快照的方式:
- 符合自定义配置的快照规则
- 执行save或者bgsave命令
- 执行flushall命令
- 执行主从复制操作 (第一次)
-
配置参数定期执行:在redis.conf中配置:save 多少秒内 数据变了多少
save "" # 不使用RDB存储 不能主从 save 900 1 # 表示15分钟(900秒钟)内至少1个键被更改则进行快照。 save 300 10 # 表示5分钟(300秒)内至少10个键被更改则进行快照。 save 60 10000 # 表示1分钟内至少10000个键被更改则进行快照。
-
命令显式触发:在客户端输入bgsave命令。
127.0.0.1:6379> bgsave Background saving started
RDB执行流程(原理)
- Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(aof文件重写命令)的子进程,如果在执行则bgsave命令直接返回。
- 父进程执行fork(调用OS函数复制主进程)操作创建子进程,这个复制过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令。
- 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令。
- 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。(RDB始终完整)
- 子进程发送信号给父进程表示完成,父进程更新统计信息。
- 父进程fork子进程后,继续工作。
RDB的优点
- RDB是二进制压缩文件,占用空间小,便于传输(传给slaver)
- 主进程fork子进程,可以最大化Redis性能,主进程不能太大,Redis的数据量不能太大,复制过程中主进程阻塞
RDB的缺点
- 不保证数据完整性,会丢失最后一次快照以后更改的所有数据
Redis持久化-AOF
-
AOF(append only file)是Redis的另一种持久化方式。Redis默认情况下是不开启的。开启AOF持久化后,Redis 将所有对数据库进行过写入的命令(及其参数)(RESP)记录到 AOF 文件, 以此达到记录数据库状态的目的,这样当Redis重启后只要按顺序回放这些命令就会恢复到原始状态了。
-
AOF会记录过程,RDB只管结果,
-
AOF持久化实现配置 redis.conf
# 可以通过修改redis.conf配置文件中的appendonly参数开启 appendonly yes # AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。 dir ./ # 默认的文件名是appendonly.aof,可以通过appendfilename参数修改 appendfilename appendonly.aof
AOF原理
- AOF文件中存储的是redis的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:
- 命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
- 缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
- 文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话,fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。
命令传播
- 当一个 Redis 客户端需要执行命令时, 它通过网络连接, 将协议文本发送给 Redis 服务器。
- 服务器在接到客户端的请求之后, 它会根据协议文本的内容, 选择适当的命令函数, 并将各个参数从字符串文本转换为 Redis 字符串对象( StringObject )。
- 每当命令函数成功执行之后, 命令参数都会被传播到AOF 程序。
缓存追加
- 当命令被传播到 AOF 程序之后, 程序会根据命令以及命令的参数, 将命令从字符串对象转换回原来的协议文本。协议文本生成之后, 它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。
- redisServer 结构维持着 Redis 服务器的状态, aof_buf 域则保存着所有等待写入到 AOF 文件的协议文本(RESP)。
文件写入和保存
- 每当服务器常规任务函数被执行、 或者事件处理器被执行时, aof.c/flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作:
- WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
- SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
AOF 保存模式
- Redis 目前支持三种 AOF 保存模式,它们分别是:
- AOF_FSYNC_NO :不保存。
- AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)
- AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐)
AOF_FSYNC_NO :不保存。
- 在这种模式下, 每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行, 但 SAVE 会被略过。
- 在这种模式下, SAVE 只会在以下任意一种情况中被执行:
- Redis 被关闭
- AOF 功能被关闭
- 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
- 这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。
AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)
- 在这种模式中, SAVE 原则上每隔一秒钟就会执行一次, 因为 SAVE 操作是由后台子线程(fork)调用的, 所以它不会引起服务器主进程阻塞。
AOF_FSYNC_ALWAYS :每执行一个命令保存一次。(不推荐)
- 在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。
- 另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求
AOF 保存模式对性能和安全性的影响,它们对服务器主进程的阻塞情况如下
AOF重写过程分析(整个重写操作是绝对安全的):
- Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
- 当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:
- 处理命令请求。
- 将写命令追加到现有的 AOF 文件中。
- 将写命令追加到 AOF 重写缓存中
- 这样一来可以保证:现有的 AOF 功能会继续执行,即使在 AOF 重写期间发生停机,也不会有任何数据丢失。所有对数据库进行修改的命令都会被记录到 AOF 重写缓存中。当子进程完成 AOF 重写之后, 它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用一个信号处理函数, 并完成以下工作:
- 将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。
- 对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。
- Redis数据库里的+AOF重写过程中的命令------->新的AOF文件---->覆盖老的
- 当步骤 1 执行完毕之后, 现有 AOF 文件、新 AOF 文件和数据库三者的状态就完全一致了
- 当步骤 2 执行完毕之后, 程序就完成了新旧两个 AOF 文件的交替
- 这个信号处理函数执行完毕之后, 主进程就可以继续像往常一样接受命令请求了。 在整个 AOF 后台重写过程中, 只有最后的写入缓存和改名操作会造成主进程阻塞, 在其他时候, AOF 后台重写都不会对主进程造成阻塞, 这将 AOF 重写对性能造成的影响降到了最低。
- 以上就是 AOF 后台重写, 也即是 BGREWRITEAOF 命令(AOF重写)的工作原理。
AOF触发方式
-
配置触发:在redis.conf中配置
# 表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过, 以启动时aof文件大小为准 auto-aof-rewrite-percentage 100 # 限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化 auto-aof-rewrite-min-size 64mb
-
执行bgrewriteaof命令
127.0.0.1:6379> bgrewriteaof Background append only file rewriting started
混合持久化
- RDB和AOF各有优缺点,Redis 4.0 开始支持 rdb 和 aof 的混合持久化。如果把混合持久化打开,aofrewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。
- RDB的头+AOF的身体---->appendonly.aof,开启混合持久化:aof-use-rdb-preamble yes
- 可以看到该AOF文件是rdb文件的头和aof格式的内容,在加载时,首先会识别AOF文件是否以REDIS字符串开头,如果是就按RDB格式加载,加载完RDB后继续按AOF格式加载剩余部分。
AOF文件的载入与数据还原
- 因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态
- Redis读取AOF文件并还原数据库状态的详细步骤如下:
- 创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令 的效果和带网络连接的客户端执行命令的效果完全一样
- 从AOF文件中分析并读取出一条写命令
- 使用伪客户端执行被读出的写命令
- 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止当完成以上步骤之后,AOF文件所保存的数据库状态就会被完整地还原出来,整个过程如下图所示:
RDB与AOF对比
- RDB存某个时刻的数据快照,采用二进制压缩存储,AOF存操作命令,采用文本存储(混合)
- RDB性能高、AOF性能较低
- RDB在配置触发状态会丢失最后一次快照以后更改的所有数据,AOF设置为每秒保存一次,则最多丢2秒的数据
- Redis以主服务器模式运行,RDB不会保存过期键值对数据,Redis以从服务器模式运行,RDB会保存过期键值对,当主服务器向从服务器同步时,再清空过期键值对。
- AOF写入文件时,对过期的key会追加一条del命令,当执行AOF重写时,会忽略过期key和del命令。
缓存过期和淘汰策略
- Redis性能高:长期使用,key会不断增加,Redis作为缓存使用,物理内存也会满,内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降
maxmemory 不设置的场景
- Redis的key是固定的,不会增加
- Redis作为DB使用,保证数据的完整性,不能淘汰 , 可以做集群,横向扩展
- 缓存淘汰策略:禁止驱逐 (默认)
maxmemory 设置的场景
- Redis是作为缓存使用,不断增加Key
- maxmemory : 默认为0 不限制
- 问题:达到物理内存后性能急剧下架,甚至崩溃
- 内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降
- 设置maxmemory后,当趋近maxmemory时,通过缓存淘汰策略,从内存中删除对象
- 设置maxmemory maxmemory-policy 要配置
expire数据结构
-
在Redis中可以使用expire命令设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动被删除
-
expire命令的使用方法如下:expire key ttl(单位秒)
127.0.0.1:6379> expire name 2 #2秒失效 (integer) 1 127.0.0.1:6379> get name (nil) 127.0.0.1:6379> set name zhangfei OK 127.0.0.1:6379> ttl name #永久有效 (integer) -1 127.0.0.1:6379> expire name 30 #30秒失效 (integer) 1 127.0.0.1:6379> ttl name #还有24秒失效 (integer) 24 127.0.0.1:6379> ttl name #失效 (integer) -2
删除策略
- Redis的数据删除有定时删除、惰性删除和主动删除三种方式
- Redis目前采用惰性删除+主动删除的方式。
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。需要创建定时器,而且消耗CPU,一般不推荐使用
- 惰性删除:在key被访问时如果发现它已经失效,那么就删除它。调用expireIfNeeded函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删除它
- 主动删除:在redis.conf文件中可以配置主动删除策略,默认是no-enviction(不删除)
maxmemory-policy allkeys-lru
缓存穿透
- 一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。
- 缓存穿透是指在高并发下查询key不存在的数据(不存在的key),会穿过缓存查询数据库。导致数据库压力过大而宕机
解决方案:
- 对查询结果为空的情况也进行缓存,缓存时间(ttl)设置短一点,或者该key对应的数据insert了之后清理缓存。问题:缓存太多空值占用了更多的空间
- 使用布隆过滤器。在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,存在再查缓存和DB。
- 布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机hash映射函数。
- 布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法。
- 布隆过滤器的原理是,当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
缓存雪崩
- 当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力
- 突然间大量的key失效了或redis重启,大量访问数据库,数据库崩溃
- 解决方案:
- key的失效期分散开 不同的key设置不同的有效期
- 设置二级缓存(数据不一定一致)
- 高可用(脏读)
缓存击穿
- 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
- 缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
- 解决方案:
- 用分布式锁控制访问的线程,使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
- 不设超时时间,volatile-lru 但会造成写一致问题,当数据库数据发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的不一致,应用会从缓存中读取到脏数据。可采用延时双删策略处理
数据不一致
- 缓存和DB的数据不一致的根源 : 数据源不一样,保证数据的最终一致性(延时双删)
- 先更新数据库同时删除缓存项(key),等读的时候再填充缓存
- 2秒后再删除一次缓存项(key)
- 设置缓存过期时间 Expired Time 比如 10秒 或1小时
- 将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除(缓存失效期过长 7*24)
数据并发竞争
- 并发指的是多个redis的client同时set 同一个key引起的并发问题。
- 多客户端(Jedis)同时并发写一个key,一个key的值是1,本来按顺序修改为2,3,4,最后是4,但是顺序变成了4,3,2,最后变成了2。
- 第一种方案:分布式锁+时间戳
- 整体技术方案:这种情况,主要是准备一个分布式锁,大家去抢锁,抢到锁就做set操作。
- 加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争。
- Redis分布式锁的实现
- 主要用到的redis函数是setnx(),用SETNX实现分布式锁
- 时间戳:由于上面举的例子,要求key的操作需要顺序执行,所以需要保存一个时间戳判断set顺序。
系统A key 1 {ValueA 7:00} 系统B key 1 { ValueB 7:05}
- 假设系统B先抢到锁,将key1设置为{ValueB 7:05}。接下来系统A抢到锁,发现自己的key1的时间戳早于缓存中的时间戳(7:00<7:05),那就不做set操作了。
- 第二种方案:利用消息队列
- 在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。
- 把Redis的set操作放在队列中使其串行化,必须的一个一个执行。
分布式锁特性
- 互斥性:任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
- 同一性:锁只能被持有该锁的客户端删除,不能由其它客户端删除。
- 可重入性:持有某个锁的客户端可继续对该锁加锁,实现锁的续租
- 容错性:锁失效后(超过生命周期)自动释放锁(key失效),其他客户端可以继续获得该锁,防止死锁