Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
pauluswi committed Nov 17, 2024
2 parents 32e2a64 + 05a8bfb commit a67ca19
Show file tree
Hide file tree
Showing 2 changed files with 268 additions and 18 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'

- name: Build
run: go build -v ./...

260 changes: 242 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,253 @@

# Concurrency Handling in Golang

Single Machine Locks Output:
![Go Version](https://img.shields.io/badge/Go-1.23-blue)
[![Go](https://github.com/pauluswi/horten/actions/workflows/go.yml/badge.svg)](https://github.com/pauluswi/horten/actions/workflows/go.yml)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)

Processed transaction of -200.00 on account 11111. New balance: 800.00
Processed transaction of 100.00 on account 11111. New balance: 900.00
Processed transaction of 300.00 on account 11111. New balance: 1200.00
Processed transaction of -500.00 on account 22222. New balance: 1500.00
Insufficient funds for account 22222

Final Account Balances:
Account 11111: 1200.00
Account 22222: 1500.00
This repository shows two approaches to handling concurrency in Golang:

1. **Single Process Lock using `sync.Mutex`**
2. **Distributed Lock using Redis Locks**

### **Use Case**

A banking system processes account balance updates (debits and credits) from multiple concurrent transactions. The system ensures:

1. **Concurrency**: Multiple transactions are processed simultaneously.
2. **Data Safety**: Consistency of account balances using synchronization mechanisms like mutexes.
---

## 1: Single Process Lock with `sync.Mutex`

### **Overview**
This approach uses Golang's `sync.Mutex` to enforce mutual exclusion within a single process. It ensures that only one goroutine can access and modify shared resources (e.g., account balance) at a time.

### **Code Highlights**
- **Use of Mutex**: A `sync.Mutex` instance is used to lock critical sections.
- **Concurrency**: Goroutines process transactions concurrently but safely.
- **Example Use Case**:
- Suitable for single-machine systems where all operations are within the same memory space.

### **Advantages**
- Simple and lightweight.
- No external dependencies.
- Very fast as operations are performed in memory.

### **Limitations**
- Cannot be used in distributed systems.
- Limited to processes running on a single machine.

### **Code**
- Each transaction is processed in a separate goroutine using the go keyword.
- A ```sync.WaitGroup``` ensures the main function waits for all transactions to complete.
- The sync.Mutex ensures that only one goroutine can update an account's balance at a time.

```go
// Account represents a bank account
type Account struct {
AccountNumber string
Balance float64
mu sync.Mutex // Mutex to protect the balance
}
```

```go
var wg sync.WaitGroup

// Process transactions concurrently
for _, t := range transactions {
account, exists := accounts[t.AccountNumber]
if !exists {
fmt.Printf("Account %s not found\n", t.AccountNumber)
continue
}

wg.Add(1)
go account.ProcessTransaction(t.Amount, &wg)
}
```

```go
// ProcessTransaction processes a single transaction on the account
func (a *Account) ProcessTransaction(amount float64, wg *sync.WaitGroup) {
defer wg.Done() // Notify when the goroutine is finished

// Lock the account to prevent race conditions
a.mu.Lock()
defer a.mu.Unlock()

if amount < 0 && a.Balance+amount < 0 {
fmt.Printf("Insufficient funds for account %s\n", a.AccountNumber)
return
}

a.Balance += amount
fmt.Printf("Processed transaction of %.2f on account %s. New balance: %.2f\n",
amount, a.AccountNumber, a.Balance)
}

```

### Output
```
Single Machine Locks Output:
Processed transaction of -200.00 on account 11111. New balance: 800.00
Processed transaction of 100.00 on account 11111. New balance: 900.00
Processed transaction of 300.00 on account 11111. New balance: 1200.00
Processed transaction of -500.00 on account 22222. New balance: 1500.00
Insufficient funds for account 22222
Final Account Balances:
Account 11111: 1200.00
Account 22222: 1500.00
```

---

## 2: Distributed Lock using Redis Locks

### **Overview**
This approach uses Redis to simulate distributed locking. Each critical section is protected by a distributed lock, ensuring data consistency across multiple processes or machines.

### **Code Highlights**
- **Mock Redis**: A mock Redis client is used for local testing.
- **Distributed Lock**: Implemented using Redis commands (`SETNX`, `GET`, `DEL`) to acquire and release locks.
- **Example Use Case**:
- Ideal for distributed systems where multiple instances or processes need to synchronize access to shared resources.

### **Advantages**
- Supports distributed systems.
- Can synchronize processes across multiple machines.
- Scales well with cloud-native environments (e.g., Kubernetes).

### **Limitations**
- Requires a running Redis instance or equivalent service.
- Slightly slower due to network latency.
- More complex to implement and maintain.

### **Mock**
- To make a simple ready-to-go process, I utilize mock for redis to simulate redis without run real Redis instance.

### **Code**
- Each transaction is processed in a separate goroutine using the go keyword.
- A ```sync.WaitGroup``` ensures the main function waits for all transactions to complete.
- Redis SET with NX ensures that the operation is atomic, meaning the lock is created and has a TTL in a single operation.
- Simulates acquiring and releasing locks without connecting to a real Redis server.

```go
// MockRedis simulates a Redis client with basic lock functionality
type MockRedis struct {
data map[string]string
mu sync.Mutex
}
```
```go
// RedisLock represents a distributed lock

type RedisLock struct {
client *MockRedis
key string
value string
}

// AcquireLock tries to acquire the lock
func (lock *RedisLock) AcquireLock(ttl time.Duration) (bool, error) {
return lock.client.SetNX(lock.key, lock.value, ttl)
}

// ReleaseLock releases the lock
func (lock *RedisLock) ReleaseLock() error {
val, err := lock.client.Get(lock.key)
if err != nil {
return fmt.Errorf("lock not found")
}

// Ensure the lock is released by the process that acquired it
if val == lock.value {
_, err = lock.client.Del(lock.key)
if err != nil {
return err
}
}
return nil
}
```


### Output
```
Distributed Locks Output:
Processed transaction of 100.00 on account 11111. New balance: 1100.00
Insufficient funds for account 22222. Transaction skipped.
Processed transaction of 300.00 on account 11111. New balance: 1400.00
Processed transaction of -500.00 on account 22222. New balance: 1500.00
Processed transaction of -200.00 on account 11111. New balance: 1200.00
Final Account Balances:
Account 11111: 1200.00
Account 22222: 1500.00
```

---

## Comparison: `sync.Mutex` vs Redis Locks

| Feature | `sync.Mutex` | Redis Locks |
|------------------------|---------------------------------------|-----------------------------------------|
| **Scope** | Single process | Distributed processes |
| **Performance** | Very fast (in-memory) | Slightly slower (network I/O overhead) |
| **Complexity** | Simple | Medium |
| **Dependencies** | None | Redis or equivalent |
| **Use Case** | Single-machine systems | Multi-machine or distributed systems |
| **Failure Handling** | Handled within process | Requires TTL and retries |

---

## When to Use

### Use `sync.Mutex` when:
- Your application runs on a single machine.
- You don’t need to synchronize across multiple processes or machines.
- Simplicity and performance are priorities.

### Use Redis Locks when:
- Your application runs in a distributed system.
- You need to synchronize access across multiple processes or machines.
- Scalability and cross-instance consistency are required.

---

## How to Run

1. **Single Process Lock (Mutex)**
- Clone the repository.
- Navigate to the `single` folder.
- Run the program:
```bash
go run main.go
```

2. **Distributed Lock (Redis)**
- Ensure Redis is running or use the mock Redis provided.
- Navigate to the `distributed\mock_redis` folder.
- Run the program:
```bash
go run main.go
```

---

## License
This project is licensed under the MIT License.




Distributed Locks Output:

Processed transaction of 100.00 on account 11111. New balance: 1100.00
Insufficient funds for account 22222. Transaction skipped.
Processed transaction of 300.00 on account 11111. New balance: 1400.00
Processed transaction of -500.00 on account 22222. New balance: 1500.00
Processed transaction of -200.00 on account 11111. New balance: 1200.00

Final Account Balances:
Account 11111: 1200.00
Account 22222: 1500.00


0 comments on commit a67ca19

Please sign in to comment.