DistributedLock is a .NET library that provides robust and easy-to-use distributed mutexes, reader-writer locks, and semaphores based on a variety of underlying technologies.
With DistributedLock, synchronizing access to a region of code across multiple applications/machines is as simple as:
await using (await myDistributedLock.AcquireAsync())
{
// I hold the lock here
}
DistributedLock contains implementations based on various technologies; you can install implementation packages individually or just install the DistributedLock NuGet package , an "umbrella" package which includes all implementations as dependencies. Note that each package is versioned independently according to SemVer.
- DistributedLock.SqlServer
: uses Microsoft SQL Server
- DistributedLock.Postgres
: uses Postgresql
- DistributedLock.MySql
: uses MySQL or MariaDB
- DistributedLock.Oracle
: uses Oracle
- DistributedLock.Redis
: uses Redis
- DistributedLock.Azure
: uses Azure blobs
- DistributedLock.ZooKeeper
: uses Apache ZooKeeper
- DistributedLock.FileSystem
: uses lock files
- DistributedLock.WaitHandles
: uses operating system global
WaitHandle
s (Windows only)
Click on the name of any of the above packages to see the documentation specific to that implementation, or read on for general documentation that applies to all implementations.
The DistributedLock.Core package contains common code and abstractions and is referenced by all implementations.
- Locks: provide exclusive access to a region of code
- Reader-writer locks: a lock with multiple levels of access. The lock can be held concurrently either by any number of "readers" or by a single "writer".
- Semaphores: similar to a lock, but can be held by up to N users concurrently instead of just one.
While all implementations support locks, the other primitives are only supported by some implementations. See the implementation-specific documentation pages for details.
Because distributed locks (and other distributed synchronization primitives) are not isolated to a single process, their identity is based on their name which is provided through the constructor. Different underlying technologies have different restrictions on name format; however, DistributedLock largely allows you to ignore these by escaping/hashing names that would otherwise be invalid.
All synchronization primitives support the same basic access pattern. The Acquire
method returns a "handle" object that represents holding the lock. When the handle is disposed, the lock is released:
var myDistributedLock = new SqlDistributedLock(name, connectionString); // e. g. if we are using SQL Server
using (myDistributedLock.Acquire())
{
// we hold the lock here
} // implicit Dispose() call from using block releases it here
While Acquire
will block until the lock is available, there is also a TryAcquire
variant which returns null
if the lock could not be acquired (due to being held elsewhere):
using (var handle = myDistributedLock.TryAcquire())
{
if (handle != null)
{
// we acquired the lock :-)
}
else
{
// someone else has it :-(
}
}
async
versions of both of these methods are also supported. These are preferred when you are writing async code since they will not consume a thread while waiting for the lock. If you are using C#8 or higher, you can also dispose of handles asynchronously:
await using (await myDistributedLock.AcquireAsync()) { ... }
Additionally, all of these methods support an optional timeout
parameter. timeout
determines how long Acquire
will wait before failing with a TimeoutException
and how long TryAcquire
will wait before returning null. The default timeout
for Acquire
is Timeout.InfiniteTimeSpan
while for TryAcquire
the default timeout
is TimeSpan.Zero
.
Finally, the methods take an optional CancellationToken
parameter, which allows for the acquire operation to be interrupted via cancellation. Note that this won't cancel the hold on the lock once the acquire succeeds.
For applications that use dependency injection, DistributedLock's providers make it easy to separate out the specification of a lock's (or other primitive's) name from its other settings (such as a database connection string). For example in an ASP.NET Core app you might do:
// in your Startup.cs:
services.AddSingleton<IDistributedLockProvider>(_ => new PostgresDistributedSynchronizationProvider(myConnectionString));
services.AddTransient<SomeService>();
// in SomeService.cs
public class SomeService
{
private readonly IDistributedLockProvider _synchronizationProvider;
public SomeService(IDistributedLockProvider synchronizationProvider)
{
this._synchronizationProvider = synchronizationProvider;
}
public void InitializeUserAccount(int id)
{
// use the provider to construct a lock
var @lock = this._synchronizationProvider.CreateLock($"UserAccount{id}");
using (@lock.Acquire())
{
// do stuff
}
// ALTERNATIVELY, for common use-cases extension methods allow this to be done with a single call
using (this._synchronizationProvider.AcquireLock($"UserAccount{id}"))
{
// do stuff
}
}
}
Contributions are welcome! If you are interested in contributing towards a new or existing issue, please let me know via comments on the issue so that I can help you get started and avoid wasted effort on your part.
- 2.3.0
- Added Oracle-based implementation (#45, DistributedLock.Oracle 1.0.0). Thanks @odin568 for testing!
- Made file-based locking more robust to transient
UnauthorizedAccessException
s (#106 & #109, DistributedLock.FileSystem 1.0.1) - Work around cancellation bug in Npgsql command preparation (#112, DistributedLock.Postgres 1.0.2)
- 2.2.0
- Added MySQL/MariaDB-based implementation (#95, DistributedLock.MySql 1.0.0). Thanks @theplacefordev for testing!
- 2.1.0
- Added ZooKeeper-based implementation (#41, DistributedLock.ZooKeeper 1.0.0)
- 2.0.2
- Fixed bug where
HandleLostToken
would hang when accessed on a SqlServer or Postgres lock handle that used keepalive (#85, DistributedLock.Core 1.0.1) - Fixed bug where broken database connections could result in future lock attempts failing when using SqlServer or Postgres locks with multiplexing (#83, DistributedLock.Core 1.0.1)
- Updated Npgsql dependency to 5.x to take advantage of various bugfixes (#61, DistributedLock.Postgres 1.0.1)
- Fixed bug where
- 2.0.1
- Fixed Redis lock behavior when using a database with
WithKeyPrefix
(#66, DistributedLock.Redis 1.0.1). Thanks @skomis-mm for contributing!
- Fixed Redis lock behavior when using a database with
- 2.0.0 (see also Migrating from 1.x to 2.x)
- Revamped package structure so that DistributedLock is now an umbrella package and each implementation technology has its own package (BREAKING CHANGE)
- Added Postgresql-based locking (#56, DistributedLock.Postgres 1.0.0)
- Added Redis-based locking (#24, DistributedLock.Redis 1.0.0)
- Added Azure blob-based locking (#42, DistributedLock.Azure 1.0.0)
- Added file-based locking (#28, DistributedLock.FileSystem 1.0.0)
- Added provider classes for improved IOC integration (#13)
- Added strong naming to assemblies. Thanks @pedropaulovc for contributing! (#47, BREAKING CHANGE)
- Made lock handles implement
IAsyncDisposable
in addition toIDisposable
#20, BREAKING CHANGE) - Exposed implementation-agnostic interfaces (e. g.
IDistributedLock
) for all synchronization primitives (#10) - Added
HandleLostToken
API for tracking if a lock's underlying connection dies (#6, BREAKING CHANGE) - Added SourceLink support (#57)
- Removed
GetSafeName
API in favor of safe naming by default (BREAKING CHANGE) - Renamed "SystemDistributedLock" to "EventWaitHandleDistributedLock" (DistributedLock.WaitHandles 1.0.0)
- Stopped supporting net45 (BREAKING CHANGE)
- Removed
DbConnection
andDbTransaction
constructors formSqlDistributedLock
, leaving the constructors that takeIDbConnection
/IDbTransaction
(#35, BREAKING CHANGE) - Changed methods returning
Task<IDisposable>
to instead returnValueTask
, making it so thatusing (@lock.AcquireAsync()) { ... } without an
await` no longer compiles (#34, BREAKING CHANGE) - Changed
UpgradeableLockHandle.UpgradeToWriteLock
to returnvoid
(#33, BREAKING CHANGE) - Switched to Microsoft.Data.SqlClient by default for all target frameworks (BREAKING CHANGE)
- Changed all locking implementations to be non-reentrant (BREAKING CHANGE)
- 1.5.0
- Added cross-platform support via Microsoft.Data.SqlClient (#25). This feature is available for .NET Standard >= 2.0. Thanks to @alesebi91 for helping with the implementation and testing!
- Added C#8 nullable annotations (#31)
- Fixed minor bug in connection multiplexing which could lead to more lock contention (#32)
- 1.4.0
- Added a SQL-based distributed semaphore (#7)
- Fix bug where SqlDistributedLockConnectionStrategy.Azure would leak connections, relying on GC to reclaim them (#14). Thanks zavalita1 for investigating this issue!
- Throw a specific exception type (
DeadlockException
) rather than the genericInvalidOperationException
when a deadlock is detected (#11)
- 1.3.1 Minor fix to avoid "leaking" isolation level changes in transaction-based locks (#8). Also switched to the VS2017 project file format
- 1.3.0 Added an Azure connection strategy to keep lock connections from becoming idle and being reclaimed by Azure's connection governor (#5)
- 1.2.0
- Added a SQL-based distributed reader-writer lock
- .NET Core support via .NET Standard
- Changed the default locking scope for SQL distributed lock to be a connection rather than a transaction, avoiding cases where long-running transactions can block backups
- Allowed for customization of the SQL distributed lock connection strategy when connecting via a connection string
- Added a new connection strategy which allows for multiplexing multiple held locks onto one connection
- Added IDbConnection/IDbTransaction constructors (#3)
- 1.1.0 Added support for SQL distributed locks scoped to existing connections/transactions
- 1.0.1 Minor fix when using infinite timeouts
- 1.0.0 Initial release