Standard library · Database

db/redis

A Redis client: keys and values, hashes, lists, sets and publishing.

import db.redis

Redis is an in memory database for caches, sessions, counters, queues and leaderboards. This package speaks the Redis protocol directly over a socket, with no outside library: one function per common command for strings, hashes, lists and sets, plus Command, which sends any other command with its arguments and hands back the raw reply. This page starts with a tutorial; the full reference of every function and type comes after it.

Tutorial: a link shortener in twelve steps

The steps below build the storage of a small link shortener: link records, click counters, a list of recent links, unique visitors and a leaderboard. Each step is a complete program, and the output under it is what it printed against a real Redis 7.4. Run them in order, because each step uses the keys the earlier ones left behind. The last step deletes everything again.

First start a server with a password. With Docker that is one command:

docker run -p 6379:6379 redis:7.4 redis-server --requirepass s3cret

The programs connect to 127.0.0.1:6379 and log in with s3cret. To start over at any point, run docker exec on the container with redis-cli -a s3cret flushall, then begin again at step 1.

1. Connect and log in

Connect opens one TCP connection and returns a Client. Ok tells you whether that worked and Error says why not. A server with a password refuses every command until Auth succeeds, which is why the first Ping is false. Select switches to another numbered database; a default server has 16, numbered 0 to 15.

db-redis-connect.salam
import db.redis

func main:
    c := redis.Connect("127.0.0.1", 6379)
    if not redis.Ok(c):
        println "cannot reach Redis:", redis.Error(c)
        ret 1
    end
    defer redis.Close(c)

    println "ping before login:", redis.Ping(c)
    println "wrong password:", redis.Auth(c, "guess")
    println "right password:", redis.Auth(c, "s3cret")
    println "ping after login:", redis.Ping(c)

    println "database 15:", redis.Select(c, 15)
    println "database 99:", redis.Select(c, 99)
    println "back to 0:", redis.Select(c, 0)

    off := redis.Connect("127.0.0.1", 6390)
    println "port 6390:", redis.Ok(off), redis.Error(off)
end
Outputping before login: false wrong password: false right password: true ping after login: true database 15: true database 99: false back to 0: true port 6390: false no connection

Every function in this package returns a plain value (a bool, a number, a string or a vector) instead of an error object. When something goes wrong you get the empty value: false, 0 or an empty string. Step 10 shows how to tell the cases apart when that matters. Always Close a client: it closes the socket and frees the buffer.

2. Strings and missing keys

The simplest Redis value is a string under a key. Set stores it, Get reads it back and Append adds to the end. Redis has no NULL: a key is either there or not. Get returns an empty string in both cases, so when an empty value is meaningful, ask Exists, or send GET through Command and test the reply with ReplyIsNil.

db-redis-strings.salam
import db.redis
import str

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    redis.Set(c, "site:name", "Short links")
    redis.Set(c, "site:motto", "")
    println "name:", redis.Get(c, "site:name")
    println "motto: [" + redis.Get(c, "site:motto") + "]", redis.Exists(c, "site:motto")
    println "owner: [" + redis.Get(c, "site:owner") + "]", redis.Exists(c, "site:owner")

    mut args := Vector {} as Vector<str>
    args.push("GET")
    args.push("site:owner")
    r := redis.Command(c, args)
    println "GET site:owner is nil:", redis.ReplyIsNil(r)
    redis.FreeReply(r)

    tricky := "x\r\nFLUSHALL\r\n"
    redis.Set(c, "site:tricky", tricky)
    println "stored bytes:", redis.StrLen(c, "site:tricky"), "of", tricky.len()
    println "same text back:", str.Equals(redis.Get(c, "site:tricky"), tricky)
    println "name still there:", redis.Exists(c, "site:name")
    redis.Del(c, "site:tricky")

    println "length after append:", redis.Append(c, "site:name", " for Salam")
    println "name:", redis.Get(c, "site:name")
end
Outputname: Short links motto: [] true owner: [] false GET site:owner is nil: true stored bytes: 13 of 13 same text back: true name still there: true length after append: 21 name: Short links for Salam

There is no injection to worry about. Each argument travels as a length followed by its bytes, so a value with spaces, quotes or line breaks is stored exactly as it is and can never be read as a second command. The one limit is the NUL byte: Salam strings end at the first one, so keep binary data out of these functions or encode it first, with Base64EncodeBytes from encoding for instance.

3. Records in hashes

A hash holds named fields under one key, which is the natural place for a record. Incr on a counter key hands out ids that never repeat, even with many programs adding links at once. HSet writes one field, HGetAll reads them all into a HashMap, and from there it is one line to fill your own struct. Every value comes back as a string, so convert numbers yourself.

db-redis-hashes.salam
import db.redis
import conv

struct Link:
    pub id: i64 = 0
    pub url: str = ""
    pub title: str = ""
    pub owner: str = ""
end

func save(c: redis.Client, url: str, title: str, owner: str): i64:
    id := redis.Incr(c, "links:next")
    key := "link:" + conv.FormatInt(id)
    redis.HSet(c, key, "url", url)
    redis.HSet(c, key, "title", title)
    redis.HSet(c, key, "owner", owner)
    ret id
end

func load(c: redis.Client, id: i64): Link:
    m := redis.HGetAll(c, "link:" + conv.FormatInt(id))
    if m.size() == 0:
        ret Link {}
    end
    ret Link { id = id, url = m.get("url"), title = m.get("title"), owner = m.get("owner") }
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    save(c, "https://salamlang.ir", "Salam home", "sara")
    save(c, "https://salamlang.ir/learn/", "Salam course", "omid")
    save(c, "https://github.com/SalamLang/Salam", "Salam on GitHub", "sara")

    repeat 3 in i:
        l := load(c, (i + 1) as i64)
        println l.id, l.title, "->", l.url, "by", l.owner
    end
    println "link 9 found:", load(c, 9).id != 0

    println "fields of link:2:", redis.HLen(c, "link:2")
    println "owner of link:2:", redis.HGet(c, "link:2", "owner")
    redis.HSet(c, "link:2", "note", "for new students")
    println "has note:", redis.HExists(c, "link:2", "note")
    println "note removed:", redis.HDel(c, "link:2", "note")
    println "has note:", redis.HExists(c, "link:2", "note")
end
Output1 Salam home -> https://salamlang.ir by sara 2 Salam course -> https://salamlang.ir/learn/ by omid 3 Salam on GitHub -> https://github.com/SalamLang/Salam by sara link 9 found: false fields of link:2: 3 owner of link:2: omid has note: true note removed: true has note: false

HGetAll returns an empty map for a key that does not exist, which is how load tells a missing link apart. To write several fields in one round trip, send HSET key f1 v1 f2 v2 with Command, as step 9 does.

4. Counters

Incr, IncrBy and Decr change a number stored as a string and return the new value. Redis runs each command alone, so two programs counting the same key never lose an update. A key that does not exist counts as 0, so there is nothing to set up first. The value is still a string: Get returns its digits and conv.ParseInt turns them into a number.

db-redis-counters.salam
import db.redis
import conv

func click(c: redis.Client, id: int): i64:
    ret redis.Incr(c, "clicks:" + conv.FormatInt(id as i64))
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    click(c, 1)
    click(c, 1)
    click(c, 3)
    click(c, 3)
    println "link 1 clicks:", click(c, 1)
    println "link 2 imported clicks:", redis.IncrBy(c, "clicks:2", 40)
    println "link 2 after a refund:", redis.IncrBy(c, "clicks:2", -2)
    println "link 3 after a bot click is removed:", redis.Decr(c, "clicks:3")

    raw := redis.Get(c, "clicks:2")
    println "stored as text:", raw, "type", redis.Type(c, "clicks:2")
    n := conv.ParseInt(raw)
    println "as a number plus one:", n + 1

    println "next link id would be:", conv.ParseInt(redis.Get(c, "links:next")) + 1
end
Outputlink 1 clicks: 3 link 2 imported clicks: 40 link 2 after a refund: 38 link 3 after a bot click is removed: 1 stored as text: 38 type string as a number plus one: 39 next link id would be: 4

5. Lists

A list keeps strings in order. LPush adds to the front and RPush to the end; LPop and RPop take one off. LRange reads a slice by position, both ends included, and negative positions count from the end, so 0, -1 is the whole list. Pushing new ids to the front gives a newest first feed.

db-redis-lists.salam
import db.redis
import conv

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    repeat 3 in i:
        redis.LPush(c, "links:recent", conv.FormatInt((i + 1) as i64))
    end
    println "recent count:", redis.LLen(c, "links:recent")

    each id in redis.LRange(c, "links:recent", 0, -1):
        println "  link", id, redis.HGet(c, "link:" + id, "title")
    end

    println "newest two:", redis.LRange(c, "links:recent", 0, 1).len()
    println "oldest:", redis.LRange(c, "links:recent", -1, -1).get(0)

    redis.RPush(c, "links:review", "2")
    redis.RPush(c, "links:review", "3")
    println "review first:", redis.LPop(c, "links:review")
    println "review last:", redis.RPop(c, "links:review")
    println "review empty: [" + redis.LPop(c, "links:review") + "]", redis.LLen(c, "links:review")
end
Outputrecent count: 3 link 3 Salam on GitHub link 2 Salam course link 1 Salam home newest two: 2 oldest: 1 review first: 2 review last: 3 review empty: [] 0

A list that becomes empty is deleted, and popping from it returns an empty string. To keep a feed at a fixed length, follow each push with LTRIM links:recent 0 99 through Command.

6. Sets

A set holds each member once. SAdd returns 1 for a new member and 0 for one that was already there, which makes it a one step "have I seen this?" check. SCard counts the members, SIsMember tests one and SMembers returns them all.

db-redis-sets.salam
import db.redis
import sort

func visit(c: redis.Client, link: str, user: str): bool:
    ret redis.SAdd(c, "visitors:" + link, user) == 1
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    println "sara first visit:", visit(c, "1", "sara")
    println "omid first visit:", visit(c, "1", "omid")
    println "sara again:", visit(c, "1", "sara")
    visit(c, "1", "reza")
    visit(c, "3", "sara")

    println "unique visitors of link 1:", redis.SCard(c, "visitors:1")
    println "reza saw link 3:", redis.SIsMember(c, "visitors:3", "reza")

    println "reza removed:", redis.SRem(c, "visitors:1", "reza")
    names := redis.SMembers(c, "visitors:1")
    sort.Sort(names)
    each n in names:
        println "  -", n
    end
