文章目录

  • 1. 硬件
  • 2. 缓存
  • 2.1 HTTP缓存
  • 2.1.1 浏览器缓存
  • 2.1.2 Nginx缓存
  • 2.1.3 CDN缓存
  • 2.2 应用缓存
  • 3 集群
  • 4. 拆分
  • 4.1 应用拆分(分布式、微服务)
  • 4.2 数据库拆分
  • 5. 静态化
  • 6. 动静分离
  • 7. 消息队列
  • 8. 池化
  • 8.1 对象池
  • 8.2 数据库连接池
  • 8.3 线程池
  • 9. 数据库优化
  • 9.1 配置优化
  • 9.2 索引优化
  • 9.3 执行计划


1. 硬件

  • CPU从32位提升为64位
  • 内存从64GB提升为256GB(比如缓存服务器);
  • 磁盘扩容,1TB扩展到2TB,比如文件系统
  • 磁盘从 HDD(Hard Disk Drive)提升为 SSD(固态硬盘(Solid State Drives)),有大量读写的应用
  • 千兆网卡提升为万兆网卡

但是不管怎么提升硬件性能,硬件性能的提升不可能永无止尽,所以最终还是要靠分布式解决

2. 缓存

2.1 HTTP缓存

2.1.1 浏览器缓存

浏览器缓存是指当我们使用浏览器访问一些网站页面时,根据服务器端返回的缓存设置响应头,将响应内容缓存到浏览器,下次可以直接使用缓存内容或者仅需要去服务器端验证内容是否过期即可(js、css、html、png),这样可以减少浏览器和服务器之间来回传输的数据量,节省带宽,提升性能

比如淘宝:http://www.taobao.com/

第一次访问返回200,第二次刷新访问,返回响应码为304,表示页面内容没有修改过,浏览器缓存的内容还是最新的,不需要从服务器获取,直接读取浏览器缓存即可

【场景】高并发解决方案_Nginx

我们也可以在Java 代码中通过设置响应头,告诉浏览器进行缓存

# web static
spring.web.resources.add-mappings=true
spring.web.resources.chain.cache=true
spring.web.resources.chain.enabled=true
# 静态资源在浏览器缓存3600s
spring.web.resources.cache.period=3600
spring.web.resources.static-locations=classpath:/static/

2.1.2 Nginx缓存

Nginx提供了expires 指令来实现缓存控制,比如:

location static {
	root /opt/static/;
	expires 1d;//全天
}

当用户访问时,Nginx拦截到请求后先从Nginx本地缓存查询数据,如果有并且没有过期,则直接返回缓存内容

2.1.3 CDN缓存

CDN的全称是Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术

CDN 它本身也是一个缓存,它把后端应用的数据缓存起来,用户要访问的时候,直接从CDN上获取,不需要走后端的 Nginx,以及具体应用服务器Tomcat,它的作用主要是加速数据的传输,也提高稳定性,如果从CDN上没有获取到数据,再走后端的Nginx 缓存,Nginx上也没有,则走后端的应用服务器,CDN主要缓存静态资源(js、css、html、png)

2.2 应用缓存

浏览器 -> CDN -> Nginx -> Redis -> DB

需要缓存的数据:经常需要读取的数据、热点数据(销量高的商品、热搜榜单、网红直播间)、IO瓶颈数据(文件、电影)、计算昂贵的数据、无需实时更新的数据(js/css/html/img),缓存的目的是减少对后端服务的访问,降低后端服务的压力

3 集群

一个单体应用,当访问流量很大无法支撑,那么可以集群部署,也叫单体应用水平扩容,原来通过部署一台服务器提供服务,现在就多部署几台,那么服务的能力就会提升

部署了多台服务器,但是用户访问入口只能是一个,比如www.web.com,所以就需要负载均衡,负载均衡是应用集群扩容后的必须步集群部署后,用户的会话 session 状态要保持的话,就需要实现 session共享。

应用服务器集群、redis集群、MySQL集群(主从复制、读写分离)

4. 拆分

4.1 应用拆分(分布式、微服务)

单体应用,随着业务的发展,应用功能的增加,单体应用就逐步变得非常庞大,很多人维护这么一个系统,开发、测试、上线都会造成很大问题,比如代码冲突,代码重复,逻辑错综混乱,代码逻辑复杂度增加,响应新需求的速度降低,隐藏的风险增大,所以需要按照业务维度进行应用拆分,采用分布式开发

应用拆分之后,就将原来在同一进程里的调用变成了远程方法调用,此时就需要使用到一些远程调用技术: httpClient、hessian、dubbo、webservice等

随着业务复杂度增加,我们需要采用一些开源方案进行开发,提升开发和维护效率,比如 Dubbo、SpringCloud

通过应用拆分之后,扩容就变得容易,如果此时系统处理能力跟不上,只需要增加服务器即可(把拆分后的每一个服务再多做几个集群)

4.2 数据库拆分

读写分离、分库分表(水平/垂直)

5. 静态化

对于一些访问量大,更新频率较低的数据,可直接定时生成静态html页供前端访问,而不是访问jsp面

常用静态化的技术: freemaker、velocity

浏览器只需要请求静态页面,暂时不需要去访问数据库或者缓存来获取数据,浏览器直接加载html页即可,局部使用ajax发起请求更新。先给用户返回静态页面,后续再利用ajax请求数据(比如用户请求电商首页时,先返回纯html,其他的图片、视频资源使用ajax更新,请求地址都写为互联网绝对路径

【场景】高并发解决方案_Nginx_02

页面静态化可以提升网站稳定性,如果程序或数据库出了问题,静态页面依然可以正常访问

还可以延迟加载,根据滚动条的位置加载数据

6. 动静分离

采用比如Nginx实现动静分离,Nginx负责代理静态资源,Tomcat 负责处理动态资源

Nginx的效率极高,利用它处理静态资源,可以减轻后端服务器的压力,动静分离架构示意图:

【场景】高并发解决方案_数据_03

Redis的并发量在7w,nginx的并发量在4w,tomcat的并发量在700左右,MySQL在600左右

7. 消息队列

队列的作用就是:异步处理、流量削峰、系统解耦

  • 异步下单:收到用户下单请求,校验后即可给用户返回下单成功,将数据放入消息队列,访问数据库的操作可以后续再做
  • 削峰填谷:请求过多时,将请求数据放到MQ中,业务线程可以以稳定的速度消费,这就防止了高并发请求直接访问业务服务器
  • 定时发送邮件:将数据带上TTL放到MQ中,时间到后转移到死信队列,业务线程从死信队列获取数据,取消订单

异步处理是使用队列的一个主要原因,比如注册成功了,发优惠券/送积分/送红包/发短信/发邮件等操作都可以异步处理

使用队列实现系统解耦,比如最主要的支付操作成功了,发消息通知物流,发票等操作都不用保证实时,无需同步调用这些接口,可以让这些消息入队,使用线程池慢慢处理

使用队列流量削峰,比如并发下单、秒杀等,可以考虑使用队列将请求暂时入队,通过队列的方式将流量削平,变成平缓请求进行处理,避免应用系统因瞬间的巨大压力而压垮。但需要保证入队的请求一定能完成

常见的消息队列产品:ActiveMQ/RabbitMQ/RocketMQ/kafka

  • RabbitMQ、RokectMQ、Kafka的并发量分别在1w、10w、100w
  • ActiveMQ是jms规范下的一个老牌的成熟的消息中间件/消息服务器
  • RabbitMQ/RocketMQ 数据可靠性极好,性能也非常优秀,在一些全融领域、电商领域使用很广泛,RocketMQ是阿里巴巴的
  • kafka主要运用在大数据领域,用于对数据的分析,日志的分析等处理它有可能产生消息的丢失问题,它追求性能,性能极好,不追求数据的可靠性

8. 池化

8.1 对象池

通过复用对象,减少对象创建和垃圾收集器回收对象的资源开销,可以采用commons-pool2实现

实际项目采用对象池并不常见,主要在开发框架或组件的时候会采用

8.2 数据库连接池

Druid、DBCP、C3P0、BoneCP

8.3 线程池

  • Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
  • Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
  • Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
  • Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
  • Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
  • Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
  • ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,会更加可控。

9. 数据库优化

9.1 配置优化

MySQL Server配置优化: mysqld启动时会加载my.cnf,配置其中的相关配置可以优化server

如果某个二级索引不断被使用,二级索引成为热数据,那么InnoDB会根据在二级索引树上的索引值在构建一个哈希索引来加速搜索(只适用于等值比较)。业务完成后,可以用show engine innodb status查看哈希索引使用频率,若不高可以关闭

  • 根据物理机的条件,合理设置InnoDB log buffer大小(redo log缓存的大小),Innodb_buffer_pool_size(缓存的大小),来减少磁盘I/O次数,因为缓存区大了,在缓冲区工作的时间就长了,redo log的效率就高了
  • 增大并发连接数量和保持连接超时时间
  • MySQL使用select + 线程池的模型,适当增加MySQL线程池中的线程thread_cache_size

9.2 索引优化

  • 选择合适的索引数据结构: 一般使用B+树,若等值查询较多,也可使用自适应哈希索引
  • 选择合适的索引列:选择对查询频率高且区分度高的列作为索引列。区分度越高,索引的效果越好。避免在索引中包含过多重复值或过长的列;
  • 尽量使用聚簇索引:聚簇索引的叶子节点存储了具体的数据,不用在像非聚簇索引一样进行回表查询,所以在查询时,尽量选择聚簇索引。此外对于聚簇索引,数据在索引结构中的位置和物理位置是一致的,访问效率更高
  • 避免过多的索引:索引会占用存储空间,并且在数据更新时需要维护索引,过多的索引会增加维护的开销。只创建必要的索引,避免创建过多的索引;
  • 联合索引符合要符合最左匹配原则、join的连接字段最好用到索引、where过滤字段的区分度是否大、过滤字段避免涉及聚合函数以及类型转换

索引失效的情况

  • 没用到索引导致全表扫描,加了表锁
  • 由于where的过滤字段做了类型转换、聚合函数,没用到索引
  • 索引不合适,导致外部排序
  • select的字段过多,导致回表
  • 联合索引没用到最左侧的字段

表数据量太大,导致查找慢:可以分库分表,或者将一些时间很久远的数据导出到专门的文件服务器中

log buffer和buffer pool太小,刷脏页频率过高

当只需要指定数量数据的时候,可以使用limit提前结束查找,无需扫描全表。limit m,n时最好先用主键id过滤掉前m个数据

9.3 执行计划

参考:【MySQL】分析SQL的几种方式