redis学习-摘抄

五种数据结构简介

Redis是使用C编写的,内部实现了一个struct结构体redisObject对象,经过结构体来模仿面向对象编程的“多态”,动态支持不一样类型的value。做为一个底层的数据支持,redisObject结构体代码以下定义:java

#define LRU_BITS 24
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->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的类型,字符串、列表、集合、有序集、哈希表linux

//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:底层实现结构,字符串、整数、跳跃表、压缩列表等web

/* 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字段。redis

redis结构与编码组合列表算法

Redis的底层数据结构有如下几种,具体的数据结构原理就不细讲了:shell

  • 简单动态字符串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),每个节点能够保存一个字节数组或者一个整数值。
压缩列表数据实现的指针指向的结构以下图所示:缓存

 

压缩列表数据结构

  • 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的大小,初始化大小为4
unsignedlong sizemask; //值老是等于(size-1)。
unsignedlong used;     //记录哈希表已有的节点(键值对)数量。
} dictht;

其中dictEntry就是存放key和value的结构体。
总体的结构以下:

 

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的结构图以下:

 

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 <ID>

2)咱们将列表裁剪为指定长度,所以Redis只须要保存最新的5000条评论:
LTRIM latest.comments 05000

3)每次咱们须要获取最新评论的项目范围时,咱们调用一个函数来完成(使用伪代码):
FUNCTION get_latest_comments(start,num_items):
id_list = redis.lrange("latest.comments",start,start+num_items-1)
IF id_list.length < num_items
id_list = SQL_DB("SELECT ... ORDER BY time LIMIT ...")
END
RETURN id_list
END

咱们作了限制不能超过5000个ID,所以咱们的获取ID函数会一直询问Redis。只有在start/count参数超出了这个范围的时候,才须要去访问数据库。
咱们的系统不会像传统方式那样“刷新”缓存,Redis实例中的信息永远是一致的。SQL数据库(或是硬盘上的其余类型数据库)只是在用户须要获取“很远”的数据时才会被触发,而主页或第一个评论页是不会麻烦到硬盘上的数据库了。
场景二:删除与过滤; 使用功能: Redis中的集合
好比邮箱的垃圾邮件功能,包含特定词或者来自特定发送方。
有些时候你想要给不一样的列表附加上不一样的过滤器。若是过滤器的数量有限,你能够简单的为每一个不一样的过滤器使用不一样的Redis列表。
场景三:根据某个属性进行排名之类; 使用功能:Redis的有序集合
另外一个很广泛的需求是各类数据库的数据并不是存储在内存中,所以在大数据量场景下按得分排序,数据库的性能不够理想。
典型的好比那些在线游戏的排行榜,根据得分你一般想要:

  • 列出前100名高分选手
  • 列出某用户当前的全球排名

若是用数据库的的order by排序,这种相应时间很是长,没法支持大并发请求。可是这些操做对于Redis来讲小菜一碟,即便你有几百万个用户,每分钟都会有几百万个新的得分。
向有序集合添加一个或多个成员,或者更新已存在成员的分数:

ZADD key score1 member1 [score2 member2]

获得前100名高分用户很简单:

ZREVRANGE key 0 99

用户的全球排名也类似,只须要:

ZRANK key

场景四:过时项目处理 ; 使用功能:Redis的有序集合和过时时间
另外一种经常使用的项目排序是按照时间排序。而且只须要保留必定时间内的数据。
这时咱们可使用current_time(unix时间)做为得分,用Redis的有序集合来存储。并同时经过expire设置time_to_live。
场景五:计数; 使用功能:Redis的原子操做
Redis是一个很好的计数器,这要感谢INCRBY和其余类似命令。能够用于分布式场景下的全局计数器。我相信你曾许屡次想要给数据库加上新的计数器,用来获取统计或显示新信息,可是最后却因为写入敏感而不得不放弃它们。如今使用Redis就不须要再担忧了。有了原子递增(atomic increment),你能够放心的加上各类计数,用GETSET重置,或者是让它们过时。
**场景六:特定时间内的特定项目; 使用功能:redis的有序集合 **
另外一项对于其余数据库很难,但Redis作起来却垂手可得的事就是统计在某段特色时间里有多少特定用户访问了某个特定资源。好比我想要知道某些特定的注册用户或IP地址,他们到底有多少访问了某篇文章。
每次得到一次新的页面浏览时只须要这样作:

SADD page:day1:<page_id>:<user_id>

固然你可能想用unix时间替换day1,好比time()-(time()%3600*24)等等。
想知道特定用户的数量吗?只须要使用

SCARD page:day1:<page_id>

须要测试某个特定用户是否访问了这个页面

SISMEMBER page:day1:<page_id>

场景七: Pub/Sub; 使用功能:经过watch命令
Redis的Pub/Sub很是很是简单,运行稳定而且快速。支持模式匹配,可以实时订阅与取消频道。你应该已经注意到像list push和list pop这样的Redis命令可以很方便的执行队列操做了,但能作的可不止这些:好比Redis还有list pop的变体命令,可以在列表为空时阻塞队列。
场景八:分布式同步、分布式锁; 使用功能:锁(访问同一个key实现)
从redis获取值N,对数值N进行边界检查,自加1,而后N写回redis中。 这种应用场景很常见,像秒杀,全局递增ID、IP访问限制等。以IP访问限制来讲,恶意攻击者可能发起无限次访问,并发量比较大,分布式环境下对N的边界检查就不可靠,由于从redis读的N可能已是脏数据。传统的加锁的作法(如java的synchronized和Lock)也没用,由于这是分布式环境,这种场景就须要分布式锁。
分布式锁能够基于不少种方式实现,无论哪一种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放经过状态值来标识。
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的链接并不存在竞争关系。redis的SETNX命令能够方便的实现分布式锁,设置成功,返回 1 ,不然返回 0 。
上面的锁定逻辑有一个问题:若是一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?咱们能够经过锁的键对应的时间戳来判断这种状况是否发生了,若是当前的时间已经大于锁对应的值,说明该锁已失效,能够被从新使用。
发生这种状况时,不能简单的经过DEL来删除锁,而后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件。 为了让分布式锁的算法更稳键些,持有锁的客户端在解锁以前应该再检查一次本身的锁是否已经超时,再去作DEL操做(不是DEL,而是getset命令),这个时候可能已经被其余线程先set值了,经过比较值钱get的值和getset返回的值是否相等,能够判别当前线程是否得到锁。
更多关于分布式锁的实现,请参考Java分布式锁三种实现方案

如何解决缓存击穿

缓存穿透是指查询一个不存在的数据,致使这个不存在的数据每次请求都要到存储层去查询。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击咱们的应用,这就是漏洞。
缓存击穿问题通常出如今某个高并发访问的key忽然到了过时时间。缓存击穿和缓存雪崩的区别在于前者针对某一key缓存,后者则是不少key。
有人会说不设置过时时间,就不会出现热点key过时问题,也就是“物理”不过时,这样就不存在缓存击穿的问题。从功能上看,若是缓存数据不过时,那就成静态的数据了,理论上也就不须要redis这种缓存。
缓存使用能够从实时性和是否热点两个维度来选择解决缓存击穿方案。
实时性
你们都知道,缓存是从数据中同步过来的,因此它是有延迟的。业务对延迟的容忍度,就是实时性要求。实时性是业务要求,是一个没法妥协的变量。有的延迟能够在秒级别,有的能到分钟级别,有的就是零容忍。不一样的实时性要求,就能够采起不一样的缓存同步策略。
热点
热点值指的的一个资源(注意不是页面)被同时访问的用户数,这个值比较高时才是热点。热点是技术要求,由于缓存击穿问题基本都是热点引发的,因此在设计缓存方案的时候必需要考虑热点。举例:商品详情页面,假设这个页面的并发量是1w,但其中最大的商品的并发量却可能很低,假设只有50并发。咱们认为这个页面不存在热点。这里的热点,指的是资源热点,或者说数据热点。

image.png

 

处理方案(绿色方框)
1)懒加载

懒加载

 

先从缓存中取,若是没有则从数据库中取,再放入缓存。
特色:维护成本低、实时性差,命中率低(遇到热点,可能出现数据库击穿的问题)
2)推送

 

推送

经过独立的任务,周期性的将数据刷入缓存。这里除了任务以外,也多是一个消息触发。
特色:维护成本适中,实时性适中(周期性任务),命中率100%
推送的方案一般能够结合任务中间件或消息中间件(公司能够考虑个人另外一篇文章DRC实战),他们具备更大的灵活性。干预度强,也能够实现降级。
3)懒加载:二级缓存

二级缓存

 

送数据库获取数据后,放入一个短时间缓存和一个长期缓存。在短时间缓存过时后,经过加锁控制去数据库加载数据的线程数。没有得到锁的,直接从二级缓存获取数据。
特色:维护成本适中,实时性适中,命中率100%,该方案能够解决动态热点。是推送方案的补充
4)双写

 

双写

 

一边写入数据库,一边写入缓存
特色:维护成本最高(侵入代码),实时性高,命中率100%

使用优先级:1)>2)>3)>4)。响应的维护成本越低越优先

做者:彦帧 连接:https://www.jianshu.com/p/85c713d07895 來源:简书 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。

相关文章
相关标签/搜索