写点什么

分布式存储中间件(1):10000 字把 Redis 扒个干净,一发入魂

用户头像
北游学Java
关注
发布于: 2021 年 04 月 27 日

前言

关于分布式系列专题,整体是打算写分布式消息中间件、分布式储存中间件和分布式框架的。分布式消息中间件选择了两个最常用的,之前已经写了,感兴趣的朋友可以看看。



这篇文章是写分布式存储中间件种的 Redis,关于 Redis 想必没有任何一个程序员会感到陌生吧,其实网上写 Redis 的文章已经很多了,甚至可以说是泛滥,但总感觉太碎片化了,所以还是想系统的写一下 Redis,当然了,篇幅所限,这篇文章肯定也是写不尽 Redis 的,但是我会尽力把重要的东西都拎出来系统的讲一下,让不了解的朋友看完后对 Redis 能够简单上手敲代码,让 Redis 有一些了解的朋友查漏补缺。


我整理的一些相关学习资料可以直接点击领取


一、Redis 数据结构

Redis 五种数据结构如下:


对 redis 来说,所有的 key(键)都是字符串。

1.String 字符串类型

是 redis 中最基本的数据类型,一个 key 对应一个 value。


String 类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg 图片或者序列化的对象。


使用:get 、 set 、 del 、 incr、 decr 等


127.0.0.1:6379> set hello worldOK127.0.0.1:6379> get hello"world"127.0.0.1:6379> del hello(integer) 1127.0.0.1:6379> get hello(nil)127.0.0.1:6379> get counter"2"127.0.0.1:6379> incr counter(integer) 3127.0.0.1:6379> get counter"3"127.0.0.1:6379> incrby counter 100(integer) 103127.0.0.1:6379> get counter"103"127.0.0.1:6379> decr counter(integer) 102127.0.0.1:6379> get counter"102"
复制代码


实战场景:


1.缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到 redis 中,redis 作为缓存层,mysql 做持久化层,降低 mysql 的读写压力。


2.计数器:redis 是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。


3.session:常见方案 spring session + redis 实现 session 共享,

2.Hash (哈希)

是一个 Mapmap,指值本身又是一种键值对结构,如 value={{field1,value1},......fieldN,valueN}}



使用:所有 hash 的命令都是  h   开头的     hget  、hset 、  hdel 等


127.0.0.1:6379> hset user name1 hao(integer) 1127.0.0.1:6379> hset user email1 hao@163.com(integer) 1127.0.0.1:6379> hgetall user1) "name1"2) "hao"3) "email1"4) "hao@163.com"127.0.0.1:6379> hget user user(nil)127.0.0.1:6379> hget user name1"hao"127.0.0.1:6379> hset user name2 xiaohao(integer) 1127.0.0.1:6379> hset user email2 xiaohao@163.com(integer) 1127.0.0.1:6379> hgetall user1) "name1"2) "hao"3) "email1"4) "hao@163.com"5) "name2"6) "xiaohao"7) "email2"8) "xiaohao@163.com"
复制代码


实战场景:


1.缓存: 能直观,相比 string 更节省空间,的维护缓存信息,如用户信息,视频信息等。

3.链表

List 说白了就是链表(redis 使用双端链表实现的 List),是有序的,value 可以重复,可以通过下标取出对应的 value 值,左右两边都能进行插入和删除数据。



使用列表的技巧


  • lpush+lpop=Stack(栈)

  • lpush+rpop=Queue(队列)

  • lpush+ltrim=Capped Collection(有限集合)

  • lpush+brpop=Message Queue(消息队列)


使用:


127.0.0.1:6379> lpush mylist 1 2 ll ls mem(integer) 5127.0.0.1:6379> lrange mylist 0 -11) "mem"2) "ls"3) "ll"4) "2"5) "1"127.0.0.1:6379>
复制代码


实战场景:


1.timeline:例如微博的时间轴,有人发布微博,用 lpush 加入时间轴,展示新的列表信息。

4.Set   集合

集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中  1. 不允许有重复的元素,2.集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集。



使用:命令都是以 s 开头的  sset 、srem、scard、smembers、sismember


