跳转到内容

Command开发指南

欢迎来到 Redisun 社区!本指南将帮助你了解如何为 Redisun 客户端贡献新的 Redis 命令实现。Redisun 是一个轻量级、高性能的 Redis 客户端,专为 Java 平台设计。我们热忱欢迎社区开发者参与贡献,一起完善 Redisun 的命令支持。

在开始开发之前,让我们先了解一下 Redisun 的核心架构:

src/main/java/tech/smartboot/redisun/
├── Command.java # 命令抽象基类
├── SimpleCommand.java # 简单命令实现类(包含常用命令常量)
├── Redisun.java # 客户端主类
├── RedisunOptions.java # 客户端配置选项
├── cmd/ # 复杂命令实现目录
│ ├── SetCommand.java # SET 命令实现(支持多种选项)
│ ├── ExpireCommand.java # EXPIRE 命令实现(支持多种选项)
│ ├── ZRangeCommand.java # ZRANGE 命令实现(支持多种选项)
│ └── ... # 其他复杂命令实现
└── resp/ # RESP 协议解析模块
├── RESP.java # RESP 协议基类
├── BulkStrings.java # 批量字符串类型
├── SimpleStrings.java # 简单字符串类型
├── Integers.java # 整数类型
├── Arrays.java # 数组类型
└── ... # 其他 RESP 类型

所有 Redis 命令实现都必须继承 Command 抽象基类。该类定义了命令构建的通用接口:

public abstract class Command {
/**
* 构建Redis命令参数列表
* @return 包含命令参数的BulkStrings列表
*/
protected List<BulkStrings> buildParams() {
throw new UnsupportedOperationException("Not implemented");
}
/**
* 将命令写入WriteBuffer
* @param writeBuffer 写入缓冲区
*/
public void writeTo(WriteBuffer writeBuffer) throws IOException {
List params = buildParams();
RESP.ofArray(params).writeTo(writeBuffer);
}
}

Redisun 提供了两种命令实现方式:

方式一:使用 SimpleCommand(推荐用于简单命令)

Section titled “方式一:使用 SimpleCommand(推荐用于简单命令)”

对于参数固定的简单命令,可以直接使用 SimpleCommand 类,在 SimpleCommand.java 中定义命令常量:

public class SimpleCommand extends Command {
// 定义命令常量
public static final BulkStrings CONSTANTS_INCR = BulkStrings.of("INCR");
public static final BulkStrings CONSTANTS_INCRBY = BulkStrings.of("INCRBY");
public static final BulkStrings CONSTANTS_DECR = BulkStrings.of("DECR");
public static final BulkStrings CONSTANTS_DECRBY = BulkStrings.of("DECRBY");
// ... 其他常量
private final BulkStrings[] params;
public SimpleCommand(BulkStrings... params) {
this.params = params;
}
@Override
protected List<BulkStrings> buildParams() {
return Arrays.asList(params);
}
}

方式二:创建专用命令类(用于复杂命令)

Section titled “方式二:创建专用命令类(用于复杂命令)”

对于支持多种选项的复杂命令(如 SET、ZRANGE、EXPIRE),需要创建专门的命令类:

public class SetCommand extends Command {
private static final BulkStrings CONSTANTS_SET = BulkStrings.of("SET");
private static final BulkStrings CONSTANTS_NX = BulkStrings.of("NX");
private static final BulkStrings CONSTANTS_XX = BulkStrings.of("XX");
private static final BulkStrings CONSTANTS_EX = BulkStrings.of("EX");
private static final BulkStrings CONSTANTS_PX = BulkStrings.of("PX");
private static final BulkStrings CONSTANTS_KEEPTTL = BulkStrings.of("KEEPTTL");
private final byte[] key;
private final byte[] value;
private BulkStrings exists; // NX/XX 选项
private Consumer<List<BulkStrings>> expire;
public SetCommand(String key, String value) {
this.key = key.getBytes();
this.value = value.getBytes();
}
@Override
protected List<BulkStrings> buildParams() {
List<BulkStrings> param = new ArrayList<>();
param.add(CONSTANTS_SET);
param.add(RESP.ofString(key));
param.add(RESP.ofString(value));
if (exists != null) {
param.add(exists);
}
if (expire != null) {
expire.accept(param);
}
return param;
}
// 链式调用方法设置选项
public SetCommand setIfNotExists() {
exists = CONSTANTS_NX;
return this;
}
public SetCommand setIfExists() {
exists = CONSTANTS_XX;
return this;
}
public SetCommand ex(int seconds) {
expire = list -> {
list.add(CONSTANTS_EX);
list.add(RESP.ofString(String.valueOf(seconds)));
};
return this;
}
}

1. 添加命令常量(使用 SimpleCommand 方式)

Section titled “1. 添加命令常量(使用 SimpleCommand 方式)”

对于简单命令,在 SimpleCommand.java 中添加命令常量:

/**
* Redis INCRBY 命令常量
* <p>
* 将 key 所储存的值加上给定的增量值(increment)。
* </p>
*
* @see <a href="https://redis.io/commands/incrby/">Redis INCRBY Command</a>
*/
public static final BulkStrings CONSTANTS_INCRBY = BulkStrings.of("INCRBY");

Redisun.java 中添加对应的方法。

首先定义响应处理函数(在类顶部与其他函数常量一起定义):

private static final Function<RESP, Long> LONG_FUTURE = resp -> {
if (resp instanceof Integers) {
return ((Integers) resp).getValue().longValue();
}
throw new RedisunException("invalid response:" + resp);
};
private static final Function<RESP, String> BULK_STRING_FUTURE = resp -> {
if (resp instanceof Nulls) {
return null;
} else if (resp instanceof BulkStrings) {
return ((BulkStrings) resp).getValue();
}
throw new RedisunException("invalid response:" + resp);
};
/**
* 将 key 所储存的值加上给定的增量值(increment)
*
* @param key 要增加的键
* @param increment 增量值
* @return 执行命令后 key 的值
*/
public long incrBy(String key, long increment) {
return get(asyncIncrBy(key, increment));
}
/**
* 将 key 所储存的值加上给定的增量值(increment)(异步版本)
*
* @param key 要增加的键
* @param increment 增量值
* @return 执行命令后 key 的值的CompletableFuture
*/
public CompletableFuture<Long> asyncIncrBy(String key, long increment) {
return execute(new SimpleCommand(SimpleCommand.CONSTANTS_INCRBY,
RESP.ofString(key),
RESP.ofString(String.valueOf(increment)))
).thenApply(LONG_FUTURE);
}

对于支持多种选项的复杂命令,需要创建专用命令类并在客户端中使用。

命令类实现(以 ExpireCommand 为例)

Section titled “命令类实现(以 ExpireCommand 为例)”
public class ExpireCommand extends Command {
private static final BulkStrings CONSTANTS_EXPIRE = BulkStrings.of("EXPIRE");
private static final BulkStrings CONSTANTS_NX = BulkStrings.of("NX");
private static final BulkStrings CONSTANTS_XX = BulkStrings.of("XX");
private static final BulkStrings CONSTANTS_GT = BulkStrings.of("GT");
private static final BulkStrings CONSTANTS_LT = BulkStrings.of("LT");
private final String key;
private final int seconds;
private BulkStrings option;
public ExpireCommand(String key, int seconds) {
this.key = key;
this.seconds = seconds;
}
@Override
protected List<BulkStrings> buildParams() {
List<BulkStrings> param = new ArrayList<>();
param.add(CONSTANTS_EXPIRE);
param.add(RESP.ofString(key));
param.add(RESP.ofString(String.valueOf(seconds)));
if (option != null) {
param.add(option);
}
return param;
}
public ExpireCommand nx() {
option = CONSTANTS_NX;
return this;
}
public ExpireCommand xx() {
option = CONSTANTS_XX;
return this;
}
public ExpireCommand gt() {
option = CONSTANTS_GT;
return this;
}
public ExpireCommand lt() {
option = CONSTANTS_LT;
return this;
}
}
/**
* 为给定 key 设置过期时间,以秒计
*
* @param key 要设置过期时间的键
* @param seconds 过期时间(秒)
* @return 设置成功返回 1,否则返回 0
*/
public int expire(String key, int seconds) {
return expire(key, seconds, null);
}
/**
* 为给定 key 设置过期时间,以秒计,并支持选项
*
* @param key 要设置过期时间的键
* @param seconds 过期时间(秒)
* @param options EXPIRE命令的额外选项配置函数
* @return 设置成功返回 1,否则返回 0
*/
public int expire(String key, int seconds, Consumer<ExpireCommand> options) {
return get(asyncExpire(key, seconds, options));
}
private CompletableFuture<Integer> asyncExpire(String key, int seconds, Consumer<ExpireCommand> options) {
ExpireCommand cmd = new ExpireCommand(key, seconds);
if (options != null) {
options.accept(cmd);
}
return execute(cmd).thenApply(INTEGER_FUTURE);
}

