1
0
mirror of https://github.com/redis/go-redis.git synced 2025-11-30 18:01:23 +03:00
Files
go-redis/example/digest-optimistic-locking
ofekshenawa f711eb0f62 feat(cluster): Implement Request and Response Policy Based Routing in Cluster Mode (#3422)
* Add search module builders and tests (#1)

* Add search module builders and tests

* Add tests

* Use builders and Actions in more clean way

* Update search_builders.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Update search_builders.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* feat(routing): add internal request/response policy enums

* feat: load the policy table in cluster client (#4)

* feat: load the policy table in cluster client

* Remove comments

* modify Tips and command pplicy in commandInfo (#5)

* centralize cluster command routing in osscluster_router.go and refactor osscluster.go (#6)

* centralize cluster command routing in osscluster_router.go and refactor osscluster.go

* enalbe ci on all branches

* Add debug prints

* Add debug prints

* FIX: deal with nil policy

* FIX: fixing clusterClient process

* chore(osscluster): simplify switch case

* wip(command): ai generated clone method for commands

* feat: implement response aggregator for Redis cluster commands

* feat: implement response aggregator for Redis cluster commands

* fix: solve concurrency errors

* fix: solve concurrency errors

* return MaxRedirects settings

* remove locks from getCommandPolicy

* Handle MOVED errors more robustly, remove cluster reloading at exectutions, ennsure better routing

* Fix: supports Process hook test

* Fix: remove response aggregation for single shard commands

* Add more preformant type conversion for Cmd type

* Add router logic into processPipeline

---------

Co-authored-by: Nedyalko Dyakov <nedyalko.dyakov@gmail.com>

* remove thread debugging code

* remove thread debugging code && reject commands with policy that cannot be used in pipeline

* refactor processPipline and cmdType enum

* remove FDescribe from cluster tests

* Add tests

* fix aggregation test

* fix mget test

* fix mget test

* remove aggregateKeyedResponses

* added scaffolding for the req-resp manager

* added default policies for the search commands

* split command map into module->command

* cleanup, added logic to refresh the cache

* added reactive cache refresh

* revert cluster refresh

* fixed lint

* addresed first batch of comments

* rewrote aggregator implementations with atomic for native or nearnative primitives

* addressed more comments, fixed lint

* added batch aggregator operations

* fixed lint

* updated batch aggregator, fixed extractcommandvalue

* fixed lint

* added batching to aggregateResponses

* fixed deadlocks

* changed aggregator logic, added error params

* added preemptive return to the aggregators

* more work on the aggregators

* updated and and or aggregators

* fixed lint

* added configurable policy resolvers

* slight refactor

* removed the interface, slight refactor

* change func signature from cmdName to cmder

* added nil safety assertions

* few small refactors

* added read only policies

* removed leftover prints

* Rebased to master, resolved comnflicts

* fixed lint

* updated gha

* fixed tests, minor consistency refactor

* preallocated simple errors

* changed numeric aggregators to use float64

* speculative test fix

* Update command.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Update main_test.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Add static shard picker

* Fix nil value handling in command aggregation

* Modify the Clone method to return a shallow copy

* Add clone method to digest command

* Optimize keyless command routing to respect ShardPicker policy

* Remove MGET references

* Fix MGET aggregation to map individual values to keys across shards

* Add clone method to hybrid search commands

* Undo changes in route keyless test

* remove comments

* Add test for DisableRoutingPolicies option

* Add Routing Policies Comprehensive Test Suite and Fix multi keyed aggregation for different step

---------

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
Co-authored-by: Nedyalko Dyakov <nedyalko.dyakov@gmail.com>
Co-authored-by: Hristo Temelski <hristo.temelski@redis.com>
2025-11-28 11:46:23 +02:00
..

Redis Digest & Optimistic Locking Example

This example demonstrates how to use Redis DIGEST command and digest-based optimistic locking with go-redis.

What is Redis DIGEST?

The DIGEST command (Redis 8.4+) returns a 64-bit xxh3 hash of a key's value. This hash can be used for:

  • Optimistic locking: Update values only if they haven't changed
  • Change detection: Detect if a value was modified
  • Conditional operations: Delete or update based on expected content

Features Demonstrated

  1. Basic Digest Usage: Get digest from Redis and verify with client-side calculation
  2. Optimistic Locking with SetIFDEQ: Update only if digest matches (value unchanged)
  3. Change Detection with SetIFDNE: Update only if digest differs (value changed)
  4. Conditional Delete: Delete only if digest matches expected value
  5. Client-Side Digest Generation: Calculate digests without fetching from Redis

Requirements

  • Redis 8.4+ (for DIGEST command support)
  • Go 1.18+

Installation

cd example/digest-optimistic-locking
go mod tidy

Running the Example

# Make sure Redis 8.4+ is running on localhost:6379
redis-server

# In another terminal, run the example
go run .

Expected Output

=== Redis Digest & Optimistic Locking Example ===

1. Basic Digest Usage
---------------------
Key: user:1000:name
Value: Alice
Digest: 7234567890123456789 (0x6478a1b2c3d4e5f6)
Client-calculated digest: 7234567890123456789 (0x6478a1b2c3d4e5f6)
✓ Digests match!

2. Optimistic Locking with SetIFDEQ
------------------------------------
Initial value: 100
Current digest: 0x1234567890abcdef
✓ Update successful! New value: 150
✓ Correctly rejected update with wrong digest

3. Detecting Changes with SetIFDNE
-----------------------------------
Initial value: v1.0.0
Old digest: 0xabcdef1234567890
✓ Value changed! Updated to: v2.0.0
✓ Correctly rejected: current value matches the digest

4. Conditional Delete with DelExArgs
-------------------------------------
Created session: session:abc123
Expected digest: 0x9876543210fedcba
✓ Correctly refused to delete (wrong digest)
✓ Successfully deleted with correct digest
✓ Session deleted

5. Client-Side Digest Generation
---------------------------------
Current price: $29.99
Expected digest (calculated client-side): 0xfedcba0987654321
✓ Price updated successfully to $24.99

Binary data example:
Binary data digest: 0x1122334455667788
✓ Binary digest matches!

=== All examples completed successfully! ===

How It Works

Digest Calculation

Redis uses the xxh3 hashing algorithm. To calculate digests client-side, use github.com/zeebo/xxh3:

import "github.com/zeebo/xxh3"

// For strings
digest := xxh3.HashString("myvalue")

// For binary data
digest := xxh3.Hash([]byte{0x01, 0x02, 0x03})

Optimistic Locking Pattern

// 1. Read current value and get its digest
currentValue := rdb.Get(ctx, "key").Val()
currentDigest := rdb.Digest(ctx, "key").Val()

// 2. Perform business logic
newValue := processValue(currentValue)

// 3. Update only if value hasn't changed
result := rdb.SetIFDEQ(ctx, "key", newValue, currentDigest, 0)
if result.Err() == redis.Nil {
    // Value was modified by another client - retry or handle conflict
}

Client-Side Digest (No Extra Round Trip)

// If you know the expected current value, calculate digest client-side
expectedValue := "100"
expectedDigest := xxh3.HashString(expectedValue)

// Update without fetching digest from Redis first
result := rdb.SetIFDEQ(ctx, "counter", "150", expectedDigest, 0)

Use Cases

1. Distributed Counter with Conflict Detection

// Multiple clients can safely update a counter
currentValue := rdb.Get(ctx, "counter").Val()
currentDigest := rdb.Digest(ctx, "counter").Val()

newValue := incrementCounter(currentValue)

// Only succeeds if no other client modified it
if rdb.SetIFDEQ(ctx, "counter", newValue, currentDigest, 0).Err() == redis.Nil {
    // Retry with new value
}

2. Session Management

// Delete session only if it contains expected data
sessionData := "user:1234:active"
expectedDigest := xxh3.HashString(sessionData)

deleted := rdb.DelExArgs(ctx, "session:xyz", redis.DelExArgs{
    Mode:        "IFDEQ",
    MatchDigest: expectedDigest,
}).Val()

3. Configuration Updates

// Update config only if it changed
oldConfig := loadOldConfig()
oldDigest := xxh3.HashString(oldConfig)

newConfig := loadNewConfig()

// Only update if config actually changed
result := rdb.SetIFDNE(ctx, "config", newConfig, oldDigest, 0)
if result.Err() != redis.Nil {
    fmt.Println("Config updated!")
}

Advantages Over WATCH/MULTI/EXEC

  • Simpler: Single command instead of transaction
  • Faster: No transaction overhead
  • Client-side digest: Can calculate expected digest without fetching from Redis
  • Works with any command: Not limited to transactions

Learn More

Comparison: XXH3 vs XXH64

Note: Redis uses XXH3, not XXH64. If you have github.com/cespare/xxhash/v2 in your project, it implements XXH64 which produces different hash values. You must use github.com/zeebo/xxh3 for Redis DIGEST operations.

See XXHASH_LIBRARY_COMPARISON.md for detailed comparison.