Change your application: migrate from Hazelcast to KiviDB
Change your application. Migrate to KiviDB: Move from Redis, Valkey, ElastiCache, Redis Cloud, Aiven, Aerospike, Hazelcast and more: getting the data out, loa
Connecting
Before (Hazelcast client):
ClientConfig cc = new ClientConfig();
cc.setClusterName("prod");
cc.getNetworkConfig().addAddress("hz-1.internal:5701", "hz-2.internal:5701");
HazelcastInstance hz = HazelcastClient.newHazelcastClient(cc);
After (Jedis):
JedisPooled kv = new JedisPooled("kividb.internal", 6380, null, System.getenv("KIVIDB_PASSWORD"));
Or Lettuce:
RedisClient client = RedisClient.create(RedisURI.builder()
.withHost("kividb.internal").withPort(6380)
.withPassword(System.getenv("KIVIDB_PASSWORD").toCharArray())
.build());
StatefulRedisConnection<String, String> conn = client.connect();
RedisCommands<String, String> kv = conn.sync();
With TLS on KiviDB Cloud, the URL starts with rediss:// and the JVM must
trust the database's CA certificate (see Loading it into KiviDB).
Hazelcast's cluster name has no equivalent; a password (or an ACL user and
password) takes its place.
Maps
Before:
IMap<String, Customer> customers = hz.getMap("customers");
customers.set("c42", new Customer("c42", "Ana", 31, "Berlin", 52.5), 1, TimeUnit.HOURS);
Customer c = customers.get("c42");
customers.delete("c42");
After, as a hash:
Transaction t = jedis.multi(); // the hash and its TTL in one step
t.hset("customer:c42", Map.of("id", "c42", "name", "Ana", "age", "31", "city", "Berlin", "balance", "52.5"));
t.expire("customer:c42", 3600);
t.exec();
Map<String, String> c = kv.hgetAll("customer:c42"); // empty map if missing
kv.del("customer:c42");
jedis here is a Jedis connection taken from a pool
(try (Jedis jedis = pool.getResource())), because a transaction needs one
connection. A hash keeps every field a string; convert numbers when you read.
As JSON, if the objects are nested:
kv.jsonSet("customer:c42", Path2.ROOT_PATH, new JSONObject(Map.of("name", "Ana", "age", 31, "address", Map.of("city", "Berlin"))));
Object c = kv.jsonGet("customer:c42");
Predicates and SQL
Before:
customers.addIndex(IndexType.HASH, "city");
customers.addIndex(IndexType.SORTED, "age");
Collection<Customer> found = customers.values(
Predicates.and(Predicates.equal("city", "Berlin"), Predicates.between("age", 30, 40)));
After: create an index once, over every key with the prefix, then query it:
FT.CREATE idx:customer ON HASH PREFIX 1 customer: SCHEMA city TAG age NUMERIC SORTABLE name TEXT balance NUMERIC
SearchResult r = kv.ftSearch("idx:customer", "@city:{Berlin} @age:[30 40]",
FTSearchParams.searchParams().limit(0, 100));
for (Document d : r.getDocuments()) { /* d.getId(), d.getString("name") */ }
For JSON documents, use ON JSON and $.field AS field in the schema. Search
matches whole words, prefixes, suffixes, infixes and fuzzy terms; it has no
stemming or phonetic matching. The index is kept up to date as keys change.
Queues and topics
Before:
IQueue<String> jobs = hz.getQueue("jobs");
jobs.offer("job-1");
String job = jobs.take();
ITopic<String> news = hz.getTopic("news");
news.addMessageListener(m -> System.out.println(m.getMessageObject()));
news.publish("hello");
After:
kv.rpush("jobs", "job-1");
List<String> taken = kv.blpop(5, "jobs"); // [key, value], or null after 5 s
// At-least-once: move to a processing list, remove when done.
String job = kv.blmove("jobs", "jobs:processing", ListDirection.LEFT, ListDirection.RIGHT, 5);
kv.lrem("jobs:processing", 1, job);
kv.publish("news", "hello");
// SUBSCRIBE holds the connection: run it on its own thread and connection.
new Thread(() -> {
try (Jedis sub = pool.getResource()) {
sub.subscribe(new JedisPubSub() {
@Override public void onMessage(String channel, String message) { System.out.println(message); }
}, "news");
}
}).start();
For a ReliableTopic, use a stream: XADD news MAXLEN ~ 10000 * value hello
to publish, and XREADGROUP with XACK in each subscriber.
Locks
Before (CP subsystem, Enterprise on 5.5 and later):
FencedLock lock = hz.getCPSubsystem().getLock("order-1");
long fence = lock.lockAndGetFence();
try { /* work */ } finally { lock.unlock(); }
After:
String token = UUID.randomUUID().toString();
boolean held = "OK".equals(kv.set("lock:order-1", token, SetParams.setParams().nx().px(30_000)));
if (held) {
long fence = kv.incr("fence:order-1"); // a fencing token, if you used the fence
try { /* work, finished well within 30 s */ }
finally {
// Delete only if it is still our lock (KiviDB 1.2; no Lua needed).
kv.sendCommand(() -> SafeEncoder.encode("DELEX"), "lock:order-1", "IFEQ", token);
}
}
On a server without DELEX, release with WATCH lock:order-1, GET, and a
MULTI/DEL/EXEC that runs only if the value is still your token.
This is not the same lock. FencedLock runs on Raft: it stays correct when a
member fails or the network splits, its fences are issued by the same
consensus, and a lock is held until it is released or its holder's session
ends. A KiviDB lock lives on one primary, with a lease you choose:
- If the primary fails and a replica takes over before the lock key was replicated, a second client can take the lock.
- If the work outlasts the lease, the lock expires while you still think you hold it. Keep the work well inside the lease, or extend it.
- A fencing token from
INCRhas the same failover caveat.
That is enough for most "only one worker at a time" uses. Where a double run
would corrupt data, make the protected operation itself check the fencing
token, as you would have with FencedLock.
Counters and ID generators
| Hazelcast | KiviDB |
|---|---|
atomicLong.incrementAndGet() | INCR key |
atomicLong.getAndIncrement() | INCR key, minus 1 |
atomicLong.addAndGet(n) | INCRBY key n |
atomicLong.compareAndSet(expect, update) | SET key update IFEQ expect (nil if it didn't match) |
atomicLong.getAndSet(v) | SET key v GET |
pnCounter.incrementAndGet() | INCR key |
flakeIdGenerator.newId() | INCR seq:key, or keep generating IDs in the application |
INCR on a missing key starts it at 0. A FlakeIdGenerator's IDs are
roughly time-ordered and unique across members without coordination; INCR
gives one strictly increasing sequence instead. Start the sequence above the
highest ID already issued (SET seq:orders <highest>), so old and new IDs
never collide.
Entry listeners
Before:
customers.addEntryListener((EntryAddedListener<String, Customer>) e -> onAdded(e.getKey()), false);
After: turn on keyspace notifications (--notify-keyspace-events Kghxe, or
CONFIG SET notify-keyspace-events Kghxe on a running server), and subscribe.
KiviDB Cloud does not let you turn keyspace notifications on yet; there, have
the writer publish its own change messages (PUBLISH, or XADD to a stream
for at-least-once delivery) in the same MULTI as the write.
jedis.psubscribe(new JedisPubSub() {
@Override public void onPMessage(String pattern, String channel, String event) {
String key = channel.substring("__keyspace@0__:".length());
// event: hset (added or updated), del (removed), expired, evicted
}
}, "__keyspace@0__:customer:*");
Notifications carry the key and the command, not the value: read the key if you need it. They are not stored, so a subscriber that is down misses them. Where every change must be processed, write the change to a stream as well and read it with a consumer group.
Near cache
KiviDB supports server-assisted client-side caching (CLIENT TRACKING, in
the default and BCAST modes): the client keeps values locally and the server
tells it when a key changes. Clients differ:
- Lettuce
ClientSideCachingworks with KiviDB. - Jedis 5 and redis-py's built-in caches check that the server calls itself Redis 7.4 or later, and refuse to start against KiviDB.
With Lettuce:
Map<String, String> local = new ConcurrentHashMap<>();
CacheFrontend<String, String> cache = ClientSideCaching.enable(
CacheAccessor.forMap(local), conn, TrackingArgs.Builder.enabled());
String v = cache.get("customer:c42:name"); // served locally until the key changes
Like a near cache, this can serve a value that changed a moment ago, until the invalidation arrives. A plain local cache with a short TTL is the other option.
Spring
Many Hazelcast users reach it through Spring. Both swaps are configuration.
Spring Cache. Spring Boot picks Hazelcast before Redis when both are on
the classpath. Remove the Hazelcast dependency (or set the type), add
spring-boot-starter-data-redis, and set:
spring.cache.type=redis
spring.cache.redis.time-to-live=10m
spring.data.redis.host=kividb.internal
spring.data.redis.port=6380
spring.data.redis.password=${KIVIDB_PASSWORD}
# with TLS
spring.data.redis.ssl.enabled=true
Spring's Redis cache stores values with Java serialization unless you set a
RedisCacheConfiguration bean with another serializer (JSON is common), so
cached classes must be Serializable or you configure one.
Spring Session. Replace the Hazelcast session module
(spring-session-hazelcast or com.hazelcast.spring:hazelcast-spring-session)
with spring-session-data-redis (in Spring Boot 4, the
spring-boot-starter-session-data-redis starter), and
@EnableHazelcastHttpSession with @EnableRedisHttpSession if you configure
it by annotation. Sessions held in Hazelcast are not copied: users sign in
again, or you run the copy for the session map.
Python: hazelcast-python-client → redis-py
Before:
import json
import hazelcast
from hazelcast.core import HazelcastJsonValue
from hazelcast.predicate import and_, equal, between
client = hazelcast.HazelcastClient(cluster_name="prod", cluster_members=["hz-1.internal:5701"])
people = client.get_map("people").blocking()
people.put("p42", HazelcastJsonValue({"name": "Ana", "age": 31, "city": "Berlin"}), ttl=3600)
p = json.loads(people.get("p42").to_string())
matches = people.values(and_(equal("city", "Berlin"), between("age", 30, 40)))
jobs = client.get_queue("jobs").blocking()
jobs.offer("job-1")
job = jobs.take()
client.get_topic("news").blocking().publish("hello")
views = client.get_pn_counter("views").blocking().increment_and_get()
After:
import os
import redis
from redis.commands.search.field import NumericField, TagField, TextField
from redis.commands.search.index_definition import IndexDefinition, IndexType
from redis.commands.search.query import Query
r = redis.Redis(host="kividb.internal", port=6380,
password=os.environ["KIVIDB_PASSWORD"], decode_responses=True)
with r.pipeline() as pipe: # MULTI/EXEC: the hash and its TTL together
pipe.hset("person:p42", mapping={"name": "Ana", "age": 31, "city": "Berlin"})
pipe.expire("person:p42", 3600)
pipe.execute()
p = r.hgetall("person:p42")
# once
r.ft("idx:person").create_index(
[TagField("city"), NumericField("age"), TextField("name")],
definition=IndexDefinition(prefix=["person:"], index_type=IndexType.HASH),
)
matches = r.ft("idx:person").search(Query("@city:{Berlin} @age:[30 40]").paging(0, 100))
r.rpush("jobs", "job-1")
_, job = r.blpop("jobs", timeout=5)
r.publish("news", "hello")
views = r.incr("counter:views")
With TLS on KiviDB Cloud, add ssl=True, ssl_ca_certs="kividb-ca.pem".
Serialization
Hazelcast serialized your objects; with KiviDB you choose the encoding.
| You used | Choose |
|---|---|
| Compact or Portable, flat objects | A hash per object. Fields are readable from any language, and searchable. |
Nested objects, or HazelcastJsonValue | JSON (JSON.SET), searchable with ON JSON indexes. |
Java Serializable | JSON (with Jackson or Gson) in a string or a JSON document. Storing Java-serialized bytes works, but only Java can read them and any class change can break them. |
IdentifiedDataSerializable / DataSerializable | The same: map to a hash or JSON. |
Whatever you choose, use it in both the copy program and the new code.