CodeGroK

马上开悟

什么是缓存

缓存,是一种存储数据的组件,它的作用是让对数据的请求更快地返回。凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。

常见硬件组件的延时情况
缓存
从这些数据中,你可以看到,做一次内存寻址大概需要 100ns,而做一次磁盘的查找则需要 10ms。如果我们将做一次内存寻址的时间类比为一个课间,那么做一次磁盘查找相当于度过了大学的一个学期。可见,我们使用内存作为缓存的存储介质相比于以磁盘作为主要存储介质的数据库来说,性能上会提高多个数量级,同时也能够支撑更高的并发量。所以,内存是最常见的一种缓存数据的介质。

缓存分类

在我们日常开发中,常见的缓存主要就是分布式缓存、热点本地缓存、静态缓存这几种。

分布式缓存

分布式缓存的大名可谓是如雷贯耳了,我们平时耳熟能详的 Memcached、Redis 就是分布式缓存的典型例子。它们性能强劲,通过一些分布式的方案组成集群可以突破单机的限制。所以在整体架构中,分布式缓存承担着非常重要的角色,后边细谈。

热点本地缓存

当我们遇到极端的热点数据查询的时候。热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。如 HashMap,Guava Cache 或者是 Ehcache 等,它们和应用程序部署在同一个进程中,优势是不需要跨网络调度,速度极快,所以可以用来阻挡短时间内的热点查询。
Guava 的 Loading Cache代码样例:

1
2
3
4
5
6
7
8
9
CacheBuilder<String, List<Product>> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize).recordStats(); //设置缓存最大值
cacheBuilder = cacheBuilder.refreshAfterWrite(30, TimeUnit.Seconds); //设置刷新间隔

LoadingCache<String, List<Product>> cache = cacheBuilder.build(new CacheLoader<String, List<Product>>() {
@Override
public List<Product> load(String k) throws Exception {
return productService.loadAll(); // 获取所有商品
}
});

由于本地缓存是部署在应用服务器中,而我们应用服务器通常会部署多台,当数据更新时,我们不能确定哪台服务器本地中了缓存,更新或者删除所有服务器的缓存不是一个好的选择,所以我们通常会等待缓存过期。因此,这种缓存的有效期很短,通常为分钟或者秒级别,以避免返回前端脏数据。

静态缓存

如CDN等,后续单独整理。

缓存的不足

  • **首先,缓存比较适合于读多写少的业务场景,并且数据最好带有一定的热点属性。**这是因为缓存毕竟会受限于存储介质不可能缓存所有数据,那么当数据有热点属性的时候才能保证一定的缓存命中率
  • **其次,缓存会给整体系统带来复杂度,并且会有数据不一致的风险。**当更新数据库成功,更新缓存失败的场景下,缓存中就会存在脏数据。对于这种场景,我们可以考虑使用较短的过期时间或者手动清理的方式来解决。
  • **再次,之前提到缓存通常使用内存作为存储介质,但是内存并不是无限的。**因此,我们在使用缓存的时候要做数据存储量级的评估,对于可预见的需要消耗极大存储成本的数据,要慎用缓存方案。同时,缓存一定要设置过期时间,这样可以保证缓存中的会是热点数据。
  • **最后,缓存会给运维也带来一定的成本。**运维需要对缓存组件有一定的了解,在排查问题的时候也多了一个组件需要考虑在内。
1
虽然有这么多的不足,但是缓存对于性能的提升是毋庸置疑的,我们在做架构设计的时候也需要把它考虑在内,只是在做具体方案的时候需要对缓存的设计有更细致的思考,才能最大化地发挥缓存的优势。

注意问题

  • 缓存可以有多层,比如上面提到的静态缓存处在负载均衡层,分布式缓存处在应用层和数据库层之间,本地缓存处在应用层。我们需要将请求尽量挡在上层,因为越往下层,对于并发的承受能力越差;
  • 缓存命中率是我们对于缓存最重要的一个监控项,越是热点的数据,缓存的命中率就越高

当在实际工作中碰到“慢”的问题时,缓存就是你第一时间需要考虑的。

如何平滑地迁移数据库中的数据

迁移过程需要满足以下几个目标:

  • 迁移应该是在线的迁移,也就是在迁移的同时还会有数据的写入;
  • 数据应该保证完整性,也就是说在迁移之后需要保证新的库和旧的库的数据是一致的;
  • 迁移的过程需要做到可以回滚,这样一旦迁移的过程中出现问题,可以立刻回滚到源库不会对系统的可用性造成影响。

一般来说,我们有两种方案可以做数据库的迁移。

“双写”方案

shuangxie

  1. 将新的库配置为源库的从库用来同步数据;如果需要将数据同步到多库多表,那么可以使用一些第三方工具获取 Binlog 的增量日志(比如开源工具 Canal),在获取增量日志之后就可以按照分库分表的逻辑写入到新的库表中了。
  2. 同时我们需要改造业务代码,在数据写入的时候不仅要写入旧库也要写入新库。当然,基于性能的考虑,我们可以异步地写入新库,只要保证旧库写入成功即可。但是我们需要注意的是,需要将写入新库失败的数据记录在单独的日志中,这样方便后续对这些数据补写,保证新库和旧库的数据一致性。
  3. 然后我们就可以开始校验数据了。由于数据库中数据量很大,做全量的数据校验不太现实。你可以抽取部分数据,具体数据量依据总体数据量而定,只要保证这些数据是一致的就可以。
  4. 如果一切顺利,我们就可以将读流量切换到新库了。由于担心一次切换全量读流量可能会对系统产生未知的影响,所以这里最好采用灰度的方式来切换,比如开始切换 10% 的流量,如果没有问题再切换到 50% 的流量,最后再切换到 100%。
  5. 由于有双写的存在,所以在切换的过程中出现任何的问题都可以将读写流量随时切换到旧库去,保障系统的性能。
  6. 在观察了几天发现数据的迁移没有问题之后,就可以将数据库的双写改造成只写新库,数据的迁移也就完成了。

如果是将数据从自建机房迁移到云上,你也可以使用这个方案,只是你需要考虑的一个重要的因素是:自建机房到云上的专线的带宽和延迟,你需要尽量减少跨专线的读操作,所以在切换读流量的时候你需要保证自建机房的应用服务器读取本机房的数据库,云上的应用服务器读取云上的数据库。这样在完成迁移之前,只要将自建机房的应用服务器停掉并且将写入流量都切到新库就可以了。

迁移上云

这种方案是一种比较通用的方案,无论是迁移 MySQL 中的数据还是迁移 Redis 中的数据,甚至迁移消息队列都可以使用这种方式,你在实际的工作中可以直接拿来使用。这种方式的好处是:迁移的过程可以随时回滚,将迁移的风险降到了最低。劣势是:时间周期比较长,应用有改造的成本。

级联同步方案

这种方案也比较简单,比较适合数据从自建机房向云上迁移的场景。因为迁移上云最担心云上的环境和自建机房的环境不一致,会导致数据库在云上运行时因为参数配置或者硬件环境不同出现问题。
所以我们会在自建机房准备一个备库,在云上环境上准备一个新库,通过级联同步的方式在自建机房留下一个可回滚的数据库,具体的步骤如下:
jilian

  1. 先将新库配置为旧库的从库,用作数据同步;
  2. 再将一个备库配置为新库的从库,用作数据的备份;
  3. 等到三个库的写入一致后,将数据库的读流量切换到新库;
  4. 然后暂停应用的写入,将业务的写入流量切换到新库(由于这里需要暂停应用的写入,所以需要安排在业务的低峰期)。

jilianhuigun
回滚过程如下:

  1. 先将读流量切换到备库再暂停应用的写入
  2. 将写流量切换到备库,这样所有的流量都切换到了备库,也就是又回到了自建机房的环境,就可以认为已经回滚了。

这种方案优势是简单易实施,在业务上基本没有改造的成本;缺点是在切写的时候需要短暂的停止写入,对于业务来说是有损的,不过如果在业务低峰期来执行切写,可以将对业务的影响降至最低。

NoSQL 数据库在性能、扩展性上的优势,以及它的一些特殊功能特性,主要有以下几点:

  1. 在性能方面,NoSQL 数据库使用一些算法将对磁盘的随机写转换成顺序写,提升了写的性能;
  2. 在某些场景下,比如全文搜索功能,关系型数据库并不能高效地支持,需要 NoSQL 数据库的支持;
  3. 在扩展性方面,NoSQL 数据库天生支持分布式,支持数据冗余和数据分片的特性。

NoSQL 数据库发展到现在,十几年间,出现了多种类型,我来给你举几个例子:

  • Redis、LevelDB 这样的 KV 存储。这类存储相比于传统的数据库的优势是极高的读写性能,一般对性能有比较高的要求的场景会使用。
  • Hbase、Cassandra 这样的列式存储数据库。这种数据库的特点是数据不像传统数据库以行为单位来存储,而是以列来存储,适用于一些离线数据统计的场景。
  • MongoDB、CouchDB 这样的文档型数据库。这种数据库的特点是 Schema Free(模式自由),数据表中的字段可以任意扩展,比如说电商系统中的商品有非常多的字段,并且不同品类的商品的字段也都不尽相同,使用关系型数据库就需要不断增加字段支持,而用文档型数据库就简单很多了。

在 4 核 8G 的云服务器上对 MySQL 5.7 做 Benchmark,大概可以支撑 500TPS 和 10000QPS,如果出现写并发量大时,该如何解决?出现数据库容量瓶颈时如何解决?单纯从数据库层面考虑一般采用垂直拆分和水平拆分来解决。
数据库分库分表的方式有两种:一种是垂直拆分,另一种是水平拆分。这两种方式,在我看来,掌握拆分方式是关键,理解拆分原理是内核。所以你在学习时,最好可以结合自身业务来思考。

拆分方式

1、垂直拆分

垂直拆分,顾名思义就是对数据库竖着拆分,也就是将数据库的表拆分到多个不同的数据库中。
垂直拆分的原则一般是按照业务类型来拆分,核心思想是专库专用,将业务耦合度比较高的表拆分到单独的库中。举个形象的例子,就是在整理衣服的时候,将羽绒服、毛衣、T 恤分别放在不同的格子里。这样可以解决我在开篇提到的第三个问题:把不同的业务的数据分拆到不同的数据库节点上,这样一旦数据库发生故障时只会影响到某一个模块的功能,不会影响到整体功能,从而实现了数据层面的故障隔离。
现在大多数公司都采用微服务架构,一般方案为按服务拆库,各服务不进行跨库读写数据。

**优点:**各业务库独立,可以按业务重要性来区别对待,优先保障核心业务库。
**缺点:**不能解决某一个业务模块的数据大量膨胀的问题

2、水平拆分

和垂直拆分的关注点不同,垂直拆分的关注点在于业务相关性,而水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点。
一般按业务类型分为两种拆分方式:

字段哈希值拆分

这种拆分规则比较适用于实体表,数据相对独立,无明显时间概念等的数据,比如说用户表,可以先对用户 ID 做哈希(哈希的目的是将 ID 尽量打散)比如拆分成16个库,每库64张表。先对库数量16取余,决定划分到哪个库,后对库中表数量64取余,决定在哪张表。
分库分表

字段区间拆分

比较常用的是时间字段,比如用户订单等按照下单时间来拆分表,用户查询订单时必须指定查询时间段。该例子不一定是最优方案哈,会存在热点问题,比如双十一一天内订单量超大就会存在问题。
时间拆分

分库分表引入的问题

分库分表引入的一个最大的问题就是引入了分库分表键,也叫做分区键,也就是我们对数据库做分库分表所依据的字段。

一旦分区后,查询条件中必须带有分区键查询,明确要查询的数据在哪个区才有效,否则会带来更严重的性能问题。现阶段如何解决跨区查询问题呢,本人总结方式如下:
1、先查后整合
一般是把两个表的数据取出后在业务代码里面做筛选,复杂是有一些,不过是可以实现的。
2、数据冗余
如用户表按用户ID拆分,但需要按用户昵称查询用户的情况。
可以冗余一份用户昵称与用户ID量字段的表,先从该表中按昵称查询ID,再进行ID精准查询。当然该表也可以进行分区。
3、借助三方中间件
涉及一些复杂的查询搜索功能,可以借助ElasticSearch等中间件,来进行搜索优化。

有很多人并没有真正从根本上搞懂为什么要拆分,拆分后会带来哪些问题,只是一味地学习大厂现有的拆分方法,从而导致问题频出。所以,你在使用一个方案解决一个问题的时候一定要弄清楚原理,搞清楚这个方案会带来什么问题,要如何来解决,要知其然也知其所以然,这样才能在解决问题的同时避免踩坑。

依据一些云厂商的 Benchmark 的结果,在 4 核 8G 的机器上运行 MySQL 5.7 时,大概可以支撑 500 的 TPS 和 10000 的 QPS。
大部分系统的访问模型是读多写少,读写请求量的差距可能达到几个数量级。
当单机MySQL达不到高并发读请求时的处理方案:主从读写分离

主从读写的两个技术关键点

一、主从复制

MySQL 的主从复制是依赖于 binlog 的,主从复制就是将 binlog 中的数据从主库传输到从库上。具体过程:

  1. 从库在连接到主节点时会创建一个 IO 线程,用以请求主库更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件中
  2. 而主库也会创建一个 log dump 线程来发送 binlog 给从库;
  3. 从库还会创建一个 SQL 线程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。
    这是一种比较常见的主从复制方式。

主从复制存在的问题

  1. 数据延时问题
    为了不影响主库的性能,主从同步为异步过程。不能保障从库无延时同步。
  2. 主从的一致性和写入性能的权衡
    如果你要保证所有从节点都写入成功,那么写入性能一定会受影响;如果你只写入主节点就返回成功,那么从节点就有可能出现数据同步失败的情况,从而造成主从不一致,而在互联网的项目中,我们一般会优先考虑性能而不是数据的强一致性。
  3. 不能无限制增加从库数量
    增加从库主库会创建log dump线程,消耗主库性能,一般一个主库最多挂 3~5 个从库

主从数据延时解决方案参考

数据延时

1. 数据冗余
异步消息传输时,不仅仅发送ID,而是发送全量信息,避免从库再次查询.
**缺点:**可能造成单条消息比较大,从而增加了消息发送的带宽和时间。
2. 使用缓存
同步写数据库的同时,将数据写入缓存:如Redis,从Redis中读取。
**缺点:**更适合新增数据,更新数据需要考虑数据不一致问题

注意:需要做好从库延时时间的监控,延时过大需要告警通知。正常的时间是在毫秒级别,一旦落后的时间达到了秒级别就需要告警了。


二、程序访问

一主多从,读写分离,存在多个数据库节点,程序需要选择性的连接,增加了访问的复杂度。
为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。

1. 内嵌组件

以淘宝的 TDDL为代表,以代码形式内嵌运行在应用程序内部,你可以把它看成是一种数据源的代理,它的配置管理着多个数据源,每个数据源对应一个数据库,可能是主库,可能是从库。当有一个数据库请求时,中间件将 SQL 语句发给某一个指定的数据源来处理,然后将处理结果返回。
**优点:**简单易用,没有多余的部署成本
**缺点:**缺乏多语言的支持,升级比较困难

2. 增加代理层

单独部署的代理层方案,中间件部署在独立的服务器上,业务代码如同在使用单一数据库一样使用它,实际上它内部管理着很多的数据源,当有数据库请求时,它会对 SQL 语句做必要的改写,然后发往指定的数据源。
市面很多成熟中间件,具体可参考:分布式数据库中间件TDDL、Amoeba、Cobar、MyCAT架构比较

优点:
* 使用标准的 MySQL 通信协议,所以可以很好地支持多语言
* 独立部署,维护升级方便
缺点:
* 增加代理层,SQL多跨一层网络,有性能损耗
* 代理层专人维护成本增加

注意:在使用任何中间件的时候一定要保证对于中间件有足够深入的了解,否则一旦出了问题没法快速地解决就悲剧了。


名词解释

  1. **QPS:**每秒查询数,是针对读请求的
  2. **TPS:**每秒执行事务数,倾向于写请求
  3. **binlog:**记录 MySQL 上的所有变化并以二进制形式保存在磁盘上二进制日志文件

扩展

  1. Redis主从复制原理总结

Tomcat配置核心参数:

max-threads:该线程池可以容纳的最大线程数。默认值:200
server.tomcat.max-threads=1000
max-connections:接受和处理的最大连接数
server.tomcat.max-connections=20000
min-SpareThreads:Tomcat应该始终打开的最小不活跃线程数。默认值:25。
server.tomcat.min-SpareThreads=20
acceptCount:可以放到处理队列中的请求数
server.tomcat.acceptCount=700
connectionTimeout 连接超时
server.tomcat.connectionTimeout=1000

Undertow配置核心参数

**io-threads:**设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程.不要设置过大,如果过大,启动项目会报错:打开文件数过多
server.undertow.io-threads=16

**worker-threads:*阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程,它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数8
server.undertow.worker-threads=256

**buffer-size:**该配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理,每块buffer的空间大小,越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可
server.undertow.buffer-size=1024

**buffers-per-region:**每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region
server.undertow.buffers-per-region=1024

**direct-buffers:**是否分配的直接内存(NIO直接分配的堆外内存)
server.undertow.direct-buffers=true

扩展文章

数据库连接池与系统线程池不同,数据库连接池并不控制应用端和数据库端的线程池的大小。而且每个数据库连接池的配置只是针对自己所在的应用服务进程,限制的是同一个进程内可以访问数据库的并行线程数目。

数据库连接池好处

  • 节省了创建数据库连接的时间,通常这个时间大大超过处理数据访问请求的时间。
  • 统一管理数据库请求连接,避免了过多连接或频繁创建/删除连接带来的性能问题。
  • 监控了数据库连接的运行状态和错误报告,减少了应用服务的这部分代码。
  • 可以检查和报告不关闭数据库连接的错误,帮助运维监测数据库访问阻塞和帮助程序员写出正确数据库访问代码。

连接池优化策略

做为应用服务和数据库的桥梁,连接池参数配置的目标是全局优化。具体的优化目的有四个:

  1. 尽可能满足应用服务的并发数据库访问
    所有需要访问数据库的线程都可以得到需要的数据库连接。如果一个线程用到多个连接,那么需要的连接数目也会成倍增加。这时,需要的连接池最大尺寸应该是最大的并发数据库访问线程数目乘以每个线程需要的连接数目。
  2. 不让数据库服务器过载
    可能有多个应用服务器的多个连接池会同时发出请求。
  3. 能发现用了不还造成的死锁
    应用程序错误会造成借了不还的情况,反复出现会造成连接池用完应用长期等待甚至死锁的状态。需要有连接借用的超时报错机制,而这个超时时间取决于具体应用。
  4. 不浪费系统资源。
    配置过大的连接池会浪费应用服务器的系统资源,包括内存,网络端口,同步信号等。同时线程池的重启和操作都会响应变慢。不过应用端连接池的开销不是很大,资源的浪费通常不是太大问题。

核心参数配置

此处以Spring默认数据库连接池HikariCP为例:

  • **maximum-pool-size:**连接池中最大连接数(包括空闲和正在使用的连接)默认值是10,这个一般预估应用的最大连接数,后期根据监测得到一个最大值的一个平均值。要知道,最大连接并不是越多越好,一个connection会占用系统的带宽和存储。但是 当连接池没有空闲连接并且已经到达最大值,新来的连接池请求(HikariPool#getConnection)会被阻塞直到connectionTimeout(毫秒),超时后便抛出SQLException。
  • **minimum-idle:**池中最小空闲连接数量。默认值10,小于池中最大连接数,一般根据系统大部分情况下的数据库连接情况取一个平均值。Hikari会尽可能、尽快地将空闲连接数维持在这个数量上。如果为了获得最佳性能和对峰值需求的响应能力,我们也不妨让他和最大连接数保持一致,使得HikariCP成为一个固定大小的数据库连接池。
  • **pool-name:**连接池的名字。一般会出现在日志和JMX控制台中。默认值:auto-genenrated。建议取一个合适的名字,便于监控。
  • **auto-commit:**是否自动提交池中返回的连接。默认值为true。一般是有必要自动提交上一个连接中的事务的。如果为false,那么就需要应用层手动提交事务。
  • **idle-timeout:**空闲时间。仅在minimum-idle小于maximum-poop-size的时候才会起作用。默认值10分钟。根据应用实际情况做调整,对于一些间歇性流量达到峰值的应用,一般需要考虑设置的比间歇时间更大,防止创建数据库连接拖慢了应用速度。
  • **max-lifetime:**连接池中连接的最大生命周期。当连接一致处于闲置状态时,数据库可能会主动断开连接。为了防止大量的同一时间处于空闲连接因为数据库方的闲置超时策略断开连接(可以理解为连接雪崩),一般将这个值设置的比数据库的“闲置超时时间”小几秒,以便这些连接断开后,HikariCP能迅速的创建新一轮的连接。
  • **connection-timeout:**连接超时时间。默认值为30s,可以接收的最小超时时间为250ms。但是连接池请求也可以自定义超时时间(com.zaxxer.hikari.pool.HikariPool#getConnection(long))。

连接创建策略

**<minimum-idle:**如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
如果连接池中有空闲连接则复用空闲连接;
**>minimum-idle,<maximum-pool-size:**如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
**>=maximum-pool-size:**如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间等待旧的连接可用;
如果等待超过了这个设定时间则向用户抛出错误。

参考文章

  1. 数据库连接池设置
  2. HikariCP重要参数配置

JDK 实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程,它比较适用于执行 CPU 密集型的任务,也就是需要执行大量 CPU 运算的任务。这是为什么呢?因为执行 CPU 密集型的任务时 CPU 比较繁忙,因此只需要创建和 CPU 核数相当的线程就好了,多了反而会造成线程上下文切换,降低任务执行效率。所以当前线程数超过核心线程数时,线程池不会增加线程,而是放在队列里等待核心线程空闲下来。

一般核心线程数与CPU核数一致,计算公式:
线程数目 = CPU核数 * CPU 利用率 * (1 + 等待时间 / CPU计算时间)

JDK线程池核心参数

1
2
3
4
5
6
7
8
//java.util.concurrent.ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
  • **corePoolSize:**线程池中的核心线程数,即使没有任务执行的时候,他们也是存在的.(不考虑配置了参数:allowCoreThreadTimeOut,allowCoreThreadTimeOut通过字面意思也能知道,就是是否允许核心线程超时,一般情况下不需要设置,本文不考虑)
  • **maximumPoolSize:**线程池中的允许存在的最大线程数
  • **keepAliveTime:**当线程池中的线程超过核心线程数的时候,这部分多余的空闲线程等待执行新任务的超时时间.例如:核心线程数为1 ,最大线程数为5,当前运行线程为4,keepAliveTime为60s,那么4-1=3个线程在空闲状态下等待60s 后还没有新任务到来,就会被销毁了.
  • **unit:**keepAliveTime 的时间单位
  • workQueue: 线程队列,如果当前时间核心线程都在运行,又来了一个新任务,那么这个新任务就会被放进这个线程队列中,等待执行.
  • threadFactory: 线程池创建线程的工厂类.
  • handler: 如果线程队列满了同事执行线程数也达到了maximumPoolSize,如果此时再来新的线程,将执行什么 handler 来处理这个线程. handler的默认提供的类型有:
    • AbortPolicy: 抛出RejectedExecutionException异常
    • DiscardPolicy: 什么都不做.
    • DiscardOldestPolicy: 将线程队列中的最老的任务抛弃掉,换区一个空间执行当前的任务.
    • CallerRunsPolicy: 使用当前的线程(比如 main)来执行这个线程.

JDK线程创建回收策略

  1. **<corePoolSize:**如果新加入一个运行的任务,当前运行的线程小于corePoolSize,这时候会在线程池中新建一个线程用于执行这个新的任务.
  2. **>corePoolSize,队列不满:**如果新加入一个运行的任务,当前运行的线程大于等于corePoolSize,这个时候就需要将这个新的任务加入到线程队列workQueue中,一旦线程中的线程执行完成了一个任务,就会马上从队列中去一个任务来执行.
  3. **>corePoolSize,<maximumPoolSize:**如果队列也满了,怎么办呢? 如果maximumPoolSize大于corePoolSize,就会新建线程来处理这个新的任务,直到总运行线程数达到maximumPoolSize.
  4. **>maximumPoolSize:**如果总运行线程数达到了maximumPoolSize,还来了新的任务怎么办呢?就需要执行上面所说的拒绝策略了handler了,按照配置的策略进行处理,默认不配置的情况下,使用的是AbortPolicy.
  5. **keepAliveTime:**超过corePoolSize的线程,在空闲时间超过keepAliveTime时会被释放
  6. **allowCoreThreadTimeOut:**在配置了allowCoreThreadTimeOut时,corePoolSize线程在空闲时也会释放,一般不配置。

衡量指标

可用性是一个抽象的概念,你需要知道要如何来度量它,与之相关的概念是:MTBFMTTR

MTBF(Mean Time Between Failure) 是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。

**MTTR(Mean Time To Repair)**表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。

系统可用性指标:
Availability = MTBF / (MTBF + MTTR)
这个公式计算出的结果是一个比例,而这个比例代表着系统的可用性。一般来说,我们会使用几个九来描述系统的可用性。
系统可用性

  • 三个九之后,系统的年故障时间从 3 天锐减到 8 小时。

  • 四个九之后,年故障时间缩减到 1 小时之内。在这个级别的可用性下,你可能需要建立完善的运维值班体系、故障处理流程和业务变更流程。你可能还需要在系统设计上有更多的考虑。比如,在开发中你要考虑,如果发生故障,是否不用人工介入就能自动恢复。当然了,在工具建设方面,你也需要多加完善,以便快速排查故障原因,让系统快速恢复。

  • 五个九之后,故障就不能靠人力恢复了。想象一下,从故障发生到你接收报警,再到你打开电脑登录服务器处理问题,时间可能早就过了十分钟了。所以这个级别的可用性考察的是系统的容灾和自动恢复的能力,让机器来处理故障,才会让可用性指标提升一个档次。

设计思路

  1. 系统设计
    1. failover(故障转移)
      心跳监测,故障转移
    2. 超时控制
      通过收集系统之间的调用日志,统计比如说 99% 的响应时间是怎样的,然后依据这个时间来指定超时时间
    3. 降级
      降级是为了保证核心服务的稳定而牺牲非核心服务的做法。
    4. 限流
      通过对并发的请求进行限速来保护系统
  2. 系统运维
    1. 灰度发布
      灰度发布指的是系统的变更不是一次性地推到线上的,而是按照一定比例逐步推进的。
    2. 故障演练
      故障演练指的是对系统进行一些破坏性的手段,观察在出现局部故障时,整体的系统表现是怎样的,从而发现系统中存在的,潜在的可用性问题。

1.应用三层架构

mvc
request

  • 表现层:顾名思义嘛,就是展示数据结果和接受用户指令的,是最靠近用户的一层;
  • 逻辑层:里面有复杂业务的具体实现;
  • 数据访问层则:是主要处理和存储之间的交互。

2.网络分层架构

http

  • OSI 网络模型,它把整个网络分成了七层,自下而上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
  • TCP/IP 协议,它把网络简化成了四层,即链路层、网络层、传输层和应用层。每一层各司其职又互相帮助,网络层负责端到端的寻址和建立连接,传输层负责端到端的数据传输等,同时相邻两层还会有数据的交互。这样可以隔离关注点,让不同的层专注做不同的事情。

3.Linux文件系统分层

linuxfile

在文件系统的最上层是虚拟文件系统(VFS),用来屏蔽不同的文件系统之间的差异,提供统一的系统调用接口。虚拟文件系统的下层是 Ext3、Ext4 等各种文件系统,再向下是为了屏蔽不同硬件设备的实现细节,我们抽象出来的单独的一层——通用块设备层,然后就是不同类型的磁盘了。


4.阿里系统分层规约

java

  • 终端显示层:各端模板渲染并执行显示的层。当前主要是 Velocity 渲染,JS 渲染, JSP 渲染,移动端展示等。
  • 开放接口层:将 Service 层方法封装成开放接口,同时进行网关安全控制和流量控制等。
  • Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  • Service 层:业务逻辑层。
  • Manager 层:通用业务处理层。这一层主要有两个作用,其一,你可以将原先 Service 层的一些通用能力下沉到这一层,比如与缓存和存储交互策略,中间件的接入;其二,你也可以在这一层封装对第三方接口的调用,比如调用支付服务,调用审核服务等。
  • DAO 层:数据访问层,与底层 MySQL、Oracle、HBase 等进行数据交互。
  • 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。
0%