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

并且注释掉:

# bind 127.0.0.1 -::1

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

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

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

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

内存淘汰机制:

maxmemory-policy allkeys-lru
# 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、跳表

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

Redis 详解 (自用)

查询:从最*索引依次查询

插入:插入的时候会根据幂次定律(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、压缩表

Redis 详解 (自用)

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 {本月日期} {本月日期}...

五、Lua 脚本

持续更新...

六、淘汰策略

持续更新...

七、持久化(RDB、AOF)

RDB 内存快照
持续更新...
AOF 追加日志
持续更新...

八、集群问题

1、raft 算法

持续更新...

2、脑裂问题

持续更新...

3、缓存倾斜

持续更新...

九、分布式锁

持续更新...

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

持续更新...

十一、参考文章

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

Redis

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

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

上一篇:Huffman编码


下一篇:Huffman树及Huffman编码的算法实现