Slide 1

Slide 1 text

Using redis streams to build more resilient services

Slide 2

Slide 2 text

@chirimoya [email protected] https://github.com/chirimoya Hey, I’m Thomas – Co-founder & CEO of Sulu GmbH – More than 15 years of experience in web technologies & development – PHP, Symfony, React, SQL, Elasticsearch, … – Open source enthusiast – Loves cooking and mountains

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Tell, don’t ask. Generate an optimised read model for your service by consuming domain events from a central stream.

Slide 5

Slide 5 text

tail -f -n 1 Probably not the most robust implementation for a real-life project.

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

“ In computer science, a stream is a sequence of data elements made available over time.

Slide 8

Slide 8 text

Stream

Slide 9

Slide 9 text

https://brandur.org/redis-streams

Slide 10

Slide 10 text

Redis Streams A new data type introduced with Redis 5.0.0.

Slide 11

Slide 11 text

Connect to Redis Assuming you have installed Redis server and client. And keep in mind, we need to use the latest version of Redis 5.0. PHP $redis = new \Redis(); $redis->connect('127.0.0.1', '6379'); CLI > redis-cli -h 127.0.0.1 -p 6379

Slide 12

Slide 12 text

Add message to stream The stream elements can be one or more key-value pairs. Let's add elements to the stream: PHP $redis->xAdd('mystream', '*', [
 'KEY' => 'VALUE'
 ]); // MESSAGE ID (e.g. 1526919030474-55) $redis->xLen('mystream'); // (integer) 1 CLI > XADD mystream * KEY VALUE # MESSAGE ID (e.g. 1526919030474-55) > XLEN mystream # (integer) 1

Slide 13

Slide 13 text

Read message from stream Read message from one or multiple streams, only returning entries with an ID greater than the last received ID reported by the caller. PHP $redis->xRead(['mystream' => 0], 2); // MESSAGES // [
 // 'STREAMNAME' => [
 // 'MESSAGEID' => [
 // 'KEY' => 'VALUE'
 // ],
 // 'MESSAGEID‘ => [
 // 'KEY' => 'VALUE'
 CLI > XREAD COUNT 2 STREAMS mystream 0 # MESSAGES # STREAMNAME [
 # MESSAGEID [
 # KEY
 # VALUE
 # ]
 # MESSAGEID [
 # KEY

Slide 14

Slide 14 text

Block for data In order to avoid polling at a fixed or adaptive interval, the command is able to block until new entries get pushed into the requested stream. PHP $redis->xRead(['mystream' => '$'], 1, 0); // MESSAGES // [
 // 'STREAMNAME' => [
 // 'MESSAGEID' => [
 // 'KEY' => 'VALUE'
 // ]
 // ],
 // ] CLI > XREAD BLOCK 0 STREAMS mystream $ # MESSAGES # STREAMNAME [
 # MESSAGEID [
 # KEY
 # VALUE
 # ]
 # ]

Slide 15

Slide 15 text

All clients are served with all the entries arriving in a stream. How can I scale my reads?

Slide 16

Slide 16 text

Consumer Groups

Slide 17

Slide 17 text

Create a Consumer Group Redis stream provides the concept of consumer groups, allowing multiple consumers to process the same stream to implement load balancing. PHP $redis->xGroup(
 'CREATE', 
 'mystream',
 'mygroup'
 ); // 1/0 CLI > XGROUP CREATE mystream mygroup $ # 1/0

Slide 18

Slide 18 text

Read data via Consumer Group Redis stream provides the concept of consumer groups, allowing multiple consumers to process the same stream to implement load balancing. PHP $redis->xReadGroup(
 'mygroup', 'myconsumer',
 ['mystream' => 0]
 ); // MESSAGES $redis->xReadGroup(
 'mygroup', 'myconsumer',
 ['mystream' => '>']
 ); CLI > XREADGROUP
 GROUP mygroup mycomsumer
 STREAMS mystream 0 # MESSAGES > XREADGROUP
 GROUP mygroup mycomsumer
 STREAMS mystream >

Slide 19

Slide 19 text

Data processed confirmation Consumer groups require explicit acknowledgement of the messages successfully processed by the consumer. PHP $redis->xAck(
 'mystream', 
 'mygroup', 
 ['message-id']
 ); // 1/0 CLI > XACK mystream mygroup message-id # 1/0

Slide 20

Slide 20 text

Failure Processing – Identify the message data that has been delivered but not confirmed – Change the owner of these data and redeliver PHP $redis->xPending('mystream', 'mygroup'); // MESSAGE IDs $redis->xClaim(
 'mystream', 
 'mygroup',
 'myconsumer',
 3600,
 ['message-id']
 ); CLI > XPENDING mystream mygroup # MESSAGE IDs > XCLAIM mystream mygroup myconsumer 3600 message-id

Slide 21

Slide 21 text

Worked pretty good out of the box, but the API is quite low-level. Let's search for a high-level abstraction.

Slide 22

Slide 22 text

Symfony Messenger Version 4.2

Slide 23

Slide 23 text

Create your own transport?

Slide 24

Slide 24 text

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

The Redis transport was introduced in Symfony 4.3 Open source … FTW!

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

# config/packages/messenger.yaml framework: messenger: buses: query_bus: ~ event_bus: ~ message_bus: middleware: - Avodi\Bundle\EventBundle\Common\DomainEvent\DomainEventMiddleware - App\Common\Messenger\SymfonyEventMiddleware - doctrine_transaction routing: 'Avodi\Bundle\EventBundle\Common\DomainEvent\DomainEventMessage': senders: ['domain_events'] transports: domain_events: 'redis://passwor@localhost:6379/mystream/mygroup/myconsumer?auto_setup=true'

Slide 33

Slide 33 text

domainEvents[] = $event; } /** * @return DomainEventMessage[] */ public function release(): array { $domainEvents = $this->domainEvents; $this->domainEvents = []; return $domainEvents; } }

Slide 34

Slide 34 text

public function cancelTicket(Ticket $ticket): void { $this->workflowRegistry->get($ticket)->apply($ticket, Ticket::TRANSITION_CANCEL); $newTicketId = Uuid::uuid4()->toString(); $this->createTicket($newTicketId, $ticket); $this->domainEventCollector->push( new DomainEventMessage( Ticket::class, 'canceled', $ticket->getId(), [ 'newTicketId' => $newTicketId, ] ) ); }

Slide 35

Slide 35 text

getType() || Ticket::class !== $message->getName()) { return; } $ticketIds = [$message->getId()]; $newTicketId = $message->getPayload()['newTicketId'] ?? ''; if ($newTicketId) { $ticketIds[] = $newTicketId; } else { $this->logger->error('Could not find newTicketId in canceled for access system.', [ 'canceledTicketId' => $message->getId(), ]); } $this->syncTickets($ticketIds); } }

Slide 36

Slide 36 text

# supervicord.conf [program:consumer] directory=/var/www/html command=bin/console messenger:consume --bus event_bus domain_events --time-limit=3600 autostart=true autorestart=true

Slide 37

Slide 37 text

Trim Data from the stream When ~ argument is used, the trimming is performed only when Redis is able to remove a whole macro node. PHP $redis->xTrim('mystream', 1000, true); CLI > XTRIM mystream MAXLEN ~ 1000

Slide 38

Slide 38 text

storage = $storage; } public function __invoke(DomainEventMessage $message): void { $this->storage->store($message); } }

Slide 39

Slide 39 text

No content