跳至主要內容

Redis开发与运维

小刘Learning大约 14 分钟

全局命令

redis有多种数据结构,redis是一种键值对数据库,对于键来说,有一些通用的命令

  • keys * 查看所有键
  • dbsize获取键的总数

dbsize在计算键的总数时,不会遍历所有的键,而是直接获取redis内置的键总数的变量,它的时间复杂度是O(1)

keys *会遍历所有的键,所以它的时间复杂度是O(n),当redis保存了大量的key的时候,使用该命令会消耗大量时间,线上环境禁用

  • exists key检查键是否存在,存在返回1,不存在返回0
  • del key [key1 key2...]删除key,返回删除的key的数量,如果key不存在,返回0
  • expires key seconds为键设置过期时间
  • ttl key查询键的剩余过期时间,有三种返回类型
    • 大于0:剩余过期时间
    • -1:键没有设置过期时间
    • -2:键不存在
  • type key键的类型,如果键不存在,返回none

数据类型和内部编码

reidis有多种数据类型,每种数据类型,都有自己底层的内部底层编码实现,而且是多种实现,redis会在合适的场景选择合适的编码,可以通过object encoding key查询内部编码,即内部实现方式,数据类型和内部编码的关系就好像接口和实现类

string

字符串是redis中一个基本的数据类型,键都是字符串类型的,字符串类型的值可以是字符串(简单的字符串,复杂的字符串(JSON,XML)),数字(整数,浮点数),甚至是二进制(音视频,图片),但是值最大不能超过512MB

内部编码

字符串类型内部编码有3种,redis会根据当前值的类型和长度决定使用哪种内部编码:

  • int:当前值为整型,长度<=8字节,使用int类型的内部编码
  • embstr:当前值为字符串,长度<=39字节,使用embstr内部编码
  • raw:长度>39字节的字符串

使用场景

缓存,计数,共享session,限制频率(例如一分钟只能获取一次验证码)

hash

hash是非常适合用来存储对象的,hash的一个key可以对应多个(field,value)

获取全部field-value,使用hscan

可以通过hgetall key获取该key对应的所有的field-value,但是,如果hash的元素比较多,会存在阻塞redis的可能,时间复杂度为O(n),如果只获取部分field-value,可以使用hmget,如果一定要获取全部的field-value,可以使用hscan,该命令会渐进式的遍历hash

内部编码

hash的内部编码有两种:

  • ziplist:当field-value元素的个数小于hash-max-ziplist-entries配置(默认512个),且所有值都小于hash-max-ziplist-value配置(默认64字节),redis会使用ziplist作为hash内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,更加节省内存
  • hashtable:当无法满足ziplist条件时,会使用hashtable作为内部实现,因为元素过多或者值占用空间过大,ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1),但是hashtable更占用内存

三种缓存用户信息的方式

  • 原生字符串类型,每个属性一个键(通常不会使用这种方法来存用户的信息)

优点:简单直观,每个属性都支持更新操作

缺点:一个用户的信息占用了太多键,内存占用量大,且用户信息的内聚性比较差


  • 序列化字符串类型,将用户信息序列化之后,使用一个键存储

优点:简化编程,如果合理使用序列化,可以提高内存使用效率

缺点:序列化和反序列化有一定的开销,每次更新用户信息都需要进行反序列化,再进行序列化


  • 使用hash

优点:简单直观,如果合理使用,能够减少内存空间的使用

缺点:要控制hash再ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存

list

list是有序的,list中的元素是可以重复的,它是一种比较灵活的数据结构,可以用来充当栈或队列

内部编码

list的内部编码有两种:

  • ziplist:当field-value元素的个数小于list-max-ziplist-entries配置(默认512个),且所有值都小于list-max-ziplist-value配置(默认64字节),redis会使用ziplist作为hash内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,更加节省内存
  • linkedlist:当无法满足ziplist条件时,会使用linkedlist作为内部实现
  • quicklist:以ziplist为节点的linkedlist,结合了二者的优势

使用场景

  • 消息队列

  • 结合hash实现文章列表

每个用户有自己的文章列表,现在需要分页展示文章列表,可以使用hash+list,因为list不仅是有序的,而且支持按照索引范围获取元素

  1. 每篇文章使用hash结构进行存储,例如每篇文章有title,timestamp,cotent三个属性
  1. 向用户文章列表添加文章,user:{id}:articles作为key

每个用户有一个自己的文章列表

set

set是无序的,元素是不可重复的,可以进行一些集合运算,以及随机获取/取出元素之类的操作

内部编码

  • intset:整数集合,当集合中的元素都是整数且元素的个数小于set-max-intset-entries配置(默认512个),会采用intset作为内部实现,从而减少内存的使用
  • hashtable:当无法满足intset,会采用hashtable作为内部实现

