Implementing Caching with Redis in Spring Cloud: Local Cache, Distributed Cache, and Locking Strategies
This article explains how to improve Spring Cloud microservice performance by adding local and Redis distributed caching, demonstrates the necessary code changes, and discusses cache pitfalls such as penetration, avalanche, and breakdown along with lock‑based solutions.
In high‑concurrency internet systems, caching is essential for achieving the "three highs" of performance, availability, and scalability. The article first introduces why caching is needed, what data is suitable for caching, and the basic read‑through cache flow.
1. Cache
1.1 Why Use Cache
Caching stores frequently accessed data in memory to reduce database I/O, lower latency, and avoid deadlocks, but only data that is timely, has low consistency requirements, or has high read‑frequency with low update frequency should be cached.
1.2 Local Cache
Using an in‑memory HashMap (or similar) is the simplest form of cache. The article shows a REST API that queries a list of question types directly from the database without caching, then adds a HashMap to store the result.
@RequestMapping("/list")
public R list(){
// from DB
typeEntityList = ITypeService.list();
return R.ok().put("typeEntityList", typeEntityList);
}After adding a local cache:
private Map<String, Object> cache = new HashMap<>();
List<TypeEntity> typeEntityListCache = (List<TypeEntity>) cache.get("typeEntityList");
if(typeEntityListCache == null){
System.out.println("The cache is empty");
List<TypeEntity> typeEntityList = ITypeService.list();
cache.put("typeEntityList", typeEntityList);
typeEntityListCache = typeEntityList;
}
return R.ok().put("typeEntityList", typeEntityListCache);Local cache reduces DB access but has drawbacks: memory consumption, loss on restart, possible data inconsistency, and issues in clustered environments.
2. Redis Cache
2.1 Install Redis via Docker
The article recommends installing Redis with Docker on Ubuntu or macOS (M1) and provides reference links.
2.2 Add Redis Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>2.3 Test Redis
Using StringRedisTemplate to store and retrieve a simple key/value pair:
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void TestStringRedisTemplate(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("悟空", "悟空聊架构_" + UUID.randomUUID().toString());
String wukong = ops.get("悟空");
System.out.println(wukong);
}2.4 Refactor Business Logic with Redis
Replace the HashMap with Redis, serialize objects to JSON before storing, and deserialize when reading:
public List<TypeEntity> getTypeEntityList(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String cache = ops.get("typeEntityList");
if(StringUtils.isEmpty(cache)){
System.out.println("The cache is empty");
List<TypeEntity> fromDb = this.list();
cache = JSON.toJSONString(fromDb);
ops.set("typeEntityList", cache);
return fromDb;
}
return JSON.parseObject(cache, new TypeReference<List<TypeEntity>>(){});
}Testing with Postman shows the first request is slower (cache miss) and subsequent requests are fast (cache hit).
3. Cache Pitfalls
3.1 Cache Penetration
When a non‑existent key is repeatedly queried, the database is hit each time. The solution is to cache the null result with a short TTL.
3.2 Cache Avalanche
If many keys share the same expiration time, they may all expire simultaneously, causing a surge of DB traffic. Adding a random offset to TTL mitigates this.
3.3 Cache Breakdown
When a hot key expires, a flood of requests may hit the DB. Using a lock so that only one request repopulates the cache prevents overload.
4. Lock‑Based Solution for Cache Breakdown
Using a synchronized block (local lock) to ensure only one thread accesses the DB when the cache is empty:
public List<TypeEntity> getTypeEntityListByLock(){
synchronized(this){
String cache = stringRedisTemplate.opsForValue().get("typeEntityList");
if(!StringUtils.isEmpty(cache)){
return JSON.parseObject(cache, new TypeReference<List<TypeEntity>>(){});
}
System.out.println("The cache is empty");
List<TypeEntity> fromDb = this.list();
cache = JSON.toJSONString(fromDb);
stringRedisTemplate.opsForValue().set("typeEntityList", cache, 1, TimeUnit.DAYS);
return fromDb;
}
}The article notes that local locks only work within a single service instance; in a distributed environment they can cause inconsistencies, and a distributed lock (to be covered in the next article) is needed.
5. Problems with Local Locks
When multiple microservice instances use local synchronized locks for stock deduction, race conditions can cause overselling because each instance sees the same cached stock value.
The article concludes by previewing the next part, which will cover distributed lock implementation.
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.
Wukong Talks Architecture
Explaining distributed systems and architecture through stories. Author of the "JVM Performance Tuning in Practice" column, open-source author of "Spring Cloud in Practice PassJava", and independently developed a PMP practice quiz mini-program.
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.