end
Outputsara first visit: true omid first visit: true sara again: false unique visitors of link 1: 3 reza saw link 3: false reza removed: 1 - omid - sara

Redis does not keep sets in any order, so sort what SMembers returns before you show it. On a set with millions of members, use SSCAN through Command instead.

7. Expiry

Any key can be given a lifetime. SetEx stores a string and its lifetime in one command; Expire adds one to a key that already exists. TTL returns the seconds left, -1 for a key that never expires and -2 for one that is not there. When the time is up Redis deletes the key by itself.

db-redis-expire.salam
import db.redis
import time

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    redis.SetEx(c, "preview:3", "<b>Salam on GitHub</b>", 600)
    println "preview ttl:", redis.TTL(c, "preview:3")
    println "link ttl:", redis.TTL(c, "link:3")
    println "missing ttl:", redis.TTL(c, "link:99")

    redis.Set(c, "link:3:promo", "half price")
    println "promo expires:", redis.Expire(c, "link:3:promo", 1)
    println "promo now:", redis.Get(c, "link:3:promo")
    time.Sleep(1500)
    println "promo later: [" + redis.Get(c, "link:3:promo") + "]", redis.Exists(c, "link:3:promo")
    println "expire a missing key:", redis.Expire(c, "link:3:promo", 10)

    redis.Set(c, "preview:3", "<b>new preview</b>")
    println "ttl after Set:", redis.TTL(c, "preview:3")
end
Outputpreview ttl: 600 link ttl: -1 missing ttl: -2 promo expires: true promo now: half price promo later: [] false expire a missing key: false ttl after Set: -1

Watch the last line: a plain Set on a key removes its lifetime. Use SetEx again when you replace a value that should keep expiring.

8. Any command: a leaderboard

Redis has about two hundred commands and this package has a function for the most common ones. Command sends any of them: put the command name and its arguments in a Vector<str>. The Reply you get back has a type (REPLY_STRING, REPLY_INTEGER, REPLY_ARRAY, REPLY_NIL, REPLY_STATUS or REPLY_ERROR), and you read it with ReplyStr, ReplyInt, ReplyArrayLen and ReplyArrayGet. This step uses a sorted set, which has no function of its own, to rank the links by clicks.

db-redis-command.salam
import db.redis
import conv

func run(c: redis.Client, words: str[]): redis.Reply:
    mut args := Vector {} as Vector<str>
    each w in words:
        args.push(w)
    end
    ret redis.Command(c, args)
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    repeat 3 in i:
        id := conv.FormatInt((i + 1) as i64)
        r := run(c, ["ZADD", "links:top", redis.Get(c, "clicks:" + id), id])
        redis.FreeReply(r)
    end

    top := run(c, ["ZREVRANGE", "links:top", "0", "-1", "WITHSCORES"])
    if redis.ReplyType(top) == redis.REPLY_ARRAY:
        mut i := 0
        until i < redis.ReplyArrayLen(top):
            id := redis.ReplyStr(redis.ReplyArrayGet(top, i))
            score := redis.ReplyStr(redis.ReplyArrayGet(top, i + 1))
            println score, "clicks:", redis.HGet(c, "link:" + id, "title")
            i += 2
        end
    end
    redis.FreeReply(top)

    rank := run(c, ["ZREVRANK", "links:top", "1"])
    println "link 1 rank:", redis.ReplyInt(rank) + 1, "integer reply:", redis.ReplyType(rank) == redis.REPLY_INTEGER
    redis.FreeReply(rank)

    many := run(c, ["HMGET", "link:1", "title", "owner", "color"])
    repeat redis.ReplyArrayLen(many) in i:
        e := redis.ReplyArrayGet(many, i)
        println "  field", i, "[" + redis.ReplyStr(e) + "]", "nil:", redis.ReplyIsNil(e)
    end
    redis.FreeReply(many)
end
Output38 clicks: Salam course 3 clicks: Salam home 1 clicks: Salam on GitHub link 1 rank: 2 integer reply: true field 0 [Salam home] nil: false field 1 [sara] nil: false field 2 [] nil: true

Free every reply from Command with FreeReply. The elements from ReplyArrayGet belong to their array and are freed with it, so never free them on their own. A field that is missing comes back as a nil element, as color does above.

9. Transactions

MULTI starts a transaction: the commands after it answer QUEUED, and EXEC runs them all at once, with nothing from other clients in between. DISCARD throws the queue away. Redis does not roll back: if one queued command fails, the others still run, so check each element of the EXEC reply when that matters.

db-redis-tx.salam
import db.redis
import conv

func run(c: redis.Client, words: str[]): str:
    mut args := Vector {} as Vector<str>
    each w in words:
        args.push(w)
    end
    r := redis.Command(c, args)
    mut out := redis.ReplyStr(r)
    if redis.ReplyType(r) == redis.REPLY_ARRAY:
        out = "array of " + conv.FormatInt(redis.ReplyArrayLen(r) as i64)
    else if redis.ReplyIsNil(r):
        out = "nil"
    else if redis.ReplyType(r) == redis.REPLY_INTEGER:
        out = conv.FormatInt(redis.ReplyInt(r))
    end
    redis.FreeReply(r)
    ret out
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    println run(c, ["MULTI"])
    println run(c, ["INCR", "links:next"])
    println run(c, ["HSET", "link:4", "url", "https://salamlang.ir/std/", "title", "Salam std", "owner", "omid"])
    println run(c, ["LPUSH", "links:recent", "4"])
    println run(c, ["EXEC"])
    println "link 4:", redis.HGet(c, "link:4", "title"), "next id:", redis.Get(c, "links:next")

    other := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(other)
    redis.Auth(other, "s3cret")

    println run(c, ["WATCH", "link:4"])
    title := redis.HGet(c, "link:4", "title")
    redis.HSet(other, "link:4", "title", "Salam standard library")
    println run(c, ["MULTI"])
    println run(c, ["HSET", "link:4", "title", title + " (checked)"])
    println "exec after a change:", run(c, ["EXEC"])
    println "title kept:", redis.HGet(c, "link:4", "title")

    println run(c, ["MULTI"])
    println run(c, ["INCR", "clicks:4"])
    println run(c, ["DISCARD"])
    println "clicks:4 exists:", redis.Exists(c, "clicks:4")
end
OutputOK QUEUED QUEUED QUEUED array of 3 link 4: Salam std next id: 4 OK OK QUEUED exec after a change: nil title kept: Salam standard library OK QUEUED OK clicks:4 exists: false

WATCH adds optimistic locking. If another client changes a watched key before EXEC, the transaction does not run and EXEC answers nil; read the key again and retry. The second client here plays that other user. A transaction lives on its connection, so keep MULTI through EXEC on one client and one thread.

10. Errors

There are two kinds of failure. The server can refuse a command, for a wrong type or a bad argument; the connection stays fine and the reply has type REPLY_ERROR with the message in ReplyStr. The shortcut functions do not show that message: Get on a hash returns an empty string and Incr on text returns 0. When you need to know, send the command with Command. The other kind is a lost connection, which this step causes on purpose with CLIENT KILL from a second client.

db-redis-errors.salam
import db.redis

func run(c: redis.Client, words: str[]): str:
    mut args := Vector {} as Vector<str>
    each w in words:
        args.push(w)
    end
    r := redis.Command(c, args)
    mut out := "ok"
    if not redis.ReplyOk(r):
        out = "no reply: " + redis.Error(c)
    else if redis.ReplyIsError(r):
        out = "server error: " + redis.ReplyStr(r)
    end
    redis.FreeReply(r)
    ret out
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    println "Get on a hash: [" + redis.Get(c, "link:1") + "]"
    println "Incr on text:", redis.Incr(c, "site:name")
    println run(c, ["GET", "link:1"])
    println run(c, ["INCR", "site:name"])
    println run(c, ["HGET", "link:1"])
    println "connection still fine:", redis.Ping(c), "[" + redis.Error(c) + "]"

    admin := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(admin)
    redis.Auth(admin, "s3cret")
    println run(admin, ["CLIENT", "KILL", "TYPE", "normal", "SKIPME", "yes"])

    println "ping after the kill:", redis.Ping(c)
    println "Ok:", redis.Ok(c), "Error:", redis.Error(c)
    println run(c, ["PING"])
end
OutputGet on a hash: [] Incr on text: 0 server error: WRONGTYPE Operation against a key holding the wrong kind of value server error: ERR value is not an integer or out of range server error: ERR wrong number of arguments for 'hget' command connection still fine: true [] ok ping after the kill: false Ok: true Error: Server closed the connection no reply: Server closed the connection

After the connection is gone Ok still says true, because it only reports whether Connect worked. Error now holds the reason, every call returns its empty value and Command returns a reply that is not ReplyOk. The client does not reconnect by itself: close it and call Connect again.

11. Several threads

A Client is one connection with one read buffer, so two threads must never use the same client at once. Give each thread its own, as below. There is no built in pool; for a server that handles many requests, open one client per worker thread and keep it for the life of the thread. The counter comes out exact because Redis applies each INCR alone.

db-redis-threads.salam
import db.redis

const WORKERS := 4
const CLICKS := 250

func worker():
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")
    repeat CLICKS:
        redis.Incr(c, "clicks:4")
        redis.SAdd(c, "visitors:4", "bot")
    end
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")
    redis.Del(c, "clicks:4")

    mut threads := Vector {} as Vector<i64>
    repeat WORKERS:
        threads.push(spawn(worker))
    end
    each t in threads:
        join(t)
    end

    println "clicks on link 4:", redis.Get(c, "clicks:4"), "expected", WORKERS * CLICKS
    println "unique visitors:", redis.SCard(c, "visitors:4")
end
Outputclicks on link 4: 1000 expected 1000 unique visitors: 1

12. Look around and clean up

Keys lists the keys matching a pattern and Type says what each one holds. Keys walks the whole database in one go and blocks the server meanwhile, which is fine here and a problem on a production server with millions of keys. SCAN walks it a page at a time with a cursor; the loop below runs it through Command. FlushDB deletes every key in the current database.

db-redis-cleanup.salam
import db.redis
import sort

func scan(c: redis.Client, pattern: str): Vector<str>:
    mut found := Vector {} as Vector<str>
    mut cursor := "0"
    until true:
        mut args := Vector {} as Vector<str>
        args.push("SCAN")
        args.push(cursor)
        args.push("MATCH")
        args.push(pattern)
        args.push("COUNT")
        args.push("100")
        r := redis.Command(c, args)
        cursor = redis.ReplyStr(redis.ReplyArrayGet(r, 0))
        page := redis.ReplyArrayGet(r, 1)
        repeat redis.ReplyArrayLen(page) in i:
            found.push(redis.ReplyStr(redis.ReplyArrayGet(page, i)))
        end
        redis.FreeReply(r)
        if cursor == "0":
            break
        end
    end
    sort.Sort(found)
    ret found
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    keys := redis.Keys(c, "*")
    sort.Sort(keys)
    each k in keys:
        println k, redis.Type(c, k)
    end

    old := scan(c, "visitors:*")
    each k in old:
        redis.Del(c, k)
    end
    println "deleted", old.len(), "visitor sets, left:", scan(c, "visitors:*").len()

    println "flush:", redis.FlushDB(c), "keys left:", redis.Keys(c, "*").len()
end
Outputclicks:1 string clicks:2 string clicks:3 string clicks:4 string link:1 hash link:2 hash link:3 hash link:4 hash links:next string links:recent list links:top zset preview:3 string site:motto string site:name string visitors:1 set visitors:3 set visitors:4 set deleted 3 visitor sets, left: 0 flush: true keys left: 0

More

  • Publish sends a message to every client subscribed to a channel and returns how many got it. This package can publish but cannot subscribe: Command reads one reply per command, and a subscriber needs to wait for messages that arrive on their own. For work that must not be lost, use a list as a queue (see below).
  • For an ACL user with a name, send AUTH name password with Command; Auth sends only the password.
  • Commands such as HINCRBY, SETNX, MGET, LTRIM, ZADD and XADD all go through Command in the same way as step 8.
  • The connection waits as long as the server takes. Commands that block on purpose, such as BLPOP, take their own timeout argument.

Guide: cache a slow page

The most common use of Redis is a cache in front of something slow. Look for the key first; on a miss, build the value and store it with SetEx, so a stale copy disappears by itself. When the source changes, delete the key and the next request rebuilds it. Exists rather than an empty Get decides the hit, so an empty page can be cached too.

db-redis-cache.salam
import db.redis

mut renders := 0

func render(id: str): str:
    renders += 1
    ret "<h1>Note " + id + "</h1>"
end

func page(c: redis.Client, id: str): str:
    key := "cache:note:" + id
    if redis.Exists(c, key):
        ret redis.Get(c, key)
    end
    html := render(id)
    redis.SetEx(c, key, html, 60)
    ret html
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    println page(c, "42")
    println page(c, "42")
    println page(c, "7")
    println "rendered", renders, "times for 3 requests"
    println "seconds left:", redis.TTL(c, "cache:note:42")

    redis.Del(c, "cache:note:42")
    println page(c, "42")
    println "rendered", renders, "times after the edit"
    redis.Del(c, "cache:note:42")
    redis.Del(c, "cache:note:7")
end
Output<h1>Note 42</h1> <h1>Note 42</h1> <h1>Note 7</h1> rendered 2 times for 3 requests seconds left: 60 <h1>Note 42</h1> rendered 3 times after the edit

Guide: limit requests per user

A fixed window rate limiter needs one counter per user. Incr returns the count, and the first request of a window sets the lifetime, so the counter resets by itself when the window ends. Because Incr is atomic, the limit holds even when many server processes check the same user.

db-redis-ratelimit.salam
import db.redis

func allow(c: redis.Client, user: str, limit: i64, seconds: int): bool:
    key := "rate:" + user
    n := redis.Incr(c, key)
    if n == 1:
        redis.Expire(c, key, seconds)
    end
    ret n <= limit
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    repeat 5 in i:
        println "sara request", i + 1, "allowed:", allow(c, "sara", 3, 60)
    end
    println "omid request 1 allowed:", allow(c, "omid", 3, 60)
    println "sara window ends in:", redis.TTL(c, "rate:sara"), "seconds"
    redis.Del(c, "rate:sara")
    redis.Del(c, "rate:omid")
end
Outputsara request 1 allowed: true sara request 2 allowed: true sara request 3 allowed: true sara request 4 allowed: false sara request 5 allowed: false omid request 1 allowed: true sara window ends in: 60 seconds

Guide: a job queue with a worker

A list makes a simple job queue: producers RPush jobs and a worker takes them from the front. LPop returns at once, so a worker that polls it wastes time. BLPOP, sent through Command, waits until a job arrives or the timeout in seconds runs out, and answers nil on a timeout. The worker runs in its own thread with its own client, as step 11 requires.

db-redis-queue.salam
import db.redis

func take(c: redis.Client, queue: str, seconds: str): str:
    mut args := Vector {} as Vector<str>
    args.push("BLPOP")
    args.push(queue)
    args.push(seconds)
    r := redis.Command(c, args)
    mut job := ""
    if redis.ReplyType(r) == redis.REPLY_ARRAY:
        job = redis.ReplyStr(redis.ReplyArrayGet(r, 1))
    end
    redis.FreeReply(r)
    ret job
end

func worker():
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")
    mut job := take(c, "queue:email", "1")
    until job.len() > 0:
        redis.RPush(c, "done:email", "sent " + job)
        job = take(c, "queue:email", "1")
    end
end

func main:
    c := redis.Connect("127.0.0.1", 6379)
    defer redis.Close(c)
    redis.Auth(c, "s3cret")

    t := spawn(worker)
    redis.RPush(c, "queue:email", "welcome:sara")
    redis.RPush(c, "queue:email", "welcome:omid")
    redis.RPush(c, "queue:email", "digest:all")
    println "queued 3 jobs"
    join(t)

    each line in redis.LRange(c, "done:email", 0, -1):
        println line
    end
    println "left in queue:", redis.LLen(c, "queue:email")
    redis.Del(c, "done:email")
end
Outputqueued 3 jobs sent welcome:sara sent welcome:omid sent digest:all left in queue: 0

A job popped by a worker that then crashes is lost. When that matters, move each job to a processing list with LMOVE and remove it only after the work is done.

Which function for which job

JobUse
Cache a value for a whileSetEx, then Get or Exists
Count views, hand out idsIncr, IncrBy, Decr
Store a record or a sessionHSet, HGetAll, then Expire
Recent items, a job queueLPush, LRange, RPush, LPop, BLPOP through Command
Unique visitors, tags, who is onlineSAdd, SCard, SIsMember
Rankings, scoresZADD, ZREVRANGE, ZINCRBY through Command
Several writes togetherMULTI and EXEC through Command
Tell another service something happenedPublish
Find keys on a big serverSCAN through Command, not Keys

Types

Client

struct Client
A connection to Redis.
NameTypeDescription
handlevoid*The connection pointer; null when Connect failed.
db-redis-Client.salam
import db.redis

func main:
    c := redis.Connect("127.0.0.1", 6379)
    if not redis.Ok(c):
        println redis.Error(c)
        ret
    end
    _ := redis.Auth(c, "s3cret")
    _ := redis.Select(c, 1)
    println redis.Ping(c)

    _ := redis.Set(c, "greeting", "hello")
    _ := redis.SetEx(c, "session:42", "token", 3600)
    println redis.Get(c, "greeting"), redis.Exists(c, "greeting"), redis.TTL(c, "session:42")
    println redis.Incr(c, "visits"), redis.IncrBy(c, "visits", 10), redis.Decr(c, "visits")
    println redis.Append(c, "greeting", "!"), redis.StrLen(c, "greeting"), redis.Type(c, "greeting")
    _ := redis.Expire(c, "greeting", 60)
    println redis.Keys(c, "session:*").len()
    _ := redis.Del(c, "greeting")

    _ := redis.HSet(c, "user:1", "name", "Maryam")
    _ := redis.HSet(c, "user:1", "city", "Tehran")
    println redis.HGet(c, "user:1", "name"), redis.HExists(c, "user:1", "age"), redis.HLen(c, "user:1")
    all := redis.HGetAll(c, "user:1")
    println all.size()
    _ := redis.HDel(c, "user:1", "city")

    _ := redis.RPush(c, "jobs", "a")
    _ := redis.LPush(c, "jobs", "b")
    println redis.LLen(c, "jobs"), redis.LRange(c, "jobs", 0, -1).len()
    println redis.LPop(c, "jobs"), redis.RPop(c, "jobs")

    _ := redis.SAdd(c, "tags", "go")
    _ := redis.SAdd(c, "tags", "salam")
    println redis.SIsMember(c, "tags", "salam"), redis.SCard(c, "tags"), redis.SMembers(c, "tags").len()
    _ := redis.SRem(c, "tags", "go")
    println redis.Publish(c, "news", "fresh news")

    mut args := Vector {} as Vector<str>
    args.push("GET")
    args.push("visits")
    r := redis.Command(c, args)
    if redis.ReplyOk(r) and not redis.ReplyIsError(r) and not redis.ReplyIsNil(r):
        println redis.ReplyType(r) == redis.REPLY_STRING, redis.ReplyStr(r)
    end
    redis.FreeReply(r)
    mut args2 := Vector {} as Vector<str>
    args2.push("LRANGE")
    args2.push("jobs")
    args2.push("0")
    args2.push("-1")
    r2 := redis.Command(c, args2)
    if redis.ReplyType(r2) == redis.REPLY_ARRAY:
        repeat redis.ReplyArrayLen(r2) in i:
            println redis.ReplyStr(redis.ReplyArrayGet(r2, i))
        end
    end
    println redis.ReplyInt(r2), redis.REPLY_INTEGER, redis.REPLY_NIL, redis.REPLY_STATUS, redis.REPLY_ERROR, redis.REPLY_DOUBLE
    redis.FreeReply(r2)
    _ := redis.FlushDB(c)
    redis.Close(c)
