Semáforos e condições de corrida com memcache e python – lock em registros

No post anterior, falei de performance em aplicações web e coloquei um ponto importante em relação ao processo caro de ficar acessando o banco de dados constantemente.

Muitas empresas utilizam soluções de cache que são muito mais rápidas e eficientes, e entre as opções disponíveis, eu gostaria de destacar o memcached. Empresas como Facebook, Google, Wikipedia fazem uso bem intenso dessa ferramenta e ela é bem otimizada para uso em larga escala.

Apesar de ser projetado para Linux, há um port para windows no qual eu uso para testes ( nunca testado em ambiente de produção). Basicamente sua função é armazenar coleções em memória, servindo os dados via socket para os clientes que conectarem ao servidor.  Usa UDP como protocolo de comunicação e um mecanismo muito eficiente para gerenciamento de dados.

Um exemplo prático de como seria usado o mecanismo de cache:

  • Usuário A chama uma página e requisita cidades do estado de SP e a mesma não está em cache;
  • Aplicação chama consulta no banco de dados, adiciona a chave ao cache: cache.set(“cidades”,coleção);
  • Usuário B chama a aplicação e verifica que os dados estão no cache. Aplicação consulta cache e retorna os dados para serem tratados: cache.get(“cidades”);

Se não tenha ficado claro, um pseudo-código pode ajudar:

  • se existir cache.get(“cidade”) então:
    -> popular controles com essa coleção
  • caso contrário:
    -> buscar dados de cidade do banco;
    -> setar cache cache.set(“cidade”, dados_do_banco);
    -> popular controles com essa coleção;

Problemas de um cenário real:

Na teoria parece muito simples, certo ? Se você estiver pensando em um ambiente que não haja restrição por condições de corrida ( usuários simultâneos realizando a mesma operação ), então é realmente simples.

Mas imagine um momento em que você precisa incrementar um registro no cache, e esse registro só pode ser incrementado se ninguém mais estiver fazendo o processo. Como você resolveria essa questão ?

Pesquisando um pouco, acabei lembrando dos mecanismos de semáforo e de entrada em região crítica, o que me levou a criar um modelo que tenta fazer algo parecido. Só para lembrar o processo de região crítica (retirado do link acima da wikipedia):

Para entrar numa região crítica, uma linha de execução deve obter um semáforo, que será descartado na saída da região crítica. Cada recurso compartilhado, ou um conjunto de recursos compartilhados em comum, possui um semáforo próprio. Qualquer outra linha de execução deverá esperar para entrar numa região crítica em uso, mas poderá usar a CPU para executar qualquer outro código, incluindo regiões críticas protegidas por outro semáforo.

Logo abaixo, postei o exemplo criado em Python, porém estou escrevendo um pseudo-código que pode ser facilmente migrado para qualquer linguagem:

  1. Conectar no memcache;
  2. Tentar adicionar a chave(“lock”);
  3. Se adicionar, então não tem ninguém esperando – incremente o registro que você precisa, remova a chave lock;
  4. Caso tenha a chave, tente esperar alguns micro-segundos, e executar a operação “N” vezes. Se “N” for atingido, retornar erro, caso contrário, basta executar o procedimento anterior;
  5. Desconectar do memcache;
import memcache
import time

def lock_update(key):

    # parametrizacoes, todo passar para um cfg
    m = memcache.Client(['127.0.0.1:11211'], debug=0)
    lock = "lock_" + key
    max_tries = 5 #tenta 5 vezes
    tries = 0
    expiration = 60 #segundos
    got_lock = False
    number = -1
    wait_time_between_tries = 101 #microsegundos

    # teste inicial, apenas para colocar algo na chave, caso não tenha no cache
    if m.get(key) is None:
        m.set(key,0)

    if(m):
        # roda o numero de tentativas para adquirir o lock
        for tries in range(0, max_tries):
            # se conseguir adicionar, adquire o lock
            if(m.add(lock, 1, expiration)):
                got_lock = True
                break

            # se nao conseguiu, espera um pouco
            time.sleep(wait_time_between_tries / 1000000.0)

        # consegui adquirir o lock
        if(got_lock):
            number = m.incr(key)
            m.delete(lock, 0)

        # retorna o valor
        return number

# só para teste pela linha de comando
if __name__=="__main__":
    print lock_update("teste")

Simples, não ?

Espero que ajude :)

abs

Robson Dantas

Bookmarksbookmark bookmark bookmark bookmark bookmark bookmark

Popularity: 4%

No Comment

Vale Presente