一、業務場景
在多線程并發情況下,假設有兩個數據庫修改請求,為保證數據庫與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使用的是讀遠遠大于改的數據緩存。
延時雙刪方案執行步驟
- 刪除緩存
- 更新數據庫
- 延時500毫秒 (根據具體業務設置延時執行的時間)
- 刪除緩存
3、為何要延時500毫秒?
這是為了我們在第二次刪除Redis之前能完成數據庫的更新操作。假象一下,如果沒有第三步操作時,有很大概率,在兩次刪除Redis操作執行完畢之后,數據庫的數據還沒有更新,此時若有請求訪問數據,便會出現我們一開始提到的那個問題。
4、為何要兩次刪除緩存?
如果我們沒有第二次刪除操作,此時有請求訪問數據,有可能是訪問的之前未做修改的Redis數據,刪除操作執行后,Redis為空,有請求進來時,便會去訪問數據庫,此時數據庫中的數據已是更新后的數據,保證了數據的一致性。
基于 Spring Boot + MyBatis Plus + Vue & Element 實現的后臺管理系統 + 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址: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;
/**
*切入點
*切入點,基于注解實現的切入點加上該注解的都是Aop切面的切入點
*
*/
@Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
publicvoidpointCut(){
}
/**
*環繞通知
*環繞通知非常強大,可以決定目標方法是否執行,什么時候執行,執行時是否需要替換方法參數,執行完畢是否需要替換返回值。
*環繞通知第一個參數必須是org.aspectj.lang.ProceedingJoinPoint類型
*@paramproceedingJoinPoint
*/
@Around("pointCut()")
publicObjectaroundAdvice(ProceedingJoinPointproceedingJoinPoint){
System.out.println("-----------環繞通知-----------");
System.out.println("環繞通知的目標方法名:"+proceedingJoinPoint.getSignature().getName());
Signaturesignature1=proceedingJoinPoint.getSignature();
MethodSignaturemethodSignature=(MethodSignature)signature1;
MethodtargetMethod=methodSignature.getMethod();//方法對象
ClearAndReloadCacheannotation=targetMethod.getAnnotation(ClearAndReloadCache.class);//反射得到自定義注解的方法對象
Stringname=annotation.name();//獲取自定義注解的方法對象的參數即name
Setkeys=stringRedisTemplate.keys("*"+name+"*");//模糊定義key
stringRedisTemplate.delete(keys);//模糊刪除redis的key值
//執行加入雙刪注解的改動數據庫的業務即controller中的方法業務
Objectproceed=null;
try{
proceed=proceedingJoinPoint.proceed();
}catch(Throwablethrowable){
throwable.printStackTrace();
}
//開一個線程延遲1秒(此處是1秒舉例,可以改成自己的業務)
//在線程中延遲刪除同時將業務代碼的結果返回這樣不影響業務代碼的執行
newThread(()->{
try{
Thread.sleep(1000);
Setkeys1=stringRedisTemplate.keys("*"+name+"*");//模糊刪除
stringRedisTemplate.delete(keys1);
System.out.println("-----------1秒鐘后,在線程中延遲刪除完畢-----------");
}catch(InterruptedExceptione){
e.printStackTrace();
}
}).start();
returnproceed;//返回業務代碼的值
}
}
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實現的后臺管理系統+用戶小程序,支持RBAC動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
>
>*項目地址:<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腳本
用于生產測試數據
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,"操作數據庫失敗");
}
publicResultdelete(Integerid){
LambdaQueryWrapperwrapper=newLambdaQueryWrapper<>();
wrapper.eq(User::getId,id);
intline=userMapper.delete(wrapper);
if(line>0)
returnResult.success(line);
returnResult.fail(888,"操作數據庫失敗");
}
publicResultupdate(Useruser){
inti=userMapper.updateById(user);
if(i>0)
returnResult.success(i);
returnResult.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
?
-
數據庫
+關注
關注
7文章
3826瀏覽量
64509 -
Redis
+關注
關注
0文章
376瀏覽量
10888 -
SpringBoot
+關注
關注
0文章
173瀏覽量
184
原文標題:SpringBoot AOP + Redis 延時雙刪功能實戰
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論