一、業(yè)務(wù)場景
在多線程并發(fā)情況下,假設(shè)有兩個數(shù)據(jù)庫修改請求,為保證數(shù)據(jù)庫與redis的數(shù)據(jù)一致性,修改請求的實現(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、此時存在的問題
A修改數(shù)據(jù)庫的數(shù)據(jù)最終保存到了Redis中,C在A之后也修改了數(shù)據(jù)庫數(shù)據(jù)。
此時出現(xiàn)了Redis中數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)不一致的情況,在后面的查詢過程中就會長時間去先查Redis, 從而出現(xiàn)查詢到的數(shù)據(jù)并不是數(shù)據(jù)庫中的真實數(shù)據(jù)的嚴(yán)重問題。
2、解決方案
在使用Redis時,需要保持Redis和數(shù)據(jù)庫數(shù)據(jù)的一致性,最流行的解決方案之一就是延時雙刪策略。
注意:要知道經(jīng)常修改的數(shù)據(jù)表不適合使用Redis,因為雙刪策略執(zhí)行的結(jié)果是把Redis中保存的那條數(shù)據(jù)刪除了,以后的查詢就都會去查詢數(shù)據(jù)庫。所以Redis使用的是讀遠遠大于改的數(shù)據(jù)緩存。
延時雙刪方案執(zhí)行步驟
- 刪除緩存
- 更新數(shù)據(jù)庫
- 延時500毫秒 (根據(jù)具體業(yè)務(wù)設(shè)置延時執(zhí)行的時間)
- 刪除緩存
3、為何要延時500毫秒?
這是為了我們在第二次刪除Redis之前能完成數(shù)據(jù)庫的更新操作。假象一下,如果沒有第三步操作時,有很大概率,在兩次刪除Redis操作執(zhí)行完畢之后,數(shù)據(jù)庫的數(shù)據(jù)還沒有更新,此時若有請求訪問數(shù)據(jù),便會出現(xiàn)我們一開始提到的那個問題。
4、為何要兩次刪除緩存?
如果我們沒有第二次刪除操作,此時有請求訪問數(shù)據(jù),有可能是訪問的之前未做修改的Redis數(shù)據(jù),刪除操作執(zhí)行后,Redis為空,有請求進來時,便會去訪問數(shù)據(jù)庫,此時數(shù)據(jù)庫中的數(shù)據(jù)已是更新后的數(shù)據(jù),保證了數(shù)據(jù)的一致性。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
二、代碼實踐
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@interfaceClearAndReloadCache{
Stringname()default"";
}
ClearAndReloadCacheAspect延時雙刪切面
@Aspect
@Component
publicclassClearAndReloadCacheAspect{
@Autowired
privateStringRedisTemplatestringRedisTemplate;
/**
*切入點
*切入點,基于注解實現(xiàn)的切入點加上該注解的都是Aop切面的切入點
*
*/
@Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
publicvoidpointCut(){
}
/**
*環(huán)繞通知
*環(huán)繞通知非常強大,可以決定目標(biāo)方法是否執(zhí)行,什么時候執(zhí)行,執(zhí)行時是否需要替換方法參數(shù),執(zhí)行完畢是否需要替換返回值。
*環(huán)繞通知第一個參數(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();
}
//開一個線程延遲1秒(此處是1秒舉例,可以改成自己的業(yè)務(wù))
//在線程中延遲刪除同時將業(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實現(xiàn)的后臺管理系統(tǒng)+用戶小程序,支持RBAC動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
>
>*項目地址:<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ù)庫失敗");
}
}
三、測試驗證
1、ID=10,新增一條數(shù)據(jù)

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

3、第一次訪問ID為10

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

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

數(shù)據(jù)庫和緩存不一致驗證方案:
打個斷點,模擬A線程執(zhí)行第一次刪除后,在A更新數(shù)據(jù)庫完成之前,另外一個線程B訪問ID=10,讀取的還是舊數(shù)據(jù)。


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

四、代碼工程及地址
核心代碼紅色方框所示
?
https://gitee.com/jike11231/redisDemo.git
?

-
數(shù)據(jù)庫
+關(guān)注
關(guān)注
7文章
3900瀏覽量
65771 -
Redis
+關(guān)注
關(guān)注
0文章
384瀏覽量
11315 -
SpringBoot
+關(guān)注
關(guān)注
0文章
175瀏覽量
318
原文標(biāo)題:SpringBoot AOP + Redis 延時雙刪功能實戰(zhàn)
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
Spring AOP如何破解java應(yīng)用

MySQL與Redis延遲雙刪策略

Redis Stream應(yīng)用案例
Java程序員筆記之mybatis結(jié)合redis實戰(zhàn)二級緩存
Springboot+redis操作多種實現(xiàn)

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

什么是 SpringBoot?

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

AOP要怎么使用

Redis實戰(zhàn)筆記

評論