1. 当订单系统撞上"库存超卖"的思考
某电商平台凌晨大促期间,10万用户同时抢购限量商品。开发者使用如下代码更新库存:
// 原始的危险代码(技术栈:C# + MongoDB.Driver 2.19)
var filter = Builders<Product>.Filter.Eq(p => p.Id, productId);
var update = Builders<Product>.Update.Inc(p => p.Stock, -1);
var result = await collection.UpdateOneAsync(filter, update);
第二天运营报告显示:库存实际减少量远大于备货量,超卖损失达百万级。问题根源在于多个并发请求同时读到相同库存值,导致原子操作也无法避免超卖。这揭示了MongoDB在无协调机制下的并发局限性。
2. 解剖MongoDB的并发控制机制
2.1 文档级原子性原理
MongoDB单个文档操作具备原子性,但多个文档的原子性需要事务支持。其WiredTiger存储引擎使用乐观并发控制,通过版本号(_id + version)实现冲突检测。
2.2 并发冲突的三种典型场景
场景类型 | 发生条件 | 风险等级 |
---|---|---|
更新丢失 | 多个写操作覆盖彼此结果 | ★★★★ |
脏读 | 读到未提交的中间状态 | ★★ |
不可重复读 | 同一事务内读取结果不一致 | ★★★ |
3. 五大核心解决方案深度剖析
3.1 乐观锁:版本号验证法(推荐方案)
public async Task<bool> SafeUpdateProduct(Product product)
{
// 创建带版本号的过滤器
var filter = Builders<Product>.Filter.And(
Builders<Product>.Filter.Eq(p => p.Id, product.Id),
Builders<Product>.Filter.Eq(p => p.Version, product.Version)
);
// 原子更新操作(版本号自增)
var update = Builders<Product>.Update
.Set(p => p.Price, product.Price)
.Inc(p => p.Version, 1);
try
{
var result = await _collection.UpdateOneAsync(filter, update);
return result.ModifiedCount > 0;
}
catch (MongoWriteException ex) when (ex.WriteError.Code == 11000)
{
// 版本冲突处理(需重试机制)
return false;
}
}
实现要点:
- 在文档中增加
Version
字段(int类型) - 每次更新必须验证当前版本号
- 配合重试策略处理冲突
3.2 事务控制:多文档操作的救星
// 分布式事务示例(需MongoDB 4.0+)
using (var session = await client.StartSessionAsync())
{
session.StartTransaction();
try
{
// 操作1:扣减库存
var productFilter = Builders<Product>.Filter.Eq(p => p.Id, productId);
var productUpdate = Builders<Product>.Update.Inc(p => p.Stock, -1);
await productsCollection.UpdateOneAsync(session, productFilter, productUpdate);
// 操作2:创建订单
var order = new Order { /* ... */ };
await ordersCollection.InsertOneAsync(session, order);
await session.CommitTransactionAsync();
}
catch
{
await session.AbortTransactionAsync();
throw;
}
}
事务配置要点:
var client = new MongoClient("mongodb://localhost/?replicaSet=myReplicaSet");
必须配置副本集才能启用事务功能
3.3 原子操作符:单文档操作的利器
// 保证库存不会超卖
var filter = Builders<Product>.Filter.And(
Builders<Product>.Filter.Eq(p => p.Id, productId),
Builders<Product>.Filter.Gt(p => p.Stock, 0)
);
var update = Builders<Product>.Update
.Inc(p => p.Stock, -1)
.CurrentDate(p => p.LastModified);
var options = new UpdateOptions { IsUpsert = false };
var result = await collection.UpdateOneAsync(filter, update, options);
适用场景:
- 简单数值增减操作
- 不需要复杂业务逻辑的更新
- 单文档原子性操作
3.4 重试策略:提升系统健壮性
// 使用Polly实现智能重试(需安装Polly库)
var retryPolicy = Policy
.Handle<MongoException>(ex => ex is MongoCommandException cmdEx &&
cmdEx.Code == 112) // 写冲突错误码
.WaitAndRetryAsync(new[]
{
TimeSpan.FromMilliseconds(100),
TimeSpan.FromMilliseconds(300),
TimeSpan.FromMilliseconds(500)
});
await retryPolicy.ExecuteAsync(async () =>
{
using var session = await client.StartSessionAsync();
// 事务操作...
});
3.5 文档设计优化:从根源减少冲突
// 反模式:频繁更新的大文档
public class User
{
public ObjectId Id { get; set; }
public int LoginCount { get; set; } // 高频更新字段
// 其他50个字段...
}
// 优化方案:分离高频字段
public class UserLoginStats
{
public ObjectId UserId { get; set; }
public int LoginCount { get; set; }
public DateTime LastLogin { get; set; }
}
设计原则:
- 垂直拆分高频变更字段
- 避免超大文档结构
- 使用内嵌文档控制更新范围
4. 技术方案选型指南
4.1 方案对比矩阵
方案 | 适用场景 | 吞吐量 | 复杂度 | 数据一致性 |
---|---|---|---|---|
乐观锁 | 高频单文档更新 | 高 | 中 | 最终一致 |
事务控制 | 跨文档强一致性 | 低 | 高 | 强一致 |
原子操作符 | 简单数值操作 | 极高 | 低 | 强一致 |
重试策略 | 所有可能失败的操作 | 中 | 中 | 依赖实现 |
4.2 黄金实践准则
- 优先使用原子操作符
- 单文档复杂操作用乐观锁
- 跨文档操作必须使用事务
- 所有写操作都要有重试机制
- 监控
WriteConflict
指标
5. 避坑指南:来自生产环境的教训
5.1 版本号管理陷阱
错误示范:
var product = await collection.Find(...).FirstOrDefaultAsync();
product.Version++; // 客户端修改版本号
await collection.ReplaceOneAsync(...); // 未使用原子操作
正确做法:
var update = Builders<Product>.Update
.Set(...)
.Inc(p => p.Version, 1); // 服务端原子操作
5.2 事务超时配置
var sessionOptions = new ClientSessionOptions
{
DefaultTransactionOptions = new TransactionOptions(
readPreference: ReadPreference.Primary,
readConcern: ReadConcern.Local,
writeConcern: WriteConcern.WMajority,
maxCommitTime: TimeSpan.FromSeconds(3))
};
5.3 索引设计的隐藏关联
在{ Version: 1 }
上建立唯一索引:
var keys = Builders<Product>.IndexKeys.Ascending(p => p.Version);
var options = new CreateIndexOptions { Unique = true };
await collection.Indexes.CreateOneAsync(new CreateIndexModel<Product>(keys, options));
这可以强制实现版本号的唯一性约束
6. 总结:构建高可靠的MongoDB应用
通过版本号控制、事务机制、原子操作的三位一体方案,结合合理的重试策略和文档设计,可以有效解决C#操作MongoDB时的并发问题。建议根据具体场景选择合适方案,同时关注:
- 监控MongoDB的oplog压力
- 合理设置写关注级别(Write Concern)
- 定期进行并发压力测试
- 使用性能分析工具(如MongoDB Atlas)