redis那些事儿

Redis作为常用的缓存/全局锁等分布式场景的选型,是有很多的考虑和设计,因此今天花比较大的篇幅来梳理下redis中涉及到的一些概念和知识点。

redis基本概念

redis是基于内存的数据库/缓存/消息代理方案,它是非关系型的、基于key-value的。它的高性能、简单性、原子操作能解决分布式系统中的全局锁等问题。

数据类型

支持5种数据类型:

  1. string(字符串):二进制安全的,可以包含任何数据,包括了jpg图片或者序列化的对象。最大能存储512Mb。
  2. hash(哈希):键值对集合,每个hash可以存储40多亿键值对。
  3. list(列表):字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部或尾部。每个列表可以存储40多亿元素。
  4. set(集合):string类型的无序集合,基于hash实现。每个集合可以存储40多亿元素。
  5. zset(sorted set,有序集合):不允许重复的集合,每个元素都会关联一个double类型的分数,redis通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但是分数(score)是可以重复的。

基本命令

  • SETNX:SETNX KEY VALUE。意义是set if not exists,当key不存在时,设置value,并返回1;否则,不设置value,返回0。
  • GETSET:GETSET KEY VALUE。给key设置value,并返回旧的value。若key不存在,则返回nil,若存在但是不是字符串类型,则返回一个错误。
  • GET:GET KEY。返回key对应的value,key若不存在,则返回nil。
  • DEL:DEL KEY。删除给定的一个或多个key,不存在的key会被忽略。
  • HMSET:HMSET KEY FIELD1 VALUE。意义是hash map set。将field1-value键值对set到key对应的hash map中。
  • HMGET:HMGET KEY FIELD1。将field1的value值从key对应的hash map中取出。
  • LPUSH:LPUSH KEY VALUE。将value放到key对应的队列的左边。(RPUSH则是右边)
  • LRANGE:LRANGE KEY START STOP。按照start、stop位置列出list中的元素。
  • SADD:SADD KEY MEMBER。将member添加到key对应的set里。
  • SMEMBERS:SMEMBERS KEY。列出set中所有的元素。
  • ZADD:ZADD KEY SCORE MEMBER。将member按照score加到key对应的有序集合中。
  • ZRANGEBYSCORE:ZRANGEBYSCORE KEY START STOP。按照start、stop位置列出zset中的元素。
  • MULTI/EXEC:multi开启事务,exec执行事务。两个命令之间的命令会入队列,并按顺序执行。它们要么都成功、要么都失败。
  • INFO MEMORY:查看内存使用情况。会返回包括:used_memory_human,used_memory_peak_human,mem_fragmentation_ratio(内存碎片率)等内存信息。

优势

  • 性能极高:redis读的速度是11w次/s,写的速度是8.1w次/s。
  • 丰富的数据类型:支持5中数据类型。
  • 原子性:支持事务,通过multi/exec指令支持,同时单个操作也具有原子性。
  • 丰富的特性:支持publish/subscribe,通知,key过期等特性。

对比memcached

从几下几点进行比较,总体来说redis有更丰富的数据类型,不需要二次封装,高级的功能如持久化,集群管理更友好等优势。

  1. 网络IO模型:
    • memcached是多线程模型
    • redis是io多路复用模型
  2. 数据支持类型:
    • memcached使用key-value形式存储和访问数据
    • redis支持5种数据类型
  3. 内存管理机制:
    • memcached使用slab allocation,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。
    • redis采用的是包装的mallc/free,相较于Memcached的内存管理方法来说,要简单很多。在Redis中,并不是所有的数据都一直存储在内存中的。这是和Memcached相比一个最大的区别。当物理内存用完时,Redis可以将一些很久没用到的value交换到磁盘。
  4. 数据存储及持久化:
    • memcached不支持持久化,所有的数据都以in-memory形式存储
    • redis支持持久化操作,包括了快照方式和AOF方式。前者将某一时刻的所有数据都写入硬盘;后者会在执行写命令的时候将写命令复制到硬盘。
  5. 数据一致性问题:
    • memcached提供了cas命令,可以保重多个并发访问操作同一份数据的一致性
    • redis则提供了事务的功能,保重一批命令的原子性
  6. 集群管理不同:
    • memcached本身不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcached的分布式存储。
    • redis更偏向于在服务器端构建分布式存储。最新版本的Redis已经支持了分布式存储功能。Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本,它没有中心节点,具有线性可伸缩的功能。

持久化方案

redis共支持两种持久化方式,分别是:

  • RDB持久化:使用fork子进程来将快照数据写入临时文件,写入成功之后,再替换原有文件,用二进制压缩存储。
  • AOF持久化:以日志的方式记录写/删除操作,追加到日志文件中。

