operações atômicas em Django?

votos
24

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.

Então nós incrementar o contador e salvar.

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?

Publicado 11/11/2008 em 06:14
fonte usuário
Em outras línguas...                            


7 respostas

votos
27

A partir de Django 1.1 você pode usar () expressões F 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 consulte 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/models/expressions/#django.db.models.F

Respondeu 23/12/2009 em 23:35
fonte usuário

votos
12

Se você realmente quer 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 despejar 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 quaisquer registros da tabela 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).

Respondeu 11/11/2008 em 15:51
fonte usuário

votos
6

Pode utilizar adesivo de http://code.djangoproject.com/ticket/2705 para bloqueio nível da base 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()
Respondeu 16/12/2008 em 06:52
fonte usuário

votos
5

Duas sugestões:

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

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

Após isto acontecer, era stlll tem uma condição de corrida menor na atualização do contador. Se você obter o tráfego suficiente para ser preocupado com isso, gostaria de sugerir olhando em operações de controle de banco de dados mais refinado. Eu não acho que o ORM tem suporte direto para bloqueio / sincronização. A documentação da transação está disponível aqui .

Respondeu 11/11/2008 em 06:56
fonte usuário

votos
1

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()
Respondeu 12/11/2008 em 03:58
fonte usuário

votos
1

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

Depois de adicionar a restrição / chave 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.

Respondeu 11/11/2008 em 07:21
fonte usuário

votos
0

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 as Django docs. Há um middleware transação, ou você pode usar decoradores em torno vista ou métodos para criar transações.

Respondeu 11/11/2008 em 11:52
fonte usuário

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more