Ensuring DB‑Redis Consistency with a Delayed Double‑Delete Strategy in Spring Boot
The article explains how concurrent database updates can cause Redis cache inconsistency, introduces the delayed double‑delete solution, details each step of the algorithm, provides full Spring Boot AOP implementation code, demonstrates verification with test cases, and shares the complete project repository.
Business Scenario
In a multithreaded environment two concurrent requests modify the database and then cascade updates to Redis. Interleaving A→C→D→B can leave Redis with stale data while the database has already been updated.
Atomic operations across threads may interleave.
Problem
When request A updates the database and later request C updates the same row, the Redis entry written by A becomes inconsistent with the database. Subsequent reads that hit Redis return stale data.
Solution – Delayed Double‑Delete
Popular strategy for read‑heavy data where writes are infrequent. The steps are:
Delete cache keys that match the affected method.
Execute the database update.
Wait a configurable delay (e.g., 500 ms) to ensure the DB transaction completes.
Delete the same cache keys again.
The first delete prevents a read from seeing the old value before the DB update; the second delete removes any value that might have been written by a concurrent read during the delay, guaranteeing consistency.
Implementation
Dependencies (Maven)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>Custom annotation
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
String name() default "";
}Aspect
@Aspect
@Component
public class ClearAndReloadCacheAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
public void pointCut() {}
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint pjp) {
MethodSignature ms = (MethodSignature) pjp.getSignature();
Method method = ms.getMethod();
ClearAndReloadCache ann = method.getAnnotation(ClearAndReloadCache.class);
String name = ann.name();
// first delete
Set<String> keys = stringRedisTemplate.keys("*" + name + "*");
stringRedisTemplate.delete(keys);
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable t) {
t.printStackTrace();
}
// delayed second delete
new Thread(() -> {
try {
Thread.sleep(1000); // example delay, adjust per business
Set<String> keys2 = stringRedisTemplate.keys("*" + name + "*");
stringRedisTemplate.delete(keys2);
System.out.println("Delayed delete completed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
return result;
}
}Application configuration (application.yml)
server:
port: 8082
spring:
redis:
host: localhost
port: 6379
cache:
redis:
time-to-live: 60000 # 60 s
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test
username: root
password: 1234
mybatis-plus:
mapper-locations: classpath*:com/pdh/mapper/*.xml
global-config:
db-config:
table-prefix:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: trueSQL script (user_db.sql)
DROP TABLE IF EXISTS `user_db`;
CREATE TABLE `user_db` (
`id` int(4) NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
INSERT INTO `user_db` VALUES
(1,'张三'),(2,'李四'),(3,'王二'),(4,'麻子'),(5,'王三'),(6,'李三');Controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/get/{id}")
@Cache(name = "get method")
public Result get(@PathVariable("id") Integer id) {
return userService.get(id);
}
@PostMapping("/updateData")
@ClearAndReloadCache(name = "get method")
public Result updateData(@RequestBody User user) {
return userService.update(user);
}
@PostMapping("/insert")
public Result insert(@RequestBody User user) {
return userService.insert(user);
}
@DeleteMapping("/delete/{id}")
public Result delete(@PathVariable("id") Integer id) {
return userService.delete(id);
}
}Service (core methods)
@Service
public class UserService {
@Resource
private UserMapper userMapper;
public Result get(Integer id) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getId, id);
User user = userMapper.selectOne(wrapper);
return Result.success(user);
}
public Result insert(User user) {
int cnt = userMapper.insert(user);
return cnt > 0 ? Result.success(cnt) : Result.fail(888, "DB insert failed");
}
public Result delete(Integer id) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getId, id);
int cnt = userMapper.delete(wrapper);
return cnt > 0 ? Result.success(cnt) : Result.fail(888, "DB delete failed");
}
public Result update(User user) {
int cnt = userMapper.updateById(user);
return cnt > 0 ? Result.success(cnt) : Result.fail(888, "DB update failed");
}
}Test Verification
Insert a new record with id=10.
First GET /user/get/10 loads data from the database and stores it in Redis.
Subsequent reads hit Redis (verified by Redis client output).
While a thread updates the username for id=10, another thread reads the same key before the delayed second delete; the read returns the stale value, demonstrating inconsistency.
After the delayed second delete (example 1 s), the Redis entry is removed; all reads fetch the fresh database state.
Project Repository
https://gitee.com/jike11231/redisDemo.git
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
