time
ARKADIUSZ KONDAS
Lead Software Architect
@ Proget Sp. z o.o.
Poland
Zend Certified Engineer
Code Craftsman
Blogger
Ultra Runner
@ ArkadiuszKondas
arkadiuszkondas.com
Zend Certified Architect
Thread
Process
Request
Session
everyone wants to modify everything
everyone wants to do everything at once
Example:
Two agents who use the flight ticket system want to work on one flight at the same time.
Martin
David
fetch
fetch
David
save
Martin
error
Martin
fetch
David
error
Martin
save
But what about the reading?
Martin
count
David
3
4
update
2
count
Martin
SUM = 6
WRONG
SUM = 5
Martin
Account A
lock
Account B
David
lock
wait
wait
Tables
Tables
mmap
fsync
Storage Engine
CRUD
Undo log
append
Redo log
append
memory
storage
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
Isolation level | Dirty reads? | Unrepeatable reads? | Phantom reads? |
---|---|---|---|
Read uncommitted | ✔️ | ✔️ | ✔️ |
Read committed | ❌ | ✔️ | ✔️ |
Repeatable reads |
❌ | ❌ | ✔️ |
Serializable
|
❌ | ❌ | ❌ |
Request
Session
Business transaction
System Transaction
Request
Request
Time
business transactions divided into system transactions
How to provide ACID between system calls?
https://github.com/akondas/flighthub
Martin's session
Application
David's session
load flight 123
return flight 123
load flight 123
return flight 123
edit
update flight 123
update flight 123
wrong version
system transaction
business
transaction
UPDATE flight
SET blocked_seat = '{...}'
WHERE id = '22c6e025-d62e-4561-920e-999d7ce83ad8'
AND version = 2
false positive
what about columns that not overlap?
import org.hibernate.annotations.OptimisticLocking;
import org.hibernate.annotations.OptimisticLockType;
@Entity
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
@Getter
@Setter
public class Car {
@Id
private Integer id;
private String model;
private String brand;
}
UPDATE CAR
SET MODEL = ?
WHERE ID = ? AND MODEL = ?
Versioning on UPDATE and DELETE does not prevent inconsistent reads
Martin's session
Application
David's session
load flight 123
return flight 123
load flight 123
error: flight locked
update flight 123
How to build?
<?php
declare(strict_types=1);
namespace FlightHub\Application;
interface LockManager
{
public function acquireLock(Uuid $lockable, Uuid $owner): void;
public function releaseLock(Uuid $lockable, Uuid $owner): void;
public function releaseAllLocks(Uuid $owner): void;
}
<?php
final class SharedReadLockManager implements LockManager
{
public function acquireLock(Uuid $lockable, Uuid $owner): void
{
if($this->hasLock($lockable, $owner)) {
return;
}
$stm = $this->connection->prepare(
'INSERT INTO read_locks WHERE lockable = :lockable AND owner = :owner'
);
try {
$stm->execute([':lockable' => $lockable, ':owner' => $owner]);
} catch (\PDOException $exception) {
throw new ConcurrencyException(sprintf(
'Can\'t get a lock for %s with owner %s',
$lockable,
$owner
));
}
}
}
<?php
final class SharedReadLockManager implements LockManager
{
public function releaseLock(Uuid $lockable, Uuid $owner): void
{
$stm = $this->connection->prepare(
'DELETE FROM read_locks
WHERE lockable = :lockable AND owner = :owner'
);
try {
$stm->execute([':lockable' => $lockable, ':owner' => $owner]);
} catch (\PDOException $exception) {
throw new ConcurrencyException(sprintf(
'Can\'t release lock for %s with owner %s',
$lockable,
$owner
));
}
}
}
<?php
final class SharedReadLockManager implements LockManager
{
public function releaseAllLocks(Uuid $owner): void
{
$stm = $this->connection->prepare(
'DELETE FROM read_locks WHERE owner = :owner'
);
try {
$stm->execute([':owner' => $owner]);
} catch (\PDOException $exception) {
throw new ConcurrencyException(sprintf(
'Can\'t release lock for owner %s',
$owner
));
}
}
}
Lock a set of related objects with a single lock
Customer
Address
Version
Lock
1
*
*
1
1
1
1
*
Customer
(aggregate)
Address
Lock
1
*
1
1
boundary
Business transaction
Application
Lock manager
load flight
acquire lock
success
return flight
Business transaction
Handler
Lock manager
cancel reservation
acquire lock
success
cancel reservation
acquire lock
wait or fail?
public function handle(CancelReservation $command): void
{
$this->lockManager->lock($command->reservationId(), 60);
try {
$flight = $this->flights
->getByReservationId($command->reservationId());
$flight->cancelReservation($command->reservationId());
$this->paymentService->returnFunds($command->reservationId());
} catch (\Throwable $exception) {
$this->lockManager->unlock($command->reservationId());
throw $exception;
}
$this->lockManager->unlock($command->reservationId());
}
public function lock(string $lockId, int $time) : bool
{
$res = $this->client->setnx($this->lockId($lockId), true);
if (!$res) {
return false;
}
$this->client->expire($this->lockId($lockId), $time);
return true;
}
https://redis.io/commands/setnx
https://redis.io/topics/distlock - redlock
@ ArkadiuszKondas
https://slides.com/arkadiuszkondas
https://github.com/akondas/flighthub