首先要考虑引入缓存的必要性,不建议用Redis缓存的数据:
- 访问频率低。
- 更新频率高。增加了Redis服务压力但是缓存命中率不高
- 对数据一致性要求极高的场景。
key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数)
应用场景:缓存对象、常规计数(increatment命令可原子性增加指定值)、分布式锁(setnx命令)、共享 Session 信息。
value为简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
应用场景:存储有序列表、消息队列(生成者头部插入,消费者使用BRPOP阻塞式读取)
底层原理:Redis 3.2 版本之前是由双向链表或压缩列表实现,之后由由 quicklist 实现。zipList就是每个元素都是变长的数组。quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。这样相对于linkedList减少了指针所占用的内存,并且减少了内存碎片。相对于zipList在一定程度上缓解了连锁更新的问题。
一个key对应的value为一个哈希表
应用场景:缓存哈希表、缓存需要频繁更改属性的对象(key为对象id,哈希表为对象的多个属性值)
底层原理:在 Redis 7.0之前是用压缩列表或哈希表实现的,之后使用listpack数据结构实现的。
一个key对应的value为一个无序、不重复的元素集合
应用场景:防重复点赞(key为帖子id,value为点赞的用户id集合)
底层原理:由哈希表或整数集合实现的。
一个key对应的value为<元素, score>的有序集合,集合中的元素不可重复,每个元素可设置一个score分值,可按score分值进行排序。
应用场景:排行榜(使用increatment命令快速对分值进行加减,通过分值排序获取排行榜)、其他需要根据数值对元素进行排序的场景。
底层原理:在 Redis 7.0之前是由压缩列表或跳表实现的,之后使用listpack数据结构实现的。listpack是ziplist结构的改进版,在存储空间上更加节省,而且比ziplist也更精简。
处理客户端请求命令的是单线程。网络IO在Redis6.0前是单线程,6.0以后多线程
Redis存储的数据结构简单,就是key-value键值对,并且通过哈希表存储,通过key可以快速查询定位到键值对
参考:Redis 多线程网络模型
https://www.cnblogs.com/wzh2010/p/15886804.html
Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
重写机制:Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
AOF 重写机制是在重写时,读取当前Redis中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件,这样就对一条数据只记录最新命令,压缩掉了历史操作。
重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」当子进程完成 AOF 重写工作后,会向主进程发送一条信号。主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
信号函数执行完后,主进程就可以继续像往常一样处理命令了。
因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢。为了解决这个问题,Redis 增加了 RDB 快照,可记录某一个瞬间的内存数据。
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。save 命令在主线程执行,会阻塞主线程; bgsave 命令会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
写时复制技术:执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术。执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件。
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
要想设计一个高可用的 Redis 服务,一定要从 Redis 的多服务节点来考虑,比如 Redis 的主从复制、哨兵模式、切片集群。
主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器。
而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。
一致性问题:如果从服务器还没有来得及执行主服务器同步过来的命令,就响应了客户端的读操作,就会出现一致性问题,数据不一致是难以避免的。
主从服务器第一次同步的时候,就是生成 RDB进行全量复制。第一次同步完成后,主从服务器都会维护着一个长连接进行写操作命令传播。如果遇到网络断开,重连后增量复制。
在使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。为了解决这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。
为了减少误判的情况,哨兵在部署的时候不会只部署一个节点,而是用多个节点部署成哨兵集群(最少需要三台机器来部署哨兵集群),通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
根据key进行hash映射决定存取节点
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
惰性删除:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
定期删除:每隔一段时间从过期字典中抽取出一定数量的 key 进行检查,并删除其中的过期key。
两种策略配合使用,既能避免过期key长期占据内存,又能防止频繁检查删除带来的消耗。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。 内存淘汰策略一共有以下八种:
LRU:最近最少使用。Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
LFU:最近最不常用的。LFU 算法是根据数据访问次数来淘汰数据的,核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了LRU中偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题。
定义:查询数据库中不存在的数据,因此在redis中没有缓存,每次都会直接命中数据库进行查询,这就导致缓存好像不存在一样,称为缓存穿透。
布隆过滤器:用于快速判断一个数据是否不存在或者可能存在,它使用一个很长的初始全为0的bitmap和一系列哈希映射函数,支持数据的插入与查询操作(不支持删除)。插入数据时,将数据通过映射函数计算出的值对应的bitmap中索引位值设置为1,查询数据是否存在时,通过判断各个映射函数计算值对应的bitmap中索引位值是否都为1,若都为1,则数据可能存在,若有不为1的,则所查询数据一定不存在。
参考:布隆(Bloom Filter)过滤器
定义:某个热点数据过期时恰好有大量的请求访问了该热点数据,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
解决方案:
定义:当大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:
因为redis的命令时单线程执行的,并且Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。
另外,目前市面上已经有了不少关于 hotKey 相对完整的应用级解决方案,比如京东零售的 hotkey 缓存组件。它的原理是在 Client端做洞察,然后上报对应 hotkey,Server 端检测到后,将对应 hotkey 下发到对应服务端做本地缓存,并且这个本地缓存在远程对应的 key 更新后,会同步更新,已经是目前较为成熟的 自动探测热key、分布式一致性缓存 的解决方案。
参考: