前言
如果一个 Node 应用有多台服务器或多个进程在跑, 每个进程都拥有自己的内存空间, 各个进程之间的数据共享就显得非常重要.
使用数据库是一个解决数据共享的方案, 但一些临时性, 高并发的数据并不太适合直接写入数据库, 比如 session.
引入 Redis 可以解决数据共享的问题, 也因为 Redis 是基于内存存储的特点, 有着非常高的性能, 可以大大降低数据库读写的压力, 提升应用的整体性能.
Redis 还可以用来: 缓存复杂的数据库查询结果, 做自增长统计, 暂存用户操作状态等功能.
最近负责的 node 项目在高并发的情况下性能表现非常的差, rt 基本会在 7 80ms 甚至 100ms 以上, 由于对外提供了 dubbo 接口, 所以经常导致上游应用和自己的 dubbo 线程池耗尽, 所以花了一点时间排查了一番, 才发现原来自己的 node 功力还有很长的路要走啊~ 之后 node 的文章可能会越来越多~
最蠢的方式
先来看看我最早是怎么用的呢:
- for(let i = 0; i < params.length; i++) {
- redisKey = getKey(params.id);
- let value = await Redis.exec('get', redisKey);
- }
这就是我最原始的调用方法, 就是在 for 循环里不断的去 await 结果的请求, 这样的结果就是每一个请求都需要等待上一个请求完成再去执行, 只要在高流量的时候有一部分请求 rt 很高, 就会引起雪崩的反应.
使用 Promise.all 优化请求
经过了一阵谷歌之后, 我发现可以通过 Promise.all 的形式来进行请求链路的优化:
- for(let i = 0; i < params.length; i++) {
- redisKey = getKey(params.id);
- arr.push(Redis.exec('get', redisKey))
- }
- await Promise.all(arr);
上面的第一种方式被我司的 node 大神严重吐槽了 10 分钟, 然后告诉我, 使用 Promise.all 的方式可以很有效的优化这种连续的网络请求, 我赶紧将代码改完并上线.
自信满满的上线之后, 迎来的确实现实无情的打击, 在高流量的时刻, 报警依然不断, 我一边和领导说 "没事, 我再看看", 心里一边想着辞职报告该怎么写.
Redis 的正确使用姿势
在继续经过了一系列的谷歌之后, 我才发现原来的是对 Redis 的理解太浅了, 针对于业务上的需求, 我不假思索的只知道使用最简单的 set 和 get, 而 Redis 对于 set 和 get 这样的命令是一条命令一个 tcp 请求, 在业务场景上确实不太合理, 于是我使用谷歌告诉我的 pipeline 机制去改造现有的 get 请求:
- let batch = await RedisClient.getClient().batch();
- for(let i = 0; i < params.length; i++) {
- batch.get(redisKey);
- }
- batch.exec();
对于 pipeline 机制大家可以看这篇文章. 在使用 pipeline 之后, 便秘一下就通畅了, 再也没有报警过, 终于可以不用辞职了.
再后面的日子里, 我觉得认真的研究一下 Redis 这个东西, 保证让上面的问题不再发生, 于是我发现其实还是有一种更加简单的方案的, 那就是使用 mget:
- for(let i = 0; i < params.length; i++) {
- redisKey = getKey(params.id);
- arr.push(redisKey);
- }
- let value = await Redis.exec('mget', arr);
使用 mget 进行批量的查询, 这是 Redis 里比较常见的一种方式了~
总结一下
在对以上四种方式进行了对比之后得出了数据上的结论:
在一个 200 次的循环中调用 Redis 请求, 第一种最蠢的方案大概是 8000ms 左右, 第二种 Promise.all 的方案大概在 2000ms 左右, 而第三和第四种方案, 大概只需要几十 ms 就能完成, 这真的是质的飞跃啊.
这个线上血淋淋的案例让我决定真的要好好的研究一下 Redis, 不能再轻视它导致犯错.
来源: https://www.jb51.net/article/149800.htm