0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

SpringBoot AOP + Redis 延時(shí)雙刪功能實(shí)戰(zhàn)

jf_ro2CN3Fa ? 來源:CSDN ? 2023-10-13 16:08 ? 次閱讀


一、業(yè)務(wù)場景

在多線程并發(fā)情況下,假設(shè)有兩個(gè)數(shù)據(jù)庫修改請求,為保證數(shù)據(jù)庫與redis的數(shù)據(jù)一致性,修改請求的實(shí)現(xiàn)中需要修改數(shù)據(jù)庫后,級聯(lián)修改Redis中的數(shù)據(jù)。

  • 請求一:A修改數(shù)據(jù)庫數(shù)據(jù) B修改Redis數(shù)據(jù)
  • 請求二:C修改數(shù)據(jù)庫數(shù)據(jù) D修改Redis數(shù)據(jù)

并發(fā)情況下就會存在A —> C —> D —> B的情況

?

一定要理解線程并發(fā)執(zhí)行多組原子操作執(zhí)行順序是可能存在交叉現(xiàn)象的

?

1、此時(shí)存在的問題

A修改數(shù)據(jù)庫的數(shù)據(jù)最終保存到了Redis中,C在A之后也修改了數(shù)據(jù)庫數(shù)據(jù)。

此時(shí)出現(xiàn)了Redis中數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)不一致的情況,在后面的查詢過程中就會長時(shí)間去先查Redis, 從而出現(xiàn)查詢到的數(shù)據(jù)并不是數(shù)據(jù)庫中的真實(shí)數(shù)據(jù)的嚴(yán)重問題。

2、解決方案

在使用Redis時(shí),需要保持Redis和數(shù)據(jù)庫數(shù)據(jù)的一致性,最流行的解決方案之一就是延時(shí)雙刪策略。

注意:要知道經(jīng)常修改的數(shù)據(jù)表不適合使用Redis,因?yàn)殡p刪策略執(zhí)行的結(jié)果是把Redis中保存的那條數(shù)據(jù)刪除了,以后的查詢就都會去查詢數(shù)據(jù)庫。所以Redis使用的是讀遠(yuǎn)遠(yuǎn)大于改的數(shù)據(jù)緩存。

延時(shí)雙刪方案執(zhí)行步驟

  1. 刪除緩存
  2. 更新數(shù)據(jù)庫
  3. 延時(shí)500毫秒 (根據(jù)具體業(yè)務(wù)設(shè)置延時(shí)執(zhí)行的時(shí)間)
  4. 刪除緩存

3、為何要延時(shí)500毫秒?

這是為了我們在第二次刪除Redis之前能完成數(shù)據(jù)庫的更新操作。假象一下,如果沒有第三步操作時(shí),有很大概率,在兩次刪除Redis操作執(zhí)行完畢之后,數(shù)據(jù)庫的數(shù)據(jù)還沒有更新,此時(shí)若有請求訪問數(shù)據(jù),便會出現(xiàn)我們一開始提到的那個(gè)問題。

4、為何要兩次刪除緩存?

如果我們沒有第二次刪除操作,此時(shí)有請求訪問數(shù)據(jù),有可能是訪問的之前未做修改的Redis數(shù)據(jù),刪除操作執(zhí)行后,Redis為空,有請求進(jìn)來時(shí),便會去訪問數(shù)據(jù)庫,此時(shí)數(shù)據(jù)庫中的數(shù)據(jù)已是更新后的數(shù)據(jù),保證了數(shù)據(jù)的一致性。

基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

  • 項(xiàng)目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 視頻教程:https://doc.iocoder.cn/video/

二、代碼實(shí)踐

1、引入Redis和SpringBoot AOP依賴



org.springframework.boot
spring-boot-starter-data-redis



org.springframework.boot
spring-boot-starter-aop

2、編寫自定義aop注解和切面

ClearAndReloadCache延時(shí)雙刪注解

/**
*延時(shí)雙刪
**/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public@interfaceClearAndReloadCache{
Stringname()default"";
}

ClearAndReloadCacheAspect延時(shí)雙刪切面

@Aspect
@Component
publicclassClearAndReloadCacheAspect{

@Autowired
privateStringRedisTemplatestringRedisTemplate;

/**
*切入點(diǎn)
*切入點(diǎn),基于注解實(shí)現(xiàn)的切入點(diǎn)加上該注解的都是Aop切面的切入點(diǎn)
*
*/

@Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
publicvoidpointCut(){

}
/**
*環(huán)繞通知
*環(huán)繞通知非常強(qiáng)大,可以決定目標(biāo)方法是否執(zhí)行,什么時(shí)候執(zhí)行,執(zhí)行時(shí)是否需要替換方法參數(shù),執(zhí)行完畢是否需要替換返回值。
*環(huán)繞通知第一個(gè)參數(shù)必須是org.aspectj.lang.ProceedingJoinPoint類型
*@paramproceedingJoinPoint
*/
@Around("pointCut()")
publicObjectaroundAdvice(ProceedingJoinPointproceedingJoinPoint){
System.out.println("-----------環(huán)繞通知-----------");
System.out.println("環(huán)繞通知的目標(biāo)方法名:"+proceedingJoinPoint.getSignature().getName());

Signaturesignature1=proceedingJoinPoint.getSignature();
MethodSignaturemethodSignature=(MethodSignature)signature1;
MethodtargetMethod=methodSignature.getMethod();//方法對象
ClearAndReloadCacheannotation=targetMethod.getAnnotation(ClearAndReloadCache.class);//反射得到自定義注解的方法對象

Stringname=annotation.name();//獲取自定義注解的方法對象的參數(shù)即name
Setkeys=stringRedisTemplate.keys("*"+name+"*");//模糊定義key
stringRedisTemplate.delete(keys);//模糊刪除redis的key值

//執(zhí)行加入雙刪注解的改動數(shù)據(jù)庫的業(yè)務(wù)即controller中的方法業(yè)務(wù)
Objectproceed=null;
try{
proceed=proceedingJoinPoint.proceed();
}catch(Throwablethrowable){
throwable.printStackTrace();
}

//開一個(gè)線程延遲1秒(此處是1秒舉例,可以改成自己的業(yè)務(wù))
//在線程中延遲刪除同時(shí)將業(yè)務(wù)代碼的結(jié)果返回這樣不影響業(yè)務(wù)代碼的執(zhí)行
newThread(()->{
try{
Thread.sleep(1000);
Setkeys1=stringRedisTemplate.keys("*"+name+"*");//模糊刪除
stringRedisTemplate.delete(keys1);
System.out.println("-----------1秒鐘后,在線程中延遲刪除完畢-----------");
}catch(InterruptedExceptione){
e.printStackTrace();
}
}).start();

returnproceed;//返回業(yè)務(wù)代碼的值
}
}

3、application.yml

server:
port:8082

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

#cachesetting
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


>基于SpringCloudAlibaba+Gateway+Nacos+RocketMQ+Vue&Element實(shí)現(xiàn)的后臺管理系統(tǒng)+用戶小程序,支持RBAC動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
>
>*項(xiàng)目地址:<https://github.com/YunaiV/yudao-cloud>
>*視頻教程:<https://doc.iocoder.cn/video/>

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

4、user_db.sql腳本

用于生產(chǎn)測試數(shù)據(jù)

DROPTABLEIFEXISTS`user_db`;
CREATETABLE`user_db`(
`id`int(4)NOTNULLAUTO_INCREMENT,
`username`varchar(32)CHARACTERSETutf8COLLATEutf8_general_ciNOTNULL,
PRIMARYKEY(`id`)USINGBTREE
)ENGINE=InnoDBAUTO_INCREMENT=8CHARACTERSET=utf8COLLATE=utf8_general_ciROW_FORMAT=Dynamic;

------------------------------
--Recordsofuser_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")
@RestController
publicclassUserController{
@Autowired
privateUserServiceuserService;

@GetMapping("/get/{id}")
@Cache(name="getmethod")
//@Cacheable(cacheNames={"get"})
publicResultget(@PathVariable("id")Integerid){
returnuserService.get(id);
}

@PostMapping("/updateData")
@ClearAndReloadCache(name="getmethod")
publicResultupdateData(@RequestBodyUseruser){
returnuserService.update(user);
}

@PostMapping("/insert")
publicResultinsert(@RequestBodyUseruser){
returnuserService.insert(user);
}

@DeleteMapping("/delete/{id}")
publicResultdelete(@PathVariable("id")Integerid){
returnuserService.delete(id);
}
}

6、UserService

