Skip to main content

Tutorial: Key/Value Store

The Kahuna key/value store gives every instance of your service the same view of small shared state: configuration, service discovery records, metadata, caches, and sessions. The commands look like Redis GET and SET, but persistent writes are replicated before they are acknowledged, so they survive the failure of a node.

In this tutorial, you start a local server and try reads, writes, expiration, revisions, compare-and-set, and transactions.

Starting Kahuna​

You need a running Kahuna server and the Kahuna CLI to execute commands on it. Check that the CLI is installed:

kahuna-cli --version

For a local standalone server with Docker, run:

docker run --rm -p 8081:8081 -p 8082:8082 -p 8083:8083 -v kahuna-data:/data --name kahuna kahunakv/kahuna:latest

Then connect over cleartext gRPC:

kahuna-cli -c "http://127.0.0.1:8083"

See Server Installation for the exposed ports, HTTPS connections, the .NET global tool, and storage options.

Setting and Retrieving Keys​

The Kahuna CLI runs commands, transactions, and scripts. Set a key and read it back:

Run these commands one at a time in the shell:

set myconfig "my-value"
get myconfig
set myconfig "my-value-2"
get myconfig

The two reads return my-value at revision r0, then my-value-2 at revision r1.

set stores the key persistently: Kahuna copies the write to a majority of the key's replicas (a quorum) before it acknowledges it. get returns the most recent committed value.

Expiration​

By default, persistent keys do not expire. To remove a key after a period, give it a time-to-live in milliseconds with ex when you create it:

set myconfig "my-value" ex 30000

An example response is r2 set 18ms: the revision, result, and elapsed time.

Or set it later with extend:

set myconfig "my-value"
extend myconfig 30000

The extend response is r3 extended 16ms in this example.

Durability​

Keys written with set are persistent: the client is notified of success only after a quorum of the key's partition commits the write, so it survives node failures. Commands prefixed with an "e" use ephemeral durability instead. They are faster because they skip replication and disk, but the data lives only in the memory of the partition's leader node and is lost if the node crashes, the leader changes, or memory pressure evicts the least-used keys. See Durability Levels.

eset tempconfig "my-value"
eget tempconfig

The read returns my-value, for example r0 my-value 32ms.

Revisions​

The r0, r1, r2 prefix in each response is the key's revision, a per-key version number that increases every time the key is updated or deleted. A revision orders the changes to one key, and it lets you make conditional writes and read older versions.

set myconf "some config"
set myconf "some other config"
set myconf "another config"
get myconf

For a new key, the writes return revisions r0, r1, and r2. The final read returns r2 another config 13ms in this example.

Compare And Swap (CAS)​

A compare-and-set (CAS) write succeeds only if the key still holds the value you expect. When several clients update the same key at once, the late writer fails instead of silently overwriting the earlier change.

set myconf "prev config"
set myconf "new config" cmp "prev config"
set myconf "some config" cmp "prev config"

The first two writes return set. The last returns not set because the current value no longer matches the one given in cmp.

Scripts and Transactions​

Kahuna Script runs several operations as one transaction, like a SQL transaction. You can run scripts from kahuna-cli or from a client. A transaction can read and modify keys on several nodes, and its changes are applied all-or-nothing.

For example, this script decrements a per-user counter, using a limit stored in another key:

set max_rate_limit 100

Then run this script:

let limit = get max_rate_limit
let current_rate = get rate_limit_user1

if not found then
let current_rate = limit
end

if to_int(current_rate) <= 0 then
throw "Rate limit exceeded. Please try again later."
end

let new_current_rate = to_int(current_rate) - 1
set rate_limit_user1 new_current_rate ex 60000

return new_current_rate

For a new counter, the script returns 99. You can read the stored value with:

get rate_limit_user1

The next example transfers 50 from one balance to another. The two keys may be led by different nodes. The transaction coordinator records the confirmed reads and writes, then applies the changes on every participating node at commit. If an error occurs, the transaction aborts and no changes are applied:

Initialize the balances, running each command separately:

set balance_user1 1000 nx
set balance_user2 1000 nx

Then run the transaction:

begin
let balance_user1_value = get balance_user1
let balance_user2_value = get balance_user2

let balance_user1_num = to_int(balance_user1_value)
let balance_user2_num = to_int(balance_user2_value)

if balance_user1_num >= 50 then
set balance_user1 balance_user1_num - 50
set balance_user2 balance_user2_num + 50
commit
end
end

Either both balances change or neither does. Next, learn how to use Distributed Locks.