高并发的优化方案(延迟队列的使用)
1.高并发优化方案
解决高并发问题从宏观角度来说有3个方向:
其中,水平扩展和服务保护侧重的是运维层面的处理。而提高单机并发能力侧重的则是业务层面的处理,也就是我们程序员在开发时可以做到的。
1.1.单机并发能力
在机器性能一定的情况下,提高单机并发能力就是要尽可能缩短业务的响应时间(ResponseTime),而对响应时间影响最大的往往是对数据库的操作。而从数据库角度来说,我们的业务无非就是读或写两种类型。
对于读多写少的业务,其优化手段主要包括两方面:
- 优化代码和SQL
- 添加缓存
对于写多读少的业务,较少碰到,因此介绍此方面的优化方案。
对于高并发写的优化方案有:
- 优化代码及SQL
- 变同步写为异步写
- 合并写请求
1.2.变同步为异步
假如一个业务比较复杂,需要有多次数据库的写业务,如图所示:
由于各个业务之间是同步串行执行,因此整个业务的响应时间就是每一次数据库写业务的响应时间之和,并发能力肯定不会太好。
优化的思路很简单,利用MQ可以把同步业务变成异步,从而提高效率。
- 当我们接收到用户请求后,可以先不处理业务,而是发送MQ消息并返回给用户结果。
- 而后通过消息监听器监听MQ消息,处理后续业务。
优点: - 无需等待复杂业务处理,大大减少响应时间
- 利用MQ暂存消息,起到流量削峰整形作用
- 降低写数据库频率,减轻数据库并发压力
缺点: - 依赖于MQ的可靠性
- 降低了些频率,但是没有减少数据库写次数
应用场景: - 比较适合应用于业务复杂, 业务链较长,有多次数据库写操作的业务。
1.3.合并写请求
合并写请求方案其实是参考高并发读的优化思路:当读数据库并发较高时,我们可以把数据缓存到Redis,这样就无需访问数据库,大大减少数据库压力,减少响应时间。
合并写请求就是指当写数据库并发较高时,不再直接写到数据库。而是先将数据缓存到Redis,然后定期将缓存中的数据批量写入数据库。
由于Redis是内存操作,写的效率也非常高,这样每次请求的处理速度大大提高,响应时间大大缩短,并发能力肯定有很大的提升。
优点:
- 写缓存速度快,响应时间大大减少
- 降低数据库的写频率和写次数,大大减轻数据库压力
缺点: - 实现相对复杂
- 依赖Redis可靠性
- 不支持事务和复杂业务
场景: - 写频率较高、写业务相对简单的场景
2. 延迟任务方案
延迟任务的实现方案有很多,常见的有四类:
DelayQueue | Redisson | MQ | 时间轮 | |
---|---|---|---|---|
原理 | JDK自带延迟队列,基于阻塞队列实现。 | 基于Redis数据结构模拟JDK的DelayQueue实现 | 利用MQ的特性。例如RabbitMQ的死信队列 | 时间轮算法 |
优点 | 不依赖第三方服务 | 分布式系统下可用 不占用JVM内存 |
分布式系统下可以 不占用JVM内存 |
不依赖第三方服务 性能优异 |
缺点 | 占用JVM内存 | 只能单机使用 | 依赖第三方服务 | 依赖第三方服务 |
2.1 DelayQueue的原理
DelayQueue的源码:
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
// ... 略
}
可以看到DelayQueue实现了BlockingQueue接口,是一个阻塞队列。队列就是容器,用来存储东西的。DelayQueue叫做延迟队列,其中存储的就是延迟执行的任务。
DelayQueue的泛型定义:
DelayQueue<E extends Delayed>
这说明存入DelayQueue内部的元素必须是Delayed类型,这其实就是一个延迟任务的规范接口。来看一下: