Metro e tornelli: Esercizio di database

Usando la metro di Milano, ogni volta che passo ai tornelli, quasi instantaneamente passo con la tessera.
E mi è venuto in mente, come si potrebbe progettare la gestione dei biglietti per i tornelli?

Quanti passeggieri hanno?

Cercando online, ho trovato un paio di fonti:

Dalla pagina di wikipedia della metro di milano, nel 2019, la cifra dovrebbe essere circa di 900 mila persone al giorno ( Pagina metropolitana wikipedia ).

Invece sul resoconto dell’ATM del 2022 dice che, per tutta la gestione in italia, il numero è di 569,8 milioni all’anno, in cui viene comprese anche i mezzi in superficie e altre città.

Quindi credo che le due cifre siano simili, quindi ipotizzando circa 1 milione di passeggeri al giorno nella metropolitana di Milano ed essendo operativa dalle 5:30 di mattina all’1 di notte, quindi per circa 19 ore, in cui non sarà sempre piena, ma ci saranno orari di punta.

Quindi, facendo una media spannometrica, cioè senza contare i picchi e tenendo costante in tutta la giornata, sarebbero circa 52,5 mila accessi all’ora, quindi circa 850 accessi al minuto su vari tornelli.

(Questa cifra credo che sia molto al di sotto del picco vero.)

Che requisiti si vogliono avere?

Viaggiando ho trovato questi come requisiti che vengono implementati:

  • Poter acquistare un biglietto singolo o carnet per viaggiare
  • La segmentazione in zone
  • Abbonamenti settimanali e mensili
  • L’uscita della metro solo se si è convalidato all’entrata
  • Per i singoli, il periodo di utilizzo che varia in base al numero di zone prese

E da qui sono partito a pensare al modo più semplice per implementare queste richieste

La base (biglietti singoli)

Partendo dalla casistica più semplice, un biglietto per un solo viaggio, le 2 azioni principali sono:

  • Generare un biglietto (con la zona)
  • Convalidare il biglietto
    • Controllare la zona
    • Se è la prima volta, contrassegnare l’orario
    • Se è stato già finito il periodo, “bloccarlo”

Quindi la tabella potrebbe essere una semplice struttura come la seguente:

In questo modo possiamo generare un biglietto in modo molto semplice con id e zona.

Quando dobbiamo leggere, cercheremo per id, poi ci sarà da controllare se la zona in cui siamo è interessata e poi, se non c’è il first_time_used, si segnerà e si farà aprire lo sportello.
Invece se c’è già, bisognerà vedere se è ancora valido, usando il numero di zone.

E fin qui il modello è semplice, ma più biglietti stampiamo e più tempo ci vorrà per controllare se il biglietto è presente e se disponibile.

Si potrebbe avere una tabella sono con i biglietti validi, in cui tutti quelli che sono stati già scaduti vengono cancellati ( ma, per motivi sia statistici e altro, verranno conservati in altri database ) e così facendo da poter tenere solo il numero minore in circolazione.

Un altro problema potrebbe essere il numero eccessivo di richieste parallele, quindi come poter ottimizzare questa cosa?

Ottimizzare è necessario?

Faccio una piccola premessa, in questo racconto cercherò di trovare più ottimizzazione possibili per il caso d’uso che sto rappresentando.

La cosa migliore sarebbe iniziare con un modello semplice, vedere come l’utente lo usa e le criticità e poi da li ottimizzare, così da non fare ottimizzazioni anche sbagliate partendo da dati presupposti.

Quindi da qui in poi è tutto un esercizio per poter spremere al massimo in una situazione ipotetica i limiti.

E quindi, come ottimizzare i biglietti singoli?

Se, avendo molte richieste in parallelo, questo non è possibile gestire abbastanza neanche aumentando la potenza del server, si potrebbe iniziare a pensare a fare lo sharding dei biglietti.

Una prima metodologia sarebbe in base alle zone, visto che sono un sottoinsieme più limitato, ma che comunque è molto utile dal punto di vista dell’utente, visto che quando si andrà a convalidare si cercherà sia in base all’id sia in base alla zona.

E per gli abbonamenti?

In questo caso l’abbonamento è più semplice perchè non dobbiamo controllare se è già stato usato, ma solo che sia valido in quel periodo.

Quindi, per ogni tessera, se usassimo una nosql come mongodb, quindi documentale, i vari settimanali e mensili sarebbero all’interno del documento tessera e sarebbe più stabile la questione, visto che l’abbonamento ha dei range ben precisi per cui sia valido.

Ma se volessi ottimizzare anche qui, che cosa potrei fare di preciso?

Oltre all’implementazione di una vista materializzata per ogni zona che viene calcolata ogni mattina (e al massimo aggiornata qualora venissero fatti nuovi abbonamenti), un modo per andare ad escludere più velocemente quelli non inseriti sarebbe un filtro di bloom o cuckoo.

In questo modo abbiamo la certezza che un dato, se non presente in questo filtro, non lo è sicuramente disponibile.

Ci potrebbero essere alcuni falsi positivi, quindi se vogliamo essere certi che sia un valore presente dovremmo controllare nel database.

E l’uscita?

Per quanto riguarda l’uscita, si potrebbe implementare un database key-value per tenere traccia di tutti quelli che sono entrati e rimuovere quando passa al tornello di uscita, in questo modo riusciamo a mantenere solo quelli attualmente in metro e ridurre il numero di richieste.

In più, una volta passato il primo controllo, potrebbe essere necessario controllare che il biglietto sia ancora nella zona, quindi la chiave che mettiamo all’interno potrebbe essere la lista di zone disponibili, in modo da poter evitare una seconda lettura al database primario.

E i carnet?

Questo per il momento non ci ho pensato, quindi sarà per un prossimo articolo

Nessun dato in più?

In tutto questo ho prioritizzato soltanto l’utilizzo, ma se si volesse tenere i dati di accesso per ogni singola persona/tornello per poi usarli per fare qualche analisi (sempre nel rispetto della privacy), si potrebbe aggiungere i dati di accesso al transito in una tabella apposta.
Questo di per se non sarebbe un eccessivo problema di tempo, visto che se la tabella viene fatta solo per la scrittura le tempistiche sarebbero molto veloci, senza aggiungere indici che la rallenterebbero.

Sicuramente questi dati potrebbero anche essere trattati come log e poi presi da un altro sistema, oppure direttamente mandarli in coda per permettere di essere processati in modo asincrono.

Conclusione

Questo era un piccolo esercizio mentale su alcune delle ottimizzazione che si potrebbero presentare nel caso.

Ci potrebbero essere altre, il modo migliore, come per tutti i sistemi, è l’analisi dei dati che si hanno per capire bene come affrontare le esigenze.

È molto interessante anche i limiti che si potrebbero avere a livello di tempistiche e infrastrutturali, come per esempio il tempo massimo per cui un tornello dovrebbe rispondere o la connessione da internet che un autobus (in questo caso se collegato anche a tutto il resto dell’infrastruttura di trasporti) potrebbe non essere sempre stabile.