Nodejs - Cách thiết kế hệ thống tặng phiếu giảm giá tăng đột biến

Nội dung bài viết

Ở những bài trước tôi đã trình bày hai vấn đề đó là thiết kế hệ thống sao cho cách tính điểm ưu đãi, tích điểm xu trong Shopee, và vấn đề còn lại là cách triển khai sử dụng Message Queue với rabbitmq. Bạn có thể tìm hiểu bài trước để hiểu hoen về bài này.

Riêng về rabbitmq thì tôi có làm một video giới thiệu về cách đặt hàng thông qua MQ. Các bạn cũng có thể xem lại.

Tình huống đồng thời thực tế trong kiến trúc phần mềm.

Thực tế trong giao diện người dùng tính đồng thời rất hiếm gặp. Có chăng thì một số tình huống điển hình gặp phải, điển hình như là Shopee hay có những chương trình lấy phiếu giảm giả vào lúc 12h, hoặc mở bán vé tàu ngày tết. Đó là những tình huống cụ thể. Bài viết này sẽ nói về các trường hợp tăng đột biến phổ biến để nói về những công nghệ nào sẽ được sử dụng khi một ứng dụng sử dụng Node.js gặp phải tình huống này. Tất nhiên như thường lệ, đây là một phải pháp trong những giải pháp chứ không hề đảm bảo cách này có thể giải quyết được 100% vấn đề đặt ra. Nhưng tối viết một, hy vọng các bạn cũng sẽ viết lại một. Cho mọi người được học hỏi với nhau. Và mượn tạm hình ảnh của Shopee cho bài viết này. Nodejs - Cách thiết kế hệ thống tặng phiếu giảm giá tăng đột biến


Và công nghệ sử dụng trong bài viết này là Node.js, Mongodb và Redis. Cho nên yêu cầu các bạn phải có kiến thức cơ bản. Bạn có thể tham khảo qua redis cheat sheetmongodb cheat sheet

Tình huống nhận phiếu giảm giá của Shopee

Trường hợp 1: Mỗi người chỉ nhận được một phiếu giảm giá.

Trước tiên để tôi thiết kế một model schema mongoose:

'use strict';
/**
 * Module dependencies.
 */

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const phieuGiamGia = new Schema({
    userId: {type: String, required: true}
})

module.exports = mongoose.model('phieuGiamGia', phieuGiamGia);

Quy trình thì bình thường chúng ta sẽ check xem người này đã nhận hay chưa, nếu nhận rồi thi không cần thiết phải làm gì, ngược lại cho người ta nhận. Code như sau:

isGiamGia = async( req, res) => {
    const {userId} = req.body;
    const record = await phieuGiamGia.findOne({
        userId
    });
    if(userId){
        return res.json({
            isGet: true})
    }
    // Tao phieu giam gia
    const creRecord = await  phieuGiamGia.create({userId})
    return res.json({
        isGet: creRecord ? true : false
    })
}

Nhìn vào quy trình bạn thấy nó đơn giản, đúng nó rất đơn giản và nếu hệ thống bình thường có lẽ nó cũng được áp dụng. Nhưng chúng ta đang xem xét, tình huống đồng thời. Thực tế, thói người dùng là không click button lấy mã giảm giá một cách ngoan ngoãn đâu, thói quen click liên tục liên tục, hoặc viết tool auto click. Đương nhiên là trên giao diện sẽ có một cơ chế dislable button đó cho đến khi request thành công, nhưng đối với mấy anh dùng tool thì sao?? Có ai vậy không? nếu có bỏ đi nghe. Thì như vậy đoạn code trên có vấn đề và có thể xảy ra một người có hai phiếu giảm giá là chuyện đương nhiên.

Vấn đề đó nằm ở chỗ là hàm check phiếu và hàm insert phiếu được tách ra làm hai giai đoạn hay nói cách khác là được thực hiện riêng biệt. Tức là có một thời điểm user A click liên tục, và khi hàm check phiếu giảm giá chưa thục hiện xong thì có cái khác chạy vào rồi. Cuối cùng có 2 mã. Đó là một sự số sương sương thôi. Sướng quá... Vậy cách giải quyết thế nào?

Tôi để lại cho các bạn comment đã, rồi tôi sẽ viết giải pháp sau. Vậy mới học chứ???

Cách giải quyết mỗi người một phiếu

Thật ra giải pháp cũng dễ dàng, nếu lý do trên kia là do hai hàm hoạt động riêng biệt đó là findOne()create(). Vậy thì làm thế nào để câu truy vấn và hàm tạo thực thi cùng nhau, loại bỏ tiến trình kẽ hở cho kẻ không ngoan ngoãn. Nếu bạn sử dụng mongoose thì việc đó trở nên dễ dàng với câu lệnh được hỗ trợ sau là findOneAndUpdate() tức là tìm và sửa đổi dữ liệu. Và tôi sẽ viết lại câu lệnh trên lại như sau:

const isGiamGia = async({userId}) => {
    const record = await phieuGiamGia.findOneAndUpdate({
        userId
    }),{
        $setOnInsert: {
            userId,
        },
    }, {
        new: false,
        upsert: true,
    });
    if (!record) {
        this.isGiamGia();
    }
}

Trên thực tế bạn phải hiểu về atom mongodb. Đây là một hoạt động atom. hay còn gọi là nguyên tử trong Db.

Atom (nguyên tử) không thể để hai nguyên tử hoạt động trên cùng một biến cùng một lúc.

Bạn có thể đọc thêm về Atomicity (database systems)). hầu hết trong database hiện nay đều support. Ở đây bạn chú ý thêm về những từ sau:

$setOnInsert: Cho biết trường sẽ được chèn khi thêm vào trong nó. upsert: true: Phần truy vấn (userId) nếu không tồn tại thì sẽ được tạo mới. new: false: Nghĩa là trả về return về giá trị query thay vì giá trị sửa đổi.

Ngay cả khi có các yêu cầu đồng thời đến tại thời điểm này, cũng không thể cho hai dữ liệu xảy ra. Yên tâm, nhớ đọc thêm về findOneAndUpdate() trong mongoose.

Nhưng thật ra, chúng tôi không làm điều này với DB trong thời gian thực. Mà thật ra chúng tôi sử dụng redis để xử lý công việc này. Yên tâm redis có thể chịu đượng được 3K đến 4K / giây request. Sướng chưa?

const isGiamGia = async({userId}) => {
  const result = await this.redis.setnx(userId, 'true');
  if (result === 1) {
    this.isGiamGia();
  }
}

Tương tự setnx là một atom redis điều đó có nghĩa là nếu userId không có giá trị thì set bằng true, ngược lại nếu có rồi thì nó sẽ không làm gì cả. Đây chỉ là một minh chứng về xử lý đồng thời, các dịch vụ trực tuyến thực tế cũng cần được xem xét trong redis.

Nên nhớ rằng khi bạn làm việc với redis thì bạn phải hiểu sâu sắc về những sự cố của redis. Không đơn giản là chỉ biết cache. Bạn nên đọc thêm những bài viết này, vì nó có liên quan đến bài viết này:

Redis – 3 vấn đề LỚN có thể mất việc khi sử dụng cache

Còn nếu một người nhận được vô số phiếu giảm thì sao????

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