BA Barış Akın~/blog

·24 dk okuma·75 okunma

OxiDB'de OxiMem: Redis protokolünü konuşan bellek-içi anahtar-değer deposu

OxiDB'nin içine RESP konuşan bir anahtar-değer katmanı yazdım: beş veri tipi, TTL, pub/sub, WATCH, Lua, anlık görüntüler. redis-cli ve redis-py ile gerçek oturumlar, Redis 8.6 ile ölçümler ve dürüst sınırlar.

OxiDB'de OxiMem: Redis protokolünü konuşan bellek-içi anahtar-değer deposu

Bir uygulamanın verisi tek tip değil. Siparişler, kullanıcı kayıtları, yazılar kaybolmamalı; bunları doküman motoruna ya da SQL motoruna yazarım, WAL'a girer, fsync beklenir, çökmeden sonra geri gelir. Ama aynı uygulamanın bir de sıcak kenarı var: oturum belirteçleri, sayfa görüntülenme sayaçları, hız sınırı pencereleri, kısa ömürlü iş kuyrukları, önbelleğe alınmış HTML. Bunlar saniyede yüzlerce kez değişir ve kaybolsalar kimse ölmez. Bu veriye dayanıklılık makinesini uygulamak israf. Normalde burada yanına bir Redis kurulur. OxiDB'de ben bunun yerine aynı sürecin içine OxiMem adında bir bellek-içi anahtar-değer katmanı yazdım ve onu Redis'in tel protokolü RESP ile konuşturdum. Sonuç: redis-cli, redis-py, go-redis, node-redis gibi hazır istemciler OxiDB'ye doğrudan bağlanıyor.

Bu yazıda OxiMem'i baştan sona anlatıyorum: nasıl açılır, hangi veri tiplerini ve komutları destekler, süre dolumu, pub/sub, MULTI/EXEC/WATCH, Lua betikleri, kalıcılık seçenekleri, içeride nasıl çalıştığı, Redis'e karşı ölçtüğüm performans ve bellek, ve en önemlisi nerede Redis'ten ayrıldığı. Yazıdaki her çıktı, kendi makinemde (Apple M4, macOS 26.3) çalıştırdığım OxiDB 0.44.2 sunucusundan alındı.

Bir not daha: okuduğunuz bu blog da OxiMem kullanıyor. Blog www.baltavista.com adresinde, sunucusu kendi yazdığım Rill dilinde, verisi OxiDB'de: yazılar ve etiketler SQL ve doküman tarafında, görseller S3 katmanında, sayfanın altında gördüğünüz canlı okunma sayısı ise OxiMem'de bir INCR. Buna yazının sonunda döneceğim.

Neden RESP?

Yeni bir bellek-içi depo yazmanın en kolay kısmı deponun kendisi. Zor kısmı onu dünyaya bağlamak: istemci kütüphaneleri, bağlantı havuzları, komut satırı araçları, çerçeve entegrasyonları. Kendi protokolümü icat etseydim bunların hepsini baştan yazmam gerekirdi. RESP'i konuşunca ekosistem bedavaya geliyor.

RESP'in kendisi de çok sade. Her mesajın ilk baytı türünü söyler: + basit dize, - hata, : tamsayı, $ uzunluk önekli dize, * dizi. Bir istemci komutu her zaman dizelerden oluşan bir dizidir. GET oturum:42 hatta şöyle görünür:

*2\r\n$3\r\nGET\r\n$9\r\noturum:42\r\n

Uzunluk önekli olması çerçeveleme sorununu çözer, ama aynı zamanda bir saldırı yüzeyi açar: uzunluk alanını istemci belirler. OxiMem'in ayrıştırıcısı bu yüzden 16 MiB'yi aşan bir dize ya da dizi bildirimini bellek ayırmadan reddeder (resp.rs içinde MAX_RESP_BULK_LEN).

Açmak: tek bir ortam değişkeni

OxiMem varsayılan olarak kapalı. Açmak için bir port vermek yeterli:

OXIDB_ADDR=127.0.0.1:15600 \
OXIDB_DATA=./data \
OXIDB_OXIMEM_PORT=15602 \
./oxidb-server

Sunucu açılırken şunu yazar:

oxidb-server listening on 127.0.0.1:15600 (pool_size=4, data_dir=./data, idle_timeout=0s)
OxiMem RESP listening on 0.0.0.0:15602 (fast mode)

İki şeye dikkat. Birincisi, adres 0.0.0.0: OxiMem dinleyicisi OXIDB_ADDR'den bağımsız olarak tüm arayüzlere bağlanır. Aşağıda güvenlik bölümünde bunun neden önemli olduğunu anlatacağım. İkincisi, fast mode: varsayılan kipte OxiMem saf bellek-içi çalışır. Diğer seçenekleri (anlık görüntü ve doküman aynası) kalıcılık bölümünde göreceğiz. Port yerine auto yazarsanız çekirdek boş bir port seçer ve OXIDB_READY_FILE dosyasına oximem=<port> satırı düşer; testlerde bu yolu kullanıyorum.

OxiMem doküman motoruna bağlı bir dinleyici sayılıyor, bu yüzden OXIDB_DOC=0 ile (doküman motoru kapalı) çalışan bir sunucu OXIDB_OXIMEM_PORT verilirse açılmayı reddeder ve nedenini söyler.

Bağlanmak için hiçbir şey kurmanıza gerek yok, standart redis-cli yeter:

$ redis-cli -p 15602 PING
PONG

Veri tipleri

OxiMem Redis'in beş temel veri tipini destekler: string, hash, list, set ve sorted set. Bunlara sorted set üzerine kurulu GEO komutlarını ve string üzerinde bit işlemlerini de ekledim.

String ve sayaçlar

En basit hali: anahtar, değer.

127.0.0.1:15602> SET user:42:name "Ayse"
OK
127.0.0.1:15602> GET user:42:name
"Ayse"
127.0.0.1:15602> APPEND user:42:name " Y."
(integer) 7
127.0.0.1:15602> SETNX user:42:name "Other"
(integer) 0
127.0.0.1:15602> GETRANGE user:42:name 0 3
"Ayse"
127.0.0.1:15602> MSET a 1 b 2 c 3
OK
127.0.0.1:15602> MGET a b c missing
1) "1"
2) "2"
3) "3"
4) (nil)
127.0.0.1:15602> GETDEL user:42:name
"Ayse Y."
127.0.0.1:15602> GET user:42:name
(nil)

Sayaçlar anahtar-değer deposunun en çok işe yarayan yüzü, çünkü oku-değiştir-yaz üçlüsünü tek komutta ve atomik olarak yaparlar. İki eşzamanlı istek birbirinin artışını yutamaz:

127.0.0.1:15602> INCR page:views
(integer) 1
127.0.0.1:15602> INCRBY page:views 10
(integer) 11
127.0.0.1:15602> DECR page:views
(integer) 10
127.0.0.1:15602> INCRBYFLOAT price 2.5
"2.5"
127.0.0.1:15602> SET counter abc
OK
127.0.0.1:15602> INCR counter
(error) ERR value is not an integer or out of range
127.0.0.1:15602> SETBIT flags 7 1
(integer) 0
127.0.0.1:15602> BITCOUNT flags
(integer) 1

Değerler bayt olarak saklanır; STRLEN Redis'teki gibi bayt sayar. redis-py ile "Ayşe" yazdığımda STRLEN 5 döndü, çünkü "ş" UTF-8'de iki bayt.

Hash

Bir nesnenin alanlarını tek anahtar altında tutar. HINCRBY ile tek bir alan atomik olarak artırılabilir:

127.0.0.1:15602> HSET user:42 name Ayse city Izmir visits 0
(integer) 3
127.0.0.1:15602> HINCRBY user:42 visits 1
(integer) 1
127.0.0.1:15602> HGETALL user:42
1) "city"
2) "Izmir"
3) "visits"
4) "1"
5) "name"
6) "Ayse"
127.0.0.1:15602> HEXISTS user:42 email
(integer) 0
127.0.0.1:15602> TYPE user:42
hash

Alan sırasının ekleme sırası olmadığına dikkat: hash içi bir HashMap, sıra garanti edilmez (Redis'te de edilmez).

List

Bir VecDeque üzerinde çalışır, yani iki uçtan da O(1) ekleme ve çıkarma. Sağdan itip soldan çekerseniz FIFO kuyruğunuz olur:

127.0.0.1:15602> RPUSH jobs job-1 job-2 job-3
(integer) 3
127.0.0.1:15602> LPUSH jobs job-0
(integer) 4
127.0.0.1:15602> LRANGE jobs 0 -1
1) "job-0"
2) "job-1"
3) "job-2"
4) "job-3"
127.0.0.1:15602> LPOP jobs
"job-0"
127.0.0.1:15602> LMOVE jobs done LEFT RIGHT
"job-1"
127.0.0.1:15602> LRANGE done 0 -1
1) "job-1"

Engelleyen komutlar da var: BLPOP, BRPOP, BZPOPMIN, BLMOVE, BRPOPLPUSH. Bir istemci boş kuyrukta BLPOP queue 5 ile beklerken başka bir istemci bir saniye sonra RPUSH queue job-42 yaptığında ölçtüğüm sonuç:

1) "queue"
2) "job-42"
waited 0.98s

Boş kuyrukta ise süre dolunca (nil) döner (1 saniyelik bekleme 1.02 sn sürdü). Bekleyenler bir Condvar ile uyandırılır; LPUSH, RPUSH, ZADD gibi her liste veya sorted set yazması bu koşul değişkenini tetikler.

Set

Üyelik testi, kesişim, birleşim:

127.0.0.1:15602> SADD online 42 77 91
(integer) 3
127.0.0.1:15602> SISMEMBER online 77
(integer) 1
127.0.0.1:15602> SMISMEMBER online 42 13
1) (integer) 1
2) (integer) 0
127.0.0.1:15602> SADD admins 42 5
(integer) 2
127.0.0.1:15602> SINTER online admins
1) "42"
127.0.0.1:15602> SUNION online admins
1) "42"
2) "5"
3) "77"
4) "91"

Sorted set

Puan tabloları, öncelik kuyrukları, emir defterleri. İçeride iki yapı var: üyeden puana bir HashMap (O(1) ZSCORE) ve (puan, üye) çiftlerinden oluşan bir BTreeSet (sıralı aralık sorguları). Puanlar f64; total_cmp ile tam sıralama yapılır, böylece NaN bile ağacı bozamaz.

127.0.0.1:15602> ZADD scores 1500 ayse 1800 mehmet 1200 zeynep
(integer) 3
127.0.0.1:15602> ZINCRBY scores 400 zeynep
"1600"
127.0.0.1:15602> ZREVRANGE scores 0 2 WITHSCORES
1) "mehmet"
2) "1800"
3) "zeynep"
4) "1600"
5) "ayse"
6) "1500"
127.0.0.1:15602> ZRANGEBYSCORE scores 1550 +inf
1) "zeynep"
2) "mehmet"
127.0.0.1:15602> ZPOPMAX scores
1) "mehmet"
2) "1800"

GEO komutları da sorted set üzerine kurulu, tıpkı Redis'teki gibi; TYPE sorusuna zset cevabı verirler:

127.0.0.1:15602> GEOADD cities 27.1428 38.4237 izmir 28.9784 41.0082 istanbul 32.8597 39.9334 ankara
(integer) 3
127.0.0.1:15602> GEODIST cities izmir istanbul km
"327.4546"
127.0.0.1:15602> GEOSEARCH cities FROMMEMBER izmir BYRADIUS 400 km ASC
1) "izmir"
2) "istanbul"
127.0.0.1:15602> TYPE cities
zset

Daha ciddi coğrafi sorgular (poligonlar, indeksler) için OxiDB'nin coğrafi ve graf sorguları yazısına bakabilirsiniz; buradaki GEO komutları Redis uyumluluğu için.

Süre dolumu (TTL)

Anahtar-değer katmanını doküman motorundan ayıran en karakteristik özellik, verinin kendiliğinden ölebilmesi. Oturum belirtecini silmek için temizlik işi yazmazsınız, ona bir ömür verirsiniz:

127.0.0.1:15602> SETEX session:abc 900 "user=42"
OK
127.0.0.1:15602> TTL session:abc
(integer) 900
127.0.0.1:15602> SET session:xyz "user=7" PX 1500
OK
127.0.0.1:15602> PTTL session:xyz
(integer) 1995
127.0.0.1:15602> EXPIRE user:42 2
(integer) 1
127.0.0.1:15602> PERSIST session:abc
(integer) 1
127.0.0.1:15602> TTL session:abc
(integer) -1
127.0.0.1:15602> TTL no:such:key
(integer) -2

İki buçuk saniye sonra:

127.0.0.1:15602> GET session:xyz
(nil)
127.0.0.1:15602> EXISTS user:42
(integer) 0

Burada iki ayrıntı var. Birincisi user:42 bir hash, ve o da süresi dolunca kayboldu. Eskiden OxiMem'de ömür string girdisinin bir alanıydı ve TTL sessizce yalnızca stringlerde çalışıyordu. 0.43.2'de bunu düzelttim: süre artık anahtarın adına bağlı ayrı bir expires haritasında duruyor ve beş tipin hepsine aynı şekilde uygulanıyor. Redis de aynı ayrımı yapar (anahtar uzayının yanında ayrı bir expires sözlüğü).

İkincisi PTTL session:xyz 1500 değil 1995 döndü. SET ... PX milisaniyeyi yukarı yuvarlayıp saniyeye çeviriyor (ms.div_ceil(1000)), yani 1.5 saniye 2 saniye oluyor. PEXPIRE ise kodda milisaniye hassasiyetinde. Milisaniye kesinliğinde ömre güveniyorsanız bunu bilmeniz gerekir.

Mekanizma iki katmanlı:

  • Tembel süre dolumu: her komut çalışmadan önce, dokunacağı anahtarların süresi geçmiş mi diye bakılır ve geçmişse anahtar o anda silinir. Böylece süresi dolmuş bir değeri asla göremezsiniz. Depoda hiç süreli anahtar yoksa bu kontrol tek bir çekişmesiz okuma kilidine iner.
  • Süpürücü thread: hiç okunmayan anahtarlar da sonsuza dek bellekte kalmasın diye saniyede bir uyanan bir thread süresi dolanları toplu siler. Silinen her anahtar için WATCH sürüm sayacını artırır ve açıksa expired bildirimi yayımlar.

Keyspace bildirimleri

CONFIG SET notify-keyspace-events ile açılır. Açıkken her yazma, mevcut pub/sub üzerinden __keyspace@0__:<anahtar> ve __keyevent@0__:<olay> kanallarına yayımlanır. Bir istemci PSUBSCRIBE '__key*__:*' ile dinlerken SET k1 v EX 1 ve INCR views:demo çalıştırdım; bir saniye sonra süpürücü k1'i sildi:

1) "pmessage"
2) "__key*__:*"
3) "__keyspace@0__:k1"
4) "set"
...
1) "pmessage"
2) "__key*__:*"
3) "__keyspace@0__:views:demo"
4) "incr"
...
1) "pmessage"
2) "__key*__:*"
3) "__keyevent@0__:expired"
4) "k1"

Redis'ten bir fark: OxiMem olay sınıflarını (K, E, g, x...) ayrı ayrı ayırt etmiyor. Boş olmayan herhangi bir değer bildirimleri tamamen açar, boş değer kapatır.

Pub/Sub

SUBSCRIBE, PSUBSCRIBE (glob kalıplarıyla) ve PUBLISH destekleniyor. Bir abone SUBSCRIBE news, diğeri PSUBSCRIBE 'price.*' ile beklerken:

127.0.0.1:15602> PUBLISH price.BTC 68500
(integer) 1
127.0.0.1:15602> PUBLISH news "hello"
(integer) 1
127.0.0.1:15602> PUBLISH nobody x
(integer) 0

Kalıp abonesinin gördüğü:

1) "pmessage"
2) "price.*"
3) "price.BTC"
4) "68500"

PUBLISH'in döndürdüğü sayı mesajı alan abone sayısı; kimse dinlemiyorsa 0. Bu "ateşle ve unut" pub/sub'dır: o anda bağlı olmayan abone mesajı kaçırır. Kalıcı kuyruk istiyorsanız OxiDB'nin MQTT ve AMQP katmanı var.

İlginç bir yan etki: OxiDB'nin MQTT brokeri aynı OxiMemStore pub/sub altyapısını kullanıyor. Bu yüzden MQTT'ye yayımlanan bir mesajı bir Redis istemcisi alabiliyor. redis-cli SUBSCRIBE sensors/temp açıkken mosquitto_pub -p 15603 -t sensors/temp -m 21.5 çalıştırdım:

1) "subscribe"
2) "sensors/temp"
3) (integer) 1
1) "message"
2) "sensors/temp"
3) "21.5"

Bir IoT cihazı MQTT ile ölçüm atıyor, bir web sunucusu redis-py ile bunu dinliyor; arada köprü yazılımı yok.

MULTI, EXEC ve WATCH

MULTI bir kuyruk açar; sonraki komutlar çalışmaz, QUEUED cevabıyla birikir. EXEC geldiğinde hepsi arka arkaya, araya başka bir işlem girmeden çalışır:

127.0.0.1:15602> MULTI
OK
127.0.0.1:15602> SET counter 5
QUEUED
127.0.0.1:15602> INCRBY counter 10
QUEUED
127.0.0.1:15602> EXEC
1) OK
2) (integer) 15

Redis bu yalıtımı tek thread'li olduğu için bedavaya alır. OxiMem çok thread'li, bu yüzden yalıtımı açıkça satın alıyor: EXEC, deponun tx_lock kilidini alır ve yalnızca kendi kuyruğunu boşaltırken tutar. İki EXEC bloğu komutlarını asla birbirine karıştıramaz.

Kuyruğa alma sırasında doğrulama da yapılır. Bilinmeyen bir komut ya da eksik argüman işlemi zehirler ve EXEC hiçbir şey çalıştırmadan reddedilir:

127.0.0.1:15602> MULTI
OK
127.0.0.1:15602> SET z 1
QUEUED
127.0.0.1:15602> NOPE z
(error) ERR unknown command 'NOPE'
127.0.0.1:15602> GET z
QUEUED
127.0.0.1:15602> EXEC
(error) ERR EXECABORT Transaction discarded because of previous errors.
127.0.0.1:15602> GET z
(nil)

SET z 1 hiç çalışmadı. Yazım hatası içeren bir işlem veriye dokunmaz.

WATCH: iyimser kilit

MULTI/EXEC atomikliği verir ama "okuduğum değer hâlâ geçerli mi" sorusunu cevaplamaz. "Bakiyeyi oku, yeterliyse düş" mantığı, okuma ile yazma arasında araya giren biri yüzünden kayıp güncellemeye açıktır. WATCH burada devreye girer: izlenen anahtar EXEC'e kadar değiştiyse işlem çalışmaz ve EXEC null dizi döner. İki istemciyle denedim:

Two redis-cli sessions: client B changes a watched key and client A's EXEC returns nil; DECRBYFLOATGE refuses an overdraft

A, wallet:42'yi izleyip 100 okudu. B araya girip 999 yazdı. A'nın EXEC'i (nil) döndü ve A'nın yazması hiç gerçekleşmedi.

Değişikliğin nasıl anlaşıldığı, tasarımın en sevdiğim kısmı. Değerin kopyası alınmıyor; büyük bir sorted set'i izlemek onu kopyalamak demek olsaydı O(n) maliyet her şeyi öldürürdü. Onun yerine her anahtarın bir mutasyon sayacı var ve WATCH anlık görüntüsü üç küçük sayıdan oluşuyor:

/// O(1) fingerprint of a key for WATCH change detection: the store epoch
/// (bumped by FLUSHALL), the key's mutation counter (bumped by every write
/// command touching it), and whether the key currently exists (so a lazy
/// TTL expiry between WATCH and EXEC still registers as a change).
type KeySnap = (u64, u64, bool);

Her başarılı yazma komutu dokunduğu anahtarların sayacını artırır. FLUSHALL tüm depo için bir epoch sayacını artırır ve böylece tüm WATCH'ları geçersiz kılar. "Var mı" bayrağı da iki okuma arasında sessizce süresi dolan bir anahtarı değişiklik olarak yakalar. EXEC, tx_lock altında bu üçlüleri yeniden hesaplayıp karşılaştırır.

Bilinçli bir asimetri var: sayaç, yazma hiçbir şeyi değiştirmese bile artar (aynı değeri yeniden yazmak gibi). Yani yanlış alarm mümkün, gereksiz bir yeniden deneme olur. Ama kaçırılmış değişiklik mümkün değil. Yanlış alarmın bedeli bir tur, kaçırmanın bedeli bozuk veri.

Sayaç haritası sınırsız büyümesin diye bir de çöp toplama var: harita 100.000 girdiyi aştığında ve o anda hiçbir oturum WATCH tutmuyorsa harita temizlenir (eşik OXIMEM_VERSIONS_GC ile değişir). Kimse izlemiyorsa sayaçların kaybolması bir şeyi bozmaz, bir sonraki WATCH sıfırdan başlar.

redis-py ile çekişme altında WATCH

Teoriyi bir de yük altında denedim. İki hesap, acct:a = 1000 ve acct:b = 0. Sekiz thread, her biri 250 kez "a'dan b'ye 1 aktar" işlemini WATCH + MULTI ile yapıyor; WatchError gelirse yeniden deniyor. Toplam 2000 aktarım denemesi var ama a'da sadece 1000 birim olduğu için 1000'i başarılı olmalı:

def transfer(n):
    c = redis.Redis(host="127.0.0.1", port=15602, decode_responses=True, protocol=2)
    for _ in range(n):
        with c.pipeline() as p:
            while True:
                try:
                    p.watch("acct:a", "acct:b")
                    a = int(p.get("acct:a"))
                    if a < 1:
                        p.unwatch()
                        break
                    p.multi()
                    p.decrby("acct:a", 1)
                    p.incrby("acct:b", 1)
                    p.execute()
                    break
                except WatchError:
                    retries += 1   # (kilitle korunan sayaç)

Sonuç:

after 2000 transfers: a=0 b=1000 sum=1000 watch_retries=3695

Toplam korunuyor, eksiye düşen yok, hiçbir güncelleme kaybolmadı. 3695 yeniden deneme, sekiz thread'in aynı iki anahtar üzerinde ne kadar çekiştiğini gösteriyor; iyimser kilidin bedeli tam olarak bu.

Lua betikleri: EVAL, EVALSHA, SCRIPT

Çekişme yüksekse WATCH döngüsü çok tur atar. Bunun yerine tüm oku-karar ver-yaz mantığını sunucuda tek adımda çalıştırabilirsiniz. OxiMem'e mlua üzerinden Lua 5.4 gömdüm; betikler redis.call ve redis.pcall ile komut çalıştırıyor, cjson da mevcut.

127.0.0.1:15602> SET wallet:7 100
OK
127.0.0.1:15602> EVAL "local b = tonumber(redis.call('GET', KEYS[1]) or '0') if b >= tonumber(ARGV[1]) then return redis.call('DECRBY', KEYS[1], ARGV[1]) end return -1" 1 wallet:7 30
(integer) 70
127.0.0.1:15602> EVAL "...aynı betik..." 1 wallet:7 500
(integer) -1
127.0.0.1:15602> SCRIPT LOAD "...aynı betik..."
"777ac28203607bd0918ff085cb356478607980cb"
127.0.0.1:15602> EVALSHA 777ac28203607bd0918ff085cb356478607980cb 1 wallet:7 20
(integer) 50
127.0.0.1:15602> EVAL "return cjson.encode({n=redis.call('INCR', KEYS[1])})" 1 hits
"{\"n\":1}"

Eşzamanlılık modeli Redis'ten farklı, çünkü burada betikler paralel çalışabiliyor:

  • Her betik bir eval_gate okuma kilidi alır (birçok betik aynı anda çalışabilir), EXEC ise aynı kilidin yazma tarafını alır ve tüm betikleri dışlar.
  • Betik, bildirdiği anahtarların (KEYS) hash'ine göre 128 şerit kilidinden ilgili olanları sıralı olarak alır. Sıralı alındığı için kilitlenme (deadlock) olmaz; farklı anahtarlara dokunan betikler tamamen paralel çalışır.
  • Bildirilmemiş bir anahtara betik içinden dokunmak komut düzeyinde atomik kalır, betik düzeyinde değil. Redis de betiklerde tüm anahtarların KEYS ile bildirilmesini ister; burada bu kural doğrudan kilitlemeyi belirliyor.
  • Lua VM her bağlantı thread'inde bir kez yaratılıp yeniden kullanılıyor (thread_local). Her EVAL'de yeni VM açıp betiği yeniden ayrıştırmak, ölçtüğüm settlement gecikmesinin büyük kısmını yiyordu.
  • Her 20.000 Lua komutunda bir kanca çalışır: betik 200 ms'yi geçerse ya da SCRIPT KILL gelirse durdurulur.

Sonsuz döngüyle denedim: EVAL "while true do end" 0 yaklaşık 200 ms sonra durdu ve sunucu sağlıklı kaldı. Ama burada bir pürüz buldum: hata mesajı Lua'nın çok satırlı yığın izini içeriyor ve RESP basit hata satırları yeni satır içeremez. redis-cli cevabı Error: Bad simple string value diye ayrıştıramadı. Bağlantı ayakta kalıyor, ama bu düzeltilmesi gereken bir hata ve listemde.

DECRBYFLOATGE: Redis'te olmayan tek komut

"Bakiye yeterliyse düş" deseni o kadar yaygın ki, Lua'ya bile gerek bırakmayan yerel bir komut ekledim. DECRBYFLOATGE anahtar miktar: değer miktardan büyük ya da eşitse atomik olarak düşer ve yeni değeri döner; değilse hiçbir şeye dokunmadan nil döner. Yukarıdaki terminal görüntüsünün alt kısmında: 100'den 30 düştü, 200 düşme denemesi reddedildi, bakiye 70'te kaldı. Bir kredi limitini aşan borçlandırma, yarış koşulu altında bile mümkün değil. Bu OxiMem'e özgü; standart Redis istemcilerinde execute_command("DECRBYFLOATGE", ...) gibi ham komut çağrısıyla kullanılır.

Unmodified istemciler: redis-cli ve redis-py

Bu yazıdaki tüm terminal çıktıları Homebrew'dan gelen standart redis-cli 8.6.1 ile alındı. Python tarafında ayrı bir venv'e pip install redis ile redis-py 8.1.0 kurdum ve şu betiği çalıştırdım (kısaltılmış):

import redis

r = redis.Redis(host="127.0.0.1", port=15602, decode_responses=True, protocol=2)

r.set("user:42:name", "Ayşe")
print("get:", r.get("user:42:name"), "strlen:", r.strlen("user:42:name"))
r.hset("user:42", mapping={"name": "Ayşe", "city": "İzmir"})
print("hgetall:", r.hgetall("user:42"))

pipe = r.pipeline(transaction=False)
for _ in range(1000):
    pipe.incr("views:hello")
print("pipeline last:", pipe.execute()[-1])

debit = r.register_script("""
local b = tonumber(redis.call('GET', KEYS[1]) or '0')
if b >= tonumber(ARGV[1]) then return redis.call('DECRBY', KEYS[1], ARGV[1]) end
return -1
""")
r.set("wallet:7", 100)
r.script_load(debit.script)
print("lua debit 30:", debit(keys=["wallet:7"], args=[30]),
      " debit 500:", debit(keys=["wallet:7"], args=[500]))

ps = r.pubsub()
ps.subscribe("news")
ps.get_message(timeout=1)
r.publish("news", "OxiMem says hi")
msg = ps.get_message(timeout=2)
print("pubsub:", msg["channel"], msg["data"])

Çıktı:

get: Ayşe strlen: 5
hgetall: {'name': 'Ayşe', 'city': 'İzmir'}
pipeline last: 1000
after 2000 transfers: a=0 b=1000 sum=1000 watch_retries=3695
lua debit 30: 70  debit 500: -1
pubsub: news OxiMem says hi

Ama dürüst olmam gereken iki nokta var, çünkü ikisini de bu yazı için denerken gördüm:

  1. protocol=2 şart. redis-py 8 bağlanırken varsayılan olarak HELLO 3 gönderip RESP3 istiyor. OxiMem yalnızca RESP2 konuşuyor ve hata dönüyor; redis-py de buna düşmek yerine bağlantıyı kesiyor: redis.exceptions.ResponseError: NOPROTO unsupported protocol version. Tek bir argüman (protocol=2) sorunu çözüyor, ama "hiç değişiklik gerekmez" demek doğru olmaz.
  2. register_script için önce script_load. redis-py önce EVALSHA dener, NoScriptError gelirse betiği yükleyip tekrar dener. OxiMem'in hata yardımcısı her mesajın başına ERR ekliyor, yani cevap -ERR NOSCRIPT ... oluyor ve redis-py bunu NoScriptError olarak tanımıyor. Aynı önek ERR EXECABORT ve ERR NOPROTO mesajlarında da görünüyor. Düzeltmesi küçük (bu kodlar için öneksiz hata), yapılacaklar listemde.

Kalıcılık: üç kip

"Bellek-içi" demek "veri kaybolabilir" demek ve çoğu kullanım için doğru karar bu. Ama "yeniden başlatmayı atlatsın yeter" diyen bir orta yol da var.

1. Hızlı kip (varsayılan)

Hiçbir şey diske yazılmaz. Süreç kapanınca keyspace gider. Önbellek ve oturumlar için ideal.

2. Anlık görüntü: OXIDB_OXIMEM_SNAPSHOT_SECS

Bu değişken verilirse OxiMem açılışta <OXIDB_DATA>/_oximem.snap dosyasını okur ve sonra her N saniyede tüm depoyu bu dosyaya yazar. Yazma "geçici dosyaya yaz, fsync, yeniden adlandır" sırasıyla yapılır; yazmanın ortasında çöken bir sunucu önceki görüntüyü bozamaz. Dosya düz JSON:

{"s":[{"k":"session:s1","v":"x"},{"k":"views:hello","v":"42"}],"h":[{"k":"user:1","m":{"name":"Ayse"}}],"l":[{"k":"q","v":["a","b","c"]}],"t":[],"z":[{"k":"board","z":[["a",10.0],["b",20.0]]}],"x":[["session:s1",1791754990]]}

x bölümü süreli anahtarların mutlak son anlarını tutar, böylece yeniden başlatmadan sonra kalan süre doğru hesaplanır. Sunucuyu OXIDB_OXIMEM_SNAPSHOT_SECS=2 ile çalıştırıp birkaç anahtar yazdım, durdurup yeniden başlattım:

OxiMem RESP listening on 0.0.0.0:15602 (fast mode)
OxiMem snapshot loaded
127.0.0.1:15602> GET views:hello
"42"
127.0.0.1:15602> TTL session:s1
(integer) 593
127.0.0.1:15602> LRANGE q 0 -1
1) "a"
2) "b"
3) "c"

Şimdi sınırı da göstereyim. Bir görüntü alındıktan sonra INCR views:hello yaptım (43 döndü) ve iki saniye dolmadan sunucuyu durdurdum:

127.0.0.1:15602> INCR views:hello
(integer) 43
# sunucu durduruldu ve yeniden başlatıldı
127.0.0.1:15602> GET views:hello
"42"

Son görüntüden sonraki pencere kaybolur. Kapanışta son bir görüntü alınmıyor; normal bir kill (SIGTERM) bile o pencereyi kaybettiriyor. Redis'in AOF'una karşılık gelen bir komut günlüğü de yok. Yani bu kip "yeniden başlatmayı büyük ölçüde atlatmak" için, "hiçbir yazmayı kaybetmemek" için değil.

3. Doküman aynası: OXIDB_OXIMEM_SQL

Adı tarihsel. Bu kip açıkken (OXIDB_OXIMEM_SQL=1) her yazma aynı zamanda doküman motorunun _kv, _hash, _list, _set ve _zset koleksiyonlarına yansıtılır; açılışta bellek bu koleksiyonlardan yeniden kurulur. Sunucu açılırken kipini söyler:

OxiMem RESP listening on 0.0.0.0:15602 (SQL mirroring)

Birkaç anahtar yazdıktan sonra OxiDB'nin kendi Python istemcisiyle (yerel protokol, port 15600) koleksiyonlara baktım:

_kv {"_key": "session:abc", "_value": "{\"user\":42}", "_ttl": 900, "_exp": 1791755332, "_version": 1}
_kv {"_key": "session:def", "_value": "{\"user\":7}", "_version": 1}
_kv {"_key": "views:hello", "_value": "1", "_version": 1}
_hash {"_key": "user:42", "name": "Ayse", "city": "Izmir", "_version": 1}
_list {"_key": "jobs", "_items": ["a", "b"], "_version": 1}
_set {"_key": "tags", "_members": ["rust", "db"], "_version": 1}
_zset {"_key": "board", "_members": [{"member": "b", "score": 20.0}, {"member": "a", "score": 10.0}], "_version": 1}
prefix query: ['session:abc', 'session:def']

Sıcak veri birdenbire sorgulanabilir hale geliyor: find("_kv", {"_key": {"$regex": "^session:"}}) ile tüm oturumları listeleyebilir, aggregation pipeline'ı çalıştırabilirsiniz. Sunucuyu yeniden başlattığımda DBSIZE 7 döndü ve TTL session:abc kalan süreyi (879) doğru verdi.

İsimle ilgili dürüst bir not: kaynak koddaki eski yorum SELECT * FROM _kv ile SQL'den sorgulanabileceğini söylüyor, ama bugünkü SQL motoru ayrı bir crate ve bu koleksiyonları görmüyor. Denedim: sql error: no such table: _kv. Ayna doküman tarafında; SQL tarafından erişim yok. Değişkenin adı, SQL yüzeyinin doküman motorunun içinde olduğu döneme ait.

Bedeli de açık ve büyük: her yazma artık doküman motoruna bir silme ve bir ekleme olarak uğruyor, varsayılan katı kipte her biri fsync'leniyor. Ölçtüm: bu kipte redis-benchmark -t set -n 2000 -c 50 saniyede 72 istek verdi (p50 462 ms). Okumalar etkilenmiyor: aynı kipte GET saniyede 200.803 istek. Yani bu kip yazması seyrek, okuması yoğun ve yeniden başlatmada mutlaka geri gelmesi gereken küçük veri kümeleri için. Bir sayaç için değil.

İçeride: veri yapıları, eşzamanlılık, bellek

OxiMem architecture: RESP listener, per-connection session, dispatcher, five RwLock maps, expiry, versions, Lua gates, pub/sub, and the sweeper, snapshot and mirror threads

Her şey oxidb-server/src/oximem.rs içinde, yaklaşık 6.700 satır. Ana yapı OxiMemStore:

pub struct OxiMemStore {
    strings: RwLock<HashMap<String, KvEntry>>,
    hashes: RwLock<HashMap<String, HashMap<String, String>>>,
    lists: RwLock<HashMap<String, VecDeque<String>>>,
    sets: RwLock<HashMap<String, HashSet<String>>>,
    sorted_sets: RwLock<HashMap<String, SortedSet>>,
    expires: RwLock<HashMap<String, Instant>>,
    // pubsub, psubs, tx_lock, eval_gate, eval_stripes, versions, epoch, ...
}

JSON yok, WAL yok, indeks yok: komutlar doğrudan standart Rust koleksiyonları üzerinde çalışıyor.

Bağlantı modeli. Her TCP bağlantısı için bir OS thread'i açılıyor (TCP_NODELAY ile). Thread, RESP değerlerini okuyup bağlantıya özel Session üzerinden (MULTI kuyruğu, WATCH listesi) komutu çalıştırıyor. Redis'in tek thread'li olay döngüsünün tersine burada farklı bağlantılar gerçekten paralel çalışıyor ve beş tip haritası ayrı RwLock'larla korunuyor: bir GET ile bir HSET birbirini beklemiyor.

Pipeline hızlı yolu. Okuma tamponunda birden fazla komut birikmişse (istemci pipeline kullanıyorsa) hepsi bir grup olarak alınıyor. Gruptaki komutların hepsi aynıysa ve bu komut GET, SET, INCR, LPUSH, RPUSH, HSET veya PING ise, ilgili kilit bir kez alınıyor ve tüm grup onun altında çalışıyor; cevaplar tek bir flush ile gidiyor. Karışık gruplar ya da işlem içeren gruplar tek tek, durumlu yoldan çalışıyor. Aşağıdaki ölçümde ZADD'nin pipeline'da geride kalmasının nedeni bu: hızlı yol listesinde yok.

Kilit sırası. expires haritası en içteki kilit. Bir tip haritası tutulurken alınabilir, ama expires tutulurken bir tip haritası asla alınmaz. Süreyi değiştiren her komut önce tip haritalarında anahtarın varlığını kurar, sonra expires'a yazar; sıra doğal olarak bundan çıkıyor. Kodda bu kural yorum olarak her kritik yerde tekrar ediliyor, çünkü bir kez ihlal edilirse ortaya çıkan şey nadir ve çok zor yakalanan bir kilitlenme olur.

Sorted set. HashMap<String, f64> + BTreeSet<(Score, String)>. Ekleme ve silme O(log n), puan sorgusu O(1), aralıklar ağaçta sıralı yürüme. Zayıf nokta: ZRANK ağacı baştan sayarak buluyor, yani O(n). Redis'in skip list'i her düğümde genişlik tuttuğu için bunu O(log n) yapar. Milyonlarca üyeli bir sette sık ZRANK çağırıyorsanız fark edersiniz.

Bellek. Bunu da ölçtüm. İki sunucuyu temiz başlatıp redis-cli --pipe ile her birine bir milyon SET key:NNNNNNN <32 baytlık değer> yükledim:

Başlangıç RSS1M anahtar sonrası RSSArtışAnahtar başına
Redis 8.6.16,8 MB107,9 MB101 MB~101 bayt
OxiDB 0.44.2 (OxiMem)14,6 MB169,6 MB155 MB~155 bayt

OxiMem aynı veri için Redis'ten yaklaşık 1,5 kat fazla bellek kullanıyor. Neden belli: her anahtar ayrı bir String (24 baytlık başlık + yığında ayrı ayırma), değer de öyle, üstüne HashMap girdisi. Redis küçük dizeler için gömülü kodlamalar (embstr) ve sıkı yapılar kullanıyor. OxiMem'in başlangıç RSS'i de daha yüksek, çünkü bu süreç aynı zamanda tam bir doküman veritabanı. Bir maxmemory sınırı ya da LRU tahliyesi de yok: bellek dolarsa süreç büyümeye devam eder. Büyüklüğü sizin sınırlamanız gerekiyor (TTL'ler, anahtar sayısı).

