Redis

Redis主从

搭建主从集群

单节点Redis的并发能力有上限(上万),要进一步提高Redis的并发能力,就要搭建主从集群,实现读写分离。

主节点会把数据同步给从节点,让每个从节点的数据和主节点一样。

  1. 启动多个Redis实例
  2. 建立集群

从节点通过命令配置主从关系:slaveof <masterip> <masterport>


info replication:查看节点状态

  • 临时:在控制台输入slaveof命令
  • 永久:在redis.conf文件中利用slaveof命令指定master节点

成功后,进入主节点,输入info replication可以看到两个slave,并且在主节点写入后,在从节点可以读取到。

主从同步原理

当主从第一次同步连接或断开重连时,从节点都会发送psync请求,尝试数据同步

【全量同步】:执行bgsave命令,将完整的内存数据生成RDB文件,把RDB文件写到磁盘中,再通过网络传送到slave(效率超级低)

【增量同步】:通过repl_backlog缓冲区,对比主从节点之间的命令差异,发送slave未同步的命令给slave

  • replicationID:每个master节点都有自己唯一的id,主从节点建立连接后,主从节点的replid都保持一致,从节点请主节点时会携带replid,如果replid和主节点的replid一致,说明从节点不是第一次来同步;否则replid就是第一次来同步
  • offset:repl_backlog中写入过的数据长度,写操作越多,offset值越大,主从的offset一致代表数据一致。

【问题】repl_backlog是一个缓冲区,用来记录slave和master建立连接后,master中的写命令,但是这个缓冲区的大小默认只有1M。

【解决】repl_backlog是一个环形数组,但是如果slave宕机了很长时间,缓冲区slave还没同步的数据又被master覆盖了,此时slave只能做全量同步(效率低)。

【执行全量同步的时机】:slave节点第一次连接master节点;slave节点断开时间太久,repl_backlog中的offset已经被覆盖。

【执行增量同步的时机】:slave节点断开又恢复,并在repl_backlog中能找到offset时。

全量同步效率低的解决

  1. 在master中配置repl-diskless-sync: yes启动无磁盘复制,避免全量同步时的磁盘IO。
  2. redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO。
  3. 适当提高repl_backlog的大小,发现slave宕机时尽快实现故障恢复,尽量避免全量同步。
  4. 限制一个master上的slave节点数量,如果实在太多slave,可以采用主-从-从链式结构,减少master压力。

Redis哨兵

哨兵原理

哨兵Sentinel机制来实现主从集群的自动故障恢复。哨兵的作用:

  1. 监控:Sentinel会不断检查master和slave是否按预期工作。
  2. 自动故障恢复:如果master故障,sentinel会将一个slave提升为master。当故障恢复后也以新的master为主。
  3. 通知:当集群发生故障转移时,sentinel会将最新节点角色信息推送给新的redis客户端。

1. 服务状态监控

sentinel基于心跳机制检测服务状态,每隔1s向集群的每个实例发送ping命令:

  • 主观下线:如果某个sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
  • 客观下线:如果超过指定数量(quorum)的sentinel都认为该实例主管下线,则该实例客观下线。quorum的值最好超过sentinel实力数量的一半。

2. 选举新的master

一旦发现master故障,sentinel需要在slave中选择一个作为新的mater,选择依据:

  1. 首先判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点。
  2. 然后判断slave节点的slave-priority值,值越小优先级越高。(如果是0则用不参与选举,默认是0)
  3. 如果slave-priority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高。
  4. 最后是判断slave节点的运行id大小,越小优先级越高。

3. 故障转移

当选中其中一个slave为新的master后,故障转移的步骤:

  1. sentinel给备选的slave节点发送slaveof no one命令,让该节点成为master。
  2. sentinel给所有其他slave发送slaveof 192.168.140.101 7002命令,让这些slave成为新的master的从节点,开始从新的master上同步数据。
  3. 最后sentinel将故障节点标记为slave(修改故障节点的配置文件redis.conf),当故障节点恢复后,会自动成为新的master的slave节点。

搭建哨兵集群

哨兵的配置文件sentinel.conf:

