Una transacción Ti está confirmada (o cancelada) en la
historia H si ci H (o ai H). Ti está activa en H si está
confirmada o cancelada. Por supuesto que una historia completa no
tiene transacciones activas.
Una historia completa H es serial si, para cada par de
transacciones Ti y Tj que aparecen en H, todas las
operaciones de Ti aparecen antes de todas las operaciones de
Tj o viceversa. Por lo tanto, una historia serial representa
una ejecución en la cual no hay intercalación de las
operaciones de transacciones diferentes. Cada transacción se
ejecuta desde el principio hasta el fin antes de que la siguiente
comience.
Decimos que dos historias son equivalentes si tienen el mismo
conjuntos de operaciones y las operaciones conflictivas están en
el mismo orden en ambas historias. Por ejemplo, las dos historias
siguientes son equivalentes
H1 = r1[x] r2[x] w1[x] c1 w2[y] c2
H2 = r2[x] r1[x] w2[y] c2w1[x] c1
Sin embargo, ninguna de ellas es equivalente a
H3 = r1[x] w1[x] r2[x] c1 w2[y] c2
porque r2[x] y w1[x] están en conflicto y
r2[x] precede a w1[x] en H1 y H2 pero w1[x] precede
a r2[x] en H3.
Definimos una historia H como serializable si es equivalente a
alguna historia serial Hs. Por ejemplo,
H1 = r1[x] r2[x] w1[x] c1w2[y] c2
es equivalente a H4 = r2[x] w2[y] c2r1[x] w1[x] c1 puesto que r2[x] y
w1[x] están en el mismo orden en H1 y H4, por lo tanto
H1 es serializable.
Podemos determinar si una historia es serializable o no analizando
un grafo derivado de la historia llamado grafo de serialización.
Sea H una historia sobre T={T1, T2, ..., Tn}. El
grafo de serialización (SG) para H es una grafo
dirigido cuyos nodos son las transacciones en T que están
confirmadas en H y cuyos arcos son todos aquellos de la forma
Ti - Tj (i =/ j) tal que una de las operaciones
de Ti preceda y esté en conflicto con una de las operaciones
de Tj en H. Por ejemplo, para la historia
H5 = r1[x] r2[x] w1[x] r3[x] w2[y] w3[x] c3 w1[y] c1 c2
su grafo de serialización es SG(H5) = T2 - T1 - T3
El arco T1 - T3 está en SG(H5) porque w1[x] <
r3[x], y el arco T2 - T3 está en SG(H5)
porque r2[x] < w3[x]. Es importante resaltar que un arco en
SG(H5) puede originarse por más de un par de operaciones
conflictivas; por ejemplo, el arco T2 - T1 es causado
tanto por r2[x] > w1[x] y w2[y] > w1[y].
Cada arco Ti - Tj en SG(H) significa que al menos
una de las operaciones de Ti precede y está en conflicto con
una de las operaciones de Tj. Esto indica que Ti debe
preceder a Tj en cualquier historia serial que es equivalente a
H. Si es posible encontrar una historia serial, Hs,
consistente de todos los arcos en SG(H), entonces Hs es
equivalente a H y por lo tanto, H es serializable siempre que
SG(H) sea acíclico.
en el ejemplo anterior, SG(H5) es acíclico. Una historia
serial en la que las transacciones aparecen en un orden
consistente con los arcos de SG(H5) es T2 T1 T3, es decir,
H6 = r2[x] w2[y] c2 r1[x] w1[x] w1[y] c1 r3[x] w3[x] c3
que es equivalente a H5 y por lo tanto H5 es serializable.
Teorema de Seriabilidad
Una historia H es serializable si y sólo si SG(H) es acíclico.
Este teorema se usa para caracterizar el conjunto de historias
permitido por un algoritmo de control de concurrencia y probar que
cualquiera de esas historias debe tener un grafo de
serialización acíclico, por lo tanto, el algoritmo de
control de concurrencia garantiza las ejecuciones serializables.
5 Métodos de Control de Concurrencia
El criterio de clasificación más común de los algoritmos de
control de concurrencia es el tipo de primitiva de
sincronización. Esto resulta en dos clases: aquellos algoritmos
que están basados en acceso mutuamente exclusivo a datos
compartidos (bloqueos) y aquellos que intentar ordenar la
ejecución de las transacciones de acuerdo a un conjunto de
reglas (protocolos). Sin embargo, esas primitivas se pueden usar
en algoritmos con dos puntos de vista diferentes: el punto de
vista pesimista que considera que muchas transacciones tienen
conflictos con otras, o el punto de vista optimista que supone que
no se presentan muchos conflictos entre transacciones.
Los algoritmos pesimistas sincronizan la ejecución concurrente
de las transacciones en su etapa inicial de su ciclo de
ejecución. Los algoritmos optimistas retrasan la
sincronización de las transacciones hasta su terminación.
Ambos grupos de métodos, pesimistas y optimistas, consisten de
algoritmos basados en bloqueos y algoritmos basados en marcas de
tiempo, entre otros.
En la siguiente sección se presenta uno de los métodos
pesimistas basado en bloqueos más utilizado conocido como
Bloqueo de Dos Fases
6 Bloqueo de Dos Fases
El bloqueo es un mecanismo usado comúnmente para resolver el
problema de la sincronización en el acceso a datos compartidos.
La idea fundamental del bloqueo es simple y de sentido común.
Cada dato tiene un bloqueo asociado con él. Antes de que una
transacción T1 pueda accesar al dato, el sincronizador
primero examina el bloqueo asociado. Si ninguna transacción
tiene el bloqueo, entonces el sincronizador obtiene el bloqueo en
representación de T1. Si alguna otra transacción T2
tiene el bloqueo, T1 debe esperar hasta que T2 libere el
bloqueo. Esto significa que el sincronizador no le asignará el
bloqueo a T1 hasta que T2 lo libere. Por lo tanto, el
sincronizador asegura que sólo una transacción a la vez pueda
tener el bloqueo y, de acuerdo a esto, sólo una transacción
puede acceder al dato a la vez.
El bloqueo puede ser usado por un sincronizador para garantizar la
seriabilidad. Debido a que las transacciones acceden a los datos
para leerlos o escribirlos, existen dos tipos de bloqueos
asociados con los datos: bloqueos de lectura y bloqueos de
escritura. Usaremos rl[x] para denotar un bloqueo de lectura
sobre el elemento x y wl[x] para denotar un bloqueo de
escritura sobre x. Específicamente, usaremos rli[x] (o
wli[x]) para denotar la operación por la cual Ti obtiene
un bloqueo de lectura (o escritura) sobre x; mientras que
rui[x] (o wui[x]) denota la operación mediante la cual
Ti libera el bloqueo de lectura (o escritura) sobre x.
Dos bloqueos cualesquiera pli[x] y qlj[y] están en
conflicto si x=y, i =/ j, y las operaciones p y q son de
tipo conflictivo. Es decir, dos bloqueos son conflictivos si son
sobre el mismo dato, son requeridos por transacciones diferentes y
uno o ambos son bloqueos de escritura. Por lo tanto, dos bloqueos
en diferentes datos no están en conflicto ni tampoco dos
bloqueos sobre el mismo dato requeridos por la misma transacción
aún cuando sean de tipo conflictivo.
La función del sincronizador de Bloqueo de Dos Fases (2PL) es
manejar los bloqueos controlando cuando las transacciones obtienen
y liberan sus bloqueos, de acuerdo a las siguientes reglas :
Cuando recibe una operación pi[x] del Administrador de
Transacciones (TM), el sincronizador verifica si pli[x] está
en conflicto con algún qlj[x] que ya esté establecido. Si
es así, retrasa pli[x], forzando a Ti a esperar hasta
que pueda recibir el bloqueo que necesita. Si no, el sincronizador
acepta pli[x] y envía pi[x] al Administrador de Datos
(DM).
Una vez que el sincronizador ha establecido un bloqueo para
Ti, digamos pli[x], no puede liberar el bloqueo al menos
hasta que el DM reconozca que se ha procesado la operación
corresponidente al bloqueo, es decir pi[x].
Una vez que el sincronizador ha liberado un bloqueo para una
transacción, no puede, subsecuentemente, obtener ningún otro
bloqueo para esa transacción (sobre ningún dato).
La primera regla previene que dos transacciones accedan
concurrentemente al mismo dato con operaciones conflictivas. Por
lo tanto, las operaciones conflictivas son planificadas en el
mismo orden en el que se obtienen sus correspondientes bloqueos.
La regla (2) complementa la primera regla, garantizando que el DM
procese operaciones sobre el dato en el orden en el que el
sincronizador las envía. Por ejemplo, supongamos que Ti
obtiene rli[x], el cual libera antes que el DM haya confirmado
que ri[x] se haya procesado. Entonces, es posible que algún
Tj obtenga un bloqueo conflictivo sobre x, wlj[x], y
envíe wj[x] al DM. A pesar que el sincronizador ha enviado
ri[x] antes que wj[x] al DM, sin la regla (2) no hay
garantía de que el DM recibirá y procesará las
operaciones en ese orden.
La tercera regla, conocida como la regla de las dos fases,
es el origen del nombre del sincronizador. Cada transacción debe
ser dividida en dos fases: una fase de crecimiento durante
la cual obtiene bloqueos, y una fase de encogimiento
durante la cual libera sus bloqueos.
Consideremos dos transacciones T1 y T2 :
T1 = r1[x] - w1[y] - c1
T2 = w2[x] - w2[y] - c2
y supongamos que se ejecutan en el siguiente
H1 = rl1[x]r1[x] ru1[x] wl2[x] w2[x] wl2[y] w2[y] wu2[x] wu2[y] c2 wl1[y] w1[y] wu1[y] c1
Como r1[x] > w2[x] y w2[y] > w1[y], se presenta el ciclo T1
- T2 - T1 y, como consecuencia, H1 no es serializable.
El problema en H1 es que T1 libera un bloqueo (ru1[x]) y
luego obtiene otro (wl1[y]), violando la regla de las dos
fases. Entre ru1[x] y wl1[y], otra transacción T2
escribe sobre x y y, y es así que aparece después de
T1 con respecto a x y la precede con respecto a y. Si T1
hubiese seguido la regla de las dos fases, esta "ventana" entre
ru1[x] y wl1[y] no se habría abierto, y T2 no se
hubiera podido ejecutar como ocurrió en H1. Por ejemplo,
T1 y T2 pudo haberse ejecutado como sigue.
Inicialmente, ninguna transacción tiene bloqueo alguno.
El sincronizador recibe r1[x] del TM. De acuerdo a esto, asigna
rl1[x] y envía r1[x] al DM. Luego el DM reconoce el
procesamiento de r2[x].
El sincronizador recibe w2[x] del TM. El sincronizador no puede
asignar wl2[x], puesto que está en conflicto con rl1[x],
por lo que retrasa la ejecución de w2[x] colocándolo en una
cola de espera.
El sincronizador recibe w1[y] del TM. Asigna wl1[y] y
envía w1[y] al DM. Luego el DM reconoce el procesamiento
de w1[y].
El sincronizador recibe c1 del TM, señalando que T1 ha
terminado. El sincronizador envía c1 al DM. Después que
el DM reconoce el procesamiento de c1, el sincronizador libera
rl1[x] y wl1[y]. Esto está de acuerdo a la regla (2)
porque r1[x] y w1[y] ya han sido procesadas, y de acuerdo a
la regla (3) porque T1 no solicitará ningún otro bloqueo.
El sincronizador asigna wl2[x] tal que w2[x], que había
sido retrasada, puede ser enviada al DM. Luego el DM reconoce
w2[x].
El sincronizador recibe w2[y] del TM. Asigna wl2[y] y
envía w2[y] al DM. El DM luego reconoce el procesamiento
de w2[y].
T2 termina y el TM envía c2 al sincronizador. El
sincronizador envía c2 al DM. Después que el DM reconoce
el procesamiento de c2, el sincronizador libera wl2[x] y
wl2[y].
Esta ejecución es representada por la siguiente historia
H2 = rl1[x] r1[x] wl1[y] w1[y] c1 ru1[x] wu1[y] wl2[x] w2[x] wl2[y] w2[y] c2 wu2[x] wu2[y]
H2 es serial y por lo tanto serializable.
Una importante desventaja de los sincronizadores de 2PL es que
están propensos a bloqueos mutuos (deadlocks), conocidos
también como "abrazo mortal". Por ejemplo, supongamos que un
sincronizador 2PL está procesando las transacciones T1 y
T3
T1 = r1[x] - w1[y] - c1
T3 = w3[y] - w3[x] - c3
y consideremos la siguiente secuencia de eventos:
Inicialmente, ninguna transacción tiene bloqueo alguno.
El sincronizador recibe r1[x] del TM. Asigna rl1[x] y
envía r1[x] al DM.
El sincronizador recibe w3[y] del TM. Asigna wl3[y] y
envía w3[y] al DM.
El sincronizador recibe w3[x] del TM. El sincronizador no puede
asignar wl3[x] porque entra en conflicto con rl1[x] que ya
está asignado. Por lo tanto, se retrasa la ejecución de
w3[x].
El sincronizador recibe w1[y] del TM. Como en (4), se retrasa
la ejecución de w1[y].
Aún cuando el sincronizador se ha desempeñado exactamente de
acuerdo a las reglas del 2PL, ni T1 ni T3 pueden ser
completadas sin violar una de estas reglas. Si el sincronizador
envía w1[y] al DM sin asignar wl1[y] estaría
violando la regla (1). Similarmente para w3[x]. Supongamos que
el sincronizador libera wl3[y], de modo que se puede asignar
wl1[y] y sería posible enviar w1[y] al DM. En este
caso, el sincronizador no podría asignar wl3[x] (y por lo
tanto, tampoco podría procesar w3[x]), o, en caso
contrario, violaría la regla (3). Similarmente si libera
rl1[x]. Esta es una clásica situación de bloqueo mutuo.
Antes de que cualquiera de las transacciones pueda ser procesada,
una de ellas debe liberar un bloqueo que la otra necesita para
continuar.
Los bloqueos mutuos también aparecen cuando las transacciones
tratan de convertir bloqueos de lectura en bloqueos de escritura,
Supongamos que la transacción Ti lee un dato x y luego
intenta escribir sobre él. Ti transmite ri[x] al
sincronizador, el cual asigna rli[x]. Cuando Ti transmite
wi[x] al sincronizador, éste debe actualizar rli[x] a
wli[x]. Esta actualización de bloqueo se conoce como
conversión de bloqueo. Para seguir las reglas de 2PL, el
sincronizador no debe liberar rli[x]. Esto no es un problema,
porque los bloqueos asignados por la misma transacción no son
conflictivos entre sí. Sin embargo, si dos transacciones
intentan concurrentemente convertir sus bloqueos de lectura sobre
un dato en bloqueos de escritura, el resultado es un bloqueo
mutuo.
Por ejemplo, supongamos que T4 y T5 transmiten operaciones a
un sincronizador 2PL.
T4 = r4[x] - w4[x] - c4
T5 = r5[x] - w5[x] - c5
y consideremos la siguiente secuencia de eventos:
El sincronizador recibe r4[x], y por consiguiente, asigna
rl4[x] y envía r4[x] al DM.
El sincronizador recibe r5[x], y por consiguiente, asigna
rl5[x] y envía r5[x] al DM.
El sincronizador recibe w4[x]. Debe retrasar la operación
porque wl4[x] está en conflicto con rl5[x].
El sincronizador recibe w5[x]. Debe retrasar la operación
porque wl5[x] está en conflicto con rl4[x].
Dado que ninguna de las transacciones puede liberar el rl[x] que
posee, y dado que ninguna de ellas puede continuar hasta que se
asigne su wl[x], las transacciones están bloqueadas
mutuamente. Este tipo de abrazo mortal ocurre comúnmente cuando
una transacción busca una gran cantidad de datos que contengan
un determinado valor y luego los actualiza. Esto origina la
asignación de un bloqueo de lectura en cada uno de estos datos y
luego la conversión de estos bloqueos en bloqueos de escritura.
El sincronizador requiere una estrategia para detectar bloqueos
mutuos, de tal manera que no existan transacciones que permanezcan
bloqueadas. Una estrategia es utilizar lapsos de tiempo. Si
el sincronizador descubre que una transacción está esperando
mucho tiempo por un bloqueo, simplemente asume que puede existir
un bloqueo mutuo que involucre esta transacción y por lo tanto
la cancela.
Referencias
Berstein P.A., Shipman D.W., Wong W.S., Formal Aspects of Serializability in Database Concurrency Control, IEEE Trans. on Software Engineering, 1979, 5, 3, 203-216, Mayo
Bjork L.A., Davies C.T., The Semantics of the Preservation and Recovery of Integrity in a Data System, Technical Report TR-02.540, IBM}, Diciembre,1972,
Papadimitriou C.H., Serializability of Concurrent Database Updates, Journal of the ACM, 1979, 26, 4, 631-653, Octubre
Papadimitriou C.H., The Theory of Concurrency Control, Computer Science Press, Rockville, MD, 1986
Lomet D.B., Process Structuring, Synchronization and Recovery using Atomic Actions, ACM SIGPLAN Notices, 1977, 12, 3, 128-137, Marzo
Devor C., Carlson C.R., Structural Locking Mechanisms and Their Effect on Database Management System Performance, Information Systems, 1982, 7, 2, 345-358.
Eswaran K.P., Gary J.N., Lorie R.A., Traiger I.L., Liu M.T., The Notions of Consistency and Predicate Locks in a Database System, Comm. ACM, 1976, 19, 11, 624-633, Noviembre.
Galler B.I., Bos L., A Model of Transaction Blocking in Databases, Performance Evaluation, 1983, 3, 95-122
Stearns R.E., Lewis P.M., Rosenkrantz D.J., Concurrency Control for Database Systems, Proc. 17th Symp. on Foundations of Computer Sience, IEEE, 1976, 19-32
Gary J.N., Lorie R.A., Putzulo G.R., Traiger J.L., Granularity of Locks and Degrees of Consistency in a Shared Database, Research Report RJ1654, IBM, 1975, Setiembre.
Yannakakis M., Serializability by Locking, Journal of the ACM, 1984, 31, 2, 227-244.
Mitra D., Weinberger P.J., Probabilistic Models of Database Locking: Solutions, Computational Algorithms and Asymptotics, Journal of the ACM, 1984, 31, 4, 855-878, Octubre.
Potier D., Leblanc P., Analysis of Locking Policies in Database Management Systems, Comm. ACM, 1980, 23, 10, 584-593, Octubre
Stearns R.E., Lewis P.M., Rosenkrantz D.J., Concurrency Control for Database Systems , Proc. 17th Symp. on Foundations of Computer Sience, IEEE, 1976, 19-32.
Hadzilacos V., An Operational Model for Database System Reliability, Proc. 2nd ACM SIGACT-SIGMOD Symp. on Principles of Database Systems, 1983, Atlanta, GA, Marzo, 244-256
Hadzilacos V., A Theory of Reliability in Database System, Proc. 4th ACM SIGACT-SIGMOD Symp. on Principles of Database Systems, 1986, Atlanta, GA, Abril, 217-225.
|