Translate

terça-feira, 1 de abril de 2014

Mutex - Exclusão mútua em Java para controle transacional absoluto

Mutex é uma abreviação de exclusão mútua em inglês (mutual exclusion). Seu propósito é evitar que duas ou mais threads acessem simultaneamente o mesmo recurso compartilhado.

Para exemplificar um problema de falta de Mutex, imagine um sistema central multi thread responsável por apoiar o processamento de pedidos de um portal de compras. Neste cenário, o usuário pode clicar duas vezes e submeter duas vezes a requisição por uma questão de impaciência, ainda a rede de comunicação pode conter falha que redunde a requisição ou a aplicação servidora pode conter algum bug capaz de emitir mais de uma vez a requisição para o sistema de processamento final.

Se você for um programador desatento pode não acreditar na possibilidade de uma requisição redundante ocasionar impacto financeiro ao usuário e até mesmo a instituição responsável pelo portal sem contar o transtorno para administrar o problema.

Para todo efeito, o problema existe e somente um controle transacional delegado a um banco de dados não resolve porque uma nova transação em banco de dados só sabe da existência de uma anterior se esta (anterior) sofrer uma confirmação ou commit. O controle transacional via banco de dados só surtirá efeito se as requisições redundantes fossem em sequencia, mas a realidade de requisições redundantes é em grande parte paralela.

A esta altura a exclusão mútua deve fazer maior sentido caso você não a tenha compreendido.

O Mutex normalmente é aplicado no serviço central (aplicação) para impedir justamente impactos gerados por falhas em algum sistema intermediário (ex.: web-container) ou sistema terminal (ex.: browser ou usuário).

Construíndo uma controladora Mutex

A fim de tornar a Mutex mais eficiente, vamos fazer o uso da coleção com limpeza automática por tempo de espera chamada WTConcurrentLinkedQueue publicada no artigo Java - Limpeza automática de coleções. Essa classe possui dependência da classe WTTimeObject também publicada no mesmo artigo.

Agora vamos ao exemplo de uma classe para controle Mutex.

import java.io.IOException;

public class MutexControl {

    private static Integer LOCKTIMEOUT_SECS = 120;

    private static String CARDPREFIX = "_CARD";
    private static String TRANSACTIONPREFIX = "_TRANSACTION";

    private static MutexControl mutexControl;

    static {
        mutexControl = null;
    }
   
    private static synchronized MutexControl getInstance() {
        if (mutexControl == null) {
            mutexControl = new MutexControl();
        }
        return mutexControl;
    }

    private WTConcurrentHashMap<String, Long> list;

    private MutexControl() {
        list = new WTConcurrentHashMap<String, Long>("MutexControl");
    }

    public WTConcurrentHashMap<String, Long> getList() {
        return list;
    }

    private static synchronized void lock(String id, Integer expirationSecs) throws IOException {

        Long obj = getInstance().getList().get(id);

        if (obj != null) {
            if (obj < System.currentTimeMillis()) {
                getInstance().getList().remove(id);
            } else {
                throw new IOException(String.format("A chave de operação (%s) simultânea está em uso.", id));
            }
        }

        Long timeout = System.currentTimeMillis() + (expirationSecs * 1000L);
        getInstance().getList().put(id, timeout);

    }
   
    private static synchronized void unlock(String id) {
        getInstance().getList().remove(id);
    }

    public static void cardLock(Long cardLogicalNumber) throws IOException {
       
        String key = String.format("%s%d", CARDPREFIX, cardLogicalNumber);
       
        lock(key, LOCKTIMEOUT_SECS);

    }

    public static void cardUnlock(Long cardLogicalNumber) throws IOException {

        String key = String.format("%s%d", CARDPREFIX, cardLogicalNumber);

        unlock(key);

    }

    public static void transactionLock(Long transactionId) throws IOException {
       
        String key = String.format("%s%d", TRANSACTIONPREFIX, transactionId);
       
        lock(key, LOCKTIMEOUT_SECS);

    }

    public static void transactionUnlock(Long transactionId) throws IOException {

        String key = String.format("%s%d", TRANSACTIONPREFIX, transactionId);

        unlock(key);

    }
   
    public static void main(String[] args) {

        /****************************************************
         * Testar bloqueio/desbloqueio de um fictício cartão por seu id
         ****************************************************/
        Long cardId = 123L;
       
        // Teste 1: Bloqueia cartão - Resultado esperado: Deve pemitir
        try {
            cardLock(cardId);
        } catch (IOException e) {
            System.err.println("Teste 1: " + e.getMessage());
        }

        // Teste 2: Bloqueia cartão - Resultado esperado: Não deve pemitir
        try {
            cardLock(cardId);
        } catch (IOException e) {
            System.err.println("Teste 2: " + e.getMessage());
        }

        // Teste 3: Desbloqueia cartão - Resultado esperado: Deve pemitir
        // Se não desbloquear até 120 segundos, desbloqueia automaticamente
        // Leia a constante LOCKTIMEOUT_SECS
        try {
            cardUnlock(cardId);
        } catch (IOException e) {
            System.err.println("Teste 3: " + e.getMessage());
        }
       
        // Teste 4: Bloqueia cartão - Resultado esperado: Deve pemitir
        try {
            cardLock(cardId);
        } catch (IOException e) {
            System.err.println("Teste 4: " + e.getMessage());
        }

        /****************************************************
         * Testar bloqueio/desbloqueio de uma fictícia transação por seu id
         ****************************************************/
        Long transactionId = 1002L;
       
        // Teste 5: Bloqueia transação - Resultado esperado: Deve pemitir
        try {
            transactionLock(transactionId);
        } catch (IOException e) {
            System.err.println("Teste 5: " + e.getMessage());
        }

        // Teste 6: Bloqueia transação - Resultado esperado: Não deve pemitir
        try {
            transactionLock(transactionId);
        } catch (IOException e) {
            System.err.println("Teste 6: " + e.getMessage());
        }

        // Teste 7: Desbloqueia transação - Resultado esperado: Deve pemitir
        // Se não desbloquear até 120 segundos, desbloqueia automaticamente
        // Leia a constante LOCKTIMEOUT_SECS
        try {
            transactionUnlock(transactionId);
        } catch (IOException e) {
            System.err.println("Teste 7: " + e.getMessage());
        }
       
        // Teste 8: Bloqueia transação - Resultado esperado: Deve pemitir
        try {
            transactionLock(transactionId);
        } catch (IOException e) {
            System.err.println("Teste 8: " + e.getMessage());
        }
       
    }

}


A classe MutexControl contém como principais funções lock e unlock para bloquear um recurso pelo seu identificador. Para facilitar o entendimento, na classe ainda foram adicionadas funções de bloqueio e desbloqueio mais concretas, são os casos das funções cardLock, cardUnlock, transactionLock e transactionUnlock. Basicamente você pode criar funções de bloqueio e desbloqueio de outros recursos diretos ao utilizar a mesma técnica de criar as funções baseadas num prefixo para o identificador para não conflitar com controles de outros recursos.

Para evitar incidentes sérios caso você se esqueça de desbloquear um recurso após a operação ou mesmo que seu controle de exceção deixe o recurso bloqueado por erro, foi determinado um limite de tempo de bloqueio em 120 segundos na constante LOCKTIMEOUT_SECS da classe MutexControl. O parâmetro pode ser alterado a seu critério de tolerância para desbloquear um recurso esquecido no Mutex.

Teste da controladora Mutex

O teste está na mesma classe controladora no método main. Execute a classe em modo de debug preferencialmente e acompanhe os exemplos de controle de operação de um cartão de id 123 e em seguida, de uma transação de id 1002.

Resultado

Na tentativas de bloqueios redundantes, devem ocorrer as seguintes exceções:

Teste 2: A chave de operação (_CARD123) simultânea está em uso.
Teste 6: A chave de operação (_TRANSACTION1002) simultânea está em uso.


Conclusão

Análogo a um controle de semáforo onde se passa apenas um carro por vez numa via, o Mutex é uma técnica simples e segura para a exclusão mútua de processamento redundante provocado por falhas de requisições redundantes;

Até o próximo artigo.