Pergunta

Eu estou tentando implementar (o que eu penso é) um modelo de dados muito simples para um contador:

class VisitorDayTypeCounter(models.Model):
    visitType = models.CharField(max_length=60)
    visitDate = models.DateField('Visit Date')
    counter = models.IntegerField()

Quando alguém vem através, ele vai olhar para uma linha que coincide com o visitType e visitDate; Se esta linha não existir, ele será criado com contador = 0.

Em seguida, incrementar o contador e salvar.

A minha preocupação é que este processo é totalmente uma corrida. Dois pedidos poderiam simultaneamente verificar se a entidade está lá, e ambos poderiam criá-lo. Entre a leitura do contador e salvar o resultado, outro pedido poderia vir através e incrementá-lo (resultando em uma contagem perdido).

Até agora eu realmente não tenho encontrado uma boa maneira de contornar isso, seja na documentação do Django ou no tutorial (na verdade, parece que o tutorial tem uma condição de corrida na parte Vote dele).

Como posso fazer isso com segurança?

Foi útil?

Solução

Este é um bocado de um hack. O SQL cru fará seu código menos portátil, mas vai se livrar da condição de corrida no incremento contador. Em teoria, isso deve incrementar o contador qualquer momento que você faz uma consulta. Eu não testei isso, então você deve certificar-se da lista é interpolada na consulta corretamente.

class VisitorDayTypeCounterManager(models.Manager):
    def get_query_set(self):
        qs = super(VisitorDayTypeCounterManager, self).get_query_set()

        from django.db import connection
        cursor = connection.cursor()

        pk_list = qs.values_list('id', flat=True)
        cursor.execute('UPDATE table_name SET counter = counter + 1 WHERE id IN %s', [pk_list])

        return qs

class VisitorDayTypeCounter(models.Model):
    ...

    objects = VisitorDayTypeCounterManager()

Outras dicas

A partir de Django 1.1 você pode usar F () expressões do ORM.

from django.db.models import F
product = Product.objects.get(name='Venezuelan Beaver Cheese')
product.number_sold = F('number_sold') + 1
product.save()

Para mais detalhes veja a documentação:

https: / /docs.djangoproject.com/en/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

https: //docs.djangoproject .com / en / 1.8 / ref / modelos / expressões / # django.db.models.F

Se você quiser realmente o contador para ser preciso, você poderia usar uma transação, mas a quantidade de simultaneidade necessário realmente vai arrastar a sua aplicação e banco de dados para baixo sob qualquer carga significativa. Em vez disso pensar em ir com uma abordagem mais estilo de mensagens e manter apenas de dumping registros de contagem em uma tabela para cada visita, onde você gostaria de incrementar o contador. Então, quando você quer que o número total de visitas fazer uma contagem na mesa de visitas. Você também pode ter um processo de fundo que corria qualquer número de vezes por dia que somar as visitas e, em seguida, armazenar que na tabela pai. Para economizar espaço seria também excluir todos os registros da tabela de visitas criança que resumiu. Você vai reduzir a sua concorrência custa uma quantidade enorme se você não tiver vários agentes que disputam os mesmos recursos (o contador).

Você pode usar o patch da http://code.djangoproject.com/ticket/2705 para o bloqueio em nível de banco de dados de suporte.

Com remendo este código será atômica:

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
visitors.counter += 1
visitors.save()

Duas sugestões:

Adicionar um unique_together ao seu modelo, e envolva a criação de um manipulador de exceção para duplicatas de captura:

class VisitorDayTypeCounter(models.Model):
    visitType = models.CharField(max_length=60)
    visitDate = models.DateField('Visit Date')
    counter = models.IntegerField()
    class Meta:
        unique_together = (('visitType', 'visitDate'))

Depois disso, você poderia stlll ter um menor condição de corrida na atualização do contador. Se você obter o tráfego suficiente para se preocupar com isso, eu sugeriria olhar para as transações para mais fino controle de banco de dados de grão. Eu não acho que o ORM tem suporte direto para bloqueio / sincronização. A documentação da transação está disponível aqui .

Por que não usar o banco de dados como a camada de concorrência? Adicionar uma chave primária ou restrição exclusiva da tabela para visitType e visitDate. Se não me engano, o Django não é exatamente apoiar esta em seu banco de dados de classe do modelo ou pelo menos eu não vi um exemplo.

Depois de adicionar a tecla / restrição para a mesa, então tudo que você tem a fazer é:

  1. Verifique se a linha está lá. se for, buscá-la.
  2. inserir a linha. se não há nenhum erro que você está bem e pode seguir em frente.
  3. Se há um erro (ou seja condição de corrida), re-buscar a linha. se não há nenhuma linha, então é um erro genuíno. Caso contrário, você está bem.

É desagradável para fazê-lo desta maneira, mas parece bastante rápido e cobriria a maioria das situações.

Seu deve usar transações de banco de dados para evitar este tipo de condição de corrida. A transação permite realizar toda a operação de criação, leitura, incrementando e salvar o contador em uma base "tudo ou nada". Se alguma coisa der errado ele vai reverter a coisa toda e você pode tentar novamente.

Confira o Django docs. há um middleware transação, ou você pode usar decoradores em torno vista ou métodos para criar transações.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top