Redis 详解(自用)

Redis

一、集群部署

环境:docker

版本:6.2.5

1、下载并修改配置文件

从 Redis 官网下载好最新的配置文件并修改:

appendonly yes

cluster-enable yes

cluster-config-file nodes.conf

cluster-node-timeout 5000

除官方文档写的修改这几个配置外,还需要修改:

protect-mode no

添加:

# 在docker或者某些部署中 因为端口被转发等其他原因 会导致地址发送变化,所以在配置中写死地址就可以解决
cluster-announce-ip 容器 IP
cluster-announce-port 容器端口(默认 6379 就可以)

并且注释掉:

# bind 127.0.0.1 -::1

否则在集群的过程中会报错。

内存不够需要指定最大内存 maxmemory 307200

配置 Redis 存储数据时指定限制的内存大小,比如 100m。当缓存消耗的内存超过这个数值时, 将触发数据淘汰。该数据配置为0时,表示缓存的数据量没有限制, 即LRU功能不生效。64位的系统默认值为0,32位的系统默认内存限制为3GB

解决脑裂情况下出现脏数据:

# 一个 master 最少有一个 slave (填的值要大于等于集群数量的一半,本次部署六个 Redis 采用一主一从,三个分片,所以填 1),这个参数会一定程度上影响可用性,slave 要是少于 1 个,这个集群就算 leader 正常也不能提供服务了
min-replicas-to-write 1
# slave 连接到 master 的最大延迟时间
min-replicas-max-lag 10

内存淘汰机制:

maxmemory-policy volatile-lfu
# maxmemory-policy一共有8个值,当内存不足时:

# noeviction: 不删除,直接返回报错信息。
# allkeys-lru:移除最久未使用(使用频率最少)使用的key。推荐使用这种。
# volatile-lru:在设置了过期时间key中,移除最久未使用的key。
# allkeys-random:随机移除某个key。
# volatile-random:在设置了过期时间的key中,随机移除某个key。
# volatile-ttl: 在设置了过期时间的key中,移除准备过期的key。
# allkeys-lfu:移除最近最少使用的key。
# volatile-lfu:在设置了过期时间的key中,移除最近最少使用的key。

2、编写 docker-compose.yml 文件

version: "3.9"
services:
  redis_1:
    image: redis
    container_name: redis_1
    ports: 
      - "6379:6379"
    volumes:
      - "/usr/local/docker/redis/redis_1/conf/redis.conf:/usr/local/etc/redis/redis.conf"
      - "/usr/local/docker/redis/redis_1/data:/data"
    restart: always
    privileged: true
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    networks:
      redis_cluster:
        ipv4_address: 172.28.0.2

  redis_2:
    image: redis
    container_name: redis_2
    ports:
      - "6380:6379"
    volumes:
      - "/usr/local/docker/redis/redis_2/conf/redis.conf:/usr/local/etc/redis/redis.conf"
      - "/usr/local/docker/redis/redis_2/data:/data"
    restart: always
    privileged: true
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    networks:
      redis_cluster:
        ipv4_address: 172.28.0.3

  redis_3:
    image: redis
    container_name: redis_3
    ports:
      - "6381:6379"
    volumes:
      - "/usr/local/docker/redis/redis_3/conf/redis.conf:/usr/local/etc/redis/redis.conf"
      - "/usr/local/docker/redis/redis_3/data:/data"
    restart: always
    privileged: true
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    networks:
      redis_cluster:
        ipv4_address: 172.28.0.4

  redis_4:
    image: redis
    container_name: redis_4
    ports:
      - "6382:6379"
    volumes:
      - "/usr/local/docker/redis/redis_4/conf/redis.conf:/usr/local/etc/redis/redis.conf"
      - "/usr/local/docker/redis/redis_4/data:/data"
    restart: always
    privileged: true
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    networks:
      redis_cluster:
        ipv4_address: 172.28.0.5

  redis_5:
    image: redis
    container_name: redis_5
    ports:
      - "6383:6379"
    volumes:
      - "/usr/local/docker/redis/redis_5/conf/redis.conf:/usr/local/etc/redis/redis.conf"
      - "/usr/local/docker/redis/redis_5/data:/data"
    restart: always
    privileged: true
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    networks:
      redis_cluster:
        ipv4_address: 172.28.0.6

  redis_6:
    image: redis
    container_name: redis_6
    ports:
      - "6384:6379"
    volumes:
      - "/usr/local/docker/redis/redis_6/conf/redis.conf:/usr/local/etc/redis/redis.conf"
      - "/usr/local/docker/redis/redis_6/data:/data"
    restart: always
    privileged: true
    command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
    networks:
      redis_cluster:
        ipv4_address: 172.28.0.7

networks:
  redis_cluster:
    ipam:
      driver: default
      config:
        - subnet: 172.28.0.0/16

需要建立虚拟网卡,指定 IP 地址,因为 Redis 貌似不支持通过容器名区访问。

3、启动并建立集群

输入 docker-compose up -d 启动 6 个 Redis。

输入 docker exec -it redis_1 redis-cli --cluster create 172.28.0.2:6379 172.28.0.3:6379 172.28.0.4:6379 172.28.0.5:6379 172.28.0.6:6379 172.28.0.7:6379 --cluster-replicas 1 建立集群。

4、部署可视化工具

创建文件夹 /usr/local/docker/redis/redis_insight/data

并给 data 文件夹赋权限 chmod 777 -R /usr/local/docker/redis/redis_insight/data

输入 docker run --name redis_insight -d -p 8001:8001 -v /usr/local/docker/redis/redis_insight/data:/db --restart=always --privileged=true redislabs/redisinsight 创建容器。

打开防火墙端口(云服务器需要添加安全组):

