go-pg-cache
is a specialized caching library for Go that uses PostgreSQL as a backend. It's designed to cache a
limited subset of a table that is frequently accessed, providing significant performance improvements for repeated
queries on the same data.
The library leverages PostgreSQL's LISTEN/NOTIFY mechanism to handle cache invalidation across multiple nodes, ensuring consistency in distributed environments.
⭐️ Star This Project ⭐️
If you find this project helpful, please give it a star on GitHub! Your support is greatly appreciated.
- Purpose and Ideal Use Cases
- Features
- Installation
- Usage
- Configuration
- Examples
- Performance Considerations
- Contributing
- License
The primary purpose of go-pg-cache
is to provide a caching layer for frequently accessed, relatively static data in
PostgreSQL tables. It's particularly useful in scenarios where:
- A subset of the table is accessed much more frequently than the rest.
- The data doesn't change very often.
- The same queries are repeated multiple times.
Ideal use cases include:
- User Profiles: Caching frequently accessed user profile data.
- Account/Tenant Information: For multi-tenant applications where tenant data is often queried.
- Configuration Settings: Storing application or user settings that are read often but updated infrequently.
- Product Catalogs: Caching frequently viewed products in an e-commerce application.
- Permission Sets: Storing user roles and permissions that are checked frequently.
It's important to note that go-pg-cache
is not designed for:
- Tables with rapidly changing data.
- Tables where access patterns are uniformly distributed (i.e., no subset of rows is accessed more frequently than others).
- Scenarios where data consistency is critical and even momentary inconsistencies are unacceptable.
By using go-pg-cache
for appropriate use cases, you can significantly reduce database load and improve application
performance.
- Selective Caching: Designed to cache a limited, frequently accessed subset of your PostgreSQL table.
- Distributed Caching: Uses PostgreSQL LISTEN/NOTIFY for real-time cache invalidation across nodes, ensuring consistency in distributed environments.
- Generic Implementation: Supports caching of any struct type, allowing flexibility in what data is cached.
- UUID Support: Uses UUIDs as the primary key type for enhanced uniqueness and compatibility with modern application designs.
- Bulk Invalidation: Supports invalidating multiple cache entries at once, useful for related data updates.
- LRU Eviction: Implements an LRU (Least Recently Used) eviction policy for the in-memory cache, ensuring the most relevant data is kept in cache.
- Configurable: Various settings can be customized via a configuration structure, allowing you to fine-tune the cache for your specific use case.
- Efficient Querying: Optimized database queries for refreshing the cache, minimizing the performance impact of cache misses.
- Automatic Synchronization: Handles out-of-sync scenarios gracefully, ensuring cache consistency even after network issues or application restarts.
- Logging: Integrated logging for easier debugging and monitoring of cache operations.
- Cache-Only Retrieval: Option to retrieve data only from the cache without hitting the database.
- Direct Cache Manipulation: Ability to directly add or update items in the cache.
- Transactional Notifications: Support for getting notification queries for use within transactions.
- Global Cache Clearing: New
ClearAllAndNotify
method to clear all caches across all nodes simultaneously.
To install the package, run:
go get github.com/tzahifadida/go-pg-cache
Here's a basic example of how to use go-pg-cache
:
import (
"github.com/tzahifadida/go-pg-cache"
"github.com/jmoiron/sqlx"
"github.com/google/uuid"
"context"
"log"
"time"
"fmt"
)
// Define your struct
type User struct {
ID uuid.UUID `db:"id"`
Name string `db:"name"`
UpdatedAt time.Time `db:"updated_at"`
}
// Initialize the cache
db, _ := sqlx.Connect("pgx", "your_connection_string")
config := gopgcache.CacheConfig{
Schema: "public",
TableName: "users",
IDFieldName: "ID",
UpdatedAtFieldName: "UpdatedAt",
ChannelName: "cache_invalidation",
MaxSize: 1000,
Context: context.Background(),
}
cache, err := gopgcache.NewCache[User, uuid.UUID](db, config)
if err != nil {
log.Fatalf("Failed to create cache: %v", err)
}
// Use the cache
userID := uuid.New()
user, exists, err := cache.Get(context.Background(), userID)
if err != nil {
log.Printf("Error getting user: %v", err)
} else if exists {
fmt.Printf("User: %v\n", user)
}
// Invalidate cache entries
err = cache.NotifyRemove(userID, uuid.New(), uuid.New())
if err != nil {
log.Printf("Error notifying removal: %v", err)
}
// Clear all caches across all nodes
err = cache.ClearAllAndNotify(context.Background())
if err != nil {
log.Printf("Error clearing all caches: %v", err)
}
The CacheConfig
struct allows you to customize the behavior of the cache:
Schema
: The database schema name.TableName
: The name of the table being cached.IDFieldName
: The name of the ID field in the struct.UpdatedAtFieldName
: The name of the field in the struct indicating when a row was last updated.ChannelName
: The PostgreSQL LISTEN/NOTIFY channel name for cache invalidation.CustomPGLN
: Optional custom PGLN instance.MaxSize
: Maximum number of items to keep in the in-memory cache.Context
: Context for database operations.Logger
: Custom logger implementing theLogger
interface.
cache, err := gopgcache.NewCache[User, uuid.UUID](db, config)
if err != nil {
log.Fatalf("Failed to create cache: %v", err)
}
defer cache.Shutdown()
userID := uuid.New()
user, exists, err := cache.Get(context.Background(), userID)
if err != nil {
log.Printf("Error retrieving user: %v", err)
} else if exists {
fmt.Printf("User: %+v\n", user)
} else {
fmt.Println("User not found")
}
user, exists, err := cache.Get(context.Background(), userID, gopgcache.CacheOnly())
if err != nil {
log.Printf("Error retrieving user: %v", err)
} else if exists {
fmt.Printf("User (from cache): %+v\n", user)
} else {
fmt.Println("User not found in cache")
}
err := cache.NotifyRemove(userID1, userID2, userID3)
if err != nil {
log.Printf("Error invalidating cache entries: %v", err)
}
notifyQueryResult, err := cache.NotifyRemoveAndGetQuery(userID1, userID2, userID3)
if err != nil {
log.Printf("Error getting notify query: %v", err)
return
}
tx, err := db.BeginTx(context.Background(), nil)
if err != nil {
log.Printf("Error starting transaction: %v", err)
return
}
_, err = tx.ExecContext(context.Background(), notifyQueryResult.Query, notifyQueryResult.Params...)
if err != nil {
tx.Rollback()
log.Printf("Error executing notify query: %v", err)
return
}
err = tx.Commit()
if err != nil {
log.Printf("Error committing transaction: %v", err)
return
}
user := User{
ID: uuid.New(),
Name: "John Doe",
UpdatedAt: time.Now(),
}
cache.Put(context.Background(), user.ID, user)
err := cache.ClearAllAndNotify(context.Background())
if err != nil {
log.Printf("Error clearing all caches: %v", err)
}
// Get the notify query for cache invalidation
notifyQueryResult, err := cache.NotifyRemoveAndGetQuery(userID1, userID2, userID3)
if err != nil {
log.Printf("Error getting notify query: %v", err)
return
}
// Use the query in a transaction or any other context
tx, err := db.BeginTx(context.Background(), nil)
if err != nil {
log.Printf("Error starting transaction: %v", err)
return
}
// Execute the notify query
_, err = tx.ExecContext(context.Background(), notifyQueryResult.Query, notifyQueryResult.Params...)
if err != nil {
tx.Rollback()
log.Printf("Error executing notify query: %v", err)
return
}
// Commit the transaction
err = tx.Commit()
if err != nil {
log.Printf("Error committing transaction: %v", err)
return
}
- Cache Size: Adjust the
MaxSize
inCacheConfig
based on your application's memory constraints and the size of your frequently accessed dataset. - Database Queries: The library uses optimized queries to refresh the cache, but be mindful of the load on your PostgreSQL server, especially for large tables.
- Invalidation Strategy: Use
NotifyRemove
judiciously to keep the cache consistent without overwhelming the system with notifications. - Bulk Invalidations: When possible, use
NotifyRemove
with multiple IDs to efficiently invalidate several cache entries at once. - Monitoring: Keep an eye on cache hit rates and database query times to fine-tune your configuration and ensure the cache is providing the expected performance benefits.
- Data Access Patterns: Regularly analyze your data access patterns to ensure you're caching the most frequently accessed subset of your data.
- Use of Cache-Only Retrieval: Utilize the
CacheOnly()
option when you want to avoid database hits for non-critical data retrieval. - Transactional Notifications: Use
NotifyRemoveAndGetQuery
when you need to ensure cache invalidation occurs only if a transaction is successfully committed. - Global Cache Clearing: Use
ClearAllAndNotify
sparingly, as it impacts all nodes and can temporarily increase database load as caches are repopulated.
Contributions to go-pg-cache
are welcome! Here are some ways you can contribute:
- Report bugs or request features by opening an issue.
- Improve documentation.
- Submit pull requests with bug fixes or new features.
Please ensure that your code adheres to the existing style and that all tests pass before submitting a pull request.
This project is licensed under the MIT License. See the LICENSE file for details.
Thank you for using go-pg-cache
! If you have any questions, suggestions, or encounter any issues, please don't
hesitate to open an issue on the GitHub repository. Your feedback and contributions are greatly appreciated and help
make this library better for everyone.