redis总结与分享

152 阅读16分钟

1. redis 的线程模型

单线程: 编程模型简单、线程安全、redis假事务、io密集型、耗时的命令会阻塞redis的正常使用; redis的很多设计也是基于单线程的,比如内存的懒惰删除机制,比如基于LRU的内存淘汰,会只选择20个过期 的KEY进行删除, 避免单线程的长时间阻塞。

高性能: io多路复用

2. redis 跟 memcached

memcached 的内存管理使用的是chunk,slab的方式。Memcached使用预分配的内存池的方式,使用slab和大小不同的chunk来管理内存,Item根据大小选择合适的chunk存储,内存池的方式可以省去申请/释放内存的开销,并且能减小内存碎片产生,但这种方式也会带来一定程度上的空间浪费。memcached的集群管理方式是依赖 一致性hash环来实现的。

3. redis的内存结构

  • 内存分配使用的是jemalloc 


  • redisObject 

typedef struct redisObject {

  unsigned type:4;
  unsigned encoding:4;
  unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
  int refcount;
  void *ptr;
} robj;

一个redisObject对象的大小为16字节。

refcount: Redis服务器在初始化时,会创建10000个字符串对象,值分别是0~9999的整数值;当Redis需要使用值为0~9999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整参数REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。

  • sds

 

  • ziplist

因为ziplist添加元素的时候有可能在原来的内存空间上扩展、也有可能发生内存的拷贝,所以非常不适合存储大量的元素。



encoding 里面可以识别出content的数据类型 和 数据长度,设计的非常复杂。

1. 字符串的数据结构

int,embstr:<=39字节的字符串(redisObejct和sds一起分配内存,为什么是39,其实是64-16-8-1=39,jemalloc正好可以分配64字节的内存单元),raw

2. 列表

ziplist(列表中元素数量小于512个,不足64字节) 和 linkedlist

3. hash


ziplist(哈希中元素数量小于512个;哈希中所有键值对的键和值字符串长度都小于64字节) 和 hashtable。

渐进式rehash,扩容的条件是元素的个数=桶的的个数,缩绒的条件是元素个数少于通个数的10%,但是如果正在做bgsave, 避免copy on write, 达到5倍才会扩容。

4. set

集合的内部编码可以是整数集合(intset 集合中元素数量小于512个;集合中所有元素都是整数值)或哈希表(hashtable)

5. zset 

ziplist(有序集合中元素数量小于128个;有序集合中所有成员长度都不足64字节) 和 skiplist

6. hyperloglog





7. 根据数据结构作出一些优化:

  • 利用jemalloc特性进行优化,如果key的长度如果是8个字节,则SDS为17字节,jemalloc分配32字节;此时将key长度缩减为7个字节,则SDS为16字节,jemalloc分配16字节;则每个key所占用的空间都可以缩小一半。
  • 如果是整型/长整型,Redis会使用int类型(8字节)存储来代替字符串,可以节省更多空间
  • 利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间
  • 内存碎片:used_memory_rss(操作系统) / used_memory(从redis角度分配的内存) 一般大于1,越大说明内存碎片越严重。<1 说明 使用了swmp交换区内存。

4. redis如何保证数据不丢失?

持久化方式:

AOF

  • 命令追加(append):将Redis的写命令追加到缓冲区aof_buf;
  • 文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步到硬盘;推荐的 刷盘 策略是 everysec 
  • 文件重写(rewrite):定期重写AOF文件,达到压缩的目的。


RDB


RDB和AOF各有优缺点:

RDB持久化

优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。

缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。

AOF持久化

与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。

在主从的架构下可以选择下面的持久化方式:

master:完全关闭持久化(包括RDB和AOF),这样可以让master的性能达到最好

slave:关闭RDB,开启AOF(如果对数据安全要求不高,开启RDB关闭AOF也可以),并定时对持久化文件进行备份(如备份到其他文件夹,并标记好备份的时间);然后关闭AOF的自动重写,然后添加定时任务,在每天Redis闲时(如凌晨12点)调用bgrewriteaof。

fork对性能的影响:

父进程通过fork操作可以创建子进程;子进程创建后,父子进程共享代码段,不共享进程的数据空间,但是子进程会获得父进程的数据空间的副本。在操作系统fork的实际实现中,基本都采用了写时复制技术,即在父/子进程试图修改数据空间之前,父子进程实际上共享数据空间;但是当父/子进程的任何一个试图修改数据空间时,操作系统会为修改的那一部分(内存的一页)制作一个副本。虽然fork时,子进程不会复制父进程的数据空间,但是会复制内存页表(页表相当于内存的索引、目录);父进程的数据空间越大,内存页表越大,fork时复制耗时也会越多。

刷盘也会导致线程阻塞:fsync,主线程每次进行AOF会对比上次fsync成功的时间;如果距上次不到2s,主线程直接返回;如果超过2s,则主线程阻塞直到fsync同步完成。因此,如果系统硬盘负载过大导致fsync速度太慢,会导致Redis主线程的阻塞;此外,使用everysec配置,AOF最多可能丢失2s的数据,而不是1s。

5. redis如何实现高可用的架构?

复制

主从连接--->数据同步--->命令广播 

从节点发送命令psync  进行全量同步:

(1)从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行全量复制;

(2)主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令

(3)主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态

(4)主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态

(5)如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态

部分复制:


核心的概念:复制缓冲区, 主从维护的字节偏移量、runId(重启后会发生变化)

单机内存过大可能造成的影响:

(1)切主:当主节点宕机时,一种常见的容灾策略是将其中一个从节点提升为主节点,并将其他从节点挂载到新的主节点上,此时这些从节点只能进行全量复制;如果Redis单机内存达到10GB,一个从节点的同步时间在几分钟的级别;如果从节点较多,恢复的速度会更慢。如果系统的读负载很高,而这段时间从节点无法提供服务,会对系统造成很大的压力。

(2)从库扩容:如果访问量突然增大,此时希望增加从节点分担读负载,如果数据量过大,从节点同步太慢,难以及时应对访问量的暴增。

(3)缓冲区溢出:(1)和(2)都是从节点可以正常同步的情形(虽然慢),但是如果数据量过大,导致全量复制阶段主节点的复制缓冲区溢出,从而导致复制中断,则主从节点的数据同步会全量复制->复制缓冲区溢出导致复制中断->重连->全量复制->复制缓冲区溢出导致复制中断……的循环。

(4)超时:如果数据量过大,全量复制阶段主节点fork+保存RDB文件耗时过大,从节点长时间接收不到数据触发超时,主从节点的数据同步同样可能陷入全量复制->超时导致复制中断->重连->全量复制->超时导致复制中断……的循环。

哨兵


(1)定时任务:每个哨兵节点维护了3个定时任务。定时任务的功能分别如下:通过向主从节点发送info命令获取最新的主从结构;通过发布订阅功能获取其他哨兵节点的信息;通过向其他节点发送ping命令进行心跳检测,判断是否下线。

(2)主观下线:在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线。顾名思义,主观下线的意思是一个哨兵节点“主观地”判断下线;与主观下线相对应的是客观下线。

(3)客观下线:哨兵节点在对主节点进行主观下线后,会通过sentinel is-master-down-by-addr命令询问其他哨兵节点该主节点的状态;如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。

需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。

(4)选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。

监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法;Raft算法的基本思路是先到先得:即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者。选举的具体过程这里不做详细描述,一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。

(5)故障转移:选举出的领导者哨兵,开始进行故障转移操作,该操作大体可以分为3个步骤:

  • 在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。
  • 更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。
  • 将已经下线的主节点(即6379)设置为新的主节点的从节点,当6379重新上线后,它会成为新的主节点的从节点。

哨兵仍然没有解决写操作无法负载均衡、及存储能力受到单机限制的问题;

codis: 基于代理的方式,codis是一个代理服务器,主要作用是 分片,根据key值将分配到1024个槽位中的某一个。槽 位跟redis 数据节点之间的映射关系 是通过zk来保存的。


缺点就是:不是官方的,更新很慢;多key不在一个实例,无法支持事务;多了代理层、网络的损耗;

redis cluster:  

去中心化的设计,客户端和redis node 存储槽位 跟 redis 实例的映射关系。存在另个非常重要的命令:Moved来纠正槽位、asking 是用来在槽位迁移过程中使用的(不会更新槽位,只是临时的状态)。




为什么是16384 其实就是类似于threadLocal的魔数。

客户端如何感知集群槽位的变化:如果某个Node down掉了,客户端收到connectionError,然后会随机选一个节点,通过move指令来实现。假如每个节点被运维剔除了集群,那么 客户端会收到clusterDown的错误,客户端抛错,等下一次命令出发槽位的更新。

cluster模式中涉及到大量的ping,pong,广播 交互,比如某个节点要加入集群,客户端发起meet指令。

6. 其他:

热点key,新版本特性(LFU)、redLock, stream,管道操作(其实是客户端改变数据包的顺序,达到性能优化的效果,服务端 没有任何的区别) 等等

7. redis的事务

不过事务中的命令和普通命令在执行上还是有一点区别的,其中最重要的两点是:

  1. 非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不一定是同一个;

    而事务状态则是以一个事务为单位,执行事务队列中的所有命令:除非当前事务执行完毕,否则服务器不会中断事务,也不会执行其他客户端的其他命令。

  2. 在非事务状态下,执行命令所得的结果会立即被返回给客户端;

    而事务则是将所有命令的结果集合到回复队列,再作为 EXEC 命令的结果返回给客户端。

探索下redis事务的ACID属性:

Atomicity  :不支持回滚,存在部分成功部分失败的case,所以无法满足;

Consistency:

  • 入队事务队列错误,比如错误的指令,那么 该事务会被标记为不合法的,当执行exec的时候,整个事务都会被丢弃。
  • 执行错误:那么失败的指令,不会影响其他指令的执行;
  • Redis 进程被终结

    如果 Redis 服务器进程在执行事务的过程中被其他进程终结,或者被管理员强制杀死,那么根据 Redis 所使用的持久化模式,可能有以下情况出现:

    • 内存模式:如果 Redis 没有采取任何持久化机制,那么重启之后的数据库总是空白的,所以数据总是一致的。

    • RDB 模式:在执行事务时,Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执行之后,保存 RDB 的工作才有可能开始。所以当 RDB 模式下的 Redis 服务器进程在事务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里。恢复数据库需要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数据库快照(snapshot),所以它的数据可能不是最新的,但只要 RDB 文件本身没有因为其他问题而出错,那么还原后的数据库就是一致的。

    • AOF 模式:因为保存 AOF 文件的工作在后台线程进行,所以即使是在事务执行的中途,保存 AOF 文件的工作也可以继续进行,因此,根据事务语句是否被写入并保存到 AOF 文件,有以下两种情况发生:

      1)如果事务语句未写入到 AOF 文件,或 AOF 未被 SYNC 调用保存到磁盘,那么当进程被杀死之后,Redis 可以根据最近一次成功保存到磁盘的 AOF 文件来还原数据库,只要 AOF 文件本身没有因为其他问题而出错,那么还原后的数据库总是一致的,但其中的数据不一定是最新的。

      2)如果事务的部分语句被写入到 AOF 文件,并且 AOF 文件被成功保存,那么不完整的事务执行信息就会遗留在 AOF 文件里,当重启 Redis 时,程序会检测到 AOF 文件并不完整,Redis 会退出,并报告错误。需要使用 redis-check-aof 工具将部分成功的事务命令移除之后,才能再次启动服务器。还原之后的数据总是一致的,而且数据也是最新的(直到事务执行之前为止)。

  • 隔离性(Isolation)

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。

  • 持久性 无法满足,在aof的模式下异步刷盘无法保证数据实时写入磁盘

ps: 客户端要么处于 事务的连接状态、要么处于 正常的状态。处于事务的状态时候,所有的该客户端的命令会被入队。