There’s a well-known website in China that almost every Chinese use and we usually only use it once a year — 12306.com, the railway ticket booking system.
When the website open the sales for tickets for Chinese New Year, many people are waiting in front of their computer and get ready to click the
reserve button at the exact time when sale starts.
Then, the website crashed and users complained.
(And then software engineers reflect how to fix it)
The example above is a typical flash sale system. It usually has the following characteristics:
When the user did not get the immediate response after submitting a request, they will tend to click the button again and again. However, those requests are duplicates and create unnecessary load for the system. (Just imagine every user resubmit 9 more requests, then 90% of the requests system received are redundant)
What we can do is to:
This will reduce most of the unnecessary requests from client side, however, it does not prevent user from writing a script to bypass the client side restriction. So we need to do more.
When a request reached the gateway, we keep track of the request number by the same user. The simple and effective solution is to keep a counter for each user ID in memory.
We can drop the request or return the same response if the same request was made recently by this user.
However, what if this user created thousands of user accounts? We will handle it in the service layer.
Now the requests that reached service layer are much less. Let’s start processing them.
The first thing we need to do is checking the available inventory. How do we get the remaining inventory size?
If we query the database with aggregation like
COUNT , firstly, we may need to have a
serialisable isolation level for the transaction which affects performance. If we do not apply this isolation level, we may end up with phantom read and thought that we still have enough inventory this request, and then we sell more than what we have.
To handle this, we can use a counter with atomicity. Redis will be a good choice for atomic decrement when a request need to be processed.
2. Buffer with message queue
Processing a transaction could be quite complex and heavy. For example, automatically applying promotion based on user’s loyalty points.
Thus, let’s use message queue to buffer the request and let the service to process it at its own pace.
Now, the number of request that reaches database is very much reduced to an acceptable number.
We can focus on designing better schema, optimising queries and maybe sharding the table.
So now I can directly copy this design to my project?
NO. Firstly, it depends on your requirements. Secondly, you may not need to spend this much effort, there might be other better ways.
This post is a brief case study of flash sale system design. It provides a general guideline and possibly inspiration to people who is solving similar problem.
Create your free account to unlock your custom reading experience.