{"id":18689299,"url":"https://github.com/derstimmler/transactionmyths","last_synced_at":"2026-04-13T02:04:31.120Z","repository":{"id":130531231,"uuid":"371076176","full_name":"DerStimmler/TransactionMyths","owner":"DerStimmler","description":"This repo shows how to solve concurrency problems in a high load multi user system with SQL transactions and NServiceBus sagas.","archived":false,"fork":false,"pushed_at":"2021-05-31T07:36:48.000Z","size":31,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-12-28T01:43:00.080Z","etag":null,"topics":["concurrency","csharp","database","dotnet","nservicebus","sql","transactions"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DerStimmler.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-05-26T15:07:16.000Z","updated_at":"2021-06-29T10:10:58.000Z","dependencies_parsed_at":null,"dependency_job_id":"9418b204-3515-4309-b0b1-d9935910384a","html_url":"https://github.com/DerStimmler/TransactionMyths","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DerStimmler%2FTransactionMyths","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DerStimmler%2FTransactionMyths/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DerStimmler%2FTransactionMyths/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DerStimmler%2FTransactionMyths/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DerStimmler","download_url":"https://codeload.github.com/DerStimmler/TransactionMyths/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239550296,"owners_count":19657538,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["concurrency","csharp","database","dotnet","nservicebus","sql","transactions"],"created_at":"2024-11-07T10:42:55.128Z","updated_at":"2026-04-13T02:04:31.070Z","avatar_url":"https://github.com/DerStimmler.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TransactionMyths\n\nThis repo shows how to solve concurrency problems in a high load multi-user system with SQL transactions and NServiceBus sagas.\n\n## Setup\n\n1. Add the connection string for your MSSQL database in `Shared/Configuration.cs`\n2. Run the project `ResetDb` to set up a new database with test data based on your connection string.\n\n## Scenario\n\nYour application is used by many users. Each user belongs to a company.\nSome users inside a company can have the role \"Admin\" to access some additional features like the user management where they can manage the roles of all users inside their own company.\nThe company has the invariant that it must have **at least one admin**.\n\nImagine the following scenario:\nCurrently, a company has two admins. Both admins are managing user roles and degrade each other at the exact same time (as it might happen in a high load multi-user system).\n\nDepending on how the application is implemented there are three possible results:\n\n1. Everything stays the same and both users are still admins\n2. Both users get degraded, and the company has no admin anymore\n3. One of both admins get degraded, and the company has one admin left (order doesn't matter)\n\nIn the following examples we try to prevent result two as it violates the invariant. But how?\n\n### Default Transactions\n\nIn the first example we try to solve the problem with SQL transactions.\n\nWhen you run the `ResetDb` project the database reflects the scenario in a simplified manner. It contains a `companies` table with a company and an `users` table containing two users. Both users have the role \"Admin\" and are in the same company.\n\nIn the solution folder `DefaultTransactions` are two projects. Each of the projects simulates a request from the user management by trying to remove the admin status of a user.\n\nFor that they call the `RemoveAdmin()` method which creates a SqlConnection and executes SQL statements inside a transaction. First the current amount of admins in the own company gets selected. If it is greater than one the user gets degraded.\n\nNote the `Thread.Sleep()` call through which the transactions overlap which helps to simulate a concurrent execution.\n\nWhen you run the projects `Transaction1` and `Transaction2` at the same time (in Rider you can use a compound for that) they both read that there are two admins in the company and therefore degrade the user. As a result, no admin is left. That's because the database uses pessimistic concurrency with the IsolationLevel \"Read Committed\" by default. Therefore, a lock gets acquired before reading and released immediately after reading is finished. So both transactions run parallel and read the same admin count of two.\n\nIn order to not violate the invariant, the time for how long the lock is acquired, needs to be changed. You can configure it by passing in an `IsolationLevel` to the `.BeginTransaction()` method.\nThe only two possible IsolationLevels for this scenario are \"RepeatableRead\" and \"Serializable\" since both of them acquire the lock from the beginning of the transaction until the end. As a result only one transaction completes while the other one fails because the required records are locked and a ConcurrencyException occurs. The invariant is kept.\n\nWhen using transactions the only solution for this problem is to lock database records so that only one of the transactions can succeed. For this you have to choose the right IsolationLevel and make sure that the failed transaction gets retried.\nNote that other transactions in your application that need to read that same table get blocked too, although they have nothing to do with your role management.\nThat's the reason why you should avoid this solution in high load multi-user systems.\n\nIn conclusion the technical issue is that you have to read the whole table to check for the invariant and that whole table needs to be locked during the transaction while other transactions get blocked.\n\n### SagaTransaction\n\nThe key to solve this issue is to reduce all data which is needed to check the invariant to one database row.\n\nFor that we are using a saga from NServiceBus. For understanding: A saga is a handler which can hold a state by persisting data to a database row.\nWe use it as a state machine which holds the count of how many admins exist in a company. So it receives the request to degrade a user from the API, checks the invariant, sends a new command to really degrade the user if the invariant isn't violated and reduces its admin count.\n\nThe solution folder `SagaTransaction` contains two projects.\n\nThe project `Api` simulates the API which receives the degradation request from the client and sends a corresponding command to the bus.\n\nThe `Backend` project handles these commands with the mentioned saga and a handler which executes a simple SQL statement that degrades a user.\n\nNow you can run the `Api` project to put some degradation request messages into the queue.\n\nWhen you are starting the `Backend` project all messages from the queue get handled simultaneously by the saga handler.\nBecause we configured the NServiceBus endpoint with the transaction mode \"SendsWithAtomicReceive\" every message has to be handled successfully before it's removed from the input queue and all messages that the handler tries to send are only sent if the handling is successful.\nNow what happens is that one message will start the saga which creates a database entry for that saga. The saga also checks the invariant and sends a message to the `RemoveAdminHandler` which degrades the user.\nAll other messages also try to start the saga, but an Exception occurs because the database entry already exists while NServiceBus ensures that only one message can start a saga. The selected transaction mode takes care that no message is sent to the `RemoveAdminHandler` and the messages get retried.\nWhen retrying, the saga uses pessimistic concurrency for handling the messages. It tries to acquire a lock on its database row to read its data before handling. This ensures that message handlers are invoked one after another. When checking the invariant for the remaining messages no additional user gets degraded because only one admin is left.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fderstimmler%2Ftransactionmyths","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fderstimmler%2Ftransactionmyths","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fderstimmler%2Ftransactionmyths/lists"}