Redis是基于内存数据库,操作效率高,提供丰富的数据结构(Redis底层对数据结构还做了优化),可用作数据库,缓存,消息中间件等。如今广泛用于互联网大厂,面试必考点之一,本文从数据结构,到集群,到常见问题逐步深入了解Redis,看完再也不怕面试官提问!

高性能之道

  1. 单线程模型
  2. 基于内存操作
  3. epoll多路复用模型
  4. 高效的数据存储结构

redis的单线程指的是数据处理使用的单线程,实际上它主要包含

  1. IO线程:处理网络消息收发
  2. 主线程:处理数据读写操作,包括事务、Lua脚本等
  3. 持久化线程:执行RDB或AOF时,使用持久化线程处理,避免主线程的阻塞
  4. 过期键清理线程:用于定期清理过期键

至于redis为什么使用单线程处理数据,是因为redis基于内存操作,并且有高效的数据类型,它的性能瓶颈并不在CPU计算,主要在于网络IO,而网络IO在后来的版本中也被独立出来了IO线程,因此它能快速处理数据,单线程反而避免了多线程所带来的并发和资源争抢的问题

全局数据存储

Redis底层存储基于全局Hash表,存储结构和Java的HashMap类似(数组+链表方式)

rehash

Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash

  1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
  2. 把哈希表 1 中的数据重新进行打散映射到hash表2中;这个过程采用渐进式hash

    即拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries
  3. 释放哈希表 1 的空间。

数据类型

查看存储编码类型:object encoding key

1. string

源码位置:t_string.c

string是最常用的类型,它的底层存储结构是SDS

存储结构

redis的string分三种情况对对象编码,目的是为了节省内存空间:

robj *tryObjectEncodingEx(robj *o, int try_trim)
  1. if: value长度小于20字节且可以转换为整数(long类型),编码为OBJ_ENCODING_INT,其中若数字在0到10000之间,还可以使用内存共享的数字对象
  2. else if: 若value长度小于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(44字节),编码为OBJ_ENCODING_EMBSTR
  3. else: 保持编码为OBJ_ENCODING_RAW

常用命令

SET key value
MSET key value [key value ...]
SETNX key value #常用作分布式锁
GET key
MGET key [key ...]
DEL key [key ...]
EXPIRE key seconds
INCR key
DECR key
INCRBY key increment
DECRBY key increment

常用场景

  • 简单键值对
  • 自增计数器

INCR作为主键的问题

  • 缺陷:若数据量大的情况下,大量使用INCR来自增主键会让redis的自增操作频繁,影响redis的正常使用
  • 优化:每台服务可以使用INCRBY一次性获取一百或者一千或者多少个id段来慢慢分配,这样能大量减少redis的incr命令所带来的消耗

2. list

源码位置:t_list.c

存储结构

redis的list首先会按紧凑列表存储(listPack),当紧凑列表的长度达到list_max_listpack_size之后,会转换为双向链表

// 1.LPUSH/RPUSH/LPUSHX/RPUSHX这些命令的统一入口
void pushGenericCommand(client *c, int where, int xx)
// 2.追加元素,并尝试转换紧凑列表
void listTypeTryConversionAppend(robj *o, robj **argv, int start, int end, beforeConvertCB fn, void *data)
// 3.尝试转换紧凑列表
static void listTypeTryConversionRaw(robj *o, list_conv_type lct, robj **argv, int start, int end, beforeConvertCB fn, void *data)
// 4.尝试转换紧凑列表
// 若紧凑列表的长度达到list_max_listpack_size之后,则转换
static void listTypeTryConvertQuicklist(robj *o, int shrinking, beforeConvertCB fn, void *data)

当redis进行list元素移除时

// 1.移除list元素的统一入口
void listElementsRemoved(client *c, robj *key, int where, robj *o, long count, int signal, int *deleted)
// 2.尝试转换
void listTypeTryConversion(robj *o, list_conv_type lct, beforeConvertCB fn, void *data)
// 3.尝试转换
static void listTypeTryConversionRaw(robj *o, list_conv_type lct, robj **argv, int start, int end, beforeConvertCB fn, void *data)
// 4.尝试转换双向链表
// 若双向链表中只剩一个节点,且是压缩节点,则对双向链表转换为紧凑列表
static void listTypeTryConvertQuicklist(robj *o, int shrinking, beforeConvertCB fn, void *data)

以下参数可在redis.conf配置

list_max_listpack_size:默认-2

常用命令

LPUSH key value [value ...]
RPUSH key value [value ...]
LPOP key
RPOP key
LRANGE key start stop
BLPOP key [key ...] timeout #从key列表头弹出一个元素,若没有元素,则阻塞等待timeout秒,0则一直阻塞等待
BRPOP key [key ...] timeout #从key列表尾弹出一个元素,若没有元素,则阻塞等待timeout秒,0则一直阻塞等待

组合数据结构

根据list的特性,可以组成实现以下常用的数据结构

  • Stack(栈):LPUSH + LPOP
  • Queue(队列):LPUSH + RPOP
  • Blocking MQ(阻塞队列):LPUSH + BRPOP

redis实现数据结构的意义在于分布式环境的实现

常用场景

  • 缓存有序列表结构
  • 构建分布式数据结构(栈、队列等)

3. hash

源码位置:t_hash.c

存储结构

redis的hash首先会按紧凑列表存储(listPack),当紧凑列表的长度达到hash_max_listpack_entries或添加的元素大小超过hash_max_listpack_value之后,会转换为Hash表

