一、为什么需要分布式锁

在分布式系统中,多个服务实例可能同时操作共享资源,比如修改数据库的同一条记录。如果没有并发控制,就可能出现数据不一致的问题。举个常见的例子:电商系统中的库存扣减。如果两个用户同时下单购买最后一件商品,系统可能会错误地认为库存充足,导致超卖。

这时候就需要分布式锁来协调多个节点的操作,确保同一时间只有一个服务实例能执行关键操作。传统单机锁(比如Java的synchronized)在分布式环境下会失效,因为锁状态无法跨进程共享。

二、MongoDB实现分布式锁的核心思路

MongoDB实现分布式锁主要依赖三个特性:

  1. 原子操作:使用findAndModify或事务保证操作的原子性
  2. TTL索引:通过自动过期机制防止死锁
  3. 唯一索引:确保锁的唯一性

基本流程是这样的:

  1. 尝试向特定集合插入一条包含唯一标识的文档(相当于获取锁)
  2. 如果插入成功就获得锁
  3. 操作完成后删除文档(释放锁)
  4. 如果插入失败则等待或重试

三、完整实现示例(基于Node.js)

下面是一个可直接在生产环境使用的实现(需要MongoDB 4.0+支持事务):

const { MongoClient } = require('mongodb');

class MongoDBLock {
  /**
   * 构造函数
   * @param {string} mongoUri - MongoDB连接字符串
   * @param {string} dbName - 数据库名
   * @param {string} collectionName - 集合名(默认'locks')
   * @param {number} ttlSeconds - 锁自动释放时间(秒)
   */
  constructor(mongoUri, dbName, collectionName = 'locks', ttlSeconds = 30) {
    this.client = new MongoClient(mongoUri);
    this.dbName = dbName;
    this.collectionName = collectionName;
    this.ttlSeconds = ttlSeconds;
    this.lockCollection = null;
  }

  // 初始化集合(创建TTL索引)
  async init() {
    await this.client.connect();
    const db = this.client.db(this.dbName);
    this.lockCollection = db.collection(this.collectionName);
    
    // 创建TTL索引(自动删除过期文档)
    await this.lockCollection.createIndex(
      { createdAt: 1 },
      { expireAfterSeconds: this.ttlSeconds }
    );
    
    // 创建唯一索引确保资源唯一性
    await this.lockCollection.createIndex(
      { resourceId: 1 },
      { unique: true }
    );
  }

  /**
   * 尝试获取锁
   * @param {string} resourceId - 要锁定的资源ID
   * @param {number} retryDelay - 重试间隔(毫秒)
   * @param {number} maxRetry - 最大重试次数
   * @returns {Promise<boolean>} - 是否成功获取锁
   */
  async acquire(resourceId, retryDelay = 100, maxRetry = 10) {
    for (let i = 0; i < maxRetry; i++) {
      const session = this.client.startSession();
      try {
        session.startTransaction();
        
        // 尝试插入锁文档
        const result = await this.lockCollection.insertOne(
          {
            resourceId,
            createdAt: new Date(),
            clientId: this.generateClientId()
          },
          { session }
        );
        
        await session.commitTransaction();
        return true;
      } catch (err) {
        await session.abortTransaction();
        if (err.code === 11000) { // 唯一键冲突(锁已被占用)
          await new Promise(resolve => setTimeout(resolve, retryDelay));
          continue;
        }
        throw err;
      } finally {
        session.endSession();
      }
    }
    return false;
  }

  /**
   * 释放锁
   * @param {string} resourceId - 要释放的资源ID
   */
  async release(resourceId) {
    await this.lockCollection.deleteOne({ resourceId });
  }

  // 生成客户端唯一标识
  generateClientId() {
    return `${process.pid}-${Date.now()}-${Math.random().toString(36).substr(2, 8)}`;
  }
}

// 使用示例
(async () => {
  const lock = new MongoDBLock('mongodb://localhost:27017', 'testDB');
  await lock.init();
  
  const resourceId = 'order_12345';
  const acquired = await lock.acquire(resourceId);
  
  if (acquired) {
    try {
      console.log('成功获取锁,执行业务逻辑...');
      // 这里执行需要加锁的操作
      await new Promise(resolve => setTimeout(resolve, 2000));
    } finally {
      await lock.release(resourceId);
      console.log('锁已释放');
    }
  } else {
    console.log('获取锁失败');
  }
  
  await lock.client.close();
})();

四、关键技术细节分析

1. 事务保障原子性

示例中使用了MongoDB事务,确保插入操作和后续业务操作要么全部成功,要么全部回滚。这是实现可靠锁的关键。

2. 自动过期机制

通过TTL索引,即使客户端崩溃没有正常释放锁,系统也会自动清理过期锁,避免死锁情况。这个时间需要根据业务操作的最长时间合理设置。

3. 客户端标识

每个锁文档包含唯一的clientId,这样可以实现更复杂的锁机制,比如:

  • 可重入锁(同一客户端可重复获取)
  • 锁续期机制(心跳保持)

五、与其他方案的对比

对比Redis分布式锁

特性 MongoDB实现 Redis实现
持久化 原生支持 需配置
原子性保障 事务支持 Lua脚本
自动释放 TTL索引 EXPIRE
监控能力 查询集合即可 需要额外实现

对比Zookeeper分布式锁

MongoDB方案更适合已经使用MongoDB作为主要存储的系统,避免引入新的中间件。Zookeeper的临时节点虽然更优雅,但增加了架构复杂度。

六、生产环境注意事项

  1. 时钟同步问题
    各节点的系统时间必须同步,否则TTL机制会失效。建议部署NTP服务。

  2. 连接池配置
    MongoDB连接需要合理配置池大小,避免锁竞争导致连接耗尽。

  3. 锁粒度控制
    过细的锁粒度会增加MongoDB压力,过粗则降低并发度。需要根据业务特点平衡。

  4. 监控告警
    建议监控锁集合的文档数量,异常增长可能表明有未释放的锁。

七、适用场景推荐

这种方案特别适合以下场景:

  1. 已经使用MongoDB作为主要数据库的系统
  2. 需要中等并发控制(非极端高并发)
  3. 希望避免引入Redis等额外组件
  4. 需要可视化监控锁状态(直接查集合即可)

不适用场景:

  1. 超高并发(每秒万次以上锁操作)
  2. 对延迟极其敏感(MongoDB网络IO有开销)
  3. 需要复杂的锁语义(如读写锁)

八、总结

MongoDB实现分布式锁是一个实用且可靠的方案,特别适合MongoDB技术栈的项目。它平衡了实现的复杂度和功能完整性,通过合理利用MongoDB的特性,可以满足大多数分布式锁的需求。关键是要理解事务、TTL和唯一索引这三个核心机制,并根据业务特点调整参数。