商品详情页系统 -- day07 redis-cluster、亿级流量系统架构、数据库+缓存双写不一致解决、安装mysql

一、redis在读写分离、高可用架构下进行横向扩容支持1T+海量数据

1、单master在海量数据存储下的瓶颈

        当架构中只有1个master和2个slave节点,此时,master节点的数据和slave节点的数据是一样的,slave节点的数据容量大小取决于master节点的内存大小,当master节点中数据达到内存的一定比例时,就会根据redis的内存清理算法,自动清除一些不太常用的数据,来保证内存中的数据最大大小,此时,如果大量请求一些不太常用的数据内容,可能会造成redis穿透的问题。这些是单master在海里数据存储下的瓶颈。

2、如何突破单master瓶颈,让架构支撑1T+海量数据

使用redis cluster进行master的横向扩展,支撑多个master node节点,每个master node 可以挂在多个slave节点。

3、redis-cluster的基本介绍

        自动将数据进行分片,每个master节点上存放一部分数据。

        提供内置的高可用支持,部分master不可用时,整个redis-culster还是可以继续使用的。

        在redis-culster架构下,每个redis要开放两个端口号,比如一个是6379,另外一个是加上10000的端口如16379,16379是用来进行节点间通信的,也就是cluster bus(集群总线)的东西,cluster bus是用来进行故障检测、配置更新以及故障转移授权等,culster bus用了另一种二进制的协议,主要用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

4、redis cluster的hash slot算法

        redis cluster有固定的16384个hash slot,请求redis存储数据时,hash slot算法会对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot,redis cluster会根据取模后得到的相应的值去跟16384个hash slot值进行比对,并且将要此次key对应的value存储到对应的hash slot所在的master节点上去。

        redis cluster中每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot,hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去。

        移动hash slot的成本是非常低的

        客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现

二、redis-cluster的核心原理分析

1、基础通信原理

(1)、redis cluster节点间采用gossip协议进行通信

        集中式:元数据的更新和读取时效性非常好,一旦元数据出现了变更,立即就能更新到集中式的存储中,其他节点读取的时候立即就可以感知到,弊端在于所有的元数据的更新压力都会集中到一个地方,可能导致元数据的存储有压力。

        gossip:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有的节点上去更新,有一定的延时,但是降低了服务器的更新压力。弊端在于元数据的更新有一定的延时,可能会导致集群的一些操作有一定的滞后。

        跟集中式不同,redis-cluster各个节点的通信不是将集群的元数据(节点信息、故障等)集中存储到某个节点上,而是相互之间不断通信,保持整个集群所有节点的数据是完整的。redis-cluster维护集群的元数据采用的是一种叫做gossip的协议。

(2)、10000端口

每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000(例如:redis本身对外端口号是6379,那么它的通信端口是16379)。每隔一定时间,节点就会通过该端口号向其他几个节点发送ping消息,同时其他几个节点接收到ping消息之后返回pong。

2、gossip协议

        gossip协议包含了多种消息,如:ping、pong、meet、fail等。

        meet:某个节点发送meet给新加入的节点,让新加入的节点加入集群中,然后新节点就会开始与其他节点进行通信。

        ping:每个节点都会频繁的给其他节点发送ping,其中包含了自己的状态还有自己维护的集群元数据,互相通过ping交流数据。

        pong:返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新。

        fail:某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点指定的节点宕机了。

3、ping消息深入

        ping的动作很频繁,而且要携带一定的元数据,所以就可能加重网络的负担,因此,每个节点每秒会执行10ping的操作,每次会选择5个最久没有通信的节点。如果发现某个节点的通信延时达到了cluster_node_timeout / 2,那么会立即发送ping,避免数据交换的延时过长。cluster_node_timeout可以调节,如果调节的较大,就会降低发送的频率。

三、redis-cluster环境搭建

1、配置前说明

        redis cluster集群至少需要3个master去组成一个高可用的健壮的分布式集群,每个master节点都建议至少给一个slave节点,一共是3个master和3个slave,正式环境中至少需要在6台主机上去搭建环境,课程中,使用3台主机去搭建环境,即:一台主机上1主1从redis实例。

2、将三台主机上的redis实例和sentinel进程全部停止

3、三台主机上分别配置redis实例的配置文件(7001~7006)

(1)、分别在三台主机上创建必须要的文件夹

# 维护redis集群配置的文件夹
mkdir -p /etc/redis-cluster

# 存放redis实例运行日志
mkdir -p /var/log/redis

# 配置redis缓存数据目录
mkdir -p /var/redis/7001

(2)、准备6分redis实例配置文件(以128主机为例,其他主机同样操作)

分别拷贝两份/usr/local/redis-3.2.8目录下的redis.conf配置文件到/etc/redis目录中,改名为7001.conf和7002.conf,并修改如下配置:

# 绑定主机ip
bind 192.168.20.128

# 修改端口号,按照实际要配置的端口号修改
port 7001

# 配置redis实例后台启动
daemonize yes

# 配置pid文件位置,redis_端口号.pid按照对应的端口号配置
pidfile /var/run/redis_7001.pid

# 配置redis日志文件位置, 按照对应的端口号配置日志文件
logfile "/var/log/redis/7001.log"

# 配置redis缓存数据目录,按照对应的端口号配置
dir /var/redis/7001

# 配置追加信息
appendonly yes

# 开启集群配置
cluster-enabled yes

# 配置集群配置文件位置,node-端口号.conf按照对应的端口号写
cluster-config-file /etc/redis-cluster/node-7001.conf

# 配置节点默认超时时间
cluster-node-timeout 15000

129和130主机上重复以上步骤配置。

4、三台主机上配置redis启动脚本

/usr/local/redis-3.2.8/utils目录下的redis_init_script启动脚本拷贝到/etc/init.d目录下并更名为redis_7001redis7002,并对启动脚本进行如下配置:

7002以及129/130主机上的启动脚本配置步骤同上。

5、创建集群

在128上安装ruby和rubygems

yum install -y ruby

yum install -y rubygems

拷贝redis-trib.rb/usr/local/bin目录下,并输入命令创建集群

# replicas : 代表每个master下挂在的slave node的个数
redis-trib.rb create --replicas 1 192.168.20.128:7001 192.168.20.128:7002 192.168.20.129:7003 192.168.20.129:7004 192.168.20.130:7005 192.168.20.130:7006

查看集群状态:

四、redis-cluster通过master水平扩容来支撑更高的读写吞吐+海量数据存储

1、添加新的master实例

(1)、以130主机上进行添加master节点为例

需要7007.conf配置文件和redis_7007启动脚本,具体设置参考redis-cluster集群搭建

启动redis实例

(2)、128主机上执行redis-trib.tb添加集群节点

(3)、查看节点信息

(4)、reshard一些数据去新添加的节点

resharding的意思是把一部分hash slot从一些node上迁移到另外一些node上

注意:4096代表本次操作要移动多少hash slot,源master节点有三个时,会将从三个节点平均分离4096个hash slot挪入目标节点中。

(5)、再次查看节点信息

2、添加新的slave node节点

(1)、在130主机上添加slave节点,端口号为7008,配置文件和启动脚本参考之前实例配置

(2)、128主机上使用redis-trib.rb脚本进行挂在操作

此处以把7008 slave node 节点挂在到7005 master node上为例

redis-trib.rb add-node --slave --master-id master节点的ID 从节点ip:端口号 任意master节点:端口号

(3)、查看所有节点状态

3、删除master节点

(1)、使用reshard命令将hash slot从7007平均分配给其他三个master节点上

# 将7007上的hash slot平均分配给其他三个子节点,需要进行三次操作
redis-trib.rb reshard 任意master节点ip:端口号

(2)、删除7008 master node

# 删除7007 master节点
redis-trib.rb del--node 192.168.20.130:7007 master节点的ID

4、redis的slave node 的自动迁移

如果某个master节点的slave node挂掉了,那么redis cluster会自动迁移一个冗余的slave(如一个master node中有两个以及以上个数的slave node,则该slave node就被视为冗余slave)给对应的master node。

五、面向集群的jedis内部实现原理

1、基于重定向的客户端

(1)、请求重定向

        用户在使用redis客户端的时候,可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令都会计算对应的key 的hash slot,如果在本机,就直接处理,如果是其他的主机,就需要让客户端进行重定向。使用命令:cluster keyslot mykey可以查看mykey对应的hash slot值。使用命令:redis-cli 登录redis-cluster集群客户端的时候,如果加上参数: -c,则在集群底层自动调用重定向的方法来存储数据(在存储数据是,redis-cluster会根据key计算出对应的hash slot,如果没有-c参数,需要手动切换到hash slot对应的主机上进行数据的存储)。

(2)、计算hash slot

计算hash slot的算法,就是根据key计算CRC16的值,然后对16384取模,拿到对应的hash slot,用hash tag可以手动指定key对应的hash slot,同一个hash tag下的key都会在一个hash slot中,例如使用命令:set mykey1:{100}  和 set mykey2:{100}

(3)、hash slot查找

节点间通过gossip协议进行数据交换,就知道每个hash slot在哪个节点上了

2、smart jedis

(1)、什么是smart jedis

        基于重定向的客户端很消耗网络的IO,因为大部分的情况下都会出现一次重定向操作才能找到正确的节点,所以大部分的客户端,比如java的Jedis就是属于smart jedis。

(2)、jedisCluster的工作原理

        在jediscluster初始化的时候,就会随机的选择一个node,初始化hash slot  ->  node 映射表,同时为每个节点创建一个对应的JedisPool连接池。每次基于JedisCluster执行操作,首先jediscluster都会在本地计算key的hash slot,然后在本地映射表找到对应的节点。如果当前node正好持有对应的hash slot,则直接进行数据操作,如果没有,则返回moved。在JedisedisCluster的API中如果发现返回值为moved,就会利用该节点的元数据更新本地的hash slot  ->  node映射表缓存。

        重复上面操作,知道找到对应的节点,如果重试超过5次,那么就报错,JedisClusterMaxRedirectionException。

(3)、hash slot迁移和ask重定向

如果hash slot正在迁移,那么会返回ask重定向给jedis。jedis接收到ask重定向之后,会重新定位到目标节点去执行,但是因为ask发生在hash slot迁移过程中,所以JedisCluster API收到ask是不会更新hashslot本地缓存。已经可以确定说,hashslot已经迁移完了,moved是会更新本地hashslot->node映射表缓存的。

六、高可用性与主备切换原理

1、判断节点宕机

2、从节点过滤

3、从节点选举

4、与哨兵比较

七、亿级流量架构

1、架构图

2、每层存在的意义

        nginx本地缓存,抗的是热数据的高并发访问,一般来说,商品的购买总是有热点的。这些热数据,利用nginx本地缓存,由于经常被访问,所以可以被锁定在nginx的本地缓存内。大量的热数据的访问,就是经常会访问的那些数据,就会被保留在nginx本地缓存内,那么对这些热数据的大量访问,就直接走nginx就可以了,不需要走后续的各种网络开销了。nginx本地内存有限,也就能cache住部分热数据,除了各种iphone、nike等热数据,其他相对不那么热的数据,可能流量会经常走到redis那里

        redis分布式大规模缓存,抗的是很高的离散访问,支撑海量的数据,高并发的访问,高可用的服务。redis缓存最大量的数据,最完整的数据和缓存,1T+数据; 支撑高并发的访问,QPS最高到几十万; 可用性,非常好,提供非常稳定的服务。利用redis cluster的多master写入,横向扩容,1T+以上海量数据支持,几十万的读写QPS,99.99%高可用性,那么就可以抗住大量的离散访问请求。

        tomcat jvm堆内存缓存,主要是抗redis大规模灾难的,如果redis出现了大规模的宕机,导致nginx大量流量直接涌入数据生产服务,那么最后的tomcat堆内存缓存至少可以再抗一下,不至于让数据库直接裸奔。同时tomcat jvm堆内存缓存,也可以抗住redis没有cache住的最后那少量的部分缓存。

八、Cache Aisde Pattern数据库读写模式

1、什么是Cache Aisde Pattern

        读数据的时候,先读缓存,缓存没有的话,再去进行数据库查询,然后取出数据,放入缓存中,同时返回响应。

        更新数据的时候,先删除缓存,然后更新数据库。

2、为什么是删除缓存,而不是更新缓存

        很多时候,复杂的缓存场景中,缓存的数据不单单是从数据库中直接查询出来的数据,往往需要加入一些复杂的运算。因此,如果只是单单更新缓存中的某一部分内容,可能还需要关联查询数据库中的其他表的数据,然后进行计算,最后将计算的结果缓存到redis中,这样的代价是很高的。如果是删除缓存,在一定时间内,缓存只是从新计算一次而已,网络开销会大大降低。删除缓存其实是懒加载的思想,是在更新的时候删除缓存,第一次查询的时候才用到计算,而不是每次更新都会进行大量的计算。

九、高并发场景下的缓存+数据库双写不一致问题的分析与解决

1、最初级的缓存不一致问题以及解决方案

        问题:先修改数据库,再删除缓存,如果删除缓存失败了,会导致数据库中的数据是新数据,而缓存中的数据是旧的数据,导致数据不一致。

        解决思路:先删除缓存,再修改数据库,如果缓存删除成功了,而修改数据库失败了,那么数据库中的数据是旧的缓存中是空的,而在此查询缓存的时候,会重新重数据库中查询数据,放入缓存中。

2、比较复杂一点的数据不一致问题分析

        数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没有修改,一个新的请求发送过来,读取缓存,而缓存是空的,会重新查询数据库,查询到了修改之前的数据,放入缓存中,此时数据库的修改操作完成,数据库中的数据变为了最新的数据,导致数据库和缓存中的数据不一致。

3、什么情况下会出现复杂的数据不一致问题?

        只有在对一个数据进行并发的读写的时候,才会出现复杂的数据不一致的情况。如果一个数据的读写每天上亿次,每秒并发几万QPS,就可能出现上述的数据库+缓存双读写数据不一致的问题。

4、解决复杂情况下数据不一致问题:数据库与缓存与读写操作进行串行化

        更新数据库的时候,根据数据的唯一标识,将操作路由之后发送到一个jvm内部的队列中,读取数据的时候,如果发现数据不在缓存中,那么将重新从数据库读取数据+写入(更新)缓存的操作根据唯一标识路由之后也放入jvm的内部队列中。

        一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行,这样的话,一个数据的变更操作,先执行删除缓存的操作,然后再去更新数据库,在没有更新完成时有一个请求过来的话,读到空的缓存,此时可以先将缓存更新的请求发送到队列中,此时会在队列中积压请求,等待数据库更新完成之后再执行队列中的查询数据库并且更新到缓存的操作。

        在没有更新数据库完成时,如果有多个请求查询到空的缓存,查询数据库的请求会有多条需要进入到队列中,此时可以对多条请求进行判断,如果是对同一条数据进行的查询请求,可以只在队列中放入一条查询请求,直接等待之前的队列任务完成之后执行相同的查询请求即可。而其他相同的请求,如果还在请求时间内,会不断地进行轮询查询,就可以从缓存中或得到最新的数据,如果超过请求时间,那么这一次直接从数据库中查询旧的数据并返回。

5、高并发的场景下,该解决方案注意的问题

(1)、读请求长时阻塞

       由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。

        该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。因此,务必通过一些模拟真实的测试,看看更新数据的频繁是怎样的。另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。

        如果一个内存队列里居然会挤压100个商品的库存修改操作,每隔库存修改操作要耗费10ms区完成,那么最后一个商品的读请求,可能等待10 * 100 = 1000ms = 1s后,才能得到数据,这个时候就导致读请求的长时阻塞。

        一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会hang多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的。

        如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。

(2)、读请求并发量过高

        必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值。但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大,按1:99的比例计算读和写的请求,每秒5万的读QPS,可能只有500次更新操作。

        如果一秒有500的写QPS,那么要测算好,可能写操作影响的数据有500条,这500条数据在缓存中失效后,可能导致多少读请求,发送读请求到库存服务来,要求更新缓存。

        一般来说,1:1,1:2,1:3,每秒钟有1000个读请求,会hang在库存服务上,每个读请求最多hang多少时间,200ms就会返回,在同一时间最多hang住的可能也就是单机200个读请求,同时hang住单机hang200个读请求,还是可以的。

(3)、多服务实例部署的请求路由

        可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上。

(4)热点商品的路由问题,导致请求的倾斜

        万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大,就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大。

十、cache-04中安装MySQL

# 安装MySQL
yum install -y mysql-server

# 启动MySQL服务
service mysqld start

# 设置开机启动
chkconfig mysqld on

# 安装mysql的java客户端驱动
yum install -y mysql-connector-java

猜你喜欢

转载自blog.csdn.net/wingzhezhe/article/details/82082014