Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bf67ed3
feat: Added support for MongoDB distributed locks
joesdu Oct 21, 2025
80ff241
提高封装性并优化代码可读性
joesdu Oct 21, 2025
e06c413
fix: 为 DistributedLock.MongoDB 添加 InternalsVisibleTo
joesdu Oct 21, 2025
003ce7b
fix: ci error
joesdu Oct 22, 2025
e26e3b7
add authors
joesdu Oct 22, 2025
b2cf5aa
fix: error RS0025,移除可空引用类型指令
joesdu Oct 22, 2025
b0fe2d0
docs: 添加 DistributedLock.MongoDB 完整文档并修正 README 排版与列表缩进
joesdu Oct 22, 2025
9b4ddf4
fix: ci
joesdu Oct 22, 2025
e260979
tests: 在 CombinatorialTests.cs 中添加 MongoDB 组合测试类 Core_Mongo_MongoDbSy…
joesdu Oct 23, 2025
3a28ef0
fix: ci error?
joesdu Oct 23, 2025
cd14125
style: fix code format
joesdu Oct 23, 2025
a064e4b
Merge branch 'master' into master
joesdu Nov 9, 2025
ba277b3
chore: Performance Optimization
joesdu Dec 17, 2025
e70c35c
fix: Ensure proper cancellation token usage with ConfigureAwait(false)
joesdu Dec 17, 2025
2a7ad15
Supports Fencing Token and Adaptive Backoff, enhancing robustness
joesdu Jan 5, 2026
14b924d
remove dotnet 10 the current ci does not support.
joesdu Jan 5, 2026
55c081a
feat: update docs
joesdu Jan 5, 2026
c01cc83
fix: collection name and diagnostics
joesdu Jan 7, 2026
0cb242f
fix: change CPM condition introduction
joesdu Jan 7, 2026
8adb4b9
fix: pass all tests, change collection name to `distributed.locks`
joesdu Jan 7, 2026
91b5878
fix: 更改Mongo分布式锁默认集合名为distributed.locks
joesdu Jan 7, 2026
80c0738
Apply suggestion from @Copilot
joesdu Jan 7, 2026
b718ce4
chore: Implementation of MongoDB Distributed Lock, Document Optimizat…
joesdu Jan 9, 2026
149d4c8
Merge branch 'master' of github.com:joesdu/DistributedLock
joesdu Jan 9, 2026
1a279a8
fix: ci error
joesdu Jan 9, 2026
2f2861b
修正GetCollection模拟以匹配重载方法签名
joesdu Jan 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 76 additions & 73 deletions README.md

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions docs/Developing DistributedLock.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,167 @@

DistributedLock has a variety of back-ends; to be able to develop and run tests against all of them you'll need to install a good amount of software.

### MongoDB