end

Reply

struct Reply
The reply to one command.
NameTypeDescription
handlevoid*The reply pointer.

The example for this item is shown with the ones above: db-redis-Client.salam

Constants and variables

REPLY_STRING

const REPLY_STRING: i32 = 1
Reply type: a string.

The example for this item is shown with the ones above: db-redis-Client.salam

REPLY_ARRAY

const REPLY_ARRAY: i32 = 2
Reply type: an array.

The example for this item is shown with the ones above: db-redis-Client.salam

REPLY_INTEGER

const REPLY_INTEGER: i32 = 3
Reply type: a whole number.

The example for this item is shown with the ones above: db-redis-Client.salam

REPLY_NIL

const REPLY_NIL: i32 = 4
Reply type: nil, a key that is not there for instance.

The example for this item is shown with the ones above: db-redis-Client.salam

REPLY_STATUS

const REPLY_STATUS: i32 = 5
Reply type: a status, such as OK.

The example for this item is shown with the ones above: db-redis-Client.salam

REPLY_ERROR

const REPLY_ERROR: i32 = 6
Reply type: an error.

The example for this item is shown with the ones above: db-redis-Client.salam

REPLY_DOUBLE

const REPLY_DOUBLE: i32 = 7
Reply type: a float, read with ReplyStr. Only a server switched to RESP3 sends it; over the default protocol floats arrive as strings.

The example for this item is shown with the ones above: db-redis-Client.salam

Functions

Connect

func Connect(host: str, port: int): Client
Connects to the Redis server at a host and port; check the result with Ok.

The example for this item is shown with the ones above: db-redis-Client.salam

Ok

func Ok(c: Client): bool
Connect worked. It stays true after the connection is lost later; Error or Ping tell you that.

The example for this item is shown with the ones above: db-redis-Client.salam

Error

func Error(c: Client): str
Why the connection failed: no connection when Connect failed, or a message such as Server closed the connection once it broke; empty while it works. Errors the server sends back for one command are not kept here.

The example for this item is shown with the ones above: db-redis-Client.salam

Close

func Close(c: Client)
Closes the connection and frees the client.

The example for this item is shown with the ones above: db-redis-Client.salam

Ping

func Ping(c: Client): bool
The server answered; false when the connection is down or it still needs a password.

The example for this item is shown with the ones above: db-redis-Client.salam

Select

func Select(c: Client, index: int): bool
Picks the numbered database, 0 to 15 on a default server.

The example for this item is shown with the ones above: db-redis-Client.salam

Auth

func Auth(c: Client, password: str): bool
Logs in with the server password. For a named ACL user send AUTH name password with Command.

The example for this item is shown with the ones above: db-redis-Client.salam

FlushDB

func FlushDB(c: Client): bool
Clears every key of the current database.

The example for this item is shown with the ones above: db-redis-Client.salam

HSet

func HSet(c: Client, key: str, field: str, val: str): bool
Sets a field of a hash. It returns false when no reply came or the server refused the command, such as on a key that is not a hash.

The example for this item is shown with the ones above: db-redis-Client.salam

HGet

func HGet(c: Client, key: str, field: str): str
One field of a hash; an empty string when it is missing.

The example for this item is shown with the ones above: db-redis-Client.salam

HDel

func HDel(c: Client, key: str, field: str): bool
Deletes a field; true when it was there.

The example for this item is shown with the ones above: db-redis-Client.salam

HExists

func HExists(c: Client, key: str, field: str): bool
The field is there.

The example for this item is shown with the ones above: db-redis-Client.salam

HLen

func HLen(c: Client, key: str): i64
The number of fields.

The example for this item is shown with the ones above: db-redis-Client.salam

HGetAll

func HGetAll(c: Client, key: str): HashMap<str, str>
Every field as a map; an empty map when the key is missing.

The example for this item is shown with the ones above: db-redis-Client.salam

Set

func Set(c: Client, key: str, val: str): bool
Sets the value of a key, replacing any value and expiry it had.

The example for this item is shown with the ones above: db-redis-Client.salam

SetEx

func SetEx(c: Client, key: str, val: str, seconds: int): bool
The same, with an expiry in seconds.

The example for this item is shown with the ones above: db-redis-Client.salam

Get

func Get(c: Client, key: str): str
The value of a key; an empty string when there is none or it does not hold a string.

The example for this item is shown with the ones above: db-redis-Client.salam

Exists

func Exists(c: Client, key: str): bool
The key is there.

The example for this item is shown with the ones above: db-redis-Client.salam

Del

func Del(c: Client, key: str): bool
Deletes the key; true when it was there.

The example for this item is shown with the ones above: db-redis-Client.salam

Expire

func Expire(c: Client, key: str, seconds: int): bool
Gives the key a lifetime in seconds; false when there is no such key.

The example for this item is shown with the ones above: db-redis-Client.salam

TTL

func TTL(c: Client, key: str): i64
The seconds left until the expiry; -1 means no expiry and -2 means no such key.

The example for this item is shown with the ones above: db-redis-Client.salam

Incr

func Incr(c: Client, key: str): i64
Raises the number of a key by one and returns the new value; a missing key counts as 0. Returns 0 when the value is not a number.

The example for this item is shown with the ones above: db-redis-Client.salam

IncrBy

func IncrBy(c: Client, key: str, delta: i64): i64
Raises it by the given amount, which may be negative.

The example for this item is shown with the ones above: db-redis-Client.salam

Decr

func Decr(c: Client, key: str): i64
Lowers it by one.

The example for this item is shown with the ones above: db-redis-Client.salam

Append

func Append(c: Client, key: str, val: str): i64
Adds text to the value and returns the new length.

The example for this item is shown with the ones above: db-redis-Client.salam

StrLen

func StrLen(c: Client, key: str): i64
The length of the value.

The example for this item is shown with the ones above: db-redis-Client.salam

Type

func Type(c: Client, key: str): str
The type of the value of a key, such as string or hash; none when there is no such key.

The example for this item is shown with the ones above: db-redis-Client.salam

Keys

func Keys(c: Client, pattern: str): Vector<str>
The keys that match a pattern such as user:*. It blocks the server while it walks every key, so use SCAN through Command on a large database.

The example for this item is shown with the ones above: db-redis-Client.salam

SAdd

func SAdd(c: Client, key: str, member: str): i64
Adds a member to a set; returns 1 when it is new and 0 when it was already there.

The example for this item is shown with the ones above: db-redis-Client.salam

SRem

func SRem(c: Client, key: str, member: str): i64
Removes a member; returns 1 when it was there.

The example for this item is shown with the ones above: db-redis-Client.salam

SIsMember

func SIsMember(c: Client, key: str, member: str): bool
It is a member.

The example for this item is shown with the ones above: db-redis-Client.salam

SCard

func SCard(c: Client, key: str): i64
The number of members.

The example for this item is shown with the ones above: db-redis-Client.salam

SMembers

func SMembers(c: Client, key: str): Vector<str>
Every member, in no particular order.

The example for this item is shown with the ones above: db-redis-Client.salam

Publish

func Publish(c: Client, channel: str, message: str): i64
Publishes a message to a channel and returns the number of receivers.

The example for this item is shown with the ones above: db-redis-Client.salam

Command

func Command(c: Client, args: Vector<str>): Reply
Sends any command with its arguments, each sent exactly as it is, and returns the raw reply. Free it with FreeReply; the reply is not ReplyOk when the connection is down.

The example for this item is shown with the ones above: db-redis-Client.salam

ReplyOk

func ReplyOk(r: Reply): bool
A reply arrived; false when the connection is down or was never made.

The example for this item is shown with the ones above: db-redis-Client.salam

ReplyType

func ReplyType(r: Reply): int
The reply type.

The example for this item is shown with the ones above: db-redis-Client.salam

ReplyIsError

func ReplyIsError(r: Reply): bool
The server refused the command; ReplyStr holds its message.

The example for this item is shown with the ones above: db-redis-Client.salam

ReplyIsNil

func ReplyIsNil(r: Reply): bool
The reply is nil, such as GET on a key that is not there.

The example for this item is shown with the ones above: db-redis-Client.salam

ReplyInt

func ReplyInt(r: Reply): i64
The reply as a number, for an integer reply; 0 for any other type.

The example for this item is shown with the ones above: db-redis-Client.salam

ReplyStr

func ReplyStr(r: Reply): str
The reply as text, for a string, status, error or float reply; empty for nil, numbers and arrays.

The example for this item is shown with the ones above: db-redis-Client.salam

ReplyArrayLen

func ReplyArrayLen(r: Reply): int
The number of elements in an array reply.

The example for this item is shown with the ones above: db-redis-Client.salam

ReplyArrayGet

func ReplyArrayGet(r: Reply, i: int): Reply
One element of an array reply, counting from 0. It belongs to the array: free only the outer reply.

The example for this item is shown with the ones above: db-redis-Client.salam

FreeReply

func FreeReply(r: Reply)
Frees a reply from Command, with all its elements.

The example for this item is shown with the ones above: db-redis-Client.salam

LPush

func LPush(c: Client, key: str, val: str): i64
Adds to the front of a list and returns the new length.

The example for this item is shown with the ones above: db-redis-Client.salam

RPush

func RPush(c: Client, key: str, val: str): i64
Adds to the end of a list.

The example for this item is shown with the ones above: db-redis-Client.salam

LPop

func LPop(c: Client, key: str): str
Takes one off the front of a list; an empty string when the list is empty.

The example for this item is shown with the ones above: db-redis-Client.salam

RPop

func RPop(c: Client, key: str): str
Takes one off the end; an empty string when the list is empty.

The example for this item is shown with the ones above: db-redis-Client.salam

LLen

func LLen(c: Client, key: str): i64
The length of the list.

The example for this item is shown with the ones above: db-redis-Client.salam

LRange

func LRange(c: Client, key: str, start: int, stop: int): Vector<str>
The elements from start to stop, both included. Negative positions count from the end, so 0, -1 is the whole list.

The example for this item is shown with the ones above: db-redis-Client.salam