Skip to content

Hash import in Lettuce

Lettuce supports the HIMPORT command family starting from Lettuce 7.7, for bulk-loading many hashes that share the same field names.

A normal HSET repeats the field names on every command. When you are importing a million records that all have the same shape, that is a million copies of name, email, age on the wire. HIMPORT lets you declare the field names once and then send only the values, positionally paired to them.

Declare the shared field names as a HashImport, then pass it to himportSet with the values for each hash:

RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").build();
try (RedisClient client = RedisClient.create(redisURI)) {
    StatefulRedisConnection<String, String> connection = client.connect();
    RedisCommands<String, String> redis = connection.sync();

    try (HashImport<String> fieldset = HashImport.of("name", "email", "age")) {
        redis.himportSet("user:1", fieldset, "Alice", "alice@example.com", "34");
        redis.himportSet("user:2", fieldset, "Bob", "bob@example.com", "41");
    }

    // the imported keys are ordinary hashes
    redis.hget("user:1", "email");   // alice@example.com
    redis.hlen("user:1");            // 3
}

The number of values must match the number of fields, in the same order. The keys HIMPORT creates are ordinary hashes — read them back with HGET, HGETALL, expire them, anything you would normally do.

Info

The field names are declared to the server once per connection, the first time you use the fieldset on it. Lettuce re-declares them for you after a reconnect, on every connection of a pool, and on each Redis Cluster node your keys route to, so a HashImport is safe to share across threads and connections without any per-connection setup.

Closing a fieldset

A fieldset holds state on every connection it was used on. Close it when the import is finished — either with try-with-resources as above, or explicitly:

HashImport<String> fieldset = HashImport.of("sku", "price");
try {
    redis.himportSet("product:1", fieldset, "SKU-1", "9.99");
} finally {
    fieldset.close();
}

Closing releases the server-side state and rejects any further use of that fieldset; a later himportSet with it fails with an IllegalStateException. Imports you have already issued are not cancelled and still complete, so the try-with-resources form is safe with the asynchronous and reactive APIs too:

RedisAsyncCommands<String, String> redis = connection.async();
List<RedisFuture<String>> results = new ArrayList<>();

try (HashImport<String> fieldset = HashImport.of("name", "email")) {
    for (User user : users) {
        results.add(redis.himportSet(user.key(), fieldset, user.name(), user.email()));
    }
}

LettuceFutures.awaitAll(5, TimeUnit.SECONDS, results.toArray(new RedisFuture[0]));

Other API flavors

himportSet is available on the synchronous, asynchronous, reactive, and Kotlin coroutine APIs, and on cluster node selections.

// reactive
Flux.fromIterable(users)
        .flatMap(user -> redis.reactive().himportSet(user.key(), fieldset, user.name(), user.email()))
        .then()
        .block();

Warning

HIMPORT is not supported inside a MULTI transaction. The field names are per-connection session state declared outside the transaction, so calling himportSet while a transaction is open fails with an UnsupportedOperationException.

Binary keys

For a codec other than String, supply a function that names the fieldset in that key type. It must return a distinct name each time it is called — two live fieldsets sharing a name also share their server-side state:

StatefulRedisConnection<byte[], byte[]> connection = client.connect(ByteArrayCodec.INSTANCE);
RedisCommands<byte[], byte[]> redis = connection.sync();

byte[][] fields = { "name".getBytes(), "email".getBytes() };
try (HashImport<byte[]> fieldset = HashImport.of(seq -> ("fields:" + seq).getBytes(), fields)) {
    redis.himportSet("user:1".getBytes(), fieldset, "Alice".getBytes(), "alice@example.com".getBytes());
}