JSON Document Management in Redis
In modern distributed architectures, Redis has evolved from a simple key-value store into a powerful Document Database through the RedisJSON module. This allows developers to store, update, and query complex nested structures with sub-millisecond latency, bypassing the overhead of full-object serialization.
1. The Security Layer: ACL and Key-Space Jailing
Before interacting with data, we must establish a secure context. Using Redis Access Control Lists (ACLs), we define a specialized user for our DevOps pipeline:
ACL SETUSER devops_api_user +json.set +json.getWhy this configuration?
- Category Enforcement: By granting
+json.set +json.get, we enable Only get ant set operations to native JSON operations.
2. Native Data Manipulation via CLI
RedisJSON stores documents in a binary format (similar to BSON), enabling efficient access to internal fields using JSONPath syntax.
Storing the Document
To initialize our dataset, we use JSON.SET. The $ sign represents the root of the document:
# Clean start
DEL user:profile:data
# JSON.SET <key> <path> <value>
JSON.SET user:profile $ '{"profile": [{"name": "Fausto", "age": 48, "city": "Porto", "skills": ["Python", "Redis", "PostgreSQL", "Jenkins"]},
{"name": "Ana Silva", "age": 28, "city": "Lisboa", "active": true, "skills": ["Java", "Spring"], "orders": [101, 105]},
{"name": "Bruno Costa", "age": 35, "city": "Braga", "active": false, "skills": ["Go", "Docker"], "orders": []},
{"name": "Carla Matos", "age": 22, "city": "Coimbra", "active": true, "skills": ["JavaScript", "React"], "orders": [200]},
{"name": "Diogo Ferro", "age": 40, "city": "Faro", "active": true, "skills": ["Python", "Django", "PostgreSQL"], "orders": [301, 302, 303]},
{"name": "Elena Santos", "age": 29, "city": "Porto", "active": true, "skills": ["Vue.js", "Firebase"], "orders": [450]},
{"name": "Filipe Rosa", "age": 31, "city": "Setúbal", "active": false, "skills": ["C#", "Azure"], "orders": []},
{"name": "Gonçalo Vaz", "age": 26, "city": "Aveiro", "active": true, "skills": ["Ruby", "Rails"], "orders": [501]},
{"name": "Helena Cruz", "age": 45, "city": "Évora", "active": true, "skills": ["PHP", "Laravel", "Redis"], "orders": [601, 605]},
{"name": "Igor Lima", "age": 33, "city": "Funchal", "active": true, "skills": ["Rust", "Wasm"], "orders": [777]}]}'
The Power of JSONPath Querying
One of RedisJSON’s greatest strengths is its ability to perform Partial Retrieval and Server-Side Filtering, reducing network bandwidth.
Few Examples:
Exact Query:
JSON.GET user:profile '$.profile[?(@.name == "Fausto")]'
"[{\"name\":\"Fausto\",\"age\":48,\"city\":\"Porto\",\"skills\":[\"Python\",\"Redis\",\"PostgreSQL\",\"Jenkins\"]}]"Broad Extraction: JSON.GET user:profile:data $.profile[*].name extracts only the names into a flat list.
JSON.GET user:profile $.profile[*].name
"[\"Fausto\",\"Ana Silva\",\"Bruno Costa\",\"Carla Matos\",\"Diogo Ferro\",\"Elena Santos\",\"Filipe Rosa\",\"Gon\xc3\xa7alo Vaz\",\"Helena Cruz\",\"Igor Lima\"]"
JSON.GET user:profile $.profile[*].name $.profile[*].age $.profile[*].city
"{\"$.profile[*].age\":[48,28,35,22,40,29,31,26,45,33],\"$.profile[*].city\":[\"Porto\",\"Lisboa\",\"Braga\",\"Coimbra\",\"Faro\",\"Porto\",\"Set\xc3\xbabal\",\"Aveiro\",\"\xc3\x89vora\",\"Funchal\"],\"$.profile[*].name\":[\"Fausto\",\"Ana Silva\",\"Bruno Costa\",\"Carla Matos\",\"Diogo Ferro\",\"Elena Santos\",\"Filipe Rosa\",\"Gon\xc3\xa7alo Vaz\",\"Helena Cruz\",\"Igor Lima\"]}"Advanced Filtering: JSON.GET user:profile:data '$.profile[?(@.age > 25 && (@.city == "Porto" || @.city == "Lisboa"))]' The query engine evaluates the logical expression inside [?(...)] directly in memory, returning only the objects that satisfy the condition.
JSON.GET user:profile '$.profile[?(@.age > 25 && (@.city == "Porto" || @.city == "Lisboa"))]'
"[{\"name\":\"Fausto\",\"age\":48,\"city\":\"Porto\",\"skills\":[\"Python\",\"Redis\",\"PostgreSQL\",\"Jenkins\"]},{\"name\":\"Ana Silva\",\"age\":28,\"city\":\"Lisboa\",\"active\":true,\"skills\":[\"Java\",\"Spring\"],\"orders\":[101,105]},{\"name\":\"Elena Santos\",\"age\":29,\"city\":\"Porto\",\"active\":true,\"skills\":[\"Vue.js\",\"Firebase\"],\"orders\":[450]}]"3. Programmatic Implementation: The Python Pattern
In a production environment, we use the redis-py client. The following implementation demonstrates a “Population and Projection” pattern, where we insert data and then project specific fields into a clean dictionary.
import redis
import json
# Connection configuration
REDIS_HOST = 'redis.devops-db.internal'
REDIS_PORT = 6379
REDIS_USER = 'devops_api_user'
REDIS_PASSWORD = 'Pr4UmfeLGGJEnBu8zX6VgAYux65C0ide'
def manage_profiles():
try:
# Client initialization with RBAC
client = redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
username=REDIS_USER,
password=REDIS_PASSWORD,
decode_responses=True
)
# 1. Dataset definition
raw_data = {
"profile": [
{"name": "Fausto", "age": 48, "city": "Porto", "skills": ["Python", "Redis", "Jenkins"]},
{"name": "Ana Silva", "age": 28, "city": "Lisboa", "active": True, "skills": ["Java", "Spring"]},
# ... (other profiles)
]
}
# 2. Atomic Storage (Key matches the ~user:profile:* ACL pattern)
client.json().set("user:profile:data", "$", raw_data)
print("Successfully stored 'user:profile:data' in Redis.")
# 3. Server-side filtering using JSONPath
# Fetching users older than 30
query_path = "$.profile[?(@.age > 30)]"
matched_profiles = client.json().get("user:profile:data", query_path)
# 4. Projection: Clean output for downstream consumers
print("\nFiltered Profiles (Age > 30):")
if matched_profiles:
formatted_output = [
{
"name": p.get("name"),
"age": p.get("age"),
"city": p.get("city")
}
for p in matched_profiles
]
for item in formatted_output:
print(json.dumps(item, ensure_ascii=False))
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
manage_profiles()
Executing
python3 json_redis.py
Successfully stored 'user:profile' in Redis.
Filtered Profiles (Age > 30):
{"name": "Fausto", "age": 48, "city": "Porto"}
{"name": "Bruno Costa", "age": 35, "city": "Braga"}
{"name": "Diogo Ferro", "age": 40, "city": "Faro"}
{"name": "Filipe Rosa", "age": 31, "city": "Setúbal"}
{"name": "Helena Cruz", "age": 45, "city": "Évora"}
{"name": "Igor Lima", "age": 33, "city": "Funchal"}4. Key Performance Insights
- Atomicity:
JSON.SETon a specific path (e.g.,$.profile[0].age) is atomic. You don’t need to lock the whole document to update one field. - Memory Efficiency: RedisJSON avoids the overhead of converting JSON to strings repeatedly.
- Path Complexity: Using recursive descent (
..) or complex filters (?()) is powerful but should be used judiciously on very large arrays to maintain low latency.
