Posso fare affidamento sulla lettura dei valori di identità di SQL Server in ordine?


24

TL; DR: La domanda che segue si riduce a: Quando si inserisce una riga, esiste una finestra di opportunità tra la generazione di un nuovo Identityvalore e il blocco della chiave di riga corrispondente nell'indice cluster, in cui un osservatore esterno potrebbe vedere un nuovo Identity valore inserito da una transazione concorrente? (In SQL Server.)

Versione dettagliata

Ho una tabella di SQL Server con una Identitycolonna chiamata CheckpointSequence, che è la chiave dell'indice cluster della tabella (che ha anche un numero di indici aggiuntivi non cluster). Le righe vengono inserite nella tabella da numerosi processi e thread simultanei (a livello di isolamento READ COMMITTEDe senza IDENTITY_INSERT). Allo stesso tempo, ci sono processi che leggono periodicamente le righe dall'indice cluster, ordinati per quella CheckpointSequencecolonna (anche a livello di isolamento READ COMMITTED, con l' READ COMMITTED SNAPSHOTopzione disattivata).

Attualmente mi affido al fatto che i processi di lettura non possono mai "saltare" un checkpoint. La mia domanda è: posso fare affidamento su questa proprietà? E se no, cosa potrei fare per renderlo vero?

Esempio: quando vengono inserite righe con valori di identità 1, 2, 3, 4 e 5, il lettore non deve vedere la riga con valore 5 prima di vedere quella con valore 4. I test mostrano che la query, che contiene una ORDER BY CheckpointSequenceclausola ( e una WHERE CheckpointSequence > -1clausola), si blocca in modo affidabile ogni volta che la riga 4 deve essere letta, ma non ancora impegnata, anche se la riga 5 è già stata impegnata.

Credo che almeno in teoria, ci potrebbe essere una condizione di razza qui che potrebbe causare la rottura di questa ipotesi. Sfortunatamente, la documentazione su Identitynon dice molto su come Identityfunziona nel contesto di più transazioni simultanee, dice solo "Ogni nuovo valore viene generato in base al seed e all'incremento correnti". e "Ogni nuovo valore per una determinata transazione è diverso dalle altre transazioni simultanee nella tabella." ( MSDN )

Il mio ragionamento è che deve funzionare in qualche modo così:

  1. Viene avviata una transazione (esplicitamente o implicitamente).
  2. Viene generato un valore di identità (X).
  3. Il blocco di riga corrispondente viene preso sull'indice cluster in base al valore dell'identità (a meno che non entri in azione l'escalation del blocco, nel qual caso l'intera tabella viene bloccata).
  4. La riga è inserita.
  5. La transazione viene impegnata (probabilmente un bel po 'di tempo dopo), quindi il blocco viene rimosso di nuovo.

Penso che tra i passaggi 2 e 3, ci sia una finestra molto piccola in cui

  • una sessione simultanea potrebbe generare il valore di identità successivo (X + 1) ed eseguire tutti i passaggi rimanenti,
  • permettendo così a un lettore che arriva esattamente in quel momento di leggere il valore X + 1, mancando il valore di X.

Naturalmente, la probabilità di ciò sembra estremamente bassa; ma ancora - potrebbe succedere. O potrebbe?

(Se sei interessato al contesto: questa è l'implementazione del motore di persistenza SQL di NEventStore. NEventStore implementa un archivio eventi di sola aggiunta in cui ogni evento ottiene un nuovo numero progressivo di sequenza di checkpoint. I client leggono gli eventi dall'archivio eventi ordinati per checkpoint per eseguire calcoli di ogni tipo. Una volta elaborato un evento con checkpoint X, i clienti considerano solo eventi "più recenti", ovvero eventi con checkpoint X + 1 e successivi. Pertanto, è fondamentale che gli eventi non possano mai essere saltati, dato che non sarebbero mai più stati presi in considerazione. Attualmente sto cercando di determinare se l' Identityimplementazione del checkpoint basata su di essa soddisfa questo requisito. Queste sono le esatte istruzioni SQL utilizzate : schema , query di Writer ,Query del lettore .)

Se ho ragione e potrebbe sorgere la situazione sopra descritta, posso vedere solo due opzioni per gestirli, entrambi insoddisfacenti:

  • Quando vedi un valore di sequenza del checkpoint X + 1 prima di aver visto X, ignora X + 1 e riprova più tardi. Tuttavia, poiché Identityovviamente può produrre lacune (ad esempio, quando viene eseguito il rollback della transazione), X potrebbe non arrivare mai.
  • Quindi, stesso approccio, ma accetta il divario dopo n millisecondi. Tuttavia, quale valore di n dovrei assumere?

Qualche idea migliore?


Hai provato a usare Sequence invece di identità? Con l'identità, non credo che tu possa prevedere in modo affidabile quale inserto otterrà un particolare valore di identità, ma questo non dovrebbe essere un problema usando una sequenza. Ovviamente, questo cambia il modo in cui fai le cose adesso.
Antoine Hernandez,

@SoleDBAGuy Una sequenza non renderebbe le condizioni di gara sopra descritte ancora più probabili? Produco un nuovo valore di sequenza X (sostituendo il passaggio 2 sopra), quindi inserisco una riga (passaggi 3 e 4). Tra 2 e 3, c'è la possibilità che qualcun altro possa produrre il successivo valore di Sequenza X + 1, lo commetta e un lettore legge quel valore X + 1 prima ancora di riuscire a inserire la mia riga con il valore di Sequenza X.
Fabian Schmied

Risposte:


26

Quando si inserisce una riga, esiste una finestra di opportunità tra la generazione di un nuovo valore Identity e il blocco della chiave di riga corrispondente nell'indice cluster, in cui un osservatore esterno potrebbe vedere un valore Identity più recente inserito da una transazione concorrente?

Sì.

L' assegnazione dei valori di identità è indipendente dalla transazione dell'utente contenente . Questo è uno dei motivi per cui i valori di identità vengono consumati anche se viene eseguito il rollback della transazione. L'operazione di incremento stessa è protetta da un fermo per impedire la corruzione, ma questa è l'estensione delle protezioni.

Nelle circostanze specifiche dell'implementazione, l'allocazione dell'identità (una chiamata a CMEDSeqGen::GenerateNewValue) viene effettuata prima ancora che la transazione dell'utente per l'inserimento sia resa attiva (e quindi prima che vengano eseguiti i blocchi).

Eseguendo due inserimenti contemporaneamente a un debugger collegato per consentirmi di bloccare un thread subito dopo l'incremento e l'allocazione del valore dell'identità, sono stato in grado di riprodurre uno scenario in cui:

  1. La sessione 1 acquisisce un valore di identità (3)
  2. La sessione 2 acquisisce un valore di identità (4)
  3. La sessione 2 esegue il suo inserimento e commette (quindi la riga 4 è completamente visibile)
  4. La sessione 1 esegue l'inserimento e commette (riga 3)

Dopo il passaggio 3, una query che utilizza row_number sotto il blocco read read ha restituito quanto segue:

Immagine dello schermo

Nell'implementazione, ciò comporterebbe che l'ID di Checkpoint 3 non venisse corretto correttamente.

La finestra della disopportunità è relativamente piccola, ma esiste. Per fornire uno scenario più realistico rispetto al collegamento di un debugger: un thread di query in esecuzione può produrre lo scheduler dopo il passaggio 1 sopra. Ciò consente a un secondo thread di allocare un valore di identità, inserire e eseguire il commit, prima che il thread originale riprenda a eseguire il suo inserimento.

Per chiarezza, non ci sono blocchi o altri oggetti di sincronizzazione che proteggono il valore dell'identità dopo che è stato allocato e prima che venga utilizzato. Ad esempio, dopo il passaggio 1 sopra, una transazione simultanea può vedere il nuovo valore di identità usando le funzioni T-SQL come IDENT_CURRENTprima che la riga esista nella tabella (anche senza commit).

Fondamentalmente, non ci sono più garanzie sui valori di identità di quanto documentato :

  • Ogni nuovo valore viene generato in base al seme e all'incremento correnti.
  • Ogni nuovo valore per una particolare transazione è diverso dalle altre transazioni simultanee nella tabella.

È proprio così.

Se è richiesta una rigorosa elaborazione FIFO transazionale, probabilmente non hai altra scelta che serializzare manualmente. Se l'applicazione ha meno requisiti unous, hai più opzioni. La domanda non è chiara al 100% al riguardo. Tuttavia, potresti trovare alcune informazioni utili nell'articolo di Remus Rusanu Usare le tabelle come code .


7

Poiché Paul White ha risposto in modo assolutamente corretto, esiste la possibilità di file di identità temporaneamente "ignorati". Ecco solo un piccolo pezzo di codice per riprodurre questo caso per te.

Creare un database e una tabella di test:

create database IdentityTest
go
use IdentityTest
go
create table dbo.IdentityTest (ID int identity, c1 char(10))
create clustered index CI_dbo_IdentityTest_ID on dbo.IdentityTest(ID)

Eseguire inserimenti e selezioni simultanee su questa tabella in un programma console C #:

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading;

namespace IdentityTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var insertThreads = new List<Thread>();
            var selectThreads = new List<Thread>();

            //start threads for infinite inserts
            for (var i = 0; i < 100; i++)
            {
                insertThreads.Add(new Thread(InfiniteInsert));
                insertThreads[i].Start();
            }

            //start threads for infinite selects
            for (var i = 0; i < 10; i++)
            {
                selectThreads.Add(new Thread(InfiniteSelectAndCheck));
                selectThreads[i].Start();
            }
        }

        private static void InfiniteSelectAndCheck()
        {
            //infinite loop
            while (true)
            {
                //read top 2 IDs
                var cmd = new SqlCommand("select top(2) ID from dbo.IdentityTest order by ID desc")
                {
                    Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
                };

                try
                {
                    cmd.Connection.Open();
                    var dr = cmd.ExecuteReader();

                    //read first row
                    dr.Read();
                    var row1 = int.Parse(dr["ID"].ToString());

                    //read second row
                    dr.Read();
                    var row2 = int.Parse(dr["ID"].ToString());

                    //write line if row1 and row are not consecutive
                    if (row1 - 1 != row2)
                    {
                        Console.WriteLine("row1=" + row1 + ", row2=" + row2);
                    }
                }
                finally
                {
                    cmd.Connection.Close();
                }
            }
        }

        private static void InfiniteInsert()
        {
            //infinite loop
            while (true)
            {
                var cmd = new SqlCommand("insert into dbo.IdentityTest (c1) values('a')")
                {
                    Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
                };

                try
                {
                    cmd.Connection.Open();
                    cmd.ExecuteNonQuery();
                }
                finally
                {
                    cmd.Connection.Close();
                }
            }
        }
    }
}

Questa console stampa una riga per ogni caso quando uno dei thread di lettura "manca" una voce.


1
Bel codice ma controlli solo gli ID consecutivi ( "// scrivi riga se riga1 e riga non sono consecutive" ). Potrebbero esserci degli spazi vuoti che il codice stamperà. Ciò non significa che queste lacune verranno colmate successivamente.
ypercubeᵀᴹ

1
Poiché il codice non attiva uno scenario in cui IDENTITYprodurrebbe lacune (come il rollback di una transazione), le linee stampate mostrano effettivamente valori "saltati" (o almeno lo hanno fatto quando l'ho eseguito e controllato sul mio computer). Esempio di riproduzione molto bello!
Fabian Schmied,

5

È meglio non aspettarsi che le identità siano consecutive perché ci sono molti scenari che possono lasciare vuoti. È meglio considerare l'identità come un numero astratto e non attribuire ad essa alcun significato commerciale.

Fondamentalmente, possono verificarsi degli spazi vuoti se si esegue il rollback delle operazioni INSERT (o si eliminano esplicitamente le righe) e possono verificarsi duplicati se si imposta la proprietà della tabella IDENTITY_INSERT su ON.

Le lacune possono verificarsi quando:

  1. I record vengono eliminati.
  2. Si è verificato un errore durante il tentativo di inserire un nuovo record (ripristinato)
  3. Un aggiornamento / inserimento con valore esplicito (opzione identity_insert).
  4. Il valore incrementale è superiore a 1.
  5. Viene ripristinata una transazione.

La proprietà identità su una colonna non ha mai garantito:

• Unicità

• Valori consecutivi all'interno di una transazione. Se i valori devono essere consecutivi, la transazione deve utilizzare un blocco esclusivo sulla tabella o utilizzare il livello di isolamento SERIALIZZABILE.

• Valori consecutivi dopo il riavvio del server.

• Riutilizzo dei valori.

Se non è possibile utilizzare valori di identità per questo motivo, creare una tabella separata contenente un valore corrente e gestire l'accesso alla tabella e l'assegnazione dei numeri con l'applicazione. Ciò ha il potenziale di influire sulle prestazioni.

https://msdn.microsoft.com/en-us/library/ms186775(v=sql.105).aspx
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.110) aspx


Penso che le lacune non siano il mio problema principale - il mio problema principale è la crescente visibilità dei valori. (Vale a dire, il valore dell'identità 7 non deve essere osservabile per una query che ordina secondo quel valore prima che sia il valore dell'identità 6).
Fabian Schmied

1
Ho visto valori di identità commessi come: 1, 2, 5, 3, 4.
stacylaray

Certo, questo è facilmente riproducibile, ad esempio, usando lo scenario della risposta di Lennart. La domanda con cui sto lottando è se posso osservare quell'ordine di commit quando utilizzo una query con una ORDER BY CheckpointSequenceclausola (che sembra essere l'ordine dell'indice cluster). Penso che si riduca alla domanda se la generazione di un valore di identità sia comunque collegata ai blocchi adottati dall'istruzione INSERT o se si tratta semplicemente di due azioni non correlate eseguite da SQL Server una dopo l'altra.
Fabian Schmied,

1
Qual è la query? Se usi read commit allora, nel tuo esempio, ordina per 1, 2, 3, 5 perché sono stati impegnati e 4 no, ovvero lettura sporca. Inoltre, la tua spiegazione di NEventStore afferma "Pertanto, è fondamentale che gli eventi non possano mai essere saltati, poiché non verranno mai considerati di nuovo".
stacylaray,

La query è data sopra ( gist.github.com/fschmied/47f716c32cb64b852f90 ) - è cercapersone, ma si riduce a un semplice SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence. Non penso che questa query potrebbe leggere oltre la riga bloccata 4 o no? (Nei miei esperimenti, si blocca quando la query tenta di acquisire il blocco KEY per la riga 4.)
Fabian Schmied

1

Ho il sospetto che occasionalmente possa portare a problemi, problemi che peggiorano quando il server è sotto carico. Prendi in considerazione due transazioni:

  1. T1: inserisci in T ... - diciamo 5 vengono inseriti
  2. T2: inserire in T ... - dire 6 vengono inseriti
  3. T2: commit
  4. Reader vede 6 ma non 5
  5. T1: commit

Nello scenario sopra il tuo LAST_READ_ID sarà 6, quindi 5 non verrà mai letto.


I miei test sembrano indicare che questo scenario non è un problema perché Reader (passaggio 4) si bloccherà (fino a quando T1 non ha rilasciato i suoi blocchi) quando tenta di leggere la riga con valore 5. Mi manca qualcosa?
Fabian Schmied,

Potresti avere ragione, non conosco molto bene il meccanismo di blocco in SQL Server (quindi sospetto nella mia risposta).
Lennart,

Dipende dal livello di isolamento del lettore. E 'il mio Vedere entrambi, blocco, o vedere solo 6.
Michael Green

0

Esecuzione di questo script:

BEGIN TRAN;
INSERT INTO dbo.Example DEFAULT VALUES;
COMMIT;

Di seguito sono riportati i blocchi che vedo acquisiti e rilasciati come acquisiti da una sessione di eventi estesi:

name            timestamp                   associated_object_id    mode    object_id   resource_type   session_id  resource_description
lock_acquired   2016-03-29 06:37:28.9968693 1585440722              IX      1585440722  OBJECT          51          
lock_acquired   2016-03-29 06:37:28.9969268 7205759890195415040     IX      0           PAGE            51          1:1235
lock_acquired   2016-03-29 06:37:28.9969306 7205759890195415040     RI_NL   0           KEY             51          (ffffffffffff)
lock_acquired   2016-03-29 06:37:28.9969330 7205759890195415040     X       0           KEY             51          (29cf3326f583)
lock_released   2016-03-29 06:37:28.9969579 7205759890195415040     X       0           KEY             51          (29cf3326f583)
lock_released   2016-03-29 06:37:28.9969598 7205759890195415040     IX      0           PAGE            51          1:1235
lock_released   2016-03-29 06:37:28.9969607 1585440722              IX      1585440722  OBJECT          51      

Si noti il ​​blocco KEY RI_N acquisito immediatamente prima del blocco X per la nuova riga che si sta creando. Questo blocco a breve durata impedisce a un inserto simultaneo di acquisire un altro blocco RI_N KEY poiché i blocchi RI_N sono incompatibili. La finestra menzionata tra i passaggi 2 e 3 non è un problema perché il blocco dell'intervallo viene acquisito prima del blocco della riga sulla chiave appena generata.

Finché SELECT...ORDER BYla scansione inizia prima delle righe appena inserite desiderate, mi aspetto il comportamento desiderato nel READ COMMITTEDlivello di isolamento predefinito fino a quando l' READ_COMMITTED_SNAPSHOTopzione del database è disattivata.


1
Secondo technet.microsoft.com/en-us/library/… , due blocchi con RangeI_Nsono compatibili , ovvero non si bloccano a vicenda (il blocco è principalmente presente per il blocco su un lettore serializzabile esistente).
Fabian Schmied,

@FabianSchmied, interessante. Tale argomento è in conflitto con la matrice di compatibilità dei blocchi in technet.microsoft.com/en-us/library/ms186396(v=sql.105).aspx , che mostra che i blocchi non sono compatibili. L'esempio di inserimento nel link che hai citato indica lo stesso comportamento mostrato nella traccia nella mia risposta (blocco intervallo di inserimento di breve durata per testare l'intervallo prima del blocco chiave esclusivo).
Dan Guzman,

1
In realtà, la matrice dice "N" per "nessun conflitto" (non per "non compatibile") :)
Fabian Schmied

0

Dalla mia comprensione di SQL Server, il comportamento predefinito è che la seconda query non visualizzi alcun risultato fino a quando la prima query non è stata impegnata. Se la prima query esegue un ROLLBACK anziché un COMMIT, nella colonna sarà presente un ID mancante.

Configurazione di base

Tabella del database

Ho creato una tabella di database con la seguente struttura:

CREATE TABLE identity_rc_test (
    ID4VALUE INT IDENTITY (1,1), 
    TEXTVALUE NVARCHAR(20),
    CONSTRAINT PK_ID4_VALUE_CLUSTERED 
        PRIMARY KEY CLUSTERED (ID4VALUE, TEXTVALUE)
)

Livello di isolamento del database

Ho verificato il livello di isolamento del mio database con la seguente dichiarazione:

SELECT snapshot_isolation_state, 
       snapshot_isolation_state_desc, 
       is_read_committed_snapshot_on
FROM sys.databases WHERE NAME = 'mydatabase'

Che ha restituito il seguente risultato per il mio database:

snapshot_isolation_state    snapshot_isolation_state_desc   is_read_committed_snapshot_on
0                           OFF                             0

(Questa è l'impostazione predefinita per un database in SQL Server 2012)

Script di test

Gli script seguenti sono stati eseguiti utilizzando le impostazioni client SSMS standard di SQL Server e le impostazioni standard di SQL Server.

Impostazioni delle connessioni client

Il client è stato impostato per utilizzare il livello di isolamento della transazione READ COMMITTED secondo le Opzioni di query in SSMS.

Query 1

La query seguente è stata eseguita in una finestra Query con SPID 57

SELECT * FROM dbo.identity_rc_test
BEGIN TRANSACTION [FIRST_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Nine')
/* Commit is commented out to prevent the INSERT from being commited
--COMMIT TRANSACTION [FIRST_QUERY]
--ROLLBACK TRANSACTION [FIRST_QUERY]
*/

Query 2

La query seguente è stata eseguita in una finestra Query con SPID 58

BEGIN TRANSACTION [SECOND_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Ten')
COMMIT TRANSACTION [SECOND_QUERY]
SELECT * FROM dbo.identity_rc_test

La query non viene completata e attende il rilascio del blocco eXclusive su una PAGINA.

Script per determinare il blocco

Questo script visualizza il blocco che si verifica sugli oggetti del database per le due transazioni:

SELECT request_session_id, resource_type,
       resource_description, 
       resource_associated_entity_id,
       request_mode, request_status
FROM sys.dm_tran_locks
WHERE request_session_id IN (57, 58)

E qui ci sono i risultati:

58  DATABASE                    0                   S   GRANT
57  DATABASE                    0                   S   GRANT
58  PAGE            1:79        72057594040549300   IS  GRANT
57  PAGE            1:79        72057594040549300   IX  GRANT
57  KEY         (a0aba7857f1b)  72057594040549300   X   GRANT
58  KEY         (a0aba7857f1b)  72057594040549300   S   WAIT
58  OBJECT                      245575913           IS  GRANT
57  OBJECT                      245575913           IX  GRANT

I risultati mostrano che la finestra della query uno (SPID 57) ha un blocco condiviso (S) sul DATABASE un blocco Intended eXlusive (IX) su OBJECT, un blocco Intended eXlusive (IX) sulla PAGINA che desidera inserire e un eXclusive lock (X) sul KEY che è stato inserito, ma non ancora eseguito il commit.

A causa dei dati non sottoposti a commit, la seconda query (SPID 58) ha un blocco condiviso (S) a livello DATABASE, un blocco condiviso previsto (IS) su OBJECT, un blocco condiviso condiviso (IS) sulla pagina un condiviso (S ) bloccare il TASTO con uno stato di richiesta WAIT.

Sommario

La query nella prima finestra della query viene eseguita senza commit. Poiché la seconda query può solo READ COMMITTEDdati, attende fino a quando si verifica il timeout o fino a quando la transazione non è stata impegnata nella prima query.

Ciò deriva dalla mia comprensione del comportamento predefinito di Microsoft SQL Server.

Si dovrebbe osservare che l'ID è effettivamente in sequenza per le letture successive delle istruzioni SELECT se la prima istruzione COMMITs.

Se la prima istruzione esegue un ROLLBACK, troverai un ID mancante nella sequenza, ma comunque con l'ID in ordine crescente (a condizione che tu abbia creato l'INDICE con l'opzione predefinita o ASC nella colonna ID).

Aggiornare:

(Senza mezzi termini) Sì, puoi fare affidamento sul corretto funzionamento della colonna identità, fino a quando non riscontri un problema. Esiste un solo HOTFIX relativo a SQL Server 2000 e alla colonna Identity sul sito Web di Microsoft.

Se non si potesse fare affidamento sull'aggiornamento corretto della colonna identità, penso che sul sito Web di Microsoft sarebbero presenti più hotfix o patch.

Se si dispone di un contratto di supporto Microsoft, è sempre possibile aprire un caso di consulenza e richiedere ulteriori informazioni.


1
Grazie per l'analisi, ma la mia domanda è se esiste una finestra temporale tra la generazione del Identityvalore successivo e l'acquisizione del blocco KEY sulla riga (in cui potrebbero rientrare letture / scrittori simultanei). Non penso che ciò sia dimostrato impossibile dalle tue osservazioni perché non si può fermare l'esecuzione della query e analizzare i blocchi durante quella finestra ultra-breve.
Fabian Schmied,

No, non puoi fermare le affermazioni, ma la mia (lenta) osservazione è ciò che accade su una base veloce / normale. Non appena uno SPID acquisisce un blocco per inserire dati, l'altro non sarà in grado di acquisire lo stesso blocco. L'istruzione più rapida avrà il vantaggio di aver già acquisito il blocco e l'ID in sequenza. L'istruzione successiva riceverà l'ID successivo dopo il rilascio del blocco.
John aka hot2use,

1
Su base normale, le tue osservazioni coincidono con le mie (e anche le mie aspettative) - è buono a sapersi. Mi chiedo se ci siano situazioni eccezionali in cui non reggeranno.
Fabian Schmied,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.