NOTE

Designing an E-Commerce Detail Page

A historical note on e-commerce detail-page caching architectures, cache consistency, two-layer Nginx caching, and cache warm-up.

System DesignCreated Updated 3 min readhistorical

This is a historical learning note and may contain outdated or incomplete understanding.

1. E-Commerce Detail Page

1.1. Small E-Commerce Site

1.1.1. Rebuild HTML

If the data volume is relatively small, a fully static design can be used: the static-generation system queries all product data from the database, renders it into HTML files with Thymeleaf + HTML templates, places the files in the Nginx directory, and serves them through Nginx.

  • Advantage: very high access efficiency.
  • Disadvantage: once the template changes, everything must be rendered again. When the data volume is very large, rerendering everything is not practical.

Small e-commerce site - rebuild HTML

1.1.2. Write Flow

Small e-commerce site - write flow

1.1.3. Read Flow

Small e-commerce site - read flow

1.2. Large E-Commerce Site

1.2.1. Low Freshness Requirement

Use a three-level cache composed of Nginx + Redis + JVM, and use Thymeleaf to dynamically render HTML templates into HTML.

1.2.1.1. Read Flow [Read-through]

Large e-commerce site - read flow, low freshness requirement

1.2.1.2. Write Flow [Write-behind]

After other services modify data, write an MQ message and route it to the cache service. The cache service reads the latest data and updates the cache.

Large e-commerce site - write flow, low freshness requirement

1.2.2. High Freshness Requirement

1.2.2.1. Write Flow [Cache Aside Pattern]

Large e-commerce site - write flow, high freshness requirement

1.2.2.2. Read Flow [Cache Aside Pattern]

Large e-commerce site - read flow, high freshness requirement

2. Cache Update Strategies

2.1. Cache Aside Pattern

  1. On a read, read the cache first. If the cache does not contain the data, read the database, put the returned data into the cache, and return the response.
  2. On an update, delete the cache first and then update the database.

2.1.1. Why Delete the Cache Instead of Updating It?

The idea is lazy computation. Some cached values need computation, which consumes a lot of CPU.

2.1.2. Problems

2.1.2.1. Why Delete the Cache First During an Update?

If the database is updated first and the cache is deleted afterward, this can happen: the database update succeeds but cache deletion fails. Subsequent reads then continue to read old data.

2.1.2.2. A Read Arrives Before the Data Write Finishes
  1. During an update, first delete the cache and then modify the database. At this moment the database modification is not yet complete.
  2. A read request arrives, finds the cache empty, queries the database, reads the old data from before the modification, and puts it back into the cache.

In this situation, the cache still returns old data.

Solution: asynchronous serialization.

  1. When updating or reading data, route operations into an in-JVM queue according to the unique identifier of the data.
  2. One queue corresponds to one worker thread. Each worker thread serially takes and executes the corresponding operations one by one.

Asynchronous serialization for cache-consistency problems

2.1.2.3. Multiple Reads Follow a Write and Keep Updating the Cache

If a read and a write are already ahead in the queue, subsequent reads do not need to enter the queue; they can simply wait for a period of time, avoiding meaningless repeated cache updates.

2.1.2.4. Many Writes Cause Reads Behind Them to Block for a Long Time

Use multiple instances so each instance shares part of the write operations and no queue contains too many writes.

2.1.2.5. Many Reads Block in the Queue

Run a load test.

2.1.2.6. Request Routing with Multiple Service Instances

After introducing multiple instances, how can requests with the same ID always be routed to a fixed instance and a fixed queue? Use Nginx to route by taking the ID modulo the number of instances.

2.1.2.7. Hot Products Cause Skewed Routing

Run a load test.

2.1.3. Implement the Inventory Service

Implementing a Cache/Database Double-Write Consistency Solution in the Inventory Service

2.2. Asynchronously Update the Cache

2.2.1. Problems

2.2.1.1. Concurrent Cache-Rebuild Conflicts

Solution: use a distributed lock.

2.2.1.2. Full Cache Updates Are Too Inefficient

A full cache update is too inefficient. For example, suppose a serialized JSON value is stored. To update it, the value is read and deserialized, a field is updated, then it is serialized to JSON again and stored back in the cache.

Solution: split the cache into dimensions.

Cache dimensionalization

2.2.1.3. Low Hit Rate of Nginx Local Cache

Solution: deploy Nginx in two layers.

Two-layer Nginx deployment

2.2.1.3.1. Distribution-Layer Nginx
  • OpenResty
    HTTP

  • nginx.conf

upstream shopping_gateway {
    server 127.0.0.1:8001;
}

server {
    listen       80;
    server_name  localhost;

    charset utf-8;

    #access_log  logs/host.access.log  main;

    location /cache {
            default_type 'text/html';
            content_by_lua_file /home/[user]/software/openresty/nginx/lua/distributed_cache.lua;
    }

    location /hello {
            default_type 'text/plain';
            content_by_lua 'ngx.say("Hello, Lua")';
    }

    location /hello2 {
            default_type 'text/plain';
            content_by_lua_file /home/[user]/software/openresty/nginx/lua/hello.lua;
    }

    location /lua {
            default_type 'application/json';
            content_by_lua_file /home/[user]/software/openresty/nginx/lua/distributed_lua.lua;
    }

    # Proxy backend requests
    location / {
            proxy_pass http://shopping_gateway;
    }

    # Serve static resources
    location ~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|js|css)$ {
    root html;
    }
}
  • distributed_cache.lua
-- Example URL: http://localhost/lua?method=hello&productId=1
-- Get the parameters after the question mark
local uri_args = ngx.req.get_uri_args()
local method = uri_args["method"]
local productId = uri_args["productId"]

-- Decide which nginx_cache to dispatch to
local nginx_host = {"127.0.0.1:9000", "127.0.0.1:9001"}
local dispatch_ngx_host_index = (ngx.crc32_long(productId) % 2) + 1
dispatch_ngx_url = "http://"..nginx_host[dispatch_ngx_host_index]

-- Build the concrete request path without the host
local request_path = "/lua?method="..method.."&productId="..productId

-- Access nginx_cache
local http = require("resty.http")
local httpc = http.new()
local resp, err = httpc:request_uri(dispatch_ngx_url, {
    method = "GET",
    path = request_path,
    keepalive=false
})

if not resp then
    ngx.say("request error :", err)
    return
end

ngx.say(resp.body)

httpc:close()
2.2.1.3.2. Application-Layer Nginx
  • OpenResty
    HTTP, cjson, template

  • nginx.conf

lua_shared_dict my_cache 128m;
server {
    listen       9000;
    server_name  localhost;

    charset utf-8;

    #access_log  logs/host.access.log  main;

    # Configure the template path
    set $template_location "/templates";
    # This path needs to exist because it will later store HTML files
    set $template_root "/home/[user]/software/openresty_cache2/nginx/html/templates";

    location / {
        root   html;
        index  index.html index.htm;
    }

    location /hello {
            default_type 'text/plain';
            content_by_lua 'ngx.say("Hello, Lua")';
    }

    location /hello2 {
            default_type 'text/plain';
            content_by_lua_file /home/[user]/software/openresty_cache/nginx/lua/hello.lua;
    }

    location /cache {
            default_type 'text/html';
            content_by_lua_file /home/[user]/software/openresty_cache/nginx/lua/render.lua;
    }

    location /lua {
            default_type 'application/json';
            content_by_lua 'ngx.say("server9000")';
    }
}
  • render.lua
local uri_args = ngx.req.get_uri_args()
local productId = uri_args["productId"]

-- Get the cache object assigned in the previous configuration
local cache_ngx = ngx.shared.my_cache

-- Build the cache key
local productCacheKey = "product_info_"..productId

-- Get the value from the cache
local productCache = cache_ngx:get(productCacheKey)

-- If the corresponding value does not exist in the cache,
-- query the backend cache service
-- (the cache service checks Redis first, then Ehcache, then the database)
if productCache == "" or productCache == nil then
        local http = require("resty.http")
        local httpc = http.new()
        -- This address is the development-machine IP.
        -- Directly accessing the development environment is convenient here.
        local resp, err = httpc:request_uri("http://127.0.0.1:8001",{
                method = "GET",
                path = "/cache/levelCache/item/"..productId,
                keepalive=false
        })

        if not resp then
                ngx.say("request error :", err)
                return
        end

        productCache = resp.body
        -- After obtaining it, put it into the cache
        cache_ngx:set(productCacheKey, productCache, 10 * 60)
end

--if productCache ~= nil then
--      ngx.say("productCache:", productCache)
--      return
--end

-- The cached value is a string,
-- so use cjson to convert the string into a JSON object
local cjson = require("cjson")
local productCacheJSON = cjson.decode(productCache)

-- Combine product information and shop information into one large JSON object.
-- The template renderer expects this form.
local context = {
        productId = productCacheJSON.data.id,
        productTitle = productCacheJSON.data.title,
        productSellPoint = productCacheJSON.data.sellPoint
}

-- Render the product.html template with template
local template = require("resty.template")
template.render("product.html", context)
  • templates/product.html
product id: {* productId *}<br />
product title: {* productTitle *}<br />
product sell point: {* productSellPoint *}<br />
2.2.1.3.3. Test
  1. Login
  • POST http://localhost/user/login
  • Parameters:
{
    "username":"<example-user>",
    "password":"<example-password>"
}
  1. Test the Nginx cache
  • GET http://localhost/cache?productId=973825
  • Request header:
token:<example-token>

3. Cache Warm-Up

Cache warm-up

Discussion

Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub