目录
- 1.使用线程池并行执行
- 2.数据库优化
- 2.1 小表关联大表
- 2.2 反三大范式操作
- 2.3 增加索引
- 2.4 减小事务粒度
- 2.5 读写分离、分库分表
- 3.拥抱缓存
- 3.1 Redis
- 3.2 内存缓存
- 4.锁和异步
- 4.1 减小锁的粒度
- 4.2 分布式锁
1.使用线程池并行执行
假如有一个接口的逻辑如下:
接口的整体耗时大约在1s左右,那么如果我们使用并行处理,类似木桶效应,接口的响应时间就不再是所有模块的耗时相加,而是取决于耗时最长的模块(600ms)了。
2.数据库优化
我们可以通过JVM参数调整、应用节点数扩容来增加系统的吞吐量,但是用户的请求最终都会落到数据库上,如果数据库的性能不高的话,就会成为整个链路的性能瓶颈。主要有以下几种数据库的优化方案:
2.1 小表关联大表
生产环境的数据库中的数据量一般都会非常的大,联表查询是一件非常耗时、吃内存的操作,操作不当的话可能会导致服务被拖垮。
所以我们在连表查询时,一般先查询小表,然后再用小表的查询结果作为条件去筛选大表的数据。
2.2 反三大范式操作
一般不会改变的字段,可以在表当中冗余一下,这样可以减少我们的关联查询次数,提升接口响应速度。
比如常见的:用户姓名字段。
2.3 增加索引
比如我们建表时所使用的主键是默认添加索引的,对于经常关联查询的字段也需要添加索引。
当然有一些特殊的查询方式会导致索引失效,我们需要注意一下:
2.4 减小事务粒度
我们在数据库操作的时候,为了保证数据的一致性,经常会需要使用到数据库的事务。
其实,我们在程序中使用数据库事务的时候,稍稍注意一下也可以提升我们接口的性能。
比如这个方法上面就加了一个 @Transactional
注解来开启事务。
@Transactional
public Boolean bigTransaction() {
Object a = queryDataFromA();
Object b = queryDataFromB();
handleData(a, b);
insertDataA(a);
updateDataB(b);
}
那么,其实这里需要保证事务的地方也就只有最后两行 insert 和 update,没有必要将整个方法都放到事务当中。
最直接的,我们可能会想把这两个事务抽出来,单独用一个带有 @Transactional
注解修饰的方法来执行。
@Transactional
public void handleABTransaction(Object a, Object b)
insertDataA(a);
updateDataB(b);
}
但是由于 @Transactional
注解底层是使用 AOP
来实现的,直接在类内部进行方法的调用,事务是不生效的。这里我们可以采用编程式事务来代替声明式事务:
import org.springframework.transaction.support.TransactionTemplate;
@Autowired
private TransactionTemplate transactionTemplate;
public Boolean bigTransaction() {
Object a = queryDataFromA();
Object b = queryDataFromB();
handleData(a, b);
// 编程式事务
transactionTemplate.execute(() -> {
insertDataA(a);
updateDataB(b);
});
}
2.5 读写分离、分库分表
随着业务的发展,数据库中的数据量会越来越多,这个时候就需要进行读写分离、分库分表的技术。尤其是现在微服务高可用的大环境下,不同业务使用不同的数据库已经成为了一种主流的设计。
具体分库分表的逻辑比较复杂,这里可以使用 ShardingJDBC
来实现。
3.拥抱缓存
3.1 Redis
Redis
相信大家都再熟悉不过了,它可以用来做缓存、分布式锁,甚至可以直接用来做数据库。
我们可以把变动不是很频繁,但是访问却非常频繁的数据放到 redis 里面,比如配置数据、热点数据等等。
3.2 内存缓存
我们常用的内存缓存有 Guava Cache
,还有现在非常火的,性能非常高的 Caffeine Cache
。
当我们在使用一些内存缓存框架的时候,我们一定要了解的一点就是内存数据是跟随GVM进程同时存在的。所以当我们重启应用,缓存就会有一段时间的真空期,也就是我们常说的缓存击穿
。所以我们需要考虑一下数据预热,或者是选择低谷的时候重启应用。
Caffeine 框架有一个非常好的功能就是它可以自动去刷新缓存,这样就可以保证我们缓存里面一直有数据,而且大概率是最新的数据。
private final LoadingCache<CountryCacheKey, String> appSettingCache = CaffeineCacheUtils
.createLoadingCache(1000, Duration.ofHours(2), Duration.ofDays(1),
"app-setting-thread", // threadName
new CacheLoader<CountryCacheKey, String>() {
@Nullable
@Override
public String load(CountryCacheKey key) throws Exception {
String k = key.getKey(String.class);
String setting = settingApi.getAppSettingByName(k);
return setting;
}
});
4.锁和异步
4.1 减小锁的粒度
这里其实和上面说到的数据库事务是一个道理,我们可以使用 Lock
类来控制锁的范围。它比 synchronized
关键字更为灵活。
现在我们的系统都是向微服务的演进,一个业务可能涉及多个服务,所以在锁定资源或者做互斥操作的时候,我们需要考虑用到分布式锁。
4.2 分布式锁
常用的分布式锁的解决方案会用到 Redis
,上层是用 Redisson
的框架来提供 java 锁的 API。当我们在使用这些框架的时候,需要注意:
- 获取锁要加一个等待时间,不能让程序一直自旋,一直在获取锁。
- 对于获取到的锁要添加一个失效时间,如果不添加失效时间的话。拿 Redisson 举例,里面有一个看门狗机制,会不断进行锁的续期,这样就会增加锁的持有时间。
- 代码中,一定要在
final
代码块中释放锁,否则因为程序 Bug 导致锁没有释放,导致请求就会卡住,当请求的线程占满以后,整个服务就不可用了。
除了锁的粒度意外呢,我们还可以采用异步的方式去提升服务的性能。比如配合使用 @EnableAsync
和 @Async
来异步地处理日志记录等操作。这样就算日志记录异常也不会影响主流程的流转,接口的响应时间也会下降,性能也会得到明显的提升。
另外,系统间交互我们会用到 MQ。MQ 是一个异步处理的方式。消息的生产者制造消息,然后把消息放到消息队列,比如说 RabbitMQ。接下来消费者只需要去监听这个消息队列,有新的消息会自动去触发和处理,如果消费者处理失败了,消息队列还会进行重发。比之前 A 系统同步调用 B 系统,等 B 系统处理之后才能做接下来的事情相比,性能提升了不少。
整理完毕,完结撒花~ 🌻
参考地址:
1.用4个方法,提升接口性能 | 多线程 | 数据库优化 | 缓存 | 异步与MQ,https://www.bilibili.com/video/BV1QG4y1g7QJ