redis-1.原理

377 阅读30分钟

需求

In-memory data structure store, used as database, cache and message broker

设计

Redis focuses on performance so most of its design decisions prioritize high performance and very low latencies.

二级制安全

redis 是二进制安全的,并不会去破坏你的编码,也不去关心你是什么编码。底层存储的时候,是按照Byte字节存储的。我们前面看到的encoding,只是为了让加减之类的运算方法变得更快一些。(HBASE也是二进制安全的)

在redis进程与外界交互的时候,redis存储的是字节流,而不会转换成字符流,也不会擅自按照某种数据类型存储,这样保证了数据不会被破坏,不会发生数据被截断/溢出等错误。

编码并不会影响数据的存储

因此,在多人使用redis的时候,我们一定要在用户端约定好数据的编码和解码。

redis-cli --raw #以自动编解码模式连接

数据模型

image.png

Redis内部使用一个redisObject对象来标识所有的key和value数据,redisObject最主要的信息如图所示:type代表一个value对象具体是何种数据类型,encoding是不同数据类型在Redis内部的存储方式,比如——type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或是int,如果是int则代表世界Redis内部是按数值类型存储和表示这个字符串。

左边的raw列为对象的编码方式:字符串可以被编码为raw(一般字符串)或Rint(为了节约内存,Redis会将字符串表示的64位有符号整数编码为整数来进行储存);列表可以被编码为ziplist或linkedlist,ziplist是为节约大小较小的列表空间而作的特殊表示;集合可以被编码为intset或者hashtable,intset是只储存数字的小集合的特殊表示;hash表可以编码为zipmap或者hashtable,zipmap是小hash表的特殊表示;有序集合可以被编码为ziplist或者skiplist格式,ziplist用于表示小的有序集合,而skiplist则用于表示任何大小的有序集合。

Command

  • 五种数据结构 string/hash/list/set/sortedset
  • 发布订阅
  • pipeline
  • 事务
  • RedisBloom模块 - 布隆过滤器
  • 过期时间

应用场景

缓存失效

缓存穿透问题

在浏览器访问网站的时候,在请求一条数据的时候,往往会先访问redis中书否存在这个数据,如果存在就直接返回,如果不存在会从数据库查询这个数据是否存在,存在这个数据会返回给浏览器并且存入Redis。下次再有请求这个数据的时候,会直接从redis中查询快速返回,这样既能快速响应浏览器,又可以减轻数据库的压力。

但是如果有很多请求的数据redis中没有,数据库里也没有,甚至有人恶意 高并发请求不存在的数据,每次都使请求命中数据库,每次都和数据库建立socket,可能会造成网站的卡顿,严重的可能让数据库挂掉。

解决穿透: 布隆过滤器

缓存击穿问题

就是在redis中某个数据刚刚过期淘汰的瞬间,用大量的用户一起访问这个数据,产生高并发。这个时候redis返回的是null,相当于在redis缓存上打了一个窟窿,这些请求就都会去访问数据库,这就是缓存击穿。

解决思路: 思考: 瞬间有大量的并发请求命中数据库,命中的原因是redis中没有key,那么怎么阻止请求并发到达数据库?

  1. 首先redis是单进程单实例的,并发请求到达redis的时候会串行化,那么肯定会有第一个请求先到达redis而没有找到缓存数据。
  2. 第一个请求没有拿到数据回到service后,调用redis的 setnx 命令在redis中创建key。setnx 命令只能创建key,如果key不存在,则能创建成功;如果key已经存在,则返回null(分布式锁)。
  3. 获得锁的请求去访问mysql数据库返回从新把数据缓存进redis,没有获得锁的请求死循环尝试在redis中获取数据,中途可以sleep一定的时间(sleep不能太久,服务链会超时),sleep结束后重复在redis中获取缓存数据,尝试获得锁的过程,直到获得数据跳出循环。
  • 有bug ?

可能会产生死锁的问题! 如果去访问数据库的请求挂了,setnx得不到释放,后续请求也无法获取到锁,那些请求就会一直循环sleep。

  • 怎么解决呢?

可以设置setnx的过期时间。

  • 锁设置了过期时间,依然会有bug?

第一个去请求数据库的请求可能没有挂,只是因为在mysql这里出现了拥塞,导致锁过期超时。

这样就会引起连锁反应,第二个请求拿到了锁,同样到达mysql发生拥塞,就会可能导致访问数据库的请求越堆越多。甚至第一个请求从mysql拿到数据后返回给redis,后续请求从redis拿到数据返回,但是之前拿到锁的请求可能还在mysql排队,或者引起一些用户访问丢失。

解决: 可以利用多线程,一个线程去mysql取数据,另外一个线程监控数据是否取回来,并且及时更新锁的过期时间。

缓存雪崩

击穿是某个缓存数据 过期淘汰,并且非常巧的对这个数据有高并发访问, 而雪崩是大量的缓存数据 同时 过期淘汰,比如:某些业务要求缓存数据 凌晨0点数据过期,需要从新加载新的数据到缓存,间接造成大量的访问命中数据库

如何解决?

  • 普通场合: 随机过期时间。但是随机过期时间不适合要求凌晨0数据过期的场合。

  • 缓存12点必须过期:

  1. 强依赖缓存击穿方案。并发时,第一个请求获得锁请求数据库,后续数据获得锁失败 -> 休眠 -> 再次尝试获得数据 -> 获取失败循环 -> 获取成功跳出循环。
  2. 使用0点延迟方案。就是在0点的时候,前置服务里面加一个随机sleep(几十毫秒),保证了请求到达redis时间的差异。
  3. 强依赖缓存击穿方案 和 0点延迟方案一起使用

redis分布式锁

实现: setnx
弊端: 过期时间。
     时间到了,活还没做完,别人又去干了。
     过期时间没到, 自己挂了。
解决弊端: 多线程,延长过期。

实现

epoll

从网络I/O模型上看,Redis使用单线程的I/O复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll、kqueue和select。

对于单纯只有I/O操作来说,单线程可以将速度优势发挥到最大,但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU计算过程中,整个I/O调度都是被阻塞住的,在这些特殊场景的使用中,需要额外的考虑。相较于memcached的预分配内存管理,Redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片。Redis跟据存储命令参数,会把带过期时间的数据单独存放在一起,并把它们称为临时数据,非临时数据是永远不会被剔除的,即便物理内存不够,导致swap也不会剔除任何非临时数据(但会尝试剔除部分临时数据)。

单线程

问:Redis单线程是为了减少用户态到内核态的切换吗?

答:不是,至少主要原因不是。

操作系统为了响应多用户的请求,而进行的从用户态到内核态的切换,造成的的性能损耗,远不及为了保证数据一致性加锁带来的损耗。

Redis单线程是为了避免加锁的过程。

过期

  1. 那么redis是如何做到数据随着访问变化只保留热数据呢?
  • 业务逻辑: key是可以设置有效期的,具体key的有效期 需要根据 用户 的关注时间窗而设定的。过了这个时间窗这个数据就不应该出现在redis里,因为这个数据被访问的次数会很低,不能称之为热数据。

  • 业务运转: 内存是有限的,当redis内存满了,应该采取相应的策略剔除掉冷数据。

    redis内存上限多少呢? 可以通过6379.config配置文件来设定:maxmemory 单位为byte .

    redis内存满了之后又如何处理? 在6379.config配置文件来设定maxmemory-policy noeviction。

      noeviction: 直接返回错误,不允许Client继续使用了。(更适合redis作为数据库,能保持数据的完整性)
    
      allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。(常用,结合业务场景,非期限key多)
    
      volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在有期限元素集合内,使得新添加的数据有空间存放。(常用,结合业务场景,期限key多)
    
      allkeys-random: 回收随机的键使得新添加的数据有空间存放。(比较随意)
    
      volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在有期限元素集合内。(比较随机)
    
      volatile-ttl: 回收在过期集合的键,并且优先回即将过期的键,使得新添加的数据有空间存放。(复杂度高)
    

Keys的过期时间

通常Redis keys创建时没有设置相关过期时间。他们会一直存在,除非使用显示的命令移除,例如,使用DEL命令。 EXPIRE一类命令能关联到一个有额外内存开销的key。当key执行过期操作时,Redis会确保按照规定时间删除他们。 key的过期时间和永久有效性可以通过EXPIRE和PERSIST命令(或者其他相关命令)来进行更新或者删除过期时间。

Redis如何淘汰过期的keys

Redis keys过期有两种方式:被动和主动方式。

  • 当一些客户端尝试访问它时,key会被发现并主动的过期。

  • 当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。

    具体就是Redis每秒10次做的事情:

    1. 测试随机的20个keys进行相关过期检测。
    2. 删除所有已经过期的keys。
    3. 如果有多于25%的keys过期,重复步奏1. 这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分百低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys。

持久化

缓存:数据可以丢,保证速度。

数据库:数据是绝对不能丢的,保证速度+持久性,内存中的数据是掉电易失的。

RDB

cow

RDB 是 Redis 默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中。即在指定目录下生成一个dump.rdb文件。Redis 重启会通过加载dump.rdb文件恢复数据。RDB具有时点性,定点保存数据。

在新版本里也可以使用bgsave指令生成dump.rdb文件。

利用fork()函数创造子进程,类似于Copy-on-write的操作,父进程进行修改,子进程负责持久化,本质:产生新数据进行写入新的内存,然后移动父进程的指针,子进程可以看作虚拟内存,指针指向相应的数据内存

rdb的优点与弊端
  • 缺点:
  1. 不支持拉链,只能有一个dump.rdb文件,需要运维人员定制定时策略,比如每天把最后一个dump.rdb重命名或者迁移到别的机器上并设置日期时间。
  2. 丢失数据相对多一些,时点与时点之间数据容易丢失。比如8点得到一个rdb,9点要落一个rdb,突然服务器挂了。
  • 优点: 类似于java的序列化,直接数据状态二进制,回复速度相对快。

AOF(append only file)

log

AOF :Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。

Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

并且在redis中,aof和rdb可以同时开启,但是如果开启了aof就只会使用aof恢复数据。因为aof恢复数据的完整性高。

在4.0版本以后aof中包含rdb全量,增加记录新的写操作。这么做的好处就是写操作日志相对少一些,执行aof文件恢复数据时,先使用rdb进行恢复,再使用写操作日志,恢复速度相对快一些。

rewrite
  • redis4.0以前:可以触发rewrite机制,就是删除抵消的命令和整合重复的命令。 例1 :set k1 1,set k1 2,set k1 4这三条命令就可以抵消为set k1 4一条命令,因为set k1 1,set k1 2属于废弃数据,k1的最终值为4。

例2:lpush k1 1,lpush k1 1,lpush k1 1这三条命令可以整合成lpush k1 1 1 1,这样就节省了2个push单次所占用的空间。 最终得到的还是一个纯指令的日志文件。最终得到的还是一个纯指令的日志文件。最终得到的还是一个纯指令的日志文件。

  • redis4.0以后:还是触发rewrite机制,不同的是用rdb的方式写入aof文件里面,之后会把增量的指令日志append到aof中。 利用了rdb的快的特点,同时还利用的append丢失少的特点。 最终得到是aof混合体最终得到是aof混合体最终得到是aof混合体

集群

AKF理论

AKF扩展立方体(Scalability Cube),是《架构即未来》一书中提出的可扩展模型,这个立方体有三个轴线,每个轴线描述扩展性的一个维度

  • X轴:Redis示例的副本,数据库的副本…读写分离,增加备用性,解决单点故障的问题,全量镜像,不能解决容量有限的问题
  • Y轴:对要存的数据按照不同的功能业务拆分,不同类别的数据分开存储,客户端实现指定查询哪个库,解决容量有限的问题(业务分库)
  • Z轴:在按照业务拆分的前提下,如果又存不下了,可以基于一定的规则,将一个业务将数据再拆分,存储到不同的库里。(分区)

cap理论

CAP理论指的是一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项

shard

谈到集群管理不得不又说到数据的分片管理(shard),为了满足数据的日益增长和扩展性,数据存储系统一般都需要进行一定的分片,如传统的MySQL进行横向分表和纵向分表,然后应用程序访问正确的位置就需要找的正确的表。这时候,这个数据定向工作一般有三个位置可以放:

数据存储系统本身支持,Redis Cluster就是典型的试图在数据存储系统上支持分片;

  • 客户端支持,Memcached的客户端对分片的支持就是客户端层面的;
  • 代理支持,twemproxy就是试图在服务器端和客户端中间建代理支持;

redis集群

1. 运行一个redis实例会有哪些问题?

  • 单点故障
  • 容量瓶颈
  • 访问压力 解决:
  • x轴:在x轴方向上,做N个主机的全量镜像数据的副本,主redis与这些副本的关系为主从。主机可以对外提供read/write ,从机可以对外提供read(读写分离)。结合高可用,可以解决单点故障的问题,只是解决了 read 的压力,而没有解决 write 的压力。
  • y轴:在y轴方向上,可以把之前一台redis中的数据按照业务功能来拆分成不同的redis实例存储,并且每个redis实例都可以再次做x轴的镜像副本进行读写分离,当然,x轴和y轴之间不是必须要结合使用。y轴的拆分解决了容量瓶颈问题和数据访问压力的问题。
  • z轴:如果y轴的某个redis实例过于臃肿,还可以把这个redis实例进行z轴的拆分,也就是把这个redis实例里面的数据按照一定规则查分。比如:取模,优先级等规则再次查分成多个redis,使得不同的数据出现在固定的redis里。

2. 扩展后如何解决数据一致性问题?主从复制

  • 强一致性 所有节点阻塞,直到数据全部一致-强一致性,成本极高,并且难以达到。如果有一个节点因为网络问题挂了,整体就写失败了,对外表现出的是整个服务不可用。强一致性会破坏可用性。而我们将单redis拆分成多redis,本来就是要解决可用性的问题。

  • 弱一致性 master收到Client命令直接返回给客户端ok, master会异步的通知两个slave写操作,如果两个slave挂了导致写入失败,master也挂了。再重启之后slave就拿不到之前的master的写操作了,等于丢了一批数据。

  • 最终一致性 在master和slave之间,添加一个可靠、响应速度够快的集群(比如:kafka),master到kafka之间为同步阻塞状态。

master在写入的时候并没有直接通知两个slave,而是通知kafka,由kafka通知两个slave。如果master和kafka之间能够足够快的写入响应成功的话,就可以直接给Client返回OK了。

只要最终两个slave从kafka中取到数据,那么最终两个slave就会和master的数据达成一致,数据就不会丢失。 如果客户端要取数据的话,有多种可能:在达到最终一致性之前,可能会取到不一致的数据

3. master挂了怎么办?哨兵

  • 哨兵机制 多个哨兵根据过半原则监控redis是否活着进行裁决

哨兵使用了redis自带的发布订阅功能,哨兵会去监控master拿到两个slave分别是谁,同时在存活的master开启发布订阅发现其他的哨兵。

Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息, 并使用投票协议(agreement protocols)来决定是否执行自动故障迁移, 以及选择哪个从服务器作为新的主服务器。

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

4. 容量不足?分区

单机redis在使用的时候会碰到三个问题:单点故障、容量不足、访问压力。

  • 可以对redis进行AKF拆分,站在x轴的角度上可以对redis进行 主从复制 + 哨兵机制。
    • 主从复制:slave做master的全量备份保证数据的可靠性。master对外提供读写,slave对外提供读操作(读写分离),解决了部分访问压力。
    • 哨兵机制:对master做高可用,使用 哨兵集群 监控master健康状态,哨兵之间相互通信,如果发现master挂了会进行投票选举 其中一个个slave作为新的master。解决了单点故障的问题。
  • 但是由于slave是master的全量数据,在容量这个维度上来说,redis依然是一个单实例的。对于臃肿的redis实例来说,还需要对其进行y轴和z轴的拆分。

解决:

  • Z轴拆分 Y轴按照业务拆分后某一业务的数据量可能还是很大,所以可以对Z轴方向上,在Client端根据算法继续拆分。(sharding 分片)
1. modula(hash + 取模):

