redis系列(七) |redis使用过程中经常出现的一些问题

1.持久化相关

 1) 问题:RDB文件损坏
    解决办法:可以使用redis提供的redis-check-rdb来检测RDB文件并生成错误报告。
    
 2) 问题:当子节点向主节点进行全量复制的时候,如果生成的RDB文件超过6GB的时候,传输文件这一步非常的耗时,速度取决于主从节点的网络带宽,通过细致分析日志,从打印FULL resync 和MASTER <->SLAVE这两行时间差,可以算出来RDB文件从创建到传输完毕消耗的总时间,如果总时间超过了repl_timeout所配置的时间(默认60s),从节点将放弃接收RDB文件并清理已经下载的临时文件,导致全量复制失败
    解决办法:可以适当调大repl_timeout参数
    
 3)当子节点进行主子复制的过程中,如果主节点生成RDB文件和传输RDB的过程比较慢,在高并发的情况下,主节点接收写命令给放入复制挤压缓冲区中,容易导致复制积压缓冲区溢出
    解决办法:client-output-buffer-limit-slave 256MB 64MB 60(在60秒内缓冲区消耗持续大雨64MB,或者直接超过了256MB,则同步失败) 调大,避免全量复制的过程中造成复制缓冲区溢出
    
 4)数据延迟问题,因为主从复制是异步的,就会有那种在主节点插入数据,不能在从节点立马看到
    解决办法:可以在启动一个监控moniter服务,专门用于监控info replication中的master_repl_offset和slave_repl_offset,如果过大,就报警,成本较高

 5)主从配置不一致问题
  比如maxmemory参数 主节点的是4G 从节点的只有3G,从节点满了之后,开始淘汰策略,但是这时候主节点也不知道,正常发数据,可以这时候并不能查询的到某些数据
    解决办法:需要保证主从配置一致

 6)全量复制的问题 
   全量复制很耗时间,应该尽量避免
    解决办法:1)比如因为主节点挂掉了之后,又重新重启这个时候 主节点的id变了,那就需要全量复制了,应该避免这种问题,应该提供故障转移功能,当主节点发生故障后,手动提升从节点为主节点或者采用支持自动故障转移的哨兵或者集群。  
    2)由于复制积压缓冲区不足,导致从节点要的offset在复制积压缓冲区没有,导致的全量复制,可以增大复制缓冲区的大小

 7)复制风暴的问题
  一主多从的时候,如果从节点同时发起全量复制,那么会导致主节点需要发送多次RDB文件给多个从节点,造成主节点网络带宽耗尽
  解决办法:采用树形结构的拓扑结构,不让多个从连同一个主。可以避免一下复制风暴
          避免将多个主节点部署到同一个机器上,避免他们互相抢资源

2.客户端操作问题

  1)连接拒绝问题
      redis通过maxclients参数控制客户端的最大连接数,默认10000,当redis连接数大于maxclients时会拒绝新的连接进入,info stats的reject_connections统计记录所有被拒绝连接的数量
      解决办法:可以看是否需要增大maxclients的大小

  2)连接溢出问题
       1.操作系统一般会对进程使用的资源做限制,其中一项就是进程可打开的文件最大数量,通过ulimit -n查看,通常默认1024,tcp连接也算是文件句柄,因此对于支撑大量连接的redis,需要增大这个值,防止too many open files
       2.系统对特定端口的TCP连接使用backlog队列来保存,redis默认的长度为511,通过tcp-backlog参数设置,如果redis用于高并发场景为了防止缓慢连接占用,可以适当的调整这个设置,但必须大于操作系统的允许值才可以启动。默认时128,可以使用echo 511>/proc/sys/net/core/somaxconn命令进行修改,也可以通过netstat -s命令获取因backlog队列溢出造成的连接拒绝的统计

  3)网络延迟
    取决于网络环境,常见的物理拓扑按网络延迟由快到慢可分为:同物理机>同机架>跨机架>同机房>同城机房>异地机房,越往后容灾越好,但是网络越差。
  可以通过redis-cli --latency 测试网络情况 。  --latency-history  --latency-dist

3.内存消耗问题

1.内存消耗
   内存消耗分为两块,进程自身消耗和子进程消耗。可以通过info memory命令获取内存相关指标
   重点关注两个used_memory_rss(操作系统角度看redis占用内存) 和used_memory(实际数据占用内存大小) 以及他们的比值mem_fragmentation_radio,当mem_fragmentation_radio>1时,说明used_memory_rss 比used_memory多出来的内存并没有用于存储数据,而是内存碎片造成。
   内存划分:自身内存(redis进程自己占用)+对象内存+缓冲内存+内存碎片
     1)自身内存(redis进程自己占用)比较少
     2)数据占用 (最大一块内存),是sizeof(keys)+sizeof(values),避免使用超长的键,因为键也占用比较大内存
     3)缓冲内存(客户端缓冲区(所有接入redis的连接的输入输出缓冲区),复制积压缓冲区(主从复制存储主节点复制过程中的命令),AOF缓冲区(存储AOF命令,以及重写的时候存储的重写缓冲区))
     4)内存碎片,redis内存采用的内存分配器是jemalloc,分配策略是采用固定范围的内存快进行分配。比如分配5kb的对象,可能就给分了一块8kb的区域,而剩下的3kb就变为了内存碎片不能再分配给别的对象存储
        1)频繁做更新操作
        2)大量删除过期键,释放的空间无法充分利用,导致碎片率上升
   解决办法:1)数据对齐,在条件允许的情况下做数据对齐 ,使用固定大小的字符串或者整形
           2)安全重启
     5)子进程内存消耗
        fork子进程主要是指执行AOF重写和RDB的时候,创建子进程的消耗。但Linux具有写时复制技术,父子进程会共享相同的物理内存页。当父进程处理些请求会对需要修改的页复制一份完成写操作,而子进程依然读取fork时整个父进程的内存快照。

2.内存管理
    1)控制内存上限,使用maxmemory当超过上限就使用淘汰策略将数据删除。
        可以动态调整内存上限,config set maxmemory 进行动态修改。(建议所有的redis服务都设置maxmemory,因为redis如果不设置maxmemory的话,会默认无限使用服务器的内存,直到系统内存耗尽)
    2)内存回收策略
        1.删除过期键对象 所有的键都可以设置expire过去时间,redis会采用两种删除策略,惰性删除和定时任务删除
          1)惰性删除,就是当客户端查询某个key时,检查它已经过期,就删除,这种策略时出于节省CPU成本,不需要单独维护TTL链表来处理过期键的删除,但是当键一直不访问的话,键一直存在,会导致内存不能被释放
          2)定时删除,redis内存维护了一个定时任务,默认每秒执行10次,定时任务采用了自适应算法,根据键过期比例,使用快慢两种速率模式回收键。
    2)内存溢出控制策略 config set maxmemory-policy {policy}
      1)noeviction默认策略,不删除任何数据,拒绝所有的写入操作
      2)volatile_lru  再设置过期时间的key里面,采用最近最少使用算法删除
      3)allkeys_lru  再所有key中,采用lru删除
      4)volatile_random 再设置过期时间的key中,随机删除
      5)allkeys_lru  再所有key里面随机删除
      6)volatile_ttl,根据键值对象的ttl属性,删除将要过期的key