conf 复制代码
sentinel announce-ip "192.168.140.101"
sentinel monitor hmaster 192.168.140.101 7001 2
sentinel down-after-milliseconds hmaster 5000
sentinel failover-timeout hmaster 60000
  • sentinel announce-ip:当前sentinel的ip
  • sentinel monitor hmaster 192.168.140.101 7001 2:sentinel monitor 主节点名 主节点ip 主节点端口 认定master下线的quorum值(sentinel监控配置)
  • sentinel down-after-milliseconds hmaster 5000:哨兵ping节点,超过5s就算超时
  • sentinel failover-timeout hmaster 60000:哨兵监测到主节点宕机后做故障恢复,如果故障恢复又失败了, 再过60s再次做故障恢复。

Redis分片集群

搭建分片集群

【问题】主从和哨兵可以解决高可用、高并发度的问题,但是还存在两个问题:海量数据存储问题;高并发写的问题。

【解决】使用分片集群可以解决,分片集群的特征:

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

分片集群可以理解成有多个主从集群组合成的,且不需要哨兵节点,master之间可以通过ping来判断是否下线。

  1. 使用docker-compose部署,新建docker-compose.yaml文件:
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"]

输入docker compose up -d启动redis集群

  1. 使用命令创建集群:

进入任意节点容器:

docker exec -it r1 bash

执行命令:

redis-cli --cluster create --cluster-replicas 1 \
192.168.140.101:7001 192.168.140.101:7002 192.168.140.101:7003 \
192.168.140.101:7004 192.168.140.101:7005 192.168.140.101:7006

1:表示副本数量,一个主、一个从

redis会默认前三个是主节点,后三个是从节点

散列插槽

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

redis数据不是与节点绑定,而是与插槽slot绑定。当读写数据时,Redis基于CRC16算法对key做hash运算,将得到的结果与16384取余,就计算出这个key的slot值。

redis在计算key的hash值又分成两种情况:

  1. key中包含{},根据{}之间的字符串计算hash slot
  2. key中不包含{},根据整个key字符串计算hash slot

user这个key计算出的hash值时5474,如果按照之前的方式建立连接,那么在集群1(范围:0-5460)中插入hash值为5474的key,会报错。

解决办法:集群模式下建立连接时,应该加上-c参数:redis-cli -c -p 7001

Redis数据结构

RedisObject

redis中任意数据类型的键和值都会被封装成一个RedisObject,也叫Redis对象

type:数据类型(string、hash、list、set、zset)

encoding:数据在内存中的存储方式

lru:对象最近一次被访问的时间(太久没被访问的key会在内存不足时淘汰)

refcount:当前对象如果被别人引用了,这个值就会+1,如果为0也会被回收

*ptr:指向实际存放数据的内存地址

SkipList

跳表,首先是链表,与传统链表的差异:

  • 元素按照升序排列
  • 节点可以包含多个指针,指针跨度不同

跳表是一个有序的双向链表,每个节点可以包含多层指针(最多允许32层),层级越高,跨度越大,增删改查效率和红黑树基本一致,实现更简单,但是空间复杂度更高

SortedSet(zset)

特点:

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

dict:hashtable(存储score、member;member为键,score为值。可以满足member唯一性)

zsl:skiplist(存储score、member,在排序时根据score排序)

根据member得到score:去查hash表

直到某个元素的score排名:去hashtable里,根据member查到score,再去skiplist里根据score得到排名

Redis内存回收

内存过期处理

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

过期key处理

redis的本质还是键值型数据库,所有的数据都存储在redisDB的结构中,其中包含两个哈希表

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

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

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

内存淘汰策略

内存淘汰:当Redis内存达到设置的阈值时,Redis就会主动挑选部分key删除以释放更多的内存。

Redis在每次处理客户端命令时,都会对内存使用情况判断,如果必要,则执行内存淘汰。内存淘汰的策略有:

  1. 前缀:
    • allkeys:对所有的key进行淘汰,从dict的哈希表中挑选
    • volatile:只对设置了TTL的key进行淘汰,从expires的哈希表中挑选
  2. 后缀:
    • ttl:淘汰ttl小的
    • random:随机挑选
    • lru:基于LRU算法
    • lfu:基于LFU算法

LRU(最近最少使用) :用当前时间 - 最后一次访问时间,值越大越先被淘汰(越久没被访问到的越先淘汰)
LFU(最少频率使用):统计每个key的访问频率,值越小越先被淘汰

那么redis怎么直到最近一次访问时间(LRU)或 访问次数(LFU)呢?

在redisObject这个数据结构里:

  • 当使用LRU策略时,lru这个变量存储的是以为单位的最近一次访问时间。
  • 当使用LFU策略时,lru这个变量:
    • 高16位以分钟为单位的最近一次访问时间。
    • 低8位记录逻辑访问次数

Redis缓存问题

缓存一致性

保证缓存一致性主要有三种模式:

  • Cache Aside Pattern(常用)
  • Read / Write Through Pattern
  • Write Behind Caching Pattern

Cache Aside模式

业务的开发者 在更新数据库的同时更新缓存。有一定的业务侵入,但是一致性更好

【注1】:如何保证redis与数据库的一致性?

从数据库中一条数据,可以直接把这条数据存到redis缓存中;

但是如果需要做增删改操作时,redis中可以直接删除,没必要对redis中的数据也做同样的增删改操作。

【注2】:在做增删改操作时,应该先删除redis再改数据库,还是先改数据库再删redis?

如果先删redis,再改数据库:当有一个线程A需要删除数据时,它先将redis中的数据删除,来到数据库,此时又有一个线程B来查询同一条数据,先去查询redis未命中,也来到数据库中,此时如果是线程B先执行了查询操作,线程B将查询后的结果又存入redis中,线程A后把这条数据删除。但是redis中却存在线程B的数据,就出现了数据的不一致性,所以需要先删除数据库再改redis。

Read / Write Through模式

缓存与数据库整合为一个服务(没有现成的哈哈哈哈哈),由服务来维护一致性。业务开发者直接调用该服务接口,无需关心一致性问题。

Write Behind Caching模式

增删改查业务直接基于缓存,由其他线程异步调用的将缓存数据持久化到数据库,保证最终一致性。(高性能,弱 / 最终一致性)

缓存穿透

缓存穿透:客户请求的数据在数据库中不存在,就不会写入缓存,这将导致每次查询这个该数据都会去访问数据库,可能导致数据库挂掉。

只要用户请求的是不存在的数据,那么每次请求都会发到数据库中

缓存空对象

实现简单,维护方便,但是会有额外的内存消耗。

布隆过滤

请求过来会先经过布隆过滤器,布隆过滤器先判断数据库中是否存在这条数据,如果不存在,就会拒绝这个请求。

注意,布隆过滤器判断一个元素不存在时,它绝对不存在;但是如果它判断一个元素存在,这个元素可能会不存在。

缓存雪崩

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

解决:

  1. 给不同的key的TTL添加随机值(避免大量的缓存key同时失效)
  2. 利用redis集群提高服务的可用性(避免服务宕机)
  3. 给缓存业务添加降级限流策略(防止大量的请求过来)
  4. 给业务添加多级缓存(建立nginx缓存、JVM本地缓存...)

缓存击穿

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

解决方案:

  1. 互斥锁:一个线程查询数据库的时候加锁,此时其他线程都无法访问。

  2. 逻辑过期:线程1第一次来查询这个数据,发现数据过期,就先获取互斥锁,(此时会开启一个新的线程,线程1暂时返回过期数据)。

    开启的新线程(线程2)就重新查询数据,写入缓存,最后释放锁。

    在新线程(线程2)写入缓存这一过程中,如果还有别的线程(线程3、4)要访问,它先去获取互斥锁,发现互斥锁之前已经被别的线程获取了,此时它也直接返回过期的数据。直到数据被更新为止。

穿透就是毫无攻击性直达数据库,因为是不存在的缓存,雪崩就是同时过期或宕机,击穿就是有攻击性因为是存在的缓存跟它战斗然后击穿。

相关推荐
余衫马41 分钟前
CentOS7 离线安装 Postgresql 指南
数据库·postgresql
E___V___E1 小时前
MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 高级篇 part 2
数据库·笔记·mysql
m0_748254882 小时前
mysql之如何获知版本
数据库·mysql
mikey棒棒棒2 小时前
Redis——优惠券秒杀问题(分布式id、一人多单超卖、乐悲锁、CAS、分布式锁、Redisson)
数据库·redis·lua·redisson·watchdog·cas·并发锁
水手胡巴3 小时前
oracle apex post接口
数据库·oracle
史迪仔01126 小时前
【SQL】SQL多表查询
数据库·sql
Quz6 小时前
MySQL:修改数据库默认存储目录与数据迁移
数据库·mysql
Familyism6 小时前
Redis
数据库·redis·缓存
隔壁老登6 小时前
查询hive指定数据库下所有表的建表语句并生成数据字典
数据库·hive·hadoop
sekaii7 小时前
ReDistribution plan细节
linux·服务器·数据库