127.0.0.1:6379> sadd myset hao hao1 xiaohao hao(integer) 3127.0.0.1:6379> SMEMBERS myset1) "xiaohao"2) "hao1"3) "hao"127.0.0.1:6379> SISMEMBER myset hao(integer) 1
复制代码


实战场景;


1.标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。


2.点赞,或点踩,收藏等,可以放到 set 中实现

5.zset  有序集合

有序集合和集合有着必然的联系,保留了集合不能有重复成员的特性,区别是,有序集合中的元素是可以排序的,它给每个元素设置一个分数,作为排序的依据。


(有序集合中的元素不可以重复,但是 score 分数 可以重复,就和一个班里的同学学号不能重复,但考试成绩可以相同)。



使用: 有序集合的命令都是 以  z  开头    zadd 、 zrange、 zscore


127.0.0.1:6379> zadd myscoreset 100 hao 90 xiaohao(integer) 2127.0.0.1:6379> ZRANGE myscoreset 0 -11) "xiaohao"2) "hao"127.0.0.1:6379> ZSCORE myscoreset hao"100"
复制代码


实战场景:


1.排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。


二、Redis 持久化机制

Redis 有两种持久化方案,RDB (Redis DataBase)和 AOF (Append Only File)。如果你想快速了解和使用 RDB 和 AOF,可以直接跳到文章底部看总结。本章节通过配置文件,触发快照的方式,恢复数据的操作,命令操作演示,优缺点来学习 Redis 的重点知识持久化。

1、RDB 详解

RDB 是 Redis 默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中。即在指定目录下生成一个 dump.rdb 文件。Redis 重启会通过加载 dump.rdb 文件恢复数据。


从配置文件了解 RDB 打开 redis.conf 文件,找到 SNAPSHOTTING 对应内容

1.1 RDB 核心规则配置(重点)

save <seconds> <changes># save ""save 900 1save 300 10save 60 10000
复制代码


解说:save <指定时间间隔> <执行指定次数更新操作>,满足条件就将内存中的数据同步到硬盘中。官方出厂配置默认是 900 秒内有 1 个更改,300 秒内有 10 个更改以及 60 秒内有 10000 个更改,则将内存中的数据快照写入磁盘。若不想用 RDB 方案,可以把 save "" 的注释打开,下面三个注释。

1.2 指定本地数据库文件名,一般采用默认的 dump.rdb

dbfilename dump.rdb
复制代码

1.3 指定本地数据库存放目录,一般也用默认配置

dir ./
复制代码

1.4 默认开启数据压缩

rdbcompression yes
复制代码


解说:配置存储至本地数据库时是否压缩数据,默认为 yes。Redis 采用 LZF 压缩方式,但占用了一点 CPU 的时间。若关闭该选项,但会导致数据库文件变的巨大。建议开启。

2、触发 RDB 快照

2.1 在指定的时间间隔内,执行指定次数的写操作 2.2 执行 save(阻塞, 只管保存快照,其他的等待) 或者是 bgsave (异步)命令 2.3 执行 flushall 命令,清空数据库所有数据,意义不大。2.4 执行 shutdown 命令,保证服务器正常关闭且不丢失任何数据,意义...也不大。

3、通过 RDB 文件恢复数据

将 dump.rdb 文件拷贝到 redis 的安装目录的 bin 目录下,重启 redis 服务即可。在实际开发中,一般会考虑到物理机硬盘损坏情况,选择备份 dump.rdb 。可以从下面的操作演示中可以体会到。

4、RDB 的优缺点

优点:1 适合大规模的数据恢复。2 如果业务对数据完整性和一致性要求不高,RDB 是很好的选择。


缺点:1 数据的完整性和一致性不高,因为 RDB 可能在最后一次备份时宕机了。2 备份时占用内存,因为 Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。所以 Redis 的持久化和数据的恢复要选择在夜深人静的时候执行是比较合理的。


操作演示


