• redis缓存问题


    缓存击穿

    缓存击穿是指某个热点数据存储在redis中,该数据在高并发的场景下,当该key过期时就会有大量的请求去查询数据库,对数据库的压力非常大,可能会导致数据库压垮。

    解决方案

    1.不为热点的key设置过期时间。

    2.使用分布式锁。

    在查询数据库前需要获取锁,没有获取锁的请求会一直在重试,这样保证只有一条请求访问数据库,在该请求访问数据库后会将获得的信息重新存放到redis中,并将锁释放,在每次获取锁并访问数据库前还会再去redis中查询一次数据,这样就可以实现在第一个请求访问数据库后,后续的请求会直接从redis中查询出数据,解决了缓存击穿。

    缓存雪崩

    缓存雪崩存在两种情况

    情况1:在redis中存的大量缓存的key设置了相同的过期时间,在这些key过期后就会大量请求访问数据库。

    1情况2:redis服务宕机了,导致大量的请求访问数据库。

    解决方案

    情况1的解决方案

    1.错开过期时间:在过期时间上添加(1~5分钟)的随机时间。

    2.服务降级:停止非核心数据查询缓存,返回预定义信息。(就是实现FallbackFactory接口)

    情况2的解决方案

    1.搭建redis集群

    2.构建二级缓存。(目前使用的就是 Caffeine作为一级缓存,redis做二级缓存)

    3.熔断:通过监控一旦雪崩出现,暂停缓存访问待实例恢复,返回预定义信息。(有损方案)

    4.限流:通过监控一旦数据库的访问量超出阈值,就限制访问数据库的请求数。(有损方案)

    实现步骤
    错开过期时间的实现为下:

    自定义 MyRedisCacheManager类继承RedisCacheManager

    1. import cn.hutool.core.util.ObjectUtil;
    2. import cn.hutool.core.util.RandomUtil;
    3. import org.springframework.data.redis.cache.RedisCache;
    4. import org.springframework.data.redis.cache.RedisCacheConfiguration;
    5. import org.springframework.data.redis.cache.RedisCacheManager;
    6. import org.springframework.data.redis.cache.RedisCacheWriter;
    7. import java.time.Duration;
    8. /**
    9. * 自定义CacheManager,用于设置不同的过期时间,防止雪崩问题的发生
    10. */
    11. public class MyRedisCacheManager extends RedisCacheManager {
    12. public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
    13. super(cacheWriter, defaultCacheConfiguration);
    14. }
    15. @Override
    16. protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
    17. //获取到原有过期时间
    18. Duration duration = cacheConfig.getTtl();
    19. if (ObjectUtil.isNotEmpty(duration)) {
    20. //在原有时间上随机增加1~10分钟
    21. //后续使用时需要修改的就是这里的时间
    22. Duration newDuration = duration.plusMinutes(RandomUtil.randomInt(1, 11));
    23. cacheConfig = cacheConfig.entryTtl(newDuration);
    24. }
    25. return super.createRedisCache(name, cacheConfig);
    26. }
    27. }

     在RedisConfig中使用MyRedisCacheManager作自定义缓存管理器配置。

    1. @Bean
    2. public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
    3. // 默认配置
    4. RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
    5. // 设置key的序列化方式为字符串
    6. .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
    7. // 设置value的序列化方式为json格式
    8. .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
    9. .disableCachingNullValues() // 不缓存null
    10. .entryTtl(Duration.ofHours(redisTtl)); // 默认缓存数据保存1小时
    11. //使用自定义缓存管理器
    12. RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
    13. MyRedisCacheManager myRedisCacheManager = new MyRedisCacheManager(redisCacheWriter, defaultCacheConfiguration);
    14. myRedisCacheManager.setTransactionAware(true); // 只在事务成功提交后才会进行缓存的put/evict操作
    15. return myRedisCacheManager;
    16. }

     缓存穿透

    一个key在缓存和数据库中都不存在,这样每次查询该key都需要访问数据库。

    • 很可能被恶意请求利用
    • 缓存雪崩与缓存击穿都是数据库中有,但缓存暂时缺失
    • 缓存雪崩与缓存击穿都能自然恢复,但缓存穿透则不能

    解决方案
    1. 如果数据库中没有,也将此key关联null存入缓存中,缺点就是这样的key没有作用,白白浪费空间。

    2. 采用BloomFilter(布隆过滤器)解决,基本思路就是将存在数据的哈希值存储到一个足够大的Bitmap(Bit为单位存储数据,可以大大节省存储空间)中,在查询redis时,先查询布隆过滤器,如果数据不存在直接返回即可,如果存在的话,再执行缓存中命中、数据库查询等操作。(通过hash函数计算出key对应的位置,如果有值就将对应位置改为1,在后续查询redis前先从布隆过滤器中查询数据是否存在),适合用来做判断不存在的操作。

    实现步骤
    布隆过滤器

    需要将数据存入布隆过滤器中,才能判断数据是否存在,存入时要通过hash算法函数计算出hash值,通过hash值确定存储的位置。

     看到这里,你一定会有这样的疑问,不同的数据经过哈希算法计算,可能会得到相同的值,也就是,【张三】和【王五】可能会得到相同的hash值,会在同一个位置标记为1,这样的话,1个位置可能会代表多个数据,也就是会出现误判,没错,这个就是布隆过滤器最大的一个缺点,也是不可避免的特性。正因为这个特性,所以布隆过滤器基本是不能做删除动作的。

    总结:使用布隆过滤器能够判断一定不存在,而不能用来判断一定存在。 

    为了降低误判率我们可以使用多哈希法。

    通过多个哈希算法计算参数多个位置,在这多个位置上进行标记,在后续查找时只有这多个位置同时为1时才说明存在数据,虽然降低了误判率,但误判数据存在还是存在的

     布隆过滤器的优缺点

    • 优点
      • 存储的二进制数据,1或0,不存储真实数据,空间占用比较小且安全。
      • 插入和查询速度非常快,因为是基于数组下标的,类似HashMap,其时间复杂度是O(K),其中k是指哈希算法个数。
    • 缺点
      • 存在误判,可以通过增加哈希算法个数降低误判率,不能完全避免误判。
      • 删除困难,因为一个位置可能会代表多个值,不能做删除。

    牢记结论:布隆过滤器能够判断一定不存在,而不能用来判断一定存在 。

     

     Redission基于Redis,使用string类型数据,生成二进制数组进行存储,最大可用长度为:4294967294

    引入依赖

    1. org.redisson
    2. redisson

     设置redission配置

    1. import cn.hutool.core.convert.Convert;
    2. import cn.hutool.core.util.StrUtil;
    3. import org.redisson.Redisson;
    4. import org.redisson.api.RedissonClient;
    5. import org.redisson.config.Config;
    6. import org.redisson.config.SingleServerConfig;
    7. import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
    8. import org.springframework.context.annotation.Bean;
    9. import org.springframework.context.annotation.Configuration;
    10. import javax.annotation.Resource;
    11. @Configuration
    12. public class RedissonConfiguration {
    13. @Resource
    14. private RedisProperties redisProperties;
    15. @Bean
    16. public RedissonClient redissonSingle() {
    17. Config config = new Config();
    18. SingleServerConfig serverConfig = config.useSingleServer()
    19. .setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort());
    20. if (null != (redisProperties.getTimeout())) {
    21. serverConfig.setTimeout(1000 * Convert.toInt(redisProperties.getTimeout().getSeconds()));
    22. }
    23. if (StrUtil.isNotEmpty(redisProperties.getPassword())) {
    24. serverConfig.setPassword(redisProperties.getPassword());
    25. }
    26. return Redisson.create(config);
    27. }
    28. }

    自定义布隆过滤器配置

    1. import lombok.Getter;
    2. import org.springframework.beans.factory.annotation.Value;
    3. import org.springframework.context.annotation.Configuration;
    4. /**
    5. * 布隆过滤器相关配置
    6. */
    7. @Getter
    8. @Configuration
    9. public class BloomFilterConfig {
    10. /**
    11. * 名称,默认:sl-bloom-filter
    12. */
    13. @Value("${bloom.name:sl-bloom-filter}")
    14. private String name;
    15. /**
    16. * 布隆过滤器长度,最大支持Integer.MAX_VALUE*2,即:4294967294,默认:1千万
    17. */
    18. @Value("${bloom.expectedInsertions:10000000}")
    19. private long expectedInsertions;
    20. /**
    21. * 误判率,默认:0.05
    22. */
    23. @Value("${bloom.falseProbability:0.05d}")
    24. private double falseProbability;
    25. }

    创建布隆过滤器的Service接口

    1. /**
    2. * 布隆过滤器服务
    3. */
    4. public interface BloomFilterService {
    5. /**
    6. * 初始化布隆过滤器
    7. */
    8. void init();
    9. /**
    10. * 向布隆过滤器中添加数据
    11. *
    12. * @param obj 待添加的数据
    13. * @return 是否成功
    14. */
    15. boolean add(Object obj);
    16. /**
    17. * 判断数据是否存在
    18. *
    19. * @param obj 数据
    20. * @return 是否存在
    21. */
    22. boolean contains(Object obj);
    23. }

    编写Service的实现类

    1. import com.sl.transport.info.config.BloomFilterConfig;
    2. import com.sl.transport.info.service.BloomFilterService;
    3. import org.redisson.api.RBloomFilter;
    4. import org.redisson.api.RedissonClient;
    5. import org.springframework.stereotype.Service;
    6. import javax.annotation.PostConstruct;
    7. import javax.annotation.Resource;
    8. @Service
    9. public class BloomFilterServiceImpl implements BloomFilterService {
    10. @Resource
    11. private RedissonClient redissonClient;
    12. @Resource
    13. private BloomFilterConfig bloomFilterConfig;
    14. private RBloomFilter getBloomFilter() {
    15. return this.redissonClient.getBloomFilter(this.bloomFilterConfig.getName());
    16. }
    17. @Override
    18. @PostConstruct // spring启动后进行初始化
    19. public void init() {
    20. RBloomFilter bloomFilter = this.getBloomFilter();
    21. bloomFilter.tryInit(this.bloomFilterConfig.getExpectedInsertions(), this.bloomFilterConfig.getFalseProbability());
    22. }
    23. @Override
    24. public boolean add(Object obj) {
    25. return this.getBloomFilter().add(obj);
    26. }
    27. @Override
    28. public boolean contains(Object obj) {
    29. return this.getBloomFilter().contains(obj);
    30. }
    31. }
    32. 改造Controller的查询逻辑,如果布隆过滤器中不存在直接返回即可,无需进行缓存命中。

      1. @ApiImplicitParams({
      2. @ApiImplicitParam(name = "transportOrderId", value = "运单id")
      3. })
      4. @ApiOperation(value = "查询", notes = "根据运单id查询物流信息")
      5. @GetMapping("{transportOrderId}")
      6. public TransportInfoDTO queryByTransportOrderId(@PathVariable("transportOrderId") String transportOrderId) {
      7. //如果布隆过滤器中不存在,无需缓存命中,直接返回即可
      8. boolean contains = this.bloomFilterService.contains(transportOrderId);
      9. if (!contains) {
      10. throw new SLException(ExceptionEnum.NOT_FOUND);
      11. }
      12. TransportInfoDTO transportInfoDTO = transportInfoCache.get(transportOrderId, id -> {
      13. //未命中,查询MongoDB
      14. TransportInfoEntity transportInfoEntity = this.transportInfoService.queryByTransportOrderId(id);
      15. //转化成DTO
      16. return BeanUtil.toBean(transportInfoEntity, TransportInfoDTO.class);
      17. });
      18. if (ObjectUtil.isNotEmpty(transportInfoDTO)) {
      19. return transportInfoDTO;
      20. }
      21. throw new SLException(ExceptionEnum.NOT_FOUND);
      22. }

       新增操作的Service中将数据写入布隆过滤器中,也就是调用bloomService层的add方法

      最终完成布隆过滤器的创建。 

    33. 相关阅读:
      mybatis plus中json格式实战
      字符函数和字符串函数(详解大全)
      MOS管防倒灌电路设计及其过程分析
      前端利器躬行记(9)——WebView中的页面调试方法
      uniapp项目创建
      34.数据库MySQL(1)
      枚举 小蓝的漆房
      nslookup google.com -bash: nslookup: 未找到命令
      【支付宝沙箱支付】麻瓜教程——申请----代码----修改测试----问题解决
      IT学习笔记--Kubernetes
    34. 原文地址:https://blog.csdn.net/Ostkakah/article/details/134006954