當前位置: 妍妍網 > 碼農

SpringBoot AOP + Redis 延時雙刪功能實戰

2024-03-23碼農

來源:blog.csdn.net/jike11231/article/details/126329789

# 業務場景

在多執行緒並行情況下,假設有兩個資料庫修改請求,為保證資料庫與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@Componentpublic classClearAndReloadCacheAspect{@Autowiredprivate 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: 8082spring: # redis settingredis:host: localhostport: 6379 # cache settingcache:redis:time-to-live: 60000 # 60sdatasource:driver- class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/testusername: rootpassword: 1234# mp settingmybatis-plus:mapper-locations: classpath*:com/pdh/mapper/*.xmlglobal-config:db-config:table-prefix:configuration: # log of sqllog-impl: org.apache.ibatis.logging.stdout.StdOutImpl # humpmap-underscore-to-camel-case: true

4、user_db.sql指令碼

用於生產測試數據

DROPTABLEIFEXISTS`user_db`;CREATETABLE`user_db` (`id`int(4) NOTNULL AUTO_INCREMENT,`username`varchar(32) CHARACTERSET utf8 COLLATE utf8_general_ci NOTNULL, PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 8CHARACTERSET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- ------------------------------ Records of user_db-- ----------------------------INSERTINTO`user_db`VALUES (1, '張三');INSERTINTO`user_db`VALUES (2, '李四');INSERTINTO`user_db`VALUES (3, '王二');INSERTINTO`user_db`VALUES (4, '麻子');INSERTINTO`user_db`VALUES (5, '王三');INSERTINTO`user_db`VALUES (6, '李三');

5、UserController

/** * 使用者控制層 */@RequestMapping("/user")@RestControllerpublic classUserController{@Autowiredprivate 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層 */@Servicepublic classUserService { @Resourceprivate 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,新增一條數據

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

3、第一次存取ID為10

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

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

資料庫和緩存不一致驗證方案:

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

6、采用第二次刪除,根據業務場景設定延時時間,兩次刪除緩存成功後,Redis結果為空。讀取的都是資料庫真實數據,不會出現讀緩存和資料庫不一致情況。

# 程式碼工程及地址

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

核心程式碼紅色方框所示

熱門推薦