How We Cut DB QPS by 80% with a Custom DAO Cache Layer

This article explains how a high‑traffic e‑commerce system reduced database QPS by over 80% and response latency by more than 40% through a systematic DAO‑level caching solution, detailing the background, design trade‑offs, implementation steps, global cache architecture, deployment practices, and observed limitations.

dbaplus Community
dbaplus Community
dbaplus Community
How We Cut DB QPS by 80% with a Custom DAO Cache Layer

Background

In early 2020 the value‑added business system saw a sharp traffic increase after the pandemic, pushing single‑instance MySQL QPS above 30,000. The resulting high latency, thread contention, and costly context switches exposed a DB‑layer bottleneck that existing Redis and Guava caches could not solve, prompting the design of a dedicated DAO cache component.

First Cache Component (daoCache)

Design Considerations

Internal factors : only six key tables needed caching, the codebase exceeded 300k lines, and changes to existing DAO code had to be minimal.

External factors : company‑wide Redis client restrictions (no batch ops, no Lua scripts) limited lock‑free optimizations.

Consistency : evaluated four classic update orders (DB‑then‑cache, cache‑then‑DB, delete‑cache‑then‑DB, DB‑then‑delete‑cache) and chose a pragmatic mix that favors most cases while falling back to DB reads for edge cases.

Performance : aim to reduce Redis round‑trips, keep cache hit rate above 80 %, handle abnormal flows, limit lock contention, and keep the solution lightweight.

Cost : low operational overhead and fast rollout.

Implementation

The component uses a MyBatis plugin to intercept DAO calls. At application start, all methods annotated with @DaoCache are scanned and a proxy is generated. The plugin intercepts CRUD operations as follows:

Read path : a cache hit returns data directly; a miss triggers a DB query followed by a Redis write.

Write path : updates never write to the cache directly; they delete relevant keys, letting the next read repopulate the cache.

No distributed locks are used; an auto‑expire marker (default 10 s) prevents stale writes during concurrent updates. All keys have a TTL of 10–20 minutes to guarantee eventual consistency.

Key data structures:

bizKey : business primary key (e.g., orderId).

bizValue : value associated with the bizKey.

selectKey / selectKeyValue : auxiliary keys used when the query does not contain the primary key.

methodReturnValue : the DAO method’s return payload.

Mappings are stored in Redis SET and HASH structures, enabling fast reverse look‑ups and bulk invalidation.

Cache flow diagram
Cache flow diagram
Cache interaction diagram
Cache interaction diagram

Global Cache Hierarchy

The system adopts a two‑level cache design:

Level‑1 (process‑local) : in‑memory cache for ultra‑low latency, but without cross‑instance consistency.

Level‑2 (Redis) : shared cache across instances, logically divided into four zones—generic service cache, daoCache, application‑specific cache, and shared memory cache.

A Provider/Consumer model is used: providers own the data source and can read/write the cache; consumers only read and trigger a provider‑side fill on a miss. Redis also serves as a registration center for cache keys.

Shared Cache Mechanics

MQ messages carry only the business primary key; consumers delete local entries and lazily reload on next access.

Providers mark updates in Redis; a leader node periodically checks for stale markers (e.g., >30 s) and cleans up.

Periodic full‑sync (default 20 min) ensures eventual consistency between DB, Redis, and local caches.

Cache updates are de‑duplicated and batched to avoid message storms during bulk data changes.

Deployment & Operations

Rollout is performed gradually: low‑risk, low‑frequency DAO methods are enabled first, with a switch to bypass the cache if anomalies appear. Continuous diffing between cache and DB responses helps catch inconsistencies early.

Redis failures are tolerated by falling back to DB reads; connection timeouts are shortened and a sliding‑window splitter limits Redis traffic to 50 % of total requests.

Limitations & Lessons Learned

Placing the cache near the request tail yields limited overall benefit; higher gains are achieved when caching earlier in the request chain.

DAO methods must include the primary key in updates; otherwise the cache cannot reliably invalidate related entries.

One‑to‑many queries require explicit annotation ( @DaoCacheSelectMethod) and are not automatically handled.

Complex multi‑table joins are not supported by the current mapping logic.

Results

The systematic cache redesign reduced database QPS by more than 80 %, cut interface latency from 150 ms to 90 ms, and lowered inter‑service RPC calls by over 40 %. While the solution is tightly coupled to the project’s coding conventions, the underlying design principles—layered caching, clear consistency trade‑offs, and lightweight deployment—are applicable to many high‑throughput backend systems.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaperformanceMyBatis
dbaplus Community
Written by

dbaplus Community

Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.