[root@itdragon bin]# vim redis.confsave 900 1save 120 5save 60 10000[root@itdragon bin]# ./redis-server redis.conf[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379127.0.0.1:6379> keys *(empty list or set)127.0.0.1:6379> set key1 value1OK127.0.0.1:6379> set key2 value2OK127.0.0.1:6379> set key3 value3OK127.0.0.1:6379> set key4 value4OK127.0.0.1:6379> set key5 value5OK127.0.0.1:6379> set key6 value6OK127.0.0.1:6379> SHUTDOWNnot connected> QUIT[root@itdragon bin]# cp dump.rdb dump_bk.rdb[root@itdragon bin]# ./redis-server redis.conf[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379127.0.0.1:6379> FLUSHALL OK127.0.0.1:6379> keys *(empty list or set)127.0.0.1:6379> SHUTDOWNnot connected> QUIT[root@itdragon bin]# cp dump_bk.rdb  dump.rdbcp: overwrite `dump.rdb'? y[root@itdragon bin]# ./redis-server redis.conf[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379127.0.0.1:6379> keys *1) "key5"2) "key1"3) "key3"4) "key4"5) "key6"6) "key2"
复制代码


第一步:vim 修改持久化配置时间,120 秒内修改 5 次则持久化一次。第二步:重启服务使配置生效。第三步:分别 set 5 个 key,过两分钟后,在 bin 的当前目录下会自动生产一个 dump.rdb 文件。(set key6 是为了验证 shutdown 有触发 RDB 快照的作用)第四步:将当前的 dump.rdb 备份一份(模拟线上工作)。第五步:执行 FLUSHALL 命令清空数据库数据(模拟数据丢失)。第六步:重启 Redis 服务,恢复数据.....咦????( ′◔ ‸◔`)。数据是空的????这是因为 FLUSHALL 也有触发 RDB 快照的功能。第七步:将备份的 dump_bk.rdb 替换 dump.rdb 然后重新 Redis。


PS:SHUTDOWN 和 FLUSHALL 命令都会触发 RDB 快照,这是一个坑,请大家注意。


其他命令


  • keys * 匹配数据库中所有 key

  • save 阻塞触发 RDB 快照,使其备份数据

  • FLUSHALL 清空整个 Redis 服务器的数据(几乎不用)

  • SHUTDOWN 关机走人(很少用)

2、AOF 详解

AOF :Redis 默认不开启。它的出现是为了弥补 RDB 的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

1、从配置文件了解 AOF

打开 redis.conf 文件,找到 APPEND ONLY MODE 对应内容 1.1 redis 默认关闭,开启需要手动把 no 改为 yes


appendonly yes
复制代码


1.2 指定本地数据库文件名,默认值为 appendonly.aof


appendfilename "appendonly.aof"
复制代码


1.3 指定更新日志条件


# appendfsync alwaysappendfsync everysec# appendfsync no
复制代码


解说:always:同步持久化,每次发生数据变化会立刻写入到磁盘中。性能较差当数据完整性比较好(慢,安全)everysec:出厂默认推荐,每秒异步记录一次(默认值)no:不同步


1.4 配置重写触发机制


auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb
复制代码


解说:当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发。一般都设置为 3G,64M 太小了。

2、触发 AOF 快照

根据配置文件触发,可以是每次执行触发,可以是每秒触发,可以不同步。

3、根据 AOF 文件恢复数据

正常情况下,将 appendonly.aof 文件拷贝到 redis 的安装目录的 bin 目录下,重启 redis 服务即可。但在实际开发中,可能因为某些原因导致 appendonly.aof 文件格式异常,从而导致数据还原失败,可以通过命令 redis-check-aof --fix appendonly.aof 进行修复 。从下面的操作演示中体会。

4、AOF 的重写机制

前面也说到了,AOF 的工作原理是将写操作追加到文件中,文件的冗余内容会越来越多。所以聪明的 Redis 新增了重写机制。当 AOF 文件的大小超过所设定的阈值时,Redis 就会对 AOF 文件的内容压缩。


重写的原理:Redis 会 fork 出一条新进程,读取内存中的数据,并重新写到一个临时文件中。并没有读取旧文件(你都那么大了,我还去读你??? o(゚Д゚)っ傻啊!)。最后替换旧的 aof 文件。


触发机制:当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发。这里的“一倍”和“64M” 可以通过配置文件修改。

5、AOF 的优缺点

优点:数据的完整性和一致性更高缺点:因为 AOF 记录的内容多,文件会越来越大,数据恢复也会越来越慢。


操作演示


[root@itdragon bin]# vim appendonly.aofappendonly yes[root@itdragon bin]# ./redis-server redis.conf[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379127.0.0.1:6379> keys *(empty list or set)127.0.0.1:6379> set keyAOf valueAofOK127.0.0.1:6379> FLUSHALL OK127.0.0.1:6379> SHUTDOWNnot connected> QUIT[root@itdragon bin]# ./redis-server redis.conf[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379127.0.0.1:6379> keys *1) "keyAOf"127.0.0.1:6379> SHUTDOWNnot connected> QUIT[root@itdragon bin]# vim appendonly.aoffjewofjwojfoewifjowejfwf[root@itdragon bin]# ./redis-server redis.conf[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379Could not connect to Redis at 127.0.0.1:6379: Connection refusednot connected> QUIT[root@itdragon bin]# redis-check-aof --fix appendonly.aof 'x              3e: Expected prefix '*', got: 'AOF analyzed: size=92, ok_up_to=62, diff=30This will shrink the AOF from 92 bytes, with 30 bytes, to 62 bytesContinue? [y/N]: ySuccessfully truncated AOF[root@itdragon bin]# ./redis-server redis.conf[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379127.0.0.1:6379> keys *1) "keyAOf"
复制代码


第一步:修改配置文件,开启 AOF 持久化配置。第二步:重启 Redis 服务,并进入 Redis 自带的客户端中。第三步:保存值,然后模拟数据丢失,关闭 Redis 服务。第四步:重启服务,发现数据恢复了。(额外提一点:有教程显示 FLUSHALL 命令会被写入 AOF 文件中,导致数据恢复失败。我安装的是 redis-4.0.2 没有遇到这个问题)。第五步:修改 appendonly.aof,模拟文件异常情况。第六步:重启 Redis 服务失败。这同时也说明了,RDB 和 AOF 可以同时存在,且优先加载 AOF 文件。第七步:校验 appendonly.aof 文件。重启 Redis 服务后正常。


补充点:aof 的校验是通过 redis-check-aof 文件,那么 rdb 的校验是不是可以通过 redis-check-rdb 文件呢???

小结

  • Redis 默认开启 RDB 持久化方式,在指定的时间间隔内,执行指定次数的写操作,则将内存中的数据写入到磁盘中。

  • RDB 持久化适合大规模的数据恢复但它的数据一致性和完整性较差。

  • Redis 需要手动开启 AOF 持久化方式,默认是每秒将写操作日志追加到 AOF 文件中。

  • AOF 的数据完整性比 RDB 高,但记录内容多了,会影响数据恢复的效率。

  • Redis 针对 AOF 文件大的问题,提供重写的瘦身机制。

  • 若只打算用 Redis 做缓存,可以关闭持久化。

  • 若打算使用 Redis 的持久化。建议 RDB 和 AOF 都开启。其实 RDB 更适合做数据的备份,留一后手。AOF 出问题了,还有 RDB。

  • Redis高频面试题33道

三、Redis 的四种模式

1、单机模式

这个最简单,一看就懂。


就是安装一个 redis,启动起来,业务调用即可。具体安装步骤和启动步骤就不赘述了,网上随便搜一下就有了。


单机在很多场景也是有使用的,例如在一个并非必须保证高可用的情况下。


咳咳咳,其实我们的服务使用的就是 redis 单机模式,所以来了就让我改为哨兵模式。


说说单机的优缺点吧。


优点:


  • 部署简单,0 成本。

  • 成本低,没有备用节点,不需要其他的开支。

  • 高性能,单机不需要同步数据,数据天然一致性。


缺点:


  • 可靠性保证不是很好,单节点有宕机的风险。

  • 单机高性能受限于 CPU 的处理能力,redis 是单线程的。


单机模式选择需要根据自己的业务场景去选择,如果需要很高的性能、可靠性,单机就不太合适了。

2、主从复制

主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。


前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。



主从模式配置很简单,只需要在从节点配置主节点的 ip 和端口号即可。


slaveof <masterip> <masterport># 例如# slaveof 192.168.1.214 6379
复制代码


启动主从节点的所有服务,查看日志即可以看到主从节点之间的服务连接。


从上面很容易就想到一个问题,既然主从复制,意味着 master 和 slave 的数据都是一样的,有数据冗余问题。


在程序设计上,为了高可用性和高性能,是允许有冗余存在的。这点希望大家在设计系统的时候要考虑进去,不用为公司节省这一点资源。


对于追求极用户体验的产品,是绝对不允许有宕机存在的。


主从模式在很多系统设计时都会考虑,一个 master 挂在多个 slave 节点,当 master 服务宕机,会选举产生一个新的 master 节点,从而保证服务的高可用性。


主从模式的优点:


  • 一旦 主节点宕机,从节点作为主节点的备份可以随时顶上来。

  • 扩展主节点的读能力,分担主节点读压力。

  • 高可用基石:除了上述作用以外,主从复制还是哨兵模式和集群模式能够实施的基础,因此说主从复制是 Redis 高可用的基石。


也有相应的缺点,比如我刚提到的数据冗余问题:


  • 一旦主节点宕机,从节点晋升成主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。

  • 主节点的 写能力受到单机的限制。

  • 主节点的存储能力受到单机的限制。

3、哨兵模式

刚刚提到了,主从模式,当主节点宕机之后,从节点是可以作为主节点顶上来,继续提供服务的。


但是有一个问题,主节点的 IP 已经变动了,此时应用服务还是拿着主节点的地址去访问,这...


于是,在 Redis 2.8 版本开始引入,就有了哨兵这个概念。


复制的基础上,哨兵实现了自动化的故障恢复。



如图,哨兵节点由两部分组成,哨兵节点和数据节点:


  • 哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 redis 节点,不存储数据。

  • 数据节点:主节点和从节点都是数据节点。


访问 redis 集群的数据都是通过哨兵集群的,哨兵监控整个 redis 集群。


一旦发现 redis 集群出现了问题,比如刚刚说的主节点挂了,从节点会顶上来。但是主节点地址变了,这时候应用服务无感知,也不用更改访问地址,因为哨兵才是和应用服务做交互的。


Sentinel 很好的解决了故障转移,在高可用方面又上升了一个台阶,当然 Sentinel 还有其他功能。


比如 主节点存活检测主从运行情况检测主从切换


Redis 的 Sentinel 最小配置是 一主一从

说下哨兵模式监控的原理

每个 Sentinel 以 每秒钟 一次的频率,向它所有的 主服务器从服务器 以及其他 Sentinel 实例 发送一个 PING 命令。



如果一个 实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 所指定的值,那么这个实例会被 Sentinel 标记为 主观下线


如果一个 主服务器 被标记为 主观下线,那么正在 监视 这个 主服务器 的所有 Sentinel 节点,要以 每秒一次 的频率确认 该主服务器是否的确进入了 主观下线 状态。


如果一个 主服务器 被标记为 主观下线,并且有 足够数量 的 Sentinel(至少要达到配置文件指定的数量)在指定的 时间范围 内同意这一判断,那么这个该主服务器被标记为 客观下线


在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率,向它已知的所有 主服务器 和 从服务器 发送 INFO 命令。


当一个 主服务器 被 Sentinel 标记为 客观下线 时,Sentinel 向 下线主服务器 的所有 从服务器 发送 INFO 命令的频率,会从 10 秒一次改为 每秒一次。


Sentinel 和其他 Sentinel 协商 主节点 的状态,如果 主节点处于 SDOWN`状态,则投票自动选出新的主节点。将剩余的 从节点 指向 新的主节点 进行 数据复制


当没有足够数量的 Sentinel 同意 主服务器 下线时, 主服务器 的 客观下线状态 就会被移除。当 主服务器 重新向 Sentinel 的 PING 命令返回 有效回复 时,主服务器 的 主观下线状态 就会被移除。

哨兵模式的优缺点

优点:


  • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。

  • 主从可以自动切换,系统更健壮,可用性更高。

  • Sentinel 会不断的检查 主服务器 和 从服务器 是否正常运行。当被监控的某个 Redis 服务器出现问题,Sentinel 通过 API 脚本向管理员或者其他的应用程序发送通知。


缺点:


  • Redis 较难支持在线扩容,对于集群,容量达到上限时在线扩容会变得很复杂。

我的任务

我部署的 redis 服务就如上图所示,三个哨兵节点,三个主从复制节点。


使用 java 的 jedis 去访问我的 redis 服务,下面来一段简单的演示代码(并非工程里面的代码):


public static void testSentinel() throws Exception {     //mastername从配置中获取或者环境变量,这里为了演示         String masterName = "master";         Set<String> sentinels = new HashSet<>();     // sentinel的IP一般会从配置文件获取或者环境变量,这里为了演示         sentinels.add("192.168.200,213:26379");         sentinels.add("192.168.200.214:26380");         sentinels.add("192.168.200.215:26381");      //初始化过程做了很多工作         JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);      //获取到redis的client         Jedis jedis = pool.getResource();     //写值到redis         jedis.set("key1", "value1");     //读取数据     jedis.get("key1");}
复制代码


具体部署的配置文件这里太长了,需要的朋友可以公众号后台回复【redis 配置】获取。


听起来是入职第二天就部署了任务感觉很难的样子。


其实现在看来是个 so easy 的任务,申请一个 redis 集群,自己配置下。在把工程里面使用到 redis 的地方改一下,之前使用的是一个两个单机节点。


干完,收工。



虽然领导的任务完成了,但并不意味着学习 redis 的路结束了。爱学习的龙叔,继续研究了下 redis 的集群模式。

4、集群模式

主从不能解决故障自动恢复问题,哨兵已经可以解决故障自动恢复了,那到底为啥还要集群模式呢?


主从和哨兵都还有另外一些问题没有解决,单个节点的存储能力是有上限,访问能力是有上限的。


Redis Cluster 集群模式具有 高可用可扩展性分布式容错 等特性。

Cluster 集群模式的原理

通过数据分片的方式来进行数据共享问题,同时提供数据复制和故障转移功能。


之前的两种模式数据都是在一个节点上的,单个节点存储是存在上限的。集群模式就是把数据进行分片存储,当一个分片数据达到上限的时候,就分成多个分片。

数据分片怎么分?

集群的键空间被分割为 16384 个 slots(即 hash 槽),通过 hash 的方式将数据分到不同的分片上的。


HASH_SLOT = CRC16(key) & 16384 
复制代码


CRC16 是一种循环校验算法,这里不是我们研究的重点,有兴趣可以看看。


这里用了位运算得到取模结果,位运算的速度高于取模运算。



有一个很重要的问题,为什么是分割为 16384 个槽?这个问题可能会被面试官随口一问

数据分片之后怎么查,怎么写?


读请求分配给 slave 节点,写请求分配给 master,数据同步从 master 到 slave 节点。


读写分离提高并发能力,增加高性能。

如何做到水平扩展?


master 节点可以做扩充,数据迁移 redis 内部自动完成。


当你新增一个 master 节点,需要做数据迁移,redis 服务不需要下线。


举个栗子:上面的有三个 master 节点,意味着 redis 的槽被分为三个段,假设三段分别是 0~7000,7001~12000、12001~16383。


现在因为业务需要新增了一个 master 节点,四个节点共同占有 16384 个槽。


槽需要重新分配,数据也需要重新迁移,但是服务不需要下线。


redis 集群的重新分片由 redis 内部的管理软件 redis-trib 负责执行。redis 提供了进行重新分片的所有命令,redis-trib 通过向节点发送命令来进行重新分片。

如何做故障转移?


假如途中红色的节点故障了,此时 master3 下面的从节点会通过 选举 产生一个主节点。替换原来的故障节点。


此过程和哨兵模式的故障转移是一样的。


四、Redis 穿透、雪崩和失效

1、redis 缓存穿透



理解


  • 重在穿透吧,也就是访问透过 redis 直接经过 mysql,通常是一个不存在的key,在数据库查询为null。每次请求落在数据库、并且高并发。数据库扛不住会挂掉。


解决方案


  • 可以将查到的 null 设成该 key 的缓存对象。

  • 当然,也可以根据明显错误的 key 在逻辑层就就行验证

  • 同时,你也可以分析用户行为,是否为故意请求或者爬虫、攻击者。针对用户访问做限制。

  • 其他等等,比如用布隆过滤器(超大型 hashmap)先过滤。

2、redis 缓存雪崩

理解


  • 雪崩,就是某东西蜂拥而至的意思,像雪崩一样。在这里,就是 redis 缓存集体大规模集体失效,在高并发情况下突然使得 key 大规模访问 mysql,使得数据库崩掉。可以想象下国家人口老年化。以后那天人集中在 70-80 岁,就没人干活了。国家劳动力就造成压力。


解决方案


  • 通常的解决方案是将 key 的过期时间后面加上一个随机数,让 key 均匀的失效。

  • 考虑用队列或者锁让程序执行在压力范围之内,当然这种方案可能会影响并发量。

  • 热点数据可以考虑不失效

3、redis 缓存击穿

理解


缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,好像蛮力击穿一样。



  • 击穿和穿透不同,穿透的意思是想法绕过redis 去使得数据库崩掉。而击穿你可以理解为正面刚击穿,这种通常为大量并发对一个 key 进行大规模的读写操作。这个 key 在缓存失效期间大量请求数据库,对数据库造成太大压力使得数据库崩掉。就比如在秒杀场景下 10000 块钱的 mac 和 100 块的 mac 这个 100 块的那个订单肯定会被抢到爆,不断的请求(当然具体秒杀有自己处理方式这里只是举个例子)。所以缓存击穿就是针对某个常用 key 大量请求导致数据库崩溃。


解决方案


  • 可以使用互斥锁避免大量请求同时落到 db。

  • 布隆过滤器,判断某个容器是否在集合中

  • 可以将缓存设置永不过期(适合部分情况)

  • 做好熔断、降级,防止系统崩溃。


五、布隆过滤器

1、布隆过滤器使用场景

比如有如下几个需求:


①、原本有 10 亿个号码,现在又来了 10 万个号码,要快速准确判断这 10 万个号码是否在 10 亿个号码库中?


解决办法一:将 10 亿个号码存入数据库中,进行数据库查询,准确性有了,但是速度会比较慢。


解决办法二:将 10 亿号码放入内存中,比如 Redis 缓存中,这里我们算一下占用内存大小:10 亿*8 字节=8GB,通过内存查询,准确性和速度都有了,但是大约 8gb 的内存空间,挺浪费内存空间的。


②、接触过爬虫的,应该有这么一个需求,需要爬虫的网站千千万万,对于一个新的网站 url,我们如何判断这个 url 我们是否已经爬过了?


解决办法还是上面的两种,很显然,都不太好。


③、同理还有垃圾邮箱的过滤。


那么对于类似这种,大数据量集合,如何准确快速的判断某个数据是否在大数据量集合中,并且不占用内存,布隆过滤器应运而生了。

2、布隆过滤器简介

带着上面的几个疑问,我们来看看到底什么是布隆过滤器。


布隆过滤器:一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是 0,就是 1,但是初始默认值都是 0。


如下所示:


①、添加数据


介绍概念的时候,我们说可以将布隆过滤器看成一个容器,那么如何向布隆过滤器中添加一个数据呢?


如下图所示:当要向布隆过滤器中添加一个元素 key 时,我们通过多个 hash 函数,算出一个值,然后将这个值所在的方格置为 1。


比如,下图 hash1(key)=1,那么在第 2 个格子将 0 变为 1(数组是从 0 开始计数的),hash2(key)=7,那么将第 8 个格子置位 1,依次类推。


②、判断数据是否存在?


知道了如何向布隆过滤器中添加一个数据,那么新来一个数据,我们如何判断其是否存在于这个布隆过滤器中呢?


很简单,我们只需要将这个新的数据通过上面自定义的几个哈希函数,分别算出各个值,然后看其对应的地方是否都是 1,如果存在一个不是 1 的情况,那么我们可以说,该新数据一定不存在于这个布隆过滤器中。


反过来说,如果通过哈希函数算出来的值,对应的地方都是 1,那么我们能够肯定的得出:这个数据一定存在于这个布隆过滤器中吗?


答案是否定的,因为多个不同的数据通过 hash 函数算出来的结果是会有重复的,所以会存在某个位置是别的数据通过 hash 函数置为的 1。


我们可以得到一个结论:布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在


③、布隆过滤器优缺点


优点:优点很明显,二进制组成的数组,占用内存极少,并且插入和查询速度都足够快。


缺点:随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。

3、Redis 实现布隆过滤器

①、bitmaps

我们知道计算机是以二进制位作为底层存储的基础单位,一个字节等于 8 位。


比如“big”字符串是由三个字符组成的,这三个字符对应的 ASCII 码分为是 98、105、103,对应的二进制存储如下:



在 Redis 中,Bitmaps 提供了一套命令用来操作类似上面字符串中的每一个位。


一、设置值


setbit key offset value
复制代码



我们知道"b"的二进制表示为 0110 0010,我们将第 7 位(从 0 开始)设置为 1,那 0110 0011 表示的就是字符“c”,所以最后的字符 “big”变成了“cig”。


二、获取值


gitbit key offset
复制代码



三、获取位图指定范围值为 1 的个数


bitcount key [start end]
复制代码



如果不指定,那就是获取全部值为 1 的个数。


注意:start 和 end 指定的是字节的个数,而不是位数组下标。

②、Redisson

Redis 实现布隆过滤器的底层就是通过 bitmap 这种数据结构,至于如何实现,这里就不重复造轮子了,介绍业界比较好用的一个客户端工具——Redisson。


Redisson 是用于在 Java 程序中操作 Redis 的库,利用 Redisson 我们可以在程序中轻松地使用 Redis。


下面我们就通过 Redisson 来构造布隆过滤器。


package com.ys.rediscluster.bloomfilter.redisson;
import org.redisson.Redisson;import org.redisson.api.RBloomFilter;import org.redisson.api.RedissonClient;import org.redisson.config.Config;
public class RedissonBloomFilter {
public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.14.104:6379"); config.useSingleServer().setPassword("123"); //构造Redisson RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList"); //初始化布隆过滤器:预计元素为100000000L,误差率为3% bloomFilter.tryInit(100000000L,0.03); //将号码10086插入到布隆过滤器中 bloomFilter.add("10086");
//判断下面号码是否在布隆过滤器中 System.out.println(bloomFilter.contains("123456"));//false System.out.println(bloomFilter.contains("10086"));//true }}
复制代码


这是单节点的 Redis 实现方式,如果数据量比较大,期望的误差率又很低,那单节点所提供的内存是无法满足的,这时候可以使用分布式布隆过滤器,同样也可以用 Redisson 来实现,这里我就不做代码演示了,大家有兴趣可以试试。

4、guava 工具

最后提一下不用 Redis 如何来实现布隆过滤器。


guava 工具包相信大家都用过,这是谷歌公司提供的,里面也提供了布隆过滤器的实现。


package com.ys.rediscluster.bloomfilter;
import com.google.common.base.Charsets;import com.google.common.hash.BloomFilter;import com.google.common.hash.Funnel;import com.google.common.hash.Funnels;
public class GuavaBloomFilter { public static void main(String[] args) { BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8),100000,0.01);
bloomFilter.put("10086");
System.out.println(bloomFilter.mightContain("123456")); System.out.println(bloomFilter.mightContain("10086")); }}
复制代码




本文写到这了,关于 Redis 平时能用到的地方文章里基本都写到了,希望对你学习 Redis 有所帮助。


给兄弟们准备的资料都放在这里了



end

发布于: 2021 年 04 月 27 日阅读数: 98
用户头像

北游学Java

关注

进群1044279583分享学习经验和分享面试心得 2020.11.16 加入

我秃了,也变强了

评论

发布
暂无评论
分布式存储中间件(1):10000字把Redis扒个干净,一发入魂