3.内存优化
   1)redisObject对象,redis存储的所有值再内部定义为redisObject结构体。
    type字段,存储的是对象类型,如String Hash List Set Zset
    encoding字段,内存实际编码类型
        对象类型 内存实际编码类型
        String int embstr raw
        Hash ziplist hashtable
        List ziplist linkedlist quicklist
        Set intset hashtable
        Zset ziplist skiplist
    lru字段,记录对象的最后一次访问的时间,当设置淘汰策略maxmemory-policy=volatile-lru时,用于复制lru算法删除键数据,可以使用object idletime {key}再不更新lru字段的前提下,查看当前键的空闲时间。
    refcount字段,用于记录当前对象的引用的次数用于通过引用次数回收内存空间
    *ptr字段,与对象的数据内容相关,如果是整数,直接存储数据,否则表示指向数据的指针
 
 2)缩减键值对象
    键的话,在能表达业务含义的情况下,越短越好。
       比如 user:{uid} friends:{fid}  可以写成 u:{uid} fs:{fid}
     值的话,优化策略,比如是对象的话,不要使用字符串一个一个的字段存,可以使用二进制存储,当然也可以使用hash进行存储

 3)共享对象池
     是指redis内部维护的[0-9999]的整数对象池,创建大量的整数类型redisObject存在内存开销,每个redisObject内存结构至少占16字节,甚至超过了数据本身空间消耗,所以redis内存维护了一个整数对象池,所以在平常开发中可以多使用整数的值。但是当使用volatile-lru allkeys-lru时,redis禁止使用共享对象池,因为共享对象池中的对象被多个引用共享,lru字段被共享,无法获取每个对象的最后访问时间,导致无法获取每个对象的最后访问时间,那也就不能删除数据了。

 4)字符串优化
     字符串结构 。 int len 已用字节长度     int free  未用字节长度  char buf[] 字节数组
     所以通过上面的结构可以看出,字符串的几个操作的算法复杂度,获取字符串长度,已用长度,未用长度是O(1)
     
   字符串采用预分配策略,可能会在开发中造成内存浪费
   比如 set key value   value占用60字节 ,所以上面的结构就是 len=60  free=0 
   这时候往里面append key value1 value1占用60字节,上面的结构变成 len=120 free=120 。 因为预分配,防止下次再进行append需要做扩容操作,提前分配了内存,但是如果这部分不用的话,那么内存一直是浪费的,可以直接使用 set key value+value1  len=120 free=0

5)编码优化
  ziplist的编码格式

  ziplist的分析:原则,追求空间和时间的平衡
  1)zlbytes:记录整个压缩列表所占字节长度,方便重新调整ziplist空间,类型为int-32,长度为4字节
  2)zltail:记录距离尾节点的偏移量,方便尾节点做弹出操作,类型是int-32,长度为4字节
  3)zllen:记录压缩链表节点数量
  4)entry,记录具体的节点
     4.1)prev_entry_bytes_length记录前一个节点所占空间,方便向后遍历
     4.2)encoidng 标示当前节点的编码和长度,前两位表示编码 字符串/整数,其余位表示数据长度
     4.3)contents 表示保存节点的值
  5)zlend 。 记录列表结尾,占用一个字节
 可以根据结构分析出:内部表现是数据紧凑的一块连续内存数组,可以模拟双向链表结构,新增删除设计内存的重新分配,加大操作复杂度,读写操作设计复杂的指针移动,最坏时间复杂度位O(n2)

  1)支持ziplist的内存编码结构的对象类型,可以在数据适当的情况下适度的调整比如list-max-ziplist-entries,以便节省内存,因为ziplist是采用数组密集排布,不需要向linkedlist一样需要维护前后指针(建议,针对性能较高的场景使用ziplist,建议长度不要超过1000,每个元素的大小控制在512字节以内)

  intset编码格式
    intset编码是集合类型编码的一种,内部表现为存储有序,不重复的整数集,当集合只包含整数且长度不超过set-max-inset-entries配置时被启用。

   1)encoding整数表示类型,根据数据集合内最长整数值确定类型,整数类型划分为三种,int-16,int-32 int-64
   2)length表示集合元素个数
   3)contents 整数数组,按从小到大顺序保存

当保存的整数超出当前类型时,将会触发自动升级操作且升级后不再做回退,升级操作将会导致重新申请内存空间,把原有数据按转换类型后拷贝到新数组。尽量使用intset的时候,保持数据范围,如都在int-16范围内,防止个别大整数触发集合升级操作,造成内存浪费。

4.集群相关

1.集群功能限制
   1)因为数据分布在多个槽上,槽在不同的节点上,所以key的批量操作会有问题,只能批量操作同一个节点上的数据
   2)事务也一样,只支持同一个节点的事务
   3)key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash list等映射到不同的节点
   4)不支持多数据库空间,只有一个库0
   5)复制结构只支持一层,从节点复制主节点,不支持嵌套树状复制结构

你可能感兴趣的:(nosql,java,数据库)