סקירה כללית על עסקאות

בדף הזה מתוארות עסקאות ב-Spanner ומוצגים ממשקי העסקאות של Spanner ל-DML עם הרשאות קריאה וכתיבה, קריאה בלבד וחלוקה למחיצות.

טרנזקציה ב-Spanner היא קבוצה של פעולות קריאה וכתיבה. כל הפעולות בעסקה הן אטומיות, כלומר כולן מצליחות או כולן נכשלות.

סשן משמש לביצוע טרנזקציות במסד נתונים של Spanner. סשן מייצג ערוץ תקשורת לוגי עם שירות מסד הנתונים של Spanner. במהלך ביקורים אפשר לבצע עסקה אחת או כמה עסקאות בו-זמנית. מידע נוסף זמין במאמר בנושא סשנים.

סוגי עסקאות

‫Spanner תומך בסוגי העסקאות הבאים, שכל אחד מהם מיועד לדפוסי אינטראקציה ספציפיים עם נתונים:

  • קריאה-כתיבה: העסקאות האלה משמשות לפעולות קריאה וכתיבה, ואחריהן מתבצעת פעולת אישור (commit). יכול להיות שהם ירכשו מנעולים. אם הן ייכשלו, יהיה צורך לנסות שוב. הם מוגבלים למסד נתונים יחיד, אבל יכולים לשנות נתונים בכמה טבלאות במסד הנתונים הזה.

  • קריאה בלבד: העסקאות האלה מבטיחות עקביות של הנתונים בכמה פעולות קריאה, אבל לא מאפשרות שינויים בנתונים. הן מופעלות בחותמת זמן שנקבעת על ידי המערכת כדי לשמור על עקביות, או בחותמת זמן מהעבר שהמשתמש הגדיר. בניגוד לעסקאות קריאה-כתיבה, הן לא דורשות פעולת אישור (commit) או נעילות. עם זאת, יכול להיות שהם יושהו כדי להמתין לסיום של פעולות כתיבה שמתבצעות כרגע.

  • חלוקת DML: סוג העסקה הזה מריץ פקודות DML כפעולות של חלוקת DML. היא מותאמת להרצת הצהרות DML בהיקף גדול, אבל עם הגבלות שמבטיחות שההצהרה היא אידמפוטנטית וניתנת לחלוקה באופן שמאפשר לה להריץ באופן עצמאי ממחיצות אחרות. אם אתם צריכים לבצע הרבה פעולות כתיבה שלא מחייבות טרנזקציה אטומית, כדאי להשתמש בכתיבה באצווה. מידע נוסף מופיע במאמר בנושא שינוי נתונים באמצעות כתיבה של קבוצות.

עסקאות קריאה-כתיבה עם נעילה רק כשנדרש.

עסקאות קריאה-כתיבה

עסקת קריאה-כתיבה מורכבת מאפס או יותר קריאות או הצהרות של שאילתות שאחריהן בקשת ביצוע (commit). בכל שלב לפני בקשת השמירה, הלקוח יכול לשלוח בקשת ביטול כדי להפסיק את הטרנזקציה.

בידוד ניתן לסריאליזציה

בשימוש ברמת הבידוד הסדרתית שמוגדרת כברירת מחדל עם מקביליות פסימית, טרנזקציות של קריאה וכתיבה קוראות, משנות וכותבות נתונים באופן אטומי. סוג העסקה הזה הוא עקבי חיצונית.

כשמשתמשים בעסקאות קריאה-כתיבה, מומלץ לצמצם את משך הזמן שבו העסקה פעילה. משך עסקאות קצר יותר גורם לכך שהנעילות מוחזקות למשך זמן קצר יותר, מה שמגדיל את הסבירות לביצוע מוצלח של פעולת commit ומפחית את המחלוקות. הסיבה לכך היא שנעלים שנשמרים למשך זמן רב עלולים להוביל לקיפאון ולביטול עסקאות. מערכת Spanner מנסה לשמור על נעילות קריאה פעילות כל עוד העסקאות ממשיכות לבצע קריאות, והעסקאות לא הסתיימו באמצעות אישור או ביטול. אם הלקוח נשאר לא פעיל למשך תקופות ארוכות, יכול להיות ש-Spanner ישחרר את הנעילות של העסקה ויבטל אותה.

כדי לבצע פעולת כתיבה שתלויה בפעולת קריאה אחת או יותר, צריך להשתמש בעסקת קריאה-כתיבה:

  • אם אתם צריכים לבצע פעולות כתיבה אחת או יותר באופן אטומי, אתם צריכים לבצע את פעולות הכתיבה האלה בתוך אותה טרנזקציית קריאה-כתיבה. לדוגמה, אם אתם מעבירים 200$ מחשבון א' לחשבון ב', אתם מבצעים את שתי פעולות הכתיבה (הפחתה של 200 $מחשבון א' והגדלה של 200 $בחשבון ב') ואת פעולות הקריאה של יתרות החשבון הראשוניות באותה עסקה.
  • אם רוצים להכפיל את היתרה בחשבון א', צריך לבצע את פעולות הקריאה והכתיבה באותה טרנזקציה. כך המערכת קוראת את היתרה לפני שהיא מכפילה אותה ומעדכנת אותה.
  • אם פעולות הכתיבה תלויות בפעולות הקריאה, צריך לבצע את שתיהן באותה טרנזקציה של קריאה-כתיבה, גם אם פעולות הכתיבה לא מבוצעות. לדוגמה, אם רוצים להעביר 200 $מחשבון א' לחשבון ב' רק אם היתרה בחשבון א' גדולה מ-500$, צריך לכלול את הקריאה של היתרה בחשבון א' ואת פעולות הכתיבה המותנות באותה עסקה, גם אם ההעברה לא מתבצעת.

כדי לבצע פעולות קריאה, צריך להשתמש בשיטת קריאה יחידה או בעסקה לקריאה בלבד:

  • אם אתם מבצעים רק פעולות קריאה, ואתם יכולים לבטא את פעולת הקריאה באמצעות שיטת קריאה יחידה, השתמשו בשיטת הקריאה היחידה או בעסקה לקריאה בלבד. בניגוד לעסקאות של קריאה וכתיבה, קריאות בודדות לא מקבלות נעילות.

אפשר גם להגדיר בידוד ניתן לסריאליזציה כדי להשתמש במקביליות אופטימית. בקרת בו-זמניות אופטימית מניחה שהתנגשויות הן נדירות. פעולות קריאה ושאילתות גם בתוך טרנזקציה של קריאה וכתיבה, מתבצעות בלי נעילת רשומות. בבידוד ניתן לסדר, קריאות מאומתות בזמן השמירה. כך מוודאים שאף עסקה אחרת שבוצעה במקביל לא שינתה את הנתונים שהעסקה קראה קודם.

בידוד קריאה שניתן לחזור עליו

ב-Spanner, בידוד קריאה חוזרת מיושם באמצעות טכניקה שנקראת בידוד תמונת מצב. בידוד של קריאה חוזרת מבטיח שכל פעולות הקריאה בתוך טרנזקציה יהיו עקביות עם מסד הנתונים כפי שהיה בתחילת הטרנזקציה. היא גם מבטיחה שפעולות כתיבה בו-זמניות לאותם נתונים יצליחו רק אם אין התנגשויות.

בשיטה האופטימית של עבודה במקביל, שהיא שיטת ברירת המחדל, פעולות הכתיבה לא מקבלות נעילות עד לזמן השמירה. אם יש סתירה בין הנתונים שנכתבו או בגלל אירועים זמניים ב-Spanner, כמו הפעלה מחדש של שרת, יכול להיות ש-Spanner עדיין יבטל את העסקאות. מכיוון שפעולות קריאה בטרנזקציות עם הרשאת קריאה וכתיבה לא מקבלות נעילות בבידוד קריאה חוזרת, אין הבדל בין ביצוע פעולות קריאה בלבד בטרנזקציה עם הרשאת קריאה בלבד לבין ביצוע פעולות קריאה בלבד בטרנזקציה עם הרשאת קריאה וכתיבה.

משתמשים במקביליות פסימית עם קריאה חוזרת כדי לקבל נעילות בלעדיות לקריאת נתונים באמצעות פסוקית FOR UPDATE או רמז lock_scanned_ranges=exclusive, ולכתיבת נתונים באמצעות שאילתות DML. הסמנטיקה הזו דומה יותר להתנהגות הנעילה של בידוד קריאה חוזרת במסדי נתונים אחרים. במקבילויות פסימיות, כל הקריאות מוגשות בצילום המצב, ולכן יכול להיות שהטרנזקציות יבוטלו אם טרנזקציות מקבילות יעדכנו את הנתונים שנקראו. עם זאת, היא מונעת מעסקאות מקבילות לעדכן את הנתונים הנעולים מרגע קבלת הנעילות ועד לזמן השמירה, מה שעשוי להפחית את הסיכוי לקונפליקטים של כתיבה-כתיבה.

כדאי להשתמש בעסקאות קריאה-כתיבה בבידוד קריאה חוזרת בתרחישים הבאים:

  • עומס העבודה הוא בעיקר קריאה, ויש מעט התנגשויות בכתיבה.
  • האפליקציה סובלת מנקודות צוואר בקבוק בביצועים בגלל עיכובים שנובעים ממחלוקות על נעילה וביטולים של טרנזקציות. הביטולים נגרמים בגלל טרנזקציות ישנות יותר עם עדיפות גבוהה יותר שפוגעות בטרנזקציות חדשות יותר עם עדיפות נמוכה יותר, כדי למנוע מצבים פוטנציאליים של קיפאון (wound-wait).
  • האפליקציה לא דורשת את ההבטחות המחמירות יותר שניתנות על ידי רמת הבידוד הניתנת לסריאליזציה.

כדאי להשתמש בבידוד של קריאה חוזרת עם נעילת נתונים אופטימית לעומסי עבודה שבהם FOR UPDATE ומשפטי DML מקבלים נעילות.

כשמבצעים פעולת כתיבה שתלויה בפעולת קריאה אחת או יותר, יכול להיות שיהיה הטיה בכתיבה בבידוד קריאה חוזרת. הטיית כתיבה נובעת מסוג מסוים של עדכון מקביל, שבו כל עדכון מתקבל באופן עצמאי, אבל ההשפעה המשולבת שלהם פוגעת בתקינות נתוני האפליקציה. לכן, חשוב לוודא שפעולות קריאה שמתבצעות בחלק הקריטי של טרנזקציה כוללות את הסעיף FOR UPDATE או את הרמז lock_scanned_ranges=exclusive כדי להימנע משינוי לא עקבי של נתונים. מידע נוסף זמין במאמרים בנושא קונפליקטים של קריאה-כתיבה ונכונות ובדוגמה שמוסברת במאמר בנושא סמנטיקה של קריאה-כתיבה.

ממשק

ספריות הלקוח של Spanner מספקות ממשק להרצת קבוצת פעולות בתוך טרנזקציה של קריאה וכתיבה, עם ניסיונות חוזרים לביטול טרנזקציות. יכול להיות שיהיה צורך לנסות לבצע עסקה כמה פעמים לפני שהיא תאושר.

יש כמה מצבים שבהם העסקה עלולה להיכשל. לדוגמה, אם שתי טרנזקציות מנסות לשנות נתונים בו-זמנית, יכול להיות שייווצר מצב של חסימה הדדית. במקרים כאלה, Spanner מבטל עסקה אחת כדי לאפשר לעסקה השנייה להתבצע. לעיתים רחוקות יותר, אירועים חולפים ב-Spanner יכולים גם לגרום לביטול עסקאות.

כל העסקאות לקריאה ולכתיבה מספקות את מאפייני ה-ACID של מסדי נתונים רלציוניים. מכיוון שעסקאות הן אטומיות, ביטול של עסקה לא משפיע על מסד הנתונים. ספריות הלקוח של Spanner מנסות לבצע שוב עסקאות כאלה באופן אוטומטי, אבל אם אתם לא משתמשים בספריות הלקוח, כדאי לנסות לבצע שוב את העסקה באותה סשן כדי לשפר את שיעורי ההצלחה. כל ניסיון חוזר שמסתיים בשגיאה ABORTED מגדיל את עדיפות הנעילה של הטרנזקציה. בנוסף, מנהלי ההתקנים של לקוח Spanner כוללים לוגיקה פנימית לניסיון חוזר של עסקאות, שמסתירה שגיאות זמניות על ידי הפעלה מחדש של העסקה.

כשמשתמשים בעסקה בספריית לקוח של Spanner, מגדירים את גוף העסקה כאובייקט פונקציה. הפונקציה הזו מכילה את פעולות הקריאה והכתיבה שמתבצעות בטבלה אחת או יותר במסד הנתונים. ספריית הלקוח של Spanner מפעילה את הפונקציה הזו שוב ושוב עד שהטרנזקציה מתבצעת בהצלחה או שמתרחשת שגיאה שלא ניתן לנסות שוב.

דוגמה

נניח שיש לכם עמודה בשם MarketingBudget בטבלה Albums:

CREATE TABLE Albums (
  SingerId        INT64 NOT NULL,
  AlbumId         INT64 NOT NULL,
  AlbumTitle      STRING(MAX),
  MarketingBudget INT64
) PRIMARY KEY (SingerId, AlbumId);

מחלקת השיווק מבקשת להעביר 200,000 $מהתקציב של Albums (2, 2) אל Albums (1, 1), אבל רק אם הסכום הזה זמין בתקציב של האלבום. כדאי להשתמש בטרנזקציה של קריאה וכתיבה עם נעילה לפעולה הזו, כי הטרנזקציה עשויה לבצע כתיבות בהתאם לתוצאה של קריאה.

בדוגמאות הבאות של ספריות לקוח מוצג איך להריץ טרנזקציה של קריאה וכתיבה באמצעות רמת הבידוד הסדרתית שמוגדרת כברירת מחדל:

C++‎

void ReadWriteTransaction(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  using ::google::cloud::Status;
  using ::google::cloud::StatusCode;
  using ::google::cloud::StatusOr;

  // A helper to read a single album MarketingBudget.
  auto get_current_budget =
      [](spanner::Client client, spanner::Transaction txn,
         std::int64_t singer_id,
         std::int64_t album_id) -> StatusOr<std::int64_t> {
    auto key = spanner::KeySet().AddKey(spanner::MakeKey(singer_id, album_id));
    auto rows = client.Read(std::move(txn), "Albums", std::move(key),
                            {"MarketingBudget"});
    using RowType = std::tuple<std::int64_t>;
    auto row = spanner::GetSingularRow(spanner::StreamOf<RowType>(rows));
    if (!row) return std::move(row).status();
    return std::get<0>(*std::move(row));
  };

  auto constexpr kInsufficientFundsMessage =
      "The second album does not have enough funds to transfer";
  auto commit = client.Commit(
      [&](spanner::Transaction const& txn) -> StatusOr<spanner::Mutations> {
        auto b1 = get_current_budget(client, txn, 1, 1);
        if (!b1) return std::move(b1).status();
        auto b2 = get_current_budget(client, txn, 2, 2);
        if (!b2) return std::move(b2).status();
        std::int64_t transfer_amount = 200000;

        if (*b2 < transfer_amount) {
          return Status(StatusCode::kFailedPrecondition,
                        kInsufficientFundsMessage);
        }

        return spanner::Mutations{
            spanner::UpdateMutationBuilder(
                "Albums", {"SingerId", "AlbumId", "MarketingBudget"})
                .EmplaceRow(1, 1, *b1 + transfer_amount)
                .EmplaceRow(2, 2, *b2 - transfer_amount)
                .Build()};
      });

  if (!commit) throw std::move(commit).status();
  std::cout << "Transfer was successful [spanner_read_write_transaction]\n";
}

C#‎


using Google.Cloud.Spanner.Data;
using System;
using System.Threading.Tasks;
using System.Transactions;

public class ReadWriteWithTransactionAsyncSample
{
    public async Task<int> ReadWriteWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        // This sample transfers 200,000 from the MarketingBudget
        // field of the second Album to the first Album. Make sure to run
        // the Add Column and Write Data To New Column samples first,
        // in that order.

        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        decimal transferAmount = 200000;
        decimal secondBudget = 0;
        decimal firstBudget = 0;

        using var connection = new SpannerConnection(connectionString);
        using var cmdLookup1 = connection.CreateSelectCommand("SELECT * FROM Albums WHERE SingerId = 2 AND AlbumId = 2");

        using (var reader = await cmdLookup1.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                // Read the second album's budget.
                secondBudget = reader.GetFieldValue<decimal>("MarketingBudget");
                // Confirm second Album's budget is sufficient and
                // if not raise an exception. Raising an exception
                // will automatically roll back the transaction.
                if (secondBudget < transferAmount)
                {
                    throw new Exception($"The second album's budget {secondBudget} is less than the amount to transfer.");
                }
            }
        }

        // Read the first album's budget.
        using var cmdLookup2 = connection.CreateSelectCommand("SELECT * FROM Albums WHERE SingerId = 1 and AlbumId = 1");
        using (var reader = await cmdLookup2.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                firstBudget = reader.GetFieldValue<decimal>("MarketingBudget");
            }
        }

        // Specify update command parameters.
        using var cmdUpdate = connection.CreateUpdateCommand("Albums", new SpannerParameterCollection
        {
            { "SingerId", SpannerDbType.Int64 },
            { "AlbumId", SpannerDbType.Int64 },
            { "MarketingBudget", SpannerDbType.Int64 },
        });

        // Update second album to remove the transfer amount.
        secondBudget -= transferAmount;
        cmdUpdate.Parameters["SingerId"].Value = 2;
        cmdUpdate.Parameters["AlbumId"].Value = 2;
        cmdUpdate.Parameters["MarketingBudget"].Value = secondBudget;
        var rowCount = await cmdUpdate.ExecuteNonQueryAsync();

        // Update first album to add the transfer amount.
        firstBudget += transferAmount;
        cmdUpdate.Parameters["SingerId"].Value = 1;
        cmdUpdate.Parameters["AlbumId"].Value = 1;
        cmdUpdate.Parameters["MarketingBudget"].Value = firstBudget;
        rowCount += await cmdUpdate.ExecuteNonQueryAsync();
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return rowCount;
    }
}

המשך


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func writeWithTransaction(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		getBudget := func(key spanner.Key) (int64, error) {
			row, err := txn.ReadRow(ctx, "Albums", key, []string{"MarketingBudget"})
			if err != nil {
				return 0, err
			}
			var budget int64
			if err := row.Column(0, &budget); err != nil {
				return 0, err
			}
			return budget, nil
		}
		album2Budget, err := getBudget(spanner.Key{2, 2})
		if err != nil {
			return err
		}
		const transferAmt = 200000
		if album2Budget >= transferAmt {
			album1Budget, err := getBudget(spanner.Key{1, 1})
			if err != nil {
				return err
			}
			album1Budget += transferAmt
			album2Budget -= transferAmt
			cols := []string{"SingerId", "AlbumId", "MarketingBudget"}
			txn.BufferWrite([]*spanner.Mutation{
				spanner.Update("Albums", cols, []interface{}{1, 1, album1Budget}),
				spanner.Update("Albums", cols, []interface{}{2, 2, album2Budget}),
			})
			fmt.Fprintf(w, "Moved %d from Album2's MarketingBudget to Album1's.", transferAmt)
		}
		return nil
	})
	return err
}

Java

static void writeWithTransaction(DatabaseClient dbClient) {
  dbClient
      .readWriteTransaction()
      .run(transaction -> {
        // Transfer marketing budget from one album to another. We do it in a transaction to
        // ensure that the transfer is atomic.
        Struct row =
            transaction.readRow("Albums", Key.of(2, 2), Arrays.asList("MarketingBudget"));
        long album2Budget = row.getLong(0);
        // Transaction will only be committed if this condition still holds at the time of
        // commit. Otherwise it will be aborted and the callable will be rerun by the
        // client library.
        long transfer = 200000;
        if (album2Budget >= transfer) {
          long album1Budget =
              transaction
                  .readRow("Albums", Key.of(1, 1), Arrays.asList("MarketingBudget"))
                  .getLong(0);
          album1Budget += transfer;
          album2Budget -= transfer;
          transaction.buffer(
              Mutation.newUpdateBuilder("Albums")
                  .set("SingerId")
                  .to(1)
                  .set("AlbumId")
                  .to(1)
                  .set("MarketingBudget")
                  .to(album1Budget)
                  .build());
          transaction.buffer(
              Mutation.newUpdateBuilder("Albums")
                  .set("SingerId")
                  .to(2)
                  .set("AlbumId")
                  .to(2)
                  .set("MarketingBudget")
                  .to(album2Budget)
                  .build());
        }
        return null;
      });
}

Node.js

// This sample transfers 200,000 from the MarketingBudget field
// of the second Album to the first Album, as long as the second
// Album has enough money in its budget. Make sure to run the
// addColumn and updateData samples first (in that order).

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

const transferAmount = 200000;

// Note: the `runTransaction()` method is non blocking and returns "void".
// For sequential execution of the transaction use `runTransactionAsync()` method which returns a promise.
// For example: await database.runTransactionAsync(async (err, transaction) => { ... })
database.runTransaction(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  let firstBudget, secondBudget;
  const queryOne = {
    columns: ['MarketingBudget'],
    keys: [[2, 2]], // SingerId: 2, AlbumId: 2
  };

  const queryTwo = {
    columns: ['MarketingBudget'],
    keys: [[1, 1]], // SingerId: 1, AlbumId: 1
  };

  Promise.all([
    // Reads the second album's budget
    transaction.read('Albums', queryOne).then(results => {
      // Gets second album's budget
      const rows = results[0].map(row => row.toJSON());
      secondBudget = rows[0].MarketingBudget;
      console.log(`The second album's marketing budget: ${secondBudget}`);

      // Makes sure the second album's budget is large enough
      if (secondBudget < transferAmount) {
        throw new Error(
          `The second album's budget (${secondBudget}) is less than the transfer amount (${transferAmount}).`,
        );
      }
    }),

    // Reads the first album's budget
    transaction.read('Albums', queryTwo).then(results => {
      // Gets first album's budget
      const rows = results[0].map(row => row.toJSON());
      firstBudget = rows[0].MarketingBudget;
      console.log(`The first album's marketing budget: ${firstBudget}`);
    }),
  ])
    .then(() => {
      console.log(firstBudget, secondBudget);
      // Transfers the budgets between the albums
      firstBudget += transferAmount;
      secondBudget -= transferAmount;

      console.log(firstBudget, secondBudget);

      // Updates the database
      // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they
      // must be converted (back) to strings before being inserted as INT64s.
      transaction.update('Albums', [
        {
          SingerId: '1',
          AlbumId: '1',
          MarketingBudget: firstBudget.toString(),
        },
        {
          SingerId: '2',
          AlbumId: '2',
          MarketingBudget: secondBudget.toString(),
        },
      ]);
    })
    .then(() => {
      // Commits the transaction and send the changes to the database
      return transaction.commit();
    })
    .then(() => {
      console.log(
        `Successfully executed read-write transaction to transfer ${transferAmount} from Album 2 to Album 1.`,
      );
    })
    .catch(err => {
      console.error('ERROR:', err);
    })
    .then(() => {
      transaction.end();
      // Closes the database when finished
      return database.close();
    });
});

PHP

use Google\Cloud\Spanner\SpannerClient;
use Google\Cloud\Spanner\Transaction;
use UnexpectedValueException;

