pgec reads: memory replicated cache, writes: PostgreSQL with a Redis API

6 minute read

pgec is an Apache licensed real-time in memory PostgreSQL logical replication cache with Redis, Memcached and REST API. Mutating operations (e.g., DEL or HSET) update PostgreSQL directly. Read-only operations (e.g., HGET or EXISTS) are serviced directly by the cache. Streaming replication ensures that the cache remains consistent with any changes on the underlying PostgreSQL database in real-time. Change notifications can also be sent to clients using PSUBSCRIBE.

Each replicated PostgreSQL table is represented as a Redis Hash. The primary or compound key of the table is used as the key for the hash. Columns are represented as fields in the hash. Only those fields matching the column names present in the table may be stored or retrieved.

High Level Architecture

Read only operations such as EXISTS, HGETALL or HGET are handled directly by the in memory ETS cache. These commands ultimately call ets:lookup/2, with the resultant tuple converted into a (typically) string representation for Redis.

Mutating operations, such as DEL or HSET automatically write to PostgreSQL (after checking the cache to determine whether an insert or update is the appropriate SQL statement to use). Streaming replication updates the in memory cache with the updated values from the database.

The real-time replication stream from PostgreSQL is also published to subscribers with table level granularity. For example, a subscription to __key*__:pub.grades.* will result in any changes to the grades table being published to that subscriber. Internally erlang message passing is used with a process group per table. Row level granularity for subscriptions is not currently implemented, but is something being considered.

In this example we will be using the grades sample data. The data is loaded as part of the initialisation of PostgreSQL, in the docker compose used by the pgec repository.

redis api

Star

The details of cloning the repository and using compose to bring all the services up are detailed in this previous article.

In particular, we will be looking at this entry for Betty Rubble and her grade:

"Rubble","Betty","234-56-7890",44.0,90.0,80.0,90.0,46.0,"C-"

Looking at the current state of the row in SQL:

 select grade from grades where ssn='234-56-7890'
 grade 
-------
 C-
(1 row)

Using the Redis API in pgec to examine the same data:

➜ redis-cli hget pub.grades.234-56-7890 grade
C-

HGET uses the pgec in memory cache, and does not result in a query to PostgreSQL for this data.

In a separate terminal we can setup a Redis subscription to monitor changes to the grades table as follows:

➜ redis-cli psubscribe '__key*__:pub.grades.*'
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__key*__:pub.grades.*"
3) (integer) 1

We can use HSET to update Betty’s grade to a C:

➜ redis-cli hset pub.grades.234-56-7890 grade C
(integer) 1

The (integer) 1 is telling us that one row was updated, pgec has issued a SQL statement to update Betty’s grade in PostgreSQL.

In the subscription terminal we can also see a notification telling us that the key pub.grades.234-56-7890 has been set:

➜ redis-cli psubscribe '__key*__:pub.grades.*'
...
1) "message"
2) "__keyspace@0__:pub.grades.234-56-7890"
3) "set"

We can confirm the updated data in PostgreSQL with:

select grade from grades where ssn='234-56-7890'
 grade 
-------
 C
(1 row)

Logical streaming replication from PostgreSQL into pgec also means that the cache is updated in real-time:

➜ redis-cli hget pub.grades.234-56-7890 grade
C

As well as updates to existing data, pgec will also insert new data with HSET.

➜ redis-cli hset pub.grades.321-21-4321 last Zella first Xiggy test1 47 test2 68 test3 54 test4 73 final 23 grade D+
(integer) 1

The subscription terminal, pings with a new message:

➜ redis-cli psubscribe '__key*__:pub.grades.*'
...
1) "message"
2) "__keyspace@0__:pub.grades.321-21-4321"
3) "set"

Looking at the inserted row in PostgreSQL:

 select * from grades where ssn='321-21-4321'
 last  | first |     ssn     | test1 | test2 | test3 | test4 | final | grade 
-------+-------+-------------+-------+-------+-------+-------+-------+-------
 Zella | Xiggy | 321-21-4321 |    47 |    68 |    54 |    73 |    23 | D+
(1 row)

The Redis protocol typically uses strings for most values, pgec understands the underlying SQL types being used and converts appropriately (text and floats in this example).

Replication means that the pgec cache remains consistent with the recently inserted data:

➜ redis-cli hgetall pub.grades.321-21-4321
 1) "test4"
 2) "73"
 3) "test3"
 4) "54"
 5) "test2"
 6) "68"
 7) "test1"
 8) "47"
 9) "ssn"
10) "321-21-4321"
11) "last"
12) "Zella"
13) "grade"
14) "D+"
15) "first"
16) "Xiggy"
17) "final"
18) "23"

(note the strings being used for some of the types that are actually floats).

Updates can be from any source, not just through pgec, so if a another process updates this record directly in PostgreSQL:

➜ update grades set grade='D' where ssn='321-21-4321'
UPDATE 1

We would receive a notification of the change in our Redis subscription:

➜ redis-cli psubscribe '__key*__:pub.grades.*'
...
1) "message"
2) "__keyspace@0__:pub.grades.321-21-4321"
3) "set"

With pgec remaining consistent with PostgreSQL too, the HGET is reading from the cache (it has been updated by streaming replication):

➜ redis-cli hget pub.grades.321-21-4321 grade
D

A DEL will result in pgec issuing a delete statement to PostgreSQL for the affected row.

➜ redis-cli del pub.grades.321-21-4321
(integer) 1

Together with a notification to our subscription:

➜ redis-cli psubscribe '__key*__:pub.grades.*'
...
1) "message"
2) "__keyspace@0__:pub.grades.321-21-4321"
3) "del"

Which is reflected in table:

 select * from grades where ssn='321-21-4321'
 last | first | ssn | test1 | test2 | test3 | test4 | final | grade 
------+-------+-----+-------+-------+-------+-------+-------+-------
(0 rows)

And also in our cache:

➜ redis-cli exists pub.grades.321-21-4321
(integer) 0

Commands

The following Redis commands are currently implemented by pgec:

del

Delete a key.

redis-cli del pub.grades.321-21-4321
(integer) 1

exists

Determine if a key exists.

$ redis-cli exists pub.grades.234-56-7890
(integer) 1

hexists

Determine if a hash field exists.

$ redis-cli hexists pub.grades.234-56-7890 test1
(integer) 1

$ redis-cli hexists pub.grades.234-56-7890 test6
(integer) 0

hget

Get the value of a hash field stored in memory.

$ redis-cli hget pub.grades.234-56-7890 test1
"44.0"

Returns nil when the key is not present, or the field is not present in the hash.

$ redis-cli hget pub.grades.234-56-7890 test6
(nil)

hgetall

Get all fields and values in a hash for a key stored in memory.

$ redis-cli hgetall pub.grades.234-56-7890
 1) "test4"
 2) "90.0"
 3) "test3"
 4) "80.0"
 5) "test2"
 6) "90.0"
 7) "test1"
 8) "44.0"
 9) "ssn"
10) "234-56-7890"
11) "last"
12) "Rubble"
13) "grade"
14) "C-"
15) "first"
16) "Betty"
17) "final"
18) "46.0"

hkeys

Get all the fields in a hash.

$ redis-cli hkeys pub.grades.234-56-7890
1) "test4"
2) "test3"
3) "test2"
4) "test1"
5) "ssn"
6) "last"
7) "grade"
8) "first"
9) "final"

An empty array is returned when the key is not present.

$ redis-cli hkeys pub.grades.234-56-7823
(empty array)

hlen

Get the number of fields in a hash.

$ redis-cli hlen pub.grades.234-56-7890
(integer) 9

Zero is returned when the key is not present.

$ redis-cli hlen pub.grades.234-56-8790 
(integer) 0

hset

Set the string value of a hash field.

insert

A SQL statement is executed by pgec to insert the row into the underlying PostgreSQL table. The inserted row is replicated updating the in memory cache.

redis-cli hset pub.grades.321-21-4321 last Zella first Xiggy test1 47 test2 68 test3 54 test4 73 final 23 grade D+

update

A SQL statement is executed by pgec to update the row in the underlying PostgreSQL table. The updated row is replicated updating the in memory cache.

$ redis-cli hset pub.grades.234-56-7890 test1 45.0
(integer) 1

Zero is returned if the field is not a member of the hash.

psubscribe

Subscriptions are supported at present to table level granularity.

$ redis-cli psubscribe '__key*__:pub.grades.*'

Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__key*__:pub.grades.*"
3) (integer) 1

In another shell update one of the grades:

redis-cli hset pub.grades.234-56-7890 test1 45.0
(integer) 1

Or via SQL:

docker compose exec \                           
    --no-TTY \   
    postgres \
    psql \
    --command="update grades set grade='C' where ssn='234-56-7890'"

A message will be streamed to the subscriber detailing the affected key.

1) "message"
2) "__keyspace@0__:pub.grades.234-56-7890"
3) "set"

Watch

Discuss

Follow @shortishly