优缺点比较

  • RDB:
    • 优点:
      1. 基于文件的备份策略在恢复时可以容易地恢复,对文件可以压缩后复制到合适的存储介质
      2. 性能最大化,只需要fork一个子进程,让子进程执行RDB,对redis对外提供的读写服务影响非常小
      3. 相比于AOF,如果数据较大,RDB的启动效率会更高
    • 缺点:
      1. 当数据较大时,可能会导致整个服务器停止服务
      2. 如果写文件途中出现宕机,则未写入文件丢失,那么数据也会丢失
  • AOF:
    • 优点:
      1. 更高的数据持久性。利用2种同步策略:每秒同步、每修改同步来将数据同步到磁盘中
      2. append模式保证了不会破坏日志文件中已存在的内容,也没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
    • 缺点:
      1. AOF通常大于RDB文件,在恢复大数据集时的速度会比RDB慢
      2. AOF运行效率往往会慢于RDB
      3. AOF开启后支持的写QPS会比RDB的写QPS低
  • 如何选择: 选择牺牲性能换取更高的缓存一致性-AOF,还是希望有更高的性能-RDB。Redis支持两种方式同时开启,用AOF保证数据不丢失,用RDB做冷备,用来快速恢复。

    过期策略

    过期策略主要由”定时删除”和”惰性删除”组成。

  • 定时删除指的是每100ms就会检查键空间中已过期的键,并将其删除。检查是随机的,而不是遍历所有键,从而将对性能的影响降到最低。
  • 惰性删除指的是当一个键被请求的时候,判断其是否已过期,已过期则将其删除。

内存淘汰机制

在redis的配置文件中可以设置maxmemory,一旦内存打到该临界值,则会触发内存淘汰机制。没有配置时,默认为no-eviction,即不删除任何键,但会在写入时报错。其他主要的内存淘汰机制有:

  • allkeys-lru:内存不足时,在所有键中移除最近最少使用的key(这个是最常用的)。
  • allkeys-random:内存不足时,在所有键中随机移除某个key。
  • volatile-lru:内存不足时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:内存不足时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:内存不足时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

锁的实现

主要是利用setNx命令来完成加锁操作,setNx命令是用来设置某个key的值,它只有在该key不存在时才能设置成功,若存在则不会设置。利用这一个原理,可以做到排他性,获取锁的思路就是:

  1. 计算重试终止时间
  2. 生成uuid
  3. 尝试用set命令带上nx参数对lockName设置uuid,并设置超时时间。set命令具有原子性,因此不会有并发问题
  4. 同时只会有一个请求能将lockName设置成功,失败则再次重试,直到到达终止时间

相应地,释放锁的思路是:

  1. 利用del命令删除锁

但是,这种方案中没有考虑死锁的场景。具体场景如下:

  1. A进程持有锁,但A崩溃,导致锁未释放
  2. B进程、C进程同时尝试获取锁,但失败。于是获取锁时间戳,同时判定锁超时。
  3. B、C先后删除、获得锁,导致B、C同时持有锁

在Redis 2.6.12之后,set命令的参数变得更加丰富,因此可以优化上述锁的实现方案。可以直接在set里使用nx参数,同时传入超时时间,那么锁就会在超时后自动释放。而释放时,可以用口令匹配的思路来保证释放的是自己持有的锁。

线程安全

Redis为单进程单线程模式,采用队列的思路将并发访问变为串行访问,利用这些特性,可以避免并发问题。同时,它支持原子性操作,以及通过multi/exce指令支持事务。

IO多路复用

io多路复用(I/O multiplexing)其实是计算机问题中的一个经典问题,它的概念可以参考铁路运输中的”分轨器”–道岔。即利用一个进程–“道岔”,来同时处理不同来向的io请求–“火车”,将请求引导到正确的处理程序–“轨道”上。 它具体指的是在单个进程里,通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流。 目前主流的多路复用实现有select, poll, epoll, kqueue这几种实现方式。而redis默认采用的是epoll方式。 之所以使用IO多路复用是因为它有如下优势:

  • 让单个线程高效的处理多个连接请求,减少多线程切换开销的同时降低了IO时间消耗
  • 主要操作都在内存,操作非常快速,也符合redis内存数据库的设计

调优思路

利用诊断工具查看ceres系统指标,从如下角度分析并调优:

  • 内存:info memory
  • 命令处理数:info stats
  • 延迟时间:redis-cli工具加–latency参数来查看,若对于1G带宽延迟高于200微秒,则可能出现问题。可进一步排查:
    • 查出慢指令:redis-cli下使用slowlog get查看
    • 查看客户端连接数:info clients
    • 限制客户端连接数:可以在配置文件中设置,或在redis-cli中设置最大连接数。连接数超过maxclients后,会返回错误消息。此举可以保证redis的性能
  • 内存碎片率:info memory中指标mem_fragmentation_ratio

参考: