. 简述:
Redis由Salvatore Sanfilippo使用C语言编写的一种支持网络、可基于内存亦可持久化的日志型、Key-Value数据库。
2. 特点:
2.1 优点:
2.1.1 redis的支持数据持久化
可以将内存中的数据保存在磁盘中,重启时通过再次加载,这样数据不会丢失;
2.1.2 Redis支持数据类型丰富
支持String,List,Set,Zset(sorted sets),Hash数据结构的存储;
2.1.3 Redis支持master-slave模式的数据备份;
2.1.4 Redis的操作支持原子性
Redis提供了简单的事务功能, 将一组需要一起执行的命令放到multi和exec两个命令之间(其中 multi命令代表事务开始, exec命令代表事务结束),它们之间的命令是原子顺序执行的。
2.1.5 性能极高
读写数据每秒可达10万次左右。
2.1.6 字符串能够存储的最大值是512M,几乎能够满足所有常见业务场景。
缺点:
2.2.1 容量收到物理内存的限制,适合小数据量级别的高性能读写操作,大数据量可以选择淘宝的Tair
2.2.2 不具有自动容错和恢复能力;
2.2.3 主机宕机之后,如果有部分没有同步到从机的数据,那么切到从机之后,会出现数据不一致的情况;
2.2.3 redis事务不支持回滚。
3.Redis的数据持久化
Redis提供了RDB(Redis DataBase)和AOF(Append Only File)两种持久化方式。
3.1 RDB是指在一定的时间间隔将数据快照存储到磁盘。
当到达数据备份时间时,Redis会启动一个线程,该线程将数据保存到一个临时文件,以替换上个时间周期备份的临时文件。优点:数据恢复快,缺点:最后一次持久化后的数据有可能会丢失。
3.2 AOF是以日志的形式记录每次对Redis的写操作
以redis协议追加保存每次写的操作到文件末尾,当Redis重启的时,重新执行这些命令来恢复原始的数据。优点:数据实时同步,缺点:AOF文件持续增长而过大(Redis通过设置文件大小的阀值来触发AOF文件的压缩)。
注:当同时开启RDB和AOF时,系统优先使用AOF。
4.主从复制
4.1 主从复制的延迟
主从复制可以通过slaveof命令配置实现,默认情况下Redis都是主节点,每个从节点只能有一个主节点,而主节点可以有多个从节点,故复制的数据流是单向的,即只能从主节点复制到从节点,因此主节点无法感知从节点的修改,所以一般从节点提供只读模式。
由于主从部署在不同的机器上,Redis提供了repl-disable-tcp-nodelay参数用于控制是否关闭
TCP_NODELAY, 默认关闭。
a.当关闭时, 主节点产生的命令数据大小都会及时地发送给从节点, 优点:延迟会变小, 缺点:网络带宽的消耗增加。 适用于同机架或同机房部署等网络环境好的场景。
b.当开启时, 主节点会合并较小的数据包发送到从节点,一般时间间隔设置为30-50毫秒。优点:节省带宽,缺点:增大主从之间的延迟。 适用于主从网络环境较差的环境, 如跨机房部署等。
注意:从节点更换主节点后从节点会清空之前所有的数据, 所以执行该操作时一定要在正确的主节点和从节点上。
4.2 Redis拓扑结构
4.2.1 一主一从
4.2.2 一主多从
4.2.3 树状主从
4.3 主从复制过程
1.从节点保存主节点信息
即执行slaveof后,从节点保存主节点的地址信息。
2.主从建立Socket连接
从节点通过执行定时任务(即每秒执行一次)发现slaveof新的主节点后,会尝试建立Socket网络连接。如果连接失败,从节点会通过定时任务来无限重试。
3.发送ping命令
Socket连接成功后,从节点发送ping请求进行首次通信,主要目的是:检测主从之间网络套接字是否可用,并检测主节点当前是否可接受处理命令。
4.权限验证
如果主节点设置了requirepass参数, 则需要密码验证,从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证。
5.同步数据集
主从复制连接正常后, 对于首次建立复制的场景, 主节点会把持有的数据全部发送给从节点,
6.命令持续复制
主节点会持续地把写命令发送给从节点, 从而保证主从数据一致。
4.4 数据延迟
Redis主从数据的延迟由于异步复制特性导致的,是无法避免的, 延迟受到网络带宽和命令阻塞情况的影响, 如果业务不允许短时间内的数据延迟, 可以编码实现监控程序,监听主从节点的复制偏移量, 当延迟较大时触发报警或通知客户端避免读取延迟过大的从节点,从而避免读不到数据的情况发生。
4.5 过期数据的处理
4.5.1 惰性删除
主节点每次处理读取命令时,除了读取key-value表,还会读取Redis超时时间表,以检查key是否超时, 如果超时则删除key对象, 之后del命令也会异步发送给从节点,从节点再执行del。
4.5.2 定时删除
Redis主节点在内部定时任务会循环读取一定数量的Key, 当发现读取的Key过期时执行del命令,之后del命令也会异步发送给从节点,从节点再执行del。
注意:为了保证复制数据的一致性, 从节点自身不会主动删除超时数据。
4.6 Redis读写分离的考量
考虑到数据延迟,过期数据等,以及Redis的高性能,因此在考虑读写分离之前,一定要在主节点上做好充分优化,如解决慢查询,持久化阻塞和合理的数据结构等,当仍然不满足时,可以先考虑Redis Cluster等分布式方案,再考虑读写分离方案。
5.Redis的阻塞问题
由于Redis是典型的单线程架构,所有的读写操作都在一条主线程中完成,因此如果主线程拥塞了,那将是业务系统的噩梦。
造成阻塞的原因如下:
5.1 API或数据结构使用不合理
问题定位:1.通过slowlog get { n } 获取最近n条慢查询(Redis默认对于执行超过10毫秒的命令都会记录到一个定长队列(队列默认长度为128)中),发现慢查询后,可以通过使用低算法复杂度的命令或避免大对象数据来优化。
5.2 CPU饱和的问题
CPU饱和是指Redis把单核CPU(由于Redis是单线程,只能使用一个CPU)使用率跑到接近100%。可以使用top命令查看Redis线程的CPU使用率,可参考:https://www.jianshu.com/p/6d7571d82304。对于Redis的使用情况,可通过redis-cli-h{ip}-p{port}--stat命令每秒输出Redis的统计信息。如果此时发现Redis的请求量比较低,那说明使用了高复杂度算法的命令;如果此时发现Redis的请求量已经很高了,而且应用的优化有限,那就需要使用新的Redis来分摊请求和CPU的压力。
5.3 持久化相关的阻塞
对于开启了持久化功能的Redis节点,要检查是否是fork阻塞,AOF刷盘阻塞和HugePage写操作阻塞导致的CPU使用率过高。具体可参考官网:http://www.redis.io/topics/latency
5.4 CPU竞争
当Redis和其他多核CPU密集型服务部署在一起时,其他进程过度消耗CPU时, 将严重影响Redis吞吐量。可以将Redis绑定到一个CPU上,避免CPU切换的开销。
5.5 内存交换
如果系统把Redis的部分内存中的数据换出并序列化到磁盘后,如果请求的数据不在内存中,需要内存交换,那么Redis性能严重下降。解决方式:扩大内存,设置Redis的最大可用内存,降低系统内存交换执行的优先级。
5.6 网络问题
解决方式:增加带宽,同机房部署,机房部署使用专线,改善机器的物理拓扑(同物理机>同机架>跨机架>同机房>同城机房>异地机房,注意:容灾性相反)
6. Redis高性能的原因:
6.1.纯内存
Redis将所有数据放在内存中,而内存的相应时长在100纳秒左右。
6.2.非阻塞I/O
Redis使用了epoll多路复用技术,另外Redis自身的事件处理模型将epoll中的连接、 读写、关闭都转换为事件。
6.3.没有线程切换(Redis 6已经支持了多线程)
Redis采用了单线程架构,避免了线程切换、竞态和锁等产生的自我消耗。
7.Redis的Pipeline流水线
Redis提供了批量操作命令(如mget、 mset、hmset等),有效的节约了多次访问的往返时间,但是有些命令没有对应的批量命令(如hgetall没有对应的mhgetall),如果n次调用,就会消耗n次往返时间。Redis针对这一问题提供了Pipeline流水线机制,Redis将一组Redis命令组装到一起,通过一次调用Redis,再将执行结果按照请求顺序返回客户端。
8 Redis内存优化:
info memory命令可以显示内存的相关指标,主要有used_memory(Redis内部存储所有数据内存占用量),used_memory_ssr(操作系统显示Redis进程占用的物理内存)以及他们的比值mem_fragmentation_ratio。
当mem_fragmentation_ratio>1时,说明有部分内存没有用于数据存储,而是被内存碎片消耗,因此要降低mem_fragmentation_ratio的值,即降低碎片率。
当mem_fragmentation_ratio<1时,是因为操作系统把Redis的内存数据交换到磁盘,因此used_memory_ssr才会大于used_memory,由于硬盘的读写速度和内存差距很大,此时Redis的性能会很差。
8.2.内存的使用划分
8.2.1 Redis的进程自身的内存
这部分数据在几MB级别,很小可以忽略不计。
8.2.2 对象内存
即所有key-value数据使用的内存,包括所有key的消耗和所有value的消耗。
8.2.3 缓存内存
主要包括客户端缓存,复制积压区缓存,AOF缓存。
8.2.3.1 客户端缓存:
指的是所有接入到Redis服务器TCP连接的输入输出缓冲。输入缓冲无法控制(最大空间为1G), 如果超过将断开连接。 输出缓冲通过参数控制(默认为1G)
a.客户端:可以通过设置最大客户端连接数(特别是有慢连接时)来限制大量客户端接入造成的内存消耗
b.主从复制客户端:当主从复制延迟较高或从节点挂载从节点数量过多时,内存消耗将增加。可通过改善网络环境和减少主节点挂载的从节点数来解决。
c.订阅客户端:使用发布/订阅时, 连接客户端使用独立的输出缓冲区,对于订阅消息短时间内量比较大时,输出缓冲区会产生积压溢出。
d 复制积压缓存区
复制积压缓存区主要用来实现主从复制功能,一个主节点只有一个复制积压缓存区,所有从节点共享该缓存区。
e AOF缓冲区
AOF缓冲区用于在Redis重写期间保存最近的写入命令,AOF缓存区的消耗大小由AOF重写时间和写入命令量决定,这部分空间占用量比较小。
8.2.3.2 内存碎片
Redis提供了jemalloc(默认),glibc和tcmalloc三种内存分配器。当存储的数据长短差异较大时,做频繁的更新,删除大量key。主要解决碎片的方法有:
a.数据对齐,尽量采用数字类型或者固定长度字符串等;
b.Redis重启,内存会重新整理。
8.2.3.3 子进程内存消耗
子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。
8.3 内存管理
8.3.1 控制内存上限
maxmemory参数设置最大可用内存,一方面方式所用内存超过物理内存,另一方面用于缓存,超过上限时,触发内存回收,以便释放空间。
8.3.2 内存回收
a.删除过期key对象
参考上文
b.内存溢出控制策略
内存达到maxmemory上限时会触发溢出控制策略,Redis提供了6种溢出控制策略:
1)noeviction:即拒绝写只响应读(默认);
2) volatile-lru:根据LRU(Least Recently Used,即最近最少使用),删除过期的key数据,以腾出空间。如果没有可删除的过期key数据,回退到noeviction;
3)allkeys-lru:不考虑过期key数据的LRU算法;
4)allkeys-random:随机删除所有键;
5)volatile-random:随机删除过期键;
6)volatile-ttl:根据键值对象的ttl属性, 删除将要过期的key数据。 如果没有可删除的数据, 回退到noeviction策略。
8.3.3 手动内存回收
可以使用scan+object idletime命令批量查询哪些key长时间未被访问, 然后对key进行清理, 可降低内存占用。
8.4 内存优化
8.4.1 缩减key(key值越短越好)和value的值的大小
主要方法有把key和value序列化成二进制数组数据(可以通过protostuff,kryo等高效序列化工具)或者序列化成json,xml压缩(如GZIP,Snappy压缩工具)再放入Redis,去掉value中不必要的字段。
8.4.2 共享对象池
共享对象池是指Redis内部维护[0-9999]的整数对象池,避免Redis封装RedisObject(Redis存储的数据都使用redisObject来封装)内存消耗。
8.4.3 字符串优化
a.Redis针对字符串采用了预分配机制,防止更新操作需要不断重分配内存和字节数据拷贝,但是同事造成了内存浪费。因此尽量减少字符串频繁修改操作(如append、setrange),直接使用set修改字符串。
8.4.4 字符串重构
避免每份数据作为字符串整体存储, 像json这样的数据可以使用hash结构, 使用二级结构存储能够节省内存。 可以使用hmget、 hmset命令支持字段的部分读取修改, 而不用每次整体存取。
8.4.5 编码优化
Redis通过不同编码实现效率和空间的平衡,编码类型转换在Redis写入数据时自动完成, 这个转换过程是不可逆的, 转换规则只能从小内存编码向大内存编码转换。针对性能要求较高的场景使用ziplist
8.4.6 控制key的数量
利用Redis的数据结构降低外层键的数量,比如把大量键分组映射到多个hash(用field记录副key)结构中降低键的数量。
9.Redis的经典问题
9.1 缓存穿透
缓存穿透:指查询的数据,在数据库不存在的情况。这样程序会先访问Redis,没有找到数据,再查询数据库,也没有找到,然后返回空。如果恶意大量的访问数据库中不存在的数据,造成资源浪费,对数据库造成压力,甚至会导致业务不可用,甚至压垮数据库。
解决方法:空值缓存,即如果数据库查询的时候,没有查询到数据,也将空值缓存到Redis中,并设置过期时间(相对普通的数据,过期时间设置较短,避免占用过大的存储空间),这样就避免造成对数据库的压力。
9.2 缓存雪崩
缓存雪崩: 指在某一时间段内,缓存数据集中过期失效。比如CRM系统,工作日早上9点左右,会迎来大量的数据访问,加入缓存是2个小时,那么到了11点,很多缓存数据就会过期,很多数据的访问都会落到数据库上,导致此时的数据库访问量大增,会对数据库产生周期性的压力。另外,Redis集群的某些节点宕机,也会导致缓存雪崩,而且是不可预知的,此时DB的访问量大增,甚至压垮DB。
解决方法: 针对不同的类型数据,如客户,销售订单等,采用不同的缓存过期时间;在同一个对象类中,采取随机因子的方式,设置不同的过期时间。
9.3 缓存击穿
缓存击穿: 指一个cache中的key并发访问量非常大,形成了热点。当这个key过期后,持续的大并发就会跳过缓存,直接访问DB,造成DB的压力。
解决方法: 对热点的key程序实现自动转为永不过期缓存,可以在value中设置超时时间,程序内部校验是否过期。如果过期,异步发起一个线程更新缓存(可以使用锁控制并发)。
10.哨兵(Sentinel)
哨兵是Redis的高可用解决方案,它由一个或者多个Sentinel实例组成Sentinel系统,Sentinel系统监视n个服务器以及对应的从服务器,当监视到主服务器线下时,自动将主服务器对应的某个从服务器升级为新的主服务器,代替主服务器继续处理请求。
哨兵系统架构:
10.1 定时监控
a.每个哨兵实例会间隔10秒向主节点和从节点发送info命令,获取最新的Redis的拓扑结构;
b.每隔2秒每个哨兵节点会向Redis数据节点的指定频道上发送 该节点对主节点的判断以及自身的信息,同时也从该频道上获取其他哨兵节点的发送的信息;
c.每隔1秒,每个哨兵节点会向主节点、从节点、其他哨兵节点发送ping命令,做心跳检测,来判断节点的可达性。
10.2 节点下线
主观下线
如果哨兵节点发送心跳检测,超过down_after_milliseconds后,仍然没有得到回复,哨兵节点会认为该节点为失效节点,称为主观下线。
客观下线
当一个哨兵节点主观下线的节点是Master时,该哨兵节点会向其他哨兵节点询问当前Master节点的状态,当超过quorum指定个数时,该哨兵节点认为Master节点确实失效,然后做出客观下线。
10.3 选举哨兵主节点
故障转移有一个哨兵节点负责,当哨兵系统有多个哨兵节点时,需要进行哨兵领导者选举:
当一个哨兵节点最先完成客观下线后,会向其他哨兵节点发送"我要成为领导者",其他哨兵节点如果没有同意过其他哨兵节点的请求,则返回同意该请求,否则拒绝。当该节点获得的同意票大于等于max(quorum,num(sentinels)/2 +1)时,该哨兵节点成为领导者;否则进入下一轮选举。实际选举过程非常快,基本谁先完成客观下线,谁就是领导者。
10.4 故障转移
哨兵领导者负责故障转移,具体过程如下:
1.选取slave节点作为Master节点;
1.1 过滤失效的slave节点;
1.2 如果有slave优先级,则根据slave节点的优先级返回slave节点,否则继续;
1.3 选择复制偏移量最大(这样节省复制时间)的slave节点,如果没有则继续;
1.4 选择runid最小的slave节点;
2.哨兵领导者根据第一步选举出的slave节点执行slaveof on one命令,使其成为Master;
3.哨兵领导者向剩余的slave节点发送命令,让他们成为新Master节点的从节点;
4.哨兵节点集合将旧的Master节点更改为Slave节点,并持续监控,当其恢复可用后,让它copy新的Master节点。
当前很少用哨兵,目前推荐使用Redis集群,这个的话就避免了单个主节点的问题。
11.Redis跳表
可参考文章,写的很详细。
(https://www.cnblogs.com/Elliott-Su-Faith-change-our-life/p/7545940.html)
跳表的优点:
1. 存储空间小
改变关于节点具有给定级别的概率的参数将使其比b树更少的内存占用
2. 范围查找
红黑树结构在范围查找时,要进行中序遍历;
跳表查询范围非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
3. 插入删除效率
红黑树的插入和删除会导致树的旋转,跳表的插入和删除只需要修改相邻节点的指针,简单又快速。
12.Redis的字典
Redis数据库使用字典作为底层的实现,利用字典提供CRUD操作。
12.1 Redis的Hash表
Redis使用的Hash表dict.h/dictht,具体格式如下:
typedef struct dictht {
dictEntry **table; // Hash表数组
unsigned long size;// Hash表的大小
unsigned long sizemasky;// Hash表的大小的掩码,大小总是等于 size-1,用于计算索引值
unsigned long size; // table中已有节点的数量
} dictht;
typedef struct dictEntry {
// key
void *key;
// value,可以是指针*val、整数u64和整数s64的其中之一
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; // 指向下一个Hash节点,形成链表,解决Hash冲突
} dictEntry;
数据保存格式
12.2 Redis字典
Redis的字典由dict.h/dict 结构:
typedef struct dict {
// 类型特定函数,指向一簇用于操作特定类型key-value的函数,主要函数有hash值计算(HashFunction)、复制key(keyDup)、复制value(valDup)、对比key大小(keyCompare)、删除key(keyDestructor)和删除value(valDestructor)的函数等。
dictType *type;
// 私有数据,保存了传给dictType的参数
void *private;
// Hash表,字典只使用ht[0],ht[1] 只会在对ht[0]进行rehash时使用
dictht ht[2];
// rehash索引,记录当前rehash的进度,当rehash不在进行中,值为-1
in trehashidx;
} dict;
没有进行rehash的字典数据结构:
Hash值的计算:
hash = dict - > type -> hashFunction(key);
Redis使用MurmurHash2作为其Hash函数。
index的计算:
index = hash & dict - > ht[i].sizemask;
//字典一般只使用ht[0],ht[1] 只会在对ht[0]进行rehash时使用
冲突解决:
Redis使用链表方法来解决hash值的冲突,很类似JDK1.7的HashMap实现。
12.3 Redis的ReHash
针对Hash表保存的key-value的增加和减少,Redis提供了对hash表进行扩展或者收缩的功能,以让hash表的负载因子维持在一个合理的范围之内。
具体步骤如下:
1.为ht[1] Hash表分配空间:
如果是扩展(负载因子大于1或者负载因子大于5(如果正在执行BGSAVE或者BGREWRIGEAOF时)),ht[1]的大小为第一个大于等于ht[0].used * 2的;
如果是收缩(负载因子小于0.1时),ht[1]的大小为第一个大于等于ht[0].used 的 2的.
2.将ht[0]中的所有key-value 重新散列到ht[1] 上,需要重新计算hash值和索引值,因为ht[1]的长度和ht[0]不一样;
3.迁移完成后,释放ht[0]的空间,交换ht[0]和ht[1],为下一次rehash准备。
负载因子load_facotr = ht[0].used / ht[0].size;
13.Redis Cluster
Redis 3.0之后,Redis推出了Redis集群,解决了单点的问题,每个节点之间是去中心化的,并且提供了sharding(数据分片)、replication(主从复制)、failover(故障转移)等解决方案。Redis Cluster的最小配置节点个数为6个(即3主3从,主节点提供读写,从节点不提供请求服务),最大16384个节点,采用虚拟槽分区的原理,即根据key的Hash值映射到0~16383个整数槽内。另外,Redis集群具有感知主备的能力。
注意Redis Cluster和哨兵的却别,而且并不是功能重叠,也不是相互替换的。