Node.js runtime mode

Let’s look at the node.js runtime with a little code

const express = require('express')
const app = express()

let times = 0

app.get('/inc',  (req, res) => {
  times += 1
  res.end('success')
})

app.get('/currentValue', (req, res) => {
    res.end(times)
})

app.listen(3000)

Copy the code

This is a classic example of why locking is necessary in a multithreaded language such as Java

We used some pressure testing tools or wrote our own code to request the/Inc interface 1000 times in a very short time

Then call /currentValue to get the value of times

We can see that the same code written in Java does not have a value of 1000 (which is usually less than 1000), but the same code written in node.js does have a value of exactly 1000. Why?

Node and Java have different runtime models

  • The Java server runs a single-process multithreaded model by default
  • The Node server runs a single-process, single-threaded model by default

When the server receives a network request, Java will open a new thread to handle the request each time. The different threads are running synchronously, so the Java server running the above example without a lock will encounter the classic problem of multiple threads simultaneously modifying a variable and resulting in a data error.

Node was designed without multithreading. With the exception of libuv, which has an internal thread pool, our own code runs on the main thread, so we don’t have to deal with multithreading competition.

Why do you need a multi-process deployment when Node is single-process and single-threaded by default?

The reason is simple: if we don’t do multi-process deployment, we’re wasting 3 cores on a 4-core CPU server. Or when the volume of user requests increases, a distributed cluster deployment is also required

The simplest way to deploy multiple processes

Run as a PM2 cluster (Document)

pm2 start index.js -i max –name my-app

Pm2 will automatically divide the requests among different processes

What are the problems with a multi-process deployment?

The most obvious problem is that process space is no longer shared, and each process has its own process space. Going back to our original example of this article, suppose we now run the demo on a two-core CPU with multiple processes and access the /inc interface 1000 times. And what happens when I go to /currentValue?

We can see that the return is constantly changing, there are two different values, but their sum adds up to 1000, obviously because there are two different times objects

So how do different processes share data? For example, when I do the development of wechat, there will be a wechat access_token, every time after getting a new token, the old token will be automatically invalidated by wechat, this token is obtained through an HTTP interface, sample code

class WeixinTokenService {
    private token = null
    
    async getToken() {
        if (!this.token) {
            this.token = await someHttpResquest()
        }
        return this.token
    }
}
Copy the code

This is a very simple example, which is basically correct when running in a single process (but there is a small probability of bug, I don’t know if anyone can find 😊) but in a multi-process model, after the first process gets the token, the second process’s token is still empty. It then initiates a new request that invalidates the token received by the first process.

So how to solve this problem? The answer is simple. There are two common ways

  • After obtaining the token each time, the token will be stored in the Redis or database, and everyone will share
  • Deploy the token acquisition as a single microservice in a single-process manner (because this code is different from business code, there is no high concurrency pressure).

The code to solve this problem with Redis is as follows:

class WeixinTokenService {
    private redisClient = initRedisClient()
    
    async getToken() {
        // line 1
        let token = await this.redisClient.get('wx-token')  
        if(! token) {// line 2
            token = await someHttpResquest()
            // line 3
            await this.redisClient.set('wx-token', token)
        }
        return token
    }
}
Copy the code

At this point, this problem was basically solved, but a new problem emerged:

Let’s imagine a scenario where two user requests arrive at the same time, they execute line 1 together, neither of them get the token, then execute line 2 together, and then execute line 3 at the same time. At this time, the token received by one process is discarded, and which one is stored in redis? That’s what we didn’t expect.

Consider: The same thing happens with the single-process model (remember I said there was a small probability of a bug?).

However, the single-process solution is relatively simple, with variable notation (setting a variable to indicate whether the token is currently being requested, to prevent multiple HTTP requests).

How do you solve this problem with multiple processes? We’ll talk about that in the next article.

Welcome to join the Node Development Advanced Advanced group, the group master has time to give you a number of nodes in the real world will encounter a variety of problems