Um Serviço de Lock Distribuído é um mecanismo projetado para coordenar o acesso a recursos compartilhados em sistemas distribuídos. Ele garante que múltiplos processos, serviços ou sistemas não acessem simultaneamente uma seção crítica de código ou dados, prevenindo inconsistências e corrupção de dados.
Em sistemas distribuídos, onde recursos são compartilhados entre diferentes nós, garantir a exclusividade no acesso a certos recursos é essencial para a consistência e integridade dos dados.
O principal objetivo de um serviço de lock distribuído é garantir exclusão mútua em ambientes distribuídos. Isso significa que apenas um processo pode possuir o lock para um determinado recurso em um momento específico.
Benefícios:
- Consistência: Evita condições de corrida (race conditions), garantindo que apenas uma entidade acesse um recurso de cada vez.
- Tolerância a Falhas: Lida com falhas de nós ou processos, liberando locks de forma segura quando necessário.
- Escalabilidade: Permite operações seguras em sistemas com milhares de nós e serviços concorrentes.
- Durabilidade: Os locks podem ser configurados para sobreviver a reinicializações, dependendo da arquitetura utilizada.
A criação de uma solução de lock distribuído é um desafio significativo em sistemas distribuídos. Isso ocorre devido à necessidade de garantir exclusão mútua, consistência, e tolerância a falhas em ambientes onde não há um relógio global ou sincronização perfeita entre os nós.
-
Falhas de Rede:
- A comunicação entre os nós pode sofrer atrasos, perdas ou desconexões momentâneas, dificultando a coordenação do estado do lock.
-
Relógios Desincronizados:
- Sistemas distribuídos não possuem relógios perfeitamente sincronizados, o que pode causar problemas ao determinar a validade de um lock.
-
Tolerância a Falhas:
- A solução deve lidar com falhas de nós ou processos sem causar deadlocks ou inconsistências.
-
Alcance do Quórum:
- Garantir que o quórum necessário para validar ou liberar locks seja atingido, mesmo em caso de falhas de alguns nós.
A solução foi desenvolvida utilizando a técnica RedLock, criada por Salvatore Sanfilippo, o criador do Redis. Essa técnica é uma abordagem prática e robusta para implementar locks distribuídos usando múltiplas instâncias do Redis.
-
Distribuição de Nós:
- A técnica requer pelo menos 3 instâncias de Redis, preferencialmente em diferentes zonas de disponibilidade, para evitar falhas correlacionadas.
-
Processo de Aquisição:
- O cliente tenta adquirir o lock em todas as instâncias Redis, uma a uma, utilizando o comando atômico
SET NX
com um TTL definido.
- O cliente tenta adquirir o lock em todas as instâncias Redis, uma a uma, utilizando o comando atômico
-
Validação do Quórum:
- O lock é considerado válido se o cliente conseguir adquirir o lock em
mais da metade
das instâncias (quórum) antes que o TTL expire.
- O lock é considerado válido se o cliente conseguir adquirir o lock em
-
Manutenção e Liberação:
- O lock pode ser renovado (refresh) para estender seu tempo de validade.
- A liberação do lock deve ser realizada em todas as instâncias que o possuem.
Este projeto implementa um serviço de lock distribuído utilizando Redis como backend, aproveitando sua capacidade de executar operações atômicas e notificações de eventos.
-
Aquisição de Locks:
- Clientes podem adquirir um lock em um recurso específico por um período de tempo definido (TTL).
- Suporte para retries automáticos com backoff exponencial.
-
Liberação de Locks:
- Locks podem ser liberados explicitamente ou expirar automaticamente.
-
Renovação de Locks:
- Permite estender o tempo de vida (TTL) de um lock ativo para evitar expiração durante operações críticas.
-
Alta Disponibilidade:
- Funciona com clusters Redis para maior tolerância a falhas e escalabilidade.
-
Redis:
- Armazena o estado dos locks.
- Utiliza operações atômicas como SETNX e EXPIRE para gerenciar os locks.
-
Serviço de API de Lock:
- Exponibiliza endpoints HTTP para aquisição, liberação e renovação de locks.
- Implementado em Go e projetado para ser stateless.
-
Clientes:
- Consumidores interagem com o serviço usando o SDK ou diretamente via HTTP.
- Docker: Para executar o Redis e o serviço de lock.
- Go: Para compilar e executar o código.
- Clone o repositório:
git clone https://github.com/Waelson/lock-manager-service.git
cd lock-manager-service
- Execute os serviços com Docker Compose:
docker-compose up --build
- Verifique se o serviço do cliente está rodando:
curl -X POST -H "Content-Type: application/json" -d '{"item_name": "item1", "quantity": 1}' http://localhost:9090/order
O projeto inclui um script de teste de carga para avaliar a eficiência do serviço. Ele realiza múltiplas requisições simultâneas para simular cenários de uso real.
O script está disponível no diretório raiz do projeto com o nome run_requests.sh
.
- Realiza múltiplos lotes de requisições simultâneas.
- Exibe uma barra de progresso e um sumário das requisições bem-sucedidas e com erro.
- Gera um log (responses.log) com o resultado de cada requisição.
- Torne o script executável:
chmod +x run_requests.sh
- Execute o script:
./run_requests.sh
- Verifique o progresso no terminal e os resultados no arquivo
responses.log
.
[############################### ] 70% (70/100) | Successful: 690 | Failed: 10
O SDK LockClient
é uma biblioteca em Go que permite integrar facilmente com o serviço de lock distribuído. Ele fornece métodos para adquirir, liberar e renovar locks de forma programática, simplificando a interação com o serviço de API.
O SDK fornece os seguintes métodos principais:
- Acquire: Adquire um lock para um recurso.
- Release: Libera um lock adquirido.
- Refresh: Renova o TTL de um lock ativo.
O cliente LockClient pode ser configurado usando o padrão de options, permitindo flexibilidade na configuração do backoff exponencial.
package main
import (
"time"
"github.com/Waelson/lock-manager-service/order-service-api/pkg/sdk/locker"
)
func main() {
// Configuração do cliente com backoff exponencial
client := sdk.NewLockClient(
"http://localhost:8181", // URL do serviço de lock
sdk.WithExponentialBackoff(&sdk.ExponentialBackoff{
Initial: 200 * time.Millisecond,
Max: 5 * time.Second,
MaxJitter: 1 * time.Second,
}),
)
// Exemplo de uso do cliente
_ = client // Evitar erro de variável não utilizada
}
- Adquirir um Lock
Para adquirir um lock, utilize o método Acquire. Este método retorna o lock adquirido e uma função para liberar o lock.
package main
import (
"context"
"fmt"
"time"
"github.com/Waelson/lock-manager-service/order-service-api/pkg/sdk/locker"
)
func main() {
client := ...
ctx := context.Background()
resource := "my-resource"
ttl := "50ms"
expire := "100ms"
// Adquirir lock
lock, releaseFunc, err := client.Acquire(ctx, resource, ttl, expire)
if err != nil {
fmt.Printf("Erro ao adquirir lock: %v\n", err)
return
}
fmt.Printf("Lock adquirido: Token=%s, Recurso=%s\n", lock.Token, lock.Resource)
// Liberar lock ao final
defer func() {
if err := releaseFunc(); err != nil {
fmt.Printf("Erro ao liberar lock: %v\n", err)
} else {
fmt.Println("Lock liberado com sucesso.")
}
}()
}
- Liberar um Lock
Para liberar um lock manualmente, use o método Release
.
package main
import (
"context"
"fmt"
"github.com/Waelson/lock-manager-service/order-service-api/pkg/sdk/locker"
)
func main() {
client := ...
ctx := context.Background()
lock := &sdk.Lock{
Token: "example-token",
Resource: "my-resource",
}
// Liberar lock
err := client.Release(ctx, lock)
if err != nil {
fmt.Printf("Erro ao liberar lock: %v\n", err)
} else {
fmt.Println("Lock liberado com sucesso.")
}
}
- Renovar um Lock
Para renovar o TTL de um lock ativo, use o método Refresh
.
package main
import (
"context"
"fmt"
"time"
"github.com/Waelson/lock-manager-service/order-service-api/pkg/sdk/locker"
)
func main() {
client := ...
ctx := context.Background()
lock := &sdk.Lock{
Token: "example-token",
Resource: "my-resource",
}
newTTL := "300ms"
// Renovar lock
err := client.Refresh(ctx, lock, newTTL)
if err != nil {
fmt.Printf("Erro ao renovar lock: %v\n", err)
} else {
fmt.Printf("Lock renovado com sucesso para %s\n", newTTL)
}
}