Offline Concurrency Patterns
time
Offline Concurrency Patterns
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
Agenda
- The concurrency
- The patterns (with code examples)
Concurrency
Concurrency
- Thread level concurrency
- Memory access (race conditions, memory consistency)
- Application level concurrency
- Database transactions
- Offline concurrency
- Business transactions
Problems
- lost updates
- inconsistent read
- failure of correctness
- liveness
- how much concurrent activity system can handle
Execution Contexts
Thread
Process
Request
Session
Solutions
-
Immutability
- data that cannot be modified
everyone wants to modify everything
Solutions
-
Isolation
- any piece of data can only be accessed by one thread or process
everyone wants to do everything at once
Solutions
-
mutable data than cannot be isolated
- Optimistic Locking
- Pessimistic Locking
Example:
Two agents who use the flight ticket system want to work on one flight at the same time.
Optimistic Concurrency Control
Martin
David
fetch
fetch
David
save
Martin
error
Optimistic Concurrency Control
- Conflict detection
- Lock during commit (on write)
- Supports concurrency
- Low frequency of conflicts
- Used for not critical features when consequences are low
Pessimistic Concurrency Control
Martin
fetch
David
error
Martin
save
Pessimistic Concurrency Control
- Conflict prevention
- Lock during entire transaction
- Does not suport concurrency
- Used for critical features when consequences are high
But what about the reading?
Inconsistent Reads
Martin
count
David
3
4
update
2
count
Martin
SUM = 6
WRONG
SUM = 5
Preventing Inconsistent Reads
- Optimistic approach
- Versioning
- Pessimistic approach
- Read ➡️ shared lock
- Write ➡️ exclusive lock
- Temporal Reads
Deadlocks
Martin
Account A
lock
Account B
David
lock
wait
wait
Deadlocks
- Choose a victim
- someone must blame
- Lock with TTL
- double check (failure)
- Techniques:
- Force to acquire all the necessary locks at the beginning
- Enforce a coherent strategy to grant lock (ex. alphabetical order of the files)
Transactions
- Bounded sequence of work
-
ACID
-
Atomicity
- everything or nothing (prevent dirty writes)
-
Consistency
- before and after (constraints)
-
Isolation
- forever alone (level ⚠️) - SQL-92
-
Durability
- must survive a crash of any sort (permanent)
-
Atomicity
Durability
Tables
Tables
mmap
fsync
Storage Engine
CRUD
Undo log
append
Redo log
append
memory
storage
Isolation level
-
Serializable
- full isolation
- execute transactions sequentially
- loss liveness and performance
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Isolation level
-
Repeatable reads
- phantoms are allowed
- applies usually for inserts
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Isolation level
-
Read committed
- unrepeatable reads allowed
- statement can only see rows committed before it began
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
Isolation level
-
Read uncommitted
- dirty reads allowed (transactions)
Isolation level | Dirty reads? | Unrepeatable reads? | Phantom reads? |
---|---|---|---|
Read uncommitted | ✔️ | ✔️ | ✔️ |
Read committed | ❌ | ✔️ | ✔️ |
Repeatable reads |
❌ | ❌ | ✔️ |
Serializable
|
❌ | ❌ | ❌ |
Transactions
Request
Session
Business transaction
System Transaction
Request
Request
Time
Offline concurrency
business transactions divided into system transactions
How to provide ACID between system calls?
Patterns
https://github.com/akondas/flighthub
Optimistic Offline Lock
Concurrency Control
Optimistic Offline Lock
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
Optimistic Offline Lock
UPDATE flight
SET blocked_seat = '{...}'
WHERE id = '22c6e025-d62e-4561-920e-999d7ce83ad8'
AND version = 2
false positive
what about columns that not overlap?
Write-based schema mapping
Versionless Optimistic Locking
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 = ?
Optimistic Offline Lock
-
Detects conflict
- Use when chance of conflict is low and resolution is easy
Versioning on UPDATE and DELETE does not prevent inconsistent reads
Pessimistic Offline Lock
Pessimistic Offline Lock
Martin's session
Application
David's session
load flight 123
return flight 123
load flight 123
error: flight locked
update flight 123
Pessimistic Offline Lock
- Prevent conflict
- Use when chance of conflict is high and resolution is hard
How to build?
- know what type of locks you need
- build a lock manager
- define procedures for a business transaction to use locks
Lock manager
-
Implementation
- table that maps locks to owners conflict
- table that maps locks to owners conflict
-
Contract
- what to lock
- when to lock
- when release
- what to do when lock cannot be acquired
Lock manager
<?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;
}
Lock manager
<?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
));
}
}
}
Lock manager
<?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
));
}
}
}
Lock manager
<?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
));
}
}
}
Coarse-Grained Lock
Lock a set of related objects with a single lock
Coarse-Grained Lock
Customer
Address
Version
Lock
1
*
*
1
1
1
1
*
Coarse-Grained Lock
Customer
(aggregate)
Address
Lock
1
*
1
1
boundary
Implicit Lock
Implicit Lock
Business transaction
Application
Lock manager
load flight
acquire lock
success
return flight
Command/Request Lock
Command/Request Lock
Business transaction
Handler
Lock manager
cancel reservation
acquire lock
success
cancel reservation
acquire lock
wait or fail?
Command/Request Lock
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());
}
Command/Request Lock
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
Summary
Summary
- Concurrency is hard
- Locking always has a cost
- Avoid whenever you can
- Avoid deadlock using timeout
- Concurrency is not parallelism
- Start with patterns then explore more
Sources
- Chapters:
- 5 - Concurrency
- 16 - Offline Concurrency Patterns
Q&A
Thanks for listening
@ ArkadiuszKondas
https://slides.com/arkadiuszkondas
https://github.com/akondas/flighthub
Offline Concurrency Patterns
By Arkadiusz Kondas
Offline Concurrency Patterns
Imaging the situation where you have an flight ticket booking system where you can edit available seats. Then think what if two people are are editing the same flight. Both introduce changes simultaneously but which ones will be saved? What if one saves over the other data? These are concurrency issues. I will present a few patterns that represent the technique of controlling concurrent work in situations where it is necessary to apply more than one system transaction.
- 2,293