使用场景

共同标签tag,生成随机数抽奖

zset

zset是有序集合,保留了集合的不能有重复元素的特性,同时是可以排序的,可以为每个元素设置score作为排序依据,zset中元素是不能重复的,但是score是可以重复的

内部编码

zset内部编码有两种实现方式:

  • ziplist:当zset元素个数小于zset-max-ziplist-entries配置(默认128个),同时,每个元素的值都小于zset-max-ziplist-value配置(默认64字节)会采用ziplist作为内部实现
  • skiplist:当ziplist条件不满足时,以此作为内部实现,因为此时ziplist读写效率下降

使用场景

理解内存

redis数据存在于内存之中,如何高效的利用redis内存非常重要,想要高效的利用内存,首先需要理解redis内存消耗在哪里,如何管理内存,最后才能考虑如何优化内存

内存消耗

理解redis内存,首先要掌握redis内存消耗在哪些方面,有些内存的消耗是必不可少的,而有些内存消耗是可以通过参数调整和合理使用来规避内存浪费,内存消耗可以分为进程内存消耗和子进程内存消耗

内存使用统计:info memory命令获取内存相关指标

127.0.0.1:6379> info memory
# Memory# Redis 保存数据申请的内存空间
used_memory:848136
used_memory_human:828.26K
# 操作系统分配给 Redis 进程的内存空间
used_memory_rss:2449408
used_memory_rss_human:2.34M
# Redis 进程在运行过程中占用的内存峰值
used_memory_peak:910608
used_memory_peak_human:889.27K
# 使用内存达到峰值内存的百分比, 即(used_memory/ used_memory_peak) *100%
used_memory_peak_perc:93.14%
# Redis为了维护数据集的内部机制所需的内存开销, 包括所有客户端输出缓冲区, 查询缓冲区, AOF重写缓
used_memory_overhead:836278
# Redis服务器启动时消耗的内存
used_memory_startup:786488
# 数据占用的内存大小, 即used_memory - used_memory_overhead
used_memory_dataset:11858
# 数据占用的内存大小的百分比# 100%*(used_memory_dataset/(used_memory-used_memory_startup))
used_memory_dataset_perc:19.24%
# 系统内存
total_system_memory:2095968256
total_system_memory_human:1.95G
# Lua脚本存储占用的内存used_memory_lua:37888
used_memory_lua_human:37.00K
# Redis实例的最大内存配置maxmemory:0
maxmemory_human:0B
# 淘汰策略
maxmemory_policy:noeviction
# 内存的碎片率, used_memory_rss/used_memory
mem_fragmentation_ratio:2.89
# 内存分配器
mem_allocator:jemalloc-4.0.3
# 表示没有活动的defrag任务正在运行, 1表示有活动的defrag任务正在运行(defrag:表示内存碎片整理)
active_defrag_running:0
# 表示redis执行lazy free操作,在等待被实际回收内容的键个数
lazyfree_pending_objects:0

重点关注指标:

  • used_memory_rss:操作系统显示redis进程占用的物理内存总量

  • used_memory:存储所有数据的内存占用量

  • mem_fragmentation_ratio:内存碎片率,used_memory_rss/used_memory,比值在1-1.5比较健康

    • mem_fragmentation_tatio>1:说明多出的部分内存并没有用于数据存储,而是被内存碎片消耗,如果两者差别很大,说明碎片化严重
    • mem_fragmentation_tatio<1:这种情况一般出现在操作系统吧redis内存swap交换到硬盘导致,由于硬盘读写速度远远慢于内存,redis性能会变得很差

进程内存消耗

redis进程内存消耗主要包括:自身内存+对象内存+缓冲内存+内存碎片,redis空进程自身内存消耗非常小,可以忽略不计,主要介绍其他3种内存消耗

  1. 对象内存

对象内存是Redis内存占用最大的一块,存储着用户所有的数据。Redis所有的数据都采用key-value数据类型,每次创建键值对时,至少创建两个类型对象:key对象和value对象。对象内存消耗可以简单理解为sizeof(keys)+sizeof(values)。键对象都是字符串,在使用Redis时很容易忽略键对内存消耗的影响,应当避免使用过长的键

  1. 缓冲内存

缓冲内存主要包括:客户端缓冲区,复制积压缓冲区,AOP缓冲区

  1. 内存碎片

redis默认的内存分配器采用jemalloc,可选的分配器还有:glibc,tcmalloc,有了内存分配器可以更好的管理和重复利用内存,内存碎片就是内存分配器给对象分配内存后,空间太小,不能够分配给其他对象存储的内存空间。

当存储的数据长短差异较大时,以下场景容易出现内存碎片问题:

  • 频繁做更新操作,例如频繁对已存在的键执行append,setrange等更新操作
  • 大量过期key删除,key对象过期删除后,释放的空间无法得到充分利用,导致碎片化率上升

出现高内存碎片常见解决方式

  • 数据对齐:在条件允许情况下尽量做数据对齐,比如数据尽量采用数字类型或固定长度字符串,但是这要视具体业务而定,有些场景无法做到
  • 安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,如Sentinel或Cluster,将碎片率过高的主节点转换为从节点,进行安全重启

哨兵模式

redis主从复制模式存在的问题:

  • 一旦主节点出现故障,需要手动将从节点设置为主节点,同时需要修改应用端的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预
  • 主节点的写能力受到了单机的限制
  • 主节点的存储能力受到了单机的限制

为了解决上面的问题,哨兵模式诞生了

哨兵模式:当主节点出现故障,redis sentinel能自动完成故障发现和故障转移,并通知应用方,哨兵模式基本结构如下

整个故障转移处理有4个步骤:

  1. 主节点出现故障,两个从节点与主节点失去连接,主从复制失败
  2. 每个sentinel节点定期监控,发现主节点发生了故障
  3. 多个sentinel节点对主节点的故障表示赞同,选举出一个sentinel节点作为领导者负责故障转移
  4. sentinel领导者节点执行故障转移:
    1. 将某个从节点设置为主节点
    2. 告诉其他从节点,主节点变更以及为其他从节点配置新的主节点的信息
    3. 通知应用方主节点变更
    4. 等原来的主节点恢复后,将其设置为从节点

redis sentinel原理

redis sentinel实现原理主要包含4个方面:

  • 三个定时任务
  • 主观下线和客观下线
  • sentinel领导者选举
  • 故障转移

3个定时任务

  1. 每隔10s,每个sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构
  1. 每隔2s,每个sentinel节点会向redis数据节点的_sentinel_:hello频道上发送该sentinel节点对于主节点的判断以及当前sentinel节点的信息,同时每个sentinel节点也会订阅该频道,来了解其他sentinel节点以及它们对主节点的判断,该定时任务完成了两个工作:
    1. 发现新的sentinel节点:通过订阅主节点的_sentinel_:hello了解其他sentinel节点信息,如果是新加入的sentinel节点,将该sentinel节点信息保存起来,并与该sentinel节点创建连接
    2. sentienl节点之间交换主节点的状态,作为后i面客观下线以及领导者选举的依据
  1. 每隔1s,每隔sentinel节点会向主节点、从节点、其余sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达

主观下线

当执行第3个定时任务(心跳检测)时,当这些节点超过down-after-milliseconds没有进行回复,sentinel节点就会对该节点做失败判定,这个行为就叫主观下线,主观下线是当前sentinel节点的主观判断,是一家之言,存在误判的可能

客观下线

当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel is-master-down-by-addr命令向其他Sentinel节点询问对主节点的判断,当超过quorum个数(通常过半),Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定,这样客观下线的含义是比较明显了,也就是大部分Sentinel节点都对主节点的下线做了同意的判定,那么这个判定就是客观的,从节点、sentinel节点主观下线后,是没有故障转移操作的

领导者sentinel节点选举

故障转移的工作只需要一个sentinel节点来完成就可以了,不需要那么多sentinel节点,所以多个sentinel节点之间会做一个领导者选举工作,选举出的节点进行故障转移工作,redis使用了raft算法实现领导者选举,领导者选举大致思路如下:

  1. 每个在线的sentinel节点都有资格成为领导者,当他确认主节点主观下线后,会向其他sentinel节点发送sentinel is-master-down-by-addr命令,要求其他节点投票自己为领导者
  2. 收到命令的sentinel节点,如果没有投过票,将投票,否则拒绝
  3. 如果该sentinel节点发现自己的票数已经>=max(quorum,num(sentinels)/2+1),它将称为领导者
  4. 如果此过程没有选举出领导者,将进行下一次选举

故障转移

  1. 在从节点列表中选出一个节点作为新的主节点
    1. 过滤一些节点,包括主观下线,短线,5s内没回复过sentinel节点的ping命令,与主节点失联超过down-after-milliseconds*10s的节点
    2. 选择从节点优先级最高的,如果不存在,则继续找
    3. 选择复制偏移量最大的从节点(复制最完整的),如果不存在则继续找
    4. 选择runid最小的从节点
  1. sentinel领导节点将选出的从节点设置为主节点

  2. sentinel领导节点向剩余从节点发送命令,让他们成为新主节点的从节点

  3. sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点