Redis学习笔记

......

Posted by 呆贝斯 on May 2, 2022

Redis数据库介绍

Redis是一个开源(BSD许可),内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。 它支持字符串、哈希表、列表、集合、有序集合,位图,hyperloglogs等数据类型。 内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能,同时通过Redis Sentinel提供高可用, 通过Redis Cluster提供自动分区。官方给出的数据显示能够达到10w/s的QPS处理,但是在生产环境的实测结果大概读取QPS在7-9w/s, 写入QPS在6-8w/s左右(注:与机器性能也有关)。

安装及配置

官网:https://redis.io/

Redis基本数据类型与常用指令

常用命令

| 命令 | 作用 | |————————–|—————————————-| | keys * | 返回所有键(keys还能用来搜索,比如keys h*:搜索所有以h开头的键) | | dbsize | 返回键数量,如果存在大量键,线上禁止使用此指令 | | exists key | 检查键是否存在,存在返回 1,不存在返回 0 | | del key | 删除键,返回删除键个数,删除不存在键返回 0 | | ttl key | 查看键存活时间,返回键剩余过期时间,不存在返回-1 | | expire key seconds | 设置过期时间(单位:s),成功返回1,失败返回0 | | expireat key timestamp | 设置key在某个时间戳(精确到秒)之后过期 | | pexpire key milliseconds | 设置过期时间(单位:ms),成功返回1,失败返回0 | | persist key | 去掉过期时间 | | monitor | 实时监听并返回Redis服务器接收到的所有请求信息 | | shutdown | 把数据同步保存到磁盘上,并关闭Redis服务 | | info | 查看当前Redis节点信息 |

基本数据类型

| 类型 | 描述 | 特性 | 场景 | |————-|——————————-|——————————————|———————————————–| | string | 二进制安全 | 可以存储任何元素(数字、字符、音视频、图片、对象…..) | 计数器、分布式锁、字符缓存、分布式ID生成、session共享、秒杀token、IP限流等 | | hash | 键值对存储,类似于Map集合 | 适合存储对象,可以将对象属性一个个存储,更新时也可以更新单个属性,操作某一个字段 | 对象缓存、购物车等 | | list | 双向链表 | 增删快 | 栈、队列、有限集合、消息队列、消息推送、阻塞队列等 | | set | 元素不能重复,每次获取无序 | 添加、删除、查找的复杂度都是O(1),提供了求交集、并集、差集的操作 | 抽奖活动、朋友圈点赞、用户(微博好友)关注、相关关注、共同关注、好友推荐(可能认识的人)等 | | sorted set | 有序集合,每个元素有一个对应的分数,不允许元素重复 | 基于分数进行排序,如果分数相等,以key值的 ascii 值进行排序 | 商品评价标签(好评、中评、差评等)、排行榜等 | | bitmaps | Bitmaps是一个字节由 8 个二进制位组成 | 在字符串类型上面定义的位操作 | 在线用户统计、用户访问统计、用户点击统计等 | | hyperloglog | Redis HyperLogLog是用来做基数统计的算法。 | 用于进行基数统计,不是集合,不保存数据,只记录数量而不是具体数据统计独立UV等 | | geospatial | Redis3.2版本新增的数据类型:GEO对地理位置的支持 | 以将用户给定的地理位置信息储存起来, 并对这些信息进行操作 | 地理位置计算 | | stream | Redis5.0之后新增的数据类型 | 支持发布订阅,一对多消费 | 消息队列 |

字符串相关操作
    set -- 设置值
    get -- 获取值
    mset -- 设置多个键值对
    mget -- 获取多个键值对
    append -- 添加字符串
    del -- 删除
    incr/decr -- 增加/减少1
列表相关操作
    lpush/rpush  -- 从左/右添加数据
    lrange -- 获取指定长度信息
    ltrim -- 截取指定长度信息
    llen -- 获取长度
    lpop/rpop -- 移除最左或右数据
    lpushx/rpushx -- key存在才会加入数据,不存在不会做任何事情
集合相关操作
    sadd/srem -- 添加/删除元素
    sismember -- 判断是否为set的一个元素
    smembers -- 返回该集合的所有成员
    sdiff -- 返回一个集合和其他集合的差异
    sinter -- 返回几个集合的交集
    sunion -- 返回几个集合的并集
散列相关操作
    hset/hget -- 设置/获取散列值
    hmset/hmget -- 设置/获取多对散列值
    hsetx -- 如果散列已经存在,则不设置
    hkeys/hvals -- 返回所有Keys/Values
    hlen -- 返回散列包含域(field)的数量
    hdel -- 删除散列指定域(field)
    hexists -- 判断是否存在

共享对象

在RedisObject对象中有一个refcount,refcount记录的是该对象被引用的次数,类型为整型。refcount的作用, 主要在于对象的引用计数和内存回收。当创建新对象时,refcount初始化为1;当有新程序使用该对象时,refcount加1; 当对象不再被一个新程序使用时,refcount减1;当refcount变为0时,对象占用的内存会被释放。 Redis中被多次使用的对象(refcount>1),称为共享对象。Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象, 而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。

共享对象的具体实现:Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡: 共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1); 对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。 虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。 就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是0-9999的整数值;当Redis需要使用值为0-9999的字符串对象时, 可以直接使用这些共享对象。10000这个数字可以通过调整参数Redis_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。 共享对象的引用次数可以通过object refcount命令查看。

redis相关问题及解决办法

缓存一致性

关于Redis与MySQL之间的数据一致性问题其实也考虑过很多方案,比如先删后改,延时双删等等很多方案, 但是在高并发情况下还是会造成数据的不一致性,所以关于DB与缓存之间的强一致性一定要保证的话那么就对于这部分数据不要做缓存, 操作直接走DB,但是如果这个数据比较热点的话那么还是会给DB造成很大的压力,所以在我们的项目中还是采用先删再改+过期的方案来做的, 虽然也会存在数据的不一致,但是勉强也能接受,因为毕竟使用缓存访问快的同时也能减轻DB压力, 而且本身采用缓存就需要接受一定的数据延迟性和短暂的不一致性,我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率, 而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,合适的缓存淘汰策略,更新数据库后及时更新缓存、缓存失败时增加重试机制等。

缓存雪崩

问题描述:指在某一个时间段,缓存集中过期失效或Redis宕机。比如 :双十一零点,抢购,这波商品应该放在缓存区,假设缓存一小时, 到了凌晨一点,商品缓存过期,而对于这批商品的访问,都跑到数据库中,对于数据库,产生压力峰。所有请求都会到达存储层,存储层的调用量增加, 存储层狗带(缓存服务节点的宕机,对数据库服务器造成的压力不可预知)。

解决方案:

  1. redis高可用(多增加redis)。
  2. 数据预热(在正式部署之前,把可能的数据先访问一遍)。
  3. 设置随机失效keys,错开过期时间。
  4. 使用分布式锁或者MQ队列使得请求串行化,从而避免同一时间请求大量落入DB(性能会受到很大的影响)。
  5. 副本key策略,就是对于一个key,在它的基础上在设置一个key,它们的value都是一样的,只不过一个设置过期时间、一个不设置过期时间, 相当于给key做了个副本,只不过在更新缓存的时候,副本key也是要更新的,避免出现数据不一致的现象。

缓存穿透

问题描述:用户想要查询一个数据,发现Redis内存数据库里没有,也就是缓存没有命中,于是向持久层的数据库查询,发现也没有, 于是本次查询失败,当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库,这会给持久层数据库造成很大的压力,出现缓存穿透。

解决方案:布隆过滤器,redis缓存空值。

布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的压力。 但是如果空值能被缓存起来,这就意味着缓存需要更多的空间存储更多的键,即使对空值设置了过期时间, 还是会存在缓存层和存储层的数据会有一段时间的窗口不一致

优点:使用二进制组成的数组,内存占用率小,且插入和查询速度够快

缺点:随着数据增加,二进制数组中值的覆盖率增加,只能判断数据不存在,不能明确判定数据存在,且无法删除数据

缓存击穿

问题描述:是指某一个key 非常热点,在不停的扛着大的并发,大并发集中对这个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存, 直接请求数据库。当某个key过期的瞬间,就会有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据, 并且回写缓存,会导致数据库瞬间压力过大。

解决方案:

  1. 设置热点数据永不过期,如果要设置过期时间,在过期的时候通知后台去更新缓存的过期时间。
  2. 加互斥锁:使用分布式锁,保证每一个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,只需等待(对分布锁要求高)

淘汰策略

maxmemory-policy:参数配置淘汰策略。maxmemory:限制内存大小。

| 策略 | 概述 | |——————|——————————————————————————————–| | volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰,没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。 | | volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。 | | volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | | volatile-lfu | 从已设置过期时间的数据集挑选使用频率最低的数据淘汰 | | allkeys-lru | 从数据集中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合(应用最广泛的策略)。 | | allkeys-lfu | 从数据集中挑选使用频率最低的数据淘汰 | | allkeys-random | 从数据集(server.db[i].dict)中任意选择数据淘汰 | | no-enviction(驱逐) | 禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。 |

  1. 在Redis中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的。
  2. 如果所有数据访问概率大致相等时,可以选择allkeys-random。
  3. 如果研发者需要通过设置不同的ttl来判断数据过期的先后顺序,此时可以选择volatile-ttl策略。
  4. 如果希望一些数据能长期被保存,而一些数据可以被淘汰掉时,选择volatile-lru或volatile-random都是比较不错的。
  5. 由于设置expire会消耗额外的内存,如果计划避免Redis内存在此项上的浪费,可以选用allkeys-lru策略,这样就可以不再设置过期时间,高效利用内存了。

键删除策略

Redis默认采用定期+惰性删除策略。

定时删除

在设置键的过期时间的同时,设置一个定时器,当键过期了,定时器马上把该键删除。 (定时删除对内存来说是友好的,因为它可以及时清理过期键;但对CPU是不友好的,如果过期键太多,删除操作会消耗过多的资源。)

惰性删除

key过期后任然留在内存中不做处理,当有请求操作这个key的时候,会检查这个key是否过期,如果过期则删除,否则返回key对应的数据信息。 (惰性删除对CPU是友好的,因为只有在读取的时候检测到过期了才会将其删除。但对内存是不友好,如果过期键后续不被访问,那么这些过期键将积累在缓存中,对内存消耗是比较大的。)

定期删除

Redis数据库默认每隔100ms就会进行随机抽取一些设置过期时间的key进行检测,过期则删除。 (定期删除是定时删除和惰性删除的一个折中方案。可以根据实际场景自定义这个间隔时间,在CPU资源和内存资源上作出权衡。)

持久化机制

Redis将数据存储在内存的,但是也会有相关的持久化机制将内存持久化备份到磁盘,以便于重启时数据能够重新恢复到内存中, 避免数据丢失的风险。而Redis持久化机制由三种,在4.X版本之前Redis只支持AOF以及RDB两种形式持久化, 但是因为AOF与RDB都存在各自的缺陷,所以在4.x版本之后Redis还提供一种新的持久化机制:混合型持久化(但是最终生成的文件还是.AOF)。

RDB持久化

RDB持久化是把内存中当前进程的数据生成快照(.rdb)文件保存到硬盘的过程,有手动触发和自动触发。

  • 自动触发 save 900 1 – 900s内存在1个写操作 save 300 10 – 300s内存在10个写操作 save 60 10000 – 60s内存在10000个写操作 如上是RDB的自动触发的默认配置,当操作满足如上条件时会被触发。

  • 手动触发 save:阻塞当前 Redis,直到RDB持久化过程完成为止,若内存实例比较大 会造成长时间阻塞,线上环境不建议用它。 bgsave:Redis 进程执行fork操作创建子进程,由子进程完成持久化,阻塞时间很短(微秒级),是save的优化, 在执行Redis-cli shutdown关闭Redis服务时或执行flushall命令时,如果没有开启AOF持久化,自动执行bgsave。 而且RDB 是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,重启时加载这个文件达到数据恢复。

RDB持久化的优缺点:

  • 优点:使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能; 而且RDB文件存储的是压缩的二进制文件,适用于备份、全量复制,可用于灾难备份,同时RDB文件的加载速度远超于AOF文件。
  • 缺点:RDB是间隔一段时间进行持久化,如果持久化之间的时间内发生故障,会出现数据丢失。所以这种方式更适合数据要求不严谨的时候, 因为RDB无法做到实时持久化,而且每次都要创建子进程,频繁创建成本过高;备份时占用内存,因为Redis 在备份时会独立创建一个子进程, 将数据写入到一个临时文件(需要的内存是原本的两倍);还有一点,RDB文件保存的二进制文件存在新老版本不兼容的问题。

AOF持久化

AOF持久化方式能很好的解决RDB持久化方式造成的数据丢失,AOF持久化到硬盘中的并不是内存中的数据快照, 而是和MySQL的binlog日志一样记录写入命令,AOF的持久化策略也有三种:

  • appendfsync always 同步持久化形式,每次发生数据更改都将命令追加到AOF文件,因为每次写入时都记录会产生大量磁盘IO,从而性能会受到影响,但是数据最安全。
  • appendfsync everysec Redis开启AOF后的缺省配置,异步操作,每秒将写入命令追加到AOF文件,如果在刚持久化之后的一秒内宕机,会造成1S的数据丢失。
  • appendfsync no Redis并不直接调用文件同步,而是交给操作系统来处理,操作系统可以根据buffer填充情况/通道空闲时间等择机触发同步。 这是一种普通的文件操作方式。性能较好,在物理服务器故障时,数据丢失量会因OS配置有关。

AOF持久化机制优缺点:

  • 优点:根据不同的fsync策略可以保证数据丢失风险降到最低,数据能够保证是最新的,fsync是后台线程在处理, 所以对于处理客户端请求的线程并不影响。
  • 缺点:文件体积由于保存的是所有命令会比RDB大上很多,而且数据恢复时也需要重新执行指令,在重启时恢复数据的时间往往会慢很多。 虽然fsync并不是共用处理客户端请求线程的资源来处理的,但是这两个线程还是在共享同一台机器的资源,所以在高并发场景下也会一定受到影响。

AOF机制重写:随着Redis在线上运行的时间越来越久,客户端执行的命令越来越多,AOF的文件也会越来越大, 当AOF达到一定程度大小之后再通过AOF文件恢复数据是异常缓慢的,那么对于这种情况Redis在开启AOF持久化机制的时候会存在AOF文件的重写, 缺省配置是当AOF文件比上一次重写时的文件大小增长100%并且文件大小不小于64MB时会对整个AOF文件进行重写从而达到“减肥”的 目的(这里的100%和64MB可以通过auto-aof-rewrite-percentage 100 与 auto-aof-rewrite-min-size 64mb来调整)。 而AOF rewrite操作就是“压缩”AOF文件的过程,当然 Redis 并没有采用“基于原aof文件”来重写的方式,而是采取了类似snapshot的方式: 基于copy-on-write,全量遍历内存中数据,然后逐个序列到aof文件中。因此AOF rewrite能够正确反应当前内存数据的状态, 这正是我们所需要的;*rewrite过程中,对于新的变更操作将仍然被写入到原 AOF文件中,同时这些新的变更操作也会被 Redis 收集 起来(buffer,copy-on-write方式下,最极端的可能是所有的key都在此期间被修改,将会耗费2倍内存),当内存数据被全部写入到新的aof文件之后, 收集的新的变更操作也将会一并追加到新的aof文件中,此后将会重命名新的aof文件为appendonly.aof, 此后所有的操作都将被写入新的aof文件。 如果在rewrite过程中,出现故障,将不会影响原AOF文件的正常工作,只有当rewrite完成之后才会切换文件,因为rewrite过程是比较可靠的, 触发rewrite的时机可以通过配置文件来声明,同时Redis中可以通过bgrewriteaof指令人工干预。

混合型持久化

RDB因为并不是实时的持久化,会出现数据丢失,但是采用AOF形式在重启、灾备、迁移的时候过程异常耗时,也并不理想。 Redis为了解决这个问题,带来了一个新的持久化选项——混合持久化。将RDB文件的内容和增量的AOF日志文件存在一起。 这里的AOF日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量AOF日志,通常这部分AOF日志很小。 Redis重启的时候,可以先加载RDB的内容,然后再重放增量AOF日志,就可以完全替代之前的AOF全量文件重放, 恢复效率因此大幅得到提升(混合型持久化最终生成的文件后缀是.aof,可以通过redis.conf文件中aof-use-rdb-preamble yes配置开启)。

混合型持久化优缺点:

  • 优点:结合了RDB和AOF的优点,使得数据恢复的效率大幅提升。
  • 缺点:兼容性不好,Redis-4.x新增,虽然最终的文件也是.aof格式的文件,但在4.0之前版本都不识别该aof文件, 同时由于前部分是RDB格式,阅读性较差

