Non tutti sono a conoscenza di alcune interessanti API messe a disposizione di MySQL per la sincronizzazione di processi; MySQL infatti rende disponibile agli sviluppatori delle basilari funzioni per la gestione di mutex remoti. Le funzioni che analizzeremo (e che sono documentate qui) sono:
Come utilizzare queste funzioni
L'utilizzo classico è:
con questo procedimento tutte le operazioni eseguite al punto 2 saranno eseguite esclusivamente da un processo per volta.
Vediamo una possibile implementazione in Groovy di una classe che sfrutti queste funzionalità di MySQL. Tale implementazione metterà a disposizione i seguenti metodi:
--- INIZIO FILE MyMutex.groovy ---
package it.lorenzoingrilli.mymutex import groovy.sql.Sql import java.sql.Connection import java.sql.SQLException class MyMutex { private String name private Connection connection private Sql sql public MyMutex(String name, Connection connection) { this.name = name; this.connection = connection; this.sql = new Sql(connection); } public void execute(Closure closure) { lock() closure.call() unlock() } public synchronized void lock() throws SQLException { while(!lock(10)); } public synchronized boolean lock(int timeout) throws SQLException { if(timeout<0) throw new IllegalArgumentException("Timeout must be >= 0"); def result = sql.firstRow("SELECT GET_LOCK(${name}, ${timeout}) AS locked"); return result!=null && result.locked==1; } public synchronized void unlock() throws SQLException { sql.execute("DO RELEASE_LOCK(${name})") } public synchronized boolean isUnlocked() throws SQLException { def result = sql.firstRow("SELECT IS_FREE_LOCK(${name}) AS free") return result!=null && result.free==1 } public synchronized boolean isLocked() throws SQLException { return !isUnlocked(); } public synchronized boolean isLockOwned() throws SQLException { // ottengo il connection ID corrente long connectionId = sql.firstRow("SELECT CONNECTION_ID() AS cid").cid // ottengo il connection ID del thread che ha il lock sul mutex Long lockOwner = sql.firstRow("SELECT IS_USED_LOCK(${name}) AS cid")?.cid return lockOwner!=null && lockOwner==connectionId; } }
La classe MyMutex necessità di una connessione al database e del nome del mutex da utilizzare.
Mettiamo alla prova la classe MyMutex. Per farlo scriviamo due test case molto rozzi, nel primo lanciamo 3 thread non sincronizzati, nel secondo invece lanciamo i thread sincronizzandoli tramite di MyMutex.
Lanciamo 3 processi differenti (e concorrenti) del test case TestWithoutMutex.groovy:
def id = System.currentTimeMillis(); println "thread ${id}: inizio" Thread.sleep(250); println "thread ${id}: fine"
tramite il comando:
groovy TestWithoutMutex.groovy & groovy TestWithoutMutex.groovy & groovy TestWithoutMutex.groovy &
thread 1298331951760: inizio thread 1298331951760: fine thread 1298331952040: inizio thread 1298331952103: inizio thread 1298331952040: fine thread 1298331952103: fine
In questo caso notiamo come i thread si "mischino" tra loro (si evince dalle più scritte inizio oppure fine di seguito); la cosa rende evidente la non sincronizzazione dei thread. In questo esempio particolare i due thread 1298331952040 e 1298331952103 si sono sovrapposti
Prendiamo invece in esame il test case TestMutex.groovy:
import java.sql.DriverManager import it.lorenzoingrilli.mymutex.MyMutex String username = "test" String password = "testpsw" String url = "jdbc:mysql://localhost/test" String mutexName = "test" def mutex = new MyMutex(mutexName, DriverManager.getConnection(url, username, password)); mutex.execute { def id = System.currentTimeMillis(); println "thread ${id}: inizio" Thread.sleep(250); println "thread ${id}: fine" }
groovy TestMutex.groovy & groovy TestMutex.groovy & groovy TestMutex.groovy &
thread 1298331820382: inizio thread 1298331820382: fine thread 1298331820723: inizio thread 1298331820723: fine thread 1298331821048: inizio thread 1298331821048: fine
notiamo che tra "inizio" e "fine" di ogni thread non ci sono intromissioni da parte di altri thread, il che ci mostra come i thread siano correttamente sincronizzati. Questa sincronizzazione avviene in tutti i casi: (a) thread differenti all'interno della stessa virtual machine, (b) processi separati in esecuzione sulla stessa macchina, (3) processi separati in esecuzione su macchine separati
NOTA 1: Questo articolo è stato scritto pensando ad un unico server MySQL che più processi utilizzeranno per sincronizzarsi tra loro. Per certo le tecniche indicate non funzioneranno in configurazioni di replication master-slave di MySQL (a meno che tutti i software si sincronizzino utilizzando solo ed esclusivamente il server master). Non è stato effettuato alcun test su MySQL Cluster.
NOTA 2: ricordatevi che per eseguire i codici di esempio dovete avere il Connector/J JDBC per MySQL nel vosto CLASSPATH