image.png 这种方式是在Client增加算法逻辑,把要存入数据的进行 % 运算,后边有几个redis实例,就和几进行 % 运算,根据取模的值,把数据存入到不同的redis实例。

  • 优点: 简单,容易操作。
  • 缺点: 取模的值是固定的,影响分布式下的扩展性,如果添加新的redis实例会使模数值发生改变,再取数据的时候根据模数去取就无法查找到数据了。
2. random

image.png

Client把数据随机放到不同的redis实例中

随机把数据放入不同的redis,取出成本很高?根本找不到数据?

如果存入的是一个list类型(lpush),key为ooxx,那么就会在两个redis中都生成ooxx的key,这个时候消费者并不是执行lpush的Client,而是执行rpop的另一个Client,都会从ooxx中取数据,无论从哪个redis取,都会取到key为ooxx的数据。这样就形成了一个类似于消息队列的功能。

3. ketama(一致性hash)

image.png

在得到一个长度随机的字符串后(redis的key),经过算法得到一个等宽的其他的值(算法:hash、crc16、crc32、fnv、md5),这个值会和字符串做一个映射。

和hash取模算法不同的是,一致性hash没有取模的过程(即:不会影响分布式扩展性),并且要求key和node都要参与计算。

会在内存中虚拟出一个环形(哈希环)。

哈希环由0-2^32个数字组成,每一个数字都是一个点。无论后面有多少个redis实例,或新增,或减少,都会给这些redis实例一个ID号(或者IP+Port之类的)唯一标识。

  • 优点: 没有固定node数量的限制,不会造成全局洗牌。
  • 缺点: 新增节点后,造成一小部分数据无法命中。

如上图,新增一个节点 node3,把node3的ID进行hash运算得到一个数值,这个数值对应 哈希环 的点 恰好在之前数据映射的点和它存入node之间(data -> node2 变为 data -> node3 -> node2), 此时如果查询data,不会从node2查询,而是会从node3里面查找data,因此查询结果为null。

这种方式只适合做缓存而不适合做数据库,缓存大多为期限,即使node2里面data数据永远不会被查询到,data也会随着时间的推移而被清除掉,或者开启缓存清理策略LRU、LFU。并且node3里面无法查询到数据,可以走数据库从新缓存到node3(缓存击穿),也可以修改为从比自己大的两个node中查找数据。

  • 问题: 如果node1和node2映射在哈希环的点分别在最左边和最右边,把哈希环切分成了上半环和下半环,在添加数据的时候就可能出现 数据集中在某个半环上而导致 数据量的倾斜。
  • 解决: 虚拟节点 我们可以每个node ID后面依次拼10个数字,让一个ID变成10个,用这些ID做hash运算映射到 哈希环 不同的点上,如果是两个设备在 哈希环 上面就会有20个点,这样做的目的是让一个物理设备出现在多个点上,就可以间接的解决数据倾斜的问题。

分区

为什么分区非常有用

Redis分区主要有两个目的:

  • 分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。
  • 分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

分区基本概念

有许多分区标准。假如我们有4个Redis实例R0, R1, R2, R3,有一批用户数据user:1, user:2, … ,那么有很多存储方案可以选择。从另一方面说,有很多different systems to map方案可以决定用户映射到哪个Redis实例。

一种最简单的方法就是范围分区,就是将不同范围的对象映射到不同Redis实例。比如说,用户ID从0到10000的都被存储到R0,用户ID从10001到20000被存储到R1,依此类推。

这是一种可行方案并且很多人已经在使用。但是这种方案也有缺点,你需要建一张表存储数据到redis实例的映射关系。这张表需要非常谨慎地维护并且需要为每一类对象建立映射关系,所以redis范围分区通常并不像你想象的那样运行,比另外一种分区方案效率要低很多。

