Как FoundationDB обрабатывает конфликтующие транзакции?

Мне интересно, как FoundationDB обрабатывает ситуацию, в которой несколько транзакций пытаются обновить один и тот же ключ?

Если один клиент выполняет эту транзакцию:

db.run((Transaction tr) -> {
  tr.set(Tuple.from("key").pack(), Tuple.from("valueA").pack());
  return null;
});

В то время как другой клиент выполняет конфликтующую транзакцию:

db.run((Transaction tr) -> {
  tr.set(Tuple.from("key").pack(), Tuple.from("valueB").pack());
  return null;
});

Что будет происходить внутри FoundationDB для разрешения этого конфликта?

1 ответ

Решение

Недавно я изучал и тестировал FoundationDB (полагаю, что все сейчас играют с ним), и в рамках своих исследований я провел несколько простых тестов. Один из них должен ответить на ваши вопросы:

Ниже приведен пример (надеюсь, вы не против Scala):

import com.apple.foundationdb._
import com.apple.foundationdb.tuple._
import resource.managed

import scala.collection.mutable
import scala.util.Random

object Example {

  val THREAD_COUNT = 1

  @volatile var v0: Long = 0
  @volatile var v1: Long = 0
  @volatile var v2: Long = 0
  @volatile var v3: Long = 0
  @volatile var v4: Long = 0

  def doJob(db: Database, x: Int): Unit = {
    db.run((tr) => {
      val key = Tuple.from("OBJ", Long.box(100)).pack()

      val current = Tuple.fromBytes(tr.get(key).join())
      if (Random.nextInt(100) < 2) {
        out(current)
      }

      val next = mutable.ArrayBuffer(current.getLong(0), current.getLong(1), current.getLong(2), current.getLong(3), current.getLong(4))

      if (x == 1 && v1 == next(1)) { println(s"again: $v1, v0=$v0, 0=${next(0)}")}
      if (x == 0 && v0 > next(0)) { out(current); ??? } else { v0 = next(0)}
      if (x == 1 && v1 > next(1)) { out(current); ??? } else { v1 = next(1)}
      if (x == 2 && v2 > next(2)) { out(current); ??? } else { v2 = next(2)}
      if (x == 3 && v3 > next(3)) { out(current); ??? } else { v3 = next(3)}
      if (x == 4 && v4 > next(4)) { out(current); ??? } else { v4 = next(4)}

      next.update(x, next(x) + 1)
      val nv = Tuple.from(next.map(v => Long.box(v)) :_*)

      tr.set(key, nv.pack())
    })

  }

  def main(args: Array[String]): Unit = {
    if (THREAD_COUNT > 5) {
      throw new IllegalArgumentException("")
    }

    val fdb: FDB = FDB.selectAPIVersion(510)
    for (db <- managed(fdb.open())) {
      // Run an operation on the database
      db.run((tr) => {
        for (x <- 0 to 10000) {
          val k = Tuple.from(s"OBJ", x.toLong.underlying()).pack()
          val v = Tuple.from(Long.box(0), Long.box(0), Long.box(0), Long.box(0), Long.box(0)).pack()
          tr.set(k, v)
          null
        }
      })


      val threads = (0 to THREAD_COUNT).map { x =>
        new Thread(new Runnable {
          override def run(): Unit = {
            while (true) {
              try {
                doJob(db, x)
              } catch {
                case t: Throwable =>
                  t.printStackTrace()
              }
            }
          }
        })
      }

      threads.foreach(_.start())
      threads.foreach(_.join())


    }
  }

  private def out(current: Tuple) = {
    println("===")
    println((v0, v1, v2, v3, v4))
    println((Thread.currentThread().getId, current))
  }
}

Итак, эта штука позволяет вам начать запись нескольких потоков в один и тот же объект. Есть некоторый ненужный код, оставленный от других экспериментов, игнорируйте его (или используйте для своих собственных экспериментов).

Этот код порождает ваши потоки, затем каждый поток читает кортеж из пяти long, например (0,1,0,0,0) от ключа ("OBJ", 100), затем увеличивает значение, соответствующее номеру потока, затем записывает его обратно и увеличивает один из энергозависимых счетчиков.

И это мои наблюдения:

  1. Когда вы запустите этот пример, настроенный с одним потоком, вы увидите, что он пишет очень быстро,
  2. Когда вы увеличите параллелизм, вы заметите, что ваши записи замедляются (ожидается)...
  3. ... И вы увидите, что этот код выполняется время от времени: println(s"again: $v1, v0=$v0, 0=${next(0)}")

Таким образом, по существу, когда возникает конфликт, клиенты FoundationDB пытаются зафиксировать транзакции, пока они не будут выполнены успешно. Вы можете найти более подробную информацию в этой главе документов. Затем посмотрите на обзорную диаграмму архитектуры.

Также не то, что ваши транзакции являются просто функциями. Надеюсь - идемпотентные функции.

И вы должны знать, что во многих случаях вы можете избежать конфликтов, используя атомарные операции для вашей ценности.

Надеюсь, что это ответ на ваш вопрос.

Я бы посоветовал вам прочитать все официальные документы, чтобы вы могли найти там много интересного, включая то, как разработчики баз данных рассматривают теорему CAP, хорошие примеры классных распределенных структур данных и много других технических деталей и интересных вещей.

Другие вопросы по тегам