在现实开发的过程中,遇到的高并发场景大概就是如下几种情况:
高可用场景 | 技术 | 具体场景 |
---|---|---|
少读少写 | MySQL 主从热备 | 历史学生数据 |
多读少写 | MySQL 读写分离 + Redis 集群缓存 | 热点文章 |
少读多写 | Redis 集群持久化 | 日志 |
海量数据读写 | ES 集群 | 点赞记录 |
强一致性读写 | 消息队列 + Redis 原子操作 | 商品库存 |
突发性高并发读写 | 限流 + Redis 预储 | 票务系统 |
分布式 + 缓存 = 高性能高可用杀器,通过这个公式就可以干掉大多数的需求了。
一个经典的数据库交互如下,客户端发起一个数据库请求,数据库返回响应。
随着流量的增多,客户端对数据库的并发访问也会急剧增加。单机数据库往往无法承受如此高并发的请求访问,会大大增加数据库服务器故障的概率。
既然单机无法承受高并发访问,那么我们就多弄几台服务器进行水平拓展,将数据分布在不同的数据库服务器上,分库分表,减轻单机的负载,分散并发请求。
但同样的,随着流量的提升,分库分表依旧无法解决流量的增长,此时我们需要再次对数据库进行扩容,这会遇到几个问题:
1. 首先传统数据库的扩展性非常差,由于水平拓展可能会影响数据的归属,导致了进行水平扩展需要进行数据的迁移,而海量数据的迁移是十分耗时的。
2. 其次客户端是通过算法计算数据归属于哪个数据库,水平扩展需要改变客户端对数据分发的算法。
3. 再者这样的集群是同样无法接受数据库服务器的故障的,万一其中一个数据库宕机了,那么有一部分数据就无法进行访问。
首先是问题1,既然水平扩容会遇到很多问题,那么我们可以进行垂直扩容,让同样的数据拥有多个在不同数据库上的副本。选出一个数据库作为 Master 节点,用于处理写请求;其他的数据库作为 Slave 节点,用于处理读请求。
主从数据库之间的数据同步是通过主从热备的方式实现的。
Mysql 主从热备是通过 Master 节点执行写操作时,产生的
SQL log
二进制文件实现的。Master 定时将该文件发送到 Slave 上,Slave 根据文件进行数据同步。这个操作是异步的,同时不会阻塞 Master 的任何操作,所以会带有稍微的延迟,属于最终一致性。
但存在一个场景,很有可能我的一个请求包含跨库跨表的请求。比如排序请求,客户端很可能需要轮询访问所有的数据库节点,然后进行排序等处理后再返回。这样给程序设计带来了很大的困难,也让代码的复杂性提高。
这时候我们就需要考虑到使用 Mycat 来解决这类问题。Mycat 本身不存储数据,但是能让它能作为一个协调点,让数据库集群像一个数据库一样操作。
同时 MyCat 支持故障转移,当 Master 节点故障时,Master 节点迅速选举出新的 Master 节点,将写请求转移到上面即可。
当然为了保证 MyCat 的可用性,不可能使用单点部署,又需要一个 MyCat 集群,这样增加了成本。
再者分库分表也并不是适合任何的场景,有时候会给数据库设计带来巨大的困难:
为了避免跨库查询,分库分表需要考虑外键关联的数据要在一个数据库中。比如:
学生的课程成绩要和学生在同一个数据库内(计算学生总分)。
一个课程的所有课程成绩需要在同一个数据库内(计算课程平均分)。
综合两者,则选择了该课程的所有学生都要在同一个数据库内。
假设:学数学的同学需要学英语,学美术的同学需要学英语。
那么,数学、英语、美术的成绩都需要在同一个数据库中,即使上数学课的同学和上美术课的同学在该例中没有任何的交集。
假设这样的问题导致了 50 门课程的 5 万名学生需要在同一个数据库,那么课程成绩表就会存在 250 万条记录。
解决这个问题的方法也很简单,那就是去规范化,增加数据冗余。
比如在课程成绩录入的时候,就将课程平均分和课程人数计算好,存储在课程表中。然后再对课程成绩表中的课程名称进行冗余。
因为课程平均分是相对静态的,因为学生个数固定,即使学生分数发生变化,也能通过算法修改平均分。当然学生分数发生变化也是小概率事件。
然后再对课程成绩表中的课程名称进行冗余。
通过这样的去规范化,就可以解除课程与学生的强耦合。
那学生和课程成绩可以解耦合吗?
不行,由于学生平均分是相对动态的,因为老师录入成绩的时间不是统一的。平均分与当前录入门数和当前总分有关,需要实时计算,所以课程成绩和学生需要在同一个数据库内。
至此,一个数据库内只包含学生和学生每门课程的成绩,分库起来就比较方便,能够有效分散课程成绩表的数据。
并不是所有的数据都像上例一样友好,也许数据的变化是每时每刻的。
比如经典的(用户、点赞、作品)模型,要计算一个用户点了多少赞,一个作品拥有多少个赞,这类数据的变化是非常常见的,而且存在热点效应。
热点效应:数据在某段时间内的变化和访问频率非常高。过了这段时间后,数据就趋于静态。
数据的变化非常频繁,不仅不利于分库分表,而且对数据库的压力也会很大。但是不进行分库分表,用户点赞表轻而易举就能达到 PB 级别,这样传统数据库就无法支持这样的场景了。
elastic search
的几个特性使得其可以满足上述场景:
- 分布式
- 去中心化
- 近实时
- 易于聚合
- 乐观并发控制
- 可故障转移
我们可以将点赞数据存储在 elastic search
中,elastic search
会将数据分散到不同的 shard
上;查询时,elastic search
会随机选举出一个协调节点,请求所有 shard
上的数据,然后聚合后返回给客户端。这对用户来说是隐式的。
由于 elastic search
需要进行主从的数据同步,以确保 es 的可用性,所以是 近实时的
。
对于热点数据,我们可能需要频繁重复的执行复杂的聚合操作,使将聚合结果存在 缓存
中,可以极大的提高性能。客户端在请求数据库数据之前,会先访问缓存,如果缓存中有这条数据,则直接获取。如果不存在则请求数据库后,将结果写入缓存中。
以 Redis
为例,非传统数据库通常有着这样的三个特性:
- 性能高、吞吐量大
- 不关注事务
- 可扩展性
但是单点部署的 Redis
是无法保证可用性的,因为 Redis
是基于内存的。当 Redis
服务器宕机时,缓存在 Redis
的数据就会丢失。
即使我们立刻重启 Redis
,其中的缓存也全部丢失,那么就会造成 缓存雪崩
,大量的并发直接打到数据库上,会使得数据库服务器很快就崩溃。
持久化
的出现为的就是解决上述问题,持久化一般存在两种方式:
其一,客户端定时任务主动将数据持久化到 MySQL
;
其二,使用 Redis
内置的持久化功能 RDB
或 AOF
。
RDB
定时给整个数据库生成一个快照,可以指定在一定时间范围内变化了多少数据执行快照的覆盖。
AOF
将每条写入命令作为日志记录到aof
文件中。每次命令都立刻写入磁盘,这会带来很大的磁盘压力。为了保证性能,会先写入
os cache
中,然后定期强制执行fsync
操作将数据刷入磁盘。
aof
文件是不断膨胀的,而 Redis 内存不足是会淘汰数据的,当aof
文件大于 内存中的数据时,就会执行rewrite
操作,基于当前数据重新构造aof
文件。
另一个方法就是使用 Redis 集群,使用 Redis 集群不仅可以提高并发,而且还可以借助 sentinel 组件实现主从切换故障转移。
和 MySQL 类似,使用主从集群需要进行主从复制,而读写分离往往也是主从集群模式下的一个重要功能。
Redis 的可扩展性也可以满足数据增长的需要。
Redis 的可扩展性可以通过 Redis 自带的 slot 方式实现。
- 他通过数据的 key 计算数据归属于哪个 slot,一台 redis 服务器可以拥有多个 slot。
另一种方式是通过 client 实现一致性哈希算法来计算数据归属。
但是 Redis Master 节点宕机之后,需要缓存的信息可能就无法进行缓存,已经缓存的信息可能也无法及时得到修改,会导致数据的不一致性和数据库并发访问的增加。
Sentinel 组件是 Redis 自带的用于实现主从切换故障转移的组件。
当一个 Sentinel
认为 Master 节点故障时,Sentinel
就认为 Master subjective down
了,此时还不能作为主从切换的标准,因为网络传输是不可靠的。
直到当 quorum
数量的 Sentinel
认为 Master 节点故障时,Sentinel
才会认为 Master 真的故障了,为 objective down
状态。
当一个 Master 被认为 objective down
时,就会从剩下的 Sentinel
中选举出一个进行故障转移,但需要多数(majority) Sentinel
来允许主从切换执行。
选举算法会考虑 slave 的一些信息,按先后顺序:
- 同 master 断开连接的时长(超过 down-after-millisecond 的 10 倍)
- slave 优先级(按配置文件排序,默认 100,越低越高)
- 复制 offset(slave 复制的数据越多,offset 越靠后,优先级越高)
- run id(同条件下选择 id 小的 slave)
切换完成后,会通过 pub/sub 消息机制对配置进行传播,这是通过版本号大小来更新 master 配置的
但即使 Redis 拥有很多的机制保证其可用性,但是还是存在数据丢失的情况。
1. 异步复制导致的数据丢失:部分数据还未复制到 slave 中 master 就宕机了。
2. 其中一个 master 并未宕机,只是存在忘了故障,此时 sentinel 启动了故障转移将 slave 提升成为 master,此时就存在两个 master 了。当我们恢复时,不管是哪台服务器作为 master,我们都无法恢复另一台服务器脑裂期间的数据。
数据丢失的问题在高可用系统中经常存在,需要做的是保证高可用的情况下,减少数据的丢失。
redis 主要通过两个配置来解决这种问题:
min-slaves-to-write 1
min-slaves-max-lag 10
上述配置表示:要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。
如果超过 1 个 slave,数据复制和同步的延迟都超过了 10 秒,这时候 master 就不会再接受任何请求了。
这样就算脑裂了,也最多只能有 10 秒的数据丢失。
当然,在 master 拒绝请求的时候,client 需要对这种异常做出响应,记录日志后直接返回或再重试。
综上所述,es 作为分布式搜索引擎,很适合做数据的去规范化。
而针对海量数据 + 高并发 + 高可用的场景,redis cluster 可以很好的减少数据库的负担。
但对于一些强一致性的请求,且对响应时间容忍度小的场景,可以对这类请求进行限流,将这类请求写入消息队列中。
比如商品出售,商品库存要求强一致性。在这种场景下,我们为确保数据一致性,不要求每次请求都能成功。可能会将其写入消息队列中,然后对请求进行限流消费。请求之后会立刻对用户进行响应,并轮询请求结果。
当请求超时时,这类请求会自动进入死信队列,而不会被消费。
进入死信队列的请求,会有专门的服务器进行处理,例如告知用户请求失败。