- vừa được xem lúc

MongoDB - Giảm 99% lượng truy vấn đến Database nhờ Caching & Distributed Locking trên Redis. Cùng thử nhé!

0 0 1

Người đăng: Đức Phúc

Theo Viblo Asia

Xin chào, lại là mình - Đức Phúc, anh chàng hơn 6 năm trong nghề vẫn nghèo technical nhưng thích viết Blog để chia sẻ kiến thức bản thân học được trong quá trình “cơm áo gạo tiền” đây. Các bạn có thể theo dõi mình thêm qua một số nền tảng bên dưới nhé:

Nếu bài này hữu ích, đừng quên tặng mình một upvote, bookmark để ủng hộ các bạn nhé

Sau một vài bài về MongoDB đơn lẻ nhàm chán nhằm tối ưu truy vấn, và cũng sau một thời gian dài mình không viết về Caching, giờ là lúc mình quay lại với thêm một chút nâng cấp nhé. Ta sẽ đi thực hành luôn thay vì kiến thức Cache khô khan nha. Bắt đầu thôi nào

1. Bài toán

Bạn có một hệ thống gồm 1 triệu documents trong bảng Users. Trong hệ thống, có rất nhiều request được gửi đến đòi hỏi phải truy xuất thông tin User dựa trên ID người dùng. Làm thế nào để tối ưu nó với Redis khi có hàng nghìn request được gửi đến chỉ trong vòng 1 phút?

Bài toán này quen quá đúng không nào. Nghĩ đơn giản thì, chỉ cần lưu thông tin User lên Redis, sau đó đọc từ Redis thay vì MongoDB là xong. Nhưng thực tế có đơn giản như vậy? Cùng mình phân tích nha

2. Cài đặt source code để thực nghiệm

Ở bài này, chúng ta sẽ thực nghiệm song song với lý thuyết để kiểm nghiệm luôn nhé.

  • Đầu tiên, các bạn tạo 1 file package.json như sau để liệt kê 1 số package sẽ được cài đặt nhé:
{ "name": "caching_demo", "version": "1.0.0", "description": "Demo project for caching with MongoDB, Redis, and Redlock", "main": "user_seed.js", "scripts": { "seed": "node user_seed.js", "start": "node index.js" }, "dependencies": { "dotenv": "^16.5.0", "express": "^4.18.2", "faker": "^5.5.3", "ioredis": "^5.3.2", "mongodb": "^5.7.0", "redlock": "^4.2.0" }, "author": "", "license": "ISC"
}
  • Chạy lệnh yarn install để cài đặt package nhé
  • Chúng ta sẽ cần sử dụng đến k6, một công cụ để thực hiện stress test nha. Do đó, các bạn có thể theo dướng dẫn tại đây để cài đặt cho phù hợp với máy của mình nhé
  • Tiếp theo, ta sẽ cần 1 triệu documents cho bảng Users, các bạn tạo file user_seed.js với nội dung như sau để random dữ liệu vào DB nhé:
const { MongoClient } = require("mongodb");
const faker = require("faker"); const uri = "mongodb://localhost:27017";
const dbName = "demo_caching";
const collectionName = "Users";
const TOTAL_USERS = 1_000_000;
const BATCH_SIZE = 2000;
const ROLES = ["admin", "buyer", "seller"]; function getRandomRole() { return ROLES[Math.floor(Math.random() * ROLES.length)];
} function getRandomDate(start, end) { return new Date( start.getTime() + Math.random() * (end.getTime() - start.getTime()) );
} function generateUser() { const firstName = faker.name.firstName(); const lastName = faker.name.lastName(); const email = faker.internet.email(firstName, lastName); const phoneNumber = faker.phone.phoneNumber(); const dob = getRandomDate(new Date(1950, 0, 1), new Date(2005, 0, 1)); const now = new Date(); return { email, firstName, lastName, phoneNumber, dob, createdAt: now, updatedAt: now, role: getRandomRole(), };
} async function seed() { const client = new MongoClient(uri); try { await client.connect(); const db = client.db(dbName); const collection = db.collection(collectionName); console.log("Connected to MongoDB"); await collection.deleteMany({}); console.log("Cleared Users collection"); for (let i = 0; i < TOTAL_USERS; i += BATCH_SIZE) { const batch = []; for (let j = 0; j < BATCH_SIZE && i + j < TOTAL_USERS; j++) { batch.push(generateUser()); } await collection.insertMany(batch); console.log(`Inserted ${i + batch.length} / ${TOTAL_USERS}`); } console.log("Seeding complete!"); } catch (err) { console.error(err); } finally { await client.close(); }
} seed(); 
  • Bây giờ, các bạn chạy lệnh yarn run seed để tiến hành import dữ liệu nha. Đợi 1 chút cho quá trình này hoàn tất image.png

  • Giờ cần thêm 1 file để chạy source code nữa là xong. Ta sẽ tạo file index.js nhé:

const express = require("express");
const { MongoClient, ObjectId } = require("mongodb");
const Redis = require("ioredis");
const Redlock = require("redlock");
require("dotenv").config(); const uri = "mongodb://localhost:27017";
const dbName = "demo_caching";
const collectionName = "Users";
const PORT = 3000; const app = express();
let db, collection;
const redis = new Redis({ host: "127.0.0.1", port: 6379, password: process.env.REDIS_PASSWORD || undefined,
});
const redlock = new Redlock([redis], { retryCount: 20, retryDelay: 250, // ms
}); // Route for DB query only
app.get("/user/:id", async (req, res) => { }); // Route for using cache: /user-cache/:id
app.get("/user-cache/:id", async (req, res) => { }); async function start() { const client = new MongoClient(uri); await client.connect(); db = client.db(dbName); collection = db.collection(collectionName); app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); console.log("GET /user/:id to query user by MongoDB _id"); console.log("GET /user-cache/:id to query user by cache first"); console.log("GET /user-cache/:id?redlock=true to use distributed lock"); });
} start(); 

Giải thích 1 chút về file này nhé: Chúng ta sẽ có 2 API router được định nghĩa sẵn:

  • /user/:id: API này chúng ta sẽ dùng để tìm kiếm User trực tiếp trên DB nha
  • /user-cache/:id: API này chúng ta sẽ dùng kết hợp DB, Cache và Distributed Lock nhé. Việc quyết định dùng distributed lock hay không phụ thuộc vào việc chúng ta có truyển query redlock=true hay không nha

Logic của các API này, chúng ta sẽ thêm dần ở các phần tiếp theo của bài viết này Ngoài ra, ở file này, mình cũng đã tạo kết nối sẵn đền Redis, và tạo 1 Redlock cho phần Distributed Lock nha. Chi tiết cũng ở phần sau luôn, đừng lo lắng nếu bạn chưa hiểu nhé. À, nếu Redis của bạn yêu cầu password để kết nối, đừng quên tạo 1 file .env vào thêm biến REDIS_PASSWORD cho phù hợp nha

REDIS_PASSWORD=123456

Rồi, vậy là việc set up hoàn thành, bắt đầu nội dung chính thôi. À để khởi chạy source code, bạn chạy lệnh yarn start nha

3. Sẽ thế nào nếu không cần dùng Cache?

Trước tiên, cùng mình phân tích nếu không dùng đến Cache thì sao nhé. Lúc đó, API đầu tiên của chúng ta sẽ đơn giản của ta chỉ đơn giản thế này:

app.get("/user/:id", async (req, res) => { try { const user = await collection.findOne({ _id: new ObjectId(req.params.id) }); res.json({ user, }); } catch (err) { res.status(500).json({ error: err.message }); }
});

Với mỗi request, ta sẽ lấy id tương ứng và gọi vào MongoDB để truy vấn rồi trả kết quả về. Bây giờ, ta sẽ tạo thêm 1 file là k6_stress_test.js để thực hiện việc gửi cùng lúc nhiều request đến API này nhé:

import http from "k6/http";
import { check, sleep } from "k6"; export let options = { vus: 5000, // Number of virtual users duration: "1m", // Test duration thresholds: { http_req_duration: ["p(95)<100"], // Expecting: 95% of requests should be below 100ms },
}; // Replace with a real user ID from your database
const USER_ID = "682e9881cc777592fda2baf7"; // any User ID you have on your DB
const BASE_URL = "http://localhost:3000"; export default function () { const res = http.get(`${BASE_URL}/user/${USER_ID}`); check(res, { "status is 200": (r) => r.status === 200, "body has user": (r) => r.json().user !== undefined, }); sleep(0.001); // Small sleep to help spread requests
} 

Ở file này, mình đang cấu hình rằng sẽ có tổng cộng 5000 User giả lập sẽ tiến hành gọi API trong thời gian là 1m (1 phút). Ở phần thresholds cho http_req_duration, mình đang mong đợi rằng các request chỉ nên tốn dưới 100ms để hoàn thành.

Có 1 điểm lưu ý mà bạn cần sửa đổi ở file này, là biến USER_ID. Bạn có thể thay thế nó bằng bất kì _id của User nào trong bảng Users ta đã tạo trước đó nhé

Ở bài test này, mình có 2 thứ để kiểm tra cho mỗi request:

check(res, { "status is 200": (r) => r.status === 200, "body has user": (r) => r.json().user !== undefined, });
  • status is 200: Request cần phải trả về status 200, tức là thành công
  • body has user: Dữ liệu trả về cần có thông tin của user được tìm thấy

Rồi, bây giờ ta chạy lệnh thôi. Mở 1 terminal mới nhé:

k6 run k6_stress_test.js

Đợi khoảng 1 phút cho quá trình test hoàn tất nhé, lúc đó, bạn sẽ thấy 1 kết quả xuất hiện trên terminal tương tự như sau:

THRESHOLDS http_req_duration ✗ 'p(95)<100' p(95)=1.35s █ TOTAL RESULTS checks_total.......................: 584604 9635.3457/s checks_succeeded...................: 97.38% 569302 out of 584604 checks_failed......................: 2.61% 15302 out of 584604 ✗ status is 200 ↳ 97% — ✓ 284651 / ✗ 7651 ✗ body has user ↳ 97% — ✓ 284651 / ✗ 7651 HTTP http_req_duration.......................................................: avg=935.82ms min=0s med=916.57ms max=4.57s p(90)=1.22s p(95)=1.35s { expected_response:true }............................................: avg=960.97ms min=147.37ms med=920.78ms max=4.57s p(90)=1.23s p(95)=1.35s http_req_failed.........................................................: 2.61% 7651 out of 292302 http_reqs...............................................................: 292302 4817.67285/s EXECUTION iteration_duration......................................................: avg=1.03s min=236.37µs med=922.3ms max=9.8s p(90)=1.25s p(95)=1.39s iterations..............................................................: 292302 4817.67285/s vus.....................................................................: 5000 min=5000 max=5000 vus_max.................................................................: 5000 min=5000 max=5000 NETWORK data_received...........................................................: 148 MB 2.4 MB/s data_sent...............................................................: 28 MB 467 kB/s running (1m00.7s), 0000/5000 VUs, 292302 complete and 0 interrupted iterations
default ✓ [======================================] 5000 VUs 1m0s
ERRO[0061] thresholds on metrics 'http_req_duration' have been crossed