You can download the MongoDB Community Server from [here](https://www.mongodb.com/try/download/community).

Alternatively, and recommended for quick testing, you can use Docker to spin up an instance without manual initialization.
Run the following command:

```bat
docker run -d -p 27017:27017 --name mongo mongo:latest
```

Or use `docker compose` to start a replica set environment:

```yaml
services:
mongo_primary:
image: bitnami/mongodb:latest
container_name: mongo_primary
environment:
- TZ=Asia/Chongqing
- MONGODB_ADVERTISED_HOSTNAME=host.docker.internal
- MONGODB_REPLICA_SET_MODE=primary
- MONGODB_REPLICA_SET_NAME=rs0
- MONGODB_ROOT_USER=yourUsername
- MONGODB_ROOT_PASSWORD=yourPassword
- MONGODB_REPLICA_SET_KEY=HxplckY2jXSwfDRE
ports:
- "27017:27017"
volumes:
- "mongodb_master_data:/bitnami/mongodb"

mongo_secondary:
image: bitnami/mongodb:latest
container_name: mongo_secondary
depends_on:
- mongo_primary
environment:
- TZ=Asia/Chongqing
- MONGODB_ADVERTISED_HOSTNAME=host.docker.internal
- MONGODB_REPLICA_SET_MODE=secondary
- MONGODB_REPLICA_SET_NAME=rs0
- MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017
- MONGODB_INITIAL_PRIMARY_HOST=host.docker.internal
- MONGODB_INITIAL_PRIMARY_ROOT_USER=yourUsername
- MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=yourPassword
- MONGODB_REPLICA_SET_KEY=HxplckY2jXSwfDRE
ports:
- "27018:27017"

mongo_arbiter:
image: bitnami/mongodb:latest
container_name: mongo_arbiter
depends_on:
- mongo_primary
environment:
- TZ=Asia/Chongqing
- MONGODB_ADVERTISED_HOSTNAME=host.docker.internal
- MONGODB_REPLICA_SET_MODE=arbiter
- MONGODB_REPLICA_SET_NAME=rs0
- MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017
- MONGODB_INITIAL_PRIMARY_HOST=host.docker.internal
- MONGODB_INITIAL_PRIMARY_ROOT_USER=yourUsername
- MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=yourPassword
- MONGODB_REPLICA_SET_KEY=HxplckY2jXSwfDRE
ports:
- "27019:27017"

volumes:
mongodb_master_data:
driver: local
```

The tests default to `mongodb://localhost:27017`. To use a custom connection string (e.g. for credentials), place it in `DistributedLock.Tests/credentials/mongodb.txt`.

If you're using a replica set or sharded cluster, your connection string might look like this:

```
mongodb://yourUsername:yourPassword@host.docker.internal:27017,host.docker.internal:27018,host.docker.internal:27019/?replicaSet=rs0&authSource=admin&serverSelectionTimeoutMS=1000
```

<details>
<summary style="font-size: 14px">中文说明</summary>

对于 MongoDB,我们可以从他的官网下载社区版本来进行测试,下载地址:[MongoDB Community Server](https://www.mongodb.com/try/download/community).
或者也可以使用 Docker 快速的启动一个测试环境.建议使用 Docker 来进行快速测试,而无需进行数据库服务初始化.以下是一个使用 Docker 启动 MongoDB 的命令:

```bat
docker run -d -p 27017:27017 --name mongo mongo:latest
```

或者使用 `docker compose` 来启动一个副本集环境:

```yaml
services:
mongo_primary:
image: bitnami/mongodb:latest
container_name: mongo_primary
environment:
- TZ=Asia/Chongqing
- MONGODB_ADVERTISED_HOSTNAME=host.docker.internal
- MONGODB_REPLICA_SET_MODE=primary
- MONGODB_REPLICA_SET_NAME=rs0
- MONGODB_ROOT_USER=yourUsername
- MONGODB_ROOT_PASSWORD=yourPassword
- MONGODB_REPLICA_SET_KEY=HxplckY2jXSwfDRE
ports:
- "27017:27017"
volumes:
- "mongodb_master_data:/bitnami/mongodb"

mongo_secondary:
image: bitnami/mongodb:latest
container_name: mongo_secondary
depends_on:
- mongo_primary
environment:
- TZ=Asia/Chongqing
- MONGODB_ADVERTISED_HOSTNAME=host.docker.internal
- MONGODB_REPLICA_SET_MODE=secondary
- MONGODB_REPLICA_SET_NAME=rs0
- MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017
- MONGODB_INITIAL_PRIMARY_HOST=host.docker.internal
- MONGODB_INITIAL_PRIMARY_ROOT_USER=yourUsername
- MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=yourPassword
- MONGODB_REPLICA_SET_KEY=HxplckY2jXSwfDRE
ports:
- "27018:27017"

mongo_arbiter:
image: bitnami/mongodb:latest
container_name: mongo_arbiter
depends_on:
- mongo_primary
environment:
- TZ=Asia/Chongqing
- MONGODB_ADVERTISED_HOSTNAME=host.docker.internal
- MONGODB_REPLICA_SET_MODE=arbiter
- MONGODB_REPLICA_SET_NAME=rs0
- MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017
- MONGODB_INITIAL_PRIMARY_HOST=host.docker.internal
- MONGODB_INITIAL_PRIMARY_ROOT_USER=yourUsername
- MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=yourPassword
- MONGODB_REPLICA_SET_KEY=HxplckY2jXSwfDRE
ports:
- "27019:27017"

volumes:
mongodb_master_data:
driver: local
```

测试项目默认连接地址为 `mongodb://localhost:27017`. 如果需要配置用户名密码或者其他连接参数, 请在 `DistributedLock.Tests/credentials/mongodb.txt` 文件中填入完整的连接字符串.

若是使用副本集或者分片集群模式,链接字符串可以填入类似如下格式:

```
mongodb://yourUsername:yourPassword@host.docker.internal:27017,host.docker.internal:27018,host.docker.internal:27019/?replicaSet=rs0&authSource=admin&serverSelectionTimeoutMS=1000
```

</details>

### Azure

For the Azure back-end, [Azurite](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite) is used for local development. See [here](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#install-azurite) for how to install.
Expand All @@ -26,6 +187,7 @@ performance_schema=ON
After doing this, restart MariaDB (on Windows, do this in the Services app).

Next, create the `distributed_lock` database and a user for the tests to run as:

```sql
CREATE DATABASE distributed_lock;
CREATE USER 'DistributedLock'@'localhost' IDENTIFIED BY '<password>';
Expand Down
82 changes: 82 additions & 0 deletions docs/DistributedLock.MongoDB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# DistributedLock.MongoDB

[Download the NuGet package](https://www.nuget.org/packages/DistributedLock.MongoDB) [![NuGet Status](http://img.shields.io/nuget/v/DistributedLock.MongoDB.svg?style=flat)](https://www.nuget.org/packages/DistributedLock.MongoDB/)

The DistributedLock.MongoDB package offers distributed locks based on [MongoDB](https://www.mongodb.com/). For example:

```C#
var client = new MongoClient("mongodb://localhost:27017");
var database = client.GetDatabase("myDatabase");
var @lock = new MongoDistributedLock("myLockName", database);
await using (await @lock.AcquireAsync())
{
// I have the lock
}
```

## APIs

- The `MongoDistributedLock` class implements the `IDistributedLock` interface.
- The `MongoDistributedSynchronizationProvider` class implements the `IDistributedLockProvider` interface.

## Implementation notes

MongoDB-based locks use MongoDB's document upsert and update operations to implement distributed locking. The implementation works as follows:

1. **Acquisition**: Attempts to insert or update a document with the lock key and a unique lock ID.
2. **Extension**: Automatically extends the lock expiry while held to prevent timeout.
3. **Release**: Deletes the lock document when disposed.
4. **Expiry**: Locks automatically expire if not extended, allowing recovery from crashed processes.

MongoDB locks can be constructed with an `IMongoDatabase` and an optional collection name. If no collection name is specified, locks will be stored in a collection named `"distributed.locks"`. The collection will automatically have an index created on the `expiresAt` field for efficient queries.

When using the provider pattern, you can create multiple locks with different names from the same provider:

```C#
var client = new MongoClient(connectionString);
var database = client.GetDatabase("myDatabase");
var provider = new MongoDistributedSynchronizationProvider(database);

var lock1 = provider.CreateLock("lock1");
var lock2 = provider.CreateLock("lock2");

await using (await lock1.AcquireAsync())
{
// Do work with lock1
}
```

**NOTE**: Lock extension happens automatically in the background while the lock is held. If lock extension fails (for example, due to network issues), the `HandleLostToken` will be signaled to notify you that the lock may have been lost.

## Options

In addition to specifying the name and database, several tuning options are available:

- `Expiry` determines how long the lock will be initially claimed for. Because of automatic extension, locks can be held for longer than this value. Defaults to 30 seconds.
- `ExtensionCadence` determines how frequently the hold on the lock will be renewed to the full `Expiry`. Defaults to 1/3 of `Expiry` (approximately 10 seconds when using the default expiry).
- `BusyWaitSleepTime` specifies a range of times that the implementation will sleep between attempts to acquire a lock that is currently held by someone else. A random time in the range will be chosen for each sleep. If you expect contention, lowering these values may increase responsiveness (how quickly a lock detects that it can now be taken) but will increase the number of calls made to MongoDB. Raising the values will have the reverse effects. Defaults to a range of 10ms to 800ms.

Example of using options:

```C#
var @lock = new MongoDistributedLock(
"MyLockName",
database,
options => options
.Expiry(TimeSpan.FromSeconds(30))
.ExtensionCadence(TimeSpan.FromSeconds(10))
.BusyWaitSleepTime(
min: TimeSpan.FromMilliseconds(10),
max: TimeSpan.FromMilliseconds(800))
);
```

You can also specify a custom collection name:

```C#
var @lock = new MongoDistributedLock("MyLockName", database, "MyCustomLocks");
```

## Stale lock cleanup

Stale locks from crashed processes will automatically expire based on the `Expiry` setting. MongoDB's built-in TTL index support ensures that expired lock documents are cleaned up automatically by the database. This means that if a process crashes while holding a lock, the lock will become available again after the expiry time has elapsed.
2 changes: 2 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.19.1" />
<PackageVersion Include="MongoDB.Driver" Version="3.5.2" />
<PackageVersion Include="Nullable" Version="1.3.1" Condition="'$(TargetFramework)' != 'netstandard2.1'" />
<PackageVersion Include="MySqlConnector" Version="2.3.5" />
<PackageVersion Include="NUnit.Analyzers" Version="4.1.0" />
Expand All @@ -21,6 +22,7 @@
<PackageVersion Include="MedallionShell.StrongName" Version="1.6.2" />
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
<PackageVersion Include="Moq" Version="4.20.70" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.1" Condition="'$(TargetFramework)' == 'netstandard2.1' OR '$(TargetFramework)' == 'net472'" />
<PackageVersion Include="System.Threading.AccessControl" Version="8.0.0" Condition="'$(TargetFramework)' != 'net462'" />
<PackageVersion Include="ZooKeeperNetEx" Version="3.4.12.4" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
Expand Down
32 changes: 14 additions & 18 deletions src/DistributedLock.Azure/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -470,15 +470,6 @@
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "6.0.1",
"contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==",
"dependencies": {
"System.Memory": "4.5.4",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.IO.Hashing": {
"type": "Transitive",
"resolved": "6.0.0",
Expand All @@ -490,13 +481,8 @@
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.4",
"contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.4.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
"resolved": "4.6.3",
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A=="
},
"System.Memory.Data": {
"type": "Transitive",
Expand All @@ -514,8 +500,8 @@
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
"resolved": "6.1.2",
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
Expand Down Expand Up @@ -546,6 +532,16 @@
},
"distributedlock.core": {
"type": "Project"
},
"System.Diagnostics.DiagnosticSource": {
"type": "CentralTransitive",
"requested": "[10.0.1, )",
"resolved": "10.0.1",
"contentHash": "wVYO4/71Pk177uQ3TG8ZQFS3Pnmr98cF9pYxnpuIb/bMnbEWsdZZoLU/euv29mfSi2/Iuypj0TRUchPk7aqBGg==",
"dependencies": {
"System.Memory": "4.6.3",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/DistributedLock.Core/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
[assembly: InternalsVisibleTo("DistributedLock.ZooKeeper, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.MySql, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.Oracle, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.MongoDB, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
#endif
Loading