在测试文件中添加新命令的测试用例:

@Test
public void testIncr() {
String key = "test_incr_key";
// 先删除可能存在的键
redisun.del(key);
// 首次调用应该返回1
Assert.assertEquals(1, redisun.incr(key));
// 再次调用应该返回2
Assert.assertEquals(2, redisun.incr(key));
// 验证GET命令能正确获取值
Assert.assertEquals("2", redisun.get(key));
}
@Test
public void testIncrBy() {
String key = "test_incrby_key";
redisun.del(key);
// 增加10
Assert.assertEquals(10, redisun.incrBy(key, 10));
// 再增加5
Assert.assertEquals(15, redisun.incrBy(key, 5));
}

对于命令中使用的固定字符串,建议定义为静态常量以提高性能:

private static final BulkStrings CONSTANTS_SET = BulkStrings.of("SET");
private static final BulkStrings CONSTANTS_NX = BulkStrings.of("NX");

使用 RESP 工具类处理参数转换:

param.add(RESP.ofString(key));
param.add(RESP.ofString(value));

使用预定义的函数常量处理响应:

// 整数响应
private static final Function<RESP, Integer> INTEGER_FUTURE = resp -> {
if (resp instanceof Integers) {
return ((Integers) resp).getValue().intValue();
}
throw new RedisunException("invalid response:" + resp);
};
// 长整数响应
private static final Function<RESP, Long> LONG_FUTURE = resp -> {
if (resp instanceof Integers) {
return ((Integers) resp).getValue().longValue();
}
throw new RedisunException("invalid response:" + resp);
};
// 字符串响应
private static final Function<RESP, String> BULK_STRING_FUTURE = resp -> {
if (resp instanceof Nulls) {
return null;
} else if (resp instanceof BulkStrings) {
return ((BulkStrings) resp).getValue();
}
throw new RedisunException("invalid response:" + resp);
};
// 布尔响应(OK)
private static final Function<RESP, Boolean> OK_FUTURE = resp -> {
if (resp instanceof SimpleStrings) {
return SimpleStrings.OK.equals(((SimpleStrings) resp).getValue());
}
throw new RedisunException("invalid response:" + resp);
};

同步和异步接口的设计原则:

  1. 同步方法名遵循 command 格式,如 set()get()del()incr()
  2. 异步方法名遵循 asyncCommand 格式,如 asyncSet()asyncGet()asyncDel()asyncIncr()
  3. 同步方法内部通过调用 get(asyncXxx()) 方式实现
  4. 异步方法通过调用 execute() 方法返回 CompletableFuture 对象,允许非阻塞操作

对于支持多种选项的命令,使用 Consumer<Command> 模式:

// 使用示例
redisun.set("key", "value", cmd -> cmd.ex(60).nx());
redisun.expire("key", 60, cmd -> cmd.nx());
redisun.zrange("key", 0, -1, cmd -> cmd.withScores().rev());
public class ZRangeCommand extends Command {
private static final BulkStrings CONSTANTS_ZRANGE = BulkStrings.of("ZRANGE");
private static final BulkStrings CONSTANTS_BYSCORE = BulkStrings.of("BYSCORE");
private static final BulkStrings CONSTANTS_BYLEX = BulkStrings.of("BYLEX");
private static final BulkStrings CONSTANTS_REV = BulkStrings.of("REV");
private static final BulkStrings CONSTANTS_WITHSCORES = BulkStrings.of("WITHSCORES");
private static final BulkStrings CONSTANTS_LIMIT = BulkStrings.of("LIMIT");
private final String key;
private final String start;
private final String stop;
private boolean byScore = false;
private boolean byLex = false;
private boolean rev = false;
private boolean withScores = false;
private boolean hasLimit = false;
private long limitOffset;
private long limitCount;
public ZRangeCommand(String key, String start, String stop) {
this.key = key;
this.start = start;
this.stop = stop;
}
@Override
protected List<BulkStrings> buildParams() {
List<BulkStrings> param = new ArrayList<>();
param.add(CONSTANTS_ZRANGE);
param.add(RESP.ofString(key));
param.add(RESP.ofString(start));
param.add(RESP.ofString(stop));
if (byScore) param.add(CONSTANTS_BYSCORE);
if (byLex) param.add(CONSTANTS_BYLEX);
if (rev) param.add(CONSTANTS_REV);
if (hasLimit) {
param.add(CONSTANTS_LIMIT);
param.add(RESP.ofString(String.valueOf(limitOffset)));
param.add(RESP.ofString(String.valueOf(limitCount)));
}
if (withScores) param.add(CONSTANTS_WITHSCORES);
return param;
}
// 链式调用方法
public ZRangeCommand byScore() { this.byScore = true; return this; }
public ZRangeCommand byLex() { this.byLex = true; return this; }
public ZRangeCommand rev() { this.rev = true; return this; }
public ZRangeCommand withScores() { this.withScores = true; return this; }
public ZRangeCommand limit(long offset, long count) {
this.hasLimit = true;
this.limitOffset = offset;
this.limitCount = count;
return this;
}
// 返回结果封装
public static class Tuple {
private String member;
private double score;
// getters and setters
}
}
public List<String> zrange(String key, long start, long stop) {
return get(asyncZrange(key, start, stop));
}
public List<ZRangeCommand.Tuple> zrange(String key, long start, long stop, Consumer<ZRangeCommand> options) {
return get(asyncZrange(key, start, stop, options));
}
public CompletableFuture<List<String>> asyncZrange(String key, long start, long stop) {
return asyncZrange(key, start, stop, null)
.thenApply(list -> list.stream()
.map(ZRangeCommand.Tuple::getMember)
.collect(Collectors.toList()));
}
public CompletableFuture<List<ZRangeCommand.Tuple>> asyncZrange(String key, long start, long stop, Consumer<ZRangeCommand> options) {
ZRangeCommand cmd = new ZRangeCommand(key, String.valueOf(start), String.valueOf(stop));
if (options != null) {
options.accept(cmd);
}
return execute(cmd).thenApply(resp -> {
if (resp instanceof Arrays) {
List<RESP> resps = ((Arrays) resp).getValue();
List<ZRangeCommand.Tuple> result = new ArrayList<>(resps.size());
for (RESP r : resps) {
ZRangeCommand.Tuple tuple = new ZRangeCommand.Tuple();
if (r instanceof Arrays) {
Arrays arrays = (Arrays) r;
tuple.setMember(((BulkStrings) arrays.getValue().get(0)).getValue());
tuple.setScore(((Doubles) arrays.getValue().get(1)).getValue());
} else if (r instanceof BulkStrings) {
tuple.setMember(((BulkStrings) r).getValue());
}
result.add(tuple);
}
return result;
}
throw new RedisunException("invalid response:" + resp);
});
}

为了评估重要命令的性能表现,我们需要为其编写对应的基准测试用例。

Redisun 使用 JUnit 编写基准测试用例,主要测试同步和异步两种操作模式的性能表现。基准测试位于 bench 包中:

src/test/java/tech/smartboot/redisun/bench/
├── Bench.java # 基准测试配置类
├── RedisunBenchmark.java # Redisun 基准测试类
└── RedissonBenchmark.java # Redisson 对照组基准测试类

