Pular para o conteúdo
Esta página ainda não foi traduzida. O conteúdo abaixo está em inglês. Ler em inglês

Database

Redis

Cache, sessions, queues, a shared rate limiter, locks and pub/sub on Redis or Valkey.

Última atualização:

Redis (or its open-source fork Valkey) is an in-memory key-value server. Vukem uses it as a shared backend: when your app runs on more than one server, the cache, sessions, queue, rate limits and locks must live somewhere every server can see. One Redis gives you all five. The step-by-step guide at the end starts from zero.

Terminal
go get github.com/AlexGalhardo/vukem/drivers/redis
Go
rc, err := redis.Open(ctx, os.Getenv("REDIS_URL")) // redis://localhost:6379/0, rediss:// for TLS

app, err := vukem.New(
	vukem.WithCacheStore(rc.CacheStore("cache:")), // also backs locks
	vukem.WithSessionStore(rc.SessionStore("")),   // "session:" keys with TTL
	vukem.WithQueueDriver(rc.Queue(redis.QueueOptions{})),
	// ...
)
app.AddHealthCheck("redis", rc.Ping)

Reading REDIS_URL from the app configuration#

os.Getenv above runs before vukem.New loads .env, .env.local and the sources vukemtest passes. To read the URL from app.Source, use the lazy options: each function runs on the first app.Cache(), app.Sessions() or app.Queue() call, after configuration loaded, and its error is returned by that call (DefaultMiddleware builds sessions at boot, so a bad URL is still a boot error).

Go
var (
	once  sync.Once
	rc    *redis.Client
	rcErr error
)

client := func(app *vukem.App) (*redis.Client, error) {
	once.Do(func() {
		if rc, rcErr = redis.Open(context.Background(), app.Source.Get("REDIS_URL")); rcErr != nil {
			return
		}

		app.AddHealthCheck("redis", rc.Ping)
		app.OnShutdown(func(context.Context) error { return rc.Close() })
	})

	return rc, rcErr
}

app, err := vukem.New(
	vukem.WithCacheStoreFunc(func(app *vukem.App) (cache.Store, error) {
		c, err := client(app)
		if err != nil {
			return nil, err
		}

		return c.CacheStore("cache:"), nil
	}),
	vukem.WithSessionStoreFunc(func(app *vukem.App) (session.Store, error) {
		c, err := client(app)
		if err != nil {
			return nil, err
		}

		return c.SessionStore(""), nil
	}),
	vukem.WithQueueDriverFunc(func(app *vukem.App) (queue.Driver, error) {
		c, err := client(app)
		if err != nil {
			return nil, err
		}

		return c.Queue(redis.QueueOptions{}), nil
	}),
)

The last of WithCacheStore / WithCacheStoreFunc (and likewise for sessions and queues) wins; SetSessionStore from a provider still overrides both. A function that returns nil, nil is an error. A function must not call the accessor it feeds (app.Cache() inside WithCacheStoreFunc, app.Sessions() or DefaultMiddleware inside WithSessionStoreFunc, app.Queue() inside WithQueueDriverFunc): it runs inside that accessor's sync.Once and would deadlock.

FeatureAPI
Cache storerc.CacheStore(prefix) implements cache.Store; Flush deletes only the prefix (SCAN + DEL) and refuses an empty one; it declares cache.Sharer (Shared() is true), so OnOneServer() and RATE_LIMIT_STORE=cache accept it. Upgrade drivers/redis together with core: a driver release without Shared() is treated as not shared and refused
Sessionsrc.SessionStore(prefix) implements session.ListStore: RevokeOthers, UserSessions and RevokeSession work as on the database store. Keys hold SHA-256 of the session id, never the cookie value; sessions saved under the clear id by older versions move to the hashed key on their first load. Each session records its owner (<prefix>owner:<hash>) and each user a set of session hashes (<prefix>user:<owner>); every command touches one key (Redis Cluster works), so listing or revoking costs three round trips per session of the user; needs Redis 6.2+
Queuerc.Queue(redis.QueueOptions{Prefix, Grace}): sorted sets under {queue}: (one Cluster slot), atomic Lua reservation, failed jobs for queue:failed, queue:retry, queue:forget
Rate limitingrc.RateLimiter("api:", 60, time.Minute) implements ratelimit.Limiter with GCRA, shared by every server
Lockscache.New(rc.CacheStore("locks:"), "").NewLock(name, ttl)
Pub/subrc.Publish(ctx, channel, b), msgs, err := rc.Subscribe(ctx, channels...)
  • Works with Redis 7+ and Valkey 8+; CI runs the cachetest and queuetest conformance suites on both.
  • Open pings the server, so the app does not boot while Redis is down; a malformed URL always fails. redis.OpenLazy(url) only parses the URL and connects on first use, so the process starts and rc.Ping reports the outage in a readiness check (app.AddHealthCheck). go-redis retries a failed command 3 times with backoff, so that Ping takes seconds while Redis is down; for a fast readiness answer set ?dial_timeout=1s&max_retries=-1 on the URL (or use a separate client with those options for the check).
  • redis.New(client) wraps an existing go-redis client (Cluster, Sentinel, custom options); rc.Redis() returns it.

