Hoppa till innehåll

Säker uppdatering i Cosmos DB med etagg, ASP.NET Core

I det här inlägget beskrivs hur du kan göra säkra uppdateringar av dokument i Cosmos DB med optimistisk samtidighetskontroll genom att använda etaggen i ett dokument. Cosmos DB implementerar optimistisk samtidighet och du kan inte låsa dokument medan du läser ifrån ett dokument eller skriver till ett dokument.

Optimistisk samtidighet innebär att om två samtidiga operationer försöker uppdatera ett dokument inom en logisk partition så kommer en operation att vinna och den andra operationen kommer att misslyckas. När detta händer så innehåller ditt dokument sannolikt felaktiga data och det kan vara viktigt att ha korrekt data i dokumentet.

Optimistisk samtidighetskontroll (OCC) ger dig möjlighet att se till att alla uppdateringar genomförs och detta innebär att din data förblir korrekt. OCC kan implementeras genom att använda etaggen i ett dokument. Värdet för etaggen genereras automatiskt och uppdateras på servern varje gång ett dokument uppdateras. Genom att kontrollera om etaggen har förändrats mellan en läsning och en uppdatering kan du se till att du uppdaterar den senaste versionen av dokumentet.

Cosmos DB klient

Jag har redan skrivit ett inlägg om en generisk cosmos db klient som innehåller de metoder vi behöver. Vi måste hämta ett dokument med en etagg och uppdatera ett dokument genom att kontrollera etaggen. Jag inkluderar också kod för ModelItem och ModelPage.

  1. public async Task<ModelItem<T>> GetByIdWithEtag<T>(string id, string partion_key)
  2. {
  3. // Create variables to return
  4. ModelItem<T> model = new ModelItem<T>();
  5. try
  6. {
  7. // Get the response
  8. ResourceResponse<Document> response = await this.client.ReadDocumentAsync(UriFactory.CreateDocumentUri(this.options.Database, this.options.Collection, id),
  9. new RequestOptions { PartitionKey = new PartitionKey(partion_key) });
  10. // Get the document
  11. Document document = response.Resource;
  12. // Get the etag
  13. model.etag = document.ETag;
  14. // Get the post
  15. model.item = (T)(dynamic)document;
  16. }
  17. catch (DocumentClientException de)
  18. {
  19. // Log the exception
  20. this.logger.LogDebug(de, $"GetByIdWithEtag", null);
  21. model.error = true;
  22. }
  23. // Return the model
  24. return model;
  25. } // End of the GetByIdWithEtag method
  26. public async Task<bool> Update<T>(string id, T item, string etag)
  27. {
  28. try
  29. {
  30. // Create an access condition
  31. AccessCondition ac = new AccessCondition { Condition = etag, Type = AccessConditionType.IfMatch };
  32. // Update the document
  33. await this.client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(this.options.Database, this.options.Collection, id), item, new RequestOptions { AccessCondition = ac });
  34. }
  35. catch (DocumentClientException de)
  36. {
  37. // Check for exceptions
  38. if (de.StatusCode == HttpStatusCode.PreconditionFailed)
  39. {
  40. return false;
  41. }
  42. else
  43. {
  44. // Log the exception
  45. this.logger.LogError(de, $"Update, id: {id}, etag: {etag}", null);
  46. }
  47. }
  48. // Return a success response
  49. return true;
  50. } // End of the Update method
  1. public class ModelItem<T>
  2. {
  3. #region Variables
  4. public T item { get; set; }
  5. public string etag { get; set; }
  6. public bool error { get; set; }
  7. #endregion
  8. #region Constructors
  9. public ModelItem()
  10. {
  11. // Set values for instance variables
  12. this.item = default(T);
  13. this.etag = "";
  14. this.error = false;
  15. } // End of the constructor
  16. public ModelItem(T item, string etag, bool error)
  17. {
  18. // Set values for instance variables
  19. this.item = item;
  20. this.etag = etag;
  21. this.error = false;
  22. } // End of the constructor
  23. #endregion
  24. } // End of the class
  25. public class ModelPage<T>
  26. {
  27. #region Variables
  28. public IList<T> items { get; set; }
  29. public string ct { get; set; }
  30. public bool error { get; set; }
  31. #endregion
  32. #region Constructors
  33. public ModelPage()
  34. {
  35. // Set values for instance variables
  36. this.items = new List<T>();
  37. this.ct = "";
  38. this.error = false;
  39. } // End of the constructor
  40. #endregion
  41. } // End of the class

Uppdatera med etagg

Metoden nedan används för att hämta en lista med orter och för att spara en fortsättningstoken i ett dokument så att vi kan fortsätta att hämta fler orter vid en senare tidpunkt. Vi måste implementera optimistisk samtidighetskontroll när vår token sparas. Vi erhåller först en fortsättningstoken och sedan loopar vi tills vi har lyckats att uppdatera dokumentet.

  1. public async Task<ModelPage<LocationDocument>> GetLocations(Int32 page_size)
  2. {
  3. // Create the locations page to return
  4. ModelPage<LocationDocument> page = new ModelPage<LocationDocument>();
  5. // Get the job locations page
  6. ModelItem<JobLocationsPage> job_tuple = await this.cosmos_database_repository.GetByIdWithEtag<JobLocationsPage>("xx481cd9-7961-4c6e-960e-7cb6e5cde5e8", "xx481cd9-7961-4c6e-960e-7cb6e5cde5e8");
  7. // Loop until a successful update
  8. while (true)
  9. {
  10. // Get locations
  11. page = await this.location_repository.GetChunk("page_name", "ASC", page_size, job_tuple.item.continuation_token);
  12. // Update the job locations page
  13. job_tuple.item.continuation_token = page.ct;
  14. bool success = await this.cosmos_database_repository.Update<JobLocationsPage>(job_tuple.item.id, job_tuple.item, job_tuple.etag);
  15. // Check if the update failed
  16. if (success == false)
  17. {
  18. // Get the tuple again
  19. job_tuple = await this.cosmos_database_repository.GetByIdWithEtag<JobLocationsPage>("xx481cd9-7961-4c6e-960e-7cb6e5cde5e8", "xx481cd9-7961-4c6e-960e-7cb6e5cde5e8");
  20. // Continue the loop
  21. continue;
  22. }
  23. // Break out from the loop
  24. break;
  25. } // End of the while(true) loop
  26. // Return the page with locations
  27. return page;
  28. } // End of the GetLocations method

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *