【问题标题】:Is it possible to use Firebase Realtime Database to implement a distributed mutex?是否可以使用 Firebase 实时数据库来实现分布式互斥锁?
【发布时间】:2018-10-29 10:35:30
【问题描述】:

我正在考虑使用这样的事务来实现一种分布式锁:

const lockId = 'myLock';
const lockRef = firebaseAdmin.database().ref(`/locks/${lockId}`);
lockRef.transaction(function(current) {
  if (current === null) {
    return '1';
  }
}, function(error, committed) {
  if (committed) {
    // .... Do the synchronized work I need ...
    lockRef.remove();
  }
});

我的问题是:只有在数据不存在的情况下才会用null调用update函数吗?

一般来说,这是实现分布式锁的有效方式吗?

【问题讨论】:

    标签: javascript firebase firebase-realtime-database transactions mutex


    【解决方案1】:

    最初会使用客户端对当前值的最佳猜测来调用事务。如果客户端在内存中没有当前值,它最好的猜测是没有当前值。

    这意味着,如果您获得null,则无法保证数据库中实际上不存在任何值。

    另见:

    【讨论】:

    • 但是committed 不应该是true 表示当前执行者“赢得”了交易并因此拥有互斥锁?所以我不明白为什么 OP 的解决方案不起作用?
    • 他们的问题是:“只有在数据不存在的情况下才会使用 null 调用更新函数?”,这就是我的回答。如果您对他们的代码有其他疑问,我建议您将其作为新问题发布。
    • 是的,但您没有回答Generally, is this a valid way to implement a distributed lock?。对此,我认为答案是肯定的,对吧?
    • ? 如果您决定发布有关该主题的新问题,请联系我。 Firebase 中的事务很有趣,但如果您是新手,就会有点不寻常。
    • 我添加了一个答案:stackoverflow.com/a/70518060/9835872
    【解决方案2】:

    由于@FrankvanPuffelen 在他们的回答中说明的原因,您的初始尝试不起作用。

    但是有可能(虽然不是那么简单)实现这一点。我与不同的边缘情况进行了很长时间的斗争,最终提出了这个解决方案,它通过了无数不同的测试,验证了这可以防止所有可能的竞争条件和死锁:

    import crypto from 'crypto';
    import { promisify } from 'util';
    
    import * as firebaseAdmin from 'firebase-admin';
    
    const randomBytes = promisify(crypto.randomBytes);
    
    // A string which is stored in the place of the value to signal that the mutex holder has
    // encountered an error. This must be unique value for each mutex so that we can distinguish old,
    // stale rejection states from the failures of the mutex that we are currently waiting for.
    const rejectionSignal = (mutexId: string): string => `rejected${mutexId}`;
    
    const isValidValue = (value: unknown): boolean => {
      // `value` could be string in the form `rejected...` which signals failure,
      // using this function makes sure we don't return that as "valid" value.
      return !!value && (typeof value !== 'string' || !value.startsWith('rejected'));
    };
    
    export const getOrSetValueWithLocking = async <T>(id: string, value: T): Promise<T> => {
      const ref = firebaseAdmin.database().ref(`/myValues/${id}`);
    
      const mutexRef = firebaseAdmin.database().ref(`/mutexes/myValues/${id}`);
    
      const attemptingMutexId = (await randomBytes(16)).toString('hex');
    
      const mutexTransaction = await mutexRef.transaction((data) => {
        if (data === null) {
          return attemptingMutexId;
        }
      });
    
      const owningMutexId = mutexTransaction.snapshot.val();
    
      if (mutexTransaction.committed) {
        // We own the mutex (meaning that `attemptingMutexId` equals `owningMutexId`).
        try {
          const existing = (await ref.once('value')).val();
          if (isValidValue(existing)) {
            return existing;
          }
          /*
            --- YOU CAN DO ANYTHING HERE ---
            E.g. create `value` here instead of passing it as an argument.
          */
          await ref.set(value);
          return value;
        } catch (e) {
          await ref.set(rejectionSignal(owningMutexId));
          throw e;
        } finally {
          // Since we own the mutex, we MUST make sure to release it, no matter what happens.
          await mutexRef.remove();
        }
      } else {
        // Some other caller owns the mutex -> if the value is not yet
        // available, wait for them to insert it or to signal a failure.
        return new Promise((resolve, reject) => {
          ref.on('value', (snapshot) => {
            const val = snapshot.val();
            if (isValidValue(val)) {
              resolve(val);
            } else if (val === rejectionSignal(owningMutexId)) {
              reject(new Error('Mutex holder encountered an error and was not able to set a value.'));
            } // else: Wait for a new value.
          });
        });
      }
    };
    

    我的用例是我在 Vercel 中运行 Next.js API 路由,其中​​并行执行的无服务器函数的唯一共享状态是 Firebase 实时数据库。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-08-25
      • 2010-12-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多