Come impedire all'utente di richiedere il metodo API più volte in parallelo?

4

Diciamo che abbiamo un'app in cui gli utenti ottengono punti e possono scambiarli per premi. La richiesta di scambio , in pseudo-codice, potrebbe avere il seguente aspetto:

function exchangePointsForReward(userId, rewardId){
    user = getUser(userId)
    reward = getReward(rewardId)
    if (user.points >= reward.requiredPoints){
        giveRewardToUser(userId, rewardId)
        reduceUserPoints(userId, reward.requiredPoints)
    }
}

Ma se abbiamo un utente malintenzionato che cosa impedisce loro di creare una richiesta nel loro linguaggio di programmazione preferito e di inviarla 20 volte allo stesso tempo? Prima che la prima richiesta raggiunga reduceUserPoints() , dieci altri hanno già ottenuto fino a addNewReward() . Certo, i punti dell'utente alla fine della giornata potrebbero andare in profondità in negativo, ma cosa impedisce all'utente di afferrare rapidamente i premi e utilizzarli? Come posso garantire che sia possibile eseguire una sola operazione per un utente contemporaneamente?

Una soluzione che posso pensare è che l'operazione tenta di acquisire un blocco all'inizio dell'operazione e solo una singola operazione bloccabile può essere eseguita per un utente in qualsiasi momento:

function aquireLock(userId){
    lockId = getRandomLockId()
    database.query("UPDATE user SET lock={lockId} WHERE user={userId} AND lock IS NULL");
    return database.query("SELECT lock WHERE user = {userId}").first === lockId;
}

function exchangePointsForReward(userId, rewardId){
    if (!aquireLock(userId)){
        throw new Error("Failed to acquire lock");
    }
    user = getUser(userId)
    reward = getReward(rewardId)
    if (user.points >= reward.requiredPoints){
        giveRewardToUser(userId, rewardId)
        reduceUserPoints(userId, reward.requiredPoints)
    }
    releaseLock(userId);
}

Ma c'è qualche strategia migliore qui? La domanda è indipendente dal database.

    
posta Maurycy 04.01.2016 - 14:49
fonte

4 risposte

2

Penso che la tua strategia di blocco funzionerà, sebbene suggerirei di consultare un gestore del blocco distribuito (ad esempio, < a href="http://helix.apache.org/0.6.2-incubating-docs/recipes/lock_manager.html"> Progetto Helix di Apache ) invece di andare al database solo per ottenere un lucchetto. Ciò assicurerebbe che gli utenti non vengano bloccati se il servizio viene riavviato a metà richiesta.

Un'altra possibilità sarebbe quella di utilizzare gli ID utente per accodarli a specifici gestori di richieste; questo serializzerebbe le richieste parallele, ma è probabilmente più complicato e potrebbe portare all'elaborazione di colli di bottiglia (almeno fino a quando la configurazione non sarà ottimizzata).

    
risposta data 04.01.2016 - 15:18
fonte
1

È possibile ridurre i puntiUserPerificare se il risultato diventa negativo e, in caso affermativo, fallire.

function exchangePointsForReward(userId, rewardId){
    user = getUser(userId)
    reward = getReward(rewardId)
    try{
        reduceUserPoints(userId, reward.requiredPoints) //throws if result would become negative
        giveRewardToUser(userId, rewardId)
    }catch(){
        //show error message to user
    }
}

La chiave qui è che il controllo viene inserito nella reduceUserPoints .

Un'altra opzione è quella di utilizzare transazioni che ti garantiranno il corretto isolamento e il blocco automatico.

    
risposta data 04.01.2016 - 15:03
fonte
0

Potresti usare un approccio ottimistico. La maggior parte dei database utilizza una sorta di meccanismo di controllo delle versioni delle righe. Ad esempio, in SQL Server ...

link

Quindi, ogni richiesta ottiene la versione di riga restituita a loro se desiderano aggiornare i dati. L'aggiornamento utilizza quindi quella versione di riga come parte dell'aggiornamento. Se la versione della riga non corrisponde, sai che qualcun altro ha aggiornato il record in precedenza e puoi rifiutarlo (non eseguire l'aggiornamento). Basta notare che la versione di riga non è un buon candidato chiave, quindi alcuni altri punti dati dovrebbero essere usati per la ricerca e confrontano la versione della riga dopo che i dati sono stati restituiti. Quindi, se ci sono più letture, vince solo 1 aggiornamento successivo.

Tutto ciò che si può fare è una semplice stored procedure che richiede e aggiorna ed è possibile utilizzare le transazioni del database per gestire il "lock" poiché la versione della riga viene aggiornata atomicamente.

Funzionerà anche il blocco pessimistico, ma eliminerai un blocco per ogni transazione, che probabilmente è eccessivo per il normale caso d'uso.

    
risposta data 04.01.2016 - 22:17
fonte
0

Avrei bisogno che per eseguire l'operazione successiva l'utente debba passare un token restituito da un'operazione precedente.

Immagina di avere una tabella di token emessi (numeri casuali opachi). Quando un utente esegue correttamente una richiesta, un nuovo token viene inserito nella tabella e restituito all'utente.

È possibile serializzare l'accesso per contrassegnare un token "riscattato", in modo che sia garantito che un token possa essere riscattato solo se è ancora nuovo; quindi i punti sono acquisiti dall'utente. Un tentativo di riscattare un token già riscattato o un token sconosciuto genera un errore e i punti non vengono conteggiati.

Anche se un utente esegue più richieste in parallelo, ciascuna richiesta può trasportare solo lo stesso token non modificato e solo una delle richieste riuscirà, poiché il riscatto è serializzato.

Periodicamente i vecchi token riscattati e token non riscossi in eccesso vengono rimossi dalla tabella.

Se un utente passa un token non corretto, i punti non vengono acquisiti, ma un nuovo token valido viene restituito all'utente nella risposta. In questo modo i nuovi client o i client che hanno perso la sincronizzazione possono riconnettersi alla sequenza.

Il problema, ovviamente, è nell'aggiornamento serializzato. Ma se blocchi solo un record corrispondente a un token e non l'intera tabella, molte richieste possono essere pubblicate in parallelo.

    
risposta data 04.01.2016 - 22:40
fonte

Leggi altre domande sui tag