Redis个人总结

1、Redis简介

Redis 是一个高性能的key-value数据库。

Redis特点

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。

Redis 优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

2、Redis数据结构

字符串string

Redis中的字符串使用SDS来实现的,它的结构基本和C中的字符串结构类似,只是增加了free和len两个int值来保存char数组中的未使用空间和字符串长度。
优点:获取字符串长度的时间复杂度从O(n)降到O(1);避免对字符串的修改时,进行频繁的内存重分配;兼容部分C字符串的函数。
缺点:占用较多的内存空间(SDS提供了释放未使用空间的方法)。
对SDS进行字符串增长操作时,会使用空间预分配的策略,分配一块未使用的空间,从而减少连续执行字符串增长操作所需的内存重分配次数。
常用命令:GET、SET、DEL
可以存储的值: 字符串、整数、浮点数

列表list

Redis中使用双向链表quicklist来实现列表数据结构。
同样,quicklist中也增加了len字段来表示链表长度,减少获取长度的时间复杂度。
常用命令:LPUSH、RPUSH、LPOP、RPOP、LINDEX、LRANGE、LLEN、LTRIM
阻塞式命令:BLPOP、BRPOP、RPOPLPUSH、BRPOPLPUSH
对于阻塞弹出和弹出并推入命令,最常见的使用场景就是消息传递和任务队列。
通常情况下,默认会配置使用ziplist(压缩列表)来代替列表,可以大大减少内存使用率。

集合set

Redis的集合用来保存无序的、不重复的元素。使用intset和dict这两种数据结构来实现。
常用命令:SADD、SREM、SISMEMBER、SCARD、SMEMBERS、SMOVE
多集合命令:SDIFF、SINTER、SUNION、SDIFFSTORE
当set中的元素都是整型且元素数目较少时,set优先使用intset(整数集合)作为底层数据结构,否则使用dict作为底层数据结构(dict的value是NULL)。
时间复杂度:intset是O(log n),dict是O(1)。但是intset能节省内存,且在元素数量较少的情况下,性能差距不大。

散列hash

Redis的散列使用dict(哈希表,具体实现细节后面详述)来实现。
常用命令:HMSET、HMGET、HDEL、HLEN、HEXISTS、HKEYS、HVALS、HGETALL

有序集合zset

有序集合和散列一样,都用于存放键值对:有序集合的键称为member,值称为score。是唯一一个既可以根据member访问元素,有可以根据score访问元素的结构。
Redis的有序集合使用skiplist(跳跃表)来实现。
Redis的skiplist和普通skiplist的主要区别是:为每个节点增加一个高度为1的后退指针,用于从表尾方向向表头方向迭代;score值允许重复。
常用命令:ZADD、ZREM、ZCARD、ZCOUNT、ZRANK、ZSCORE、ZRANGE、ZINCRBY

3、扩展数据结构

Redis如何定位到key

Redis相当于一个巨大的hash结构,所以所有key的查找都是通过dict来实现的。

dict结构

和一般实现hash表的结构类似,redis的dict也采用了数组加链表的方式存储hash表数据。ht对象保存了数组的大小size(一定是2的指数),用于将哈希值映射到table位置的sizemask(总是等于size-1),还有已存放的对象个数used。
然而,redis中一个dict存在2个上述这种对象ht[2],用来实现rehash,并在存放了一个rehashidx来表示当前rehash的进度。
一般的,当rehash为-1(即没有在rehash过程中),则只需要在ht[0]中查找数据,此时也只有ht[0]中存在数据,ht[1]为空。
如果查找数据时,发现正在rehash过程中,则先推动一次rehash(redis的rehash过程不是一次性完成的,是分步进行,每步最多执行10个数组的rehash工作),然后再从ht[0]中查找,如果存在,则直接返回。否则,当前还在rehash时,再从ht[1]中继续查找。
同理,插入和删除都会推动一步rehash动作。当rehash结束时,会将ht[1]直接复制给ht[0],然后将ht[1]清空。
优点:这种rehash方式,将rehash带来的庞大计算量分摊到对字典的给个添加、删除和查找操作上,避免了服务在rehash时长时间卡顿。
缺点:占用了一部分的内存空间;在rehash时,进行添加、删除和查找操作会降低部分性能。

压缩列表ziplist

压缩列表是Redis为了节约内存锁产生的。使用约束条件:单节点大小和节点个数。
quicklist存储方式:链表的每个节点,都会带有指向链表前后节点的2个指针,这样大大浪费了内存空间。
快速链表
ziplist存储方式:是由一系列特殊编码的连续内存块组成的。zlbytes表示整个压缩列表的占用字节数;zltail表示压缩列表尾节点到起始节点的偏移量,可以直接定位到尾结点;zllen表示压缩列表的节点数量(只能记录小于65535,如果等于65535,则需要遍历整个列表才能计算出压缩列表的长度);entry表示列表的各个节点;zlend表示压缩列表的末端。
压缩链表
压缩列表节点组成:previous_entry_length表示前一个节点的长度,如果长度能够使用1个字节保存,则就使用一个字节保存,否则使用5个字节保存(第一个字节会被填充为全1);encoding表示当前节点数据的类型和长度,值的最高两位表示字节数组的编码(11表示整数,其余表示字节数组),整数类型固定使用一个字节表示,字节数组类型可分为1、2、5字节表示;content表示节点数据,可以是一个字节数组或者整数。
压缩链表节点
遍历:正向遍历使用压缩列表节点中的encoding字段读取当前节点长度,然后直接操作指针读取下一个节点;反向遍历使用压缩列表节点中的previous_entry_length字段读取前一个节点的长度,然后操作指针读取前一个节点。
连锁更新:由于previous_entry_length表示前一个节点的长度,且该字段为1个或5个字节保存。所以当在链表中(非表尾)添加或者删除一个节点时,都会导致该操作节点的后一个节点的previous_entry_length所占用的字节数发生变化,从而导致链表后面的节点都有可能需要重新分配previous_entry_length属性的空间大小,形成了连锁更新。由于使用压缩列表的约束,所以性能上并不会造成太大的影响。(注:Redis为了性能问题,不会将5字节的空间重新分配为1字节的)

整数集合intset

由于使用dict需要花费较大的内存,Redis使用intset来保存一定数量内的纯整数集合。
intset结构:encoding表示编码方式,即congtents中元素的最大编码方式;length表示元素数量;contents表示元素数组,数组中的数值按值的大小从小到大有序的排列。
查询值使用二分查找实现。
升级:当intset中加入一个新元素,且该新元素比现有所有元素的类型都要长时,intset需要先进行升级,然后才能将新元素添加到intset中。具体步骤如下:

  1. 根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变。
  3. 将新元素添加到底层数组里面。(新元素要么最大,要么最小,所以位置只有头尾两种)
    intset不支持降级。

4、数据安全

持久化

快照持久化:snapshotting。Redis调用fork创建子进程,然后子进程负责将快照数据写入到硬盘中,父进程继续处理请求。特点:从快照恢复速度快,快照大小比较固定,方便不同服务器之前传输;快照写入到硬盘的时间较长,下一次快照写入之前都不能保证这段时间的数据安全性。
AOF持久化:append-only file。选项:always表示每隔写命令都会写入硬盘中,性能很差;everysec表示每秒同步一次AOF文件,性能和no相差无几,最多丢失1秒内的数据;no表示让操作系统来决定何时进行同步,丢失不定数量的数据。特点:写入到硬盘时间短,间隔短,最多只丢失一定数量的数据;AOF文件可能会过大(可以使用子进程来做重写和压缩AOF的工作)。

复制

开启复制:只需要从服务器配置slaveof host port即可。不支持主主复制。
关闭服务:只需要从服务器配置slaveof no one即可。
从服务器连接主服务器时的步骤:
主从复制流程
主从链:多个从服务器连接一个主服务器时,可能会让主服务器生成多个快照文件,导致主服务器性能下降,所以推荐使用主从链来控制连接到主服务器的数量。

事务

Redis事务以特殊命令MULTI开始,之后用户传入多个命令,最后以EXEC命令为结束。Redis在接收到EXEC命令之前不会执行任何实际的操作,所以用户无法根据读取到的数据来做决定。
很多Redis客户端在执行事务时,会将MULTI,用户命令和EXEC命令一起全部发送给Redis,这样子可以提升执行的性能。
WATCH:该命令会对键进行监控,如果在用户执行EXEC之前,有其他客户端对该监视的键进行了写操作,那么用户支持EXEC时将会返回错误。
UNWATCH:在WATCH命令后,取消对该键的监视。
DISCARD:在MULTI和EXEC命令之间,取消WATCH命令。并清空所有事务队列中的命令。

5、集群

分片

为什么要有分片:1、redis存储上限由单台主机的内存大小限制;2、redis性能将由单台主机的cpu限制。分片技术可以通过使用多台主机来存储更大的数据量,可以通过将计算任务分散到不同的主机来提升计算性能。
分片方式
客户端分片:由客户端计算分片确定需要连接哪一个redis实例(Jedis)
代理分片:由代理去计算分片,连接相应的redis实例,将结果返回给客户端(twemproxy)
redis服务器分片:客户端随机访问一个redis实例,由这个redis实例将请求转发给正确的redis实例,或者让客户端重定向到正确的redis实例。(Redis Cluster)
缺点:由于多个键可能被分片到不同的reids实例,无法支持多键操作,如集合交集SINTER;Redis事务中涉及到多个键时,也将不可以使用;在扩缩容时,操作比较复杂。

sentinel

sentinel是一种特殊的redis服务器,它能监控多个master-slave集群,发现master宕机后能进行自动的切换。
一般的,会使用一个sentinel集群来管理redis集群,来增加稳定性。
sentinel管理master-slave信息
sentinel在启动时,会读取配置文件中的master信息,并在主备切换后修改该配置文件; slave节点的信息可以从master节点中获取(INFO命令)。
sentinel管理客户端连接
客户端遍历sentinel节点列表,获取一个可用的sentinel节点;获取该节点上配置的master信息;客户端验证是否是master节点;正常连接到master节点;master发生变化时,sentinel向客户端订阅的频道publish一条消息(发布订阅模式),让客户端重新获取master连接。
sentinel互相发现
每个sentinel都会向master的hello管道中,每秒发送一次自己的配置信息,来宣布它的存在。同样也会订阅hello管道的内容,如果有新的sentinel,则加入到自身维护的master监控列表中。如果新的sentinel的配置版本比自身的高,则使用该配置更新自己的master配置。
sentinel触发failover
一个sentinel节点每隔一段时间都会向master发送心跳PING来确认master是否存活,如果master在一定时间范围内没有回复PONG或者回复了错误的消息,那么这个sentinel就会主观认为这个master不可用了。不过需要注意的是,这个时候sentinel并不会马上进行failover主备切换,这个sentinel还需要参考sentinel集群中其他sentinel的意见(使用Gossip协议来判断),如果超过某个数量的sentinel也主观地认为该master死了,那么这个master就会被客观地认为已经死了(ODOWN),此时将会触发故障恢复流程(具体细节在后面详述)。

RedisCluster

Redis Cluster

Redis Cluster是redis的分布式解决方案,是一个去中心化的多主集群,每一个主都负责一部分数据(称之为slot)。节点之间使用Gossip协议进行通信。
优点

  1. 数据自动分片:每个节点负责一定数量的slot,每个key都会映射到一个slot上。
  2. 提供hash tag功能:将多个不同的key映射到相同的slot上,方便进行多key操作,只要在key中包含"{}",那么在计算hash时,只会计算{}中的字符串。
  3. 自动故障恢复:自动检测失效的节点,并选举该主节点的其中一个从节点作为新的master。还支持手动故障恢复。
  4. 灵活扩缩容:灵活增加删除节点,自动完成slot的迁移。线性扩展到1000节点。

缺点

  1. 由于使用Gossip协议进行通信,那么集群的数据将无法保证强一致性,只能保证最终一致性。
  2. 所以读写分离将无法实现,所有从节点的读都会重定向到key对应的slot主节点上。
  3. 只支持单层复制,不支持主从链。
  4. 不支持节点自动发现,必须手动广播meet消息。
  5. 虽然使用了hash tag对多键操作进行了支持,但是在slot迁移时仍然无法支持多键操作。
  6. PUBLISH命令会向所有节点进行广播,加重了带宽负担。

查找key的流程

  1. 通过hash算法计算出key所在的slot;
  2. 在节点的clusterState中找到该slot被负责的node节点。
  3. 如果不是本节点,则返回MOVED重定向到指定节点。
  4. 在节点上查找该key,若找到,则返回该key的结果。
  5. 如果未找到,则判断该slot是否在MIGRATING,如果是,则ASK重定向到指定节点。
  6. 否则判断该slot是否在IMPORTING,如果是且有ASKING标记,则在该节点查找。
  7. 否则返回未找到。

Jedis连接Redis Cluster
Jedis缓存了对每个master节点的连接池,和每个slot对应的连接池。所以Jedis在执行命令时,会做CRC16算法计算slot,并通过对应的连接池获取连接。直接执行命令,如果该连接异常了,则会从所有连接中随机获取一个,来重新获取连接后,并刷新连接池(默认重试5次)。
如果发生了重定向异常,如果返回的是 moved,则刷新连接池。如果是 ASK,则不刷新连接池,在下次递归中直接使用 ASK 返回的信息进行调用。下次递归时,先执行 asking 命令打开新的客户端连接,如果成功,则执行真正的命令。
和sentinel集群不同的是,Redis Cluster并不会通知master的变化,所以Jedis在连接失败或者被MOVED重定向时,再去更新slot对应的master的连接池信息。
故障恢复
故障发现的流程和sentinel类似,都是故障节点没有在指定时间内回复给主节点PONG消息,或者回复了错误的消息。主节点就会向其他节点广播pfail状态,当半数以上主节点都判断该节点故障时,就会向集群中广播fail消息,开启故障恢复流程。
当故障的是带有slot的主节点时,将会从该主节点的从节点列表中选出一个新的主节点。
选出新的主节点后,会将该从节点取消复制变为主节点,并将故障主节点负责的slot进行删除,将这些slot委派给自己。最后向集群广播PONG消息,通知其他节点当前从节点变为主节点和接管slot的信息。
Redis Cluster的选举过程和sentinel十分相似,只是Redis Cluster中只有master节点才有投票的权利,而且被选举的也只能是故障节点的从节点。

6、其他

键过期

Redis使用EXPIRE命令来给键设置过期时间。
Redis底层会给这个键设置一个超时时间(一个绝对的unix时间戳,修改系统时间将会有影响)。
常用命令:EXPIRE、PERSIST、TTL。
过期删除策略:使用定期删除策略,定时任务每次检查20个key,如果找到已经过期的key,则删除。而且当已经过期的key数量比较多时,则会进入到快速清理模式(定期删除时间间隔变短)。如果在用户查询或者设置值时,这个key若已经过期,则会直接删除该key。
slave不会主动触发键过期,会等待master过期删除的DEL命令,来删除slave中的过期键;在master同步DEL命令之前,查询slave上的过期键将会被告知是不存在的(其实是存在的)。

订阅/发布

频道订阅:SUBSCRIBE、UNSUBSCRIBE
底层实现:保存了一个dict,dict中的键为channel,值为订阅客户端的链表;客户端订阅时,在链表中增加该客户端,取消订阅时,在链表中删除该客户端。
模式订阅:PSUBSCRIBE、PUNSUBSCRIBE
底层实现:带有客户端信息和pattern的链表。
发布:PUBLISH
底层实现:根据channel定位到dict中的客户端链表,循环给这些客户端发送消息。然后再遍历模式订阅链表,查看该channel是否匹配pattern,如果是,则给客户端信息发送消息。

sentinel选举

sentinel选举使用Raft算法来实现,必须先保证该master处于ODOWN状态才会触发故障转移。
Raft算法中有三个角色:Leader、Follower、Candidate。
正常运行状态下没有这些角色,当sentinel集群运行过程中发生故障转移时,才会使用这些角色,并让自己成为Follower。
选举流程:

  1. 如果一个sentinel认为master已经down的情况下(ODOWN),首先会判断自己有没有投过票,如果自己已经投过票给其他sentinel了,那么在2被故障转移超时时间内,自己都会是Follower。
  2. 如果该sentinel没有投过票,则它就成为Candidate。
  • 更新故障转移状态为start
  • 当前epoch加1,进入新的term
  • 向其他节点发送请求投票某节点down的命令,命令中携带自己的epoch。
  • 给自己投一票,即将自己的leader和leader_epoch改成自己和自己的epoch
  1. 其他sentinel收到Candidate的某节点down命令时,如果该sentinel已经投过票,判断epoch是否比当前投票的epoch大,如果是,则投给该Candidate,否则忽略。否则投票给该Candidate。
  2. Candidate会不断的统计自己的票数,直到发现票数超过一半且超过了配置的quorum。如果是,则成为Leader。否则,超时后认为自己选举失败了,重新增加epoch开启新一轮的投票。
  3. 成为Leader后,并不会像Raft那样通知其他sentinel,而是从slave中选出master,master开始正常工作后。
  4. 只要Candidate收到了新的master正常工作时,都会终止故障转移流程。

慢日志

Redis自带的慢日志记录,可以通过slowlog-log-slower-than来设置执行时间超过多少微妙就会被认为是慢日志,slowlog-max-len来设置服务器最多保存多少条慢日志。
常用命令:SLOWLOG GET、SLOWLOG LEN、SLOWLOG RESET

7、应用

分布式锁

Redis可以通过SETNX命令实现分布式锁,SETNX是指当且仅当key不存在时,才会设置值。客户端可以通过SETNX命令返回成功与否来判断是否获取锁成功。
为了避免客户端宕机,导致该锁没有被及时清除而造成死锁,建议给该key设置超时时间。由于SETNX和EXPIRE需要使用事务来保证并发,所以Redis使用了SET命令代替:SET key value [EX seconds] [PX milliseconds] [NX|XX]。

队列

可以使用列表数据结构的BLPOP和BRPOP等命令实现一个消息队列。在可靠性需求不大的情况下,可以用来代替较重的MQ。
优先级队列:BLPOP和BRPOP等命令支持传入多个列表,会依次从列表中取值,知道取到值为止。所以可以使用多个不同优先级的队列,将高优先级的列表放置在命令的左侧即可。

8、缓存问题

缓存一致性

问题:1、数据时效性要求很高;2、需要保证缓存中的数据与数据库保持一致;3、需要保证缓存节点和副本中的数据也保持一致。
分析:主要问题在于,写操作如何去处理redis和数据库之间的一致性,即缓存的更新策略。
解决方案:

  1. Cache Aside:在更新操作时,先写入数据库,然后将缓存中的值失效。这个策略使用最为普遍,但是也存在并发问题(一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据)。但是由于写操作比读操作慢很多,所以这个场景出现的概率很小。
  2. Read/Write Through:让缓存自己代理去更新数据库。当缓存失效时,读请求会导致缓存服务主动加载数据,并将数据缓存和返回给用户。写请求在缓存失效时,不操作缓存,直接修改数据库。在命中缓存时,更新缓存,然后缓存服务自己更新数据库。这种策略还是会存在并发问题,且实现方式比较复杂。
  3. Write Behind Caching:所有操作都只写缓存,不更新数据库,缓存会异步地批量更新数据库。缺点是数据不能保持一致性,可能会存在丢失的情况。优点是性能高。

缓存并发

问题:缓存过期或者正在更新,同时有大量的并发请求该key。
分析:缓存过期时,大量请求落在DB上,可能导致雪崩发生;缓存正在更新,大量请求获取到的结果可能是更新前或者更新后的,导致缓存一致性问题。
解决方案:加入类似锁的机制,在缓存过期或者更新的情况下,先尝试获取锁,当更新或者从数据库获取完成后再释放锁,其他请求需要牺牲一些等待时间,即可从缓存中直接获取数据。

缓存雪崩

问题:高并发时,多个热点缓存同时更新或者过期导致大量请求直接落在DB上,导致DB崩溃。
分析:热点缓存key在同一时间过期。
解决方案:设置过期时间时,最好将热点缓存key在不同时间段过期。

缓存击穿

问题:大量请求去查找一个不存在的key,导致DB雪崩。
分析:处理不存在的key。
解决方案:
布隆过滤器:将所有key的哈希值都存放到足够大的bitmap中,请求key时,先经过布隆过滤器。
设置指定值:为不存在的key设置一个指定的值,然后设置超时时间。

大key优化

问题:一个key可能存放一个超大的字符串;一个key可能存放着很大的集合、散列、列表等。
分析:超大字符串读取和写入都会影响redis的性能;在一个集合、散列、列表中,大量的数据也会导致redis的性能下降。
解决方案:对字符串或者集合、散列、列表进行分片(一般可以采用固定的质数来做hash),将单个数据结构的大小降下来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值