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

בדף הזה מתוארות עסקאות ב-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;
    }
}

Go


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, מעבירה כתיבה ל-A ואז מבצעת commit בהצלחה.
  • העסקה 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 מקבלת נעילות קריאה משותפות על הנתונים הרלוונטיים. המנעולים המשותפים האלה מאפשרים לפעולות קריאה מקבילות אחרות לגשת לאותם נתונים. המקביליות הזו נשמרת עד שהטרנזקציה מתכוננת לבצע את השינויים.

בבידוד ניתן לסדר, במהלך שלב השמירה, כשפעולות הכתיבה מוחלות, העסקה מנסה לשדרג את הנעילות שלה לנעילות בלעדיות. כדי לעשות את זה, 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), מובטח שהנעילות יוחזקו רק במהלך ניסיון השמירה המוצלח.

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

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

מערכת 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;
    }
}

Go


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 בוחר את חותמת הזמן החדשה ביותר במסגרת מגבלת ה-staleness שהוגדרה על ידי המשתמש, ומאפשר ביצוע בשכפול הזמין הקרוב ביותר בלי לחסום. כל השורות שמוחזרות עקביות. בדומה לקריאות חזקות, גם קריאות עם חוסר עדכניות מוגבל לא ניתנות לשחזור, כי קריאות שונות עשויות להתבצע בחותמות זמן שונות גם אם המגבלה זהה. הקריאות האלה פועלות בשני שלבים (משא ומתן על חותמת זמן, ואז קריאה) ובדרך כלל הן קצת יותר איטיות מאשר קריאות עם רמת טריות מדויקת, אבל הן לרוב מחזירות תוצאות עדכניות יותר ויש סיכוי גבוה יותר שהן יתבצעו בעותק מקומי. המצב הזה זמין רק לעסקאות חד-פעמיות לקריאה בלבד, כי כדי לנהל משא ומתן על חותמת זמן צריך לדעת מראש אילו שורות ייקראו. פרטים נוספים זמינים במאמרים בנושא 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;
    }
}

Go


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. גם עסקאות קטנות ועצמאיות שמשמשות לכל מחיצה מחזיקות נעילות לפרק זמן קצר יותר.

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

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

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

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

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

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

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

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

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

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

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