Nodejs适用于哪些场景?
- 后端开发,Nodejs的异步I/O天生适合做Web高并发。
- BFF开发,比如SSR中间层或者GraphQL中间层。
- 前端基建,Webpack、Gulp、Babel、Jest等等前端工程化的工具或插件。
事务
// 在配置文件 config/plugin.js 中启用 egg-mysql 插件
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
// 在 Controller 中使用事务
async function transactionExample() {
const conn = await this.app.mysql.beginTransaction();
try {
// 第一个 SQL 语句
await conn.query('INSERT INTO table1 SET ?', { name: 'Alice' });
// 第二个 SQL 语句
await conn.query('INSERT INTO table2 SET ?', { name: 'Bob' });
// 提交事务
await conn.commit();
} catch (err) {
// 发生错误时回滚事务
await conn.rollback();
throw err;
} finally {
// 释放连接
await conn.release();
}
}
Node.js 的事件循环包括以下几个主要的阶段:
- timers(定时器) :这个阶段负责处理定时器的回调函数,例如
setTimeout()和setInterval()创建的定时器。 - pending callbacks(待处理的回调) :这个阶段用于执行系统操作(例如 TCP 错误、处理错误类型)的回调函数。
- idle, prepare(空闲、准备) :这个阶段主要是内部使用的,通常不需要关注。
- poll(轮询) :这个阶段处理 I/O 事件,包括获取新的 I/O 事件,执行 I/O 相关的回调函数(例如网络请求、文件 I/O 等)。
- check(检查) :在此阶段,执行
setImmediate()的回调函数。 - close callbacks(关闭回调) :在此阶段执行一些关闭的回调函数,例如
socket.on('close', ...)。
console.log('start');
setTimeout(() => {
console.log('timeout');
process.nextTick(() => {
console.log('nextTick1');
});
}, 1000);
setImmediate(() => {
console.log('immediate');
});
const fs = require('fs');
fs.readFile(__filename, () => {
console.log('readFile');
Promise.resolve(123).then(() => {
console.log('promise 4')
})
setImmediate(() => {
console.log('immediate in readFile callback');
});
setTimeout(() => {
console.log('timeout in readFile callback');
}, 0);
});
Promise.resolve(123).then(() => {
console.log('promise1')
})
Promise.resolve(123).then(() => {
console.log('promise2')
})
setTimeout(() => {
console.log('timeout 0');
Promise.resolve(123).then(() => {
console.log('promise 3')
})
}, 0);
setImmediate(() => {
console.log('immediate 0');
}, 0);
process.nextTick(() => {
console.log('nextTick2');
});
console.log('end');
start
end
nextTick2
promise1
promise2
timeout 0
promise 3
immediate
immediate 0
readFile
promise 4
immediate in readFile callback
timeout in readFile callback
timeout
nextTick1
在这个案例中,有以下几个重要的步骤:
- 执行
console.log('Start'),输出 "Start"。 - 调用
setTimeout()创建一个定时器,由于超时时间设置为 0,因此它会被添加到事件循环的"定时器"阶段。 - 执行两个 Promise 的
then()方法,它们会添加到微任务队列中。 - 调用
fs.readFile()执行文件 I/O 操作,该操作会被添加到事件循环的"轮询"阶段。 - 执行
console.log('End'),输出 "End"。 - 完成同步任务后,事件循环开始执行阶段,首先是"定时器"阶段。在这个阶段,定时器回调函数会被执行,输出 "Timeout 1"。
- 然后,事件循环会检查微任务队列,执行微任务。这时会输出 "Promise 1" 和 "Promise 2"。
- 最后,事件循环继续执行"轮询"阶段,处理文件 I/O 操作,当文件读取完成后,会触发回调函数,输出 "File read"。
**宏任务(macrotasks)**是指在事件循环中需要执行的任务,这些任务通常包括定时器回调函数、I/O 操作、UI 渲染等。在 Node.js 中,每个事件循环阶段都可以包含一个或多个宏任务。
**微任务(microtasks)**则是在宏任务执行完毕之后立即执行的任务,它们比宏任务具有更高的优先级。典型的微任务包括 Promise 的回调函数(then() 和 catch() 方法中的回调)和 process.nextTick() 等。微任务通常用于执行一些需要立即响应的操作,例如更新界面状态、执行后续的逻辑等。
Buffer
在 Node.js 中,Buffer 是一个用于处理二进制数据的对象。Node.js 的设计目标之一是支持高性能的网络和文件 I/O 操作,而处理二进制数据是这些操作中的重要一部分。以下是 Node.js 中使用 Buffer 的几个原因:
- 性能:Buffer 对象在处理二进制数据时非常高效。它底层是使用 C++ 实现的,可以直接操作底层的内存,避免了 JavaScript 中频繁的数据拷贝操作。这使得 Node.js 能够快速地处理大量的二进制数据,例如在网络传输、文件 I/O 等场景下。
- 内存分配:Buffer 对象可以预先分配一块内存空间,然后通过填充、复制等操作来处理数据,这种方式比动态分配内存更加高效。在一些需要频繁创建和销毁二进制数据的场景下,Buffer 可以减少内存分配和释放的开销。
- 二进制数据处理:Node.js 应用程序通常需要处理各种形式的二进制数据,例如图像、音频、视频等。Buffer 提供了一组方法来操作和处理这些二进制数据,包括读取、写入、拼接、截取等,使得开发者能够方便地进行数据处理。
- 与底层系统交互:在 Node.js 中,与操作系统进行交互的许多 API 都是基于二进制数据的,例如文件 I/O、网络通信等。Buffer 提供了与这些底层 API 进行交互的接口,使得 Node.js 能够直接操作二进制数据,并且与操作系统进行高效的交互。
二进制数据比文本数据效率更高的主要原因有几个方面:
- 内存占用: 二进制数据通常比文本数据更紧凑,因为二进制数据是直接以字节的形式存储,而文本数据则需要考虑字符编码的影响。例如,在文本数据中,一个字符可能需要多个字节来表示,而在二进制数据中,每个字节都可以直接表示一定范围的值,因此在存储相同数量的信息时,二进制数据通常会占用更少的内存空间。
- 处理速度: 二进制数据的处理速度通常比文本数据更快,因为在处理二进制数据时,无需考虑字符编码和字符集的转换,也不需要进行字符串的拆分和连接操作,而是直接对字节进行操作。此外,很多硬件和软件平台都对二进制数据进行了优化,提供了更高效的处理方式。
- 数据传输: 二进制数据在网络传输和存储时通常比文本数据更高效。因为二进制数据不需要进行字符编码和解码,可以直接以字节的形式进行传输和存储,而文本数据则需要考虑字符编码的转换和解析,这会增加数据传输和存储的开销。
Koa中间件原理了解吗?
Koa洋葱圈中间件实现原理主要有以下两点:
- 数组里面存函数:使用middleware来存储中间函数。
javascript
复制代码
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
- compose函数:将一组中间件函数组合成一个大的异步函数。这个大的异步函数会依次执行每个中间件函数,并将每个中间件函数的执行结果传递给下一个中间件函数。最终,这个大的异步函数会返回一个Promise对象,表示整个中间件链的执行结果。
javascript
复制代码
function compose(middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
什么是Stream流,有哪些应用场景?
Stream是一种处理流式数据的抽象接口,用于读取、写入、转换和操作数据流。它是一个基于事件的 API,可以让我们以高效、低延迟的方式处理大型数据集。
说直白点就是基于Stream封装的API,性能更好。
比如读取文件,使用流我们可以一点一点来读取文件,每次只读取或写入文件的一小部分数据块,而不是一次性将整个文件读取或写入到内存中或磁盘中,这样做能够降低内存占用。
有没有了解过Redis?
可以从以下方面来回答:
- 用Redis实现缓存:将热门数据和热门页面存到Redis进行缓存,比如热门商品信息,商品页面和网站首页。
- 缓存遇到的问题:缓存穿透、缓存雪崩、缓存击穿。
- Redis的进阶功能:Redis有各种数据结构,除了缓存之外,还能实现很多功能。比如:消息队列、附近的人、排行榜等等。
- Redis持久化:Redis可以将缓存持久化到本地,持久化策略包括RDB和AOF。
- 集群:如果单机Redis不够用的话,可以考虑搭建Redis集群,Redis集群有主从和哨兵两种模式。
Redis对于后端来说,是一个专门的话题,我将会在我的后端面试手册中详细讲解,感兴趣的小伙伴可以持续关注。
什么是ORM?Nodejs的ORM框架有哪些?
ORM框架是通过对SQL语句进行封装,并将数据库的数据表和用户代码里的模型对象进行自动映射。
有没有做过数据库优化?
常见的优化有:
- 使用explain执行计划查看SQL的执行信息,进而定位慢SQL来源。
- 索引是Mysql调优首先能想到的方案,合理设置索引可以很大程度上提高查询效率。
- 大分页也是一个常见的性能问题出现的地方,因为MySQL需要扫描大量的数据,造成性能瓶颈。可以通过使用主键或者游标分页的方式来优化。
- 读写分离,单机顶不住的时候,可以使用主从架构,把数据库读写分担到不同的机器上。
- 分库分表,如果数据表存了海量数据,除了读写分离之外,还要考虑分库分表,把一张表分成多张表,减轻数据库压力。
假设我们有一个社交网络应用,其中有用户发布动态(写操作)和查看动态(读操作)两种操作。我们将通过引入读写分离来优化这个应用。
变化前:
javascriptCopy code
// 单一数据库连接
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/social_network', { useNewUrlParser: true, useUnifiedTopology: true });
// 用户模型
const User = mongoose.model('User', { name: String, email: String });
// 创建用户
const createUser = async (name, email) => {
const user = new User({ name, email });
await user.save();
console.log('User created:', user);
};
// 获取所有用户
const getAllUsers = async () => {
const users = await User.find();
console.log('All users:', users);
};
// 示例操作
createUser('John', 'john@example.com');
getAllUsers();
变化后:
javascriptCopy code
// 主从数据库连接
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/social_network_master', { useNewUrlParser: true, useUnifiedTopology: true });
// 从数据库连接
const mongooseSlave = require('mongoose');
mongooseSlave.connect('mongodb://localhost:27017/social_network_slave', { useNewUrlParser: true, useUnifiedTopology: true });
// 用户模型
const User = mongoose.model('User', { name: String, email: String });
// 创建用户(写操作)
const createUser = async (name, email) => {
const user = new User({ name, email });
await user.save();
console.log('User created:', user);
};
// 获取所有用户(读操作)
const getAllUsers = async () => {
const users = await User.find().read('secondary');
console.log('All users:', users);
};
// 示例操作
createUser('John', 'john@example.com');
getAllUsers();
在变化后的代码中,我们引入了一个从数据库连接,并且在执行读操作时指定了使用从数据库。这样,写操作将集中在主数据库,而读操作则会分担到从数据库,实现了读写分离。
分库分表
在分库分表的场景下,你需要根据业务需求来确定要检索哪个表。一般来说,你可能会遵循一定的规则或策略来选择要检索的表,这些规则可能包括:
- 数据分片规则:根据数据的某些属性(例如用户ID、时间范围等)来选择要检索的表。例如,你可以根据用户ID来决定检索哪个用户的数据,或者根据时间范围来选择检索哪个时间段内的数据。
- 性能优化策略:根据表的特性和性能指标来选择要检索的表。例如,你可能会根据表的大小、索引情况、存储引擎等因素来选择要检索的表,以提高查询效率和减轻数据库压力。
- 负载均衡策略:根据数据库服务器的负载情况来选择要检索的表。例如,你可以根据不同数据库服务器的负载情况来动态选择要检索的表,以实现负载均衡和资源优化。
在实际应用中,通常会根据以上策略来动态地选择要检索的表。你可能需要在代码中实现这些策略,并根据具体的查询条件和业务需求来确定要检索的表。
有了解过分布式和微服务吗?
当单体应用撑不住的时候,就得考虑上集群,把应用部署在多个机器上,就形成了分布式架构。
分布式的集群不仅带来了算力和并发能力,也带来了各种问题,这其中包括:分布式通信、分布式事务、分布式id、分布式容错、负载均衡等。
所以就需要有各种中间件来解决这些问题,比如Nginx、Zookeeper、Dubbo、MQ、RPC等。
然后当项目规模进一步扩大的时候,不仅要考虑集群,还要考虑项目的拆分,这时候就要上微服务架构了。把一个大项目根据业务拆分成很多功能单一的模块,可以由不同的团队独立开发和部署。
比如一个电商的后台API,可以拆分成用户服务、商品服务、订单服务、优惠券服务、广告服务,这些服务由不同的团队去维护。
当然,微服务也带来了更多的复杂性,所以就会有像Spring Cloud、Spring Cloud Alibaba这样的解决方案去解决这些复杂性。