1000W用户1Wqps高并发签到系统的架构和实操

Posted 疯狂创客圈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1000W用户1Wqps高并发签到系统的架构和实操相关的知识,希望对你有一定的参考价值。

文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :

免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


说在前面

在尼恩的(50+)读者社群中,经常有小伙伴面试的时候,遇到一个一个高并发 架构方面的问题,比如:

(1) 高并发秒杀系统如何架构?

(2) 高并发签到系统如何架构?

(3) 等等等等......

刚刚,在尼恩的社群(50+)中,有小伙伴又问了这个问题。 尼恩作为技术中台的架构师, 高并发架构是尼恩架构的重点, 所以,一直想带大家从 架构到实操, 这里,带大家完成一个1000W用户1Wqps+ 签到系统的架构和实操。

本文也一并收入咱们的 《尼恩Java面试宝典》V64版本PDF,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

当然,作为一篇文章,仅仅是抛砖引玉,后面有机会,带大家做一下这个高质量的实操,并且指导大家写入简历。

注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从公众号 【技术自由圈】获取

本文目录

目录

签到场景业务需求分析

一般像微博,各种社交软件,游戏等APP,都会有一个签到功能,连续签到多少天,送什么东西,比如:

  • 签到1天送10积分,连续签到2天送20积分,3天送30积分,4天以上均送50积分等
  • 如果连续签到中断,则重置计数,每月初重置计数
  • 显示用户某个月的签到次数

1000W级用户场景的签到优化

优化1:利用Bitmap实现用户签到存储优化

为了实现用户的签到功能,通过mysql来完成,

为了实现签到的存储, 可以设计一个类似下面的表

假如有1000万用户,平均每人每年签到次数为10次,一年下来,则这张表数据量为多少呢?

数据比较吓人: 1亿条

每签到一次需要使用(8 + 8 + 1 + 1 + 3 + 1)共22 字节的内存,一个月则最多需要600多字节

我们如何能够简化一点呢?

其实可以考虑小时候一个挺常见的方案,就是小时候,咱们准备一张小小的卡片,你只要签到就打上一个勾,我最后判断你是否签到,其实只需要到小卡片上看一看就知道了。

我们可以采用类似这样的方案来实现我们的签到需求。

我们按月来统计用户签到信息,签到记录为1,未签到则记录为0。

把每一个bit位对应当月的每一天,形成了映射关系。用0和1标示业务状态,这种思路就称为位图(BitMap)。

这样我们就用极小的空间,来实现了大量数据的表示。

那么,一个月的签到数据,可以使用一个无符号整数来存储

之前一个月要 600个字节,现在保存一个人一个月的签到数据, 只要 24个字节 ,一下就提升了 25倍

注意:是存储空间,节约了25倍哈

优化2:利用Redis Bitmap实现用户签到的性能优化

刚好redis 有Bitmap结构。

可以利用Redis Bitmap实现用户签到的性能优化,将当前用户当天签到信息保存到Redis中。

具体的架构如下:

redis的吞吐量为 2Wqps以上, 完全可以满足 1000W用户签到的需求。

但是 SpringCloud 微服务够呛,怎么办呢?

优化3:利用Nginx +lus 实现更进一步的用户签到的性能优化

SpringCloud 微服务够呛,但是nginx可以,

nignx的吞吐量为 2Wqps以上, 完全可以满足 1000W用户签到的需求。

具体的架构如下:

架构介绍完了,接下来就开始实操。

但是,在开始实操之前,咱们得补一下基础知识。

基础知识:什么是Bitmap

Bitmap是一种位图数据结构,它用来表示一个集合中每个元素是否出现。

在Bitmap中,每个元素都与一个位(bit)相对应,如果元素出现,则对应位的值为1,否则为0。

Bitmap可以用来进行快速的集合运算,如并集、交集、差集等操作。

Bitmap的优点在于它的空间利用率非常高,通常只需要占用一个二进制位来表示一个元素是否出现,因此对于稀疏的数据集合,Bitmap可以显著减少存储空间的使用。

此外,由于Bitmap的位运算操作非常高效,因此它也能够快速地进行集合运算操作。

Redis中是利用string类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是 2^32个bit位。

Bitmap的使用场景

Bitmap可以应用于许多场景,主要包括以下几个方面:

  • 去重:

    可以使用Bitmap去重,将出现的元素映射到Bitmap中,若Bitmap中对应的位置已经被标记为1,即表示该元素已经出现过,可以避免重复。

  • 排序:

    可以使用Bitmap实现排序,将元素映射到Bitmap中,遍历Bitmap中的所有位,对于值为1的位,输出对应元素。

  • 数据库查询:

    在数据库中,可以使用Bitmap对记录进行索引,

    例如,在一个表中记录用户的性别,可以使用Bitmap将每个用户的性别映射到Bitmap中,然后通过位运算来查询特定性别的用户。

  • 网络流量分析:

    在网络流量分析中,可以使用Bitmap来记录每个IP地址的访问情况,然后通过位运算进行流量统计和分析。

  • 压缩编码:

    可以使用Bitmap进行数据压缩编码,将数据压缩为一系列0和1的位序列,通过位运算进行解码。

总的来说,Bitmap具有高效、灵活、易用等优点,在许多数据处理和分析的场景中都有广泛的应用。

Redis 中Bitmap的相关命令

客户端连接 redis

docker exec -it redis-standalone  redis-cli  

auth 123456

Redis提供了一系列Bitmap相关的指令,包括以下几个:

指定key中的offset位设置为value

将指定key中的offset位设置为value(0或1),若key不存在,则会自动创建一个新的空Bitmap。

SETBIT key offset value

例:SETBIT mybitmap 1001 1 将mybitmap的第1001位设置为1。

127.0.0.1:6379> setbit mybitmap 1001 1
(integer) 0

获取指定 key 中的offset位的值

获取指定key中的offset位的值(0或1),若key不存在或offset超出范围,则返回0。

GETBIT key offset

例如:GETBIT mybitmap 1001将返回mybitmap的第1001位的值。

127.0.0.1:6379> getbit mybitmap 1001
(integer) 1

