ioredis

Redis client for dart.

3.0.0 has breaking changes from 1.x. See CHANGELOG.md.

Install

dependencies:
  ioredis: ^3.0.0

Features

  • Support Pub/Sub
  • Support pool connection by default
  • Auto-pipelining: commands sent in the same event-loop turn go out in one write
  • Support auto reconnect on failed
  • Support retry strategy
  • Support selectable database

Basic Usage

/// Create a new redis instance
Redis redis = new Redis();
Redis redis = new Redis(RedisOptions(host: '127.0.0.1', port: 6379));

/// Set value
await redis.set('key', value);

/// Set value with expiry time
await redis.set('key', value, 'EX', 10);

/// Get value
String? value = await redis.get('key');

/// Get multiple values
List<String?> value = await redis.mget(['key1', 'key2']);

Create an instance

Redis redis = new Redis();
Redis redis = new Redis(RedisOptions(host: '127.0.0.1', port: 6379));
Redis redis = new Redis(
    RedisOptions(
        username: 'root', 
        password: 'password',
        db: 1,
    ),
);

Duplicate

Redis redis = new Redis();
Redis duplicatedRedis = redis.duplicate();

Pub/Sub

Redis subClient = new Redis();
Redis pubClient = subClient.duplicate();

RedisSubscriber subscriber = await subClient.subscribe('chat')
subscriber.onMessage = (String channel, String? message) {
    print(channel, message);
}

await pubClient.publish('chat', 'hello');

// pattern subscribe
RedisSubscriber news = await subClient.psubscribe('news.*');
news.onPatternMessage = (String pattern, String channel, String? message) {
    print('$channel $message');
};

await subClient.unsubscribe('chat');   // no argument: all channels
await subClient.punsubscribe('news.*'); // no argument: all patterns

Binary messages

publish() takes a String or a Uint8List. Bytes are sent as they are. Use onMessageBytes and onPatternMessageBytes to get the exact bytes back, for example for msgpack. Channel and pattern names stay String.

RedisSubscriber sub = await subClient.subscribe('room');
sub.onMessageBytes = (String channel, Uint8List message) {
    print(message); // the exact bytes that were published
};

RedisSubscriber psub = await subClient.psubscribe('room.*');
psub.onPatternMessageBytes =
    (String pattern, String channel, Uint8List message) {};

await pubClient.publish('room', Uint8List.fromList([0x93, 0xc4, 0x02, 0xff, 0xfe]));

A subscriber can have both a text and a bytes callback. Then both are called. Bytes that are not valid UTF-8 never throw. In the text callback they become U+FFFD.

After a lost connection, the client connects again and subscribes again to the same channels and patterns.

Delete

Redis redis = new Redis();
await redis.delete('key')
await redis.mdelete(['key1', 'key2'])

Flushdb

Redis redis = new Redis();
await redis.flushdb()

Send command

Redis redis = new Redis();
await redis.sendCommand(['GET', 'key'])

Any command with call()

call() sends any command, like redis.call in Node ioredis. Arguments can be strings or numbers. The key prefix is NOT added.

await redis.call('GEOADD', ['places', 13.361389, 38.115556, 'Palermo']);
List<dynamic> res = await redis.call('GEORADIUS', ['places', 15, 37, 200, 'km', 'WITHDIST']);

Errors

An error reply from Redis throws RedisError. The connection keeps working.

try {
  await redis.lpush('a-string-key', 'x');
} on RedisError catch (e) {
  print(e.message); // WRONGTYPE Operation against a key holding the wrong kind of value
}

Reply types

  • Integer replies are int.
  • Strings are String. Missing values are null.
  • Arrays are List<dynamic> and can be nested.
  • Float replies (incrbyfloat, hincrbyfloat, zincrby, zscore) are double.

Keys

await redis.del(['a', 'b']);          // 2
await redis.exists('a');              // 0
await redis.expire('a', 60);          // also pexpire, expireat, persist
await redis.ttl('a');                 // also pttl
await redis.type('a');                // 'string'
await redis.rename('a', 'b');         // also renamenx, copy, unlink, randomkey
List<String> keys = await redis.keys('user:*');
RedisScanResult res = await redis.scan(0, match: 'user:*', count: 100);
print(res.cursor); // '0' when done
print(res.items);

The key prefix is added to every key argument. Like Node ioredis, it is NOT added to keys() and scan() patterns, and NOT to channel names.

Strings

await redis.set('k', 'v', 'EX', 10);
await redis.set('k', 'v', 'NX');            // null when the key exists
await redis.set('k', 'v', 'EX', 10, 'NX');
String? old = await redis.set('k', 'v2', 'GET');
await redis.mset({'a': 1, 'b': 2});         // also msetnx
await redis.incr('n');                       // also incrby, decr, decrby, incrbyfloat
await redis.append('k', 'more');             // also strlen, getrange, setrange
await redis.getdel('k');                     // also getset, getex, setnx, setex, psetex

Hashes

await redis.hset('user:1', {'name': 'Ann', 'age': 30});
Map<String, String> user = await redis.hgetall('user:1');
await redis.hget('user:1', 'name');
await redis.hincrby('user:1', 'age', 1);
// also hsetnx, hmget, hdel, hexists, hkeys, hvals, hlen, hincrbyfloat, hstrlen, hscan

Lists

await redis.rpush('q', ['a', 'b']);
await redis.lpop('q');                // 'a'
await redis.lpop('q', 2);             // ['b']
await redis.lrange('q', 0, -1);
List<String>? item = await redis.blpop(['q'], 5); // [key, value] or null
// also lpushx, rpushx, rpop, llen, lindex, lset, lrem, ltrim, linsert, lmove, brpop

Sets

await redis.sadd('tags', ['a', 'b']);
await redis.smembers('tags');
await redis.sismember('tags', 'a');   // 1
// also srem, smismember, scard, spop, srandmember, smove,
// sinter, sunion, sdiff, sinterstore, sunionstore, sdiffstore, sscan

Sorted sets

await redis.zadd('board', {'ann': 10, 'bob': 7.5});
await redis.zadd('board', {'ann': 20}, gt: true, ch: true);
await redis.zrange('board', 0, -1, withScores: true); // ['bob', '7.5', 'ann', '20']
await redis.zscore('board', 'ann');   // 20.0
// also zrem, zincrby, zcard, zcount, zrank, zrevrank, zrevrange,
// zrangebyscore, zrevrangebyscore, zremrangebyrank, zremrangebyscore,
// zpopmin, zpopmax, zscan

Server

await redis.ping();       // 'PONG'
await redis.echo('hi');
await redis.select(1);    // all connections of this client switch
await redis.dbsize();
await redis.info('server');
await redis.time();
await redis.flushall();
await redis.quit();       // QUIT, then disconnect

Scripting

await redis.eval("return redis.call('GET', KEYS[1])", 1, ['foo']);
String sha = await redis.script('LOAD', ["return 'hi'"]);
await redis.evalsha(sha, 0);

The first numKeys items are keys and get the key prefix.

Pool connection and auto-pipelining

Commands share one connection. Commands sent in the same event-loop turn are written together in one socket write (auto-pipelining). Replies still come back to the right caller, in order.

The pool is used when a blocking command (BLPOP, BRPOP, BLMOVE, XREAD BLOCK, WAIT, ...) waits on a connection. Then other commands go to a pool connection, so they do not wait behind it. maxConnection is the most connections (main connection included). A pool connection is closed after idleTimeout without use.

Redis redis = new Redis(RedisOptions(maxConnection: 10));
String? value = await redis.get('key');

Transaction (multi)

All commands run inside MULTI ... EXEC on one connection, sent in one write. exec() returns the EXEC result. If one command fails while running, its item is a RedisError (no throw). If Redis rejects a command before EXEC, exec() throws the EXECABORT RedisError.

Redis redis = new Redis();
List<dynamic> result = await redis.multi()
    .set('key', 'value')
    .incr('counter')
    .call('HSET', ['hash', 'field', 'value']) // any command
    .get('key')
    .exec();

// result => ['OK', 1, 1, 'value']

Pipeline

All commands are sent in one write on one connection, without MULTI. Results come back in order. Errors are RedisError items in the result.

List<dynamic> result = await redis.pipeline()
    .set('key', 'value')
    .get('key')
    .call('PING')
    .exec();

// result => ['OK', 'value', 'PONG']

multi() and pipeline() have call() plus these typed methods: set, get, del, exists, expire, ttl, incr, incrby, decr, hset, hget, hdel, lpush, rpush, sadd, srem, zadd. Results are raw replies.

Running tests

The tests need two Redis servers: one on port 6379 and one on port 8379.

redis-server --port 8379 --daemonize yes --save '' --appendonly no
dart test
redis-cli -p 8379 shutdown nosave

You can change the host and ports with env vars:

  • REDIS_HOST (default 127.0.0.1)
  • REDIS_PORT (default 6379)
  • REDIS_PORT_2 (default 8379)

Libraries

ioredis