SpringBoot AOP + Redis
Delayed double deletion ensures data consistency
1. Business scenario
1.1 Problems
When using
Redis
as a cache, there will be inconsistencies between the data in Redis and the database data. In the subsequent query process, it will take a long time to checkRedis
first, thus The occurrence of queried data is not a serious problem with the real data in the database.
1.2 Solution
- When using
Redis
, you need to ensure the consistency ofRedis
and database data. There are many solutions, such as delayed double deletion,canal + MQ
and other strategies, here we use the delayed double deletion strategy to achieve it. - Note:
Redis
is used in a scenario where reading data is much greater than writing data, because the result of the double delete strategy is to delete the data saved inRedis
. Subsequent queries will query the database. Therefore, frequently modified data tables are not suitable for usingRedis
.
Implementation steps of delayed double deletion plan
1> Delete cache
2> Update database
3> Delay 1000 milliseconds (set the delay execution time according to the specific business)
4> Delete cache
2. Code implementation
2.1 Introduce Redis
and SpringBoot AOP
dependencies
<!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2.2 Customized SpringBoot AOP
annotations
2.2.1 Cache
Annotations
@Retention(RetentionPolicy.RUNTIME) @Documented @Target(ElementType.METHOD) public @interface Cache {<!-- --> String name() default ""; }
2.2.2 Cache
Aspects
package com.xxx.demo.aop; import cn.hutool.core.lang.Assert; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; import com.xxx.demo.modules.system.entity.User; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; @Aspect @Component @Slf4j public class CacheAspect {<!-- --> @Resource private StringRedisTemplate stringRedisTemplate; /** * entry point */ @Pointcut("@annotation(com.xxx.demo.aop.Cache)") public void pointCut(){<!-- --> } /** * Surround notifications */ @Around("pointCut()") public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){<!-- --> log.info("---------- Surround notification -----------"); log.info("Target method name of surrounding notification: {}", proceedingJoinPoint.getSignature().getName()); Signature signature1 = proceedingJoinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature)signature1; //method object Method targetMethod = methodSignature.getMethod(); //Reflect to get the method object of the custom annotation Cache annotation = targetMethod.getAnnotation(Cache.class); String userId = getUserId(); Assert.notNull(userId, "id is null"); //Get the parameter of the method object of the custom annotation, which is name String name = annotation.name(); String redisKey = name + ":" + userId; //Fuzzy definition key String res = stringRedisTemplate.opsForValue().get(redisKey); if (!ObjectUtils.isEmpty(res)) {<!-- --> log.info("Returned from cache, data is: {}",res); return JSONUtil.toBean(res, User.class); } Object proceed = null; try {<!-- --> proceed = proceedingJoinPoint.proceed(); } catch (Throwable throwable) {<!-- --> throwable.printStackTrace(); } if (!ObjectUtils.isEmpty(proceed)) {<!-- --> stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(proceed)); } log.info("Returned from the database, the data is: {}",proceed); return proceed; } private static String getUserId() {<!-- --> ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // Get all path parameters in the request path String requestURI = request.getRequestURI(); String[] pathSegments = requestURI.split("/"); return pathSegments[3]; } }
2.2.3 ClearAndReloadCache
Annotations
@Retention(RetentionPolicy.RUNTIME) @Documented @Target(ElementType.METHOD) public @interface ClearAndReloadCache {<!-- --> String name() default ""; }
2.2.3 ClearAndReloadCache
Aspects
package com.xxx.demo.aop; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; import com.xxx.demo.modules.system.entity.User; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.lang.reflect.Method; @Aspect @Component @Slf4j public class ClearAndReloadCacheAspect {<!-- --> @Resource private StringRedisTemplate stringRedisTemplate; /** * entry point */ @Pointcut("@annotation(com.xxx.demo.aop.ClearAndReloadCache)") public void pointCut() {<!-- --> } /** * Surround notifications */ @Around("pointCut()") public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {<!-- --> log.info("---------- Surround notification -----------"); log.info("Target method name of surrounding notification: {}", proceedingJoinPoint.getSignature().getName()); Object[] args = proceedingJoinPoint.getArgs(); String userString = JSON.toJSONString(args[0]); User bean = JSONUtil.toBean(userString, User.class); Signature signature1 = proceedingJoinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature1; // method object Method targetMethod = methodSignature.getMethod(); // Reflect to get the method object of the custom annotation ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class); // Get the parameter of the method object of the custom annotation, which is name String name = annotation.name(); String redisKey = name + ":" + bean.getId(); //Delete the key value of redis stringRedisTemplate.delete(redisKey); // Execute the business of modifying the database with double deletion annotations, that is, the method business in the controller Object proceed = null; try {<!-- --> proceed = proceedingJoinPoint.proceed(); } catch (Throwable throwable) {<!-- --> throwable.printStackTrace(); } //Open a thread and delay for 1 second (here is an example of 1 second, you can change it to your own business) // Delay deletion in the thread and return the result of the business code at the same time so that it does not affect the execution of the business code new Thread(() -> {<!-- --> try {<!-- --> Thread.sleep(1000); stringRedisTemplate.delete(redisKey); log.info("-----------After 1 second, the delayed deletion in the thread will be completed -----------"); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); } }).start(); // Return the value of the business code return proceed; } }
2.3 SQL
Script
DROP TABLE IF EXISTS sys_user; CREATE TABLE sys_user ( 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 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO sys_user VALUES (1, 'Prince'); INSERT INTO sys_user VALUES (2, 'Pig Girl'); INSERT INTO sys_user VALUES (3, 'robot'); INSERT INTO sys_user VALUES (4, 'Tsar'); INSERT INTO sys_user VALUES (5, 'Gwen'); INSERT INTO sys_user VALUES (6, 'crocodile');
2.4 UserController.java
package com.xxx.demo.modules.system.api; import com.xxx.demo.aop.Cache; import com.xxx.demo.aop.ClearAndReloadCache; import com.xxx.demo.modules.system.entity.User; import com.xxx.demo.modules.system.service.IUserService; import io.swagger.annotations.Api; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/user") @Api(description = "Test-Interface") public class UserController {<!-- --> @Autowired private IUserService iUserService; @GetMapping("/get/{id}") @Cache(name = "getUser") public User get(@PathVariable("id") Integer id){<!-- --> return iUserService.get(id); } @PostMapping("/update") @ClearAndReloadCache(name = "getUser") public int updateData(@RequestBody User user){<!-- --> return iUserService.update(user); } }
3. Testing and verification
3.1 Get id=1
, the cache type does not exist, check the database
3.2 Get id=1
, it exists in the cache, and directly return the cached data
3.3 Modify id=1, username=Jiawen IV
, delete cache
3.4 Get id=1
again. At this time, the cache does not exist. Check the database
4. Summary
- Why the delay of 1000 milliseconds?
Answer: This is to complete the database update operation before deletingRedis
for the second time. If there is no delayed operation, there is a high probability that after the two deletionRedis
operations are completed, the data in the database has not been updated. If there is a request to access the data at this time, the problem we started with will still appear. The data inconsistency issues mentioned. In addition, the delay time here needs to be set according to the business time of your own system.- Why delete the cache twice?
Answer: If we do not have a second deletion operation and there is a request to access data at this time, it may be the accessedRedis
data that has not been modified before. After the deletion operation is executed,Redis
> is empty. When a request comes in, the database will be accessed. At this time, the data in the database is already updated, ensuring data consistency.