统计指定key中值为1的位的数量。

BITCOUNT key [start end]

可以通过可选参数start和end指定统计的范围(单位是位),若不指定,则默认统计整个Bitmap。

例如:BITCOUNT mybitmap 100 1000 将统计mybitmap中第100位到第1000位值为1的位的数量。

127.0.0.1:6379> bitcount mybitmap 100 1000
(integer) 1

对多个Bitmap进行位运算,并将结果保存到destkey中。

BITOP operation destkey key [key ...]

operation可以是AND、OR、XOR和NOT中的一个,表示进行与、或、异或和非运算。

例如:BITOP AND mybitmap1 mybitmap2 mybitmap3将mybitmap2和mybitmap3进行与运算,并将结果保存到mybitmap1中。

127.0.0.1:6379> bitop and mybitmap1 mybitmap2 mybitmap3
(integer) 0

查找指定key中第一个值为bit(0或1)的位的位置

查找指定key中第一个值为bit(0或1)的位的位置,并返回其索引(单位是位)。

BITPOS key bit [start] [end]

可以通过可选参数start和end指定查找的范围。若bit不存在于指定范围内,则返回-1。

例如:BITPOS mybitmap 1 100 1000将在mybitmap的第100位到第1000位中查找值为1的位的位置。

127.0.0.1:6379> bitpos mybitmap 1 100 1000
(integer) 1001

BITFIELD:在位图中存储整数值

BITFIELD命令允许用户在位图中的任意区域(field)存储指定长度的整数值,并对这些整数值执行加法或减法操作。

BITFIELD命令支持SET、GET、INCRBY、OVERFLOW这4个子命令,接下来将分别介绍这些子命令。

语法

官方文档

BITFIELD key [GET encoding offset | [OVERFLOW <WRAP | SAT | FAIL>]
  <SET encoding offset value | INCRBY encoding offset increment>
  [GET encoding offset | [OVERFLOW <WRAP | SAT | FAIL>]
  <SET encoding offset value | INCRBY encoding offset increment>
  ...]]

根据偏移量对区域进行设置

除了根据偏移量对位图进行设置之外,SET子命令还允许用户根据给定类型的位长度,对位图在指定索引上存储的整数值进行设置。

通过使用BITFIELD命令的SET子命令,用户可以在位图的指定偏移量offset上设置一个type类型的整数值value,

来一个列子,从第0位开始,设置 sign:1880000000::202305 的 值为 198 :

BITFIELD  sign:1880000000::202306   set u8  0 198

主要的参数如下:

  • offset 操作的偏移量
  • type 操作的值的数值类型
  • value 要操作的值

对于参数的介绍如下:

参数1:offset参数用于指定设置的起始偏移量。

这个偏移量从0开始计算,偏移量为0表示设置从位图的第1个二进制位开始。

如果被设置的值长度不止一位,那么设置将自动延伸至之后的二进制位。

参数2:type参数用于指定等待设置的值类型,

type参数的值需要以i或者u为前缀,后跟被设置值的位长度,

其中i表示被设置的值为有符号整数,而u则表示被设置的值为无符号整数。

比如i8表示被设置的值为有符号8位整数,

而u16则表示被设置的值为无符号16位整数,诸如此类。

BITFIELD的各个子命令目前最大能够对64位长的有符号整数(i64)和63位长的无符号整数(u63)进行操作。

参数3:value参数用于指定被设置的整数值

这个值的类型应该和type参数指定的类型一致。

如果给定值的长度超过了type参数指定的类型,那么SET命令将根据type参数指定的类型截断给定值。

比如,如果用户尝试将整数123(二进制表示为01111011)存储到一个u4类型的区域中,那么命令会先将该值截断为4位长的二进制数字1011(即十进制数字11),然后再进行设置。

设置的结果

设置 sign:1880000000::202305 的 值为 198 :

BITFIELD  sign:1880000000::202306   set u8  0 198

通过命令,我们可以从偏移量0开始,设置一个8位长的无符号整数值198(二进制表示为11000110):

从子命令返回的结果可以看出,该区域被设置之前存储的整数值为0。下图展示了执行设置命令之后的位图:

批量操作:一次调用中执行多个子命令

尼恩提示:高并发场景,为了减少io 次数,一般都是建议 批量操作。

BITFIELD命令允许用户在一次调用中执行多个子命令,比如,通过在一次BITFIELD调用中使用多个SET子命令,我们可以同时对位图的多个区域进行设置:

BITFIELD  sign:1880000000::202306   SET u8 0 123 SET i32 20 10086

  • 第1个子命令SET u8 0 123从偏移量0开始,设置一个8位长无符号整数值123。
  • 第2个子命令SET i32 20 10086从偏移量20开始,设置一个32位长有符号整数值10086。

对于这2个子命令,BITFIELD命令返回了一个包含2个元素的数组作为命令的执行结果,

这2个元素分别代表2个指定区域被设置之前存储的整数值,比如第一个子命令返回的结果就是我们之前为该区域设置的值123。

下图展示了这个BITFIELD命令创建出的位图以及被设置的3个整数值在位图中所处的位偏移量。

上图也展示了SET子命令的两个特点:

  • 设置可以在位图的任意偏移量上进行,被设置区域之间不必是连续的,也不需要进行对齐(align)。

    各个区域之间可以有空洞,即未被设置的二进制位,这些二进制位会自动被初始化为0。

  • 在同一个位图中可以存储多个不同类型和不同长度的整数。

虽然这两个特点可以带来很大的灵活性,但是从节约内存、避免发生错误等情况考虑,我们一般还是应该:

  • 以对齐的方式使用位图,并且让位图尽可能地紧凑,避免包含过多空洞。

  • 每个位图只存储同一种类型的整数,并使用int-8bit、unsigned-16bit这样的键名前缀来标识位图中存储的整数类型。

获取区域存储的值

通过使用BITFIELD命令的GET子命令,用户可以从给定的偏移量或者索引中取出指定类型的整数值:

GET子命令各个参数的意义与SET子命令中同名参数的意义完全一样。

执行加法操作或减法操作

除了设置和获取整数值之外,BITFIELD命令还可以对位图存储的整数值执行加法操作或者减法操作,

这两个操作都可以通过INCRBY子命令实现。

语法

BITFIELD命令并没有提供与INCRBY子命令相对应的DECRBY子命令,但是用户可以通过向INCRBY子命令传入负数增量来达到执行减法操作的效果。

INCRBY子命令在执行完相应的操作之后会返回整数的当前值作为结果。例子如下:

处理溢出

BITFIELD命令除了可以使用INCRBY子命令来执行加法操作和减法操作之外,还可以使用OVERFLOW子命令去控制INCRBY子命令在发生计算溢出时的行为。

语法

OVERFLOW子命令的参数可以是WRAP、SAT或者FAIL中的一个:

  • WRAP表示使用回绕(wrap around)方式处理溢出,这也是C语言默认的溢出处理方式。在这一模式下,向上溢出的整数值将从类型的最小值开始重新计算,而向下溢出的整数值则会从类型的最大值开始重新计算。
  • SAT表示使用饱和运算(saturation arithmetic)方式处理溢出,在这一模式下,向上溢出的整数值将被设置为类型的最大值,而向下溢出的整数值则会被设置为类型的最小值。
  • FAIL表示让INCRBY子命令在检测到计算会引发溢出时拒绝执行计算,并返回空值表示计算失败。

OVERFLOW子命令在执行时将不产生任何回复。此外,如果用户在执行BITFIELD命令时没有指定具体的溢出处理方式,那么INCRBY子命令默认使用WRAP方式处理计算溢出。

需要注意的是,因为OVERFLOW子命令只会对同一个BITFIELD调用中排在它之后的那些INCRBY子命令产生效果,所以用户必须把OVERFLOW子命令放到它想要影响的INCRBY子命令之前。

其他命令

除此之外,还有其他一些Bitmap相关的指令,如BITMAP等,可以根据实际需求进行选择。

redis bitmap 常用命令:

#向指定位置(offset)存入0或1
SETBIT key offset value
 
#获取指定位置(offset)的bit值
GETBIT key offset
 
#统计bitmap中(从start到end,如果不写起始位置,就统计整个key)值为1的bit数量
BITCOUNT key [start end]
 
#操作(查询,修改,自增)Bitmap中指定的位置(offset)的值
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
 
#获取Bitmap中的bit数组,并以十进制返回
BITFIELD_RO

在线练习工具:https://try.redis.io/
查看更多命令:https://redis.io/commands

尼恩建议,优先使用位图存储整数

尼恩建议,在redis中,优先使用 位图存储整数, 而不是使用 String 存储整数

为啥呢?

在一般情况下,当用户使用字符串或者散列去存储整数的时候,Redis都会为被存储的整数分配一个long类型的值(通常为32位长或者64位长),并使用对象去包裹这个值,然后再把对象关联到数据库或者散列中。

与此相反,BITFIELD命令允许用户自行指定被存储整数的类型,并且不会使用对象去包裹这些整数,因此当我们想要存储长度比long类型短的整数,并且希望尽可能地减少对象包裹带来的内存消耗时,就可以考虑使用位图来存储整数。

使用SpringCloud+Redis BitMap 用户场景的签到实操

实操包括以下的操作:

  • 用户签到实操
  • 用户签到查询
  • 查询 获取本月签到信息 签到

用户签到实操

代码如下:

测试如下:

用户签到查询

代码如下:

测试如下:

查询 获取本月签到信息 签到

代码如下:

测试如下:

本文的版本计划和基础学习资料

尼恩作为技术中台、数据中台的架构师, 高并发是尼恩架构的重点, 所以,后面会大家从 架构到实操, 多个维度、多种场景的高并发实操。

而且尼恩指导简历的过程中,也指导过小伙伴写过 10Wqps 超高并发 网关、超高并发秒杀、超高并发物联网等等简历,里边涉及到大量的设计模式,

经过尼恩的指导之后,很多 小伙伴的简历,里边立马金光闪闪,并且顺利走上了架构师之路。

后面有机会,带大家做一下这个高质量的实操,并且指导大家写入简历。

所以,这个材料后面也会进行持续迭代,大家可以找尼恩来获取最新版本和技术交流。

尼恩编著的400页PDF电子书 《SpringCloud Alibaba 技术圣经》

尼恩编著的500页 PDF电子书 《Java高并发核心编程 卷3 加强版》,清华大学出版社出版

pdf领取方式,请参见公众号: 技术自由圈

技术自由的实现路径:

实现你的 架构自由:

吃透8图1模板,人人可以做架构

10Wqps评论中台,如何架构?B站是这么做的!!!

阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了

峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?

100亿级订单怎么调度,来一个大厂的极品方案

2个大厂 100亿级 超大流量 红包 架构方案

… 更多架构文章,正在添加中

实现你的 响应式 自由:

响应式圣经:10W字,实现Spring响应式编程自由

这是老版本 《Flux、Mono、Reactor 实战(史上最全)

实现你的 spring cloud 自由:

Spring cloud Alibaba 学习圣经》 PDF

分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)

实现你的 linux 自由:

Linux命令大全:2W多字,一次实现Linux自由

实现你的 网络 自由:

TCP协议详解 (史上最全)

网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!

实现你的 分布式锁 自由:

Redis分布式锁(图解 - 秒懂 - 史上最全)

Zookeeper 分布式锁 - 图解 - 秒懂

实现你的 王者组件 自由:

队列之王: Disruptor 原理、架构、源码 一文穿透

缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)

缓存之王:Caffeine 的使用(史上最全)

Java Agent 探针、字节码增强 ByteBuddy(史上最全)

实现你的 面试题 自由:

4000页《尼恩Java面试宝典 》 40个专题

免费获取11个技术圣经PDF:

redis cluster 集群 HA 原理和实操(史上最全面试必备)

文章很长,建议收藏起来慢慢读!疯狂创客圈总目录 语雀版 | 总目录 码云版| 总目录 博客园版 为您奉上珍贵的学习资源 :


SpringCloud 微服务 精彩博文
nacos 实战(史上最全) sentinel (史上最全+入门教程)
SpringCloud gateway (史上最全)分库分表sharding-jdbc底层原理与实操(史上最全,5W字长文,吐血推荐)

说明

redis cluster是 生存环境常用的组件,很多小伙伴没有玩过,很可惜

本文从原理到实操,都给大家做了一个介绍,后面会 持续完善

实际上,尼恩给大家准备了一键搭建一套redis cluster集群的 docker-compose 编排文件,可以在虚拟机上 体验一下,比单机 安装还简单,
如果有 docker-compose 编排文件 需要或者 需要技术交流,可以来疯狂创客圈 jAVA高并发 社群,一起研究硬核技术

Redis集群高可用常见的三种方式:

Redis高可用常见的有两种方式:

  • Replication-Sentinel模式
  • Redis-Cluster模式
  • 中心化代理模式(proxy模式)

Replication-Sentinel模式

Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移。

Redis sentinel 其中三个特性:

  • 监控(Monitoring):

Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。

  • 提醒(Notification):

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

  • 自动故障迁移(Automatic failover):

当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。

哨兵本身也有单点故障的问题,可以使用多个哨兵进行监控,哨兵不仅会监控redis集群,哨兵之间也会相互监控。

每一个哨兵都是一个独立的进程,作为进程,它会独立运行。

特点:

  • 1、保证高可用

  • 2、监控各个节点

  • 3、自动故障迁移

缺点:

主从模式,切换需要时间丢数据

没有解决 master 写的压力

Redis-Cluster模式

redis在3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的数据。

cluster模式为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器,内存/QPS不受限于单机,可受益于分布式集群高扩展性。

RedisCluster 是 Redis 的亲儿子,它是 Redis 作者自己提供的 Redis 集群化方案。

相对于 Codis 的不同,它是去中心化的,如图所示,该集群有三个 Redis 节点组成, 每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相 互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息。

如上图,官方推荐,集群部署至少要 3 台以上的master节点,最好使用 3 主 3 从六个节点的模式。

Redis Cluster 将所有数据划分为 16384 的 slots,它比 Codis 的 1024 个槽划分得更为精细,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,它不像 Codis,它不 需要另外的分布式存储来存储节点槽位信息。

Redis Cluster是一种服务器Sharding技术(分片和路由都是在服务端实现),采用多主多从,每一个分区都是由一个Redis主机和多个从机组成,片区和片区之间是相互平行的。

Redis Cluster集群采用了P2P的模式,完全去中心化。

3 主 3 从六个节点的Redis集群(Redis-Cluster)

Redis 集群是一个提供在多个Redis节点间共享数据的程序集。

下图以三个master节点和三个slave节点作为示例。

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。

集群的每个节点负责一部分hash槽,如图中slots所示。

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有1-n个从节点。

例如master-A节点不可用了,集群便会选举slave-A节点作为新的主节点继续服务。

中心化代理模式(proxy模式)

这种方案,将分片工作交给专门的代理程序来做。代

理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的 Redis 实例并返回给业务程序。

其基本原理是:通过中间件的形式,Redis客户端把请求发送到代理 proxy,代理 proxy 根据路由规则发送到正确的Redis实例,最后 代理 proxy 把结果汇集返回给客户端。

redis代理分片用得最多的就是Twemproxy,由Twitter开源的Redis代理,其基本原理是:通过中间件的形式,Redis客户端把请求发送到Twemproxy,Twemproxy根据路由规则发送到正确的Redis实例,最后Twemproxy把结果汇集返回给客户端。

这种机制下,一般会选用第三方代理程序(而不是自己研发),因为后端有多个 Redis 实例,所以这类程序又称为分布式中间件。

这样的好处是,业务程序不用关心后端 Redis 实例,运维起来也方便。虽然会因此带来些性能损耗,但对于 Redis 这种内存读写型应用,相对而言是能容忍的。

Twemproxy 代理分片

Twemproxy 是一个 Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器; Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和 redis 协议。

Twemproxy是由Twitter开源的集群化方案,它既可以做Redis Proxy,还可以做Memcached Proxy。

它的功能比较单一,只实现了请求路由转发,没有像Codis那么全面有在线扩容的功能,它解决的重点就是把客户端分片的逻辑统一放到了Proxy层而已,其他功能没有做任何处理。

Tweproxy推出的时间最久,在早期没有好的服务端分片集群方案时,应用范围很广,而且性能也极其稳定。

但它的痛点就是无法在线扩容、缩容,这就导致运维非常不方便,而且也没有友好的运维UI可以使用。

Codis代理分片

Codis 是一个分布式 Redis 解决方案, 对于上层的应用来说, 连接到 Codis Proxy 和连接原生的 Redis Server 没有明显的区别 (有一些命令不支持), 上层应用可以像使用单机的 Redis 一样使用, Codis 底层会处理请求的转发, 不停机的数据迁移等工作, 所有后边的一切事情, 对于前面的客户端来说是透明的, 可以简单的认为后边连接的是一个内存无限大的 Redis 服务,

现在美团、阿里等大厂已经开始用codis的集群功能了,

什么是Codis?

Twemproxy不能平滑增加Redis实例的问题带来了很大的不便,于是豌豆荚自主研发了Codis,一个支持平滑增加Redis实例的Redis代理软件,其基于Go和C语言开发,并于2014年11月在GitHub上开源 codis开源地址

Codis的架构图:

在Codis的架构图中,Codis引入了Redis Server Group,其通过指定一个主CodisRedis和一个或多个从CodisRedis,实现了Redis集群的高可用。

当一个主CodisRedis挂掉时,Codis不会自动把一个从CodisRedis提升为主CodisRedis,这涉及数据的一致性问题(Redis本身的数据同步是采用主从异步复制,当数据在主CodisRedis写入成功时,从CodisRedis是否已读入这个数据是没法保证的),需要管理员在管理界面上手动把从CodisRedis提升为主CodisRedis。

如果手动处理觉得麻烦,豌豆荚也提供了一个工具Codis-ha,这个工具会在检测到主CodisRedis挂掉的时候将其下线并提升一个从CodisRedis为主CodisRedis。

Codis的预分片

Codis中采用预分片的形式,启动的时候就创建了1024个slot,1个slot相当于1个箱子,每个箱子有固定的编号,范围是1~1024。

Codis的分片算法

Codis proxy 代理通过一种算法把要操作的key经过计算后分配到各个组中,这个过程叫做分片。

在Codis里面,它把所有的key分为1024个槽,每一个槽位都对应了一个分组,具体槽位的分配,可以进行自定义,现在如果有一个key进来,首先要根据CRC32算法,针对key算出32位的哈希值,然后除以1024取余,然后就能算出这个KEY属于哪个槽,然后根据槽与分组的映射关系,就能去对应的分组当中处理数据了。

CRC全称是循环冗余校验,主要在数据存储和通信领域保证数据正确性的校验手段,CRC校验(循环冗余校验)是数据通讯中最常采用的校验方式。

slot这个箱子用作存放Key,至于Key存放到哪个箱子,可以通过算法“crc32(key)%1024”获得一个数字,这个数字的范围一定是1~1024之间,Key就放到这个数字对应的slot。

例如,如果某个Key通过算法“crc32(key)%1024”得到的数字是5,就放到编码为5的slot(箱子)。

slot和Server Group的关系

1个slot只能放1个Redis Server Group,不能把1个slot放到多个Redis Server Group中。1个Redis Server Group最少可以存放1个slot,最大可以存放1024个slot。

因此,Codis中最多可以指定1024个Redis Server Group。

槽位和分组的映射关系就保存在codis proxy当中

redis主从复制

主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

what is ?

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

  • 前者称为主节点(master),后者称为从节点(slave);

  • 数据的复制是单向的,只能由主节点到从节点。

  • 默认情况下,每台Redis服务器都是主节点;

  • 且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用

主从复制的作用主要包括:

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  3. 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  4. 高可用、高并发基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

开启主从复制的方式

需要注意,主从复制的开启,完全是在从节点发起的;不需要我们在主节点做任何事情。

从节点开启主从复制,有3种方式:

(1)配置文件

在从服务器的配置文件中加入:slaveof

(2)启动命令

redis-server启动命令后加入 --slaveof

(3)客户端命令

Redis服务器启动后,直接通过客户端执行命令:slaveof ,则该Redis实例成为从节点。

上述3种方式是等效的,下面以客户端命令的方式为例,看一下当执行了slaveof后,Redis主节点和从节点的变化。

主从复制实例

准备工作:启动两个节点

实验所使用的主从节点是在一台机器上的不同Redis实例,其中:

  • 主节点监听6379端口,
  • 从节点监听6380端口;
  • 从节点监听的端口号可以在配置文件中修改:

启动后可以看到:

两个Redis节点启动后(分别称为6379节点和6380节点),默认都是主节点。

建立复制关系

此时在6380节点执行slaveof命令,使之变为从节点:

观察效果

下面验证一下,在主从复制建立后,主节点的数据会复制到从节点中。

(1)首先在从节点查询一个不存在的key:

(2)然后在主节点中增加这个key:

(3)此时在从节点中再次查询这个key,会发现主节点的操作已经同步至从节点:

(4)然后在主节点删除这个key:

(5)此时在从节点中再次查询这个key,会发现主节点的操作已经同步至从节点:

断开复制

通过slaveof 命令建立主从复制关系以后,可以通过slaveof no one断开。需要注意的是,从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。

从节点执行slaveof no one后,打印日志如下所示;

可以看出断开复制后,从节点又变回为主节点。

断开复制后,主节点打印日志如下:

主从复制的核心原理

1 当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

2 如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。

master node 怎么进行 full resynchronization 全量复制?

此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。

RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,

slave node 接收到RDB ,干啥呢?

会先写入本地磁盘,然后再从本地磁盘加载到内存中,

3 数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段master 将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

4 部分复制。如果slave node跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

主从复制的核心流程

主从复制过程大体可以分为3个阶段:

  • 连接建立阶段(即准备阶段)
  • 数据同步阶段
  • 命令传播阶段;

下面分别进行介绍。

连接建立阶段

该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。

步骤1:保存主节点信息

从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。

需要注意的是,slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。

这个过程中,可以看到从节点打印日志如下:

步骤2:建立socket连接

slave 从节点每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。

如果连接成功,则:

  • 从节点:

为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。

  • 主节点:

接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。

这个过程中,从节点打印日志如下:

步骤3:发送ping命令

从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。

从节点发送ping命令后,可能出现3种情况:

(1)返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。

(2)超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。

(3)返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。

在主节点返回pong情况下,从节点打印日志如下:

步骤4:身份验证

如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。

从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的master auth的值。

  • 则身份验证通过,复制过程继续;

  • 如果不一致,则从节点断开socket连接,并重连。

步骤5:发送从节点端口信息

身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;

该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。

数据同步阶段

主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。

具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。

数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。

在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;

在Redis2.8及以后,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。后文介绍以Redis2.8及以后版本为例。

  1. 全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
  2. 部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。

全量复制的过程

Redis通过psync命令进行全量复制的过程如下:

(1)从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;

(2)主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令

(3)主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点接收完成之后,首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态

(4)主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态

(5)如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态

下面是执行全量复制时,主从节点打印的日志;可以看出日志内容与上述步骤是完全对应的。

主节点的打印日志如下:

从节点打印日志如下图所示:

其中,有几点需要注意:

  • 从节点接收了来自主节点的89260个字节的数据;

  • 从节点在载入主节点的数据之前要先将老数据清除;

  • 从节点在同步完数据后,调用了bgrewriteaof。

通过全量复制的过程可以看出,全量复制是非常重型的操作:

(1)性能损耗:主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的;

(2)带宽占用:主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗

(3)停服载入:从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗

题外话:什么是Redis Bgrewriteaof ?

Redis Bgrewriteaof 命令用于异步执行一个 AOF(AppendOnly File) 文件重写操作。

Bgrewriteaof 重写会创建一个当前 AOF 文件的体积优化版本。

即使 Bgrewriteaof 执行失败,也不会有任何数据丢失,因为旧的 AOF 文件在 Bgrewriteaof 成功之前不会被修改。

