本系列的最后一篇,主要提供一些高并发场景的解决思路,实现的细节虽然需要在实战中学习,但解决的思路一定要有。

扩容

垂直扩容(纵向扩展)

提高单个服务(服务器、数据库)自身能力

但会增大单个服务中其他软件设施的依赖与管理、服务内部复杂度

水平扩容(横向扩展)

增加更多服务成员

但会增加网络、数据库IO开销、管理多个服务器的难度

对数据库的扩容方案

确定业务类型

  • 读操作多:采用垂直扩容方案(redis、CDN)。采用水平扩容没有太大的意义,因为性能的瓶颈不在写操作,所以不需要实时去完成,用更多的服务器来分担压力性价比太低。所以针对单个系统去强化它的读性能就可以了
  • 写操作多:采用水平扩容方案(HBase、增加服务器、数据库)。也可以考虑垂直扩容提升单个数据库的性能,但会发现资金与硬盘的IO能力是有限的,所以需要增加更多数据库来分担写的压力。

缓存

应用需要支撑大量并发量,但数据库的性能有限,所以使用缓存来减少数据库压力与提高访问性能

特征

  • 命中率 = 命中数 / (命中数 + 没有命中数)
  • 最大空间:缓存最大空间
  • 清空策略:FIFO/LFU/LRU/过期时间/随机

FIFO:最先进入缓存的数据,在缓存空间不足时被清除,为了保证最新数据可用
LFU(Least Frequently Used):最近最不常用,基于访问次数,去除命中次数最少的元素 LRU(Least Recently Used):最近最少使用,基于访问时间,在被访问过的元素中去除最久未使用的元素

本地缓存Guava Cache

其实编程时候写的成员变量、静态变量、常亮也被看做缓存

分布式缓存Redis

高并发缓存问题

缓存一致性

全量、增量同步

根据业务需求决定分布式锁

缓存并发

当大量请求访问同一个没有被缓存的数据的时候,会发送大量请求给数据库,导致数据库压力过大,还会导致一致性问题,所以解决方式就是在缓存获取的时候加上针对单个数据的锁,直到缓存被重建成功得到最新数据

缓存击穿/穿透

查询一个数据库中不存在的数据,比如商品详情,查询一个不存在的ID,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成过大地压力。

解决方案:当通过某一个key去查询数据的时候,如果对应在数据库中的数据都不存在,我们将此key对应的value设置为一个默认的值。

缓存失效

在高并发的环境下,如果此时key对应的缓存失效,此时有多个进程就会去同时去查询DB,然后再去同时设置缓存。这个时候如果这个key是系统中的热点key或者同时失效的数量比较多时,DB访问量会瞬间增大,造成过大的压力。

解决方案:

  1. 将系统中key的缓存失效时间均匀地错开  
  2. 当我们通过key去查询数据时,首先查询缓存,如果此时缓存中查询不到,就通过分布式锁进行加锁

热点key

缓存中的某些Key(可能对应用与某个促销商品)对应的value存储在集群中一台机器,使得所有流量涌向同一机器,成为系统的瓶颈,该问题的挑战在于它无法通过增加机器容量来解决。

解决方案:

  1. 客户端热点key缓存:将热点key对应value并缓存在客户端本地,并且设置一个失效时间。
  2. 将热点key分散为多个子key,然后存储到缓存集群的不同机器上,这些子key对应的value都和热点key是一样的。

消息队列

消息队列是为了解决生产和消费的速度不一致导致的问题,有以下好处:

  1. 减少请求响应时间。比如注册功能需要调用第三方接口来发短信,如果等待第三方响应可能会需要很多时间
  2. 服务之间解耦。主服务只关心核心的流程,其他不重要的、耗费时间流程是否如何处理完成不需要知道,只通知即可
  3. 流量削锋。对于不需要实时处理的请求来说,当并发量特别大的时候,可以先在消息队列中作缓存,然后陆续发送给对应的服务去处理

如果想要实现一个消息队列,可以参考这里
最简单的消息队列就是一个消息转发器,基本功能只有三个:消息存储、消息发送、消息删除,可使用LinkedBlockingQueue、ConcurrentLinkedQueue实现

应用拆分

之前翻译过的一篇博文已经提到,如何将已经存在的巨无霸单体应用重构成微服务,点击上面链接即可

限流

限流是为了解决高并发情况下,大量请求导致数据库或服务器压力过大出现延迟或出错的方式,在图中,如果一次性将100多万数据发送给master库,那么服务器与数据库的IO性能将会被大量占用,导致其他服务对数据库的不可用,master库还需要很久的时间将数据同步给slave库

控制某段代码在一定时间内的执行次数,可通过Guava或Semaphore实现

降级与熔断

数据库切库、分库、分表

切库

切库:数据库读写分离导致的数据库切换操作

当单个数据库的读写性能达到瓶颈的时候,可根据业务来判断读与写的比重,然后通过将数据库设置为Master-Slave模式完成读写分离并配置好所有库的读写权限。
当查询业务多余读取业务的时候,通过负载均衡,将查询的操作分担给不同的从库,从而减轻主库的压力。

可以通过Spring注解来完成配置

分库分表

当单库的性能达到瓶颈,或当单表容量达到瓶颈,通过SQL与索引的优化之后还是很慢,那么就需要分表

水平分表:表结构保持不变,根据固定的ID将数据划分到不同表中,需要在写入与查询的时候进行ID的路由
垂直分表:将表结构根据数据的活跃度拆分成多个表,来分别提高不同的单表处理能力

问题:

  1. 事务问题。在执行分库之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
  2. 跨库跨表的join问题。在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上,我们无法join位于不同分库的表,也无法join分表粒度不同的表,结果原本一次查询能够完成的业务,可能需要多次查询才能完成。
  3. 额外的数据管理负担和数据运算压力。额外的数据管理负担,最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。

参考

http://coding.imooc.com/class/195.html
以及其他超连接引用

号外号外

最近在总结一些针对Java面试相关的知识点,感兴趣的朋友可以一起维护~
地址:https://github.com/xbox1994/2018-Java-Interview

Comments