高并发设计
高并发设计归纳起来有三种方法:
Scale-out(横向扩展)。
Scale-out指的是将多个机器组成集群。该方案较复杂,需要管理集群。
Scale-up指的是升级单台机器的配置。该方案足够简单,用到业务发展初期。
缓存。CPU内存寻址在ns级别,从网卡上读取数据在us级别,磁盘的寻道时间在ms级别。
异步。调用方不需要等待结果就可以返回以执行其他的逻辑。
架构分层设计
- 不同的人专注做某一层次的事;
- 分层可以做到很好的复用;
- 分层可以更好地横向扩展。
如何做:可参考《阿里巴巴Java开发手册》
高并发系统有三大目标:高性能,高可用,可扩展。
高性能
性能优化:
- 问题导向。不能为了技术而技术,技术服务于业务;
- 二八原则。抓住主要矛盾去解决。
- 优化需要用数据说话。
阿姆达尔定律加速比=1/(1-p+p/s)
,其中p表示任务并行部分占比,s表示并行进程数。从定律看,要想提高加速比,可以通过增加进程数、增加任务并行占比、缩短整体任务时间。
高可用
分为系统设计和系统运维两个部分。提高系统的可用性有时候会牺牲系统性能。
系统设计
“Design for failure”,在有着大量机器的集群中,是一定会出现单台机器故障的情况。针对这个特点,我们可以使用
failover。在集群情况下将请求转移出去。
超时控制
限流:在极端情况下,损害部分用户体验来保障另一部分用户;
降级:保证核心服务稳定而牺牲非核心服务
系统运维
- 灰度发布。故障一般发生在系统变更时,做好灰度发布。
- 故障演练。
可扩展
能通过加机器解决问题的架构是好架构。
机器不是简单地加上去就可以的,机器加上去后,系统瓶颈可能会转移到其他地方。
将系统进行拆分,可以清楚地知道目前系统瓶颈在什么地方,这样可以针对性的扩展。
存储层拆分
按照业务进行垂直拆分。
按照数据特征进行水平拆分。
业务层拆分
相同的业务拆到单独的业务池:扩容不影响其他业务
每个业务有单独的存储资源:扩容不影响其他业务
区分核心和非核心的业务接口:便于优先扩容核心业务。
池化技术
JDK原生线程池的特点决定了它比较适合CPU密集型任务。任务数量达到core之后,任务会进队列。
Tomcat的线程池比较适合IO密集型任务。任务数量达到core,会继续创建新线程。
数据库读写分离
基于性能的考虑,在进行写操作时,写完主库就会返回,然后从库会异步更新。
这样会产生读写延迟的问题,写主库之后立马读从库,可能读不到新写入的值。
- 数据冗余。写完主库后,可以将具体内容以MQ的形式发出去,就暂时不需要去读从库了。
- 使用缓存。但又会带来数据一致性的问题。
- 查询主库。可以全部请求全部强制走主库,也可以部分请求走主库。比如写操作结束后,可以将刚写的key放入缓存,设置较小的过期时间。读操作时,如果key在缓存中存在,说明可能存在延迟,需要从主库读。如果key不在缓存中,则走从库。
数据库分库分表
按照业务进行垂直拆分。
按照数据特征进行水平拆分:引入分区键,即需要先确认去哪张表里查询。
如果我们要查询的字段不是分区键该怎么办,有以下两个办法:
- 建立以该字段为分区键的拆分存储,这会极大增加存储成本;
- 建立该字段与分区键的映射表,由于该表字段较少,可以节省存储成本。
迁移方案
- 将新的库配置为源库的从库,那么从库里的数据会慢慢追上源库;
- 改造业务代码,写操作变为双写源库和新库,读操作还是在源库上。这里要注意的是,改为双写之前需要停止从库与源库之间的数据同步;
- 校验数据。对比源库和新库里的数据是否一致,是否有漏数据的情况。
- 灰度切读流量,将流量由源库切到从库。如果切的过程中有问题,可以安全切回至源库,因为这时两个库的数据是一样的。
缓存分类
静态缓存
可放在CDN、Nginx服务器上
分布式缓存
如Redis
本地缓存
遇到极端热点数据时,可采用本地缓存。如Guava Cache。
如何更新各个服务器上的缓存:
- 使用配置中心
- 等待缓存过期之后触发加载
缓存穿透
- 回种空值是一种最常见的解决思路,实现起来也最简单,如果评估空值缓存占据的缓存空间可以接受,那么可以优先使用这种方案;
- 布隆过滤器会引入一个新的组件,也会引入一些开发上的复杂度和运维上的成本。所以只有在存在海量查询数据库中,不存在数据的请求时才会使用,在使用时也要关注布隆过滤器对内存空间的消耗;
- 对于极热点缓存数据穿透造成的“狗桩效应”,可以通过设置分布式锁或者后台线程定时加载的方式来解决。
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:无依赖,时间戳+机器号+递增号
缓存读写策略
Cache Aside是我们在使用分布式缓存时最常用的策略。
对于写操作,先写数据库,再删除缓存。
对于读操作,缓存命中则返回,未命中需读数据库然后回种缓存。
以上是比较标准的Cache Aside模式,实际使用中都会针对性的修改一下。如果采用标准的Cache Aside模式,会出现并发读写导致数据不一致的问题。
Read/Write Through和Write Back策略需要缓存组件的支持,所以比较适合本地缓存,如Guava Cache。
Write Back策略是计算机体系结构中的策略,不过写入策略中的只写缓存,异步写入后端存储的策略倒是有很多的应用场景。