Redis

关于内存与硬盘的特点

硬盘
内存

寻址速度

ms级别

ns级别(快了10w倍)

带宽

G/M

很大很大

可持久存储

硬盘的原理:

  • https://www.bilibili.com/video/BV11a4y1x7PC

  • https://blog.csdn.net/xiaominthere/article/details/19756551

基础知识:

  1. 扇区,是指磁盘上划分的区域。

  2. 磁道按照512个字节为单位划分为等分,每个等分的弧锻叫做扇区。

  3. 磁盘驱动器在向磁盘读取和写入数据时,要以扇区为单位。

buffer io:

  1. 因为磁盘的读取数据单位只有512byte很小,所以索引数据时的全量索引会造成更多次数的读取操作,所以读取速度过慢

  2. 所以在操作系统读取磁盘时,会增加一个4k大小的buffer io,操作系统每读取一次io,都会至少读取4k的内容

  3. 这样就提高了硬盘的读取速度

数据读取的发展历程

  1. 基于IO的全量读取(比如cat/grep):随着文件内容增大,硬盘的IO将会成为瓶颈,硬盘的读取速度追不上程序的处理速度

  2. 数据库数据的读取:

    1. 数据库将要存储的数据按照4k大小的data page小格子存储,并给data page创建编号

    2. 提供了数据库索引的功能,索引也是data page存储的数据,存储的内容主要是对应索引指向的所有数据的data page编号

    3. 为了避免数据增删改会造成数据移动、甚至是重建数据库索引的情况,创建表时,每个字段的宽度都是要指定的

    4. 在查询时,会将B+T加载到内存中,根据B+T的区间和偏移,确定叶子结点的data page编号,根据编号最后读取数据

      image-20220216232429393
    5. 因为根据数据库索引直接查找到了对应的data page,所以数据库索引会提高查询的速度

  3. 内存数据库

    1. 数据在磁盘和内存的体积不同,相同的数据在内存中的占用往往更小

    2. 因为内存不存在带宽、寻址速度的限制,所以出现了内存数据库

    3. 数据全部存储在内存中,造价昂贵

  4. 内存缓存

    1. 因为内存数据库造价过于昂贵,所以出现了折中方案

    2. 将一部分频繁读取的数据存储到内存中,用作缓存,减轻磁盘数据库的压力

    3. 比如 memcached+oracleredis + mysql

Redis介绍

  • https://redis.io/

  • http://redis.cn/

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。

Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

历史

Redis作者是 Salvatore Sanfilippo,来自意大利的西西里岛。👇

图片

2008年他在开发一个LLOOGG的网站时,需要一个高性能的队列功能,最开始使用MySQL来实现,无奈性能总是提不上去,所以决定自己实现一个专属于LLOOGG的数据库,他就是Redis的前身。后台 Sanfilippoj将 Redis1.0放到Github上大受欢迎。

BSD协议

Redis基于BSD开源协议, BSD开源协议 是一个给于使用者很大自由的协议。可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。当你发布使用了BSD协议的代码,或者以BSD协议代码为基础做二次开发自己的产品时,需要满足三个条件:

  • 如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。

  • 如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。

  • 不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。

BSD代码鼓励代码共享,但需要尊重代码作者的著作权。BSD由于 允许使用者修改重新发布代码,也 允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。

很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者 二次开发。

Redis vs Memcache

世界上的数据只有三种表现形式:

  1. k=v

  2. k=[v1, v2,...]

  3. k={x=y, a=[1, 2], b={a,1}}

所以任何数据都可以以json的形式发送或者记录。Memchache是一个 没有数据类型 的缓存系统。它想要表示数据,通常需要客户端自己将数据转换为json。当我需要修改json的某个字段时,需要获取整个字段值,修改完毕后生成新的json,再发送给memcache服务端:

而拥有 数据类型的redis 可以通过数据类型提供的方法,直接修改某部分信息(向list k1头部插入数据):

Redis的优势计算向数据移动

  • 客户端代码更简洁,不需要做数据序列化反序列化处理

  • 网络传输数据较少,客户端和服务端只通讯修改的地方

Redis架构图

Redis是单进程、单线程、单实例处理用户请求的架构,但是依然可以处理高并发下的请求,并且保证单个客户端发送的命令式按照顺序执行的:

image-20220216235124189

redis-cli连接redis

redis-cli 是一个redis的命令行客户端工具,默认集成在redis中,可以使用 redis-cli --help 查看使用帮助,默认链接本地的6379的redis-server服务。

  • 使用交互方式连接redis: redis-cli -h {host} -p {port}

  • 使用命令行方式连接redis并执行命令: redis-cli -h 127.0.0.1 -p 6379 get hello

可以使用 redis-cli --help 来查看帮助。

Redis的DB

Redis默认提供了16个DB库,从 0 ~ 15,默认为0号库。

接连接进入默认0号库:

连接进入指定的9号库:

在内部切换库12:

help 命令

在redis客户端中,使用help命令,可以查看redis的各种命令帮助:

目前在redis6中的group包含:

示例,查看基础操作:

示例,查看字符串的操作:

示例,查看DEL命令的具体用法:

string

字符串类型,实际存储字节数据,其他数据结构都是基于字符串类型的基础上构建的,其值最大不可以超过512M

针对string,可以将命令分为三种:

  1. 面向字符串的操作

  2. 面向数值的操作

  3. 面向位图的操作

面向字符串的操作

面向数值的操作

面向位图的操作

getset

注意,此命令不能保证原子性,因为是先读后写。此方法仅仅是为了减少一次IO操作(与先GET 后 SET 比较):

mset/mget 批量操作

m代表more, 一次设置多个值,一次获取多个值,原子操作

nx/xx

  • nx 表示Not Exist,如果key已经存在,则不会设置

    • 通常用于分布式锁的场景,只允许一个第一个现成取到锁对象

    • set k1 hello nx

    • setnx k1 hello

    • msetnx k1 hello

  • xx 表示 只能更新,如果key不存在,则不会设置

    • set k2 hello xx

list

压入和弹出

可以实现堆栈、队列

左操作
右操作

压入

LPUSH

RPUSH

弹出并获取

LPOP

RPOP

查询List长度:

获取当前list的长度:

索引操作

可以实现数组

阻塞的操作

主要用于实现同步单播阻塞队列,即如果不消费客户端将会持续阻塞,且每个元素只能消费一次。

左操作
右操作

压入

LPUSH

RPUSH

阻塞弹出并获取

BLPOP

BRPOP

客户端1 使用BROP阻塞读取一个元素:

客户端2 使用RPUSH向队列中推入一个元素:

客户端发现有数据,停止阻塞:

hash

一种键值对的value类型,通常用来存储对象。

存储/操作一个对象数据

定义zhangsan这个对象的信息:

查看zhangsan的信息:

可针对数值进行计算

set

set也用于存储元素,相比于list:

  1. list按照插入顺序进行排序,set则是无序的(以内部一种有序的方式实现无序)

  2. list维护索引,set则不维护

  3. list中可以有重复元素,set中的元素都是唯一的

set的命令都是以S开头的:

存储/删除/查询操作

交集/并集/差集操作

随机事件

sorted_set

是一个有序的set,不同于基于放入顺序的有序的list,而是基于元素的一些属性进行排序。

所有的sorted_set操作都已z开头,故sorted_set又称作zset。

存储元素时要指定socre,用于排序

在添加元素时,需要指定元素的score,按照score值左小右大的方式,对元素进行排序:

取出元素

修改元素分值

集合操作

注意权重

type与encoding

Redis的每个value都有其对应的类型,可以使用如下方式查看value的数据类型type:

类型与操作方法时对应的,指定的类型只能进行指定的方法操作。

此外,每种type内部都有自己多种编码实现,比如string类型内部就有三种编码实现:

  • raw

  • int

  • embstr

二进制安全

在redis中,所有数据都是二进制安全的。redis只会保存原始的二进制流数据,不会对他们做处理。即使value的内部编码不同,也不会影响数据数据在redis内部的存储方式,所谓的内部编码只不过在对数据处理时,将其转换为其他编码以方便处理。

所以,redis存储的数据的格式、文本编码等信息,需要客户端自己在存入和读取时保持一致。

为什么需要encoding

  1. 这样可以自由改进每种类型的内部编码,从而不影响外部数据结构以及命令

  2. 多种内部编码可以在不同的场景下发挥各自的优势,比如string的int编码就可以很方便的实现INCR命令

查看内部编码

string

  • int:8字节长整型

  • embstr:小于等于39字节的字符串

  • raw:大于39字节的字符串

Pipelining 管道

redis的通信是建立在tcp基础上的,也就是说每一次命令(get、set)都需要经过tcp三次握手,而且redis一般都是部署在局域网内,网络开销非常小,针对频次较低的操作,网络开销都是可以忽略的。

每一次操作redis的时候我们都需要和服务端建立连接,针对量小的情况下网络延迟都是可以忽略的,但是针对大批量的业务,就会产生雪崩效应。假如一次操作耗时2ms,理论上100万次操作就会有2ms*100万ms延迟,中间加上服务器处理开销,耗时可能更多.对应客户端来讲,这种长时间的耗时是不能接受的。所以为了解决这个问题,redis的管道pipeline就派上用场了。

下面是一个Redis管道的示例:

使用管道建立了一次TCP连接,一次连接发送了3个PING命令,并一次全部响应。

Pub/Sub 发布订阅

服务端先发布,客户端后订阅

发布方先发布:

订阅方后订阅:

结果:客户端收不到订阅前的消息

客户端先订阅,服务端后发布

订阅方订阅:

发布方发布:

订阅方:

结果:可正常接受实时消息

怎么看订阅之前的消息

  1. pub/sub只能保证实时消息下发到订阅端

  2. 怎么保证可查看历史性消息?

    1. 历史性消息需要持久化,可以将消息存储到磁盘数据库中

    2. 历史消息根据查询的压力可以分为两个档次

      1. 比如三天内的消息,查询压力可能较高

      2. 三天前、几周前、或者更早的消息,基本没有什么压力

    3. 根据查询压力,可以将最近三天的消息窗口缓存到sorted_set中,减轻缓存压力

    4. 将更早的消息直接放入数据库中

事务

  1. 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。

  2. Redis是单进程的,事务中的命令无论什么情况都会连续按照顺序执行,不会被其他客户端发送来的命令请求所打断。

  3. 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

  4. 事务下包含如下几个命令:

事务示例

watch示例

在上一客户端执行watch后,另一个客户端修改了k1,造成了k1的值发生变化:

执行失败事务不会回滚,而是会继续执行

  1. Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。

  2. 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

作为缓存应用

redis作为数据库与缓存的区别

  1. 缓存不存放全量数据,只存储热点数据

  2. 业务逻辑会决定缓存存在的有效期

  3. 内存有限,随着时间变化,冷数据应该被淘汰

配置key的缓存时间

Redis作为缓存系统,其最重要的特点之一就是其缓存的失效系统,redis的key可以通过如下几条命令设置有效期:

当一些客户端尝试访问它时,key会被发现并主动的过期。

当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。

  1. 主动:

  2. 被动:

配置冷数据淘汰策略

可修改redis的配置文件,配置如下项目:

共有以下几种值:

  1. noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)

    数据不可丢时,将redis作为数据库时使用次策略

  2. allkeys-lru(常用):尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。

    无论该数据有效期是否过期,都会去回收

  3. volatile-lru(常用):尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。

    只回收设置了有效期的key

  4. allkeys-random:回收随机的键使得新添加的数据有空间存放。

  5. volatile-random:回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。

  6. volatile-ttl:回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

LRU是一种缓存淘汰算法,全拼为Least recently used,最近最少使用算法

LFU也是一种缓存淘汰算法,全拼为Least frequently used,最不常用算法,在一段时间内很久没有发生操作

TTL是 Time To Live的缩写,代表生存时间值

如何淘汰过期的key?

Redis keys过期有两种方式:

  1. 被动方式:客户端访问key时,被动的发现key过期,并处理

  2. 主动方式:周期轮训判定,每十秒运行一次

    1. 测试随机的20个keys进行相关过期检测。

    2. 删除所有已经过期的keys。

    3. 如果有多于25%的keys过期,重复步奏1.

应用场景

实现分布式锁

使用setnx

统计指定时间段内的用户登录次数

  • 使用string 位图实现

  • key为用户唯一标志

  • 使用365位存储365天内的登录状态

  • 使用bitcount统计用户在某一随机时间段内的登录次数

  • 使用bitlen统计总共登录的次数

  • 运算速度快、节省空间

统计最近的活跃用户(排除僵尸用户)

  • 使用string位图实现

  • key为日期

  • value的每一位代表用户的登录状态,1代表已经登录

  • 将要统计的日期的所有位图相或,可以得出这几个日期中所有至少登录一次的用户结果

  • 将要统计的日期的所有位图相与,可以得出这几个日期中每天都登陆的用户结果

统计点赞、收藏、访问量

  • 使用Hash实现

  • key为要统计的目标页面、目标文章等

  • value则记录该篇文章的浏览量、点赞量等

抽奖:选取幸运粉丝,赠送奖品

给我的微博的所有粉丝中抽取几名幸运粉丝赠送奖品。

  1. 将所有的粉丝增加到set中

  2. 使用SRANDMEMBER fans 奖品数量,假设奖品数量为3

  3. 如果一个粉丝只能获得一次奖励:SRANDMEMBER fans 3

  4. 如果一个粉丝可以重复获得奖励:SRANDMEMBER fans -3

抽奖:年会抽奖

  1. 奖项数 < 参与抽奖的人数

  2. 如果某个奖项已经被抽取了,那么这个奖项则不在抽奖列表内

将所有的奖品添加到set中,使用SPOP指令,每次弹出一个随机元素

员工是否可以重复抽奖,是否可以重复中奖,可以由应用程序控制。

抽奖:奖品多,人少

  1. 将所有的抽奖人员放入到set中

  2. 抽取10个奖品的归属人:SRANDMEMBER fans -10

直播间消息推送

使用pub/sub,参考PubSub章节。

实现CAS锁

使用事物的watch机制,配合代码循环设置分布式CAS锁。

常见缓存问题解决方案

缓存穿透

用户/黑客,频繁查询一个不存在的商品,导致每次缓存都不命中,导致每次查询都到达数据库层,然后拖垮数据库。

解决方案:记录所有的已有商品,拒绝无效查询

将所有的可命中的商品查询存储到缓存中,如果用户输入的条目无法命中,则不走数据库。

但是因为可查询的目标条目较多,内存缓存无法全部存储,这时候就需要布隆过滤器。

解决方案:布隆过滤器

image-20220217211425853
  1. 将已有的商品标记到bitmap中 => 1

  2. 没有标记的商品在bitmap中没有标记 => 0

  3. 因为有可能发生hash冲突,请求的商品可能被误标记

  4. 但是,一定概率会大量减少

  5. 所以布隆过滤器又被称为概率数据结构

布隆过滤器可以放在:

  1. client端实现bloom算法,自己承载bitmap

  2. client端实现bloom算法,bitmap交给redis

  3. client端什么都不做,将bloom算法与bitmap统一交给redis处理,涉及redis模块:bloom

docker下启动含有布隆过滤器模块的Redis:

使用redis bloom:

解决方案:counting bloom 布隆过滤器升级

解决方案:布谷鸟过滤器

同布隆过滤器类似。

缓存击穿

同一时间内同时高并发请求一个缓存过期的数据,导致大量请求同一时间到达数据库。这成为缓存击穿。

解决方案:使用缓存锁

  1. 当目标缓存没有命中时,先使用setnx获取一个分布式锁,然后再去数据库中查询

  2. 当同一时刻的请求也命中了该缓存,也会去尝试获得对应的分布式锁,此时他会获取失败

  3. 这样就避免了大量的请求同时到达数据中

  4. 当请求未获得缓存锁时,可以每隔一段时间(比如1s)重新获得锁,直到获取成功,但是有如下几个问题:

    1. 问题1:第一个持有锁的人挂掉,锁没有被释放

      解决: 设置锁过期时间

    2. 问题2:数据库访问时间较长或者锁时间设置过短,导致数据库执行了但是缓存设置没有成功,所有请求都失败

      解决:使用两个线程,A线程从DB中取数据,B线程监控数据是否取出,然后重新设置锁的时间

缓存雪崩

大量的key同时失效,间接造成大量的访问到达数据库。

解决方案:时点性无关-随机过期时间

没有可以要求数据的缓存时间,可以对key的缓存时间进行随机。

解决方案:时点性有关-使用击穿方案

一些系统缓存一定要在指定的时间失效并且更新,比如银行的信贷信息一定要凌晨12点更新,否则就是过期数据。使用击穿方案解决。

Redis的持久化

持久化一半都分为两种:

  • 定时全量备份

    • 优点:恢复速度快

    • 缺点:会导致某部分时间的数据丢失

  • 实时日志

    • 优点:不会丢失数据,数据会被实时备份

    • 缺点:恢复速度较慢

RDB(快照)

快照、副本这种持久化是有时点性的:缓存备份的过程是需要一段时间的,磁盘中的文件在这段时间会有变化。所以就会导致副本前部分备份的数据是20分钟前的数据,而后部分是最近时间的数据,数据时点混乱。

Redis提供了两种解决方案:

  1. SAVE:阻塞redis,暂时不提供服务,直到备份完毕才恢复(不推荐)

  2. BGSAVE:创建子进程用于备份redis数据,根据linux父子进程特性,进程间的的数据时隔离的,子进程被创建后会拷贝所有父进程数据到子进程。这样就避免了数据时点混乱的问题。

    因为将数据完全拷贝,会占用双倍的内存空间,并且要花费很长时间拷贝,所以Redis采用系统调用fork

    1. fork会创建一个子进程

    2. 创建子进程时,不会立即拷贝父进程的数据,而是采用copy on write的方式拷贝,也就是说如果用到了这个数据才会进行拷贝

    3. 他不会真正将所有数据拷贝一份,只是使用虚拟地址去引用真实的物理内存地址

主动触发RDB

主动触发通常用语关机维护前的备份

使用配置文件自动配置

实际是bgsave的备份方式:

恢复备份

将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可。

AOF(日志)

Append Only File,他会将redis的写操作记录到文件中。每次写操作都会触发。

RDB 和 AOF同时开启

RDB和AOF可以同时开启,但是只会使用AOF。

怎么保证AOF日志最小

时间越久,AOF日志就会越大,所以Redis在不同的版本提供了不同的日志重写功能:

  1. 在Redis4.0前:删除抵消的命令日志、合并重复的命令日志,最终会获得一个纯指令的日志文件

  2. 在Redis4.0后:先将老数据RDB到AOF文件中,将增量的指令append到AOF文件中(更快更轻便)

配置AOF

开启aof

分布式的Redis

主从复制

Redis的主从复制是标准的主从复制方式,一个主节点对应多个从节点,从节点的数据完全备份自主节点,主节点负责写入数据,而从节点只提供读服务,负责减轻读取的压力。

主从复制不提供高可用的方案,高可用方案由哨兵提供

配置主从复制

修改三台机器的配置文件中的如下配置选项:

另外在redis服务器上配置如下选项,表示跟随的主节点:

这里可以使用命令行配置,但是配置之后重启会失效

启动3台redis服务,可以使用info replication查看主从复制的信息:

验证主从复制同步

从服务器不能更新值,只能读取

