SpringBoot AOP Redis가 지연된 이중 삭제 기능을 구현하는 방법

PHPz
풀어 주다: 2023-05-29 08:53:08
앞으로
1986명이 탐색했습니다.

    1. 비즈니스 시나리오

    다중 스레드 동시성의 경우 데이터베이스와 Redis 간의 데이터 일관성을 보장하기 위해 두 개의 데이터베이스 수정 요청이 있다고 가정합니다.
    수정 요청 구현에는 계단식이 필요합니다. Redis의 데이터 수정 후 수정.
    요청 1: A는 데이터베이스 데이터를 수정합니다. B는 Redis 데이터를 수정합니다.
    요청 2: C는 데이터베이스 데이터를 수정합니다. D는 Redis 데이터를 수정합니다.
    동시 상황에서는 A —>C —> ;> B
    (스레드에서 동시에 실행되는 여러 세트의 원자 연산의 실행 순서가 겹칠 수 있음을 이해하십시오.)

    1. 이때 문제

    A가 데이터베이스 데이터를 수정하여 최종적으로 Redis에 저장했습니다. C도 A 데이터 이후에 데이터베이스를 수정했습니다.

    이때, Redis의 데이터와 데이터베이스 데이터 사이에 불일치가 발생하여 후속 쿼리 과정에서 Redis를 오랜 시간 동안 먼저 확인하게 되어 쿼리된 데이터가 실제 데이터가 아닌 심각한 문제가 발생하게 됩니다. 데이터베이스에서.

    2. 솔루션

    Redis를 사용할 때 가장 인기 있는 솔루션 중 하나는 지연 이중 삭제 전략입니다.
    참고: 이중 삭제 전략의 결과로 Redis에 저장된 데이터가 삭제되고 후속 쿼리가 데이터베이스에 쿼리되므로 자주 수정되는 데이터 테이블은 Redis 사용에 적합하지 않다는 점을 알아야 합니다. 따라서 Redis는 변경 사항보다 훨씬 더 많은 것을 읽는 데이터 캐시를 사용합니다.
    지연된 이중 삭제 계획의 실행 단계

    1> 캐시 삭제
    2> 데이터베이스 업데이트
    3> 지연 500밀리초(특정 비즈니스에 따라 지연 실행 시간 설정)
    4> 지연 이유는 무엇입니까? 500밀리초?

    두 번째 Redis 삭제 전에 데이터베이스 업데이트 작업을 완료해야 합니다. 세 번째 단계가 없다면 두 번의 Redis 삭제 작업이 완료된 후에도 데이터베이스의 데이터가 업데이트되지 않았을 가능성이 높다고 상상해보십시오. 이때 데이터에 액세스하라는 요청이 있으면 문제가 발생합니다. 처음에 언급한 질문이 나옵니다.

    4. 캐시를 두 번 삭제해야 하는 이유는 무엇인가요?

    두 번째 삭제 작업이 없고 이때 데이터에 액세스하라는 요청이 있는 경우 이전에 수정되지 않은 Redis 데이터일 수 있습니다. 삭제 작업이 수행된 후 Redis는 비어 있게 됩니다. 요청이 들어오면 데이터베이스에 액세스하게 됩니다. 이때 데이터베이스의 데이터는 이미 업데이트된 데이터이므로 데이터 일관성이 보장됩니다.

    2. 코드 연습

    1. Redis 및 SpringBoot AOP 종속성 소개

      org.springframework.boot spring-boot-starter-data-redis    org.springframework.boot spring-boot-starter-aop 
    로그인 후 복사

    2. 사용자 정의 AOP 주석 및 측면 작성

    ClearAndReloadCache 지연된 이중 삭제 주석

    /** *延时双删 **/ @Retention(RetentionPolicy.RUNTIME) @Documented @Target(ElementType.METHOD) public @interface ClearAndReloadCache { String name() default ""; }
    로그인 후 복사

    ClearAndReloadCacheAspect 지연된 측면 이중 삭제

    @Aspect @Component public class ClearAndReloadCacheAspect { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 切入点 *切入点,基于注解实现的切入点 加上该注解的都是Aop切面的切入点 * */ @Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)") public void pointCut(){ } /** * 环绕通知 * 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。 * 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型 * @param proceedingJoinPoint */ @Around("pointCut()") public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){ System.out.println("----------- 环绕通知 -----------"); System.out.println("环绕通知的目标方法名:" + proceedingJoinPoint.getSignature().getName()); Signature signature1 = proceedingJoinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature)signature1; Method targetMethod = methodSignature.getMethod();//方法对象 ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class);//反射得到自定义注解的方法对象 String name = annotation.name();//获取自定义注解的方法对象的参数即name Set keys = stringRedisTemplate.keys("*" + name + "*");//模糊定义key stringRedisTemplate.delete(keys);//模糊删除redis的key值 //执行加入双删注解的改动数据库的业务 即controller中的方法业务 Object proceed = null; try { proceed = proceedingJoinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } //开一个线程 延迟1秒(此处是1秒举例,可以改成自己的业务) // 在线程中延迟删除 同时将业务代码的结果返回 这样不影响业务代码的执行 new Thread(() -> { try { Thread.sleep(1000); Set keys1 = stringRedisTemplate.keys("*" + name + "*");//模糊删除 stringRedisTemplate.delete(keys1); System.out.println("-----------1秒钟后,在线程中延迟删除完毕 -----------"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); return proceed;//返回业务代码的值 } }
    로그인 후 복사

    3. 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

    테스트 데이터 생성에 사용됨

    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, '张三'); INSERT INTO `user_db` VALUES (2, '李四'); INSERT INTO `user_db` VALUES (3, '王二'); INSERT INTO `user_db` VALUES (4, '麻子'); INSERT INTO `user_db` VALUES (5, '王三'); INSERT INTO `user_db` VALUES (6, '李三');
    로그인 후 복사

    5, UserController

    /** * 用户控制层 */ @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层 */ @Service public class UserService { @Resource private UserMapper userMapper; public Result get(Integer id){ LambdaQueryWrapper 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,"操作数据库失败"); } public Result delete(Integer id) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getId, id); int line = userMapper.delete(wrapper); if (line > 0) return Result.success(line); return Result.fail(888, "操作数据库失败"); } public Result update(User user){ int i = userMapper.updateById(user); if(i > 0) return Result.success(i); return Result.fail(888,"操作数据库失败"); } }
    로그인 후 복사
    3 테스트 검증

    1, ID=10, 새 데이터 추가

    2 Redis에 저장

    SpringBoot AOP Redis가 지연된 이중 삭제 기능을 구현하는 방법

    5, ID 10에 해당하는 사용자 이름 업데이트(데이터베이스 및 캐시 불일치 구성표 확인)

    SpringBoot AOP Redis가 지연된 이중 삭제 기능을 구현하는 방법

    데이터베이스 및 캐시 불일치 확인 구성표:

    SpringBoot AOP Redis가 지연된 이중 삭제 기능을 구현하는 방법중단점 만들기, 시뮬레이션 첫 번째 실행 스레드 삭제 후 A가 데이터베이스 업데이트를 완료하기 전에 다른 스레드 B가 ID=10에 액세스하여 이전 데이터를 읽습니다.

    SpringBoot AOP Redis가 지연된 이중 삭제 기능을 구현하는 방법

    두 번째 삭제를 사용하고 비즈니스 시나리오에 따라 적절한 지연 시간을 설정합니다. 캐시가 두 번 성공적으로 삭제되면 Redis의 출력 결과는 비어 있게 됩니다. 읽는 것은 데이터베이스의 실제 데이터이며, 읽기 캐시와 데이터베이스 간에 불일치가 없습니다.

    SpringBoot AOP Redis가 지연된 이중 삭제 기능을 구현하는 방법

    4. 코드 엔지니어링

    빨간색 상자에 핵심 코드가 표시됩니다

    SpringBoot AOP Redis가 지연된 이중 삭제 기능을 구현하는 방법

    위 내용은 SpringBoot AOP Redis가 지연된 이중 삭제 기능을 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    관련 라벨:
    원천:yisu.com
    본 웹사이트의 성명
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
    최신 이슈
    최신 다운로드
    더>
    웹 효과
    웹사이트 소스 코드
    웹사이트 자료
    프론트엔드 템플릿
    회사 소개 부인 성명 Sitemap
    PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!