✍个人博客:Pandaconda-CSDN博客
📣专栏地址:https://blog.csdn.net/newin2020/category_12903849.html
📚专栏简介:在这个专栏中,我将会分享后端开发面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
1. 如何优化一个高并发场景下的数据库读写性能?
数据库层面
-
索引优化
- 合理创建索引,对经常用于查询条件、排序和连接的字段添加索引,如在用户表的 username 字段上添加索引,可加快按用户名查询的速度。
- 避免过多索引,因为索引会增加写操作的开销,且占用额外的磁盘空间。定期分析查询语句,删除不必要的索引。
- 使用复合索引,当多个字段经常一起用于查询条件时,创建复合索引可以提高查询效率。例如,对于经常同时根据 user_id 和 create_time 查询订单的场景,创建 (user_id, create_time) 复合索引。
-
数据库架构优化
- 读写分离:采用主从复制架构,主库负责写操作,从库负责读操作。这样可以将读压力分散到多个从库上,提高系统的读性能。例如,在电商系统中,商品信息的查询可以从从库获取,而订单的创建则在主库进行。
- 分库分表:当数据量非常大时,将数据分散存储在多个数据库或表中。水平分表按照数据的行进行划分,如按时间将订单表拆分为多个表;垂直分表按照数据的列进行划分,将不常用的字段拆分到另一个表中。
-
数据库参数调整
- 根据服务器的硬件资源和业务特点,调整数据库的配置参数。例如,调整 innodb_buffer_pool_size 以提高 MySQL 的缓存效率,确保常用数据能在内存中快速访问。
应用层面
- 根据服务器的硬件资源和业务特点,调整数据库的配置参数。例如,调整 innodb_buffer_pool_size 以提高 MySQL 的缓存效率,确保常用数据能在内存中快速访问。
-
缓存机制
- 使用内存缓存(如 Redis)来减轻数据库的读压力。将经常访问的数据(如热门商品信息、用户信息等)存储在缓存中,应用程序优先从缓存中获取数据,只有在缓存中不存在时才去查询数据库,并将查询结果更新到缓存中。
- 缓存更新策略:采用合适的缓存更新策略,如缓存失效、缓存更新和缓存删除等。例如,当数据发生更新时,及时更新缓存中的数据,以保证数据的一致性。
-
异步处理
- 将一些非关键的数据库操作(如日志记录、统计信息更新等)进行异步处理。可以使用消息队列(如 Kafka、RabbitMQ)将这些操作发送到队列中,由专门的消费者进行处理,避免阻塞主业务流程。
-
限流和熔断
- 实现限流机制,限制对数据库的并发访问量,防止过多的请求压垮数据库。可以使用令牌桶算法或漏桶算法来实现限流。
- 引入熔断机制,当数据库出现故障或响应时间过长时,自动熔断对数据库的访问,返回默认数据或错误信息,避免系统雪崩。
2. 简述 Go 语言中 goroutine 和线程的区别
内存占用
-
线程:操作系统线程的栈空间通常较大,一般为几兆字节。这是因为线程需要保存大量的上下文信息,以支持复杂的系统调用和切换操作。较大的栈空间会占用较多的系统内存资源,当创建大量线程时,容易导致内存耗尽。
-
goroutine:Go 语言中的 goroutine 栈空间初始时非常小,通常只有几 KB,并且可以根据需要动态增长和收缩。这使得在 Go 程序中可以轻松创建大量的 goroutine,而不会像线程那样消耗过多的内存。
调度开销
-
线程:线程的调度由操作系统内核负责,涉及用户态和内核态的切换,这种切换开销较大,因为需要保存和恢复大量的上下文信息。此外,操作系统线程的调度是全局的,会影响整个系统的性能。
-
goroutine:goroutine 的调度由 Go 运行时(runtime)负责,在用户态进行调度,避免了频繁的内核态切换,调度开销较小。Go 运行时采用了 M:N 调度模型,将 M 个 goroutine 映射到 N 个操作系统线程上,提高了并发性能。
创建和销毁开销
-
线程:创建和销毁操作系统线程的开销较大,因为涉及到内核资源的分配和释放。例如,创建一个新线程需要分配栈空间、内核数据结构等,销毁线程时需要回收这些资源。
-
goroutine:goroutine 的创建和销毁开销非常小,Go 运行时可以快速地创建和销毁 goroutine,使得程序可以高效地处理大量的并发任务。
并发性能
-
线程:由于线程的调度开销和内存占用问题,在处理大量并发任务时,线程的性能会受到限制。过多的线程会导致上下文切换频繁,增加系统的负载,降低并发性能。
-
goroutine:goroutine 由于其轻量级的特点,可以轻松创建大量的并发任务,并且调度开销小,因此在高并发场景下表现出色。Go 语言的并发模型使得开发者可以更方便地编写高效的并发程序。
3. 在 Node.js 中,如何处理异步操作中的错误?
回调函数方式
在传统的异步操作中,通常使用回调函数来处理结果和错误。回调函数的第一个参数通常用于传递错误信息,如果没有错误则为 null。
javascript
const fs = require('fs');
fs.readFile('nonexistentfile.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取文件时出错:', err);
return;
}
console.log('文件内容:', data);
});
Promise 方式
使用 Promise 可以更好地管理异步操作和错误处理。Promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。可以使用 .then() 方法处理成功的结果,使用 .catch() 方法处理错误。
javascript
const fs = require('fs').promises;
fs.readFile('nonexistentfile.txt', 'utf8')
.then(data => {
console.log('文件内容:', data);
})
.catch(err => {
console.error('读取文件时出错:', err);
});
async/await 方式
async/await 是基于 Promise 的语法糖,使异步代码看起来更像同步代码。可以使用 try...catch 块来捕获和处理异步操作中的错误。
javascript
const fs = require('fs').promises;
async function readFileAsync() {
try {
const data = await fs.readFile('nonexistentfile.txt', 'utf8');
console.log('文件内容:', data);
} catch (err) {
console.error('读取文件时出错:', err);
}
}
readFileAsync();
全局错误处理
在 Node.js 中,可以使用 process.on('uncaughtException') 来捕获未被处理的同步错误,使用 process.on('unhandledRejection') 来捕获未被处理的 Promise 拒绝。
javascript
process.on('uncaughtException', (err) => {
console.error('未处理的同步错误:', err);
// 可以进行一些清理操作,然后退出进程
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
});