高并发

高并发设计

高并发设计归纳起来有三种方法:

  1. Scale-out(横向扩展)。

    Scale-out指的是将多个机器组成集群。该方案较复杂,需要管理集群。

    Scale-up指的是升级单台机器的配置。该方案足够简单,用到业务发展初期。

  2. 缓存。CPU内存寻址在ns级别,从网卡上读取数据在us级别,磁盘的寻道时间在ms级别。

  3. 异步。调用方不需要等待结果就可以返回以执行其他的逻辑。

架构分层设计

  1. 不同的人专注做某一层次的事;
  2. 分层可以做到很好的复用;
  3. 分层可以更好地横向扩展。

如何做:可参考《阿里巴巴Java开发手册》

高并发系统有三大目标:高性能高可用可扩展

高性能

性能优化:

  1. 问题导向。不能为了技术而技术,技术服务于业务;
  2. 二八原则。抓住主要矛盾去解决。
  3. 优化需要用数据说话。

阿姆达尔定律加速比=1/(1-p+p/s),其中p表示任务并行部分占比,s表示并行进程数。从定律看,要想提高加速比,可以通过增加进程数增加任务并行占比缩短整体任务时间

高可用

分为系统设计和系统运维两个部分。提高系统的可用性有时候会牺牲系统性能。

系统设计

“Design for failure”,在有着大量机器的集群中,是一定会出现单台机器故障的情况。针对这个特点,我们可以使用

  1. failover。在集群情况下将请求转移出去。

  2. 超时控制

  3. 限流:在极端情况下,损害部分用户体验来保障另一部分用户;

  4. 降级:保证核心服务稳定而牺牲非核心服务

系统运维

  1. 灰度发布。故障一般发生在系统变更时,做好灰度发布。
  2. 故障演练。

可扩展

能通过加机器解决问题的架构是好架构。

机器不是简单地加上去就可以的,机器加上去后,系统瓶颈可能会转移到其他地方。

将系统进行拆分,可以清楚地知道目前系统瓶颈在什么地方,这样可以针对性的扩展。

存储层拆分

按照业务进行垂直拆分。

按照数据特征进行水平拆分。

业务层拆分

相同的业务拆到单独的业务池:扩容不影响其他业务

每个业务有单独的存储资源:扩容不影响其他业务

区分核心和非核心的业务接口:便于优先扩容核心业务。

池化技术

JDK原生线程池的特点决定了它比较适合CPU密集型任务。任务数量达到core之后,任务会进队列。

Tomcat的线程池比较适合IO密集型任务。任务数量达到core,会继续创建新线程。

数据库读写分离

基于性能的考虑,在进行写操作时,写完主库就会返回,然后从库会异步更新。

这样会产生读写延迟的问题,写主库之后立马读从库,可能读不到新写入的值。

  1. 数据冗余。写完主库后,可以将具体内容以MQ的形式发出去,就暂时不需要去读从库了。
  2. 使用缓存。但又会带来数据一致性的问题。
  3. 查询主库。可以全部请求全部强制走主库,也可以部分请求走主库。比如写操作结束后,可以将刚写的key放入缓存,设置较小的过期时间。读操作时,如果key在缓存中存在,说明可能存在延迟,需要从主库读。如果key不在缓存中,则走从库。

数据库分库分表

按照业务进行垂直拆分。

按照数据特征进行水平拆分:引入分区键,即需要先确认去哪张表里查询。

如果我们要查询的字段不是分区键该怎么办,有以下两个办法:

  1. 建立以该字段为分区键的拆分存储,这会极大增加存储成本;
  2. 建立该字段与分区键的映射表,由于该表字段较少,可以节省存储成本。

迁移方案

  1. 将新的库配置为源库的从库,那么从库里的数据会慢慢追上源库;
  2. 改造业务代码,写操作变为双写源库和新库,读操作还是在源库上。这里要注意的是,改为双写之前需要停止从库与源库之间的数据同步;
  3. 校验数据。对比源库和新库里的数据是否一致,是否有漏数据的情况。
  4. 灰度切读流量,将流量由源库切到从库。如果切的过程中有问题,可以安全切回至源库,因为这时两个库的数据是一样的。

缓存分类

静态缓存

可放在CDN、Nginx服务器上

分布式缓存

如Redis

本地缓存

遇到极端热点数据时,可采用本地缓存。如Guava Cache。

如何更新各个服务器上的缓存:

  1. 使用配置中心
  2. 等待缓存过期之后触发加载

缓存穿透

  1. 回种空值是一种最常见的解决思路,实现起来也最简单,如果评估空值缓存占据的缓存空间可以接受,那么可以优先使用这种方案;
  2. 布隆过滤器会引入一个新的组件,也会引入一些开发上的复杂度和运维上的成本。所以只有在存在海量查询数据库中,不存在数据的请求时才会使用,在使用时也要关注布隆过滤器对内存空间的消耗;
  3. 对于极热点缓存数据穿透造成的“狗桩效应”,可以通过设置分布式锁或者后台线程定时加载的方式来解决。

Redis高可用

redis扩展主要两方面。主备方案以及集群方案。

  • 主备的话可以用redis的sentinel(哨兵)。主要解决redis主节点故障后的自动切换。哨兵负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点。

  • Redis集群主要涉及数据分片。分片主要有三种方案:客户端分片,中间代理分片,服务端分片。

    客户端分片方案不是在多个语言之间复用,而且升级客户端SDK需要推到业务去升级。

    中间代理方案可以做到跨语言的使用,该方案有豌豆荚的Codis、Twitter的Twemproxy、FB的Mcrouter等。

    服务端分片是Redis3.0推出的官方集群解决方案,在该方案中,redis中共有16384个slot,slot = CRC16(key) % 16384,每个实例负责其中的几个solt。主流方案有codis和官方的cluster。

分库分表

发号器

leaf-segment:基于数据库,可通过id反推出主键数量。

leaf-snowflake:无依赖,时间戳+机器号+递增号

缓存读写策略

  1. Cache Aside是我们在使用分布式缓存时最常用的策略。

    对于写操作,先写数据库,再删除缓存。

    对于读操作,缓存命中则返回,未命中需读数据库然后回种缓存。

    以上是比较标准的Cache Aside模式,实际使用中都会针对性的修改一下。如果采用标准的Cache Aside模式,会出现并发读写导致数据不一致的问题。

  2. Read/Write Through和Write Back策略需要缓存组件的支持,所以比较适合本地缓存,如Guava Cache。

  3. Write Back策略是计算机体系结构中的策略,不过写入策略中的只写缓存,异步写入后端存储的策略倒是有很多的应用场景。