fbpx

Czym jest transakcja w programowaniu?

Zaczniemy od zdefiniowania czym są transakcje i standardowego przykładu z bankomatem. Najprościej rzecz ujmując transakcja to zestaw kroków danego algorytmu, który musi być wykonany w całości lub, jeśli wystąpi błąd w którymkolwiek z kroków, każda operacja musi zostać „cofnięta”, a stan systemu zostanie przywrócony do momentu sprzed rozpoczęcia pierwszego kroku w transakcji. Zacznijmy od oklepanego przykładu konta bankowego i wypłaty środków z bankomatu.

I tak j,eśli dwa kroki z tego algorytmu – zmiana stanu konta i wypłata gotówki – nie byłyby „owinięte” w transakcje to mogłoby dojść do dość nieprzyjemnej sytuacji, jeśli w bankomacie nie byłoby gotówki – stan konta zostałby zmniejszony, natomiast nie otrzymalibyśmy gotówki z bankomatu.

Jeśli użyjemy transakcji i wywołamy w niej te dwa kroki to, gdy nie będziemy mieli środków na koncie, bądź w bankomacie zabraknie gotówki – transakcja zostanie oznaczona jako 'failed’ i stan naszego konta wróci do momentu kiedy to deklarowaliśmy, że chcemy wypłacić gotówkę – czyli nic z naszego konta nie ubędzie.

Zaimplementujmy ten prosty przykład.

Utwórzmy w IntelliJ (czy jakimkolwiek innym IDE) nowy projekt javowy, wspierany przez maven’a.

Zacznijmy od utworzenia klasy dla konta bankowego

 

package domain;

public class Account {

    private int balance;

    public Account(int balance) {
        this.balance = balance;
    }

    public void withdraw(int amount) {
        this.balance-=amount;
    }

    public void deposit(int amount) {
        this.balance+=amount;
    }

    @Override
    public String toString() {
        return "Aktualny stan konta: " + this.balance;
    }
}

oraz dla bankomatu

package domain;

public class CashMachine {

    private int cashAmount;

    public CashMachine(int cashAmount) {
        this.cashAmount = cashAmount;
    }

    public void withdraw(int amount) {
        
        if(this.cashAmount-amount<0) { 
           throw new IllegalStateException("Brak dostatecznej ilosci gotówki."); 
        } 

        this.cashAmount-=amount; 
    } 

    @Override 
    public String toString() { 
        return "Pozostała gotówka: " + this.cashAmount; 
    } 
}

 

Następnie w klasie głównej programu odegrajmy przypadek pozytywny

import domain.Account;
import domain.CashMachine;

public class App {

    public static void main(String[] args) {
        Account account = new Account(100);
        CashMachine machine = new CashMachine(50);
        System.out.println(account + System.lineSeparator() + machine); 
        account.withdraw(20);
        machine.withdraw(20);
        System.out.println(account + System.lineSeparator() + machine);
    }
}

 

Wynik operacji jest dokładnie taki jaki byśmy chcieli

Aktualny stan konta: 100
Pozostała gotówka: 50

Aktualny stan konta: 80
Pozostała gotówka: 30

 

Czyli zmniejszył nam się stan konta, w bankomacie zostało mniej gotówki, a my mamy grubszy portfel. Co natomiast jeśli podamy zbyt dużą ilość gotówki do wypłaty?

import domain.Account;
import domain.CashMachine;

public class App {

    public static void main(String[] args) {

       Account account = new Account(100);
       CashMachine machine = new CashMachine(50);

       System.out.println(account + System.lineSeparator() + machine);

       try {
           account.withdraw(60);
           machine.withdraw(60);
       } catch (Exception e) {
           System.out.println(e.getMessage());
       }

       System.out.println(account + System.lineSeparator() + machine);
    }
}

 

Efekt działania tego przypadku jest taki, że zmienia nam się bilans konta, jednak gotówki nie otrzymujemy, zostanie ona w bankomacie.

Aktualny stan konta: 100
Pozostała gotówka: 50

Brak dostatecznej ilosci gotówki.

Aktualny stan konta: 40
Pozostała gotówka: 50

 

Jak już wspomniałem mogą nam tu pomóc transakcje. Chcemy, by w wypadku niepowodzenia tej operacji stan systemu wrócił do stanu przed wykonaniem operacji.

W tym wypadku stan naszego systemu jest dość banalny – są to dwa obiekty. Jeden odpowiadający za konto, drugi za bankomat. Wystarczy więc, że zapamiętamy ich stan przed wykonaniem tych metod (czyli ile pieniędzy jest gdzie) i przywrócimy go w razie wystąpienia wyjątku. Rozbudujmy klasy Account i CashMachine o takie zwracające kopie danego obiektu oraz odtwarzają stan obiektu na podstawie tejże kopii.

package domain;

public class Account {

    //...

    public Account copy() {
        return new Account(this.balance);
    }

    public void recreate(Account account) {
        this.balance = account.balance;
    }
}

I podobnie dla CashMachine

package domain;

public class CashMachine {

    //...

    public CashMachine copy() {
        return new CashMachine(this.cashAmount);
    }

    public void recreate(CashMachine machine) {
       this.cashAmount = machine.cashAmount;
    }
}

 

Zaimplementujmy teraz proces wypłaty z bankomatu tak by był transakcyjny, czyli wykonał się w całości bądź wcale.

import domain.Account;
import domain.CashMachine;

public class App {

    public static void main(String[] args) {
        Account account = new Account(100);
        CashMachine machine = new CashMachine(50);
        System.out.println(account + System.lineSeparator() + machine); 
        transactionalWithdrawal(account, machine, 60);
        System.out.println(account + System.lineSeparator() + machine);
    }

    private static void transactionalWithdrawal(Account account, CashMachine machine, int amount) {
        Account accountBackup = account.copy(); //1
        CashMachine machineBackup = machine.copy(); //2

        try { //3
           account.withdraw(amount);
           machine.withdraw(amount);
        } catch (Exception e) {
           System.out.println(e.getMessage()); //4
           System.out.println("Coś poszło nie tak. Przywracam stan systemu");
           account.recreate(accountBackup);
           machine.recreate(machineBackup);
        }
    }
}

 

Dla większej przejrzystości kodu przeniosłem te procedurę do osobnej metody.

W pierwszych dwóch krokach (1,2) tworzymy kopie obiektów, na których będziemy działać tak by gdyby coś poszło nie tak mieć stan systemu sprzed rozpoczęcia wykonywania operacji transakcyjnej.

W cześci 'try’ (3) bloku 'try – catch’ wykonujemy operacje, które chcemy, by spięte były transakcją – w naszym wypadku zmiana stanu konta i wypłata gotówki z bankomatu. Jeśli wszystko pójdzie dobrze i nie zostanie rzucony żaden wyjątek wykonywanie metody 'transactionalWithdrawal’ tutaj się zakończy.

Natomiast jeśli pojawi się jakikolwiek wyjątek (zauważ, że łapiemy każdy 'Exception’, a nie tylko IllegalStateException rzucany przez klase CashMachine – więc gdyby oprogramować dodatkowe problemy np. brak środków już na koncie bankowym to również zostaną one przechwycone) to odpalony zostanie blok catch (4) i przywrócimy stan naszego konta bankowego i bankomatu z naszych 'backupów’ wykonanych zanim zaczęliśmy cokolwiek zmieniać.

I to tyle jeśli chodzi o transakcje w czystej postaci. Bez żadnych baz danych, JPA, JTA, Springa czy innych skomplikowanych rzeczy, o których w kolejnych wpisach.

Kod przykładu znajduje się na Githubie

2 komentarze do “Czym jest transakcja w programowaniu?”

  1. hej 🙂 szczerze nie podoba mi się ten kod na stronie strasznie chaotycznie to wygląda może jakieś formatowanie tego tekstu się przyda??

    1. Cześć! Dzięki za zwrócenie uwagi. Formatowanie postu nie przetrwało zmian w wersjach wordpresa/pluginu i jak zauważyłeś – rozjechało się zupełnie. Już poprawione. Dzięki jeszcze raz!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *