Redis总结

1,900 阅读21分钟

Redis优势

  • 速度快:读写性能达到10万/秒
    • 所有数据存放在内存
    • 使用C语言实现,距离操作系统更近
    • 单线程架构,避免多线程产生的竞争问题,以及多线程切换性能问题
  • 支持多种数据结构
    • 支持字符串、哈希、列表、集合、有序集合,并演变出了Bitmaps、HyperLogLog、GEO结构
  • 功能丰富
    • 提供了键过期功能,可以用来实现缓存
    • 提供了发布订阅功能,可以用来实现消息系统
    • 支持Lua脚本功能,可以利用Lua创造出新的Redis命令
    • 提供了简单的事务功能,能在一定程度上保证事务特性
    • 提供了流水线(Pipeline)功能,这样客户端能将一批命令一次性传到Redis,减少了网络的开销。
  • 提供多种客户端语言实现
    • 提供了简单的TCP通信协议,许多编程语言可以便捷接入Redis,如Java、PHP、C、C++、Nodejs等
  • 持久化
    • 将数据放在内存中是不安全的,断电或故障数据可能会丢失。Redis提供了RDB和AOF两种将数据保存到硬盘中的策略
  • 主从复制
    • Redis实现了复制功能,实现了多个相同数据的Redis副本
  • 高可用和分布式
    • 提供了高可用实现RedisSentinel、Redis Cluster

Redis常用数据结构及使用场景

Redis中5种基本数据结构分别是string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合),每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景会选择合适的内部编码,Redis的设计者实现了数据结构与命令的解耦,可以在改进内部编码而对外的数据结构和命令没有影响

String

字符串类型的值实际可以是字符串、Json、数字(整数、浮点数),甚至是二进制编码等,最大值不能超过512MB。

  • 内部编码:

    • int:8个字节的长整型。
    • embstr:小于等于39个字节的字符串。
    • raw:大于39个字节的字符串。

    Redis会根据当前值的类型和长度决定使用哪种内部编码实现

  • 使用场景:

    • String是Redis缓存最常用的key/value数据结构,通常在一些场景中,会将一些热点数据进行缓存,一个请求进来先请求缓存,缓存不存在再查询DB,再写入缓存,返回
    • 计数器,使用INCR命令将Key值对应的value加1,可用于统计视频播放量,系统限流等

List

list是有序的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部或者尾部,也可以修改删除list中的元素,一个列表最多可以包含 2的32次方 - 1 个元素

  • 内部编码:
    • ·ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
    • ·linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现
    • 使用场景:
      • 分页,基于list的有序,最常见的场景是使用lrange 命令实现高性能分页,类似一些网站里展示的不断下拉分页功能
    • 消息队列,因为list是有序的,所以可以使用lpushbrpop命令插入或弹出元素,实现消息队列的功能

Hash

hash 是一个 string 类型的 field(字段,注意不是key) 和 value(值) 的映射表,hash 特别适合用于存储对象。最多可以包含 2的32次方 - 1 个元素

  • 内部编码:
    • ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
    • hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1
  • 使用场景:目前想到的是,对象需要序列化并持久化到数据库是,可以考虑使用Redis Hash的结构替换

Set

set可以用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的

  • 内部编码:
    • intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
    • hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
  • 使用场景:可以基于多个 set集合,进行一些交集、并集、差集的数据统计

Sorted Set

Sorted Set是一种有序集合结构,同样不允许重复数据,元素可以排序,但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据

  • 内部编码:
    • ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplistentries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
    • skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。
  • 使用场景:
    • 排行榜

Redis缓存雪崩、穿透、击穿、并发竞争

Redis缓存雪崩

  • 场景描述 : 假设某系统每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机,缓存挂了,此时 5000 个请求全部落数据库。缓存在某一瞬间全部失效,大量请求全部打到数据库上,这就是缓存雪崩
  • 解决方案:
    • Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃
    • 系统限流
    • 对于设定了同一过期时间的缓存,可以在过期时间中加入随机数,避免缓存同时失效造成数据库压力过大

Redis穿透

  • 场景描述 :
    正常的使用缓存流程是对数据查询先进行缓存查询,如果 key 不存在或者key 过期,再对数据库进行查询,并把查询到的对象,放进缓存,如果数据库查询对象为空,则不放进缓存。大批量恶意查询一个数据库中一定不存在的数据,压力就会穿透缓存,打到数据库上。
  • 解决方案:
    • 在接口层增加校验。不合法的参数直接返回
    • 缓存空对象。缓存查不到,DB 中也没有的情况,可以将对应的 key 的 value 写为 null 。此方案使适用于数据频繁变化实时性高的场景,同时会导致一定时间内数据的一致性问题,可能存储层有但缓存中却为null
    • 使用高级用户布隆过滤器(Bloom Filter)。将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力,Bloom Filter能够利用高效的数据结构和算法快速判断出你这个 Key 是否在 DB 中存在,不存在则返回,存在则查询DB刷新KV再返回,此方案适用于数据固定实时性相对低的场景

Redis击穿

  • 场景描述 :
    热点数据缓存在某个时间点过期,恰好在这个时间点有大量并发请求这个key,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,大量请求会造成数据库压力剧增。
  • 解决方案:
    • 互斥锁:可使用Redis的SETNX去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法

Redis并发竞争

  • 场景描述 : 多系统实例短时间内需要按顺序操作Redis中同一个key,可能因为网络抖动等原因导致Redis写数据顺序错误
  • 解决方案:
    • 分布式锁:在业务层进行控制,每个系统在操作Redis前获取分布式锁,拿到锁才能操作,确保同一时间,只有一个系统在操作某个key
    • 时间戳:在写入时保存一个时间戳,写入前先比较自己的时间戳是不是早于现有记录的时间戳,如果早于,就不写入
    • 消息队列:使用消息队列进行串行处理

Redis 持久化策略之RDB与AOF

持久化主要用做灾难恢复、数据恢复。Redis支持RDB和AOF两种持久化机制,持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复

RDB

RDB持久化是把当前进程数据生成快照保存到硬盘的过程,通常通过bgsave命令对Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束

  • RDB的优点:
    • RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景
    • ·Redis加载RDB恢复数据远远快于AOF的方式
    • RDB 对 Redis 对外提供的读写服务,影响非常小,可以让 Redis 保持高性能,因为 Redis 主进程只需要 fork 一个子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化即可。
  • RDB的缺点:
    • RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高
    • RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题

AOF

以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。AOF的工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)

  • AOF 优点
    • AOF 可以更好的保护数据不丢失,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次 fsync 操作,最多丢失 1 秒钟的数据。
    • AOF 日志文件以 append-only 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
    • AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在 rewrite log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。
    • AOF 日志文件的命令通过可读较强的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用 flushall 命令清空了所有数据,只要这个时候后台 rewrite 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将该 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据。
  • AOF缺点
    • 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。
    • AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低,因为 AOF 一般会配置成每秒 fsync 一次日志文件,当然,每秒一次 fsync ,性能也还是很高的。(如果实时写入,那么 QPS 会大降,Redis 性能会大大降低)
    • 以前 AOF 发生过 bug,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令日志 merge 回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug。不过 AOF 就是为了避免 rewrite 过程导致的 bug,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。

Redis 数据过期策略

Redis 过期策略是:定期删除+惰性删除

  • 定期删除,指的是 Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
  • 惰性删除,在获取某个 key 的时候,Redis 会检查这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西

当定期删除漏掉了很多过期 key,然后你也没及时去查,也也未发生惰性删除时,大量过期 key 堆积在内存里,导致 Redis 内存块耗尽,此时会触发内存淘汰机制

  • 内存淘汰机制
    • noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
    • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
    • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
    • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
    • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
    • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

对RDB的影响

持久化到RDB:在生成RDB文件是会先检查key是否过期,如过期则不会进入RDB

从RDB文件恢复:先对key先进行过期检查,如果过期,不导入数据库(主库情况)。

对AOF的影响

持久化数据到AOF:当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令) 当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉) AOF重写:重写时,会先判断key是否过期,已过期的key不会重写到aof文件

Redis高可用方案

Redis 主从

Redis主从的结构中,主节点可以进行读、写操作,而从节点只能进行读操作。由于主节点可以写,数据会发生变化,当主节点的数据发生变化时,会将变化的数据同步给从节点,这样从节点的数据就可以和主节点的数据保持一致了(基于Redis提供的replication功能)。一个主节点可以有多个从节点,但是一个从节点会只会有一个主节点。

Redis主从会先尝试进行增量同步,如不成功,会进行全量同步

系统运行时,如果master挂掉了,可以在一个从库(如slave1)上手动执行命令slaveof no one,将slave1变成新的master;在其他从节点分别执行slaveof IP:PORT 将这机器的主节点指向的这个新的master;同时,挂掉的原master启动后作为新的slave也指向新的master上。

  • 优点
    • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
    • 为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务依然必须由Master来完成;
    • Slave同样可以接受其他Slaves的连接和同步请求,这样可以有效地分载Master的同步压力;
    • Master是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求;
    • Slave同样是以阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据。
  • 缺点
    • Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复;
    • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性;
    • 如果多个Slave断线了,需要重启的时候,尽量不要在同一时间段进行重启。因为只要Slave启动,就会发送sync请求和主机全量同步,当多个Slave重启的时候,可能会导致Master IO剧增从而宕机。
    • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;
    • Redis的主节点和从节点中的数据是一样的,降低的内存的可用性

Redis Sentinel(哨兵)

主从模式下,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这种方式并不推荐,实际生产中,我们优先考虑哨兵模式。这种模式下,master宕机,哨兵会自动选举master并将其他的slave指向新的master

通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器;当哨兵监测到master宕机,会自动将slave切换到master,然后通过发布订阅模式通过其他的从服务器,修改配置文件,让它们切换主机;然而一个哨兵进程对Redis服务器进行监控,也可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

  • 优点
    • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
    • 主从可以自动切换,系统更健壮,可用性更高。
  • 缺点
    • 具有主从模式的缺点,每台机器上的数据是一样的,内存的可用性较低。
    • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

Redis Cluster

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,对数据进行分片,也就是说每台 Redis 节点上存储不同的内容,对客户端来说,整个cluster被看做是一个整体,客户端可以连接任意一个node进行操作,就像操作单一Redis实例一样,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node。集群部署至少要 3 台以上的master节点,最好使用 3 主 3 从六个节点的模式

redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node<->slot<->value。每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

当数据写入到对应的master节点后,这个数据会同步给这个master对应的所有slave节点。为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点。当其它主节点ping主节点master 1时,如果半数以上的主节点与master 1通信超时,那么认为master 1宕机了,就会启用master 1的从节点slave 1,将slave 1变成主节点继续提供服务。如果master 1和它的从节点slave 1都宕机了,整个集群就会进入fail状态,因为集群的slot映射不完整。如果集群超过半数以上的master挂掉,无论是否有slave,集群都会进入fail状态。

redis-cluster采用去中心化的思想,没有中心节点的说法,客户端与Redis节点直连,不需要中间代理层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

  • 优点
    • 采用去中心化思想,数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布;
    • 可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除;
    • 高可用性:部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升;
    • 降低运维成本,提高系统的扩展性和可用性。
  • 缺点
    • Redis Cluster是无中心节点的集群架构,依靠Goss协议(谣言传播)协同自动化修复集群的状态 但 GosSIp有消息延时和消息冗余的问题,在集群节点数量过多的时候,节点之间需要不断进行 PING/PANG通讯,不必须要的流量占用了大量的网络资源。虽然Reds4.0对此进行了优化,但这个问题仍然存在。
    • 数据迁移问题 Redis Cluster可以进行节点的动态扩容缩容,这一过程,在目前实现中,还处于半自动状态,需要人工介入。在扩缩容的时候,需要进行数据迁移。 而 Redis为了保证迁移的一致性,迁移所有操作都是同步操作,执行迁移时,两端的 Redis均会进入时长不等的阻塞状态,对于小Key,该时间可以忽略不计,但如果一旦Key的内存使用过大,严重的时候会接触发集群内的故障转移,造成不必要的切换。