今天确实学到了蛮多东西的,忙里偷闲的感觉真好

回顾一下缓存,如同计组里面cache和内存之间的关系。在java项目中redis作为缓存,mysql就相当于内存。基本的逻辑是先找缓存,如果缓存没有命中就找mysql,然后再写到缓存中。
这里还有很多可以考虑的点,写策略和调度,后续都会考虑一遍。

首先是redis如何作为缓存的,很简单:

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
@Override
public Result queryById(Long id) throws InterruptedException {
//读写锁
RLock lock = redissonClient.getLock("lock:shop:write");
while (!lock.tryLock(1000,2000,TimeUnit.MILLISECONDS)){
Thread.sleep(100);
}
try {
//return Result.ok(getById(id));
//1.先去查redis
String shopJsonStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP + id);
//2.如果redis没有就找数据库
if (shopJsonStr == null || shopJsonStr.isEmpty()){
//2.1 找数据库
Shop shop = getById(id);
//2.2 然后再写回redis里
String jsonString = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP + id,jsonString);
//设置30s的过期时间
stringRedisTemplate.expire(CACHE_SHOP + id,30, TimeUnit.MINUTES);
return Result.ok(shop);
}
//3.如果redis有就返回
else {
Shop shop = JSONUtil.toBean(shopJsonStr, Shop.class);
//刷新过期时间
stringRedisTemplate.expire(CACHE_SHOP + id,30, TimeUnit.MINUTES);
return Result.ok(shop);
}
}
finally {
lock.unlock();
}

}

这里主要考虑双写一致性。先删后写和先写后删都会有问题,详情见原来的blog。主要有三种解决方法:

  • 延迟双删:删除->写->删除,这样可以解决第一次删除之前读操作变更redis的脏数据,这里的最后一次删除为什么要延迟,因为至少得等存数据库操作做完才行,这是异步的,一般都以业务的平均时间作为延迟时间。
  • 分布式锁:直接上读写锁,就没有这么多事情了
  • 先写后删:其实这样的概率挺低的,一种投机方法。

延迟双删

这里用一个异步线程池完成,在写数据库的时候就开一个新的线程,最后根据延迟时间删除就行了。

值得注意的是这里可以用aop+注解的方式完成无侵入实现。相比前面的,后面这种的实用性更广泛。

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
/**
* 延时双删
* @param shop
* @return
*/
public Result updateShopDoubleDel(Shop shop) {
stringRedisTemplate.delete(CACHE_SHOP + shop.getId().toString());
//更新数据库
updateById(shop);
//开启一个新的线程延时删除
shopDoubleDelThreadPool.submit(new doubleDelThread(CACHE_SHOP + shop.getId().toString()));
return Result.ok();
}
private ExecutorService shopDoubleDelThreadPool = Executors.newFixedThreadPool(4);
/**
* 延迟双删
* @param
* @return
*/
private class doubleDelThread implements Callable<Result>{
private String id;
public doubleDelThread(String id) {
this.id = id;
}
@Override
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("延迟双删出错");
}
}
}

aop+注解

1
2
3
4
5
6
7
8
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DelayDoubleDelete {
//必须填就不要写default
String redisKey();
int delayTime() default 1000;
}

这是第一次开发注解,踩了不少坑:

如果您在某个实现类的成员方法上使用注解但没有在接口中声明该方法,而该类实现了接口,那么默认情况下,Spring AOP 使用 JDK 动态代理,导致代理对象无法拦截实现类中没有在接口中声明的方法。这是因为 JDK 动态代理只能代理接口中的方法。

  1. 使用 CGLIB 代理:明确要求 Spring 使用 CGLIB 代理。这可以通过在 Spring 配置中设置代理模式来实现。
  2. 确保接口中声明方法:将需要代理的方法声明在接口中,以便 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
    @Aspect
    @Component
    @Slf4j
    public class DelayDoubleDeleteAspect
    {
    @Resource
    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;
    }
    @Override
    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("延迟双删出错");
    }
    }
    }

    @Pointcut("@annotation(com.hmdp.annotation.DelayDoubleDelete)")
    public void pointCut(){

    }

    @Around("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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 分布式锁
* @param shop
* @return
*/
public Result updateShopLock(Shop shop) throws InterruptedException {
RLock lock = redissonClient.getLock("lock:shop:write");
while (!lock.tryLock(1000,2000,TimeUnit.MILLISECONDS)){
Thread.sleep(100);
}
try {
//更新数据库
updateById(shop);
stringRedisTemplate.delete(CACHE_SHOP + shop.getId().toString());
return Result.ok(shop);
}
finally {
lock.unlock();
}
}

读操作

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
@Override
public Result queryById(Long id) throws InterruptedException {
//读写锁
RLock lock = redissonClient.getLock("lock:shop:write");
while (!lock.tryLock(1000,2000,TimeUnit.MILLISECONDS)){
Thread.sleep(100);
}
try {
//return Result.ok(getById(id));
//1.先去查redis
String shopJsonStr = stringRedisTemplate.opsForValue().get(CACHE_SHOP + id);
//2.如果redis没有就找数据库
if (shopJsonStr == null || shopJsonStr.isEmpty()){
//2.1 找数据库
Shop shop = getById(id);
//2.2 然后再写回redis里
String jsonString = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP + id,jsonString);
//设置30s的过期时间
stringRedisTemplate.expire(CACHE_SHOP + id,30, TimeUnit.MINUTES);
return Result.ok(shop);
}
//3.如果redis有就返回
else {
Shop shop = JSONUtil.toBean(shopJsonStr, Shop.class);
//刷新过期时间
stringRedisTemplate.expire(CACHE_SHOP + id,30, TimeUnit.MINUTES);
return Result.ok(shop);
}
}
finally {
lock.unlock();
}

}