当你为 Redisun 实现了一个重要命令后,需要在 RedisunBenchmark.java 中添加相应的基准测试方法。

INCR 命令为例:

@Test
public void incr() {
String key = "bench_incr_key";
// 初始化键值
redisun.set(key, "0");
long start = System.currentTimeMillis();
for (int i = 0; i < SET_COUNT; i++) {
redisun.incr(key);
}
System.out.println("redisun incr cost " + (System.currentTimeMillis() - start) + "ms");
}
@Test
public void incrConcurrent() throws InterruptedException {
String key = "bench_incr_concurrent_key";
redisun.set(key, "0");
CountDownLatch latch = new CountDownLatch(SET_COUNT);
long start = System.currentTimeMillis();
for (int i = 0; i < CONCURRENT_CLIENT_COUNT; i++) {
Thread thread = new Thread(() -> {
while (latch.getCount() > 0) {
redisun.incr(key);
latch.countDown();
}
});
thread.setDaemon(true);
thread.start();
}
latch.await();
System.out.println("redisun concurrent incr cost " + (System.currentTimeMillis() - start) + "ms");
}
  1. 测试环境一致性

    • 确保所有测试都在相同的硬件和网络环境下运行
    • 每次测试前清理 Redis 数据库,避免历史数据影响测试结果
  2. 测试覆盖全面

    • 同步操作性能测试
    • 异步操作性能测试
    • 并发场景性能测试
  3. 结果记录与对比

    • 记录执行时间作为性能指标
    • 与其他客户端(如 Redisson)进行对比测试
    • 在不同负载条件下测试性能表现

我们欢迎任何形式的贡献!请按照以下流程参与贡献:

  1. Fork 项目 - 在 Gitee 或 GitHub 上 Fork Redisun 项目
  2. 创建分支 - 为你的功能创建一个新的分支
  3. 实现命令 - 按照本指南实现新的 Redis 命令
  4. 编写测试 - 为新命令编写完整的测试用例
  5. 添加基准测试 - 为重要的新命令添加性能基准测试(非必需项)
  6. 编写文档 - 在 cmd 目录下为新命令编写相应的文档
  7. 提交代码 - 提交你的代码并推送至你的仓库
  8. 发起 PR - 创建 Pull Request,描述你的实现内容

当你实现一个新的 Redis 命令后,需要为其编写相应的文档。 文档位于 /pages/src/content/docs/cmd 目录下,每个命令都有一个对应的 .mdx 文件。

请遵循以下文档编写规范:

每个命令文档应包含以下部分:

  1. 文件头部 - 包含title和description等元数据
  2. 命令简介 - 简要介绍该命令的作用
  3. Redis原生命令语法 - 展示Redis原生的命令语法
  4. 参数说明 - 详细说明每个参数的含义
  5. 详细说明 - 对命令的详细解释和使用场景
  6. Redisun使用方式 - 展示如何在Redisun中使用该命令
  7. 注意事项 - 使用该命令时需要注意的事项
  8. 参考资料 - 指向Redis官方文档的链接
---
title: 命令名称
description: 简要描述该命令的功能
sidebar:
order: 数字(按字母顺序排列)
---

在文件头部之后,应简要介绍该命令的作用,无需一级标题。

使用三级标题”### Redis原生命令语法”,并使用Code组件展示语法:

### Redis原生命令语法
<Code code={`命令语法`} lang="bash" />

使用加粗格式列出参数说明,无需标题:

**参数说明**
- **参数名**: 参数说明

使用三级标题”### 详细说明”,可以包含子级标题来组织内容。

使用二级标题”## Redisun使用方式”,展示如何在Redisun中使用该命令,并提供代码示例。

列出使用该命令时需要注意的事项。

提供指向Redis官方文档的链接。

如果你在开发过程中遇到任何问题:

  • 查看 Redis 官方文档 了解命令规范
  • 在项目 Issues 中提问
  • 联系项目维护者获取帮助

让我们一起构建更好的 Redisun!✨