**注意:**从 Redis 2.4 开始, AOF 重写由 Redis 自行触发, BGREWRITEAOF 仅仅用于手动触发重写操作。

redis Bgrewriteaof 命令基本语法如下:

redis 127.0.0.1:6379> BGREWRITEAOF 

redis2.8 版本之前主从复制流程

redis2.8 版本之前主从复制流程:

  • 从服务器连接主服务器,发送SYNC命令;
  • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  • 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

全量复制的弊端:

场景:(1)新创建的slave,从主机master同步数据。(2)刚宕机一小会的slave,从主机master同步数据。

前者新建的slave则从主机master全量同步数据,这没啥问题。但是后者slave可能只与主机master存在小量的数据差异,要是全量同步肯定没有只同步差异(部分复制)的那点数据性能高

部分复制

由于全量复制在主节点数据量较大时效率太低,因此Redis2.8开始提供部分复制,用于处理网络中断时的数据同步。

部分复制的实现,依赖于三个重要的概念:

(1)offset复制偏移量

  • 主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数

  • 主节点每次向从节点传播N个字节数据时,主节点的offset增加N;

  • 从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。

offset复制偏移量的用途

offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致;如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据。

例如,如果主节点的offset是1000,而从节点的offset是500,那么部分复制就需要将offset为501-1000的数据传递给从节点。

而offset为501-1000的数据存储的位置,就是下面要介绍的复制积压缓冲区。

(2)复制积压缓冲区( repl-backlog-buffer )

复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;

当主节点开始有从节点时, master创建一个积压缓冲区,其作用是备份主节点最近收到的redis命令,后续会发送给从节点的数据。

注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。

在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;

除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。

由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最复制积压缓冲区近执行的写命令;时间较早的写命令会被挤出缓冲区。

由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。

反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size);

例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。

从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制:

  • 如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
  • 如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。

(3)服务器运行ID(runid)

每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。

通过info Server命令,可以查看节点的runid:

主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

slavof命令的执行流程

在了解了复制偏移量、复制积压缓冲区、节点运行id之后,

接下来,看看slavof命令的执行流程

从节点收到slaveof命令之后,首先决定是使用全量复制还是部分复制:

(1)首先,从节点根据当前状态,决定如何调用psync命令:

  • 如果从节点之前未执行过slaveof或最近执行了slaveof no one,则从节点发送命令为psync ? -1,向主节点请求全量复制;
  • 如果从节点之前执行了slaveof,则发送命令为psync runid offset,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。

(2)主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制:

  • 如果主节点版本低于Redis2.8,则返回-ERR回复,此时从节点重新发送sync命令执行全量复制;
  • 如果主节点版本够新,且runid与从节点发送的runid相同,且从节点发送的offset之后的数据在复制积压缓冲区中都存在,则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
  • 如果主节点版本够新,但是runid与从节点发送的runid不同,或从节点发送的offset之后的数据已不在复制积压缓冲区中(在队列中被挤出了),则回复+FULLRESYNC runid offset,表示要进行全量复制,其中runid表示主节点当前的runid,offset表示主节点当前的offset,从节点保存这两个值,以备使用。

重新连接之后的部分复制

部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync runId offset 命令实现。

当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区存在这部分数据,则直接发送给从节点,这样就保证了主从节点复制的一致性。

补发的这部分数据一般远远小于全量数据,所以开销很小。

  1. 当主从节点之间网络出现中断时,如果超过了 repl-timeout 时间,主节点会认为从节点故障并中断复制连接。

  2. 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在复制积压缓冲区( repl-backlog-buffer ),依然可以保存最近一段时间的写命令数据,默认最大缓存 1MB。

  3. 当主从节点网络恢复后,从节点会再次连上主节点。

  4. 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们作为 psync 参数发送给主节点,要求进行补发复制操作。

  5. 主节点接到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数 offset 在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送 +CONTINUE 响应,表示可以进行部分复制。

  6. 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

命令传播阶段

数据同步阶段完成后,主从节点进入命令传播阶段;

在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。

心跳机制对于主从复制的超时判断、数据安全等有作用。

1.主->从:PING

每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。

PING发送的频率由 repl-ping-slave-period 参数控制,单位是秒,默认值是10s。

关于该PING命令究竟是由主节点发给从节点,还是相反,有一些争议;

因为在Redis的官方文档中,对该参数的注释中说明是从节点向主节点发送PING命令,如下图所示:

但是通过源码可以看到, PING命令是主节点会向从节点发送.

可能的原因是:代码的迭代和注释的迭代,没有完全同步。 可能早期是 从发给主,后面改成了主发从,而并没有配套修改注释, 就像尼恩的很多代码一样。

2. 从->主:REPLCONF ACK

在命令传播阶段,**从节点会向主节点发送REPLCONF ACK命令,**频率是每秒1次;

命令格式为:REPLCONF ACK offset,其中offset指从节点保存的复制偏移量。

REPLCONF ACK命令的作用包括:

(1)实时监测主从节点网络状态:该命令会被主节点用于复制超时的判断。此外,在主节点中使用info Replication,可以看到其从节点的状态中的lag值,代表的是主节点上次收到该REPLCONF ACK命令的时间间隔,在正常情况下,该值应该是0或1,如下图所示:

(2)检测命令丢失:从节点发送了自身的offset,主节点会与自己的offset对比,如果从节点数据缺失(如网络丢包),主节点会推送缺失的数据(这里也会利用复制积压缓冲区)。

注意,offset和复制积压缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。

(3)辅助保证从节点的数量和延迟:Redis主节点中使用min-slaves-to-write和min-slaves-max-lag参数,来保证主节点在不安全的情况下不会执行写命令;所谓不安全,是指从节点数量太少,或延迟过高。

例如min-slaves-to-write和min-slaves-max-lag分别是3和10,含义是如果从节点数量小于3个,或所有从节点的延迟值都大于10s,则主节点拒绝执行写命令。而这里从节点延迟值的获取,就是通过主节点接收到REPLCONF ACK命令的时间来判断的,即前面所说的info Replication中的lag值。

数据分片(sharding)的基本原理

什么是数据分片?

名词说明:

数据分片(sharding)也叫数据分区

为什么要做数据分片?

全量数据较大的场景下,单节点无法满足要求,需要数据分片

什么是数据分片?

按照分片规则把数据分到若干个shard、partition当中

range 分片

一种是按照 range 来分,就是每个片,一段连续的数据,这个一般是按比如时间范围/数据范围来的,但是这种一般较少用,因为很容易发生数据倾斜,大量的流量都打在最新的数据上了。

比如,安装数据范围分片,把1到100个数字,要保存在3个节点上

按照顺序分片,把数据平均分配三个节点上

  • 1号到33号数据保存到节点1上
  • 34号到66号数据保存到节点2上
  • 67号到100号数据保存到节点3上

ID取模分片

此种分片规则将数据分成n份(通常dn节点也为n),从而将数据均匀的分布于各个表中,或者各节点上。

扩容方便。

ID取模分片常用在关系型数据库的设计

具体请参见 秒杀视频的 亿级库表架构设计

hash 哈希分布

使用hash 算法,获取key的哈希结果,再按照规则进行分片,这样可以保证数据被打散,同时保证数据分布的比较均匀

哈希分布方式分为三个分片方式:

  • 哈希取余分片
  • 一致性哈希分片
  • 虚拟槽分片

哈希取余模分片

例如1到100个数字,对每个数字进行哈希运算,然后对每个数的哈希结果除以节点数进行取余,余数为1则保存在第1个节点上,余数为2则保存在第2个节点上,余数为0则保存在第3个节点,这样可以保证数据被打散,同时保证数据分布的比较均匀

比如有100个数据,对每个数据进行hash运算之后,与节点数进行取余运算,根据余数不同保存在不同的节点上

哈希取余分片是非常简单的一种分片方式

哈希取模分片有一个问题

即当增加或减少节点时,原来节点中的80%的数据会进行迁移操作,对所有数据重新进行分布

哈希取余分片,建议使用多倍扩容的方式,例如以前用3个节点保存数据,扩容为比以前多一倍的节点即6个节点来保存数据,这样只需要适移50%的数据。

数据迁移之后,第一次无法从缓存中读取数据,必须先从数据库中读取数据,然后回写到缓存中,然后才能从缓存中读取迁移之后的数据

哈希取余分片优点:

  • 配置简单:对数据进行哈希,然后取余

哈希取余分片缺点:

  • 数据节点伸缩时,导致数据迁移
  • 迁移数量和添加节点数据有关,建议翻倍扩容

一致性哈希分片

一致性哈希原理:

将所有的数据当做一个token环,

token环中的数据范围是0到2的32次方。

然后为每一个数据节点分配一个token范围值,这个节点就负责保存这个范围内的数据。

对每一个key进行hash运算,被哈希后的结果在哪个token的范围内,则按顺时针去找最近的节点,这个key将会被保存在这个节点上。

一致性哈希分片的节点扩容

在下面的图中:

  • 有4个key被hash之后的值在在n1节点和n2节点之间,按照顺时针规则,这4个key都会被保存在n2节点上

  • 如果在n1节点和n2节点之间添加n5节点,当下次有key被hash之后的值在n1节点和n5节点之间,这些key就会被保存在n5节点上面了

下图的例子里,添加n5节点之后:

  • 数据迁移会在n1节点和n2节点之间进行
  • n3节点和n4节点不受影响
  • 数据迁移范围被缩小很多

同理,如果有1000个节点,此时添加一个节点,受影响的节点范围最多只有千分之2。所以,一致性哈希一般用在节点比较多的时候,节点越多,扩容时受影响的节点范围越少

分片方式:哈希 + 顺时针(优化取余)

一致性哈希分片优点:

  • 一致性哈希算法解决了分布式下数据分布问题。比如在缓存系统中,通过一致性哈希算法把缓存键映射到不同的节点上,由于算法中虚拟节点的存在,哈希结果一般情况下比较均匀。
  • 节点伸缩时,只影响邻近节点,但是还是有数据迁移

“但没有一种解决方案是银弹,能适用于任何场景。所以实践中一致性哈希算法有哪些缺陷,或者有哪些场景不适用呢?”

一致性哈希分片缺点:

一致性哈希在大批量的数据场景下负载更加均衡,但是在数据规模小的场景下,会出现单位时间内某个节点完全空闲的情况出现。

虚拟槽分片 (范围分片的变种)

Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现;

虚拟槽分片是Redis Cluster采用的分片方式.

虚拟槽分片 ,可以理解为范围分片的变种, hash取模分片+范围分片, 把hash值取余数分为n段,一个段给一个节点负责

虚拟槽分片 (范围分片的变种)

Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现;

虚拟槽分片是Redis Cluster采用的分片方式.

在该分片方式中:

  • 首先 预设虚拟槽,每个槽为一个hash值,每个node负责一定槽范围。
  • 每一个值都是key的hash值取余,每个槽映射一个数据子集,一般比节点数大

Redis Cluster中预设虚拟槽的范围为0到16383

虚拟槽分片的映射步骤:

1.把16384槽按照节点数量进行平均分配,由节点进行管理
2.对每个key按照CRC16规则进行hash运算
3.把hash结果对16383进行取余
4.把余数发送给Redis节点
5.节点接收到数据,验证是否在自己管理的槽编号的范围

  • 如果在自己管理的槽编号范围内,则把数据保存到数据槽中,然后返回执行结果
  • 如果在自己管理的槽编号范围外,则会把数据发送给正确的节点,由正确的节点来把数据保存在对应的槽中

需要注意的是:Redis Cluster的节点之间会共享消息,每个节点都会知道是哪个节点负责哪个范围内的数据槽

虚拟槽分布方式中,由于每个节点管理一部分数据槽,数据保存到数据槽中。

当节点扩容或者缩容时,对数据槽进行重新分配迁移即可,数据不会丢失。

3个节点的Redis集群虚拟槽分片结果:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

虚拟槽分片特点:

虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。槽是集群内数据管理和迁移的基本单位。

槽的范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。

采用大范围槽的主要目的是为了方便数据拆分和集群扩展,每个节点会负责一定数量的槽。

Redis虚拟槽分区的优点:

  • 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。

  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。

  • 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

  • 无论数据规模大,还是小,Redis虚拟槽分区各个节点的负载,都会比较均衡 。而一致性哈希在大批量的数据场景下负载更加均衡,但是在数据规模小的场景下,会出现单位时间内某个节点完全空闲的情况出现。

Redis集群如何高可用

要实现Redis高可用,前提条件之一,是需要进行Redis的节点集群

集群的必要性

所谓的集群,就是通过添加服务节点的数量,不同的节点提供相同的服务,从而让服务器达到高可用、自动failover的状态。

面试题:单个redis节点,面临哪些问题?

答:

(1)单个redis存在不稳定性。当redis服务宕机了,就没有可用的服务了。

(2)单个redis的读写能力是有限的。单机的 redis,能够承载的 QPS 大概就在上万到几万不等。

对于缓存来说,一般都是用来支撑读高并发、高可用。

单个redis节点,二者都做不到。

Redis集群模式的分类,可以从下面角度来分:

  • 客户端分片
  • 代理分片
  • 服务端分片
  • 代理模式和服务端分片相结合的模式

客户端分片包括:

ShardedJedisPool

ShardedJedisPool是redis没有集群功能之前客户端实现的一个数据分布式方案,

使用shardedJedisPool实现redis集群部署,由于shardedJedisPool的原理是通过一致性哈希进行切片实现的,不同点key被分别分配到不同的redis实例上。

代理分片包括:

  • Codis
  • Twemproxy

服务端分片包括:

  • Redis Cluster

从否中心化来划分

它们还可以用是否中心化来划分

  • 无中心化的集群方案

其中客户端分片、Redis Cluster属于无中心化的集群方案

  • 中心化的集群方案

Codis、Tweproxy属于中心化的集群方案。

是否中心化是指客户端访问多个Redis节点时,是直接访问还是通过一个中间层Proxy来进行操作,直接访问的就属于无中心化的方案,通过中间层Proxy访问的就属于中心化的方案,它们有各自的优劣,下面分别来介绍。

如何学习redis集群

说明:

 (1)redis集群中,每一个redis称之为一个节点。
 (2)redis集群中,有两种类型的节点:主节点(master)、从节点(slave)。
  (3)redis集群,是基于redis主从复制实现。

Docker方式部署redis-cluster步骤

1、redis容器初始化
2、redis容器集群配置

这里引用了别人的一个镜像publicisworldwide/redis-cluster,方便快捷。

redis-cluster的节点端口共分为2种,

  • 一种是节点提供服务的端口,如6379、6001;

  • 一种是节点间通信的端口,固定格式为:10000+6379/10000+6001。

若不想使用host模式,也可以把network_mode去掉,但就要加ports映射。

这里使用host(主机)网络模式,把redis数据挂载到本机目录/data/redis/800*下。

Docker网络

Docker使用Linux桥接技术,在宿主机虚拟一个Docker容器网桥(docker0),Docker启动一个容器时会根据Docker网桥的网段分配给容器一个IP地址,称为Container-IP,同时Docker网桥是每个容器的默认网关。

因为在同一宿主机内的容器都接入同一个网桥,这样容器之间就能够通过容器的Container-IP直接通信。

Docker网桥是宿主机虚拟出来的,并不是真实存在的网络设备,外部网络是无法寻址到的,这也意味着外部网络无法通过直接Container-IP访问到容器。

如果容器希望外部访问能够访问到,可以通过映射容器端口到宿主主机(端口映射),即docker run创建容器时候通过 -p 或 -P 参数来启用,访问容器的时候就通过[宿主机IP]:[容器端口]访问容器。

Docker容器的四类网络模式

Docker网络模式配置说明
host模式–net=host容器和宿主机共享Network namespace。
container模式–net=container:NAME_or_ID容器和另外一个容器共享Network namespace。 kubernetes中的pod就是多个容器共享一个Network namespace。
none模式–net=none容器有独立的Network namespace,但并没有对其进行任何网络设置,如分配veth pair 和网桥连接,配置IP等。
bridge模式–net=bridge(默认为该模式)

桥接模式(default)

Docker容器的默认网络模式为桥接模式,如图所示:

Docker安装时会创建一个名为docker0的bridge虚拟网桥

bridge模式是docker的默认网络模式,不写–net参数,就是bridge模式。新创建的容器都会自动连接到这个虚拟网桥。

bridge网桥用于同一主机上的docker容器相互通信,连接到同一个网桥的docker容器可以相互通信。

bridge 对宿主机来讲相当于一个单独的网卡设备 ,对于运行在宿主机上的每个容器来说相当于一个交换机,所有容器的虚拟网线的一端都连接到docker0上。

容器通过本地主机进行上网,容器会创建名为veth的虚拟网卡,网卡一端连接到docker0网桥,另一端连接容器,容器就可以通过网桥通过分配的IP地址进行上网。

docker exec -it rmqbroker-a cat /etc/hosts

[root@localhost ~]# docker exec -it rmqbroker-a cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.30.0.5      c55ea6edcc14

使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。

可以使用iptables -t nat -vnL查看。

 pkts bytes target     prot opt in     out     source               destination
15141  908K RETURN     all  --  br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0
 536K   32M RETURN     all  --  br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0
    0     0 RETURN     all  --  br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0
    0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0
   11   572 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:3306 to:172.19.0.2:3306
    0     0 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0  

以上是关于1000W用户1Wqps高并发签到系统的架构和实操的主要内容,如果未能解决你的问题,请参考以下文章

阿里面试官说: 请你设计一个支撑1000W并发的系统?

3W字吃透:微服务 sentinel 限流 底层原理和实操

1W字长文:K8S Ingress 原理和实操

3W字吃透:微服务网关SpringCloud gateway底层原理和实操

redis cluster 集群 HA 原理和实操(史上最全面试必备)

K8S学习圣经6:资源控制+SpringCloud动态扩容原理和实操