Memoization - Code Performance

Nội dung bài viết

Video học lập trình mỗi ngày

Memcached bạn đã từng nghe nói về cụm từ này bao giờ? Và nếu nghe rồi bạn đã sử dụng chúng ở đâu cho những ứng dụng của mình? Và bạn dùng công nghệ hay công cụ gì để memcached ? Tất cả những câu hỏi đó, sẽ được trả lời trong bài viết này. Để có cái nhìn tổng quát hơn, chúng ta sẽ đi vào một trường hợp thực tế.

Trong suốt thời gian của những lập trình viên, sẽ có một vài lần sau khi viết code và chuyển những dòng code đến team để tổng hợp lại hoàn thành một module hay app bạn sẽ bị những tiếng lèm bèm của một nhân viên nào đó nói rằng: "Ở một chỗ nào đó diễn ra rất chậm cho dù dữ liệu đã có rồi". Điều đó chứng tỏ một điều rằng, khi dữ liệu đã được Users lấy đi rồi, thì không nên lấy lại một lần nữa (dưới database). Trừ khi nó được updated, và nếu khần cấp thì bạn có thể đẩy lên cho users những thông tin vừa được update. Còn không hay để cho người dùng sử dụng một thông tin đã được cache.

Nói sao cho bạn hiểu nhỉ, thật ra những người trong ngành lập trình đều có thể hiểu cụm từ "Memoization" nhưng rất có thể front-end lại chưa hiểu nó diễn ra như thế nào. Chúng ta có thể hiểu như sau.


Bạn muốn mua một chiếc xe hơi, thì bạn sẽ đến đại lý để tìm kiếm thông tin về nó, chứ không cần phải có mặt tại hãng xe này, vì nó rất mất nhiều thời gian. Vì đại lý có thể nằm ở bất cứ đâu trên thế giới, miễn là phục vụ cho bạn (users) tìm kiếm thông tin một cách nhanh chóng. Và khi có một sự cố or thêm một thông tin nào đó thì bạn cũng không trực tiếp đến hãng mà nhiệm vụ của hãng sẽ tự động làm việc với đại lý để cho bạn có thể tìm lại thông tin một cách mới nhất và nhanh nhất. Đó chính là Memoization.

Example

Một điều quan trọng cần đề cập là kỹ thuật này không phải là một phần của bất kỳ ngôn ngữ lập trình nào, có nghĩa là nó có thể được sử dụng trong bất kỳ ngôn ngữ nào bạn thích. Trong ví dụ của chúng tôi, chúng tôi sẽ sử dụng JavaScript. Lý do cho điều này là đơn giản, chúng ta có thể dễ dàng sử dụng bất kỳ trình soạn thảo văn bản nào và thực hiện các bài kiểm tra nhanh trong bảng điều khiển trình duyệt. Đầu tiên, hãy tạo một function như sau có vấn đề. Với mục đích này, chúng ta có thể sử dụng triển khai Fibonacci Array chẳng hạn hoặc tạo các vòng lặp lồng nhau:

# Inefficient Function - kém hiệu quả

const fibonnaci = (n) => {
  if (n < 2) return 1;
  return fibonnaci(n - 1) + fibonnaci(n - 2);
}

console.time("First run");
fibonnaci(42);
console.timeEnd("First run");

console.time("Second run");
fibonnaci(42);
console.timeEnd("Second run");

// First run: 2853.337890625ms
// Second run: 2854.5107421875ms

Đoạn mã trên chạy khá lâu, gần 3 giây. Lưu ý rằng thời gian này có thể thay đổi tùy thuộc vào sức mạnh của PC của bạn, tuy nhiên nó sẽ dài hơn thời gian cần thiết. Có lẽ bạn đã nhận thấy rằng chúng ta đã gọi hàm fibonnaci hai lần với cùng một tham số và mặc dù chúng ta biết rằng đầu ra sẽ giống nhau, chúng ta vẫn cần đợi thêm 3 giây để mã cung cấp cho chúng ta cùng một kết quả. Đáng tiếc là như vậy. Chính vì lẽ đó chúng tôi sẽ có một cách làm khác, nhưng hiệu quả mang đến thực sự là quã mỹ mãn

Memoization Function

Chúng tôi sử dụng new Map() để cache chúng lại như code dưới đây. Nếu bạn chưa có có hội làm việc với new Map() thì đọc những bài viết trước đây  của chúng tôi.

const memoize = (func) => {
  const cache = new Map();
    
  return (...args) => {
    const key = args.join('-');

    if(!cache.has(key)) {
      cache.set(key, func(args))
    }

    return cache.get(key);
  }
}

Giờ chúng ta tiếp tục thử nghiệm với hàm mới này.

const fibonnaciMemo = memoize(fibonnaci);

console.time("First run");
fibonnaciMemo(42);
console.timeEnd("First run");

console.time("Second run");
fibonnaciMemo(42);
console.timeEnd("Second run");

console.time("Third run");
fibonnaciMemo(42);
console.timeEnd("Third run");

// First run: 2858.337158203125ms
// Second run: 0.005859375ms
// Third run: 0.003173828125ms

WOW, lần đâu tiên thì nó vẫn giữ thời gian như vậy, nhưng những lần sau thì không, chúng sẽ hiệu quả hơn hẳn cho những lần trước. Quả thực kỹ thuật này không mới, nhưng tôi cam đoan rằng, có rất nhiều devjs đã bỏ qua yếu tố này trong việc phát triển một tấng dưới. Cụ thể như bài viết mà các bạn đang đọc, chúng tôi đã cache ở Redis rồi đấy. Chúng tôi đã thực hiện nó như thể nào khi sử dụng cache với redis, ngoài ra chúng tôi còn sử dụng cả firebase cho blog này.

NodeJS + Redis = Improved Performance

Đây là lý do vì sao redis là một kẻ huỷ diệt

Trên đó là một ví dụ cho các bạn hiểu performance khi có xuất hiện của kỹ thuật cache. Và sau đây chúng ta sẽ đi đến một code thực tế hơn. Đầu tiên chúng ta sẽ

Install npm packages:

npm install express body-parser mongoose redis --save

Sau đó tạo một server đơn giản để connect tới mongodb, các bạn lưu ý vừa đọc code và vừa xem comments của tôi trong những dòng code dưới đây

const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const redis = require('redis');

const app = express();

app.use(bodyParser.json());

// connect tới redis, dùng port và address mặc định 

const client = redis.createClient('redis://127.0.0.1:6379');
client.on("error", (err) => {
    console.error(err);
});

//MongoDB connection
mongoose.connect('mongodb://localhost:27017/', {
    dbName: 'notes',
    useNewUrlParser: true,
    useUnifiedTopology: true
}, err => err ? console.log(err) : console.log('Connected to database'));

//Mongoose Model
const NoteSchema = new mongoose.Schema({
    title: String,
    note: String
});

const Note = mongoose.model('Note', NoteSchema);

//api post một bài viết
app.post('/api/notes', (req, res, next) => {

    const { title, note } = req.body;

    const _note = new Note({
        title: title,
        note: note
    });
	//save vào mongodb 
    _note.save((err, note) => {
        if (err) {
            return res.status(404).json(err);
        }
	
        // Nếu thành công ở mongodb thì chúng ta add nó vào reddis

        client.setex(note.id, 60, JSON.stringify(note), (err, reply) => {
            if (err) {
                console.log(err);
            }
            console.log(reply);
        });

        return res.status(201).json({
            message: 'Note has been saved',
            note: note
        });
    })

});

const isCached = (req, res, next) => {

    const { id } = req.params;

    //First check in Redis
    client.get(id, (err, data) => {
        if (err) {
            console.log(err);
        }
        if (data) {
            const reponse = JSON.parse(data);
            return res.status(200).json(reponse);
        }
        next();
    });
}
// khi lấy ra thì chúng ta sẽ lấy ở redis đầu tiên thay vì truy cập xuống mongodb để lấy dữ liệu này.

app.get('/api/notes/:id', isCached, (req, res, next) => {

    const { id } = req.params;

    Note.findById(id, (err, note) => {
        if (err) {
            return res.status(404).json(err);
        }
        return res.status(200).json({
            note: note
        });
    });
});

app.listen(3000, () => console.log('Server running at port 3000'));

Những điều lưu ý

Ở những dòng code trên chúng ta lưu ý về việc set và get redis:

client.setex(note.id, 60, JSON.stringify(note), (err, reply) => {
            if (err) {
                console.log(err);
            }
            console.log(reply);
});

/* id : Unique ID must be provided to store data. It must be a string. seconds : Expiry time in seconds as a number. value : Actual data to store in Redis. It must be a string. So we are serializing object into string . callback : Callback takes two parameters err and reply . */

Và khi lấy một documents thì nó sẽ vào isCached() để lấy, nếu bị một vấn đề nào đó như hết thời gian của cache thì bạn sẽ tiếp tục lấy dữ liệu ở mongodb.

app.get('/api/notes/:id', isCached, (req, res, next) => {

    const { id } = req.params;

    Note.findById(id, (err, note) => {
        if (err) {
            return res.status(404).json(err);
        }
        return res.status(200).json({
            note: note
        });
    });
});

So sánh cache giữa Redis và Firebase

Việc update cũng tương tự như việc insert của một docs. Ở bài viết tiếp theo tôi sẽ giúp các bạn sử dụng fỉebase cho việc cache data. Và so sánh giữa Firebase + Redis bạn nên dùng cái nào.

Có thể bạn đã bị missing