Django: ¿cómo envolver una operación de actualización / inserción masiva en una transacción?

Este es mi caso de uso:

  • Tengo varias tareas de apio que se ejecutan en paralelo
  • Cada tarea podría crear o actualizar a granel muchos objetos. Para esto estoy usando django-bulk

Así que básicamente estoy usando una función muy conveniente insert_or_update_many :

  1. primero realiza un Select
  2. Si encuentra objetos los actualiza.
  3. De lo contrario los crea

Pero esto introduce problemas de concurrencia. Por ejemplo: si un objeto no existió durante el paso 1, entonces se agrega a una lista de objetos que se insertarán más adelante. Pero durante este período puede suceder que otra tarea de apio haya creado ese objeto y, cuando intenta realizar una inserción masiva (paso 3), aparece un error de entrada duplicada.

Supongo que necesito envolver los 3 pasos en un bloque de “locking”. He leído acerca de Transacciones y he tratado de envolver el paso 1,2,3 dentro de una with transaction.commit_on_success: bloquear

 with transaction.commit_on_success(): cursor.execute(sql, parameters) existing = set(cursor.fetchall()) if not skip_update: # Find the objects that need to be updated update_objects = [o for (o, k) in object_keys if k in existing] _update_many(model, update_objects, keys=keys, using=using) # Find the objects that need to be inserted. insert_objects = [o for (o, k) in object_keys if k not in existing] # Filter out any duplicates in the insertion filtered_objects = _filter_objects(con, insert_objects, key_fields) _insert_many(model, filtered_objects, using=using) 

Pero esto no funciona para mí. No estoy seguro de tener una comprensión completa de las transacciones. Básicamente necesito un bloque donde pueda colocar varias operaciones asegurándome de que ningún otro proceso o subproceso está accediendo (por escrito) a mis recursos de db.

Básicamente necesito un bloque donde pueda colocar varias operaciones asegurándome de que ningún otro proceso o subproceso está accediendo (por escrito) a mis recursos de db.

Las transacciones de Django no lo garantizan, en general, para usted. Si viene de otras áreas de la informática, naturalmente piensa que una transacción se está bloqueando de esta manera, pero en el mundo de la base de datos hay diferentes tipos de lockings, en diferentes niveles de aislamiento , y varían para cada base de datos. Por lo tanto, para asegurarse de que sus transacciones hagan esto, tendrá que aprender sobre las transacciones, los lockings y sus características de rendimiento, y sobre los mecanismos provistos por su base de datos para controlarlos.

Sin embargo, tener un montón de procesos que intentan bloquear la mesa para realizar inserciones de la competencia no parece una buena idea. Si las colisiones fueran poco frecuentes, podría hacer una forma de locking optimista y simplemente reintentar la transacción si falla. O tal vez puede dirigir todas estas tareas de apio a un solo proceso (no hay ninguna ventaja de rendimiento en paralelismo si de todas formas va a adquirir un locking de tabla).

Mi sugerencia sería comenzar olvidando las operaciones masivas y solo hacer una fila a la vez usando update_or_create de Django. Siempre que su base de datos tenga restricciones que eviten las entradas duplicadas (lo que parece que lo hace), esto debería estar libre de las condiciones de carrera que describe anteriormente. Si el rendimiento realmente resulta inaceptable, entonces busque opciones más complejas.

Adoptar el enfoque de concurrencia optimista significa que, en lugar de evitar conflictos, al adquirir un locking de tabla, digamos, simplemente debe proceder de manera normal y luego volver a intentar la operación si resulta que hay un problema. En tu caso podría verse algo como:

 while True: try: with transaction.atomic(): # do your bulk insert / update operation except IntegrityError: pass else: break 

Por lo tanto, si se topa con su condición de carrera, el IntegrityError resultante hará que el bloque transaction.atomic() revierta cualquier cambio que se haya realizado, y el bucle while forzará un rebash de la transacción (donde presumiblemente la operación masiva ahora vea la nueva fila existente y márquela para actualizarla en lugar de insertarla).

Este tipo de enfoque puede funcionar realmente bien si las colisiones son raras, y muy mal si son frecuentes.