DEV Community

Apache SeaTunnel
Apache SeaTunnel

Posted on

Redis Data Migration Without Scripts: How Apache SeaTunnel Handles String, Hash, Set, and ZSet

Apache SeaTunnel’s Redis connector supports reading and writing four Redis data types: string, hash, set, and zset. This means you can move data from one Redis instance to another without writing custom migration scripts. This article provides a complete synchronization configuration for each data type, based on the connector behavior in SeaTunnel 2.3.13, as a practical reference for developers and data engineers working with Redis data migration.

Data Type Read Behavior Typical Write Method Notes
string Reads the entire value; when format=json, fields are parsed according to the schema Writes by key; later writes overwrite earlier values for the same key Key templates support {field} placeholders for dynamic key rewriting
hash Without a schema, the entire hash is serialized as a single JSON row Appends to a list The hash key itself is not included in the data row
set Each member is emitted as a separate row Appends to a list Unordered and does not deduplicate across source sets
zset Each member is emitted as a separate row Appends to a list Scores are not read and will be lost

String: Structured Pass-Through

With data_type = string, the connector reads the complete value of each Redis key. When format = json is configured, the value is parsed field by field according to the schema.

On the sink side, data_type = key writes each record back using a Redis key. If the same key is written multiple times, the later value overwrites the earlier one. The {uid} placeholder in the key template is replaced with the value of the corresponding field in each record.

This setup works well for scenarios such as cache migration and Redis key prefix changes, where you want to preserve the original data structure while rewriting the destination key.

source {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    keys = "user:profile:*"
    data_type = string
    batch_size = 200
    read_key_enabled = true
    key_field_name = key
    single_field_name = value
    format = json
    schema = {
      table = "UserDB.UserProfile"
      columns = [
        { name = "key",       type = "string" },
        { name = "uid",       type = "bigint" },
        { name = "nickname",  type = "string" },
        { name = "age",       type = "int" }
      ]
    }
  }
}

sink {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    key = "user:profile:v2:{uid}"
    support_custom_key = true
    data_type = key
    batch_size = 200
  }
}
Enter fullscreen mode Exit fullscreen mode

Test data:

SET user:profile:1001 '{"uid":1001,"nickname":"xiaoma","age":30}'
SET user:profile:1002 '{"uid":1002,"nickname":"candy","age":25}'
Enter fullscreen mode Exit fullscreen mode

After the job finishes, verify the result with:

GET user:profile:v2:1001
"{\"key\":\"user:profile:1001\",\"uid\":1001,\"nickname\":\"xiaoma\",\"age\":30}"
Enter fullscreen mode Exit fullscreen mode

The data is preserved while the key is rewritten with the v2 prefix.

Because value_field is not configured, the entire row is serialized as JSON before being written. With read_key_enabled = true, the original Redis key is also included as a field in the output record.

Hash: Flatten and Merge into a List

When no schema is configured, the connector serializes all field-value pairs in each hash into a single JSON object and sends it as one row. The values remain strings, while the hash key itself is not included in the data row.

If the sink is configured with data_type = list, records from multiple hashes can be appended to the same Redis list. This is useful when you need to aggregate hashes with different field structures into a single downstream stream.

source {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    keys = "user:ext:*"
    data_type = hash
    batch_size = 200
  }
}

sink {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    key = "audit:user:ext:queue"
    data_type = list
    batch_size = 200
  }
}
Enter fullscreen mode Exit fullscreen mode

Test data:

HSET user:ext:1001 city Shanghai level gold
HSET user:ext:1002 city Beijing level silver device iOS
Enter fullscreen mode Exit fullscreen mode

Verify the result with:

LRANGE audit:user:ext:queue 0 -1
1) "{\"city\":\"Shanghai\",\"level\":\"gold\"}"
2) "{\"city\":\"Beijing\",\"level\":\"silver\",\"device\":\"iOS\"}"
Enter fullscreen mode Exit fullscreen mode

The two hashes have different field structures, but both are flattened into the same Redis list.

If you want to split the hash into individual columns instead of keeping the entire hash as a JSON object, configure a schema and set hash_key_parse_mode = kv. In this mode, the first field in the schema is used to store the original hash key, and each key-value pair is emitted as a separate row.

Set: Expand Each Member into a List

For Redis sets, each member is emitted as an individual row. In other words, the data granularity changes from a collection to a stream of individual values.

Configure the sink with data_type = list to append these values to a Redis list one by one.

source {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    keys = "tag:members:*"
    data_type = set
    batch_size = 200
  }
}

sink {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    key = "tag:members:merged"
    data_type = list
    batch_size = 200
  }
}
Enter fullscreen mode Exit fullscreen mode

Test data:

SADD tag:members:vip     1001 1003 1007
SADD tag:members:newuser 1002 1003
Enter fullscreen mode Exit fullscreen mode

Verify the result with:

LRANGE tag:members:merged 0 -1
1) "1001"
2) "1003"
3) "1007"
4) "1002"
5) "1003"
Enter fullscreen mode Exit fullscreen mode

There are two things to keep in mind.

First, Redis sets are unordered. The order shown above is only an example and should not be treated as meaningful business ordering.

Second, the list does not deduplicate values across different source sets. Since 1003 exists in both source sets, it appears twice in the destination list.

If you want to merge the data and remove duplicates, change the sink to data_type = set.

ZSet: Expand Each Member, but Scores Are Lost

A Redis sorted set (zset) behaves similarly to a set in this connector scenario: each member is emitted as a separate row. However, the score is not read and is not included in the output record, so the downstream system receives only the member values.

source {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    keys = "rank:board:*"
    data_type = zset
    batch_size = 200
  }
}

sink {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    key = "rank:members:merged"
    data_type = list
    batch_size = 200
  }
}
Enter fullscreen mode Exit fullscreen mode

Test data:

ZADD rank:board:day  9800 1001 9500 1003
ZADD rank:board:week 7200 1002
Enter fullscreen mode Exit fullscreen mode

Verify the result with:

LRANGE rank:members:merged 0 -1
1) "1001"
2) "1003"
3) "1002"
Enter fullscreen mode Exit fullscreen mode

The scores of the three members, 9800, 9500, and 7200, are all lost during the migration.

The same limitation applies when writing to a zset: the sink uses a fixed score of 1 rather than preserving the original score. In other words, neither the source read nor the destination write carries the actual zset score.

Therefore, this approach should not be used for use cases such as leaderboards or priority queues, where the score is part of the business logic.

Summary

The four Redis data types correspond to three different data transformation patterns:

  • String: structured pass-through
  • Hash: flatten and merge
  • Set/ZSet: expand each member into an individual record

For many Redis-to-Redis migration scenarios, Apache SeaTunnel can handle the data transfer directly through connector configuration, without requiring custom migration scripts.

At the same time, the connector's boundaries are clear. By default, the hash key, set ordering, and zset scores are not preserved.

If your application depends on any of these pieces of metadata, you will need to consider a custom script or another migration approach.

Top comments (0)