Native GET and SET operations on REDIS

Mayank Choubey
Tech Tonic
Published in
5 min readJan 25, 2022

--

Redis (Remote Dictionary Server) is an in-memory data structure store, used as a distributed, in-memory key–value database, cache and message broker, with optional durability. Redis supports different kinds of abstract data structures, such as strings, lists, maps, sets, sorted sets, HyperLogLogs, bitmaps, streams, and spatial indices.

Redis popularized the idea of a system that can be considered at the same time a store and a cache, using a design where data is always modified and read from the main computer memory, but also stored on disk in a format that is unsuitable for random access of data, but only to reconstruct the data back in memory once the system restarts.

According to monthly DB-Engines rankings, Redis is often the most popular key–value database.

Redis server is accessible through a TCP connection. The server takes commands, executes them, and returns the result.

In this article, we’ll perform basic GET <key> and SET <key> <val> operations natively, i.e. without using any frameworks.

To know about writing a TCP client in Deno, please visit an article here.

Interface

A client can connect to a Redis server by establishing a TCP connection to the default Redis server port, 6379.

Redis clients communicate with the Redis server using a protocol called RESP (REdis Serialization Protocol). The RESP protocol is documented here. While RESP is technically non-TCP specific, in the context of Redis the protocol is only used with TCP connections (or equivalent stream oriented connections like Unix sockets). In the scope of this article, we’ll look at two heavily used RESP commands:

GET <key>

The GET <key> command gets the value of the given key. If the key does not exist, the special value nil is returned. The response is textual and would need some level of basic parsing to extract the data.

Request: 
GET key1
Response:
$4
val1
Request:
GET key2
Response:
$-1

SET <key> <value>

The SET <key> <value> command sets the key to a given value. If the key already holds a value, it is overwritten. If the key gets set correctly, a simple OK is returned.

Request:
SET key1 val1
Response:
+OK

Binary data

As we’ll be working with low-level APIs like connect, read, & write, we’ll need to work with binary data (Uint8Array). It’ll be useful to write utility functions that convert string to bytes and vice versa.

const encoder = new TextEncoder(),
decoder = new TextDecoder();
const enc = (d: string) => encoder.encode(d),
dec = (b: Uint8Array) => decoder.decode(b);
const CRLF = "\r\n";

The enc utility function convert string to bytes, while the dec utility function converts bytes to string.

Establishing connection

The Deno.connect API can be used to establish a TCP connection with Redis server. The TCP connection would remain active so that the requests gets served as fast as possible. The Redis server exposes a TCP server on port 6379.

In the following example, Redis server is running on localhost

const conn = await Deno.connect({ port: 6379 });

SET request

The SET request takes two inputs:

  • key: The key to set in the database
  • value: The value for the key to set in the database

A SET request is a simple CRLF terminated string that contains SET <key> <value>. The SET request can be sent using write API.

const key = "key1", val = "val1";
await conn.write(enc(`SET ${key} ${val}${CRLF}`));

Once the SET request is sent out, we need to wait for a response using read API. The response to a successful SET is always ‘+OK’, so there is no need to do any parsing.

const n=await conn.read(buf) || 0;
if (dec(buf.slice(0, n)).replaceAll(CRLF, "") == "+OK") {
console.log(`${key}=${val} has been set`);
}

The following is the complete code of a function to set a key in Redis. The set function returns true if the key got set, false otherwise.

const set = async (
conn: Deno.Conn,
key: string,
val: string,
): Promise<Boolean> => {
const buf = new Uint8Array(50);
await conn.write(enc(`SET ${key} ${val}${CRLF}`));
const n = await conn.read(buf) || 0;
if (dec(buf.slice(0, n)).replaceAll(CRLF, "") == "+OK") {
return true;
}
return false;
};

The following code uses the set function to set a key in Redis:

const key = "key1", val = "val1", keyInexistent = "key2";
if (await set(conn, key, val)) {
console.log(`${key} has been set`);
} else {
console.error(`${key} failed to set`);
}
//key1 has been set

GET request

The GET request takes a single input:

  • key: The key to set in the database

As the case with SET, a GET request is also a simple CRLF terminated string that contains GET <key>. The GET request can be sent using write API.

const CRLF = "\r\n";const key = "key1";
await conn.write(enc(`GET ${key}${CRLF}`));

Once the GET request is sent out, we need to wait for a response using read API. The GET response could be empty ($-1) or a string that is the value of the requested key ($<length>\r\n<value>). The response of the GET request needs a bit of parsing to extract the value.

const n = await conn.read(buf) || 0;
const r = dec(buf.slice(0, n)).split(CRLF).slice(0, -1);
if (r[0] === "$-1") {
//key not found
}
r[1]; //the value of the key

The following is the complete code of a function to get a key from Redis. The get function returns the value if the key was present in Redis, undefined otherwise.

const get = async (
conn: Deno.Conn,
key: string,
): Promise<string | undefined> => {
const buf = new Uint8Array(50);
await conn.write(enc(`GET ${key} ${CRLF}`));
const n = await conn.read(buf) || 0;
const r = dec(buf.slice(0, n)).split(CRLF).slice(0, -1);
if (r[0] === "$-1") {
return;
}
if (!r[1]) {
return;
}
return r[1];
};

The following code uses the get function to get a couple of keys from Redis (one exists, one doesn’t):

await get(conn, "key1"); //val1
await get(conn, "key2"); //undefined

Complete code

Now that we’ve understood how to access Redis natively, let’s take a look at a complete example. The following is the complete code to set and get data from Redis:

const encoder = new TextEncoder(),
decoder = new TextDecoder();
const enc = (d: string) => encoder.encode(d),
dec = (b: Uint8Array) => decoder.decode(b);
const CRLF = "\r\n";
const set = async (
conn: Deno.Conn,
key: string,
val: string,
): Promise<Boolean> => {
const buf = new Uint8Array(50);
await conn.write(enc(`SET ${key} ${val}${CRLF}`));
const n = await conn.read(buf) || 0;
if (dec(buf.slice(0, n)).replaceAll(CRLF, "") == "+OK") {
return true;
}
return false;
};
const get = async (
conn: Deno.Conn,
key: string,
): Promise<string | undefined> => {
const buf = new Uint8Array(50);
await conn.write(enc(`GET ${key} ${CRLF}`));
const n = await conn.read(buf) || 0;
const r = dec(buf.slice(0, n)).split(CRLF).slice(0, -1);
if (r[0] === "$-1") {
return;
}
if (!r[1]) {
return;
}
return r[1];
};
const conn = await Deno.connect({ port: 6379 });const key = "key1", val = "val1", keyInexistent = "key2";
if (await set(conn, key, val)) {
console.log(`${key} has been set`);
} else {
console.error(`${key} failed to set`);
}
console.log(await get(conn, key));
console.log(await get(conn, keyInexistent));

A sample run of the above code:

$ deno run --allow-net=:6379 app.ts 
key1 has been set
val1
undefined

--

--