redis面试题3

redis的key如何命名

Redis的key虽然可以自定义,但最好遵循下面几个最佳实践约定

  • 遵循基本格式:[业务名称]:[数据名称]:[id]
  • 长度不超过44字节
  • 不包含特殊字符

key的格式不一定非要三断,也可以四段,5断,不要太长即可,列入存储用户登录信息

优点

  1. 可读性强
  2. 避免key冲突
  3. 方便管理
  4. 更节省内存:key是string类型,底层编码包含int,embstr,raw三种.embstr小于44字节使用,采用连续内存空间,内存占用更小

拒绝使用bigkey

bigkey通常以key的大小和key中成员的数量来总和判定

  • key本身的数据过大:一个String类型的key,它的值为5MB
  • Key中的成员过多,一个zset类型的key,成员数量为10000个
  • key中成员的数量过大,一个Hash类型的key,他的成员虽然只有1000个但这些成员的value值为100mb

推荐值

单个key的value小于10kb

对于集合类型的key建议元素数量小于1000(redis默认是500,可以修改为1000,hash-max-ziplist-entries)

bigkey的危害

  1. 网络阻塞:对bogkey执行读请求时,少量的qps就看你被沾满,导致redis实例乃至所在物理机变慢
  2. 数据倾斜:bigkey所在的redis实例内存使用率远超其他实例,无法使数据被分片的内存资源达到均衡
  3. Redis阻塞:对于元素比较多的hash,list,zset等做运算会耗时很久,让主线程被阻塞
  4. cpu压力:对bigkey的序列化和反序列化,会导致cpu占用率飙升,影响redis实例和本机其他应用

如何发现bigkey

redis-cli --bigkeys:利用redis-cli提供的--bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key;

scan扫描:自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE);

如何删除bigkey

bigkey内存占用较多,即便是删除这样的key也需要很长的时间,导致Redis主线程阻塞,引发一系列问题

redis3.0以下版本

如果是集合类型,则便利bigKey的元素,逐个删除子元素,最后删除BigKey

Redis4.0以后

Redis在4.0以后提供了异步删除的命令:unlink

使用恰当的数据结构

  1. json字符串,简单粗暴,但数据耦合不够灵活
  2. 将字段打散:可以灵活访问对象,但是占用空间大,没办法做统一控制
  3. hash存储最优选择

hash存储底层使用ziplist,空间占用小,可灵活访问对象的任意字段,缺点是代码相对复杂

Hash类型的key数据量过大怎么办

假如有hash类型的key,其中有100万对field和value,field是自增id,这个key存在什么问题?如何优化?

存在的问题

hash的entry数量超过500时,会使用哈希表而不是ziplist,内存占用较多
可以通过hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题

拆分为string类型

  • string结构底层没有太多内存优化,内存占用较多。
  • 想要批量获取这些数据比较麻烦

比使用一个hash更大了

拆分为小哈希

拆分为小的hash,将 id / 100 作为key, 将id % 100 作为field,这样每100个元素为一个Hash

  • 合理的拆分数据,拒绝BigKey
  • 选择合适数据结构
  • Hash结构的entry数量不要超过1000(默认为500)
  • 设置合理的超时时间

redisKey的编码

字符串类型编码

  • int:存储8个类型的长整型(long 2^63-1) 四个字节
  • embstr:代表embstr:代表embstr格式的SDS(Simple Dynamic String 简单动态字符串),存储小于44个字节的字符串,只分配一次内存空间(Redis Object和SDS是连续的)
  • row:存储大于44个字节的字符串(3.2版本之前是39个字节),需要分配俩次内存空间(分别为 Redis Object 和 SDS 分配空间)。

embstr结构

今天主要是来研究一下,embstr编码的问题,先来看一下embstr的内存结构:

embstr分配的是连续的一块内存,包含redisObjecsds,redisObject占用了16个字节,先来看一下redisObject的存储结构:

typedef struct redisObject {
    /*对象的类型*/
    unsigned type:4;
    /*具体的数据结构,embstr、sds、*/
    unsigned encoding:4;
    /* 24位,对象最后一次被命令程序访问的时间,与内存回收有关*/
    /* 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;
    /*引用计数。当refcount为0的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了*/
    int refcount;
    /*指向对象实际的数据结构*/
    void *ptr;
} robj;

Reids3.2之前版本

因为在之前很多书上都会说存储小于39个字节,编码为embstr,超过39个字节,编码为raw。这是在3.2版本之前。下面我们就先来看一下之前的版本:

/*
 * 保存字符串对象的结构
 */
struct sdshdr {
    // buf 中已占用空间的长度
    unsigned int len;
    // buf 中剩余可用空间的长度
    unsigned int free;
    // 数据空间
    char buf[];
};