firewall-cmd --zone=public --add-port=8001/tcp --permanent
firewall-cmd --zone=public --add-port=6379/tcp --permanent

访问 http://{服务器 ip}:8001 就可以看到可视化界面了。

二、常用指令

1、字符串

① 设置指定 key 的值

set {key} {value}

② 获取指定 key 的值

get {key}

③ 自增指定 key 的值

incr {key}

④ 自减指定 key 的值

decr {key}

⑤ 自增指定 key 指定值

incrby {key} {incr value}

⑥ 自减指定 key 指定值

decrby {key} {decr value}

⑦ 在指定 key 的 value 后追加字符串

append {key} {value}

使用该命令后会编码方式会直接变为 raw

⑧ 在指定 key 不存在时设置值

setnx {key} {value}

⑨ 获取多个 key 的值

mget {key} [{key}...]

⑩ 设置并返回旧值

getset {key} {value}

⑪ 获取指定 key 的长度

strlen {key}

⑫ 删除指定 key

del {key}

2、列表

① 在头或尾部插入

lpush {key} {value} [{value}...]

rpush {key} {value} [{value}...]

② 移除并获取头部或尾部的值

lpop {key} [{count}]

rpop {key} [{count}]

③ 获取指定范围内的元素

lrange {key} {start} {stop}

④ 获取列表长度

llen {key}

⑤ 通过索引获取列表中的元素

lindex {key} {index}

⑥ 移除列表元素

lrem {key} {count} {value}

count > 0:从表头开始,移除与 value 相同的值,数量为 count

count < 0:从表尾开始,移除与 value 相同的值,数量为 count

count = 0:移除表中所有与 count 相同的值

⑦ 通过索引替换列表中元素的值

lset {key} {index} {value}

3、哈希

① 将哈希表 key 中 field 的值设为 value

hset {key} {field} {value} [{field} {value}...]

② 在 field 不存在时设置字段的值

hsetnx {key} {field} {value}

③ 获取值

hget {key} {value}

hmget {key} {value} [{field} {value}...]

④ 获取指定 key 的所有键和值

hgetall {key}

⑤ 获取指定 key 的所有值

hvals key

⑥ 获取指定 key 所有值的数量

hlen {key}

⑦ 获取指定 key 的所有键

hkeys {key}

⑧ 删除一个或多个 key 的键

