Command Development Guide
Redis Command Extension Development Guide 🚀
Section titled “Redis Command Extension Development Guide 🚀”Welcome to the Redisun community! This guide will help you understand how to contribute new Redis command implementations to the Redisun client. Redisun is a lightweight, high-performance Redis client designed for the Java platform. We warmly welcome community developers to participate in contributions and improve Redisun’s command support together.
Project Architecture Overview 🏗️
Section titled “Project Architecture Overview 🏗️”Before starting development, let’s first understand Redisun’s core architecture:
src/main/java/tech/smartboot/redisun/├── Command.java # Command abstract base class├── Redisun.java # Client main class├── RedisunOptions.java # Client configuration options├── cmd/ # Specific command implementation directory│ ├── SetCommand.java # SET command implementation│ ├── GetCommand.java # GET command implementation│ ├── DelCommand.java # DEL command implementation│ ├── IncrCommand.java # INCR command implementation│ ├── DecrCommand.java # DECR command implementation│ ├── StrlenCommand.java # STRLEN command implementation│ ├── AppendCommand.java # APPEND command implementation│ ├── HSetCommand.java # HSET command implementation│ ├── HGetCommand.java # HGET command implementation│ ├── SAddCommand.java # SADD command implementation│ ├── ZAddCommand.java # ZADD command implementation│ ├── ZRemCommand.java # ZREM command implementation│ ├── ZRangeCommand.java # ZRANGE command implementation│ ├── ZScoreCommand.java # ZSCORE command implementation│ ├── ExistsCommand.java # EXISTS command implementation│ ├── DBSizeCommand.java # DBSIZE command implementation│ ├── FlushAllCommand.java # FLUSHALL command implementation│ ├── FlushDbCommand.java # FLUSHDB command implementation│ ├── SelectCommand.java # SELECT command implementation│ └── ... # Other command implementations└── resp/ # RESP protocol parsing module ├── RESP.java # RESP protocol base class ├── BulkStrings.java # Bulk string type ├── SimpleStrings.java # Simple string type └── ... # Other RESP typesCommand Implementation Basics 🎯
Section titled “Command Implementation Basics 🎯”Command Abstract Base Class
Section titled “Command Abstract Base Class”All Redis command implementations must extend the Command abstract base class. This class defines the common interface for command construction:
public abstract class Command { protected abstract List<BulkStrings> buildParams();}Each specific command implementation class needs to implement the buildParams() method to build command parameter lists that conform to the Redis protocol specification.
Example: SET Command Implementation
Section titled “Example: SET Command Implementation”Let’s take SetCommand as an example to understand how to implement a Redis command:
public class SetCommand extends Command { private static final BulkStrings CONSTANTS_SET = BulkStrings.of("SET"); private final String key; private final String value;
public SetCommand(String key, String value) { this.key = key; this.value = value; }
@Override protected List<BulkStrings> buildParams() { List<BulkStrings> param = new ArrayList<>(); // Add command name param.add(CONSTANTS_SET); // Add key param.add(RESP.ofString(key)); // Add value param.add(RESP.ofString(value)); return param; }}Steps to Develop New Commands 🛠️
Section titled “Steps to Develop New Commands 🛠️”1. Create Command Class
Section titled “1. Create Command Class”Create a new command class in the cmd directory, for example, implementing the INCR command:
package tech.smartboot.redisun.cmd;
import tech.smartboot.redisun.Command;import tech.smartboot.redisun.resp.BulkStrings;import tech.smartboot.redisun.resp.RESP;
import java.util.ArrayList;import java.util.List;
/** * Redis INCRBY Command Implementation Class * <p> * Adds the given increment value (increment) to the value stored at key. * If key does not exist, the key's value will first be initialized to 0, and then the INCRBY operation will be performed. * If the value contains the wrong type, or the string type value cannot be represented as a number, an error is returned. * The value of this operation is limited to 64-bit (bit) signed number representation. * </p> * * @see <a href="https://redis.io/commands/incrby/">Redis INCRBY Command</a> */public class IncrByCommand extends Command { private static final BulkStrings CONSTANTS_INCRBY = BulkStrings.of("INCRBY"); private final String key; private final long increment;
public IncrByCommand(String key, long increment) { this.key = key; this.increment = increment; }
@Override protected List<BulkStrings> buildParams() { List<BulkStrings> param = new ArrayList<>(); param.add(CONSTANTS_INCRBY); param.add(RESP.ofString(key)); param.add(RESP.ofString(String.valueOf(increment))); return param; }}package tech.smartboot.redisun.cmd;
import tech.smartboot.redisun.Command;import tech.smartboot.redisun.resp.BulkStrings;import tech.smartboot.redisun.resp.RESP;
import java.util.ArrayList;import java.util.List;
/** * Redis INCR Command Implementation Class * <p> * Increments the number value stored in key by one. * If key does not exist, the key's value will first be initialized to 0, and then the INCR operation will be performed. * </p> * * @see <a href="https://redis.io/commands/incr/">Redis INCR Command</a> */public class IncrCommand extends Command { private static final BulkStrings CONSTANTS_INCR = BulkStrings.of("INCR"); private final String key;
public IncrCommand(String key) { this.key = key; }
@Override protected List<BulkStrings> buildParams() { List<BulkStrings> param = new ArrayList<>(); param.add(CONSTANTS_INCR); param.add(RESP.ofString(key)); return param; }}2. Add Methods in Client
Section titled “2. Add Methods in Client”Add corresponding methods in Redisun.java:
/** * Adds the given increment value (increment) to the value stored at key * * @param key The key to increment * @param increment The increment value * @return The value of key after executing the command */public long incrBy(String key, long increment) { RESP r = syncExecute(new IncrByCommand(key, increment)); if (r instanceof Integers) { return ((Integers) r).getValue(); } throw new RedisunException("invalid response:" + r);}For some core commands, asynchronous interfaces also need to be provided to meet the needs of high-concurrency scenarios:
/** * Adds the given increment value (increment) to the value stored at key (asynchronous version) * * @param key The key to increment * @param increment The increment value * @return The value of key after executing the command */public CompletableFuture<Long> asyncIncrBy(String key, long increment) { return execute(new IncrByCommand(key, increment)).thenApply(resp -> { if (resp instanceof Integers) { return ((Integers) resp).getValue(); } throw new RedisunException("invalid response:" + resp); });}List Command Client Method Examples
Section titled “List Command Client Method Examples”List commands usually need to handle different return types. For example, insert commands return list length (integer), while pop commands return element values (string) or null:
/** * Inserts one or more values at the head (left side) of the list * * @param key The key of the list * @param values One or more values to insert * @return The length of the list after execution */public long lpush(String key, String... values) { RESP r = syncExecute(new LPushCommand(key, values)); if (r instanceof Integers) { return ((Integers) r).getValue(); } throw new RedisunException("invalid response:" + r);}/** * Removes and returns the first element of the list (left side) * * @param key The key of the list * @return The first element of the list, or null if the list is empty */public String lpop(String key) { RESP r = syncExecute(new LPopCommand(key)); if (r instanceof Nulls) { return null; } else if (r instanceof BulkStrings) { return ((BulkStrings) r).getValue(); } throw new RedisunException("invalid response:" + r);}For some core commands, asynchronous interfaces also need to be provided to meet the needs of high-concurrency scenarios:
/** * Inserts one or more values at the head (left side) of the list (asynchronous version) * * @param key The key of the list * @param values One or more values to insert * @return The length of the list after execution */public CompletableFuture<Long> asyncLpush(String key, String... values) { return execute(new LPushCommand(key, values)).thenApply(resp -> { if (resp instanceof Integers) { return ((Integers) resp).getValue().longValue(); } throw new RedisunException("invalid response:" + resp); });}/** * Removes and returns the first element of the list (left side) (asynchronous version) * * @param key The key of the list * @return The first element of the list, or null if the list is empty */public CompletableFuture<String> asyncLpop(String key) { return execute(new LPopCommand(key)).thenApply(resp -> { if (resp instanceof Nulls) { return null; } else if (resp instanceof BulkStrings) { return ((BulkStrings) resp).getValue(); } throw new RedisunException("invalid response:" + resp); });}/** * Increments the number value stored in key by one * * @param key The key to increment * @return The value of key after executing the command */public long incr(String key) { RESP r = syncExecute(new IncrCommand(key)); if (r instanceof Integers) { return ((Integers) r).getValue(); } throw new RedisunException("invalid response:" + r);}For some core commands, asynchronous interfaces also need to be provided to meet the needs of high-concurrency scenarios:
/** * Increments the number value stored in key by one (asynchronous version) * * @param key The key to increment * @return The value of key after executing the command */public CompletableFuture<Long> asyncIncr(String key) { return execute(new IncrCommand(key)).thenApply(resp -> { if (resp instanceof Integers) { return ((Integers) resp).getValue(); } throw new RedisunException("invalid response:" + resp); });}Design principles for synchronous and asynchronous interfaces:
- Synchronous method names follow the
commandformat, such as set, get, del - Asynchronous method names follow the
asyncCommandformat, such as asyncSet, asyncGet, asyncDel - Synchronous methods internally call the syncExecute method to execute commands and block waiting for results
- Asynchronous methods return CompletableFuture objects by calling the execute method, allowing non-blocking operations
3. Write Test Cases
Section titled “3. Write Test Cases”Add test cases for the new command in the test file:
@Testpublic void testIncr() { String key = "test_incr_key"; // First delete any existing keys redisun.del(key); // The first call should return 1 Assert.assertEquals(1, redisun.incr(key)); // The second call should return 2 Assert.assertEquals(2, redisun.incr(key)); // Verify that the GET command can correctly get the value Assert.assertEquals("2", redisun.get(key));}Command Implementation Best Practices ✅
Section titled “Command Implementation Best Practices ✅”1. Constant Optimization
Section titled “1. Constant Optimization”For fixed strings used in commands, it is recommended to define them as static constants to improve performance:
private static final BulkStrings CONSTANTS_SET = BulkStrings.of("SET");private static final BulkStrings CONSTANTS_NX = BulkStrings.of("NX");2. Parameter Processing
Section titled “2. Parameter Processing”Use RESP utility classes to handle parameter conversion:
param.add(RESP.ofString(key));param.add(RESP.ofString(value));3. Response Processing
Section titled “3. Response Processing”Appropriately handle responses according to the Redis command’s response type:
// Handle integer responsesif (r instanceof Integers) { return ((Integers) r).getValue();}
// Handle string responsesif (r instanceof SimpleStrings) { return SimpleStrings.OK.equals(((SimpleStrings) r).getValue());}
// Handle null responsesif (r instanceof Nulls) { return null;}Complex Command Examples 💡
Section titled “Complex Command Examples 💡”Let’s look at a more complex command example - SetCommand, which supports multiple options:
public class SetCommand extends Command { // Constant definitions 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 final String key; private final String value; private BulkStrings exists; // NX/XX options
// Expiration time option processor private Consumer<List<BulkStrings>> expire;
public SetCommand(String key, String value) { this.key = key; this.value = value; }
@Override protected List<BulkStrings> buildParams() { List<BulkStrings> param = new ArrayList<>(); param.add(CONSTANTS_SET); param.add(RESP.ofString(key)); param.add(RESP.ofString(value));
// Add NX/XX options if (exists != null) { param.add(exists); }
// Add expiration time options if (expire != null) { expire.accept(param); } return param; }
// Set NX option public SetCommand setIfNotExists() { exists = CONSTANTS_NX; return this; }
// Set XX option public SetCommand setIfExists() { exists = CONSTANTS_XX; return this; }}List Command Examples
Section titled “List Command Examples”List commands are typically used to handle list data structures in Redis. Here are some list command implementation examples:
public class LPushCommand extends Command { private static final BulkStrings CONSTANTS_LPUSH = BulkStrings.of("LPUSH"); private final String key; private final String[] values;
public LPushCommand(String key, String... values) { this.key = key; this.values = values; }
@Override protected List<BulkStrings> buildParams() { List<BulkStrings> param = new ArrayList<>(); param.add(CONSTANTS_LPUSH); param.add(RESP.ofString(key)); for (String value : values) { param.add(RESP.ofString(value)); } return param; }}public class LPopCommand extends Command { private static final BulkStrings CONSTANTS_LPOP = BulkStrings.of("LPOP"); private final String key;
public LPopCommand(String key) { this.key = key; }
@Override protected List<BulkStrings> buildParams() { List<BulkStrings> param = new ArrayList<>(); param.add(CONSTANTS_LPOP); param.add(RESP.ofString(key)); return param; }}Sorted Set Command Examples
Section titled “Sorted Set Command Examples”Sorted set commands are used to handle sorted set data structures in Redis. Here are some sorted set command implementation examples:
public class ZRemCommand extends Command { private static final BulkStrings CONSTANTS_ZREM = BulkStrings.of("ZREM"); private final String key; private final String[] members;
public ZRemCommand(String key, String... members) { this.key = key; this.members = members; }
@Override protected List<BulkStrings> buildParams() { List<BulkStrings> param = new ArrayList<>(2 + members.length); param.add(CONSTANTS_ZREM); param.add(RESP.ofString(key)); for (String member : members) { param.add(RESP.ofString(member)); } return param; }}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;
// Four option parameters private boolean byScore = false; private boolean byLex = false; private boolean rev = false; private boolean withScores = false;
// LIMIT parameters 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<>(10); param.add(CONSTANTS_ZRANGE); param.add(RESP.ofString(key)); param.add(RESP.ofString(start)); param.add(RESP.ofString(stop));
// Add option parameters if (byScore) { param.add(CONSTANTS_BYSCORE); }
if (byLex) { param.add(CONSTANTS_BYLEX); }
if (rev) { param.add(CONSTANTS_REV); }
// Add LIMIT parameters 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; }
/** * Set BYSCORE option: query by score * * @return Current ZRangeCommand instance, supports method chaining */ public ZRangeCommand byScore() { this.byScore = true; return this; }
/** * Set BYLEX option: query by lexicographical order * * @return Current ZRangeCommand instance, supports method chaining */ public ZRangeCommand byLex() { this.byLex = true; return this; }
/** * Set REV option: reverse order * * @return Current ZRangeCommand instance, supports method chaining */ public ZRangeCommand rev() { this.rev = true; return this; }
/** * Set WITHSCORES option: also return member scores * * @return Current ZRangeCommand instance, supports method chaining */ public ZRangeCommand withScores() { this.withScores = true; return this; }
/** * Set LIMIT option: limit the number of returned results * * @param offset Number of elements to skip * @param count Number of elements to return * @return Current ZRangeCommand instance, supports method chaining */ public ZRangeCommand limit(long offset, long count) { this.hasLimit = true; this.limitOffset = offset; this.limitCount = count; return this; }}public class ZScoreCommand extends Command { private static final BulkStrings CONSTANTS_ZSCORE = BulkStrings.of("ZSCORE"); private final String key; private final String member;
public ZScoreCommand(String key, String member) { this.key = key; this.member = member; }
@Override protected List<BulkStrings> buildParams() { List<BulkStrings> param = new ArrayList<>(3); param.add(CONSTANTS_ZSCORE); param.add(RESP.ofString(key)); param.add(RESP.ofString(member)); return param; }}Performance Benchmark Test Case Development 📊
Section titled “Performance Benchmark Test Case Development 📊”To evaluate the performance of important commands, we need to write corresponding benchmark test cases for them. Note that not all commands require benchmark testing; typically only core or high-frequency commands are benchmarked to evaluate their performance.
Benchmark Testing Framework Introduction
Section titled “Benchmark Testing Framework Introduction”Redisun uses JUnit to write benchmark test cases, mainly testing the performance of synchronous and asynchronous operation modes. Benchmarks are located in the bench package:
src/test/java/tech/smartboot/redisun/bench/├── Bench.java # Benchmark configuration class├── RedisunBenchmark.java # Redisun benchmark class└── RedissonBenchmark.java # Redisson control group benchmark classAdding Benchmark Tests for New Commands
Section titled “Adding Benchmark Tests for New Commands”When you implement an important command for Redisun, you need to add the corresponding benchmark test methods in RedisunBenchmark.java. As mentioned earlier, only core or high-frequency commands need benchmark testing.
Taking the INCR command we implemented earlier as an example, you can add the following benchmark test methods:
@Testpublic void incr() { String key = "bench_incr_key"; // Initialize key-value 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");}
@Testpublic void incrConcurrent() throws InterruptedException { String key = "bench_incr_concurrent_key"; // Initialize key-value 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(() -> { int j = 0; while (latch.getCount() > 0) { redisun.incr(key); j++; latch.countDown(); } }); thread.setDaemon(true); thread.start(); }
latch.await(); System.out.println("redisun concurrent incr cost " + (System.currentTimeMillis() - start) + "ms");}Taking list commands as an example, you can add the following benchmark test methods:
@Testpublic void lpush() { String key = "bench_lpush_key"; // First delete any existing keys redisun.del(key);
long start = System.currentTimeMillis(); for (int i = 0; i < SET_COUNT; i++) { redisun.lpush(key, "value" + i); } System.out.println("redisun lpush cost " + (System.currentTimeMillis() - start) + "ms");}
@Testpublic void lpushConcurrent() throws InterruptedException { String key = "bench_lpush_concurrent_key"; // First delete any existing keys redisun.del(key);
CountDownLatch latch = new CountDownLatch(SET_COUNT); long start = System.currentTimeMillis();
for (int i = 0; i < CONCURRENT_CLIENT_COUNT; i++) { Thread thread = new Thread(() -> { int j = 0; while (latch.getCount() > 0) { redisun.lpush(key, "value" + j); j++; latch.countDown(); } }); thread.setDaemon(true); thread.start(); }
latch.await(); System.out.println("redisun concurrent lpush cost " + (System.currentTimeMillis() - start) + "ms");}Benchmark Testing Best Practices ✅
Section titled “Benchmark Testing Best Practices ✅”-
Test Environment Consistency
- Ensure all tests run in the same hardware and network environment
- Clean the Redis database before each test to avoid historical data affecting test results
-
Comprehensive Test Coverage
- Synchronous operation performance testing
- Asynchronous operation performance testing
- Concurrent scenario performance testing
-
Result Recording and Comparison
- Record execution time as a performance metric
- Compare with other clients (such as Redisson)
- Test performance under different load conditions
Contribution Process 🤝
Section titled “Contribution Process 🤝”We welcome any form of contribution! Please follow this process to participate in contributions:
- Fork the project - Fork the Redisun project on Gitee or GitHub
- Create a branch - Create a new branch for your feature
- Implement the command - Implement the new Redis command according to this guide
- Write tests - Write complete test cases for the new command
- Add benchmark tests - Add performance benchmark tests for important new commands (optional)
- Write documentation - Write corresponding documentation for the new command in the cmd directory
- Commit code - Commit your code and push it to your repository
- Create PR - Create a Pull Request describing your implementation
Documentation Writing Specifications 📝
Section titled “Documentation Writing Specifications 📝”When you implement a new Redis command, you need to write corresponding documentation for it. The documentation is located in the /pages/src/content/docs/cmd directory, with each command having a corresponding .mdx file.
Please follow these documentation writing specifications:
Document Structure
Section titled “Document Structure”Each command document should contain the following sections:
- File header - Contains metadata such as title and description
- Command introduction - Briefly introduces the function of the command
- Redis native command syntax - Shows the native Redis command syntax
- Parameter description - Detailed description of each parameter’s meaning
- Detailed explanation - Detailed explanation and usage scenarios of the command
- Redisun usage - Shows how to use the command in Redisun
- Notes - Things to note when using the command
- References - Links to Redis official documentation
Detailed Specifications
Section titled “Detailed Specifications”1. File Header
Section titled “1. File Header”---title: Command Namedescription: Briefly describe the function of this commandsidebar: order: Number (arranged alphabetically)---2. Command Introduction
Section titled “2. Command Introduction”After the file header, briefly introduce the function of the command without a level-one heading.
3. Redis Native Command Syntax
Section titled “3. Redis Native Command Syntax”Use level-three heading ”### Redis Native Command Syntax” and use the Code component to display the syntax:
### Redis Native Command Syntax
<Code code={`Command Syntax`} lang="bash" />4. Parameter Description
Section titled “4. Parameter Description”List parameter descriptions in bold format without headings:
**Parameter Description**
- **Parameter Name**: Parameter Description5. Detailed Explanation
Section titled “5. Detailed Explanation”Use level-three heading ”### Detailed Explanation” and can include subheadings to organize content.
6. Redisun Usage
Section titled “6. Redisun Usage”Use level-two heading ”## Redisun Usage” to show how to use the command in Redisun and provide code examples.
7. Notes
Section titled “7. Notes”List things to note when using the command.
8. References
Section titled “8. References”Provide links to Redis official documentation.
Community Support 💬
Section titled “Community Support 💬”If you encounter any problems during development:
- Check Redis Official Documentation to understand command specifications
- Ask questions in project Issues
- Contact project maintainers for help
Let’s build a better Redisun together! ✨