mirror of
https://github.com/redis/go-redis.git
synced 2025-11-14 10:22:26 +03:00
feat(cmd): Add CAS/CAD commands (#3583)
* add cas/cad commands
* feat(command): Add SetIFDEQ, SetIFDNE and *Get cmds
Decided to move the *Get argument as a separate methods, since the
response will be always the previous value, but in the case where
the previous value is `OK` there result may be ambiguous.
* fix tests
* matchValue to be interface{}
* Only Args approach for DelEx
* use uint64 for digest, add example
* test only for 8.4
This commit is contained in:
200
example/digest-optimistic-locking/README.md
Normal file
200
example/digest-optimistic-locking/README.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
cd example/digest-optimistic-locking
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
## Running the Example
|
||||
|
||||
```bash
|
||||
# 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`:
|
||||
|
||||
```go
|
||||
import "github.com/zeebo/xxh3"
|
||||
|
||||
// For strings
|
||||
digest := xxh3.HashString("myvalue")
|
||||
|
||||
// For binary data
|
||||
digest := xxh3.Hash([]byte{0x01, 0x02, 0x03})
|
||||
```
|
||||
|
||||
### Optimistic Locking Pattern
|
||||
|
||||
```go
|
||||
// 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)
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
```go
|
||||
// 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
|
||||
|
||||
- [Redis DIGEST command](https://redis.io/commands/digest/)
|
||||
- [Redis SET command with IFDEQ/IFDNE](https://redis.io/commands/set/)
|
||||
- [xxh3 hashing algorithm](https://github.com/Cyan4973/xxHash)
|
||||
- [github.com/zeebo/xxh3](https://github.com/zeebo/xxh3)
|
||||
|
||||
## 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](../../XXHASH_LIBRARY_COMPARISON.md) for detailed comparison.
|
||||
|
||||
16
example/digest-optimistic-locking/go.mod
Normal file
16
example/digest-optimistic-locking/go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/redis/go-redis/example/digest-optimistic-locking
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/redis/go-redis/v9 => ../..
|
||||
|
||||
require (
|
||||
github.com/redis/go-redis/v9 v9.16.0
|
||||
github.com/zeebo/xxh3 v1.0.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
)
|
||||
11
example/digest-optimistic-locking/go.sum
Normal file
11
example/digest-optimistic-locking/go.sum
Normal file
@@ -0,0 +1,11 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
245
example/digest-optimistic-locking/main.go
Normal file
245
example/digest-optimistic-locking/main.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/zeebo/xxh3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Connect to Redis
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer rdb.Close()
|
||||
|
||||
// Ping to verify connection
|
||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||
fmt.Printf("Failed to connect to Redis: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("=== Redis Digest & Optimistic Locking Example ===")
|
||||
fmt.Println()
|
||||
|
||||
// Example 1: Basic Digest Usage
|
||||
fmt.Println("1. Basic Digest Usage")
|
||||
fmt.Println("---------------------")
|
||||
basicDigestExample(ctx, rdb)
|
||||
fmt.Println()
|
||||
|
||||
// Example 2: Optimistic Locking with SetIFDEQ
|
||||
fmt.Println("2. Optimistic Locking with SetIFDEQ")
|
||||
fmt.Println("------------------------------------")
|
||||
optimisticLockingExample(ctx, rdb)
|
||||
fmt.Println()
|
||||
|
||||
// Example 3: Detecting Changes with SetIFDNE
|
||||
fmt.Println("3. Detecting Changes with SetIFDNE")
|
||||
fmt.Println("-----------------------------------")
|
||||
detectChangesExample(ctx, rdb)
|
||||
fmt.Println()
|
||||
|
||||
// Example 4: Conditional Delete with DelExArgs
|
||||
fmt.Println("4. Conditional Delete with DelExArgs")
|
||||
fmt.Println("-------------------------------------")
|
||||
conditionalDeleteExample(ctx, rdb)
|
||||
fmt.Println()
|
||||
|
||||
// Example 5: Client-Side Digest Generation
|
||||
fmt.Println("5. Client-Side Digest Generation")
|
||||
fmt.Println("---------------------------------")
|
||||
clientSideDigestExample(ctx, rdb)
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("=== All examples completed successfully! ===")
|
||||
}
|
||||
|
||||
// basicDigestExample demonstrates getting a digest from Redis
|
||||
func basicDigestExample(ctx context.Context, rdb *redis.Client) {
|
||||
// Set a value
|
||||
key := "user:1000:name"
|
||||
value := "Alice"
|
||||
rdb.Set(ctx, key, value, 0)
|
||||
|
||||
// Get the digest
|
||||
digest := rdb.Digest(ctx, key).Val()
|
||||
|
||||
fmt.Printf("Key: %s\n", key)
|
||||
fmt.Printf("Value: %s\n", value)
|
||||
fmt.Printf("Digest: %d (0x%016x)\n", digest, digest)
|
||||
|
||||
// Verify with client-side calculation
|
||||
clientDigest := xxh3.HashString(value)
|
||||
fmt.Printf("Client-calculated digest: %d (0x%016x)\n", clientDigest, clientDigest)
|
||||
|
||||
if digest == clientDigest {
|
||||
fmt.Println("✓ Digests match!")
|
||||
}
|
||||
}
|
||||
|
||||
// optimisticLockingExample demonstrates using SetIFDEQ for optimistic locking
|
||||
func optimisticLockingExample(ctx context.Context, rdb *redis.Client) {
|
||||
key := "counter"
|
||||
|
||||
// Initial value
|
||||
rdb.Set(ctx, key, "100", 0)
|
||||
fmt.Printf("Initial value: %s\n", rdb.Get(ctx, key).Val())
|
||||
|
||||
// Get current digest
|
||||
currentDigest := rdb.Digest(ctx, key).Val()
|
||||
fmt.Printf("Current digest: 0x%016x\n", currentDigest)
|
||||
|
||||
// Simulate some processing time
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Try to update only if value hasn't changed (digest matches)
|
||||
newValue := "150"
|
||||
result := rdb.SetIFDEQ(ctx, key, newValue, currentDigest, 0)
|
||||
|
||||
if result.Err() == redis.Nil {
|
||||
fmt.Println("✗ Update failed: value was modified by another client")
|
||||
} else if result.Err() != nil {
|
||||
fmt.Printf("✗ Error: %v\n", result.Err())
|
||||
} else {
|
||||
fmt.Printf("✓ Update successful! New value: %s\n", rdb.Get(ctx, key).Val())
|
||||
}
|
||||
|
||||
// Try again with wrong digest (simulating concurrent modification)
|
||||
wrongDigest := uint64(12345)
|
||||
result = rdb.SetIFDEQ(ctx, key, "200", wrongDigest, 0)
|
||||
|
||||
if result.Err() == redis.Nil {
|
||||
fmt.Println("✓ Correctly rejected update with wrong digest")
|
||||
}
|
||||
}
|
||||
|
||||
// detectChangesExample demonstrates using SetIFDNE to detect if a value changed
|
||||
func detectChangesExample(ctx context.Context, rdb *redis.Client) {
|
||||
key := "config:version"
|
||||
|
||||
// Set initial value
|
||||
oldValue := "v1.0.0"
|
||||
rdb.Set(ctx, key, oldValue, 0)
|
||||
fmt.Printf("Initial value: %s\n", oldValue)
|
||||
|
||||
// Calculate digest of a DIFFERENT value (what we expect it NOT to be)
|
||||
unwantedValue := "v0.9.0"
|
||||
unwantedDigest := xxh3.HashString(unwantedValue)
|
||||
fmt.Printf("Unwanted value digest: 0x%016x\n", unwantedDigest)
|
||||
|
||||
// Update to new value only if current value is NOT the unwanted value
|
||||
// (i.e., only if digest does NOT match unwantedDigest)
|
||||
newValue := "v2.0.0"
|
||||
result := rdb.SetIFDNE(ctx, key, newValue, unwantedDigest, 0)
|
||||
|
||||
if result.Err() == redis.Nil {
|
||||
fmt.Println("✗ Current value matches unwanted value (digest matches)")
|
||||
} else if result.Err() != nil {
|
||||
fmt.Printf("✗ Error: %v\n", result.Err())
|
||||
} else {
|
||||
fmt.Printf("✓ Current value is different from unwanted value! Updated to: %s\n", rdb.Get(ctx, key).Val())
|
||||
}
|
||||
|
||||
// Try to update again, but this time the digest matches current value (should fail)
|
||||
currentDigest := rdb.Digest(ctx, key).Val()
|
||||
result = rdb.SetIFDNE(ctx, key, "v3.0.0", currentDigest, 0)
|
||||
|
||||
if result.Err() == redis.Nil {
|
||||
fmt.Println("✓ Correctly rejected: current value matches the digest (IFDNE failed)")
|
||||
}
|
||||
}
|
||||
|
||||
// conditionalDeleteExample demonstrates using DelExArgs with digest
|
||||
func conditionalDeleteExample(ctx context.Context, rdb *redis.Client) {
|
||||
key := "session:abc123"
|
||||
value := "user_data_here"
|
||||
|
||||
// Set a value
|
||||
rdb.Set(ctx, key, value, 0)
|
||||
fmt.Printf("Created session: %s\n", key)
|
||||
|
||||
// Calculate expected digest
|
||||
expectedDigest := xxh3.HashString(value)
|
||||
fmt.Printf("Expected digest: 0x%016x\n", expectedDigest)
|
||||
|
||||
// Try to delete with wrong digest (should fail)
|
||||
wrongDigest := uint64(99999)
|
||||
deleted := rdb.DelExArgs(ctx, key, redis.DelExArgs{
|
||||
Mode: "IFDEQ",
|
||||
MatchDigest: wrongDigest,
|
||||
}).Val()
|
||||
|
||||
if deleted == 0 {
|
||||
fmt.Println("✓ Correctly refused to delete (wrong digest)")
|
||||
}
|
||||
|
||||
// Delete with correct digest (should succeed)
|
||||
deleted = rdb.DelExArgs(ctx, key, redis.DelExArgs{
|
||||
Mode: "IFDEQ",
|
||||
MatchDigest: expectedDigest,
|
||||
}).Val()
|
||||
|
||||
if deleted == 1 {
|
||||
fmt.Println("✓ Successfully deleted with correct digest")
|
||||
}
|
||||
|
||||
// Verify deletion
|
||||
exists := rdb.Exists(ctx, key).Val()
|
||||
if exists == 0 {
|
||||
fmt.Println("✓ Session deleted")
|
||||
}
|
||||
}
|
||||
|
||||
// clientSideDigestExample demonstrates calculating digests without fetching from Redis
|
||||
func clientSideDigestExample(ctx context.Context, rdb *redis.Client) {
|
||||
key := "product:1001:price"
|
||||
|
||||
// Scenario: We know the expected current value
|
||||
expectedCurrentValue := "29.99"
|
||||
newValue := "24.99"
|
||||
|
||||
// Set initial value
|
||||
rdb.Set(ctx, key, expectedCurrentValue, 0)
|
||||
fmt.Printf("Current price: $%s\n", expectedCurrentValue)
|
||||
|
||||
// Calculate digest client-side (no need to fetch from Redis!)
|
||||
expectedDigest := xxh3.HashString(expectedCurrentValue)
|
||||
fmt.Printf("Expected digest (calculated client-side): 0x%016x\n", expectedDigest)
|
||||
|
||||
// Update price only if it matches our expectation
|
||||
result := rdb.SetIFDEQ(ctx, key, newValue, expectedDigest, 0)
|
||||
|
||||
if result.Err() == redis.Nil {
|
||||
fmt.Println("✗ Price was already changed by someone else")
|
||||
actualValue := rdb.Get(ctx, key).Val()
|
||||
fmt.Printf(" Actual current price: $%s\n", actualValue)
|
||||
} else if result.Err() != nil {
|
||||
fmt.Printf("✗ Error: %v\n", result.Err())
|
||||
} else {
|
||||
fmt.Printf("✓ Price updated successfully to $%s\n", newValue)
|
||||
}
|
||||
|
||||
// Demonstrate with binary data
|
||||
fmt.Println("\nBinary data example:")
|
||||
binaryKey := "image:thumbnail"
|
||||
binaryData := []byte{0xFF, 0xD8, 0xFF, 0xE0} // JPEG header
|
||||
|
||||
rdb.Set(ctx, binaryKey, binaryData, 0)
|
||||
|
||||
// Calculate digest for binary data
|
||||
binaryDigest := xxh3.Hash(binaryData)
|
||||
fmt.Printf("Binary data digest: 0x%016x\n", binaryDigest)
|
||||
|
||||
// Verify it matches Redis
|
||||
redisDigest := rdb.Digest(ctx, binaryKey).Val()
|
||||
if binaryDigest == redisDigest {
|
||||
fmt.Println("✓ Binary digest matches!")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user