Spring Boot + Redis delayed double deletion function, actual combat is here!

Because the official account has changed the push rules, please click “Looking” and add “star” to get exciting technology sharing as soon as possible

Click to follow the #InternetArchitect official account and get the full set of architect information here 42c7a6b696f0dc5a7b3a755c70bf84 6d.png

0,2T architect learning materials dry content

Previous article: Sharing of useful learning materials for 2T architects

Hi everyone, I am an Internet Architect!

1. Business scenario

In the case of multi-thread concurrency, suppose there are two database modification requests. In order to ensure the data consistency between the database and redis, the implementation of the modification request requires modifying the database and then cascadingly modifying the data in Redis.

  • Request 1: A modifies database data and B modifies Redis data

  • Request 2: C modifies database data and D modifies Redis data

In a concurrent situation, there will be a situation like A -> C -> D -> B.

It is important to understand that when threads execute multiple sets of atomic operations concurrently, the execution order may overlap.

1. Problems at this time

The data modified by A in the database is eventually saved in Redis, and C also modified the database data after A.

At this time, there is an inconsistency between the data in Redis and the database data. In the subsequent query process, Redis will be checked first for a long time, resulting in a serious problem that the queried data is not the real data in the database.

2. Solution

When using Redis, you need to maintain the consistency of Redis and database data. One of the most popular solutions is the delayed double delete strategy.

Note: You must know that frequently modified data tables are not suitable for using Redis, because the result of the double delete strategy is to delete the data saved in Redis, and subsequent queries will query the database. Therefore, Redis uses a data cache that reads far more than changes.

Implementation steps of delayed double deletion plan

  1. Delete cache

  2. Update database

  3. Delay 500 milliseconds (set the delay execution time according to the specific business)

  4. Delete cache

3. Why is there a delay of 500 milliseconds?

This is so that we can complete the database update operation before deleting Redis for the second time. Let’s pretend that if there is no third step, there is a high probability that after the two deletion Redis 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 mentioned at the beginning will appear. That question.

4. Why do you need to delete the cache twice?

If we do not have a second deletion operation and there is a request to access data at this time, it may be the Redis data that has not been modified before. After the deletion operation is executed, Redis will be empty. When a request comes in, the database will be accessed. At that time, the data in the database is already updated data, ensuring data consistency.

2. Code Practice

1. Introduce Redis and SpringBoot AOP dependencies

<!-- redis usage -->
<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. Write custom aop annotations and aspects

ClearAndReloadCache delayed double deletion annotation

/**
 *Delayed double deletion
 **/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
    String name() default "";
}

ClearAndReloadCacheAspect delayed double deletion of aspects

@Aspect
@Component
public class ClearAndReloadCacheAspect {

@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* entry point
*Entry point, entry point implemented based on annotations. Those with this annotation are all entry points for Aop aspects.
*
*/

@Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
public void pointCut(){

}
/**
* Surround notifications
* Surround notifications are very powerful and can determine whether and when the target method is executed, whether the method parameters need to be replaced during execution, and whether the return value needs to be replaced after execution.
* The first parameter of the surround notification must be of type org.aspectj.lang.ProceedingJoinPoint
* @param proceedingJoinPoint
*/
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
    System.out.println("---------- Surround notification -----------");
    System.out.println("Target method name of surrounding notification: " + proceedingJoinPoint.getSignature().getName());

    Signature signature1 = proceedingJoinPoint.getSignature();
    MethodSignature methodSignature = (MethodSignature)signature1;
    Method targetMethod = methodSignature.getMethod();//Method object
    ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class);//Reflection to get the method object of the custom annotation

    String name = annotation.name();//Get the parameter of the method object of the custom annotation, which is name
    Set<String> keys = stringRedisTemplate.keys("*" + name + "*");//Fuzzy definition key
    stringRedisTemplate.delete(keys);//Fuzzy delete redis key value

    //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);
            Set<String> keys1 = stringRedisTemplate.keys("*" + name + "*");//Fuzzy delete
            stringRedisTemplate.delete(keys1);
            System.out.println("----------After 1 second, the delayed deletion in the thread will be completed -----------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    return proceed;//Return the value of the business code
    }
}

3. application.yml

server:
  port: 8082

spring:
  # redis setting
  redis:
    host: localhost
    port: 6379

  # cache setting
  cache:
    redis:
      time-to-live: 60000 # 60s

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 1234

# mp setting
mybatis-plus:
  mapper-locations: classpath*:com/pdh/mapper/*.xml
  global-config:
    db-config:
      table-prefix:
  configuration:
    # log of sql
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    #hump
    map-underscore-to-camel-case: true

4. user_db.sql script

Used to produce test data

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 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

----------------------------
-- Records of user_db
----------------------------
INSERT INTO `user_db` VALUES (1, 'Zhang San');
INSERT INTO `user_db` VALUES (2, '李思');
INSERT INTO `user_db` VALUES (3, '王二');
INSERT INTO `user_db` VALUES (4, 'Mazi');
INSERT INTO `user_db` VALUES (5, '王三');
INSERT INTO `user_db` VALUES (6, '李三');

5. UserController

/**
 * User control layer
 */
@RequestMapping("/user")
@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/get/{id}")
    @Cache(name = "get method")
    //@Cacheable(cacheNames = {"get"})
    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);
    }
}

6. UserService

/**
 * service layer
 */
@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 line = userMapper.insert(user);
        if(line > 0)
            return Result.success(line);
        return Result.fail(888,"Failed to operate database");
    }

    public Result delete(Integer id) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getId, id);
        int line = userMapper.delete(wrapper);
        if (line > 0)
            return Result.success(line);
        return Result.fail(888, "Failed to operate the database");
    }

    public Result update(User user){
        int i = userMapper.updateById(user);
        if(i > 0)
            return Result.success(i);
        return Result.fail(888,"Failed to operate database");
    }
}

3. Testing and verification

1. ID=10, add a new piece of data

6ef5576b76ad462ffd62dd252890a362.jpeg

2. When querying the database for the first time, Redis will save the query results

d25ae059c1f3485fd96f698c85a106bf.jpeg

3. The first visit ID is 10

3a0ef3b82903b2eb4c82b51a8f8d3c8b.jpeg

4. The first access database ID is 10, and store the result in Redis

0ec249a0d53b9ff58b607ab2dbe39f55.jpeg

5. Update the user name corresponding to ID 10 (verify database and cache inconsistency scheme)

b100270331baa01fa847258aa8bce286.jpeg

Database and cache inconsistency verification scheme:

Make a breakpoint and simulate that after thread A performs the first deletion, before A completes updating the database, another thread B accesses ID=10 and reads the old data.

b477eaa31d2373b84dd02b37135e3a0f.png

b62e4f2a84b486ca47122ffd03275496.jpeg

6. Use the second deletion and set the delay time according to the business scenario. After the cache is deleted successfully twice, the Redis result will be empty. What is read is the real data of the database, and there will be no inconsistency between the read cache and the database.

a1ffc54c3d90aaf2879cc957e61912a8.jpeg

4. Code project and address

The core code is shown in the red box

Code: https://gitee.com/jike11231/redisDemo

0e7941161d2fb7da0e23e32d970881f9.png

Original text: blog.csdn.net/jike11231/article/details/126329789

1.2TSharing of useful learning materials for architects

2.10000 + TB resources, Alibaba cloud disk, awesome! !

3. Basically covers a summary of all core knowledge points of Spring

· END ·

Finally, follow the public account Internet Architect and reply in the background: 2T, you can get the Java series interview questions and answers I compiled, which is very complete.

If this article is helpful or inspiring to you, please scan the QR code above to pay attention. Your support is my biggest motivation to keep writing.

Please like, retweet and watch three times in one click