导读:本期聚焦于长沙GEO公司创作的《如何用Node.js实现一个轻量级的Memcached模拟服务用于本地测试?》,敬请观看详情。单元测试中如果依赖真实的Memcached服务,测试环境的搭建成本会明显上升,而且网络波动还可能让测试结果不稳定。本文介绍如何用Node.js的net模块从零实现一个兼容Memcached文本协议的模拟服务,支持set、get、delete、incr等常用命令,并通过TTL过期、容量上限等机制模拟真实行为。文章还会讲解如何在测试框架中随用随启、自动清理数据,以及对比npm社区现成mock方案的优缺点,帮助你在不安装任何外部依赖的情况下完成稳定的缓存层单元测试。

在单元测试或者本地开发环境中,直接依赖一个真实的Memcached实例往往不是好主意:它需要额外安装、占用端口,而且测试之间容易因为残留数据互相干扰。其实Memcached的文本协议非常简单,用Node.js的net模块实现一个兼容协议的模拟服务并不困难,这就是所谓的Memcached Mock思路。本文会带你一步步实现一个支持set、get、delete、incr等命令的轻量级模拟服务,并说明如何在测试中方便地使用它。

如何用Node.js实现一个轻量级的Memcached模拟服务用于本地测试?

一、Memcached文本协议的基本原理

Memcached支持两套协议:二进制协议和文本协议。绝大多数客户端默认使用文本协议,也就是客户端通过TCP发送ASCII命令,服务端逐行返回结果。以最常用的set命令为例,客户端发送的格式是:

set mykey 0 300 5
hello

第一行包含四个部分:命令名key、flags标志位、过期时间(秒)和即将发送的字节数。紧跟一行是值内容,字节数必须和声明的完全一致。服务端处理完成后返回STORED表示成功。get命令则更简单,客户端发送get mykey,服务端返回:

VALUE mykey 0 5
hello
END

理解了这套一问一答的格式,实现模拟服务就只剩下三件事:解析命令行、读取定长的数据体、维护一个内存中的Map存储。文本协议设计得很宽松,命令以\r\n结尾,字段之间用空格分隔,这让我们可以用简单的字符串切分完成解析。

二、用net模块实现核心服务

Node.js内置的net模块可以直接创建TCP服务器,不需要任何第三方依赖。核心思路是用一个Map存储数据,值为对象形式,记录flags、过期时间和内容。收到数据后先按行切分,遇到set、add、replace这类携带数据体的命令时,记一个状态标记,等下一行数据到达再完成写入。

const net = require('net');

class MemcachedMock {
  constructor() {
    this.store = new Map();
    this.server = net.createServer(socket => this.handleConnection(socket));
  }

  handleConnection(socket) {
    let buffer = '';
    socket.on('data', chunk => {
      buffer += chunk.toString();
      // 按行解析,可能一次到达多条命令
      let index;
      while ((index = buffer.indexOf('\r\n')) !== -1) {
        const line = buffer.slice(0, index);
        buffer = buffer.slice(index + 2);
        const resp = this.execute(line, socket);
        if (resp === 'WAIT_DATA') return; // 等待数据体
        if (resp) socket.write(resp + '\r\n');
      }
    });
  }

  execute(line, socket) {
    const parts = line.split(' ');
    const cmd = parts[0];
    if (cmd === 'set' || cmd === 'add' || cmd === 'replace') {
      const [key, flags, exptime, bytes] = [parts[1], parts[2], parts[3], parts[4]];
      // 简化处理:直接从缓冲区再读一行作为值
      this.pending = { cmd, key, flags: Number(flags), exptime: Number(exptime) };
      return 'WAIT_DATA';
    }
    return this.runCommand(cmd, parts.slice(1));
  }

  runCommand(cmd, args) {
    const key = args[0];
    switch (cmd) {
      case 'get': {
        const item = this.getItem(key);
        if (!item) return 'END';
        return `VALUE ${key} ${item.flags} ${item.value.length}\r\n${item.value}\r\nEND`;
      }
      case 'delete':
        return this.store.delete(key) ? 'DELETED' : 'NOT_FOUND';
      default:
        return 'ERROR';
    }
  }

  getItem(key) {
    const item = this.store.get(key);
    if (!item) return null;
    if (item.expires > 0 && Date.now() > item.expires) {
      this.store.delete(key); // 惰性过期
      return null;
    }
    return item;
  }
}

module.exports = MemcachedMock;

上面的代码为了篇幅做了简化,把数据体也当作一行处理。严格来说应该按声明的字节数精确读取,如果值中包含\r\n字符,按行切分会出错。更健壮的做法是维护一个字节数计数器,用Buffer操作而非字符串拼接,这一点在处理二进制值时尤其重要。

三、TTL过期与incr、decr命令的实现

真实Memcached的过期机制是惰性删除加后台轮询,模拟服务里只需要实现惰性删除就够了,也就是get时检查时间戳,过期即删。这里有一个容易被忽略的细节:exptime小于30天的秒数按相对时间算,大于等于30天时会被当作绝对时间戳处理。模拟时建议照搬这个规则,否则测试环境里某些客户端行为会与线上不一致。

const THIRTY_DAYS = 60 * 60 * 24 * 30;

function parseExptime(exptime) {
  if (exptime === 0) return 0; // 永不过期
  if (exptime > THIRTY_DAYS) return exptime * 1000; // 绝对时间戳
  return Date.now() + exptime * 1000; // 相对秒数
}

incr和decr命令要求值为64位以内的数字字符串。实现时先取值、转换成整数、加减后再转回字符串,同时注意返回的新值不带换行符、直接作为一行响应。如果key不存在返回NOT_FOUND,如果值不是数字则返回CLIENT_ERROR,这两个错误路径在测试中经常被遗漏,建议专门写用例覆盖。

四、在测试中集成与现成方案的取舍

有了模拟服务,在测试框架里启动就非常简单。以Mocha为例,在before钩子里监听一个随机端口,after钩子里关闭服务器并清空Map,这样每个测试文件都能获得完全隔离的缓存环境:

const MemcachedMock = require('./memcached-mock');

describe('cache test', function () {
  let mock;
  before(function (done) {
    mock = new MemcachedMock();
    mock.server.listen(0, () => { // 0表示随机端口
      const port = mock.server.address().port;
      process.env.CACHE_PORT = port;
      done();
    });
  });

  after(function (done) {
    mock.store.clear();
    mock.server.close(done);
  });

  it('should store and get value', async function () {
    // 用任意memcached客户端连接 127.0.0.1:CACHE_PORT
  });
});

如果不想自己维护代码,npm上也有现成的选择,比如memcached-mock这类直接替换客户端库的方案,它们重写了客户端API,不经过网络层。这类方案集成更快,但缺点是绕过了真实协议解析,无法测出序列化、flags传递这类与协议相关的问题。自建模拟服务走真实TCP链路,可以配合任何语言、任何客户端使用,保真度更高。

总的来说,用Node.js实现Memcached模拟服务是一个投入产出比很高的小项目:核心代码两三百行,却能显著降低测试环境依赖,让缓存相关的单元测试跑得又快又稳。如果你的项目对协议保真度要求不高,可以直接使用社区mock库;如果需要测试协议细节或者跨语言客户端,自建模拟服务是更可靠的选择。

Node.jsMemcached单元测试修改时间:2026-09-15 01:38:32

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260915/56961.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。