SpringBoot?AOP?Redis實(shí)現(xiàn)延時(shí)雙刪功能實(shí)戰(zhàn)
一、業(yè)務(wù)場(chǎng)景
在多線程并發(fā)情況下,假設(shè)有兩個(gè)數(shù)據(jù)庫(kù)修改請(qǐng)求,為保證數(shù)據(jù)庫(kù)與redis的數(shù)據(jù)一致性,
修改請(qǐng)求的實(shí)現(xiàn)中需要修改數(shù)據(jù)庫(kù)后,級(jí)聯(lián)修改Redis中的數(shù)據(jù)。
請(qǐng)求一:A修改數(shù)據(jù)庫(kù)數(shù)據(jù) B修改Redis數(shù)據(jù)
請(qǐng)求二:C修改數(shù)據(jù)庫(kù)數(shù)據(jù) D修改Redis數(shù)據(jù)
并發(fā)情況下就會(huì)存在A —> C —> D —> B的情況
(一定要理解線程并發(fā)執(zhí)行多組原子操作執(zhí)行順序是可能存在交叉現(xiàn)象的)
1、此時(shí)存在的問(wèn)題
A修改數(shù)據(jù)庫(kù)的數(shù)據(jù)最終保存到了Redis中,C在A之后也修改了數(shù)據(jù)庫(kù)數(shù)據(jù)。
此時(shí)出現(xiàn)了Redis中數(shù)據(jù)和數(shù)據(jù)庫(kù)數(shù)據(jù)不一致的情況,在后面的查詢過(guò)程中就會(huì)長(zhǎng)時(shí)間去先查Redis,從而出現(xiàn)查詢到的數(shù)據(jù)并不是數(shù)據(jù)庫(kù)中的真實(shí)數(shù)據(jù)的嚴(yán)重問(wèn)題。
2、解決方案
在使用Redis時(shí),需要保持Redis和數(shù)據(jù)庫(kù)數(shù)據(jù)的一致性,最流行的解決方案之一就是延時(shí)雙刪策略。
注意:要知道經(jīng)常修改的數(shù)據(jù)表不適合使用Redis,因?yàn)殡p刪策略執(zhí)行的結(jié)果是把Redis中保存的那條數(shù)據(jù)刪除了,以后的查詢就都會(huì)去查詢數(shù)據(jù)庫(kù)。所以Redis使用的是讀遠(yuǎn)遠(yuǎn)大于改的數(shù)據(jù)緩存。
延時(shí)雙刪方案執(zhí)行步驟
1> 刪除緩存
2> 更新數(shù)據(jù)庫(kù)
3> 延時(shí)500毫秒 (根據(jù)具體業(yè)務(wù)設(shè)置延時(shí)執(zhí)行的時(shí)間)
4> 刪除緩存
3、為何要延時(shí)500毫秒?
這是為了我們?cè)诘诙蝿h除Redis之前能完成數(shù)據(jù)庫(kù)的更新操作。假象一下,如果沒(méi)有第三步操作時(shí),有很大概率,在兩次刪除Redis操作執(zhí)行完畢之后,數(shù)據(jù)庫(kù)的數(shù)據(jù)還沒(méi)有更新,此時(shí)若有請(qǐng)求訪問(wèn)數(shù)據(jù),便會(huì)出現(xiàn)我們一開(kāi)始提到的那個(gè)問(wèn)題。
4、為何要兩次刪除緩存?
如果我們沒(méi)有第二次刪除操作,此時(shí)有請(qǐng)求訪問(wèn)數(shù)據(jù),有可能是訪問(wèn)的之前未做修改的Redis數(shù)據(jù),刪除操作執(zhí)行后,Redis為空,有請(qǐng)求進(jìn)來(lái)時(shí),便會(huì)去訪問(wèn)數(shù)據(jù)庫(kù),此時(shí)數(shù)據(jù)庫(kù)中的數(shù)據(jù)已是更新后的數(shù)據(jù),保證了數(shù)據(jù)的一致性。
二、代碼實(shí)踐
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、編寫(xiě)自定義aop注解和切面
ClearAndReloadCache延時(shí)雙刪注解
/** *延時(shí)雙刪 **/ @Retention(RetentionPolicy.RUNTIME) @Documented @Target(ElementType.METHOD) public @interface ClearAndReloadCache { String name() default ""; }
ClearAndReloadCacheAspect延時(shí)雙刪切面
@Aspect @Component public class ClearAndReloadCacheAspect { @Autowired private StringRedisTemplate stringRedisTemplate; /** * 切入點(diǎn) *切入點(diǎn),基于注解實(shí)現(xiàn)的切入點(diǎn) 加上該注解的都是Aop切面的切入點(diǎn) * */ @Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)") public void pointCut(){ } /** * 環(huán)繞通知 * 環(huán)繞通知非常強(qiáng)大,可以決定目標(biāo)方法是否執(zhí)行,什么時(shí)候執(zhí)行,執(zhí)行時(shí)是否需要替換方法參數(shù),執(zhí)行完畢是否需要替換返回值。 * 環(huán)繞通知第一個(gè)參數(shù)必須是org.aspectj.lang.ProceedingJoinPoint類型 * @param proceedingJoinPoint */ @Around("pointCut()") public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){ System.out.println("----------- 環(huán)繞通知 -----------"); System.out.println("環(huán)繞通知的目標(biāo)方法名:" + proceedingJoinPoint.getSignature().getName()); Signature signature1 = proceedingJoinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature)signature1; Method targetMethod = methodSignature.getMethod();//方法對(duì)象 ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class);//反射得到自定義注解的方法對(duì)象 String name = annotation.name();//獲取自定義注解的方法對(duì)象的參數(shù)即name Set<String> keys = stringRedisTemplate.keys("*" + name + "*");//模糊定義key stringRedisTemplate.delete(keys);//模糊刪除redis的key值 //執(zhí)行加入雙刪注解的改動(dòng)數(shù)據(jù)庫(kù)的業(yè)務(wù) 即controller中的方法業(yè)務(wù) Object proceed = null; try { proceed = proceedingJoinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } //開(kāi)一個(gè)線程 延遲1秒(此處是1秒舉例,可以改成自己的業(yè)務(wù)) // 在線程中延遲刪除 同時(shí)將業(yè)務(wù)代碼的結(jié)果返回 這樣不影響業(yè)務(wù)代碼的執(zhí)行 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;//返回業(yè)務(wù)代碼的值 } }
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腳本
用于生產(chǎn)測(cè)試數(shù)據(jù)
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<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,"操作數(shù)據(jù)庫(kù)失敗"); } 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, "操作數(shù)據(jù)庫(kù)失敗"); } public Result update(User user){ int i = userMapper.updateById(user); if(i > 0) return Result.success(i); return Result.fail(888,"操作數(shù)據(jù)庫(kù)失敗"); } }
三、測(cè)試驗(yàn)證
1、ID=10,新增一條數(shù)據(jù)
2、第一次查詢數(shù)據(jù)庫(kù),Redis會(huì)保存查詢結(jié)果
3、第一次訪問(wèn)ID為10
4、第一次訪問(wèn)數(shù)據(jù)庫(kù)ID為10,將結(jié)果存入Redis
5、更新ID為10對(duì)應(yīng)的用戶名(驗(yàn)證數(shù)據(jù)庫(kù)和緩存不一致方案)
數(shù)據(jù)庫(kù)和緩存不一致驗(yàn)證方案:
打個(gè)斷點(diǎn),模擬A線程執(zhí)行第一次刪除后,在A更新數(shù)據(jù)庫(kù)完成之前,另外一個(gè)線程B訪問(wèn)ID=10,讀取的還是舊數(shù)據(jù)。
6、采用第二次刪除,根據(jù)業(yè)務(wù)場(chǎng)景設(shè)置延時(shí)時(shí)間,兩次刪除緩存成功后,Redis結(jié)果為空。讀取的都是數(shù)據(jù)庫(kù)真實(shí)數(shù)據(jù),不會(huì)出現(xiàn)讀緩存和數(shù)據(jù)庫(kù)不一致情況。
四、代碼工程及地址
核心代碼紅色方框所示
參考文章
數(shù)據(jù)庫(kù)面試題——redis緩存主動(dòng)更新策略(延時(shí)雙刪)
redis數(shù)據(jù)一致性之延時(shí)雙刪詳解
SpringAop應(yīng)用三之Aop實(shí)現(xiàn)Redis緩存雙刪(自定義注解實(shí)現(xiàn)切入)
SpringBoot整合Redis實(shí)現(xiàn)緩存(自動(dòng)緩存 + 手動(dòng)aop緩存)
到此這篇關(guān)于SpringBoot AOP Redis實(shí)現(xiàn)延時(shí)雙刪功能實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)SpringBoot AOP Redis延時(shí)雙刪內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Springboot中使用Redisson+AOP+自定義注解實(shí)現(xiàn)訪問(wèn)限流與黑名單攔截
- SpringBoot整合redis+Aop防止重復(fù)提交的實(shí)現(xiàn)
- SpringBoot+Redis使用AOP防止重復(fù)提交的實(shí)現(xiàn)
- SpringBoot?使用AOP?+?Redis?防止表單重復(fù)提交的方法
- SpringBoot使用自定義注解+AOP+Redis實(shí)現(xiàn)接口限流的實(shí)例代碼
- SpringBoot AOP控制Redis自動(dòng)緩存和更新的示例
- 淺談SpringBoot集成Redis實(shí)現(xiàn)緩存處理(Spring AOP實(shí)現(xiàn))
- Springboot整合AOP和redis的示例詳解
相關(guān)文章
Spring解讀@Component和@Configuration的區(qū)別以及源碼分析
通過(guò)實(shí)例分析@Component和@Configuration注解的區(qū)別,核心在于@Configuration會(huì)通過(guò)CGLIB代理確保Bean的單例,而@Component不會(huì),在Spring容器中,使用@Configuration注解的類會(huì)被CGLIB增強(qiáng),保證了即使在同一個(gè)類中多次調(diào)用@Bean方法2024-10-10Java使用wait() notify()方法操作共享資源詳解
這篇文章主要為大家詳細(xì)介紹了Java使用wait() notify()方法操作共享資源,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10Java文件分級(jí)目錄打包下載zip的實(shí)例代碼
這篇文章主要介紹了Java文件分級(jí)目錄打包下載zip的實(shí)例代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08java類訪問(wèn)權(quán)限與成員訪問(wèn)權(quán)限解析
這篇文章主要針對(duì)java類訪問(wèn)權(quán)限與成員訪問(wèn)權(quán)限進(jìn)行解析,對(duì)類與成員訪問(wèn)權(quán)限進(jìn)行驗(yàn)證,感興趣的小伙伴們可以參考一下2016-02-02SpringBoot HATEOAS用法簡(jiǎn)介(入門(mén))
這篇文章主要介紹了SpringBoot HATEOAS用法簡(jiǎn)介(入門(mén)),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10jmeter添加自定義擴(kuò)展函數(shù)之圖片base64編碼示例詳解
這篇文章主要介紹了jmeter添加自定義擴(kuò)展函數(shù)之圖片base64編碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01IDEA SpringBoot項(xiàng)目配置熱更新的步驟詳解(無(wú)需每次手動(dòng)重啟服務(wù)器)
這篇文章主要介紹了IDEA SpringBoot項(xiàng)目配置熱更新的步驟,無(wú)需每次手動(dòng)重啟服務(wù)器,本文通過(guò)圖文實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04解決Java項(xiàng)目中request流只能獲取一次的問(wèn)題
Java項(xiàng)目開(kāi)發(fā)中可能存在以下幾種情況,你需要在攔截器中統(tǒng)一攔截請(qǐng)求和你項(xiàng)目里可能需要搞一個(gè)統(tǒng)一的異常處理器,這兩種情況是比較常見(jiàn)的,本文將給大家介紹如何解決Java項(xiàng)目中request流只能獲取一次的問(wèn)題,需要的朋友可以參考下2024-02-02Spring Cloud Alibaba配置多環(huán)境管理詳解與實(shí)戰(zhàn)代碼
本文通過(guò)實(shí)際案例詳細(xì)介紹了springboot配置多環(huán)境管理的使用,以及基于nacos的配置多環(huán)境管理的實(shí)踐,在實(shí)際開(kāi)發(fā)中,配置多環(huán)境管理是一個(gè)很難避開(kāi)的問(wèn)題,同時(shí)也是微服務(wù)治理中一個(gè)很重要的內(nèi)容,感興趣的朋友跟隨小編一起看看吧2024-06-06