缓存的设计与实现

参考文章

缓存穿透、缓存击穿、缓存雪崩区别和解决方案

设计思路

没有缓存的时候,获取数据是直接查询数据库的。如果某时间段内并发查询的次数太多,会对数据库造成很大的压力,甚至崩盘。这时我们就会想到使用缓存。查询数据时先查询缓存,如果缓存有则直接还回,如果没有则查询数据库,把结果放入缓存中并返回。下次查询时可以直接从缓存中获取。

现有一商品表 goods ,对goods的查询做redis缓存优化。goods的key,value如下。

key = 固定值+version(可变)+参数  = "cache:goods:" + +version(可变)+参数
value = 从数据库查询的结果。

其中version也是 redis缓存中的值

key = 国定值="cache:goods:version"
value = 1  //初始值1,goods每次增删改,value都加1

思路:

  1. 创建2个注解:CacheVersion ,CyCache

    @interface CacheVersion {versionKey,expireTime=3600}:缓存版本
    @interface CyCache {cacheKey,versionKey,expireTime=3600} :缓存
    
  2. 创建2个aspect:CacheVersionAspect,CacheAspect

    class CacheVersionAspect:拦截标记了注解CacheVersion 的增删改方法。
    	在方法执行,CacheVersion.versionKey对应的缓存加1
    class CacheAspect :拦截标记了注解CyCache的查询方法。
    	在执行数据库查询之前先查一下redis缓存(CyCache.cacheKey+version+参数)是否有数据,
    	如果有则直接返回,没有则执行查询方法,得到结果后把结果放入redis中并返回。
    
  3. 给service的增删改方法添加注解@CacheVersion ,给查询方法添加注册@CyCache

    通过@CyCache,查询不用每次查询数据库,可以从redis缓存中获取数据。
    通过@CacheVersion,每次增删改后version的值会递增。下次查询时,
    (CyCache.key+version+参数)的缓存就没有(因为version的值变了),需要重新查询数据库并缓存。
    

实现

创建2个注解:CacheVersion ,CyCache

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 缓存版本注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheVersion {
    /**
     * 版本key
     */
    String[] versionKeys() default {};

    /**
     * 过期时间,单位秒
     */
    int expireTime() default 3600;
}
package com.example.demo.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 缓存注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
    /**
     * 版本key 需要和 CacheVersion。versionKey 一样
     */
    String versionKey() default "";

    /**
     * 数据key的固定值部分
     */
    String cacheKey() default "";

    /**
     * 方法的参数集合
     */
    String[] paramName() default {};

    /**
     * 过期时间,单位秒
     */
    int expireTime() default 3600;
}

创建2个aspect:CacheVersionAspect,CacheAspect

package com.example.demo.aspect;

import com.example.demo.annotation.CacheVersion;
import com.example.demo.cache.CacheClient;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 缓存版本处理切面
 */
@Slf4j
@Component
@Aspect
public class CacheVersionAspect {
    @Resource
    private CacheClient cacheClient;

    @After(value = "@annotation(cacheVersion))")
    public void handlerServiceCacheVersion(JoinPoint point, CacheVersion cacheVersion) {
        try {
            String[] versionKeys = cacheVersion.versionKeys();
            for (String versionKey : versionKeys) {
                cacheClient.increaseCount(versionKey);
                cacheClient.expire(versionKey, cacheVersion.expireTime());
                log.info("update version of : {}", versionKey);
            }
        } catch (Exception e) {
            log.info("缓存异常:", e);
        }
    }
}


package com.example.demo.aspect;


import com.example.demo.annotation.Cache;
import com.example.demo.cache.CacheClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
@Aspect
public class CacheAspect {
    @Resource
    private CacheClient cacheClient;

    public CacheAspect() {
    }

    @Around(value = "@annotation(cache))")
    public Object handlerServiceCache(ProceedingJoinPoint point, Cache cache) {
        try {
            //获取参数拼凑的key
            String paramKey = getParamKey(point, cache.paramName());
            //获取version的key
            String versionKey = getCacheVersion(cache);
            //dataKey= cache.cacheKey + 参数拼凑的key +version的key
            String dataKey = cache.cacheKey() + paramKey + versionKey;

            // 处理缓存
            Object data = cacheClient.get(dataKey);
            if (data != null) {
                System.out.println("从缓存中获取数据,key = " + dataKey);
                return data;
            }
            data = point.proceed();
            if (data != null) {
                cacheClient.set(dataKey, data);
                cacheClient.expire(dataKey, cache.expireTime());
            }
            return data;
        } catch (Throwable e) {
            log.error("缓存异常:", e);
        }
        return null;
    }

    /**
     * 获取version的key
     */
    private String getCacheVersion(Cache cache) {
        String versionKey = cache.versionKey();
        if (StringUtils.isBlank(versionKey)) {
            return "";
        }
        Integer cacheVersion;
        if (cacheClient.get(versionKey) == null) {
            cacheVersion = cacheClient.increaseCount(versionKey);
        } else {
            cacheVersion = (Integer) cacheClient.get(versionKey);
        }
        cacheClient.expire(versionKey, cache.expireTime());
        return ":" + cacheVersion.toString();
    }