Tek keyspace. OxiDB çok veritabanlı bir motor, ama OxiMem'in anahtar uzayı bilinçli olarak küresel (ADR-0012). SELECT 3 OK döner ama bir şey yapmaz; hangi veritabanına bağlı olursanız olun aynı keyspace'e yazarsınız. Oturum belirteçleri ve sayaçlar mantıksal veritabanı sınırlarını umursamıyor; ayrım isteyen anahtar öneki kullanıyor (musteri:7:oturum:abc). Cluster modunda da OxiMem Raft ile çoğaltılmıyor, her düğümün kendi keyspace'i var.

Komut yüzeyi

README "50+ komut" diyor; bu artık fazlasıyla mütevazı. Dağıtıcıda yaklaşık 135 komut adı var:

AileKomutlar
Bağlantı / sunucuPING ECHO QUIT SELECT AUTH HELLO CLIENT COMMAND INFO CONFIG DBSIZE FLUSHDB FLUSHALL
StringSET (EX/PX/NX/XX) GET GETSET SETNX SETEX PSETEX MSET MGET APPEND STRLEN GETRANGE SETRANGE GETDEL GETEX
SayaçINCR DECR INCRBY DECRBY INCRBYFLOAT DECRBYFLOATGE
BitSETBIT GETBIT BITCOUNT
Anahtar / TTLDEL EXISTS TYPE KEYS SCAN RANDOMKEY RENAME COPY EXPIRE PEXPIRE EXPIREAT PEXPIREAT PERSIST TTL PTTL
HashHSET HMSET HSETNX HGET HMGET HGETALL HDEL HEXISTS HKEYS HVALS HLEN HINCRBY HRANDFIELD HSCAN
ListLPUSH RPUSH LPOP RPOP LLEN LRANGE LINDEX LSET LREM LTRIM LMOVE RPOPLPUSH LMPOP
SetSADD SREM SMEMBERS SISMEMBER SMISMEMBER SCARD SPOP SRANDMEMBER SINTER SUNION SDIFF SINTERSTORE SUNIONSTORE SDIFFSTORE SSCAN
Sorted setZADD ZREM ZSCORE ZCARD ZCOUNT ZINCRBY ZRANK ZREVRANK ZRANGE ZREVRANGE ZRANGEBYSCORE ZREVRANGEBYSCORE ZRANGEBYLEX ZPOPMIN ZPOPMAX ZREMRANGEBYRANK ZREMRANGEBYSCORE ZUNIONSTORE ZINTERSTORE ZMPOP ZSCAN
GEOGEOADD GEOPOS GEODIST GEOHASH GEOSEARCH
EngelleyenBLPOP BRPOP BZPOPMIN BLMOVE BRPOPLPUSH
Pub/SubPUBLISH SUBSCRIBE PSUBSCRIBE UNSUBSCRIBE PUNSUBSCRIBE
İşlemMULTI EXEC DISCARD WATCH UNWATCH
BetikEVAL EVALSHA SCRIPT (LOAD, EXISTS, FLUSH, KILL)

Olmayanlar da önemli. Denediğim ve unknown command alanlar: UNLINK, MSETNX, HINCRBYFLOAT, ZRANGESTORE, OBJECT, WAIT, SAVE, BGSAVE. Stream'ler (XADD ailesi) ve HyperLogLog (PFADD) yok. ACL, SLOWLOG, MEMORY yok. CONFIG büyük ölçüde bir kabuk: yalnızca notify-keyspace-events gerçekten bir şey yapıyor, diğer CONFIG GET istekleri boş değer, CONFIG SET istekleri OK dönüyor. AUTH, SELECT, CLIENT, COMMAND istemcilerin el sıkışmasını bozmamak için OK dönüyor.

INFO da kısa:

# Server
oxidb_version:0.44.2
oximem_mode:raw
resp_compat:true

# Keyspace
db0:keys=19,expires=0

Performans: Redis'e karşı

Redis uyumlu bir katmanın tek anlamlı ölçütü Redis'in kendisi. Aynı makinede (Apple M4, 10 çekirdek, 24 GB, macOS 26.3) Homebrew'dan gelen Redis 8.6.1'i (--save "" --appendonly no, yani kalıcılık kapalı) ve OxiMem'i hızlı kipte yan yana çalıştırdım. İki sunucu da localhost'ta, redis-benchmark 8.6.1 ile: 200.000 istek, 50 istemci, 64 baytlık değerler, önce pipeline'sız, sonra 16'lık pipeline ile.

redis-benchmark results for Redis 8.6.1 and OxiMem, without pipelining and with a pipeline of 16

Okurken dikkat edilmesi gerekenler:

  • Pipeline'sız (-P 1) iki sunucu da saniyede yaklaşık 180-245 bin istek bandında ve fark gürültü düzeyinde. Testi ikinci kez çalıştırdığımda sıralama bazı komutlarda yer değiştirdi: Redis GET'te 179 binden 239 bine çıktı, OxiMem INCR'de 230 binden 185 bine düştü. Bu modda darboğaz sunucu değil, localhost üzerindeki gidiş-dönüş.
  • Pipeline'lı (-P 16) OxiMem çoğu komutta önde (SET 2,9M'e karşı 2,3M, HSET 2,9M'e karşı 1,4M). Nedeni yukarıda anlattığım iki şey: 50 bağlantı ayrı thread'lerde paralel çalışıyor ve aynı komuttan oluşan gruplar tek kilit altında işleniyor. Redis tek thread'de kalıyor. İkinci çalıştırmada da tablo aynıydı.
  • Geride kaldığı yerler: PING (2,4M'e karşı 3,2M) ve özellikle ZADD (1,4M'e karşı 1,8M; ikinci çalıştırmada 1,35M'e karşı 2,0M). ZADD pipeline hızlı yolunda değil, her komut tek tek dağıtıcıdan geçiyor. MSET (10 anahtar) da pipeline'da geride (393 bine karşı 473 bin).
  • Bunlar tek makinede, localhost'ta, birer ikişer çalıştırmalık ölçümler. Gerçek ağda, farklı çekirdek sayısında ve farklı yükte sonuçlar değişir. Özellikle Redis'in çok thread'li G/Ç ayarlarını (io-threads) açmadım.

Kısacası: OxiMem ham hızda Redis ile aynı ligde ve çok çekirdeği daha iyi kullanabiliyor; buna karşılık daha fazla bellek harcıyor. Ama benim için asıl kazanç sayılar değil, bu hızın aynı süreçte, doküman ve SQL motorunun yanı başında olması: ayrı bir Redis kurmak, izlemek ve sürümlemek gerekmiyor.

Sınırlar ve dürüst ödünleşimler

Bir aracın sınırlarını bilmemek, onu yanlış yerde kullanmanın en kısa yolu. OxiMem'in bugünkü sınırları:

Tipler arası WRONGTYPE kontrolü yok. Beş tip ayrı haritalarda yaşadığı için aynı isim aynı anda hem string hem set olabiliyor. Redis'te bir set üzerinde INCR WRONGTYPE hatası verir; OxiMem'de ise yanına bir string yaratır:

127.0.0.1:15602> INCR online
(integer) 1
127.0.0.1:15602> TYPE online
string
127.0.0.1:15602> SCARD online
(integer) 3
127.0.0.1:15602> DEL online
(integer) 1
127.0.0.1:15602> EXISTS online
(integer) 1

DEL stringi sildi, ama set hâlâ orada. Anahtar adlarına tip öneki koymak (set:online, cnt:online) bunu pratikte önler, ama Redis'ten gelen biri bunu beklemez. Düzeltilmesi gereken bir uyumsuzluk.

Kimlik doğrulama yok, dinleyici tüm arayüzlerde. AUTH her şeye OK der. OxiDB'nin SCRAM kimlik doğrulaması ve rol tabanlı erişim denetimi OxiMem portunu kapsamıyor. Port 0.0.0.0'a bağlandığı için tek doğru kullanım şu: OxiMem portunu güvenlik duvarıyla dışarıya kapatın, yalnızca aynı makineden ya da güvenilir özel ağdan erişilsin. Bu blogda da öyle. Kimlik doğrulama bu katmanın en önemli eksiği.

RESP3 yok. HELLO 3 hata alır; bazı istemcilerde (redis-py 8) bunun için protocol=2 vermeniz gerekir.

Hata önekleri. NOSCRIPT, EXECABORT ve NOPROTO mesajları başta ERR ile geliyor; istemcilerin bu kodlara göre davranan mantığı (redis-py register_script gibi) çalışmıyor.

Kayıpsız kalıcılık yok. AOF benzeri bir günlük yok, kapanışta son görüntü alınmıyor. Doküman aynası kalıcı ama yazmalarda çok yavaş.

Bellek sınırı ve tahliye yok, Redis'ten ~1,5 kat fazla bellek. Çoğaltma yok: keyspace düğüm yerel.

Peki ne zaman OxiMem, ne zaman başka bir şey? Karar tek bir soruyla verilir: bu veriyi kaybetsem ne olur? Cevap "hiçbir şey, yeniden hesaplanır ya da zaten kısa ömürlü" ise (oturumlar, önbellekler, sayaçlar, hız sınırları, kısa kuyruklar, canlı fiyatlar) yer OxiMem. Cevap "para kaybederim, denetim kaydım eksilir" ise yer doküman motoru, SQL motoru ya da ledger motoru. En güçlüsü ikisini birlikte kullanmak: sıcak yol hızlı, soğuk yol dayanıklı.

Bu blogda OxiMem

Bu blogun sunucusu Rill'de yazılı ve OxiDB'ye üç ayrı protokolle bağlanıyor: yazılar ve etiketler için yerel protokol (SQL ve doküman), görseller için S3, sıcak veri için RESP. OxiMem'de duranlar:

  • Okunma sayaçları. Bir yazı açıldığında sunucu INCR views:<slug> çalıştırıyor ve dönen sayıyı sayfadaki bir yer tutucunun yerine koyuyor. Yani sayfanın altında gördüğünüz sayı tam o anda OxiMem'den geldi. Botlar sayacı artırmıyor, yalnızca GET ile okuyor.
  • Sayfa önbelleği. Render edilmiş HTML, beş dakikalık ömürle (SETEX ... 300) page:<dil>:post:<slug> anahtarlarında duruyor. Önbellekte varsa sayfa X-Cache: hit başlığıyla doğrudan oradan gidiyor; yoksa SQL'den render edilip OxiMem'e yazılıyor. Bir yazı düzenlendiğinde KEYS page:* ile tüm sayfa anahtarları siliniyor; blog seyrek yazılıp sık okunduğu için bu yeterince ucuz.
  • Oturumlar ve hız sınırlama. Yönetim paneli oturumu yedi günlük ömürle bir sess:<belirteç> anahtarı. Yorum ve giriş denemeleri sabit pencereli hız sınırından geçiyor: ilk INCR 1 döndürünce anahtara EXPIRE ile pencere süresi veriliyor, sayı sınırı aşarsa istek reddediliyor, pencere bitince anahtar kendiliğinden ölüyor.

Ayrıntılı analitik ise OxiMem'de değil: her okuma, kendi strand'inde (Rill'in hafif iş parçacığı) doküman motorundaki events koleksiyonuna da yazılıyor. Böylece sayfa doküman motorunun fsync'ini beklemiyor, ama analitik kalıcı. Yönetim panelindeki toplam okunma sayısı da OxiMem'den değil, bu koleksiyonun sayımından geliyor. OxiMem'deki sayaç bir yeniden başlatmada kaybolsa bile gerçek kayıt events koleksiyonunda duruyor. Bu yazının "bu veriyi kaybetsem ne olur" sorusunun canlı bir örneği: hızlı sayı OxiMem'de, gerçeğin kaydı doküman motorunda.

Denemek için

OxiDB tek bir ikili dosya. GitHub releases sayfasından işletim sisteminize uygun oxidb-server'ı indirin ya da depodaki Dockerfile ile bir Docker imajı oluşturun, sonra:

OXIDB_OXIMEM_PORT=6380 OXIDB_OXIMEM_SNAPSHOT_SECS=30 ./oxidb-server
redis-cli -p 6380 PING

Elinizdeki herhangi bir Redis istemcisi çalışacaktır (redis-py 8 için protocol=2 ile). Listemde sıradakiler belli: tipler arası WRONGTYPE kontrolü, ERR öneksiz hata kodları, çok satırlı Lua hatalarının düzeltilmesi, kapanışta son anlık görüntü ve kimlik doğrulama. Serinin bir sonraki yazısında OxiDB'nin S3 uyumlu nesne deposunu anlatıyorum.

BA
Barış Akın

25 yılı aşkın deneyime sahip yazılım çözümleri mimarı. FinTech ve dağıtık sistemler; OxiDB, Rill ve Cobra'nın geliştiricisi.