事务机制

Redis作为数据库当然是支持事务的,只不过Redis的事务机制是弱事务,相对来说比较鸡肋,官方给出如下几个指令来进行Redis的事务控制:

  • MULTI:标记一个事务块的开始
  • DISCARD:取消事务,放弃执行事务块内的所有命令
  • EXEC:执行所有事务块内的命令
  • UNWATCH:取消WATCH命令对所有key的监视
  • WATCH key [key …]:监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断

内存模型及内存划分

Redis的内存模型可以通过客户端连接之后使用内存统计命令 info memory 去查看,如下:

  • used_memory(单位:字节): Redis分配器分配的内存总量,包括使用的虚拟内存
  • used_memory_rss(单位:字节): Redis进程占据操作系统的内存;除了分配器分配的内存之外, used_memory_rss还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存
  • mem_fragmentation_ratio: 内存碎片比率,该值是used_memory_rss / used_memory;一般大于1,且该值越大, 内存碎片比例越大。而小于1,说明Redis使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多,当这种情况出现时, 应该及时排查,如果内存不足应该及时处理,如增加Redis节点、增加Redis服务器的内存、优化应用等; 一般来说,mem_fragmentation_ratio在1.03左右是比较健康的状态(对于jemalloc分配器来说), 由于在实际应用中,Redis的数据量会比较大,此时进程运行占用的内存与Redis数据量和内存碎片相比, 都会小得多,mem_fragmentation_ratio便成了衡量Redis内存碎片率的参数
  • mem_allocator: Redis使用的内存分配器,在编译时指定;可以是libc 、jemalloc或tcmalloc,默认是jemalloc

说明:used_memory是从Redis角度得到的量,used_memory_rss是从操作系统角度得到的量。二者之所以有所不同, 一方面是因为内存碎片和Redis进程运行需要占用内存,使得used_memory_rss可能更大;另一方面虚拟内存的存在,使得used_memory可能更大。

Redis作为内存数据库,在内存中存储的主要是数据,但除了数据之外,redis的其他部分也会占用内存。Redis的内存占用可以划分为以下几个部分:

  • 数据:作为数据库,数据是最主要的部分;这部分占用的内存会统计在used_memory中。
  • 进程本身运行需要的内存:Redis主进程本身运行肯定需要占用内存,如代码、常量池等等,这部分内存大约几兆, 在大多数生产环境中与Redis数据占用的内存相比可以忽略。这部分内存不是由jemalloc分配,因此不会统计在used_memory中。 除了主进程外,Redis创建的子进程运行也会占用内存,如Redis执行AOF、RDB重写时创建的子进程。当然,这部分内存不属于Redis进程, 也不会统计在used_memory和used_memory_rss中。
  • 缓冲内存:缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等;其中,客户端缓冲存储客户端连接的输入输出缓冲。 复制积压缓冲用于部分复制功能;AOF缓冲区用于在进行AOF重写时,保存最近的写入命令。在了解相应功能之前,不需要知道这些缓冲的细节; 这部分内存由jemalloc分配,因此会统计在used_memory中。
  • 内存碎片:内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大, 可能导致Redis释放的空间在物理内存中并没有释放,但Redis又无法有效利用,这就形成了内存碎片。内存碎片不会统 计在used_memory中。 内存碎片的产生与对数据进行的操作、数据的特点等都有关;此外,与使用的内存分配器也有关系:如果内存分配器设计合理, 可以尽可能的减少内存碎片的产生。如果Redis服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:因为重启之后, Redis重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。

虚拟内存

Redis虚拟内存的配置也存在于redis.conf文件中,如下:

  • vm-enabled ves:#开启虚拟内存功能
  • vm-swap-file ../redis.swap:#交换出来value保存的文件路径
  • Vm-max-memory 268435456:# Redis使用的最大内存上限(256MB),超过上限后Redis开始交换value到磁盘swap文件中。 建议设置为系统空闲内存的60%-80%
  • vm-page-size 32:#每个 Redis页的大小32个字节
  • vm-pages 134217728:#最多在文件中使用多少个页,交换文件的大小
  • vm-max-threads 8:#用于执行value对象换入换出的工作线程数量,0表示不使用工作线程(详情后面介绍)。

