redis项目-缓存和读写一致性
今天确实学到了蛮多东西的,忙里偷闲的感觉真好
回顾一下缓存,如同计组里面cache和内存之间的关系。在java项目中redis作为缓存,mysql就相当于内存。基本的逻辑是先找缓存,如果缓存没有命中就找mysql,然后再写到缓存中。
这里还有很多可以考虑的点,写策略和调度,后续都会考虑一遍。
首先是redis如何作为缓存的,很简单:
1 |
|
这里主要考虑双写一致性。先删后写和先写后删都会有问题,详情见原来的blog。主要有三种解决方法:
- 延迟双删:删除->写->删除,这样可以解决第一次删除之前读操作变更redis的脏数据,这里的最后一次删除为什么要延迟,因为至少得等存数据库操作做完才行,这是异步的,一般都以业务的平均时间作为延迟时间。
- 分布式锁:直接上读写锁,就没有这么多事情了
- 先写后删:其实这样的概率挺低的,一种投机方法。
延迟双删
这里用一个异步线程池完成,在写数据库的时候就开一个新的线程,最后根据延迟时间删除就行了。
值得注意的是这里可以用aop+注解的方式完成无侵入实现。相比前面的,后面这种的实用性更广泛。
1 | /** |
aop+注解
1 |
|
这是第一次开发注解,踩了不少坑:
- 首先aop只能作用于接口上,未在接口中声明的成员方法是不生效的,具体的可以看这篇文章:https://blog.csdn.net/Show_line/article/details/136786252?ops_request_misc=&request_id=&biz_id=102&utm_term=aop%E5%BF%85%E9%A1%BB%E4%BD%9C%E7%94%A8%E4%BA%8E%E6%8E%A5%E5%8F%A3%E5%90%97&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-1-136786252.142^v100^pc_search_result_base1&spm=1018.2226.3001.4187
gpt的回答也很有意思:在 Spring 中,AOP 是通过代理对象来实现的,代理对象的创建方式有两种主要模式:JDK 动态代理和 CGLIB 代理。默认情况下,Spring 会根据目标类是否实现了接口来决定使用哪种代理机制:
- JDK 动态代理:如果目标类实现了一个或多个接口,Spring 会使用 JDK 动态代理。JDK 动态代理只能代理接口中的方法。
- CGLIB 代理:如果目标类没有实现任何接口,Spring 会使用 CGLIB 来生成目标类的子类,从而创建代理对象。CGLIB 代理可以代理类中的所有方法(包括没有在接口中声明的方法)。
如果您在某个实现类的成员方法上使用注解但没有在接口中声明该方法,而该类实现了接口,那么默认情况下,Spring AOP 使用 JDK 动态代理,导致代理对象无法拦截实现类中没有在接口中声明的方法。这是因为 JDK 动态代理只能代理接口中的方法。
- 使用 CGLIB 代理:明确要求 Spring 使用 CGLIB 代理。这可以通过在 Spring 配置中设置代理模式来实现。
- 确保接口中声明方法:将需要代理的方法声明在接口中,以便 JDK 动态代理能够正常工作。
- 其次是,注解是无法直接访问被注解方法的参数的,但是可以进行隐式处理,proceedingJoinPoint.getArgs();可以得到这个函数的参数。这里用了around,因为延迟双删除刚好是执行业务的上下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class DelayDoubleDeleteAspect
{
private StringRedisTemplate stringRedisTemplate;
private ExecutorService shopDoubleDelThreadPool = Executors.newFixedThreadPool(4);
/**
* 延迟双删
* @param
* @return
*/
private class doubleDelThread implements Callable<Result> {
private String id;
private int DELAY_TIME;
public doubleDelThread(String id,int DELAY_TIME) {
this.id = id;
this.DELAY_TIME = DELAY_TIME;
}
public Result call() {
try {
Thread.sleep(DELAY_TIME);
stringRedisTemplate.delete(id);
log.debug("延迟1秒删除");
return Result.ok();
} catch (InterruptedException e) {
log.debug("延迟双删出错");
return Result.fail("延迟双删出错");
}
}
}
public void pointCut(){
}
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
//方法签名
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
//被环绕的方法名
String methodName = signature.getName();
//方法参数
Object[] args = proceedingJoinPoint.getArgs();
Shop shop = (Shop) args[0];
//找到注解
DelayDoubleDelete annotation = AnnotationUtil.getAnnotation(signature.getMethod(), DelayDoubleDelete.class);
String redisKey = annotation.redisKey();
int delayTime = annotation.delayTime();
stringRedisTemplate.delete(redisKey+shop.getId());
//proceed用来接受业务产生的结果
Object proceed = null;
//由于最后一个删除是要业务都做完了,所以需要在之后进行线程提交
try {
//继续业务
proceed = proceedingJoinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
//最后删除
shopDoubleDelThreadPool.submit(new doubleDelThread(redisKey+shop.getId(), delayTime));
//不用修改直接返回
return proceed;
}
}
分布式锁
写操作
1 | /** |
读操作
1 |
|