SpringBoot?AOP+Redis實(shí)現(xiàn)延時雙刪功能的實(shí)戰(zhàn)指南
一、業(yè)務(wù)場景
在多線程并發(fā)情況下,假設(shè)有兩個數(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、此時存在的問題
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í)數(shù)據(jù)的嚴(yán)重問題。
2、解決方案
在使用Redis時,需要保持Redis和數(shù)據(jù)庫數(shù)據(jù)的一致性,最流行的解決方案之一就是延時雙刪策略。
注意:要知道經(jīng)常修改的數(shù)據(jù)表不適合使用Redis,因?yàn)殡p刪策略執(zhí)行的結(jié)果是把Redis中保存的那條數(shù)據(jù)刪除了,以后的查詢就都會去查詢數(shù)據(jù)庫。所以Redis使用的是讀遠(yuǎn)遠(yuǎn)大于改的數(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為空,有請求進(jìn)來時,便會去訪問數(shù)據(jù)庫,此時數(shù)據(jù)庫中的數(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、編寫自定義aop注解和切面
ClearAndReloadCache延時雙刪注解
/**
*延時雙刪
**/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
String name() default "";
}
ClearAndReloadCacheAspect延時雙刪切面
@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í)行,什么時候執(zhí)行,執(zhí)行時是否需要替換方法參數(shù),執(zhí)行完畢是否需要替換返回值。
* 環(huán)繞通知第一個參數(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();//方法對象
ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class);//反射得到自定義注解的方法對象
String name = annotation.name();//獲取自定義注解的方法對象的參數(shù)即name
Set<String> keys = stringRedisTemplate.keys("*" + name + "*");//模糊定義key
stringRedisTemplate.delete(keys);//模糊刪除redis的key值
//執(zhí)行加入雙刪注解的改動數(shù)據(jù)庫的業(yè)務(wù) 即controller中的方法業(yè)務(wù)
Object proceed = null;
try {
proceed = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//開一個線程 延遲1秒(此處是1秒舉例,可以改成自己的業(yè)務(wù))
// 在線程中延遲刪除 同時將業(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)測試數(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ù)庫失敗");
}
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ù)庫失敗");
}
public Result update(User user){
int i = userMapper.updateById(user);
if(i > 0)
return Result.success(i);
return Result.fail(888,"操作數(shù)據(jù)庫失敗");
}
}
三、測試驗(yàn)證
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)的用戶名(驗(yàn)證數(shù)據(jù)庫和緩存不一致方案)

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


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

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

以上就是SpringBoot AOP+Redis實(shí)現(xiàn)延時雙刪功能的實(shí)戰(zhàn)指南的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Redis延時雙刪的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring boot實(shí)現(xiàn)文件上傳實(shí)例(多文件上傳)
本篇文章主要介紹了Spring boot實(shí)現(xiàn)文件上傳實(shí)例(多文件上傳),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
Java注解Annotation原理及自定義注解代碼實(shí)例
這篇文章主要介紹了Java注解Annotation原理及自定義注解代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10
淺談Spring學(xué)習(xí)之request,session與globalSession作用域
這篇文章主要介紹了Spring學(xué)習(xí)之request,session與globalSession作用域的相關(guān)內(nèi)容,需要的朋友可以參考下。2017-09-09
IDEA創(chuàng)建Maven一直爆紅無法下載的問題解決辦法
這篇文章主要介紹了關(guān)于IDEA創(chuàng)建Maven一直爆紅無法下載的問題的解決辦法,文中圖文結(jié)合的方式給大家講解的非常詳細(xì),對大家解決辦法非常有用,需要的朋友可以參考下2024-06-06
Java關(guān)鍵字this使用方法詳細(xì)講解(通俗易懂)
這篇文章主要介紹了Java關(guān)鍵字this使用方法的相關(guān)資料,Java關(guān)鍵字this主要用于在方法體內(nèi)引用當(dāng)前對象,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-01-01

