Optimizing System Design for a High‑Traffic Gather‑Order Page with Multi‑Level Caching and Chain‑of‑Responsibility
The author refactors a high‑traffic gather‑order page by introducing a reusable multi‑level cache with update‑key validation, Redis distributed locks and asynchronous refresh, and by restructuring the processing flow using a chain‑of‑responsibility/command pattern, while also handling pagination, weighted sorting, and time‑bound marketing keys to improve reliability, extensibility, and performance.
This article describes how the author refactored and optimized the design of a "gather‑order" (凑单) page that experiences rapid business growth. The focus is on improving cache reliability, reducing empty‑result errors, and structuring the processing logic with design patterns.
Multi‑Level Cache
The original implementation used a local cache that falls back to a Redis cache. When both caches miss, the data source is queried. The basic code is:
return LOCAL_CACHE.get(key, () -> {
String cache = rdbCommonTairCluster.get(key);
if (StringUtils.isNotBlank(cache)) {
return JSON.parseObject(cache, new TypeReference<List<ItemShow>>(){});
}
List<ItemShow> itemShows = getRankingItemOriginal(context, rankingRequest);
rdbCommonTairCluster.set(key, JSON.toJSONString(itemShows), new SetParams().ex(CommonSwitch.rankingExpireSecond));
return itemShows;
});Problems appeared when the cache expired and downstream services returned an empty result, causing the empty value to be cached and the ranking module to disappear for some users.
Cache Update Strategy
To distinguish real empty results from cache‑miss empty values, the author introduced an update‑key with a short TTL. The revised logic updates the value only when the new data is non‑empty:
return LOCAL_CACHE.get(key, () -> {
String updateKey = getUpdateKey(key);
String value = rdbCommonTairCluster.get(key);
List<ItemShow> cache = StringUtils.isBlank(value) ? Collections.emptyList()
: JSON.parseObject(value, new TypeReference<List<ItemShow>>(){});
if (rdbCommonTairCluster.exists(updateKey)) {
return cache;
}
rdbCommonTairCluster.set(updateKey, currentTime, cacheUpdateSecond);
List<ItemShow> itemShows = getRankingItemOriginal(context, rankingRequest);
if (CollectionUtils.isNotEmpty(itemShows)) {
rdbCommonTairCluster.set(key, JSON.toJSONString(itemShows), new SetParams().ex(CommonSwitch.rankingExpireSecond));
}
return itemShows;
});A further improvement abstracted the cache into a reusable GatherCache class:
public class GatherCache<V> {
@Setter private Cache<String, List<V>> localCache;
@Setter private CenterCache centerCache;
public List<V> get(boolean needCache, String key, @NonNull Callable<List<V>> loader, Function<String, List<V>> parse) {
try {
return needCache ? localCache.get(key, () -> getCenter(key, loader, parse)) : loader.call();
} catch (Throwable e) {
GatherContext.error(this.getClass().getSimpleName() + " get catch exception", e);
}
return Collections.emptyList();
}
private List<V> getCenter(String key, Callable<List<V>> loader, Function<String, List<V>> parse) throws Exception {
String updateKey = getUpdateKey(key);
String value = centerCache.get(key);
boolean blankValue = StringUtils.isBlank(value);
List<V> cache = blankValue ? Collections.emptyList() : parse.apply(value);
if (centerCache.exists(updateKey)) {
return cache;
}
centerCache.set(updateKey, currentTime, cacheUpdateSecond);
List<V> newCache = loader.call();
if (CollectionUtils.isNotEmpty(newCache)) {
centerCache.set(key, JSON.toJSONString(newCache), new SetParams().ex(CommonSwitch.rankingExpireSecond));
}
return newCache;
}
}Distributed Lock & Asynchronous Update
When both local and update caches expire simultaneously, multiple requests could hit the downstream service. The solution adds a Redis distributed lock and updates the cache asynchronously:
private List<V> getCenter(String key, Callable<List<V>> loader, Function<String, List<V>> parse) throws Exception {
String updateKey = getUpdateKey(key);
String value = centerCache.get(key);
boolean blankValue = StringUtils.isBlank(value);
List<V> cache = blankValue ? Collections.emptyList() : parse.apply(value);
if (!centerCache.setNx(updateKey, currentTime) && !blankValue) {
return cache;
}
centerCache.set(updateKey, currentTime, cacheUpdateSecond);
CompletableFuture.runAsync(() -> updateCache(key, loader));
return cache;
}
private void updateCache(String key, Callable<List<V>> loader) {
List<V> newCache = loader.call();
if (CollectionUtils.isNotEmpty(newCache)) {
centerCache.set(key, JSON.toJSONString(newCache), cacheExpireSecond);
}
}If the lock acquisition fails, the old cache is returned; the background task refreshes the value later.
Chain of Responsibility & Command Pattern
To handle the complex assembly logic of the gather‑order page (product assembly, cart assembly, ranking, request parameters), the author introduced a chain of responsibility combined with command objects. The core handler executes a list of commands, each of which can perform pre‑checks, timing, and decide whether to continue.
public class ChainBaseHandler<T extends Context> {
public void execute(T context) {
List<String> executeCommands = Lists.newArrayList();
for (Command<T> c : commands) {
try {
if (!c.check(context)) continue;
boolean isContinue = timeConsuming(() -> execute(context, c), c, executeCommands);
if (!isContinue) break;
} catch (Throwable e) {
GatherContext.error(c.getClass().getSimpleName() + " catch exception", e);
}
}
GatherContext.debug(this.getClass().getSimpleName() + "-execute", executeCommands);
}
}
private boolean timeConsuming(Supplier<Boolean> supplier, Command<T> c, List<String> executeCommands) {
long start = System.currentTimeMillis();
boolean result = supplier.get();
long cost = System.currentTimeMillis() - start;
executeCommands.add(c.getClass().getSimpleName() + ":" + cost);
return result;
}Two command types are defined:
interface Command<T> { boolean check(T ctx); boolean execute(T ctx); }
interface SingleCommand<T, D> extends Command<T> { void execute(D data, T ctx); }
interface CommonCommand<T> extends Command<T> { boolean execute(T ctx); }An example command that processes coupons:
public class CouponCustomCommand implements CommonCommand<CartContext> {
@Override public boolean check(CartContext ctx) {
return Objects.equals(BenefitEnum.kdmj, ctx.getRequestContext().getCouponData().getBenefitEnum())
|| Objects.equals(BenefitEnum.plCoupon, ctx.getRequestContext().getCouponData().getBenefitEnum());
}
@Override public boolean execute(CartContext ctx) {
// command logic
return true;
}
}The chain makes the processing flow explicit, improves extensibility, and isolates each concern.
Pagination & Sorting for the Gather‑Cart
The page must keep the original cart order, group items by shop, and support dynamic top‑placement for newly added items. Because the data source is external, the ordering is stored on the client side in a sign object:
public class Sign {
/** The weight already loaded */
private Integer weight;
/** Latest add time of items in this request */
private Long endTime;
/** Sorted list of items from previous request */
private List<CartItemData> activityItemList;
}When the first page loads, items are sorted by shop and add‑time, assigned decreasing weights (e.g., 200, 199…) and saved in sign.activityItemList. Subsequent page requests send the previous sign back; the server uses the stored weights to fetch the next slice and updates sign.weight and sign.endTime. If a newly added item has an add‑time greater than sign.endTime, it is placed on top with the maximum weight.
During pagination, items that become invalid are moved to the bottom of later pages, while newly valid items are promoted.
Marketing Engine Key Design
To retrieve time‑bound marketing items (e.g., flash‑sale products), the key encodes the activity type, start/end timestamps and a pre‑heat period: mkt_fn_t_60_08200000_60 where the segments represent index, function, type, pre‑heat minutes, start time, and duration. This allows the engine to filter only currently active items.
Final Thoughts
The article emphasizes that design patterns should be applied to solve concrete problems—complexity, extensibility, and maintainability—rather than for their own sake. Continuous refactoring is recommended to avoid over‑engineering and to keep the codebase clean.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
