相关阅读
简介
Redis(Remote Dictionary Server即远程字典服务)是一个开源的使用C语言编写的、支持网络、基于内存亦可持久化的日志型、Key-Value数据库;
所有的数据都缓存在内存中,会周期性地把更新的数据写入磁盘或者把修改操作追加写入记录文件,并实现了master-slave同步;
基本数据类型
Redis所有的数据都是以Key/Value存储的,通过这个唯一的Key来获取相应的Value数据,Value的数据类型有:string
、list
、hash
、set
、zset
、stream
;
定义如下:
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
#define OBJ_MODULE 5 /* Module object. */
#define OBJ_STREAM 6 /* Stream object. */
string
string
是Redis最简单的数据类型;支持设置过期时间;支持批量读写;
如果value
是整数,还支持自增操作,自增的范围是signed long
的最大最小值;
最大能存储512MB,可以是简单的字符串、复杂的XML/JSON字符串、二进制图像或者音频的字符串;
应用场景
- 缓存功能;
- 计数器;
- 共享用户Session;
常用命令
127.0.0.1:6379> get key
(nil)
127.0.0.1:6379> set key value EX 5
OK
127.0.0.1:6379> get key
"value"
127.0.0.1:6379> get key
(nil)
127.0.0.1:6379> setex key 5 value
OK
127.0.0.1:6379> get key
"value"
127.0.0.1:6379> get key
(nil)
127.0.0.1:6379> mset key value key2 value2 key3 value3
OK
127.0.0.1:6379> mget key key2 key3
1) "value"
2) "value2"
3) "value3"
127.0.0.1:6379> set key 9223372036854775807
OK
127.0.0.1:6379> incr key
(error) ERR increment or decrement would overflow
list
list
相当于Java中的LinkedList
,支持从两端插入(push)和弹出(pop)元素;
左进左出可实现栈;左进右出可实现队列;
当list
弹出最后一个元素后,该Key自动被删除,内存被回收;
最大存储2^32-1
个元素,元素可重复;
应用场景
- 消息队列:将需要延后处理的任务信息塞入
list
中,由其它线程阻塞读取该list
来执行任务; - 列表展示数据;
常用命令
127.0.0.1:6379> lpush key 1 2 3
(integer) 3
127.0.0.1:6379> lindex keys 0
"1"
127.0.0.1:6379> llen key
(integer) 3
127.0.0.1:6379> rpop key
"1"
127.0.0.1:6379> lpop key
"3"
127.0.0.1:6379> brpop key 5
1) "key"
2) "2"
127.0.0.1:6379> brpop key 5
(nil)
(5.05s)
hash
hash
相当于Java中的HashMap
;
Redis 4.0.0以后,hmset
被视为已弃用,hset
可实现相同功能;
应用场景
- 存储关系型对象;
- 存储用户相关信息;
常用命令
127.0.0.1:6379> hset keys key value key2 value2
(integer) 2
127.0.0.1:6379> hget keys key
"value"
127.0.0.1:6379> hgetall keys
1) "key"
2) "value"
3) "key2"
4) "value2"
127.0.0.1:6379> hlen keys
(integer) 2
127.0.0.1:6379> hmget keys key key2
1) "value"
2) "value2"
set
set
相当于Java中的HashSet
,内部的值是无序且唯一的;内部实现相当于一个所有value
为NULL
的hash
;当最后一个元素移除后,该Key自动被删除,内存被回收;
应用场景
- 标签:根据标签将同类事物归并;
- 统计数据:根据数据的唯一性,统计数据数量;
常用命令
127.0.0.1:6379> sadd keys key key2
(integer) 2
127.0.0.1:6379> smembers keys
1) "key"
2) "key2"
127.0.0.1:6379> sismember keys key
(integer) 1
127.0.0.1:6379> sismember keys key3
(integer) 0
127.0.0.1:6379> scard keys
(integer) 2
127.0.0.1:6379> spop keys
"key2"
zset
zset
相当于Java中的SortedSet
和HashMap
的结合,内部的值是有序且唯一的,会给值增加一个score
属性,代表该值的排序权重;
应用场景
- 排行榜;
- 带权重的队列;
常用命令
127.0.0.1:6379> zadd keys 1 key 2 key2 3 key3
(integer) 3
127.0.0.1:6379> zcard keys
(integer) 3
127.0.0.1:6379> zrange keys 0 -1
1) "key"
2) "key2"
3) "key3"
127.0.0.1:6379> zrevrange keys 0 -1
1) "key3"
2) "key2"
3) "key"
127.0.0.1:6379> zscore keys key
"1"
127.0.0.1:6379> zrangebyscore keys 1 2
1) "key"
2) "key2"
127.0.0.1:6379> zrangebyscore keys 2 1
(empty array)
127.0.0.1:6379> zrem keys key2
(integer) 1
127.0.0.1:6379> zadd keys 0 key0
(integer) 1
127.0.0.1:6379> zrange keys 0 -1
1) "key0"
2) "key"
3) "key3"
stream
Redis5.0引入,是一个新的强大的支持多播的可持久化的消息队列;
应用场景
- 消息队列;
常用命令
127.0.0.1:6379> xadd messages * key value
"1648390689206-0"
127.0.0.1:6379> xlen messages
(integer) 1
127.0.0.1:6379> xrange messages 1648390689206 1648390689207
1) 1) "1648390689206-0"
2) 1) "key"
2) "value"
127.0.0.1:6379> xrange messages 1648390689205-9 1648390689207-0
1) 1) "1648390689206-0"
2) 1) "key"
2) "value"
127.0.0.1:6379> xdel messages 1648390689206-0
(integer) 1
127.0.0.1:6379> xlen messages
(integer) 0
127.0.0.1:6379> del messages
(integer) 1
数据存储结构
Key/Value的存储结构
Redis内部整体的存储结构是一个大的Hashtable
,内部由数组和链表实现,每一个dictEntry
是一个Key/Value对象,Value为定义的redisObject
结构体;
dictEntry
结构体定义如下:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
键的结构为redisObject
,结构体定义如下:
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
/* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
type
:表示该对象的数据类型,即string、list、set、hash、zset、stream中一种,但是为了提高存储效率和程序执行效率,每种数据类型的数据结构实现都可能不止一种;
encoding
:表示对象数据类型底层所使用的数据结构的编码;
*ptr
:指向具体的数据结构的地址;
Redis对象底层的数据结构编码类型如下:
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#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 */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
Redis的数据类型和数据结构编码关系如下:
string数据结构
string类型的数据结构为OBJ_ENCODING_EMBSTR
和OBJ_ENCODING_RAW
,两者区别如下:
OBJ_ENCODING_EMBSTR
只分配一次内存空间,redisObject和SDS是连续的内存,查询效率会快很多;正因为redisObject和SDS是连续的,当字符串长度增加时,需要重新分配内存,导致redisObject和SDS都需要重新分配空间,这样会影响性能;所以OBJ_ENCODING_EMBSTR
分配内存后,只允许读,如果修改数据,那么就会转成OBJ_ENCODING_RAW
,不再使用OBJ_ENCODING_EMBSTR
;
OBJ_ENCODING_EMBSTR
和OBJ_ENCODING_RAW
都是使用SDS数据结构,SDS结构体定义如下:
// 针对不同长度做了相应的数据结构
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
创建string类型的Value代码如下:
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
- 当字符串长度不超过44字节时,使用
OBJ_ENCODING_EMBSTR
; - 当字符串长度超过44字节时,使用
OBJ_ENCODING_RAW
; - 当字符串为64位有符号整数时,会使用
OBJ_ENCODING_INT
类型来存储;
OBJ_ENCODING_EMBSTR
使用最小的sdshdr8
数据结构,redisObject
结构体占用16字节,sdshdr8
结构体占用3字节,用于存储空字符的1字节,初始最小分配为64字节,所以还剩下64-16-3-1=44字节,可用于存储字符串;
list数据结构
Redis3.2及以后的底层实现方式:OBJ_ENCODING_QUICKLIST
;
ziplist是一种压缩链表,能节省内存空间,它所存储的内容都是在连续的内存区域当中;当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储;但当数据量过大时,每次插入ziplist都会重新realloc来保证存储内容在内存中的连续性,并将内容复制到新地址,这会消耗大量时间;
创建ziplist的代码如下:
/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
unsigned char *zl = zmalloc(bytes);
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
ZIPLIST_LENGTH(zl) = 0;
zl[bytes-1] = ZIP_END;
return zl;
}
ziplist结构图如下:
- zlbytes:记录整个压缩列表占用的内存字节数;
- zltail:记录列表尾节点距离压缩列表的起始地址的字节数;
- zllen:记录压缩列表中包含的节点数量;
- entryX:节点;
- zlend:标记压缩列表的末端;
quicklist是基于ziplist的linkedlist链表,结合了linkedlist和ziplist的优点;将linkedlist按段切分,每一段用ziplist来紧凑存储,ziplist之间使用双向指针连接;
linkedlist的附加空间(prev、next指针占用16字节)相对太高,且加剧内存的碎片化,影响内存管理效率;
quicklist结构体定义如下:
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32 bytes.
* count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
* attempted_compress: 1 bit, boolean, used for verifying during testing.
* extra: 10 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
* 'sz' is byte length of 'compressed' field.
* 'compressed' is LZF data with total (compressed) length 'sz'
* NOTE: uncompressed length is stored in quicklistNode->sz.
* When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor. */
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
quicklist结构图如下:
#define SIZE_SAFETY_LIMIT 8192
quicklist内部默认单个ziplist长度为8K字节,超过这个字节数,就会新起一个ziplist;
压缩深度
为进一步节约内存空间,Redis还会对ziplist进行压缩存储,使用LZF算法压缩,可以选择压缩深度;quicklist默认的压缩深度是0,也就是不压缩;压缩的实际深度由配置参数list-compress-depth
决定;
hash
创建hash时,底层使用OBJ_ENCODING_ZIPLIST
存储数据,随着数据的增加,底层使用OBJ_ENCODING_HT
,相关配置如下:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
dict结构体定义如下:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *obj);
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
} dictType;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
每个dict有两个dictht,结构图如下:
虽然dict结构体中有两个dictht,但通常情况下只有一个dictht有值;当dict扩容缩容时,需要分配新的dictht,然后进行渐进式搬迁,这时候两个dictht就存储新、旧dictht,搬迁结束后,旧dictht删除;
扩容条件
当hash表中元素个数等于第一维数组的长度时,就会开始扩容;扩容的大小是原数组的两倍;
当Redis在做bgsave
(RDB持久化操作)时,为了减少内存页的过多分离,不会去扩容;当表中元素已经达到第一维数组的长度的5倍,就会强制扩容;
缩容条件
当hash表中元素个数小于第一维数组的长度的10%时,Redis就会对hash表进行缩容来减少第一维数组的空间占用,不考虑是否在做bgsave
;
渐进式rehash
大字典的扩容时比较消耗时间的,对于单线程的Redis,这是无法接受的,所以采用渐进式rehash小步搬迁;
rehash步骤如下:
- 为
dictht[1]
分配空间,让字典同时持有dictht[0]
和dictht[1]
; - 定时维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash开始;
- 在rehash进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将
dictht[0]
中的数据rehash到dictht[1]
中,并将rehashidx加一; - 当
dictht[0]
中所有数据都转移到dictht[1]
中后,将rehashidx设置为-1,表示rehash结束; - 将
dictht[0]
释放,然后将dictht[1]
设置为dictht[0]
,最后为dictht[1]
分配一个空白hash表;
过程如下图:
采用渐进式rehash的好处在于分而治之,避免了集中式rehash带来的庞大计算量;在进行rehash时,只能对dictht[0]
进行查询和删除,对dictht[1]
可进行插入、查询和删除;
set
Redis的set类型的底层数据结构为:OBJ_ENCODING_HT
和OBJ_ENCODING_INTSET
;
当存储的数据同时满足以下条件时,Redis就会采用OBJ_ENCODING_INTSET
实现:
- 存储的数据都是整数;
- 存储的数据元素个数小于set-max-intset-entires(默认512)个;
若不能同时满足这两个条件,Redis就会采用OBJ_ENCODING_HT
实现;
inset结构体定义如下:
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
/* Note that these encodings are ordered, so:
* INTSET_ENC_INT16 < INTSET_ENC_INT32 < INTSET_ENC_INT64. */
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
inset是一个有序集合,查找元素的复杂度为O(logN)(二分法);若集合中全是int16_t类型的整数,当插入一个int32_t类型整数时,为了维持集合中数据类型的一致性,集合中所有数据都会转换成int32_t类型,这会涉及内存的重新分配,此时插入的复杂度就是O(N);inset不支持降级操作;
zset
zset保留了集合中元素不重复的特性,还支持元素排序,会给每个元素设置score
,作为排序的依据;
Redis的zset类型的底层数据结构为:OBJ_ENCODING_ZIPLIST
和OBJ_ENCODING_SKIPLIST
;
ziplist做排序
集合中每个元素使用两个紧挨的压缩列表节点来存储,第一个节点存储元素的内容,第二个节点存储元素的score;如下图:
skiplist
skiplist结构体定义如下:
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
Redis的skiplist结构如下图:
- header:指向跳跃表的表头节点(不存储数据),通过这个指针定位表头节点的时间复杂度为O(1);
- tail:指向跳跃表的表尾节点;
- level:记录目前跳跃表内层数最大的节点的层数(表头节点的层数不计算在内);
- length:记录跳跃表的长度,即跳跃表中包含的节点个数(表头节点不计算在内);
- level[]:节点中用L1、L2标记节点的各个层信息;每个层带有两个属性:前进指针和跨度;
- 前进指针:用于访问位于表尾方向的其它节点;
- 跨度:记录前进指针所指向节点和当前节点的距离;
- backward:后退指针指向当前节点的前一个节点,在从表尾向表头遍历时使用;
- score:分值,跳跃表中,节点按照所保存的分值从小到大排列;
- ele:节点的内容;
stream
Redis的stream类型的底层数据结构为:OBJ_ENCODING_STREAM
;
stream结构体定义如下:
typedef struct streamID {
uint64_t ms; /* Unix time in milliseconds. */
uint64_t seq; /* Sequence number. */
} streamID;
typedef struct stream {
rax *rax; /* The radix tree holding the stream. */
uint64_t length; /* Number of elements inside this stream. */
streamID last_id; /* Zero if there are yet no items. */
rax *cgroups; /* Consumer groups dictionary: name -> streamCG */
} stream;
持久化
Redis是一种内存数据库,一旦服务器进程退出,或者服务区宕机,那么数据库的数据就会全部丢失,为了解决数据丢失问题,Redis提供两种持久化方案:RDB和AOF,将内存中的数据保存到磁盘上;
Redis的持久化可以禁用,也可以同时存在,当Redis重启后,优先使用AOF文件重建数据;
RDB
RDB持久化会在特定的间隔保存当前时间点的数据快照,保存的文件是一个经过压缩的二进制文件,可以通过这个文件还原数据库的状态;可以在redis.conf配置文件中配置执行策略,也可以手动执行;
配置项
# 时间策略 save m n m秒内修改n次key,触发rdb
save 900 1
save 300 10
save 60 10000
# 文件名称
dbfilename dump.rdb
# 文件保存路径
dir /home/work/app/redis/data/
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
过程
在进行RDB时,Redis的主进程不会做IO操作,会fork一个子进程来完成操作:
- Redis调用fork函数,创建子进程,此时父进程无法处理客户端请求;
- 子进程将数据集写入到一个临时RDB文件中,父进程可继续处理客户端的请求;
- 当子进程完成对新RDB文件的写入后,Redis用新RDB文件替换旧RDB文件,并删除旧RDB文件;
Redis借助copy-on-write机制实现子进程进行写操作,fork函数发生时那刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(执行一个写操作),操作系统会将该片数据复制一份以保证子进程的数据不会受到影响,所以新的RDB文件存储的是执行fork那一刻的内存数据;
触发方式
自动触发
- redis.conf配置文件配置RDB持久化规则,其中
save
为自动触发的配置,多个save
配置之间是或的关系; - 从节点全量复制时,主节点发送RDB文件给从节点完成复制操作,主节点会触发
bgsave
命令; - 执行
flushall
命令会触发; - 退出Redis且没有开启AOF时;
手动触发
save
命令是同步的,会占用主进程,造成阻塞;bgsave
命令是异步的,在后台进行持久化时,主进程还可以继续响应客户端请求;
| 命令 | save | bgsave |
|: —: | :—: | :—: |
| IO类型 | 同步 | 异步 |
| 阻塞 | 是 | 是(阻塞发生在fork,通常非常快) |
| 复杂度 | O(N) | O(N) |
| 优点 | 不会消耗额外的内存 | 不阻塞客户端命令 |
| 缺点 | 阻塞客户端的命令 | 需要fork子进程,消耗内存(受益于copy-on-write机制,内存消耗并不大) |
总结
优点
- RDB文件是压缩后的二进制文件,体积小,更适合做备份文件;
- RDB文件恢复数据比AOF格式文件更快;
缺点
- RDB只能保存某个时间点的数据,存在丢失一段时间内数据的风险;
- RDB需要经常fork子进程完成持久化,如果数据集很大,fork可能会比较耗时;
AOF
AOF持久化方式会以日志形式记录每一个写操作,当Redis重启后,记录的写操作会被重放从而重建原来的数据;
配置项
# 默认不开启aof 而是使用rdb的方式
appendonly no
# 默认文件名
appendfilename "appendonly.aof"
# 每次修改都会sync 消耗性能
# appendfsync always
# 每秒执行一次 sync 可能会丢失这一秒的数据
appendfsync everysec
# 不执行 sync ,这时候操作系统自己同步数据,速度最快
# appendfsync no
# 开启自动重写
no-appendfsync-on-rewrite yes
# AOF文件比上次重写后大小增加了100%才出发自动重写
auto-aof-rewrite-percentage 100
# AOF文件至少达到64mb才会触发自动重写
auto-aof-rewrite-min-size 64mb
保存模式
AOF文件追加写分为两步骤:
write
:写入,将aof_buf
写入到AOF文件,都是主线程阻塞完成的;fsync
:保存,将AOF文件保存到磁盘上;
AOF支持三种保存模式,具体如下:
#define AOF_FSYNC_NO 0
#define AOF_FSYNC_ALWAYS 1
#define AOF_FSYNC_EVERYSEC 2
#define CONFIG_DEFAULT_AOF_FSYNC AOF_FSYNC_EVERYSEC
AOF_FSYNC_NO
不主动保存,实际是依赖系统的缓存刷新机制将AOF文件保存到磁盘上,该刷新动作会阻塞主进程;
AOF_FSYNC_ALWAYS
每执行一个写命令保存一次,fsync
是主进程执行的,会阻塞主进程;
AOF_FSYNC_EVERYSEC
fsync
动作是异步执行,不会阻塞主进程;原则上每一秒保存一次;
AOF重写
AOF采用文件追加方式,文件会越来越大,为避免出现此情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集;可以使用命令bgrewriteaof
主动触发AOF重写;
过程
fork
子进程来重写AOF文件(fork
会阻塞主进程),先写新AOF文件,最后再替换原AOF文件;- 子进程通过
set
命令将子进程的内存中数据重写到新AOF文件,类似于快照; - 父进程fork出子进程后,会继续处理客户端请求,若有新写请求,会:
- 将写请求追加到现有的(旧)AOF文件中;
- 将写请求追加到AOF重写缓存中;
这样可以保证AOF重写期间服务异常,数据不会丢失;
- 子进程完成AOF重写后,向父进程发送完成信号;
- 父进程接收到完成信号后,会调用信号处理函数(阻塞主线程),做:
- 将AOF重写缓存中的内容全部写入到新AOF文件中;
- 对新的AOF文件重命名,替换原AOF文件;
触发条件
服务器在 AOF 功能开启的情况下, 会维持以下三个变量:
- 记录当前 AOF 文件大小的变量
aof_current_size
; - 记录最后一次 AOF 重写之后, AOF 文件大小的变量
aof_rewrite_base_size
; - 增长百分比变量
aof_rewrite_perc
;
每次当 serverCron
函数执行时,它都会检查以下条件是否全部满足,如果是的话,就会触发自动的 AOF重写:
- 没有
BGSAVE
命令在进行; - 没有
BGREWRITEAOF
在进行; - 当前AOF文件大小大于
server.aof_rewrite_min_size
; - 当前AOF文件大小和最后一次AOF重写后的大小之间的比率大于等于指定的增长百分比;
触发方式
- 手动触发:
bgrewriteaof
- 自动触发:根据配置规则触发;
总结
优点
- 数据安全,AOF持久化可以配置
appendfsync
为always
; - 通过append模式追加写文件;
- AOF机制的rewrite模式,在AOF文件被rewrite之间,可以删除其中的某些命令,如误操作的
flushall
;
缺点
- 相同数据集的数据AOF文件要远大于RDB文件;
- 重启恢复速度慢于RDB;
混合
Redis4.0支持混合持久化,前提是开启AOF重写;AOF重写时,将当时内存的数据以RDB快照形式写到新AOF文件,再将AOF重写缓存中的命令追加写入新AOF文件,最后用新AOF文件替换旧AOF文件;这样Redis重启,会先加载AOF文件中的RDB快照,再重放增量的AOF日志,效率大幅提升;
混合持久化AOF文件结构如下:
appendonly.aof |
---|
RDB格式 |
AOF格式 |
配置项
aof-use-rdb-preamble yes
主从复制
将一台Redis服务器(主)的数据复制到其它Redis服务器(从);
主机数据更新后根据配置和策略,自动同步到备机的机制,Master以写为主,Slave以读为主;
数据冗余:实现了数据的热备份;
故障恢复:当主节点出现问题后,可以由从节点提供服务;
负载均衡:配合读写分离,由主节点提供写服务,从节点提供读服务;提升Redis服务的并发量;
全量复制:发生在第一次复制时;
增量复制:只会把主从库网络断连期间主库收到的命令,同步给从库;
全量复制
- 在Slave启动后并连接到Master之后,Slave会主动发送一个SYNC命令;
- Master收到SYNC命令后,会开始在后台保存快照,并将保存快照期间接收到的命令缓存起来;当快照完成后Master会将快照文件和所有缓存的命令发送给Slave;
- Slave收到后,会载入快照文件并执行收到的缓存命令,该过程成为复制初始化(全量同步);
- 复制初始化后,Master执行的任何会导致数据发生变化的命令都会异步地传送给Slaves,从而保证主从数据库数据一致,这个过程称为复制同步阶段(增量同步);
- 复制同步阶段会贯穿整个主从同步过程的始终,直到主从关系终止;
- 复制同步阶段中从数据库并不会阻塞,可以继续处理客户端发来的请求,默认情况下从数据库会用同步之前的数据进行响应,可以配置
slave-server-stale-data no
使从数据库在同步完成之前对所有命令都回复错误SYNC with master in progress
;
增量复制
Redis 2.8版之后,从数据库发送的是PSYNC命令,格式为PSYNC 主数据库的运行ID 断开前最新的命令偏移量
;主数据库收到PSYNC
命令后,会:
- 会首先判断从数据库传送来的运行ID是否和自己的运行ID一致,确保从数据库之前确实和本库同步的,避免从数据库拿到错误的数据;
- 判断从数据库最后同步成功的命令偏移量是否在积压队列中,如果在则可以执行增量复制,并将积压队列中相应的命令发送给从数据库;默认积压队列的大小为 1MB,可以通过配置文件
repl-backlog-size
调整;积压队列越大,允许的主从数据库断线的时间就越长;repl-backlog-ttl
表示当所有从数据库与主数据库断开连接后,经过多长时间可以释放积压队列的内存空间,默认为 1小时 - 如果此次重连不满足增量复制的条件,主数据库会进行一次全量同步;
事务
Redis事务提供一种将多个命令打包然后一次性按顺序执行的机制,在事务执行期间,不会主动中断,执行完事务中所有命令后,才会继续处理其它客户端的其它命令;
事务执行时,对于命令的不同错误的处理方式如下:
- 命令语法错误(编译时错误),所有命令都不执行;
- 命令逻辑错误(运行时错误),其它命令正常执行,这种情况下不保证事务的原子性;
Redis不支持回滚来保证原子性的原因如下:
- 逻辑错误的命令应该在开发的过程中被发现,而不应该出现在生产环境;
- 这类错误通常不会在生产环境发生,不对回滚支持可以保持内部简单且快速;
Redis使用watch key监控指定数据,watch保证事务只能在所有被监视键都没有被修改的前提下执行,如果这个前提不满足的话,事务就不会被执行;
watch执行流程如下:
缓存淘汰策略
当Redis内存超出物理内存限制时,内存数据就会和磁盘产生频繁交换,使得Redis性能急剧下降;所以如何淘汰无用数据释放空间来存储新数据就变得尤为重要;
在生产环境中,采用配置参数maxmemory
的方式来限制内存大小,到实际存储内存超过maxmemory
时,就采取Redis内存淘汰策略;
淘汰策略
Redis支持的淘汰策略定义如下:
configEnum maxmemory_policy_enum[] = {
{"volatile-lru", MAXMEMORY_VOLATILE_LRU},
{"volatile-lfu", MAXMEMORY_VOLATILE_LFU},
{"volatile-random",MAXMEMORY_VOLATILE_RANDOM},
{"volatile-ttl",MAXMEMORY_VOLATILE_TTL},
{"allkeys-lru",MAXMEMORY_ALLKEYS_LRU},
{"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU},
{"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM},
{"noeviction",MAXMEMORY_NO_EVICTION},
{NULL, 0}
};
- volatile-lru:从设置过期时间的数据集中挑选出最近最少使用的数据淘汰;
- volatile-lfu:从设置过期时间的数据集中挑选出使用频率最低的数据淘汰;
- volatile-random:从设置过期时间的数据集中任意选择数据淘汰;
- volatile-ttl:从设置过期时间的数据集中挑选出将要过期的数据淘汰,TTL值越大越优先被淘汰;
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰,面向所有的key;
- allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰,面向所有的key;
- allkeys-random:从数据集中选择任意数据淘汰,面向所有的key;
- noenviction:默认,禁止驱逐数据;当内存不足时,新写入操作就会报错,读请求正常执行;
淘汰机制
- LRU:Least recently used(最近最少使用)
根据数据的历史访问记录来进行淘汰数据,在服务器配置中保存了lru计数器server.lrulock
,会定时(redis定时程序serverCorn()
)更新;server.lrulock
的值是根据server.unixtime
计算出来进行排序,然后选择最近使用时间最久的数据进行删除;在Redis中,LRU算法是一个近似算法,默认情况下,Redis会随机挑选5个键,并从中选择一个最久未使用的key进行淘汰; - LFU
挑选使用频率最低的数据淘汰; - TTL淘汰
Redis数据集数据结构中保存了键值对过期时间的表,即redisDb.expires
,与LRU淘汰机制类似,TTL机制会先从过期时间的表中随机挑选几个键值对,取出其中的TTL最大的键值对淘汰; - 随机淘汰
随机找hash桶,再次hash指定位置的dictEntry;
缓存穿透、击穿、雪崩
缓存穿透
问题描述
请求缓存和数据库都没有的数据,查询数据量巨大,引起数据库压力过大;
解决方案
- 接口层增加校验,过滤无效请求;
- 缓存无效key,设置短点的过期时间;
- 布隆过滤器,用于快速判某个元素是否存在于集合中;
缓存击穿
问题描述
某个Key非常热点,访问非常频繁,处于高并发访问的情况;当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,引起数据库压力瞬间增大,造成过大压力;
解决方案
- 设置热点数据永远不过期;
- 接口限流与熔断,降级;重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制;
- 基于zookeeper、redis实现分布式锁,等待第一个请求构建完缓存,再释放锁;
缓存雪崩
问题描述
缓存同一时间大面积失效,但是查询数据量巨大,引起数据库压力过大;
和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了;
解决方案
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生;
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中;
- 设置热点数据永远不过期;
- 事前:选择合适的内存淘汰策略,尽量保证redis集群高可用;
- 事中:本地encache缓存+hystrix限流&降级;
- 事后:利用redis持久化机制保存的数据尽快恢复缓存;
一致性哈希算法DHT
简介
有效地解决分布式存储结构下动态增加和删除节点所带来的问题;
过程
- 把全量的缓存空间当作一个环形存储结构,环形空间可以分为2^32个缓存区,Redis中则把缓存key分配到16384个slot中;
- 每一个缓存key通过Hash算法转化成一个32位二进制数,对应着环形空间的某一个缓存区;
- 每一个缓存节点也遵循同样的Hash算法,比如利用IP做Hash,映射到环形空间中;
- 每一个缓存key顺时针找到最近的节点,这个节点就是这个key归属的存储节点;
效果
- 增加节点时,为了保持一致性哈希的顺时针规则,只会有一小部分key的归属受到影响,将受影响的key的缓存数据迁移到新节点(查询时未命中而刷新缓存);
- 删除节点时,同样会有一小部分key的归属受到影响,将受影响的key的缓存数据迁移到新节点(查询时未命中而刷新缓存);
虚拟节点
为解决优化节点太少产生的不均衡情况而引入的概念;
基于原来物理节点映射出N个虚拟子节点,代替物理节点映射到环形空间上;
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/4768.html