`hdel {key} {field} [{field}...]

⑨ 查看 key 中指定键是否存在

hexitst {key} {field}

4、集合

① 添加

sadd {key} {value} [{value}...]

② 获取个数

scard {key}

③ 返回指定集合的交集

sinter key [{key}...]

④ 判断 key 是否包含 value

sismember {key} {value}

⑤ 获取所有值

smembers {key}

⑥ 所有随机一个或多个成员

srandmember {key} {count}

⑦ 删除

srem {key} {value} [{value}...]

5、有序集合

① 添加

zadd {key} {score} {value} [{score} {value}...]

② 获取个数

zcard {key}

③ 获取指定区间的成员个数

zcount {key} {min} {max}

④ 对成员的 score 增加 increment

zincrby {key} {increment} {member}

⑤ 获取指定区间成员

zrange {key} {start} {stop} [withscores]

⑥ 返回成员索引

zrank {key} {value}

⑦ 移除

zrem {key} {value} [{value}...]

⑧ 获取指定区间的成员(通过索引)

zrevrange {key} {start} {stop} [withscores]

⑨ 获取成员分数

zscore {key} {value}

6、Stream

7、bitmap

8、GeoHash

9、HyperLogLog

三、内部编码

1、动态字符串

Redis 自己定义的对象,类似 Java 一个存放字符的集合

struct sdshdr{
    //int 记录buf数组中未使用字节的数量 如上图free为0代表未使用字节的数量为0
    int free;
    //int 记录buf数组中已使用字节的数量即sds的长度 如上图len为5代表未使用字节的数量为5
    int len;
    //字节数组用于保存字符串 sds遵循了c字符串以空字符结尾的惯例目的是为了重用c字符串函数库里的函数
    char buf[];
}

时间复杂度:

操作 时间复杂度
获取长度 O(1)
获取未使用空间 O(1)
清除保存内容 O(1)
创建长度为 n 的字符串 O(n)
拼接长度为 n 的字符串 O(n)

作用:

  • 杜绝缓冲区溢出

    C字符串,如果程序员在字符串修改的时候如果忘记给字符串重新分配足够的空间,那么就会发生内存溢出。

  • 减少字符串操作中的内存重分配次数

    在C字符串中,如果对字符串进行修改,那么我们就不得不面临内存重分配。因为C字符串是由一个N+1长度的数组组成,如果字符串的长度变长,我们就必须对数组进行扩容,否则会产生内存溢出。而如果字符串长度变短,我们就必须释放掉不再使用的空间,否则会发生内存泄漏。

  • 二进制安全

    C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。

2、链表

双向链表

// 节点
typedef struct listNode
{ 
	// 前置节点 
	struct listNode *prev; 
	// 后置节点 
	struct listNode *next; 
	// 节点的值 
	void *value; 
} listNode;
// 链表
typedef struct list{
    //表头节点
    listNode *head;
    //表尾节点
    listNode *tail;
    //链表所包含的节点数量
    unsigned long len;
    //节点值复制函数
    void *(*dup)(void *ptr);
    //节点值释放函数
    void *(*free)(void *ptr);
    //节点值对比函数
    int (*match)(void *ptr,void *key);
}list;

拥有双向链表的所有优点

3、字典

基于哈希表的实现

typedef struct dict{
         //类型特定函数
         void *type;
         //私有数据
         void *privdata;
         //哈希表-见2.1.2
         dictht ht[2];
         //rehash 索引 当rehash不在进行时 值为-1
         int trehashidx; 
}dict;
  • type属性是一个指向dictType结构的指针,每个dictType用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。

  • privdata属性则保存了需要传给给那些类型特定函数的可选参数。

    typedef struct dictType
    {
             //计算哈希值的函数 
             unsigned int  (*hashFunction) (const void *key);
             //复制键的函数
             void *(*keyDup) (void *privdata,const void *key);
             //复制值的函数
             void *(*keyDup) (void *privdata,const void *obj);
              //复制值的函数
             void *(*keyCompare) (void *privdata,const void *key1, const void *key2);
             //销毁键的函数
             void (*keyDestructor) (void *privdata, void *key);
             //销毁值的函数
             void (*keyDestructor) (void *privdata, void *obj);
    }dictType;
    
    • ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表, 一般情况下,字典只使用ht[0] 哈希表, ht[1]哈希表只会对ht[0]哈希表进行rehash时使用。
    • rehashidx记录了rehash目前的进度,如果目前没有进行rehash,值为-1。
typedef struct dictht
{
         //哈希表数组,C语言中,*号是为了表明该变量为指针,有几个* 号就相当于是几级指针,这里是二级指针,理解为指向指针的指针
         dictEntry **table;
         //哈希表大小
         unsigned long size;
         //哈希表大小掩码,用于计算索引值
         unsigned long sizemask;
         //该哈希已有节点的数量
         unsigned long used;
}dictht;
  • table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对

  • size属性记录了哈希表的大小,也是table数组的大小

  • used属性则记录哈希表目前已有节点(键值对)的数量

  • sizemask属性的值总是等于 size-1(从0开始),这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面(索引下标值)。

    //哈希表节点定义dictEntry结构表示,每个dictEntry结构都保存着一个键值对。
    typedef struct dictEntry
    {
             //键
             void *key;
             //值
             union{
               void *val;
                uint64_tu64;
                int64_ts64;
                }v;
             // 指向下个哈希表节点,形成链表
             struct dictEntry *next;
    }dictEntry;
    
  • key属性保存着键值中的键,而v属性则保存着键值对中的值,其中键值(v属性)可以是一个指针,或uint64_t整数,或int64_t整数。 next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,解决键冲突问题。

rehash

随着操作的进行,散列表中保存的键值对会也会不断地增加或减少,为了保证负载因子维持在一个合理的范围,当散列表内的键值对过多或过少时,内需要定期进行rehash,以提升性能或节省内存。Redis的rehash的步骤如下:

① 为字典的ht[1]散列表分配空间,这个空间的大小取决于要执行的操作以及ht[0]当前包含的键值对数量(即:ht[0].used的属性值)
  • 扩展操作:ht[1]的大小为 第一个大于等于ht[0].used*2的2的n次方幂。如:ht[0].used=3则ht[1]的大小为8,ht[0].used=4则ht[1]的大小为8。
  • 收缩操作: ht[1]的大小为 第一个大于等于ht[0].used的2的n次方幂。
② 将保存在ht[0]中的键值对重新计算键的散列值和索引值,然后放到ht[1]指定的位置上。
③ 将ht[0]包含的所有键值对都迁移到了ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并创建一个新的ht[1]哈希表为下一次rehash做准备。

rehash 需要满足的条件

  1. 服务器目前没有执行BGSAVE(rdb持久化)命令或者BGREWRITEAOF(AOF文件重写)命令,并且散列表的负载因子大于等于1。
  2. 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且负载因子大于等于5。
  3. 当负载因子小于0.1时,程序自动开始执行收缩操作。

Redis这么做的目的是基于操作系统创建子进程后写时复制技术,避免不必要的写入操作。

渐进式 rehash

对于rehash我们思考一个问题如果散列表当前大小为 1GB,要想扩容为原来的两倍大小,那就需要对 1GB 的数据重新计算哈希值,并且从原来的散列表搬移到新的散列表。这种情况听着就很耗时,而生产环境中甚至会更大。为了解决一次性扩容耗时过多的情况,可以将扩容操作穿插在插入操作的过程中,分批完成。当负载因子触达阈值之后,只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,都重复上面的过程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次一次性数据搬移,插入操作就都变得很快了。

Redis为了解决这个问题采用渐进式rehash方式。以下是Redis渐进式rehash的详细步骤:

  1. ht[1] 分配空间, 让字典同时持有 ht[0]ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 ,表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

**说明: **

1.因为在进行渐进式 rehash 的过程中,字典会同时使用 ht[0]ht[1] 两个哈希表,所以在渐进式 rehash 进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。

2. 在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到 ht[1] 里面,而 ht[0] 则不再进行任何添加操作:这一措施保证了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。

4、跳表

跳表就是链表加了多级索引。

查询:从最顶级索引依次查询

插入:插入的时候会根据幂次定律(powerlaw,越大的数出现的概率越小)随机生成一个介于 1 和 32 之间的值作为索引的层级。例如,插入的时候生成 2 ,表示要生成两级索引,在插入节点的正上放生成两层节点(这说明同层级节点的距离不一定是一样的)

删除:如果删除的节点上有层级索引连索引一起删除

5、整数集合

//每个intset结构表示一个整数集合
typedef struct intset{
    //编码方式
    uint32_t encoding;
    //集合中包含的元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;
  • contents数组是整数集合的底层实现,整数集合的每个元素都是 contents数组的个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
  • length属性记录了数组的长度。
  • intset结构将contents属性声明为int8_t类型的数组,但实际上 contents数组并不保存任何int8t类型的值, contents数组的真正类型取决于encoding属性的值。encoding属性的值为INTSET_ENC_INT16则数组就是uint16_t类型,数组中的每一个元素都是int16_t类型的整数值(-32768——32767),encoding属性的值为INTSET_ENC_INT32则数组就是uint32_t类型,数组中的每一个元素都是int16_t类型的整数值(-2147483648——2147483647)。

整数集合升级

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。升级整数集合并添加新元素主要分三步来进行。

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
  3. 将新元素添加到底层数组里面。

6、压缩表

img

7、快速表

双向链表 + 压缩表

四、数据结构

1、字符串

字符串对象的内部编码有3种 :intrawembstr

长度 >= 20 int -> embstr

长度 >= 45 embstr -> raw

embstr编码是专门用于保存短字符串的一种优化编码方式,我们可以看到embstrraw编码都会使用SDS来保存值,但不同之处在于embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObjectSDS。而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObjectSDS。Redis这样做会有很多好处。

  • embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
  • 释放 embstr编码的字符串对象同样只需要调用一次内存释放函数
  • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用CPU缓存提升性能。

应用场景

① 缓存

如上图,Redis经常作为缓存层,来缓存一些热点数据。来加速读写性能从而降低后端的压力。一般在读取数据的时候会先从Redis中读取,如果Redis中没有,再从数据库中读取。在Redis作为缓存层使用的时候,必须注意一些问题,如:缓存穿透、雪崩以及缓存更新问题......

② 计数器、限速器、分布式 ID

计数器\限速器\分布式ID等主要是利用Redis字符串自增自减的特性。

  • 计数器:经常可以被用来做计数器,如微博的评论数、点赞数、分享数,抖音作品的收藏数,京东商品的销售量、评价数等。
  • 限速器:如验证码接口访问频率限制,用户登陆时需要让用户输入手机验证码,从而确定是否是用户本人,但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。
  • 分布式ID:由于Redis自增自减的操作是原子性的因此也经常在分布式系统中用来生成唯一的订单号、序列号等。
③ 分布式共享 session

把Session存到一个公共的地方,让每个Web服务,都去这个公共的地方存取Session。而Redis就可以是这个公共的地方。(数据库、memecache等都可以各有优缺点)。

2、列表

在Redis3.2版本以前列表类型的内部编码有两种。

  • ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
  • linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。

而在Redis3.2版本开始对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.

应用场景

① 文章(商品等)列表

当用户和文章都越来越多时,为了加快程序的响应速度,我们可以把用户自己的文章存入到 List 中,因为 List 是有序的结构,所以这样又可以完美的实现分页功能,从而加速了程序的响应速度。

  1. 每篇文章我们使用哈希结构存储,例如每篇文章有3个属性title、timestamp、content

    Copyhmset acticle:1 title xx timestamp 1476536196 content xxxx
    ...
    hmset acticle:k title yy timestamp 1476512536 content yyyy
    ...
    
  2. 向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键:

    Copylpush user:1:acticles article:1 article3
    ...
    lpush
    ...
    
  3. 分页获取用户文章列表,例如下面伪代码获取用户id=1的前10篇文章

    Copyarticles = lrange user:1:articles 0 9
    for article in {articles}
    {
    	hgetall {article}
    }
    

注意:使用列表类型保存和获取文章列表会存在两个问题。

  • 如果每次分页获取的文章个数较多,需要执行多次hgetall操作,此时可以考虑使用Pipeline批量获取,或者考虑将文章数据序列化为字符串类型,使用mget批量获取。
  • 分页获取文章列表时,lrange命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或者使用Redis3.2的quicklist内部编码实现,它结合ziplist和linkedlist的特点,获取列表中间范围的元素时也可以高效完成。

关于列表的使用场景可参考以下几个命令组合:

  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpush+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息队列)

3、哈希表

哈希类型的内部编码有两种:ziplist(压缩列表),hashtable(哈希表)。只有当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型。具体需要满足两个条件:

  • 当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)
  • 所有值都小于hash-max-ziplist-value配置(默认64字节)
    ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。

应用场景

① 存储对象

相比较于使用Redis字符串存储,其有以下几个优缺点:

  1. 原生字符串每个属性一个键。

    Copyset user:1:name Tom
    set user:1:age 15
    

    优点:简单直观,每个属性都支持更新操作。
    缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。

  2. 序列化字符串后,将用户信息序列化后用一个键保存

    Copyset user:1 serialize(userInfo)
    

    优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
    缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。

  3. 序列化字符串后,将用户信息序列化后用一个键保存

    Copyhmset user:1 name Tom age 15 
    

    优点:简单直观,如果使用合理可以减少内存空间的使用。
    缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。

② 购物车

很多电商网站都会使用 cookie实现购物车,也就是将整个购物车都存储到 cookie里面。这种做法的一大优点:无须对数据库进行写入就可以实现购物车功能,这种方式大大提高了购物车的性能,而缺点则是程序需要重新解析和验证( validate) cookie,确保cookie的格式正确,并且包含的商品都是真正可购买的商品。cookie购物车还有一个缺点:因为浏览器每次发送请求都会连 cookie一起发送,所以如果购物车cookie的体积比较大,那么请求发送和处理的速度可能会有所降低。

购物车的定义非常简单:我们以每个用户的用户ID(或者CookieId)作为Redis的Key,每个用户的购物车都是一个哈希表,这个哈希表存储了商品ID与商品订购数量之间的映射。在商品的订购数量出现变化时,我们操作Redis哈希对购物车进行更新:

如果用户订购某件商品的数量大于0,那么程序会将这件商品的ID以及用户订购该商品的数量添加到散列里面。

Copy//用户1 商品1 数量1
127.0.0.1:6379> HSET uid:1 pid:1 1
(integer) 1 //返回值0代表改field在哈希表中不存在,为新增的field

如果用户购买的商品已经存在于散列里面,那么新的订购数量会覆盖已有的订购数量;

Copy//用户1 商品1 数量5
127.0.0.1:6379> HSET uid:1 pid:1 5
(integer) 0 //返回值0代表改field在哈希表中已经存在

相反地,如果用户订购某件商品的数量不大于0,那么程序将从散列里面移除该条目。

Copy//用户1 商品1
127.0.0.1:6379> HDEL uid:1 pid:2
(integer) 1
③ 计数器

Redis 哈希表作为计数器的使用也非常广泛。它常常被用在记录网站每一天、一月、一年的访问数量。每一次访问,我们在对应的field上自增1

Copy//记录我的
127.0.0.1:6379> HINCRBY MyBlog  202001 1
(integer) 1
127.0.0.1:6379> HINCRBY MyBlog  202001 1
(integer) 2
127.0.0.1:6379> HINCRBY MyBlog  202002 1
(integer) 1
127.0.0.1:6379> HINCRBY MyBlog  202002 1
(integer) 2

也经常被用在记录商品的好评数量,差评数量上

Copy127.0.0.1:6379> HINCRBY pid:1  Good 1
(integer) 1
127.0.0.1:6379> HINCRBY pid:1  Good 1
(integer) 2
127.0.0.1:6379> HINCRBY pid:1  bad  1
(integer) 1

也可以实时记录当天的在线的人数。

Copy//有人登陆
127.0.0.1:6379> HINCRBY MySite  20200310 1
(integer) 1
//有人登陆
127.0.0.1:6379> HINCRBY MySite  20200310 1
(integer) 2
//有人登出
127.0.0.1:6379> HINCRBY MySite  20200310 -1
(integer) 1

4、集合

集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
  • hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。

应用场景

① 标签系统

集合类型比较典型的使用场景是标签(tag)。

  1. 给用户添加标签。

    Copysadd user:1:tags tag1 tag2 tag5
    sadd user:2:tags tag2 tag3 tag5
    ...
    sadd user:k:tags tag1 tag2 tag4
    ...
    
  2. 给标签添加用户

    Copysadd tag1:users user:1 user:3
    sadd tag2:users user:1 user:2 user:3
    ...
    sadd tagk:users user:1 user:2
    ...
    
  3. 使用sinter命令,可以来计算用户共同感兴趣的标签

    Copysinter user:1:tags user:2:tags
    

这种标签系统在电商系统、社交系统、视频网站,图书网站,旅游网站等都有着广泛的应用。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个社交系统可以根据用户的标签进行好友的推荐,已经用户感兴趣的新闻的推荐等,一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。

5、有序集合

有序集合是由 ziplist (压缩列表)skiplist (跳跃表) 组成的。

当数据比较少时,有序集合使用的是 ziplist 存储的,有序集合使用 ziplist 格式存储必须满足以下两个条件:

  • 有序集合保存的元素个数要小于 128 个;
  • 有序集合保存的所有元素成员的长度都必须小于 64 字节。

如果不能满足以上两个条件中的任意一个,有序集合将会使用 skiplist 结构进行存储。

应用场景

① 排行榜

有序集合比较典型的使用场景就是排行榜系统。例如学生成绩的排名。某视频(博客等)网站的用户点赞、播放排名、电商系统中商品的销量排名等。我们以博客点赞为例。

  1. 添加用户赞数

例如小编Tom发表了一篇博文,并且获得了10个赞。

Copyzadd user:ranking arcticle1 10
  1. 取消用户赞数

这个时候有一个读者又觉得Tom写的不好,又取消了赞,此时需要将文章的赞数从榜单中减去1,可以使用zincrby。

Copyzincrby user:ranking arcticle1 -1
  1. 查看某篇文章的赞数
CopyZSCORE user:ranking arcticle1
  1. 展示获取赞数最多的十篇文章

此功能使用zrevrange命令实现:

Copyzrevrangebyrank user:ranking  0 9
② 电话号码(姓名)排序

使用有序集合的ZRANGEBYLEX(点击可查看该命令详细说明)ZREVRANGEBYLEX可以帮助我们实现电话号码或姓名的排序,我们以ZRANGEBYLEX为例
注意:不要在分数不一致的SortSet集合中去使用 ZRANGEBYLEX和 ZREVRANGEBYLEX 指令,因为获取的结果会不准确。

  1. 电话号码排序

我们可以将电话号码存储到SortSet中,然后根据需要来获取号段:

Copyredis> zadd phone 0 13100111100 0 13110114300 0 13132110901 
(integer) 3
redis> zadd phone 0 13200111100 0 13210414300 0 13252110901 
(integer) 3
redis> zadd phone 0 13300111100 0 13310414300 0 13352110901 
(integer) 3

获取所有号码:

Copyredis> ZRANGEBYLEX phone - +
1) "13100111100"
2) "13110114300"
3) "13132110901"
4) "13200111100"
5) "13210414300"
6) "13252110901"
7) "13300111100"
8) "13310414300"
9) "13352110901"

获取132号段:

Copyredis> ZRANGEBYLEX phone [132 (133
1) "13200111100"
2) "13210414300"
3) "13252110901"

获取132、133号段:

Copyredis> ZRANGEBYLEX phone [132 (134
1) "13200111100"
2) "13210414300"
3) "13252110901"
4) "13300111100"
5) "13310414300"
6) "13352110901"
  1. 姓名排序

将名称存储到SortSet中:

Copyredis> zadd names 0 Toumas 0 Jake 0 Bluetuo 0 Gaodeng 0 Aimini 0 Aidehua 
(integer) 6

获取所有人的名字:

Copyredis> ZRANGEBYLEX names - +
1) "Aidehua"
2) "Aimini"
3) "Bluetuo"
4) "Gaodeng"
5) "Jake"
6) "Toumas"

获取名字中大写字母A开头的所有人:

Copyredis> ZRANGEBYLEX names [A (B
1) "Aidehua"
2) "Aimini"

获取名字中大写字母C到Z的所有人:

Copyredis> ZRANGEBYLEX names [C [Z
1) "Gaodeng"
2) "Jake"
3) "Toumas"

6、Stream

7、bitmap

应用场景

① 用户签到

很多网站都提供了签到功能,并且需要展示最近一个月的签到情况,这种情况可以使用 BitMap 来实现。
根据日期 offset = (今天是一年中的第几天) % (今年的天数),key = 年份:用户id。

如果需要将用户的详细签到信息入库的话,可以考虑使用一个一步线程来完成。

② 统计活跃用户(用户登录情况)

使用日期作为 key,然后用户 id 为 offset,如果当日活跃过就设置为1。具体怎么样才算活跃这个标准大家可以自己指定。

假如 20201009 活跃用户情况是: [1,0,1,1,0]
20201010 活跃用户情况是 :[ 1,1,0,1,0 ]

统计连续两天活跃的用户总数:

bitop and dest1 20201009 20201010 
# dest1 中值为1的offset,就是连续两天活跃用户的ID
bitcount dest1

统计20201009 ~ 20201010 活跃过的用户:

bitop or dest2 20201009 20201010 
③ 统计用户是否在线

如果需要提供一个查询当前用户是否在线的接口,也可以考虑使用 BitMap 。即节约空间效率又高,只需要一个 key,然后用户 id 为 offset,如果在线就设置为 1,不在线就设置为 0。

8、GeoHash

9、HyperLogLog

应用场景

① 统计日活、月活

日活:pfadd {日期} {ip} {ip}...

月活:pfmerge {本月日期} {本月日期}...

五、三种 Java 客户端对比(Jedis、lettuce 和 Redisson)

https://www.cnblogs.com/54chensongxia/p/13815761.html

六、Lua 脚本

https://redis.io/docs/manual/programmability/eval-intro/

七、常用缓存读写策略

1、旁路缓存模式(Cache Aside)

这是现在最常用的模式,下面两种为什么不常用稍后会解释。

写:

  • 更新数据库
  • 删除缓存

读:

  • 命中缓存, 读缓存
  • 没命中缓存, 读数据库, 更新缓存

优点:
1、旁路缓存模式可以在一定程度上有效的解决双写不一致的问题。在更新数据库后删除缓存的时候可以选择异步延时删除缓存,异步可以让响应更快的返回,而延时可以防止更新数据库和删除缓存的操作快于查询和更新缓存的操作(例如:A 线程读取旧数据,B 更新数据后删除缓存,A 线程将旧数据放入缓存)而造成数据库和缓存数据不一致,另外延时操作可以一定程度上保证数据库在读写分离的模式下,主库数据同步到从库再删除缓存,这样可以防止数据还未同步就删除缓存,然后脏数据又被从从库中读取出来放入缓存的情况。不过异步可能会存在缓存删除失败的场景,所以需要设计删除失败后重试的逻辑。

缺陷:
1、在写操作过多的场景下,会频繁删除数据,读的压力会直接落在数据库上(下面的读写穿透模式会解决这个问题)。
2、首次请求的数据也需要先读数据库,不过可以通过预热数据来解决。
3、旁路缓存模式还是会存在双写不一致的情况(读和更新缓存操作时间大于写和删除缓存操作),解决的方案就是不删除缓存,而是更新缓存,然后用分布式锁来保证缓存的更新是线程安全的,牺牲部分性能来保证强一致。而对一致性要求不高或者允许短时间内数据库和缓存数据不一致的场景,可以给数据一个合理的过期时间来降低影响。

2、读写穿透模式(Read/Write Through)

读写穿透模式其实就是把缓存当作是数据库,更新和读取都是操作缓存,然后缓存在去同步数据库,不常用。

写:

  • 命中直接更新缓存,缓存再同步数据库
  • 没有命中的话直接更新数据库

读:

  • 命中缓存直接获取数据
  • 没有命中缓存直接读数据库,将数据缓存后返回

优点:
1、解决了双写一致性问题,保证了强一致的前提下没有牺牲性能。
2、应用只和缓存打交道,所有数据库数据的更新和读取都由缓存完成,使代码更加简洁,提高了可维护性。

缺点:
1、需要中间件支撑或使用的缓存有同步数据库的功能,但是现在主流的缓存都没有这个功能或支持这个功能的中间件,所以只能是使用本地缓存用用这个模式。
2、要统一缓存和数据库数据的结构。

3、Write-Behind

不知道中文名是什么,反正这个模式和读写穿透的区别就是更新数据库数据是异步的,性能会大大提升,但是会有双写不一致问题。

Write-Behind 的核心思想就是异步去同步数据,方法有很多,你可以立刻异步执行,或者一段时间后执行,或者触发某个条件后批量插入等等。

写:

  • 写缓存

读:

  • 读缓存,读不到从数据库读

优点:
1、因为是异步写入,所以性能会好很多。

缺陷:
1、同样需要缓存支持这种功能(https://docs.redis.com/latest/modules/redisgears/python/recipes/write-behind/)。
2、会造成双写不一致的情况,比如还没有同步缓存就挂掉,或者还没同步就直接通过数据库去查询数据(可以通过查询数据库触发数据同步的方式解决这个问题),所以适合对一致性要求很低的场景,浏览数和点赞数等。

八、慢查询日志

客户端发送命令给 Redis 后,由于 Redis 是单线程的,所以首先需要排队,然后才是执行命令。

慢查询日志记录的是执行过程慢的命令,不包括排队过程。

因为 Redis 中命令执行的排队机制,慢查询会导致其他命令的级联阻塞,所以当客户端出现请求超时的时候,需要检查该时间点是否有慢查询,从而分析出由于慢查询导致的命令级联阻塞。

配置文件中有两个参数可以配置慢查询日志的记录:


# 命令执行时常的阈值,单位是微秒
# 当查询时间大于 xx 微秒时记录下该命令,如果设置成 0 则每条都会记录。
# 在实际生产环境中需要根据 Redis 并发量来调整该配置。
slowlog-log-slower-than 

# 慢查询日志存储日志的最大条数
# 当达到最大条数的时候会删除最早的日志再插入新日志
# 记录慢查询是Redis会对长命令进行截断,不会大量占用大量内存。在实际的生产环境中,为了减缓慢查询被移出的可能和更方便地定位慢查询,建议将慢查询日志的长度调整的大一些。比如可以设置为1000以上。
slowlog-max-len 

动态配置:

> config set slowlog-log-slower-than 1000
OK
> config set slowlog-max-len 1200
OK
> config rewrite
OK

配置会被持久化到本地

九、Redis 淘汰策略

Redis 根据所选的淘汰策略不同,可能会删除没有设置过期时间的数据,所以需要了解每个淘汰策略的特点,才能根据软件的业务场景选择正确的过期策略。

1、过期数据

再说淘汰策略之前先看下 Redis 是怎么保存数据的过期时间的,先看下 Redis 存储数据的数据结构:

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB (指向所有 key 存放空间) */
    dict *expires;              /* Timeout of keys with a timeout set (指向数据的过期字典) */
    ...
}

dict 的 key 和 expire 中的 key 指向的都是同一个键值,expire 的值是 key 的过期时间。

Redis 对过期 key 的删除策略为惰性删除+定时删除

惰性删除

惰性删除是等到查询的时候才去检测数据是否过期,过期的话删除并返回 null,下面是相关源码:

int expireIfNeeded(redisDb *db, robj *key) {
    // 键未过期返回0
    if (!keyIsExpired(db,key)) return 0;

    // 如果运行在从节点上,直接返回1,因为从节点不执行删除操作,可以看下面的复制部分
    if (server.masterhost != NULL) return 1;

    // 运行到这里,表示键带有过期时间且运行在主节点上
    // 删除过期键个数
    server.stat_expiredkeys++;
    // 向从节点和AOF文件传播过期信息
    propagateExpire(db,key,server.lazyfree_lazy_expire);
    // 发送事件通知
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    // 根据配置(默认是同步删除)判断是否采用惰性删除(这里的惰性删除是指采用后台线程处理删除操做,这样会减少卡顿)
    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
                                         dbSyncDelete(db,key);
}

我们通常说 Redis 是单线程的,其实 Redis 把处理网络收发和执行命令的操作都放到了主线程,但 Redis 还有其他后台线程在工作,这些后台线程一般从事 IO 较重的工作,比如刷盘等操作。
上面源码中根据是否配置 lazyfree_lazy_expire(4.0版本引进) 来判断是否执行惰性删除,原理是先把过期对象进行逻辑删除,然后在后台进行真正的物理删除,这样就可以避免对象体积过大,造成阻塞。

定时删除

定期策略是每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU 时间的影响,同时也减少了内存浪费

Redis 默认会每秒进行 10 次(redis.conf 中通过 hz 配置)过期扫描,扫描并不是遍历过期字典中的所有键,而是采用了如下方法

  • 从过期字典中随机取出 20 个键
  • 删除这 20 个键中过期的键
  • 如果过期键的比例超过 25% ,重复步骤 1 和 2
    为了保证扫描不会出现循环过度,导致线程卡死现象,还增加了扫描时间的上限,默认是 25 毫秒(即默认在慢模式下,如果是快模式,扫描上限是 1 毫秒)

对应源码 expire.c/activeExpireCycle 方法

void activeExpireCycle(int type) {
        ...
        do {
           ...
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                // 选过期键的数量,为 20
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

            while (num--) {
                dictEntry *de;
                long long ttl;
                // 随机选 20 个过期键
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ...
                // 尝试删除过期键    
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                ...
            }
            ...
           // 只有过期键比例 < 25% 才跳出循环
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
    ...
}

因为 Redis 在扫描过期键时,一般会循环扫描多次,如果请求进来,且正好服务器正在进行过期键扫描,那么需要等待 25 毫秒,如果客户端设置的超时时间小于 25 毫秒,那就会导致链接因为超时而关闭,就会造成异常,这些现象还不能从慢查询日志(之前分享过慢查询日志的文章 Redis慢查询日志)中查询到,因为慢查询只记录逻辑处理过程,不包括等待时间。
所以我们在设置过期时间时,一定要避免同时大批量键过期的现象,所以如果有这种情况,最好给过期时间加个随机范围,缓解大量键同时过期,造成客户端等待超时的现象

AOF、RDB 和复制功能对过期键的处理

RDB文件
生成 RDB 文件

在执行 save 命令或 bgsave 命令创建一个新的 RDB文件时,程序会对数据库中的键进行检查,已过期的键就不会被保存到新创建的 RDB文件中

载入 RDB 文件

主服务器:载入 RDB 文件时,会对键进行检查,过期的键会被忽略

从服务器:载入 RDB文件时,所有键都会载入。但是会在主从同步的时候,清空从服务器的数据库,所以过期的键载入也不会造成啥影响

AOF文件
AOF 文件写入

当过期键被惰性删除或定期删除后,程序会向 AOF 文件追加一条 del 命令,来显示的记录该键已经被删除

AOF 重写

重启过程会对键进行检查,如果过期就不会被保存到重写后的 AOF 文件中

复制

从服务器的过期键删除动作由主服务器控制

主服务器在删除一个过期键后,会显示地向所有从服务器发送一个 del 命令,告知从服务器删除这个过期键

从服务器收到在执行客户端发送的读命令时,即使碰到过期键也不会将其删除,只有在收到主服务器的 del 命令后,才会删除,这样就能保证主从服务器的数据一致性

2、Redis 的淘汰策略

常见的淘汰策略大致分为三类:

  • FIFO 先进先出,如果缓存容量满,则优先移出最早加入缓存的数据。
  • LFO 根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
  • LRU least recently used,是目前最常用的缓存算法和设计方案之一,其移除策略为“当缓存(页)满时,优先移除最近最久未使用的数据”,优点是易于设计和使用,适用场景广泛。

淘汰策略相关配置:

#设置Redis 内存大小的限制,我们可以设置maxmemory ,当数据达到限定大小后,会选择配置的策略淘汰数据
maxmemory 300mb

#设置Redis的淘汰策略。
maxmemory-policy volatile-lru

而 Redis 的淘汰策略一共有 8 种,分为三类:

不淘汰 noeviction

  • noeviction 默认情况下,Redis 在使用的内存空间超过 maxmemory 值时,并不会淘汰数据,也就是设定的 noeviction 策略。对应到 Redis 缓存,也就是指,一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误。

在设置了过期时间的数据中进行淘汰 volatile-random、volatile-ttl、volatile-lru、volatile-lfu

  • volatile-ttl 在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
  • volatile-random 就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
  • volatile-lru 会使用 LRU 算法筛选设置了过期时间的键值对。
  • volatile-lfu 会使用 LFU 算法选择设置了过期时间的键值对。

在所有数据范围内进行淘汰 allkeys-lru、allkeys-random、allkeys-lfu

  • allkeys-random 策略,从所有键值对中随机选择并删除数据。
  • allkeys-lru 策略,使用 LRU 算法在所有数据中进行筛选。
  • allkeys-lfu 策略,使用 LFU 算法在所有数据中进行筛选。

3、Redis 中的 LRU 和 LFU

LRU

因为使用 LRU 需要维护一个非常大的链表,需要很大的成本,所以 Redis 对原来的 LRU 算法做了一些精简。

首先了解一次 Redis 怎么知道一个数据有多旧。Redis 维护了一个时钟字段(秒),在数据插入时会 key 获取并维护这个时钟,这个时钟字段是 24 位的,最大可以存储 194 天,也就是说这个时钟在 194 天后会被重置,所以在 key 的时钟大于系统时钟时,计算时间差的方式为相加。

然后说一下 Redis 的 LRU 是如何淘汰数据的。Redis 会获取一定数量的样本,然后对这些样本进行计算获得一个创建时距离当前时间的一个权值 idle,然后根据 idle 升序排序,将样本数据插入缓冲池 eviction pool 中,缓冲池是一个大小为 16 数组,存储的是上一次的样本数据,这次的样本数据只有大于缓冲池中最大的数据才能够进入缓冲池,然后缓冲池中最大的数据会被淘汰。

下面为 LRU 的相关配置:

# 每次选取的样本数量,配置越大,越接近LRU
maxmemory-samples 10

LFU

Redis 在 4.0 时新添加了 LFU 的策略,为了是解决一个使用频率很高但是最近没有使用过的数据被删除,而新访问却很少被用到的数据被保留下来的情况。LFU 就是为了解决这个问题而出现的。

LFU 是基本使用频率的淘汰策略,它把原先 key 的时钟字段分成了两部分,前 16 位还是表示时钟(时),后 8 位为频率计数器,频率的计算公式为 1.0 / (计算器旧值 * lfu-log-factor + 1),而 lfu-log-factor 是可以配置的,如果一个key经过几分钟没有被命中,那么后8位的值是需要递减几分钟,具体递减几分钟根据衰减因子 lfu-decay-time 来控制,下面是相关源码和配置:

  uint8_t LFULogIncr(uint8_t counter) {
      if (counter == 255) return 255;
      double r = (double)rand()/RAND_MAX;
      double baseval = counter - LFU_INIT_VAL;
      if (baseval < 0) baseval = 0;
      double p = 1.0/(baseval*server.lfu_log_factor+1);
      if (r < p) counter++;
      return counter;
  }

  unsigned long LFUDecrAndReturn(robj *o) {
    unsigned long ldt = o->lru >> 8;
    unsigned long counter = o->lru & 255;
    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    return counter;
}
lfu-log-factor 10
lfu-decay-time 1

九、持久化(RDB、AOF)

https://redis.io/docs/manual/persistence/

十一、内存碎片问题

十二、集群问题

1、raft 算法

2、脑裂问题

3、缓存倾斜

十三、分布式锁

十四、缓存穿透、缓存击穿、缓存雪崩

十五、参考文章

Redis数据结构——简单动态字符串SDS - Mr于 - 博客园 (cnblogs.com)(及其其他 Redis 文章)

Redis

Redis 中 BitMap 的使用场景 - 程序员自由之路 - 博客园 (cnblogs.com)

Redis缓存有哪些淘汰策略 - 掘金 (juejin.cn)

缓存读写策略

redis的过期时间和过期删除机制

一篇文章快速搞懂Redis的慢查询分析

缓存淘汰策略的三个代表

Redis 的过期策略是如何实现的?

posted @ 2022-04-14 23:35  快点ヤ给我起来♪♫  阅读(111)  评论(0编辑  收藏  举报