Step by step: sessions and cache on Redis#

1. Start Redis locally

compose.yaml
services:
  redis:
    image: redis:7.4
    ports: ["6379:6379"]
Terminal
docker compose up -d redis

2. Add the driver and the URL

Terminal
go get github.com/AlexGalhardo/vukem/drivers/redis
.env
REDIS_URL=redis://localhost:6379/0

3. Plug Redis into the app

Use the lazy options shown in Reading REDIS_URL from the app configuration, so the URL comes from .env and from vukemtest sources. For a quick start, the eager form is enough:

bootstrap/app.go
rc, err := redis.Open(ctx, os.Getenv("REDIS_URL")) // fails when Redis is down
if err != nil {
	return nil, err
}

app, err := vukem.New(
	vukem.WithCacheStore(rc.CacheStore("myapp:cache:")),
	vukem.WithSessionStore(rc.SessionStore("myapp:session:")),
	// ...
)
if err != nil {
	return nil, err
}

app.AddHealthCheck("redis", rc.Ping)
app.OnShutdown(func(context.Context) error { return rc.Close() })

4. Share rate limits

.env
RATE_LIMIT_STORE=cache   # limiters count in app.Cache(), now Redis

Or use the Redis GCRA limiter directly: ratelimit.Middleware(rc.RateLimiter("login:", 5, time.Minute), ratelimit.ByIP).

5. Check that it works

Terminal
vukem serve
curl -s -c jar.txt http://localhost:3000/ > /dev/null   # creates a session
docker compose exec redis redis-cli --scan --pattern 'myapp:*'
curl -s 'http://localhost:3000/up?checks=1'               # "redis" is reported

Pub/sub#

Go
msgs, err := rc.Subscribe(ctx, "orders") // the channel closes when ctx ends
if err != nil {
	return err
}

go func() {
	for b := range msgs {
		log.Printf("order event: %s", b)
	}
}()

err = rc.Publish(ctx, "orders", []byte(`{"id":42}`))

Pub/sub is fire-and-forget: subscribers that are offline miss messages. Use the queue for work that must not be lost.

Pitfalls#

  • One Redis, several apps. Give every store its own prefix per app (myapp:cache:, myapp:session:), or two apps read each other's keys.
  • Eviction policies. A Redis configured with maxmemory-policy allkeys-lru may evict sessions and queued jobs under memory pressure. Use noeviction (or volatile-lru) for a Redis that holds sessions or queues, or run a separate one for the cache.
  • os.Getenv before .env loads. The eager form reads the process environment only; use the lazy With...Func options to read app.Source.
  • Slow readiness checks while Redis is down. go-redis retries; add ?dial_timeout=1s&max_retries=-1 to the URL used by the health check.
  • Persistence. A default Redis may lose recent writes on restart. Queues and sessions need AOF or RDB persistence enabled (managed Redis usually has it).

Security notes#

  • Never expose Redis to the internet. Bind it to a private network, require a password (redis://:password@host:6379/0) and use rediss:// (TLS) across networks.
  • Keep the password in an environment variable, never in code or in a committed .env.
  • Sessions in Redis hold only session data; still, anyone with Redis access can read or forge sessions, so treat Redis credentials like database credentials.