先来剖析一下为什么之前embstr在存储小于39个字符都是embstr的,大于39个之后就是raw的了。

sdshdr = unsigned int * 2 = 4 * 2 = 8

embstr : redisObject(16)+sds{char(39个) + 8 + 1} =64

此时可以看出如果是39个字符就是64个字节,那么也就是说只要是小于39个字符的,分配的空间都是64个字节。超过64个字节就为raw。

Reids3.2之后版本

本文选用的源代码是5.0.8版本,在 3.2 以后的版本中,SDS 又有多种结构(sds.h文件中):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 25=32byte,28=256byte,216=65536byte=64KB,232byte=4GB。先来查看一下sds的结构:

/* object.c */
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 //制定长度
struct __attribute__ ((__packed__)) sdshdr8 {
    //当前字符数组的长度
    uint8_t len; /* used */
//    当前字符数组总共分配的内存大小
    uint8_t alloc; /* excluding the header and null terminator */
//    当前字符数组的属性、用来标识到底是 sdshdr8 还是 sdshdr16 等
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
//    字符串真正的值
    char buf[];
};

我们来看一下5.0的中的sds的结构,经过分析可以看到sds使用了uint8_t代替int,占用的内存空间更小了,下面计算一下sds的头部信息占用的空间大小:sdsdr8 = uint8_t (1个字节)* 2 + char(1个字节) = 3个字节

由于对sds的头部信息结构做了调整,所以在原来39的基础上又可以多存储了5个字节,为44个字节。

从2.4版本开始,redis开始使用jemalloc内存分配器。这个比glibc的malloc要好不少,还省内存。在这里可以简单理解,jemalloc会分配8,16,32,64等字节的内存。

所以embstr分配的最新内存为:

redisObject(16)+sds(uint8_t (1个字节)* 2 + char(1个字节)+8(最小分配内存) +1(0结束符))=28个字节;

总结

embstr存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。embstr的最小占用空间为19(16+3),而64-19-1(结尾的0)=44,所以empstr只能容纳44字节。

当字节数小于44时,分配的大小一直都是64个字节。一旦超过44个字节,整体的大小超过64个字节,在Redis中将认为是一个大的字符串,不再使用 emdstr 形式存储,存储结构将变为raw。

redis与memached的区别

redis和memached都是基于内存的数据库存储系统

Memmcached是高性能分布式内存缓存服务,本质上是一个内存key-value数据库,redis是一个开源的key-value存储系统

redis将大部分数据存储在内存中,支持的数据包括,字符串,set,list,zset,hash以及基于这些数据类型的相关操作

数据不同

A.memached仅支持key-value数据存储,redis支持String Hash List Set sortSet五中数据类型
B.memached不支持持久化,redis提供了持久化和复制等功能

内存管理机制

  1. 在redis中,并不是所有数据一直存储在内存中的,这是和memached最大的区别
  2. 当物理内存用完时,Redis可以将一些很久没用到的value交换到磁盘
  3. .Redis只会缓存所有的key的信息,如果Redis发现内存的使用量超过了某一个阀值,将触发swap的操作,Redis根据“swappability = age*log(size_in_memory)”计算出哪些key对应的value需要swap到磁盘。然后再将这些key对应的value持久化到磁盘中,同时在内存中清除。这种特性使得Redis可以保持超过其机器本身内存大小的数据。
  4. Memcached默认使用Slab Allocation机制管理内存,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。
  5. 从内存利用率来讲,使用简单的key-value存储的话,Memcached的内存利用率更高。而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。

性能

由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis也在存储大数据的性能上进行了优化,但是比起Memcached,还是稍有逊色。

集群

memached本身不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现memcached的分布式存储

相较于Memcached只能采用客户端实现分布式存储,Redis更偏向于在服务器端构建分布式存储。最新版本的Redis已经支持了分布式存储功能。Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本,它没有中心节点,具有线性可伸缩的功能。下图给出Redis Cluster的分布式存储架构,其中节点与节点之间通过二进制协议进行通信,节点与客户端之间通过ascii协议进行通信。在数据的放置策略上,Redis Cluster将整个key的数值域分成4096个哈希槽,每个节点上可以存储一个或多个哈希槽,也就是说当前Redis Cluster支持的最大节点数就是4096。Redis Cluster使用的分布式算法也很简单:crc16( key ) % HASH_SLOTS_NUMBER。
Redis支持数据的备份,即master-slave模式的数据备份Memcached提供了cas命令保证多个并发访问操作同一份数据的一致性问题。
Redis提供了事务的功能,可以保证一串命令的原子性

Last modification:November 17, 2023
如果觉得我的文章对你有用,请随意赞赏