主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题
  • 高并发写的问题

 使用分片集群可以解决上述问题,分片集群特征:

  • 集群中有多个master,每个master保存不同数据。
  • 每个master都可以有多个slave节点
  • master之间通过ping监测彼此健康状态

【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_内存回收

搭建分片集群

【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_Redis_02

计划部署的节点信息如下:

容器名

角色

IP

映射端口

r1

master

192.168.21.129

7001

r2

master

192.168.21.129

7002

r3

master

192.168.21.129

7003

r4

slave

192.168.21.129

7004

r5

slave

192.168.21.129

7005

r6

slave

192.168.21.129

7006

分片集群中的Redis节点必须开启集群模式,一般在配置文件中添加下面参数:

port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • cluster-enabled:是否开启集群模式
  • cluster-config-file:集群模式的配置文件名称,无需手动创建,由集群自动维护
  • cluster-node-timeout:集群中节点之间心跳超时时间

一般搭建部署集群肯定是给每个节点都配置上述参数,不过考虑到我们计划用docker-compose部署,因此可以直接在启动命令中指定参数

在虚拟机的/root目录下新建一个redis-cluster目录,然后在其中新建一个docker-compose.yaml文件,内容如下:

version: "3.2"

services:
  r1:
    image: redis
    container_name: r1
    network_mode: "host"
    entrypoint: ["redis-server", "--port", "7001", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
  r2:
    image: redis
    container_name: r2
    network_mode: "host"
    entrypoint: ["redis-server", "--port", "7002", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
  r3:
    image: redis
    container_name: r3
    network_mode: "host"
    entrypoint: ["redis-server", "--port", "7003", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
  r4:
    image: redis
    container_name: r4
    network_mode: "host"
    entrypoint: ["redis-server", "--port", "7004", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
  r5:
    image: redis
    container_name: r5
    network_mode: "host"
    entrypoint: ["redis-server", "--port", "7005", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
  r6:
    image: redis
    container_name: r6
    network_mode: "host"
    entrypoint: ["redis-server", "--port", "7006", "--cluster-enabled", "yes", "--cluster-config-file", "node.conf"]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

注意:使用Docker部署Redis集群,network模式必须采用host

进入/root/redis-cluster目录,使用命令启动redis:

docker-compose up -d
  • 1.

启动成功,可以通过命令查看启动进程:

ps -ef | grep redis
# 结果:
root       4822   4743  0 14:29 ?        00:00:02 redis-server *:7002 [cluster]
root       4827   4745  0 14:29 ?        00:00:01 redis-server *:7005 [cluster]
root       4897   4778  0 14:29 ?        00:00:01 redis-server *:7004 [cluster]
root       4903   4759  0 14:29 ?        00:00:01 redis-server *:7006 [cluster]
root       4905   4775  0 14:29 ?        00:00:02 redis-server *:7001 [cluster]
root       4912   4732  0 14:29 ?        00:00:01 redis-server *:7003 [cluster]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

可以发现每个redis节点都以cluster模式运行。不过节点与节点之间并未建立连接。

接下来,我们使用命令创建集群:

# 进入任意节点容器
docker exec -it r1 bash
# 然后,执行命令
redis-cli --cluster create --cluster-replicas 1 \
192.168.21.129:7001 192.168.21.129:7002 192.168.21.129:7003 \
192.168.21.129:7004 192.168.21.129:7005 192.168.21.129:7006
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
    • redis-cli --cluster:代表集群操作命令
    • create:代表是创建集群
    • --cluster-replicas 1 :指定集群中每个master的副本个数为1

    接着,我们可以通过命令查看集群状态:

    redis-cli -p 7001 cluster nodes
    • 1.

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_缓存_03

    散列插槽

    在Redis集群中,共有16384个hash slots,集群中的每个master节点都会分配一定数量的hash slots:

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_Redis_04

    Redis数据不是与节点绑定,而是与插槽slot绑定。当我们读写数据时,Redis基于CRC16算法对key做hash运算,得到的结果与16384取余,就计算出了这个key的slot值。然后到slot所在的Redis节点执行读写操作。

    redis在计算key的hash值是不一定是根据整个key计算,分两种情况∶

    • 当key中包含0时,根据{之间的字符串计算hash slot
    • 当key中不包含0时,则根据整个key字符串计算hash slot

    例如: key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。

    Redis数据结构

    RedisObject

    Redis中的任意数据类型的键和值都会被封装为一个RedisObject,也叫做Redis对象,源码如下(Redis用c语言写的):

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_缓存_05

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_Redis_06

    Redis的编码方式

    Redis中会根据存储的数据类型不同,选择不同的编码方式,共包含12种不同类型:

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_缓存_07

    五种数据结构

    Redis中会根据存储的数据类型不同,选择不同的编码方式。每种数据类型的使用的编码方式如下:

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_内存回收_08

    SkipList

    SkipList(跳表)首先是链表,但与传统链表相比有几点差异:

    元素按照升序排列存储

    节点可能包含多个指针,指针跨度不同。

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_内存回收_09

    最多32级指针

    特点:

    • 跳跃表是一个有序的双向链表
    • 每个节点都可以包含多层指针,层数是1到32之间的随机数
    • 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
    • 增删改查效率与红黑树基本一致,实现却更简单。但空间复杂度更高

    SortedSet

    SortedSet数据结构的特点是:

    • 每组数据都包含score和member
    • member唯一
    • 可根据score排序

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_缓存_10

    SortedSet的底层数据结构是怎样的?

    • 首先SortedSet需要能存储score和member值,而且要快捷的根据member查询score,因此底层有一个哈希表以member为键,以score为value
    • 其次SortedSet还需要能根据score排序,因此底层还维护了一个跳表。
    • 当需要根据member查询score时,就去哈希表中查询;当需要根据score排序查询时,则基于跳表查询

    Redis内存回收

    过期KEY处理

    Redis提供了expire命令,给key设置TTL(存活时间)

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_缓存_11

    可以发现,当key的TTL到期以后,再次访问name返回的是nil,说明这个key已经不存在了,对应的内存也得到释放。从而起到内存回收的目的。

    Redis是如何知道一个key是否过期

    Redis的本身是键值型数据库,其所有数据都存在一个redisDB的结构体中,其中包含两个哈希表:.

    • dict:保存Redis中所有的键值对
    • expires:保存Redis中所有的设置了过期时间的KEY及其到期时间(写入时间+TTL)

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_缓存_12

    是不是TTL到期就立即删除了

    Redis并不会实时监测key的过期时间,在key过期后立刻删除。而是采用两种延迟删除的策略:

    • 惰性删除:当有命令需要操作一个key的时候,检查该key的存活时间,如果已经过期才执行删除。
    • 周期删除:通过一个定时任务,周期性的抽样部分有TTL的key,如果过期则执行删除。

    周期删除的定时任务执行周期有两种:

    • SLOW模式:默认执行频率为每秒10次,但每次执行时长不能超过25ms,受server.hz参数影响。
    • FAST模式:频率不固定,跟随Redis内部I0事件循环执行。两次任务之间间隔不低于2ms,执行时长不超过1ms

    内存淘汰策略

    内存淘汰:就是当Redis内存使用达到设置的阈值时,Redis主动挑选部分key删除以释放更多内存的流程。Redis会在每次处理客户端命令时都会对内存使用情况做判断,如果必要则执行内存淘汰。内存淘汰的策略

    noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。

    volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰

    allkeys-random:对全体key,随机进行淘汰。也就是直接从db->dict中随机挑选

    volatile-random:对设置了TTL的key,随机进行淘汰。也就是从db->expires中随机挑选。

    allkeys-Iru:对全体key,基于LRU算法进行淘汰

    volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰

    allkeys-lfu:对全体key,基于LFU算法进行淘汰

    volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰(推荐)

    LRU(Least Recently Used),最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

    LFu(Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_数据结构_13

    LFU的访问次数之所以叫做逻辑访问次数,是因为并不是每次key被访问都计数,而是通过运算:

    1)生成[0~1)之间的随机数R

    2)计算1/(旧次数*lfu_log_factor + 1),记录为P,lfu_log_factor默认为10

    3)如果R<P,则计数器+1,且最大不超过255

    4)访问次数会随时间衰减,距离上一次访问时间每隔lfu_decay_time分钟(默认1),计数器-1

    缓存问题

    缓存一致性

    我们先看下目前企业用的最多的缓存模型。缓存的通用模型有三种:

    • Cache Aside:有缓存调用者自己维护数据库与缓存的一致性。即:
    • 查询时:命中则直接返回,未命中则查询数据库并写入缓存
    • 更新时:更新数据库并删除缓存,查询时自然会更新缓存
    • Read/Write Through:数据库自己维护一份缓存,底层实现对调用者透明。底层实现:
    • 查询时:命中则直接返回,未命中则查询数据库并写入缓存
    • 更新时:判断缓存是否存在,不存在直接更新数据库。存在则更新缓存,同步更新数据库
    • Write Behind Cahing:读写操作都直接操作缓存,由线程异步的将缓存数据同步到数据库

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_缓存_14

    那到底是先更新数据库再删除缓存,还是先删除缓存再更新数据库呢?

    假如先删除Redis

    现在假设有两个线程,一个来更新数据,一个来查询数据。

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_内存回收_15

    • 线程1删除缓存后,还没来得及更新数据库,
    • 此时线程2来查询,发现缓存未命中,于是查询数据库,写入缓存。由于此时数据库尚未更新,查询的是旧数据。也就是说刚才的删除白删了,缓存又变成旧数据了。

    所以先操作数据库,再删Redis

    当然这种也有并发安全问题,但是概率极低

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_缓存_16

    缓存一致性策略的最佳实践方案:

    1.低一致性需求:使用Redis的key过期清理方案

    2.高一致性需求:主动更新,并以超时剔除作为兜底方案

    读操作:

    • 缓存命中则直接返回
    • 缓存未命中则查询数据库,并写入缓存,设定超时时间

    写操作:

    • 先写数据库,然后再删除缓存.
    • 要确保数据库与缓存操作的原子性

    缓存穿透

    缓存穿透是指客户端请求的数据在数据库中根本不存在,从而导致请求穿透缓存,直接打到数据库的问题。

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_数据结构_17

    常见的解决方案有两种:

    缓存空对象

    优点:实现简单,维护方便

    缺点:额外的内存消耗布隆过滤

    布隆过滤

    优点:内存占用较少,没有多余key

    缺点:

    实现复杂

    存在误判可能

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_Redis_18

    布隆过滤是一种数据统计的算法,用于检索一个元素是否存在一个集合中。但是布隆过滤无需存储元素到集合,而是把元素映射到一个很长的二级制数位上。

    • 首先需要一个很长很长的二级制数,默认每一位都是0
    • 然后需要N个不同算法的哈希函数
    • 将集合中的元素根据N个哈希函数做运算,得到N个数字,然后将每个数字对应的bit位标记为1
    • 要判断某个元素是否存在,只需要把元素按照上述方式运算,判断对应的bit位是否是1即可

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_内存回收_19

    缓存雪崩

    缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_数据结构_20

    解决方案:

    • 给不同的Key的TTL添加随机值
    • 利用Redis集群提高服务的可用性
    • 给缓存业务添加降级限流策略
    • 给业务添加多级缓存

    缓存击穿

    缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

    由于我们采用的是Cache Aside模式,当缓存失效时需要下次查询时才会更新缓存。当某个key缓存失效时,如果这个key是热点key,并发访问量比较高。就会在一瞬间涌入大量请求,都发现缓存未命中,于是都会去查询数据库,尝试重建缓存。可能一瞬间就把数据库压垮了。

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_Redis_21

    解决方案:

    • 互斥锁
    • 逻辑过期

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_内存回收_22

    【Redis】-分片、数据结构、内存回收以及缓存问题(详细版)_内存回收_23