    /**
     * 获取参数拼凑的key
     */
    private String getParamKey(ProceedingJoinPoint point, String[] params) {
        if (params == null || params.length == 0) {
            return "";
        }
        StringBuilder keyExtra = new StringBuilder();
        try {
            Map<String, Object> nameAndArgs = this.getFieldsName(point, point.getArgs());
            for (String param : params) {
                if (nameAndArgs.get(param) != null) {
                    keyExtra.append(":").append(nameAndArgs.get(param));
                }
            }
        } catch (Exception e) {
            log.info("缓存异常:", e);
        }
        return keyExtra.toString();
    }

    /**
     * 基于Aspect,java1.8以上用java自带的参数反射,以下用spring的实现,似乎跟javassist一样
     */
    private Map<String, Object> getFieldsName(ProceedingJoinPoint point, Object[] args) {
        Map<String, Object> map = new HashMap<>();
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] names = methodSignature.getParameterNames();
        for (int i = 0; i < args.length; i++) {
            map.put(names[i], args[i]);
        }
        return map;
    }

}

给service的增删改方法添加注解@CacheVersion ,给查询方法添加注册@CyCache

package com.example.demo.service;

import com.example.demo.annotation.Cache;
import com.example.demo.annotation.CacheVersion;
import com.example.demo.demain.Good;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class GoodService {
    private Integer goodNum = 2;
    private String description = "好商品";


    @Cache(cacheKey = "cache:good", versionKey = "cache:good:version", paramName = {"name"})
    public List<Good> getGoodList(String name) {
        System.out.println("从数据库中获取数据");
        List<Good> goods = new ArrayList<>();
        for (int i = 1; i <= goodNum; i++) {
            Good good = Good.builder().id(i).name(i + "号商品").price(2000L).description(i + "号商品 " + description).build();
            goods.add(good);
        }
        return goods;
    }

    @CacheVersion(versionKeys = {"cache:good:version"})
    public void addGood() {
        System.out.println("增长商品");
        goodNum++;
    }

    @CacheVersion(versionKeys = {"cache:good:version"})
    public void deleteGood() {
        System.out.println("减少商品");
        goodNum--;
    }

    @CacheVersion(versionKeys = {"cache:good:version"})
    public void updateGood(Integer id, String description) {
        System.out.println("修改商品描述 description = " + description);
        this.description = description;
    }

}

测试

package com.example.demo.service;

import com.alibaba.fastjson.JSON;
import com.example.demo.demain.Good;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.List;

@SpringBootTest
class GoodServiceTest {
    @Resource
    private GoodService goodService;

    @Test
    void getGoodList() {
        System.out.println("第1次查询:");
        List<Good> goods = goodService.getGoodList("你好");
        System.out.println(JSON.toJSONString(goods));
        System.out.println("第2次查询:");
        goods = goodService.getGoodList("你好");
        System.out.println(JSON.toJSONString(goods));

        System.out.println("########## 修改商品 ##########");
        goodService.updateGood(2,"非常好的商品");
        System.out.println("########## 修改商品 ##########");

        System.out.println("修改后第1次查询:");
        goods = goodService.getGoodList("你好");
        System.out.println(JSON.toJSONString(goods));
        System.out.println("修改后第2次查询:");
        goods = goodService.getGoodList("你好");
        System.out.println(JSON.toJSONString(goods));
    }
}

运行结果
在这里插入图片描述

问题

1、缓存雪崩

缓存失效,请求全部到数据库中,很可能就把我们的数据库搞垮,导致整个服务瘫痪!缓存失效可能如下原因:

1,Redis缓存数据时,需设置过期时间,如果大量过期时间相同,极端的情况下,可能会出现这些缓存同时失效,全部请求到数据库中。

2,Redis挂掉了,全部请求到数据库中。
,
如何解决缓存雪崩

第一种情况

在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。

第二种情况可分为 三步处理

事发前

  1. redis持久化。
  2. 实现Redis的高可用(主从架构+Sentinel 或者Redis Cluster),避免Redis全部挂掉。
  3. 设置备份缓存,如本地缓存(ehcache),当redis缓存雪崩后,启用备份缓存。

事发中

  1. 用 hystrix 对源服务访问进行 限流、降级。降级在高并发系统中是非常正常的:比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据。可参考文章: Hystrix学习总结
  2. 启用备份缓存

事发后

  1. 立即重启。redis持久化后,redis重启后自动从磁盘上加载数据,快速恢复缓存数据。
2、缓存穿透

缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存
这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。这就是缓存穿透:请求的数据在缓存大量不命中,导致请求走数据库。缓存穿透如果发生了,也可能把我们的数据库搞垮,导致整个服务瘫痪!

如何解决缓存雪崩

  1. 由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就不让这个请求到数据库层!
  2. 当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。这种情况我们一般会将空对象设置一个较短的过期时间。
3、缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

如何解决缓存雪崩

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁参考代码如下:
    在这里插入图片描述

说明:

1)缓存中有数据,直接走上述代码13行后就返回结果了

2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现。

3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,上面代码明显做不到这点。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值