Giờ phân tích các con số nè:

  • running (1m00.7s), 0000/5000 VUs, 292302 complete and 0 interrupted iterations: Thời gian thực hiện bài kiểm tra là khoảng 1 phút với 5_000 VUs, đã có tổng cộng 292_302 requests được gửi đến Server
  • ✗ 'p(95)<100' p(95)=1.35s: Dòng này mang ý nghĩa là 95% trong tổng số 292_302 requests cần phải chờ dưới **1.35s** để hoàn thành. Tuy nhiên, mong đợi của chúng ta là < **100ms**, do đó, phần kiểm tra này không đạt ()
  • Ở phần TOTAL RESULTS, là phần kết quả thống kê mà ta định nghĩa ở hàm check trước đó. Với 292_302 requests, mỗi request ta kiểm tra 2 điều kiện, vậy ta sẽ có tổng cộng: 292302 * 2 = 584_604 lần kiểm tra. 97.38% trong số đó thành công, và 2.62% còn lại thất bại (Nghĩa là request chúng ta không trả về dữ liệu như mong muốn do các lỗi khác. Ví dụ như: Server timeout,...)
  • Ở phần HTTP, ta sẽ chú ý đến dòng http_req_duration:
    • avg=935.82ms: Thời gian trung bình mà 1 request hoàn thành là 935.82ms
    • min=0s med=916.57ms max=4.57s: Thời gian min, med, max mà requests hoàn thành. Ta có thể thấy, có những request cần đến hơn 4s cho 1 query đơn giản
    • p(90)=1.22s: 90% số requests cần dưới 1.22s để hoàn thành. p(95) thì mình đã giải thích ở trên rồi nhé

Vậy là xong API đầu tiên mà ta query đến DB. Như vậy, trong vòng 1 phút, đã có tổng cộng 100% requests (292_302) thực hiện truy vấn đến DB. Này thì cũng khá nặng cho DB, CPU của chúng ta đấy

4. Sử dụng Cache thuần túy

Tại sao mình lại dùng từ "thuần túy". Trong bài này nó mang ý nghĩa là Cache dữ liệu mà không dùng đến Redlock các bạn nhé. Giờ ta sẽ tiến hành update API thứ 2 nha:

app.get("/user-cache/:id", async (req, res) => { const userId = req.params.id; const redisKey = `user:${userId}:info`; try { let userData = await redis.get(redisKey); if (userData) { // Found in Redis res.json({ user: JSON.parse(userData), source: 'redis' }); return; } // Not found in Redis, fetch from DB const user = await collection.findOne({ _id: new ObjectId(userId) }); if (user) { await redis.set(redisKey, JSON.stringify(user)); } res.json({ user, source: 'mongodb' }); } catch (err) { res.status(500).json({ error: err.message }); }
});

API này sẽ hoạt động như sau:

  • Đầu tiên, tìm thông tin User trên Redis. Nếu thông tin đã có, thì trả ngay thông tin đó cho client, kèm thông tin source: 'redis' để đánh dấu nhé
  • Ngược lại, nếu không tìm thấy trên Cache, ta sẽ tìm trên MongoDB. Sau khi tìm thấy, ta sẽ đồng thời lưu thông tin đó lên Redis, như vậy, ở request sau, ta có thể đọc từ Redis như bước đầu tiên mà không cần truy vấn trên MongoDB nữa. Cuối cùng, trả dữ liệu kèm source: 'mongodb' để đánh dấu request này truy vấn trên MongoDB nhé

Rồi, để test API này, ta tạo thêm 1 file là k6_stress_test_cache.js nha, nội dung như sau:

import http from "k6/http";
import { check, sleep } from "k6"; export let options = { vus: 500, duration: "1m", thresholds: { http_req_duration: ["p(95)<100"], },
}; const USER_ID = "682e9881cc777592fda2baf7";
const BASE_URL = "http://localhost:3000"; export default function () { const res = http.get(`${BASE_URL}/user-cache/${USER_ID}`); check(res, { "status is 200": (r) => r.status === 200, "body has user": (r) => r.json().user !== undefined, "read from mongodb": (r) => r.json().source === "mongodb", "read from redis without waitting": (r) => r.json().source === "redis" && r.json().waited === false, "read from redis and have to wait": (r) => r.json().source === "redis" && r.json().waited === true, }); sleep(0.001);
}

Về cơ bản, nó vẫn như file trước đó, điểm khác là chúng ta sẽ đổi API endpoint sang API mới. Ngoài ra, sẽ có thêm 3 phần mới ở hàm check:

  • read from mongodb: Số lượng request tìm kiếm trên DB
  • read from redis without waitting: Số lượng request được tìm trên Redis. Ở đây, bạn sẽ thấy có thêm điều kiện là r.json().waited === false, thì cũng không quan trọng nhé, điều kiện này sẽ được dùng ở phần Redlock nha. Còn hiện tại thì điều kiện này vẫn đúng vì chúng ta không trả field waited về ở response body
  • read from redis and have to wait: Phần này cũng được đề cập đến ở Redlock, nên con số này ta tạm bỏ qua nhé

Rồi, giờ mở terminal mới lên và chạy thôi:

k6 run k6_stress_test_cache.js

Và đây là kết quả:

THRESHOLDS http_req_duration ✓ 'p(95)<100' p(95)=47.84ms █ TOTAL RESULTS checks_total.......................: 3590333 59806.463246/s checks_succeeded...................: 59.96% 2153103 out of 3590333 checks_failed......................: 40.03% 1437230 out of 3590333 ✗ status is 200 ↳ 99% — ✓ 717701 / ✗ 914 ✗ body has user ↳ 99% — ✓ 717701 / ✗ 914 ✗ read from mongodb ↳ 0% — ✓ 77 / ✗ 717624 ✗ read from redis without waitting ↳ 99% — ✓ 717624 / ✗ 77 ✗ read from redis and have to wait ↳ 0% — ✓ 0 / ✗ 717701 HTTP http_req_duration.......................................................: avg=40.07ms min=0s med=38.26ms max=2.24s p(90)=43.9ms p(95)=47.84ms { expected_response:true }............................................: avg=40.12ms min=2.46ms med=38.26ms max=2.24s p(90)=43.9ms p(95)=47.85ms http_req_failed.........................................................: 0.12% 914 out of 718615 http_reqs...............................................................: 718615 11970.427697/s EXECUTION iteration_duration......................................................: avg=41.74ms min=385.91µs med=39.35ms max=5.23s p(90)=45.12ms p(95)=49.13ms iterations..............................................................: 718615 11970.427697/s vus.....................................................................: 500 min=500 max=500 vus_max.................................................................: 500 min=500 max=500 NETWORK data_received...........................................................: 396 MB 6.6 MB/s data_sent...............................................................: 75 MB 1.3 MB/s running (1m00.0s), 000/500 VUs, 718615 complete and 0 interrupted iterations
default ✓ [======================================] 500 VUs 1m0s

Vì đã giải thích ý nghĩa các con số ở trên, nên ở đây, mình sẽ giải thích những điểm khác biệt thôi nhé:

  • Đầu tiên, p(95) của chúng ta bây giờ là 47.84ms, nghĩa là 95% requests của chúng ta cần dưới thời gian đó để hoàn thành. Vậy là ta đã đạt được mục tiêu đầu tiên là thời gian < 100ms
  • Số lượng request được gửi là 718_615, lớn hơn rất nhiều nhờ thời gian response nhanh. 99% trong số đó thành công mĩ mãn, 1% còn lại failed. Như vậy, ta vừa tăng được số lượng request có thể xử lý, và cũng tăng luôn số % request thành công. 99% là con số tuyệt vời đấy
  • 99% request được đọc từ Redis, chỉ duy nhất 1% phải đọc từ MongoDB, chính xác là 77 requests, thay vì toàn bộ 718_615 requests như phương án đầu tiên
  • Các con số về http_req_duration, sẽ tỉ lệ thuận vs p(95) nên các bạn có thể tự so sánh để thấy thời gian đã giảm đi rất nhiều nhé.

Nhưng mà khoan đã, đừng vội mừng. Theo như mục đích ban đầu của chúng ta là:

Nếu không tìm thấy trên Cache, ta sẽ tìm trên MongoDB. Sau khi tìm thấy, ta sẽ đồng thời lưu thông tin đó lên Redis, như vậy, ở request sau, ta có thể đọc từ Redis như bước đầu tiên mà không cần truy vấn trên MongoDB nữa

Như vậy, theo lý thuyết, thì chỉ có duy nhất request đầu tiên phải cần truy vấn đến DB, những request còn lại phải đọc từ Redis chứ, tại sao lại có đến 77 requests được gửi đến MongoDB?

Thật ra là vì, chúng ta gửi rất nhiều request cùng lúc đến API. với 718_615 requests, ta đã gửi trung bình 11_970 requests/s. Với số lượng như thế, mà quá trình đọc dữ liệu từ DB rồi lưu lên cache không thể hoàn tất ngay tức thì, do đó, đôi khi những requests sau đó không tìm thấy trên Cache, vì vậy nó lại tiếp tục truy vấn đến DB để tìm dữ liệu. Và khi request thứ 77 hoàn tất, dữ liệu mới có trên Cache, do vậy, mà ta có con số ở trên. Cũng dễ hiểu phải không nào?

5. Distributed Lock vào việc

Như vậy, ta vẫn chưa đạt được mục tiêu về số request truy vấn đến DB. Thực ra con số 77 này rất nhỏ thôi, nhưng nếu trong trường hợp chúng ta tăng số requests lên, hoặc giảm thời gian đi, con số này có thể tăng lên. Chưa kể, sẽ có rất nhiều API chạm đến DB nữa, giảm đến mức tối thiểu nhất có thể là điều chúng ta nên làm

Và đây là lúc Distributed Lock xuất hiện. Mình sẽ ứng dụng nó trên Redis nhờ Redlock. Trước tiên, cùng mình update lại API thứ 2 sử dụng Cache của chúng ta nhé:

// New route: /user-cache/:id
app.get("/user-cache/:id", async (req, res) => { const userId = req.params.id; const redisKey = `user:${userId}:info`; const useRedlock = req.query.redlock === "true"; try { let userData = await redis.get(redisKey); if (userData) { // Found in Redis res.json({ user: JSON.parse(userData), source: "redis", waited: false }); return; } if (!useRedlock) { // No redlock, normal flow const user = await collection.findOne({ _id: new ObjectId(userId) }); if (user) { await redis.set(redisKey, JSON.stringify(user)); } res.json({ user, source: "mongodb" }); return; } // Use redlock const lockKey = `lock:user:${userId}:info`; let lock; try { lock = await redlock.acquire([lockKey], 3000); // 3s TTL // Double-check cache after acquiring lock let userData = await redis.get(redisKey); if (userData) { await redlock.release(lock); res.json({ user: JSON.parse(userData), source: "redis", waited: false, }); return; } const user = await collection.findOne({ _id: new ObjectId(userId) }); if (user) { await redis.set(redisKey, JSON.stringify(user)); } await redlock.release(lock); res.json({ user, source: "mongodb", redlock: true }); } catch (lockErr) { // If can't acquire lock, retry for cache with max attempts console.log("lockErr: ", lockErr); const userDataRetry = await waitForCache(redisKey, 5, 200); if (userDataRetry) { res.json({ user: userDataRetry, source: "redis", waited: true }); } else { res .status(500) .json({ error: "Failed to acquire lock and no cache available." }); } } } catch (err) { res.status(500).json({ error: err.message }); }
}); // Helper function for safer retry
async function waitForCache(redisKey, maxAttempts = 5, delay = 200) { for (let i = 0; i < maxAttempts; i++) { const userDataRetry = await redis.get(redisKey); if (userDataRetry) return JSON.parse(userDataRetry); await new Promise((resolve) => setTimeout(resolve, delay)); } return null;
}

Nhìn dài dòng hoa mắt rồi đấy, nhưng để mình giải thích cho.

  • Như đã đề cập trước đó, API này cung cấp tùy chọn cho chúng ta xem có muốn sử dụng redlock không thông qua query redlock=true. Do đó, ta sẽ có biến useRedlock dùng để check nhé
  • Dù cho có dùng Redlock hay không, thì việc đầu tiên vẫn phải tìm trước trên Redis đã nhé
  • Nếu không tìm thấy trên Redis:
    • Với trường hợp không dùng Redlock, ta sẽ giữ flow cũ như phần trước nha. Tìm trên DB rồi sync lên Redis, sau đó trả về client
    • Với trường hợp dùng Redlock: Ta sẽ tạo ra 1 log key theo userId có dạng lock:user:${userId}:info. Ta sẽ lock key này trong vòng 3 giây để quá trình đọc dữ liệu từ MongoDB lên Redis được thực hiện mà không có bất kì request nào khác được chen ngang. Nghĩ theo hướng trừu tượng, thì bạn là 1 người chung thủy, mỗi lúc chỉ yêu 1 người thôi. Khi có người chiếm được trái tim của bạn, bạn sẽ khóa nó lại, giữ đó mà không cho ai chen vào nữa cả. Khi mối tình đó kết thúc mà không có happy ending, bạn mới mở cửa trái tim cho người khác vào.
  • Sau 3s hoặc sau khi quá trình sync lên Redis thành công, tùy điều kiện nào đến trước, lock sẽ được release để tránh bị block.
  • Với những request không thể lấy được lock vì đang có 1 request giữ nó. Nó sẽ chia làm 2 giai đoạn:
    • Retry by itself: Giai đoạn này là chính nó sẽ tự retry việc lấy lock đó thêm 1 số lần sau một khoảng thời gian nhất định. Ở đây, bạn để ý rằng từ đầu bài viết, chúng ta đã set up phần Redlock với retryCount: 20retryDelay: 250ms, tức là sẽ tự động retry tối đa 20 lần, mỗi lần cách nhau 250ms. Một điều lưu ý là, có thể ở những lần Retry sau đó, request lấy được log, tuy nhiên, vì log trước đó đã hoàn thành, nên trên Redis có thể đã có dữ liệu. Do đó, ta sẽ có bước Double-check cache after acquiring lock để tìm trên Redis trước nhé, có là trả luôn, không cần đến DB nữa.
    • Trường hợp việc retry cũng thất bại nốt, ta sẽ có phần xử lý ở trong catch, nơi ta sẽ gọi đến hàm waitForCache để cố gắng đọc từ Cache thêm 1 số lần nữa. Ở đây, mình sẽ thử lại 5 lần, mỗi lần cách nhau 200ms

Flow là thế đấy, giờ chạy thử thôi. Ta vẫn sẽ dùng file k6_stress_test_cache.js để chạy test. Tuy nhiên, trước khi chạy, bạn điều chỉnh endpoint 1 chút để truyền thêm flag redlock=true nhé:

const res = http.get(`${BASE_URL}/user-cache/${USER_ID}?redlock=true`);

Và đây là kết quả:

THRESHOLDS http_req_duration ✓ 'p(95)<100' p(95)=55.58ms █ TOTAL RESULTS checks_total.......................: 3367430 56003.345868/s checks_succeeded...................: 59.96% 2019438 out of 3367430 checks_failed......................: 40.03% 1347992 out of 3367430 ✗ status is 200 ↳ 99% — ✓ 673146 / ✗ 850 ✗ body has user ↳ 99% — ✓ 673146 / ✗ 850 ✗ read from mongodb ↳ 0% — ✓ 1 / ✗ 673145 ✗ read from redis without waitting ↳ 99% — ✓ 673145 / ✗ 1 ✗ read from redis and have to wait ↳ 0% — ✓ 0 / ✗ 673146 HTTP http_req_duration.......................................................: avg=43.1ms min=0s med=40.21ms max=3.85s p(90)=49.58ms p(95)=55.58ms { expected_response:true }............................................: avg=43.14ms min=745µs med=40.22ms max=3.85s p(90)=49.58ms p(95)=55.6ms http_req_failed.........................................................: 0.12% 850 out of 673996 http_reqs...............................................................: 673996 11209.150926/s EXECUTION iteration_duration......................................................: avg=44.53ms min=517.7µs med=41.33ms max=3.86s p(90)=50.81ms p(95)=56.96ms iterations..............................................................: 673996 11209.150926/s vus.....................................................................: 500 min=500 max=500 vus_max.................................................................: 500 min=500 max=500 NETWORK data_received...........................................................: 372 MB 6.2 MB/s data_sent...............................................................: 80 MB 1.3 MB/s running (1m00.1s), 000/500 VUs, 673996 complete and 0 interrupted iterations
default ✓ [======================================] 500 VUs 1m0s

Các con số lúc này cơ bản giống như cách dùng Cache thuần túy. Tuy nhiên, bạn có thể thấy rằng, **read from mongodb** của chúng ta gần như là 0%, và chỉ có đúng 1 request duy nhất chạm đến được DB

Vì có thêm xử lý Redlock, nên ta tốn thêm trung bình 8ms cho p(95) nhé. Nhưng không sao cả, ta đã đạt được mục đích của bài này rồi đấy

6. Độ chính xác của dữ liệu khi Cache

Hmm, nhiều bạn sẽ thắc mắc rằng, lưu Cache theo request như thế này, thì khi dữ liệu thay đổi phải làm sao? Câu hỏi hay đấy, ta sẽ cần những phương pháp song song để đảm bảo dữ liệu trên Cache sẽ hợp lệ hết mức có thể. Thực tế chẳng hệ thống Cache nào đúng 100% được đâu, mà quan trọng là chúng ta chấp nhận hy sinh bao nhiêu, đánh đổi điều gì mà thôi

Còn nếu các bạn muốn biết thêm về những phương pháp đó, thì chờ những bài sau nhé!!! Mình sẽ cố gắng gửi đến các bạn sớm nhất. Còn với bài này, mục tiêu của chúng ta là ứng dụng Distributed Lock trên Redis, thì ta đã hoàn thành rồi

7. Áp tờ cờ re đuýt

Nếu bài này hữu ích, đừng quên tặng mình một upvote, bookmark để ủng hộ các bạn nhé. Nếu có thắc mắc gì hay ý kiến gì trao đổi, bạn có thể để lại comment nha. Ngoài ra, các bạn có thể kết nối với mình qua:

Xin chào và hẹn gặp lại

Bình luận

Bài viết tương tự

- vừa được xem lúc

TÌM HIỂU VỀ MONGODB

. 1. Định nghĩa về MongoDB. . MongoDB là một cơ sở dữ liệu mã nguồn mở và là cơ sở dữ liệu NoSQL(*) hàng đầu, được hàng triệu người sử dụng.

0 0 55

- vừa được xem lúc

Mongo DB cho người mới bắt đầu !

Lời nói đầu. Gần đây, mình mới bắt đầu nghiên cứu và sử dụng mongo db nên có chút kiến thức cơ bản về Mongo muốn share và note ra đây coi như để nhở (Biết đâu sẽ có ích cho ai đó).

0 0 41

- vừa được xem lúc

Áp dụng kiến trúc 3 Layer Architecture vào project NodeJS

The problem encountered. Các framework nodejs phổ biết như Express cho phép chúng ta dễ dàng tạo ra Resful API xử lí các request từ phía client một cách nhanh chóng và linh hoạt.

0 0 89

- vừa được xem lúc

Mongo DB cho người mới bắt đầu ! (P2)

Lời nói đầu. Gần đây, mình mới bắt đầu nghiên cứu và sử dụng mongo db nên có chút kiến thức cơ bản về Mongo muốn share và note ra đây coi như để nhở (Biết đâu sẽ có ích cho ai đó).

0 0 187

- vừa được xem lúc

Xây dựng CRUD RESTful API sử dụng Node, Express, MongoDB.

Introduction. Trong phạm vi bài viết này chúng ta sẽ cùng tìm hiểu về cách tạo restful api với Node, Express và MongoDB. . Xử lý các hoạt động crud.

0 0 232

- vừa được xem lúc

MongoDB là gì? Cơ sở dữ liệu phi quan hệ

Bài viết này mình sẽ giúp các bạn có cái nhìn tổng quan về MongoDB. Chúng ta không lạ gì với cơ sở dữ liệu quan hệ, còn với cơ sở dữ liệu phi quan hệ thì sao? MEAN stack (MongoDB, Express, AngularJS,

0 0 61