/**
*service層
*/
@Service
publicclassUserService{

@Resource
privateUserMapperuserMapper;

publicResultget(Integerid){
LambdaQueryWrapperwrapper=newLambdaQueryWrapper<>();
wrapper.eq(User::getId,id);
Useruser=userMapper.selectOne(wrapper);
returnResult.success(user);
}

publicResultinsert(Useruser){
intline=userMapper.insert(user);
if(line>0)
returnResult.success(line);
returnResult.fail(888,"操作數(shù)據(jù)庫失敗");
}

publicResultdelete(Integerid){
LambdaQueryWrapperwrapper=newLambdaQueryWrapper<>();
wrapper.eq(User::getId,id);
intline=userMapper.delete(wrapper);
if(line>0)
returnResult.success(line);
returnResult.fail(888,"操作數(shù)據(jù)庫失敗");
}

publicResultupdate(Useruser){
inti=userMapper.updateById(user);
if(i>0)
returnResult.success(i);
returnResult.fail(888,"操作數(shù)據(jù)庫失敗");
}
}

三、測試驗(yàn)證

1、ID=10,新增一條數(shù)據(jù)

1ab9e8f0-699e-11ee-939d-92fbcf53809c.jpg

2、第一次查詢數(shù)據(jù)庫,Redis會保存查詢結(jié)果

1acd49a4-699e-11ee-939d-92fbcf53809c.jpg

3、第一次訪問ID為10

1ae5f3b4-699e-11ee-939d-92fbcf53809c.jpg

4、第一次訪問數(shù)據(jù)庫ID為10,將結(jié)果存入Redis

1b1661e8-699e-11ee-939d-92fbcf53809c.jpg

5、更新ID為10對應(yīng)的用戶名(驗(yàn)證數(shù)據(jù)庫和緩存不一致方案)

1b28ddb4-699e-11ee-939d-92fbcf53809c.jpg

數(shù)據(jù)庫和緩存不一致驗(yàn)證方案:

打個(gè)斷點(diǎn),模擬A線程執(zhí)行第一次刪除后,在A更新數(shù)據(jù)庫完成之前,另外一個(gè)線程B訪問ID=10,讀取的還是舊數(shù)據(jù)。

1b33ac9e-699e-11ee-939d-92fbcf53809c.png1b415498-699e-11ee-939d-92fbcf53809c.jpg

6、采用第二次刪除,根據(jù)業(yè)務(wù)場景設(shè)置延時(shí)時(shí)間,兩次刪除緩存成功后,Redis結(jié)果為空。讀取的都是數(shù)據(jù)庫真實(shí)數(shù)據(jù),不會出現(xiàn)讀緩存和數(shù)據(jù)庫不一致情況。

1b48ce6c-699e-11ee-939d-92fbcf53809c.jpg

四、代碼工程及地址

核心代碼紅色方框所示

?

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

?

1b58a9d6-699e-11ee-939d-92fbcf53809c.png


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 數(shù)據(jù)庫
    +關(guān)注

    關(guān)注

    7

    文章

    3879

    瀏覽量

    65524
  • Redis
    +關(guān)注

    關(guān)注

    0

    文章

    381

    瀏覽量

    11229
  • SpringBoot
    +關(guān)注

    關(guān)注

    0

    文章

    175

    瀏覽量

    272

原文標(biāo)題:SpringBoot AOP + Redis 延時(shí)雙刪功能實(shí)戰(zhàn)

文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 0人收藏

    評論

    相關(guān)推薦

    Spring AOP如何破解java應(yīng)用

    預(yù)編譯方式和運(yùn)行期間動態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),從另一視角擴(kuò)展了對面向?qū)ο缶幊痰男问健@?b class='flag-5'>AOP可以對業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度
    的頭像 發(fā)表于 09-25 11:16 ?1056次閱讀
    Spring <b class='flag-5'>AOP</b>如何破解java應(yīng)用

    MySQL與Redis延遲策略

    背景 在當(dāng)前環(huán)境下,通常我們會首選redis緩存來減輕我們數(shù)據(jù)庫訪問壓力。但是也會遇到以下這種情況:大量用戶來訪問我們系統(tǒng),首先會去查詢緩存, 如果緩存中沒有數(shù)據(jù),則去查詢數(shù)據(jù)庫,然后更新數(shù)據(jù)到緩存
    的頭像 發(fā)表于 09-25 14:28 ?1065次閱讀
    MySQL與<b class='flag-5'>Redis</b>延遲<b class='flag-5'>雙</b><b class='flag-5'>刪</b>策略

    求...

    本帖最后由 871881392 于 2014-11-28 08:21 編輯 求
    發(fā)表于 11-24 14:56

    Redis Stream應(yīng)用案例

    的基本使用介紹和設(shè)計(jì)理念可以看我之前的一篇文章(Redis Stream簡介)。Redis Stream本質(zhì)上是在Redis內(nèi)核上(非Redis Module)實(shí)現(xiàn)的一個(gè)消息發(fā)布訂閱
    發(fā)表于 06-26 17:15

    Spring boot中Redis的使用

    【本人禿頂程序員】springboot專輯:Spring boot中Redis的使用
    發(fā)表于 03-27 11:42

    Java程序員筆記之mybatis結(jié)合redis實(shí)戰(zhàn)二級緩存

    Java程序員筆記——mybatis結(jié)合redis實(shí)戰(zhàn)二級緩存
    發(fā)表于 06-10 09:15

    觸摸、聲控功能延時(shí)燈電路圖

    觸摸、聲控功能延時(shí)燈電路圖
    發(fā)表于 05-25 14:02 ?1831次閱讀
    觸摸、聲控<b class='flag-5'>雙</b><b class='flag-5'>功能</b><b class='flag-5'>延時(shí)</b>燈電路圖

    Springboot+redis操作多種實(shí)現(xiàn)

    一、Jedis,Redisson,Lettuce三者的區(qū)別共同點(diǎn):都提供了基于Redis操作的Java API,只是封裝程度,具體實(shí)現(xiàn)稍有不同。 不同點(diǎn): 1.1、Jedis 是Redis的Java
    的頭像 發(fā)表于 09-22 10:48 ?1954次閱讀
    <b class='flag-5'>Springboot+redis</b>操作多種實(shí)現(xiàn)

    基于SpringBoot+Redis的轉(zhuǎn)盤抽獎

    基于SpringBoot+Redis等技術(shù)實(shí)現(xiàn)轉(zhuǎn)盤抽獎活動項(xiàng)目,含前端、后臺及數(shù)據(jù)庫文件
    的頭像 發(fā)表于 02-28 14:24 ?1779次閱讀
    基于<b class='flag-5'>SpringBoot+Redis</b>的轉(zhuǎn)盤抽獎

    什么是 SpringBoot?

    本文從為什么要有 `SpringBoot`,以及 `SpringBoot` 到底方便在哪里開始入手,逐步分析了 `SpringBoot` 自動裝配的原理,最后手寫了一個(gè)簡單的 `start` 組件,通過
    的頭像 發(fā)表于 04-07 11:28 ?1548次閱讀
    什么是 <b class='flag-5'>SpringBoot</b>?

    如何在SpringBoot中解決Redis的緩存穿透等問題

    今天給大家介紹一下如何在SpringBoot中解決Redis的緩存穿透、緩存擊穿、緩存雪崩的問題。
    的頭像 發(fā)表于 04-28 11:35 ?855次閱讀

    如何用Springboot整合Redis

    本篇文件我們來介紹如何用Springboot整合Redis。 1、Docker 安裝 Redis 1.1 下載鏡像 docker pull redis: 6 . 2 . 6 1.2 創(chuàng)
    的頭像 發(fā)表于 10-08 14:56 ?716次閱讀
    如何用<b class='flag-5'>Springboot</b>整合<b class='flag-5'>Redis</b>

    AOP要怎么使用

    AOP(Aspect-Oriented Programming)經(jīng)常會出現(xiàn)在面試過程中,AOP到底有沒有用,要怎么使用呢。本篇來一起撥開迷霧! 1 第一個(gè)AOP示例 我們會一次將所有的通知類型都覆蓋
    的頭像 發(fā)表于 10-09 16:18 ?852次閱讀
    <b class='flag-5'>AOP</b>要怎么使用

    一個(gè)注解搞定SpringBoot接口防刷

    技術(shù)要點(diǎn):springboot的基本知識,redis基本操作,
    的頭像 發(fā)表于 11-28 10:46 ?495次閱讀

    Redis實(shí)戰(zhàn)筆記

    《 2024最新Redis 實(shí)戰(zhàn)筆記》,這份筆記對 Redis 的相關(guān)知識做了系統(tǒng)全面的介紹,還是PDF版本,可自由復(fù)制,特別適合 Redis 初學(xué)者快速入門和提高。 ? 本筆記適合人
    的頭像 發(fā)表于 02-09 09:12 ?254次閱讀
    <b class='flag-5'>Redis</b><b class='flag-5'>實(shí)戰(zhàn)</b>筆記

    電子發(fā)燒友

    中國電子工程師最喜歡的網(wǎng)站

    • 2931785位工程師會員交流學(xué)習(xí)
    • 獲取您個(gè)性化的科技前沿技術(shù)信息
    • 參加活動獲取豐厚的禮品