Concurrencia + bloqueo base de datos

282 views
Skip to first unread message

Diego Ponce de León

unread,
Aug 12, 2015, 1:00:10 PM8/12/15
to django-es
Hola, 
tengo un método en un api web (django-rest-framework) que se llama "follow". Es una red social. Para seguir a un usuario creo un objeto Follow. Para dejar de seguirlo lo que hago es borrar ese mismo objeto.
Por tanto: 
/user/follow crea un objeto Follow en la base de datos
/user/unfollow destruye dicho objeto

En el método "user/follow/" lo que haga es comprobar si ya existe un follow y devuelvo error en caso afirmativo:

# return error if the follow is already done
current_follow_query = request.user.follows.filter(influencer=influencer)
if current_follow_query.count() > 0:
error = {'detail': 'Ya eres follower de este influencer'}
return Response(data=error, status=status.HTTP_403_FORBIDDEN)

# save the actual follow object
obj = Follow(follower=request.user, influencer=influencer)
obj.save()
Todo esto parecía funcionar bien, hasta que encontré follows duplicados en la base de datos. Es decir, un usuario siguiendo a otro más de una vez, lo cuál es un error.
La única manera que se me ocurre para que esto pueda pasar es que entre el condicional que veis (if current_follow_query.count() > 0:) y el bloque de texto que guarda el objeto, se procese esta misma llamada paralelamente.
¿Sabéis como puedo bloquear la tabla "Follow" para que todo este código corra del tirón antes de procesar otra llamada?

Saludos

Aztrock

unread,
Aug 12, 2015, 1:11:24 PM8/12/15
to djan...@googlegroups.com
Me paso algo similar. logre tener dos soluciones.

1. Si es un boton en la pagina web, al momento de hacer click, desactivo el boton para evitar enviar dos veces la misma peticion casi simultaneamente.
2. Como tenia una aplicacion en tiempo real, para facturacion de vozip, con todo el sistema echo en djando, cree un tigger en PostgreSQL, para que solo me procesara una peticion cuando hiciera un update, la tabla afectada.

Para tu caso ya creo que deberias utilizar los niveles de ailamiento.

https://docs.djangoproject.com/en/1.8/ref/databases/#isolation-level
http://www.postgresql.org/docs/current/static/transaction-iso.html

Esos niveles, te dan la capacidad de bloquear una transaccion para qeu otras la lean antes o despues de ejecutar el commit.


--
--
Ha recibido este mensaje porque está suscrito a Grupo "Grupo de Usuarios del Framework Django de habla hispana" de Grupos de Google.
Si quieres publicar en este grupo, envía un mensaje de correo
electrónico a djan...@googlegroups.com
Para anular la suscripción a este grupo, envíe un mensaje a django-es-...@googlegroups.com
Para obtener más opciones, visita este grupo en http://groups.google.com.bo/group/django-es.
---
Has recibido este mensaje porque estás suscrito al grupo "Django-es" de Grupos de Google.
Para anular la suscripción a este grupo y dejar de recibir sus mensajes, envía un correo electrónico a django-es+...@googlegroups.com.
Para acceder a más opciones, visita https://groups.google.com/d/optout.



--
Un hombre de carácter podrá ser derrotado pero jamás destruido.

ERNEST HEMINGWAY.

Diego Ponce de León

unread,
Aug 12, 2015, 5:05:20 PM8/12/15
to djan...@googlegroups.com
Gracias Aztrock, pero resulta que cambiar el "isolation-level" de postgres te obliga a controlar varios errores que se pueden producir.
Estaba buscando otra manera más sencilla de manejar esto. Había pensado en usar una caché que dure unas milésimas de segundo (suficientes para realizar la consulta a la bd) y de esa forma evitar dos llamadas simultáneas. Aunque tal vez no sirva puesto que la caché empieza a funcionar una vez que el método termina su ejecución y mi problema es cuando se llama al mismo método dos veces antes de que termine la ejecución de la primera vez

Aztrock

unread,
Aug 13, 2015, 1:25:58 AM8/13/15
to djan...@googlegroups.com
isolation-level el que te obliga a controlar los errores es "Serializable", los otros dos esta mas controlado la cuestion de errores, si no estoy mal ya esta por defecto "Read committed"

para maxima velocidad yo creo que seria un tigger o puede utilizar una logica diferente en django para ver si te va mejor.


yo probaria de esta forma


obj. created = Follow.object.update_or_create(follower=request.user, , influencer=influencer)
if created:
    return Response(data="Ahora estas siguiendo a {}".format(influencer))
else:

   error = {'detail': 'Ya eres follower de este influencer'}
return Response(data=error, status=status.HTTP_403_FORBIDDEN)


Yo tengo un sistema de facturacion de vozip, mayormente creado en django que se llama DataBilling.co, cuando ingresaban muchas llamadas concurrentes 
el ORM o el SQL directo en python, en ocaciones sobre-escribien descuentos de saldos, por ejemplo si tenia tres llamadas y las tres terminaban al mismo tiempo
solo me descontaba una llamada, y las otras dos dos solo quedaban en el  registro, la unicasolucion facil y que hasta el momento me ha funcionado perfectamente fue
un tigger en la base de datos, que es lo suficiente rapido y controlado para algo que ocurre casi al mismo tiempo o al mismo tiempo.

Diego Ponce de León

unread,
Aug 15, 2015, 11:55:34 AM8/15/15
to djan...@googlegroups.com
Hola de nuevo. Me parece interesante el trigger.
Supongo que en mi caso lo podría utilizar para ejecutar el count() en la tabla Follow, pero seguimos con el problema de que el método realiza un insert después del count() y otras llamadas paralelas podrían seguir interpretando que el count() == 0.

Voy a probar con tu solución de update_or_create(). Esta solución me parece más acertada ya que evito hacer dos llamadas (count + insert) y probablemente no tenga el mismo problema de concurrencia... a no ser qué internamente "update_or_create()" haga más de una consulta sql

Muchas gracias por tu ayuda

Diego Ponce de León

unread,
Aug 15, 2015, 12:00:23 PM8/15/15
to djan...@googlegroups.com
Acabo de ver la doc de django: https://docs.djangoproject.com/en/dev/ref/models/querysets/#update-or-create

Básicamente hace algo parecido pero sin el count(), así que ahora dudo de nuevo que funcione. 
De todas formas probaré varios escenarios y ya os contaré si consigo arreglarlo:

try:
    obj = Person.objects.get(first_name='John', last_name='Lennon')
    for key, value in updated_values.iteritems():
        setattr(obj, key, value)
    obj.save()
except Person.DoesNotExist:
    updated_values.update({'first_name': 'John', 'last_name': 'Lennon'})
    obj = Person(**updated_values)
    obj.save()


Diego Ponce de León

unread,
Aug 15, 2015, 12:02:39 PM8/15/15
to djan...@googlegroups.com
Tal vez, en lugar de pensar en términos de bloqueo de base de datos, se podría pensar en hilos.
Si existe alguna manera de que no se ejecute otro thread con la misma llamada hasta que la ejecución del método termine, todo estaría solucionado.
No tengo ni idea si esto es posible con django

Diego Ponce de León

unread,
Aug 15, 2015, 6:27:53 PM8/15/15
to djan...@googlegroups.com
Buenas, al final lo he solucionado de la manera más sencilla posible. 
Qué manera de complicarme la vida por dios =)

Dado que un follow es algo único (usuario pepe solo puede tener un follow apuntando a usuario juan), la solución es usar  unique_together = [('influencer', 'follower')] y listo!!

Después, en el método que tenía, solo hace falta comprobar IntegrityError a la hora de crear el objeto (si ya existe lanzará un IntegrityError). Así de fácil:

try:
Follow.objects.create(follower=request.user, influencer=influencer)
except IntegrityError:
# the object already exists
   error = {'detail': 'Ya eres follower de este influencer'}
return Response(data=error, status=status.HTTP_403_FORBIDDEN)
else:
return Response(status=status.HTTP_204_NO_CONTENT)

A veces tienes la solución delante de tus narices y te vas por los cerros de Úbeda...
De todas formas, gracias otra vez por la ayuda!
Reply all
Reply to author
Forward
0 new messages