实现原理

  1. 建立连接

    1. slaveof命令执行之后,从服务器根据设置的master的ip地址和端口,创建连向主服务器的socket套接字连接,连接成功后,从服务器会为这个套接字关联一个专门的处理器,用于处理后续的复制工作

    2. 建立连接之后,从服务器会向主服务器发送ping命令,确认主服务器是否可用,以及当前是否可用接受处理命令。如果收到主服务器的pong回复说明是可用的,否则有可能是网络超时或主服务器阻塞,从服务器会断开连接发起重连

    3. 身份验证。如果主服务器设置了requirepass选项,那么从服务器必须配置masterauth选项,且保证密码一致才能通过验证

    4. 身份验证完成之后,从服务器会发送自己的监听端口,主服务器会保存下来

    5. 建立连接后可以通过如下命令查看redis集群的状态

  2. 数据同步

    1. 在主从服务器建立连接确认各自身份之后,就开始数据同步,从服务器向主服务器发送PSYNC命令,执行同步操作,并把自己的数据库状态更新至主服务器的数据库状态

    2. 当slave第一此链接到master上时,会触发完全重同步(runid代表master的节点id;offset代表将发送快照数据的偏移量,记录这个偏移量是因为执行完全重同步后会根据偏移量发送剩余的redis命令)

      img
    3. 当slaver与master断线后重新连接,将会执行部分重同步,重新同步失联情况缺失的数据(runid代表master的节点id,不是同一个id不能重新连接;offset代表从服务器上次同步的数据位置;复制积压缓冲区是一个默认1MB大小的FIFO队列,主要用于备份主库发送给从库的数据,然后异步发送给从库,在主库中只有一个复制积压缓冲器,所有从库共享他们;)

      img
  3. 命令传播:

    1. 当完成数据同步之后,主从服务器的数据暂时达到一致状态,当主服务器执行了客户端的写命令之后,主从的数据便不再一致

    2. 此时为了能够使主从服务器的数据保持一致性,主服务器会对从服务器执行命令传播操作,即每执行一个写命令就会向从服务器发送同样的写命令

    3. 在命令传播阶段,从服务器会默认以每秒一次的频率向主服务器发送心跳检测 :

      1. 从服务器发送命令REPLCONF ACK <replication_offset> 进行检测

      2. 检测主从服务器的网络连接状态

      3. 传入replication_offset,防止部分数据丢失

Redis的主从复制并不能保证一致性

  1. 假如master新写入一条数据,此时可能还未出发命令传播,所以说Redis无法保证实时一致性

  2. 假如master新写入一个数据,但是命令传播还未发出,这时master已经挂了,那么该条数据永远不会同步给slaver,所以说Redis主从复制连最终一致性都无法完全保证

  3. 完全重同步采用的是异步队列的方式,如果积压缓冲区中的数据还未发送给slaver,此时master就挂掉了,那么这里的数据就可能发生丢失

主从复制方案:哨兵

主从复制默认不支持高可用,redis提供了sentinel哨兵

  • 监控,监控主节点的健康情况

  • 提醒,当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知

  • 自动故障转移,哨兵通过选举重新设置一个主节点

配置哨兵

配置三个哨兵的配置文件,监控主节点 127.0.0.1:6379

启动这三个哨兵:

或者使用:

特点

  1. 哨兵是单独的进程,不同于server

  2. 哨兵只连接master

  3. 哨兵可以根据master获取master的所有slave信息

  4. 哨兵可以根据master发现其他的哨兵(通过master发布订阅的方式)

哨兵是怎么选举新的master上的

Redis中哨兵选举算法 锦鱼不忘旧时晨的博客-CSDN博客 redis哨兵选举算法

容量限制的问题

无论是主从复制还是哨兵,都是全量备份的,无法解决容量限制的问题。解决容量限制有如下几个方式:

按照业务划分

在客户端按照业务划分,将数据存储到不同的redis中。比如将redis拆分为 user、order等多个redis,每个业务使用对应的redis。

image-20220219174435108

使用随机算法划分

存储kv到某个随机redis中,他的缺点是客户端无法确定key在哪个redis中。通常用于消息队列的场景:

  1. 提供方使用lpush向随机redis添加kv

  2. 消费方连接所有redis,使用rpop消费list,不管队列是哪个redis

image-20220219174235284

使用hash+取模的方式划分

在客户端按照算法拆分,根据hash+取模的方式将数据分在不同的redis中,也就是基于取模的数据分片:

image-20220219174329787

缺点是模的值必须是固定的,如果发生了变化,数据落地的目标redis就有可能发生变化,造成数据混乱。

使用一致性hash算法划分

image-20220219174900099

Hash环:

img

Hash环有2次Hash:

  1. 把所有机器编号hash到这个环上

  2. 把key也hash到这个环上。然后在这个环上进行匹配,看这个key和哪台机器匹配。

首先计算出每台Cache服务器在环上的位置(图中的大圆圈);然后每来一个(key, value),计算出在环上的位置(图中的小圆圈),然后顺时针走,遇到的第1个机器,就是其要存储的机器。

这里的关键点是:当你增加/减少机器时,其他机器在环上的位置并不会发生改变。这样只有增加的那台机器、或者减少的那台机器附近的数据会失效,其他机器上的数据都还是有效的。

哈希环数据倾斜的问题

当你机器不多的时候,很可能出现几台机器在环上面贴的很近,不是在环上均匀分布。这将会导致大部分数据,都会集中在某1台机器上。

为了解决这个问题,可以引入“虚拟机器”的概念,也就是说:1台机器,我在环上面计算出多个位置。怎么弄呢? 假设用机器的ip来hash,我可以在ip后面加上几个编号, ip_1, ip_2, ip_3, … 把1台物理机器生个多个虚拟机器的编号。

数据首先映射到“虚拟机器上”,再从“虚拟机器”映射到物理机器上。因为虚拟机器可以很多,在环上面均匀分布,从而保证数据均匀分布到物理机器上面。

使用预分区(哈希槽)划分

前面说的方法都有一些问题,比如hash环会丢失一部分数据,所以使用预分区划分。

将所有的hash定义为槽位,假设定义10个槽位,分配各两台redis:

image-20220219184224945

现在加入第三台redis3,redis1和redis2会均分给redis3一部分槽位:

image-20220219184733713

需要部分迁移,但是相比全部迁移与丢失数据相比,已经很完善了。

连接成本高的问题

如果按照客户端拆分的方式,redis客户端去连接每个redis服务端,那么就会有如下的拓扑结构,将会增加redis的连接成本。

image-20220219180825239

设置负载均衡与代理

image-20220219181239500

代理压力大可以增加LVS

LVS是四层处理,不会握手,压力更小

image-20220219182441213

第三方redis代理工具

image-20220219192327837

分区方案:Redis Cluster

Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令. Redis 集群的优势:

  • 自动分割数据到不同的节点上。

  • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

解决容量限制的问题

采用预分区,默认提供了16384个hash槽。

解决连接成本较高的问题

image-20220219190739435

客户端在连接redis集群时,只需要连接集群内的任意一个节点。当发生操作时,连接的目标节点会自动计算目标key的所属节点位置,并重定向到目标节点。

这样就解决了连接成本高的问题。

故障转移

Redis集群的主节点内置了类似Redis Sentinel的节点故障检测和自动故障转移功能,当集群中的某个主节点下线时,集群中的其他在线主节点会注意到这一点,并对已下线的主节点进行故障转移。

img

集群进行故障转移的方法和Redis Sentinel进行故障转移的方法基本一样,不同的是,在集群里面,故障转移是由集群中其他在线的主节点负责进行的,所以集群不必另外使用Redis Sentinel。

缺点:不支持跨节点聚合、事务操作

Redis集群会直接拒绝不同节点的聚合、事务操作,因为会发生数据复制。

解决办法:可以通过hash tag,让需要进行聚合操作或者事务的key放在同一个集群节点上即可完成。

创建redis-cluster

最小集群大小为6个节点,其中有3个主节点,3个从节点:

img

三个从节点主要是对主节点的备份与对读操作的扩展,三个主节点主要是对数据进行分区。

最后更新于

这有帮助吗?