Getting started with Rust and Redis
Tags: rust, redis, programming, showdev
Published: 2021-01-20 12:35:46

Are you learning [Rust](https://www.rust-lang.org/) and looking for ways to get some hands-on practice with concrete examples? A good approach might be to try and integrate Rust with external systems. Why not try to use it with [Redis](https://redis.io/)? It is a powerful, versatile database but dead simple to get started with!

In this blog post, you will learn how to use the Rust programming language to interact with Redis using the [redis-rs](https://github.com/mitsuhiko/redis-rs) client. We will walk through commonly used Redis data structures such as [String](https://redis.io/topics/data-types-intro#redis-strings), [Hash](https://redis.io/topics/data-types-intro#redis-hashes), [List](https://redis.io/topics/data-types-intro#redis-lists) etc. The Redis client used in the sample code exposes both high and low-level APIs and you will see both these styles in action.

> Code is available on GitHub - https://github.com/abhirockzz/rust-redis-101

Redis is an in-memory data structure store which is often used as a database, cache, and message broker. It provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams. 

In this blog post, I have included instructions for [Azure Cache for Redis](https://docs.microsoft.com/azure/azure-cache-for-redis/?WT.mc_id=data-12975-abhishgu), but the sample application should work with any Redis instance. Azure Cache for Redis offers both the Redis open-source and a [commercial product from Redis Labs](https://docs.microsoft.com/azure/azure-cache-for-redis/quickstart-create-redis-enterprise?WT.mc_id=data-12975-abhishgu) as a managed service.

## Pre-requisites

You will need [Rust](https://www.rust-lang.org/tools/install) (version 1.39 or above) installed on your computer. If you intend to use Azure Cache for Redis, simply [create a free Azure subscription](https://azure.microsoft.com/free/), and [setup an Azure Cache for Redis instance](https://docs.microsoft.com/azure/azure-cache-for-redis/quickstart-create-redis?WT.mc_id=data-12975-abhishgu) using the Azure portal. 

> For the purposes of this tutorial, I would recommend setting up a [Basic tier instance](https://docs.microsoft.com/azure/azure-cache-for-redis/cache-overview?WT.mc_id=data-12975-abhishgu#service-tiers) which is ideal for development/test and non-critical workloads.

You can also choose to simply use the Redis Docker container as such:

```bash
docker run --rm -p 6379:6379 redis
```

That's it, you're ready to get started!

## Code walk through

For you to get a better understanding, this section covers a step-by-step walk through of the code. It covers all the functions, each of which covers a specific Redis data structure. It is followed by the **Run the sample application** section.

### Connect to Redis

The `connect` function is used to establish a connection to Redis. It expects host name and the password to be passed in as environment variables `REDIS_HOSTNAME` and `REDIS_PASSWORD` respectively. The format for the connection URL is `<uri scheme>://<username>:<password>@<hostname>`. 

> Azure Cache for Redis only accepts secure connections with [TLS 1.2 as the minimum required version](https://docs.microsoft.com/azure/azure-cache-for-redis/cache-remove-tls-10-11?WT.mc_id=data-12975-abhishgu). The URI scheme would be `rediss` in case of a TLS connection, otherwise it's `redis`.

The call to [redis::Client::open](https://docs.rs/redis/0.19.0/redis/struct.Client.html#method.open) performs basic validation while [get_connection()](https://docs.rs/redis/0.19.0/redis/struct.Client.html#method.get_connection) actually initiates the connection - the program exits if the connectivity fails due to any reason such as an incorrect password.

```rust
fn connect() -> redis::Connection {
    //format - host:port
    let redis_host_name =
        env::var("REDIS_HOSTNAME").expect("missing environment variable REDIS_HOSTNAME");
    
    let redis_password = env::var("REDIS_PASSWORD").unwrap_or_default();

    //if Redis server needs secure connection
    let uri_scheme = match env::var("IS_TLS") {
        Ok(_) => "rediss",
        Err(_) => "redis",
    };

    let redis_conn_url = format!("{}://:{}@{}", uri_scheme, redis_password, redis_host_name);

    redis::Client::open(redis_conn_url)
        .expect("Invalid connection URL")
        .get_connection()
        .expect("failed to connect to Redis")
}
```

### Basic operations on Strings and Integers

This function covers [SET](https://redis.io/commands/set), [GET](https://redis.io/commands/get), and [INCR](https://redis.io/commands/incr) commands. The low-level API is used for `SET` and `GET`, which sets and retrieves the value for a key named `foo`. The `INCRBY` command is executed using a high-level API i.e. [incr](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.incr) increments the value of a key (named `counter`) by `2` followed by a call to [get](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.get) to retrieve it.

```rust
fn basics() {
    let mut conn = connect();
    let _: () = redis::cmd("SET")
        .arg("foo")
        .arg("bar")
        .query(&mut conn)
        .expect("failed to execute SET for 'foo'");

    let bar: String = redis::cmd("GET")
        .arg("foo")
        .query(&mut conn)
        .expect("failed to execute GET for 'foo'");
    println!("value for 'foo' = {}", bar);

    let _: () = conn
        .incr("counter", 2)
        .expect("failed to execute INCR for 'counter'");
    let val: i32 = conn
        .get("counter")
        .expect("failed to execute GET for 'counter'");
    println!("counter = {}", val);
}
```

### How to use a Hash data structure?

The below code snippet demonstrates the functionality of a Redis `HASH` data structure. [HSET](https://redis.io/commands/hset) is invoked using the low-level API to store information (`name`, `version`, `repo`) about Redis drivers (clients). For example, details for the Rust driver (one being used in this sample code!) is captured in form of a [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html) and then passed on to the low-level API. It is then retrieved using [HGETALL](https://redis.io/commands/hgetall).

`HSET` can also be executed using a high-level API using [hset_multiple](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.hset_multiple) that accepts an array of tuples. [hget](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.hget) is then executed to fetch the value for a single attribute (the `repo` in this case).

```rust
fn hash() {
    let mut conn = connect();

    let mut driver: BTreeMap<String, String> = BTreeMap::new();
    let prefix = "redis-driver";
    driver.insert(String::from("name"), String::from("redis-rs"));
    driver.insert(String::from("version"), String::from("0.19.0"));
    driver.insert(
        String::from("repo"),
        String::from("https://github.com/mitsuhiko/redis-rs"),
    );

    let _: () = redis::cmd("HSET")
        .arg(format!("{}:{}", prefix, "rust"))
        .arg(driver)
        .query(&mut conn)
        .expect("failed to execute HSET");

    let info: BTreeMap<String, String> = redis::cmd("HGETALL")
        .arg(format!("{}:{}", prefix, "rust"))
        .query(&mut conn)
        .expect("failed to execute HGETALL");
    println!("info for rust redis driver: {:?}", info);

    let _: () = conn
        .hset_multiple(
            format!("{}:{}", prefix, "go"),
            &[
                ("name", "go-redis"),
                ("version", "8.4.6"),
                ("repo", "https://github.com/go-redis/redis"),
            ],
        )
        .expect("failed to execute HSET");

    let repo_name: String = conn
        .hget(format!("{}:{}", prefix, "go"), "repo")
        .expect("HGET failed");
    println!("go redis driver repo name: {:?}", repo_name);
}
```

### Using Redis Lists

In the function below, you can see how to use a `LIST` data structure. [LPUSH](https://redis.io/commands/lpush) is executed (with the low-level API) to add an entry to the list and the high-level [lpop](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.lpop) method is used to retrieve that from the list. Then, the [rpush](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.rpush) method is used to add a couple of entries to the list which are then fetched using the low-level [lrange](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.lrange) method.

```rust
fn list() {
    let mut conn = connect();
    let list_name = "items";

    let _: () = redis::cmd("LPUSH")
        .arg(list_name)
        .arg("item-1")
        .query(&mut conn)
        .expect("failed to execute LPUSH for 'items'");

    let item: String = conn
        .lpop(list_name)
        .expect("failed to execute LPOP for 'items'");
    println!("first item: {}", item);

    let _: () = conn.rpush(list_name, "item-2").expect("RPUSH failed");
    let _: () = conn.rpush(list_name, "item-3").expect("RPUSH failed");

    let len: isize = conn
        .llen(list_name)
        .expect("failed to execute LLEN for 'items'");
    println!("no. of items in list = {}", len);

    let items: Vec<String> = conn
        .lrange(list_name, 0, len - 1)
        .expect("failed to execute LRANGE for 'items'");

    println!("listing items in list");
    for item in items {
        println!("item: {}", item)
    }
}
```

### Store unique items in a Redis Set

Here you can see some of the `SET` operations. The [sadd](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.sadd) (high-level API) method is used to add couple of entries to a `SET` named `users`. [SISMEMBER](https://redis.io/commands/hset) is then executed (low-level API) to check whether `user1` exists. Finally, [smembers](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.smembers) is used to fetch and iterate over all the set entries in the form of a Vector ([Vec<String>](https://doc.rust-lang.org/std/vec/struct.Vec.html)).

```rust
fn set() {
    let mut conn = connect();
    let set_name = "users";

    let _: () = conn
        .sadd(set_name, "user1")
        .expect("failed to execute SADD for 'users'");
    let _: () = conn
        .sadd(set_name, "user2")
        .expect("failed to execute SADD for 'users'");

    let ismember: bool = redis::cmd("SISMEMBER")
        .arg(set_name)
        .arg("user1")
        .query(&mut conn)
        .expect("failed to execute SISMEMBER for 'users'");
    println!("does user1 exist in the set? {}", ismember);

    let users: Vec<String> = conn.smembers(set_name).expect("failed to execute SMEMBERS");
    println!("listing users in set");

    for user in users {
        println!("user: {}", user)
    }
}
```

### Take advantage of Redis Sorted Sets

`sorted_set` function below demonstrates the Sorted Set data structure. [ZADD](https://redis.io/commands/zadd) is invoked (with the low-level API) to add a random integer score for a player (`player-1`). Next, the [zadd](https://docs.rs/redis/0.19.0/redis/trait.Commands.html#method.zadd) method (high-level API) is used to add more players (`player-2` to `player-5`) and their respective (randomly generated) scores. The number of entries in the sorted set is figured out using [ZCARD](https://redis.io/commands/zcard) and that's used as the limit to the [ZRANGE](https://redis.io/commands/zrange) command (invoked with the low-level API) to list out the players with their scores in ascending order.

```rust
fn sorted_set() {
    let mut conn = connect();
    let sorted_set = "leaderboard";

    let _: () = redis::cmd("ZADD")
        .arg(sorted_set)
        .arg(rand::thread_rng().gen_range(1..10))
        .arg("player-1")
        .query(&mut conn)
        .expect("failed to execute ZADD for 'leaderboard'");

    for num in 2..=5 {
        let _: () = conn
            .zadd(
                sorted_set,
                String::from("player-") + &num.to_string(),
                rand::thread_rng().gen_range(1..10),
            )
            .expect("failed to execute ZADD for 'leaderboard'");
    }

    let count: isize = conn
        .zcard(sorted_set)
        .expect("failed to execute ZCARD for 'leaderboard'");

    let leaderboard: Vec<(String, isize)> = conn
        .zrange_withscores(sorted_set, 0, count - 1)
        .expect("ZRANGE failed");

    println!("listing players and scores in ascending order");

    for item in leaderboard {
        println!("{} = {}", item.0, item.1)
    }
}
```

Now that you've gone through the code, it's time to run the application and check the output.

## Run the sample application

Start by cloning the GitHub repo:

```bash
git clone https://github.com/abhirockzz/rust-redis-101.git
cd rust-redis-101
```

If you're using a local Redis server (e.g. with Docker) over an insecure connection without any password, simply use:

```bash
export REDIS_HOSTNAME=localhost:6379
```

If you're using Azure Cache for Redis, [fetch the Host name and Access Keys](https://docs.microsoft.com/azure/azure-cache-for-redis/cache-python-get-started?WT.mc_id=data-12975-abhishgu#retrieve-host-name-ports-and-access-keys-from-the-azure-portal) from the [Azure portal](https://portal.azure.com/). Set the respective environment variables:

```bash
export REDIS_HOSTNAME=<Host name>:<port> (e.g. <name of cache>.redis.cache.windows.net:6380)
export REDIS_PASSWORD=<Primary Access Key>
export IS_TLS=true
```

To run the application:

```bash
cargo run
```
    
You will see an output as such:

```bash
******* Running SET, GET, INCR commands *******
value for 'foo' = bar
counter = 29
******* Running HASH commands *******
info for rust redis driver: {"name": "redis-rs", "repo": "https://github.com/mitsuhiko/redis-rs", "version": "0.19.0"}
go redis driver repo name: "https://github.com/go-redis/redis"
******* Running LIST commands *******
first item: item-1
no. of items in list = 2
listing items in list
item: item-2
item: item-3
******* Running SET commands *******
does user1 exist in the set? true
listing users in set
user: user2
user: user1
user: user3
******* Running SORTED SET commands *******
listing players and scores
player-4 = 3
player-3 = 7
player-1 = 8
player-2 = 8
player-5 = 8
```

> Important: If you were using Azure Cache for Redis, please ensure that you delete the Redis instance.

## Conclusion

In this tutorial, you learned how to use the Rust driver for Redis to connect and execute operations in Azure Cache for Redis. If you found this helpful, you may want to explore these additional resources:

- [An introduction to Redis data types and abstractions](https://redis.io/topics/data-types-intro)
- Read [The Rust Book](https://doc.rust-lang.org/book/index.html)
- [Try small exercises to get you used to reading and writing Rust code](https://github.com/rust-lang/rustlings)
- [Considerations for designing and using a cache](https://docs.microsoft.com/azure/architecture/best-practices/caching?toc=%2fazure%2fredis-cache%2ftoc.json&WT.mc_id=data-12975-abhishgu)
- [Best practices for Azure Cache for Redis](https://docs.microsoft.com/azure/azure-cache-for-redis/cache-best-practices?WT.mc_id=data-12975-abhishgu)
- [High availability for Azure Cache for Redis](https://docs.microsoft.com/azure/azure-cache-for-redis/cache-high-availability?WT.mc_id=data-12975-abhishgu)