目录
2.1.2 Redis 单线程如何处理那么多的并发客户端连接?
一、Redis 基础数据结构
Redis 有 5 种基础数据结构,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。
1.1 string (字符串)
字符串 string 是 Redis 最简单的数据结构。Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。字符串结构使用非常广泛,一个常见的用途就是缓存用户信息。我们将用户信息结构体使用 JSON 序列化成字符串,然后将序
列化后的字符串塞进 Redis 来缓存。同样,取用户信息会经过一次反序列化的过程。
键值对
批量键值对:可以批量对多个字符串进行读写,节省网络耗时开销,这是一个原子命令
过期和 set 命令扩展:可以对 key 设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间
setnx如果redis数据库中没有name这个key的话,就会执行这个set命令,并且返回1表示执行成功。如果数据库中已经存在了这个key的话就不会执行set命令,并且返回0。这个命令可以用来实现分布式锁
原子计数:如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值,超过了这个值,Redis 会报错
自减命令是:decr age decrby age 5
他们之所以能实现原子操作,是因为redis是单线程的,保证每一个客户端操作的时候只有一个用户对数据操作。这个原子性的原理和JUC中的原子性操作原理不同。redis因为是单线程的,每一个客户端操作redis的时候是串行一个一个执行的,都在一个队列中依次操作。JUC的原子性操作原理是通过CAS来实现的。
1.2 list (列表)
Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n),这点让人非常意外。 当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。
右边进左边出:队列
右边进右边出:栈
1.3 hash (字典)
Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。 hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡。
1.4 set (集合)
Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL。 当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。
1.5 zset (有序集合)
zset 似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。
zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。
zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。
1.6 其他高级命令
keys:全量遍历键,用来列出所有满足特定正则字符串规则的key,当redis数据量比较大时,性能比较差,要避免使用。
scan:渐进式遍历键,类似于分页查找。scan 参数提供了三个参数,第一个是 cursor 整数值(查找的第一个数据的游标指针),第二个是 key 的正则模式,第三个是遍历的 limit hint(指这一轮查找要查找多少个数据)。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。当数据量比较大的时候,可以使用scan来遍历key,这样效率比较高。
第一条命令的含义就是从哈希游标为0的这个数据开始找起,从这个位置开始向下查找1000个数据,然后找出这个哈希表的1000个数据中满足正则表达式的数据。
- 第一行返回的是本次查找的最后一个位置是哈希游标为13976的数据。
- 后面那些是查找到的符合正则表达式的数据。
第二条命令就是说接着上一条命令开始找起,从13976这个位置开始向下再遍历1000条数据,查找出符合正则表达式的数据。注意这里返回的第一行本次查找的最后一个位置的哈希游标变成了1996,之所以比起始的游标还要小,是因为在哈希表中,虽然数据是顺序存放的,但是他们的哈希值并不是顺序排列的。
一直执行这样的操作,直到返回得游标变成最开始遍历的那个游标数后(上面的这个例子就是又回到了0),那么就证明我们已经遍历完了整个redis的Hash表,把所有数据都遍历查找完一遍了。
Redis存储键值对实际使用的是hashtable的数据结构
Info:查看redis服务运行信息,分为 9 大块,每个块都有非常多的参数,这 9 个块分别是:
Server 服务器运行的环境参数
Clients 客户端相关信息
Memory 服务器运行内存统计数据
Persistence 持久化信息
Stats 通用统计数据
Replication 主从复制相关信息
CPU CPU 使用情况
Cluster 集群信息
KeySpace 键值对统计数量信息
flushall:清空redis的所有库的数据(默认redis是16个库)
flushdb:清空当前库的数据
DBSIZE:当前数据库有多少个数据
二、核心原理
2.1 Redis的单线程和高性能
2.1.1 Redis 单线程为什么还能这么快?
因为它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如keys),一定要谨慎使用,因为redis单线程同一时间只能执行一个命令,keys命令非常耗时,所以在很长一段时间就不能去执行其他命令,可能会导致 Redis 卡顿。
2.1.2 Redis 单线程如何处理那么多的并发客户端连接?
Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。
Nginx也是采用IO多路复用原理解决C10K问题
三、持久化
说白了,就是在指定的时间间隔内,将内存当中的数据集快照写入磁盘,它恢复时是将快照文件直接读到内存。
什么意思呢?我们都知道,内存当中的数据,如果我们一断电,那么数据必然会丢失,但是玩过redis的同学应该都知道,我们一关机之后再启动的时候数据是还在的,所以它必然是在redis启动的时候重新去加载了持久化的文件。
redis提供两种方式进行持久化,一种是RDB持久化默认,另外一种是AOF(append only file)持久化。
3.1 RDB快照(snapshot)
原理是redis会单独创建(fork)一个与当前线程一模一样的子进程来进行持久化,这个子进程的所有数据(变量。环境变量,程序程序计数器等)都和原进程一模一样,会先将数据写入到一个临时文件中,待持久化结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程不进行任何的io操作,这就确保了极高的性能。
在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。
他什么时候fork子线程,或者什么时候触发rdb持久化机制
- shutdown时,如果没有开启aof,会触发配置文件中默认的快照配置
- save:是只管保存,其他不管,全部阻塞
- bgsave: redis会在后台异步进行快照操作,同时可以响应客户端的请求
- 执行flushall命令,但是数据库是空的,无意义
你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。
比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集(这里是备份整个内存的数据快照):
# save 60 1000
3.2 AOF(append-only file)
快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化,将修改的每一条指令记录进文件。原理是将Reids的操作日志以追加的方式写入文件,读操作是不记录的。
你可以通过修改配置文件来打开 AOF 功能:
# appendonly yes
从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾(这里是只备份新追加的数据)。
这样的话, 当 Redis 重新启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。
你可以配置 Redis 多久才将数据 fsync 到磁盘一次。
有三个选项:
- appendfsync always:同步持久化,每次有新命令追加到 AOF 文件时就执行一次 fsync,非常慢,也非常安全。
- appendfsync everysec:每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
- appendfsync no:从不 fsync,将数据交给操作系统来处理,等操作系统进行数据缓存同步到磁盘。更快,也更不安全的选择。
推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
3.3 RDB 和 AOF ,我应该用哪一个?
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,因为RDB将多条命令聚合到了一起,但是AOF都是分条的命令。
比如AOF就只会记录下面的每一条命令
但是RDB只记录一条
set count 5
RDB在占用空间和恢复速度上都高于AOF,但是AOF比RDB更能保证数据安全,最大限度保证数据不丢失。
3.4 Redis 4.0 混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。
AOF在重写(aof文件里可能有太多没用指令,所以aof会定期根据内存的最新数据生成aof文件)时将重写这一刻之前的内存rdb快照文件的内容和增量的 AOF修改内存数据的命令日志文件存在一起,都写入新的aof文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,原子的覆盖原有的AOF文件,完成新旧两个AOF文件的替换;也就是说重写其实就是给AOF“瘦身”。
可以看到在AOF文件中上半部分是一些乱码,就是二进制的RDB快照,后半部分就是一些字符串的命令,就是AOF持久化文件,这就是混合持久化的文件。
混合持久化aof文件结构
AOF根据配置规则在后台自动重写,也可以人为执行命令bgrewriteaof重写AOF。 于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
set count 5
lpush list a1 a2 a3
aof重写机制
- 当AOF文件增长到一定大小的时候Redis能够调用 bgrewriteaof对日志文件进行重写 。当AOF文件大小的增长率大于该配置项时自动开启重写(这里指超过原大小的100%)。
- auto-aof-rewrite-percentage 100
- 当AOF文件增长到一定大小的时候Redis能够调用 bgrewriteaof对日志文件进行重写 。当AOF文件大小大于该配置项时自动开启重写
- auto-aof-rewrite-min-size 64mb
- 手动执行重写命令bgrewriteaof,这个命令是异步命令
4.0版本的混合持久化默认关闭的,通过aof-use-rdb-preamble配置参数控制,yes则表示开启,no表示禁用,5.0之后默认开启。
开启混合持久化:
# aof-use-rdb-preamble yes
混合持久化是通过bgrewriteaof完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入新的aof文件(4),然后再将重写缓冲区的增量命令以AOF方式写入到文件(5-2),写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件(5-3)。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据。
优点:混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。
缺点:兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差
3.5 小总结:
1.redis提供了rdb持久化方案,为什么还要aof?
优化数据丢失问题,rdb会丢失最后一次快照后的数据,aof丢失不会超过2秒的数据
2.如果aof和rdb同时存在,听谁的?
aof
3.rdb和aof优势劣势
rdb 适合大规模的数据恢复,对数据完整性和一致性不高 , 在一定间隔时间做一次备份,如果redis意外down机的话,就会丢失最后一次快照后的所有操作。aof 根据配置项而定。官方建议 两种持久化机制同时开启,如果两个同时开启 优先使用aof持久化机制
3.6 性能建议(这里只针对单机版redis持久化做性能建议):
因为RDB文件只用作后备用途,只要15分钟备份一次就够了,只保留save 900 1这条规则。
如果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。 代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。 只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。 默认超过原大小100%大小时重写可以改到适当的数值。
四、缓存淘汰策略
当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap)。交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。
在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。
当实际内存超出 maxmemory 时,Redis 提供了几种可选策略 (maxmemory-policy) 来让用户自己决定该如何腾出新的空间以继续提供读写服务。
noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。
volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。
volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰。