In this comprehensive tutorial on Redis 7.2's Triggers and Functions, you'll gain insights and practical skills in the following areas:
On-demand
Triggers, KeySpace
Triggers, and Stream
Triggers.Lets take a look at the architecture of the demo application:
products service
: handles querying products from the database and returning them to the frontendorders service
: handles validating and creating ordersorder history service
: handles querying a customer's order historypayments service
: handles processing orders for paymentapi gateway
: unifies the services under a single endpointmongodb/ postgresql
: serves as the write-optimized database for storing orders, order history, products, etc.The e-commerce microservices application consists of a frontend, built using Next.js with TailwindCSS. The application backend uses Node.js. The data is stored in Redis and either MongoDB or PostgreSQL, using Prisma. Below are screenshots showcasing the frontend of the e-commerce app.
Dashboard: Displays a list of products with different search functionalities, configurable in the settings page.
Settings: Accessible by clicking the gear icon at the top right of the dashboard. Control the search bar, chatbot visibility, and other features here.
Dashboard (Semantic Text Search): Configured for semantic text search, the search bar enables natural language queries. Example: "pure cotton blue shirts."
Shopping Cart: Add products to the cart and check out using the "Buy Now" button.
Order History: Post-purchase, the 'Orders' link in the top navigation bar shows the order status and history.
Admin Panel: Accessible via the 'admin' link in the top navigation. Displays purchase statistics and trending products.
Triggers and functions represent a revolutionary step in Redis's programmability, introduced in Redis 7.2. This feature empowers developers to program, store, and execute JavaScript code in response to data changes directly within the Redis database, similar to stored procedures or triggers in traditional SQL databases.
This capability lets developers define events (called triggers
) to execute functions
closer to the data. That is, developers define business logic that executes in response to database events or commands. That speeds up the code and related interactions, because there is no wait to bring code from clients into the database.
Incorporating triggers and functions into Redis capitalizes on its renowned real-time performance and simplicity:
Triggers and functions in Redis can be categorized into three types, based on their activation methods:
To illustrate the application of triggers and functions, let's consider a simplified e-commerce dataset. This dataset includes detailed product information, which we will use throughout our tutorial.
const products = [
{
productId: '11000',
price: 3995,
productDisplayName: 'Puma Men Slick 3HD Yellow Black Watches',
variantName: 'Slick 3HD Yellow',
brandName: 'Puma',
ageGroup: 'Adults-Men',
gender: 'Men',
displayCategories: 'Accessories',
masterCategory_typeName: 'Accessories',
subCategory_typeName: 'Watches',
styleImages_default_imageURL:
'http://host.docker.internal:8080/images/11000.jpg',
productDescriptors_description_value: 'Stylish and comfortable, ...',
stockQty: 25,
},
//...
];
On-demand triggers in Redis are JavaScript functions that are explicitly invoked to perform specific tasks.
In our e-commerce demo, consider a feature where we need to reset the stock quantity of all products. We'll implement this by clicking a RESET STOCK QTY
button in the UI dashboard, triggering the resetInventory
function.
Let's craft a function named resetInventory
under the namespace OnDemandTriggers
. This function will reset the inventory (stock quantity) of all products to 25.
#!js name=OnDemandTriggers api_version=1.0
redis.registerAsyncFunction('resetInventory', async function (client) {
let cursor = '0';
const DEFAULT_PRODUCT_QTY = 25;
redis.log('resetInventory');
do {
client.block((client) => {
//scan all the product keys in the database
let res = client.call('scan', cursor, 'match', 'products:productId:*');
cursor = res[0];
let keys = res[1];
// loop through all the product keys and set the stockQty to 25
keys.forEach((key) => {
if (!key.match('index:hash')) {
client.call(
'JSON.SET',
key,
'$.stockQty',
DEFAULT_PRODUCT_QTY.toString(),
);
}
});
});
} while (cursor != '0');
return 'resetInventory completed !';
});
We can add functions to Redis using various methods:
redis-cli -x TFUNCTION LOAD < ./on-demand-trigger.js
# or if you want to replace the function
redis-cli -x TFUNCTION LOAD REPLACE . < ./on-demand-trigger.js
2. Using code
import type { NodeRedisClientType } from './config.js';
import * as path from 'path';
import * as fs from 'fs/promises';
async function addTriggerToRedis(
fileRelativePath: string,
redisClient: NodeRedisClientType,
) {
const filePath = path.join(__dirname, fileRelativePath);
const fileData = await fs.readFile(filePath);
let jsCode = fileData.toString();
jsCode = jsCode.replace(/\r?\n/g, '\n');
try {
const result = await redisClient.sendCommand([
'TFUNCTION',
'LOAD',
'REPLACE',
jsCode,
]);
console.log(`addTriggersToRedis ${fileRelativePath}`, result);
} catch (err) {
console.log(err);
}
}
addTriggerToRedis('triggers/on-demand-trigger.js', redisClient);
3. Using RedisInsight
Navigate to the Triggers and Functions section in RedisInsight, then to Libraries, and use create library to paste and save your function.
1. Using redis-cli
redis-cli TFCALLASYNC OnDemandTriggers.resetInventory 0
2. Using code
Clicking on the 'RESET STOCK QTY' button triggers the triggerResetInventory API.
POST http://localhost:3000/products/triggerResetInventory
{
}
This invokes the resetInventory
function:
const triggerResetInventory = async () => {
const redisClient = getNodeRedisClient();
//@ts-ignore
const result = await redisClient.sendCommand(
['TFCALLASYNC', 'OnDemandTriggers.resetInventory', '0'],
{
isolated: true,
},
);
console.log(`triggerResetInventory : `, result);
return result;
};
3. Using RedisInsight
Test the command in RedisInsight's workbench and view the results.
Post-execution, check whether the stockQty for each product is reset to the default value.
A KeySpace trigger allows you to execute custom logic whenever a set of keys matching a specific pattern is added/ modified in the Redis database. It provides a way to react to changes in the data and perform actions based on those changes.
In our e-commerce demo, let's address a common need: decreasing product stock quantity upon placing an order. We'll achieve this using a KeySpace trigger
that listens to orders:orderId
keys and updates the product stock quantities accordingly.
We'll develop updateProductStockQty
under the KeySpaceTriggers
namespace. This function will be responsible for adjusting stock quantities based on order details.
#!js name=KeySpaceTriggers api_version=1.0
redis.registerKeySpaceTrigger(
'updateProductStockQty',
'orders:orderId:', // Keys starting with this prefix are monitored
function (client, data) {
const errors = [];
try {
if (
client &&
data?.event == 'json.set' &&
data?.key != 'orders:orderId:index:hash'
) {
const orderId = data.key;
// get the order details from the orderId key
let result = client.call('JSON.GET', orderId);
result = result ? JSON.parse(result) : '';
const order = Array.isArray(result) ? result[0] : result;
if (order?.products?.length && !order.triggerProcessed) {
try {
//create a log stream to log the trigger events and errors
client.call(
'XGROUP',
'CREATE',
'TRIGGER_LOGS_STREAM',
'TRIGGER_LOGS_GROUP',
'$',
'MKSTREAM',
);
} catch (streamConErr) {
// if log stream already exists
}
// reduce stockQty for each product in the order
for (const product of order.products) {
let decreaseQtyBy = (-1 * product.qty).toString();
client.call(
'JSON.NUMINCRBY',
`products:productId:${product.productId}`,
'.stockQty',
decreaseQtyBy,
);
// add log entry
client.call(
'XADD',
'TRIGGER_LOGS_STREAM',
'*',
'message',
`For productId ${product.productId}, stockQty ${decreaseQtyBy}`,
'orderId',
orderId,
'function',
'updateProductStockQty',
);
}
// set triggerProcessed flag to avoid duplicate processing
client.call('JSON.SET', orderId, '.triggerProcessed', '1');
}
}
} catch (generalErr) {
generalErr = JSON.stringify(
generalErr,
Object.getOwnPropertyNames(generalErr),
);
errors.push(generalErr);
}
if (errors.length) {
//log error
client.call(
'XADD',
'TRIGGER_LOGS_STREAM',
'*',
'message',
JSON.stringify(errors),
'orderId',
data.key,
'function',
'updateProductStockQty',
);
}
},
);
In this script, we listen to changes in the orders:orderId:
keys. Upon detecting a new order, the function retrieves the order details and accordingly decreases the stock quantity for each product in the order.
We can add functions to Redis using various methods:
1. Using redis-cli
redis-cli -x TFUNCTION LOAD < ./key-space-trigger.js
# or if you want to replace the function
redis-cli -x TFUNCTION LOAD REPLACE . < ./key-space-trigger.js
2. Using code
import type { NodeRedisClientType } from './config.js';
import * as path from 'path';
import * as fs from 'fs/promises';
async function addTriggerToRedis(
fileRelativePath: string,
redisClient: NodeRedisClientType,
) {
const filePath = path.join(__dirname, fileRelativePath);
const fileData = await fs.readFile(filePath);
let jsCode = fileData.toString();
jsCode = jsCode.replace(/\r?\n/g, '\n');
try {
const result = await redisClient.sendCommand([
'TFUNCTION',
'LOAD',
'REPLACE',
jsCode,
]);
console.log(`addTriggersToRedis ${fileRelativePath}`, result);
} catch (err) {
console.log(err);
}
}
addTriggerToRedis('triggers/key-space-trigger.js', redisClient);
3. Using RedisInsight
Navigate to the Triggers and Functions section in RedisInsight, then to Libraries, and use create library to paste and save your function.
In our demo, placing an order through the Buy Now
button triggers the createOrder
API, which in turn creates a new orders:orderId:
key, activating the updateProductStockQty
function.
Sample createOrder API request:
POST http://localhost:3000/orders/createOrder
{
"products": [
{
"productId": "11002",
"qty": 1,
"productPrice": 4950,
},
{
"productId": "11012",
"qty": 2,
"productPrice": 1195,
}
]
}
A sample order creation command in Redis:
"JSON.SET" "orders:orderId:24b38a47-2b7d-4c5d-ba25-b74749e34c65" "$" "{"products":[{"productId":"10381","qty":1,"productPrice":2499,"productData":{}},{"productId":"11030","qty":1,"productPrice":1099,"productData":{}}],"userId":"USR_f0f00a86-7131-40e1-9d89-765b4cc1927f","orderId":"24b38a47-2b7d-4c5d-ba25-b74749e34c65","orderStatusCode":1}"
The creation of this new key triggers updateProductStockQty
, leading to the adjustment of stock quantities.
Monitor the trigger's activity in the TRIGGER_LOGS_STREAM
for logs and potential errors.
After the function execution, verify the decreased stockQty
for each involved product.
A stream trigger allows you to listen to a Redis stream and execute a function whenever new data is added to the stream. It is commonly used for real-time data processing and event-driven architectures.
In our e-commerce demo, let's consider a feature where we need to calculate sales statistics for the products. We'll implement this using a Stream trigger
that listens to TRANSACTION_STREAM
and updates the sales statistics accordingly.
We'll develop calculateStats
under the StreamTriggers
namespace. This function will be responsible for calculating sales statistics based on the order details.
#!js name=StreamTriggers api_version=1.0
redis.registerStreamTrigger(
'calculateStats', // trigger name
'TRANSACTION_STREAM', // Detects new data added to the stream
function (client, data) {
var streamEntry = {};
for (let i = 0; i < data.record?.length; i++) {
streamEntry[data.record[i][0]] = data.record[i][1];
}
streamEntry.transactionPipeline = JSON.parse(
streamEntry.transactionPipeline,
);
streamEntry.orderDetails = JSON.parse(streamEntry.orderDetails);
if (
streamEntry.transactionPipeline?.length == 1 &&
streamEntry.transactionPipeline[0] == 'PAYMENT_PROCESSED' &&
streamEntry.orderDetails
) {
//log
client.call(
'XADD',
'TRIGGER_LOGS_STREAM',
'*',
'message',
`${streamEntry.transactionPipeline}`,
'orderId',
`orders:orderId:${streamEntry.orderDetails.orderId}`,
'function',
'calculateStats',
);
const orderAmount = parseInt(streamEntry.orderDetails.orderAmount); //remove decimal
const products = streamEntry.orderDetails.products;
// sales
client.call('INCRBY', 'statsTotalPurchaseAmount', orderAmount.toString());
for (let product of products) {
const totalProductAmount =
parseInt(product.qty) * parseInt(product.productPrice);
// trending products
client.call(
'ZINCRBY',
'statsProductPurchaseQtySet',
product.qty.toString(),
product.productId,
);
// category wise purchase interest
const category = (
product.productData.masterCategory_typeName +
':' +
product.productData.subCategory_typeName
).toLowerCase();
client.call(
'ZINCRBY',
'statsCategoryPurchaseAmountSet',
totalProductAmount.toString(),
category,
);
// largest brand purchases
const brand = product.productData.brandName;
client.call(
'ZINCRBY',
'statsBrandPurchaseAmountSet',
totalProductAmount.toString(),
brand,
);
}
}
},
{
isStreamTrimmed: false, //whether the stream should be trimmed automatically after the data is processed by the consumer.
window: 1,
},
);
In above calculateStats
function, we are listening to TRANSACTION_STREAM
and updating different sales statistics like
statsTotalPurchaseAmount
variable stores total purchase amountstatsProductPurchaseQtySet
is a sorted set which tracks trending products based on highest purchase quantitystatsCategoryPurchaseAmountSet
is a sorted set which tracks category wise purchase intereststatsBrandPurchaseAmountSet
is a sorted set which tracks largest brand purchasesWe can add functions to Redis using various methods:
1. Using redis-cli
redis-cli -x TFUNCTION LOAD < ./stream-trigger.js
# or if you want to replace the function
redis-cli -x TFUNCTION LOAD REPLACE . < ./stream-trigger.js
2. Using code
import type { NodeRedisClientType } from './config.js';
import * as path from 'path';
import * as fs from 'fs/promises';
async function addTriggerToRedis(
fileRelativePath: string,
redisClient: NodeRedisClientType,
) {
const filePath = path.join(__dirname, fileRelativePath);
const fileData = await fs.readFile(filePath);
let jsCode = fileData.toString();
jsCode = jsCode.replace(/\r?\n/g, '\n');
try {
const result = await redisClient.sendCommand([
'TFUNCTION',
'LOAD',
'REPLACE',
jsCode,
]);
console.log(`addTriggersToRedis ${fileRelativePath}`, result);
} catch (err) {
console.log(err);
}
}
addTriggerToRedis('triggers/stream-trigger.js', redisClient);
3. Using RedisInsight
Navigate to the Triggers and Functions
section in RedisInsight, then to Libraries
, and use create library to paste and save your function.
In our demo, placing an order through the Buy Now
button creates a new order involving different transaction steps like transaction risk assessment, payment fulfillment etc. All these steps along with order details are logged in TRANSACTION_STREAM
.
Sample code to add details to a stream:
const addMessageToTransactionStream = async (message) => {
if (message) {
const streamKeyName = 'TRANSACTION_STREAM';
try {
const nodeRedisClient = getNodeRedisClient();
if (nodeRedisClient && message) {
const id = '*'; //* = auto generate
await nodeRedisClient.xAdd(streamKeyName, id, message);
}
} catch (err) {
console.error('addMessageToTransactionStream error !', err);
}
}
};
A sample command to add details to the stream:
"XADD" "TRANSACTION_STREAM" "*" "action" "PAYMENT_PROCESSED" "userId" "USR_f0f00a86-7131" "orderDetails" "{'orderId':'bc438c5d-117e-41bd-97fa-943c03be0b1c','products':[],'paymentId':'clrrl8yp50007pf851m7f92u2'}" "transactionPipeline" "['PAYMENT_PROCESSED']"
The calculateStats
function listens to TRANSACTION_STREAM
stream for PAYMENT_PROCESSED
action and updates the sales statistics accordingly.
Check different stats variable values in RedisInsight which were used in trigger function calculateStats
.
After the function execution, verify the updated admin dashboard.
Admin Panel: Accessible via the 'admin' link in the top navigation. Check purchase statistics and trending products in UI.
We've covered key concepts like On-demand
, KeySpace
, and Stream
triggers, and applied them in real e-commerce scenarios. These advanced functionalities of Redis open up a myriad of possibilities for data processing and automation, allowing you to build applications that are not only faster but also more intelligent.
As you continue to explore Redis and its evolving ecosystem, remember that these triggers and functions
are just the beginning. Redis offers a rich set of features that can be combined in creative ways to solve complex problems and deliver high-performance solutions.
Please hit your text content and then hit enter.
Redis Triggers and Functions (Experimental preview feature) is currently marked as deprecated as of version (Redis Stack) 7.4
We recommend to explore alternatives like RDI (Redis Data Integration)
Below is a command to the clone the source code for the application used in this tutorial
git clone --branch v9.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions
You don't need to use MongoDB/ Postgresql as your write-optimized database in the demo application; you can use other prisma supported databases as well. This is just an example.