高并发优化方案,可从以下几个角度进行优化
- 存储角度
- 缩短链路长度角度
- 减少请求次数角度
- 资源复用角度
- 异步编程
从存储角度
数据库
sql优化
索引优化
分库分表
读写分离
读写分离数据不一致的解决方案
- 通过第三方软件给修改的数据做标记
如redis,在每次修改之后,往redis添加一个该记录的修改了的标记,并设置一个过期时间(一般要求大于主从同步的时间即可),当查数据的请求到来时,先从redis中取标记,如果能取到,说明还没完成主从同步,从主库取数据;如果不能取到,说明主从同步已完成,从从库里取数据 - 通过客户端标记
通过服务端的第三方做标记,那么所有的sql请求都会先走一遍第三方,会把压力都给到服务端,在实时性要求不高的应用场景,可以通过客户端cookie标记。比如,发博客,当用户发完博客后,会去查看自己发布的博客,这时候客户端带标记过来,直接从主库里读数据。而其他客户端并不要求立马查看到发布的博客,照样还是从从库里读数据
缓存
适用场景
读多写少的场景
读多写少: 比如电商里的商品详情页面,访问频率很高,但是一般写入只在店家上架商品和修改信息的时候发生。如果把热点商品的信息缓存起来,这将拦截掉很多对数据库的访问,提高系统整体的吞吐量。 因为一般数据库的 QPS 由于有「ACID」约束、并且数据是持久化在硬盘的,所以比 Redis 这类基于内存的 NoSQL 存储低不少。常常是一个系统的瓶颈,如果我们把大部分的查询都在 Redis 缓存中命中了,那么系统整体的 QPS 也就上去了。计算耗时大,且实时性不高的场景
比如王者荣耀里的全区排行榜,一般一周更新一次,并且计算的数据量也比较大,所以计算后缓存起来,请求排行榜直接从缓存中取出,就不用实时计算了。
缓存更新策略
cache-aside
这应该是最容易想到的模式了,获取数据时先从缓存读,如果 cache hit 则直接返回,没命中就从数据源获取,然后更新缓存。
写数据的时候则先更新数据源,然后设置缓存失效,下一次获取数据的时候必然 cache miss,然后触发回源。即缓存的更新,只通过查询,查询没命中的时候再更新
cache-as-sor
从字面上来看,就是把 Cache 当作 SoR,也就是数据源,所以一切读写操作都是针对 Cache 的,由 Cache 内部自己维护和数据源的一致性。
这样对于使用者来说就和直接操作 SoR 没有区别了,完全感知不到 Cache 的存在。
Cache-As-SoR 又分为以下三种方式:
- Read Through:这种方式和 Cache-Aside 非常相似,都是在查询时发生 cache miss 去更新缓存,但是区别在于 Cache-Aside 需要调用方手动更新缓存,而 Cache-As-SoR 则是由缓存内部实现自己负责,对应用层透明。
- Write Through: 直写式,就是在将数据写入缓存的同时,缓存也去更新后面的数据源,并且必须等到数据源被更新成功后才可返回。这样保证了缓存和数据库里的数据一致性。
- Write Back:回写式,数据写入缓存即可返回,缓存内部会异步的去更新数据源,这样好处是写操作特别快,因为只需要更新缓存。并且缓存内部可以合并对相同数据项的多次更新,但是带来的问题就是数据不一致,可能发生写丢失。
从链路长度角度
微服务 + 消息队列
将微服务单独出来讲,实际上并不能算做减少链路长度的一种方案,需要配合消息队列。
如以下场景:
一个论坛网站,当用户成功发布一条帖子有一系列的流程要做,有积分服务计算积分,推送服务向发布者的粉丝推送一条消息
常规实现方案:
会有两个问题:
- 那么一个完整的流程,需要完成:发布 + 增加积分 + 推送消息给粉丝 + 会员服务,整个链路就变的很长,势必会导致响应不够及时。
- 且一旦后续要再加一个数据分析的服务,就又得回头修改发布服务,让发布服务去调用数据分析服务,这违背了依赖倒置原则,即上层服务不应该依赖下层服务
消息队列方案
引入消息队列作为中间层,完美解决上述两个问题。
- 当帖子发布完成后,发送一个事件到消息队列里,而关心帖子发布成功这件事的下游服务就可以订阅这个事件,这样即使后续继续增加新的下游服务,只需要订阅该事件,然后在各自的服务内完成对应功能即可。
- 发布服务只管发布,不需要关心其业务下游以后再加什么别的服务,以后增加了新服务,订阅消息队列的这个事件就可以了
流程异步处理
有些业务涉及到的处理流程非常多,但是很多步骤并不要求实时性。那么我们就可以通过消息队列异步处理。比如淘宝下单,一般包括了风控、锁库存、生成订单、短信/邮件通知等步骤。但是核心的就风控和锁库存, 只要风控和扣减库存成功,那么就可以返回结果通知用户成功下单了。后续的生成订单,短信通知都可以通过消息队列发送给下游服务异步处理。大大提高了系统响应速度。
这就是处理流程异步化。
预处理(减少服务间调用)
支付宝联合杭州市政府发放消费劵,但是要求只有杭州市常驻居民才能领取,那么需要在抢卷请求进入后台的时候就判断一下用户是否是杭州常驻居民。
而判断用户是否是常驻居民这个是另外一个微服务接口,如果直接实时的去调用那个接口,短时的高并发很有可能把这个服务也拖挂,最终导致整个系统不可用,并且 RPC 本身也是比较耗时的,所以就考虑在这里进行优化。
那么该怎么做呢?很简单的一个思路,提前将杭州所有常驻居民的 user_id 存到缓存中, 比如可以直接存到 Redis。大概就是千万量级,这样,当请求到来的时候我们直接通过缓存可以快速判断是否来自杭州常驻居民。如果不是则直接在这里返回前端。这里通过预先处理减少了实时链路上的 RPC 调用,既减少了系统的外部依赖,也极大的提高了系统的吞吐量。
延后处理
一些对于实时性要求不高的场景,可以将业务结果延后反馈给用户。
如:
一些活动,会在活动结束的时间节点,给用户反馈类似“结算中”,“稍后到账”等等,目的就是为了将海量的计算,海里的网络IO拆开,防止系统满足不了这么高的并发量。
减少请求次数角度
批处理
在涉及到网络连接、IO等情况时,将操作批量进行处理能够有效提高系统的传输速率和吞吐量
在前后端通信中,通过合并一些频繁请求的小资源可以获得更快的加载速度。
比如我们后台 RPC 框架,经常有更新数据的需求,而有的数据更新的接口往往只接受一项,这个时候我们往往会优化下更新接口,使其能够接受批量更新的请求,这样可以将批量的数据一次性发送,大大缩短网络 RPC 调用耗时
从资源复用角度
池化
内存、连接、线程这些都是资源,创建线程、分配内存、数据库连接这些操作都有一个特征, 那就是创建和销毁过程都会涉及到很多系统调用或者网络 IO。 每次都在请求中去申请创建这些资源,就会增加请求处理耗时,但是如果我们用一个容器(池) 把它们保存起来,下次需要的时候,直接拿出来使用,避免重复创建和销毁浪费的时间。
内存池
线程池
连接池
mysql连接池
一次 SQL 查询请求会经过哪些步骤:
- MySQL server 建立 TCP 连接: 三次握手
- MySQL 权限认证:
2.1 Server 向 Client 发送 密钥
2.2 Client 使用密钥加密用户名、密码等信息,将加密后的报文发送给 ServerServer
2.3 根据 Client 请求包,验证是否是合法用户,然后给 Client 发送认证结果 - Client 发送 SQL 语句
- Server 返回语句执行结果
- MySQL 关闭
- TCP 连接断开:四次挥手
可以看出不使用连接池的话,为了执行一条 SQL,会花很多时间在安全认证、网络IO上。如果使用连接池,执行一条 SQL 就省去了建立连接和断开连接所需的额外开销。还能想起哪里用到了连接池的思想吗?我认为 HTTP 长链接也算一个变相的链接池,虽然它本质上只有一个连接,但是思想却和连接池不谋而合,都是为了复用同一个连接发送多个 HTTP 请求,避免建立和断开连接的开销。
异步编程
思想是,一个完整的请求主要瓶颈是IO,不管是网络IO还是磁盘IO,读写速度往往不能及时响应,这时候可以使用异步编程,在程序遇到IO操作导致程序阻塞时,将cpu切换到别的程序或者请求,当之前的IO操作完成时,再切换回去,处理后续功能
具体异步编程可看:https://www.jianshu.com/p/beb147e27041