AWS Türkçe Blog

Amazon DynamoDB transactions ile birden çok kayıtta koordineli değişiklikler yapma

Orijinal makale: Link (Baris Yasin ve Serdar Nevruzoglu)

Gittikçe daha fazla kuruluş, NoSQL veritabanlarını, ilişkisel veritabanı yönetim sistemleri (RDBMS) kısıtlamalarından kurtaran çözümler olarak buldukça, NoSQL veritabanlarının kullanımı da son yıllarda önemli ölçüde artmıştır. NoSQL veritabanlarının esnekliği, çevikliği ve performansı; bu yöne geçişi tetikleyen temel avantajlar olarak görülse de, yine de RDBMS’in popülaritesi, kuruluşların bazı önemli gereksinimleri nedeniyle aynı kaldı.

RDBMS’ler, en yaygın bilinen ve üstünde konuşulan özelliği olan transactional desteği nedeni ile, kritik veri işlemleri için genellikle NoSQL veritabanları yerine tercih edilmektedir.

Amazon DynamoDB transactions’u neden kullanmalıyız?

Bazı uygulamalar, genelde bir veya daha fazla öge üzerinde ‘atomik’, yani ya-hep-ya-hiç denilen bir veritabanı işlemi gerektiren mantığı takip etmeye ihtiyaç duyar. Bu mantık, herhangi bir hatalı işlem durumunda, yapılan tüm veritabanı işlemlerini geriye alma anlamına gelir. Bu ihtiyacı yerine getirmek, bağlı tüm işlemleri takip etmek ve buna göre tersine çevirmek demek olduğundan, genellikle uygulama geliştirmesini zorlaştırmaktadır.

Transactions, DynamoDB‘nin yeni bir özelliği olarak re:Invent 2018’de duyuruldu. Tek bir AWS hesabı ve bir Bölge’deki birden çok öğeye ACID (atomiklik, tutarlılık, izolasyon, dayanıklılık) özellikleri verebilmek için, veritabanı içindeki işlemlerde kendinden bir destek sağlar.

Daha önceden DynamoDB bu özellikleri yalnızca tek bir öge için desteklemekteydi. Verilerin erişilebilirliğini ve dayanıklılığını sağlamak için böyle tasarlanmıştı. Transactions; birden fazla ögede, bir veya daha fazla tablo için atomiklik (ya-hep-ya-hiç) ve izolasyon (birbirini etkilemeyen işlemler) desteği ekledi.

Ayrıca, transactions ClientRequestToken anahtarı, API isteklerinin eşgüçlü (idempotent) olmasını sağlar. Böylece birden çok özdeş istek, aynı işlemi tekrarlamaz ve tek bir istek ile aynı etkiyi yapar.

Transactions ile artık veritabanı içindeki geri alma işlemleri için endişelenmenize veya onlarla uğraşmanıza gerek yok. Transactions, birden çok öge ve tablodaki eylemleri koordine ederek veri bütünlüğünü korumanıza yardımcı olur.

Nasıl çalışır

DynamoDB transactions, TransactWriteItems ve TransactGetItems diye adlandırılan iki temel API eylemi sunuyor. Bu sayede, birkaç eylemi birleştirebilir ve bunları tek bir ya-hep-ya-hiç işlemi olarak gönderebilirsiniz.

Örneğin, otelinizin rezervasyon yönetimi uygulamasını ilişkisel bir veritabanından DynamoDB’ye taşımaya karar verdiniz. Aşağıdaki şema ER yapınızı temsil eder. Diyagramda, 3 birim var: Misafir, Rezervasyon ve Oda. Misafir ve Rezervasyon arasında çoktan çoka (many-to-many) bir ilişki ve ayrıca Rezervasyon ile Oda arasında da çoktan çoka bir ilişki bulunuyor.

Burada heterojen tablo yaklaşımını uyguluyoruz ve üç eski tabloyu tek bir DynamoDB tablosunda eşliyoruz. DynamoDB’deki örnek verilerle birlikte otel rezervasyonu veri modeli aşağıdaki tabloda bulunmakta.

HotelManagement Record Type Id (PK) Attributes
Guest “John” ActiveReservations : { “501” }
OccupiesRooms : { “20014” }
Reservation “501” GuestId: “John”
ReservationStatus: “FULFILLED”
FulfilledByRoom: “20014”
Room “R20014” RoomStatus: “OCCUPIED”
RentedToReservation : “501”

Örnek kod

Misafirler üç çeşit işlem yapabiliyor: rezervasyon oluşturma, check in, ve check out. Bu makalede, DynamoDB’nin işlemsel yeteneklerini gösterme amaçlı olarak, aşağıdaki şemadaki biçimiyle, üç farklı Java metoduna konulmuş üç senaryo var. Kullanıcı sıralı olarak üç işlemi gerçekleştirebiliyor. Birincisi Create reservation (Rezervasyon oluştur), ikincisi Check-in ve üçüncüsü de Check-out.

İlk olarak, üstünde çalışabileceğiniz bir tablo oluşturmalısınız.

private static void createTable(String tableName) {
 try {
     // Create a table with a primary hash key named 'Id', which holds a string
     CreateTableRequest createTableRequest = new CreateTableRequest().withTableName(tableName)
         .withKeySchema(new KeySchemaElement().withAttributeName("Id").withKeyType(KeyType.HASH))
         .withAttributeDefinitions(new AttributeDefinition().withAttributeName("Id").withAttributeType(ScalarAttributeType.S))
         .withProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(1L).withWriteCapacityUnits(1L));

     // Create table if it does not exist yet
     TableUtils.createTableIfNotExists(dynamoDB, createTableRequest);

     // wait for the table to move into ACTIVE state
     TableUtils.waitUntilActive(dynamoDB, tableName);
     
     // Describe our new table
     DescribeTableRequest describeTableRequest = new DescribeTableRequest().withTableName(tableName);
     TableDescription tableDescription = dynamoDB.describeTable(describeTableRequest).getTable();
     System.out.println("Table Created Successfully. Table Description: " + tableDescription);
     
 } catch (InterruptedException e) {
     System.out.println("Occupied thread is interrupted");
 }
}
Java

Rezervasyon oluşturma

Ardından, sistemde örnek bir rezervasyonu temsil eden bir dizi kayıt oluşturun.

    private static void createReservation() {
 
 // Create guest item
 HashMap<String, AttributeValue> guestItem = new HashMap<String, AttributeValue>();
 guestItem.put("Id", new AttributeValue("John"));
 guestItem.put("ActiveReservations", new AttributeValue("500"));
 
 // Create room item
 HashMap<String, AttributeValue> roomItem = new HashMap<String, AttributeValue>();
 roomItem.put("Id", new AttributeValue("R20014"));
 roomItem.put("RoomStatus", new AttributeValue("FREE"));
 
 // Create reservation item
 HashMap<String, AttributeValue> reservationItem = new HashMap<String, AttributeValue>();
 reservationItem.put("Id", new AttributeValue("500"));
 reservationItem.put("GuestId", new AttributeValue("John"));
 reservationItem.put("ReservationStatus", new AttributeValue("PENDING"));

 Put createGuest = new Put().withTableName(TABLE_NAME).withItem(guestItem);
 Put createRoom = new Put().withTableName(TABLE_NAME).withItem(roomItem);
 Put createReservation = new Put().withTableName(TABLE_NAME).withItem(reservationItem);

 Collection<TransactWriteItem> actions = Arrays.asList(
         new TransactWriteItem().withPut(createGuest),
         new TransactWriteItem().withPut(createRoom),
         new TransactWriteItem().withPut(createReservation));
 
 TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
         .withTransactItems(actions)
         .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

 // Execute the transaction and process the result.
 // The following code snippet illustrates how to execute the actions defined previously as a single all-or-nothing operation
 dynamoDB.transactWriteItems(createReservationTransaction);
 System.out.println("Create Reservation transaction is successful");
}
Java

Create reservation işleminden sonra tablomuz aşağıdaki gibi.

HotelManagement Record Type Id (PK) Attributes
Guest “John” ActiveReservations : { “500”}
OccupiesRooms : null
Reservation “500” GuestId: “John”
ReservationStatus: “PENDING”
FulfilledByRoom: null
Room “R20014” RoomStatus: “FREE”
RentedToReservation : null

Check-in

Bir sonraki işlemse Check-in. Bu işlem sırasında tablodaki üç öge (Misafir, Rezervasyon ve Oda) güncellenir.

    private static void checkIn() {
 
 //Create updateGuest object
 HashMap<String, AttributeValue> guestItemKey = new HashMap<String, AttributeValue>();
 guestItemKey.put("Id", new AttributeValue("John"));
 
 HashMap<String, AttributeValue> guestExpressionAttributeValues = new HashMap<String, AttributeValue>();
 guestExpressionAttributeValues.put(":occupies_rooms", new AttributeValue("R20014"));
 
 Update updateGuest = new Update()
         .withTableName(TABLE_NAME)
         .withKey(guestItemKey)
         .withUpdateExpression("SET OccupiesRooms = :occupies_rooms")
         .withExpressionAttributeValues(guestExpressionAttributeValues);

 
 //Create updateRoom object
 HashMap<String, AttributeValue> roomItemKey = new HashMap<String, AttributeValue>();
 roomItemKey.put("Id", new AttributeValue("R20014"));
 
 HashMap<String, AttributeValue> roomExpressionAttributeValues = new HashMap<String, AttributeValue>();
 roomExpressionAttributeValues.put(":room_status", new AttributeValue("OCCUPIED"));
 roomExpressionAttributeValues.put(":rented_to_reservation", new AttributeValue("500"));
 
 Update updateRoom = new Update()
         .withTableName(TABLE_NAME)
         .withKey(roomItemKey)
         .withUpdateExpression("SET RoomStatus = :room_status, RentedToReservation = :rented_to_reservation")
         .withExpressionAttributeValues(roomExpressionAttributeValues);

 //Create updateReservation object
 HashMap<String, AttributeValue> reservationItemKey = new HashMap<String, AttributeValue>();
 reservationItemKey.put("Id", new AttributeValue("500"));
 
 HashMap<String, AttributeValue> reservationExpressionAttributeValues = new HashMap<String, AttributeValue>();
 reservationExpressionAttributeValues.put(":reservation_status", new AttributeValue("FULLFILLED"));
 reservationExpressionAttributeValues.put(":fullfilled_by_room", new AttributeValue("R20014"));
 
 Update updateReservation = new Update()
         .withTableName(TABLE_NAME)
         .withKey(reservationItemKey)
         .withUpdateExpression("SET ReservationStatus = :reservation_status, FullfilledByRoom = :fullfilled_by_room")
         .withExpressionAttributeValues(reservationExpressionAttributeValues);
 
 //Execute transaction
 Collection<TransactWriteItem> actions = Arrays.asList(
         new TransactWriteItem().withUpdate(updateGuest),
         new TransactWriteItem().withUpdate(updateRoom),
         new TransactWriteItem().withUpdate(updateReservation));
 
 TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
         .withTransactItems(actions)
         .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

 dynamoDB.transactWriteItems(createReservationTransaction);
 System.out.println("Check-in transaction is successful");
 
}
Java

Check-in işleminin ardından tablomuzun durumu da aşağıda görülmektedir.

HotelManagement Record Type Id (PK) Attributes
Guest “John” ActiveReservations : { “500”}
OccupiesRooms : { “R20014” }
Reservation “500” GuestId: “John”
ReservationStatus: “FULFILLED”
FulfilledByRoom: “20014”
Room “R20014” RoomStatus: “OCCUPIED”
RentedToReservation : “500”

Check-out

Son işlem de Check-out. DynamoDB’de null veya boş bölümlere izin verilmediğinden, bazı nitelikleri de kaldıralım.

    private static void checkOut() {
 //Create updateGuest object
 HashMap<String, AttributeValue> guestItemKey = new HashMap<String, AttributeValue>();
 guestItemKey.put("Id", new AttributeValue("John"));
 
 Update updateGuest = new Update()
         .withTableName(TABLE_NAME)
         .withKey(guestItemKey)
         .withUpdateExpression("REMOVE OccupiesRooms, ActiveReservations");
 //Since there is no value in the attribute, it is removed
 
 //Create updateRoom object
 HashMap<String, AttributeValue> roomItemKey = new HashMap<String, AttributeValue>();
 roomItemKey.put("Id", new AttributeValue("R20014"));
 
 HashMap<String, AttributeValue> roomExpressionAttributeValues = new HashMap<String, AttributeValue>();
 roomExpressionAttributeValues.put(":room_status", new AttributeValue("FREE"));
 
 Update updateRoom = new Update()
         .withTableName(TABLE_NAME)
         .withKey(roomItemKey)
         .withUpdateExpression("SET RoomStatus = :room_status REMOVE RentedToReservation") //Since there is no value in the attribute, it is removed
         .withExpressionAttributeValues(roomExpressionAttributeValues);
 
 //Create updateReservation object
 HashMap<String, AttributeValue> reservationItemKey = new HashMap<String, AttributeValue>();
 reservationItemKey.put("Id", new AttributeValue("500"));
 
 HashMap<String, AttributeValue> reservationExpressionAttributeValues = new HashMap<String, AttributeValue>();
 reservationExpressionAttributeValues.put(":reservation_status", new AttributeValue("CLOSED"));
 
 Update updateReservation = new Update()
         .withTableName(TABLE_NAME)
         .withKey(reservationItemKey)
         .withUpdateExpression("SET ReservationStatus = :reservation_status")
         .withExpressionAttributeValues(reservationExpressionAttributeValues);
 
 //Execute transaction
 Collection<TransactWriteItem> actions = Arrays.asList(
         new TransactWriteItem().withUpdate(updateGuest),
         new TransactWriteItem().withUpdate(updateRoom),
         new TransactWriteItem().withUpdate(updateReservation));
 
 TransactWriteItemsRequest createReservationTransaction = new TransactWriteItemsRequest()
         .withTransactItems(actions)
         .withReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);

 dynamoDB.transactWriteItems(createReservationTransaction);
 System.out.println("Check-out transaction is successful");
}
Java

Check-out işleminden sonraki tablo da aşağıda gösterilmiştir.

HotelManagement Record Type Id (PK) Attributes
Guest “John” ActiveReservations : {}
Reservation “500” GuestId: “John”
ReservationStatus: “CLOSED”
FulfilledByRoom: “20014”
Room “R20014” RoomStatus: “FREE”

Sonuç

DynamoDB transactional API’leri, herhangi bir DynamoDB tablosundaki herhangi bir öge için ek ücret ödemeden ACID desteği sağlayarak geliştirme deneyimini basitleştirir. Transactions varsayılan olarak tüm single-region DynamoDB tabloları için aktiftir ve isteğe bağlı olarak da global tablolarda etkinleştirilebilir. Bağlı veritabanı işlemleri için uzun süredir bulunan ihtiyaca cevap vererek, DynamoDB’nin ölçeğini, performansını ve ticari kullanıma hazır olan avantajlarını daha geniş bir iş yüküne de genişletecektir.

Transactions ile ilgili daha fazla bilgiyi Amazon DynamoDB Geliştirici Kılavuzu‘nda bulabilirsiniz.