// 1.添加hash元素
void hsetCommand(client *c)
void hsetnxCommand(client *c)
// 2.尝试转换Hash表
// 若紧凑列表的长度达到hash_max_listpack_entries
// 或添加的元素大小超过hash_max_listpack_value
// 则进行转换
void hashTypeTryConversion(robj *o, robj **argv, int start, int end)
// 3.尝试转换Hash表
void hashTypeConvert(robj *o, int enc)
// 4.转换Hash表
void hashTypeConvertListpack(robj *o, int enc)

以下参数可在redis.conf配置

hash_max_listpack_value:默认64

hash_max_listpack_entries:默认512

常用命令

HSET key field value
HSETNX key field value
HMSET key field value [field value ...]
HGET key field
HMGET key field [field ...]
HDEL key field [field ...]
HLEN key
HGETALL key
HINCRBY key field increment

常用场景

  • 对象缓存

4. set

源码位置:t_set.c

存储结构

  1. redis的set添加元素时,若存储对象是整形数字且集合小于set_max_intset_entries,则存储为OBJ_ENCODING_INTSET,若集合长度小于set_max_listpack_entries时,存储为紧凑列表。否则,存储为Hash表
// 1.添加set元素
void saddCommand(client *c)
// 2.1.创建set表
// 若存储对象是整形数字且集合小于set_max_listpack_entries,则存储为OBJ_ENCODING_INTSET
// 若集合长度小于set_max_listpack_entries时,存储为紧凑列表
// 否则存储为Hash表
robj *setTypeCreate(sds value, size_t size_hint)
// 2.2 尝试转换set表
// 如果编码是OBJ_ENCODING_LISTPACK(紧凑列表),且集合长度大于set_max_listpack_entries
// 或编码是OBJ_ENCODING_INTSET(整形集合),且集合长度大于set_max_intset_entries
// 则进行转换为Hash表
void setTypeMaybeConvert(robj *set, size_t size_hint)
// 2.3 添加元素
int setTypeAdd(robj *subject, sds value)
int setTypeAddAux(robj *set, char *str, size_t len, int64_t llval, int str_is_sds)
// 2.4 若整形数组添加元素,长度超过set_max_intset_entries,则转换为Hash表
static void maybeConvertIntset(robj *subject)

以下参数可在redis.conf配置

set_max_intset_entries:默认512

set_max_listpack_entries:默认128

常用命令

SADD key member [member ...]
SREM key member [member ...]
SMEMBERS key
SCARD key
SISMEMBERS key member
SRANDMEMBER key [count]
SPOP key [count]
SRANDOMEMBER key [count]
SINTER key [key ...] #交集运算
SINTERSTORE destination key [key ...] #将交集结果存入新集合destination
SUNION key [key ...] #并集运算
SUNIONSTORE destination key [key ...] #将并集结果存入新集合destination
SDIFF key [key ...] #差集运算
SDIFFSTORE destination key [key ...] #将差集结果存入新集合destination

常用场景

  • 缓存无序集合
  • 需要求交集并集差集的场景

5. sortedset

源码位置:t_zset.c

存储结构

根据情况可能创建紧凑列表或跳表

// 1.添加元素
void zaddCommand(client *c)
void zaddGenericCommand(client *c, int flags)
// 2.1 创建元素
// 若集合长度<=zset_max_listpack_entries 并且值的长度<=zset_max_listpack_value,则创建紧凑列表
// 否则创建跳表节点
robj *zsetTypeCreate(size_t size_hint, size_t val_len_hint)
// 2.2 添加元素
// 若集合是紧凑列表,且集合元素超过zset_max_listpack_entries
// 或当前添加的元素长度超过zset_max_listpack_value
// 则将紧凑列表转换为跳表
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore)

以下参数可在redis.conf配置

zset_max_listpack_entries:默认128

zset_max_listpack_value:默认64

跳表仅在以下情况转换回压缩列表

  1. 使用命令georadius时,判断元素长度若小于等于zset_max_listpack_entries,并且最大元素的长度小于等于zset_max_listpack_value
void georadiusGeneric(client *c, int srcKeyIndex, int flags)
  1. 使用命令zunion/zinter/zdiff命令(求并集交集差集)时,判断元素长度若小于等于zset_max_listpack_entries,并且最大元素的长度小于等于zset_max_listpack_value
void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIndex, int op, int cardinality_only)

常用命令

ZADD key score member [[score member]...]
ZREM key member [member ...]
ZSCORE key member
ZINCRBY key increment member
ZCARD key
ZRANGE key start stop [WITHSCORES]
ZREVRANGE key start stop [WITHSCORES]
ZUNIONSTORE destkey numkeys key [key ...] # 并集计算
ZINTERSTORE destkey numkeys key [key ...] # 交集计算

常用场景

  • 排行榜

底层数据结构

RedisObject

源码位置:server.h

{
unsigned type:4;//类型 五种对象类型
unsigned encoding:4;//编码
void *ptr;//指向底层实现数据结构的指针
int refcount;//引用计数
unsigned lru:24;//记录最后一次被命令程序访问的时间
}robj;
  • type :表示对象的类型,占4个比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
  • encoding:占4个比特,Redis支持的每种类型,都有至少两种内部编码,例如对于字符串,有int、embstr、raw三种编码。通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率。以列表对象为例,有紧凑列表双端链表两种编码方式;如果列表中的元素较少,Redis倾向于使用紧凑列表进行存储,因为紧凑列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,紧凑列表就会转化为更适合存储大量元素的双端链表
  • ptr:指针指向具体的数据。
  • refcount:记录的是该对象被引用的次数,类型为整型。主要用于对象的引用计数内存回收Redis中被多次使用的对象(refcount>1),称为共享对象。Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象。Redis服务器在初始化时,会创建10000个字符串对象,值分别是0~9999的整数值;
  • lru:Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。
    • 在 LRU 算法中,Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
    • 在 LFU 算法中,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。
  • 一个redisObject对象的大小为16字节:4bit+4bit+24bit+4Byte+8Byte=16Byte

SDS 简单动态字符串(Simple Dynamic String)

源码位置:sds.h

typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr5 { // 对应的字符串长度小于 1<<5 32字节
unsigned char flags; /* 3 lsb of type, and 5 msb of string length intembstr*/
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { // 对应的字符串长度小于 1<<8 256
uint8_t len; /* used */ //目前字符创的长度 用1字节存储
uint8_t alloc; //已经分配的总长度 用1字节存储
unsigned char flags; //flag用3bit来标明类型,类型后续解释,其余5bit目前没有使用 embstr raw
char buf[]; //柔性数组,以'\0'结尾
};
struct __attribute__ ((__packed__)) sdshdr16 { // 对应的字符串长度小于 1<<16
uint16_t len; /*已使用长度,用2字节存储*/
uint16_t alloc; /* 总长度,用2字节存储*/
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 { // 对应的字符串长度小于 1<<32
uint32_t len; /*已使用长度,用4字节存储*/
uint32_t alloc; /* 总长度,用4字节存储*/
unsigned char flags;/* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__ ((__packed__)) sdshdr64 { // 对应的字符串长度小于 1<<64
uint64_t len; /*已使用长度,用8字节存储*/
uint64_t alloc; /* 总长度,用8字节存储*/
unsigned char flags; /* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};

字符串类型的内部编码有3种

  • int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示。
  • embstr:**<=44字节的字符串embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时整个redisObject和sds都需要重新分配空间**,因此redis中的embstr实现为只读。
  • raw:大于44个字节的字符串

embstr和raw进行区分的长度,是44;是因为redisObject的长度是16字节sds的长度是4+字符串长度;因此当字符串长度是44时,embstr的长度正好是16+4+44 =64,jemalloc正好可以分配64字节的内存单元。

压缩列表zipList

ziplist 被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,不仅可以利用 CPU 缓存,而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销。

ziplist 是一个特殊双向链表,不像普通的链表使用前后指针关联在一起,它是存储在连续内存上的。

/* 创建一个空的 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;
}

  1. zlbytes: 32 位无符号整型,记录 ziplist 整个结构体的占用空间大小。当然了也包括 zlbytes 本身。这个结构有个很大的用处,就是当需要修改 ziplist 时候不需要遍历即可知道其本身的大小。 这和SDS中记录字符串的长度有相似之处。
  2. zltail: 32 位无符号整型, 记录整个 ziplist 中最后一个 entry 的偏移量。所以在尾部进行 POP 操作时候不需要先遍历一次。
  3. zllen: 16 位无符号整型, 记录 entry 的数量, 所以只能表示 2^16。但是 Redis 作了特殊的处理:当实体数超过 2^16 ,该值被固定为 2^16 - 1。 所以这种时候要知道所有实体的数量就必须要遍历整个结构了。
  4. entry: 真正存数据的结构。
  5. zlend: 8 位无符号整型, 固定为 255 (0xFF)。为 ziplist 的结束标识。

zipList缺陷

ziplist 在更新或者新增时候,如空间不够则需要对整个列表进行重新分配。当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

ziplist 节点的 prevlen 属性会根据前一个节点的长度进行不同的空间大小分配:

  • 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值。
  • 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值。

假设有这样的一个 ziplist,每个节点都是等于 253 字节的。新增了一个大于等于 254 字节的新节点,由于之前的节点 prevlen 长度是 1 个字节。

为了要记录新增节点的长度所以需要对节点 1 进行扩展,由于节点 1 本身就是 253 字节,再加上扩展为 5 字节的 pervlen 则长度超过了 254 字节,这时候下一个节点又要进行扩展了

zipList特性

  1. ziplist 为了节省内存,采用了紧凑的连续存储。所以在修改操作下并不能像一般的链表那么容易,需要从新分配新的内存,然后复制到新的空间。
  2. ziplist 是一个双向链表,可以在时间复杂度为 O(1) 从下头部、尾部进行 pop 或 push。
  3. 新增或更新元素可能会出现连锁更新现象。
  4. 不能保存过多的元素,否则查询效率就会降低。

紧凑列表listPack

Redis7.0之后采用listPack全面替代zipList

在 Redis5.0 出现了 listpack,目的是替代压缩列表,其最大特点是 listpack 中每个节点不再包含前一个节点的长度,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。

unsigned char *lpNew(size_t capacity) {
unsigned char *lp = lp_malloc(capacity > LP_HDR_SIZE+1 ? capacity : LP_HDR_SIZE+1);
if (lp == NULL) return NULL;
lpSetTotalBytes(lp,LP_HDR_SIZE+1);
lpSetNumElements(lp,0);
lp[LP_HDR_SIZE] = LP_EOF;
return lp;
}

  1. listpack 中每个节点不再包含前一个节点的长度,避免连锁更新的隐患发生。
  2. listpack 相对于 ziplist,没有了指向末尾节点地址的偏移量,解决 ziplist 内存长度限制的问题。但一个 listpack 最大内存使用不能超过 1GB。

跳表

数组:查询快,插入删除慢

链表:查询慢,插入删除快

跳表:跳表是基于链表的一个优化,在链表的插入删除快的特性之上,也增加了它的查询效率。它是将有序链表改造为支持折半查找算法,它的插入、删除、查询都很快

跳表缺陷:需要额外空间来建立索引层,以空间换时间,因此zset一开始是以紧凑列表存储,后续才会转换为跳表

  • 跳表的创建(添加元素时)

    1. 当前zset不存在时,若添加元素时集合长度达到zset_max_listpack_entries,或添加的最后一个元素的大小超过zset_max_listpack_value,则直接创建跳表,跳表头结点创建最大层数(ZSKIPLIST_MAXLEVEL:32)的索引,并插入跳表当前添加的元素
    2. 当前zset存在时,判断若元素长度超过zset_max_listpack_entries,则将紧凑列表转换为跳表,跳表头结点创建最大层数(ZSKIPLIST_MAXLEVEL:32)的索引,然后把其他元素依次插入跳表
  • 跳表的查询

    从起始节点开始,通过多级索引进行折半查找,最终找到需要的数据
  • 跳表的插入

    先通过折半查找找到节点对应要插入的链表位置,然后通过随机得到一个要插入的节点的索引层数,然后插入节点,并构建对应的多级索引
  • 跳表的删除

    先通过折半查找找到要删除的节点的链表位置,删除节点,并删除对应的多级索引

淘汰策略

  1. noeviction(默认策略): 不会删除任何数据,拒绝所有写入操作并返回客户端错误消息(error)OOM command not allowed when used memory,此时 Redis 只响应删和读操作;
  2. allkeys-lru: 从所有 key 中使用 LRU(Least Recently Used)算法进行淘汰(LRU 算法:最近最少使用算法);
  3. allkeys-lfu: 从所有 key 中使用 LFU(Least Frequently Used)算法进行淘汰(LFU 算法:最不常用算法,根据使用频率计算,4.0 版本新增);
  4. volatile-lru: 从设置了过期时间的 key 中使用 LRU 算法进行淘汰;
  5. volatile-lfu: 从设置了过期时间的 key 中使用 LFU 算法进行淘汰;
  6. allkeys-random: 从所有 key 中随机淘汰数据;
  7. volatile-random: 从设置了过期时间的 key 中随机淘汰数据;
  8. volatile-ttl: 在设置了过期时间的key中,淘汰过期时间剩余最短的。

Redis的LRU实现

由于Redis 主要运行在单个线程中,它采用的是一种近似的 LRU 算法,而不是传统的完全 LRU 算法(没有把所有key组织为链表)。这种实现方式在保证性能的同时,仍然能够有效地识别并淘汰最近最少使用的键。当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。

Redis的LFU实现

Redis 在访问 key 时,对 logc进行变化:

  • 先按照上次访问距离当前的时长,来对 logc 进行衰减;
  • 再按照一定概率增加 logc 的值

redis.conf 提供了两个配置项,用于调整 LFU 算法从而控制 logc 的增长和衰减:

  • lfu-decay-time 用于调整 logc 的衰减速度,它是一个以分钟为单位的数值,默认值为1,lfu-decay-time 值越大,衰减越慢;
  • lfu-log-factor 用于调整 logc 的增长速度,lfu-log-factor 值越大,logc 增长越慢

删除策略

redis的key过期删除策略采用惰性删除+定期删除实现:

  • 惰性删除:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key

Redis 的惰性删除策略由 db.c 文件中的 expireIfNeeded 函数实现,代码如下:

int expireIfNeeded(redisDb *db, robj *key) {
// 判断 key 是否过期
if (!keyIsExpired(db,key)) return 0;
....
/* 删除过期键 */
....
// 如果 server.lazyfree_lazy_expire 为 1 表示异步删除,反之同步删除;
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
  • 定期删除:定期删除策略的做法是,每隔一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中的过期key

在 Redis 中,默认每秒进行 10 次过期检查一次数据库,此配置可通过 Redis 的配置文件 redis.conf 进行配置,配置键为 hz 它的默认值是 hz 10;定期删除的实现在 expire.c 文件下的 activeExpireCycle 函数中,其中随机抽查的数量由 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 定义的,它是写死在代码中的,数值是 20;也就是说,数据库每轮抽查时,会随机选择 20 个 key 判断是否过期。

管道Pipeline

redis提供pipeline,可以让客户端一次发送一连串的命令给服务器执行,然后再返回执行结果

  • 应用场景:

    • 需要多次执行一连串的redis命令,且命令之间没有依赖的场景
  • 缺陷:
    1. 不保证原子性,pipeline拿到命令只管串行执行,不管执行成功与否,也没有回滚机制
    2. pipeline在执行过程中无法知道执行结果,只有全部执行结束才会返回全部结果
    3. pipeline也不宜一次性发送过多命令,尽管节省了IO,但在redis端也依然会进行执行队列顺序执行

使用示例

/**
* 一次io获取个值
*
* @param redisKeyEnum
* @param ids
* @param clz
* @param <T>
* @param <E>
* @return
*/
public <T, E extends T> List<T> multiGet(RedisKeyEnum redisKeyEnum, List<String> ids, Class<E> clz) {
ShardRedisConnectionFactory factory = getShardRedisConnectionFactory(redisKeyEnum);
ShardedJedis shardedJedis = factory.getConnection();
return execute(factory, shardedJedis, new Supplier<List<T>>() {
@Override
public List<T> get() {
// 1.获取管道
ShardedJedisPipeline pipeline = shardedJedis.pipelined();
List<T> list = new ArrayList<>();
List<Response<String>> respList = new ArrayList<>();
for (String id : ids) {
String key = getKey(redisKeyEnum, id);
// 2.通过管道执行命令
Response<String> resp = pipeline.get(key);
respList.add(resp);
}
// 3.统一提交命令
pipeline.sync();
for (Response<String> resp : respList) {
// 4.遍历获取全部的命令执行返回结果
String result = resp.get();
if (result == null) {
continue;
}
if (clz.equals(String.class)) {
list.add((E) result);
} else {
list.add(JsonUtil.json2Obj(result, clz));
}
}
return list;
}
});
}

事务

Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

事务的命令:

  • MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
  • EXEC:执行事务中的所有操作命令。
  • DISCARD:取消事务,放弃执行事务块中的所有命令。
  • WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH:取消WATCH对所有key的监视。

redis事务在编译错误可以回滚,而运行时错误不能回滚,简单说,redis事务不支持回滚

Redis的持久化

redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。

  • RDB,简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上;
  • AOF,则是换了一个角度来实现持久化,那就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。AOF类似MySQL的binlog

其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF方式来进行数据恢复,这是因为AOF方式的数据恢复完整度更高。

如果你没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库

1. AOF

AOF日志是一种追加式持久化方式,它记录了每个写操作命令,以追加的方式将命令写入AOF文件。通过重新执行AOF文件中的命令,可以重建出数据在内存中的状态。AOF日志提供了更精确的持久化,适用于需要更高数据安全性和实时性的场景。

优点:

  • AOF日志可以实现更精确的数据持久化,每个写操作都会被记录。
  • 在AOF文件中,数据可以更好地恢复,因为它保存了所有的写操作历史。
  • AOF日志适用于需要实时恢复数据的场景,如秒级数据恢复要求。

缺点:

  • AOF日志相对于RDB快照来说,可能会占用更多的磁盘空间,因为它是记录每个写操作的文本文件。
  • AOF日志在恢复大数据集时可能会比RDB快照慢,因为需要逐条执行写操作。

根据不同的需求,可以选择RDB快照、AOF日志或两者结合使用。你可以根据数据的重要性、恢复速度要求以及磁盘空间限制来选择合适的持久化方式。有时候,也可以通过同时使用两种方式来提供更高的数据保护级别。

2. RDB

RDB快照是一种全量持久化方式,它会周期性地将内存中的数据以二进制格式保存到磁盘上的RDB文件。RDB文件是一个经过压缩的二进制文件,包含了数据库在某个时间点的数据快照。RDB快照有助于实现紧凑的数据存储,适合用于备份和恢复。

优点:

  • RDB快照在恢复大数据集时速度较快,因为它是全量的数据快照。
  • 由于RDB文件是压缩的二进制文件,它在磁盘上的存储空间相对较小。
  • 适用于数据备份和灾难恢复。

缺点:

  • RDB快照是周期性的全量持久化,可能导致某个时间点之后的数据丢失。
  • 在保存快照时,Redis服务器会阻塞,可能对系统性能造成影响。

发布订阅

Redis提供了基于“发布/订阅”模式的消息机制。此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel) 发布消息,订阅该频道的每个客户端都可以收到该消息。结构如下:

该消息通信模式可用于模块间的解耦

# 订阅消息
subscribe channel [channel ...]
# 发布消息
publish channel "hello"
# 按模式订阅频道
psubscribe pattern [pattern ...]
# 退订频道
unsubscribe pattern [pattern ...]
# 按模式退订频道
punsubscribe pattern [pattern ...]

Redis发布订阅与消息队列的区别

  1. 消息队列可以支持多种消息协议,但 Redis 没有提供对这些协议的支持;
  2. 消息队列可以提供持久化功能,但 Redis无法对消息持久化存储,一旦消息被发送,如果没有订阅者接收,那么消息就会丢失
  3. 消息队列可以提供消息传输保障,当客户端连接超时或事务回滚等情况发生时,消息会被重新发送给客户端,Redis 没有提供消息传输保障。
  4. 发布订阅消息量过多过频繁,也会占用redis的内存空间,挤占业务逻辑key的空间(可以通过放到不同redis解决)

Redis集群模式

redis集群主要有三种模式:主从复制,哨兵模式和Cluster

主从复制

主从复制模式中包含一个主数据库实例(master)与一个或多个从数据库实例(slave)

工作机制

  1. slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照,并使用缓冲区记录保存快照这段时间内执行的写命令
  2. master将保存的快照文件发送给slave,并继续记录执行的写命令
  3. slave接收到快照文件后,加载快照文件,载入数据
  4. master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
  5. master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性

主从复制配置

replicaof 127.0.0.1 6379 # master的ip,port
masterauth 123456 # master的密码
replica-serve-stale-data no # 如果slave无法与master同步,设置成slave不可读,方便监控脚本发现问题

优缺点

优点

  1. master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
  2. master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求

缺点

  1. 不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复
  2. master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
  3. 难以支持在线扩容,Redis的容量受限于单机配置

哨兵模式

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

哨兵配置

  1. 主从服务器配置
# 使得Redis服务器可以跨网络访问
bind 0.0.0.0
# 设置密码
requirepass "123456"
# 指定主服务器,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
slaveof 192.168.11.128 6379
# 主服务器密码,注意:有关slaveof的配置只是配置从服务器,主服务器不需要配置
masterauth 123456
  1. 配置哨兵

    在Redis安装目录下有一个sentinel.conf文件,copy一份进行修改
# 禁止保护模式
protected-mode no
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 192.168.11.128 6379 2
# sentinel author-pass定义服务的密码,mymaster是服务名称,123456是Redis服务器密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster 123456
  1. 启动服务器和哨兵
# 启动Redis服务器进程
./redis-server ../redis.conf
# 启动哨兵进程
./redis-sentinel ../sentinel.conf

Cluster模式

哨兵模式解决了主从复制不能自动故障转移,达不到高可用的问题,但还是存在难以在线扩容,Redis容量受限于单机配置的问题。

Cluster模式实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题

Cluster特点

  1. 无中心结构:所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
  2. 分布式存储:Redis Cluster将数据分散存储在多个节点上,每个节点负责存储和处理其中的一部分数据。这种分布式存储方式允许集群处理更大的数据集,并提供更高的性能和可扩展性。
  3. 数据复制:每个主节点都有一个或多个从节点,从节点会自动复制主节点上的数据。数据复制可以提供数据的冗余备份,并在主节点故障时自动切换到从节点,以保证系统的可用性。
  4. 自动分片和故障转移:Redis Cluster会自动将数据分片到不同的节点上,同时提供自动化的故障检测和故障转移机制。当节点发生故障或下线时,集群会自动检测并进行相应的故障转移操作(投票机制:节点的fail是通过集群中超过半数的节点检测失效时才生效),以保持数据的可用性和一致性。
  5. 节点间通信:Redis Cluster中的节点之间通过内部通信协议进行交互,共同协作完成数据的分片、复制和故障转移等操作。节点间通信的协议和算法确保了数据的正确性和一致性。

工作机制

  1. 在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383
  2. 当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
  3. 为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
  4. 当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了

Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

Cluster部署

redis.conf配置:

port 7100 # 本示例6个节点端口分别为7100,7200,7300,7400,7500,7600
daemonize yes # r后台运行
pidfile /var/run/redis_7100.pid # pidfile文件对应7100,7200,7300,7400,7500,7600
cluster-enabled yes # 开启集群模式
masterauth passw0rd # 如果设置了密码,需要指定master密码
cluster-config-file nodes_7100.conf # 集群的配置文件,同样对应7100,7200等六个节点
cluster-node-timeout 15000 # 请求超时 默认15秒,可自行设置

启动redis:

[root@dev-server-1 cluster]# redis-server redis_7100.conf
[root@dev-server-1 cluster]# redis-server redis_7200.conf

组成集群:

redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7100 127.0.0.1:7200 127.0.0.1:7300 127.0.0.1:7400 127.0.0.1:7500 127.0.0.1:7600 -a passw0rd

--cluster-replicas:表示副本数量,也就是从服务器数量,因为我们一共6个服务器,这里设置1个副本,那么Redis会收到消息,一个主服务器有一个副本从服务器,那么会计算得出:三主三从。

Cluster注意点

  • 数据分片和哈希槽:Redis Cluster 使用数据分片和哈希槽来实现数据的分布式存储。每个节点负责一部分哈希槽,确保数据在集群中均匀分布。在设计应用程序时,需要考虑数据的分片规则和哈希槽的分配,以便正确地将数据路由到相应的节点。
  • 节点的故障和扩展:Redis Cluster 具有高可用性和可伸缩性。当节点发生故障或需要扩展集群时,需要正确处理节点的添加和删除。故障节点会被自动检测和替换,而添加节点需要进行集群重新分片的操作。
  • 客户端的重定向:Redis Cluster 在处理键的读写操作时可能会返回重定向错误(MOVED 或 ASK)。应用程序需要正确处理这些错误,根据重定向信息更新路由表,并将操作重定向到正确的节点上。
  • 数据一致性的保证:由于 Redis Cluster 使用异步复制进行数据同步,所以在节点故障和网络分区恢复期间,可能会发生数据不一致的情况。应用程序需要考虑数据一致性的问题,并根据具体业务需求采取适当的措施。
  • 客户端连接的负载均衡:在连接 Redis Cluster 时,应该使用适当的负载均衡策略,将请求均匀地分布到集群中的各个节点上,以避免单个节点过载或出现热点访问。
  • 事务和原子性操作:Redis Cluster 中的事务操作只能在单个节点上执行,无法跨越多个节点。如果需要执行跨节点的原子性操作,可以使用 Lua 脚本来实现。
  • 集群监控和管理:对 Redis Cluster 进行监控和管理是很重要的。可以使用 Redis 自带的命令行工具或第三方监控工具来监控集群的状态、性能指标和节点健康状况,以及执行管理操作,如节点添加、删除和重新分片等。

Redis常见问题

当使用redis作为数据库的缓存层时,会经常遇见这几种问题,以下是这些问题的描述以及对应的解决方案

缓存穿透

概念:请求过来之后,访问不存在的数据,redis中查询不到,则穿透到数据库进行查询

现象:大量穿透访问造成redis命中率下降,数据库压力飙升

解决方案

  1. 空值缓存:如果一个查询的数据返回空,仍然把这个结果缓存到redis,以缓解数据库的查询压力
  2. 布隆过滤器:布隆过滤器由一个很长的二进制数组结合n个hash算法计算出n个数组下标,将这些数据下标置为1。在查找数据时,再次通过n个hash算法计算出数组下标,如果这些下标的值为1,表示该值可能存在(存在hash冲突的原因),如果为0,则表示该值一定不存在。因此,布隆过滤器中存在,数据不一定存在,但若布隆过滤器中不存在,则数据一定不存在,依靠此特性可以过滤掉一定的空值数据

缓存击穿

概念:请求访问的key对应的数据存在,但key在redis中已过期,则访问击穿到数据库

现象:若大批请求中访问的key均过期,那么redis正常运行,但数据库的瞬时并发压力会飙升

解决方案

  1. 热点数据永不过期:热点数据可以一直在redis中请求到,不会过期,则不会出现缓存击穿现象
  2. 使用互斥锁:当访问redis的key过期之后,在请求数据库重新加载数据之前,先获取互斥锁(单进程可以synchronized,分布式使用分布式锁),获取到锁的请求加载数据并放进缓存,没有获取到锁的请求可以进行重试,重试之后便能重新获取到redis中的数据

缓存雪崩

概念:同一时间大批量key同时过期,造成瞬时对这些key的请求全部击穿到数据库;或redis服务不可用(宕机)

缓存雪崩与缓存击穿的区别在于:缓存击穿是单个热点数据过期,而缓存雪崩是大批量热点数据过期

现象:大量热点数据的查询请求会增加数据库瞬时压力

解决方案

  1. 设置随机过期时间:避免大量key的过期时间过于集中,可以通过随机算法均匀分布key的过期时间点
  2. 热点数据永不过期:可以和缓存击穿一样让热点数据不过期
  3. 搭建高可用redis服务:针对redis服务不可用,可以对redis进行分布式部署,并实现故障转移(如redis哨兵模式)
  4. 控制系统负载:实现熔断限流或服务降级,让系统负载在可控范围内

大key问题

概念:redis中存在占用内存空间较多的key,其中包含多种情况,如string类型的value值过大,hash类型的所有成员总值过大,zset的成员数量过大等。大key的具体值的界定,要根据实际业务情况判断。

现象:大key对业务会产生多方面的影响:

  1. redis内存占用过高:大key可能导致内存空间不足,从而触发redis的内存淘汰策略。
  2. 阻塞其他操作:对某些大key操作可能导致redis实例阻塞,例如使用Del命令删除key等。
  3. 网络拥塞:大key在网络传输中更消耗带宽,可能造成机器内部网络带宽打满。
  4. 主从同步延迟:大key在redis进行主从同步时也更容易导致同步延迟,影响数据一致性。

原因

  1. 业务设计不合理:在业务设计上,没有考虑大数据量问题,导致一个key存储了大量的数据
  2. 未定期清理数据:没有合适的删除机制或过期机制,造成value不断增加
  3. 业务逻辑问题:业务逻辑bug导致key的value只增不减

排查

  1. SCAN命令:通过redis的scan命令逐步遍历数据库中的所有key,通过比较大小,站到占用内存较多的大key
  2. bigkeys参数:使用redis-cli命令客户端,连接Redis服务的时候,加上 —bigkeys 参数,可以扫描每种数据类型数量最大的key。
redis-cli -h 127.0.0.1 -p 6379 —bigkeys
  1. Redis RDB Tools工具:使用开源工具Redis RDB Tools,分析RDB文件,扫描出Redis大key。

例如:输出占用内存大于1kb,排名前3的keys。

rdb —commond memory —bytes 1024 —largest 3 dump.rbd
  1. Redis云商提供的工具:现在基本使用云商提供的redis实例,其本身也提供一定的方法能快速定位大key

解决方案

  1. 大key拆分:可以根据实际业务场景,拆分多个小key,确保value大小在合理范围内
  2. 大key清理:redis4.0之后可以使用unlink命令以非阻塞方式安全的删除大key
  3. 合理设置过期时间:设置过期时间可以让数据自动失效清理,一定程度避免大key的长时间存在。
  4. 合理设置淘汰策略:redis中使用合适的淘汰策略,能在redis内存不足时,淘汰数据,防止大key长时间占用内存
  5. 数据压缩:使用string类型,可以对value通过压缩算法进行压缩。可以用gzip,bzip2等常用算法压缩和解压。需要注意的是,这种方法会增加CPU的开销以及处理的响应延迟,同时也增加逻辑代码的复杂性

热key问题

概念:redis中某个key的访问次数比较多且明显多于其他key,则这个key被定义为热key

现象

  1. Redis的CPU占用过高,效率降低,影响其他业务
  2. 若热key请求超出redis处理能力,会造成redis宕机,请求击穿到数据库,影响数据库性能

原因:某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品

排查

  1. hotkeys参数:Redis 4.0.3 版本中新增了 hotkeys 参数,该参数能够返回所有 key 的被访问次数(使用前提:redis淘汰策略设置为lfu)
# redis-cli -p 6379 --hotkeys
  1. MONITOR命令:MONITOR 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。该命令对 Redis 性能的影响比较大,因此禁止长时间开启 MONITOR(生产环境中建议谨慎使用该命令)
  2. 根据业务情况分析:根据实际业务场景分析,可以提前预估可能出现的热key现象,比如秒杀活动的商品数据等
  3. 云商redis工具:云服务一般会提供redis的热key分析工具,合理利用,发现热key

解决方案

  1. 热key拆分:设计一定的规则,给热key增加后缀,变成多个key,结合Redis Cluster模式,能分散到不同的节点。会带来业务复杂度,以及可能产生数据一致性问题
  2. 二级缓存:在应用和redis中间再引入一层缓存层,如本地缓存,来缓解redis压力
  3. 热key单独集群部署:针对热key单独做集群部署,和其他业务key进行隔离

更多技术干货,欢迎关注我!

【Redis】一文掌握Redis原理及常见问题的更多相关文章

  1. Redis | 一文轻松搞懂redis集群原理及搭建与使用

    转载:https://juejin.im/post/5ad54d76f265da23970759d3 作者:SnailClimb 这里总结一下redis集群的搭建以便日后所需同时也希望能对你有所帮助. ...

  2. 【Redis】四、Redis设计原理及相关问题

    (六)Redis设计原理及相关问题   通过前面关于Redis五种数据类型.相关高级特性以及一些简单示例的使用,对Redis的使用和主要的用途应该有所掌握,但是还有一些原理性的问题我们在本部分做一个探 ...

  3. 一文解读Redis (转)

    本文由葡萄城技术团队编撰并首发 转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 引言 在Web应用发展的初期,那时关系型数据库受到了较为广泛的关注和应用,原 ...

  4. (77)一文了解Redis

    为什么我们做分布式使用Redis? 绝大部分写业务的程序员,在实际开发中使用 Redis 的时候,只会 Set Value 和 Get Value 两个操作,对 Redis 整体缺乏一个认知.这里对  ...

  5. [翻译自官方]什么是RDB和AOF? 一文了解Redis持久化!

    ​概述 本文提供Redis持久化技术说明,  建议所有Redis用户阅读. 如果您想更深入了解Redis持久性原理机制和底层持久性保证, 请参考文章 揭秘Redis持久化: http://antire ...

  6. 一文入门Redis

    一文入门Redis 目录 一文入门Redis 一.Redis简介 二.常用数据类型 1.String(字符串) 2.Hash(哈希) 3.List(列表) 4.Set(集合) 5.Zset(有序集合) ...

  7. Redis单机数据库的实现原理

    本文主要介绍Redis的数据库结构,Redis两种持久化的原理:RDB持久化.AOF持久化,以及Redis事件分类及执行原理.最后,分别介绍了单机班Redid客户端和Redis服务器的使用和实现原理. ...

  8. redis命令参考和redis文档中文翻译版

    找到了一份redis的中文翻译文档,觉得适合学习和查阅.这份文档翻译的真的很良心啊,他是<Redis 设计与实现>一书的作者黄健宏翻译的. 地址:http://redisdoc.com/i ...

  9. Redis cluster集群:原理及搭建

    Redis cluster集群:原理及搭建 2018年03月19日 16:00:55 阅读数:6120 1.为什么使用redis? redis是一种典型的no-sql 即非关系数据库 像python的 ...

  10. Redis数据持久化机制AOF原理分析一---转

    http://blog.csdn.net/acceptedxukai/article/details/18136903 http://blog.csdn.net/acceptedxukai/artic ...

随机推荐

  1. Oracle:Ora-01652无法通过128(在temp表空间中)扩展temp段的过程-解决步骤

    现象:查询select * from v$sql时提示"Ora-01652无法通过128(在temp表空间中)扩展temp段的过程" 临时文件是不存储的,可以将数据库重启,重启后重 ...

  2. Python基础——深浅拷贝、python内存泄露、你并不了解的format、decimal

    文章目录 深浅拷贝 先看赋值运算 浅拷贝copy 深拷贝deepcopy 相关面试题 python内存泄露 起因 方案 编写安全的代码 弱引用 你并不了解的format.decimal format格 ...

  3. Android项目Library导入的问题整理

    Android项目Library导入的问题整理 本来帮助朋友找寻一下android的一些特效的demo,结果找到了一个,朋友试验可以,自己却是在导入项目需要的library的时候总是出问题,真的很是丢 ...

  4. 一个树状数组求逆序对的进阶 [USACO17JAN] Promotion Counting P

    题面就这样,就是在树上求一个逆序对但是我笨笨地求了对于每一个下属有几个上司能力比他低还一遍就写对了,结果发现看错题目了难得一遍过,但是没有完全过

  5. JVM 学习

    目录 1. 类加载器及类加载过程 1.1 基本流程 1.2 类加载器子系统作用 1.3 类加载器角色 1.4 加载过程 (1) 加载 loading (2) 链接 linking 验证 verify ...

  6. eNSP小实验——配置路由器与主机

    练习一 在eNSP里配置路由器与主机,IP地址与端口 配置PC1 配置PC2,特别注意IP地址与网关 配置路由器一 <Huawei>sys[Huawei]int g0/0/0 [Huawe ...

  7. 子组件emit 父组件方法,成功后回调执行子组件方法

    场景: 父组件 update方法 子组件 确定按钮  getlist 刷新列表 子组件点击确定按钮,调用父组件新增接口,新增成功以后,子组件列表刷新 子组件: emit("confirmPa ...

  8. html笔记重点

    第五周-周二 一.视频和音频 <video src="路径" controls="controls"></video> 1.加contr ...

  9. vue3.0父级组件调用子组件方法

    vue3.0父级组件调用子组件方法 场景:在页面开发过程中,我经常涉及到不同组件之间的元素和方法的调用.就此记录在vue3.0项目,也是我开发的开源项目中的实现方式. 父级组件调用子级 1.应用场景 ...

  10. Welcome to YARP - 4.限流 (Rate Limiting)

    目录 Welcome to YARP - 1.认识YARP并搭建反向代理服务 Welcome to YARP - 2.配置功能 2.1 - 配置文件(Configuration Files) 2.2 ...