目錄
一、業務場景
1、此時存在的問題
2、解決方案
3、為何要延時500毫秒?
4、為何要兩次刪除快取?
二、程式碼實作
1、引入Redis和SpringBoot AOP依賴
2、寫自訂aop註解與切面
3、application.yml
4、user_db.sql腳本
5、UserController
6、UserService
三、測試驗證
首頁 資料庫 Redis SpringBoot AOP Redis如何實現延時雙刪功能

SpringBoot AOP Redis如何實現延時雙刪功能

May 29, 2023 am 08:53 AM
redis aop springboot

    一、業務場景

    在多執行緒並發情況下,假設有兩個資料庫修改請求,為保證資料庫與redis的資料一致性,
    修改請求的實作中需要修改資料庫後,級聯修改Redis中的資料。
    請求一:A修改資料庫資料B修改Redis資料
    請求二:C修改資料庫資料D修改Redis資料
    並發就會存在A —> C —> D — > B的情況
    (一定要理解執行緒並發執行多組原子操作執行順序是可能存在交叉現象的)

    1、此時存在的問題

    A修改資料庫的資料最終保存到了Redis中,C在A之後也修改了資料庫資料。

    此時出現了Redis中資料和資料庫資料不一致的情況,在後面的查詢過程中就會長時間去先查Redis,從而出現查詢到的資料並不是資料庫中的真實資料的嚴重問題。

    2、解決方案

    在使用Redis時,需要維持Redis和資料庫資料的一致性,最受歡迎的解決方案之一就是延時雙刪策略。
    注意:要知道經常修改的資料表不適合使用Redis,因為雙刪策略執行的結果是把Redis中保存的那條資料刪除了,以後的查詢就會去查詢資料庫。所以Redis使用的是讀遠大於改的資料快取。
    延時雙刪方案執行步驟

    1> 刪除快取
    2> 更新資料庫
    3> 延遲500毫秒(根據特定業務設定延遲執行的時間)
    4> 刪除快取

    3、為何要延時500毫秒?

    我們需要在第二次Redis刪除之前完成資料庫的更新操作。假像一下,如果沒有第三步驟操作時,有很大機率,在兩次刪除Redis操作執行完畢之後,資料庫的數據還沒有更新,此時若有請求存取數據,便會出現我們一開始提到的那個問題。

    4、為何要兩次刪除快取?

    如果我們沒有第二次刪除操作,此時有請求訪問數據,有可能是訪問的之前未做修改的Redis數據,刪除操作執行後,Redis為空,有請求進來時,便會去存取資料庫,此時資料庫中的數據已是更新後的數據,保證了數據的一致性。

    二、程式碼實作

    1、引入Redis和SpringBoot AOP依賴

    <!-- 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、寫自訂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<String> 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<String> keys1 = stringRedisTemplate.keys("*" + name + "*");//模糊删除
                stringRedisTemplate.delete(keys1);
                System.out.println("-----------1秒钟后,在线程中延迟删除完毕 -----------");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    
        return proceed;//返回业务代码的值
        }
    }
    登入後複製

    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腳本

    #用於生產測試資料

    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, &#39;张三&#39;);
    INSERT INTO `user_db` VALUES (2, &#39;李四&#39;);
    INSERT INTO `user_db` VALUES (3, &#39;王二&#39;);
    INSERT INTO `user_db` VALUES (4, &#39;麻子&#39;);
    INSERT INTO `user_db` VALUES (5, &#39;王三&#39;);
    INSERT INTO `user_db` VALUES (6, &#39;李三&#39;);
    登入後複製

    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<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,"操作数据库失败");
        }
    
        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, "操作数据库失败");
        }
    
        public Result update(User user){
            int i = userMapper.updateById(user);
            if(i > 0)
                return Result.success(i);
            return Result.fail(888,"操作数据库失败");
        }
    }
    登入後複製

    三、測試驗證

    1、ID=10,新增一條資料

    SpringBoot AOP Redis如何實現延時雙刪功能

    2、第一次查詢資料庫,Redis會儲存查詢結果

    SpringBoot AOP Redis如何實現延時雙刪功能

    3、第一次存取ID為10

    SpringBoot AOP Redis如何實現延時雙刪功能

    4、第一次存取資料庫ID為10,將結果存入Redis

    SpringBoot AOP Redis如何實現延時雙刪功能

    ##5、更新ID為10對應的使用者名稱(驗證資料庫和快取不一致方案)

    SpringBoot AOP Redis如何實現延時雙刪功能

    #資料庫和快取不一致驗證方案:

    打個斷點,模擬A線程執行第一次刪除後,在A更新資料庫完成前,另外一個執行緒B存取ID=10,讀取的還是舊資料。

    SpringBoot AOP Redis如何實現延時雙刪功能

    SpringBoot AOP Redis如何實現延時雙刪功能

    利用第二次刪除,根據業務場景設定適當的延遲時間後,待兩次刪除快取成功後, Redis的輸出結果將為空。讀取的都是資料庫真實數據,不會出現讀取快取和資料庫不一致情況。

    SpringBoot AOP Redis如何實現延時雙刪功能

    四、程式碼工程

    核心程式碼紅色方塊所示

    SpringBoot AOP Redis如何實現延時雙刪功能

    以上是SpringBoot AOP Redis如何實現延時雙刪功能的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

    熱AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智慧驅動的應用程序,用於創建逼真的裸體照片

    AI Clothes Remover

    AI Clothes Remover

    用於從照片中去除衣服的線上人工智慧工具。

    Undress AI Tool

    Undress AI Tool

    免費脫衣圖片

    Clothoff.io

    Clothoff.io

    AI脫衣器

    AI Hentai Generator

    AI Hentai Generator

    免費產生 AI 無盡。

    熱門文章

    R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
    1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.最佳圖形設置
    1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.如果您聽不到任何人,如何修復音頻
    1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.聊天命令以及如何使用它們
    1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

    熱工具

    記事本++7.3.1

    記事本++7.3.1

    好用且免費的程式碼編輯器

    SublimeText3漢化版

    SublimeText3漢化版

    中文版,非常好用

    禪工作室 13.0.1

    禪工作室 13.0.1

    強大的PHP整合開發環境

    Dreamweaver CS6

    Dreamweaver CS6

    視覺化網頁開發工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神級程式碼編輯軟體(SublimeText3)

    redis集群模式怎麼搭建 redis集群模式怎麼搭建 Apr 10, 2025 pm 10:15 PM

    Redis集群模式通過分片將Redis實例部署到多個服務器,提高可擴展性和可用性。搭建步驟如下:創建奇數個Redis實例,端口不同;創建3個sentinel實例,監控Redis實例並進行故障轉移;配置sentinel配置文件,添加監控Redis實例信息和故障轉移設置;配置Redis實例配置文件,啟用集群模式並指定集群信息文件路徑;創建nodes.conf文件,包含各Redis實例的信息;啟動集群,執行create命令創建集群並指定副本數量;登錄集群執行CLUSTER INFO命令驗證集群狀態;使

    redis數據怎麼清空 redis數據怎麼清空 Apr 10, 2025 pm 10:06 PM

    如何清空 Redis 數據:使用 FLUSHALL 命令清除所有鍵值。使用 FLUSHDB 命令清除當前選定數據庫的鍵值。使用 SELECT 切換數據庫,再使用 FLUSHDB 清除多個數據庫。使用 DEL 命令刪除特定鍵。使用 redis-cli 工具清空數據。

    redis指令怎麼用 redis指令怎麼用 Apr 10, 2025 pm 08:45 PM

    使用 Redis 指令需要以下步驟:打開 Redis 客戶端。輸入指令(動詞 鍵 值)。提供所需參數(因指令而異)。按 Enter 執行指令。 Redis 返迴響應,指示操作結果(通常為 OK 或 -ERR)。

    redis怎麼使用單線程 redis怎麼使用單線程 Apr 10, 2025 pm 07:12 PM

    Redis 使用單線程架構,以提供高性能、簡單性和一致性。它利用 I/O 多路復用、事件循環、非阻塞 I/O 和共享內存來提高並發性,但同時存在並發性受限、單點故障和不適合寫密集型工作負載的局限性。

    redis怎麼讀源碼 redis怎麼讀源碼 Apr 10, 2025 pm 08:27 PM

    理解 Redis 源碼的最佳方法是逐步進行:熟悉 Redis 基礎知識。選擇一個特定的模塊或功能作為起點。從模塊或功能的入口點開始,逐行查看代碼。通過函數調用鏈查看代碼。熟悉 Redis 使用的底層數據結構。識別 Redis 使用的算法。

    redis底層怎麼實現 redis底層怎麼實現 Apr 10, 2025 pm 07:21 PM

    Redis 使用哈希表存儲數據,支持字符串、列表、哈希表、集合和有序集合等數據結構。 Redis 通過快照 (RDB) 和追加只寫 (AOF) 機制持久化數據。 Redis 使用主從復制來提高數據可用性。 Redis 使用單線程事件循環處理連接和命令,保證數據原子性和一致性。 Redis 為鍵設置過期時間,並使用 lazy 刪除機制刪除過期鍵。

    redis怎麼讀取隊列 redis怎麼讀取隊列 Apr 10, 2025 pm 10:12 PM

    要從 Redis 讀取隊列,需要獲取隊列名稱、使用 LPOP 命令讀取元素,並處理空隊列。具體步驟如下:獲取隊列名稱:以 "queue:" 前綴命名,如 "queue:my-queue"。使用 LPOP 命令:從隊列頭部彈出元素並返回其值,如 LPOP queue:my-queue。處理空隊列:如果隊列為空,LPOP 返回 nil,可先檢查隊列是否存在再讀取元素。

    redis怎麼使用鎖 redis怎麼使用鎖 Apr 10, 2025 pm 08:39 PM

    使用Redis進行鎖操作需要通過SETNX命令獲取鎖,然後使用EXPIRE命令設置過期時間。具體步驟為:(1) 使用SETNX命令嘗試設置一個鍵值對;(2) 使用EXPIRE命令為鎖設置過期時間;(3) 當不再需要鎖時,使用DEL命令刪除該鎖。

    See all articles