另一种可选的范围分区方案是散列分区,这种方案要求更低,不需要key必须是object_name:的形式,如此简单:

  • 使用散列函数 (如 crc32 )将键名称转换为一个数字。例:键foobar, 使用crc32(foobar)函数将产生散列值93024922。
  • 对转换后的散列值进行取模,以产生一个0到3的数字,以便可以使这个key映射到4个Redis实例当中的一个。93024922 % 4 等于 2, 所以 foobar 会被存储到第2个Redis实例。 R2 注意: 对一个数字进行取模,在大多数编程语言中是使用运算符% 还有很多分区方法,上面只是给出了两个简单示例。有一种比较高级的散列分区方法叫一致性哈希,并且有一些客户端和代理(proxies)已经实现。

不同的分区实现方案

分区可以在程序的不同层次实现。

  • 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。
  • 代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy
  • 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

分区的缺点

有些特性在分区的情况下将受到限制:

  • 涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
  • 同时操作多个key,则不能使用Redis事务.
  • 分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set).
  • 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
  • 分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

持久化数据还是缓存?

无论是把Redis当做持久化的数据存储还是当作一个缓存,从分区的角度来看是没有区别的。当把Redis当做一个持久化的存储(服务)时,一个key必须严格地每次被映射到同一个Redis实例。当把Redis当做一个缓存(服务)时,即使Redis的其中一个节点不可用而把请求转给另外一个Redis实例,也不对我们的系统产生什么影响,我们可用任意的规则更改映射,进而提高系统的高可用(即系统的响应能力)。

一致性哈希能够实现当一个key的首选的节点不可用时切换至其他节点。同样地,如果你增加了一个新节点,立刻就会有新的key被分配至这个新节点。

重要结论如下:

  • 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
  • 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样

预分片

从上面获知,除非我们把Redis当做缓存使用,否则(在生产环境动态)增加和删除节点将非常麻烦,但是使用固定的keys-instances则比较简单。

一般情况下随着时间的推移,数据存储需求总会发生变化。今天可能10个Redis节点就够了,但是明天可能就需要增加到50个节点。

既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

使用Redis复制技术,你可以做到极短或者不停机地对用户提供服务:

  • 在你新服务器启动一个空Redis实例。
  • 把新Redis实例配置为原实例的slave节点
  • 停止你的客户端
  • 更新你客户端配置,以便启用新的redis实例(更新IP)。
  • 在新Redis实例中执行SLAVEOF NO ONE命令
  • (更新配置后)重启你的客户端
  • 停止你原服务器的Redis实例

redis分区实现

redis自带的拆分:cluster

Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念。

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽

image.png

  1. 拆分逻辑中进行预分区

redis1映射0,1,2,3,4

redis2映射5,6,7,8,9

(各领取5个槽位)。

如果redis3加入集群,会让redis1和redis2让出几个槽位,比如从redis1中拿到了3和4,在redis2中拿到了8和9(如上图)。移动数据的时候,不需要把redis全部rehash

只需要把3,4,8,9槽位的时点数据找到直接传输给redis3,redis3接收完数据后,会根据时点数据跟redis1和redis2传输期间内的数据做一个追平更新,追平的一刹那,再往3,4,8,9槽位存数据就会直接存入redis3。 Client只需要知道映射关系,就可以根据槽位正确的读取数据了

  1. 存取数据 Client想要存入k1,会随机访问redis1和redis2。可以让每个redis实例都能当家做主。

k1随机进入一个redis实例后(假设这个实例是redis2),redis2会先拿k1进行hash取模算出槽位,用计算的槽位和自己的mapping映射的槽位做一个匹配,如果和自己匹配就直接存入redis2,

如果不匹配再和redis2内保存的其他实例的槽位映射关系进行匹配,就能找到k1需要存的redis实例是哪个,把k1返回给Client并重定向到正确的redis实例中(假设匹配结果为redis3)

redis3拿到k1后同样要进行hash运算并且把结果和自身映射槽位进行匹配,匹配成功直接存入自己这个实例当中。

  • 问题: 聚合操作很难实现 操作的key都在相同的节点,可以用 hash tag 。

比如:把key设置成为{oo}k1,{oo}k2,{oo}k3这种带有{}内部有固定标识的 格式,这样在存入key的时候会使用 oo 进行取模,而不会使用k1,k2,k3进行取模。进而存入同一个redis节点。

twemproxy

predixy

参考

redis 文档 Redis 命令参考