/**
 * Performs a read-write transaction to update two sample records in the
 * database.
 *
 * This will transfer 200,000 from the `MarketingBudget` field for the second
 * Album to the first Album. If the `MarketingBudget` for the second Album is
 * too low, it will raise an exception.
 *
 * Before running this sample, you will need to run the `update_data` sample
 * to populate the fields.
 * Example:
 * ```
 * read_write_transaction($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_write_transaction(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $database->runTransaction(function (Transaction $t) use ($spanner) {
        $transferAmount = 200000;

        // Read the second album's budget.
        $secondAlbumKey = [2, 2];
        $secondAlbumKeySet = $spanner->keySet(['keys' => [$secondAlbumKey]]);
        $secondAlbumResult = $t->read(
            'Albums',
            $secondAlbumKeySet,
            ['MarketingBudget'],
            ['limit' => 1]
        );

        $firstRow = $secondAlbumResult->rows()->current();
        $secondAlbumBudget = $firstRow['MarketingBudget'];
        if ($secondAlbumBudget < $transferAmount) {
            // Throwing an exception will automatically roll back the transaction.
            throw new UnexpectedValueException(
                'The second album\'s budget is lower than the transfer amount: ' . $transferAmount
            );
        }

        $firstAlbumKey = [1, 1];
        $firstAlbumKeySet = $spanner->keySet(['keys' => [$firstAlbumKey]]);
        $firstAlbumResult = $t->read(
            'Albums',
            $firstAlbumKeySet,
            ['MarketingBudget'],
            ['limit' => 1]
        );

        // Read the first album's budget.
        $firstRow = $firstAlbumResult->rows()->current();
        $firstAlbumBudget = $firstRow['MarketingBudget'];

        // Update the budgets.
        $secondAlbumBudget -= $transferAmount;
        $firstAlbumBudget += $transferAmount;
        printf('Setting first album\'s budget to %s and the second album\'s ' .
            'budget to %s.' . PHP_EOL, $firstAlbumBudget, $secondAlbumBudget);

        // Update the rows.
        $t->updateBatch('Albums', [
            ['SingerId' => 1, 'AlbumId' => 1, 'MarketingBudget' => $firstAlbumBudget],
            ['SingerId' => 2, 'AlbumId' => 2, 'MarketingBudget' => $secondAlbumBudget],
        ]);

        // Commit the transaction!
        $t->commit();

        print('Transaction complete.' . PHP_EOL);
    });
}

Python

def read_write_transaction(instance_id, database_id):
    """Performs a read-write transaction to update two sample records in the
    database.

    This will transfer 200,000 from the `MarketingBudget` field for the second
    Album to the first Album. If the `MarketingBudget` is too low, it will
    raise an exception.

    Before running this sample, you will need to run the `update_data` sample
    to populate the fields.
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    def update_albums(transaction):
        # Read the second album budget.
        second_album_keyset = spanner.KeySet(keys=[(2, 2)])
        second_album_result = transaction.read(
            table="Albums",
            columns=("MarketingBudget",),
            keyset=second_album_keyset,
            limit=1,
        )
        second_album_row = list(second_album_result)[0]
        second_album_budget = second_album_row[0]

        transfer_amount = 200000

        if second_album_budget < transfer_amount:
            # Raising an exception will automatically roll back the
            # transaction.
            raise ValueError("The second album doesn't have enough funds to transfer")

        # Read the first album's budget.
        first_album_keyset = spanner.KeySet(keys=[(1, 1)])
        first_album_result = transaction.read(
            table="Albums",
            columns=("MarketingBudget",),
            keyset=first_album_keyset,
            limit=1,
        )
        first_album_row = list(first_album_result)[0]
        first_album_budget = first_album_row[0]

        # Update the budgets.
        second_album_budget -= transfer_amount
        first_album_budget += transfer_amount
        print(
            "Setting first album's budget to {} and the second album's "
            "budget to {}.".format(first_album_budget, second_album_budget)
        )

        # Update the rows.
        transaction.update(
            table="Albums",
            columns=("SingerId", "AlbumId", "MarketingBudget"),
            values=[(1, 1, first_album_budget), (2, 2, second_album_budget)],
        )

    database.run_in_transaction(update_albums)

    print("Transaction complete.")

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner         = Google::Cloud::Spanner.new project: project_id
client          = spanner.client instance_id, database_id
transfer_amount = 200_000

client.transaction do |transaction|
  first_album  = transaction.read("Albums", [:MarketingBudget], keys: [[1, 1]]).rows.first
  second_album = transaction.read("Albums", [:MarketingBudget], keys: [[2, 2]]).rows.first

  raise "The second album does not have enough funds to transfer" if second_album[:MarketingBudget] < transfer_amount

  new_first_album_budget  = first_album[:MarketingBudget] + transfer_amount
  new_second_album_budget = second_album[:MarketingBudget] - transfer_amount

  transaction.update "Albums", [
    { SingerId: 1, AlbumId: 1, MarketingBudget: new_first_album_budget  },
    { SingerId: 2, AlbumId: 2, MarketingBudget: new_second_album_budget }
  ]
end

puts "Transaction complete"

דוגמאות לאופן הביצוע של טרנזקציית קריאה-כתיבה באמצעות בידוד קריאה חוזרת מופיעות במאמר שימוש ברמת בידוד קריאה חוזרת.

סמנטיקה

בקטע הזה מוסבר על הסמנטיקה של עסקאות קריאה-כתיבה ב-Spanner.

מאפיינים

בידוד ניתן לסדרות הוא רמת הבידוד שמוגדרת כברירת מחדל ב-Spanner. במסגרת בידוד ניתן לסדר את העסקאות בסדר מסוים, ו-Spanner מספק ללקוחות את ההבטחות המחמירות ביותר לגבי בקרת מקביליות של עסקאות, כלומר עקביות חיצונית. בעסקת קריאה-כתיבה מבוצעת קבוצה של קריאות וכתיבות באופן אטומי. פעולות כתיבה יכולות להמשיך בלי להיחסם על ידי עסקאות לקריאה בלבד. חותמת הזמן שבה מתבצעות טרנזקציות של קריאה וכתיבה תואמת לזמן שחלף. סדר הסריאליזציה תואם לסדר חותמות הזמן.

בגלל המאפיינים האלה, מפתחי אפליקציות יכולים להתמקד בנכונות של כל עסקה בנפרד, בלי לדאוג לגבי האופן שבו אפשר להגן על הביצוע שלה מפני עסקאות אחרות שעשויות להתבצע באותו הזמן.

אפשר גם להריץ את עסקאות הקריאה והכתיבה באמצעות בידוד קריאה חוזרת. בידוד של קריאה חוזרת מבטיח שכל פעולות הקריאה בתוך טרנזקציה יראו תמונה עקבית וחזקה של מסד הנתונים כפי שהיה בתחילת הטרנזקציה. מידע נוסף זמין במאמר בנושא בידוד של קריאה חוזרת.

עסקאות קריאה-כתיבה עם בידוד ניתן לסריאליזציה

אחרי שמאשרים בהצלחה עסקה שמכילה סדרה של פעולות קריאה וכתיבה בבידוד ברירת המחדל שניתן לסדר, מתרחש הדבר הבא:

  • העסקה מחזירה ערכים שמשקפים תמונת מצב עקבית בחותמת הזמן של אישור העסקה.
  • שורות או טווחים ריקים נשארים ריקים בזמן השמירה.
  • כל פעולות הכתיבה בעסקה מתבצעות בחותמת הזמן של אישור העסקה.
  • אף עסקה לא יכולה לראות את הפעולות עד שהעסקה מתבצעת.

מנהלי התקנים של לקוח Spanner כוללים לוגיקה של ניסיון חוזר של עסקאות שמסתירה שגיאות זמניות על ידי הפעלה מחדש של העסקה ואימות הנתונים שהלקוח רואה.

התוצאה היא שכל פעולות הקריאה והכתיבה נראות כאילו הן התרחשו בנקודת זמן אחת, גם מנקודת המבט של העסקה עצמה וגם מנקודת המבט של קוראים וכותבים אחרים במסד הנתונים של Spanner. כלומר, פעולות הקריאה והכתיבה מתרחשות באותה חותמת זמן. לדוגמה, ראו Serializability and external consistency.

עסקאות עם הרשאות קריאה וכתיבה עם בידוד קריאה חוזרת

אחרי שמאשרים עסקה עם בידוד קריאה חוזרת, מתקיימים התנאים הבאים:

  • הטרנזקציה מחזירה ערכים שמשקפים תמונת מצב עקבית של מסד הנתונים. תמונת המצב נוצרת בדרך כלל במהלך פעולת הטרנזקציה הראשונה, שיכול להיות שהיא לא זהה לחותמת הזמן של השמירה.
  • מכיוון שהבידוד של תמונת המצב משמש להטמעה של קריאה חוזרת, העסקה מבצעת את כל הפעולות של כתיבה רק בחותמת הזמן של אישור העסקה, אם קבוצת הכתיבה לא השתנתה בין חותמת הזמן של תמונת המצב של העסקה לבין חותמת הזמן של אישור העסקה.
  • עסקאות אחרות לא רואות את הפעולות עד שהעסקה מתבצעת.

בידוד של טרנזקציות לקריאה וכתיבה עם פעולות לקריאה בלבד

כשעסקה עם הרשאת קריאה וכתיבה מבצעת רק פעולות קריאה, היא מספקת ערבויות עקביות דומות לעסקה עם הרשאת קריאה בלבד. כל הקריאות בתוך העסקה מחזירות נתונים מחותמת זמן עקבית, כולל אישור של שורות שלא קיימות.

הבדל אחד הוא המצב שבו מתבצעת פעולת אישור של טרנזקציה עם הרשאות קריאה וכתיבה בלי לבצע פעולת כתיבה. בתרחיש הזה, אין ערובה לכך שהנתונים שנקראו במסגרת הטרנזקציה נשארו ללא שינוי במסד הנתונים בין פעולת הקריאה לבין שמירת הטרנזקציה.

כדי לוודא שהנתונים עדכניים ושהם לא השתנו מאז האחזור האחרון שלהם, צריך לבצע קריאה נוספת. אפשר לבצע את הקריאה מחדש במסגרת טרנזקציה אחרת של קריאה וכתיבה, או באמצעות קריאה חזקה.

כדי להשיג יעילות אופטימלית, אם טרנזקציה מבצעת קריאות בלבד, כדאי להשתמש בטרנזקציה לקריאה בלבד במקום בטרנזקציה לקריאה ולכתיבה, במיוחד כשמשתמשים בבידוד ניתן לסדר.

מה ההבדל בין סריאליזציה ועקביות חיצונית לבין קריאה חוזרת

כברירת מחדל, Spanner מציע ערבויות חזקות לעסקאות, כולל אפשרות לסדר את העסקאות בסדר מסוים ועקביות חיצונית. המאפיינים האלה מבטיחים שהנתונים יישארו עקביים והפעולות יתבצעו בסדר צפוי, גם בסביבה מבוזרת.

הסדרות מבטיחה שכל העסקאות ייראו כאילו הן מתבצעות אחת אחרי השנייה בסדר רציף, גם אם הן מעובדות במקביל. ב-Spanner, כדי להשיג את זה, מוקצים חותמות זמן של ביצוע (commit) לעסקאות, שמשקפות את הסדר שבו הן בוצעו.

‫Spanner מספק ערבות חזקה עוד יותר שנקראת עקביות חיצונית. המשמעות היא שלא רק שהטרנזקציות מתבצעות בסדר שמשתקף בחותמות הזמן שלהן, אלא שחותמות הזמן האלה גם תואמות לזמן בעולם האמיתי. כך תוכלו להשוות בין חותמות הזמן של השליחות לבין הזמן האמיתי, ולקבל תצוגה עקבית ומסודרת של הנתונים שלכם.

במילים אחרות, אם טרנזקציה Txn1 מתבצעת לפני טרנזקציה Txn2 בזמן אמת, חותמת הזמן של ביצוע Txn1 מוקדמת יותר מחותמת הזמן של ביצוע Txn2.

דוגמה:

ציר זמן שבו מוצגת ההפעלה של שתי טרנזקציות שקוראות את אותם נתונים

במקרה כזה, במהלך ציר הזמן t:

  • העסקה Txn1 קוראת נתונים A, מעבירה כתיבה ל-staging אל A, ואז מתבצעת בהצלחה.
  • העסקה Txn2 מתחילה אחרי שTxn1 מתחיל. הוא קורא נתונים B ואז קורא נתונים A.

למרות ש-Txn2 התחיל לפני ש-Txn1 הסתיים, Txn2 רואה את השינויים ש-Txn1 ביצע ב-A. הסיבה לכך היא ש-Txn2 קורא את A אחרי ש-Txn1 מבצע את הכתיבה שלו ל-A.

יכול להיות שזמן הביצוע של Txn1 ושל Txn2 יחפוף, אבל חותמות הזמן של השמירה שלהם, c1 ו-c2 בהתאמה, מבטיחות סדר לינארי של העסקאות. זה אומר:

  • נראה שכל פעולות הקריאה והכתיבה בתוך Txn1 התרחשו בנקודת זמן אחת, c1.
  • נראה שכל פעולות הקריאה והכתיבה בתוך Txn2 התרחשו בנקודת זמן אחת, c2.
  • חשוב לציין ש-c1 מוקדם יותר מ-c2 לכתיבות מחויבות, גם אם הכתיבות התרחשו במכונות שונות. אם Txn2 מבצע רק קריאות, c1 מוקדם מ-c2 או באותו זמן.

הסדר החזק הזה אומר שאם פעולת קריאה עוקבת מבחינה בהשפעות של Txn2, היא מבחינה גם בהשפעות של Txn1. הערך של המאפיין הזה הוא true לכל העסקאות שאושרו בהצלחה.

לעומת זאת, אם משתמשים בבידוד קריאה חוזרת, התרחיש הבא מתרחש עבור אותן טרנזקציות:

  • Txn1 מתחיל לקרוא נתונים A, ויוצר תמונת מצב משלו של מסד הנתונים באותו רגע.
  • Txn2 מתחיל לקרוא נתונים B ויוצר תמונת מצב משלו.
  • לאחר מכן, Txn1 משנה את הנתונים A ומבצע את השינויים בהצלחה.
  • Txn2 ניסיונות לקריאת נתונים A. חשוב לזכור: מכיוון שהיא פועלת על סמך תמונת מצב מוקדמת יותר, Txn2 לא רואה את העדכון Txn1 שבוצע זה עתה ב-A. ‫Txn2 קורא את הערך הישן.
  • Txn2 modifies data B and commits.

בתרחיש הזה, כל עסקה פועלת על תמונת מצב עקבית משלה של מסד הנתונים, שנוצרה מרגע תחילת העסקה. הרצף הזה יכול לגרום לאנומליה של הטיה בכתיבה אם הכתיבה אל B על ידי Txn2 הייתה תלויה באופן לוגי בערך שנקרא מ-A. במילים אחרות, Txn2 ביצע את העדכונים שלו על סמך מידע לא עדכני, והפעולה הבאה שלו עלולה להפר את הכלל הבלתי משתנה ברמת האפליקציה. כדי למנוע את התרחיש הזה, אפשר להשתמש ב-SELECT...FOR UPDATE כדי לבודד קריאות חוזרות או ליצור אילוצי בדיקה בסכימה.

השינויים נשמרים במאגר מקומי ומוצגים רק אחרי שהטרנזקציה מתבצעת.

קריאה וכתיבה של ערבויות במקרה של כשל בעסקה

אם קריאה להפעלת טרנזקציה נכשלת, ערבויות הקריאה והכתיבה שאתם מקבלים תלויות בשגיאה שגרמה לכישלון של קריאת ה-commit הבסיסית.

יכול להיות ש-Spanner יבצע את הפעולות של עסקה מסוימת כמה פעמים באופן פנימי. אם ניסיון ההפעלה נכשל, השגיאה שמוחזרת מציינת את התנאים שהתרחשו ומציינת את ההתחייבויות שאתם מקבלים. עם זאת, אם Spanner ינסה שוב לבצע את העסקה, יכול להיות שכל תופעות הלוואי מהפעולות שלה (לדוגמה, שינויים במערכות חיצוניות או במצב המערכת מחוץ למסד נתונים של Spanner) יתרחשו מספר פעמים.

כשטרנזקציה ב-Spanner נכשלת, ההבטחות שאתם מקבלים לגבי פעולות קריאה וכתיבה תלויות בשגיאה הספציפית שנתקלתם בה במהלך פעולת השמירה.

לדוגמה, הודעת שגיאה כמו Row Not Found (השורה לא נמצאה) או Row Already Exists (השורה כבר קיימת) מציינת בעיה במהלך כתיבת שינויים זמניים שנשמרו בזיכרון. מצב כזה יכול לקרות, למשל, אם השורה שהלקוח מנסה לעדכן לא קיימת. במקרים הבאים:

  • הקריאות עקביות: כל קריאת נתונים במהלך העסקה מובטחת להיות עקבית עד לנקודה שבה מתרחשת השגיאה.
  • פעולות הכתיבה לא מוחלות: המוטציות שהעסקה ניסתה לבצע לא נשמרות במסד הנתונים.
  • עקביות השורה: אי-הקיום (או מצב הקיום) של השורה שהפעילה את השגיאה עקבי עם פעולות הקריאה שבוצעו בתוך העסקה.

אתם יכולים לבטל פעולות קריאה אסינכרוניות ב-Spanner בכל שלב, בלי להשפיע על פעולות אחרות שמתבצעות באותה טרנזקציה. הגמישות הזו שימושית אם פעולה ברמה גבוהה יותר מבוטלת, או אם מחליטים להפסיק קריאה על סמך התוצאות הראשוניות.

עם זאת, חשוב להבין ששליחת בקשה לביטול קריאה לא מבטיחה שהיא תופסק באופן מיידי. אחרי בקשת ביטול, יכול להיות שפעולת הקריאה עדיין:

  • השלמה מוצלחת: יכול להיות שעיבוד הקריאה יסתיים והתוצאות יוחזרו לפני שהביטול ייכנס לתוקף.
  • הקריאה נכשלה מסיבה אחרת: הקריאה יכולה להסתיים בגלל שגיאה אחרת, כמו ביטול.
  • החזרת תוצאות לא מלאות: יכול להיות שהקריאה תחזיר תוצאות חלקיות, שייבדקו במסגרת תהליך אישור העסקה.

ביטול פעולת אישור מבטל את כל העסקה, אלא אם העסקה כבר אושרה או נכשלה מסיבה אחרת.

אטומיות, עקביות, עמידות

בנוסף לבידוד, Spanner מספק את שאר ההבטחות של מאפייני ACID:

  • אטומיות: עסקה נחשבת אטומית אם כל הפעולות שלה הושלמו בהצלחה, או אם אף אחת מהן לא הושלמה. אם פעולה כלשהי בתוך טרנזקציה נכשלת, הטרנזקציה כולה מוחזרת למצב המקורי שלה, וכך נשמרת תקינות נתונים.
  • עקביות: עסקה צריכה לשמור על התקינות של הכללים והאילוצים של מסד הנתונים. אחרי השלמת העסקה, מסד הנתונים צריך להיות במצב תקין, בהתאם לכללים שהוגדרו מראש.
  • עמידות: אחרי שמאשרים עסקה, השינויים שלה נשמרים באופן קבוע במסד הנתונים, והם נשמרים גם במקרה של כשלים במערכת, הפסקות חשמל או שיבושים אחרים.

ביצועים

בקטע הזה מוסבר על בעיות שמשפיעות על הביצועים של טרנזקציות עם הרשאות קריאה וכתיבה.

נעילה של בקרת בו-זמניות

כברירת מחדל, Spanner מאפשר לכמה לקוחות ליצור אינטראקציה עם אותו מסד נתונים בו-זמנית ברמת הבידוד הסדרתית שמוגדרת כברירת מחדל. כדי לשמור על עקביות הנתונים בעסקאות מקבילות כאלה, ל-Spanner יש מנגנון נעילה שמשתמש גם בנעילות משותפות וגם בנעילות בלעדיות. נעילת קריאה כזו מתבצעת רק בטרנזקציות שניתנות לסדרות, ולא בטרנזקציות שמשתמשות בבידוד קריאה חוזרת.

כשעסקה שניתנת לסריאליזציה מבצעת פעולת קריאה, מערכת Spanner מקבלת נעילות קריאה משותפות על הנתונים הרלוונטיים. המנעולים המשותפים האלה מאפשרים לפעולות קריאה מקבילות אחרות לגשת לאותם נתונים. הבו-זמניות (concurrency) הזו נשמרת עד שהטרנזקציה מוכנה לשמור (Commit) את השינויים.

בבידוד ניתן לסדר, במהלך שלב השמירה, כשמחילים פעולות כתיבה, העסקה מנסה לשדרג את הנעילות שלה לנעילות בלעדיות. כדי לעשות את זה, Spanner מבצע את הפעולות הבאות:

  • חסימת בקשות חדשות לנעילת קריאה משותפת בנתונים המושפעים.
  • המערכת תמתין עד שכל נעילות הקריאה הקיימות של הנתונים האלה ישוחררו.
  • אחרי שכל נעילות הקריאה המשותפות יוסרו, המערכת תציב נעילה בלעדית ותעניק גישה בלעדית לנתונים למשך הכתיבה.

לעומת זאת, בבידוד ניתן לסדר את הפעולות ב-Spanner עם אופטימיזציה של מקביליות, וגם בבידוד ניתן לקרוא את הפעולות שוב עם אופטימיזציה של מקביליות, בלי להשתמש בנעילות במהלך הביצוע של העסקה. כשמבצעים קומיט, העסקה מקבלת נעילות בלעדיות לנתונים שנכתבו. יכול להיות שהעסקה תצטרך לחכות לנעילות אם עסקה מקבילה גם מבצעת פעולות כתיבה לאותם נתונים.

בבידוד של קריאה חוזרת עם נעילה פסימית של נתונים במקביל, נעילות בלעדיות נרכשות עבור הנתונים שנקראים על ידי שאילתות שמשתמשות ב-FOR UPDATE או ברמז lock_scanned_ranges=exclusive, ועבור נתונים שנכתבים באמצעות שאילתות DML. לכן, לא צריך להשיג נעילות נוספות בזמן השמירה.

הערות לגבי נעילות:

  • גרנולריות: נעילות ב-Spanner מוחלות ברמת השורה והעמודה. המשמעות היא שאם טרנזקציה T1 מחזיקה נעילה בעמודה A של שורה albumid, טרנזקציה T2 עדיין יכולה לכתוב בו-זמנית בעמודה B של אותה שורה albumid ללא התנגשות.
  • פעולות כתיבה ללא פעולות קריאה:

    • אם אין פעולות קריאה בעסקה, יכול להיות ש-Spanner לא ידרוש נעילה בלעדית לפעולות כתיבה ללא קריאה. במקום זאת, יכול להיות שהיא תשתמש בנעילה משותפת של הכותב. הסיבה לכך היא שסדר ההחלה של פעולות כתיבה ללא קריאה נקבע לפי חותמות הזמן של ביצוע הפעולות, וכך כמה כותבים יכולים לפעול על אותו פריט בו-זמנית ללא התנגשות. נעילה בלעדית נדרשת רק אם העסקה קוראת קודם את הנתונים שהיא מתכוונת לכתוב.
    • בבידוד קריאה חוזרת, טרנזקציות בדרך כלל מקבלות נעילות בלעדיות לתאים שנכתבו בזמן ביצוע הפעולה.
  • אינדקסים משניים לחיפושים בשורות: בבידוד ניתן לסריאליזציה, כשמבצעים קריאות בתוך טרנזקציה של קריאה-כתיבה, שימוש באינדקסים משניים יכול לשפר משמעותית את הביצועים. באמצעות אינדקסים משניים כדי להגביל את השורות שנסרקות לטווח קטן יותר, Spanner נועל פחות שורות בטבלה, וכך מאפשר שינוי מקביל גדול יותר של שורות מחוץ לטווח הספציפי הזה.

  • גישה בלעדית למשאב חיצוני: נעילות פנימיות של Spanner מיועדות לשמירה על עקביות הנתונים במסד הנתונים של Spanner עצמו. אל תשתמשו בהם כדי להבטיח גישה בלעדית למשאבים מחוץ ל-Spanner. יכול להיות ש-Spanner יבטל טרנזקציות מסיבות שונות, כולל אופטימיזציות פנימיות של המערכת, כמו העברת נתונים בין משאבי מחשוב. אם מתבצע ניסיון חוזר לביצוע טרנזקציה (באופן מפורש על ידי קוד האפליקציה או באופן מרומז על ידי ספריות לקוח כמו Spanner JDBC driver), מובטח שהנעילות יוחזקו רק במהלך ניסיון השמירה המוצלח.

  • נתוני נעילה: כדי לאבחן ולחקור התנגשויות נעילה במסד הנתונים, אפשר להשתמש בכלי נתוני נעילה.

זיהוי מצב של חסימה הדדית

מערכת Spanner מזהה מצבים שבהם יכול להיות שיהיו כמה טרנזקציות במצב של חסימה הדדית, ומבטלת את כל הטרנזקציות חוץ מאחת. תארו לעצמכם את התרחיש הבא: Txn1 מחזיק נעילה ברשומה A וממתין לנעילה ברשומה B, בזמן שTxn2 מחזיק נעילה ברשומה B וממתין לנעילה ברשומה A. כדי לפתור את הבעיה, אחת מהעסקאות צריכה להתבטל, כך שהנעילה שלה תוסר והעסקה השנייה תוכל להתבצע.

‫Spanner משתמש באלגוריתם הסטנדרטי wound-wait לזיהוי מצב של חסימה הדדית. מתחת לפני השטח, מערכת Spanner עוקבת אחרי הגיל של כל טרנזקציה שמבקשת נעילות מתנגשות. הוא מאפשר לעסקאות ישנות יותר לבטל עסקאות חדשות יותר. עסקה ישנה יותר היא עסקה שהפעולה הכי מוקדמת שלה (קריאה, שאילתה או ביצוע) התרחשה מוקדם יותר.

על ידי מתן עדיפות לעסקאות ישנות יותר, Spanner מוודא שכל עסקה בסופו של דבר מקבלת נעילות אחרי שהיא מספיק ישנה כדי לקבל עדיפות גבוהה יותר. לדוגמה, עסקה ישנה יותר שזקוקה לנעילה משותפת של כתיבה יכולה לבטל עסקה חדשה יותר שמחזיקה בנעילה משותפת של קריאה.

הרצה מבוזרת

‫Spanner יכול לבצע טרנזקציות על נתונים שמתפרסים על פני כמה שרתים, אבל היכולת הזו כרוכה בעלות ביצועים בהשוואה לטרנזקציות בשרת יחיד.

אילו סוגים של עסקאות עשויים להיות מופצים? ‫Spanner יכול לחלק את האחריות לשורות במסד הנתונים בין שרתים רבים. בדרך כלל, שורה ושורות הטבלה המקבילות שלה מוגשות על ידי אותו שרת, כמו גם שתי שורות באותה טבלה עם מפתחות סמוכים. ‫Spanner יכול לבצע טרנזקציות בשורות בשרתים שונים. עם זאת, ככלל, טרנזקציות שמשפיעות על הרבה שורות שמוצבות זו לצד זו הן מהירות וזולות יותר מאלה שמשפיעות על הרבה שורות שמפוזרות במסד הנתונים או בטבלה גדולה.

העסקאות היעילות ביותר ב-Spanner כוללות רק את פעולות הקריאה והכתיבה שצריך להחיל באופן אטומי. העסקאות הכי מהירות מתבצעות כשכל פעולות הקריאה והכתיבה ניגשות לנתונים באותו חלק של מרחב המפתחות.

עסקאות עם הרשאת קריאה בלבד

בנוסף לנעילת עסקאות עם הרשאת קריאה וכתיבה, Spanner מציע עסקאות עם הרשאת קריאה בלבד.

משתמשים בעסקת קריאה בלבד כשצריך לבצע יותר מקריאה אחת באותו חותמת זמן. אם אפשר לבטא את הקריאה באמצעות אחת משיטות הקריאה היחידה של Spanner, כדאי להשתמש בשיטת הקריאה היחידה הזו במקום זאת. הביצועים של קריאת נתונים יחידה כזו צריכים להיות דומים לביצועים של קריאה יחידה שמתבצעת בעסקת קריאה בלבד.

אם אתם קוראים כמות גדולה של נתונים, כדאי להשתמש במחיצות כדי לקרוא את הנתונים במקביל.

מכיוון שעסקאות לקריאה בלבד לא מבצעות כתיבה, הן לא מחזיקות נעילות ולא חוסמות עסקאות אחרות. עסקאות לקריאה בלבד מתבססות על קידומת עקבית של היסטוריית אישור העסקאות, כך שהאפליקציה תמיד מקבלת נתונים עקביים.

ממשק

‫Spanner מספק ממשק להרצת קבוצת פעולות בהקשר של טרנזקציה לקריאה בלבד, עם ניסיונות חוזרים לביטול טרנזקציות.

דוגמה

בדוגמה הבאה מוצג איך משתמשים בעסקת קריאה בלבד כדי לקבל נתונים עקביים לשתי קריאות באותה חותמת זמן:

C++‎

void ReadOnlyTransaction(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto read_only = spanner::MakeReadOnlyTransaction();

  spanner::SqlStatement select(
      "SELECT SingerId, AlbumId, AlbumTitle FROM Albums");
  using RowType = std::tuple<std::int64_t, std::int64_t, std::string>;

  // Read#1.
  auto rows1 = client.ExecuteQuery(read_only, select);
  std::cout << "Read 1 results\n";
  for (auto& row : spanner::StreamOf<RowType>(rows1)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
  // Read#2. Even if changes occur in-between the reads the transaction ensures
  // that Read #1 and Read #2 return the same data.
  auto rows2 = client.ExecuteQuery(read_only, select);
  std::cout << "Read 2 results\n";
  for (auto& row : spanner::StreamOf<RowType>(rows2)) {
    if (!row) throw std::move(row).status();
    std::cout << "SingerId: " << std::get<0>(*row)
              << " AlbumId: " << std::get<1>(*row)
              << " AlbumTitle: " << std::get<2>(*row) << "\n";
  }
}

C#‎


using Google.Cloud.Spanner.Data;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Transactions;

public class QueryDataWithTransactionAsyncSample
{
    public class Album
    {
        public int SingerId { get; set; }
        public int AlbumId { get; set; }
        public string AlbumTitle { get; set; }
    }

    public async Task<List<Album>> QueryDataWithTransactionAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        var albums = new List<Album>();
        using TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
        using var connection = new SpannerConnection(connectionString);

        // Opens the connection so that the Spanner transaction included in the TransactionScope
        // is read-only TimestampBound.Strong.
        await connection.OpenAsync(SpannerTransactionCreationOptions.ReadOnly, options: null, cancellationToken: default);
        using var cmd = connection.CreateSelectCommand("SELECT SingerId, AlbumId, AlbumTitle FROM Albums");

        // Read #1.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                Console.WriteLine("SingerId : " + reader.GetFieldValue<string>("SingerId")
                    + " AlbumId : " + reader.GetFieldValue<string>("AlbumId")
                    + " AlbumTitle : " + reader.GetFieldValue<string>("AlbumTitle"));
            }
        }

        // Read #2. Even if changes occur in-between the reads,
        // the transaction ensures that Read #1 and Read #2
        // return the same data.
        using (var reader = await cmd.ExecuteReaderAsync())
        {
            while (await reader.ReadAsync())
            {
                albums.Add(new Album
                {
                    AlbumId = reader.GetFieldValue<int>("AlbumId"),
                    SingerId = reader.GetFieldValue<int>("SingerId"),
                    AlbumTitle = reader.GetFieldValue<string>("AlbumTitle")
                });
            }
        }
        scope.Complete();
        Console.WriteLine("Transaction complete.");
        return albums;
    }
}

המשך


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/iterator"
)

func readOnlyTransaction(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	ro := client.ReadOnlyTransaction()
	defer ro.Close()
	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := ro.Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		var singerID int64
		var albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}

	iter = ro.Read(ctx, "Albums", spanner.AllKeys(), []string{"SingerId", "AlbumId", "AlbumTitle"})
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID int64
		var albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
		fmt.Fprintf(w, "%d %d %s\n", singerID, albumID, albumTitle)
	}
}

Java

static void readOnlyTransaction(DatabaseClient dbClient) {
  // ReadOnlyTransaction must be closed by calling close() on it to release resources held by it.
  // We use a try-with-resource block to automatically do so.
  try (ReadOnlyTransaction transaction = dbClient.readOnlyTransaction()) {
    try (ResultSet queryResultSet =
        transaction.executeQuery(
            Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
      while (queryResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            queryResultSet.getLong(0), queryResultSet.getLong(1), queryResultSet.getString(2));
      }
    } // queryResultSet.close() is automatically called here
    try (ResultSet readResultSet =
        transaction.read(
          "Albums", KeySet.all(), Arrays.asList("SingerId", "AlbumId", "AlbumTitle"))) {
      while (readResultSet.next()) {
        System.out.printf(
            "%d %d %s\n",
            readResultSet.getLong(0), readResultSet.getLong(1), readResultSet.getString(2));
      }
    } // readResultSet.close() is automatically called here
  } // transaction.close() is automatically called here
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

// Gets a transaction object that captures the database state
// at a specific point in time
database.getSnapshot(async (err, transaction) => {
  if (err) {
    console.error(err);
    return;
  }
  const queryOne = 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums';

  try {
    // Read #1, using SQL
    const [qOneRows] = await transaction.run(queryOne);

    qOneRows.forEach(row => {
      const json = row.toJSON();
      console.log(
        `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`,
      );
    });

    const queryTwo = {
      columns: ['SingerId', 'AlbumId', 'AlbumTitle'],
    };

    // Read #2, using the `read` method. Even if changes occur
    // in-between the reads, the transaction ensures that both
    // return the same data.
    const [qTwoRows] = await transaction.read('Albums', queryTwo);

    qTwoRows.forEach(row => {
      const json = row.toJSON();
      console.log(
        `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}`,
      );
    });

    console.log('Successfully executed read-only transaction.');
  } catch (err) {
    console.error('ERROR:', err);
  } finally {
    transaction.end();
    // Close the database when finished.
    await database.close();
  }
});

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Reads data inside of a read-only transaction.
 *
 * Within the read-only transaction, or "snapshot", the application sees
 * consistent view of the database at a particular timestamp.
 * Example:
 * ```
 * read_only_transaction($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function read_only_transaction(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $snapshot = $database->snapshot();
    $results = $snapshot->execute(
        'SELECT SingerId, AlbumId, AlbumTitle FROM Albums'
    );
    print('Results from the first read:' . PHP_EOL);
    foreach ($results as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }

    // Perform another read using the `read` method. Even if the data
    // is updated in-between the reads, the snapshot ensures that both
    // return the same data.
    $keySet = $spanner->keySet(['all' => true]);
    $results = $database->read(
        'Albums',
        $keySet,
        ['SingerId', 'AlbumId', 'AlbumTitle']
    );

    print('Results from the second read:' . PHP_EOL);
    foreach ($results->rows() as $row) {
        printf('SingerId: %s, AlbumId: %s, AlbumTitle: %s' . PHP_EOL,
            $row['SingerId'], $row['AlbumId'], $row['AlbumTitle']);
    }
}

Python

def read_only_transaction(instance_id, database_id):
    """Reads data inside of a read-only transaction.

    Within the read-only transaction, or "snapshot", the application sees
    consistent view of the database at a particular timestamp.
    """
    spanner_client = spanner.Client()
    instance = spanner_client.instance(instance_id)
    database = instance.database(database_id)

    with database.snapshot(multi_use=True) as snapshot:
        # Read using SQL.
        results = snapshot.execute_sql(
            "SELECT SingerId, AlbumId, AlbumTitle FROM Albums"
        )

        print("Results from first read:")
        for row in results:
            print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

        # Perform another read using the `read` method. Even if the data
        # is updated in-between the reads, the snapshot ensures that both
        # return the same data.
        keyset = spanner.KeySet(all_=True)
        results = snapshot.read(
            table="Albums", columns=("SingerId", "AlbumId", "AlbumTitle"), keyset=keyset
        )

        print("Results from second read:")
        for row in results:
            print("SingerId: {}, AlbumId: {}, AlbumTitle: {}".format(*row))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

client.snapshot do |snapshot|
  snapshot.execute("SELECT SingerId, AlbumId, AlbumTitle FROM Albums").rows.each do |row|
    puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
  end

  # Even if changes occur in-between the reads, the transaction ensures that
  # both return the same data.
  snapshot.read("Albums", [:AlbumId, :AlbumTitle, :SingerId]).rows.each do |row|
    puts "#{row[:AlbumId]} #{row[:AlbumTitle]} #{row[:SingerId]}"
  end
end

סמנטיקה

בקטע הזה מתוארת הסמנטיקה של טרנזקציות לקריאה בלבד.

תמונת מצב של עסקאות לקריאה בלבד

כשעסקה לקריאה בלבד מבוצעת ב-Spanner, כל הקריאות שלה מתבצעות בנקודה לוגית אחת בזמן. המשמעות היא שגם העסקה לקריאה בלבד וגם כל קורא או כותב אחרים שפועלים בו-זמנית רואים תמונת מצב עקבית של מסד הנתונים באותו רגע ספציפי.

הגישה הזו פשוטה יותר לקריאות עקביות בהשוואה לנעילת עסקאות לקריאה ולכתיבה. אלו הסיבות לכך:

  • ללא נעילות: טרנזקציות לקריאה בלבד לא מקבלות נעילות. במקום זאת, הן פועלות על ידי בחירת חותמת זמן של Spanner והרצת כל פעולות הקריאה מול הגרסה ההיסטורית הזו של הנתונים. הם לא משתמשים בנעילות, ולכן הם לא יחסמו עסקאות קריאה-כתיבה בו-זמניות.
  • ללא ביטולים: העסקאות האלה אף פעם לא מתבטלות. יכול להיות שהן ייכשלו אם חותמת הזמן של הקריאה שנבחרה תעבור איסוף אשפה, אבל מדיניות ברירת המחדל של Spanner לגבי איסוף אשפה היא בדרך כלל נדיבה מספיק, כך שרוב האפליקציות לא ייתקלו בבעיה הזו.
  • אין ביצועים או ביטולים: עסקאות לקריאה בלבד לא דורשות קריאות ל-sessions.commit או ל-sessions.rollback, ולמעשה נמנעות מלעשות זאת.

כדי לבצע טרנזקציית תמונת מצב, הלקוח מגדיר חותמת זמן של גבול, שמנחה את Spanner איך לבחור חותמת זמן של קריאה. סוגי הגבולות של חותמות הזמן כוללים את האפשרויות הבאות:

  • קריאות חזקות: קריאות כאלה מבטיחות שתראו את ההשפעות של כל העסקאות שבוצעו לפני שהקריאה התחילה. כל השורות בקריאה אחת עקביות. עם זאת, קריאות חזקות לא ניתנות לחזרה, למרות שהן מחזירות חותמת זמן. קריאה חוזרת באותה חותמת זמן אפשרית. יכול להיות ששתי טרנזקציות עוקבות של קריאה בלבד יניבו תוצאות שונות בגלל פעולות כתיבה מקבילות. בשביל להריץ שאילתות בפידים של שינויים, צריך להשתמש בגבול הזה. פרטים נוספים זמינים במאמר בנושא TransactionOptions.ReadOnly.strong.
  • חוסר עדכניות מדויק: האפשרות הזו מבצעת קריאות בחותמת זמן שאתם מציינים, כחותמת זמן מוחלטת או כמשך חוסר עדכניות ביחס לזמן הנוכחי. היא מוודאת שמוצג קידומת עקבית של היסטוריית העסקאות הגלובלית עד לחותמת הזמן הזו, וחוסמת עסקאות סותרות שעשויות להתבצע עם חותמת זמן שקטנה מחותמת הזמן של הקריאה או שווה לה. הוא קצת יותר מהיר ממצבי חוסר עדכניות מוגבלים, אבל יכול להיות שהוא יחזיר נתונים ישנים יותר. פרטים נוספים זמינים במאמרים בנושא TransactionOptions.ReadOnly.read_timestamp ו-TransactionOptions.ReadOnly.exact_staleness.
  • Bounded staleness: Spanner בוחר את חותמת הזמן החדשה ביותר במסגרת מגבלת עדכניות שהוגדרה על ידי המשתמש, ומאפשר ביצוע בשכפול הזמין הקרוב ביותר בלי לחסום. כל השורות שמוחזרות עקביות. בדומה לקריאות חזקות, קריאות עם חוסר עדכניות מוגבל לא ניתנות לשחזור, כי קריאות שונות עשויות להתבצע בחותמות זמן שונות גם עם אותו גבול. הקריאות האלה פועלות בשני שלבים (משא ומתן על חותמת זמן, ואז קריאה) ובדרך כלל הן קצת יותר איטיות מאשר קריאות עם רמת טריות מדויקת, אבל הן לרוב מחזירות תוצאות עדכניות יותר ויש סיכוי גבוה יותר שהן יתבצעו בעותק מקומי. המצב הזה זמין רק לעסקאות חד-פעמיות לקריאה בלבד, כי כדי לנהל משא ומתן על חותמת זמן צריך לדעת מראש אילו שורות ייקראו. פרטים נוספים זמינים במאמרים בנושא TransactionOptions.ReadOnly.max_staleness ו-TransactionOptions.ReadOnly.min_read_timestamp.

עסקאות DML עם חלוקה למחיצות

אתם יכולים להשתמש ב-partitioned DML כדי להריץ הצהרות UPDATE ו-DELETE בקנה מידה גדול בלי להיתקל במגבלות על טרנזקציות או לנעול טבלה שלמה. כדי להשיג את זה, Spanner מחלק את מרחב המפתחות ומריץ את הצהרות ה-DML בכל מחיצה בתוך טרנזקציית קריאה-כתיבה נפרדת.

כדי להשתמש ב-DML לא מחולק, צריך להריץ הצהרות בתוך טרנזקציות לקריאה ולכתיבה שיוצרים באופן מפורש בקוד. פרטים נוספים מופיעים במאמר בנושא שימוש ב-DML.

ממשק

‫Spanner מספק את הממשק TransactionOptions.partitionedDml לביצוע של פקודת DML מחולקת אחת.

דוגמאות

בדוגמה הבאה של קוד מתבצע עדכון של העמודה MarketingBudget בטבלה Albums.

C++‎

משתמשים בפונקציה ExecutePartitionedDml() כדי להריץ פקודת DML עם חלוקה למחיצות.

void DmlPartitionedUpdate(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("UPDATE Albums SET MarketingBudget = 100000"
                            "  WHERE SingerId > 1"));
  if (!result) throw std::move(result).status();
  std::cout << "Updated at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_update]\n";
}

C#‎

משתמשים ב-ExecutePartitionedUpdateAsync() method כדי להריץ פקודת DML עם חלוקה למחיצות.


using Google.Cloud.Spanner.Data;
using System;
using System.Threading.Tasks;

public class UpdateUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> UpdateUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) updated...");
        return rowCount;
    }
}

המשך

משתמשים ב-PartitionedUpdate() method כדי להריץ פקודת DML עם חלוקה למחיצות.


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func updateUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "%d record(s) updated.\n", rowCount)
	return nil
}

Java

משתמשים ב-executePartitionedUpdate() method כדי להריץ פקודת DML עם חלוקה למחיצות.

static void updateUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records updated.\n", rowCount);
}

Node.js

משתמשים ב-runPartitionedUpdate() method כדי להריץ פקודת DML עם חלוקה למחיצות.

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1',
  });
  console.log(`Successfully updated ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

משתמשים ב-executePartitionedUpdate() method כדי להריץ פקודת DML עם חלוקה למחיצות.

use Google\Cloud\Spanner\SpannerClient;

/**
 * Updates sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function update_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1'
    );

    printf('Updated %d row(s).' . PHP_EOL, $rowCount);
}

Python

משתמשים ב-execute_partitioned_dml() method כדי להריץ פקודת DML עם חלוקה למחיצות.

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml(
    "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

print("{} records updated.".format(row_ct))

Ruby

משתמשים ב-execute_partitioned_update() method כדי להריץ פקודת DML עם חלוקה למחיצות.

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "UPDATE Albums SET MarketingBudget = 100000 WHERE SingerId > 1"
)

puts "#{row_count} records updated."

בדוגמה הבאה של הקוד מוצגת מחיקה של שורות מהטבלה Singers, על סמך העמודה SingerId.

C++‎

void DmlPartitionedDelete(google::cloud::spanner::Client client) {
  namespace spanner = ::google::cloud::spanner;
  auto result = client.ExecutePartitionedDml(
      spanner::SqlStatement("DELETE FROM Singers WHERE SingerId > 10"));
  if (!result) throw std::move(result).status();
  std::cout << "Deleted at least " << result->row_count_lower_bound
            << " row(s) [spanner_dml_partitioned_delete]\n";
}

C#‎


using Google.Cloud.Spanner.Data;
using System;
using System.Threading.Tasks;

public class DeleteUsingPartitionedDmlCoreAsyncSample
{
    public async Task<long> DeleteUsingPartitionedDmlCoreAsync(string projectId, string instanceId, string databaseId)
    {
        string connectionString = $"Data Source=projects/{projectId}/instances/{instanceId}/databases/{databaseId}";

        using var connection = new SpannerConnection(connectionString);
        await connection.OpenAsync();

        using var cmd = connection.CreateDmlCommand("DELETE FROM Singers WHERE SingerId > 10");
        long rowCount = await cmd.ExecutePartitionedUpdateAsync();

        Console.WriteLine($"{rowCount} row(s) deleted...");
        return rowCount;
    }
}

המשך


import (
	"context"
	"fmt"
	"io"

	"cloud.google.com/go/spanner"
)

func deleteUsingPartitionedDML(w io.Writer, db string) error {
	ctx := context.Background()
	client, err := spanner.NewClient(ctx, db)
	if err != nil {
		return err
	}
	defer client.Close()

	stmt := spanner.Statement{SQL: "DELETE FROM Singers WHERE SingerId > 10"}
	rowCount, err := client.PartitionedUpdate(ctx, stmt)
	if err != nil {
		return err

	}
	fmt.Fprintf(w, "%d record(s) deleted.", rowCount)
	return nil
}

Java

static void deleteUsingPartitionedDml(DatabaseClient dbClient) {
  String sql = "DELETE FROM Singers WHERE SingerId > 10";
  long rowCount = dbClient.executePartitionedUpdate(Statement.of(sql));
  System.out.printf("%d records deleted.\n", rowCount);
}

Node.js

// Imports the Google Cloud client library
const {Spanner} = require('@google-cloud/spanner');

/**
 * TODO(developer): Uncomment the following lines before running the sample.
 */
// const projectId = 'my-project-id';
// const instanceId = 'my-instance';
// const databaseId = 'my-database';

// Creates a client
const spanner = new Spanner({
  projectId: projectId,
});

// Gets a reference to a Cloud Spanner instance and database
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);

try {
  const [rowCount] = await database.runPartitionedUpdate({
    sql: 'DELETE FROM Singers WHERE SingerId > 10',
  });
  console.log(`Successfully deleted ${rowCount} records.`);
} catch (err) {
  console.error('ERROR:', err);
} finally {
  // Close the database when finished.
  database.close();
}

PHP

use Google\Cloud\Spanner\SpannerClient;

/**
 * Delete sample data in the database by partition with a DML statement.
 *
 * This updates the `MarketingBudget` column which must be created before
 * running this sample. You can add the column by running the `add_column`
 * sample or by running this DDL statement against your database:
 *
 *     ALTER TABLE Albums ADD COLUMN MarketingBudget INT64
 *
 * Example:
 * ```
 * update_data($instanceId, $databaseId);
 * ```
 *
 * @param string $instanceId The Spanner instance ID.
 * @param string $databaseId The Spanner database ID.
 */
function delete_data_with_partitioned_dml(string $instanceId, string $databaseId): void
{
    $spanner = new SpannerClient();
    $instance = $spanner->instance($instanceId);
    $database = $instance->database($databaseId);

    $rowCount = $database->executePartitionedUpdate(
        'DELETE FROM Singers WHERE SingerId > 10'
    );

    printf('Deleted %d row(s).' . PHP_EOL, $rowCount);
}

Python

# instance_id = "your-spanner-instance"
# database_id = "your-spanner-db-id"
spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)

row_ct = database.execute_partitioned_dml("DELETE FROM Singers WHERE SingerId > 10")

print("{} record(s) deleted.".format(row_ct))

Ruby

# project_id  = "Your Google Cloud project ID"
# instance_id = "Your Spanner instance ID"
# database_id = "Your Spanner database ID"

require "google/cloud/spanner"

spanner = Google::Cloud::Spanner.new project: project_id
client  = spanner.client instance_id, database_id

row_count = client.execute_partition_update(
  "DELETE FROM Singers WHERE SingerId > 10"
)

puts "#{row_count} records deleted."

סמנטיקה

בקטע הזה מתוארת הסמנטיקה של DML עם חלוקה למחיצות.

הסבר על ביצוע של DML עם חלוקה למחיצות

אפשר להריץ רק פקודת DML מחולקת אחת בכל פעם, בין אם משתמשים בשיטה של ספריית לקוח או ב-Google Cloud CLI.

עסקאות עם חלוקה למחיצות לא תומכות בפעולות commit או rollback. מערכת Spanner מריצה את פקודת ה-DML ומחילת אותה באופן מיידי. אם מבטלים את הפעולה או שהיא נכשלת, Spanner מבטל את כל המחיצות הפעילות ולא מתחיל את אלה שנותרו. עם זאת, מחיצות שכבר בוצעו לא יוחזרו לאחור ב-Spanner.

אסטרטגיה להשגת נעילה של DML עם חלוקה למחיצות

כדי לצמצם את התחרות על נעילה, פקודות DML עם חלוקה למחיצות מקבלות נעילות קריאה רק בשורות שתואמות לסעיף WHERE. גם עסקאות קטנות ועצמאיות שמשמשות לכל מחיצה מחזיקות נעילות לפרק זמן קצר יותר.

חותמות זמן ישנות של קריאה ומנגנון איסוף של גרסאות

Spanner מבצע איסוף גרסאות אשפה כדי לאסוף נתונים שנמחקו או נמחקו ולנצל אחסון. כברירת מחדל, נתונים מלפני יותר משעה נמחקים. אי אפשר לבצע קריאות ב-Spanner בחותמות זמן ישנות יותר מVERSION_RETENTION_PERIOD שהוגדר, שערך ברירת המחדל שלו הוא שעה אחת, אבל אפשר להגדיר אותו עד שבוע אחד. אם הקריאות ישנות מדי במהלך הביצוע, הן נכשלות ומוחזרת השגיאה FAILED_PRECONDITION.

שאילתות לגבי סנכרון שינויים בזרמי נתונים

Change stream הוא אובייקט סכימה שאפשר להגדיר כדי לעקוב אחרי שינויים בנתונים בכל מסד הנתונים, בטבלאות ספציפיות או בקבוצה מוגדרת של עמודות במסד הנתונים.

כשיוצרים זרם שינויים, Spanner מגדיר פונקציה תואמת של SQL עם ערך טבלה (TVF). אפשר להשתמש בפונקציה הזו כדי לשלוח שאילתות לרשומות השינויים בזרם השינויים המשויך באמצעות השיטה sessions.executeStreamingSql. השם של TVF נוצר מהשם של זרם השינויים ותמיד מתחיל ב-READ_.

כל השאילתות בפונקציות TVF של שינוי הנתונים חייבות להתבצע באמצעות ה-API של sessions.executeStreamingSql בעסקה לקריאה בלבד לשימוש חד-פעמי עם timestamp_bound חזק לקריאה בלבד. הפונקציה TVF של שינוי הזרם מאפשרת לציין את start_timestamp ואת end_timestamp לטווח הזמן. אפשר לגשת לכל רשומות השינויים בתוך תקופת השמירה באמצעות timestamp_bound חזק לקריאה בלבד. כל שאר TransactionOptions לא תקפים לשאילתות של עדכונים בזמן אמת.

בנוסף, אם הערך של TransactionOptions.read_only.return_read_timestamp מוגדר כ-true, ההודעה Transaction שמתארת את העסקה מחזירה ערך מיוחד של 2^63 - 2 במקום חותמת זמן קריאה תקינה. צריך להתעלם מהערך המיוחד הזה ולא להשתמש בו בשום שאילתה נוספת.

מידע נוסף מופיע במאמר בנושא תהליך העבודה של שליחת שאילתות בסנכרון שינויים בזרמי נתונים.

עסקאות במצב לא פעיל

טרנזקציה נחשבת לא פעילה אם אין לה פעולות קריאה או שאילתות SQL ממתינות, והיא לא התחילה פעולה כזו ב-10 השניות האחרונות. ‫Spanner יכול לבטל טרנזקציות לא פעילות כדי למנוע מהן להחזיק נעילות ללא הגבלת זמן. אם עסקה במצב המתנה מבוטלת, הפעולה commit נכשלת ומוחזרת שגיאת ABORTED. ביצוע שאילתה קטנה באופן תקופתי, כמו SELECT 1, בתוך העסקה יכול למנוע את מצב חוסר הפעילות.

המאמרים הבאים