La mayoría de las bases de datos se utilizan en entornos multi-usuario, en los que muchos clientes utilizando la misma aplicación, o muchas aplicaciones cada una con uno o muchos clientes acceden a la misma base de datos. Cada una de esas aplicaciones enviará consultas al gestor, y normalmente cada hilo de ejecución será una transacción diferente.
En la mayoría de los sistemas operativos actuales, las diferentes tareas o hilos se ejecutan de forma intercalada (incluso en el caso de máquinas con varios procesadores). Es decir, el sistema operativo decide por su cuenta cuando suspender una de las tareas y darle un poco de tiempo de ejecución a otra. Si hay tareas simultáneas o concurrentes sobre la misma base de datos, esta intercalación puede resultar en que las lecturas y escrituras de las diferentes tareas o aplicaciones en el medio físico se realicen en cualquier orden y secuencia.
El acceso simultáneo descrito puede dar como resultados información inconsistente o simplemente incorrecta, dependiendo de la mala o buena suerte que tengamos en la intercalación de las lecturas y escrituras simultáneas. Esta problemática ha llevado a diseñar e implementar diferentes estrategias de control de concurrencia, que se encargan de evitar todos esos problemas, de modo que los desarrolladores de las aplicaciones pueden “olvidarse” de ellos al escribir su código.
Por ejemplo, si tenemos una estructura de tablas relacional que incluye las siguientes:
PEDIDO(id, num-cliente, id-prod, cantidad, precio)
PRODUCTO(id-prod, nombre, ..., stock)
...
Pueden ocurrir diferentes problemas relacionados con la escritura simultánea con otras escrituras o lecturas, incluyendo los siguientes:
- Dos sentencias
UPDATEque actualicen un mismo producto decrementando el stock del mismo en una unidad podrían terminar en que una de ellas no se realizase. Si pensamos en unUPDATEcomo una secuencia de una lectura y una escritura, puede que ambosUPDATEhagan la lectura, por ejemplo, de un stock de 10, y después las escrituras, decrementan ese dato, quedando el resultado en 9, mientras que lo correcto era un resultado de 8. - Supongamos una sentencia que primero comprueba que hay stock del producto
P, y después inserta un nuevoPEDIDOde diez unidades del productoP, que tiene un stock de 10, seguido de unUPDATEal stock por esa cantidad. Puede que otra inserción de un pedido se ejecute antes delUPDATEpero después de la comprobación, haciendo quedar el stock del producto en negativo.
Existen varias técnicas para controlar la concurrencia. Los bloqueos son los más conocidos, aunque también se utiliza el control multi-versión y otras técnicas como las marcas de tiempo.
Los bloqueos como solución al problema de la concurrencia
Una forma de controlar la concurrencia es hacer que cada transacción deba adquirir un derecho de acceso exclusivo a cada fragmento de datos que necesite modificar. A estos “derechos” se les denomina bloqueos.
Bloqueos binarios
La forma más simple de bloquear es utilizar bloqueos binarios. En un bloqueo binario, cada transacción debe solicitar el bloqueo de cada fragmento de datos A que vaya a utilizar antes de acceder a él (sea para leerlo o escribirlo), mediante una operación bloquear(A). Deberá liberar todos los bloqueos, mediante una operación desbloquear(A) de modo que otras tareas puedan tomarlos.
Este sistema de bloqueos tiene una implementación muy simple, ya que solo requiere mantener una tabla que indica qué partes de los datos está bloqueada y por qué transacción.
Bloqueos de lectura/escritura
El sistema de bloqueos binarios es simple pero demasiado restrictivo, ya que no permite que dos transacciones que van a leer el mismo fragmento de datos A lo hagan simultáneamente, cuando en realidad, no puede haber problemas en varios lectores simultáneos. Los bloqueos de lectura/escritura hacen más débil la restricción permitiendo la siguiente compatibilidad de bloqueos.
| LECTURA | ESCRITURA | |
| LECTURA | Compatible | Incompatible |
| ESCRITURA | Incompatible | Incompatible |
En este caso, las operaciones que las transacciones deben realizar son tres: desbloquear(A) y bloquear_para_lectura(A) o bloquear_para_escritura(A).
Nótese que esas llamadas se implementan de diferentes formas en diferentes gestores de bases de datos. Por ejemplo, en MySQL, tanto las solicitudes de bloqueos como las liberaciones se realizan mediante una sola llamada del API de los gestores de almacenamiento:
store_lock(THD *thd, THR_LOCK_DATA **to, enum thr_lock_type lock_type)
Cada llamada a store_lock utiliza el manejador de una tabla thd concreta, y se indica la información de los datos a bloquear mediante la variable to, y el tipo de bloqueo mediante lock_type (el número de tipos definidos en MySQL es muy grande, ya que cubre todos los tipos de bloqueo que implementan los múltiples gestores de almacenamiento disponibles).
Serialización de los bloqueos de lectura/escritura
La serialización de las operaciones de lectura y escritura consiste en ordenar esas operaciones para un conjunto de transacciones concurrentes de modo que los resultados de las operaciones sean correctos. Por ejemplo, si tenemos las siguientes transacciones X e Y, puede darse la siguiente situación.
![]() |
En esa situación, la ejecución de TX y TY hace que el dato original sólo se haya incrementado una vez, cuando tenía que haberse incrementado dos veces. Sin embargo, si hubiésemos tenido suerte y todas las operaciones de TX se hubiesen realizado antes que las de TY, el resultado habría sido correcto.
La conclusión es que el mero mecanismo de los bloqueos garantiza el acceso exclusivo a un dato o fragmento de información (evitando ciertos problemas), pero los problemas asociados a la intercalación de las operaciones compuestas aún pueden darse.
Por lo tanto, hace falta alguna política o mecanismo de adquisición y liberación de bloqueos que permita hacer las operaciones serializables.
El bloqueo en dos fases permite la serialización
El protocolo de bloqueo en dos fases fuerza a las transacciones cuando todas las operaciones de adquisición de bloqueos (bloquear_lectura, bloquear_escritura) preceden a la primera operación de desbloqueo (desbloquear). Dicho de otro modo, primero hay que adquirir todos los bloqueos, y después se pueden liberar.
Cuando se utiliza el protocolo de bloqueo en dos fases, puede demostrarse que la ejecución será serializable.
Inconvenientes de los bloqueos y la serialización
Un problema del protocolo de bloqueo en dos fases es que puede llevar a situaciones de interbloqueo. La siguiente es una secuencia de operaciones que lleva al interbloqueo pero cumple perfectamente l protocolo de bloqueo en dos fases.
![]() |
El inconveniente está en que puede que en la fase de adquisición de bloqueos (fase de expansión), más de una transacción esté interesada en los mismos dos fragmentos de datos, y la mala suerte nos lleve a una situación en la que ambos quedan suspendidos, sin posibilidad de avanzar.
¿Bloqueos más grandes o más pequeños?
Un aspecto que aún no se ha tratado en la discusión anterior es cuán “grandes” son los elementos de datos que se deben bloquear. Una opción posible es bloquear tablas enteras. Esto hace que la gestión de los bloqueos sea simple y tenga poca sobrecarga (solo hay que guardar el estado de bloqueo de las N tablas). No obstante, esto impide que dos transacciones que van a manipular filas diferentes de una tabla puedan progresar en paralelo.
Una segunda opción es utilizar bloqueos al nivel de las filas. En este caso, se hacen mayores las posibilidades de concurrencia, pero por otro lado, hay que mantener mucha más información sobre los bloqueos (ya que el número de filas es en general muy grande respecto al número de tablas), y el servidor se sobrecarga más con la gestión de los bloqueos.
Hay gestores de bases de datos que permiten seleccionar el tipo de bloqueo que queremos para nuestra base de datos. Por ejemplo, en MySQL hay gestores de almacenamiento que ofrecen bloqueo a nivel de fila, y otros bloqueo a nivel de tabla.
No obstante lo anterior, hay sentencias que siempre producirán un bloqueo de tabla, como por ejemplo una sentencia ALTER TABLE.
Guardando “instantáneas” de los datos: el Control Multi-versión
El protocolo de bloqueo en dos fases limita considerablemente las posibilidades de concurrencia. Si observamos los problemas que causan los bloqueos no serializados, veremos que muchos de los problemas están en que una transacción lee un cierto dato y antes de escribir el resultado, otra transacción lee el dato “antiguo”. En ese momento, cada transacción trabaja con un estado de información inconsistente.
Para paliar esos problemas, pero permitir la mayor concurrencia posible, se han diseñado los protocolos de control multi-versión. La idea básica es que cuando una transacción modifica un dato, se crea una nueva versión del mismo, pero se guarda la anterior. De este modo, al acabar la ejecución de las transacciones, se puede utilizar para cada una de ellas la versión de los datos “que hace la ejecución correcta”, es decir, que hace la ejecución serializable.
Lógicamente, estas técnicas requieren más espacio de almacenamiento para guardar las diferentes versiones.






