欢迎关注头条号:java小马哥
周一至周日下午三点半!精品技术文章准时送上!!!
精品学习资料获取通道,参见文末
![9a4aeee8d84acd99b6516348d03c479c.png](https://i-blog.csdnimg.cn/blog_migrate/a38d329ef160d4c318ccbba92fd94407.jpeg)
五种数据结构简介
Redis是使用C编写的,内部实现了一个struct结构体redisObject对象,通过结构体来模仿面向对象编程的“多态”,动态支持不同类型的value。作为一个底层的数据支持,redisObject结构体代码如下定义:
#define LRU_BITS 24#define LRU_CLOCK_MAX ((1<lru */#define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */typedef struct redisObject { //对象的数据类型,占4bits,共5种类型 unsigned type:4; //对象的编码类型,占4bits,共10种类型 unsigned encoding:4; //least recently used //实用LRU算法计算相对server.lruclock的LRU时间 unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ //引用计数 int refcount; //指向底层数据实现的指针 void *ptr;} robj;
下面介绍type、encoding、ptr3个属性定义枚举。
type:redisObject的类型,字符串、列表、集合、有序集、哈希表
//type的占5种类型:/* Object types */#define OBJ_STRING 0 //字符串对象#define OBJ_LIST 1 //列表对象#define OBJ_SET 2 //集合对象#define OBJ_ZSET 3 //有序集合对象#define OBJ_HASH 4 //哈希对象
encoding:底层实现结构,字符串、整数、跳跃表、压缩列表等
/* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */// encoding 的10种类型#define OBJ_ENCODING_RAW 0 /* Raw representation */ //原始表示方式,字符串对象是简单动态字符串#define OBJ_ENCODING_INT 1 /* Encoded as integer */ //long类型的整数#define OBJ_ENCODING_HT 2 /* Encoded as hash table */ //字典#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ //不在使用#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */ //双端链表,不在使用#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ //压缩列表#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ //整数集合#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ //跳跃表和字典#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ //embstr编码的简单动态字符串#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */ //由压缩列表组成的双向列表-->快速列表
ptr:实际指向保存值的数据结构
如果一个 redisObject 的 type 属性为 OBJ_LIST,encoding 属性为 REDIS_ENCODING_LINKEDLIST,那么这个对象就是一个 Redis 列表,它的值保存在一个双链表内,而 ptr 指针就指向这个双向链表;如果一个 redisObject 的type属性为OBJ_HASH,encoding 属性REDIS_ENCODING_ZIPMAP,那么这个对象就是一个 Redis 哈希表,它的值保存在一个 zipmap 里,而 ptr 指针就指向这个 zipmap 。
下面这张图片中的OBJ_STRING/OBJ_LIST/OBJ_ZSET/OBJ_HASH/OBJ_SET针对的是redisObject中的type,后面指向的REDIS_ENCODING_INT、REDIS_ENCODING_RAW、REDIS_ENCODING_LINKEDLIST等针对的是encoding字段。
![44fd042731534b8cdb540c8a83eb62ed.png](https://i-blog.csdnimg.cn/blog_migrate/fa4b4f4b7553d2a413bb028df39ef08c.jpeg)
redis结构与编码组合列表
Redis的底层数据结构有以下几种,具体的数据结构原理就不细讲了:
- 简单动态字符串sds(Simple Dynamic String)
- 双向链表(LinkedList)
- 字典(Map)
- 跳跃表(SkipList)
String
字符串对象的底层实现类型如下:
编码—encoding 对象—ptr OBJ_ENCODING_RAW 简单动态字符串实现的字符串对象 OBJ_ENCODING_INT 整数值实现的字符串对象 OBJ_ENCODING_EMBSTR embstr编码的简单动态字符串实现的字符串对象 如果一个String类型的value能够保存为整数,则将对应redisObject 对象的encoding修改为REDIS_ENCODING_INT,将对应redisObject对象的ptr值改为对应的数值;如果不能转为整数,保持原有encoding为REDIS_ENCODING_RAW。因此String类型的数据可能使用原始的字符串存储(实际为sds - Simple Dynamic Strings,对应encoding为REDIS_ENCODING_RAW或OBJ_ENCODING_EMBSTR)或者整数存储。
字符串编码存在OBJ_ENCODING_RAW和OBJ_ENCODING_EMBSTR两种,redis会根据value中字符串的大小动态选择。创建一个String类型的redis值,分配空间的代码如下:
RedisObj *o = zmalloc(sizeof(RedisObj)+sizeof(struct sdshdr8)+len+1);
其中:sdshdr8(保存字符串对象的结构)的大小为3个字节,加上1个结束符共4个字节;redisObject的大小为16个字节;一个embstr固定的大小为16+3+1 = 20个字节,因此一个最大的embstr字符串为64-20 = 44字节。创建字符串对象,根据长度使用不同的编码类型--createRawStringObject或createEmbeddedStringObject。当字符串长度大于44字节时,使用createRawStringObject,此时redisobj结构和sdshdr结构(存储具体字符串内容)在内存上是分开的;当字符串长度小于等于44字节时,使用createEmbeddedStringObject,此时redisObj结构和sdshdr结构在内存上是连续的。
List
列表的底层实现有2种:REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST,ZIPLIST(压缩列表)相比LINKEDLIST(链接列表)可以节省内存,当创建新的列表时,默认是使用压缩列表作为底层数据结构的。Redis内部会对相关操作做判断,当list的元数小于配置值: hash-max-ziplist-entries 或者elem_value字符串的长度小于 hash-max-ziplist-value, 可以编码成 REDIS_ENCODING_ZIPLIST 类型存储,以节约内存。
压缩列表ziplist结构本身就是一个连续的内存块,由表头、若干个entry节点和压缩列表尾部标识符zlend组成,通过一系列编码规则,提高内存的利用率,使用于存储整数和短字符串。
压缩列表是一系列特殊编码的连续内存块组成的顺序序列数据结构,可以包含任意多个节点(entry),每一个节点可以保存一个字节数组或者一个整数值。
压缩列表数据实现的指针指向的结构如下图所示:
![ce89fb6112851a5ffc47b3480758b46d.png](https://i-blog.csdnimg.cn/blog_migrate/ff4a85fccaec2a0aa9729a06ca499653.jpeg)
压缩列表数据结构
- zlbytes:占4个字节,记录整个压缩列表占用的内存字节数。
- zltail_offset:占4个字节,记录压缩列表尾节点entryN距离压缩列表的起始地址的字节数。
- zllength:占2个字节,记录了压缩列表的节点数量。
- entry[1-N]:长度不定,保存数据。
- zlend:占1个字节,保存一个常数255(0xFF),标记压缩列表的末端。
压缩列表ziplist结构的缺点是:每次插入或删除一个元素时,都需要进行频繁的调用realloc()函数进行内存的扩展或减小,然后进行数据”搬移”,甚至可能引发连锁更新,造成严重效率的损失。
Hash
创建新的Hash类型时,默认也使用ziplist存储value,保存数据过多时,使用hash table。
redisObject对象中存放的是结构体dict,定义如下:
typedefstruct dict { dictType *type; //指向dictType结构,dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据。 void *privdata; //私有数据,保存着dictType结构中函数的参数。 dictht ht[2]; //两张哈希表。用于扩展或收缩 long rehashidx; //rehash的标记,rehashidx==-1,表示没在进行rehash int iterators; //正在迭代的迭代器数量} dict;
其中dictht(Redis中哈希表)定义如下:
typedefstruct dictht { //哈希表dictEntry **table; //数组地址,数组存放着哈希表节点dictEntry的地址。unsignedlong size; //哈希表table的大小,初始化大小为4unsignedlong sizemask; //值总是等于(size-1)。unsignedlong used; //记录哈希表已有的节点(键值对)数量。} dictht;
其中dictEntry就是存放key和value的结构体。
整体的结构如下:
![270af810ee48302b5c1be188baf2f71b.png](https://i-blog.csdnimg.cn/blog_migrate/6ec4d911dce0a23b4dd18178b8d60ccd.jpeg)
hash类型的redis值结构
Set
集合的底层实现也有两种:REDIS_ENCODING_INTSET和REDIS_ENCODING_HT(字典),创建Set类型的key-value时,如果value能够表示为整数,则使用intset类型保存value。否则切换为使用hash table保存各个value(hash table,参考上面Hash的介绍),虽然使用散列表对集合的加入删除元素,判断元素是否存在等操作时间复杂度为O(1),但是当存储的元素是整型且元素数目较少时,如果使用散列表存储,就会比较浪费内存,因此整数集合(intset)类型因为节约内存而存在。
整数集合(intset)结构体定义如下:
typedefstruct intset { uint32_t encoding; //编码格式,有如下三种格式,初始值默认为INTSET_ENC_INT16 uint32_t length; //集合元素数量 int8_t contents[]; //保存元素的数组,元素类型并不一定是ini8_t类型,柔性数组不占intset结构体大小,并且数组中的元素从小到大排列。} intset; //整数集合结构
整数集合(intset)类型的编码格式有下面三种:
#define INTSET_ENC_INT16 (sizeof(int16_t)) //16位,2个字节,表示范围-32,768~32,767#define INTSET_ENC_INT32 (sizeof(int32_t)) //32位,4个字节,表示范围-2,147,483,648~2,147,483,647#define INTSET_ENC_INT64 (sizeof(int64_t)) //64位,8个字节,表示范围-9,223,372,036,854,775,808~9,223,372,036,854,775,807
intset整数集合之所以有三种表示编码格式的宏定义,是因为根据存储的元素数值大小,能够选取一个最”合适”的类型存储,”合适”可以理解为:既能够表示元素的大小,又可以节省空间。因此,当新添加的元素,例如:65535,超过当前集合编码格式所能表示的范围,就要进行升级操作。
Sorted Set
有序集合的底层编码实现也是2种:REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_SKIPLIST。跳跃表在redis中当数据较多时作为有序集合键的实现方式之一。跳跃表是一个有序链表,其中每个节点包含不定数量的链接,节点中的第i个链接构成的单向链表跳过含有少于i个链接的节点。
跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,大部分情况下,跳跃表的效率可以和平衡树相媲美。
Redis的持久化
我们知道redis与memcached的一个很大的不同是redis可以将数据持久化到磁盘,能持久化意味着数据的可靠性的提升。
RDB(redis database)是一个磁盘存储的数据库文件,其中保存的是最后一次写入时内存数据的最后状态。由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化,这称为“半持久化模式”),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件,这称为“全持久化模式”)。
RDB的持久化方式通过配置的定时执行间隔定时将内存中的数据写入到一个新的临时RDB文件中,然后用这个临时文件替换上次持久化的RDB文件,如此不断的定时更替。
当redis server重启时,会检查当前配置的持久化方式,如果是AOF(Append Of File)则以AOF数据作为恢复数据,因为AOF备份的准确性往往比RDB更高。如果是只开启了RDB模式的话则会加载最新的RDB文件内容到内存中。
另外redis也提供了手动调用的命令来实施RDB备份,包括阻塞的持久化和非阻塞的持久化。
RDB持久化
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
RDB持久化的时间间隔可以配置,和该配置项一起配合使用的还有另一个指标“变更次数”,每次时间间隔时必须同时符合“间隔时间”和“变更次数”两个条件才会进行RDB持久化,否则当次的持久化过程会推迟到下一个时间间隔再判断是否符合条件。
AOF持久化
当Redis开启AOF持久化时,每次接收到操作指令后,先将操作命令和数据以格式化的方式追加到操作日志文件的尾部,追加成功后才进行内存数据库的数据变更。这样操作日志文件就保存了所有的历史操作过程。该过程与MySQL的bin.log、zookeeper的txn-log十分相似。
AOF保存的是每次操作的操作序列,相比较而言RDB保存的是数据快照,因此AOF的操作日志文件内容往往比RDB文件大。
需要注意的是,因为linux对文件的写操作采取了“延迟写入”手段,因此redis提供了always、everysec、no三种选择来决定直接调用操作系统文件写入的刷盘动作。
AOF先记录后变更的特性决定了数据的可靠性更高,因此当AOF和RDB持久化都配置时,Redis服务在重启后会优先选择AOF数据作为数据恢复标准。
执行AOF数据恢复时,Redis读取AOF文件中的“操作+数据”集,通过逐条重放的方式恢复内存数据库。
AOF文件会不断增大,它的大小直接影响“故障恢复”的时间,而且AOF文件中历史操作是可以丢弃的。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过程是比较可靠的。
Redis事务
Redis事务通常会使用MULTI,EXEC,WATCH等命令来完成,redis实现事务的机制与常见的关系型数据库有很大的却别,比如redis的事务不支持回滚,事务执行时会阻塞其它客户端的请求执行等。
事务实现相关的指令
MULTI
用于标记事务块的开始。Redis会将后续的命令逐个放入队列中,每一个指令的返回结果都是“QUEUED”。只有先执行MULTI指令后才能使用EXEC命令原子化地执行这个命令序列。总是返回OK。
EXEC
在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。EXEC指令的返回值是队列中多条指令的有序结果。
当在事务中使用了WATCH命令监控的KEY时,只有当受监控的键没有被修改时,EXEC命令才会执行事务中的队列命令集合。
DISCARD
清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
如果使用了WATCH命令,那么DISCARD命令就会将当前连接监控的所有键取消监控。
WATCH
watch 用于在进行事务操作的最后一步也就是在执行exec 之前对某个key进行监视,如果这个被监视的key被改动,那么事务就被取消,否则事务正常执行。一般在MULTI 命令前就用watch命令对某个key进行监控。如果当前连接监控的key值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。
WATCH命令的作用只是当被监控的键值被修改后阻止事务的执行,而不能保证其他客户端不修改这一键值。
UNWATCH
清除所有先前为一个事务监控的键。执行EXEC命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用UNWATCH命令来取消监控。UNWATCH命令,清除所有受监控的键。在运行UNWATCH命令之后,Redis连接便可以再次自由地用于运行新事务。
redis事务从开始到结束通常会通过三个阶段:
1)事务开始
2)命令入队
3)事务执行
标记事务的开始,MULTI命令可以将执行该命令的客户端从非事务状态切换成事务状态,这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识完成, 在打开事务标识的客户端里,这些命令,都会被暂存到一个命令队列里,不会因为用户的输入而立即执行。客户端打开了事务标识后,只有命令: EXEC, DISCARD, WATCH,MULTI命令会被立即执行,其它命令服务器不会立即执行,而是将这些命令放入到一个事务队列里面,然后向客户端返回一个QUEUED回复 。redis客户端有自己的事务状态,这个状态保存在客户端状态mstate属性中。
事务的ACID性质详解
在redis中事务总是具有原子性(Atomicity),一致性(Consistency)和隔离性(Isolation),并且当redis运行在某种特定的持久化模式下,事务也具有持久性(Durability)。
原子性
事务具有原子性指的是事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行。但是对于redis的事务功能来说,事务队列中的命令要么就全部执行,要么就一个都不执行,因此redis的事务是具有原子性的(有条件的原子性)。我们通常会知道两种关于redis事务原子性的说法:一种是要么事务都执行,要么都不执行;另外一种说法是redis事务,当事务中的命令执行失败后面的命令还会执行,错误之前的命令不会回滚。其实这个两个说法都是正确的,redis分语法错误和运行错误。
- 语法错误:如果redis出现了语法错误,Redis 2.6.5之前的版本会忽略错误的命令,执行其他正确的命令,2.6.5之后的版本会忽略这个事务中的所有命令,都不执行。
- 运行错误:运行错误表示命令在执行过程中出现错误,比如用GET命令获取一个散列表类型的键值。这种错误在命令执行之前Redis是无法发现的,所以在事务里这样的命令会被Redis接受并执行。如果事务里有一条命令执行错误,其他命令依旧会执行(包括出错之后的命令)。
只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。
Redis已经在系统内部进行功能简化,这样可以确保更快的运行速度,因为Redis不需要事务回滚的能力。
一致性
事务具有一致性指的是如果在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然一致的。 “一致”指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。redis通过谨慎的错误检测和简单的设计来保证事务一致性。如果遇到运行错误,redis的原子性也不能保证,所以一致性也是有条件的一致性。
隔离性
事务的隔离性指的是即使有多个事务并发在执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。 因为redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事物进行中断。因此redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性的 。
持久性
事务的持久性指的是当一个事务执行完毕时,执行这个事务所得的结果已经被保持到永久存储介质里面。 因为redis事务不过是简单的用队列包裹起来一组redis命令,redis并没有为事务提供任何额外的持久化功能,所以redis事务的持久性由redis使用的模式决定 :
- 当服务器在无持久化的内存模式下运行时,事务不具有持久性,一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失 ;
- 当服务器在RDB持久化模式下运作的时候,服务器只会在特定的保存条件满足的时候才会执行BGSAVE命令,对数据库进行保存操作,并且异步执行的BGSAVE 不能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有持久性 ;
- 当服务器运行在AOF持久化模式下,并且appedfsync的选项的值为always时,程序总会在执行命令之后调用同步函数,将命令数据真正的保存到硬盘里面,因此这种配置下的事务是具有持久性的;
- 当服务器运行在AOF持久化模式下,并且appedfsync的选项的值为everysec时,程序会每秒同步一次命令数据到磁盘因为停机可能会恰好发生在等待同步的那一秒内,这种可能造成事务数据丢失,所以这种配置下的事务不具有持久性。
过期数据清除
数据过期时间
通过EXPIRE key seconds命令来设置数据的过期时间。返回1表明设置成功,返回0表明key不存在或者不能成功设置过期时间。key的过期信息以绝对Unix时间戳的形式存储(Redis2.6之后以毫秒级别的精度存储)。这意味着,即使Redis实例没有运行也不会对key的过期时间造成影响。
key被DEL命令删除或者被SET、GETSET命令重置后与之关联的过期时间会被清除。
更新了存储在key中的值而没有用全新的值替换key原有值的所有操作都不会影响在该key上设置的过期时间。例如使用INCR命令增加key的值或者通过LPUSH命令在list中增加一个新的元素或者使用HSET命令更新hash字段的值都不会清除原有的过期时间设置。
若key被RENAME命令重写,比如本存在名为mykey_a和mykey_b的key一个RENAME mykey_b mykey_a命令将mykey_b重命名为本已存在的mykey_a。那么无论mykey_a原来的设置如何都将继承mykey_b的所有特性,包括过期时间设置。
EXPIRE key seconds应用于一个已经设置了过期时间的key上时原有的过期时间将被更新为新的过期时间。
过期数据删除策略--被动方式结合主动方式
当clients试图访问设置了过期时间且已过期的key时,这个时候将key删除再返回空,为主动过期方式。但仅是这样是不够的,因为可能存在一些key永远不会被再次访问到,这些设置了过期时间的key也是需要在过期后被删除的。因此,Redis会周期性的随机测试一批设置了过期时间的key并进行处理。测试到的已过期的key将被删除,这种为被动过期方式。典型的方式为,Redis每秒做10次如下的步骤:
1)随机测试100个设置了过期时间的key
2)删除所有发现的已过期的key
3)若删除的key超过25个则重复步骤1
这是一个基于概率的简单算法,基本的假设是抽出的样本能够代表整个key空间,redis持续清理过期的数据直至将要过期的key的百分比降到了25%以下。这也意味着在任何给定的时刻已经过期但仍占据着内存空间的key的量最多为每秒的写操作量除以4。
redis集群方案
Redis官方集群方案Redis Cluster(P2P模式)
redis 3.0版本开始提供的集群服务,服务端实现的集群。Redis Cluster将所有Key映射到16384个Slot中,集群中每个Redis实例负责一部分,实例之间双向通信。业务程序通过集成的Redis Cluster客户端进行操作。客户端可以向任一实例发出请求,如果所需数据不在该实例中,则该实例引导客户端自动去对应实例读写数据。
redis启动之后,用户必须开启集群模式,通过cluster-enabled yes 设置。通过执行cluster meet 命令来完成连接各个redis单例服务,redis 节点必须进行槽(slot)指派,这样就建立一个redis 集群了。没有槽指派,集群是不能正常运用起来.
redis 集群是通过分片方式来存储键值的,集群默认将整个redis 数据库分成16384个槽(slot),每个节点必须做槽指派。否则集群处于fail 状态。通过shell命令来指派槽,必须把16384槽都分配到不同节点。
此种方式集群在添加和删除节点时,需通过手动脚本命令进行添加和删除,槽必须需要重新分配。这种集群不能自动发现节点,节点的健康状况,缺乏管理页面监控整个集群的状况。
RedisSharding集群
redis 3.0之前版本的集群方式,是客户端实现集群的方案。建立由N个节点组成一个集群,各redis节点相互独立,不会进行相互通信。客户端预先设置的路由规则,直接对多个Redis实例进行分布式访问。
采用一致性hash算法(将key和节点name同时hashing)将redis 数据散列对应的节点,这样客户端就知道从哪个Redis节点获取数据。当增加或减少节点时,不会产生由于重新匹配造成的rehashing。
客户端实现的集群缺点:
- 各个节点相互独立
- 一个节点挂的,整个集群不可用,因此一般redis节点都主从备份,一但某个节点挂了,备份节点成为master。
- 增加节点时,尽管采用一致性哈希发送,还是会有key匹配不到而丢失,导致缓存被击穿
- 增加节点时,客户端需重新调整路由规则,有多少个客户端业务接入,就有多少个客户端得重新调整。
利用代理中间件实现大规模Redis集群
通过中间代理层实现的集群方案以codis最为经典,codis的结构图如下:
![846d158a9372e9cb9dec3062b73ab592.png](https://i-blog.csdnimg.cn/blog_migrate/d6b9da295fd2b561cce167b55e0a32fb.jpeg)
codis结构图
这里以codis为例分析,codis-proxy 是Redis客户端连接的代理服务,客户端通过连接codis-proxy,codis-proxy指定连接后面具体的redis实例。Redis客户端通过zk上的注册信息来获得当前可用的proxy列表,从而保证代理的高可用性。
我们为什么选用codis方案作为redis的集群方案,原因如下:
- 整个多台codis-server 就是一个大的存储系统, 实现负责均衡
- 由于dashhoard功能,可通过web界面来管理,观察Codis集群的状态,做到可视化操作,添加/删除组、数据分片、添加/删除redis实例等操作。
- 支持热扩容。即:在不停止服务的情况下,实现集群设备的增减。
- 数据在迁移过程中,不需要停机等待迁移完成,数据平滑的迁移到新的节点,客户端可以正常通过Proxy访问节点数据,用户正常访问,无感知。
- 高可性:通过codis-ha会自动观察发现某组master出现异常,就会将改组中节点的salve为master,实现codis-server的主从切换。
redis 典型使用
典型使用场景简介
场景一:显示最新的列表; 使用功能:Redis中的列表
在Web应用中,“列出最新的回复”之类的查询非常普遍,这通常会带来可扩展性问题。类似的问题就可以用Redis来解决。比如说,我们的一个Web应用想要列出用户贴出的最新20条评论。在最新的评论边上我们有一个“显示全部”的链接,点击后就可以获得更多的评论。
我们假设数据库中的每条评论都有一个唯一的递增的ID字段。我们可以使用分页来制作主页和评论页,使用Redis的模板:
1)每次新评论发表时,我们会将它的ID添加到一个Redis列表:
LPUSH latest.comments
2)我们将列表裁剪为指定长度,因此Redis只需要保存最新的5000条评论:
LTRIM latest.comments 05000
3)每次我们需要获取最新评论的项目范围时,我们调用一个函数来完成(使用伪代码):
FUNCTION get_latest_comments(start,num_items):
id_list = redis.lrange("latest.comments