237 lines
6.4 KiB
Markdown
237 lines
6.4 KiB
Markdown
# Distributed Mutex
|
||
|
||
A Redis-based distributed mutex implementation for coordinating access to shared resources across multiple application instances.
|
||
|
||
## Overview
|
||
|
||
This package provides a distributed locking mechanism using Redis as the coordination backend. It's built on top of the `redislock` library and provides a simple interface for acquiring and releasing locks across distributed systems.
|
||
|
||
## Features
|
||
|
||
- **Distributed Locking**: Coordinate access to shared resources across multiple application instances
|
||
- **Automatic Retry**: Built-in retry logic with configurable backoff strategy
|
||
- **Thread-Safe**: Safe for concurrent use within a single application
|
||
- **Formatted Keys**: Support for formatted lock keys using `Acquiref` and `Releasef`
|
||
- **Logging**: Integrated zap logging for debugging and monitoring
|
||
|
||
## Installation
|
||
|
||
The package is already included in the project. The required dependency (`github.com/bsm/redislock`) is automatically installed.
|
||
|
||
## Interface
|
||
|
||
```go
|
||
type Adapter interface {
|
||
Acquire(ctx context.Context, key string)
|
||
Acquiref(ctx context.Context, format string, a ...any)
|
||
Release(ctx context.Context, key string)
|
||
Releasef(ctx context.Context, format string, a ...any)
|
||
}
|
||
```
|
||
|
||
## Usage
|
||
|
||
### Basic Example
|
||
|
||
```go
|
||
import (
|
||
"context"
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/distributedmutex"
|
||
)
|
||
|
||
// Create Redis client
|
||
redisClient := redis.NewClient(&redis.Options{
|
||
Addr: "localhost:6379",
|
||
})
|
||
|
||
// Create logger
|
||
logger, _ := zap.NewProduction()
|
||
|
||
// Create distributed mutex adapter
|
||
mutex := distributedmutex.NewAdapter(logger, redisClient)
|
||
|
||
// Acquire a lock
|
||
ctx := context.Background()
|
||
mutex.Acquire(ctx, "my-resource-key")
|
||
|
||
// ... perform operations on the protected resource ...
|
||
|
||
// Release the lock
|
||
mutex.Release(ctx, "my-resource-key")
|
||
```
|
||
|
||
### Formatted Keys Example
|
||
|
||
```go
|
||
// Acquire lock with formatted key
|
||
tenantID := "tenant-123"
|
||
resourceID := "resource-456"
|
||
|
||
mutex.Acquiref(ctx, "tenant:%s:resource:%s", tenantID, resourceID)
|
||
|
||
// ... perform operations ...
|
||
|
||
mutex.Releasef(ctx, "tenant:%s:resource:%s", tenantID, resourceID)
|
||
```
|
||
|
||
### Integration with Dependency Injection (Wire)
|
||
|
||
```go
|
||
// In your Wire provider set
|
||
wire.NewSet(
|
||
distributedmutex.ProvideDistributedMutexAdapter,
|
||
// ... other providers
|
||
)
|
||
|
||
// Use in your application
|
||
func NewMyService(mutex distributedmutex.Adapter) *MyService {
|
||
return &MyService{
|
||
mutex: mutex,
|
||
}
|
||
}
|
||
```
|
||
|
||
## Configuration
|
||
|
||
### Lock Duration
|
||
|
||
The default lock duration is **1 minute**. Locks are automatically released after this time to prevent deadlocks.
|
||
|
||
### Retry Strategy
|
||
|
||
- **Retry Interval**: 250ms
|
||
- **Max Retries**: 20 attempts
|
||
- **Total Max Wait Time**: ~5 seconds (20 × 250ms)
|
||
|
||
If a lock cannot be obtained after all retries, an error is logged and the `Acquire` method returns without blocking indefinitely.
|
||
|
||
## Best Practices
|
||
|
||
1. **Always Release Locks**: Ensure locks are released even in error cases using `defer`
|
||
```go
|
||
mutex.Acquire(ctx, "my-key")
|
||
defer mutex.Release(ctx, "my-key")
|
||
```
|
||
|
||
2. **Use Descriptive Keys**: Use clear, hierarchical key names
|
||
```go
|
||
// Good
|
||
mutex.Acquire(ctx, "tenant:123:user:456:update")
|
||
|
||
// Not ideal
|
||
mutex.Acquire(ctx, "lock1")
|
||
```
|
||
|
||
3. **Keep Critical Sections Short**: Minimize the time locks are held to improve concurrency
|
||
|
||
4. **Handle Timeouts**: Use context with timeout for critical operations
|
||
```go
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
mutex.Acquire(ctx, "my-key")
|
||
```
|
||
|
||
5. **Avoid Nested Locks**: Be careful with acquiring multiple locks to avoid deadlocks
|
||
|
||
## Logging
|
||
|
||
The adapter logs the following events:
|
||
|
||
- **Debug**: Lock acquisition and release operations
|
||
- **Error**: Failed lock acquisitions, timeout errors, and release failures
|
||
- **Warn**: Attempts to release non-existent locks
|
||
|
||
## Thread Safety
|
||
|
||
The adapter is safe for concurrent use within a single application instance. It uses an internal mutex to protect the lock instances map from concurrent access by multiple goroutines.
|
||
|
||
## Error Handling
|
||
|
||
The current implementation logs errors but does not return them. Consider this when using the adapter:
|
||
|
||
- Lock acquisition failures are logged but don't panic
|
||
- The application continues running even if locks fail
|
||
- Check logs for lock-related issues in production
|
||
|
||
## Limitations
|
||
|
||
1. **Lock Duration**: Locks automatically expire after 1 minute
|
||
2. **No Lock Extension**: Currently doesn't support extending lock duration
|
||
3. **No Deadlock Detection**: Manual deadlock prevention is required
|
||
4. **Redis Dependency**: Requires a running Redis instance
|
||
|
||
## Example Use Cases
|
||
|
||
### Preventing Duplicate Processing
|
||
|
||
```go
|
||
func ProcessJob(ctx context.Context, jobID string, mutex distributedmutex.Adapter) {
|
||
lockKey := fmt.Sprintf("job:processing:%s", jobID)
|
||
|
||
mutex.Acquire(ctx, lockKey)
|
||
defer mutex.Release(ctx, lockKey)
|
||
|
||
// Process job - guaranteed only one instance processes this job
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### Coordinating Resource Updates
|
||
|
||
```go
|
||
func UpdateTenantSettings(ctx context.Context, tenantID string, mutex distributedmutex.Adapter) error {
|
||
mutex.Acquiref(ctx, "tenant:%s:settings:update", tenantID)
|
||
defer mutex.Releasef(ctx, "tenant:%s:settings:update", tenantID)
|
||
|
||
// Safe to update tenant settings
|
||
// ...
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### Rate Limiting Operations
|
||
|
||
```go
|
||
func RateLimitedOperation(ctx context.Context, userID string, mutex distributedmutex.Adapter) {
|
||
lockKey := fmt.Sprintf("ratelimit:user:%s", userID)
|
||
|
||
mutex.Acquire(ctx, lockKey)
|
||
defer mutex.Release(ctx, lockKey)
|
||
|
||
// Perform rate-limited operation
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## Troubleshooting
|
||
|
||
### Lock Not Acquired
|
||
|
||
**Problem**: Locks are not being acquired (error in logs)
|
||
|
||
**Solutions**:
|
||
- Verify Redis is running and accessible
|
||
- Check network connectivity to Redis
|
||
- Ensure Redis has sufficient memory
|
||
- Check for Redis errors in logs
|
||
|
||
### Lock Contention
|
||
|
||
**Problem**: Frequent lock acquisition failures due to contention
|
||
|
||
**Solutions**:
|
||
- Reduce critical section duration
|
||
- Use more specific lock keys to reduce contention
|
||
- Consider increasing retry limits if appropriate
|
||
- Review application architecture for excessive locking
|
||
|
||
### Memory Leaks
|
||
|
||
**Problem**: Lock instances accumulating in memory
|
||
|
||
**Solutions**:
|
||
- Ensure all `Acquire` calls have corresponding `Release` calls
|
||
- Use `defer` to guarantee lock release
|
||
- Monitor lock instance map size in production
|