Concurrencia y transacciones - script - ejemplo
From Ibbddunq
Contents |
El esquema de BD
encomienda <nroEncomienda, cliente, peso> servicio <nroServicio, origen, destino, disponible, cargaTotal> encomiendaEnServicio <nroServicio, nroEncomienda>
agregarEncomienda
procedure agregarEncomienda(elCliente, elPeso, elServicio) begin declare nroNuevaEnco integer; declare disponibleNuevo integer; declare cantEncomiendas integer; select max(nroEncomienda) + 1 from encomienda into nroNuevaEnco; select disponible - elPeso from servicio where nroServicio = elServicio into disponibleNuevo; select count(*) from encomiendaEnServicio where nroServicio = elServicio into cantEncomiendas; if (cantEncomiendas < 10) then insert into encomienda (nroEncomienda, cliente, peso) values (nroNuevaEnco, elCliente, elPeso); insert into encomiendaEnServicio (nroEncomienda, nroServicio) values (nroNuevaEnco, elServicio); update servicio set disponible = disponibleNuevo where nroServicio = elServicio; end if; end
con este stored procedure se puede
- mostrar un schedule de dos tx en el que el disponible de un servicio quede mal.
- llegar a la conclusión de qué condición deben tener dos ejecuciones concurrentes para que pueda haber anomalías de concurrencia: que estén agregando encomiendas en el mismo servicio.
- ver cómo tiene que ser un schedule para ser "malo": ambos deben leer el disponibleNuevo antes de que cualquiera de ellos haga update. Ver que si es serial, entonces naturalmente es "bueno", el problema está relacionado con cruces.
- corregir el update seteando disponible - elPeso, y sacando el select del servicio de arriba. Aclarar que toma el disponible en el momento que se ejecuta el update. Llegar a la conclusión de que se arregla la anomalía observada antes ...
- ... pero queda otra: puedo poner 11 encomiendas en un servicio. Esta la vamos a tener que solucionar reservando un recurso, o sea, que sea válido que yo diga "yo agarro un servicio y hasta que yo no termine nadie puede hacer nada con el servicio"
pasarPeso
procedure pasarPeso (servDesde, servHacia, cuanto) begin declare nuevoDisponible; start transaction update servicio set disponible = disponible + cuanto where nroServicio = servDesde; select disponible - cuanto from servicio where nroServicio = servHacia into nuevoDisponible; if (nuevoDisponible >= 0) then update servicio set disponible = disponible - cuanto where nroServicio = servHacia; commit; else rollback; end if; end
Un ejemplo armadito: supongamos esta instancia de servicio (considerando solamente nro y peso)
104, 500 123, 200 148, 33000 149, 50 150, 820
Veamos posibles transacciones concurrentes
pasarPeso(desde 150, hacia 104, 320) con pasarPeso(desde 148, hacia 104, 400)
en este caso el 104 me puede quedar con disponible negativo, p.ej. ver este schedule
150 --> 104 - 320 148 --> 104 - 400 start update del 150 select del 104 (da 180) start update del 148 select (da 100) if update del 104 (deja 180) commit fin if update del 104 (deja -220) commit fin
obsérvese que en la 2da transacción, el valor de disponible que leyó el select no coincide con el que tomó el update, o el que tomaría en un select puesto en el mismo lugar del update. Esa diferencia es el non repeatable read.
pasarPeso(desde 123, hacia 149, 120) con pasarPeso(desde 150, hacia 123, 240)
en este caso el 104 me puede quedar con disponible negativo, p.ej. ver este schedule
123 --> 149 - 120 150 --> 123 - 240 start update del 123 a 320 select (da -70) start update del 150 select del 123 (da ... ¿cuánto? ponele 80) if update del 123 ... debe esperar if rollback, deja 200 en el 123 fin ahora sí update ... oops a -40 commit fin
sí, el rollback puede traer efectos cruzados extraños ... más sobre esto en SO. En IBD nos alcanza con ver que hay un problema, y con resolverlo en forma robusta usando lockeos.
(si da)
pasarPeso(desde 123, hacia 149, 20) con pasarPeso(desde 149, hacia 123, 40)
este hacerlo cuando ya se haya hecho el lockeo
123 --> 149 - 120 149 --> 123 - 240 start update del 123 start update del 149 select for update del 149 ... debe esperar select for update del 123 ... deadlock
cantidadServiciosADestinos
function cantidadServiciosADestinos(dest1, dest2) begin
declare cantServ1, cantServ2; select count(*) from servicio where destino = dest1 into cantServ1; select count(*) from servicio where destino = dest2 into cantServ2; return cantServ1 + cantServ2;
end
ponele esta instancia de servicio (sólo nroServicio, origen, destino)
104, La Plata, Brandsen 148, Ranchos, Chascomus 150, La Plata, Chascomus 151, La Plata, Chascomus
y tengo dos tx concurrentes
cantidadS...(Brandsen,Chascomus) primer select --> 1 update servicio set destino = 'Brandsen' where nroServicio in (150,151); segundo select --> 1 return --> ¡¡2!!
lo mismo con
cantidadS...(Chascomus,Brandsen)
da 6.