高并发系统设计学习笔记
高并发系统设计的三大目标:高性能、高可用、可扩展
性能的度量指标
一般来说,度量性能的指标是系统接口的响应时间:
平均值
平均值是把这段时间所有请求的响应时间数据相加,再除以总请求数。
最大值
这段时间内所有请求响应时间最长的值,但它的问题又在于过于敏感了。
分位值
以 90 分位为例,我们把这段时间请求的响应时间从小到大排序,假如一共有 100 个请求,那么排在第 90 位的响应时间就是90 分位值。 分位值是最适合作为时间段内,响应时间统计值来使用的,在实际工作中也应用最多。
我们通常使用吞吐量或者同时在线用户数来度量并发和流量
如我们设立性能优化的目标时通常会这样表述:在每秒 1 万次的请求量下,响应时间 99 分位值在 10ms 以下。
系统怎样做到高可用?
高可用性(High Availability,HA)
可用性的度量
MTBF(Mean Time Between Failure)是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。
MTTR(Mean Time To Repair)表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。
Availability = MTBF / (MTBF + MTTR)
数据库篇
池化技术
线程池、数据库连接池
主从读写分离
主从复制
MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上二进制日志文件。主从复制就是将 binlog 中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待 binlog 同步的完成。
首先从库在连接到主节点时会创建一个 IO 线程,用以请求主库更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件中,而主库也会创建一个 log dump 线程来发送 binlog 给从库;同时,从库还会创建一个 SQL 线程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。这是一种比较常见的主从复制方式。
除了带来了部署上的复杂度,还有就是会带来一定的主从同步的延迟,这种延迟有时候会对业务产生一定的影响。
如何访问数据库
中间件: TDDL
单独部署的代理层: 期阿里巴巴开源的Cobar,基于 Cobar 开发出来的 Mycat,360 开源的 Atlas,美团开源的基于 Atlas 开发的 DBProxy 等等
分库分表
分库分表是一种常见的将数据分片的方式,它的基本思想是依照某一种策略将数据尽量平均的分配到多个数据库节点或者多个表中。 数据库分库分表的方式有两种:一种是垂直拆分,另一种是水平拆分。
如何对数据库做垂直拆分
垂直拆分,顾名思义就是对数据库竖着拆分,也就是将数据库的表拆分到多个不同的数据库中。垂直拆分的原则一般是按照业务类型来拆分,核心思想是专库专用,将业务耦合度比较高的表拆分到单独的库中。
如何对数据库做水平拆分
水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点。
- 按照某一个字段的哈希值做拆分,这种拆分规则比较适用于实体表,比如说用户表,内容表,我们一般按照这些实体表的 ID 字段来拆分。
- 另一种比较常用的是按照某一个字段的区间来拆分,比较常用的是时间字段。你知道在内容表里面有“创建时间”的字段,而我们也是按照时间来查看一个人发布的内容。我们可能会要看昨天的内容,也可能会看一个月前发布的内容,这时就可以按照创建时间的区间来分库分表,比如说可以把一个月的数据放入一张表中,这样在查询时就可以根据创建时间先定位数据存储在哪个表里面,再按照查询条件来查询。
分库分表引入的问题
如何保证分库分表后ID的全局唯一性?
使用业务字段作为主键,比如说对于用户表来说,可以使用手机号,email 或者身份证号作为主键。
使用生成的唯一 ID 作为主键。
snowflake 雪花算法
可在分布式环境下用于生成唯一ID的算法。该算法生成的是一个64位的ID,故在Java下正好可以通过8字节的long类型存放。所生成的ID结构如下所示
41位长度的时间戳可以保证使用69年 最低的12位为序列号,可用于标识、区分同一个计算机在相同毫秒时间内的生产的ID
缓存篇
缓存使用策略
cache aside
Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。
Read/Write Through(读穿 / 写穿)策略
Write Through 的策略是这样的:先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)”。
Write Back(写回)策略
这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中。
分布式缓存高可用
客户端:缓存数据如何分片
Hash 分片算法
一致性 Hash 算法
代理层
服务端方案
redis sentinel 所有缓存的读写请求都是经过代理层完成的。代理层是无状态的,主要负责读写请求的路由功能,并且在其中内置了一些高可用的逻辑,不同的开源中间代理层方案中使用的高可用策略各有不同。比如在 Twemproxy 中,Proxy 保证在某一个
Redis 节点挂掉之后会把它从集群中移除,后续的请求将由其他节点来完成;而 Codis 的实现略复杂,它提供了一个叫 Codis Ha 的工具来实现自动从节点提主节点,在 3.2 版本之后换做了 Redis Sentinel 方式,从而实现 Redis 节点的高可用。
缓存穿透
缓存穿透其实是指从缓存中没有查到数据,而不得不从后端系统(比如数据库)中查询的情况。
回种空值
当我们从数据库中查询到空值或者发生异常时,我们可以向缓存中回种一个空值。但是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个比较短的过期时间,让空值在短时间之内能够快速过期淘汰。回种空值虽然能够阻挡大量穿透的请求,但如果有大量获取未注册用户信息的请求,缓存内就会有有大量的空值缓存,也就会浪费缓存的存储空间,如果缓存空间被占满了,还会剔除掉一些已经被缓存的用户信息反而会造成缓存命中率的下降。 是一种最常见的解决思路,实现起来也最简单,如果评估空值缓存占据的缓存空间可以接受,那么可以优先使用这种方案;
布隆过滤器
布隆过滤器会引入一个新的组件,也会引入一些开发上的复杂度和运维上的成本。所以只有在存在海量查询数据库中,不存在数据的请求时才会使用,在使用时也要关注布隆过滤 器对内存空间的消耗;
设置分布式锁
通过在 Memcached 或者 Redis 中设置分布式锁,只有获取到锁的请求才能够穿透到数据库。 对于极热点缓存数据穿透造成的“狗桩效应”,可以通过设置分布式锁或者后台线程定时加载的方式来解决。
CDN加速
CDN(Content Delivery Network/Content Distribution Network,内容分发网络)。
消息队列篇
秒杀场景
削峰填谷
将秒杀请求暂存在消息队列中,然后业务服务器会响应用户“秒杀结果正在计算中”,释放了系统资源之后再处理其它用户的请求
通过异步处理简化秒杀请求中的业务流程
服务解耦合
分布式服务篇
RPC框架
的序列化备选方案主要有以下几种:
- JSON、XML
- Thrift、Protobuf,需要引入IDL文件
Thrift 是 Facebook 开源的高性能的序列化协议,也是一个轻量级的 RPC 框架;
Protobuf 是谷歌开源的序列化协议。它们的共同特点是,无论在空间上还是时间上都有着很高的性能,缺点就是由于 IDL 存在带来一些使用上的不方便。
注册中心
负载均衡
LVS:四层负载 NGINX:七层负载均衡
分库分表->缓存->消息队列->微服务化