Redis的虚拟内存与操作系统虚拟内存不是一码事,但是思路和目的都是相冋的。就是暂时把不经常访问的数据从內存交换到磁盘中, 从而腾出宝贵的内存空间。对于Redis这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个Redis实例以外。 另外的能够提高数据库容量的办法就是使用虚拟内存技术把那些不经常访问的数据交换到磁盘上。 如果我们存储的数据总是有少部分数据被经常访问,大部分数据很少被访问,对于网站来说确实总是只有少量用户经常活跃。 当少量数据被经常访问时,使用虚拟内存不但能提高单台 Redis数据库服务器的容量, 而且也不会对性能造成太多影响Redis没有使用操作系统提供的虚拟内存机制而是自己在用户态实现了自己的虚拟内存机制。主要的理由有以下两点:

  1. 操作系统的虚拟内存是以4k/页为最小单位进行交换的。而Redis的大多数对象都远小于4k,所以一个操作系统页上可能有多个Redis对象。 另外 Redis的集合对象类型如list,set可能行在于多个操作系统页上。最终可能造成只有10%的key被经常访问, 但是所有操作系统页都会被操作系统认为是活跃的,这样只有内存真正耗尽时操作系统才会进行页的交换
  2. 相比操作系统的交换方式,Redis可以将被交换到磁盘的对象进行压缩,保存到磁盘的对象可以去除指针和对象元数据信息。 一般压缩后的对象会比内存中的对象小10倍。这样Redis的虛拟内存会比操作系统的虚拟内存少做很多I0操作

Redis的虚拟内存在设计上为了保证key的查询速度,只会将value交换到swap文件。如果是由于太多key很小的value造成的内存问题, 那么Redis的虚拟内存并不能解决问题。和操作系统一样 Redis也是按页来交换对象的。Redis规定同一个页只能保存一个对象。 但是一个对象可以保存在多个页中。在Redis使用的内存没超过vm-max-memory之前是不会交换任何value的。当超过最大内存限制后, Redis会选择把较老的对象交换到swap文件中去。如果两个对象一样老会优先交换比较大的对象, 精确的交换计算公式swappability=age*1og(size_Inmemory)。 对于vm-page-size的设置应该根据自己应用将页的大小设置为可以容纳大多数对象的尺寸。太大了会浪费磁盘空间, 太小了会造成交换文件出现过多碎片。对于交换文件中的每个页, Redis会在内存中用一个1bit值来对应记录页的空闲状态。 所以像上面配置中页数量(vm pages134217728)会占用16MB内存用来记录页的空內状态。vm-max-threads表示用做交换任务的工作线程数量。 如果大于0推荐设为服务器的cpu的核心数。如果是0则交换过程在上线程进行。具体工作模式如下:

  • 阻塞模式(vm-max-threads=0): 换出:主线程定期检査发现内存超出最大上限后,会直接以阻塞的方式,将选中的对象保存到swap文件中,并释放对象占用的内存空间, 此过程会一直重复直到下面条件满足。
    • 内存使用降到最大限制以下
    • swap文件满了
    • 几乎全部的对象都被交换到磁盘了

    换入:当有客户端请求已经被换出的value时,主线程会以阳塞的方式从swap文件中加载对应的value对象,加载时此时会阻塞所客户端。 然后处理该客户端的请求

  • 非阻塞模式(vm-max-threads>0): 换出:当主线程检测到使用内存超过最大上限,会将选中要父换的对象信息放到一个队列中父给工作线程后台处理,主线程会继续处理客户端请求。 换入:如果有客户端请求的key已终被换出了,主线程会先阳塞发出命令的客户端,然后将加载对象的信息放到一个队列中,让工作线程去加载。 加载完毕后工作线程通知主线程。主线程再执行客户端的命令。这种方式只阻塞请求的value是已经被换出key的客户端总的来说阻塞方式的性 能会好些,因为不需要线程同步、创建线程和恢复被阻塞的客户端等开销。但是也相应的牺牡了响应性。工作线稈方式主线程不会阳塞在磁盘1O上, 所以响应性更好。如果我们的应用不太经常发生换入换出,而且也不太在意有点延迟的话推荐使用阻塞方式(详细介绍参考)。

Redis客户端通信RESP协议

RESP是Redis序列化协议,Redis客户端RESP协议与Redis服务器通信。RESP协议在Redis 1.2中引入, 但在Redis 2.0中成为与Redis服务器通信的标准方式。这个通信方式就是Redis客户端实现的协议。 RESP实际上是一个序列化协议,它支持以下数据类型:简单字符串、错误、整数、大容量字符串和数组。 当我们在客户端中像Redis发送操作命令时,比如:set name 张三 这条命令,不会直接以这种格式的形式发送到Redis Server, 而是经过RESP的序列化之后再发送给Redis执行,而AOF持久化机制持久化之后生成的AOF文件中也并不是存储set name 张三 这个指令, 而是存储RESP序列化之后的指令,RESP的特点如下:

  • 实现简单
  • 能被计算机快速地解析
  • 可读性好能够被人工解析

高可用机制

Redis有提供了主从、哨兵、代理集群与分片集群的高可用机制来保证出现单点问题时能够及时的切换机器以保障整个系统不受到影响, 后面的三种高可用机制都是基于主从的基础上来实现的.

主从机制

虽然我们之前讲到过持久化机制可以保证数据重启情况下也不丢失,但是由于是存在于一台服务器上的,如果机器磁盘坏了、机房爆炸(玩笑~) 等也会导致数据丢失,而主从复制可以将数据同步到多台不同机器,也能够保证在主节点宕机时任然对外提供服务, 还可以做到通过读写分离的形式提升整体缓存业务群吞吐量。一般在线上环境时我们去搭建主从环境时,为了保证数据一致性, 从节点是不允许写的,而是通过复制主节点数据的形式保障数据同步。所以在整个Redis节点群中只能同时运行存在一台主,其他的全为从节点, 示意图如下(读的QPS可以通过对从节点的线性扩容来提升)。

Redis2.8之前使用sync[runId][offset]同步命令,Redis2.8之后使用psync[runId][offset]命令。psync[runid][offset]命令三种返回值:

  • FULLRESYNC:第一次连接,进行全量复制
  • CONTINUE:进行部分复制
  • ERR:不支持psync命令,进行全量复制 两者不同在于,sync命令仅支持全量复制过程,psync支持全量和部分复制。介绍同步之前,先介绍几个概念:
  • runId:每个Redis节点启动都会生成唯一的uuid,每次Redis重启后,runId都会发生变化
  • offset:主节点和从节点都各自维护自己的主从复制偏移量offset,当主节点有写入命令时, offset=offset+命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的offset, 并把自己的offset发送给主节点。这样,主节点同时保存自己的offset和从节点的offset,通过对比offset来判断主从节点数据是否一致
  • repl_back_buffer:复制缓冲区,用来存储增量数据命令
  • 主从数据同步具体过程如下:

当然psync命令除了支持全量复制之外还支持部分复制,因为在做主从数据同步时会导致主从机器网络带宽开销非常大, 而在2.8之前Redis仅支持全量复制,这样非常容易导致Redis在线上出现网络瓶颈,而在2.8之后的增量(部分)复制, 用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当slave再次连上master后, 如果条件允许,master会补发丢失数据给slave。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。 部分复制流程图如下(复制缓存区溢出也会导致全量复制):

主从机制优缺点

  • 优点
    1. 能够为后续的高可用机制打下基础
    2. 在持久化的基础上能够将数据同步到其他机器,在极端情况下做到灾备的效果
    3. 能够通过主写从读的形式实现读写分离提升Redis整体吞吐,并且读的性能可以通过对从节点进行线性扩容无限提升
  • 缺点
    1. 全量数据同步时如果数据量比较大,在之前会导致线上短暂性的卡顿
    2. 一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预
    3. 写入的QPS性能受到主节点限制,虽然主从复制能够通过读写分离来提升整体性能,但是只有从节点能够做到线性扩容升吞吐, 写入的性能还是受到主节点限制
    4. 木桶效应,整个Redis节点群能够存储的数据容量受到所有节点中内存最小的那台限制,比如一主两从架构:master=32GB、 slave1=32GB、slave2=16GB,那么整个Redis节点群能够存储的最大容量为16GB

哨兵

代理式集群

去中心化分布式集群

新版本特性